local setmetatable = setmetatable local tostring = tostring local pairs = pairs local type = type local ipairs = ipairs local find = string.find local insert = table.insert local remove = table.remove local concat = table.concat local next = next local tokenizer = require("aspect.tokenizer") local tags = require("aspect.tags") local tests = require("aspect.tests") local err = require("aspect.error") local quote_string = require("aspect.utils").quote_string local get_keys = require("aspect.utils").keys local strcount = require("aspect.utils").strcount local compiler_error = err.compiler_error local sub = string.sub local strlen = string.len local pcall = pcall local config = require("aspect.config") local import_type = config.macro.import_type local loop_keys = config.loop.keys local tag_type = config.compiler.tag_type local strip_pattern = config.compiler.strip local func = require("aspect.funcs") local filters = require("aspect.filters") local ast = require("aspect.ast") local utils = require("aspect.utils") local rstrip = utils.rtrim local lstrip = utils.ltrim local special = config.compiler.special --- @class aspect.tag --- @field id number unique ID of the tag in compilation --- @field name string tag name --- @field line number where the tag opened --- @field ctx aspect.compiler.context|nil --- @field append_text fun(tpl:aspect.compiler, text:string) --- @field append_expr fun(tpl:aspect.compiler, expr:string) --- @field append_code fun(tpl:aspect.compiler, code:string) --- @class aspect.compiler.var_ref --- @field name string --- @field line number first use --- @field keys table key-name and line number of first usage --- @class aspect.compiler.block --- @field ctx aspect.compiler.context --- @field parent boolean has parent function inside --- @field desc string the description --- @field start_line number --- @field end_line number --- @field start_pos number --- @field end_pos number --- @class aspect.compiler.macro --- @field ctx aspect.compiler.context --- @field args aspect.compiler.macro.arg[] list of used variables --- @field desc string the description --- @field start_line number of line there block started --- @field end_line number of line there block ended --- @class aspect.compiler.macro.arg --- @field name string --- @field default any --- @class aspect.compiler.context --- @field name string --- @field code string[] --- @field tpl_refs table --- @field var_refs table --- @field vars table local variables --- @class aspect.compiler --- @field ctx_stack aspect.compiler.context[] --- @field ctx aspect.compiler.context current context --- @field aspect aspect.template --- @field name string --- @field blocks table table of blocks --- @field macros table table of macros with their code (witch array) --- @field extends string|boolean if true then we has dynamic extend --- @field import table --- @field line number --- @field prev_line number --- @field tag_name string the current tag --- @field ignore string|nil --- @field tags aspect.tag[] --- @field stats boolean local compiler = { version = 1, } local mt = { __index = compiler } --- @param aspect aspect.template --- @param name string --- @return aspect.compiler function compiler.new(aspect, name, stats) return setmetatable({ aspect = aspect, ctx = nil, ctx_stack = {}, name = name, line = 1, prev_line = 1, macros = {}, extends = nil, blocks = {}, uses = {}, tags = {}, idx = 0, tag_type = nil, import = {}, ignore = nil, stats = stats or true, }, mt) end --- Start compiler --- @param source string source code of the template --- @return boolean ok or not ok --- @return aspect.error if not ok returns error function compiler:run(source) local ok, e = pcall(self.parse, self, source) if ok then return true else return false, err.new(e):set_name(self.name, self.line) end end function compiler:get_code() local code = { "local _self = {", "\tv = " .. self.version .. ",", "\tname = " .. quote_string(self.name) .. ",", "\tblocks = {},", "\tmacros = {},", } if self.extends then if self.extends.static then insert(code,"\textends = " .. self.extends.value .. ",") else insert(code,"\textends = true") end end if self.uses and #self.uses > 0 then insert(code,"\tuses = {") for _, use in ipairs(self.uses) do insert(code,"\t\t{") insert(code,"\t\t\tname = " .. use.name .. ", ") insert(code,"\t\t\tline = " .. use.line .. ", ") if use.with then insert(code,"\t\t\twith = { " .. utils.implode_hashes(use.with) .. " }, ") end insert(code,"\t\t}") end insert(code,"\t}") end insert(code, "}\n") insert(code, "function _self.body(__, _context)") if self.extends then insert(code, "\treturn " .. self.extends.value) elseif #self.ctx.code then insert(code, "\t__:push_state(_self, 1)") for _, v in ipairs(self.ctx.code) do insert(code, "\t" .. v) end insert(code, "\t__:pop_state()") end insert(code, "end\n") if self.blocks then for n, b in pairs(self.blocks) do insert(code, "function _self.blocks." .. n .. "(__, _context)") for _, v in ipairs(b.ctx.code) do insert(code, "\t" .. v) end insert(code, "end\n") end end if self.macros then for n, m in pairs(self.macros) do insert(code, "function _self.macros." .. n .. "(__, _context)") for _, v in ipairs(m.ctx.code) do insert(code, "\t" .. v) end insert(code, "end\n") end end insert(code, "return _self") return concat(code, "\n") end --- Main parse function --- @param source string the template source code function compiler:parse(source) local l = 1 local tag_pos = find(source, "{", l, true) local strip = false -- found tag delimiter local delim = sub(source, tag_pos or 1, (tag_pos or 1) + 1) local open = { ["{{"] = tag_type.EXPRESSION, ["{%"] = tag_type.CONTROL, ["{#"] = tag_type.COMMENT, } self.ctx_stack = {} self.macros = {} self.blocks = {} -- create global context space self:start_context(true, true) while tag_pos do if open[delim] then if l <= tag_pos - 1 then -- cut text before tag local frag = sub(source, l, tag_pos - 1) self.line = self.line + strcount(frag, "\n") if strip then frag = lstrip(frag, strip) strip = nil end local wcp = strip_pattern[sub(source, tag_pos + 2, tag_pos + 2)] if wcp then -- checks if tag has space control frag = rstrip(frag, wcp) end if frag ~= "" then self:append_text(frag) end end local t, p = sub(source, tag_pos + 1, tag_pos + 1), tag_pos + 2 if strip_pattern[sub(source, tag_pos + 2, tag_pos + 2)] then tag_pos = tag_pos + 1 end if t == "{" then -- '{{' self.tag_type = tag_type.EXPRESSION local tok = tokenizer.new(source, tag_pos + 2) local info = {} local expr = self:parse_expression(tok, info) self:append_expr(expr, info.raw) if tok:is_valid() then compiler_error(tok, "syntax", "expecting end of tag") end local path = tok:get_path_as_string() l = tag_pos + 2 + strlen(path) + strlen(tok.finish_token) -- start tag pos + '{{' + tag length + '}}' self.line = self.line + strcount(path, "\n") strip = strip_pattern[sub(tok.finish_token, 1, 1)] tok = nil elseif t == "%" then -- '{%' self.tag_type = tag_type.CONTROL local tok = tokenizer.new(source, tag_pos + 2) self.tag_name = tok:get_token() local tag_name = 'tag_' .. tok:get_token() if tags[tag_name] then self:append_code(tags[tag_name](self, tok:next())) -- call tags.tag_{{ name }}(compiler, tok) if tok:is_valid() then compiler_error(tok, "syntax", "expecting end of tag") end else compiler_error(nil, "syntax", "unknown tag '" .. tok:get_token() .. "'") end local path = tok:get_path_as_string() l = tag_pos + 2 + strlen(path) + strlen(tok.finish_token) -- start tag pos + '{%' + tag length + '%}' self.line = self.line + strcount(path, "\n") strip = strip_pattern[sub(tok.finish_token, 1, 1)] tok = nil self.tag_name = nil elseif t == "#" then -- '{#' local end_comment = find(source, "#}", p, true) self.line = self.line + strcount(sub(source, tag_pos, end_comment - 1), "\n") tag_pos = end_comment l = tag_pos + 2 end self.tag_type = nil else tag_pos = tag_pos + 2 end while true do tag_pos = find(source, "{", tag_pos + 2, true) if not tag_pos then break end delim = sub(source, tag_pos, tag_pos + 1) if open[delim] then break end end if self.ignore and tag_pos then while tag_pos do -- skip all tags until self.ignore tag if find(source, "^{%%[-~]?%s*" .. self.ignore, tag_pos) == tag_pos then self.ignore = nil break end tag_pos = find(source, "{", tag_pos + 1, true) end end end if #self.tags > 0 then local tag = self.tags[#self.tags] compiler_error(nil, "syntax", "Unexpected end of the template '" .. self.name .. "', expecting tag 'end" .. tag.name .. "' (opened on line " .. tag.line .. ")") end local frag = sub(source, l) if strip then frag = lstrip(frag, strip) end if frag ~= "" then self:append_text(frag) end if #self.ctx_stack > 1 then compiler_error(nil, "syntax", "Unexpected extra contexts (" .. #self.ctx_stack .. ")") end self.ctx = self:end_context(self.ctx) -- finish last context end --- @param tok aspect.tokenizer --- @param opts table function compiler:parse_var_name(tok, opts) opts = opts or {} opts.var_system = false if tok:is_word() then local var = tok:get_token() if var == "_context" or var == "__" or var == "_self" then opts.var_system = true else opts.var_system = false end tok:next() return var else compiler_error(tok, "syntax", "expecting variable name") end end --- Parse basic variable name like: --- one --- one.two --- one["two"].three --- @param tok aspect.tokenizer function compiler:parse_variable(tok) if tok:is_word() then local var, keys if tok:is('loop') then -- magick variable name {{ loop }} var = {"loop"} local tag, pos = self:get_last_tag('for') while tag do if tag.has_loop ~= true then tag.has_loop = tag.has_loop or {} local loop_key = tok:next():require("."):next():get_token() if not loop_keys[loop_key] then compiler_error(tok, "syntax", "loop-variable expects one of [" .. concat(get_keys(loop_keys), ", ") .. "] keys") end var[#var + 1] = '"' .. loop_key .. '"' if loop_key == "parent" then tag.has_loop.parent = true tag, pos = self:get_last_tag('for', pos - 1) else tag.has_loop[loop_key] = true tag = false end end tok:next() --else -- tok:next() -- var = {"loop"} end elseif tok:is("_context") then -- magick variable name {{ _context }} tok:next() -- var = {"_context"} end local var_info if not var then var = {self:parse_var_name(tok)} var_info = self:touch_var(var[1]) if not self.ctx.vars[var[1]] then var[1] = "_context." .. var[1] end else var_info = self:touch_var(var[1]) end while tok:is(".") or tok:is("[") do local mode = tok:get_token() tok:next() if mode == "." then -- a.b insert(var, '"' .. tok:require_type('word'):get_token() .. '"') if var_info then var_info.keys[tok:get_token()] = self.line end tok:next() elseif mode == "[" then -- a[b] insert(var, self:parse_expression(tok)) tok:require("]"):next() end end if #var == 1 then return var[1] else return "__.v(" .. concat(var, ", ") .. ")" end else compiler_error(tok, "syntax", "expecting variable name") end end --- @param tok aspect.tokenizer --- @param args_info table --- @param assoc boolean returns table or values or list function compiler:parse_args(tok, args_info, assoc, fname) local args, i = {}, 1 if tok:is(")") then return args end while tok:is_valid() do local key, value, info, arg_info = nil, nil, {}, nil if tok:is_word() and tok:is_next("=") then key = tok:get_token() tok:next():require("="):next() end value = self:parse_expression(tok, info) if key then for _, arg in ipairs(args_info) do if arg.name == key then arg_info = arg break end end if not arg_info then compiler_error(tok, "compile", (fname or "callable") .. " has no '" .. key .. "' argument") end else if args_info[i] then arg_info = args_info[i] else compiler_error(tok, "compile", (fname or "callable") .. " has no argument #" .. i) end end if assoc then args[arg_info.name] = utils.cast_lua(value, info.type, args_info.type) else args[i] = utils.cast_lua(value, info.type, args_info.type) end i = i + 1 if tok:is(",") then tok:next() else break end end return args end --- parse coma separated arguments --- @param tok aspect.tokenizer function compiler:parse_function(tok) local name, args if tok:is_word() then name = tok:get_token() else compiler_error(tok, "syntax", "expecting function name") end if not func.fn[name] then compiler_error(tok, "compile", "function " .. name .. "() not found") end tok:next():require("("):next() if func.args[name] then args = self:parse_args(tok, func.args[name], true, "function " .. name) tok:require(")"):next() if func.parsers[name] then return func.parsers[name](self, args) else return "__.fn." .. name .. "(__, {" .. utils.implode_hashes(args) .. "})" end elseif func.parsers[name] then local code = func.parsers[name](self, nil, tok) tok:require(")"):next() return code else tok:require(")"):next() return "__.fn." .. name .. "(__, {})" end end --- @param tok aspect.tokenizer function compiler:parse_macro(tok) if tok:is("_self") then local name = tok:next():require("."):next():get_token() if not self.macros[name] then compiler_error(tok, "syntax", "Macro " .. name .. " not defined in this (" .. self.name .. ") template") end return "_self.macros." .. name .."(__, " .. self:parse_macro_args(tok:next():require("(")) .. ")", false elseif tok:is_word() then if tok:is_next(".") then local var = tok:get_token() if self.import[var] ~= import_type.GROUP then compiler_error(tok, "syntax", "Macro " .. var .. " not imported") end local name = tok:next():require("."):next():get_token() return "(" .. var .. "." .. name .. " and " .. var .. "." .. name .. "(__, " .. self:parse_macro_args(tok:next()) .. "))", false elseif tok:is_next("(") then local name = tok:get_token() if self.import[name] ~= import_type.SINGLE then compiler_error(tok, "syntax", "Macro " .. name .. " not imported") end tok:next():require("(") return name .."(__, " .. self:parse_macro_args(tok) .. ")", false end end end --- Parse arguments for macros --- @param tok aspect.tokenizer function compiler:parse_macro_args(tok) tok:require("("):next() local i, args = 1, {} while true do local key, value = nil, nil if tok:is_word() then if tok:is_next("=") then key = tok:get_token() tok:next():require("="):next() value = self:parse_expression(tok) else value = self:parse_expression(tok) end else value = self:parse_expression(tok) end if key then args[ key ] = value else args[ i ] = value end if tok:is(",") then tok:next() else break end i = i + 1 end tok:require(")"):next() args = utils.implode_hashes(args) if args then return "{ " .. args .. " }" else return "{}" end end --- Parse array (list or hash) --- [1,2,3] --- {"one": 1, "two": 2} --- @param tok aspect.tokenizer --- @param plain boolean return table without brackets function compiler:parse_array(tok, plain) local vals = {} if tok:is("[") then tok:next() if tok:is("]") then return "{}" end while true do insert(vals, self:parse_expression(tok)) if tok:is(",") then tok:next() elseif tok:is(":") then compiler_error(tok, "syntax", "list can't have named keys, use hash instead") else break end end tok:require("]"):next() elseif tok:is("{") then tok:next() if tok:is("}") then tok:next() return "{}" end while true do local key if tok:is_word() then key = '"' .. tok:get_token() .. '"' tok:next() elseif tok:is_string() or tok:is_number() then key = tok:get_token() tok:next() elseif tok:is("(") then key = self:parse_expression(tok) end insert(vals, "[" .. key .. "] = " .. self:parse_expression(tok:require(":"):next())) if tok:is(",") then tok:next() else break end end tok:require("}"):next() else compiler_error(tok, "syntax", "expecting list or hash") end if plain then return concat(vals, ", ") else return "{" .. concat(vals, ", ") .. "}" end end --- @param tok aspect.tokenizer --- @return table function compiler:parse_hash(tok) local hash = {} tok:require('{'):next() if tok:is("}") then tok:next() return hash end while true do local key if tok:is_word() then key = tok:get_token() tok:next() else compiler_error(tok, "syntax", "expecting hash key") end hash[key] = self:parse_expression(tok:require(":"):next()) if tok:is(",") then tok:next() else break end end tok:require("}"):next() return hash end --- @param tok aspect.tokenizer --- @param info table --- @return string function compiler:parse_filters(tok, var, info) info = info or {} info.raw = false while tok:is("|") do -- parse pipeline filter if tok:next():is_word() then local filter = tok:get_token() if tok:next():is(".") then filter = filter .. "." .. tok:next():require_type("word"):get_token() tok:next() end if not filters.fn[filter] then compiler_error(tok, "compile", "unknown filter " .. filter) end if filter == "raw" then info.raw = true end local args if tok:is("(") then args = self:parse_args(tok:next(), filters.info[filter].args, false, "filter " .. filter) tok:require(")"):next() end if info.type then var = utils.cast_lua(var, info.type, filters.info[filter].input) end if args and #args > 0 then var = "__.f['" .. filter .. "'](" .. var .. ", " .. concat(args, ", ") .. ")" else var = "__.f['" .. filter .. "'](" .. var .. ")" end info.type = filters.info[filter].output else compiler_error(tok, "syntax", "expecting filter name") end end return var end --- Parse operand of the expression --- @param tok aspect.tokenizer --- @param info table function compiler:parse_value(tok, info) local var info = info or {} info.type = nil info.raw = false if tok:is_word() then -- is variable name if special[tok:get_token()] then if tok:is("true") or tok:is("false") then -- is regular true/false/nil var = tok:get_token() tok:next() info.type = "boolean" elseif tok:is("null") or tok:is("nil") then -- is null var = 'nil' info.type = "nil" tok:next() else compiler_error(tok, "syntax", "unknown special token") end elseif tok:is_next("(") then if self.import[tok:get_token()] == import_type.SINGLE then var = self:parse_macro(tok) info.type = "nil" else var = self:parse_function(tok) info.type = "any" end elseif tok:is_seq{"word", ".", "word", "("} then var = self:parse_macro(tok) info.type = "nil" else var = self:parse_variable(tok) info.type = "any" end elseif tok:is_string() then -- is string or number var = tok:get_token() tok:next() info.type = "string" elseif tok:is_number() then var = tok:get_token() tok:next() info.type = "number" elseif tok:is("[") or tok:is("{") then -- is list or hash var = self:parse_array(tok) info.type = "table" elseif tok:is("(") then -- is expression local expr_info = {} var = self:parse_expression(tok:next(), expr_info) info.type = expr_info.type tok:require(")"):next() else compiler_error(tok, "syntax", "expecting any value") end if tok:is("|") then var = self:parse_filters(tok, var, info) end return var end --- Parse any expression --- @param tok aspect.tokenizer --- @param opts table function compiler:parse_expression(tok, opts) opts = opts or {} local expr = ast.new():parse(self, tok):pack() opts.type = expr.type opts.bracket = expr.bracket opts.raw = expr.raw return expr.value end --- @param tok aspect.tokenizer --- @return table test information function compiler:parse_is(tok) local test = {} if tok:require("is"):next():is('not') then test["not"] = true tok:next() end test.name = tok:get_token() tok:next() if tests.args[test.name] then if type(tests.args[test.name]) == "string" then -- has function: is divisible by(expr) tok:require(tests.args[test.name]):next() test.name = test.name .. "_" .. tests.args[test.name] end test.expr = self:parse_expression(tok:require("("):next()) tok:next() end if not tests.fn["is_" .. test.name] then compiler_error(tok, "syntax", "expecting valid test name") end return test end --- Append any text --- @param text string function compiler:append_text(text) local tag = self:get_last_tag() local line = self:get_checkpoint() local code = self.ctx.code if line then insert(code, line) end if tag and tag.append_text then insert(code, tag.append_text(tag, text)) else insert(code, "__(" .. quote_string(text) .. ")") end end function compiler:append_expr(lua, raw) local tag = self:get_last_tag() local line = self:get_checkpoint() local code = self.ctx.code if line then insert(code, line) end if lua then if tag and tag.append_expr then insert(code, tag.append_expr(tag, lua, raw)) elseif raw then insert(code, "__(" .. lua .. ")") else insert(code, "__:e(" .. lua .. ")") end end end --- Append any lua code --- @param lua string function compiler:append_code(lua) local tag = self:get_last_tag() local line = self:get_checkpoint() local code = self.ctx.code if line then insert(code, line) end if type(lua) == "table" then for _, l in ipairs(lua) do if tag and tag.append_code then insert(code, tag.append_code(tag, l)) else insert(code, l) end end elseif lua then if tag and tag.append_code then insert(code, tag.append_code(tag, lua)) else insert(code, lua) end end end function compiler:get_checkpoint() if self.prev_line ~= self.line then self.prev_line = self.line return "__.line = " .. self.line else return nil end end --- Add ref to another template --- @param name string template name function compiler:add_template_ref(name) if not self.tag_name then return end if not self.ctx.tpl_refs[name] then self.ctx.tpl_refs[name] = { [self.tag_name] = self.line } elseif not self.ctx.tpl_refs[name][self.tag_name] then self.ctx.tpl_refs[name][self.tag_name] = self.line end end --- Add local variable name to scope (used for includes, blocks and macros) --- @param name string function compiler:push_var(name) if #self.tags > 0 then local tag = self.tags[#self.tags] if not tag.vars then tag.vars = {[name] = true} else tag.vars[name] = true end end if not self.ctx.vars[name] then self.ctx.vars[name] = 1 else self.ctx.vars[name] = self.ctx.vars[name] + 1 end end --- Analyze and add variable ref if its global variable --- @param name string template name --- @return aspect.compiler.var_ref function compiler:touch_var(name) if self.ctx.vars[name] then -- this is local variable return end if not self.ctx.var_refs[name] then self.ctx.var_refs[name] = { line = self.line, keys = {} } end return self.ctx.var_refs[name] end --- Returns all variable names defined in the scope (without global) --- @return table|nil list of variables like ["variable"] = variable function compiler:get_local_vars() local vars = {} for k, _ in pairs(self.ctx.vars) do vars[k] = k end if next(vars) then return vars else return nil end end --- Push the tag into scope stack --- @param name string the tag name --- @param create_context boolean|string if string, the string will be name of context --- @param isolate_context boolean isolate variables and refs from previous context --- @return aspect.tag function compiler:push_tag(name, create_context, isolate_context) self.idx = self.idx + 1 --- @type aspect.tag local tag = { id = self.idx, name = name, line = self.line, vars = {} } if create_context or isolate_context then tag.ctx = self:start_context(create_context, isolate_context) end local prev = self.tags[#self.tags] if prev then if prev.append_text then tag.append_text = prev.append_text end if prev.append_expr then tag.append_expr = prev.append_expr end if prev.append_code then tag.append_code = prev.append_code end end insert(self.tags, tag) return tag end --- Remove tag from stack --- @param name string the tag name --- @return aspect.tag function compiler:pop_tag(name) if #self.tags then --- @type aspect.tag local tag = self.tags[#self.tags] if tag.name == name then if tag.vars then -- pop variables local vars = self.ctx.vars for var_name, _ in pairs(tag.vars) do if vars[var_name] and vars[var_name] > 0 then vars[var_name] = vars[var_name] - 1 if vars[var_name] == 0 then vars[var_name] = nil end else compiler_error(nil, "compiler", "broken variable scope") end end end if tag.ctx then self:end_context(tag.ctx) end return remove(self.tags) else compiler_error(nil, "syntax", "unexpected tag 'end" .. name .. "'. Expecting tag 'end" .. tag.name .. "' (opened on line " .. tag.line .. ")") end else compiler_error(nil, "syntax", "unexpected tag 'end" .. name .. "'. Tag ".. name .. " never opened") end end --- Returns last tag from stack --- @param name string if set then returns last tag with this name --- @param from number|nil stack offset --- @return aspect.tag --- @return number|nil stack position function compiler:get_last_tag(name, from) if name then if from and from < 1 then return nil end from = from or #self.tags if #self.tags > 0 then for i=from, 1, -1 do if self.tags[i].name == name then return self.tags[i], i end end end elseif #self.tags then return self.tags[#self.tags], #self.tags end return nil end --- Crate new context space for code, variables, refs ... --- @param name boolean|string --- @param isolate boolean --- @return aspect.compiler.context function compiler:start_context(name, isolate) --- @type aspect.compiler.context local ctx = {} ctx.id = #self.ctx_stack + 1 if type(name) == "string" then ctx.name = name ctx.code = { "__:push_state(_self, " .. self.line .. ", " .. quote_string(ctx.name) .. ")" } elseif not name and ctx.id > 1 then ctx.code = self.ctx_stack[#self.ctx_stack].code else ctx.code = {} end if isolate then ctx.vars = {} ctx.tpl_refs = {} ctx.var_refs = {} elseif self.ctx then ctx.vars = self.ctx.vars ctx.tpl_refs = self.ctx.tpl_refs ctx.var_refs = self.ctx.var_refs end self.ctx_stack[ctx.id] = ctx self.ctx = ctx return ctx end --- @return aspect.compiler.context function compiler:end_context(ctx) if ctx.id ~= self.ctx.id then -- just assert compiler_error(nil, "compiler", "incorrect termination of context") end if #self.ctx_stack > 0 then --- @type aspect.compiler.context local prev_ctx = remove(self.ctx_stack) if prev_ctx.name then insert(prev_ctx.code, "__:pop_state()") end self.ctx = self.ctx_stack[#self.ctx_stack] return prev_ctx else compiler_error(nil, "compiler", "broken context stack") end end return compiler