Two compilers, a benchmark suite, a 3,000-line spec — and I’d been avoiding the obvious question: what happens when someone actually tries to use this thing?
I didn’t have real users, so I faked it. Wrote fifteen programs from scratch, pretending I’d never seen Hew before. Just a developer working from the docs. Write the code, see what breaks, take notes. (Not a formal usability study. More like method acting, but for compilers.)
Actors felt natural. The spawn syntax read clearly, basic programs compiled and ran. The core held up, but the failures were more interesting. The type checker was flagging things that weren’t actually wrong — programs ran fine despite warnings, which is exactly how you teach people to ignore your type checker. Error messages leaked MLIR internals like !llvm.ptr instead of showing Hew types. There was no else if syntax, so anything past a two-way branch turned into a nesting pyramid. Lambda syntax didn’t match the spec.
Each problem was specific and fixable. I had a list.
Making the compiler honest
Pattern exhaustiveness checking for match expressions came first. Previously the compiler just stayed quiet about missing cases — you’d find out at runtime when the program crashed. Now it tells you which variants you forgot.
“Did you mean?” suggestions for undefined names followed. Levenshtein distance against identifiers in scope:
error: undefined name 'prnt'
--> main.hew:5:5
|
5 | prnt("hello");
| ^^^^ did you mean 'print'?Small thing. Disproportionate impact when you’re learning a language and every unknown name feels like hitting a wall.
Generic monomorphization for user-defined types closed a gap that had been bugging me. Generics worked for built-ins like Vec<T> and Option<T>, but user-defined generic types hit codegen errors. The fix was generating specialized versions for each concrete type argument — same approach as Rust, zero-cost abstractions at the expense of binary size.
Else-if chains were embarrassingly overdue. Without them:
// Before: nested blocks
if condition_a {
handle_a()
} else {
if condition_b {
handle_b()
} else {
if condition_c {
handle_c()
} else {
handle_default()
}
}
}After:
// After: flat else-if chains
if condition_a {
handle_a()
} else if condition_b {
handle_b()
} else if condition_c {
handle_c()
} else {
handle_default()
}The parser change was tiny, but the difference in how the code reads was not.
Then false positive elimination. Enums, Vec::new(), and for-loop iteration variables were all generating spurious type warnings — exactly what the UX exercise had surfaced. Programs that ran correctly but got warnings anyway. Fixing these wasn’t intellectually exciting. It was the single most important change for user trust. (A type checker that cries wolf teaches you to ignore it.)
The playground, rebuilt
While the compiler was getting more honest, the playground got a new engine. I replaced the Rust-based hew-wasm crate with an Emscripten-compiled version of hewcpp. The MLIR compiler now runs directly in the browser via WebAssembly — no server round-trip. For execution, compiled programs run inside a gVisor sandbox, each one isolated in a container that gets torn down after a timeout.
Infrastructure kept expanding in parallel. Wire type codegen in MLIRGen, select and join codegen, structured concurrency scope codegen. A module import system so programs could finally span multiple files. And then I removed the old Rust frontend crates entirely. hewc had been the reference implementation, the safety net, the “at least something works” fallback. Dropping it meant hewcpp had matured past needing one.