Skip to content

Add set formatting to formatter.ex (Phase 6 of #164) #177

@andreasronge

Description

@andreasronge

Summary

Add set literal formatting support to the PTC-Lisp formatter, enabling {:set, [...]} AST nodes to be serialized as #{...} source code strings. This completes the set literal feature by enabling roundtrip verification (parse → format → parse) and debugging output for set expressions.

Context

Architecture reference: Set Literal Implementation Plan - Phase 7: Formatter
Dependencies: Phases 1-5 complete (parser, AST types, analyzer, evaluator, runtime)
Related issues: Part of epic #164

Current State

The formatter (lib/ptc_runner/lisp/formatter.ex) handles all AST node types except sets:

  • {:vector, elems}"[#{format_list(elems)}]" (line 27-29)
  • {:map, pairs}"{#{format_pairs(pairs)}}" (line 31-33)
  • No {:set, elems} clause exists

Attempting to format a set AST will raise a FunctionClauseError.

Acceptance Criteria

  • Formatter.format({:set, []}) returns "#{}" (empty set)
  • Formatter.format({:set, [1, 2, 3]}) returns "#{1 2 3}"
  • Nested sets format correctly: {:set, [{:set, [1]}]}"#{#{1}}"
  • Sets containing other types format correctly (keywords, strings, vectors, maps)
  • Roundtrip test passes: parse set → format → parse → compare AST
  • Existing formatter tests pass (no regressions)

Implementation

Files to modify:

  • lib/ptc_runner/lisp/formatter.ex - Add format/1 clause for {:set, elems}
  • test/ptc_runner/lisp/formatter_test.exs - Add set formatting tests

Pattern to follow (line 27-29):

def format({:vector, elems}) do
  "[#{format_list(elems)}]"
end

Implementation (add after map formatting, ~line 33):

def format({:set, elems}) do
  "\#{#{format_list(elems)}}"
end

Critical escaping note:
Elixir's string interpolation uses #{} syntax. The set delimiter # must be escaped:

  • Use "\#{#{format_list(elems)}}" (escape the first #)
  • The inner #{...} is Elixir interpolation, the outer \# becomes literal #

Test Plan

Unit tests to add in test/ptc_runner/lisp/formatter_test.exs:

describe "sets" do
  test "empty set" do
    # Use string concat to avoid Elixir interpolation issues
    assert Formatter.format({:set, []}) == "#" <> "{}"
  end

  test "set with elements" do
    assert Formatter.format({:set, [1, 2, 3]}) == "#" <> "{1 2 3}"
  end

  test "set with mixed types" do
    assert Formatter.format({:set, [1, {:keyword, :a}, {:symbol, :x}]}) ==
             "#" <> "{1 :a x}"
  end

  test "nested set" do
    assert Formatter.format({:set, [{:set, [1, 2]}]}) == "#" <> "{#" <> "{1 2}}"
  end
end

Roundtrip tests:

describe "roundtrip verification" do
  test "set roundtrip" do
    set_str = "#" <> "{1 2 3}"
    {:ok, ast} = Parser.parse(set_str)
    formatted = Formatter.format(ast)
    {:ok, reparsed} = Parser.parse(formatted)
    assert ast == reparsed
  end

  test "nested set roundtrip" do
    set_str = "#" <> "{#" <> "{1} [2 3]}"
    {:ok, ast} = Parser.parse(set_str)
    formatted = Formatter.format(ast)
    {:ok, reparsed} = Parser.parse(formatted)
    assert ast == reparsed
  end
end

Note: Tests use string concatenation ("#" <> "{}") to avoid Elixir's #{} interpolation, following the same pattern used in parser_test.exs.

Out of Scope

  • Pretty-printing with indentation (all collections are single-line)
  • MapSet runtime value formatting (formatter handles AST, not evaluated values)
  • Configurable output styles

Documentation Updates

None - the formatter is an internal module with no public API documentation.

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