From c8478630342447425a4b62ea544d6ed74468e98e Mon Sep 17 00:00:00 2001 From: mid <> Date: Tue, 11 Jun 2024 17:46:13 +0300 Subject: [PATCH] Initial commit --- bon.lua | 7 + des.lua | 81 +++++++ gen.lua | 224 ++++++++++++++++++ lex.lua | 215 +++++++++++++++++ main.lua | 16 ++ parse.lua | 696 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1239 insertions(+) create mode 100644 bon.lua create mode 100644 des.lua create mode 100644 gen.lua create mode 100644 lex.lua create mode 100644 main.lua create mode 100644 parse.lua diff --git a/bon.lua b/bon.lua new file mode 100644 index 0000000..ee2578b --- /dev/null +++ b/bon.lua @@ -0,0 +1,7 @@ +local next_unique_id = 0 +local function unique_name() + next_unique_id = next_unique_id + 1 + return "__" .. "KY" .. next_unique_id +end + +return {unique_name = unique_name} diff --git a/des.lua b/des.lua new file mode 100644 index 0000000..f386284 --- /dev/null +++ b/des.lua @@ -0,0 +1,81 @@ +local unique_name = require"bon".unique_name + +local function pass_destandardify(chu) + local function is_builtin_method(c) + if c.type ~= 'dot' then + return false + end + + if c.a.et and c.a.et.type == 'list' and c.b == 'pop' then + return {change_stmt = function(stmt) + assert(stmt.type == 'call') + + local temp = {et = stmt.what.a.et, name = unique_name()} + + return { + { + type = 'let', + var = temp, + expr = stmt.what.a + }, + { + type = 'assign', + dest = { + type = 'index', + what = {type = 'var', which = temp, et = temp.et}, + idx = {type = 'lengthof', sub = {type = 'var', which = temp, et = temp.et}, et = {type = 'integer'}} + }, + src = {type = 'null', et = {type = 'null'}} + }, + } + end} + end + + return false + end + + if chu.type == "chunk" then + for k, t in pairs(chu.types) do + for _, m in pairs(t.members) do + if m.type == "field" then + -- Do nothing. + elseif m.type == "static" then + pass_destandardify(m.value) + else + error("Invalid AST") + end + end + end + + local stmtIdx = 1 + while stmtIdx <= #chu.stmts do + local stmt = chu.stmts[stmtIdx] + + local builtinmethod = stmt.type == 'call' and is_builtin_method(stmt.what) + if builtinmethod then + table.remove(chu.stmts, stmtIdx) + for p, o in ipairs(builtinmethod.change_stmt(stmt)) do + table.insert(chu.stmts, stmtIdx + p - 1, o) + end + else + pass_destandardify(stmt) + stmtIdx = stmtIdx + 1 + end + end + elseif chu.type == 'function' then + pass_destandardify(chu.chunk) + elseif chu.type == 'if' then + for i = 1, #chu do + pass_destandardify(chu[i].chu) + end + if chu.elsa then + pass_destandardify(chu.elsa) + end + elseif chu.type == 'fori' then + pass_destandardify(chu.chu) + elseif chu.type == 'loop' then + pass_destandardify(chu.chu) + end +end + +return pass_destandardify diff --git a/gen.lua b/gen.lua new file mode 100644 index 0000000..f76f344 --- /dev/null +++ b/gen.lua @@ -0,0 +1,224 @@ +return function(ast, out) + local function compile(chu) + if chu.type == "chunk" then + if chu.toplevel then + for k, import in pairs(chu.imports) do + out("local %s = require(%q)\n", import[#import], table.concat(import, ".")) + end + for k, var in pairs(chu.vars) do + out("local %s\n", k) + end + for k, expr in pairs(chu.vars) do + if expr then + out("%s=", k) + compile(expr) + out(";") + end + end + for k, t in pairs(chu.types) do + out("local %s={}%s.__index=%s;setmetatable(%s, %s)", k, k, k, k, k) + for _, m in pairs(t.members) do + if m.type == "field" then + -- Do nothing. + elseif m.type == "static" then + out("%s.%s=", k, m.name) + compile(m.value) + out(";") + else + error("Invalid AST") + end + end + end + out("return{") + for k,v in pairs(chu.vars) do + out("%s=%s,", k, k) + end + for k,v in pairs(chu.types) do + out("%s=%s,", k, k) + end + out("}") + else + for k, stmt in ipairs(chu.stmts) do + compile(stmt) + + if stmt.type == 'call' then + out(";") --Safety delimiter. + end + end + end + elseif chu.type == "function" then + out("function(%s%s)", chu.is_constructor and "_," or "", table.concat(chu.args, ",")) + + if SAFE_MODE then + for k, argname in pairs(chu.args) do + local et = chu.et.args[k] + if et then + local checks = {} + if et.type == 'integer' or et.type == 'number' or et.type == 'string' or et.type == 'boolean' then + if et.type == 'integer' then + table.insert(checks, 'type(' .. argname .. ')=="number"') + table.insert(checks, argname .. '%1==0') + else + table.insert(checks, 'type(' .. argname .. ')=="'..et.type..'"') + end + elseif et.type == 'list' then + table.insert(checks, 'type(' .. argname .. ')=="table"') + end + if #checks > 0 then + out("assert(%s, %q);", table.concat(checks, " and "), string.format("Invalid argument %q", argname)) + end + end + end + end + + if chu.is_constructor then + out("local self=setmetatable({}, %s);", chu.et.ret.name) + end + + compile(chu.chunk) + + if chu.is_constructor then + out("return self;") + end + + out("end") + elseif chu.type == "let" then + out("local %s", chu.var.name) + + if chu.expr then + out("=") + compile(chu.expr) + end + + out(";") + elseif chu.type == "num" then + out("%g", chu.value) + elseif chu.type == "var" then + out("%s", chu.which.name) + elseif chu.type == "binop" then + local paren + + paren = chu.a.type == "binop" and chu.a.level > chu.level + if paren then out("(") end + compile(chu.a) + if paren then out(")") end + + local op = chu.op + + if op == "**" then + op = "^" + elseif op == "/" and (not chu.a.et or chu.a.et.type == 'integer') and (not chu.b.et or chu.b.et.type == 'integer') then + op = "//" + end + + out("%s", op) + + paren = chu.b.type == "binop" and chu.b.level > chu.level + if paren then out("(") end + compile(chu.b) + if paren then out(")") end + elseif chu.type == "lengthof" then + out("#(") + compile(chu.sub) + out(")") + elseif chu.type == "dot" then + compile(chu.a) + out("%s%s", chu.colon and ":" or ".", chu.b) + elseif chu.type == "index" then + out("(") + compile(chu.what) + out(")[") + compile(chu.idx) + out("]") + elseif chu.type == "call" then + local guard = chu.type == 'binop' + if guard then + out("(") + end + compile(chu.what) + if guard then + out(")") + end + out("(") + for k, v in pairs(chu.args) do + compile(v) + + if k < #chu.args then + out(",") + end + end + out(")") + elseif chu.type == "dict" then + out("{") + for k, v in pairs(chu.mappings) do + out("[") + compile(k) + out("]=") + compile(v) + out(",") + end + out("}") + elseif chu.type == "list" then + out("{") + for k, v in pairs(chu.values) do + out("[%i]=", k) + compile(v) + out(",") + end + out("}") + elseif chu.type == "string" then + out("%q", chu.value) + elseif chu.type == "null" then + out("nil") + elseif chu.type == "return" then + out("return ") + compile(chu.expr) + out(";") + elseif chu.type == "if" then + out("if ") + compile(chu[1].pred) + out(" then ") + compile(chu[1].chu) + + for i = 2, #chu do + out("elseif ") + compile(chu[i].pred) + out("then ") + compile(chu[i].chu) + end + + if chu.elsa then + out("else ") + compile(chu.elsa) + end + + out("end;") + elseif chu.type == "fori" then + out("for " .. chu.varname .. "=") + compile(chu.from) + out(",") + compile(chu.to) + out("-1 do ") + compile(chu.chu) + out("end ") + elseif chu.type == "loop" then + out("while true do ") + compile(chu.chu) + out("end ") + elseif chu.type == "break" then + out("break ") + elseif chu.type == "noop" then + elseif chu.type == "exprstat" then + compile(chu.expr) + out(";") + elseif chu.type == "assign" then + compile(chu.dest) + out("=") + compile(chu.src) + out(";") + else + error(string.format("Invalid AST node (type %q)", chu.type)) + end + end + return compile(ast) +end diff --git a/lex.lua b/lex.lua new file mode 100644 index 0000000..85e1d14 --- /dev/null +++ b/lex.lua @@ -0,0 +1,215 @@ +--[[ + Titty has two main design goals: + 1. Should look natural from the outside, Lua code's PoV, and + 2. Should compile to Lua code that also appears natural from within + The second is less important than the first. +]] + +local inspect = require"inspect".inspect + +local function codepoints(str) + local it, state, v1, v2 = utf8.codes(str) + local idx = 0 + return function() + if not it then return nil end + + v1, v2 = it(state, v1) + if v1 then + local r = idx + idx = idx + 1 + return r, v2 + else + it = nil + end + end +end + +local function is_digit(cp) + return cp and (cp >= 48 and cp <= 57) +end + +local function is_ident_start(cp) + return cp and ((cp >= 65 and cp <= 90) or (cp >= 97 and cp <= 122) or cp == 95) +end + +local function is_ident_nonstart(cp) + return cp and (is_ident_start(cp) or is_digit(cp)) +end + +local function forall(tbl, f, startI) + startI = startI or 1 + + for k, v in pairs(tbl) do + if not f(v) then + return false + end + end + return true +end + +local function is_whitespace(cp) + return cp and (cp == 32 or cp == 10 or cp == 9) +end + +local function clear(tbl) + for k in pairs(tbl) do + tbl[k] = nil + end +end + +local function lex(cpgetraw) + local row, column, idx = 0, 0, 0 + local pull + local function cpget() + if pull then + local ret = pull + pull = nil + return ret + else + local cp + idx, cp = cpgetraw() + column = column + 1 + if cp == 10 then + column = 0 + row = row + 1 + end + return cp + end + end + + local buf = {} + + return function() + local cp + + while true do + cp = cpget() + + if not is_whitespace(cp) then + break + end + end + + if not cp then + return nil + end + + local rowStart, columnStart = row, column + + if cp == 40 then + return "(", "(", rowStart, columnStart + elseif cp == 41 then + return ")", ")", rowStart, columnStart + elseif cp == 123 then + return "{", "{", rowStart, columnStart + elseif cp == 125 then + return "}", "}", rowStart, columnStart + elseif cp == 44 then + return ",", ",", rowStart, columnStart + elseif cp == 46 then + return ".", ".", rowStart, columnStart + elseif cp == 58 then + return ":", ":", rowStart, columnStart + elseif cp == 61 then + local after = cpget() + if after == 61 then + return "==", "==", rowStart, columnStart + else + pull = after + end + return "=", "=", rowStart, columnStart + elseif cp == 43 then + return "+", "+", rowStart, columnStart + elseif cp == 45 then + return "-", "-", rowStart, columnStart + elseif cp == 42 then + local after = cpget() + if after == 42 then + return "**", "**", rowStart, columnStart + else + pull = after + end + return "*", "*", rowStart, columnStart + elseif cp == 47 then + return "/", "/", rowStart, columnStart + elseif cp == 37 then + return "%", "%", rowStart, columnStart + elseif cp == 35 then + return "#", "#", rowStart, columnStart + elseif cp == 91 then + return "[", "[", rowStart, columnStart + elseif cp == 93 then + return "]", "]", rowStart, columnStart + elseif cp == 39 then + while true do + cp = cpget() + + if cp ~= 39 then + buf[#buf + 1] = cp + else + break + end + end + + local ret = utf8.char(table.unpack(buf)) + + clear(buf) + + return "string", ret, rowStart, columnStart + elseif is_ident_start(cp) then + buf[1] = cp + + while true do + cp = cpget() + + if is_ident_nonstart(cp) then + buf[#buf + 1] = cp + else + pull = cp + break + end + end + + local ret = utf8.char(table.unpack(buf)) + + clear(buf) + + if ret == "func" or ret == "end" or ret == "if" + or ret == "while" or ret == "do" or ret == "then" + or ret == "elseif" or ret == "type" or ret == "interf" + or ret == "let" or ret == "return" or ret == "else" + or ret == "for" or ret == "import" or ret == "constr" + or ret == "loop" or ret == "break" or ret == "nil" then + + return ret, ret, rowStart, columnStart + end + + return "id", ret, rowStart, columnStart + elseif is_digit(cp) then + buf[1] = cp + + local dotFound = false + + while true do + cp = cpget() + if is_digit(cp) then + buf[#buf + 1] = cp + elseif cp == 46 and not dotFound then + dotFound = true + buf[#buf + 1] = cp + else + pull = cp + break + end + end + + local ret = utf8.char(table.unpack(buf)) + clear(buf) + return "num", ret, rowStart, columnStart + else + error(string.format("%i:%i unknown character %q (code point %i)", row, column, utf8.char(cp), cp)) + end + end +end + +return {lex = lex, codepoints = codepoints} diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..04a0323 --- /dev/null +++ b/main.lua @@ -0,0 +1,16 @@ +local Lexer = require"lex" +local Parser = require"parse" +local Des = require"des" +local Gen = require"gen" + +local tokens = {} + +for tt, ts, tr, tc in Lexer.lex(Lexer.codepoints(io.open(arg[1], "r"):read("*a"))) do + tokens[#tokens + 1] = {tt, ts, tr, tc} +end + +local ast = Parser(tokens, false) + +Des(ast) + +Gen(ast, function(str, ...) io.write(string.format(str, ...)) end) diff --git a/parse.lua b/parse.lua new file mode 100644 index 0000000..0729aeb --- /dev/null +++ b/parse.lua @@ -0,0 +1,696 @@ +local unique_name = require"bon".unique_name + +local function do_everything(Tokens, Skeleton) + local TokensIdx = 1 + local IdxStack = {} + + local function gettoken() + local r = Tokens[TokensIdx] + if r then + TokensIdx = TokensIdx + 1 + end + return table.unpack(r) + end + local function peektoken() + local r = Tokens[TokensIdx] + if r then + return table.unpack(Tokens[TokensIdx]) + end + end + local function maybe(t) + local a, b, c, d = peektoken() + if a == t then return gettoken() end + return nil + end + local function pushlexi() + table.insert(IdxStack, TokensIdx) + end + local function poplexi() + TokensIdx = table.remove(IdxStack) + end + local function forgetlexi() + table.remove(IdxStack) + end + + local function expect(t) + local a, b, c, d = gettoken() + if t ~= a then + error(string.format("%i:%i expected %s, got %s", c, d, t, a)) + end + return a, b, c, d + end + + local parse_chunk + + local function new_parse_context(parent) + return setmetatable({["the parent"] = parent}, { + __index = function(self, key) + while true do + local r = rawget(self, key) + if r then + return r + end + self = rawget(self, "the parent") + if not self then + return + end + end + end, + __newindex = function(self, key, value) + local z = self + local r = nil + while true do + r = rawget(z, key) + if r then break end + z = rawget(z, "the parent") + if not z then break end + end + rawset(z or self, key, value) + end, + }) + end + + local ParseContext, ParseContextTypes + + local function push_parse_context() + ParseContext = new_parse_context(ParseContext) + ParseContextTypes = new_parse_context(ParseContextTypes) + end + + local function pop_parse_context() + ParseContext = rawget(ParseContext, "the parent") + ParseContextTypes = rawget(ParseContextTypes, "the parent") + end + + local Types = {} + function Types.castable(this, to) + return not to or (this and this.type == to.type) + end + function Types.is_number(this) + return not this or this.type == 'number' or this.type == 'integer' + end + function Types.can_do_arith(this) + return Types.is_number(this) + end + function Types.is_callable(this) + return not this or this.type == 'function' + end + function Types.is_indexable(this) + return not this or this.type == 'list' + end + function Types.has_field(this, field) + if not this then + return true, nil + end + + if this.type == 'list' and field == 'pop' then + return true, {type = 'function', ret = false, args = {this}} + end + + if this.type ~= 'structure' then + return false, nil + end + + for k, v in ipairs(this.members) do + if v.name == field then + return true, v.et + end + end + + return false, nil + end + + local function parse_et() + local A + + if peektoken() == "id" then + local name = select(2, gettoken()) + if name == "number" then + A = {type = 'number'} + elseif name == "integer" then + A = {type = 'integer'} + elseif name == "string" then + A = {type = 'string'} + elseif name == "boolean" then + A = {type = 'boolean'} + elseif name == "list" then + A = {type = 'list'} + elseif name == "dict" then + A = {type = 'dict'} + else + A = ParseContextTypes[name] + end + end + + if not A then + error("Invalid type description") + end + + if maybe("[") then + A.parametrization = {parse_et()} + + expect("]") + end + + return A + end + + local parse_function + + local function parse_function_arg() + local name = select(2, expect("id")) + local et + if maybe(":") then + et = parse_et() + end + return name, et + end + + local function parse_type() + expect("type") + + local name = select(2, expect("id")) + + local ret = {type = 'structure', members = {}, name = name} + + while true do + if maybe("end") then + break + end + + if peektoken() == "constr" or peektoken() == "func" then + local isConstr = gettoken() == "constr" + local fname + + if isConstr then + fname = "__call" + else + fname = select(2, expect("id")) + end + + local argnames = {} + local argets = {} + + if not isConstr then + table.insert(argnames, "self") + table.insert(argets, ret) + end + + push_parse_context() + rawset(ParseContext, "self", {et = ret, name = "self"}) + + if maybe("(") then + if not maybe(")") then + + while true do + local name, et = parse_function_arg() + + table.insert(argnames, name) + argets[#argnames] = et + + rawset(ParseContext, name, {et = et, name = name}) + + if maybe(")") then + break + else + expect(",") + end + end + end + end + + local fret + if isConstr then + fret = ret + else + if maybe(":") then + fret = parse_et() + end + end + + local chu = parse_chunk(false) + + local et = {type = "function", args = argets, ret = fret} + + table.insert(ret.members, { + type = "static", + name = fname, + et = et, + value = {type = "function", et = et, args = argnames, chunk = chu, is_constructor = isConstr}, + }) + + pop_parse_context() + + expect("end") + else + local membname = select(2, expect("id")) + + expect(":") + + local et = parse_et() + + table.insert(ret.members, {type = "field", name = membname, et = et}) + end + end + + return name, ret + end + + parse_function = function() + expect("func") + + local name = select(2, maybe("id")) + + push_parse_context() + + local argnames = {} + local argets = {} + + if maybe("(") then + if not maybe(")") then + while true do + local name, et = parse_function_arg() + + table.insert(argnames, name) + argets[#argnames] = et + + rawset(ParseContext, name, {et = et, name = name}) + + if maybe(")") then + break + else + expect(",") + end + end + end + end + + local retet + if maybe(":") then + retet = parse_et() + end + + rawset(ParseContext, "the function name", name) + rawset(ParseContextTypes, "the function return type", retet) + + local ret = { + type = "function", + et = {type = "function", args = argets, ret = retet}, + chunk = parse_chunk(false), + args = argnames + } + + pop_parse_context() + + expect("end") + + return ret, name + end + + local active_stmt_list_stack = {} + + BINOP_LEVELS = {["+"] = 3, ["-"] = 3, ["=="] = 4, ["/"] = 2, ["*"] = 2, ["%"] = 2, ["**"] = 1} + local function parse_expr(level) + level = level or 4 + + if level == 4 then + local A = parse_expr(level - 1) + + if peektoken() == "==" then + local op = gettoken() + + local B = parse_expr(level - 1) + + A = {type = "binop", level = level, et = {type = 'boolean'}, op = op, a = A, b = B} + end + + return A + elseif level == 3 then + local A = parse_expr(level - 1) + + if peektoken() == "+" or peektoken() == "-" then + local op = gettoken() + + local B = parse_expr(level - 1) + + if not Types.can_do_arith(A.et) or not Types.can_do_arith(B.et) then + error(op .. " operator not supported for non-number types") + end + + A = {type = "binop", level = level, et = A.et, op = op, a = A, b = B} + end + + return A + elseif level == 2 then + local A = parse_expr(level - 1) + + if peektoken() == "*" or peektoken() == "/" or peektoken() == "%" then + local op = gettoken() + + local B = parse_expr(level - 1) + + if not Types.can_do_arith(A.et) or not Types.can_do_arith(B.et) then + error(op .. " operator not supported for non-number types") + end + + A = {type = "binop", level = level, et = A.et, op = op, a = A, b = B} + end + + return A + elseif level == 1 then + local A = parse_expr(level - 1) + + if peektoken() == "**" then + local op = gettoken() + + local B = parse_expr(level - 1) + + if not Types.can_do_arith(A.et) or not Types.can_do_arith(B.et) then + error(op .. " operator not supported for non-number types") + end + + A = {type = "binop", level = level, et = A.et, op = op, a = A, b = B} + end + + return A + elseif level == 0 then + local A + + if maybe("#") then + A = {type = 'lengthof', et = {type = 'integer'}, sub = parse_expr(level)} + + if A.sub.et and A.sub.et.type ~= 'list' then + error("# requires a list") + end + elseif maybe("(") then + A = parse_expr() + expect(")") + elseif peektoken() == "func" then + A = parse_function() + elseif peektoken() == "num" then + local lit = select(2, gettoken()) + A = {type = "num", et = lit:find("%.") and {type = 'number'} or {type = 'integer'}, value = tonumber(lit)} + elseif peektoken() == "id" then + local name = select(2, gettoken()) + local v = ParseContext[name] + if not v then + error(string.format("No value with name %s exists.", name)) + end + A = {type = "var", which = v, et = v.et} + elseif maybe("{") then + A = {type = "dict", et = {type = "dict", parametrization = {}}, mappings = {}} + expect("}") + elseif maybe("[") then + A = {type = "list", et = {type = "list", parametrization = {}}, values = {}} + expect("]") + elseif peektoken() == "string" then + A = {type = "string", et = {type = 'string'}, value = select(2, gettoken())} + elseif maybe("nil") then + A = {type = 'null', et = {type = 'null'}} + else + error("Unexpected token " .. select(2, peektoken())) + end + + while peektoken() == "[" or peektoken() == "(" or peektoken() == "." do + if maybe("[") then + if A.et and A.et.type ~= 'list' and A.et.type ~= 'dict' and A.et.type ~= 'string' then + error("Indexed value is not a list nor dict.") + end + + local newet + if A.et.type == 'list' then + newet = A.et.parametrization and A.et.parametrization[1] + elseif A.et.type == 'dict' then + newet = A.et.parametrization and A.et.parametrization[2] + elseif A.et.type == 'string' then + newet = A.et + else + error() + end + + local idx = parse_expr() + + if A.et.type == 'string' then + local temp = {name = unique_name(), et = nil} + + table.insert(active_stmt_list_stack[#active_stmt_list_stack], { + type = 'let', + var = temp, + expr = {type = 'binop', level = BINOP_LEVELS['+'], op = '+', a = idx, b = {type = 'num', value = 1, et = {type = 'integer'}}} + }) + + A = { + type = 'call', + what = {type = 'dot', colon = true, a = A, b = 'sub', et = nil}, + args = { + {type = 'var', which = temp, et = temp.et}, + {type = 'var', which = temp, et = temp.et} + } + } + else + if A.et.type == 'list' then + -- Switch to 1-based indexing + idx = {type = 'binop', level = BINOP_LEVELS['+'], op = '+', a = idx, b = {type = 'num', value = 1, et = {type = 'integer'}}} + end + + A = {type = 'index', what = A, idx = idx, et = newet} + end + + expect("]") + elseif maybe("(") then + if not Types.is_callable(A.et) then + error("Called value is not a function.") + end + + local args = {} + + if not maybe(")") then + while true do + table.insert(args, parse_expr()) + + if maybe(")") then break end + + expect(",") + end + end + + A = {type = 'call', what = A, et = A.et and A.et.ret or nil, args = args} + elseif maybe(".") then + local B = select(2, expect("id")) + + local has_field, field_type = Types.has_field(A.et, B) + + if not has_field then + error(string.format("No such field %s", B)) + end + + A = {type = 'dot', a = A, b = B, et = field_type} + end + end + + return A + end + + error("Invalid expression") + end + + local function parse_stmt() + if maybe("let") then + local name = select(2, expect("id")) + + local et + if maybe(":") then + et = parse_et() + end + + local expr + if maybe("=") then + expr = parse_expr() + end + + if not et and not expr then + error(string.format("Cannot create name %s without a given type", name)) + end + + if not et then + et = expr.et + elseif expr and not Types.castable(expr.et, et) then + error(string.format("Cannot cast expression to type %s", et.type)) + end + + local newvar = {et = et, name = name} + + ParseContext[name] = newvar + + return {type = "let", var = newvar, expr = expr} + elseif maybe("if") then + local ret = {type = "if"} + + while true do + local expr = parse_expr() + if not et and not Types.castable(expr.et, {type = 'boolean'}) then + error("If predicate must be castable to boolean") + end + + expect("then") + + local chu = parse_chunk(false) + + table.insert(ret, {pred = expr, chu = chu}) + + if maybe("else") then + ret.elsa = parse_chunk(false) + + expect("end") + + break + end + + if maybe("end") then + break + end + + expect("elseif") + end + + return ret + elseif maybe("for") then + local varname = select(2, expect("id")) + + expect("=") + + local from = parse_expr() + + expect(",") + + local to = parse_expr() + + expect("do") + + push_parse_context() + + rawset(ParseContext, varname, {et = {type = 'integer'}, name = varname}) + + local chu = parse_chunk(false) + + pop_parse_context() + + expect("end") + + return {type = "fori", varname = varname, from = from, to = to, chu = chu} + elseif maybe("loop") then + push_parse_context() + local chu = parse_chunk(false) + pop_parse_context() + expect("end") + + return {type = "loop", chu = chu} + elseif maybe("break") then + return {type = "break"} + elseif maybe("return") then + local expr = parse_expr() + + if expr.et and not Types.castable(expr.et, ParseContextTypes["the function return type"]) then + error(string.format("Function %s returns different type.", ParseContext["the function name"])) + end + + return {type = "return", expr = expr} + else + local e = parse_expr() + + if maybe("=") then + local src = parse_expr() + + if not Types.castable(src.et, e.et) then + error("Cannot cast for assignment") + end + + return {type = "assign", dest = e, src = src} + else + if e.type ~= "call" then + error("Expected statement.") + end + + return e + end + end + + error(string.format("Unexpected token %s", (select(2, peektoken())))) + end + + local SAFE_MODE = true + + parse_chunk = function(toplevel) + push_parse_context() + + local ast = {type = "chunk", types = {}, vars = {}, stmts = {}, imports = {}, toplevel = toplevel} + + table.insert(active_stmt_list_stack, ast.stmts) + + while true do + local tt, ts, tr, tc = peektoken() + + if tt == "end" or tt == "else" or tt == "elseif" then + if toplevel then + error("Unexpected end in top-level chunk") + end + + break + elseif tt == nil then + if not toplevel then + error("Unexpected EOF in non top-level chunk") + end + + break + end + + if toplevel and tt == "import" then + gettoken() + + local path = {} + + while true do + local part = select(2, expect("id")) + table.insert(path, part) + + if not maybe(".") then + break + end + end + + + + rawset(ParseContext, path[#path], {et = nil, name = path[#path]}) + + table.insert(ast.imports, path) + elseif tt == "type" then + local name, t = parse_type() + ast.types[name] = t + rawset(ParseContextTypes, name, t) + elseif tt == "func" then + local expr, name = parse_function() + ast.vars[name] = expr + rawset(ParseContext, name, {et = expr.et, name = name}) + elseif not toplevel then + table.insert(ast.stmts, parse_stmt()) + else + error(string.format("Unexpected %s in top-level chunk", tt)) + end + end + + table.remove(active_stmt_list_stack) + + pop_parse_context() + + return ast + end + + return parse_chunk(true) +end + +--local chu = parse_chunk(true) +--pass_destandardify(chu) +--compile(chu) +--print() + +return do_everything