Making progress
This commit is contained in:
parent
c476d05f7d
commit
542e5d5fbc
91
NEXT.md
91
NEXT.md
@ -137,9 +137,85 @@ the program does not break the test that reads it. It runs at both `-O0` and
|
||||
`call` to either failure function. (The two `declare`s stay in the header
|
||||
unconditionally; LLVM drops the unused ones.)
|
||||
|
||||
## The IR, inspected
|
||||
|
||||
Verified by reading `flan emit` output rather than by trusting the tests:
|
||||
|
||||
- Every check site is `icmp` → `br` → cold block → `call` → `unreachable`. The
|
||||
verifier accepts it; no dominance or phi problems.
|
||||
- `-O2` folds the two literal-index checks in `bounds.flan` and keeps the other
|
||||
seven, which is exactly the intent.
|
||||
- **A redundant check is not eliminated, and `index_ty` is why.** calc-me's
|
||||
`peek` already guards with `(< (.pos c) (len (.src c)))`, yet the bounds check
|
||||
survives `-O2`. `len` truncates the i64 length to i32 and the index is a
|
||||
*signed* i32, so the guard emits `icmp slt i32 %pos, (trunc %len)` while the
|
||||
check emits `icmp ult i64 (sext %pos), %len`. LLVM cannot bridge those and is
|
||||
right not to: the trunc loses bits above 2³¹, and `slt` does not imply
|
||||
`pos >= 0`. Lengths as i64, or unsigned indices, would let the two merge —
|
||||
but that is `index_ty`, a plan.org-level decision, so it is left alone.
|
||||
- Cost, measured: a 50M-iteration serial dependency chain over a 1024-element
|
||||
array, argv-seeded so nothing folds, runs at 0.11–0.12s checked against
|
||||
0.12–0.13s unchecked. Indistinguishable. The branch predicts perfectly and
|
||||
the loop is latency-bound.
|
||||
- Cosmetic: the fail block is emitted *before* the continuation block, so at
|
||||
`-O0` the cold path sits inline in the hot path. `cold` plus LLVM's block
|
||||
placement fixes it at `-O2`; nothing fixes it at `-O0`.
|
||||
|
||||
## Where build time goes
|
||||
|
||||
`flan build calc-me.flan` is ~140ms, and ~95% of it is clang:
|
||||
|
||||
| Step | Cost |
|
||||
|---|---|
|
||||
| frontend: read → parse → check → emit | <10ms, below the timer |
|
||||
| `clang` on the `.ll` | 60ms — `llc` does the same codegen in **20ms** |
|
||||
| `clang` on `flan_rt.c` | 40ms — recompiled every build, and it never changes |
|
||||
| link | 20ms |
|
||||
|
||||
Two cheap wins take it to ~60ms: cache `flan_rt.o`, and skip the clang driver
|
||||
for the `.ll` (`llc` + link directly). This reproduces plan.org's own
|
||||
measurement — the driver is the cost, not codegen — and it is a subset of the
|
||||
dev path's machinery, so doing it now is not wasted work.
|
||||
|
||||
**There is still no REPL.** Nothing in `lib/` or `bin/` does redefinition,
|
||||
`dlopen`, or nREPL; `build.ml`'s docstring describes the dev path and says
|
||||
nothing at milestone 2 needs it yet. `build` is the only way to run code, so
|
||||
its 140ms is what you actually pay.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
A lowercase name is a type variable (plan.org, Types), which meant a mistyped
|
||||
primitive — `f65` for `f64` — was reported as *unimplemented generics, see
|
||||
plan.org*, sending you to the plan instead of to the character you mistyped.
|
||||
`resolve_name` now tries `near_miss` first: one edit (substitution, insertion,
|
||||
deletion, or a transposed pair) against the primitives, aliases, structs and
|
||||
unions. Bounded at one edit, because two is a guess, and because a real
|
||||
single-letter type variable like `t` must still reach the milestone-5 message.
|
||||
|
||||
```
|
||||
(defn f [x f65]) unknown type f65 — did you mean f64?
|
||||
(defn f [x stirng]) unknown type stirng — did you mean string?
|
||||
(defn f [x t]) generic code over the type variable t … milestone 5
|
||||
(defn f [x Widget]) unknown type Widget
|
||||
```
|
||||
|
||||
`Types.primitive_names` exists now because the list had only ever been match
|
||||
arms. Caveat found while testing: a bare `(defn f [] f65 0.0)` says *unknown
|
||||
name* instead, because with a single body form the parser cannot tell a return
|
||||
type from the first expression. Only the parameter position and `(Option …)`
|
||||
are unambiguous.
|
||||
|
||||
## Next
|
||||
|
||||
1. **wasm32.** The backend is there (`llc` lists `wasm32`) and
|
||||
Recommended order — start with milestone 4; wasm32 needs a system install only
|
||||
you can authorize, and the REPL is worth more once there is a frame loop for it
|
||||
to not stutter.
|
||||
|
||||
1. **Milestone 4 — sand.flan.** `dotimes`, `defer`, and typed raylib FFI with
|
||||
keyword→enum coercion. Fixed 2-D arrays are done. `flan check sand.flan`
|
||||
already fails on `(import rl ...)`, as it should. FFI is the part most likely
|
||||
to expose layout bugs the calc-me surface cannot reach.
|
||||
2. **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` (`dnf search wasi` for the Fedora
|
||||
@ -148,8 +224,17 @@ unconditionally; LLVM drops the unused ones.)
|
||||
to survive the port — no unwinding, and `exit(134)` rather than `abort()`,
|
||||
so the same trap assertion should hold on wasm32 — but that is intent, not a
|
||||
tested result: nothing here has ever been built for wasm32.
|
||||
2. **Then milestone 4** — sand.flan: fixed 2-D arrays (done), `dotimes`,
|
||||
`defer`, and typed raylib FFI with keyword→enum coercion.
|
||||
WASI is the syscall interface wasm has to import to get stdout, argv and
|
||||
exit at all; `flan_rt.c` calls libc (`fwrite`, `snprintf`, `strtod`,
|
||||
`malloc`), so it needs `wasi-libc`. The alternative is a second freestanding
|
||||
shim that imports host functions directly and links no libc — which is what
|
||||
"one narrow host ABI, implemented twice" points at, and the ABI is small
|
||||
enough to make it plausible. Note plan.org has the *web* build linking
|
||||
raylib via emscripten, which brings its own sysroot: wasi-sdk is right for
|
||||
the headless acceptance table, not necessarily for the eventual game build.
|
||||
3. **The dev path / REPL.** `llc` + `ld -shared` + `dlopen` ≈ 16ms, a compiler
|
||||
daemon plus an in-game reload agent (plan.org, Dev architecture). The build
|
||||
wins above are a down payment on this.
|
||||
|
||||
## Watch for
|
||||
|
||||
|
||||
37
lib/check.ml
37
lib/check.ml
@ -120,6 +120,37 @@ let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
|
||||
fail loc
|
||||
"%s takes no type arguments — generics are milestone 5" name)
|
||||
|
||||
(* One edit away from a type that exists — a substitution, an insertion, a
|
||||
deletion or a transposition of neighbours. Bounded at one, because two edits
|
||||
is no longer a typo, it is a guess. *)
|
||||
and near_miss env n =
|
||||
let one_edit a b =
|
||||
let la = String.length a and lb = String.length b in
|
||||
if abs (la - lb) > 1 then false
|
||||
else begin
|
||||
(* Walk both until they diverge, then require the tails to match with the
|
||||
single edit applied. *)
|
||||
let i = ref 0 in
|
||||
while !i < la && !i < lb && a.[!i] = b.[!i] do incr i done;
|
||||
let ta s k = String.sub s k (String.length s - k) in
|
||||
if la = lb then
|
||||
!i < la
|
||||
&& (ta a (!i + 1) = ta b (!i + 1)
|
||||
(* stirng/string: two neighbours swapped. *)
|
||||
|| (!i + 1 < la && a.[!i] = b.[!i + 1] && a.[!i + 1] = b.[!i]
|
||||
&& ta a (!i + 2) = ta b (!i + 2)))
|
||||
else if la < lb then ta a !i = ta b (!i + 1)
|
||||
else ta a (!i + 1) = ta b !i
|
||||
end
|
||||
in
|
||||
let candidates =
|
||||
Types.primitive_names
|
||||
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.aliases []
|
||||
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.structs []
|
||||
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.unions []
|
||||
in
|
||||
List.find_opt (fun c -> c <> n && one_edit n c) candidates
|
||||
|
||||
and resolve_name env ~seen loc n =
|
||||
match Types.ikind_of_name n with
|
||||
| Some k -> Types.Int k
|
||||
@ -138,6 +169,12 @@ and resolve_name env ~seen loc n =
|
||||
else resolve env ~seen:(n :: seen) (Hashtbl.find env.aliases n)
|
||||
| _ when Hashtbl.mem env.structs n || Hashtbl.mem env.unions n ->
|
||||
Types.Named n
|
||||
(* A typo in a primitive is lowercase too, and the type-variable rule
|
||||
below would otherwise report [f65] as unimplemented generics and send
|
||||
you to plan.org instead of to the character you mistyped. *)
|
||||
| _ when near_miss env n <> None ->
|
||||
fail loc "unknown type %s — did you mean %s?" n
|
||||
(Option.get (near_miss env n))
|
||||
(* Lowercase is a type variable, Capitalized is concrete — no sigil
|
||||
(plan.org, Types). A variable parses, but nothing at milestone 2 can
|
||||
give a value one, so it is rejected here rather than later. *)
|
||||
|
||||
@ -48,6 +48,12 @@ let ikind_of_name = function
|
||||
let fkind_of_name = function
|
||||
| "f32" -> Some F32 | "f64" -> Some F64 | _ -> None
|
||||
|
||||
(* Every name the resolver accepts as a primitive type. The list exists so a
|
||||
near-miss can be reported as the typo it is. *)
|
||||
let primitive_names =
|
||||
[ "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "u64";
|
||||
"f32"; "f64"; "bool"; "string"; "Unit"; "Never" ]
|
||||
|
||||
let ikind_name k =
|
||||
(if signed k then "i" else "u") ^ string_of_int (bits k)
|
||||
|
||||
|
||||
@ -403,6 +403,24 @@ let () =
|
||||
rejects_check "if branches disagree"
|
||||
"(defn f [] i32 (if true 1 true))" ~needle:"expected i32";
|
||||
|
||||
(* ── Unknown types ─────────────────────────────────────────────── *)
|
||||
(* A lowercase name is a type variable (plan.org, Types), so a mistyped
|
||||
primitive would otherwise be reported as unimplemented generics and send
|
||||
you to plan.org instead of to the character you mistyped. *)
|
||||
rejects_check "a mistyped primitive" "(defn f [x f65])"
|
||||
~needle:"did you mean f64?";
|
||||
rejects_check "a transposed primitive" "(defn f [x stirng])"
|
||||
~needle:"did you mean string?";
|
||||
rejects_check "a mistyped struct"
|
||||
"(defstruct Cursor [x i32]) (defn f [c Curser])"
|
||||
~needle:"did you mean Cursor?";
|
||||
(* Nothing close: the type-variable rule still applies, and still names the
|
||||
milestone. *)
|
||||
rejects_check "a real type variable" "(defn f [x t])"
|
||||
~needle:"milestone 5";
|
||||
rejects_check "an unknown concrete type" "(defn f [x Widget])"
|
||||
~needle:"unknown type Widget";
|
||||
|
||||
(* ── Static bounds ─────────────────────────────────────────────── *)
|
||||
(* A literal index into a fixed array is known now, so it is an error now
|
||||
rather than a trap later; everything else is the emitted bounds check's
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user