diff --git a/CHANGELOG.md b/CHANGELOG.md index ada4da9..e755806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,25 +73,35 @@ is in the [`1.0.0-rc.0`](#100-rc0---2026-05-26) entry below. shim that always returned `false` and was imported into every `use Lua.API` module; the VM has no MFA references. Remove any `when is_mfa(value)` clauses (they never matched). +- **The public runtime exception is now solely `Lua.RuntimeException`.** The + five internal VM error structs — `Lua.VM.RuntimeError`, `Lua.VM.TypeError`, + `Lua.VM.ArgumentError`, `Lua.VM.AssertionError`, `Lua.VM.InternalError` — are + no longer part of the public surface. They are wrapped into + `Lua.RuntimeException` before crossing any API boundary, so the whole public + exception surface is just `Lua.RuntimeException` (runtime failures) and + `Lua.CompilerException` (compile-time failures). To discriminate a runtime + failure, read the wrapper's new `:kind` field + (`:error | :type | :argument | :assertion | :internal`); the underlying VM + struct remains available on `:original`. - The public `{:error, _}`-returning APIs now hand back the exception struct uniformly, instead of a pre-rendered message string, so callers own rendering (`Exception.message/1`) and can pattern-match the concrete error. - `Lua.call_function/3` returns `{:error, exception, lua}` where `exception` - is the VM struct (`Lua.VM.RuntimeError`, `Lua.VM.TypeError`, - `Lua.VM.ArgumentError`, …). The raised Lua value (`error(42)`, a table, - `nil`, `false`) is preserved on the struct's `:value`, matching what - `pcall` hands back inside Lua. Code matching on a string reason should - switch to the struct (or call `Exception.message/1` on it). + is always a `Lua.RuntimeException`. The raised Lua value (`error(42)`, a + table, `nil`, `false`) is preserved on `:value`, matching what `pcall` + hands back inside Lua, with the category on `:kind` and the underlying VM + struct on `:original`. Code matching on a string reason should switch to + the exception (or call `Exception.message/1` on it). - `Lua.parse_chunk/1` returns `{:error, %Lua.CompilerException{}}` instead of `{:error, [String.t()]}`. Call `Exception.message/1` to render the full, human-readable report; the `:errors` field carries the bare, ANSI-free messages for programmatic inspection. - Lua exceptions render their message lazily at `Exception.message/1` call - time, so the `:message` **struct field** is removed from - `Lua.VM.RuntimeError`, `Lua.VM.TypeError`, and `Lua.VM.AssertionError`, and - is `nil` on the `Lua.RuntimeException` wrapper when it wraps a VM exception. - Read the message through `Exception.message/1` (the idiomatic accessor) - rather than the `e.message` field. Likewise `Lua.CompilerException`'s + time, so the `:message` **struct field** is `nil` on the + `Lua.RuntimeException` wrapper when it wraps an internal VM exception (and on + the internal VM structs themselves). Read the message through + `Exception.message/1` (the idiomatic accessor) rather than the `e.message` + field. Likewise `Lua.CompilerException`'s `:errors` field now holds bare, ANSI-free messages rather than the fully formatted (and previously ANSI-colored) diagnostics — the rich report moved to `Exception.message/1`. This is what lets the ANSI gate hold at output time diff --git a/lib/lua.ex b/lib/lua.ex index 057b9d2..0c72526 100644 --- a/lib/lua.ex +++ b/lib/lua.ex @@ -708,8 +708,9 @@ defmodule Lua do ## Errors When the function raises, `call_function/3` returns `{:error, exception, - lua}`, where `exception` is the Lua VM exception struct — a - `Lua.VM.RuntimeError`, `Lua.VM.TypeError`, `Lua.VM.ArgumentError`, etc. Call + lua}`, where `exception` is a `Lua.RuntimeException`. Read `:kind` + (`:error | :type | :argument | :assertion | :internal`) to discriminate the + failure, and `:original` to reach the internal VM exception. Call `Exception.message/1` yourself to render it; unlike the raising `call_function!/3`, the boundary does *not* pre-render it (no ANSI, no `at ::` header, no `Suggestion:` block). @@ -719,8 +720,8 @@ defmodule Lua do iex> Exception.message(exception) =~ "boom" true - The raised Lua value is preserved on the struct — exactly what `pcall` hands - back inside Lua. `error(42)` carries `42` on the `RuntimeError`'s `:value`: + The raised Lua value is preserved on `:value` — exactly what `pcall` hands + back inside Lua. `error(42)` carries `42`: iex> {[ref], lua} = Lua.eval!(Lua.new(), "return function() error(42) end", decode: false) iex> {:error, exception, _lua} = Lua.call_function(lua, ref, []) @@ -735,25 +736,36 @@ defmodule Lua do end # `call_function/3` is a programmatic protected-call boundary. Its `:error` - # payload is the VM exception struct itself, carried out verbatim so callers - # own the rendering (`Exception.message/1`) and can pattern-match on the - # concrete error struct. In-Lua `pcall`/`xpcall` still project the raw §6.1 - # value via `ProtectedCall.error_value/1`; this boundary deliberately does - # not. The raising variant `call_function!/3` re-raises the same exception - # through `Lua.RuntimeException` to get the rich `ErrorFormatter` render. + # payload is always the public `Lua.RuntimeException`: the internal VM + # exception is wrapped so callers pattern-match one type, read the raised Lua + # value off `:value` and the category off `:kind`, and render with + # `Exception.message/1`. In-Lua `pcall`/`xpcall` still project the raw §6.1 + # value via `ProtectedCall.error_value/1` inside the VM; they never reach + # this boundary. defp finish_call(%__MODULE__{} = lua, {:ok, results, new_state}) do {:ok, results, %{lua | state: new_state}} end - defp finish_call(%__MODULE__{} = lua, {:error, e, new_state}) when is_exception(e) do + # Already a host-facing exception (a nested eval, or a compiler error from + # `load`) — carry it out unchanged rather than re-wrapping. + defp finish_call(%__MODULE__{} = lua, {:error, %Lua.RuntimeException{} = e, new_state}) do + {:error, e, %{lua | state: new_state}} + end + + defp finish_call(%__MODULE__{} = lua, {:error, %Lua.CompilerException{} = e, new_state}) do {:error, e, %{lua | state: new_state}} end + defp finish_call(%__MODULE__{} = lua, {:error, e, new_state}) when is_exception(e) do + {:error, Lua.RuntimeException.exception(e), %{lua | state: new_state}} + end + # Defensive: every `resolve_and_call/3` error path yields an exception today. - # If a bare Lua value ever reaches here, wrap it so the boundary's contract - # stays uniform — the `:error` payload is always an exception struct. + # If a bare Lua value ever reaches here, wrap it as an `error()` runtime + # exception so the boundary's contract stays uniform. defp finish_call(%__MODULE__{} = lua, {:error, reason, new_state}) do - {:error, %RuntimeError{value: reason}, %{lua | state: new_state}} + exception = Lua.RuntimeException.exception(RuntimeError.exception(value: reason)) + {:error, exception, %{lua | state: new_state}} end # Resolves `name`/`func` to a callable and invokes it under protection. diff --git a/lib/lua/runtime_exception.ex b/lib/lua/runtime_exception.ex index 399b327..d7a421c 100644 --- a/lib/lua/runtime_exception.ex +++ b/lib/lua/runtime_exception.ex @@ -14,18 +14,26 @@ defmodule Lua.RuntimeException do * `:message` — pre-rendered string for terminal-independent errors, or `nil` when rendered lazily from `:original` * `:original` — the underlying VM error term + * `:kind` — the category of failure: `:error` (an explicit `error()` + call), `:type`, `:argument`, `:assertion`, or `:internal`; `nil` for + host-side API errors that don't originate from a Lua value + * `:value` — the raised Lua value (per §6.1), as `pcall`/`xpcall` + would hand it back; `nil` when there is no Lua-side value * `:state` — the internal VM state at the point of failure * `:line` — line number where the error was raised * `:source` — source name (filename or the default ``) * `:call_stack` — list of Lua frames at failure """ alias Lua.Util + alias Lua.VM.ProtectedCall @runtime_prefix "Lua runtime error: " + @type kind :: :error | :type | :argument | :assertion | :internal + @type t :: %__MODULE__{} - defexception [:message, :original, :state, :line, :source, :call_stack] + defexception [:message, :original, :kind, :value, :state, :line, :source, :call_stack] @impl true def exception({:lua_error, error, _state}) do @@ -57,9 +65,12 @@ defmodule Lua.RuntimeException do def exception(error) do # Copy structured fields off VM exceptions (TypeError, RuntimeError, - # AssertionError) so consumers can pattern-match on `:line` / `:source` - # without having to re-parse the message string. + # AssertionError, ArgumentError, InternalError) so consumers can + # pattern-match on `:kind` / `:value` / `:line` / `:source` without having + # to re-parse the message string. `:kind`/`:value` come back `nil` for + # arbitrary Elixir exceptions, which carry no Lua-side value. {line, source, call_stack} = extract_context(error) + kind = kind(error) # `:message` is left nil so `message/1` renders the wrapped error lazily. # The inner VM exceptions gate ANSI on `IO.ANSI.enabled?/0` at render time @@ -67,6 +78,8 @@ defmodule Lua.RuntimeException do # codes from the TTY-attached VM process and leak them into log sinks. %__MODULE__{ original: error, + kind: kind, + value: kind && ProtectedCall.error_value(error), line: line, source: source, call_stack: call_stack @@ -99,6 +112,15 @@ defmodule Lua.RuntimeException do defp extract_context(_), do: {nil, nil, nil} + # Classify a wrapped VM exception into its user-facing category. Arbitrary + # Elixir exceptions (no Lua-side value) return `nil`. + defp kind(%Lua.VM.RuntimeError{}), do: :error + defp kind(%Lua.VM.TypeError{}), do: :type + defp kind(%Lua.VM.ArgumentError{}), do: :argument + defp kind(%Lua.VM.AssertionError{}), do: :assertion + defp kind(%Lua.VM.InternalError{}), do: :internal + defp kind(_), do: nil + defp format_function([], function), do: "#{function}()" defp format_function(scope, function) do diff --git a/lib/lua/vm/argument_error.ex b/lib/lua/vm/argument_error.ex index 3325073..12efb0b 100644 --- a/lib/lua/vm/argument_error.ex +++ b/lib/lua/vm/argument_error.ex @@ -1,45 +1,30 @@ defmodule Lua.VM.ArgumentError do - @moduledoc """ - Raised when a function is called with invalid arguments. - - This exception provides standardized error messages for bad arguments across - all Lua standard library functions. - - ## Fields - - - `:function_name` - The fully qualified function name (e.g., "string.rep") - - `:arg_num` - The argument number (1-based) - - `:expected` - What type or value was expected (e.g., "number", "string") - - `:got` - What was actually received (optional, e.g., "nil", "boolean") - - `:details` - Additional details about the error (optional) - - `:line` - Source line where the call originated (auto-populated from - `Lua.VM.Executor.current_position/0` when not given explicitly) - - `:source` - Source name where the call originated (auto-populated) - - `:call_stack` - Call stack frames at the raise site (default `[]`) - - ## Examples - - # Basic type error — line/source filled in automatically when raised - # from inside a Lua execution. - raise ArgumentError, - function_name: "string.rep", - arg_num: 2, - expected: "number" - - # With actual type received - raise ArgumentError, - function_name: "string.sub", - arg_num: 2, - expected: "number", - got: "string" - - # With additional details - raise ArgumentError, - function_name: "string.char", - arg_num: 1, - expected: "number", - details: "value out of range" - """ + @moduledoc false + + # Internal VM exception. Never surfaces to the host directly — it is wrapped + # into the public `Lua.RuntimeException` (kind: `:argument`) at the API + # boundary, which projects `raw_message/1` onto `:value`. + # + # Raised when a function is called with invalid arguments; provides + # standardized "bad argument #N to 'F'" messages across all Lua standard + # library functions. + # + # Fields: + # + # - `:function_name` - fully qualified function name (e.g., "string.rep") + # - `:arg_num` - the argument number (1-based) + # - `:expected` - what type or value was expected ("number", "string", …) + # - `:got` - what was actually received (optional, "nil", "boolean", …) + # - `:details` - additional details about the error (optional) + # - `:line` / `:source` - call origin, auto-populated from + # `Lua.VM.Executor.current_position/0` when not given explicitly + # - `:call_stack` - call stack frames at the raise site (default `[]`) + # + # Examples: + # + # raise ArgumentError, function_name: "string.rep", arg_num: 2, expected: "number" + # raise ArgumentError, function_name: "string.sub", arg_num: 2, expected: "number", got: "string" + # raise ArgumentError, function_name: "string.char", arg_num: 1, expected: "number", details: "value out of range" alias Lua.VM.RuntimeError diff --git a/lib/lua/vm/assertion_error.ex b/lib/lua/vm/assertion_error.ex index 0ce0356..a89f534 100644 --- a/lib/lua/vm/assertion_error.ex +++ b/lib/lua/vm/assertion_error.ex @@ -1,11 +1,14 @@ defmodule Lua.VM.AssertionError do - @moduledoc """ - Raised by the Lua `assert()` function when the condition is falsy. + @moduledoc false - When raised without explicit `:line` / `:source` opts, `exception/1` - populates them from the calling Lua source position via - `Lua.VM.Executor.current_position/0`. - """ + # Internal VM exception. Never surfaces to the host directly — it is wrapped + # into the public `Lua.RuntimeException` (kind: `:assertion`) at the API + # boundary. + # + # Raised by the Lua `assert()` function when the condition is falsy. When + # raised without explicit `:line` / `:source` opts, `exception/1` populates + # them from the calling Lua source position via + # `Lua.VM.Executor.current_position/0`. alias Lua.VM.ErrorFormatter diff --git a/lib/lua/vm/internal_error.ex b/lib/lua/vm/internal_error.ex index 0702d8b..7a1dc7a 100644 --- a/lib/lua/vm/internal_error.ex +++ b/lib/lua/vm/internal_error.ex @@ -1,8 +1,12 @@ defmodule Lua.VM.InternalError do - @moduledoc """ - Raised for internal VM errors: bad native function returns, - unimplemented instructions, and other invariant violations. - """ + @moduledoc false + + # Internal VM exception. Never surfaces to the host directly — it is wrapped + # into the public `Lua.RuntimeException` (kind: `:internal`) at the API + # boundary. + # + # Raised for internal VM errors: bad native function returns, unimplemented + # instructions, and other invariant violations. defexception [:value, :message] diff --git a/lib/lua/vm/runtime_error.ex b/lib/lua/vm/runtime_error.ex index 7168429..9d4edda 100644 --- a/lib/lua/vm/runtime_error.ex +++ b/lib/lua/vm/runtime_error.ex @@ -1,17 +1,18 @@ defmodule Lua.VM.RuntimeError do - @moduledoc """ - Raised by the Lua `error()` function. + @moduledoc false - Carries the original Lua error value, which may be any Lua type (string, - number, table reference, etc.). - - When raised without explicit `:line` / `:source` opts (e.g. from a stdlib - bad-argument check), `exception/1` populates them from the calling Lua - source position via `Lua.VM.Executor.current_position/0`. That position - is stashed in the process dictionary at every native-call boundary, so - any raise site reachable from a Lua execution inherits the correct - attribution automatically. - """ + # Internal VM exception. Never surfaces to the host directly — it is wrapped + # into the public `Lua.RuntimeException` (kind: `:error`) at the API boundary. + # + # Raised by the Lua `error()` function. Carries the original Lua error value, + # which may be any Lua type (string, number, table reference, etc.). + # + # When raised without explicit `:line` / `:source` opts (e.g. from a stdlib + # bad-argument check), `exception/1` populates them from the calling Lua + # source position via `Lua.VM.Executor.current_position/0`. That position is + # stashed in the process dictionary at every native-call boundary, so any + # raise site reachable from a Lua execution inherits the correct attribution + # automatically. alias Lua.VM.ErrorFormatter alias Lua.VM.Value diff --git a/lib/lua/vm/type_error.ex b/lib/lua/vm/type_error.ex index c496c08..18774e6 100644 --- a/lib/lua/vm/type_error.ex +++ b/lib/lua/vm/type_error.ex @@ -1,16 +1,18 @@ defmodule Lua.VM.TypeError do - @moduledoc """ - Raised when a Lua operation is applied to a value of the wrong type. + @moduledoc false - Examples: calling a nil value, calling a number, indexing a boolean. - - When raised without explicit `:line` / `:source` opts (e.g. from a stdlib - type check), `exception/1` populates them from the calling Lua source - position via `Lua.VM.Executor.current_position/0`. That position is - stashed in the process dictionary at every native-call boundary, so - any raise site reachable from a Lua execution inherits the correct - attribution automatically. - """ + # Internal VM exception. Never surfaces to the host directly — it is wrapped + # into the public `Lua.RuntimeException` (kind: `:type`) at the API boundary. + # + # Raised when a Lua operation is applied to a value of the wrong type. + # Examples: calling a nil value, calling a number, indexing a boolean. + # + # When raised without explicit `:line` / `:source` opts (e.g. from a stdlib + # type check), `exception/1` populates them from the calling Lua source + # position via `Lua.VM.Executor.current_position/0`. That position is stashed + # in the process dictionary at every native-call boundary, so any raise site + # reachable from a Lua execution inherits the correct attribution + # automatically. alias Lua.VM.ErrorFormatter diff --git a/mix.exs b/mix.exs index 33cc32a..3a14706 100644 --- a/mix.exs +++ b/mix.exs @@ -2,9 +2,6 @@ defmodule Lua.MixProject do use Mix.Project alias Lua.Parser.Error - alias Lua.VM.ArgumentError - alias Lua.VM.RuntimeError - alias Lua.VM.TypeError alias Mix.Tasks.Lua.Eval @url "https://github.com/tv-labs/lua" @@ -20,9 +17,6 @@ defmodule Lua.MixProject do Lua.Chunk, Lua.RuntimeException, Lua.CompilerException, - RuntimeError, - TypeError, - ArgumentError, Error, Eval ] @@ -59,22 +53,25 @@ defmodule Lua.MixProject do source_ref: "v#{@version}", # Render only the curated public surface; keep internals in source/IEx. filter_modules: fn module, _meta -> module in @public_modules end, - # The public VM exception structs' moduledocs name internal plumbing - # (`Lua.VM.Executor`, `Lua.VM.ErrorFormatter`) that stays filtered. - # Render those references as plain code instead of autolinking to - # filtered pages, which errors under `--warnings-as-errors`. + # Docs (CHANGELOG, moduledocs) name internal plumbing that stays + # filtered — the VM exception structs behind `Lua.RuntimeException` and + # their helpers. Render those references as plain code instead of + # autolinking to filtered pages, which errors under + # `--warnings-as-errors`. skip_code_autolink_to: [ "Lua.VM.Executor.current_position/0", - "Lua.VM.ErrorFormatter.to_map/3" + "Lua.VM.ErrorFormatter.to_map/3", + "Lua.VM.RuntimeError", + "Lua.VM.TypeError", + "Lua.VM.ArgumentError", + "Lua.VM.AssertionError", + "Lua.VM.InternalError" ], groups_for_modules: [ Core: [Lua, Lua.API, Lua.Table, Lua.Chunk], Errors: [ Lua.RuntimeException, Lua.CompilerException, - RuntimeError, - TypeError, - ArgumentError, Error ], "Mix Tasks": [Eval] diff --git a/test/lua/call_function_error_value_test.exs b/test/lua/call_function_error_value_test.exs index da0035f..4a22ac5 100644 --- a/test/lua/call_function_error_value_test.exs +++ b/test/lua/call_function_error_value_test.exs @@ -1,18 +1,22 @@ defmodule Lua.CallFunctionErrorValueTest do @moduledoc """ Pins the `Lua.call_function/3` protected-call boundary: its `{:error, - exception, _}` payload is the VM exception struct itself — a - `Lua.VM.RuntimeError`, `Lua.VM.TypeError`, or `Lua.VM.ArgumentError` — - carried out verbatim. The boundary does no rendering: callers own that by - calling `Exception.message/1`, and can pattern-match on the concrete struct - and its structured fields. The raw Lua value handed to `error()` (§6.1) is - preserved on the struct's `:value`, so pcall-parity data survives. - - The raising variant `call_function!/3` re-raises the same exception through - `Lua.RuntimeException`, keeping the rich `ErrorFormatter` render. + exception, _}` payload is always the public `Lua.RuntimeException`. The + internal VM exception is wrapped so callers pattern-match one type and read: + + * `:kind` — the category (`:error`, `:type`, `:argument`, …) + * `:value` — the Lua-facing error value exactly as `pcall`/`xpcall` + would hand it back (§6.1) + * `:original` — the underlying VM exception, for deep inspection of + structured fields (`error_kind`, `function_name`, …) + + The boundary does no rendering: callers own that by calling + `Exception.message/1`. The raising variant `call_function!/3` re-raises the + same `Lua.RuntimeException`, keeping the rich `ErrorFormatter` render. """ use ExUnit.Case, async: true + alias Lua.RuntimeException alias Lua.VM.ArgumentError alias Lua.VM.RuntimeError alias Lua.VM.TypeError @@ -22,40 +26,48 @@ defmodule Lua.CallFunctionErrorValueTest do {ref, lua} end - describe "call_function/3 returns the VM exception struct" do - test "error('boom') comes back as a RuntimeError carrying the raised value" do + describe "call_function/3 returns a Lua.RuntimeException" do + test "error('boom') comes back with kind :error and the §6.1 value" do {ref, lua} = fun!("function() error('boom') end") - assert {:error, %RuntimeError{value: "boom"} = error, %Lua{}} = + assert {:error, %RuntimeException{kind: :error, value: value} = error, %Lua{}} = Lua.call_function(lua, ref, []) + # String errors are position-prefixed by `error()`, matching pcall. + assert value =~ "boom" + assert %RuntimeError{value: "boom"} = error.original assert Exception.message(error) =~ "boom" end - test "a call-nil failure comes back as a TypeError struct" do + test "a call-nil failure comes back with kind :type" do {ref, lua} = fun!("function() local f = nil; f() end") - assert {:error, %TypeError{error_kind: :call_nil} = error, %Lua{}} = + assert {:error, %RuntimeException{kind: :type} = error, %Lua{}} = Lua.call_function(lua, ref, []) + assert %TypeError{error_kind: :call_nil} = error.original assert Exception.message(error) =~ "attempt to call a nil value" end - test "an index-nil failure comes back as a TypeError struct" do + test "an index-nil failure comes back with kind :type" do {ref, lua} = fun!("function() local t = nil; return t.x end") - assert {:error, %TypeError{error_kind: :index_non_table} = error, %Lua{}} = + assert {:error, %RuntimeException{kind: :type} = error, %Lua{}} = Lua.call_function(lua, ref, []) + assert %TypeError{error_kind: :index_non_table} = error.original assert Exception.message(error) =~ "attempt to index" end - test "a stdlib bad-argument failure comes back as an ArgumentError struct" do + test "a stdlib bad-argument failure comes back with kind :argument" do {ref, lua} = fun!(~S|function() for i in pairs("asdf") do end end|) - assert {:error, %ArgumentError{function_name: "pairs", arg_num: 1} = error, %Lua{}} = + assert {:error, %RuntimeException{kind: :argument, value: value} = error, %Lua{}} = Lua.call_function(lua, ref, []) + assert %ArgumentError{function_name: "pairs", arg_num: 1} = error.original + assert value =~ "bad argument #1 to 'pairs'" + message = Exception.message(error) assert message =~ "bad argument #1 to 'pairs'" assert message =~ "table expected" @@ -67,9 +79,11 @@ defmodule Lua.CallFunctionErrorValueTest do test "an undefined name names what was looked up, not the resolved nil" do {_, lua} = Lua.eval!(Lua.new(), "function foo() return 1 end") - assert {:error, %TypeError{error_kind: :call_nil} = error, %Lua{}} = + assert {:error, %RuntimeException{kind: :type} = error, %Lua{}} = Lua.call_function(lua, [:bar], []) + assert %TypeError{error_kind: :call_nil} = error.original + # Regression: previously reported the resolved value ("undefined # function 'nil'"). It must name the requested global instead. assert Exception.message(error) =~ "attempt to call a nil value (global 'bar')" @@ -78,7 +92,8 @@ defmodule Lua.CallFunctionErrorValueTest do test "an existing non-function value reports its type, not 'undefined'" do {_, lua} = Lua.eval!(Lua.new(), "x = 5") - assert {:error, %TypeError{} = error, %Lua{}} = Lua.call_function(lua, [:x], []) + assert {:error, %RuntimeException{kind: :type} = error, %Lua{}} = + Lua.call_function(lua, [:x], []) assert Exception.message(error) =~ "attempt to call a number value (global 'x')" end @@ -86,7 +101,8 @@ defmodule Lua.CallFunctionErrorValueTest do test "a nested path attributes to the final field" do {_, lua} = Lua.eval!(Lua.new(), "t = {}") - assert {:error, %TypeError{} = error, %Lua{}} = Lua.call_function(lua, [:t, :missing], []) + assert {:error, %RuntimeException{kind: :type} = error, %Lua{}} = + Lua.call_function(lua, [:t, :missing], []) assert Exception.message(error) =~ "attempt to call a nil value (field 'missing')" end @@ -97,7 +113,7 @@ defmodule Lua.CallFunctionErrorValueTest do {_, lua} = Lua.eval!(Lua.new(), "function foo() return 1 end") error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, [:bar], []) end @@ -113,7 +129,7 @@ defmodule Lua.CallFunctionErrorValueTest do {_, lua} = Lua.eval!(Lua.new(), "function foo() return 1 end") error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, [:bar], []) end @@ -121,30 +137,35 @@ defmodule Lua.CallFunctionErrorValueTest do end end - describe "call_function/3 preserves the raised Lua value on the struct (pcall parity)" do + describe "call_function/3 preserves the raised Lua value on :value (pcall parity)" do test "a table error object is preserved on :value" do {ref, lua} = fun!("function() error({code = 1}) end") - assert {:error, %RuntimeError{value: value}, %Lua{} = lua} = Lua.call_function(lua, ref, []) + assert {:error, %RuntimeException{kind: :error, value: value}, %Lua{} = lua} = + Lua.call_function(lua, ref, []) + assert Lua.decode!(lua, value) == [{"code", 1}] end test "a number error object is preserved on :value" do {ref, lua} = fun!("function() error(42) end") - assert {:error, %RuntimeError{value: 42}, %Lua{}} = Lua.call_function(lua, ref, []) + assert {:error, %RuntimeException{kind: :error, value: 42}, %Lua{}} = + Lua.call_function(lua, ref, []) end test "a nil error object is preserved on :value" do {ref, lua} = fun!("function() error(nil) end") - assert {:error, %RuntimeError{value: nil}, %Lua{}} = Lua.call_function(lua, ref, []) + assert {:error, %RuntimeException{kind: :error, value: nil}, %Lua{}} = + Lua.call_function(lua, ref, []) end test "a false error object is preserved on :value" do {ref, lua} = fun!("function() error(false) end") - assert {:error, %RuntimeError{value: false}, %Lua{}} = Lua.call_function(lua, ref, []) + assert {:error, %RuntimeException{kind: :error, value: false}, %Lua{}} = + Lua.call_function(lua, ref, []) end end @@ -153,7 +174,7 @@ defmodule Lua.CallFunctionErrorValueTest do {ref, lua} = fun!("function() error('boom') end") error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, ref, []) end @@ -171,7 +192,7 @@ defmodule Lua.CallFunctionErrorValueTest do {ref, lua} = fun!(~S|function() pairs("asdf") end|) error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, ref, []) end @@ -187,7 +208,7 @@ defmodule Lua.CallFunctionErrorValueTest do {ref, lua} = fun!("function() error('boom') end") error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, ref, []) end @@ -198,7 +219,7 @@ defmodule Lua.CallFunctionErrorValueTest do {ref, lua} = fun!("function() local t = nil; return t.x end") error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, ref, []) end @@ -217,7 +238,7 @@ defmodule Lua.CallFunctionErrorValueTest do {_, lua} = Lua.eval!(Lua.new(), code, source: "regression.lua") error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, [:foo], []) end @@ -240,7 +261,7 @@ defmodule Lua.CallFunctionErrorValueTest do {_, lua} = Lua.eval!(Lua.new(), code, source: "regression.lua") error = - assert_raise Lua.RuntimeException, fn -> + assert_raise RuntimeException, fn -> Lua.call_function!(lua, [:foo], []) end diff --git a/test/lua/runtime_exception_test.exs b/test/lua/runtime_exception_test.exs index bb739f3..0a48fdf 100644 --- a/test/lua/runtime_exception_test.exs +++ b/test/lua/runtime_exception_test.exs @@ -319,6 +319,87 @@ defmodule Lua.RuntimeExceptionTest do end end + describe "kind/value projection when wrapping a VM exception" do + test "wrapping a RuntimeError yields kind :error and the raised value" do + inner = Lua.VM.RuntimeError.exception(value: "boom", source: "t.lua", line: 3) + + exception = RuntimeException.exception(inner) + + assert exception.kind == :error + assert exception.value == "boom" + assert exception.original == inner + # Structured context is copied onto the wrapper for pattern-matching. + assert exception.line == 3 + assert exception.source == "t.lua" + end + + test "wrapping a RuntimeError projects the §6.1 lua_value when present" do + inner = Lua.VM.RuntimeError.exception(value: "boom", lua_value: "t.lua:3: boom") + + exception = RuntimeException.exception(inner) + + # pcall parity: string errors carry the position-prefixed view. + assert exception.value == "t.lua:3: boom" + end + + test "wrapping a RuntimeError preserves a non-string Lua value verbatim" do + inner = Lua.VM.RuntimeError.exception(value: 42) + + exception = RuntimeException.exception(inner) + + assert exception.kind == :error + assert exception.value == 42 + end + + test "wrapping a TypeError yields kind :type" do + inner = Lua.VM.TypeError.exception(value: "attempt to index a nil value") + + exception = RuntimeException.exception(inner) + + assert exception.kind == :type + assert exception.value == "attempt to index a nil value" + end + + test "wrapping an ArgumentError yields kind :argument and the raw bad-argument string" do + inner = + Lua.VM.ArgumentError.exception( + function_name: "string.rep", + arg_num: 2, + expected: "number" + ) + + exception = RuntimeException.exception(inner) + + assert exception.kind == :argument + assert exception.value == "bad argument #2 to 'string.rep' (number expected)" + end + + test "wrapping an AssertionError yields kind :assertion" do + inner = Lua.VM.AssertionError.exception(value: "nope") + + exception = RuntimeException.exception(inner) + + assert exception.kind == :assertion + assert exception.value == "nope" + end + + test "wrapping an InternalError yields kind :internal" do + inner = Lua.VM.InternalError.exception(value: "invariant violated") + + exception = RuntimeException.exception(inner) + + assert exception.kind == :internal + assert exception.value == "invariant violated" + end + + test "wrapping an arbitrary Elixir exception leaves kind and value nil" do + exception = RuntimeException.exception(KeyError.exception(key: :missing, term: %{})) + + assert exception.kind == nil + assert exception.value == nil + end + end + describe "format_function/2 (private function tested via keyword list exception)" do test "formats function with empty scope" do exception = diff --git a/test/lua/vm/pcall_state_preservation_test.exs b/test/lua/vm/pcall_state_preservation_test.exs index 8649c1c..c174e7f 100644 --- a/test/lua/vm/pcall_state_preservation_test.exs +++ b/test/lua/vm/pcall_state_preservation_test.exs @@ -521,7 +521,9 @@ defmodule Lua.VM.PcallStatePreservationTest do end """) - assert {:error, %Lua.VM.RuntimeError{} = error, lua} = Lua.call_function(lua, [:f], []) + assert {:error, %Lua.RuntimeException{kind: :error} = error, lua} = + Lua.call_function(lua, [:f], []) + assert Exception.message(error) =~ "boom" assert Lua.get!(lua, [:x]) == 2 end