# 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 a `load`, assigning is a `store`, and a `store` of an aggregate *is* the copy `spec-memory.md` requires — value structs and fixed arrays copy, a slice copies only its view. `addr` of a local is then just the alloca, and `mem2reg` removes the ones nobody addressed. `test/programs/values.flan` pins 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 a `getelementptr` on 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 1. **wasm32.** The backend is there (`llc` lists `wasm32`) and `Build.opts.target` already plumbs `--target`, but there is **no wasi sysroot on this machine** — `clang --target=wasm32-wasi` cannot find `stdio.h`. Install `wasi-sdk`/`wasi-libc`, then run the same acceptance table on both targets in CI. That is milestone 3's real remaining work. 2. **Bounds checks.** `at` and `slice` emit a bare `getelementptr`. Dev builds should trap; release should not. 3. **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`.