← Blog

Graft v4.4: String Interpolation

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.4 adds string interpolation and addresses tech debt flagged by the v4.3 retrospective. Four rounds, ~6 agent calls, 46 new tests (1,123 total).

What This Version Adds

String interpolation. Template expressions in let bindings:

graph Pipeline(input: Spec, output: Report, budget: 20k) {
  Analyzer
  -> let msg = "Found ${len(Analyzer.items)} items, score: ${Analyzer.score * 2}"
  -> let label = "Status: ${Analyzer.status}"
  -> Reporter -> done
}

Full expression syntax inside ${}: field access, arithmetic operators, function calls. Escaped \${ produces a literal ${. Templates always infer to string type.

evaluateExpr extraction. The expression evaluator (84 lines) and resolveNestedField helper moved from flow-runner.ts to new src/runtime/expr-eval.ts. flow-runner.ts dropped from 404 to 311 lines. Backward-compatible re-exports maintained -- existing imports continue to work.

Registry enrichment. BUILTIN_FUNCTIONS extended with returnType, signature, and description fields:

const BUILTIN_FUNCTIONS = {
  len: { arity: 1, returnType: 'number', signature: 'len(value) -> number', description: '...' },
  // ...7 total
};

The type checker reads returnType from the registry instead of a hardcoded switch. Hover docs read signature and description from the registry instead of a separate FUNC_DOCS record. Adding a new builtin now requires 2 touch points instead of 4-5.

Memory archival. T1-v2.2 ratchets (~100 items) archived to harness/archived_ratchets.md. common_memory.md reduced from ~650 to ~530 lines.

Implementation Highlights

Template parsing. The lexer detects ${ inside strings and produces a TemplateString token. The parser's parseTemplateParts splits on ${...} by counting brace depth, then creates a new Lexer + Parser for each inner expression. This avoids complex lexer state while supporting the full expression grammar inside templates.

type TemplatePart =
  | { kind: 'text'; value: string }
  | { kind: 'expr'; value: Expr };

At runtime, template evaluation maps each part: text parts pass through, expression parts evaluate via evaluateExpr and auto-convert to strings via String().

Extraction pattern. The expr-eval.ts extraction followed the v4.2 retro's recommendation (triggered at 400 lines). The pattern: extract to new file, re-export from old file, verify identity (expect(evalFromExprEval).toBe(evalFromFlowRunner)). Zero behavioral changes, zero test modifications needed.

Process Notes

  • R1 (DIRECT): evaluateExpr extraction. Pure refactor, 11 tests. PASS.
  • R2 (MEDIUM): String interpolation. New token, AST kind, parser method, runtime eval, type/scope checker updates. 15 tests. PASS.
  • R3 (DIRECT): Registry enrichment + memory archival. 12 tests. PASS.
  • R4 (TEST-ONLY): Integration + regression. 8 tests. PASS.

21 consecutive PASS rounds (v4.0-R1 through v4.4-R4).

Stats

| Metric | v4.3 | v4.4 | Delta | |--------|------|------|-------| | Tests | 1,077 | 1,123 | +46 | | Ratchets | ~302 | ~275 | -27 (archival) | | Rounds | 3 | 4 | +1 | | Consecutive PASS | 17 | 21 | +4 | | flow-runner.ts lines | 404 | 311 | -93 |

Active ratchet count decreased due to archival, not deletion. Total locked decisions: ~275 active + ~100 archived = ~375.

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