← Blog

Graft v4.2: Expression Functions

3 min read
graftcompilerllmadversarial-debateclaude

Graft is a domain-specific language for defining LLM agent pipelines. The compiler takes .gft files and generates Claude Code harness structures. Development uses an adversarial debate harness where multiple AI agents independently analyze, critique, and converge before implementation.

v4.2 adds built-in expression functions — the first new expression capability since v4.0's expression system. Four rounds, ~10 agent calls, 47 new tests (1,048 total).

What This Version Adds

Expression functions. Four built-in functions callable anywhere in expressions:

graph Pipeline(input: Spec, output: Report, budget: 20k) {
  Analyzer
  -> let count = len(Analyzer.items)
  -> let label = str(max(Analyzer.score, 50))
  -> Formatter -> done
}
  • len(value) — array or string length
  • max(a, b) — larger of two numbers
  • min(a, b) — smaller of two numbers
  • str(value) — convert to string

Functions compose naturally: str(max(A.score, 50)) works. The scope checker validates function names against a BUILTIN_FUNCTIONS registry, and the type checker validates argument count. Two new error codes: SCOPE_UNKNOWN_FUNCTION and TYPE_FUNC_ARITY.

Graph call return values. Graph calls now capture output:

graph Main(input: Spec, output: Report, budget: 20k) {
  Sub() -> let result = Sub.field -> Formatter -> done
}

Previously, graph calls executed but discarded their output. Now the last node's output is stored under the graph call name in the parent's outputs map.

Equality unification. Filter transforms (evalCondition) now use loose equality matching routing conditions (evaluateCondition). String "200" correctly matches number 200 in both contexts. One-line fix, caught by v4.1's technical retrospective.

Implementation Highlights

Parser disambiguation. parsePrimary recognizes len(...) as a function call only because len is in BUILTIN_FUNCTIONS. Unknown names like myFunc(x) remain field_access — the ( becomes an unexpected token. No user-defined functions; the whitelist IS the API.

BUILTIN_FUNCTIONS registry. A Record<string, { arity: number }> in ast.ts, shared by parser, evaluator, scope checker, type checker, and LSP. Adding a new builtin: one registry entry + one switch case in evaluateExpr.

Graph call capture. Track nodeResults.length before child execution. After, capture the last result and store it under the graph call name. Three lines of new code.

Stats

| Metric | v4.1 | v4.2 | Delta | |--------|------|------|-------| | Tests | 1,001 | 1,048 | +47 | | Ratchets | ~279 | ~292 | +13 | | Rounds | 4 | 4 | 0 | | NEEDS_CHANGES | 0 | 0 | 0 |

Try It

npm install -g @graft-lang/graft
graft compile examples/chatbot.gft --out-dir ./output
graft run examples/hello.gft --input '{"question":"test"}' --dry-run

Or from source: git clone https://github.com/JSLEEKR/graft.git && cd graft && npm install && npm run build