diff options
author | Case Duckworth | 2024-03-30 22:20:36 -0500 |
---|---|---|
committer | Case Duckworth | 2024-03-30 22:20:36 -0500 |
commit | ab8a02fd30451207578927c7e69aa397ad596459 (patch) | |
tree | 24803910776ed692f1610f44e35d0f23b9712ca3 | |
parent | Special-case '.', '...', '+', '-' (diff) | |
download | lam-ab8a02fd30451207578927c7e69aa397ad596459.tar.gz lam-ab8a02fd30451207578927c7e69aa397ad596459.zip |
Read from ports now
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | base.lua | 257 | ||||
-rw-r--r-- | core.lua | 75 | ||||
-rw-r--r-- | eval.lua | 102 | ||||
-rw-r--r-- | read.lua | 318 | ||||
-rw-r--r-- | repl.lua | 48 | ||||
-rw-r--r-- | type.lua | 202 |
7 files changed, 394 insertions, 610 deletions
diff --git a/Makefile b/Makefile index 0e1786c..51b3fc2 100644 --- a/Makefile +++ b/Makefile | |||
@@ -13,7 +13,7 @@ luarepl: | |||
13 | 13 | ||
14 | .PHONY: repl | 14 | .PHONY: repl |
15 | repl: | 15 | repl: |
16 | $(LUA) -e 'require("repl").repl()' | 16 | $(LUA) -e 'require("repl").repl("lam> ")' |
17 | 17 | ||
18 | .PHONY: test | 18 | .PHONY: test |
19 | test: | 19 | test: |
diff --git a/base.lua b/base.lua deleted file mode 100644 index b6e3a30..0000000 --- a/base.lua +++ /dev/null | |||
@@ -1,257 +0,0 @@ | |||
1 | --- lam.base --- base environment | ||
2 | |||
3 | local base = {} | ||
4 | local type = require "type" | ||
5 | local isNull, isa, totable = type.isNull, type.isa, type.totable | ||
6 | local math = math | ||
7 | |||
8 | base.env = { | ||
9 | -- Equivalence | ||
10 | ["eqv?"] = | ||
11 | function (r) | ||
12 | local a, b = r.car, r.cdr.car | ||
13 | if a == b then | ||
14 | return true | ||
15 | else | ||
16 | return false | ||
17 | end | ||
18 | end, | ||
19 | -- ["eq?"] = function (r) end, -- how would this be different to eqv? | ||
20 | -- Numbers | ||
21 | ["number?"] = function (r) return isa(r.car, "Number") end, | ||
22 | -- ["complex?"] = function (r) end, | ||
23 | -- ["real?"] = function (r) end, | ||
24 | -- ["rational?"] = function (r) end, | ||
25 | -- ["integer?"] = function (r) end, | ||
26 | -- ["exact?"] = function (r) end, | ||
27 | -- ["inexact?"] = function (r) end, | ||
28 | -- ["exact-integer?"] = function (r) end, | ||
29 | -- ["finite?"] = function (r) end, | ||
30 | -- ["infinite?"] = function (r) end, | ||
31 | ["="] = | ||
32 | function (r) | ||
33 | local n, r = r.car, r.cdr | ||
34 | while r.cdr do | ||
35 | if n ~= r.car then return false end | ||
36 | r = r.cdr | ||
37 | end | ||
38 | return true | ||
39 | end, | ||
40 | ["<"] = | ||
41 | function (r) | ||
42 | local n, r = r.car, r.cdr | ||
43 | while r.cdr do | ||
44 | if n >= r.car then return false end | ||
45 | r = r.cdr | ||
46 | end | ||
47 | return true | ||
48 | end, | ||
49 | [">"] = | ||
50 | function (r) | ||
51 | local n, r = r.car, r.cdr | ||
52 | while r.cdr do | ||
53 | if n <= r.car then return false end | ||
54 | r = r.cdr | ||
55 | end | ||
56 | return true | ||
57 | end, | ||
58 | ["<="] = | ||
59 | function (r) | ||
60 | local n, r = r.car, r.cdr | ||
61 | while r.cdr do | ||
62 | if n > r.car then return false end | ||
63 | r = r.cdr | ||
64 | end | ||
65 | return true | ||
66 | end, | ||
67 | [">="] = | ||
68 | function (r) | ||
69 | local n, r = r.car, r.cdr | ||
70 | while r.cdr do | ||
71 | if n < r.car then return false end | ||
72 | r = r.cdr | ||
73 | end | ||
74 | return true | ||
75 | end, | ||
76 | -- Math | ||
77 | ["+"] = | ||
78 | function (r) | ||
79 | local r, a = r, 0 | ||
80 | while r.cdr do | ||
81 | r, a = r.cdr, a + r.car | ||
82 | end | ||
83 | return a | ||
84 | end, | ||
85 | ["-"] = | ||
86 | function (r) | ||
87 | if isNull(r) then return -1 end | ||
88 | if isNull(r.cdr) then return (- r.car) end | ||
89 | local r, a = r.cdr, r.car | ||
90 | while r.cdr do | ||
91 | r, a = r.cdr, a - r.car | ||
92 | end | ||
93 | return a | ||
94 | end, | ||
95 | ["*"] = | ||
96 | function (r) | ||
97 | local r, a = r, 1 | ||
98 | while r.cdr do | ||
99 | if r.cdr == 0 then return 0 end | ||
100 | r, a = r.cdr, a * r.car | ||
101 | end | ||
102 | return a | ||
103 | end, | ||
104 | ["/"] = | ||
105 | function (r) | ||
106 | if isNull(r) then error("Wrong arity") end | ||
107 | if isNull(r.cdr) then return (1 / r.car) end | ||
108 | local r, a = r.cdr, r.car | ||
109 | while r.cdr do | ||
110 | r, a = r.cdr, a / r.car | ||
111 | end | ||
112 | return a | ||
113 | end, | ||
114 | quotient = | ||
115 | function (r) | ||
116 | end, | ||
117 | remainder = | ||
118 | function (r) | ||
119 | end, | ||
120 | modulo = | ||
121 | function (r) | ||
122 | end, | ||
123 | -- numerator = function (r) end, | ||
124 | -- denominator = function (r) end, | ||
125 | abs = function (r) return math.abs(r.car) end, | ||
126 | floor = -- largest integer <= x | ||
127 | function (r) return math.floor(r.car) end, | ||
128 | ceiling = -- smallest integer >= x | ||
129 | function (r) return math.ceil(r.car) end, | ||
130 | truncate = -- smallest |integer| <= |x| | ||
131 | function (r) | ||
132 | local i, _ = math.modf(r.car) | ||
133 | return i | ||
134 | end, | ||
135 | round = -- closest integer to x (ties go even) | ||
136 | function (r) | ||
137 | local i, f = math.modf(r.car) | ||
138 | if f == 0.5 then | ||
139 | if i % 2 == 0 then | ||
140 | return i | ||
141 | else | ||
142 | return i+1 | ||
143 | end | ||
144 | else | ||
145 | return i -- is this right? | ||
146 | end | ||
147 | end, | ||
148 | -- Trig | ||
149 | exp = function (r) return math.exp(r.car) end, | ||
150 | log = function (r) return math.log(r.car) end, | ||
151 | pi = math.pi, -- extension | ||
152 | sin = function (r) return math.sin(r.car) end, | ||
153 | cos = function (r) return math.cos(r.car) end, | ||
154 | tan = function (r) return math.tan(r.car) end, | ||
155 | asin = function (r) return math.asin(r.car) end, | ||
156 | acos = function (r) return math.acos(r.car) end, | ||
157 | atan = -- the two-argument variant of atan computes | ||
158 | -- (angle (make-rectangular x y)), even in implementations that | ||
159 | -- don't support general complex numbers [ed. note: whatever | ||
160 | -- that means] --- atan2 ??? | ||
161 | function (r) return math.atan(r.car) end, | ||
162 | sqrt = function (r) return math.sqrt(r.car) end, | ||
163 | expt = function (r) return r.car ^ r.cdr.car end, | ||
164 | -- ["make-rectangular"] = function (r) end, | ||
165 | -- ["make-polar"] = function (r) end, | ||
166 | -- ["real-part"] = function (r) end, | ||
167 | -- ["imag-part"] = function (r) end, | ||
168 | -- ["magnitude"] = function (r) end, | ||
169 | -- ["angle"] = function (r) end, | ||
170 | -- ["exact->inexact"] = function (r) end, | ||
171 | -- ["inexact->exact"] = function (r) end, | ||
172 | ["number->string"] = | ||
173 | function (r) | ||
174 | -- this will be somewhat complicated | ||
175 | end, | ||
176 | ["string->number"] = | ||
177 | function (r) | ||
178 | local n = r.car | ||
179 | if not isNull(r.cdr) then | ||
180 | local radix = r.cdr.car | ||
181 | end | ||
182 | -- This is technically an extension to r5rs | ||
183 | return tonumber(n, radix) | ||
184 | end, | ||
185 | -- Pairs | ||
186 | ["pair?"] = function (r) end, | ||
187 | cons = function (r) return type.Cons(r.car, r.cdr.car) end, | ||
188 | car = function (r) return r.car.car end, | ||
189 | cdr = function (r) return r.car.cdr end, | ||
190 | ["set-car!"] = function (r) r.car.car = r.cdr.car end, | ||
191 | ["set-cdr!"] = function (r) r.car.cdr = r.cdr.car end, | ||
192 | -- cxr | ||
193 | ["null?"] = function (r) return isNull(r.car) end, | ||
194 | ["list?"] = function (r) return type.isList(r.car) end, | ||
195 | list = function (r) return r end, -- r is already a list | ||
196 | -- Symbols | ||
197 | ["symbol?"] = function (r) return isa(r.car, "Symbol") end, | ||
198 | ["symbol->string"] = function (r) return type.String(r.car) end, | ||
199 | ["string->symbol"] = function (r) return type.Symbol(r.car.value) end, | ||
200 | -- Characters | ||
201 | ["char?"] = function (r) end, | ||
202 | ["char=?"] = function (r) end, | ||
203 | ["char<?"] = function (r) end, | ||
204 | ["char>?"] = function (r) end, | ||
205 | ["char<=?"] = function (r) end, | ||
206 | ["char>=?"] = function (r) end, | ||
207 | ["char->integer"] = function (r) end, | ||
208 | ["integer->char"] = function (r) end, | ||
209 | -- Strings | ||
210 | ["string?"] = function (r) end, | ||
211 | ["make-string"] = function (r) end, | ||
212 | ["string-length"] = function (r) end, | ||
213 | ["string-ref"] = function (r) end, | ||
214 | -- ["string-set!"] = function (r) end, -- not sure if i'll implement | ||
215 | -- Vectors | ||
216 | ["vector?"] = function (r) end, | ||
217 | ["make-vector"] = function (r) end, | ||
218 | ["vector-length"] = function (r) end, | ||
219 | ["vector-ref"] = function (r) end, | ||
220 | ["vector-set!"] = function (r) end, | ||
221 | -- Control | ||
222 | ["procedure?"] = function (r) end, | ||
223 | apply = function (r) end, | ||
224 | ["call-with-current-continuation"] = function (r) end, | ||
225 | values = function (r) end, | ||
226 | ["call-with-values"] = function (r) end, | ||
227 | ["dynamic-wind"] = function (r) end, | ||
228 | -- Eval | ||
229 | eval = function (r) end, | ||
230 | ["scheme-report-environment"] = function (r) end, | ||
231 | ["null-environment"] = function (r) end, | ||
232 | -- Ports | ||
233 | ["input-port?"] = function (r) end, | ||
234 | ["output-port?"] = function (r) end, | ||
235 | ["current-input-port"] = function (r) end, | ||
236 | ["current-output-port"] = function (r) end, | ||
237 | ["open-input-file"] = function (r) end, | ||
238 | ["open-output-file"] = function (r) end, | ||
239 | ["close-input-port"] = function (r) end, | ||
240 | ["close-output-port"] = function (r) end, | ||
241 | -- Input | ||
242 | read = function (r) end, | ||
243 | ["read-char"] = function (r) end, | ||
244 | ["peek-char"] = function (r) end, | ||
245 | ["eof-object?"] = function (r) end, | ||
246 | ["char-ready?"] = function (r) end, | ||
247 | -- Output | ||
248 | write = function (r) end, | ||
249 | display = function (r) end, | ||
250 | newline = function (r) end, | ||
251 | ["write-char"] = function (r) end, | ||
252 | -- System | ||
253 | load = function (r) end, | ||
254 | } | ||
255 | |||
256 | --- | ||
257 | return base | ||
diff --git a/core.lua b/core.lua new file mode 100644 index 0000000..e8ad42b --- /dev/null +++ b/core.lua | |||
@@ -0,0 +1,75 @@ | |||
1 | --- lam.core --- core procedures | ||
2 | |||
3 | local m = {} | ||
4 | local type = require "type" | ||
5 | local isa, null = type.isa, type.null | ||
6 | local math = math | ||
7 | |||
8 | local function fold (kons, knil, r) | ||
9 | if r == null then | ||
10 | return knil | ||
11 | else | ||
12 | local step, early_return = kons(r[1], knil) | ||
13 | if early_return then return step end | ||
14 | return fold(kons, step, r[2]) | ||
15 | end | ||
16 | end | ||
17 | |||
18 | m.env = { -- all functions here take R, which is the list of arguments | ||
19 | ------- numbers | ||
20 | ["number?"] = function (r) return isa(r[1], "number") end, | ||
21 | ["="] = | ||
22 | function (r) | ||
23 | local function go (a, b) | ||
24 | if a ~= b then return false, 1 end | ||
25 | return b | ||
26 | end | ||
27 | return fold(go, r[1], r[2]) and true | ||
28 | end, | ||
29 | ["<"] = | ||
30 | function (r) | ||
31 | local function go (a, b) | ||
32 | if a >= b then return false, 1 end | ||
33 | return b | ||
34 | end | ||
35 | return fold(go, r[1], r[2]) and true | ||
36 | end, | ||
37 | [">"] = | ||
38 | function (r) | ||
39 | local function go (a, b) | ||
40 | if a <= b then return false, 1 end | ||
41 | return b | ||
42 | end | ||
43 | return fold(go, r[1], r[2]) and true | ||
44 | end, | ||
45 | ["<="] = function (r) return not m.env[">"](r) end, | ||
46 | [">="] = function (r) return not m.env["<"](r) end, | ||
47 | ------- math | ||
48 | ["+"] = | ||
49 | function (r) | ||
50 | return fold(function (a, b) return a + b end, 0, r) | ||
51 | end, | ||
52 | ["-"] = | ||
53 | function (r) | ||
54 | if r == null then return -1 end | ||
55 | if r[2] == null then return (- r[1]) end | ||
56 | return fold(function (a, b) return a-b end, r[1], r[2]) | ||
57 | end, | ||
58 | ["*"] = | ||
59 | function (r) | ||
60 | local function go (a, b) | ||
61 | if a == 0 or b == 0 then return 0, 1 end | ||
62 | return a * b | ||
63 | end | ||
64 | return fold(go, 1, r) | ||
65 | end, | ||
66 | ["/"] = | ||
67 | function (r) | ||
68 | if r == null then error("Wrong arity") end | ||
69 | if r[2] == null then return (1 / r[1]) end | ||
70 | return fold(function (a, b) return a/b end, r[1], r[2]) | ||
71 | end, | ||
72 | } | ||
73 | |||
74 | -------- | ||
75 | return m | ||
diff --git a/eval.lua b/eval.lua index 610b902..4a4ad0e 100644 --- a/eval.lua +++ b/eval.lua | |||
@@ -1,114 +1,100 @@ | |||
1 | --- lam.eval | 1 | --- lam.eval |
2 | 2 | ||
3 | local eval = {} | 3 | local m = {} |
4 | local base = require "base" | 4 | local core = require "core" |
5 | local type = require "type" | 5 | local type = require "type" |
6 | local isNull, isList, isa, List, Cons = | ||
7 | type.isNull, type.isList, type.isa, type.List, type.Cons | ||
8 | local unpack = table.unpack or unpack | ||
9 | 6 | ||
10 | function eval.Env (inner, outer) | 7 | function m.environ (inner, outer) |
11 | local mt = { | 8 | local mt = { |
12 | __type = "Environment", | 9 | __type = "environment", |
13 | __index = outer, | 10 | __index = outer, |
14 | __newindex = | 11 | __newindex = |
15 | function (self, key, value) | 12 | function (self, key, val) |
16 | if rawget(self, key) then | 13 | if rawget(self, key) then |
17 | -- Set the current environment's value | 14 | rawset(self, key, val) |
18 | rawset(self, key, value) | ||
19 | else | 15 | else |
20 | -- Set the outer value | 16 | getmetatable(self).__index[key] = val |
21 | getmetatable(self).__index[key] = value | ||
22 | end | 17 | end |
23 | end, | 18 | end, |
24 | } | 19 | } |
25 | return setmetatable(inner, mt) | 20 | return setmetatable(inner, mt) |
26 | end | 21 | end |
27 | 22 | ||
28 | function eval.Proc (params, body, env) | 23 | function m.procedure (params, body, env) |
29 | local v = { | 24 | local t = { |
30 | params = params, | 25 | params = params, |
31 | body = body, | 26 | body = body, |
32 | env = env, | 27 | env = env, |
33 | } | 28 | } |
34 | local mt = { | 29 | local mt = { |
35 | __type = "Procedure", | 30 | __type = "procedure", |
36 | __call = | 31 | __call = |
37 | function (self, args) | 32 | function (self, args) |
38 | local inner = {} | 33 | local inner = {} |
39 | local p, a = self.params, args | 34 | local p, a = self.params, args |
40 | while p.cdr and a.cdr do | 35 | while p[2] and a[2] do |
41 | inner[p.car] = a.car | 36 | inner[p[1]] = a[1] |
42 | p, a = p.cdr, a.cdr | 37 | p, a = p[2], a[2] |
43 | end | 38 | end |
44 | local b = self.body | 39 | local b = self.body |
45 | local e = eval.Env(inner, self.env) | 40 | local e = m.environ(inner, self.env) |
46 | while not isNull(b.cdr) do | 41 | while not b[2] == type.null do |
47 | eval.eval(b.car, e) | 42 | m.eval(b[1], e) |
48 | b = b.cdr | 43 | b = b[2] |
49 | end | 44 | end |
50 | return eval.eval(b.car, e) | 45 | return m.eval(b[1], e) |
51 | end, | 46 | end, |
52 | } | 47 | } |
53 | return setmetatable(v, mt) | 48 | return setmetatable(t, mt) |
54 | end | 49 | end |
55 | 50 | ||
56 | local specials = { | 51 | local specials = { |
52 | -- each of these takes R (a list of args) and E (an environment) | ||
57 | quote = | 53 | quote = |
58 | function (args, env) | 54 | function (r, e) return r[1] end, |
59 | return args.car | ||
60 | end, | ||
61 | define = | 55 | define = |
62 | function (args, env) | 56 | function (r, e) rawset(e, r[1], m.eval(r[2][1], e)) end, |
63 | rawset(env, args.car, eval(args.cdr.car, env)) | ||
64 | return nil | ||
65 | end, | ||
66 | lambda = | 57 | lambda = |
67 | function (args, env) | 58 | function (r, e) return m.procedure(r[1], r[2], e) end, |
68 | return Proc(args.car, args.cdr, env) | ||
69 | end, | ||
70 | ["set!"] = | 59 | ["set!"] = |
71 | function (args, env) | 60 | function (r, e) e[r[1]] = m.eval(r[2][1], e) end, |
72 | env[args.car] = eval(args.cdr.car, env) | ||
73 | return nil | ||
74 | end, | ||
75 | ["if"] = | 61 | ["if"] = |
76 | function (args, env) | 62 | function (r, e) |
77 | local test, conseq, alt = | 63 | local test, conseq, alt = |
78 | args.car, args.cdr.car, args.cdr.cdr.car | 64 | r[1], r[2][1], r[2][2][1] |
79 | if eval(test) | 65 | if m.eval(test) |
80 | then return eval(conseq) | 66 | then return m.eval(conseq) |
81 | else return eval(alt) | 67 | else return m.eval(alt) |
82 | end | 68 | end |
83 | end, | 69 | end, |
84 | -- TODO: include, import, define-syntax, define-values(?) ... | 70 | -- TODO: include, import, define-syntax, ... |
85 | } | 71 | } |
86 | -- Aliases | 72 | -- Aliases |
87 | specials.lam = specials.lambda | 73 | specials.lam = specials.lambda |
88 | specials.def = specials.define | 74 | specials.def = specials.define |
89 | 75 | ||
90 | function eval.eval (x, env) | 76 | function m.eval (x, env) |
91 | env = env or base.env | 77 | local env = env or core.env |
92 | if isa(x, "Symbol") then | 78 | if type.isa(x, "symbol") then |
93 | return env[x] | 79 | return env[x] |
94 | elseif not isList(x) then | 80 | elseif not type.islist(x) then |
95 | return x | 81 | return x |
96 | else | 82 | else |
97 | local op, args = x.car, x.cdr | 83 | local op, args = x[1], x[2] |
98 | if specials[op] then | 84 | if specials[op] then |
99 | return specials[op](args, env) | 85 | return specials[op](args, env) |
100 | else -- procedure | 86 | else -- procedure call |
101 | local proc = eval.eval(op, env) | 87 | local fn = m.eval(op, env) |
102 | local params = {} | 88 | local params = {} |
103 | local a = args | 89 | local r = args |
104 | while a.cdr do | 90 | while r[2] do |
105 | table.insert(params, eval.eval(a.car, env)) | 91 | table.insert(params, m.eval(r[1], env)) |
106 | a = a.cdr | 92 | r = r[2] |
107 | end | 93 | end |
108 | return proc(List(params)) | 94 | return fn(type.list(params)) |
109 | end | 95 | end |
110 | end | 96 | end |
111 | end | 97 | end |
112 | 98 | ||
113 | --- | 99 | -------- |
114 | return eval | 100 | return m |
diff --git a/read.lua b/read.lua index f23c5cc..226af51 100644 --- a/read.lua +++ b/read.lua | |||
@@ -1,230 +1,170 @@ | |||
1 | --- lam.read | 1 | --- lam.read |
2 | 2 | ||
3 | local read = {} | 3 | local m = {} |
4 | local type = require "type" | 4 | local t = require "type" |
5 | local utf8 = require "utf8" | 5 | local utf8 = require "utf8" |
6 | local util = require "util" | 6 | local pop = require("util").pop |
7 | local pop = util.pop | 7 | |
8 | local unpack = table.unpack or unpack | 8 | local pp = require("pp").pp |
9 | 9 | ||
10 | local function program_characters (program) | 10 | function m.inport (source, kind) |
11 | local chars = {} | 11 | -- KIND can be one of "file", "string"; defaults to "file" |
12 | for pos, code in utf8.codes(program) do | 12 | -- SOURCE is the name of the file or the string to read, or nil; if nil, |
13 | table.insert(chars, code) | 13 | -- read from standard input. |
14 | end | 14 | local f, l |
15 | return chars | 15 | local k = kind or "file" |
16 | end | 16 | if source then |
17 | 17 | if kind == "file" then | |
18 | local function consume_string_whitespace (chars) | 18 | f = io.open(source, "r") |
19 | -- \<intraline ws>*<line ending> <intraline ws>* : nothing | 19 | elseif kind == "string" then |
20 | local s = {"\\"} | 20 | l = m.tochars(source) |
21 | while chars[1]:match("[ \t]") do | 21 | end |
22 | table.insert(s, pop(chars)) | 22 | else |
23 | end | 23 | -- KIND is ignored here |
24 | if chars[1] ~= "\n" then | 24 | f = io.input() |
25 | table.insert(s, chars[1]) | ||
26 | return table.concat(s), chars | ||
27 | end | ||
28 | while chars[1]:match("%s") do | ||
29 | pop(chars) | ||
30 | end | 25 | end |
31 | return chars[1], chars | 26 | local t = { |
32 | end | 27 | file = f, |
33 | 28 | line = l or {}, | |
34 | local function consume_string_hexvalue (chars) | 29 | next_token = |
35 | -- \x<hex scalar value>; : specified character | 30 | function (self) |
36 | local u8ch = {} | 31 | local tok, toktype |
37 | repeat | 32 | while true do |
38 | local c = pop(chars) | 33 | if #self.line == 0 and self.file then |
39 | table.insert(u8ch, c) | 34 | self.line = m.tochars( |
40 | until c == ";" | 35 | self.file:read("*l")) |
41 | table.remove(u8ch) -- remove semicolon | 36 | end |
42 | return utf8.char(tonumber(table.concat(u8ch), 16)), chars | 37 | if not self.line or #self.line == 0 then |
43 | end | 38 | return nil |
44 | 39 | end | |
45 | local function consume_string (chars) | 40 | tok, toktype, self.line = |
46 | local str = {} | 41 | m.scan(self.line)() |
47 | local backslash = { | 42 | if tok then return tok, toktype end |
48 | a = "\a", | ||
49 | b = "\b", | ||
50 | t = "\t", | ||
51 | n = "\n", | ||
52 | r = "\r", | ||
53 | ["\""] = "\"", | ||
54 | ["\\"] = "\\", | ||
55 | ["|"] = "|", | ||
56 | [" "] = consume_string_whitespace, | ||
57 | ["\t"] = consume_string_whitespace, | ||
58 | ["\n"] = consume_string_whitespace, | ||
59 | x = consume_string_hexvalue, | ||
60 | } | ||
61 | pop(chars) -- throw initial " away | ||
62 | repeat | ||
63 | local c = pop(chars) | ||
64 | if c == [[\]] then | ||
65 | c = chars[1] | ||
66 | if backlash[c] then | ||
67 | if type(backslash[c]) == "function" then | ||
68 | c, chars = backslash[c](chars) | ||
69 | table.insert(str, c) | ||
70 | else | ||
71 | table.insert(str, backlash[c]) | ||
72 | end | 43 | end |
73 | else | 44 | end, |
74 | table.insert(str, "\\"..c) | 45 | } |
75 | end | 46 | if t.file then t.close = function (self) self.file:close() end; end |
76 | pop(chars) | 47 | local mt = { |
77 | elseif c == [["]] then | 48 | __type = "port", |
78 | break | 49 | __tostring = |
79 | else | 50 | function (self) |
80 | table.insert(str, c) | 51 | return string.format("#<port %s>", |
81 | end | 52 | self.file or "(string)") |
82 | until #chars == 0 | 53 | end, |
83 | return table.concat(str), "string", chars | 54 | } |
55 | return setmetatable(t, mt) | ||
84 | end | 56 | end |
85 | 57 | ||
86 | local function consume_token (chars) | 58 | function m.tochars (s) |
87 | local tok = {} | 59 | local chars = {} |
88 | while #chars>0 and chars[1]:match("[^%s()\"#'`,@;]") do | 60 | for _, code in utf8.codes(s) do |
89 | table.insert(tok, pop(chars)) | 61 | table.insert(chars, code) |
90 | end | 62 | end |
91 | return table.concat(tok), chars | 63 | return chars |
92 | end | 64 | end |
93 | 65 | ||
94 | local consume_symbol = consume_token | 66 | --- Consumers |
67 | -- These take a table of characters (cs) and return a token and the rest of the | ||
68 | -- chars | ||
95 | 69 | ||
96 | local function consume_number (chars) | 70 | local function consume_token (cs) |
97 | local digits, chars = consume_token(chars) | 71 | local token = {} |
98 | -- The signs by themselves are symbols, as well as '...' | 72 | while #cs > 0 and cs[1]:match("[^%s()\"#'`,@;]") do |
99 | if digits:match("[-+.]") or digits == "..." then | 73 | table.insert(token, pop(cs)) |
100 | return digits, chars | ||
101 | end | 74 | end |
102 | -- Otherwise try converting the digits to a number | 75 | return table.concat(token), cs |
103 | local num = tonumber(digits) | ||
104 | if num == nil then error("Bad number: " .. num) end | ||
105 | return num, chars | ||
106 | end | 76 | end |
107 | 77 | ||
108 | local function consume_whitespace (chars) | 78 | local function consume_whitespace (cs) |
109 | while #chars>0 and chars[1]:match("%s") do pop(chars) end | 79 | while #cs > 0 and cs[1]:match("%s") do pop(cs) end |
110 | return chars | 80 | return nil, cs |
111 | end | 81 | end |
112 | 82 | ||
113 | local function consume_comment (chars) | 83 | --- Reading from a port |
114 | local comment = {} | ||
115 | repeat | ||
116 | table.insert(comment, pop(chars)) | ||
117 | until #chars == 0 or chars[1]:match("\n") | ||
118 | return table.concat(comment), "comment", chars | ||
119 | end | ||
120 | 84 | ||
121 | --- API | 85 | m.readtable = { |
122 | 86 | ["("] = function (cs) return pop(cs), "open", cs end, | |
123 | read.readtable = { | 87 | [")"] = function (cs) return pop(cs), "close", cs end, |
124 | ["("] = function(chars) return pop(chars), "open", chars end, | ||
125 | [")"] = function(chars) return pop(chars), "close", chars end, | ||
126 | ["'"] = function(chars) return pop(chars), "quote", chars end, | ||
127 | ["`"] = function(chars) return pop(chars), "quote", chars end, | ||
128 | [","] = function(chars) return pop(chars), "quote", chars end, | ||
129 | ["\""] = consume_string, | ||
130 | [";"] = consume_comment, | ||
131 | -- ["#"] = ..., | ||
132 | } | 88 | } |
133 | 89 | ||
134 | function read.scan (chars) | 90 | -- Return an iterator over a character table, so you can do: |
135 | local chars = chars | 91 | -- for token, chars in scan(cs) do ... end |
92 | function m.scan (cs) | ||
93 | local cs = cs | ||
136 | return function () | 94 | return function () |
137 | if not next(chars) then return nil end | 95 | if not next(cs) then return nil end |
138 | local token, toktype = "", nil | 96 | local token, toktype |
139 | while true do | 97 | while true do |
140 | if read.readtable[chars[1]] then | 98 | if m.readtable[cs[1]] then |
141 | token, toktype, chars = | 99 | token, toktype, cs = m.readtable[cs[1]](cs) |
142 | read.readtable[chars[1]](chars) | 100 | -- return { v = token, u = toktype }, cs |
143 | return token, toktype | 101 | return token, toktype, cs |
144 | elseif chars[1]:match("%s") then | 102 | elseif cs[1]:match("%s") then |
145 | chars = consume_whitespace(chars) | 103 | _, cs = consume_whitespace(cs) |
146 | elseif chars[1]:match("%d") then | 104 | return nil, nil, cs |
147 | token, chars = consume_number(chars) | 105 | -- return nil, cs |
148 | return token, "number" | 106 | elseif cs[1]:match("[%d.+-]") then |
149 | elseif chars[1]:match("[.+-]") then | 107 | -- numbers, +, -, ., ... |
150 | -- special casing for ., ..., +, - | 108 | local token, cs = consume_token(cs) |
151 | token, chars = consume_number(chars) | 109 | if token:match("[-+]") or token == "..." then |
152 | if token == "." then | 110 | return token, "symbol", cs |
153 | return token, "dot" | 111 | -- return { v = token, u = "symbol" }, cs |
154 | elseif token == "..." then | 112 | elseif token == "." then |
155 | return token, "symbol" | 113 | return token, "dot", cs |
114 | -- return { v = token, u = "dot" }, cs | ||
156 | else | 115 | else |
157 | return token, "number" | 116 | local n = tonumber(token) |
117 | assert (n ~= nil, "Bad number: "..n) | ||
118 | return n, "number", cs | ||
119 | -- return { v = n, u = "number" }, cs | ||
158 | end | 120 | end |
159 | else | 121 | else |
160 | token, chars = consume_symbol(chars) | 122 | token, cs = consume_token(cs) |
161 | return token, "symbol" | 123 | return token, "symbol", cs |
124 | -- return { v = token, u = "symbol" }, cs | ||
162 | end | 125 | end |
163 | if #chars == 0 then return nil end | ||
164 | end | 126 | end |
165 | end | 127 | end |
166 | end | 128 | end |
167 | 129 | ||
168 | function read.tokenize (program) | 130 | function m.readchar (port) |
169 | if not program or #program == 0 then return nil end | 131 | if #port.line > 0 then |
170 | local tokens = {} | 132 | local ch = pop(port.line) |
171 | for token, toktype in read.scan(program_characters(program)) do | 133 | return ch |
172 | table.insert(tokens, {type = toktype, value = token}) | 134 | else |
135 | return port.file.read(1) | ||
173 | end | 136 | end |
174 | return tokens | ||
175 | end | 137 | end |
176 | 138 | ||
177 | read.readmacros = { | 139 | function m.read (port) |
178 | open = | 140 | local function read_ahead (tok, toktype) |
179 | function (token, tokens) | 141 | if not tok then error("Unexpected EOF") end |
180 | local L, lt = {}, nil | 142 | if toktype == "open" then |
181 | while tokens[1].type ~= "close" do | 143 | local L = {} |
182 | local nt = read.parse(tokens) | 144 | while true do |
183 | -- this isn't .. my /favorite/ implementation, | 145 | local tok, toktype = port:next_token() |
184 | -- but it works | 146 | if toktype == "close" then |
185 | if nt == "." then | 147 | return t.list(L) |
186 | lt = read.parse(tokens) | ||
187 | break | ||
188 | else | 148 | else |
189 | table.insert(L, nt) | 149 | table.insert(L, |
150 | read_ahead(tok, toktype)) | ||
190 | end | 151 | end |
191 | assert(tokens[1], "Unexpected EOF") | ||
192 | end | 152 | end |
193 | pop(tokens) -- remove final ")" | 153 | elseif toktype == "close" then |
194 | return type.List(L, lt) | 154 | error("Unexpected ')'") |
195 | end, | 155 | else return tok |
196 | close = | 156 | end |
197 | function (token, tokens) | 157 | end |
198 | error ("Unexpected '" .. token.value .. "'") | 158 | -- body of read |
199 | end, | 159 | local tok1, toktype1 = port:next_token() |
200 | quote = | 160 | if not tok1 then return nil |
201 | function (token, tokens) | 161 | else return read_ahead(tok1, toktype1) |
202 | local Q | ||
203 | if token.value == "'" then | ||
204 | Q = {"quote"} | ||
205 | elseif token.value == "`" then | ||
206 | Q = {"quasiquote"} | ||
207 | elseif token.value == "," then | ||
208 | Q = {"unquote"} | ||
209 | end | ||
210 | table.insert(Q, read.parse(tokens)) | ||
211 | return type.List(Q) | ||
212 | end, | ||
213 | } | ||
214 | |||
215 | function read.parse (tokens) | ||
216 | if not next(tokens) then return nil end | ||
217 | local token = pop(tokens) | ||
218 | if read.readmacros[token.type] then | ||
219 | return read.readmacros[token.type](token, tokens) | ||
220 | else | ||
221 | return token.value | ||
222 | end | 162 | end |
223 | end | 163 | end |
224 | 164 | ||
225 | function read.read (program) | 165 | function m.read_string (str) |
226 | return read.parse(read.tokenize(program)) | 166 | return m.read(m.inport(str, "string")) |
227 | end | 167 | end |
228 | 168 | ||
229 | --- | 169 | --- |
230 | return read | 170 | return m |
diff --git a/repl.lua b/repl.lua index b198880..556525c 100644 --- a/repl.lua +++ b/repl.lua | |||
@@ -1,10 +1,12 @@ | |||
1 | --- lam.repl | 1 | --- lam.repl |
2 | 2 | ||
3 | local repl = {} | 3 | local m = {} |
4 | local eval = require("eval").eval | 4 | local read = require("read") |
5 | local read = require("read").read | 5 | local eval = require("eval") |
6 | local pp = require("pp").pp | ||
6 | 7 | ||
7 | local function schemeprint (x) | 8 | local function schemeprint (x) |
9 | -- if x == nil then return end | ||
8 | if x == true then | 10 | if x == true then |
9 | print("#t") | 11 | print("#t") |
10 | elseif x == false then | 12 | elseif x == false then |
@@ -14,19 +16,33 @@ local function schemeprint (x) | |||
14 | end | 16 | end |
15 | end | 17 | end |
16 | 18 | ||
17 | function repl.repl (prompt) | 19 | function m.repl (prompt, infile, out) |
18 | if not prompt then prompt = "lam> " end | 20 | -- PROMPT should be a string, INFILE is a filename, and OUT is either a |
19 | io.input():setvbuf("line") | 21 | -- filename, nil (in which case it will be stdout), or false (which |
20 | repeat | 22 | -- suppresses output) |
21 | io.write(prompt) | 23 | local inport = read.inport(infile) |
22 | io.output():flush() | 24 | if out ~= false then io.output(out) end |
23 | local input = io.read() | 25 | io.output():setvbuf("line") |
24 | if input ~= "" then | 26 | if prompt then |
25 | local value = eval(read(input)) | 27 | stderr = io.open("/dev/stderr", "w") -- Linux-only ! |
26 | if value ~= nil then schemeprint(value) end | 28 | end |
29 | while true do | ||
30 | if prompt then | ||
31 | stderr:write(prompt) | ||
32 | stderr:flush() | ||
33 | end | ||
34 | local x = read.read(inport) | ||
35 | if x then | ||
36 | local val = eval.eval(x) | ||
37 | if out ~= false then | ||
38 | schemeprint(val) | ||
39 | end | ||
27 | end | 40 | end |
28 | until false | 41 | end |
42 | inport:close() | ||
43 | stderr:close() | ||
44 | io.output():close() | ||
29 | end | 45 | end |
30 | 46 | ||
31 | --- | 47 | -------- |
32 | return repl | 48 | return m |
diff --git a/type.lua b/type.lua index 0a0c62d..3c26188 100644 --- a/type.lua +++ b/type.lua | |||
@@ -1,138 +1,162 @@ | |||
1 | --- lam.type | 1 | --- lam.type |
2 | -- lisp types | ||
3 | 2 | ||
4 | local t = {} | 3 | local m = {} |
5 | local util = require "util" | 4 | local utf8 = require "utf8" |
6 | local unpack = table.unpack or unpack | 5 | utf_char, utf_codepoint = utf8.char, utf8.codepoint |
7 | 6 | ||
8 | --- Determining types | 7 | --- atomic types |
9 | 8 | ||
10 | t.luatype = type | 9 | -- true, false and nil are just ... true, false, and nil |
11 | 10 | ||
12 | function t.lamtype (x) | 11 | -- Characters contain both their string reputations and their codepoints |
13 | if t.luatype(x) == "number" then | 12 | function m.character (x) |
14 | return "Number" | 13 | -- is storing a character with its string and numerical representation |
15 | elseif t.luatype(x) == "string" then | 14 | -- overkill? ... maybe. |
16 | return "Symbol" | 15 | local s = tostring(x) |
17 | elseif getmetatable(x) and getmetatable(x).__type then | 16 | local uc = utf_codepoint(s) |
18 | return getmetatable(x).__type | 17 | local t = { -- String representation of the character |
19 | else | 18 | v = utf_char(uc), |
20 | return t.luatype(x) | 19 | u = uc, |
21 | end | 20 | } |
22 | end | 21 | local mt = { |
23 | 22 | __type = "character", | |
24 | -- isa is really only useful on basic types (i.e., not Lists) | 23 | __eq = function (self) return self.v end, |
25 | function t.isa (x, type) | 24 | __lt = function (a, b) return a.u < b.u end, |
26 | return t.lamtype(x) == type | 25 | __tostring = |
26 | function (self) | ||
27 | local v = self.v | ||
28 | if v == "\n" then | ||
29 | return "#\\newline" | ||
30 | elseif v == " " then | ||
31 | return "#\\space" | ||
32 | else | ||
33 | return "#\\" .. v | ||
34 | end | ||
35 | end, | ||
36 | } | ||
37 | return setmetatable(t, mt) | ||
27 | end | 38 | end |
28 | 39 | ||
29 | --- Creating types | 40 | -- a symbol is just a string, unadorned. I was going to have a character be |
30 | 41 | -- represented by a one-character string, but then it would be indistinguishable | |
31 | -- Symbols and Numbers are strings and numbers, respectively. At some point | 42 | -- from a one-character symbol internally. |
32 | -- I'll want to implement a full numeric tower and symbol tables or namespaces | 43 | m.symbol = tostring |
33 | -- or whatever, but today is not that day | ||
34 | 44 | ||
35 | t.Symbol = tostring | 45 | -- for now, number will just be lua's number. At *some* point, it will be the |
36 | t.Number = tonumber | 46 | -- whole numeric tower, yaaayyy |
47 | m.number = tonumber | ||
37 | 48 | ||
38 | -- Strings are (lightly) wrapped | 49 | -- strings are wrapped strings |
39 | function t.String (str) | 50 | function m.string (x) |
40 | local v = { | 51 | local x = tostring(x) |
41 | value = str, | 52 | local t = { |
53 | v = x, | ||
42 | escape = | 54 | escape = |
43 | function (self) | 55 | function (self) |
44 | return self.gsub("[\\\"]", "\\%1") | 56 | return self.v:gsub("[\\\"]", "\\%1") |
45 | end, | 57 | end, |
46 | } | 58 | } |
47 | local mt = { | 59 | local mt = { |
48 | __type = "String", | 60 | __type = "string", |
49 | __tostring = | 61 | __tostring = |
50 | function (self) | 62 | function (self) |
51 | return string.format("\"%s\"", self:escape()) | 63 | return "\"" .. self:escape() .. "\"" |
52 | end, | 64 | end, |
53 | } | 65 | } |
54 | return setmetatable(v, mt) | 66 | return setmetatable(t, mt) |
55 | end | 67 | end |
56 | 68 | ||
57 | function t.totable (cons) | 69 | -- null () is both an atom and a list (yay) |
58 | local out = {} | 70 | -- this one is NOT a function |
59 | local car, cdr = cons.car, cons.cdr | 71 | m.null = setmetatable({}, { |
60 | while cdr do | 72 | __type = "null", |
61 | table.insert(out, tostring(car)) | 73 | __tostring = function (self) return "()" end, |
62 | if t.luatype(cdr) == "table" then | 74 | }) |
63 | car = cdr.car | 75 | |
64 | cdr = cdr.cdr | 76 | --- collection types |
65 | else | ||
66 | table.insert(out, cdr) | ||
67 | break | ||
68 | end | ||
69 | end | ||
70 | return out | ||
71 | end | ||
72 | 77 | ||
73 | -- Conses are Lisp's fundamental collection type | 78 | -- cons are lisp's fundamental collection type |
74 | function t.Cons (a, b) | 79 | function m.cons (a, b) |
75 | local v = { a, b, } | 80 | local t = { a, b, } |
76 | local mt = { | 81 | local mt = { |
77 | __type = "Cons", | 82 | __type = "cons", |
78 | __index = | ||
79 | function (self, key) | ||
80 | if key == "car" then | ||
81 | return self[1] | ||
82 | elseif key == "cdr" then | ||
83 | return self[2] | ||
84 | end | ||
85 | end, | ||
86 | __tostring = | 83 | __tostring = |
87 | function (self) | 84 | function (self) |
88 | local out = {} | 85 | local out = {} |
89 | local car, cdr = self.car, self.cdr | 86 | local car, cdr = self[1], self[2] |
90 | while cdr do | 87 | while cdr do |
91 | table.insert(out, tostring(car)) | 88 | table.insert(out, tostring(car)) |
92 | if t.luatype(cdr) == "table" then | 89 | if m.luatype(cdr) == "table" then |
93 | car = cdr.car | 90 | car = cdr[1] |
94 | cdr = cdr.cdr | 91 | cdr = cdr[2] |
95 | else | 92 | else |
96 | table.insert(out, ".") | 93 | table.insert(out, ".") |
97 | table.insert(out, cdr) | 94 | table.insert(out, cdr) |
98 | break | 95 | break |
99 | end | 96 | end |
100 | end | 97 | end |
101 | return "("..table.concat(out, " ")..")" | 98 | return "(" .. table.concat(out, " ") .. ")" |
102 | end, | 99 | end, |
103 | } | 100 | } |
104 | return setmetatable(v, mt) | 101 | return setmetatable(t, mt) |
105 | end | 102 | end |
106 | 103 | ||
107 | -- Null is the one value that is both an atom and a list | 104 | -- lists are singly-linked cons cells |
108 | t.Null = setmetatable({}, { | 105 | function m.list (items, last) |
109 | __type = "Null", | 106 | -- ITEMS is a table and LAST is an optional final cdr. If it's nil, the |
110 | __tostring = function (self) return "()" end, | 107 | -- list is a "proper" list; that is, it ends in (). |
111 | }) | 108 | local function tolist (base, items) |
109 | if #items == 0 then return base end | ||
110 | return tolist(m.cons(table.remove(items), base), items) | ||
111 | end | ||
112 | return tolist(last or m.null, items) | ||
113 | end | ||
112 | 114 | ||
113 | function t.isNull (x) | 115 | -- convert a list to a lua table |
114 | return x == t.Null | 116 | function m.totable (cons) |
117 | local t = {} | ||
118 | local car, cdr = cons[1], cons[2] | ||
119 | while cdr do | ||
120 | table.insert(t, car) | ||
121 | if m.luatype(cdr) == "table" then | ||
122 | car = cdr[1] | ||
123 | cdr = cdr[2] | ||
124 | else | ||
125 | table.insert(t, cdr) | ||
126 | end | ||
127 | end | ||
128 | return t | ||
115 | end | 129 | end |
116 | 130 | ||
117 | -- Lists are chained Conses ending in Null | 131 | -- testing types |
118 | function t.List (items, last) | 132 | |
119 | local function tolist (base, items) | 133 | -- we love name collisions |
120 | if #items == 0 then return base end | 134 | m.luatype = type |
121 | return tolist(t.Cons(table.remove(items), base), items) | 135 | |
136 | function m.lamtype (x) | ||
137 | if m.luatype(x) == "string" then | ||
138 | return "symbol" | ||
139 | elseif getmetatable(x) and getmetatable(x).__type then | ||
140 | return getmetatable(x).__type | ||
141 | else | ||
142 | return m.luatype(x) | ||
122 | end | 143 | end |
123 | return tolist(last or t.Null, items) | ||
124 | end | 144 | end |
125 | 145 | ||
126 | function t.isList (x) | 146 | function m.isa (x, t) |
127 | -- TODO: this does not detect circular lists yet | 147 | return m.lamtype(x) == t |
128 | if t.isNull(x) then | 148 | end |
149 | |||
150 | function m.islist (x) | ||
151 | -- TODO: detect circular lists | ||
152 | if x == m.null then | ||
129 | return true | 153 | return true |
130 | elseif t.isa(x, "Cons") then | 154 | elseif m.isa(x, "cons") then |
131 | return t.isList(x.cdr) | 155 | return m.islist(x[2]) |
132 | else | 156 | else |
133 | return false | 157 | return false |
134 | end | 158 | end |
135 | end | 159 | end |
136 | 160 | ||
137 | --- | 161 | -------- |
138 | return t | 162 | return m |