5.4 KiB
Where this is
Milestones 2 and 3 of plan.org were merged: the interpreter was dropped
(open decision #7, settled — see below) and the compiled path is the only
backend. calc-me.flan compiles and runs.
reader ✅ → parse ✅ → check ✅ → emit ✅ → clang ✅
| File | What it does |
|---|---|
lib/loc.ml |
source locations + Loc.Error, the frontend's one exception |
lib/form.ml |
reader output: Sym Kw Int Float Str Byte List Vec Map |
lib/reader.ml |
hand-written S-expression reader, no menhir/ocamllex |
lib/ast.ml |
AST: texpr, expr, place, pattern, decl |
lib/parse.ml |
forms → AST; special forms, desugaring, declarations |
lib/types.ml |
resolved types; structural equality, Never fits anywhere |
lib/tast.ml |
the typed IR the backend consumes |
lib/check.ml |
AST → typed IR; two passes, bidirectional |
lib/prelude.ml |
print-str/print-f64/print-line, written in Flan |
lib/emit.ml |
typed IR → LLVM IR text |
lib/build.ml |
.ll + the shim → clang → executable |
runtime/flan_rt.c |
the whole host ABI: argv, stdout, exit, 4 conversions |
bin/main.ml |
flan read | parse | check | emit | build | run |
test/test_flan.ml |
reader, parser and checker |
test/test_acceptance.ml |
20 expression/result pairs + 3 whole programs |
test/programs/*.flan |
the milestone-2 surface calc-me does not reach |
$ flan run calc-me.flan "1 + 2 * (3 - 0.5) / 2"
3.5
dune build && dune test is green, and the whole-program cases run at -O2
and -O0 — mem2reg launders a sloppy alloca, so -O0 is what tests the IR
actually emitted. flan emit is byte-reproducible. flan check sand.flan fails on
(import rl ...), which is milestone 4 — as it should.
Why there is no interpreter
Open decision #7 is settled: the compiled path is the only backend. The two
arguments for a permanent interpreter had both already expired in plan.org —
the instrumentation step debugger that wanted it is cut, and compiled
redefinition measured at ~16ms, which is perceptually instant for expression
eval too. CCL and SBCL both do full interactive development without leaning on
an interpreter; what makes a live image work is a fast compiler callable at
runtime.
The remaining argument was that milestone 3 needs an oracle to check the compiler against. It does not: the acceptance test is a hand-written table of expression/result pairs, so the table is the oracle.
Consequences, both already applied: milestone 2's "measured interpreted calls
per second" exit criterion is dropped — milestone 4 runs on the compiled build
and nothing depended on that number — and the host ABI moved onto the critical
path, which is why runtime/flan_rt.c exists now rather than at milestone 3.
The layout, which is the whole backend design
i8..i64 / u8..u64 i8..i64 signedness lives in the ops
f32 f64 float double
bool i1
[T] and string { ptr, i64 } ptr+len, non-owning
[n T] [n x T] inline, a value
(Ptr T) ptr opaque pointers
(Option T) { i8, T } tag 0 None, 1 Some
a struct a literal struct, declaration order
Unit and Never {}
No object headers anywhere, so a Flan struct is exactly its C struct and nothing marshals. Two consequences carry the semantics:
- Every slot is an
alloca. Reading a local is aload, assigning is astore, and astoreof an aggregate is the copyspec-memory.mdrequires — value structs and fixed arrays copy, a slice copies only its view.addrof a local is then just the alloca, andmem2regremoves the ones nobody addressed.test/programs/values.flanpins this down: mutate the original, the copy is unchanged. - A place is a pointer, a value is a load from it.
(set (.pos c) …)through a(Ptr Cursor)becomes agetelementptron the pointer, not on a copy. This is the split that would have made a tree-walker silently wrong.
Non-local exit is lowered explicitly: return and some are branches to a
ret, never platform unwinding, so wasm32 needs no exception proposal.
Next
- wasm32. The backend is there (
llclistswasm32) andBuild.opts.targetalready plumbs--target, but there is no wasi sysroot on this machine —clang --target=wasm32-wasicannot findstdio.h. Installwasi-sdk/wasi-libc, then run the same acceptance table on both targets in CI. That is milestone 3's real remaining work. - Bounds checks.
atandsliceemit a baregetelementptr. Dev builds should trap; release should not. - Then milestone 4 — sand.flan: fixed 2-D arrays (done),
dotimes,defer, and typed raylib FFI with keyword→enum coercion.
Watch for
The rule that caught the two misparse bugs applies unchanged: anything that
binds a name, alters control flow, or is not yet implemented must be recognised
explicitly and rejected if unsupported. check.ml rejects Vec, Map,
Result/try, union values, closures, dotimes, defer, keywords at call
sites, imports, generics and function values by name, each with the milestone
it belongs to. The tests assert on the reason, not just on the failure.
Untracked on purpose
old-ocaml/ — the pre-rewrite menhir/ocamllex frontend, kept as reference and
excluded from the build by the root dune file. Its contents are also in git
history at 2c232dd.