Skip to content

Add \#{...} set literal parsing to PTC-Lisp parser #165

@andreasronge

Description

@andreasronge

Summary

Add #{...} set literal parsing to the PTC-Lisp parser. Sets are currently not supported and LLMs commonly use this Clojure syntax causing parse errors. This issue adds parser support for the set literal syntax.

Context

Architecture reference: Set Literal Implementation Plan - Section 5.1 Phase 1: Parser
Dependencies: None (first phase of epic #164)
Related issues: Epic #164

Current State

Verified: The parser in lib/ptc_runner/lisp/parser.ex currently has no handling for #{...} syntax. The :expr combinator (lines 147-162) includes vector, map_literal, and list but no set. Attempting to parse #{1 2 3} will fail with a parse error since # is not a valid start character for any expression type.

Acceptance Criteria

  • Parser.parse("#{}") returns {:ok, {:set, []}}
  • Parser.parse("#{1 2 3}") returns {:ok, {:set, [1, 2, 3]}}
  • Parser.parse("#{:a \"b\" 3}") returns {:ok, {:set, [{:keyword, :a}, {:string, "b"}, 3]}}
  • Parser.parse("#{#{1 2}}") returns {:ok, {:set, [{:set, [1, 2]}]}} (nested sets)
  • Parser.parse("#{[1 2]}") returns {:ok, {:set, [{:vector, [1, 2]}]}} (set containing vector)
  • Parser.parse("#{ 1 , 2 , 3 }") returns {:ok, {:set, [1, 2, 3]}} (whitespace/comma handling)
  • Parser.parse("#{1 2 3") (unclosed) returns {:error, {:parse_error, _}}
  • Parser.parse("# {1 2}") (space between # and {) returns {:error, {:parse_error, _}}
  • Existing tests pass (no regressions to map parsing)

Implementation

Files to Modify

File Change
lib/ptc_runner/lisp/parser.ex Add :set combinator, update :expr choice list
lib/ptc_runner/lisp/parser_helpers.ex Add build_set/1 helper function
test/ptc_runner/lisp/parser_test.exs Add set parsing tests

Step 1: Add build_set/1 helper to parser_helpers.ex

Add after build_list/1 (line 64):

def build_set({:set, elements}), do: {:set, elements}

Step 2: Add :set combinator to parser.ex

Add after the :map_literal combinator (after line 131):

defcombinatorp(
  :set,
  ignore(string("#{"))
  |> concat(parsec(:ws))
  |> repeat(parsec(:expr) |> concat(parsec(:ws)))
  |> ignore(string("}"))
  |> tag(:set)
  |> map({ParserHelpers, :build_set, []})
)

Step 3: Update :expr combinator

Critical: Add parsec(:set) BEFORE parsec(:map_literal) in the choice list.

Both combinators use } as closing delimiter, but #{ must be matched before {. If :set comes after :map_literal, the parser will fail on #{...} because # isn't recognized, then {...} matches as a map.

Update lines 147-162:

defcombinatorp(
  :expr,
  choice([
    nil_literal,
    true_literal,
    false_literal,
    float_literal,
    integer_literal,
    string_literal,
    keyword,
    symbol,
    parsec(:vector),
    parsec(:set),         # <-- NEW: must come before map_literal
    parsec(:map_literal),
    parsec(:list)
  ])
)

Test Plan

Add a new describe "sets" block to test/ptc_runner/lisp/parser_test.exs:

describe "sets" do
  test "empty set" do
    assert {:ok, {:set, []}} = Parser.parse("#{}")
  end

  test "set with elements" do
    assert {:ok, {:set, [1, 2, 3]}} = Parser.parse("#{1 2 3}")
  end

  test "set with mixed types" do
    assert {:ok, {:set, [{:keyword, :a}, {:string, "b"}, 3]}} =
           Parser.parse("#{:a \"b\" 3}")
  end

  test "nested set" do
    assert {:ok, {:set, [{:set, [1, 2]}]}} = Parser.parse("#{#{1 2}}")
  end

  test "set containing vector" do
    assert {:ok, {:set, [{:vector, [1, 2]}]}} = Parser.parse("#{[1 2]}")
  end

  test "set with whitespace and commas" do
    assert {:ok, {:set, [1, 2, 3]}} = Parser.parse("#{ 1 , 2 , 3 }")
  end

  test "unclosed set returns error" do
    assert {:error, {:parse_error, _}} = Parser.parse("#{1 2 3")
  end

  test "space between # and { is invalid" do
    assert {:error, {:parse_error, _}} = Parser.parse("# {1 2}")
  end
end

Regression tests (already exist, verify they still pass):

  • Map parsing: {:a 1}{:ok, {:map, [{{:keyword, :a}, 1}]}}
  • Empty map: {}{:ok, {:map, []}}

Out of Scope

  • Evaluation of sets (Phase 4)
  • AST type spec updates (Phase 2)
  • Analyzer support (Phase 3)
  • Runtime set operations (Phase 5)
  • Formatter support (Phase 6)
  • Duplicate handling/deduplication (happens at evaluation time, not parsing)

Documentation Updates

None - documentation updates are handled in Phase 7 after all implementation phases complete.

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-approvedMaintainer-approved for Claude automationenhancementNew feature or requestready-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