diff --git a/Makefile.impls b/Makefile.impls index 6ac35b23e5..4bb0e11894 100644 --- a/Makefile.impls +++ b/Makefile.impls @@ -36,7 +36,7 @@ wasm_MODE = wasmtime IMPLS = ada ada.2 awk bash basic bbc-basic c c.2 chuck clojure coffee common-lisp cpp crystal cs d dart \ elisp elixir elm erlang es6 factor fantom fennel forth fsharp go groovy gnu-smalltalk \ - guile haskell haxe hy io janet java java-truffle js jq julia kotlin livescript logo lua make mal \ + guile haskell haxe hy io janet java java-truffle js jq julia kotlin livescript logo lua lua.2 make mal \ matlab miniMAL nasm nim objc objpascal ocaml perl perl6 php picolisp pike plpgsql \ plsql powershell prolog ps purs python python.2 r racket rexx rpython ruby ruby.2 rust scala scheme skew sml \ swift swift3 swift4 swift5 tcl ts vala vb vhdl vimscript wasm wren yorick xslt zig @@ -149,6 +149,7 @@ kotlin_STEP_TO_PROG = impls/kotlin/$($(1)).jar livescript_STEP_TO_PROG = impls/livescript/$($(1)).js logo_STEP_TO_PROG = impls/logo/$($(1)).lg lua_STEP_TO_PROG = impls/lua/$($(1)).lua +lua.2_STEP_TO_PROG = impls/lua.2/$($(1)).lua make_STEP_TO_PROG = impls/make/$($(1)).mk mal_STEP_TO_PROG = impls/mal/$($(1)).mal matlab_STEP_TO_PROG = impls/matlab/$($(1)).m diff --git a/README.md b/README.md index 3fa1c13df2..cee3bd3a7a 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ FAQ](docs/FAQ.md) where I attempt to answer some common questions. | [LiveScript](#livescript) | [Jos van Bakel](https://github.com/c0deaddict) | | [Logo](#logo) | [Dov Murik](https://github.com/dubek) | | [Lua](#lua) | [Joel Martin](https://github.com/kanaka) | +| [Lua #2](#lua-2) | [Hüseyin Er](https://github.com/chemindefer) | | [GNU Make](#gnu-make-381) | [Joel Martin](https://github.com/kanaka) | | [mal itself](#mal) | [Joel Martin](https://github.com/kanaka) | | [MATLAB](#matlab-gnu-octave-and-matlab) (GNU Octave & MATLAB) | [Joel Martin](https://github.com/kanaka) | @@ -715,6 +716,22 @@ make # to build and link linenoise.so and rex_pcre.so ./stepX_YYY.lua ``` +### Lua.2 + +The second Lua implementation of mal has been tested with Lua 5.3 and +5.4 this implementation does not include readline functionality. In +order to have it one can use `rlwrap`. This implementation uses manual +tokenizer instead of using regular expressions. + +``` +cd impls/lua.2 +./stepX_YYY.lua +# with readline functionality +rlwrap ./stepX_YYY.lua +# with readline functionality +rlwrap ./stepX_YYY.lua +``` + ### Mal Running the mal implementation of mal involves running stepA of one of diff --git a/docs/graph/base_data.yaml b/docs/graph/base_data.yaml index 3ff703d35a..4648a3f47c 100644 --- a/docs/graph/base_data.yaml +++ b/docs/graph/base_data.yaml @@ -43,6 +43,7 @@ languages: - [livescript , LiveScript , ML , Dynamic , []] - [logo , Logo , OTHER , Dynamic , []] - [lua , Lua , Algol , Dynamic , []] + - [lua.2 , Lua , Algol , Dynamic , []] - [make , GNU Make , OTHER , OTHER , []] - [mal , mal itself , Lisp , Dynamic , []] - [matlab , MATLAB , Algol , Dynamic , []] diff --git a/impls/lua.2/Dockerfile b/impls/lua.2/Dockerfile new file mode 100644 index 0000000000..a94c444088 --- /dev/null +++ b/impls/lua.2/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:18.04 +MAINTAINER Hüseyin Er + +########################################################## +# General requirements for testing or common across many +# implementations +########################################################## + +RUN apt-get -y update + +# Required for running tests +RUN apt-get -y install make python + +# Some typical implementation and test requirements +RUN apt-get -y install curl +RUN mkdir -p /mal +WORKDIR /mal + +########################################################## +# Specific implementation requirements +########################################################## +# rlwrap for readline program +RUN apt-get -y install rlwrap +# Lua +RUN apt-get -y install gcc libreadline-dev unzip + +RUN \ +curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz && \ +tar -zxf lua-5.3.5.tar.gz && \ +cd lua-5.3.5 && \ +make linux test && \ +make install + diff --git a/impls/lua.2/README.md b/impls/lua.2/README.md new file mode 100644 index 0000000000..6368008198 --- /dev/null +++ b/impls/lua.2/README.md @@ -0,0 +1,14 @@ +# Second Lua implementation of [mal](https://github.com/kanaka/mal) +Major difference from first Lua implementation is tokenisation is done manually instead of using regular expressions. + +# requirements +* Lua 5.3 or greater +* rlwrap (optional) for readline editing [rlwrap](https://github.com/hanslub42/rlwrap) +```sh +# for repl with a readline +rlwrap ./stepA_mal.lua +# for repl +./stepA_mal.lua +# for evaluating script file +./stepA_mal.lua +``` diff --git a/impls/lua.2/core.lua b/impls/lua.2/core.lua new file mode 100644 index 0000000000..51877f0974 --- /dev/null +++ b/impls/lua.2/core.lua @@ -0,0 +1,586 @@ +local core = {} +local Printer = require "printer" +local types = require "types" +local List = types.MalList +local Sym = types.Sym +local Vector = types.MalVector +local is_instanceOf = types.isinstanceof +local is_sequence = types.is_sequence +local is_func = types.is_func +local Function = types.MalFunction +local Nil = types.Nil +local throw = types.throw +local Reader = require "reader" +local Atom = types.Atom +local Err = types.Err +local Hashmap = types.MalHashMap + +core['pr-str'] = function (...) + local res = "" + local args = table.pack(...) + for i,v in ipairs(args) do + res = res .. Printer.stringfy_val(v, true) .. " " + end + + return res:sub(1,#res-1) +end + +core['str'] = function (...) + local args = table.pack(...) + local res = "" + for i,v in ipairs(args) do + res = res .. Printer.stringfy_val(v, false) + end + return res +end + +core['prn'] = function (...) + local res = "" + local args = table.pack(...) + for i,v in ipairs(args) do + res = res .. Printer.stringfy_val(v, true) .. " " + end + + print(res:sub(1,#res-1)) + return Nil +end + +core['println'] = function (...) + local args = table.pack(...) + local res = "" + for i,v in ipairs(args) do + res = res .. Printer.stringfy_val(v, false) .. " " + end + print(res:sub(1,#res-1)) + return Nil +end + + + + +core['list'] = function (...) + local args = table.pack(...) + return List.new(args) +end + +core['list?'] = function (v) + if is_instanceOf(v, List) then + return true + else + return false + end +end + +core['vector'] = function (...) + local args = table.pack(...) + return Vector.new(args) +end + +core['vec'] = function (a) + local nt = table.pack(table.unpack(a)) -- this is done for copying + return Vector.new(nt) +end + + + +core['empty?'] = function (v) + if is_sequence(v) then + return #v == 0 + end + throw("'empty? expects a parameter to be sequence'") +end + +core['count'] = function (v) + if v == Nil then + return 0 + end + if is_sequence(v) then + return #v + end + throw("'count expects parameter to be sequence or nil'") +end + +core['<'] = function (a, b) return a < b end +core['>'] = function (a, b) return a > b end +core['<='] = function (a, b) return a <= b end +core['>='] = function (a, b) return a >= b end + +core['+'] = function (a, b) return a + b end +core['-'] = function (a, b) return a - b end +core['*'] = function (a, b) return a * b end +core['/'] = function (a, b) return a / b end + + +core['='] = types.is_equal + +core['read-string'] = Reader.read_str + +core['slurp'] = function (filename) + local f = io.open(filename) + if f == nil then + throw(string.format("file '%s' cannot be opened", filename)) + end + local res = f:read('a') + f:close() + return res +end + +core['atom'] = function (v) return Atom.new(v) end +core['atom?'] = function (v) return types.is_atom(v) end +core['deref'] = function (v) return v.val end + +core['reset!'] = function (v, malval) + v.val = malval + return v.val end +core['swap!'] = function (v, f, ...) + if not(is_instanceOf(f, Function) or type(f) == "function") then + throw(string.format("second argument to swap! should be function")) + end + if is_instanceOf(f, Function) then + f = f.fn + end + + v.val = f(v.val, ...) + return v.val +end + +core['cons'] = function (first, second, ...) + if ... ~= nil then throw("cons expect expects 2 args got: " .. 2 + #table.pack(...)) end + if not(is_sequence(second)) then + throw("second argument to cons should be Sequence") + end + local res = List.new({first, table.unpack(second)}) + + return res + +end + +core['concat'] = function (...) + local args = table.pack(...) + local tmp = {} + for i, v in ipairs(args) do + if not(is_sequence(v)) then + throw("argument to concat should be sequence at index:" .. i) + end + for ii, vv in ipairs(v) do + table.insert(tmp, vv) + end + end + local res = List.new(tmp) + + return res + +end + +core['nth'] = function (v, idx, ...) + if ... ~= nil then throw("nth expect expects 2 args got: " .. 2 + #table.pack(...)) end + if not(is_sequence(v)) then + throw("first argument to nth should be Sequence") + end + if not(type(idx) == "number") then + throw("second argument to nth should be number") + end + if idx > #v - 1 or idx < 0 then + throw("index out of range") + end + return v[idx+1] or Nil +end + +core['first'] = function (v, ...) + if ... ~= nil then throw("first expect expects 1 args got: " .. 1 + #table.pack(...)) end + if not(is_sequence(v) or v == Nil) then + throw("first argument to first should be Sequence or nil") + end + return v[1] or Nil +end + +core['rest'] = function (v, ...) + if ... ~= nil then throw("rest expect expects 1 args got: " .. 1 + #table.pack(...)) end + if not(is_sequence(v) or v == Nil) then + throw("first argument to rest should be Sequence or nil") + end + if false and #v <= 1 then + return Nil + end + return List.new({table.unpack(v,2)}) +end + +core['throw'] = function (v, ...) + if ... ~= nil then throw("throw expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then + throw("") + end + + + throw(v) +end + +--fixme +core['map'] = function (f, seq, ...) + if ... ~= nil then + throw("map expect expects 1 args got: " .. 1 + #table.pack(...)) + end + + if not(is_instanceOf(f, Function) or type(f) == "function" ) then + throw("map expect first argument to be function") + end + if not(is_sequence(seq)) then + throw("map expect 2nd argument to be sequence") + end + local constructor = Err.new + if is_instanceOf(f, Function) then + f = f.fn + end + local acc = {} + for k,v in ipairs(seq) do + table.insert(acc, f(v)) + end + return List.new(acc) + +end + +core['apply'] = function (...) + local args = table.pack(...) + if #args < 2 then + throw("apply expect at leasth 2 args got: " .. #args) + end + local f = args[1] + if not(is_instanceOf(f, Function) or type(f) == "function" ) then + throw("apply expect first argument to be function") + end + local last_arg = args[#args] + if not(is_sequence(last_arg)) then + throw("apply expect last argument to be sequence") + end + if is_instanceOf(f, Function) then + f = f.fn + end + for i=#args-1,2,-1 do + table.insert(last_arg, 1, args[i]) + end + return f(table.unpack(last_arg)) +end + + + +core['nil?'] = function (v, ...) + if ... ~= nil then throw("nil? expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("nil? expect expects 1 args got: 0") end + return v == Nil +end + +core['true?'] = function (v, ...) + if ... ~= nil then throw("true? expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("true? expect expects 1 args got: 0") end + return v == true +end + +core['false?'] = function (v, ...) + if ... ~= nil then throw("false? expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("false? expect expects 1 args got: 0") end + return v == false +end + +core['symbol?'] = function (v, ...) + if ... ~= nil then throw("symbol? expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("symbol? expect expects 1 args got: 0") end + return is_instanceOf(v, Sym) +end + +core['keyword?'] = function (v, ...) + if ... ~= nil then throw("keyword? expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("keyword? expect expects 1 args got: 0") end + return type(v) == "string" and "\u{029e}" == string.sub(v,1,2) + +end + +core['keyword'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("keyword expect expects 1 args got: " .. #args) + end + local val = args[1] + if core['keyword?'](val) then + return val + end + if not( type(val) == "string") then + throw("keyword expects string or keyword type") + end + return "\u{029e}" .. val + +end + +core['symbol'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("symbol expect expects 1 args got: " .. #args) + end + local val = args[1] + if core['symbol?'](val) then + return val + end + if not( type(val) == "string") then + throw("symbol expects string or symbol type") + end + return Sym.new(val) + +end + + + +core['sequential?'] = is_sequence + +core['vector?'] = function (v, ...) + if ... ~= nil then throw("vector? expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("vector? expect expects 1 args got: 0") end + return is_instanceOf(v, Vector) +end + +core['hash-map'] = Hashmap.new + +core['keys'] = function (v, ...) + if ... ~= nil then throw("keys expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("keys expect expects 1 args got: 0") end + if not(is_instanceOf(v, Hashmap)) then throw("keys expects its argument to be HashMap but got:" .. type(v) ) end + + local res = {} + for k,_ in pairs(v) do + table.insert(res, k) + end + return List.new(res) + +end + +core['vals'] = function (v, ...) + if ... ~= nil then throw("vals expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("vals expect expects 1 args got: 0") end + if not(is_instanceOf(v, Hashmap)) then throw("vals expects its argument to be HashMap but got:" .. type(v) ) end + + local res = {} + for _,val in pairs(v) do + table.insert(res, val) + end + return List.new(res) + +end + + +core['map?'] = function (v, ...) + if ... ~= nil then throw("map? expect expects 1 args got: " .. 1 + #table.pack(...)) end + if v == nil then throw("map? expect expects 1 args got: 0") end + return is_instanceOf(v, Hashmap) +end + +core['hash-map'] = Hashmap.new + +core['get'] = function (...) + local args = table.pack(...) + if #args ~= 2 then + throw("get expect expects 2 args got: " .. #args) + end + local map = args[1] + local key = args[2] + if not(is_instanceOf(map, Hashmap) or map == Nil) then + throw("get expects first arg to be hashmap or nil") + end + + return map[key] and map[key] or Nil +end + +core['contains?'] = function (...) + local args = table.pack(...) + if #args ~= 2 then + throw("contains? expect expects 2 args got: " .. #args) + end + local map = args[1] + local key = args[2] + if not(is_instanceOf(map, Hashmap)) then + throw("contains? expects first arg to be hashmap") + end + + return map[key] and true or false +end + + +core['assoc'] = function (...) + local args = table.pack(...) + if #args % 2 ~= 1 then + throw("assoc expect expects odd number of args got: " .. #args) + end + local map = table.remove(args,1) + if not(is_instanceOf(map, Hashmap)) then + throw("assoc expects first arg to be hashmap") + end + local res = Hashmap.new() + for k,v in pairs(map) do + res[k] = v + end + for i=1,#args,2 do + res[args[i]] = args[i+1] + end + + return res +end + +core['dissoc'] = function (...) + local args = table.pack(...) + if #args < 2 then + throw("assoc expect expects at least 2 args got: " .. #args) + end + local map = table.remove(args,1) + if not(is_instanceOf(map, Hashmap)) then + throw("assoc expects first arg to be HashMap") + end + local res = Hashmap.new() + for k,v in pairs(map) do + local keep = true + for _, listval in ipairs(args) do + if k == listval then + keep = false + end + end + if keep then + res[k] = v + end + end + return res +end + +core['readline'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("readline expect expects 1 args got: " .. #args) + end + local prompt = args[1] + if type(prompt) ~= "string" then + throw("readline expects first arg to be string") + end + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v or Nil +end + +core['time-ms'] = function (...) + return math.floor(os.clock()*1000000) +end + +core['meta'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("meta expect expects 1 args got: " .. #args) + end + local first = args[1] + if not(is_instanceOf(first, Hashmap) or is_sequence(first) or is_func(first)) then + throw("argument to meta should be one of {hashmap, list, vector, function}." .. type(first)) + end + + local m = getmetatable(first) + if m == nil or m.meta == nil then + return Nil + end + return m.meta +end + +core['with-meta'] = function (...) + local args = table.pack(...) + if #args ~= 2 then + throw("with-meta expect expects 2 args got: " .. #args) + end + local first = args[1] + local meta = args[2] + if not(is_instanceOf(first, Hashmap) or is_sequence(first) or is_func(first)) then + throw("argument to with-meta should be one of {hashmap, list, vector, function}. got: " .. type(first)) + end + + local new_obj = types.copy(first) + getmetatable(new_obj).meta = meta + return new_obj +end + +core['fn?'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("fn? expect expects 1 args got: " .. #args) + end + local first = args[1] + return (is_instanceOf(first,Function) and not(first.is_macro) ) or type(first) == "function" + +end + +core['number?'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("number? expect expects 1 args got: " .. #args) + end + return type(args[1]) == "number" +end + +core['string?'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("string? expect expects 1 args got: " .. #args) + end + return type(args[1]) == "string" and "\u{029e}" ~= string.sub(args[1], 1, 2) +end + +core['macro?'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("macro? expect expects 1 args got: " .. #args) + end + return is_instanceOf(args[1], Function) and args[1].is_macro +end + +core['seq'] = function (...) + local args = table.pack(...) + if #args ~= 1 then + throw("seq expects 1 args got: " .. #args) + end + local first = args[1] + if not(is_sequence(first) or type(first) == "string" or first == Nil) then + throw("seq expects its arguments to be type of {list, vector, string or nil}.") + end + if first == Nil or first == "" or (is_sequence(first) and #first == 0 ) then + return Nil + elseif (is_sequence(first)) then + return List.new({table.unpack(first)}) + elseif type(first) == "string" then + local res = {} + for i= 1,#first do + table.insert(res,string.sub(first, i, i)) + end + return List.new(res) + else + assert(nil, "unreachable in built-in seq") + end + + +end + +core['conj'] = function (...) + local args = table.pack(...) + if #args < 1 then + throw("conj expects at least 1 args got: " .. #args) + end + local first = args[1] + if not(is_sequence(first)) then + throw("conj expects its first argument to be type of {list, vector}.") + end + local cls = is_instanceOf(first, List) and List or Vector + local res = types.copy(first) + + + for i = 2,#args do + if is_instanceOf(first, List) then + table.insert(res, 1, args[i]) + else + table.insert(res, args[i]) + end + end + return cls.new(res) +end + +return core diff --git a/impls/lua.2/env.lua b/impls/lua.2/env.lua new file mode 100644 index 0000000000..fecf64dc15 --- /dev/null +++ b/impls/lua.2/env.lua @@ -0,0 +1,69 @@ +local types = require "types" +local throw = types.throw +local Sym = types.Sym +local List = types.MalList +local is_instanceOf = types.isinstanceof +local is_sequence = types.is_sequence + +local Env = {} +Env.__index = Env + +function Env.new(outer) + local data = {} + local self = {} + self.outer = outer + self.data = data + setmetatable(self, Env) + return self +end + +function Env:bind(binds, exprs) + if not(is_sequence(binds)) then throw("binds should be sequence") end + if not(is_sequence(exprs)) then throw("exprs should be sequence") end + + + + for i, b in ipairs(binds) do + if not(is_instanceOf(b, Sym)) then + throw(string.format("%d/%d in the binds should be Symbol ", i, #binds)) + end + if b.val ~= '&' then + self.data[b.val] = exprs[i] + else + if i == #binds or not(is_instanceOf(binds[i+1],Sym)) then + throw("Symbol '&' should be followed by an another symbol") + end + self.data[binds[i+1].val] = List.new(table.pack(table.unpack(exprs,i))) + break + end + end + +end + +function Env:set(key, val) + assert(is_instanceOf(key, Sym), "key should be symbol") + self.data[key.val] = val + return self +end + +function Env:find(key) + assert(is_instanceOf(key, Sym), "key should be symbol") + if self.data[key.val] ~= nil then + return self + end + if self.outer ~= nil then + return self.outer:find(key) + end + return nil +end + +function Env:get(key) + assert(is_instanceOf(key, Sym), "key should be symbol") + local env = self:find(key) + if env then + return env.data[key.val] + end + throw(string.format("'%s' not found", key.val)) +end + +return Env diff --git a/impls/lua.2/printer.lua b/impls/lua.2/printer.lua new file mode 100644 index 0000000000..d2f987cbab --- /dev/null +++ b/impls/lua.2/printer.lua @@ -0,0 +1,83 @@ +local Printer = {} + +local Scanner = require "scanner" +local types = require "types" + +local List = types.MalList +local Vector = types.MalVector +local Nil = types.Nil +local HashMap = types.MalHashMap +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local Function = types.MalFunction +local FunctionRef = types.FunctionRef +local Atom = types.Atom + + + +function Printer.stringfy_val(val, readably) + local res = '' + if is_instanceOf(val, Vector) then + res = res .. '[' + for i=1, #val do + res = res .. Printer.stringfy_val(val[i],readably) + if i ~= #val then + res = res .. " " + end + end + res = res .. ']' + elseif is_instanceOf(val, List) then + res = res .. '(' + for i=1, #val do + res = res .. Printer.stringfy_val(val[i],readably) + if i ~= #val then + res = res .. " " + end + end + res = res .. ')' + elseif is_instanceOf(val, HashMap) then + res = res .. '{' + for i,v in pairs(val) do + res = res .. Printer.stringfy_val(i, readably) .. " " .. Printer.stringfy_val(v,readably) + res = res .. " " + end + if #res > 1 then + res = string.sub(res, 1, #res-1) -- trim last space + end + res = res .. '}' + + elseif is_instanceOf(val, Sym) then + return val.val + elseif is_instanceOf(val, Err) then + return Printer.stringfy_val(val.val,readably) + elseif is_instanceOf(val, Function) then + res = "(fn* " .. Printer.stringfy_val(val.params) .. + "-->" .. Printer.stringfy_val(val.ast) ..")" .. "ismacro: " .. tostring(val.is_macro) + elseif is_instanceOf(val, Atom) then + res = "(atom " .. Printer.stringfy_val(val.val) .. ")" + + elseif type(val) == "string" then + if "\u{29E}" == string.sub(val, 1, 2) then + return ":" .. string.sub(val, 3) end + if readably then + res = Scanner.unescape(val) + else + res = val + end + elseif type(val) == "number" then + res = tostring(val) + elseif val == Nil then + res = "nil" + elseif type(val) == "boolean" then + res = tostring(val) + elseif type(val) == "function" or is_instanceOf(val, FunctionRef) then + res = "#" + else + error(string.format("Error: unknown type %s", val)) + end + return res +end + + +return Printer diff --git a/impls/lua.2/reader.lua b/impls/lua.2/reader.lua new file mode 100644 index 0000000000..c176ca9ae4 --- /dev/null +++ b/impls/lua.2/reader.lua @@ -0,0 +1,150 @@ +local Reader = {} +Reader.__index = Reader + +local Scanner = require "scanner" +local types = require "types" + +local List = types.MalList +local Vector = types.MalVector +local Nil = types.Nil +local HashMap = types.MalHashMap +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local throw = types.throw +local Function = types.MalFunction + +setmetatable(Reader, { + __call = function (cls, ...) + return cls.new(...) + end, +}) + +function Reader.new(tokens) + local self = setmetatable({}, Reader) + self.tokens = tokens + self.index = 1 + return self +end + +function Reader.peek(self) + return self.tokens[self.index] +end + +function Reader.advance(self) + local tok = self.tokens[self.index] + self.index = self.index + 1 + return tok +end + + + + +function Reader.read_form(self) + local tok = self:peek() + if tok.typeof == '(' then + return List.new( self:read_seq('(', ')', { '}', ']' })) + elseif tok.typeof == '[' then + return Vector.new( self:read_seq('[', ']', { ')' , '}'})) + elseif tok.typeof == '{' then + return HashMap.new(table.unpack(self:read_seq('{', '}', { ')' , ']'}))) + elseif tok.typeof == 'CMT' then + self:advance() + return Nil + elseif tok.typeof == '@' then + self:advance() + return List.new({Sym.new('deref'), self:read_form()}) + elseif tok.typeof == '~@' then + self:advance() + return List.new({Sym.new('splice-unquote'), self:read_form()}) + elseif tok.typeof == '`' then + self:advance() + return List.new({Sym.new('quasiquote'), self:read_form()}) + elseif tok.typeof == '~' then + self:advance() + return List.new({Sym.new('unquote'), self:read_form()}) + elseif tok.typeof == "'" then + self:advance() + return List.new({Sym.new('quote'), self:read_form()}) + elseif tok.typeof == '^' then + self:advance() + local meta = self:read_form() + return List.new({Sym.new('with-meta'), self:read_form(), meta}) + elseif tok.typeof == ')' or tok.typeof == ']' or tok.typeof == '}' then + throw("Syntax error unexpected '" .. tok.typeof .. "'") + else + return self:read_atom() + end +end + +function Reader.read_atom(self) + local token = self:advance() + if token.typeof == "STR" then + return token.val + elseif token.typeof == "SYM" then + if token.val == "true" then + return true + elseif token.val == "false" then + return false + elseif token.val == 'nil' then + return Nil + elseif string.match(token.val, '^-?%d+%.?%d*$') then + return tonumber(token.val) + else + return Sym.new(token.val) + end + elseif token.typeof == "EOF" then + return Nil + + else + throw(string.format("Error: is not atomic %s", token.typeof)) + end +end + +function Reader.read_seq(self, opening, closing, invalids) + local tok = self:advance() -- consume opening + if tok.typeof ~= opening then + throw("Error: expected '" .. opening .. "' got '" .. tok.typeof .. "'.") + end + tok = self:peek() + local res = {} + while tok.typeof ~= closing do + if tok.typeof == "EOF" then + throw("Error: unexpected EOF before matching '" .. closing .. "'at line:" .. tok.line) + + end + + + for i= 1, #invalids do + if tok.typeof == invalids[i] then + throw("invalid syntax un expected '" .. tok.typeof .. "'") + end + + end + table.insert(res, self:read_form()) + tok = self:peek() + end + self:advance() -- consume closing + return res +end + + + +function Reader.print_tokens(tokens) + for i,v in ipairs(tokens) do print(i,v:tostring()) end +end + +function Reader.read_str(a) + local s = Scanner(a) + local toks = s:scanTokens() + if not toks then + return Err.new("No token") + end + -- Reader.print_tokens(toks) + local r = Reader(toks) + return r:read_form() +end + + + +return Reader diff --git a/impls/lua.2/readline.lua b/impls/lua.2/readline.lua new file mode 100644 index 0000000000..6a861b4d82 --- /dev/null +++ b/impls/lua.2/readline.lua @@ -0,0 +1,23 @@ +local history = {} +assert(nil, "Not implemented module") +function readline(prompt) + io.write(prompt) + io.stdin:setvbuf("no") + char = io.read("n", 1) + line = char + while char do + print('char is ' .. char) + line = line .. char + if char == 'x' then + line = line[#line-1] + end + if char == 'k' then + line = history[#history] + end + char = io.read(1) + end + if line then + table.insert(history, line) + end + return line +end diff --git a/impls/lua.2/run b/impls/lua.2/run new file mode 100755 index 0000000000..a53fbc60ba --- /dev/null +++ b/impls/lua.2/run @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec lua $(dirname $0)/${STEP:-stepA_mal}.lua "${@}" diff --git a/impls/lua.2/scanner.lua b/impls/lua.2/scanner.lua new file mode 100644 index 0000000000..75e9ebd797 --- /dev/null +++ b/impls/lua.2/scanner.lua @@ -0,0 +1,199 @@ +local Token = require('token') +local types = require("types") +local throw = types.throw +local Scanner = {} +Scanner.__index = Scanner + +setmetatable(Scanner, { + __call = function (cls, ...) + return cls.new(...) +end, +}) + +function Scanner.new(source) + local self = setmetatable({}, Scanner) + self.source = source + self.line = 1 + self.index = 1 + self.start = 1 + self.tokens = {} + return self +end + +function Scanner.advance(self) + local ch = string.sub(self.source, self.index, self.index) + self.index = self.index + 1 + return ch +end + +function Scanner.peek(self) + if self:isAtEnd() then + return '\0' + end + return string.sub(self.source, self.index, self.index) +end + +function Scanner.isAtEnd(self) + return self.index > #self.source +end + +function Scanner.is_special(char) + return char == '(' or char == ')' or char == '[' or char == ']' or + char == '{' or char == '}' or char == '\'' or char == '`' or + char == '"' or char == '@' or char == '~' or char == '^' or + char == '\0'or char == ' ' or char == '\t' or char == '\n' or char == ',' or char == ';' +end + +function Scanner.escape(str) + local target = string.byte('\\') + local dq = string.byte('"') + local nl = string.byte('n') + local res = '' + local idx = 1 + while idx <= #str do + if str:byte(idx) == target and str:byte(idx+1) == dq then + res = res .. '"' + idx = idx + 1 + elseif str:byte(idx) == target and str:byte(idx+1) == nl then + res = res .. '\n' + idx = idx + 1 + elseif str:byte(idx) == target and str:byte(idx+1) == target then + res = res .. '\\' + idx = idx + 1 + else + res = res .. str:sub(idx,idx) + end + + idx = idx + 1 + + end + return res +end +function Scanner.unescape(str) + local nl = string.byte('\n') + local bs = string.byte('\\') + local dq = string.byte('"') + local res = '"' + local idx = 1 + while idx <= #str do + if str:byte(idx) == nl then + res = res .. '\\n' + elseif str:byte(idx) == bs then + res = res .. '\\\\' + elseif str:byte(idx) == dq then + res = res .. '\\"' + else + res = res .. str:sub(idx,idx) + end + idx = idx + 1 + end + res = res .. '"' + return res +end + + +function Scanner.scanTokens(self) + while not self:isAtEnd() do + self.start = self.index + self:scanToken() + end + table.insert(self.tokens,Token("EOF", "", self.line)) + return self.tokens +end + +function Scanner.match(self, char) + if self:peek() ~= char then + return false + end + self:advance() + return true +end + +function Scanner.string(self) + while self:peek() ~= '"' and not(self:isAtEnd()) do + if self:peek() == '\\' then + self:advance() + end + self:advance() + end + if self:isAtEnd() then + throw(string.format("Error unbalanced string at line %d", self.line)) + end + + -- trimmed opening and closing " + local val = Scanner.escape(string.sub(self.source, self.start+1, self.index-1)) + self:advance() -- closing " + + table.insert(self.tokens, Token("STR", val, self.line)) + +end + +function Scanner.scanToken(self) + local char = self:advance() + + -- print(string.format("b c:%s, i:%d", char, self.index)) + if char == ' ' or char == ',' or char == '\n' or char =='\t' then + if char == '\n' then + self.line = self.line + 1 + end + elseif char == '~' then + if self:match('@') then + table.insert(self.tokens, Token("~@", "", self.line)) + else + table.insert(self.tokens, Token("~", "", self.line)) + end + elseif char == '[' then + table.insert(self.tokens, Token("[", "", self.line)) + elseif char == ']' then + table.insert(self.tokens, Token("]", "", self.line)) + elseif char == '(' then + table.insert(self.tokens, Token("(", "", self.line)) + elseif char == ')' then + table.insert(self.tokens, Token(")", "", self.line)) + elseif char == '{' then + table.insert(self.tokens, Token("{", "", self.line)) + elseif char == '}' then + table.insert(self.tokens, Token("}", "", self.line)) + elseif char == '\'' then + table.insert(self.tokens, Token("'", "", self.line)) + elseif char == '`' then + table.insert(self.tokens, Token("`", "", self.line)) + elseif char == '^' then + table.insert(self.tokens, Token("^", "", self.line)) + elseif char == '@' then + table.insert(self.tokens, Token("@", "", self.line)) + elseif char == '"' then + self:string() + + elseif char == ';' then + while self:peek() ~= '\n' and not(self:isAtEnd()) do + self:advance() + end + --local val = string.sub(self.source, self.start + 1, self.current) + --table.insert(self.tokens, Token("CMT", val, self.line)) + elseif char == ':' then + while not (self.is_special(self:peek())) and not(self:isAtEnd()) do + self:advance() + end + local val = "\u{29E}" .. string.sub(self.source, self.start+1, self.index-1) + table.insert(self.tokens, Token("STR", val, self.line)) + elseif not self.is_special(char) then + while not(self.is_special(self:peek())) do + self:advance() + end + local val = string.sub(self.source,self.start, self.index - 1) + table.insert(self.tokens, Token("SYM", val, self.line)) + else + throw(string.format("Error: unknown char: %s at %d, %d", char , self.line , self.index) ) + end + +end +--[[ examples +print(Scanner.escape("\\\\\\\\")) + +print(Scanner.escape('\\"he"l\\nlo"')) + +print(Scanner.unescape("\n")) +print(Scanner.unescape('"hello"')) +]] +return Scanner diff --git a/impls/lua.2/step0_repl.lua b/impls/lua.2/step0_repl.lua new file mode 100755 index 0000000000..de2b8615a2 --- /dev/null +++ b/impls/lua.2/step0_repl.lua @@ -0,0 +1,24 @@ +#!/usr/bin/env lua +function READ(prompt) + io.write(prompt) + return io.read() +end +function EVAL(a) + return a +end +function PRINT(a) + print(a) +end + + +function main() + while true do + line = READ('user> ') + if not line then + break + end + PRINT(EVAL(line)) + end +end + +main() diff --git a/impls/lua.2/step1_read_print.lua b/impls/lua.2/step1_read_print.lua new file mode 100755 index 0000000000..23b09e8ffa --- /dev/null +++ b/impls/lua.2/step1_read_print.lua @@ -0,0 +1,56 @@ +#!/usr/bin/env lua +Reader = require "reader" +Printer = require "printer" + +types = require "types" +--local throw = types.throw +local Err = types.Err +local is_instanceOf = types.isinstanceof + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + + +function READ(v) + return Reader.read_str(v) +end + +function EVAL(a) + return a +end + +function PRINT(a) + print(Printer.stringfy_val(a, true)) +end + +function rep(str) + return PRINT(EVAL(READ(str))) +end + + + +function main() + local line = '' + while true do + local line = raw_read('user> ') + if line == nil then + break + end + status, err = pcall( function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step2_eval.lua b/impls/lua.2/step2_eval.lua new file mode 100755 index 0000000000..135456560e --- /dev/null +++ b/impls/lua.2/step2_eval.lua @@ -0,0 +1,114 @@ +#!/usr/bin/env lua +Reader = require "reader" +Printer = require "printer" +types = require "types" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function EVAL(a, env) + if is_instanceOf(a, List) then + if #a == 0 then + return a + elseif #a == 3 then + new_list = eval_ast(a, env) + return new_list[1](new_list[2],new_list[3]) + else + throw("'" .. Printer.stringfy_val(a[1]) .. "' should be called with 2 elements") + end + else + return eval_ast(a, env) + end +end +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return Sym.new(":" .. string.sub(ast.val, 3, #ast.val)) + end + if env[ast.val] == nil then + types.throw(string.format("Value : '%s' does not exist", ast.val)) + else + return env[ast.val] + end + elseif type(ast) == "number" or type(ast) == "string" or type(ast) == "function" then + return ast + else + throw("dont know how to eval") + end +end + + +function PRINT(a) + print(Printer.stringfy_val(a, true)) +end + + +local repl_env = { +['+'] = function (a,b) return a+b end, +['-'] = function (a,b) return a-b end, +['*'] = function (a,b) return a*b end, +['/'] = function (a,b) return a/b end, +['a'] = types.MalList.new({1,2,3}), + +['n'] = 3, +} + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + +function main() + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + status, err = pcall(function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step3_env.lua b/impls/lua.2/step3_env.lua new file mode 100755 index 0000000000..5dbdb77f51 --- /dev/null +++ b/impls/lua.2/step3_env.lua @@ -0,0 +1,153 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function EVAL(a, env) + if is_instanceOf(a, List) then + if #a == 0 then + return a + end + local first_elem = a[1] + + if is_instanceOf(first_elem, Sym) and first_elem.val == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + end + + if is_instanceOf(first_elem, Sym) and first_elem.val == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length ofSecond arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + return EVAL(a[3], let_env) + + end + + local new_list = eval_ast(a, env) + if type(new_list[1]) ~= "function" then + throw("First elem should be function or special form got :'" .. type(new_list[1]) .. "'.") + end + + if #a ~= 3 then + throw("currently all builtins expects 2 arguments") + end + return new_list[1](new_list[2],new_list[3]) --fixme: varargs? + + else + return eval_ast(a, env) + end +end +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return Sym.new(":" .. string.sub(ast.val, 3, #ast.val)) + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + print(Printer.stringfy_val(a, true)) +end + + +local repl_env = Env.new(nil) + +repl_env = repl_env:set(Sym.new('+'), function (a,b) return a+b end) +repl_env:set(Sym.new('-'), function (a,b) return a-b end) +repl_env:set(Sym.new('*'), function (a,b) return a*b end) +repl_env:set(Sym.new('/'), function (a,b) return a/b end) + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + +function main() + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + status, err = pcall(function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step4_if_fn_do.lua b/impls/lua.2/step4_if_fn_do.lua new file mode 100755 index 0000000000..36692b66ac --- /dev/null +++ b/impls/lua.2/step4_if_fn_do.lua @@ -0,0 +1,189 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +local Nil = types.Nil +local core = require "core" + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function EVAL(a, env) + if is_instanceOf(a, List) then + if #a == 0 then + return a + end + local first_elem = a[1] + + if is_instanceOf(first_elem, Sym) and first_elem.val == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + end + + if is_instanceOf(first_elem, Sym) and first_elem.val == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length ofSecond arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + return EVAL(a[3], let_env) + + end + + if is_instanceOf(first_elem, Sym) and first_elem.val == "do" then + local res + for i=2,#a do + res = EVAL(a[i], env) + end + return res + end + + if is_instanceOf(first_elem, Sym) and first_elem.val == "if" then + if not (#a == 3 or #a == 4) then + throw("if expected 2 or 3 arguments but got '" .. #a-1 .. "'.") + end + local cond = EVAL(a[2], env) + if cond ~= false and cond ~= Nil then + return EVAL(a[3], env) + else + if #a == 4 then return EVAL(a[4], env) end + return Nil + end + end + if is_instanceOf(first_elem, Sym) and first_elem.val == "fn*" then + if not (#a == 3 ) then throw("fn* expected 3 arguments but got '" .. #a-1 .. "'.") end + if false then throw("second parameter to fn* should have length 2 but got '" .. #a[2] .. "'.") end + return function (...) + local closed_over_env = Env.new(env) + local exprs = List.new(table.pack(...)) + local binds = a[2] + closed_over_env:bind(binds, exprs) + + return EVAL(a[3], closed_over_env) + end + end + + local new_list = eval_ast(a, env) + if type(new_list[1]) ~= "function" then + throw("First elem should be function or special form got :'" .. type(gonna_eval) .. "'.") + end + + if false then + throw("currently all builtins expects 2 arguments") + end + return new_list[1](table.unpack(new_list,2)) --fixme: varargs? + + else + return eval_ast(a, env) + end +end +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return Sym.new(":" .. string.sub(ast.val, 3, #ast.val)) + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + print(Printer.stringfy_val(a, true)) +end + + +local repl_env = Env.new(nil) + +for k,v in pairs(core) do + repl_env:set(Sym.new(k),v) +end + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + +function main() + rep("(def! not (fn* (a) (if a false true)))") + + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + local status, err = pcall(function () rep(line) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step5_tco.lua b/impls/lua.2/step5_tco.lua new file mode 100755 index 0000000000..e05dd3cbdc --- /dev/null +++ b/impls/lua.2/step5_tco.lua @@ -0,0 +1,199 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +local Nil = types.Nil +local core = require "core" + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function EVAL(a, env) + while true do + if not(is_instanceOf(a, List)) then + return eval_ast(a, env) + end + + if #a == 0 then + return a + end + local first_elem = a[1] + local first_sym = is_instanceOf(first_elem, Sym) and first_elem.val or "" + + if first_sym == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + + elseif first_sym == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length ofSecond arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + a = a[3] + env = let_env + + + elseif first_sym == "do" then + for i=2,#a-1 do + eval_ast(a[i], env) + end + a = a[#a] --tco + + + elseif first_sym == "if" then + if not (#a == 3 or #a == 4) then + throw("if expected 2 or 3 arguments but got '" .. #a-1 .. "'.") + end + local cond = EVAL(a[2], env) + if cond ~= false and cond ~= Nil then + a = a[3] + else + if #a == 4 then + a = a[4] + else + return Nil + end + end + elseif first_sym == "fn*" then + if not (#a == 3 ) then throw("fn* expected 3 arguments but got '" .. #a-1 .. "'.") end + if false then throw("second parameter to fn* should have length 2 but got '" .. #a[2] .. "'.") end + return types.MalFunction.new(function (...) + local closed_over_env = Env.new(env) + local exprs = List.new(table.pack(...)) + local binds = a[2] + closed_over_env:bind(binds, exprs) + + return EVAL(a[3], closed_over_env) + end, a[3], env, a[2]) + + else + + local args = eval_ast(a, env) + local f = table.remove(args,1) + if types.is_malfunc(f) then + a = f.ast + env = Env.new(f.env) + env:bind(f.params, args) + + else + if type(f) ~= "function" then + throw("First elem should be function or special form got :'" .. type(f) .. "'.") + end + + return f(table.unpack(args)) --fixme: varargs? + end + end + +end +end +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return Sym.new(":" .. string.sub(ast.val, 3, #ast.val)) + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + print(Printer.stringfy_val(a, true)) +end + + +local repl_env = Env.new(nil) + +for k,v in pairs(core) do + repl_env:set(Sym.new(k),v) +end + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + +function main() + rep("(def! not (fn* (a) (if a false true)))") + + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + local status, err = pcall(function () rep(line) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step6_file.lua b/impls/lua.2/step6_file.lua new file mode 100755 index 0000000000..2ff6d89cff --- /dev/null +++ b/impls/lua.2/step6_file.lua @@ -0,0 +1,211 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +local Nil = types.Nil +local core = require "core" + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function EVAL(a, env) + while true do + if not(is_instanceOf(a, List)) then + return eval_ast(a, env) + end + + if #a == 0 then + return a + end + local first_elem = a[1] + local first_sym = is_instanceOf(first_elem, Sym) and first_elem.val or "" + + if first_sym == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + + elseif first_sym == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length ofSecond arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + a = a[3] + env = let_env + + + elseif first_sym == "do" then + for i=2,#a-1 do + EVAL(a[i], env) + end + a = a[#a] --tco + + + elseif first_sym == "if" then + if not (#a == 3 or #a == 4) then + throw("if expected 2 or 3 arguments but got '" .. #a-1 .. "'.") + end + local cond = EVAL(a[2], env) + if cond ~= false and cond ~= Nil then + a = a[3] + else + if #a == 4 then + a = a[4] + else + return Nil + end + end + elseif first_sym == "fn*" then + if (#a) ~= 3 then throw("fn* expected 2 arguments but got '" .. #a-1 .. "'.") end + if false then throw("second parameter to fn* should have length 2 but got '" .. #a[2] .. "'.") end + return types.MalFunction.new(function (...) + local closed_over_env = Env.new(env) + local exprs = List.new(table.pack(...)) + local binds = a[2] + closed_over_env:bind(binds, exprs) + + return EVAL(a[3], closed_over_env) + end, a[3], env, a[2]) + + else + + local args = eval_ast(a, env) + local f = table.remove(args,1) + if types.is_malfunc(f) then + a = f.ast + env = Env.new(f.env) + env:bind(f.params, args) + + else + if type(f) ~= "function" then + throw("First elem should be function or special form got :'" .. type(f) .. "'.") + end + + return f(table.unpack(args)) --fixme: varargs? + end + end + +end +end +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return Sym.new(":" .. string.sub(ast.val, 3, #ast.val)) + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + return Printer.stringfy_val(a, true) +end + + +local repl_env = Env.new(nil) + +for k,v in pairs(core) do + repl_env:set(Sym.new(k),v) +end + +repl_env:set(Sym.new('eval'), function (ast) + return EVAL(ast, repl_env) +end) + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + + +function main() + rep("(def! not (fn* (a) (if a false true)))") + rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \"(slurp f) \"\nnil)\")))))") + repl_env:set(Sym.new("*ARGV*"), List.new(table.pack(table.unpack(arg,2)))) + if #arg > 0 then + local file_to_run = table.remove(arg,1) + rep("(load-file \"" .. file_to_run .. "\")") + os.exit(0) + end + + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + local status, err = pcall(function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step7_quote.lua b/impls/lua.2/step7_quote.lua new file mode 100755 index 0000000000..3ad0d2d2e4 --- /dev/null +++ b/impls/lua.2/step7_quote.lua @@ -0,0 +1,268 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +local Nil = types.Nil +local core = require "core" + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function starts_with(a, v) +return #a > 0 and is_instanceOf(a[1],Sym) and + a[1].val == v +end + + +function quasiloop(a) + local res = List.new({}) + for i=#a,1,-1 do + local elt = a[i] + if is_instanceOf(elt, List) and + starts_with(elt, "splice-unquote") then + + if #elt ~= 2 then throw("splice-unquote expected 1 argument bot got : " .. #elt) end + + res = List.new({Sym.new( "concat"), elt[2], res}) + else + res = List.new({Sym.new( "cons"), quasiquote(elt), res}) + end + end + return res +end + +function quasiquote(a) + + if is_instanceOf(a,List) then + if starts_with(a, "unquote") then + if #a-1 ~= 1 then + throw("unquote expected 1 argument bot got : " .. #a) + end + return a[2] + else + return quasiloop(a) + end + elseif is_instanceOf(a, Vector) then + local tmp = quasiloop(a) + return List.new({Sym.new("vec"), tmp}) + elseif is_instanceOf(a,HashMap) or is_instanceOf(a,Sym) then + return List.new({Sym.new('quote'), a}) + + else + return a + + end +end + +function EVAL(a, env) + while true do + if not(is_instanceOf(a, List)) then + return eval_ast(a, env) + end + + if #a == 0 then + return a + end + local first_elem = a[1] + local first_sym = is_instanceOf(first_elem, Sym) and first_elem.val or "" + + if first_sym == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + + elseif first_sym == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length ofSecond arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + a = a[3] + env = let_env + + + elseif first_sym == "do" then + for i=2,#a-1 do + EVAL(a[i], env) + end + a = a[#a] --tco + + + elseif first_sym == "if" then + if not (#a == 3 or #a == 4) then + throw("if expected 2 or 3 arguments but got '" .. #a-1 .. "'.") + end + local cond = EVAL(a[2], env) + if cond ~= false and cond ~= Nil then + a = a[3] + else + if #a == 4 then + a = a[4] + else + return Nil + end + end + elseif first_sym == "fn*" then + if (#a) ~= 3 then throw("fn* expected 2 arguments but got '" .. #a-1 .. "'.") end + if false then throw("second parameter to fn* should have length 2 but got '" .. #a[2] .. "'.") end + return types.MalFunction.new(function (...) + local closed_over_env = Env.new(env) + local exprs = List.new(table.pack(...)) + local binds = a[2] + closed_over_env:bind(binds, exprs) + + return EVAL(a[3], closed_over_env) + end, a[3], env, a[2]) + + elseif first_sym == "quote" then + if #a-1 ~= 1 then throw("quote expects 1 argument got '" .. #a-1 .. "'.") end + return a[2] + elseif first_sym == "quasiquote" then + if #a-1 ~= 1 then throw("quasiquote expects 1 argument got '" .. #a-1 .. "'.") end + a = quasiquote(a[2]) + elseif first_sym == "quasiquoteexpand" then + if #a-1 ~= 1 then throw("quasiquoteexpand expects 1 argument got '" .. #a-1 .. "'.") end + return quasiquote(a[2]) + + else + + local args = eval_ast(a, env) + local f = table.remove(args,1) + if types.is_malfunc(f) then + a = f.ast + env = Env.new(f.env) + env:bind(f.params, args) + + else + if type(f) ~= "function" then + throw("First elem should be function or special form got :'" .. type(f) .. "'.") + end + return f(table.unpack(args)) --fixme: varargs? + end + end + +end +end + + +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return Sym.new(":" .. string.sub(ast.val, 3, #ast.val)) + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + return Printer.stringfy_val(a, true) +end + + +local repl_env = Env.new(nil) + +for k,v in pairs(core) do + repl_env:set(Sym.new(k),v) +end + +repl_env:set(Sym.new('eval'), function (ast) + return EVAL(ast, repl_env) +end) + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + + +function main() + rep("(def! not (fn* (a) (if a false true)))") + rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \"(slurp f) \"\nnil)\")))))") + repl_env:set(Sym.new("*ARGV*"), List.new(table.pack(table.unpack(arg,2)))) + if #arg > 0 then + local file_to_run = table.remove(arg,1) + rep("(load-file \"" .. file_to_run .. "\")") + os.exit(0) + end + + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + local status, err = pcall(function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step8_macros.lua b/impls/lua.2/step8_macros.lua new file mode 100755 index 0000000000..e16f5b6873 --- /dev/null +++ b/impls/lua.2/step8_macros.lua @@ -0,0 +1,311 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +local Nil = types.Nil +local core = require "core" +local Function = types.MalFunction + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function starts_with(a, v) +return #a > 0 and is_instanceOf(a[1],Sym) and + a[1].val == v +end + + +function quasiloop(a) + local res = List.new({}) + for i=#a,1,-1 do + local elt = a[i] + if is_instanceOf(elt, List) and + starts_with(elt, "splice-unquote") then + + if #elt ~= 2 then throw("splice-unquote expected 1 argument bot got : " .. #elt) end + + res = List.new({Sym.new( "concat"), elt[2], res}) + else + res = List.new({Sym.new( "cons"), quasiquote(elt), res}) + end + end + return res +end + +function quasiquote(a) + + if is_instanceOf(a,List) then + if starts_with(a, "unquote") then + if #a-1 ~= 1 then + throw("unquote expected 1 argument bot got : " .. #a) + end + return a[2] + else + return quasiloop(a) + end + elseif is_instanceOf(a, Vector) then + local tmp = quasiloop(a) + return List.new({Sym.new("vec"), tmp}) + elseif is_instanceOf(a,HashMap) or is_instanceOf(a,Sym) then + return List.new({Sym.new('quote'), a}) + + else + return a + + end +end + +function is_macro_call(ast, env) + if is_instanceOf(ast, List) and #ast >= 1 and is_instanceOf(ast[1], Sym) then + local status, first_env = pcall( function () return env:get(ast[1]) end) + if not status then return false end + if is_instanceOf(first_env, Function) and first_env.is_macro then + return true + else + return false + end + else + return false + end + +end + +function macro_expand(ast, env) + while is_macro_call(ast, env) do + local macro = env:get(ast[1]) + local f = macro.fn + ast = f(table.unpack(ast, 2)) + end + + return ast +end + +function EVAL(a, env) + while true do + a = macro_expand(a, env) + if not(is_instanceOf(a, List)) then + return eval_ast(a, env) + end + + if #a == 0 then + return a + end + local first_elem = a[1] + local first_sym = is_instanceOf(first_elem, Sym) and first_elem.val or "" + + if first_sym == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + + elseif first_sym == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length ofSecond arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + a = a[3] + env = let_env + + + elseif first_sym == "do" then + for i=2,#a-1 do + EVAL(a[i], env) + end + a = a[#a] --tco + + + elseif first_sym == "if" then + if not (#a == 3 or #a == 4) then + throw("if expected 2 or 3 arguments but got '" .. #a-1 .. "'.") + end + local cond = EVAL(a[2], env) + if cond ~= false and cond ~= Nil then + a = a[3] + else + if #a == 4 then + a = a[4] + else + return Nil + end + end + elseif first_sym == "fn*" then + if (#a) ~= 3 then throw("fn* expected 2 arguments but got '" .. #a-1 .. "'.") end + if false then throw("second parameter to fn* should have length 2 but got '" .. #a[2] .. "'.") end + return Function.new(function (...) + local closed_over_env = Env.new(env) + local exprs = List.new(table.pack(...)) + local binds = a[2] + closed_over_env:bind(binds, exprs) + + return EVAL(a[3], closed_over_env) + end, a[3], env, a[2], false) + + elseif first_sym == "quote" then + if #a-1 ~= 1 then throw("quote expects 1 argument got '" .. #a-1 .. "'.") end + return a[2] + elseif first_sym == "quasiquote" then + if #a-1 ~= 1 then throw("quote expects 1 argument got '" .. #a-1 .. "'.") end + a = quasiquote(a[2]) + elseif first_sym == "defmacro!" then + if #a ~= 3 then + throw(string.format("defmacro! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to defmacro! must be symbol") + end + local value = EVAL(a[3], env) + if not(is_instanceOf(value, Function)) then + throw("second argument to defmacro must be function") + end + value.is_macro = true + env:set(a[2], value) + return value + + + elseif first_sym == "macroexpand" then + if (#a) ~= 2 then throw("macroexpand expected 1 arguments but got '" .. #a-1 .. "'.") end + return macro_expand(a[2],env) + else + + local args = eval_ast(a, env) + local f = table.remove(args,1) + if types.is_malfunc(f) then + a = f.ast + env = Env.new(f.env) + env:bind(f.params, args) + + else + if type(f) ~= "function" then + throw("First elem should be function or special form got :'" .. type(f) .. "'.") + end + return f(table.unpack(args)) --fixme: varargs? + end + end + +end +end + + +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return Sym.new(":" .. string.sub(ast.val, 3, #ast.val)) + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + return Printer.stringfy_val(a, true) +end + + +local repl_env = Env.new(nil) + +for k,v in pairs(core) do + repl_env:set(Sym.new(k),v) +end + +repl_env:set(Sym.new('eval'), function (ast) + return EVAL(ast, repl_env) +end) + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + + +function main() + rep("(def! not (fn* (a) (if a false true)))") + rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \"(slurp f) \"\nnil)\")))))") +rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))") + repl_env:set(Sym.new("*ARGV*"), List.new(table.pack(table.unpack(arg,2)))) + if #arg > 0 then + local file_to_run = table.remove(arg,1) + rep("(load-file \"" .. file_to_run .. "\")") + os.exit(0) + end + + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + local status, err = pcall(function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err) + end + print(err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/step9_try.lua b/impls/lua.2/step9_try.lua new file mode 100755 index 0000000000..070ce3c6a9 --- /dev/null +++ b/impls/lua.2/step9_try.lua @@ -0,0 +1,344 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +local Nil = types.Nil +local core = require "core" +local Function = types.MalFunction + +function raw_read(prompt) + io.write(prompt) + local v = io.read() + if v == nil then + io.write('\n') + end + return v +end + +function READ(str) + return Reader.read_str(str) +end + +function starts_with(a, v) +return #a > 0 and is_instanceOf(a[1],Sym) and + a[1].val == v +end + + +function quasiloop(a) + local res = List.new({}) + for i=#a,1,-1 do + local elt = a[i] + if is_instanceOf(elt, List) and + starts_with(elt, "splice-unquote") then + + if #elt ~= 2 then throw("splice-unquote expected 1 argument bot got : " .. #elt) end + + res = List.new({Sym.new( "concat"), elt[2], res}) + else + res = List.new({Sym.new( "cons"), quasiquote(elt), res}) + end + end + return res +end + +function quasiquote(a) + + if is_instanceOf(a,List) then + if starts_with(a, "unquote") then + if #a-1 ~= 1 then + throw("unquote expected 1 argument bot got : " .. #a) + end + return a[2] + else + return quasiloop(a) + end + elseif is_instanceOf(a, Vector) then + local tmp = quasiloop(a) + return List.new({Sym.new("vec"), tmp}) + elseif is_instanceOf(a,HashMap) or is_instanceOf(a,Sym) then + return List.new({Sym.new('quote'), a}) + + else + return a + + end +end + +function is_macro_call(ast, env) + if is_instanceOf(ast, List) and #ast >= 1 and is_instanceOf(ast[1], Sym) then + local status, first_env = pcall( function () return env:get(ast[1]) end) + if not status then return false end + if is_instanceOf(first_env, Function) and first_env.is_macro then + return true + else + return false + end + else + return false + end + +end + +function macro_expand(ast, env) + while is_macro_call(ast, env) do + local macro = env:get(ast[1]) + local f = macro.fn + ast = f(table.unpack(ast, 2)) + end + + return ast +end + +function try(a, env) + --assert(nil, "try is not refactored yet") + if #a > 2 and #a < 1 then + throw("try expected at 1 or 2 arguments but got '" .. #a .. "'.") + end + if #a == 2 then + if not(is_instanceOf(a[2], List) and #a[2] == 3 and is_instanceOf(a[2][1],Sym) and + a[2][1].val == "catch*" and is_instanceOf(a[2][2],Sym) ) then + throw("try expected 2nd argument as list with 3 elems " .. + "first elem being symbol 'catch*' " .. "second being symbol") + end + end + + local tb = a[1] + local cb = a[2] or List.new({ nil , Sym.new('_'), Sym.new('_')}) + local status, val = pcall( function () + return EVAL(tb, env) + end ) + if not status then + local env_with_ex = Env.new(env) + env_with_ex:set(cb[2],val) + return EVAL(cb[3], env_with_ex) + end + return val +end + + + +function EVAL(a, env) + while true do + a = macro_expand(a, env) + if not(is_instanceOf(a, List)) then + return eval_ast(a, env) + end + + if #a == 0 then + return a + end + local first_elem = a[1] + local first_sym = is_instanceOf(first_elem, Sym) and first_elem.val or "" + --print("First symbol= '" .. first_sym .. "'") + + if first_sym == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + + elseif first_sym == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length ofSecond arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + a = a[3] + env = let_env + + + elseif first_sym == "do" then + for i=2,#a-1 do + EVAL(a[i], env) + end + a = a[#a] --tco + + + elseif first_sym == "if" then + if not (#a == 3 or #a == 4) then + throw("if expected 2 or 3 arguments but got '" .. #a-1 .. "'.") + end + local cond = EVAL(a[2], env) + if cond ~= false and cond ~= Nil then + a = a[3] + else + if #a == 4 then + a = a[4] + else + return Nil + end + end + elseif first_sym == "fn*" then + if (#a) ~= 3 then throw("fn* expected 2 arguments but got '" .. #a-1 .. "'.") end + if false then throw("second parameter to fn* should have length 2 but got '" .. #a[2] .. "'.") end + return Function.new(function (...) + local closed_over_env = Env.new(env) + local exprs = List.new(table.pack(...)) + local binds = a[2] + closed_over_env:bind(binds, exprs) + + return EVAL(a[3], closed_over_env) + end, a[3], env, a[2], false) + + elseif first_sym == "quote" then + if #a-1 ~= 1 then throw("quote expects 1 argument got '" .. #a-1 .. "'.") end + return a[2] + elseif first_sym == "quasiquote" then + if #a-1 ~= 1 then throw("quote expects 1 argument got '" .. #a-1 .. "'.") end + a = quasiquote(a[2]) + elseif first_sym == "defmacro!" then + if #a ~= 3 then + throw(string.format("defmacro! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to defmacro! must be symbol") + end + local value = EVAL(a[3], env) + if not(is_instanceOf(value, Function)) then + throw("second argument to defmacro must be function") + end + value.is_macro = true + env:set(a[2], value) + return value + + + elseif first_sym == "macroexpand" then + if (#a) ~= 2 then throw("macroexpand expected 1 arguments but got '" .. #a-1 .. "'.") end + return macro_expand(a[2],env) + elseif first_sym == "try*" then + table.remove(a,1) + return try(a, env) + + else + + local args = eval_ast(a, env) + local f = table.remove(args,1) + if types.is_malfunc(f) then + a = f.ast + env = Env.new(f.env) + env:bind(f.params, args) + + else + if type(f) ~= "function" then + throw("First elem should be function or special form got :'" .. type(f) .. "'.") + end + return f(table.unpack(args)) --fixme: varargs? + end + end + +end +end + + +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return ast + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + return Printer.stringfy_val(a, true) +end + + +local repl_env = Env.new(nil) + +for k,v in pairs(core) do + repl_env:set(Sym.new(k),v) +end + +repl_env:set(Sym.new('eval'), function (ast) + return EVAL(ast, repl_env) +end) + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + + +function main() + rep("(def! not (fn* (a) (if a false true)))") + rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \"(slurp f) \"\nnil)\")))))") +rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))") + repl_env:set(Sym.new("*ARGV*"), List.new(table.pack(table.unpack(arg,2)))) + if #arg > 0 then + local file_to_run = table.remove(arg,1) + rep("(load-file \"" .. file_to_run .. "\")") + os.exit(0) + end + + local line = '' + while true do + line = raw_read('user> ') + if line == nil then + break + end + local status, err = pcall(function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err, true) + end + print("Error: " .. err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/stepA_mal.lua b/impls/lua.2/stepA_mal.lua new file mode 100755 index 0000000000..87876a570c --- /dev/null +++ b/impls/lua.2/stepA_mal.lua @@ -0,0 +1,352 @@ +#!/usr/bin/env lua +local Reader = require "reader" +local Printer = require "printer" +local types = require "types" +local Env = require "env" +local Sym = types.Sym +local is_instanceOf = types.isinstanceof +local Err = types.Err +local List = types.MalList +local throw = types.throw +local HashMap = types.MalHashMap +local Vector = types.MalVector +local Nil = types.Nil +local core = require "core" +local Function = types.MalFunction +local FunctionRef = types.FunctionRef +local readline = core.readline + +function READ(str) + return Reader.read_str(str) +end + +function starts_with(a, v) + return #a > 0 and is_instanceOf(a[1],Sym) and + a[1].val == v +end + + +function quasiloop(a) + local res = List.new({}) + for i=#a,1,-1 do + local elt = a[i] + if is_instanceOf(elt, List) and + starts_with(elt, "splice-unquote") then + + if #elt ~= 2 then + throw("splice-unquote expected 1 argument bot got : " .. #elt) + end + + res = List.new({Sym.new( "concat"), elt[2], res}) + else + res = List.new({Sym.new( "cons"), quasiquote(elt), res}) + end + end + return res +end + +function quasiquote(a) + + if is_instanceOf(a,List) then + if starts_with(a, "unquote") then + if #a-1 ~= 1 then + throw("unquote expected 1 argument bot got : " .. #a) + end + return a[2] + else + return quasiloop(a) + end + elseif is_instanceOf(a, Vector) then + local tmp = quasiloop(a) + return List.new({Sym.new("vec"), tmp}) + elseif is_instanceOf(a,HashMap) or is_instanceOf(a,Sym) then + return List.new({Sym.new('quote'), a}) + + else + return a + + end +end + +function is_macro_call(ast, env) + if is_instanceOf(ast, List) and #ast >= 1 and + is_instanceOf(ast[1], Sym) then + local status, first_env = pcall( function () return env:get(ast[1]) end) + if not status then return false end + if is_instanceOf(first_env, Function) and first_env.is_macro then + return true + else + return false + end + else + return false + end + +end + +function macro_expand(ast, env) + while is_macro_call(ast, env) do + local macro = env:get(ast[1]) + local f = macro.fn + ast = f(table.unpack(ast, 2)) + end + + return ast +end + +function try(a, env) + --assert(nil, "try is not refactored yet") + a = table.pack(table.unpack(a,2)) -- removing try* symbol + + if #a > 2 and #a < 1 then + throw("try expected at 1 or 2 arguments but got '" .. #a .. "'.") + end + if #a == 2 then + if not(is_instanceOf(a[2], List) and #a[2] == 3 and is_instanceOf(a[2][1],Sym) and + a[2][1].val == "catch*" and is_instanceOf(a[2][2],Sym) ) then + throw("try expected 2nd argument as list with 3 elems " .. + "first elem being symbol 'catch*' " .. "second being symbol") + end + end + + local tb = a[1] + local cb = a[2] or List.new({ nil , Sym.new('_'), Sym.new('_')}) + local status, val = pcall( function () + return EVAL(tb, env) + end ) + if not status then + local env_with_ex = Env.new(env) + env_with_ex:set(cb[2],val) + return EVAL(cb[3], env_with_ex) + end + return val +end + + + +function EVAL(a, env) + while true do + a = macro_expand(a, env) + if not(is_instanceOf(a, List)) then + return eval_ast(a, env) + end + + if #a == 0 then + return a + end + local first_elem = a[1] + local first_sym = is_instanceOf(first_elem, Sym) and first_elem.val or "" + --print("First symbol= '" .. first_sym .. "'") + if first_sym == "def!" then + if #a ~= 3 then + throw(string.format("def! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to def! must be symbol") + end + local value = EVAL(a[3], env) + env:set(a[2], value) + return value + + elseif first_sym == "let*" then + if #a ~= 3 then + throw(string.format("let* expects 2 arguments got: %d", #a-1)) + end + + local let_env = Env.new(env) + if not(is_instanceOf(a[2], List) or is_instanceOf(a[2], Vector)) then + throw("Second arg to let* should be list or vector") + end + if #a[2] % 2 ~= 0 then + throw(string.format("Length of second arg to let* should be even number got: %d", #a[2])) + end + + for i=1,#a[2],2 do + if not(is_instanceOf(a[2][i], Sym)) then + throw("Expected symbol in let*'s second argument") + end + local key = a[2][i] + local value = EVAL(a[2][i+1],let_env) + let_env:set(key,value) + end + a = a[3] + env = let_env + + + elseif first_sym == "do" then + for i=2,#a-1 do EVAL(a[i],env) end + a = a[#a] --tco + + + elseif first_sym == "if" then + if not (#a == 3 or #a == 4) then + throw("if expected 2 or 3 arguments but got '" .. #a-1 .. "'.") + end + local cond = EVAL(a[2], env) + if cond ~= false and cond ~= Nil then + a = a[3] + else + if #a == 4 then + a = a[4] + else + return Nil + end + end + elseif first_sym == "fn*" then + if #a ~= 3 then + throw("fn* expected 2 arguments but got '" .. #a-1 .. "'.") + end + if false then throw("second parameter to fn* should have length 2 but got '" .. #a[2] .. "'.") end + return Function.new(function (...) + local closed_over_env = Env.new(env) + local exprs = List.new(table.pack(...)) + local binds = a[2] + closed_over_env:bind(binds, exprs) + + return EVAL(a[3], closed_over_env) + end, a[3], env, a[2], false) + + elseif first_sym == "quote" then + if #a-1 ~= 1 then throw("quote expects 1 argument got '" .. #a-1 .. "'.") end + return a[2] + elseif first_sym == "quasiquote" then + if #a-1 ~= 1 then throw("quote expects 1 argument got '" .. #a-1 .. "'.") end + a = quasiquote(a[2]) + elseif first_sym == "defmacro!" then + if #a ~= 3 then + throw(string.format("defmacro! expects 2 arguments got: %d", #a-1)) + end + if not(is_instanceOf(a[2], Sym)) then + throw("first argument to defmacro! must be symbol") + end + local value = EVAL(a[3], env) + if not(is_instanceOf(value, Function)) then + throw("second argument to defmacro must be function") + end + local c_val = types.copy(value) + c_val.is_macro = true + env:set(a[2], c_val) + return c_val + + + elseif first_sym == "macroexpand" then + if (#a) ~= 2 then throw("macroexpand expected 1 arguments but got '" .. #a-1 .. "'.") end + return macro_expand(a[2],env) + elseif first_sym == "try*" then + return try(a, env) + + else + + local args = eval_ast(a, env) + local f = table.remove(args,1) + if types.is_malfunc(f) then + a = f.ast + env = Env.new(f.env) + env:bind(f.params, args) + + else + if not(type(f) == "function" or is_instanceOf(f, FunctionRef)) then + throw("First elem should be function or special form got :'" .. type(f) .. "'.") + end + return f(table.unpack(args)) + end + end + + end +end + + +function eval_ast(ast, env) + if is_instanceOf(ast, List) then + local l = List.new() + for i=1,#ast do + table.insert(l, EVAL(ast[i], env)) + end + return l + elseif is_instanceOf(ast, Vector) then + local v = Vector.new() + for i=1, #ast do + table.insert(v, EVAL(ast[i], env)) + end + return v + elseif is_instanceOf(ast, HashMap) then + local map = HashMap.new() + for k,v in pairs(ast) do + map[EVAL(k, env)] = EVAL(v, env) + end + return map + elseif is_instanceOf(ast, Sym) then + if string.byte(ast.val, 1, 1) == 202 and + string.byte(ast.val, 2, 2) == 158 then -- this magic numbers come from \u{29E} + return ast + end + return env:get(ast) + + else + return ast + end +end + + +function PRINT(a) + return Printer.stringfy_val(a, true) +end + + +local repl_env = Env.new(nil) + +for k,v in pairs(core) do + repl_env:set(Sym.new(k),v) +end + +repl_env:set(Sym.new('eval'), function (ast) + return EVAL(ast, repl_env) +end) + + +function rep(str) + return PRINT(EVAL(READ(str), repl_env)) +end + + + +function main() + rep("(def! not (fn* (a) (if a false true)))") + rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \"(slurp f) \"\nnil)\")))))") + rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))") + repl_env:set(Sym.new("*ARGV*"), List.new(table.pack(table.unpack(arg,2)))) + repl_env:set(Sym.new("*host-language*"), "lua") + if #arg > 0 then + local file_to_run = table.remove(arg,1) + local status, err = pcall(function () + rep("(load-file \"" .. file_to_run .. "\")") + end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err, true) + end + print("lua mal Error: " .. err) + print(debug.traceback()) + end + + os.exit(0) + end + rep("(println (str \"Mal [\" *host-language* \"]\"))") + local line = '' + while true do + line = readline('user> ') + if line == Nil then + break + end + local status, err = pcall(function () print(rep(line)) end) + if not status then + if is_instanceOf(err, Err) then + err = Printer.stringfy_val(err, true) + end + print("Error: " .. err) + print(debug.traceback()) + end + end +end + +main() diff --git a/impls/lua.2/token.lua b/impls/lua.2/token.lua new file mode 100644 index 0000000000..7cf8a749b8 --- /dev/null +++ b/impls/lua.2/token.lua @@ -0,0 +1,24 @@ +local Token = {} +Token.__index = Token + +setmetatable(Token, { + __call = function (cls, ...) + return cls.new(...) + end, +}) + +function Token.new(typeof, val, line) + local self = setmetatable({}, Token) + self.typeof = typeof + self.val = val + self.line = line + return self +end + +function Token.tostring(self) + -- https://youtu.be/S4eNl1rA1Ns?t=1435 + return string.format("Token: %s:%s at %d", self.typeof, self.val, self.line) +end + + +return Token diff --git a/impls/lua.2/types.lua b/impls/lua.2/types.lua new file mode 100644 index 0000000000..2eafdca673 --- /dev/null +++ b/impls/lua.2/types.lua @@ -0,0 +1,195 @@ +local M = {} + +M.MalHashMap = {} +M.MalHashMap.__index = M.MalHashMap + +function M.MalHashMap.new(...) + arg = table.pack(...) + if #arg % 2 ~= 0 then + error("Hash map input must be even but got '" .. #arg .. "'") + end + + local self = {} + setmetatable(self, M.MalHashMap) + for i= 1, #arg, 2 do + if M.isinstanceof(arg[i], M.Sym) and "\u{29E}" == string.sub(arg[i].val,1,2) then + self[arg[i].val] = arg[i+1] + else + + self[arg[i]] = arg[i+1] + end + + end + + return self +end + +function M.MalHashMap:keys() + local res = {} + for k,_ in pairs(self) do + table.insert(k) + end + return M.MalList.new(res) +end + +M.MalList = {} +M.MalList.__index = M.MalList + +function M.MalList.new(lst) + local self = lst and lst or {} + setmetatable(self, M.MalList) + return self +end + +M.MalVector = {} +M.MalVector.__index = M.MalVector +function M.MalVector.new(lst) + local self = lst and lst or {} + setmetatable(self, M.MalVector) + return self +end + +M.Sym = {} +M.Sym.__index = M.Sym +function M.Sym.new(val) + local self = {} + self.val = val + setmetatable(self, M.Sym) + return self +end +M.Err = {} +M.Err.__index = M.Err +function M.Err.new(val) + local self = {} + self.val = val + setmetatable(self, M.Err) + return self +end + +function M.throw(val) + error(M.Err.new(val)) +end + + +M.MalNilType = {} +M.MalNilType.__index = M.MalNilType +function M.MalNilType.new() + local self = setmetatable({}, M.MalNilType) + return self +end + +function M.MalNilType:tostring() + return "Nil" +end + +M.Nil = M.MalNilType.new() + +M.MalFunction = {} +M.MalFunction.__index = M.MalFunction +function M.MalFunction.new(fn, ast, env, params, is_macro) + local self = {fn = fn, ast = ast, env = env, params = params, is_macro = is_macro} + return setmetatable(self, M.MalFunction) +end + +function M.is_malfunc(a) + return M.isinstanceof(a, M.MalFunction) +end + +function M.is_func(a) + return type(a) == "function" or (M.isinstanceof(a, M.MalFunction) and not a.is_macro) + or M.isinstanceof(a, M.FunctionRef) +end + + +function M.is_sequence(a) + return M.isinstanceof(a, M.MalList) or M.isinstanceof(a, M.MalVector) +end + +M.Atom = {} +M.Atom.__index = M.Atom +function M.Atom.new(val) + local self = {val = val} + return setmetatable(self, M.Atom) +end + +function M.is_atom(v) + return M.isinstanceof(v, M.Atom) +end + + +function M.is_equal(a, b) + if M.isinstanceof(a, M.Sym) and M.isinstanceof(b, M.Sym) then + return a.val == b.val + elseif M.is_sequence(a) and M.is_sequence(b) then + if #a ~= #b then return false end + for i,v in ipairs(a) do + if not M.is_equal(v, b[i]) then + return false + end end + return true + elseif M.isinstanceof(a, M.MalHashMap) and M.isinstanceof(b, M.MalHashMap) then + for k,v in pairs(a) do + if not ( M.is_equal(a[k],b[k])) then + return false + end + end + return true + + else + return a == b + end + +end + + + +function M.isinstanceof(obj, super) + local mt = getmetatable(obj) + while true do + if mt == nil then return false end + if mt == super then return true end + mt = getmetatable(mt) + end +end + + +function M.copy(obj) + if type(obj) == "function" then + return M.FunctionRef.new(obj) + end + if type(obj) ~= "table" then return obj end + + -- copy object data + local new_obj = {} + for k,v in pairs(obj) do + new_obj[k] = v + end + + -- copy metatable and link to original + local old_mt = getmetatable(obj) + if old_mt ~= nil then + local new_mt = {} + for k,v in pairs(old_mt) do + new_mt[k] = v + end + setmetatable(new_mt, old_mt) + setmetatable(new_obj, new_mt) + end + + return new_obj +end + +M.FunctionRef = {} +M.FunctionRef.__index = M.FunctionRef + +function M.FunctionRef.new(fn) + local self = {fn = fn} + return setmetatable(self, M.FunctionRef) +end + +function M.FunctionRef.__call(self, ...) + return self.fn(...) +end + + +return M