Testing¶
Project: tests/LLLangTests/LLLangTests.fsproj — xUnit 2.6.3.
The test suite spans host-compiler passes and bootstrap/self-hosting scenarios (see CI for current totals). The main per-pass files cover the
host F# compiler, and a matching set of *RealTests.fs plus the
dedicated parser slices cover the self-hosted bootstrap compiler
corpus (spec/examples/valid/20-bootstrap-compiler.lll and its
incremental ancestors). BootstrapCompilerTests.fs runs the
bootstrap compiler end-to-end via System.Diagnostics.Process.
Host compiler coverage:
| File | Covers |
|---|---|
LexerTests.fs |
Lexer.fs — tokens, INDENT/DEDENT |
ParserTests.fs |
Parser.fs — every decl and expr form |
ElaboratorTests.fs |
Elaborator.fs — E001-E005, exhaustiveness |
HMInferTests.fs |
HMInfer.fs, Types.fs — unification, generalize, E008 |
CodegenTests.fs |
Codegen.fs — emitted F# shape |
StdlibTests.fs |
Stdlib builtins resolve + infer + run |
Bootstrap / self-hosting corpus coverage:
| File | Drives |
|---|---|
RealLexerTests.fs |
09-lexer-real.lll |
ArithmeticParserTests.fs |
11-parser-real.lll |
TypeParserTests.fs |
12-typeparser-real.lll |
FnParserTests.fs |
13-fnparser-real.lll |
ExprParserTests.fs |
14-exprparser-real.lll |
ModuleParserTests.fs |
15-moduleparser-real.lll |
ElaboratorRealTests.fs |
16-elaborator-real.lll |
PipelineRealTests.fs |
17-pipeline-real.lll |
HMInferRealTests.fs |
18-hminfer-real.lll |
CodegenRealTests.fs |
19-codegen-real.lll |
BootstrapCompilerTests.fs |
20-bootstrap-compiler.lll (end-to-end self-host) |
Running¶
dotnet test # whole suite
dotnet test --filter LexerTests # one file
dotnet test --filter "ClassName.Method" # one test
./tools/update-fixpoint-snapshot.sh # refresh compiler1-latest.fs from self-compile output
Most tests are pure in-memory — no temp files, no subprocess. The
*RealTests.fs and BootstrapCompilerTests.fs suites are the
exception: they shell out to lllc run via
System.Diagnostics.Process to execute compiled F# and compare
stdout. The full suite finishes in ~40s on a warm build.
Fixpoint snapshot workflow¶
BootstrapCompilerTests.fs contains a strict regression test that compares
bootstrap self-compile output to
docs/compiler-dev/fixpoint-snapshots/compiler1-latest.fs.
When a deliberate bootstrap frontend/codegen change legitimately changes emitted output, refresh the snapshot with:
./tools/update-fixpoint-snapshot.sh
dotnet test --filter "self-compile output matches fixpoint snapshot"
dotnet test
The updater script:
- Temporarily swaps
20a-bootstrap-input.lllwith20-bootstrap-compiler.lll. - Runs
lllc run spec/examples/valid/20-bootstrap-compiler.lll. - Writes the output to
compiler1-latest.fs. - Normalizes line endings to LF with exactly one trailing newline.
- Restores the shared fixture file.
Helper patterns¶
Each test file defines helpers at the top that are reused across cases. They are the only "framework" the project has.
HMInferTests.fs¶
let private inferSrc (src: string) : Result<TypedModule, LLError list> =
match tokenize src |> Result.bind parseModule with
| Error e -> failwith $"parse: {e}"
| Ok m ->
match elaborate m with
| Error es -> failwith $"elaborator: {es}"
| Ok env -> infer m env
let private inferOk (src: string) : TypedModule =
match inferSrc src with
| Ok tm -> tm
| Error es -> failwith $"unexpected hm errors: {es}"
let private inferErrs (src: string) : LLError list =
match inferSrc src with
| Ok _ -> []
| Error es -> es
let private schemeOf (tm: TypedModule) (name: string) : TypeScheme =
Map.find name tm.Env
Usage:
[<Fact>]
let ``id has polymorphic scheme`` () =
let tm = inferOk "module M\nid(x A) A = x"
let sch = schemeOf tm "id"
Assert.Contains("A", sch.Vars)
inferOk fails the test if inference errors. inferErrs returns the
error list for assertion-based matching. schemeOf extracts a
function's inferred scheme for pinning.
CodegenTests.fs¶
let private codegenSrc (src: string) : string =
match LLLang.Compiler.compile src with
| Ok fs -> fs
| Error es -> failwith $"codegen failed: {es}"
Tests assert substring containment on the emitted F# string:
[<Fact>]
let ``TDType sum type emits DU header`` () =
let src = "module M\nShape = Circle Float | Rect Float Float | Empty"
Assert.Contains("type Shape =", codegenSrc src)
[<Fact>]
let ``TDType sum type emits multi-arg branch`` () =
let src = "module M\nShape = Circle Float | Rect Float Float | Empty"
Assert.Contains("| Rect of float * float", codegenSrc src)
Corpus-driven tests¶
Both HMInferTests.fs and CodegenTests.fs read files from the
shared spec/examples/ tree:
let private readValid name =
File.ReadAllText(Path.Combine(__SOURCE_DIRECTORY__, "../../spec/examples/valid", name))
let private readInvalid name =
File.ReadAllText(Path.Combine(__SOURCE_DIRECTORY__, "../../spec/examples/invalid", name))
__SOURCE_DIRECTORY__ is an F# compile-time constant pointing to the
directory of the test file — so the path ../../spec/examples/...
resolves relative to the test project regardless of where dotnet test
is invoked from.
Corpus test skeleton:
[<Fact>]
let ``01-basics.lll type-checks`` () =
let src = readValid "01-basics.lll"
inferOk src |> ignore
[<Fact>]
let ``E001-type-mismatch.lll emits E001`` () =
let src = readInvalid "E001-type-mismatch.lll"
let errs = inferErrs src
Assert.Contains(errs, fun e -> e.Code = E001)
The invalid corpus files declare their expected code on line 1 as a
comment (-- expect: E001). Tests cross-check that the stated code
actually fires.
Adding a new corpus example¶
Valid program¶
- Create
spec/examples/valid/NN-feature.lllwith a short module demonstrating the feature. - Verify it compiles with
lllc build:bash dotnet run --project src/LLLangTool -- build spec/examples/valid/NN-feature.lll - Add a test in either
HMInferTests.fsorCodegenTests.fs:fsharp [<Fact>] let ``NN-feature type-checks`` () = let src = readValid "NN-feature.lll" inferOk src |> ignore dotnet test --filter "NN-feature"to confirm.
Invalid program¶
- Create
spec/examples/invalid/EXXX-name.lll. - First line is a comment declaring the expected error:
lll -- expect: E003 - Write the minimal trigger.
- Add a test:
fsharp [<Fact>] let ``EXXX-name triggers EXXX`` () = let src = readInvalid "EXXX-name.lll" let errs = inferErrs src Assert.Contains(errs, fun e -> e.Code = EXXX)
Organizing tests¶
Tests within a file are grouped by feature, not by function. Naming convention uses backtick strings for readability in failure output:
[<Fact>]
let ``EApp curried application infers flex return`` () = ...
Avoid setup fixtures — each test is self-contained and calls the helpers at the top of the file. If a test needs state beyond what the helpers provide, the helpers are usually the thing to extend.
Snapshot-style testing¶
There is no snapshot library. For codegen tests we assert substring
containment (Assert.Contains, Assert.DoesNotContain) rather than
byte-for-byte equality, so small formatting tweaks don't cascade into
dozens of test updates. If you need exact-shape testing, write a small
assertion explicitly.
CI¶
.github/workflows/build.yml (referenced in the README badge) runs
dotnet build followed by dotnet test. All tests must pass on
merges to main.