Summary
Add set literal evaluation to the PTC-Lisp evaluator so that #{1 2 3} produces an Elixir MapSet. This enables set literals parsed in Phase 1 and analyzed in Phase 3 to be executed at runtime.
Context
Architecture reference: Set Literal Implementation Plan - Phase 4
Dependencies: Phase 3 (Analyzer) - #170 (merged)
Related issues: Epic #164, Parser #165 (merged), AST #167 (merged), Analyzer #170 (merged)
Current State
The evaluator (lib/ptc_runner/lisp/eval.ex) currently handles:
- Vectors at lines 57-71 using
Enum.reduce_while pattern
- Maps at lines 73-84 with key-value pair evaluation
Sets are parsed as {:set, [elements]} AST nodes (verified in analyze.ex:58-62) but there's no evaluation clause in eval.ex. The @type value spec (lines 15-24) doesn't include MapSet.t().
CoreAST already supports sets at line 28: | {:set, [t()]}
Acceptance Criteria
Implementation
File to modify: lib/ptc_runner/lisp/eval.ex
1. Update @type value (line 22)
Add MapSet.t() to the value type union:
@type value ::
nil
| boolean()
| number()
| String.t()
| atom()
| list()
| map()
| MapSet.t() # <-- NEW
| function()
| {:closure, [CoreAST.pattern()], CoreAST.t(), env()}
2. Add set evaluation clause (after line 84)
Follow the vector pattern from lines 57-71 but wrap result in MapSet.new/1:
# Sets: evaluate all elements, then create MapSet
defp do_eval({:set, elems}, ctx, memory, env, tool_exec) do
result =
Enum.reduce_while(elems, {:ok, [], memory}, fn elem, {:ok, acc, mem} ->
case do_eval(elem, ctx, mem, env, tool_exec) do
{:ok, v, mem2} -> {:cont, {:ok, [v | acc], mem2}}
{:error, _} = err -> {:halt, err}
end
end)
case result do
{:ok, values, memory2} -> {:ok, MapSet.new(values), memory2}
{:error, _} = err -> err
end
end
Key difference from vector: Uses MapSet.new(values) instead of Enum.reverse(values).
Test Plan
File: test/ptc_runner/lisp/eval_test.exs
Add a describe "set evaluation" block following the existing describe "map evaluation" pattern:
describe "set evaluation" do
test "empty set" do
assert {:ok, result, %{}} = Eval.eval({:set, []}, %{}, %{}, %{}, &dummy_tool/2)
assert result == MapSet.new([])
end
test "set with literals" do
assert {:ok, result, %{}} = Eval.eval({:set, [1, 2, 3]}, %{}, %{}, %{}, &dummy_tool/2)
assert MapSet.equal?(result, MapSet.new([1, 2, 3]))
end
test "set with mixed types" do
assert {:ok, result, %{}} =
Eval.eval(
{:set, [1, {:string, "test"}, {:keyword, :foo}]},
%{},
%{},
%{},
&dummy_tool/2
)
assert MapSet.equal?(result, MapSet.new([1, "test", :foo]))
end
test "nested sets" do
inner = {:set, [1, 2]}
assert {:ok, result, %{}} =
Eval.eval({:set, [inner]}, %{}, %{}, %{}, &dummy_tool/2)
assert MapSet.equal?(result, MapSet.new([MapSet.new([1, 2])]))
end
test "set containing vector" do
assert {:ok, result, %{}} =
Eval.eval({:set, [{:vector, [1, 2]}]}, %{}, %{}, %{}, &dummy_tool/2)
assert MapSet.equal?(result, MapSet.new([[1, 2]]))
end
test "set containing nil" do
assert {:ok, result, %{}} = Eval.eval({:set, [nil, 1, 2]}, %{}, %{}, %{}, &dummy_tool/2)
assert MapSet.equal?(result, MapSet.new([nil, 1, 2]))
end
end
describe "set error propagation" do
test "error in set element propagates" do
set_ast = {:set, [1, 2, {:var, :unbound}]}
assert {:error, {:unbound_var, :unbound}} =
Eval.eval(set_ast, %{}, %{}, %{}, &dummy_tool/2)
end
test "error in nested set propagates" do
nested = {:set, [{:set, [1, {:var, :x}]}]}
assert {:error, {:unbound_var, :x}} =
Eval.eval(nested, %{}, %{}, %{}, &dummy_tool/2)
end
end
describe "set memory threading" do
test "memory is threaded through set evaluation" do
memory = %{count: 5}
{:ok, result, new_memory} =
Eval.eval({:set, [1, 2, 3]}, %{}, memory, %{}, &dummy_tool/2)
assert MapSet.equal?(result, MapSet.new([1, 2, 3]))
assert new_memory == memory
end
end
Note: Full E2E tests and runtime function tests (set?, contains? on sets, etc.) will be added in Phase 5 (Runtime).
Edge Cases
| Case |
Expected Result |
#{} (empty set) |
MapSet.new([]) |
#{1 2 3} |
MapSet.new([1, 2, 3]) |
#{nil} |
MapSet.new([nil]) - valid set containing nil |
#{#{1}} |
Nested MapSet - MapSet.new([MapSet.new([1])]) |
#{[1 2]} |
MapSet containing a list - MapSet.new([[1, 2]]) |
| Set with unbound var |
Propagates {:error, {:unbound_var, name}} |
Deduplication behavior: Duplicate values silently coalesce during MapSet.new/1 construction - this happens automatically at the Elixir level.
Out of Scope
- Runtime functions (
set?, set, contains? on sets) - Phase 5
- MapSet guards for existing functions (
map, filter, contains?, etc.) - Phase 5
- Formatter support - Phase 6
- Documentation updates - Phase 7
Documentation Updates
None required for this phase - internal implementation only. Public documentation will be updated in Phase 7.
Summary
Add set literal evaluation to the PTC-Lisp evaluator so that
#{1 2 3}produces an ElixirMapSet. This enables set literals parsed in Phase 1 and analyzed in Phase 3 to be executed at runtime.Context
Architecture reference: Set Literal Implementation Plan - Phase 4
Dependencies: Phase 3 (Analyzer) - #170 (merged)
Related issues: Epic #164, Parser #165 (merged), AST #167 (merged), Analyzer #170 (merged)
Current State
The evaluator (
lib/ptc_runner/lisp/eval.ex) currently handles:Enum.reduce_whilepatternSets are parsed as
{:set, [elements]}AST nodes (verified in analyze.ex:58-62) but there's no evaluation clause in eval.ex. The@type valuespec (lines 15-24) doesn't includeMapSet.t().CoreAST already supports sets at line 28:
| {:set, [t()]}Acceptance Criteria
{:set, elements}AST nodes@type valueincludesMapSet.t()Implementation
File to modify:
lib/ptc_runner/lisp/eval.ex1. Update
@type value(line 22)Add
MapSet.t()to the value type union:2. Add set evaluation clause (after line 84)
Follow the vector pattern from lines 57-71 but wrap result in
MapSet.new/1:Key difference from vector: Uses
MapSet.new(values)instead ofEnum.reverse(values).Test Plan
File:
test/ptc_runner/lisp/eval_test.exsAdd a
describe "set evaluation"block following the existingdescribe "map evaluation"pattern:Note: Full E2E tests and runtime function tests (
set?,contains?on sets, etc.) will be added in Phase 5 (Runtime).Edge Cases
#{}(empty set)MapSet.new([])#{1 2 3}MapSet.new([1, 2, 3])#{nil}MapSet.new([nil])- valid set containing nil#{#{1}}MapSet.new([MapSet.new([1])])#{[1 2]}MapSet.new([[1, 2]]){:error, {:unbound_var, name}}Deduplication behavior: Duplicate values silently coalesce during
MapSet.new/1construction - this happens automatically at the Elixir level.Out of Scope
set?,set,contains?on sets) - Phase 5map,filter,contains?, etc.) - Phase 5Documentation Updates
None required for this phase - internal implementation only. Public documentation will be updated in Phase 7.