test "get with 2 arguments returns value or nil" do
env = Env.initial()
data = %{name: "Alice", age: 30}
# Key exists
call_ast = {:call, {:var, :get}, [{:var, :data}, {:keyword, :name}]}
assert {:ok, "Alice", %{}} = Eval.eval(call_ast, %{}, %{}, Map.merge(env, %{data: data}), &dummy_tool/2)
# Key missing
call_ast = {:call, {:var, :get}, [{:var, :data}, {:keyword, :missing}]}
assert {:ok, nil, %{}} = Eval.eval(call_ast, %{}, %{}, Map.merge(env, %{data: data}), &dummy_tool/2)
end
test "get with 3 arguments uses default for missing key" do
env = Env.initial()
data = %{name: "Alice"}
# Key exists - returns value, not default
call_ast = {:call, {:var, :get}, [{:var, :data}, {:keyword, :name}, {:string, "default"}]}
assert {:ok, "Alice", %{}} = Eval.eval(call_ast, %{}, %{}, Map.merge(env, %{data: data}), &dummy_tool/2)
# Key missing - returns default
call_ast = {:call, {:var, :get}, [{:var, :data}, {:keyword, :missing}, {:string, "not found"}]}
assert {:ok, "not found", %{}} = Eval.eval(call_ast, %{}, %{}, Map.merge(env, %{data: data}), &dummy_tool/2)
end
test "get-in with 3 arguments uses default for missing path" do
env = Env.initial()
data = %{user: %{name: "Alice"}}
# Path exists
call_ast = {:call, {:var, :"get-in"}, [{:var, :data}, {:vector, [{:keyword, :user}, {:keyword, :name}]}, {:string, "default"}]}
assert {:ok, "Alice", %{}} = Eval.eval(call_ast, %{}, %{}, Map.merge(env, %{data: data}), &dummy_tool/2)
# Path missing
call_ast = {:call, {:var, :"get-in"}, [{:var, :data}, {:vector, [{:keyword, :user}, {:keyword, :age}]}, 0]}
assert {:ok, 0, %{}} = Eval.eval(call_ast, %{}, %{}, Map.merge(env, %{data: data}), &dummy_tool/2)
end
Context
From PR #160 review: The
{:multi_arity, tuple()}pattern was successfully implemented forsort-byto support both 2-arity and 3-arity forms.Problem
Runtime.get/3andRuntime.get_in/3exist (with default values) but are only registered as 2-arity inenv.ex:{:get, {:normal, &Runtime.get/2}}{:"get-in", {:normal, &Runtime.get_in/2}}This means users cannot provide default values:
Proposed Solution
Use the same
{:multi_arity, tuple()}pattern to support optional default values:Benefits
sort-by(get m :key :default)and(get-in m [:a :b] :default)Implementation Steps
Update
lib/ptc_runner/lisp/env.ex(lines 72-73):{:get, {:normal, &Runtime.get/2}}to{:get, {:multi_arity, {&Runtime.get/2, &Runtime.get/3}}}{:"get-in", {:normal, &Runtime.get_in/2}}to{:"get-in", {:multi_arity, {&Runtime.get_in/2, &Runtime.get_in/3}}}Add unit tests in
test/ptc_runner/lisp/eval_test.exs:getwith 2 args (existing behavior, regression test)getwith 3 args (key exists → returns value)getwith 3 args (key missing → returns default)get-inwith 2 args (existing behavior, regression test)get-inwith 3 args (path exists → returns value)get-inwith 3 args (path missing → returns default)Documentation: No changes required -
architecture.mddoesn't enumerate all builtinsTest Plan
Add tests to the existing
describe "multi-arity builtins"block ineval_test.exs:Edge Cases
(get nil :key :default)→ should return:default(existingget/3handlesnilmaps)(get-in {:a nil} [:a] :default)→ should returnnil(value exists), not:defaultget_in/3implementation returns default if value isnil. This may need fixing to match Clojure semantics.Related
{:multi_arity, tuple()}pattern forsort-by