grammar-inference-engine/docs/adr/0006-argument-pattern-extraction.md
tobjend ca7ccb36ff
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
docs: add 8 architecture decision records
ADR 0001: nvim-treesitter highlights.scm as capture source
ADR 0002: language-agnostic method extraction via child_by_field_name
ADR 0003: method-level n-gram clustering before inference
ADR 0004: frequency filter with min_coverage threshold
ADR 0005: import extraction per cluster
ADR 0006: argument pattern extraction via AST node classification
ADR 0007: JSON output for LLM prompt injection
ADR 0008: BEX ensemble for grammar inference
2026-07-03 22:01:35 +02:00

2.8 KiB

6. Argument pattern extraction via AST node classification

Date: 2026-07-03

Status: Accepted

Context

A behavioral token like assertEquals tells the LLM that the function is called, but not how. Two codebases both use assertEquals — one writes assertEquals(expected, actual) and the other writes assertEquals(actual, expected) with swapped argument order. An LLM guessing the wrong order writes broken tests.

The highlights.scm captures tell us that a function is called. We need the argument structure — number of arguments, their types, and the common patterns.

Decision

For each behavioral capture node, walk up to its parent call_expression (or equivalent), find the argument list node, and classify each argument by structural role.

Argument classification is language-agnostic:

Classification Matches
lit string, number, boolean, null
var identifiers, names
call nested call expressions, method invocations
lambda lambda expressions, blocks, do-blocks
kwarg keyword/named arguments
expr binary/unary/ternary/operator expressions
template string interpolation, template literals
other anything else (fallback)

Argument list node detection uses a tiered approach:

  1. child_by_field_name("arguments") — works for Python, JS, TS, Java, Go, Ruby, Rust.
  2. Fallback: scan children for argument_list, arguments, call_suffix (Kotlin), template_string (JS tagged templates).
  3. Kotlin special case: call_suffix may contain a direct lambda_expression child (for every { ... } syntax) or a value_arguments → value_argument chain (for func(a, b) syntax).

Results are aggregated per cluster into a summary showing min/max/common arg counts and the top argument-type patterns.

Consequences

Positive:

  • Reveals argument ordering conventions: assertEquals: n=2 [lit,var] means expected-first.
  • Reveals calling convention variance: verify: n=0 [] | n=1 [lambda] | n=1 [var] means three styles coexist.
  • No per-language branches — the tiered arglist detection handles all 10 grammars.

Negative:

  • kwarg detection only covers named arguments, not default values or spread operators.
  • Nested destructuring patterns fall into other bucket — no granularity for complex argument shapes.
  • other is a catch-all that can hide meaningful distinctions we haven't classified yet.

Alternatives Considered

  • Extract raw argument text: Language-agnostic but fragile — variable names change per test, producing high variance and low signal.
  • No argument extraction: The LLM sees assertEquals but doesn't know argument order. Leads to wrong code.
  • Per-language argument extractors: Would be more precise but violate the zero-adapters constraint.