diff --git a/documentation/dsls/DSL-AshLua.EvalActions.md.license b/documentation/dsls/DSL-AshLua.EvalActions.md.license new file mode 100644 index 0000000..ead952d --- /dev/null +++ b/documentation/dsls/DSL-AshLua.EvalActions.md.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 ash_lua contributors + +SPDX-License-Identifier: MIT diff --git a/lib/ash_lua/encoder.ex b/lib/ash_lua/encoder.ex index 01b46b3..8401b97 100644 --- a/lib/ash_lua/encoder.ex +++ b/lib/ash_lua/encoder.ex @@ -164,17 +164,21 @@ defmodule AshLua.Encoder do end end - # Luerl reference records — Lua functions / userdata / raw table refs that + # Lua VM reference records — Lua functions / userdata / raw table refs that # decoding leaves in place because there's no Elixir equivalent. They reach # us when a script returns a table containing methods (e.g. `return loop.item`) # or returns a function directly (e.g. `return print`). Render them as an # opaque marker so the consumer knows the slot is non-data, instead of # crashing Jason downstream. + def encode_result({:native_func, _}), do: %{"opaque" => "function"} + def encode_result({:lua_closure, _, _}), do: %{"opaque" => "function"} + def encode_result({:compiled_closure, _, _}), do: %{"opaque" => "function"} def encode_result({:funref, _, _}), do: %{"opaque" => "function"} def encode_result({:erl_func, _}), do: %{"opaque" => "function"} def encode_result({:erl_mfa, _, _, _}), do: %{"opaque" => "function"} def encode_result({:tref, _}), do: %{"opaque" => "table_reference"} def encode_result({:usdref, _}), do: %{"opaque" => "userdata"} + def encode_result({:udref, _}), do: %{"opaque" => "userdata"} # Luerl also returns the decoded shape `{module, function, arity_or_undefined}` # for built-in callables like `print` (module=:luerl_lib_basic). def encode_result({m, f, a}) @@ -209,6 +213,10 @@ defmodule AshLua.Encoder do # `:display` mode it surfaces as the opaque "forbidden" marker. Must precede # the generic `%_struct{}` clause so the struct's internals never leak. def encode_result(%Ash.ForbiddenField{}), do: forbidden_value(nil) + def encode_result(%Lua.VM.Display.NativeFunc{}), do: %{"opaque" => "function"} + def encode_result(%Lua.VM.Display.Closure{}), do: %{"opaque" => "function"} + def encode_result(%Lua.VM.Display.Userdata{}), do: %{"opaque" => "userdata"} + def encode_result(%Lua.VM.Display.Table{}), do: %{"opaque" => "table_reference"} def encode_result(%_struct{} = record) do record diff --git a/lib/ash_lua/eval_actions/run/eval.ex b/lib/ash_lua/eval_actions/run/eval.ex index 3fe003f..f38a29d 100644 --- a/lib/ash_lua/eval_actions/run/eval.ex +++ b/lib/ash_lua/eval_actions/run/eval.ex @@ -71,28 +71,29 @@ defmodule AshLua.EvalActions.Run.Eval do # Detect that case and decode the original error table back out so the # action's `error` slot carries the same structured shape the script would # have seen on the unwrapped path. - defp extract_structured_error(%Lua.RuntimeException{original: original, state: state}) - when not is_nil(state) do - original - |> raised_value() - |> case do - nil -> nil - value -> value |> safe_decode(state) |> normalize_error_table() + defp extract_structured_error(%Lua.RuntimeException{original: original, state: state}) do + case {raised_value(original), lua_state(original, state)} do + {nil, _state} -> nil + {_value, nil} -> nil + {value, lua_state} -> value |> safe_decode(lua_state) |> normalize_error_table() end end defp extract_structured_error(_), do: nil - # Pull the actual raised Lua value out of Luerl's error tagging. `assert(x, - # err)` raises `{:assert_error, err}`; `error(err)` raises `{:error_call, - # [err]}`. Anything else (`{:illegal_index, ...}`, etc.) isn't an - # ash_lua-shaped error and we let the default rescue formatter handle it. + # Pull the actual raised Lua value out of the VM error shape. Older Luerl + # paths raise `{:assert_error, err}` / `{:error_call, [err]}`; the native VM + # raises error structs with the Lua value in `:value`. defp raised_value({:assert_error, value}), do: value defp raised_value({:error_call, [value | _]}), do: value + defp raised_value(error) when is_struct(error), do: Map.get(error, :value) defp raised_value(_), do: nil + defp lua_state(original, nil) when is_struct(original), do: Map.get(original, :state) + defp lua_state(_original, state), do: state + defp safe_decode(value, state) do - :luerl.decode(value, state) + Lua.decode!(%Lua{state: state}, value) rescue _ -> nil end diff --git a/lib/ash_lua/runtime.ex b/lib/ash_lua/runtime.ex index 2c32c5f..93baad4 100644 --- a/lib/ash_lua/runtime.ex +++ b/lib/ash_lua/runtime.ex @@ -130,10 +130,14 @@ defmodule AshLua.Runtime do _ -> "" end + defp format_print_arg(_state, {:native_func, _}), do: "" + defp format_print_arg(_state, {:lua_closure, _, _}), do: "" + defp format_print_arg(_state, {:compiled_closure, _, _}), do: "" defp format_print_arg(_state, {:funref, _, _}), do: "" defp format_print_arg(_state, {:erl_func, _}), do: "" defp format_print_arg(_state, {:erl_mfa, _, _, _}), do: "" defp format_print_arg(_state, {:usdref, _}), do: "" + defp format_print_arg(_state, {:udref, _}), do: "" defp format_print_arg(_state, value), do: inspect(value) defp append_print(state, line) do @@ -225,12 +229,10 @@ defmodule AshLua.Runtime do end # Build a full error envelope (same shape as a real action failure) and - # hand it back as a Lua-side error. `Lua.set!/3`'s wrapper sees the - # `{:error, tref, lua}` return and calls `:luerl_lib.lua_error/2`, which - # throws inside the Lua VM. The outer `Lua.call_function/3` in - # `run_transaction/3` then catches that as `{:error, tref, state}`, and - # our message-channel stash carries the tref out to the transaction - # handler — which decodes it and surfaces the envelope as the + # hand it back as a Lua-side error. `Lua.set!/3` raises the encoded error + # inside the Lua VM; the outer `Lua.call_function/3` in `run_transaction/3` + # then catches it and our message-channel stash carries the value out to the + # transaction handler, which decodes it and surfaces the envelope as the # transaction's `err`. defp rollback_callback do fn args, state -> @@ -449,14 +451,25 @@ defmodule AshLua.Runtime do encode_lua_transaction_error_body(state, value) end - # `:luerl_lib.lua_error/2` (used by our `utils.transaction.rollback` - # callback) raises with the encoded value directly, so the captured error - # reason is a bare `{:tref, _}` rather than one of the wrapped shapes - # above. + # `utils.transaction.rollback` raises with the encoded value directly, so + # the captured error reason can be a bare `{:tref, _}` rather than one of + # the wrapped shapes above. defp encode_lua_transaction_error(state, {:tref, _} = tref) do encode_lua_transaction_error_body(state, tref) end + defp encode_lua_transaction_error(state, message) when is_binary(message) do + encode_lua_transaction_error_body(state, strip_lua_location(message)) + end + + defp encode_lua_transaction_error(state, error) when is_struct(error) do + case {Map.get(error, :value), Map.get(error, :state)} do + {nil, _state} -> encode_lua_transaction_error(state, :unknown) + {_value, nil} -> encode_lua_transaction_error(state, :unknown) + {value, lua_state} -> encode_lua_transaction_error_body(%Lua{state: lua_state}, value) + end + end + defp encode_lua_transaction_error(state, _other) do err = %{ "class" => "invalid", @@ -476,12 +489,7 @@ defmodule AshLua.Runtime do end defp encode_lua_transaction_error_body(state, value) do - decoded = - try do - Lua.decode!(state, value) - rescue - _ -> nil - end + decoded = decode_lua_error_value(state, value) envelope = case decoded do @@ -519,6 +527,20 @@ defmodule AshLua.Runtime do end end + defp decode_lua_error_value(state, {:tref, _} = value), do: decode_lua_ref(state, value) + defp decode_lua_error_value(state, {:udref, _} = value), do: decode_lua_ref(state, value) + defp decode_lua_error_value(_state, value), do: value + + defp decode_lua_ref(state, value) do + Lua.decode!(state, value) + rescue + _ -> nil + end + + defp strip_lua_location(message) do + String.replace(message, ~r/^.*?:\d+:\s*/s, "") + end + defp wrap_user_leaf(leaf) when is_map(leaf) do %{ "class" => "invalid", diff --git a/mix.exs b/mix.exs index 69df595..77257b2 100644 --- a/mix.exs +++ b/mix.exs @@ -115,7 +115,7 @@ defmodule AshLua.MixProject do defp deps do [ {:ash, ash_version("~> 3.25 and >= 3.25.2")}, - {:lua, "~> 0.3"}, + {:lua, "~> 1.0.0-rc"}, {:jason, "~> 1.2"}, {:igniter, "~> 0.6", optional: true}, {:spark, ">= 2.2.10"}, @@ -150,8 +150,10 @@ defmodule AshLua.MixProject do "docs", "spark.replace_doc_links" ], - "spark.formatter": "spark.formatter --extensions AshLua.Domain,AshLua.Resource,AshLua.EvalActions", - "spark.cheat_sheets": "spark.cheat_sheets --extensions AshLua.Domain,AshLua.Resource,AshLua.EvalActions" + "spark.formatter": + "spark.formatter --extensions AshLua.Domain,AshLua.Resource,AshLua.EvalActions", + "spark.cheat_sheets": + "spark.cheat_sheets --extensions AshLua.Domain,AshLua.Resource,AshLua.EvalActions" ] end end diff --git a/mix.lock b/mix.lock index 3916e22..3d2e612 100644 --- a/mix.lock +++ b/mix.lock @@ -21,8 +21,7 @@ "igniter": {:hex, :igniter, "0.8.0", "c7cab589440e5f20ff68e00f60eb094378114dab3105c0784ce8140f8dfdd2c0", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "fcd99096fde4797f7b48bebddcfc58785569acd696346a3eb385bf813f47a7cc"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, - "lua": {:hex, :lua, "0.4.0", "de0f04871fdd133cd13a0662690b4fd3ba7a73ca5857493c4665a0a4251908fe", [:mix], [{:luerl, "~> 1.5.1", [hex: :luerl, repo: "hexpm", optional: false]}], "hexpm", "648e17ab9faa1ab1a788fa58ed608366a7026d0eeaec2f311510c065817c4067"}, - "luerl": {:hex, :luerl, "1.5.1", "f6700420950fc6889137e7a0c11c4a8467dea04a8c23f707a40d83566d14e786", [:rebar3], [], "hexpm", "abf88d849baa0d5dca93b245a8688d4de2ee3d588159bb2faf51e15946509390"}, + "lua": {:hex, :lua, "1.0.0-rc.3", "349b5aa596d1b1101c1d6f8787bfc6a79c4a4927aa1be6fc2ff5ec273d116bd5", [:mix], [], "hexpm", "cb2c5d6e38cf245cadc6e3da3a1c526088c2fc0bc884c70054bb44fc60a25926"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, diff --git a/test/support/fixtures/custom_mcp_actions.ex b/test/support/fixtures/custom_mcp_actions.ex index d294691..cb36e8b 100644 --- a/test/support/fixtures/custom_mcp_actions.ex +++ b/test/support/fixtures/custom_mcp_actions.ex @@ -9,8 +9,8 @@ defmodule AshLua.Test.Posts.CustomMCPActions do extensions: [AshLua.EvalActions] eval_actions do - eval_action_name(:run) - docs_action_name(:describe) + eval_action_name :run + docs_action_name :describe resource AshLua.Test.Posts.Post, actions: [:read] end diff --git a/test/support/fixtures/forbidden_display_mcp_actions.ex b/test/support/fixtures/forbidden_display_mcp_actions.ex index 040d6f4..aaf1c00 100644 --- a/test/support/fixtures/forbidden_display_mcp_actions.ex +++ b/test/support/fixtures/forbidden_display_mcp_actions.ex @@ -9,7 +9,7 @@ defmodule AshLua.Test.Posts.ForbiddenDisplayMCPActions do extensions: [AshLua.EvalActions] eval_actions do - forbidden_fields(:display) + forbidden_fields :display resource AshLua.Test.Posts.SecretPost, actions: [:read] end end