flan/NEXT.md
Joseph Ferano c64e91bdfa Add AST and forms->AST parser
Second stage of the milestone-2 frontend. calc-me.flan (12 decls) and
sand.flan (20 decls) both parse end to end, and both are test deps so a
regression fails `dune test` rather than surfacing at the CLI.

Three silent-misparse bugs fixed along the way -- all cases that read
cleanly and meant something else:

- dotimes/defer/some/try/fn fell through to Call, discarding their
  binding and control-flow meaning. Now special forms. Forms from later
  milestones (handler-bind, restart-case, loop/recur, defmacro, signal,
  with-allocator, errdefer, await) are rejected outright rather than
  parsed as calls.
- (Some 1) in first body position was read as a return type, because
  (Option f64) and (Some 1) are identical s-expressions and the
  heuristic was capitalisation. Now decided by the set of names actually
  declared as types, collected in a pre-pass -- exact, and
  order-independent so a type declared below its user still resolves.
- Array literals in value position were rejected outright.

Also adds NEXT.md with the handoff for the checker.
2026-09-10 14:56:35 +07:00

2.7 KiB

Where this is

Milestone 2 of the build sequence in plan.org: run calc-me.flan on the interpreter. Two of four stages exist.

reader ✅ → parse ✅ → check ⬜ → interpret ⬜
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
bin/main.ml flan read <file> and flan parse <file>
test/test_flan.ml 110+ assertions; calc-me.flan and sand.flan are deps

dune build && dune test is green. flan parse calc-me.flan and flan parse sand.flan both succeed.

Next: lib/types.ml and the checker

  1. Type representation. Resolve Ast.texpr into a real type. Needs the declared-type environment Parse.declared_types already computes — that pre-pass exists and should be reused rather than rebuilt.
  2. Top-level environment. Two passes, because top-level names are order-independent (plan.org, Modules): collect all signatures, then check bodies.
  3. Check calc-me.flan. It needs: i32/u8/f64/bool, structs, [u8] slices, (Ptr T) with one level of auto-deref on .field, (Option T) with Some/None, Unwrap (Usome, _) as early-return-None, while, return, set on the five places, match on Option, and the milestone-2 primitive list in plan.org.
  4. Then the interpreter over the typed IR.

Watch for

The two bugs found so far were both silent misparses — code that read fine and meant something else:

  • 'skip-form became a symbol named 'skip-form
  • dotimes/defer/some fell through to Call, discarding their binding and control-flow meaning
  • (Some 1) in first body position was eaten as a return type

The rule that catches this class: anything that binds a name, alters control flow, or is not yet implemented must be recognised explicitly and rejected if unsupported — never allowed to fall through to a generic case. parse.ml rejects handler-bind, restart-case, loop/recur, defmacro, signal, with-allocator, errdefer and await for exactly this reason. Keep doing that in the checker.

Open decisions that touch the checker

None block milestone 2. plan.org tags each open decision with the milestone it is due by; #1 (host language) is now settled as OCaml.

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.