After merging the two codebases in Part 11, I could finally see all the seams in one place. So I walked the entire pipeline — lexer, parser, type checker, enrichment, codegen, runtime, stdlib — and started closing every open loop I could find. There were more than I expected.
The tail call that wasn’t
Tail call optimization was “implemented.” The parser detected tail-position calls, set an is_tail_call flag on the AST node, codegen respected that flag, tests passed. Then I wired it into the full compilation pipeline and it stopped working.
The enrichment pass — enrich_program() — reconstructs Call expressions to resolve overloads and insert implicit conversions. It builds new AST nodes. New nodes with default field values. Default is_tail_call: false. Every tail call marker set by mark_tail_calls() was being silently erased by the pass that ran after it. (I’m a bit embarrassed about how long I stared at that one before checking pass ordering.)
The fix was reordering: call mark_tail_calls() after enrichment, not before. One line moved, and a recursive Fibonacci that had been blowing the stack started running in constant space.
Defer was already done
This one surprised me. I went looking for defer expecting to find a skeleton — maybe a parsed AST node with no codegen. Instead I found a complete implementation: keyword in the frontend, MLIR codegen emitting cleanup in LIFO order, editor highlighting recognizing the keyword. It all worked — it just wasn’t registered as an E2E test, and the semantics weren’t documented anywhere. (The feature had been complete for days. Nobody noticed.)
fn process_file(path: string) {
let handle = open(path);
defer close(handle); // runs last (LIFO)
let lock = acquire_lock();
defer release(lock); // runs second
let tx = begin_transaction();
defer rollback_if_open(tx); // runs first
// ... work with handle, lock, and tx ...
// on exit: rollback_if_open, release, close
}The test was registered and the LIFO guarantee documented. Deferred calls collect during forward execution and emit at function exit in reverse order — exactly what you want for resource cleanup.
Closing the lexer-parser gap
The lexer recognized three tokens that the parser had no rules for: Duration, Isolated, and Foreign. Each one was a promise the language had made and not kept.
Duration literals got the full pipeline treatment — the parser converts 100ms, 5s, 1m, 1h to i64 milliseconds at parse time, the type checker sees an i64, codegen emits a constant:
// Duration literals — parser converts to i64 milliseconds
let timeout = 100ms; // 100
let interval = 5s; // 5000
let window = 1m; // 60000
let expiry = 1h; // 3600000
// Used directly with actor timeouts
select {
worker.result() => handle(it),
after 30s => println("timed out"),
}The isolated keyword marks actors with no shared state access — a hint for the scheduler to run them with fewer synchronization constraints. The foreign keyword declares external functions with C-compatible calling conventions. Both had been tokenized since the lexer was written. Now every token the lexer produces has parser support.
Value struct field assignment was another gap. Compound operators like += and -= worked on actor fields, which use memref-backed storage, but not on value struct fields. The fix was a load-insertvalue-store pattern in codegen — read the struct, modify the field, write it back. Three MLIR operations that should have been there from the start.
The stdlib had SQL injection
When I audited the runtime’s database bindings, the postgres query function was taking a format string and interpolating user values directly into the SQL. (In a language that’s supposed to be safe-by-default. Great.)
// Before: string interpolation in queries (ca67d57)
hew_pg_query(conn, f"SELECT * FROM users WHERE id = {user_id}")
// After: parameterized queries — no injection
hew_pg_query_params(conn, "SELECT * FROM users WHERE id = $1", params)The fix added hew_pg_query_params and hew_pg_execute_params with proper parameterized queries. Same commit expanded Redis support with 19 new functions — pipeline, hash, and set commands — and added a generic hew_http_request with configurable headers and timeouts. The HTTP client had been hardcoded to GET requests with no timeout, which is the kind of thing you manage to forget until you try some test URLs.
The C ABI layer got its own cleanup. Runtime functions had been exporting their C-compatible signatures inline, with #[no_mangle] extern "C" wrappers duplicated across modules. Extracting these into a dedicated cabi.rs eliminated roughly 548 lines of duplication and made the ABI surface auditable from one file. A separate type signature fix caught Vec<string> being passed as an opaque i64 pointer across the C boundary — the kind of bug that works until it doesn’t.
993 tests, zero failures
The testing surge added 120+ new tests across the parser, type checker, enrichment pass, and runtime. Not because any of these systems were untested — they all had coverage — but because the coverage was concentrated on happy paths. Edge cases around nested generics, recursive type aliases, and actor supervision hierarchies had been exercised by E2E tests but never unit-tested in isolation.
Eight pre-existing E2E test failures got fixed in the same pass: supervisor spawn pre-registration was racing with actor initialization, Vec was using int instead of i64 after the default integer type change, and byte stream examples had stale linkage flags. Each failure had been individually ignorable, but together they eroded confidence in ctest output — when some tests always fail, you stop reading the results carefully.
Grammar and spec alignment brought grammar.ebnf in line with 10+ new productions and updated HEW-SPEC.md for accuracy. Receive functions gained type parameter and where clause syntax in the grammar. The spec is now accurate enough to implement from.
After all of it: 993 tests passing across the Rust workspace, 225 E2E tests in hewcpp, zero failures. Every token the lexer produces has a parser rule, every stdlib function that touches a database uses parameterized queries, and every deferred cleanup runs in the right order. None of it makes for a compelling demo, but I sleep better.