Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions documentation/dsls/DSL-AshLua.EvalActions.md.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2026 ash_lua contributors <https://github.com/ash-project/ash_lua/graphs/contributors>

SPDX-License-Identifier: MIT
10 changes: 9 additions & 1 deletion lib/ash_lua/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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
Expand Down
25 changes: 13 additions & 12 deletions lib/ash_lua/eval_actions/run/eval.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 38 additions & 16 deletions lib/ash_lua/runtime.ex
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,14 @@ defmodule AshLua.Runtime do
_ -> "<table>"
end

defp format_print_arg(_state, {:native_func, _}), do: "<function>"
defp format_print_arg(_state, {:lua_closure, _, _}), do: "<function>"
defp format_print_arg(_state, {:compiled_closure, _, _}), do: "<function>"
defp format_print_arg(_state, {:funref, _, _}), do: "<function>"
defp format_print_arg(_state, {:erl_func, _}), do: "<function>"
defp format_print_arg(_state, {:erl_mfa, _, _, _}), do: "<function>"
defp format_print_arg(_state, {:usdref, _}), do: "<userdata>"
defp format_print_arg(_state, {:udref, _}), do: "<userdata>"
defp format_print_arg(_state, value), do: inspect(value)

defp append_print(state, line) do
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down Expand Up @@ -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
3 changes: 1 addition & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
4 changes: 2 additions & 2 deletions test/support/fixtures/custom_mcp_actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/support/fixtures/forbidden_display_mcp_actions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading