Skip to content

[Property Testing] Create LispGenerators module with StreamData generators #130

@andreasronge

Description

@andreasronge

Summary

Create the StreamData generators module for property-based testing of PTC-Lisp. This module generates valid Raw AST nodes that can be serialized by the Formatter and parsed back, enabling roundtrip and invariant property tests.

Context

Architecture reference: docs/ptc-lisp-property-testing-plan.md Section 3
Dependencies: Issue #128 (closed) - StreamData dependency and Formatter module ✅
Related issues: None

Current State

  • StreamData dependency is installed in mix.exs
  • Formatter module exists at lib/ptc_runner/lisp/formatter.ex
  • Test support directory exists with existing utilities: llm_benchmark.ex, llm_client.ex, ptc_lisp_benchmark.ex
  • No lisp_generators.ex exists yet

The AST types are defined in lib/ptc_runner/lisp/ast.ex:

  • Literals: nil, boolean(), number(), {:string, String.t()}, {:keyword, atom()}
  • Collections: {:vector, [t()]}, {:map, [{t(), t()}]}
  • Symbols: {:symbol, atom()}, {:ns_symbol, :ctx | :memory, atom()}
  • Calls: {:list, [t()]}

Acceptance Criteria

  • Create test/support/lisp_generators.ex with PtcRunner.TestSupport.LispGenerators module
  • Implement primitive generators: gen_nil, gen_boolean, gen_integer, gen_float, gen_string, gen_string_with_escapes, gen_keyword, gen_identifier
  • Implement symbol generators: gen_builtin_symbol, gen_variable_from_scope, gen_ctx_access
  • Implement collection generators: gen_vector, gen_map (with depth control)
  • Implement expression generator: gen_leaf_expr, gen_expr (main entry point with depth control)
  • Implement special form generators: gen_if, gen_let, gen_fn, gen_arithmetic_call, gen_comparison, gen_and, gen_or, gen_where, gen_tool_call
  • All generators produce valid AST that can be formatted and parsed back
  • Unit tests verify each generator produces expected AST structure
  • mix compile --warnings-as-errors passes

Implementation Hints

Files to create:

  • test/support/lisp_generators.ex - Main generators module

Files to modify:

  • None (new file only)

Patterns to follow:

  • Follow the existing test support pattern from test/support/llm_benchmark.ex (module structure, documentation)
  • Use use ExUnitProperties as shown in the property testing plan Section 3
  • Use StreamData functions: constant/1, map/2, bind/2, frequency/1, member_of/1, filter/2, tuple/1, list_of/2, one_of/1

Key implementation details from Section 3:

  1. Primitive generators should be bounded (integers -1_000_000..1_000_000, floats with similar bounds)
  2. Float generator must filter out NaN values using filter(fn f -> not (f != f) end)
  3. String generators should use alphanumeric for simplicity, with separate gen_string_with_escapes for roundtrip testing
  4. Depth control is critical - use depth parameter to prevent infinite recursion
  5. Scope tracking for let and fn generators to produce well-scoped programs
  6. Division / is excluded from arithmetic to avoid divide-by-zero

Note on String.to_atom/1:
The plan specifies this is safe in generators because gen_identifier produces bounded strings from a fixed character set, preventing atom table exhaustion.

Test Plan

Unit tests (create test/support/lisp_generators_test.exs):

defmodule PtcRunner.TestSupport.LispGeneratorsTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  alias PtcRunner.TestSupport.LispGenerators, as: Gen
  alias PtcRunner.Lisp.{Formatter, Parser}

  describe "primitive generators" do
    property "gen_integer produces integers" do
      check all n <- Gen.gen_integer() do
        assert is_integer(n)
        assert n >= -1_000_000 and n <= 1_000_000
      end
    end

    property "gen_float produces valid floats (no NaN)" do
      check all f <- Gen.gen_float() do
        assert is_float(f)
        # NaN check: f != f is true only for NaN
        refute f != f
      end
    end

    property "gen_string produces valid string AST" do
      check all str <- Gen.gen_string() do
        assert {:string, s} = str
        assert is_binary(s)
      end
    end

    property "gen_keyword produces valid keyword AST" do
      check all kw <- Gen.gen_keyword() do
        assert {:keyword, k} = kw
        assert is_atom(k)
      end
    end
  end

  describe "expression generator" do
    property "gen_expr with depth 0 produces leaf expressions" do
      check all expr <- Gen.gen_expr(0) do
        # Should be a leaf: literal, symbol, or ctx access
        refute match?({:list, _}, expr)
        refute match?({:vector, _}, expr)
        refute match?({:map, _}, expr)
      end
    end

    property "gen_expr produces formattable AST" do
      check all expr <- Gen.gen_expr(2) do
        source = Formatter.format(expr)
        assert is_binary(source)
        assert String.length(source) > 0
      end
    end
  end

  describe "special forms" do
    property "gen_if produces valid if structure" do
      check all if_expr <- Gen.gen_if(1, []) do
        assert {:list, [{:symbol, :if}, _cond, _then, _else]} = if_expr
      end
    end

    property "gen_let produces valid let structure" do
      check all let_expr <- Gen.gen_let(1, []) do
        assert {:list, [{:symbol, :let}, {:vector, _bindings}, _body]} = let_expr
      end
    end
  end
end

E2E test (in same file or separate):

describe "roundtrip integration" do
  property "generated AST roundtrips through format -> parse" do
    check all expr <- Gen.gen_expr(2) do
      source = Formatter.format(expr)
      assert {:ok, _parsed} = Parser.parse(source)
    end
  end
end

Out of Scope

  • Property tests themselves (next issue)
  • when, cond, threading macros (->, ->>), predicate combinators (all-of, any-of, none-of)
  • Memory namespace access (memory/key) - excluded per plan Section 7, point 2
  • Custom shrinkers

Documentation Updates

None - this is test infrastructure only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphase:property-testingProperty-based testing phase (Phase 5)ptc-lispPTC-Lisp language implementationready-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