flan/NEXT.md

115 lines
5.4 KiB
Markdown

# 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`.