115 lines
5.4 KiB
Markdown
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`.
|