Eskiueskiu v0.7.0

Debugging the Eskiu Compiler

How to diagnose issues in the lexer, parser, type checker, and code generator.

Test Modes Reference

Flag What it does What it outputs When to use
--test-lexer Tokenizes source, stops after lexing One token per line with type, lexeme, line, and column Diagnosing unrecognized tokens, bad keyword handling, or unexpected token splits
--test-parser Tokenizes + parses, stops before type checking Indented AST via the ASTVisitor/ASTPrinter pass Diagnosing missing nodes, wrong nesting, or silently skipped declarations
--test-typechecker Tokenizes + parses + type checks Errors prefixed with file:line:col, or "Type checking succeeded!" Diagnosing undefined variables, type mismatches, struct field errors
--test-codegen Full pipeline except linking LLVM IR text via module->print() Diagnosing wrong IR types, missing terminators, LLVM verification failures

Each mode exits 0 on success; diagnostics go to stderr as file.esk:line:col: message. Pick the earliest mode whose output is already wrong: a defect at the lexer level is much easier to read in --test-lexer than in the final IR.

Golden-IR Snapshot Oracle

tests/type_zoo/snapshot.sh is the codegen-regression guard. The type zoo is a corpus of programs that exercise the breadth of the type grammar; the script emits each program's LLVM IR and compares it against the checked-in baseline under tests/type_zoo/golden/.

Use this whenever you touch codegen, the ty::Type IR, or anything in sema/ that feeds the resolved type table: a behavior-preserving refactor must produce byte-identical golden IR.

Fuzzer and the O0-vs-O2 Differential

tests/fuzz/eskiu_fuzz.py is a generative + mutation fuzzer. It synthesizes (and mutates) .esk programs and runs them through a differential oracle: each program is compiled at -O0 and at -O2 and executed, and any divergence in output (or a crash at one level but not the other) is reported as a miscompile.

Why this catches real bugs: a correct program must produce the same observable result regardless of optimization level. When the two disagree, the front end emitted IR that was accidentally correct at -O0 but wrong once LLVM's optimizer was allowed to exploit it (e.g. an undef value, a wrong integer width, or a missing extension that -O0 happened to mask). This is precisely the class of latent miscompile the v0.2.4 single-resolver work fixed. When the fuzzer flags a case, minimize the generated program, then bisect with --test-codegen to find the construct whose IR is wrong.

Some generators carry their own expected stdout instead of relying on the O0/O2 differential, notably the backslash-newline-in-comments/strings generators, which catch uniformly mis-lexed programs (where O0 and O2 agree but are both wrong, e.g. a // comment ending in \ swallowing the next line). A mismatch against the known output (or a build failure) is the finding.

The corpus-wide counterpart is tests/opt_differential.sh (v0.3.1): it compiles every tests/*.esk with eskiuc -O0 and with eskiuc -O2 (the -O0/-O1/-O2/-O3 flag runs the LLVM middle-end before code generation) and fails on any exit/stdout divergence. Reach for it when diagnosing a bug that only appears under optimization: run it, then re-run the one failing program at each level and diff --test-codegen output. It caught the v0.3.1 float-closure return-type miscompile.

Resolver Consistency: ESKIU_RESOLVER_DEBUG

The single resolver (v0.2.4) makes codegen consume the type checker's resolved-type table instead of re-deriving types. The table doesn't annotate every expression, so codegen keeps a structural fallback (deriveExprEskiuType). To guard against the two ever disagreeing (the latent-miscompile class), set ESKIU_RESOLVER_DEBUG=1: whenever the table has an entry, codegen also runs the derivation and prints [resolver-disagree] table=… derive=… <ExprKind> on any semantic difference (benign representational noise, the struct:/interface: tag and alias spelling, is normalized away first).

for f in tests/*.esk; do ESKIU_RESOLVER_DEBUG=1 build/eskiuc --test-codegen "$f" 2>&1 >/dev/null; done | grep resolver-disagree

Across the corpus this is silent except for one benign enum-as-underlying-int case (Color vs int), i.e. the two evaluators agree on every behavior-affecting type. Run it after any change to sema/ type resolution or codegen's type derivation; a new disagreement is the diagnosis.

Runtime Memory / UB: --asan and --ubsan

For defects that only show up at run time:

Both compose with eskiuc run (flags go before the script: eskiuc run --asan f.esk). Reach for these when a program type-checks, compiles, and produces wrong or nondeterministic results at run time rather than a compile-time diagnostic. They are also run as a CI gate over the test corpus.

Editor Tooling: --hover-at / --definition-at

The VS Code extension drives two CLI flags that are also useful for manual diagnosis:

Because --hover-at reports the resolver's own answer, it is the quickest way to see whether a bug is "the type checker resolved the wrong type" versus "codegen lowered a correctly-resolved type wrongly."

Note: the type checker runs twice (single resolver)

The pipeline runs the type checker, then the async transform rewrites the AST, then the type checker is re-run on the transformed AST to produce the per-expression ty::Type table that codegen consumes. Codegen does not re-derive expression types; it reads that table, and getTypeFromString interprets type spellings through the single ty::Type::parse grammar.

This gives a defined diagnosis path for codegen bugs: