diff --git a/spec/traceback/depth_recursion/depth_recursion.pln b/spec/traceback/depth_recursion/depth_recursion.pln new file mode 100644 index 00000000..855dacb3 --- /dev/null +++ b/spec/traceback/depth_recursion/depth_recursion.pln @@ -0,0 +1,17 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local mod: module = {} + +function mod.pallene_fn(lua_fn: integer -> (), depth: integer) + if depth == 0 then + -- Call 'lua_fn' for the last time so that we can raise an error. + lua_fn(depth) + end + + lua_fn(depth - 1) +end + +return mod diff --git a/spec/traceback/depth_recursion/main.lua b/spec/traceback/depth_recursion/main.lua new file mode 100644 index 00000000..1688ee67 --- /dev/null +++ b/spec/traceback/depth_recursion/main.lua @@ -0,0 +1,25 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local pallene = require 'spec.traceback.depth_recursion.depth_recursion' + +-- luacheck: globals lua_fn +function lua_fn(depth) + if depth == 0 then + error "Depth reached 0!" + end + + pallene.pallene_fn(lua_fn, depth - 1) +end + +-- Should be local. +-- Making it global so that it is visible in the traceback. +-- luacheck: globals wrapper +function wrapper() + lua_fn(10) +end + +-- luacheck: globals pallene_tracer_debug_traceback +xpcall(wrapper, pallene_tracer_debug_traceback) diff --git a/spec/traceback/module_lua/another_module.lua b/spec/traceback/module_lua/another_module.lua new file mode 100644 index 00000000..98307fe0 --- /dev/null +++ b/spec/traceback/module_lua/another_module.lua @@ -0,0 +1,12 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local function call_lua_callback(callback) + callback() +end + +return { + call_lua_callback = call_lua_callback +} diff --git a/spec/traceback/module_lua/main.lua b/spec/traceback/module_lua/main.lua new file mode 100644 index 00000000..ef764119 --- /dev/null +++ b/spec/traceback/module_lua/main.lua @@ -0,0 +1,36 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local another_module = require 'spec.traceback.module_lua.another_module' +local pallene = require 'spec.traceback.module_lua.module_lua' + +-- luacheck: globals lua_1 +function lua_1() + -- luacheck: globals lua_2 + pallene.pallene_1(lua_2) +end + +-- luacheck: globals lua_2 +function lua_2() + -- luacheck: globals lua_3 + pallene.pallene_2(lua_3, 33, 79) +end + +-- luacheck: globals lua_3 +function lua_3(sum) + print("The summation is: ", sum) + + error "Any normal error from Lua!" +end + +-- Should be local. +-- Making it global so that it is visible in the traceback. +-- luacheck: globals wrapper +function wrapper() + another_module.call_lua_callback(lua_1) +end + +-- luacheck: globals pallene_tracer_debug_traceback +xpcall(wrapper, pallene_tracer_debug_traceback) diff --git a/spec/traceback/module_lua/module_lua.pln b/spec/traceback/module_lua/module_lua.pln new file mode 100644 index 00000000..0c756db3 --- /dev/null +++ b/spec/traceback/module_lua/module_lua.pln @@ -0,0 +1,16 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local mod: module = {} + +function mod.pallene_1(lua_2: () -> ()) + lua_2() +end + +function mod.pallene_2(lua_3: integer -> (), a: integer, b: integer) + lua_3(a + b) +end + +return mod diff --git a/spec/traceback/module_pallene/main.lua b/spec/traceback/module_pallene/main.lua new file mode 100644 index 00000000..62f7902c --- /dev/null +++ b/spec/traceback/module_pallene/main.lua @@ -0,0 +1,27 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local pallene = require 'spec.traceback.module_pallene.module_pallene' +local pallene_alt = require 'spec.traceback.module_pallene.module_pallene_alt' + +-- luacheck: globals lua_2 +function lua_2() + error "There's an error in everyday life. Shame!" +end + +-- luacheck: globals lua_1 +function lua_1() + pallene_alt.alternate_everyday_fn(lua_2) +end + +-- Should be local. +-- Making it global so that it is visible in the traceback. +-- luacheck: globals wrapper +function wrapper() + pallene.normal_everyday_fn(lua_1) +end + +-- luacheck: globals pallene_tracer_debug_traceback +xpcall(wrapper, pallene_tracer_debug_traceback) diff --git a/spec/traceback/module_pallene/module_pallene.pln b/spec/traceback/module_pallene/module_pallene.pln new file mode 100644 index 00000000..ffc4d707 --- /dev/null +++ b/spec/traceback/module_pallene/module_pallene.pln @@ -0,0 +1,12 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local mod: module = {} + +function mod.normal_everyday_fn(callback: () -> ()) + callback() +end + +return mod diff --git a/spec/traceback/module_pallene/module_pallene_alt.pln b/spec/traceback/module_pallene/module_pallene_alt.pln new file mode 100644 index 00000000..cf933ff6 --- /dev/null +++ b/spec/traceback/module_pallene/module_pallene_alt.pln @@ -0,0 +1,13 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local mod: module = {} + +function mod.alternate_everyday_fn(callback: () -> ()) + callback() +end + +return mod + diff --git a/spec/traceback/rect/main.lua b/spec/traceback/rect/main.lua new file mode 100644 index 00000000..3a4da419 --- /dev/null +++ b/spec/traceback/rect/main.lua @@ -0,0 +1,16 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local rect = require 'spec.traceback.rect.rect' + +-- Should be local. +-- Making it global so that it is visible in the traceback. +-- luacheck: globals wrapper +function wrapper() + print(rect.area { width = "Huh, gotcha!", height = 16.0 }) +end + +-- luacheck: globals pallene_tracer_debug_traceback +xpcall(wrapper, pallene_tracer_debug_traceback) diff --git a/spec/traceback/rect/rect.pln b/spec/traceback/rect/rect.pln new file mode 100644 index 00000000..91cb86eb --- /dev/null +++ b/spec/traceback/rect/rect.pln @@ -0,0 +1,17 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local mod: module = {} +typealias rect = { width: any, height: any } + +function mod.universal_calc_area(x: any, y: any): any + return (x as float * y as float) as any +end + +function mod.area(r: rect): float + return mod.universal_calc_area(r.width, r.height) as float +end + +return mod diff --git a/spec/traceback_spec.lua b/spec/traceback_spec.lua new file mode 100644 index 00000000..af7666fe --- /dev/null +++ b/spec/traceback_spec.lua @@ -0,0 +1,96 @@ +-- Copyright (c) 2024, The Pallene Developers +-- Pallene is licensed under the MIT license. +-- Please refer to the LICENSE and AUTHORS files for details +-- SPDX-License-Identifier: MIT + +local util = require "pallene.util" + +local function assert_test(test, expected_traceback) + local plnfile = util.shell_quote("spec/traceback/"..test.."/"..test..".pln") + local ok, err = util.execute("pallenec "..plnfile.." --use-traceback") + assert(ok, err) + + -- Compile the second Pallene file if exists. + local alt_plnfile = util.shell_quote("spec/traceback/"..test.."/"..test.."_alt.pln") + local ok, _ = util.execute("test -f "..alt_plnfile) + if ok then + local ok, err = util.execute("pallenec "..alt_plnfile.." --use-traceback") + assert(ok, err) + end + + local luafile = util.shell_quote("spec/traceback/"..test.."/main.lua") + local ok, err, _, err_content = util.outputs_of_execute("lua "..luafile) + assert(ok, err) + assert.are.same(expected_traceback, err_content) +end + +it("Rectangle", function() + assert_test("rect", [[ +Runtime error: spec/traceback/rect/main.lua:12: file spec/traceback/rect/rect.pln: line 10: wrong type for downcasted value, expected float but found string +Stack traceback: + spec/traceback/rect/rect.pln:10: in function 'universal_calc_area' + spec/traceback/rect/rect.pln:13: in function 'area' + spec/traceback/rect/main.lua:12: in function 'wrapper' + C: in function 'xpcall' + spec/traceback/rect/main.lua:16: in
+ C: in function '' +]]) +end) + +it("Multi-module Lua", function() + assert_test("module_lua", [[ +Runtime error: spec/traceback/module_lua/main.lua:25: Any normal error from Lua! +Stack traceback: + C: in function 'error' + spec/traceback/module_lua/main.lua:25: in function 'lua_3' + spec/traceback/module_lua/module_lua.pln:12: in function 'pallene_2' + spec/traceback/module_lua/main.lua:18: in function 'lua_2' + spec/traceback/module_lua/module_lua.pln:8: in function 'pallene_1' + spec/traceback/module_lua/main.lua:12: in function 'callback' + ./spec/traceback/module_lua/another_module.lua:7: in function 'call_lua_callback' + spec/traceback/module_lua/main.lua:32: in function 'wrapper' + C: in function 'xpcall' + spec/traceback/module_lua/main.lua:36: in
+ C: in function '' +]]) +end) + +it("Multi-module Pallene", function() + assert_test("module_pallene", [[ +Runtime error: spec/traceback/module_pallene/main.lua:11: There's an error in everyday life. Shame! +Stack traceback: + C: in function 'error' + spec/traceback/module_pallene/main.lua:11: in function 'lua_2' + spec/traceback/module_pallene/module_pallene_alt.pln:8: in function 'alternate_everyday_fn' + spec/traceback/module_pallene/main.lua:16: in function 'lua_1' + spec/traceback/module_pallene/module_pallene.pln:8: in function 'normal_everyday_fn' + spec/traceback/module_pallene/main.lua:23: in function 'wrapper' + C: in function 'xpcall' + spec/traceback/module_pallene/main.lua:27: in
+ C: in function '' +]]) +end) + +it("Depth recursion", function() + assert_test("depth_recursion", [[ +Runtime error: spec/traceback/depth_recursion/main.lua:11: Depth reached 0! +Stack traceback: + C: in function 'error' + spec/traceback/depth_recursion/main.lua:11: in function 'lua_fn' + spec/traceback/depth_recursion/depth_recursion.pln:8: in function 'pallene_fn' + spec/traceback/depth_recursion/main.lua:14: in function 'lua_fn' + spec/traceback/depth_recursion/depth_recursion.pln:8: in function 'pallene_fn' + spec/traceback/depth_recursion/main.lua:14: in function 'lua_fn' + spec/traceback/depth_recursion/depth_recursion.pln:8: in function 'pallene_fn' + spec/traceback/depth_recursion/main.lua:14: in function 'lua_fn' + spec/traceback/depth_recursion/depth_recursion.pln:8: in function 'pallene_fn' + spec/traceback/depth_recursion/main.lua:14: in function 'lua_fn' + spec/traceback/depth_recursion/depth_recursion.pln:8: in function 'pallene_fn' + spec/traceback/depth_recursion/main.lua:14: in function 'lua_fn' + spec/traceback/depth_recursion/main.lua:21: in function 'wrapper' + C: in function 'xpcall' + spec/traceback/depth_recursion/main.lua:25: in
+ C: in function '' +]]) +end) + diff --git a/src/pallene/coder.lua b/src/pallene/coder.lua index ed70a210..d1d1cc20 100644 --- a/src/pallene/coder.lua +++ b/src/pallene/coder.lua @@ -23,8 +23,8 @@ local define_union = tagged_union.in_namespace(coder, "coder") local Coder local RecordCoder -function coder.generate(module, modname, pallene_filename) - local c = Coder.new(module, modname, pallene_filename) +function coder.generate(module, modname, pallene_filename, flags) + local c = Coder.new(module, modname, pallene_filename, flags) local code = c:generate_module_header() .. c:generate_module_body() return code, {} end @@ -57,10 +57,11 @@ end -- Coder = util.Class() -function Coder:init(module, modname, filename) +function Coder:init(module, modname, filename, flags) self.module = module self.modname = modname self.filename = filename + self.flags = flags self.current_func = false @@ -212,13 +213,20 @@ end -- Raise an error if the given table contains a metatable. Pallene would rather raise an error in -- these cases instead of invoking the metatable operations, which may impair program optimization -- even if they are never called. -local function check_no_metatable(src, loc) +local function check_no_metatable(self, src, loc) + local setline = "" + if self.flags.use_traceback then + setline = string.format("PALLENE_SETLINE(%d);", loc.line) + end + return (util.render([[ if ($src->metatable) { + ${setline} pallene_runtime_array_metatable_error(L, PALLENE_SOURCE_FILE, $line); } ]], { src = src, + setline = setline, line = C.integer(loc.line), })) end @@ -245,14 +253,22 @@ function Coder:get_stack_slot(typ, dst, slot, loc, description_fmt, ...) else assert(not typ.is_upvalue_box) local extra_args = table.pack(...) + + local setline = "" + if self.flags.use_traceback then + setline = string.format("PALLENE_SETLINE(%d);", loc.line) + end + check_tag = util.render([[ if (l_unlikely(!$test)) { + ${setline} pallene_runtime_tag_check_error(L, $file, $line, $expected_type, $slot, ${description_fmt}${opt_comma}${extra_args}); } ]], { test = self:test_tag(typ, slot), + setline = setline, file = C.string(loc and loc.file_name or ""), line = C.integer(loc and loc.line or 0), expected_type = C.string(pallene_type_tag(typ)), @@ -289,7 +305,7 @@ function Coder:get_luatable_slot(typ, dst, slot, tab, loc, description_fmt, ...) } ]], { slot = slot, - check_no_metatable = check_no_metatable(tab, loc), + check_no_metatable = check_no_metatable(self, tab, loc), })) end @@ -440,17 +456,40 @@ function Coder:pallene_entry_point_definition(f_id) local max_frame_size = self.gc[func].max_frame_size local slots_needed = max_frame_size + self.max_lua_call_stack_usage[func] + + local setline = "" + local void_frameexit = "" + if self.flags.use_traceback then + table.insert(prologue, util.render([[ + PALLENE_C_FRAMEENTER(L, "$name"); + /**/ + ]], { + name = func.name + })); + + setline = string.format("PALLENE_SETLINE(%d);", func.loc and func.loc.line or 0) + + if #func.typ.ret_types == 0 then + void_frameexit = "PALLENE_FRAMEEXIT(L);" + end + end + if slots_needed > 0 then table.insert(prologue, util.render([[ - if (!lua_checkstack(L, $n)) { pallene_runtime_cant_grow_stack_error(L); } + if (!lua_checkstack(L, $n)) { + ${setline} + pallene_runtime_cant_grow_stack_error(L); + } ]], { - n = C.integer(slots_needed) + n = C.integer(slots_needed), + setline = setline, })) end table.insert(prologue, "StackValue *base = L->top.p;"); table.insert(prologue, self:savestack()) table.insert(prologue, "/**/") + for v_id = #arg_types + 1, #func.vars do -- To avoid -Wmaybe-uninitialized warnings we have to initialize our local variables of type -- "Any". Nils and Booleans only set the type tag of the TValue and leave the "._value" @@ -470,12 +509,14 @@ function Coder:pallene_entry_point_definition(f_id) ${prologue} /**/ ${body} + ${void_fe} } ]], { name_comment = C.comment(name_comment), fun_decl = self:pallene_entry_point_declaration(f_id), prologue = table.concat(prologue, "\n"), body = body, + void_fe = void_frameexit })) end @@ -552,13 +593,28 @@ function Coder:lua_entry_point_definition(f_id) Udata *K = uvalue(&func->upvalue[0]); ]] + local frameenter = "" + local setline = "" + local frameexit = "" + if self.flags.use_traceback then + frameenter = util.render([[ PALLENE_LUA_FRAMEENTER(L, $fun_name); ]], { + fun_name = self:lua_entry_point_name(f_id), + }) + + setline = string.format("PALLENE_SETLINE(%d);", func.loc and func.loc.line or 0) + + frameexit = "PALLENE_FRAMEEXIT(L);" + end + local arity_check = util.render([[ int nargs = lua_gettop(L); if (l_unlikely(nargs != $nargs)) { + ${setline} pallene_runtime_arity_error(L, $fname, $nargs, nargs); } ]], { nargs = C.integer(#arg_types), + setline = setline, fname = C.string(fname), }) @@ -590,8 +646,8 @@ function Coder:lua_entry_point_definition(f_id) table.insert(ret_decls, C.declaration(ctype(typ), ret)..";") end - local call_pallene = self:call_pallene_function(ret_vars, f_id, "func", arg_vars) - + local call_pallene = self:call_pallene_function(ret_vars, f_id, "func", arg_vars, + self:lua_entry_point_name(f_id)) local push_results = {} for i, typ in ipairs(ret_types) do @@ -601,6 +657,7 @@ function Coder:lua_entry_point_definition(f_id) return (util.render([[ ${fun_decl} { + ${lua_fenter} StackValue *base = L->ci->func.p; ${init_global_userdata} /**/ @@ -613,10 +670,12 @@ function Coder:lua_entry_point_definition(f_id) ${ret_decls} ${call_pallene} ${push_results} + ${lua_fexit} return $nresults; } ]], { fun_decl = self:lua_entry_point_declaration(f_id), + lua_fenter = frameenter, init_global_userdata = init_global_userdata, arity_check = arity_check, arg_decls = table.concat(arg_decls, "\n"), @@ -624,7 +683,8 @@ function Coder:lua_entry_point_definition(f_id) ret_decls = table.concat(ret_decls, "\n"), call_pallene = call_pallene, push_results = table.concat(push_results, "\n"), - nresults = C.integer(#ret_types), + lua_fexit = frameexit, + nresults = C.integer(#ret_types) })) end @@ -937,7 +997,7 @@ gen_cmd["Unop"] = function(self, cmd, _func) ${check_no_metatable} $dst = luaH_getn($x); ]], { - check_no_metatable = check_no_metatable(x, cmd.loc), + check_no_metatable = check_no_metatable(self, x, cmd.loc), line = C.integer(cmd.loc.line), dst = dst, x = x @@ -1392,7 +1452,13 @@ gen_cmd["CallStatic"] = function(self, cmd, func) end table.insert(parts, self:update_stack_top(func, cmd)) - table.insert(parts, self:call_pallene_function(dsts, f_id, cclosure, xs)) + + if self.flags.use_traceback then + table.insert(parts, string.format("PALLENE_SETLINE(%d);\n", + func.loc and func.loc.line or 0)) + end + + table.insert(parts, self:call_pallene_function(dsts, f_id, cclosure, xs, nil)) table.insert(parts, self:restorestack()) return table.concat(parts, "\n") end @@ -1426,15 +1492,24 @@ gen_cmd["CallDyn"] = function(self, cmd, func) })) end + local setline = "" + if self.flags.use_traceback then + setline = util.render([[ PALLENE_SETLINE($line); ]], { + line = C.integer(func.loc and func.loc.line or 0) + }) + end + return util.render([[ ${update_stack_top} ${push_arguments} + ${setline} lua_call(L, $nargs, $nrets); ${pop_results} ${restore_stack} ]], { update_stack_top = self:update_stack_top(func, cmd), push_arguments = table.concat(push_arguments, "\n"), + setline = setline, pop_results = table.concat(pop_results, "\n"), nargs = C.integer(#f_typ.arg_types), nrets = C.integer(#f_typ.ret_types), @@ -1572,8 +1647,14 @@ gen_cmd["Seq"] = function(self, cmd, func) end gen_cmd["Return"] = function(self, cmd) + local frameexit = "" + if self.flags.use_traceback then + frameexit = "PALLENE_FRAMEEXIT(L);" + end + if #cmd.srcs == 0 then - return [[ return; ]] + return util.render([[ ${fexit} + return; ]], { fexit = frameexit }) else -- We assign the dsts from right to left, in order to match Lua's semantics when a -- destination variable appears more than once in the LHS. For example, in `x,x = f()`. @@ -1585,7 +1666,8 @@ gen_cmd["Return"] = function(self, cmd) util.render([[ *$reti = $v; ]], { reti = self:c_ret_var(i), v = src })) end local src1 = self:c_value(cmd.srcs[1]) - table.insert(returns, util.render([[ return $v; ]], { v = src1 })) + table.insert(returns, util.render([[ ${fexit} + return $v; ]], { fexit = frameexit, v = src1 })) return table.concat(returns, "\n") end end @@ -1806,6 +1888,17 @@ function Coder:generate_luaopen_function() init_function = self:lua_entry_point_name(1), }) + local init_pt = "" + + if self.flags.use_traceback then + init_pt = [[ + /* Initialize Pallene Tracer. */ + pallene_tracer_init(L); + /**/ + ]] + end + + -- NOTE: Version compatibility -- --------------------------- -- We have both a compile-time and a run-time test. The compile-time test ensures that the @@ -1821,6 +1914,7 @@ function Coder:generate_luaopen_function() #error "Lua version must be exactly 5.4.6" #endif + ${init_pt} luaL_checkcoreversion(L); /**/ @@ -1845,6 +1939,7 @@ function Coder:generate_luaopen_function() } ]], { name = "luaopen_" .. self.modname, + init_pt = init_pt, n_upvalues = C.integer(#self.constants), init_constants = table.concat(init_constants, "\n"), init_initializers = init_initializers, diff --git a/src/pallene/driver.lua b/src/pallene/driver.lua index b16300a8..161c6613 100644 --- a/src/pallene/driver.lua +++ b/src/pallene/driver.lua @@ -99,7 +99,7 @@ function driver.compile_internal(filename, input, stop_after, opt_level) error("impossible") end -local function compile_pallene_to_c(pallene_filename, c_filename, mod_name, opt_level) +local function compile_pallene_to_c(pallene_filename, c_filename, mod_name, opt_level, flags) local input, err = driver.load_input(pallene_filename) if not input then return false, { err } @@ -111,7 +111,7 @@ local function compile_pallene_to_c(pallene_filename, c_filename, mod_name, opt_ end local c_code - c_code, errs = coder.generate(module, mod_name, pallene_filename) + c_code, errs = coder.generate(module, mod_name, pallene_filename, flags) if not c_code then return false, errs end @@ -178,7 +178,8 @@ end -- Writes the resulting output to [output_file_name] with extension [output_ext]. -- If [output_file_name] is nil then the output is written to a file in the same -- directory as [input_file_name] and having the same base name as the input file. -function driver.compile(argv0, opt_level, input_ext, output_ext, input_file_name, output_file_name) +function driver.compile(argv0, opt_level, input_ext, output_ext, + input_file_name, output_file_name, flags) local input_base_name, err = check_source_filename(argv0, input_file_name, input_ext) if not input_base_name then return false, {err} end @@ -212,7 +213,7 @@ function driver.compile(argv0, opt_level, input_ext, output_ext, input_file_name local f = compiler_steps[i].f local src = file_names[i] local out = file_names[i+1] - ok, errs = f(src, out, mod_name, opt_level) + ok, errs = f(src, out, mod_name, opt_level, flags) if not ok then break end end diff --git a/src/pallene/pallenec.lua b/src/pallene/pallenec.lua index 85e650ed..281a4cf2 100644 --- a/src/pallene/pallenec.lua +++ b/src/pallene/pallenec.lua @@ -26,13 +26,16 @@ do -- What the compiler should output. p:mutex( - p:flag("--emit-c", "Generate a .c file instead of an executable"), - p:flag("--emit-lua", "Generate a .lua file instead of an executable"), - p:flag("--compile-c", "Compile a .c file generated by --emit-c"), - p:flag("--only-check","Check for syntax or type errors, without compiling"), - p:flag("--print-ir", "Show the intermediate representation for a program") + p:flag("--emit-c", "Generate a .c file instead of an executable"), + p:flag("--emit-lua", "Generate a .lua file instead of an executable"), + p:flag("--compile-c", "Compile a .c file generated by --emit-c"), + p:flag("--only-check", "Check for syntax or type errors, without compiling"), + p:flag("--print-ir", "Show the intermediate representation for a program") ) + -- No Pallene tracebacks + p:flag("--use-traceback", "Use Pallene Tracer function traceback for debugging") + p:option("-O", "Optimization level") :args(1):convert(tonumber) :choices({"0", "1", "2", "3"}) @@ -43,9 +46,9 @@ do opts = p:parse() end -local function compile(in_ext, out_ext) +local function compile(in_ext, out_ext, flags) local ok, errs = driver.compile(compiler_name, opts.O, in_ext, out_ext, opts.source_file, - opts.output) + opts.output, flags) if not ok then util.abort(table.concat(errs, "\n")) end end @@ -69,12 +72,16 @@ local function do_print_ir() end function pallenec.main() - if opts.emit_c then compile("pln", "c") - elseif opts.emit_lua then compile("pln", "lua") - elseif opts.compile_c then compile("c" , "so") + local flags = { + use_traceback = opts.use_traceback and true or false + } + + if opts.emit_c then compile("pln", "c", flags) + elseif opts.emit_lua then compile("pln", "lua", flags) + elseif opts.compile_c then compile("c" , "so", flags) elseif opts.only_check then do_check() elseif opts.print_ir then do_print_ir() - else --[[default]] compile("pln", "so") + else --[[default]] compile("pln", "so", flags) end end diff --git a/src/pallene/pallenelib.lua b/src/pallene/pallenelib.lua index 097892d0..17c891c5 100644 --- a/src/pallene/pallenelib.lua +++ b/src/pallene/pallenelib.lua @@ -43,9 +43,71 @@ return [==[ #include #include #include +#include #define PALLENE_UNREACHABLE __builtin_unreachable() +/* Part of Pallene Tracer. */ +#define PALLENE_C_FRAMEENTER(L, name) \ + static pt_fn_details_t _details = { \ + .fn_name = name, \ + .mod_name = PALLENE_SOURCE_FILE \ + }; \ + pt_frame_t _frame = { \ + .type = PALLENE_TRACER_FRAME_TYPE_C, \ + .shared = { \ + .details = &_details \ + } \ + }; \ + pallene_tracer_frameenter(L, &_frame) + +#define PALLENE_LUA_FRAMEENTER(L, sig) \ + pt_frame_t _frame = { \ + .type = PALLENE_TRACER_FRAME_TYPE_LUA, \ + .shared = { \ + .frame_sig = sig \ + } \ + }; \ + pallene_tracer_frameenter(L, &_frame) + +#define PALLENE_SETLINE(line) pallene_tracer_setline(&_frame, line) +#define PALLENE_FRAMEEXIT(...) pallene_tracer_frameexit(L); + +/* PALLENE TRACER RELATED DATA-STRUCTURES. */ + +/* Whether the frame is a Pallene->Pallene or Lua->Pallene call. */ +typedef enum frame_type { + PALLENE_TRACER_FRAME_TYPE_C, + PALLENE_TRACER_FRAME_TYPE_LUA +} frame_type_t; + +/* Details of a single function such as what is the name + and where it is from. */ +typedef struct pt_fn_details { + const char *const fn_name; + const char *const mod_name; +} pt_fn_details_t; + +/* A single frame representation. */ +typedef struct pt_frame { + frame_type_t type; + int line; + + union { + const pt_fn_details_t *details; + const lua_CFunction frame_sig; + } shared; + + struct pt_frame *prev; +} pt_frame_t; + +/* Pallene Tracer. */ +static void pallene_tracer_frameenter(lua_State *L, pt_frame_t *restrict frame); +static void pallene_tracer_setline(pt_frame_t *restrict frame, int line); +static void pallene_tracer_frameexit(lua_State *L); +static int pallene_tracer_debug_traceback(lua_State *L); +static void pallene_tracer_init(lua_State *L); + /* Type tags */ static const char *pallene_type_name(lua_State *L, const TValue *v); static int pallene_is_truthy(const TValue *v); @@ -90,12 +152,258 @@ static lua_Number pallene_math_log(lua_Integer x, lua_Integer base); static lua_Integer pallene_math_modf(lua_State *L, const char* file, int line, lua_Number n, lua_Number* out); /* Other builtins */ -static TString* pallene_string_char(lua_State *L, const char* file, int line, lua_Integer c); -static TString* pallene_string_sub(lua_State *L, TString *str, lua_Integer start, lua_Integer end); +static TString *pallene_string_char(lua_State *L, const char* file, int line, lua_Integer c); +static TString *pallene_string_sub(lua_State *L, TString *str, lua_Integer start, lua_Integer end); static TString *pallene_type_builtin(lua_State *L, TValue v); static TString *pallene_tostring(lua_State *L, const char* file, int line, TValue v); static void pallene_io_write(lua_State *L, TString *str); +/* Pallene tracer implementation. */ + +/* Private routines. */ + +static bool _findfield(lua_State *L, int fn_idx, int level) { + if(level == 0 || !lua_istable(L, -1)) + return false; + + lua_pushnil(L); /* Initial key. */ + + while(lua_next(L, -2)) { + /* We are only interested in String keys. */ + if(lua_type(L, -2) == LUA_TSTRING) { + /* Avoid "_G" recursion in global table. The global table is also part of + global table. */ + if(!strcmp(lua_tostring(L, -2), "_G")) { + /* Remove value and continue. */ + lua_pop(L, 1); + continue; + } + + /* Is it the function we are looking for? */ + if(lua_rawequal(L, fn_idx, -1)) { + /* Remove value and keep name. */ + lua_pop(L, 1); + + return true; + } + /* If not go one level deeper and get the value recursively. */ + if(_findfield(L, fn_idx, level - 1)) { + /* Remove the table but keep name. */ + lua_remove(L, -2); + + /* Add a "." in between. */ + lua_pushliteral(L, "."); + lua_insert(L, -2); + + /* Concatenate last 3 values, resulting "table.some_func". */ + lua_concat(L, 3); + + return true; + } + } + + /* Pop the value. */ + lua_pop(L, 1); + } + + return false; +} + +/* Pushes a function name if found in the global table and returns true. + Returns false otherwise. */ +/* Expects the funtion to be pushed in the stack. */ +static bool _pgf_name(lua_State *L) { + int top = lua_gettop(L); + + lua_pushglobaltable(L); + + if(_findfield(L, top, 2)) { + lua_remove(L, -2); + + return true; + } + + lua_pop(L, 1); + return false; +} + +/* Private routines end. */ + +static void pallene_tracer_frameenter(lua_State *L, pt_frame_t *restrict frame) { + /* Retrieve the end of the stack. */ + lua_getglobal(L, "__pallene_tracer_stack"); + pt_frame_t *stack = (pt_frame_t *) lua_topointer(L, -1); + lua_pop(L, 1); + + /* If there is no frame in the stack. */ + /* No matter what type of frame we got (Lua or plain C), it will be + in the general stack. */ + if(l_unlikely(stack == NULL)) { + frame->prev = NULL; + stack = frame; + + goto out; + } + + frame->prev = stack; + stack = frame; + +out: + lua_pushlightuserdata(L, stack); + lua_setglobal(L, "__pallene_tracer_stack"); +} + +static void pallene_tracer_setline(pt_frame_t *restrict frame, int line) { + frame->line = line; +} + +static void pallene_tracer_frameexit(lua_State *L) { + /* Retrieve the end of the stack. */ + lua_getglobal(L, "__pallene_tracer_stack"); + pt_frame_t *stack = (pt_frame_t *) lua_topointer(L, -1); + lua_pop(L, 1); + + /* We are popping the very last frame. */ + if(stack->prev == NULL) { + stack = NULL; + goto out; + } + + stack = stack->prev; + +out: + lua_pushlightuserdata(L, stack); + lua_setglobal(L, "__pallene_tracer_stack"); +} + +static int pallene_tracer_debug_traceback(lua_State *L) { + const char *message = lua_tostring(L, 1); + fprintf(stderr, "Runtime error: %s\nStack traceback:\n", message); + + /* Lua: 1, Pallene: 0 */ + int context = 1; + int level = 1; + bool l_stack = true; + lua_CFunction f_sig = NULL; + + lua_getglobal(L, "__pallene_tracer_stack"); + pt_frame_t *stack = (pt_frame_t *) lua_topointer(L, -1); + lua_pop(L, 1); + + /* To store lua call stack information, to use it in both contexts. */ + lua_Debug ar; + + /* We will restore to this top everytime. */ + int top = lua_gettop(L); + + while(l_stack || stack != NULL) { + /* Generally, we would spend most of our time dealing with Pallene->Pallene calls. */ + if(l_unlikely(context == 1)) { + if(!(l_stack = lua_getstack(L, level++, &ar))) + continue; + + /* We need more info for a good traceback entry. */ + /* Also push the function on the stack. */ + lua_getinfo(L, "Slntf", &ar); + + /* We have got a C frame. Time to make a context switch. */ + if(lua_iscfunction(L, -1)) { + /* Set the signature and switch to Pallene stack. */ + f_sig = lua_tocfunction(L, -1); + context = 0; + } else { + /* It's a regular Lua function. */ + + /* Do we have a name? */ + if(*ar.namewhat != '\0') + lua_pushfstring(L, "function '%s'", ar.name); + /* Is it the main chunk? */ + else if(*ar.what == 'm') + lua_pushliteral(L, "
"); + /* Can we deduce the name from the global table? */ + else if(_pgf_name(L)) + lua_pushfstring(L, "function '%s'", lua_tostring(L, -1)); + else lua_pushliteral(L, "function ''"); + + fprintf(stderr, " %s:%d: in %s\n", ar.short_src, + ar.currentline, lua_tostring(L, -1)); + } + + lua_settop(L, top); + } else { + /* We can still use the debug structure storing the last Lua call info. */ + lua_getinfo(L, "f", &ar); + + /* Deduce name from global table. */ + if(_pgf_name(L)) + lua_pushfstring(L, "C: in function '%s'", lua_tostring(L, -1)); + else lua_pushliteral(L, "C: in function ''"); + + if(stack == NULL) { + if(f_sig != NULL) + fprintf(stderr, " %s\n", lua_tostring(L, -1)); + + context = 1; + goto end; + } + + if(f_sig != NULL) { + /* Check if the frame signature matches. */ + pt_frame_t *check = stack; + + while(check->type != PALLENE_TRACER_FRAME_TYPE_LUA) + check = check->prev; + + /* It's an untracked C function. */ + if(f_sig != check->shared.frame_sig) { + fprintf(stderr, " %s\n", lua_tostring(L, -1)); + + /* Now we switch to Lua stack. */ + context = 1; + goto end; + } + + /* Bingo! We have found a signature. Erase the signature so that + in the next iteration we don't care about rechecking the + frame signature agian. */ + f_sig = NULL; + } + + /* If we find a Lua interface, we simply ignore and switch. */ + if(stack->type == PALLENE_TRACER_FRAME_TYPE_LUA) { + stack = stack->prev; + + context = 1; + goto end; + } + + fprintf(stderr, " %s:%d: in function '%s'\n", stack->shared.details->mod_name, + stack->line, stack->shared.details->fn_name); + + stack = stack->prev; + + end: + lua_settop(L, top); + } + } + + return 0; +} + +static void pallene_tracer_init(lua_State *L) { + lua_getglobal(L, "__pallene_tracer_stack"); + + /* Setup the state and pallene traceback fn. */ + if(l_likely(lua_isnil(L, -1) == true)) { + /* The first value is the tail of the LinkedList stack. */ + lua_pushlightuserdata(L, NULL); + + lua_setglobal(L, "__pallene_tracer_stack"); + + /* The debug traceback fn. */ + lua_register(L, "pallene_tracer_debug_traceback", pallene_tracer_debug_traceback); + } +} static const char *pallene_type_name(lua_State *L, const TValue *v) {