Skip to content

[From PR #160] Extend multi-arity support to get and get-in #161

@andreasronge

Description

@andreasronge

Context

From PR #160 review: The {:multi_arity, tuple()} pattern was successfully implemented for sort-by to support both 2-arity and 3-arity forms.

Problem

Runtime.get/3 and Runtime.get_in/3 exist (with default values) but are only registered as 2-arity in env.ex:

  • Line 72: {:get, {:normal, &Runtime.get/2}}
  • Line 73: {:"get-in", {:normal, &Runtime.get_in/2}}

This means users cannot provide default values:

;; Currently not supported (should be):
(get m :key :default)
(get-in m [:a :b] :default)

Proposed Solution

Use the same {:multi_arity, tuple()} pattern to support optional default values:

{:get, {:multi_arity, {&Runtime.get/2, &Runtime.get/3}}},
{:"get-in", {:multi_arity, {&Runtime.get_in/2, &Runtime.get_in/3}}},

Benefits

  • Consistent API - same pattern as sort-by
  • More Clojure-like: (get m :key :default) and (get-in m [:a :b] :default)
  • Useful for providing fallback values when keys don't exist

Implementation Steps

  1. Update lib/ptc_runner/lisp/env.ex (lines 72-73):

    • Change {:get, {:normal, &Runtime.get/2}} to {:get, {:multi_arity, {&Runtime.get/2, &Runtime.get/3}}}
    • Change {:"get-in", {:normal, &Runtime.get_in/2}} to {:"get-in", {:multi_arity, {&Runtime.get_in/2, &Runtime.get_in/3}}}
  2. Add unit tests in test/ptc_runner/lisp/eval_test.exs:

    • Test get with 2 args (existing behavior, regression test)
    • Test get with 3 args (key exists → returns value)
    • Test get with 3 args (key missing → returns default)
    • Test get-in with 2 args (existing behavior, regression test)
    • Test get-in with 3 args (path exists → returns value)
    • Test get-in with 3 args (path missing → returns default)
    • Test arity error with wrong argument count (1 arg or 4+ args)
  3. Documentation: No changes required - architecture.md doesn't enumerate all builtins

Test Plan

Add tests to the existing describe "multi-arity builtins" block in eval_test.exs:

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

Edge Cases

  • (get nil :key :default) → should return :default (existing get/3 handles nil maps)
  • (get-in {:a nil} [:a] :default) → should return nil (value exists), not :default
    • Note: Current get_in/3 implementation returns default if value is nil. This may need fixing to match Clojure semantics.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-approvedMaintainer-approved for Claude automationenhancementNew feature or requestfrom-pr-reviewIssue created from PR review feedbackready-for-implementationIssue is approved and ready to implement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions