Skip to content

Commit 12fc073

Browse files
authored
Add lambda calculus NbE example/benchmark (#898)
This is a cleaned up version of my [effectful Pi-Day submission](https://gist.github.com/marvinborner/44236c67bbcfdaa184f37bc9b784a73f). Here I've decided to not use the lookup effect boxing for looking up bindings from the environment and used a hashmap instead.
1 parent 4716b1a commit 12fc073

File tree

3 files changed

+162
-1
lines changed

3 files changed

+162
-1
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
S ~> S:
2+
λ1.λ2.λ3.((1 3) (2 3)) ~> λ1.λ2.λ3.((1 3) (2 3))
3+
(ι (ι (ι (ι ι)))) ~> S:
4+
(λ1.((1 λ2.λ3.λ4.((2 4) (3 4))) λ5.λ6.5) (λ7.((7 λ8.λ9.λ10.((8 10) (9 10))) λ11.λ12.11) (λ13.((13 λ14.λ15.λ16.((14 16) (15 16))) λ17.λ18.17) (λ19.((19 λ20.λ21.λ22.((20 22) (21 22))) λ23.λ24.23) λ25.((25 λ26.λ27.λ28.((26 28) (27 28))) λ29.λ30.29))))) ~> λ1.λ2.λ3.((1 3) (2 3))
5+
(2^3)%3 == 2 // using Church numerals:
6+
(((λ1.λ2.λ3.(((3 λ4.λ5.(4 λ6.((5 λ7.λ8.(7 ((6 7) 8))) 6))) (λ9.λ10.(10 9) λ11.λ12.12)) λ13.(((2 1) (((3 λ14.λ15.λ16.(14 λ17.((15 17) 16))) λ18.18) λ19.λ20.(20 19))) (((3 λ21.λ22.21) λ23.23) λ24.24))) λ25.λ26.(25 (25 26))) λ27.λ28.(27 (27 (27 28)))) λ29.λ30.(29 (29 (29 30)))) ~> λ1.λ2.(1 (1 2))
7+
5! == 120 // using Church numerals:
8+
(λ1.λ2.(((1 λ3.λ4.(4 (3 λ5.λ6.((4 5) (5 6))))) λ7.2) λ8.8) λ9.λ10.(9 (9 (9 (9 (9 10)))))) ~> λ1.λ2.(1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 (1 2))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
9+
(prime? 7) == true // using Church numerals and Wilson's Theorem:
10+
(λ1.(λ2.((2 λ3.λ4.λ5.5) λ6.λ7.6) ((λ8.λ9.λ10.λ11.(((8 (λ12.λ13.(13 12) λ14.λ15.14)) ((8 λ16.(((9 λ17.λ18.λ19.((19 (17 (10 18))) 18)) λ20.16) 11)) λ21.11)) λ22.λ23.23) (λ24.λ25.λ26.(25 ((24 25) 26)) (λ27.λ28.(((27 λ29.λ30.(30 (29 λ31.λ32.((30 31) (31 32))))) λ33.28) λ34.34) (λ35.λ36.λ37.((λ38.λ39.(39 38) λ40.40) ((35 λ41.(λ42.λ43.(43 42) (41 36))) λ44.37)) 1)))) 1)) λ45.λ46.(45 (45 (45 (45 (45 (45 (45 46)))))))) ~> λ1.λ2.1
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/// Demo of an effectful Normalization by Evaluation (NbE) implementation for the pure, untyped lambda calculus.
2+
/// Uses the Call-by-Value reduction order, same as host.
3+
4+
import io
5+
import map
6+
import stream
7+
import process
8+
import stringbuffer
9+
10+
type Name = Int
11+
12+
effect fresh(): Name
13+
effect lookup(n: Name): Int
14+
15+
/// lambda term
16+
type Term {
17+
Abs(n: Name, t: Term)
18+
App(a: Term, b: Term)
19+
Var(n: Name)
20+
}
21+
22+
/// lambda term in normal domain
23+
type Value {
24+
VNeu(neu: Neutral)
25+
VClo(n: Name, t: Term, env: Map[Name, Value])
26+
}
27+
28+
/// lambda term in neutral domain (not yet reducible)
29+
type Neutral {
30+
NVar(n: Name)
31+
NApp(a: Neutral, b: Value)
32+
}
33+
34+
/// evaluate a single term without going into abstractions
35+
def eval(env: Map[Name, Value], t: Term): Value = t match {
36+
case Abs(n, t) => VClo(n, t, env)
37+
case App(a, b) => apply(eval(env, a), eval(env, b))
38+
case Var(n) => env.getOrElse(n) { VNeu(NVar(n)) }
39+
}
40+
41+
/// apply terms in their normal domain
42+
/// this does the actual substitution via environment lookup
43+
def apply(a: Value, b: Value): Value = a match {
44+
case VNeu(neu) => VNeu(NApp(neu, b))
45+
case VClo(n, t, env) => eval(env.put(n, b), t)
46+
}
47+
48+
/// reflect variable name to the neutral domain
49+
def reflect(n: Name): Value = VNeu(NVar(n))
50+
51+
/// convert terms to their normal form (in term domain)
52+
def reify(v: Value): Term / fresh = v match {
53+
case VNeu(NVar(n)) => Var(n)
54+
case VNeu(NApp(a, b)) => App(reify(VNeu(a)), reify(b))
55+
case _ => {
56+
val n = do fresh()
57+
Abs(n, reify(apply(v, reflect(n))))
58+
}
59+
}
60+
61+
/// strong normalization of the term
62+
def normalize(t: Term): Term = {
63+
var i = 0
64+
try reify(eval(empty[Name, Value](compareInt), t))
65+
with fresh { i = i + 1; resume(i) }
66+
}
67+
68+
/// parse named term from BLC stream (de Bruijn)
69+
def parse(): Term / { read[Bool], stop } = {
70+
def go(): Term / { fresh, lookup } =
71+
(do read[Bool](), do read[Bool]) match {
72+
case (false, false) => {
73+
val n = do fresh()
74+
Abs(n, try go() with lookup { i =>
75+
resume(if (i == 0) n else do lookup(i - 1))
76+
})
77+
}
78+
case (false, true ) => App(go(), go())
79+
case (true , false) => Var(do lookup(0))
80+
case (true , true ) => {
81+
var i = 1
82+
while (do read[Bool]()) i = i + 1
83+
Var(do lookup(i))
84+
}
85+
}
86+
87+
var i = 0
88+
try go()
89+
with fresh { i = i + 1; resume(i) }
90+
with lookup { n =>
91+
println("error: free variable " ++ n.show)
92+
exit(1)
93+
}
94+
}
95+
96+
/// helper function for pretty string interpolation of terms
97+
def pretty { s: () => Unit / { literal, splice[Term] } }: String = {
98+
with stringBuffer
99+
100+
try { s(); do flush() }
101+
with literal { l => resume(do write(l)) }
102+
with splice[Term] { t =>
103+
t match {
104+
case Abs(n, t) => do write("λ" ++ n.show ++ pretty".${t}")
105+
case App(a, b) => do write(pretty"(${a} ${b})")
106+
case Var(v) => do write(v.show)
107+
}
108+
resume(())
109+
}
110+
}
111+
112+
/// convert char stream to bit stream, skipping non-bit chars
113+
def bits { p: () => Unit / { read[Bool], stop } }: Unit / read[Char] =
114+
try exhaustively { p() }
115+
with read[Bool] {
116+
with exhaustively
117+
val c = do read[Char]()
118+
if (c == '0') resume { false }
119+
if (c == '1') resume { true }
120+
}
121+
122+
/// evaluate the input BLC string and prettify it
123+
def testNormalization(input: String) = {
124+
with feed(input)
125+
with bits
126+
val t = parse()
127+
println(pretty"${t} ~> ${normalize(t)}")
128+
}
129+
130+
def main() = {
131+
var t = "00000001011110100111010"
132+
println("S ~> S:")
133+
testNormalization(t)
134+
135+
t = "010001011000000001011110100111010000011001000101100000000101111010011101000001100100010110000000010111101001110100000110010001011000000001011110100111010000011000010110000000010111101001110100000110"
136+
println("(ι (ι (ι (ι ι)))) ~> S:")
137+
testNormalization(t)
138+
139+
t = "010101000000010101100000011100001011100000011100101111011010100100000110110000010000101011110111100101011100000000111100001011110101100010000001101100101011100000110001000100000011100111010000001110011100111010000001110011100111010"
140+
println("(2^3)%3 == 2 // using Church numerals:")
141+
testNormalization(t)
142+
143+
// TODO: is there an off-by-one in the string buffer?
144+
t = "010000010101110000001100111000000101111011001110100011000100000011100111001110011100111010"
145+
println("5! == 120 // using Church numerals:")
146+
testNormalization(t)
147+
148+
t = "010001000101100000001000001100101000000000101011111001000001101100000110010111110000101011111000000001011001111001111111011011000110110001100000100100000001110010111101101001000001010111000000110011100000010111101100111010001100010010000000101000001101100010010111100001000001101100110111000110101000000111001110011100111001110011100111010"
149+
println("(prime? 7) == true // using Church numerals and Wilson's Theorem:")
150+
testNormalization(t)
151+
}

libraries/common/stringbuffer.effekt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def stringBuffer[A] { prog: => A / StringBuffer }: A = {
1414
var pos = 0
1515

1616
def ensureCapacity(sizeToAdd: Int): Unit = {
17-
val cap = buffer.size - pos + 1
17+
val cap = buffer.size - pos
1818
if (sizeToAdd <= cap) ()
1919
else {
2020
// Double the capacity while ensuring the required capacity

0 commit comments

Comments
 (0)