diff --git a/lib/lua/vm/dispatcher.ex b/lib/lua/vm/dispatcher.ex index e04d96b..830a5c8 100644 --- a/lib/lua/vm/dispatcher.ex +++ b/lib/lua/vm/dispatcher.ex @@ -919,7 +919,7 @@ defmodule Lua.VM.Dispatcher do else: callee_proto frame = {code, pc + 1, regs, upvalues, proto, cont, :discard, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -973,7 +973,7 @@ defmodule Lua.VM.Dispatcher do # `return_one/7` skips Map.fetch! lookups and lets the BEAM # bind everything in a single `move` per slot. frame = {code, pc + 1, regs, upvalues, proto, cont, base, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1035,7 +1035,7 @@ defmodule Lua.VM.Dispatcher do else: callee_proto frame = {code, pc + 1, regs, upvalues, proto, cont, base, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1088,7 +1088,7 @@ defmodule Lua.VM.Dispatcher do else: callee_proto frame = {code, pc + 1, regs, upvalues, proto, cont, base, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1142,7 +1142,7 @@ defmodule Lua.VM.Dispatcher do else: callee_proto frame = {code, pc + 1, regs, upvalues, proto, cont, base, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1194,7 +1194,7 @@ defmodule Lua.VM.Dispatcher do else: callee_proto frame = {code, pc + 1, regs, upvalues, proto, cont, :discard, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1246,7 +1246,7 @@ defmodule Lua.VM.Dispatcher do else: callee_proto frame = {code, pc + 1, regs, upvalues, proto, cont, :discard, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1299,7 +1299,7 @@ defmodule Lua.VM.Dispatcher do else: callee_proto frame = {code, pc + 1, regs, upvalues, proto, cont, :discard, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1354,12 +1354,9 @@ defmodule Lua.VM.Dispatcher do # with the same depth, and the callee starts with an empty # open-upvalue map while the caller's rides in the frame. - # `line` is carried for shape parity with the other call opcodes and - # for tooling that reads the encoded stream; the handler never needs - # it, because a self-call can never reach the native bridge that - # attributes errors to a source line, and the frame's own line slot - # is `0` for every dispatcher-side call. - {@op_call_self, base, arg_count, result_count, name_hint, _line} -> + # `line` goes in the frame's call info, as for every call opcode, so + # `error(msg, level)` can name the line a function was called from. + {@op_call_self, base, arg_count, result_count, name_hint, line} -> callee_regs = init_callee_regs(proto, regs, base + 1, arg_count) callee_proto = @@ -1375,7 +1372,7 @@ defmodule Lua.VM.Dispatcher do end frame = {code, pc + 1, regs, upvalues, proto, cont, dest, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1866,7 +1863,7 @@ defmodule Lua.VM.Dispatcher do end frame = {code, pc + 1, regs, upvalues, proto, cont, dest, ou} - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -1887,7 +1884,7 @@ defmodule Lua.VM.Dispatcher do {:lua_closure, _, _} = closure -> args = collect_args(regs, base + 1, total_args) - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -2452,7 +2449,7 @@ defmodule Lua.VM.Dispatcher do {:lua_closure, _, _} = closure, args, name_hint, - _line, + line, code, pc, regs, @@ -2466,7 +2463,7 @@ defmodule Lua.VM.Dispatcher do cd, ou ) do - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) @@ -2513,7 +2510,7 @@ defmodule Lua.VM.Dispatcher do args, base, name_hint, - _line, + line, code, pc, regs, @@ -2527,7 +2524,7 @@ defmodule Lua.VM.Dispatcher do cd, ou ) do - call_info = {proto.source, 0, name_hint} + call_info = {proto.source, line, name_hint} instruction_count = tick(state, instruction_count, cs, cd) ckdepth(state, cs, cd) diff --git a/lib/lua/vm/stdlib.ex b/lib/lua/vm/stdlib.ex index 6967692..f820ddc 100644 --- a/lib/lua/vm/stdlib.ex +++ b/lib/lua/vm/stdlib.ex @@ -189,12 +189,13 @@ defmodule Lua.VM.Stdlib do # Per Lua 5.3 §6.1 the raised value passes through pcall/xpcall verbatim, # and string messages gain a `source:line:` prefix unless `level == 0`. # The prefixed view lives in `:lua_value` (Lua-facing only); `:value` - # stays raw so host rendering is untouched. Levels >= 2 (attribute to the - # caller's caller) need per-frame lines the call stack does not carry and - # are treated as level 1 for now. + # stays raw so host rendering is untouched. Level 2 names the line the + # erroring function was called from, level 3 its caller's, and so on: each + # call-stack frame records its call site. A level past the stack omits the + # prefix, as PUC Lua does. defp lua_error([message | rest], state) do - {line, source} = Executor.current_position() level = error_level(rest) + {line, source} = error_position(level, state) raise RuntimeError, value: message, @@ -209,9 +210,19 @@ defmodule Lua.VM.Stdlib do raise RuntimeError, value: nil, line: line, source: source, state: state end - defp error_level([level | _]) when is_number(level), do: level + defp error_level([level | _]) when is_integer(level), do: level + defp error_level([level | _]) when is_float(level), do: trunc(level) defp error_level(_), do: 1 + defp error_position(level, state) when level >= 2 do + case Enum.at(state.call_stack, level - 2) do + {source, line, _name} when is_integer(line) and line > 0 -> {line, source} + _ -> {nil, nil} + end + end + + defp error_position(_level, _state), do: Executor.current_position() + # §6.1 position prefix: only string messages, only when `level ~= 0`, and # only when the executor recorded a usable position. A `{nil, _}` position # (native call inside a compiled closure — the dispatcher does not plumb diff --git a/test/lua/error_gallery_test.exs b/test/lua/error_gallery_test.exs index a964f9c..849137f 100644 --- a/test/lua/error_gallery_test.exs +++ b/test/lua/error_gallery_test.exs @@ -121,16 +121,16 @@ defmodule Lua.ErrorGalleryTest do Lua runtime error: runtime error: stack overflow Stack trace: - gallery.lua:0: in function 'f' - gallery.lua:0: in function 'f' - gallery.lua:0: in function 'f' - gallery.lua:0: in function 'f' - gallery.lua:0: in function 'f' - gallery.lua:0: in function 'f' - gallery.lua:0: in function 'f' + gallery.lua:1: in function 'f' + gallery.lua:1: in function 'f' + gallery.lua:1: in function 'f' + gallery.lua:1: in function 'f' + gallery.lua:1: in function 'f' + gallery.lua:1: in function 'f' + gallery.lua:1: in function 'f' ... 20 more frames ... - gallery.lua:0: in function 'f' - gallery.lua:0: in function 'f' + gallery.lua:1: in function 'f' + gallery.lua:1: in function 'f' gallery.lua:2: in function 'f'\ """} ] diff --git a/test/lua/vm/pcall_error_value_test.exs b/test/lua/vm/pcall_error_value_test.exs index 9e6b2d9..d6050af 100644 --- a/test/lua/vm/pcall_error_value_test.exs +++ b/test/lua/vm/pcall_error_value_test.exs @@ -203,6 +203,45 @@ defmodule Lua.VM.PcallErrorValueTest do assert results == [false, "hi"] end + + test "level 2 blames the caller's line" do + {results, _state} = + run( + """ + local function check(x) + if not x then error("bad input", 2) end + end + local ok, err = pcall(function() + check(false) + end) + return err + """, + @engine + ) + + assert results == ["test.lua:5: bad input"] + end + + test "level 3 blames the caller's caller" do + {results, _state} = + run( + """ + local function check(x) + if not x then error("deep", 3) end + end + local function validate(x) + check(x) + end + local ok, err = pcall(function() + validate(false) + end) + return err + """, + @engine + ) + + assert results == ["test.lua:8: deep"] + end end describe "xpcall hands the raw value to the handler (#{engine} engine)" do