From 26c53e0a19e5cf55458c1e00c95f2a2ff24149db Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 23:06:40 +0700 Subject: [PATCH] Every defn in the tree states its return type, and Unit is written () The mechanical half, ahead of the parser change that needs it. tools/unit-return.py fills the empty slot with () and rewrites Unit as () wherever a type is spelled -- (Fn [i32] Unit), (Map i32 Unit), a return type written out. Deciding whether a defn already had a return type is the whole difficulty, and the script does it the way parse.ml did: is_type_form is transcribed rather than improved, because being identical to the parser it replaces is what makes the sweep meaning-preserving. It is re-runnable, so the lanes that branched before this can have the same pass at merge: python3 tools/unit-return.py . python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \ test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el python3 tools/unit-return.py --raw-ml lib/prelude.ml python3 tools/unit-return.py --in-html web/index.html -v logs every defn it saw and what it decided, which is how a sweep of 440 sites gets reviewed at all. Embedded modes pool a file's type declarations across all its fragments, because a snippet split across concatenation -- decls ^ "(defn f [s [u8]] Cursor ...)" -- cannot see the names the other half declared; pooled names count only in bare-symbol position, for the same reason the prelude's do. A fragment that cuts off mid-form is skipped rather than guessed at. Five sites in test_flan.ml still needed a hand, and they are in this commit. Two things ride along because the sweep needs them: parse.ml reads a lone () as the return type of a function with no body, which was not a shape the old optional slot could produce; and the map refusals name () rather than Unit, since that is now the spelling a caller wrote. --- calc-me.flan | 4 +- conditions-play.flan | 2 +- emacs/test-flan-mode.el | 6 +- examples/core-basic-window.flan | 2 +- examples/core-delta-time.flan | 2 +- examples/core-input-gamepad.flan | 8 +- examples/core-input-gestures-testbed.flan | 12 +- examples/core-input-gestures.flan | 2 +- examples/core-input-keys.flan | 2 +- examples/core-input-mouse-wheel.flan | 2 +- examples/core-input-mouse.flan | 2 +- examples/core-input-multitouch.flan | 2 +- examples/core-input-virtual-controls.flan | 6 +- lib/check.ml | 6 +- lib/parse.ml | 4 + lib/prelude.ml | 34 +- sand.flan | 56 +-- syntax-sketch.flan | 10 +- test/programs/algorithms.flan | 4 +- test/programs/bytes2.flan | 6 +- test/programs/cleanup.flan | 2 +- test/programs/conditions.flan | 2 +- test/programs/defer-let.flan | 8 +- test/programs/destructure.flan | 2 +- test/programs/edn.flan | 6 +- test/programs/fn-values.flan | 6 +- test/programs/format.flan | 2 +- test/programs/machine.flan | 2 +- test/programs/map-iter.flan | 10 +- test/programs/math.flan | 2 +- test/programs/math2.flan | 2 +- test/programs/println.flan | 2 +- test/programs/raylib-audio.flan | 8 +- test/programs/raylib-ffi.flan | 10 +- test/programs/raylib-font.flan | 10 +- test/programs/raylib-image.flan | 8 +- test/programs/slices.flan | 4 +- test/programs/string-of-bytes.flan | 2 +- test/programs/strings.flan | 2 +- test/programs/text.flan | 2 +- test/programs/unit-main.flan | 2 +- test/programs/utf8.flan | 10 +- test/test_acceptance.ml | 36 +- test/test_flan.ml | 148 +++--- test/test_session.ml | 4 +- tools/unit-return.py | 536 ++++++++++++++++++++++ vendor/edn/edn.flan | 4 +- web/examples/arrays.flan | 2 +- web/examples/boom.flan | 2 +- web/examples/bounds.flan | 2 +- web/examples/breakdemo.flan | 2 +- web/examples/conds.flan | 4 +- web/examples/control.flan | 4 +- web/examples/defer.flan | 2 +- web/examples/enums.flan | 2 +- web/examples/ffi.flan | 2 +- web/examples/globals.flan | 2 +- web/examples/hello.flan | 2 +- web/examples/option.flan | 2 +- web/examples/pkg.flan | 2 +- web/examples/places.flan | 2 +- web/examples/printing.flan | 2 +- web/examples/restart.flan | 2 +- web/examples/shimdemo.flan | 2 +- web/examples/structs.flan | 4 +- web/index.html | 38 +- 66 files changed, 814 insertions(+), 270 deletions(-) create mode 100755 tools/unit-return.py diff --git a/calc-me.flan b/calc-me.flan index bd099b7..f5fc8fb 100644 --- a/calc-me.flan +++ b/calc-me.flan @@ -27,10 +27,10 @@ (at (.src c) (.pos c)) 0)) ; 0 doubles as end-of-input -(defn advance [c (Ptr Cursor)] +(defn advance [c (Ptr Cursor)] () (set (.pos c) (+ (.pos c) 1))) ; field access auto-derefs one level -(defn skip-spaces [c (Ptr Cursor)] +(defn skip-spaces [c (Ptr Cursor)] () (while (= (peek c) \space) (advance c))) diff --git a/conditions-play.flan b/conditions-play.flan index 291598a..05e0451 100644 --- a/conditions-play.flan +++ b/conditions-play.flan @@ -29,7 +29,7 @@ (use-placeholder [] -1) (retry [] 7))) -(defn run-once [] +(defn run-once [] () (print (fetch 1)) (println "") (handler-bind [(AssetMissing [c] (set seen (+ seen (i64 (.id c)))))] diff --git a/emacs/test-flan-mode.el b/emacs/test-flan-mode.el index 37e5e42..25435c3 100644 --- a/emacs/test-flan-mode.el +++ b/emacs/test-flan-mode.el @@ -70,7 +70,7 @@ ;; the first argument — `(+ gravity …)' — instead of under `vel'. (test-flan-mode--check "a let's bindings align name under name" - "(defn settle [row i32 col i32] + "(defn settle [row i32 col i32] () (let [vel (+ gravity (at velocity row col)) y (min (- rows 1) (+ row (i32 vel)))] (while (> y row) @@ -117,7 +117,7 @@ (test-flan-mode--check "and a defn without one indents it the same" - "(defn show-trim [s string] + "(defn show-trim [s string] () (print s) (println \"\"))") @@ -127,7 +127,7 @@ "a wrapped parameter list aligns under the first parameter" "(defn move-grain [row i32 col i32 to-row i32 to-col i32 - vel f32] + vel f32] () (set moved true))") ;; `handler-bind' clauses: the vector is the special argument, and each clause diff --git a/examples/core-basic-window.flan b/examples/core-basic-window.flan index 6afbeec..811d2f0 100644 --- a/examples/core-basic-window.flan +++ b/examples/core-basic-window.flan @@ -17,7 +17,7 @@ (defconst screen-width 800) (defconst screen-height 450) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - basic window") (defer (rl/close-window)) diff --git a/examples/core-delta-time.flan b/examples/core-delta-time.flan index dc24721..af61d90 100644 --- a/examples/core-delta-time.flan +++ b/examples/core-delta-time.flan @@ -35,7 +35,7 @@ (defvar frame-circle rl/Vector2) (defvar current-fps i32) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - delta time") (defer (rl/close-window)) diff --git a/examples/core-input-gamepad.flan b/examples/core-input-gamepad.flan index 6e4b574..fdde691 100644 --- a/examples/core-input-gamepad.flan +++ b/examples/core-input-gamepad.flan @@ -90,7 +90,7 @@ (defn axis-at [pad i32 index i32] f32 (rl/get-gamepad-axis-movement pad (rl/GamepadAxis index))) -(defn draw-pad-background [] +(defn draw-pad-background [] () (rl/draw-rectangle-rounded (rl/Rectangle {.x 175.0 .y 110.0 .width 460.0 .height 220.0}) 0.3 16 rl/darkgray) @@ -119,7 +119,7 @@ (rl/Rectangle {.x 495.0 .y 98.0 .width 100.0 .height 10.0}) 0.5 16 rl/darkgray)) -(defn draw-pad-buttons [] +(defn draw-pad-buttons [] () (when (rl/gamepad-button-down? gamepad :middle-left) (rl/draw-circle 365 170 10.0 rl/red)) (when (rl/gamepad-button-down? gamepad :middle) @@ -153,13 +153,13 @@ (rl/Rectangle {.x 495.0 .y 98.0 .width 100.0 .height 10.0}) 0.5 16 rl/red))) -(defn draw-stick [cx i32 cy i32 ax f32 ay f32 thumb-down bool] +(defn draw-stick [cx i32 cy i32 ax f32 ay f32 thumb-down bool] () (rl/draw-circle cx cy 40.0 rl/black) (rl/draw-circle cx cy 35.0 rl/lightgray) (rl/draw-circle (+ cx (i32 (* ax 20.0))) (+ cy (i32 (* ay 20.0))) 25.0 (if thumb-down rl/red rl/black))) -(defn main [] +(defn main [] () ;; Before init-window, and it has to be: raylib reads the flags while it is ;; creating the context, so the same call afterwards is accepted, logged, and ;; has no effect on the window that already exists. diff --git a/examples/core-input-gestures-testbed.flan b/examples/core-input-gestures-testbed.flan index 833968c..3721bf9 100644 --- a/examples/core-input-gestures-testbed.flan +++ b/examples/core-input-gestures-testbed.flan @@ -147,7 +147,7 @@ (= log-mode 1) (not (= g previous-gesture)) :else true)) -(defn push-log [g rl/Gesture] +(defn push-log [g rl/Gesture] () (set previous-gesture g) (set gesture-color (gesture-color-of g)) (when (<= gesture-log-index 0) (set gesture-log-index gesture-log-size)) @@ -162,11 +162,11 @@ (defconst prot-y f32 315.0) (defconst angle-length f32 90.0) -(defn swipe-box [gx i32 gy i32 which rl/Gesture] +(defn swipe-box [gx i32 gy i32 which rl/Gesture] () (rl/draw-rectangle gx gy 20 20 (if (= last-gesture which) rl/red rl/lightgray))) -(defn draw-last-gesture [touch-count i32] +(defn draw-last-gesture [touch-count i32] () (rl/draw-text "Last gesture" (+ last-x 33) (- last-y 47) 20 rl/black) (rl/draw-text "Swipe Tap Pinch Touch" (+ last-x 17) (- last-y 18) 10 rl/black) @@ -213,7 +213,7 @@ (rl/draw-circle (+ last-x 180) (+ (+ last-y 7) (* i 15)) 5.0 (if (<= touch-count i) rl/lightgray gesture-color)))) -(defn draw-log [] +(defn draw-log [] () (rl/draw-text "Log" 10 10 20 rl/black) ;; Forward from the newest, wrapping — the inverted queue read the right way ;; round. @@ -234,7 +234,7 @@ (rl/draw-text "Hide" 115 10 10 rl/white) (rl/draw-text "Hold" 115 20 10 rl/white))) -(defn draw-protractor [] +(defn draw-protractor [] () (rl/draw-text "Angle" (+ (i32 prot-x) 55) (+ (i32 prot-y) 76) 10 rl/black) ;; The C's TextFormat/TextFindIndex/TextSubtext dance to get two decimals, ;; in one call. It rounds where the C truncated, so the last digit can @@ -272,7 +272,7 @@ .y (+ (* angle-length (cos-f32 rad)) prot-y)}) 3.0 gesture-color)))) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - input gestures testbed") (defer (rl/close-window)) diff --git a/examples/core-input-gestures.flan b/examples/core-input-gestures.flan index 3c97431..5f9ac39 100644 --- a/examples/core-input-gestures.flan +++ b/examples/core-input-gestures.flan @@ -52,7 +52,7 @@ (= g :pinch-out) "GESTURE PINCH OUT" :else "")) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - input gestures") (defer (rl/close-window)) diff --git a/examples/core-input-keys.flan b/examples/core-input-keys.flan index 6eabef1..635394a 100644 --- a/examples/core-input-keys.flan +++ b/examples/core-input-keys.flan @@ -21,7 +21,7 @@ (defvar ball rl/Vector2) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - input keys") (defer (rl/close-window)) diff --git a/examples/core-input-mouse-wheel.flan b/examples/core-input-mouse-wheel.flan index 1cebf26..825fb92 100644 --- a/examples/core-input-mouse-wheel.flan +++ b/examples/core-input-mouse-wheel.flan @@ -27,7 +27,7 @@ (defvar box-y i32) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - input mouse wheel") (defer (rl/close-window)) diff --git a/examples/core-input-mouse.flan b/examples/core-input-mouse.flan index a6d5d51..206d689 100644 --- a/examples/core-input-mouse.flan +++ b/examples/core-input-mouse.flan @@ -19,7 +19,7 @@ (defvar ball-color rl/Color) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - input mouse") (defer (rl/close-window)) diff --git a/examples/core-input-multitouch.flan b/examples/core-input-multitouch.flan index 5345054..2bcad18 100644 --- a/examples/core-input-multitouch.flan +++ b/examples/core-input-multitouch.flan @@ -31,7 +31,7 @@ (defvar touch-positions [max-touch-points rl/Vector2]) -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - input multitouch") (defer (rl/close-window)) diff --git a/examples/core-input-virtual-controls.flan b/examples/core-input-virtual-controls.flan index aba327b..5fdff19 100644 --- a/examples/core-input-virtual-controls.flan +++ b/examples/core-input-virtual-controls.flan @@ -69,7 +69,7 @@ (defvar player rl/Vector2) -(defn reset-player [] +(defn reset-player [] () (set player (rl/Vector2 {.x (/ (f32 screen-width) 2.0) .y (/ (f32 screen-height) 2.0)}))) @@ -91,7 +91,7 @@ ;; The C's switch on the pressed button. Nothing moves for button-none, which ;; is the `default: break`. -(defn move-player [button i32 dt f32] +(defn move-player [button i32 dt f32] () (let [step (* player-speed dt)] (cond (= button button-up) (set (.y player) (- (.y player) step)) @@ -149,7 +149,7 @@ (rl/Color {.r 230 .g 41 .b 55 .a 255}) ; red, right (rl/Color {.r 0 .g 228 .b 48 .a 255})]) ; green, down -(defn main [] +(defn main [] () (rl/init-window screen-width screen-height "raylib [core] example - input virtual controls") (defer (rl/close-window)) diff --git a/lib/check.ml b/lib/check.ml index b07e489..d22ad87 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -326,17 +326,17 @@ let map_type loc (k : Types.t) (v : Types.t) = copying, and free would leak what they own. Owned entries arrive with \ drop (step 5 in NEXT.md)" (Types.to_string k) (Types.to_string v); - (* Unit has no bytes, so a slot for one is a slot of nothing: the cell + (* () has no bytes, so a slot for one is a slot of nothing: the cell geometry divides the cache line by the element size and there is nothing to divide by. It is also the natural spelling of a *set*, which is why someone will write it, so it is refused by name rather than by a crash. *) if Types.equal v Types.Unit then fail loc - "a map value cannot be Unit — there is nothing to store. A set of keys \ + "a map value cannot be () — there is nothing to store. A set of keys \ is not built yet; use (Map %s bool) and ignore the value" (Types.to_string k); if Types.equal k Types.Unit then - fail loc "a map key cannot be Unit — every key would be the same key"; + fail loc "a map key cannot be () — every key would be the same key"; (* The key, as far as the type alone can say. A struct passes here and is decided at the operation, by [key_pair], which walks its fields — the struct table is not necessarily complete while a type is being resolved, diff --git a/lib/parse.ml b/lib/parse.ml index 7d3153d..2963017 100644 --- a/lib/parse.ml +++ b/lib/parse.ml @@ -731,6 +731,10 @@ let rec decl types (f : Form.t) : Ast.decl = (* An omitted return type means Unit. A leading form that is a type and is not the whole body is the return type. *) | [] -> None, [] + (* A lone [()] is the return type and an empty body, never a body of + one form: [()] is not an expression, so there is nothing for the + [more <> []] guard below to protect here. *) + | [ ({ v = List []; _ } as only) ] -> Some (texpr only), [] | first :: more when more <> [] && is_type_form types first -> Some (texpr first), body_of more | _ -> None, body_of rest diff --git a/lib/prelude.ml b/lib/prelude.ml index 03b6c48..1e0b8b9 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -62,7 +62,7 @@ let source = {flan| ;; behaviour a release build wants and gets for free. (defstruct Pause []) -(defn pause [] +(defn pause [] () (restart-case (error (Pause {})) (continue [] (do)))) @@ -72,7 +72,7 @@ let source = {flan| ;; down to 32 bits by an xorshift and rotated by the state's top five bits. (defvar rand-state u64 6364136223846793005) -(defn rand-seed [seed u64] +(defn rand-seed [seed u64] () (set rand-state (+ (* seed 6364136223846793005) 1442695040888963407))) (defn rand-u32 [] u32 @@ -111,12 +111,12 @@ let source = {flan| ;; shape and the same argument — so the set is the same i32 and f32 the rest of ;; this family covers. -(defn swap-i32! [s [i32] i i32 j i32] +(defn swap-i32! [s [i32] i i32 j i32] () (let [t (at s i)] (set (at s i) (at s j)) (set (at s j) t))) -(defn reverse-i32! [s [i32]] +(defn reverse-i32! [s [i32]] () (let [i 0 j (- (len s) 1)] (while (< i j) @@ -128,7 +128,7 @@ let source = {flan| ;; comparison function — quicksort would want a stack and mergesort a buffer, ;; and neither exists. Ascending, and stable, though with no payload type to ;; carry that is not yet observable. -(defn sort-i32! [s [i32]] +(defn sort-i32! [s [i32]] () (let [i 1] (while (< i (len s)) (let [j i] @@ -190,12 +190,12 @@ let source = {flan| ;; also the only fix, since there is no ordering of the reals that a NaN sits ;; anywhere in. -(defn swap-f32! [s [f32] i i32 j i32] +(defn swap-f32! [s [f32] i i32 j i32] () (let [t (at s i)] (set (at s i) (at s j)) (set (at s j) t))) -(defn reverse-f32! [s [f32]] +(defn reverse-f32! [s [f32]] () (let [i 0 j (- (len s) 1)] (while (< i j) @@ -203,7 +203,7 @@ let source = {flan| (set i (+ i 1)) (set j (- j 1))))) -(defn sort-f32! [s [f32]] +(defn sort-f32! [s [f32]] () (let [i 1] (while (< i (len s)) (let [j i] @@ -268,11 +268,11 @@ let source = {flan| ;; lifted into a function of its own and sees its parameters and the globals ;; and nothing else. -(defn map-i32! [s [i32] f (Fn [i32] i32)] +(defn map-i32! [s [i32] f (Fn [i32] i32)] () (dotimes [i (len s)] (set (at s i) (f (at s i))))) -(defn map-f32! [s [f32] f (Fn [f32] f32)] +(defn map-f32! [s [f32] f (Fn [f32] f32)] () (dotimes [i (len s)] (set (at s i) (f (at s i))))) @@ -317,7 +317,7 @@ let source = {flan| ;; strict weak ordering — one answering true for both (a b) and (b a) — is the ;; caller's mistake and shows up as an order, not as a loop: the inner while is ;; bounded by j reaching 0 whatever the comparison says. -(defn sort-i32-by! [s [i32] before? (Fn [i32 i32] bool)] +(defn sort-i32-by! [s [i32] before? (Fn [i32 i32] bool)] () (let [i 1] (while (< i (len s)) (let [j i] @@ -327,7 +327,7 @@ let source = {flan| (set j (- j 1)))) (set i (+ i 1))))) -(defn sort-f32-by! [s [f32] before? (Fn [f32 f32] bool)] +(defn sort-f32-by! [s [f32] before? (Fn [f32 f32] bool)] () (let [i 1] (while (< i (len s)) (let [j i] @@ -996,7 +996,7 @@ let source = {flan| (return (< (at a i) (at b i))))) (< (len a) (len b)))) -(defn swap-bytes! [s [[u8]] i i32 j i32] +(defn swap-bytes! [s [[u8]] i i32 j i32] () (let [t (at s i)] (set (at s i) (at s j)) (set (at s j) t))) @@ -1006,7 +1006,7 @@ let source = {flan| ;; fields borrowed from one buffer without touching the buffer. Stable, and ;; here that is observable — two equal fields are two distinct slices of ;; different parts of the input, and a caller can see which one came first. -(defn sort-bytes! [s [[u8]]] +(defn sort-bytes! [s [[u8]]] () (let [i 1] (while (< i (len s)) (let [j i] @@ -1051,7 +1051,7 @@ let source = {flan| ;; It takes a (Ptr (Vec u8)) and not a (Vec u8), and the difference is not ;; style: a Vec parameter *moves*, so (append! b s) taking one by value would ;; consume the caller's builder on the first call and refuse the second. -(defn append! [b (Ptr (Vec u8)) s [u8]] +(defn append! [b (Ptr (Vec u8)) s [u8]] () (dotimes [i (len s)] (push (deref b) (at s i)))) @@ -1062,10 +1062,10 @@ let source = {flan| ;; views of the same bytes — the second call overwrote the first. These copy ;; out of that buffer before returning, so the hazard ends at the call: a ;; builder can hold as many numbers as it likes. -(defn append-i64! [b (Ptr (Vec u8)) n i64] +(defn append-i64! [b (Ptr (Vec u8)) n i64] () (append! b (i64->bytes n))) -(defn append-f64! [b (Ptr (Vec u8)) x f64] +(defn append-f64! [b (Ptr (Vec u8)) x f64] () (append! b (f64->bytes x))) ;; concat and join. Both take a slice of slices, which is the shape a caller diff --git a/sand.flan b/sand.flan index 7aff84f..3338764 100644 --- a/sand.flan +++ b/sand.flan @@ -59,20 +59,20 @@ ;; An index into colors, not a colour. (defvar current-color i32) -(defn clear-grid [] +(defn clear-grid [] () (set grid (zeroed)) (set velocity (zeroed))) (defn empty-at? [row i32 col i32] bool (= 0 (at grid row col))) -(defn next-color [] +(defn next-color [] () (set current-color (% (+ current-color 1) (len colors)))) ;; Drop a brush-sized cloud of grains centred on [row col]. This is what the ;; mouse drives interactively and what the headless run calls directly — the ;; only difference between the two is where the centre comes from. -(defn paint-at [row i32 col i32] +(defn paint-at [row i32 col i32] () (let [half (/ brush-size 2)] (dotimes [x brush-size] (dotimes [y brush-size] @@ -87,7 +87,7 @@ (defn move-grain [from-row i32 from-col i32 to-row i32 to-col i32 - vel f32] + vel f32] () (set (at grid to-row to-col) (at grid from-row from-col)) (set (at grid from-row from-col) 0) (set (at velocity to-row to-col) vel) @@ -99,7 +99,7 @@ ;; Imperative `while` with early `return`, not loop/recur — see plan.org ;; "Loop story". The recur version read as a tail call but was a countdown ;; over a mutable scan position, which is what a while loop is. -(defn settle [row i32 col i32] +(defn settle [row i32 col i32] () (let [vel (+ gravity (at velocity row col)) some-point (rl/Vector2 {.x 15.0 .y 12}) y (min (- rows 1) (+ row (i32 vel)))] @@ -122,7 +122,7 @@ (set (at velocity row col) 0.0))) ;; One frame of physics. Bottom-up, so a grain settles at most once per frame. -(defn step [] +(defn step [] () (let [row (- rows 2)] (while (>= row 0) (dotimes [col cols] @@ -193,7 +193,7 @@ ;; because ImageFlipHorizontal rewrites the buffer in place and the second ;; upload has to see the changed pixels — if the two badges look the same, ;; either the flip did nothing or the order here was swapped. -(defn load-brush [] +(defn load-brush [] () (let [sheet (rl/load-image-from-memory ".png" brush-png)] (when (rl/image-valid? sheet) (set brush (rl/load-texture-from-image sheet)) @@ -212,7 +212,7 @@ ;; needs a source rectangle; the three badges in the corner are the whole ;; sheet, at integer coordinates, at a Vector2 tinted with the current sand ;; colour, and scaled up — draw-texture, draw-texture-v and draw-texture-ex. -(defn draw-brush [] +(defn draw-brush [] () (when brush-ok (let [m (rl/get-mouse-position) frame (f32 (if (rl/mouse-button-down? :left) 8.0 0.0))] @@ -256,14 +256,14 @@ ;; a run-time number, and no Flan type says what the buffer holds. (defvar tone-pcm [8820 u8]) -(defn write-sample [i i32 v i32] +(defn write-sample [i i32 v i32] () (set (at tone-pcm (* i 2)) (u8 (bit-and v 255))) (set (at tone-pcm (+ (* i 2) 1)) (u8 (bit-and (>> v 8) 255)))) ;; A square wave that decays to nothing over its length, which is the ;; cheapest thing that sounds like a plink rather than a click. `period` is ;; the half-period in frames, so a smaller one is a higher note. -(defn build-tone [period i32] +(defn build-tone [period i32] () (dotimes [i tone-frames] (let [amp (/ (* 9000 (- tone-frames i)) tone-frames)] (write-sample i (if (= 0 (% (/ i period) 2)) amp (- 0 amp)))))) @@ -275,7 +275,7 @@ (defvar music-ok bool) (defvar music-on bool) -(defn start-audio [] +(defn start-audio [] () (rl/init-audio-device) (set audio-ok (rl/audio-device-ready?)) (unless audio-ok @@ -303,19 +303,19 @@ (rl/set-music-pitch music 0.5))))) (rl/set-master-volume 0.6)) -(defn stop-audio [] +(defn stop-audio [] () (when music-ok (rl/unload-music-stream music)) (when tone-ok (rl/unload-sound tone)) (rl/close-audio-device)) ;; Every plink goes through here, so a silent build — no device, or a wave ;; that would not load — is one branch and not a guard at every call site. -(defn plink [pitch f32] +(defn plink [pitch f32] () (when tone-ok (rl/set-sound-pitch tone pitch) (rl/play-sound tone))) -(defn toggle-music [] +(defn toggle-music [] () (when music-ok (set music-on (not music-on)) (if music-on @@ -339,7 +339,7 @@ (defvar scene rl/RenderTexture2D) (defvar scene-ok bool) -(defn load-scene [] +(defn load-scene [] () (set scene (rl/load-render-texture screen-width screen-height)) (set scene-ok (rl/render-texture-valid? scene)) (unless scene-ok @@ -357,7 +357,7 @@ (defvar hud-font rl/Font) (defvar hud-font-ok bool) -(defn load-hud-font [] +(defn load-hud-font [] () (set hud-font (rl/get-font-default)) (set hud-font-ok (rl/font-valid? hud-font))) @@ -375,13 +375,13 @@ ;; A fresh (Camera2D {}) has a zoom of 0, which is singular: both conversions ;; hand back NaN and nothing draws. 1.0 is the identity. -(defn reset-view [] +(defn reset-view [] () (set view (rl/Camera2D {.offset (rl/Vector2 {.x 0.0 .y 0.0}) .target (rl/Vector2 {.x 0.0 .y 0.0}) .rotation 0.0 .zoom 1.0}))) -(defn set-view [target-x f32 target-y f32 zoom f32] +(defn set-view [target-x f32 target-y f32 zoom f32] () (set view (rl/Camera2D {.offset (.offset view) .target (rl/Vector2 {.x target-x .y target-y}) .rotation (.rotation view) @@ -391,7 +391,7 @@ ;; neither changes with the frame rate. That is the whole of what ;; get-frame-time is for, and a loop that assumed it hit its target fps would ;; be a loop that moves differently on a slower machine. -(defn move-view [] +(defn move-view [] () (let [dt (rl/get-frame-time) pan (* (f32 600.0) dt) tx (.x (.target view)) @@ -414,7 +414,7 @@ ;; The mouse is in screen pixels and the grid is in world cells, and with a ;; camera in the way those stopped being the same thing — so this is the one ;; place get-screen-to-world-2d is not a test case but a requirement. -(defn paint [] +(defn paint [] () (let [m (rl/get-screen-to-world-2d (rl/get-mouse-position) view) row (/ (i32 (.y m)) cell-size) col (/ (i32 (.x m)) cell-size)] @@ -430,7 +430,7 @@ ;; `settle` while `game-update` is mid-frame is safe ;; because old code is never unloaded; changing its SIGNATURE is not, and the ;; reload rejects it. See plan.org "What redefinition cannot do". -(defn game-update [] +(defn game-update [] () (when (rl/key-pressed? :r) (clear-grid)) (move-view) ;; key-released? and mouse-button-pressed? were bound and called by nothing @@ -457,7 +457,7 @@ (step)) -(defn draw-grid [] +(defn draw-grid [] () (dotimes [row rows] (dotimes [col cols] (let [c (at grid row col)] @@ -471,7 +471,7 @@ ;; scales with the grid. That is the point: a shape binding that is subtly ;; wrong is easiest to see when it is supposed to sit exactly on the cursor ;; and does not. -(defn draw-world-cursor [] +(defn draw-world-cursor [] () (let [p (rl/get-screen-to-world-2d (rl/get-mouse-position) view) tint (rl/get-color (at colors current-color)) x (.x p) @@ -513,7 +513,7 @@ ;; so it is built to be *looked* at: every shape binding appears once, and each ;; one is asymmetric enough that a wrapper with its arguments crossed is ;; visible rather than merely different. -(defn draw-hud [] +(defn draw-hud [] () (let [title "SAND" keys "arrows pan , . zoom 0 reset r clear space colour m music" ;; measure-text is what sizes the panel, so the box fits the string @@ -619,7 +619,7 @@ ;; the marker. Every read-out below is asymmetric on purpose — the stick dot ;; is offset by x and y separately, and the trigger bars are different lengths ;; — so a crossed wrapper is visible rather than merely different. -(defn draw-input-state [] +(defn draw-input-state [] () (let [ox (f32 200.0) oy (f32 90.0) r (f32 34.0)] @@ -683,14 +683,14 @@ .y (+ oy (* (.y q) (f32 200.0)))}) (f32 4.0) rl/white)))))) -(defn draw-world [] +(defn draw-world [] () ;; Everything between these two is in world space and moves with the camera. (rl/begin-mode-2d view) (draw-grid) (draw-world-cursor) (rl/end-mode-2d)) -(defn game-draw [] +(defn game-draw [] () ;; The world goes into the render texture first, if there is one. Note the ;; clear inside the texture mode: the framebuffer keeps last frame's pixels ;; otherwise, which looks like a trail and not like a bug. @@ -718,7 +718,7 @@ (draw-input-state) (rl/draw-fps 20 20)) -(defn main [] +(defn main [] () (rl/set-trace-log-level :warning) (rl/init-window screen-width screen-height "SAND") (defer (rl/close-window)) diff --git a/syntax-sketch.flan b/syntax-sketch.flan index def2402..c783a5a 100644 --- a/syntax-sketch.flan +++ b/syntax-sketch.flan @@ -77,7 +77,7 @@ ;; a copy and `set` would mutate the copy. `resolve` yields (Option (Ptr a)) ;; instead, and the pointer is visible in the binding's type. `deref` is the ;; by-value counterpart. Both are overloaded on (Ptr a)/(Handle a). -(defn damage [w (Ptr World) h (Handle Enemy) amount i32] +(defn damage [w (Ptr World) h (Handle Enemy) amount i32] () (match (resolve w h) (Some e) (set (.hp e) (- (.hp e) amount)) ; e : (Ptr Enemy), field derefs None (log "stale enemy handle"))) @@ -134,7 +134,7 @@ ;; Intermediate frames say nothing about AssetMissing. Nothing to thread. ;; invoke-restart has type Never: it does not return to the handler. -(defn load-level [path string] Level +(defn load-level [path string] () Level (handler-bind [AssetMissing (fn [c] (log "missing asset:" (.path c)) (invoke-restart 'use-placeholder))] @@ -168,7 +168,7 @@ ;; `filter` allocates a (Vec Enemy) from the current allocator, which is why ;; this is wrapped: the frame arena is bulk-reset, so the Vec is never freed ;; individually. `each` borrows it as a slice. -(defn draw-frame [w (Ptr World) dt f32] +(defn draw-frame [w (Ptr World) dt f32] () (with-allocator context/temp (->> (as-slice (.enemies w)) (filter (fn [e] (on-screen? (.pos e)))) @@ -177,7 +177,7 @@ ;; ── defer for explicit resources ────────────────────────────────────── ;; defer DOES run when a restart transfer passes through this frame. -(defn save-world [w (Ptr World) path string] +(defn save-world [w (Ptr World) path string] () (let [f (open path :write)] (defer (close f)) (write-bytes f (serialize w)))) @@ -194,7 +194,7 @@ ;; An imperative loop, not (each (fn [p] (try ...))): `try` and `return` inside a ;; `fn` exit the FN, so a callback would swallow the Err instead of propagating ;; it out of preload. -(defn ^:async preload [paths [string]] (Result Unit) +(defn ^:async preload [paths [string]] (Result ()) (for [p paths] (try (await (load-texture-async p)))) (Ok unit)) diff --git a/test/programs/algorithms.flan b/test/programs/algorithms.flan index 8d59e43..04f3072 100644 --- a/test/programs/algorithms.flan +++ b/test/programs/algorithms.flan @@ -7,13 +7,13 @@ ;;;; honest form is the concrete one, and the claim this file makes is only ;;;; that the concrete ones are right. -(defn show-f32 [s [f32]] +(defn show-f32 [s [f32]] () (dotimes [i (len s)] (print (at s i)) (print " ")) (println "")) -(defn show-fields [s [[u8]]] +(defn show-fields [s [[u8]]] () (dotimes [i (len s)] (print (string (at s i))) (print " ")) diff --git a/test/programs/bytes2.flan b/test/programs/bytes2.flan index b05f5a7..5117785 100644 --- a/test/programs/bytes2.flan +++ b/test/programs/bytes2.flan @@ -11,17 +11,17 @@ ;;;; for and a caller cannot tell from a real one: "", "abc", "1x", ".", ;;;; "1e", " 1", "0x10" and "nan". Each must be None. -(defn show-idx [o (Option i32)] +(defn show-idx [o (Option i32)] () (print (match o (Some i) i None -1)) (print " ")) -(defn show-bool [b bool] +(defn show-bool [b bool] () (print (if b "t" "f"))) ;; Brackets around the result so an empty trim is visible as [] rather than ;; as nothing at all — the all-whitespace case is otherwise indistinguishable ;; from a trim that printed the wrong slice of length zero. -(defn show-trim [s string] +(defn show-trim [s string] () (print "[") (print (trim (bytes s))) (print "]")) diff --git a/test/programs/cleanup.flan b/test/programs/cleanup.flan index 4fa80d2..5e1f60d 100644 --- a/test/programs/cleanup.flan +++ b/test/programs/cleanup.flan @@ -11,7 +11,7 @@ (defvar order i64) (defvar seen i64) -(defn note [n i64] (set order (+ (* order 10) n))) +(defn note [n i64] () (set order (+ (* order 10) n))) ;;; (1) An early return must run the defers registered above it, and (2) it ;;; must run them innermost-first. A defer that (8) *calls* something is the diff --git a/test/programs/conditions.flan b/test/programs/conditions.flan index b2b24b8..c439cfd 100644 --- a/test/programs/conditions.flan +++ b/test/programs/conditions.flan @@ -12,7 +12,7 @@ (defvar other i64) ;;; Signals twice and keeps going both times — that is the whole of §1. -(defn load-all [] +(defn load-all [] () (signal (AssetMissing {.id 1})) (signal (AssetMissing {.id 2})) (signal (Corrupt {.id 3}))) diff --git a/test/programs/defer-let.flan b/test/programs/defer-let.flan index 49cf425..420bd13 100644 --- a/test/programs/defer-let.flan +++ b/test/programs/defer-let.flan @@ -11,7 +11,7 @@ (defvar order i64) -(defn note [n i32] (set order (+ (* order 10) (i64 n)))) +(defn note [n i32] () (set order (+ (* order 10) (i64 n)))) ;;; One defer in a let, with the acquisition it is paired with above it. This ;;; is the shape the relaxation exists for: acquire, defer the release beside @@ -25,7 +25,7 @@ ;;; Two resources in one let. This is the case a flag granted once per block ;;; instead of once per form gets wrong: the first defer registers and the ;;; second is refused. -(defn two [] +(defn two [] () (let [a 1 b 2] (defer (note a)) (defer (note b)) @@ -33,7 +33,7 @@ ;;; A nested let still has the function's extent, so a defer in it registers ;;; too — and it registers *later* than the outer one, so it runs first. -(defn nested [] +(defn nested [] () (let [a 1] (defer (note a)) (let [b 2] @@ -43,7 +43,7 @@ ;;; Registration order is one order across the boundary: a defer at the top ;;; level and a defer inside a let interleave by where they are written, not by ;;; which construct they are in. Written 1, 2, 3; run 3, 2, 1. -(defn mixed [] +(defn mixed [] () (defer (note 1)) (let [x 2] (defer (note x)) diff --git a/test/programs/destructure.flan b/test/programs/destructure.flan index 94ecd29..5ebf6be 100644 --- a/test/programs/destructure.flan +++ b/test/programs/destructure.flan @@ -22,7 +22,7 @@ (set calls (+ calls 1)) (Point {.x 3 .y 4})) -(defn show2 [label string a i32 b i32] +(defn show2 [label string a i32 b i32] () (print label) (print " ") (print a) diff --git a/test/programs/edn.flan b/test/programs/edn.flan index 000cfcc..b27b24f 100644 --- a/test/programs/edn.flan +++ b/test/programs/edn.flan @@ -37,7 +37,7 @@ (= k edn/tok-list-close) ")" :else "?")) -(defn dump [src string] +(defn dump [src string] () (let [b (bytes src) c (edn/cursor b) t (edn/next (addr c))] @@ -55,7 +55,7 @@ ;; The refusals. Asserted on the *reason*, not on the fact of failing: a ;; tokenizer that answered err-unexpected-byte for every one of these would ;; pass a test that only checked that it failed. -(defn refusal [src string] +(defn refusal [src string] () (let [b (bytes src) c (edn/cursor b)] (while (and (edn/ok? (addr c)) @@ -118,7 +118,7 @@ (return e))))) e)) -(defn show-enemy [src string] +(defn show-enemy [src string] () (let [b (bytes src) c (edn/cursor b) e (read-enemy (addr c))] diff --git a/test/programs/fn-values.flan b/test/programs/fn-values.flan index 212e595..fc001a6 100644 --- a/test/programs/fn-values.flan +++ b/test/programs/fn-values.flan @@ -10,7 +10,7 @@ ;; The shape map/filter/reduce want: the function arrives as a parameter, is ;; called, and is never stored. -(defn each! [xs [i32] f (Fn [i32] i32)] Unit +(defn each! [xs [i32] f (Fn [i32] i32)] () (dotimes [i (len xs)] (set (at xs i) (f (at xs i))))) @@ -23,7 +23,7 @@ ;; A comparator, which is the other half of what was blocked: a sort that is ;; told the order rather than having it written in. Insertion sort, because the ;; point here is the parameter and not the algorithm. -(defn sort-by! [xs [i32] before? (Fn [i32 i32] bool)] Unit +(defn sort-by! [xs [i32] before? (Fn [i32 i32] bool)] () (dotimes [i (len xs)] (let [j i] (while (and (> j 0) (before? (at xs j) (at xs (- j 1)))) @@ -52,7 +52,7 @@ ;; A handler-bind and an fn literal in *one* function, which is the case that ;; would catch the two lifted-function name sequences sharing a counter: both ;; are lifted out of [handles] and both are numbered within it. -(defn handles [] Unit +(defn handles [] () (handler-bind [(TooBig [c] (set seen (+ seen (.n c))))] (let [xs [5 200 7 300]] (println (fold (slice xs 0 4) checked)) diff --git a/test/programs/format.flan b/test/programs/format.flan index d6931b0..cfe4f53 100644 --- a/test/programs/format.flan +++ b/test/programs/format.flan @@ -9,7 +9,7 @@ ;;;; sign, which belongs to the number and not to its integer part, because ;;;; -0.5 has an integer part of 0 and 0 carries no sign. -(defn show [x f64 p i32] +(defn show [x f64 p i32] () (let [v (format-f64 x p)] (println (string (as-slice v))) (free v))) diff --git a/test/programs/machine.flan b/test/programs/machine.flan index 59027b7..c089a2c 100644 --- a/test/programs/machine.flan +++ b/test/programs/machine.flan @@ -9,7 +9,7 @@ (defstruct P [x i32 y i32]) (defstruct Line [a P b P]) -(defn bump [p (Ptr P)] +(defn bump [p (Ptr P)] () (set (.x p) (+ (.x p) 1))) (defn sum-grid [] i32 diff --git a/test/programs/map-iter.flan b/test/programs/map-iter.flan index c5718c1..9b69829 100644 --- a/test/programs/map-iter.flan +++ b/test/programs/map-iter.flan @@ -7,7 +7,7 @@ ;; membership test, all of them order-free. That is not a weakness of the test, ;; it is the contract — a caller that wants an order sorts what it collected. -(defn sum-and-count [] Unit +(defn sum-and-count [] () (let [m (map-new i32 i32)] (put m 1 10) (put m 2 20) @@ -29,7 +29,7 @@ ;; A map that never allocated has no block at all, and one that allocated and ;; holds nothing has a block of nothing but zeroed hashes. Both walk zero ;; times, and they are different code paths to get there. -(defn the-empty-cases [] Unit +(defn the-empty-cases [] () (let [m (map-new i32 i32) cur (i64 0) k 0 @@ -47,7 +47,7 @@ ;; A cursor left past the end keeps answering false rather than wrapping, so a ;; second loop over a spent cursor is empty and not a repeat. -(defn a-spent-cursor [] Unit +(defn a-spent-cursor [] () (let [m (map-new i32 i32)] (put m 5 50) (put m 6 60) @@ -64,7 +64,7 @@ ;; would catch the two runs being indexed with one geometry. (defstruct Point [x i32 y i32]) -(defn wider-entries [] Unit +(defn wider-entries [] () (let [m (map-new string Point)] (put m "a" (Point {.x 1 .y 2})) (put m "bb" (Point {.x 3 .y 4})) @@ -85,7 +85,7 @@ ;; Growth past the 75% threshold rehashes into a new block, so this walks a map ;; whose layout is nothing like its insertion order and at a capacity several ;; doublings past the minimum. -(defn after-growth [] Unit +(defn after-growth [] () (let [m (map-new i64 i64)] (dotimes [i 500] (put m (i64 i) (* (i64 i) 2))) diff --git a/test/programs/math.flan b/test/programs/math.flan index 811fb7c..5df607b 100644 --- a/test/programs/math.flan +++ b/test/programs/math.flan @@ -10,7 +10,7 @@ ;;;; past 2^24, where an f32 has no fractional bits and the guard, not the ;;;; cast, has to produce the answer. -(defn show [x f32] +(defn show [x f32] () (print x) (print " ")) diff --git a/test/programs/math2.flan b/test/programs/math2.flan index 145cbb0..56c2f48 100644 --- a/test/programs/math2.flan +++ b/test/programs/math2.flan @@ -13,7 +13,7 @@ ;;;; one copy per type. The proof that the macro is not one copy per type is ;;;; that the same three-word call below is made at i32, i64, u8 and f32. -(defn show [x f32] +(defn show [x f32] () (print x) (print " ")) diff --git a/test/programs/println.flan b/test/programs/println.flan index 2b252c7..1e105f7 100644 --- a/test/programs/println.flan +++ b/test/programs/println.flan @@ -34,7 +34,7 @@ (defstruct Long [s string]) (defvar long-one Long) -(defn nothing [] ) +(defn nothing [] () ) (defn find-it [s [i32] k i32] (Option i32) (dotimes [i (len s)] diff --git a/test/programs/raylib-audio.flan b/test/programs/raylib-audio.flan index dc29ff1..91c41f2 100644 --- a/test/programs/raylib-audio.flan +++ b/test/programs/raylib-audio.flan @@ -64,7 +64,7 @@ ;; different one. (defvar pcm [16 u8]) -(defn load-pcm [] +(defn load-pcm [] () (set (at pcm 0) 0x00) (set (at pcm 1) 0x00) ; 0 (set (at pcm 2) 0xE8) (set (at pcm 3) 0x03) ; 1000 (set (at pcm 4) 0xD0) (set (at pcm 5) 0x07) ; 2000 @@ -76,7 +76,7 @@ (defconst wav-path "/tmp/flan-raylib-audio.wav") -(defn show-wave [name string w rl/Wave] +(defn show-wave [name string w rl/Wave] () (print name) (print " ") (print (.frame-count w)) (print " ") (print (.sample-rate w)) @@ -84,7 +84,7 @@ (print " ") (print (.channels w)) (println "")) -(defn show-bool [name string b bool] +(defn show-bool [name string b bool] () (print name) (print " ") (println (if b "yes" "no"))) @@ -121,7 +121,7 @@ (rl/unload-wave one) v))) -(defn show-frame [name string w rl/Wave i i32 want f32] +(defn show-frame [name string w rl/Wave i i32 want f32] () (show-bool name (near? (frame-at w i) want))) (defn main [] i32 diff --git a/test/programs/raylib-ffi.flan b/test/programs/raylib-ffi.flan index 391d47b..a5a8400 100644 --- a/test/programs/raylib-ffi.flan +++ b/test/programs/raylib-ffi.flan @@ -11,14 +11,14 @@ ;; survives it unchanged. Every case below is asymmetric — raylib does ;; something to the fields that depends on which is which. -(defn show-texture [t rl/Texture2D] +(defn show-texture [t rl/Texture2D] () (print (.id t)) (println "") (print (.width t)) (println "") (print (.height t)) (println "") (print (.mipmaps t)) (println "") (print (.format t)) (println "")) -(defn show-rect [r rl/Rectangle] +(defn show-rect [r rl/Rectangle] () (print (.x r)) (println "") (print (.y r)) (println "") (print (.width r)) (println "") @@ -46,11 +46,11 @@ ;; ((140-100)/2)+8 = 28 and ((90-50)/2)+4 = 24. Swap offset and target in the ;; defstruct and this reads (143,-16); swap rotation and zoom and the zoom ;; becomes 0, the transform is singular, and both come back NaN. -(defn show-bool [name string b bool] +(defn show-bool [name string b bool] () (print name) (print " ") (println (if b "yes" "no"))) -(defn show-v [v rl/Vector2] +(defn show-v [v rl/Vector2] () (print (.x v)) (println "") (print (.y v)) (println "")) @@ -71,7 +71,7 @@ (let [d (- a b)] (< (if (< d 0.0) (- 0.0 d) d) 0.0001))) -(defn show-near [name string v rl/Vector2 x f32 y f32] +(defn show-near [name string v rl/Vector2 x f32 y f32] () (print name) (println (if (and (near? (.x v) x) (near? (.y v) y)) " ok" " bad"))) diff --git a/test/programs/raylib-font.flan b/test/programs/raylib-font.flan index 1f81afa..4ead0dd 100644 --- a/test/programs/raylib-font.flan +++ b/test/programs/raylib-font.flan @@ -67,7 +67,7 @@ ;; and glyph C answers with A's numbers. (defvar glyphs [3 rl/GlyphInfo]) -(defn build-glyphs [] +(defn build-glyphs [] () (set (at glyph-recs 0) (rl/Rectangle {.x 0.0 .y 0.0 .width 5.0 .height 10.0})) (set (at glyph-recs 1) (rl/Rectangle {.x 5.0 .y 0.0 .width 7.0 .height 10.0})) (set (at glyph-recs 2) (rl/Rectangle {.x 12.0 .y 0.0 .width 9.0 .height 10.0})) @@ -80,20 +80,20 @@ (set (at glyphs 2) (rl/GlyphInfo {.value 67 .offset-x 3 .offset-y 0 .advance-x 0 .image (rl/Image {})}))) -(defn show-bool [name string b bool] +(defn show-bool [name string b bool] () (print name) (print " ") (println (if b "yes" "no"))) -(defn show-i [name string v i32] +(defn show-i [name string v i32] () (print name) (print " ") (print v) (println "")) -(defn show-v [name string v rl/Vector2] +(defn show-v [name string v rl/Vector2] () (print name) (print " ") (print (.x v)) (print " ") (print (.y v)) (println "")) -(defn show-rect [name string r rl/Rectangle] +(defn show-rect [name string r rl/Rectangle] () (print name) (print " ") (print (.x r)) (print " ") (print (.y r)) diff --git a/test/programs/raylib-image.flan b/test/programs/raylib-image.flan index f59d914..8ac680a 100644 --- a/test/programs/raylib-image.flan +++ b/test/programs/raylib-image.flan @@ -34,7 +34,7 @@ ;; write identical bytes, so sharing one path between runs is harmless. (defconst png-path "/tmp/flan-raylib-image.png") -(defn show-image [name string i rl/Image] +(defn show-image [name string i rl/Image] () (print name) (print " ") (print (.width i)) (print " ") (print (.height i)) @@ -42,7 +42,7 @@ (print " ") (print (.format i)) (println "")) -(defn show-color [name string c rl/Color] +(defn show-color [name string c rl/Color] () (print name) (print " ") (print (.r c)) (print " ") (print (.g c)) @@ -50,13 +50,13 @@ (print " ") (print (.a c)) (println "")) -(defn show-bool [name string b bool] +(defn show-bool [name string b bool] () (print name) (print " ") (println (if b "yes" "no"))) ;; Every pixel read names its coordinates in the label, so a failure says ;; which one moved rather than only that something did. -(defn show-pixel [name string i rl/Image x i32 y i32] +(defn show-pixel [name string i rl/Image x i32 y i32] () (show-color name (rl/get-image-color i x y))) (defn main [] i32 diff --git a/test/programs/slices.flan b/test/programs/slices.flan index fd2d19f..ad66103 100644 --- a/test/programs/slices.flan +++ b/test/programs/slices.flan @@ -15,13 +15,13 @@ (defvar ys [5 i32]) (defvar zs [8 i32]) -(defn show [s [i32]] +(defn show [s [i32]] () (dotimes [i (len s)] (when (> i 0) (print " ")) (print (at s i))) (println "")) -(defn load-xs [] +(defn load-xs [] () (set (at xs 0) 5) (set (at xs 1) -3) (set (at xs 2) 5) diff --git a/test/programs/string-of-bytes.flan b/test/programs/string-of-bytes.flan index 9e218be..93a2ef8 100644 --- a/test/programs/string-of-bytes.flan +++ b/test/programs/string-of-bytes.flan @@ -20,7 +20,7 @@ ;; which shares stdout's buffer with puts, so the interleaving is stable. (declare-c c-puts [s string] i32 "puts") -(defn shows [s string] +(defn shows [s string] () (print "[") (print s) (print "] ") diff --git a/test/programs/strings.flan b/test/programs/strings.flan index 17d8929..7e2e989 100644 --- a/test/programs/strings.flan +++ b/test/programs/strings.flan @@ -11,7 +11,7 @@ ;;; A (Vec u8) printed as text, without the caller writing the two-step every ;;; time. as-slice borrows -- it copies ptr+len and never the elements -- so v ;;; is still the owner afterwards and is still free-able. -(defn show [v (Ptr (Vec u8))] +(defn show [v (Ptr (Vec u8))] () (println (string (as-slice (deref v))))) (defn main [] i32 diff --git a/test/programs/text.flan b/test/programs/text.flan index b4da894..5ca88ec 100644 --- a/test/programs/text.flan +++ b/test/programs/text.flan @@ -6,7 +6,7 @@ ;;;; end, and for parse-i64 every shape strtoll answers 0 for — "", "abc", ;;;; "12x", "-" — each of which a caller could not tell from a real 0. -(defn show-bool [b bool] +(defn show-bool [b bool] () (print (if b "t" "f"))) (defn main [] i32 diff --git a/test/programs/unit-main.flan b/test/programs/unit-main.flan index 8938a8e..9f86544 100644 --- a/test/programs/unit-main.flan +++ b/test/programs/unit-main.flan @@ -1,4 +1,4 @@ ;;;; The short entry point: both the parameter and the i32 status are optional, ;;;; and an omitted return type means Unit, so the process exits 0. -(defn main [] +(defn main [] () (println "ok")) diff --git a/test/programs/utf8.flan b/test/programs/utf8.flan index 41e5e9b..6ad4d8f 100644 --- a/test/programs/utf8.flan +++ b/test/programs/utf8.flan @@ -50,17 +50,17 @@ ;; code/width/ok, so a wrong answer names which of the three it got wrong ;; rather than just failing. -(defn show-dec [s [u8]] +(defn show-dec [s [u8]] () (let [r (decode-rune s)] (print (.code r)) (print "/") (print (.width r)) (print "/") (print (if (.ok r) "t" "f")) (print " "))) -(defn show-bool [b bool] +(defn show-bool [b bool] () (print (if b "t" "f"))) -(defn show-opt [o (Option i32)] +(defn show-opt [o (Option i32)] () (print (match o (Some v) v None -1)) (print " ")) @@ -74,11 +74,11 @@ (let [r (decode-rune (slice scratch 0 w))] (if (and (.ok r) (= (.width r) w)) (.code r) -1)))) -(defn show-i32 [x i32] +(defn show-i32 [x i32] () (print x) (print " ")) -(defn show-split [s [u8] sep u8] +(defn show-split [s [u8] sep u8] () (let [it (split-on-byte s sep) going true] (while going diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 95a4fbe..54fd10b 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -382,7 +382,7 @@ let () = "a quoted restart name and then its arguments"; (* A clause parameter is a binding, so it needs something to hold. *) refuses_src "a restart parameter that is not a value" - "(defn main [] i32 (restart-case 0 (use-value [v Unit] 1)))" + "(defn main [] i32 (restart-case 0 (use-value [v ()] 1)))" "which is not a value"; (* And so does an argument: a [println] is Unit, and there would be nothing to store into the clause's buffer. *) @@ -1669,7 +1669,7 @@ ERR@7 unexpected token: not the kind the caller was reading "(declare-c name [] string \"Name\")" "a string only crosses as a parameter"; shim_refuses "declare-c: a callback" - "(declare-c each [f (Fn [i32] Unit)] \"Each\")" + "(declare-c each [f (Fn [i32] ())] \"Each\")" "a C callback is not implemented"; shim_refuses "declare-c: an unknown type" "(declare-c f [x Nope] \"F\")" @@ -1679,7 +1679,7 @@ ERR@7 unexpected token: not the kind the caller was reading "field xs of S is a slice"; shim_refuses "declare-c: the generated name is already taken" (v2 - ^ "(defn mid-c [a (Ptr Vector2) out (Ptr Vector2)])\n\ + ^ "(defn mid-c [a (Ptr Vector2) out (Ptr Vector2)] ())\n\ (declare-c mid [a Vector2] Vector2 \"Mid\")") "needs the name mid-c for the declaration it generates"; shim_refuses "declare-c: two Flan names for one C function" @@ -1724,19 +1724,19 @@ ERR@7 unexpected token: not the kind the caller was reading copied into every exit path cannot. A [let] inside either one inherits the refusal, not the permission: its extent is the loop's or the arm's. *) refuses_src "defer in a loop body" - "(defn g [] 0)\n(defn f [] (while true (defer (g))))" + "(defn g [] () 0)\n(defn f [] () (while true (defer (g))))" "not allowed inside a loop body"; refuses_src "defer in a dotimes body" - "(defn g [] 0)\n(defn f [] (dotimes [i 3] (defer (g))))" + "(defn g [] () 0)\n(defn f [] () (dotimes [i 3] (defer (g))))" "not allowed inside a loop body"; refuses_src "defer in a branch" - "(defn g [] 0)\n(defn f [] (if true (defer (g)) 0))" + "(defn g [] () 0)\n(defn f [] () (if true (defer (g)) 0))" "not allowed inside a branch"; refuses_src "defer in a let inside a loop" - "(defn g [] 0)\n(defn f [] (while true (let [x 1] (defer (g)))))" + "(defn g [] () 0)\n(defn f [] () (while true (let [x 1] (defer (g)))))" "not allowed inside a loop body"; refuses_src "defer in a let inside a branch" - "(defn g [] 0)\n(defn f [] (if true (let [x 1] (defer (g))) 0))" + "(defn g [] () 0)\n(defn f [] () (if true (let [x 1] (defer (g))) 0))" "not allowed inside a branch"; (* ── (Map K V), spec-memory.md step 4 ────────────────────────── @@ -1798,13 +1798,13 @@ ERR@7 unexpected token: not the kind the caller was reading Unit as a value is refused rather than dividing a cache line by zero, and it is named because it is the natural spelling of a set. *) refuses_src "a float is not a map key" - "(defn f [m (Map f32 i32)] 0)" "is not a map key"; + "(defn f [m (Map f32 i32)] () 0)" "is not a map key"; refuses_src "a Ptr is not a map key" - "(defn f [m (Map (Ptr i32) i32)] 0)" "hash an address"; + "(defn f [m (Map (Ptr i32) i32)] () 0)" "hash an address"; refuses_src "a map value may not own storage" - "(defn f [m (Map i32 (Vec i32))] 0)" "holds a move-only value"; - refuses_src "a map value may not be Unit" - "(defn f [m (Map i32 Unit)] 0)" "cannot be Unit"; + "(defn f [m (Map i32 (Vec i32))] () 0)" "holds a move-only value"; + refuses_src "a map value may not be ()" + "(defn f [m (Map i32 ())] () 0)" "cannot be ()"; refuses_src "map-new with nothing to say what it maps" "(defn main [] i32 (let [m (map-new)] (free m)) 0)" "nothing here says what (map-new) maps"; @@ -1967,16 +1967,16 @@ ERR@7 unexpected token: not the kind the caller was reading unions through each other. (Ptr T) breaks the cycle and is exercised in the program above -- it is the shape a Form has. *) refuses_src "a union that contains itself by value" - "(defunion T [Leaf (Node [l T r T])])\n(defn f [t T] 0)" + "(defunion T [Leaf (Node [l T r T])])\n(defn f [t T] () 0)" "T contains itself by value"; refuses_src "two unions that contain each other by value" - "(defunion A [(X [b B])])\n(defunion B [(Y [a A])])\n(defn f [a A] 0)" + "(defunion A [(X [b B])])\n(defunion B [(Y [a A])])\n(defn f [a A] () 0)" "contains itself by value"; refuses_src "a union with no cases" - "(defunion U [])\n(defn f [u U] 0)" + "(defunion U [])\n(defn f [u U] () 0)" "declares no cases"; refuses_src "a union case that owns a Vec" - "(defunion U [(A [v (Vec i32)])])\n(defn f [u U] 0)" + "(defunion U [(A [v (Vec i32)])])\n(defn f [u U] () 0)" "which is move-only"; (* At the operation, not at the type: a struct key is decided by walking its fields and the struct table is not necessarily complete while a @@ -1984,7 +1984,7 @@ ERR@7 unexpected token: not the kind the caller was reading pair is emitted. A union reaches the same place. *) refuses_src "a union is not a map key" "(defunion U [A B])\n\ - (defn f [m (Map U i32) k U] (put m k 1))" + (defn f [m (Map U i32) k U] () (put m k 1))" "the payload past the case in hand is indeterminate"; (* A global cannot hold a case, because writing one at link time means serialising the fields into the payload blob and a string field is a diff --git a/test/test_flan.ml b/test/test_flan.ml index 761458e..ff4a484 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -178,8 +178,8 @@ let () = reads "two discards" "(f #_#_a b c)" "(f c)"; reads "three discards" "(f #_#_#_a b c d)" "(f d)"; (* Every position a form can appear in. *) - reads "discard at top level" "#_(defn a [] 1) (defn b [] 2)" "(defn b [] 2)"; - reads "discard a whole file" "#_(defn a [] 1)" ""; + reads "discard at top level" "#_(defn a [] () 1) (defn b [] () 2)" "(defn b [] () 2)"; + reads "discard a whole file" "#_(defn a [] () 1)" ""; reads "discard before quote" "(f #_a 'b)" "(f (quote b))"; reads "discard of a quote" "(f #_'a b)" "(f b)"; (* Nested, which the recursive read gives for free. *) @@ -368,7 +368,7 @@ let () = (* ── Types: brackets mean different things by position ─────────── *) let ty src = - match parse_decl (Printf.sprintf "(defn f [x %s])" src) with + match parse_decl (Printf.sprintf "(defn f [x %s] ())" src) with | { d = Defn { params = [ { fty; _ } ]; _ }; _ } -> fty.t | _ -> failwith "bad type test" in @@ -389,10 +389,14 @@ let () = (match (parse_decl "(defn f [x i32] bool x)").d with | Defn { ret = Some _; params = [ _ ]; fbody = [ _ ]; _ } -> () | _ -> check "defn with return type" false); - (* An omitted return type means Unit — the body must not be eaten as a type *) - (match (parse_decl "(defn f [x i32] (g x))").d with - | Defn { ret = None; fbody = [ _ ]; _ } -> () - | _ -> check "defn without return type" false); + (* () is the unit return type, and the body is what follows it. *) + (match (parse_decl "(defn f [x i32] () (g x))").d with + | Defn { ret = Some { t = Tname "Unit"; _ }; fbody = [ _ ]; _ } -> () + | _ -> check "defn returning ()" false); + (* A lone () is the return type and an empty body, not a body of one form. *) + (match (parse_decl "(defn f [x i32] ())").d with + | Defn { ret = Some { t = Tname "Unit"; _ }; fbody = []; _ } -> () + | _ -> check "defn returning () with no body" false); (match (parse_decl "(defvar grid [4 u32])").d with | Defvar ("grid", Some _, Zeroed) -> () | _ -> check "defvar is ZII" false); @@ -429,7 +433,7 @@ let () = ~needle:"defmacro is (defmacro name [param ...] body ...)"; parse_rejects "defmacro with a non-name param" "(defmacro m [1] x)" ~needle:"expected a name"; - parse_rejects "defmacro in expression position" "(defn f [] (defmacro m [] 1))" + parse_rejects "defmacro in expression position" "(defn f [] () (defmacro m [] 1))" ~needle:"top-level declaration"; (* Quasiquote is a desugaring over Form, and it has already run by the time @@ -463,9 +467,9 @@ let () = ~needle:"quasiquote inside a quasiquote"; (* Not a missing feature — an unquote outside a quasiquote is a mistake, and the reader cannot catch it because it does not track where it is. *) - parse_rejects "unquote outside a quasiquote" "(defn f [] ~x)" + parse_rejects "unquote outside a quasiquote" "(defn f [] () ~x)" ~needle:"means nothing outside a quasiquote"; - parse_rejects "splice where a splice makes no sense" "(defn f [] (+ 1 ~@xs))" + parse_rejects "splice where a splice makes no sense" "(defn f [] () (+ 1 ~@xs))" ~needle:"splices only into a list or a vector"; (* A splice with no bracket around it. The quasiquote is real here, so this one is the desugaring's refusal and not the parser's. *) @@ -477,17 +481,17 @@ let () = parse_rejects "odd field pairs" "(defstruct S [a])"; parse_rejects "cond without body" "(cond a)"; parse_rejects "unknown top form" "(nope x)"; - parse_rejects "break takes only a label" "(defn f [] (break 1))" + parse_rejects "break takes only a label" "(defn f [] () (break 1))" ~needle:"break is (break) or (break :label)"; - parse_rejects "continue takes only a label" "(defn f [] (continue x))" + parse_rejects "continue takes only a label" "(defn f [] () (continue x))" ~needle:"continue is (continue) or (continue :label)"; - parse_rejects "a labelled while still needs a test" "(defn f [] (while :o))" + parse_rejects "a labelled while still needs a test" "(defn f [] () (while :o))" ~needle:"(while :label test body ...)"; - parse_rejects "array with no type" "(defn f [] (array 4))" + parse_rejects "array with no type" "(defn f [] () (array 4))" ~needle:"array is (array COUNT TYPE)"; - parse_rejects "array given a value, not a type" "(defn f [] (array 4 5))" + parse_rejects "array given a value, not a type" "(defn f [] () (array 4 5))" ~needle:"expected a type"; - parse_rejects "array with a non-constant count" "(defn f [] (array (+ 1 1) f32))" + parse_rejects "array with a non-constant count" "(defn f [] () (array (+ 1 1) f32))" ~needle:"an array length is an integer or a constant's name"; (* ── The corpus parses ─────────────────────────────────────────── *) @@ -664,11 +668,11 @@ let () = rejects_check "bare None has no type" "(defconst x None)" ~needle:"what None is an Option of"; accepts "param types the literal" - "(defn g [x u8]) (defn f [] (g 3))"; + "(defn g [x u8] ()) (defn f [] () (g 3))"; rejects_check "wrong argument type" - "(defn g [x u8]) (defn f [] (g 0.5))" ~needle:"expected u8"; + "(defn g [x u8] ()) (defn f [] () (g 0.5))" ~needle:"expected u8"; rejects_check "wrong arity" - "(defn g [x u8]) (defn f [] (g 1 2))" ~needle:"takes 1 argument"; + "(defn g [x u8] ()) (defn f [] () (g 1 2))" ~needle:"takes 1 argument"; rejects_check "wrong return type" "(defn f [] bool 1)" ~needle:"expected bool"; rejects_check "if branches disagree" @@ -678,18 +682,18 @@ let () = (* 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])" + rejects_check "a mistyped primitive" "(defn f [x f65] ())" ~needle:"did you mean f64?"; - rejects_check "a transposed primitive" "(defn f [x stirng])" + 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])" + "(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])" + rejects_check "a real type variable" "(defn f [x t] ())" ~needle:"milestone 5"; - rejects_check "an unknown concrete type" "(defn f [x Widget])" + rejects_check "an unknown concrete type" "(defn f [x Widget] ())" ~needle:"unknown type Widget"; (* ── Static bounds ─────────────────────────────────────────────── *) @@ -746,7 +750,7 @@ let () = accepts "field through a pointer auto-derefs" (cursor ^ "(defn f [c (Ptr Cursor)] i32 (.pos c))"); accepts "set through a pointer" - (cursor ^ "(defn f [c (Ptr Cursor)] (set (.pos c) 1))"); + (cursor ^ "(defn f [c (Ptr Cursor)] () (set (.pos c) 1))"); rejects_check "field of a non-struct" "(defn f [x i32] i32 (.pos x))" ~needle:"is not a struct"; @@ -754,14 +758,14 @@ let () = accepts "a local is assignable" "(defn f [] i32 (let [x 1] (set x 2) x))"; rejects_check "a parameter is not assignable" - "(defn f [x i32] (set x 2))" ~needle:"parameters are not assignable"; + "(defn f [x i32] () (set x 2))" ~needle:"parameters are not assignable"; rejects_check "a constant is not assignable" - "(defconst k 1) (defn f [] (set k 2))" ~needle:"is a constant"; + "(defconst k 1) (defn f [] () (set k 2))" ~needle:"is a constant"; accepts "addr of a local gives a pointer" (cursor ^ "(defn g [c (Ptr Cursor)] i32 (.pos c)) \ (defn f [s [u8]] i32 (let [c (Cursor {.src s})] (g (addr c))))"); rejects_check "addr of a non-place" - "(defn f [] (addr (+ 1 2)))" ~needle:"addr takes the address of a place"; + "(defn f [] () (addr (+ 1 2)))" ~needle:"addr takes the address of a place"; (* ── Option, some, match ───────────────────────────────────────── *) accepts "some unwraps in an Option-returning function" @@ -787,11 +791,11 @@ let () = rejects_check "unknown name" "(defn f [] i32 nope)" ~needle:"unknown name"; rejects_check "unknown function" "(defn f [] i32 (nope 1))" ~needle:"unknown function"; - rejects_check "defined twice" "(defn f []) (defn f [])" + rejects_check "defined twice" "(defn f [] ()) (defn f [] ())" ~needle:"defined twice"; - accepts "main with no parameters and no return" "(defn main [])"; + accepts "main with no parameters and no return" "(defn main [] ())"; accepts "main with argv and a status" "(defn main [args [string]] i32 0)"; - rejects_check "main with a wrong parameter" "(defn main [n i32])" + rejects_check "main with a wrong parameter" "(defn main [n i32] ())" ~needle:"main takes no parameters"; rejects_check "main returning the wrong type" "(defn main [] bool true)" ~needle:"main returns i32"; @@ -802,14 +806,14 @@ let () = (* (Vec T) is built. What is still refused is the arity: one element type, and a near-miss there would otherwise resolve to a type variable and come back as generics. *) - rejects_check "Vec takes one type" "(defn f [x (Vec i32 i32)])" + rejects_check "Vec takes one type" "(defn f [x (Vec i32 i32)] ())" ~needle:"exactly one type"; (* {K V} resolves now — it is the Map type spelling, and the only one, since a bare map form in expression position is a struct literal's field list. What is still refused is the arity, for the same reason Vec's is: a near-miss would otherwise resolve to a type variable and come back as generics. *) - rejects_check "Map takes two types" "(defn f [x (Map i32)])" + rejects_check "Map takes two types" "(defn f [x (Map i32)] ())" ~needle:"exactly two types"; rejects_check "Result is milestone 6" "(defn f [] (Result i32 i32) None)" ~needle:"milestone 6"; @@ -823,10 +827,10 @@ let () = rather than once per iteration, and a branch cannot say "maybe registered". *) rejects_check "defer is refused in a loop body" - "(defn g [] 0) (defn f [] (while true (defer (g))))" + "(defn g [] () 0) (defn f [] () (while true (defer (g))))" ~needle:"a loop body"; rejects_check "defer is refused in a branch" - "(defn g [] 0) (defn f [] (if true (defer (g)) 0))" + "(defn g [] () 0) (defn f [] () (if true (defer (g)) 0))" ~needle:"a branch"; (* break and continue. The interesting half is the *relative* rule: a jump may not cross a construct that has work to do on the way out, and the @@ -834,34 +838,34 @@ let () = [return] still carries, and the accepting cases below are the ones a blanket rule would have got wrong. *) accepts "break leaves the innermost loop" - "(defn f [] (while true (break)))"; + "(defn f [] () (while true (break)))"; accepts "a labelled break leaves the named loop" - "(defn f [] (while :o true (while true (break :o))))"; + "(defn f [] () (while :o true (while true (break :o))))"; accepts "continue in a dotimes" - "(defn f [] (dotimes [i 3] (continue)))"; + "(defn f [] () (dotimes [i 3] (continue)))"; rejects_check "break outside a loop" - "(defn f [] (break))" ~needle:"only allowed inside a loop"; + "(defn f [] () (break))" ~needle:"only allowed inside a loop"; rejects_check "continue outside a loop" - "(defn f [] (continue))" ~needle:"only allowed inside a loop"; + "(defn f [] () (continue))" ~needle:"only allowed inside a loop"; rejects_check "a label naming no enclosing loop" - "(defn f [] (while true (break :nope)))" ~needle:"no loop named :nope"; + "(defn f [] () (while true (break :nope)))" ~needle:"no loop named :nope"; (* The rule the blanket one could not express, both ways round. A loop wholly inside a restart-case body keeps its local break; a break that would *leave* the restart-case is refused, and says so. *) accepts "a loop inside a restart-case may break out of itself" - "(defn f [] (restart-case (while true (break)) (go [] (println \"\"))))"; + "(defn f [] () (restart-case (while true (break)) (go [] (println \"\"))))"; rejects_check "break may not leave a restart-case" - "(defn f [] (while true (restart-case (break) (go [] (println \"\")))))" + "(defn f [] () (while true (restart-case (break) (go [] (println \"\")))))" ~needle:"a restart-case"; (* A clause is a barrier for the same reason the body is: it runs after a transfer landed, with the form's frames still to be popped. *) rejects_check "break may not leave a restart-case from a clause" - "(defn f [] (while true (restart-case (println \"\") (go [] (break)))))" + "(defn f [] () (while true (restart-case (println \"\") (go [] (break)))))" ~needle:"a restart-case"; accepts "a loop inside a handler-bind may break out of itself" - "(defstruct C [n i32]) (defn f [] (handler-bind [(C [c] 0)] (while true (break))))"; + "(defstruct C [n i32]) (defn f [] () (handler-bind [(C [c] 0)] (while true (break))))"; rejects_check "break may not leave a handler-bind" - "(defstruct C [n i32]) (defn f [] (while true (handler-bind [(C [c] 0)] (break))))" + "(defstruct C [n i32]) (defn f [] () (while true (handler-bind [(C [c] 0)] (break))))" ~needle:"a handler-bind"; (* An import is resolved by [Load] before the checker runs, so one that reaches [Check] means a driver skipped that step. *) @@ -869,14 +873,14 @@ let () = "(import rl \"vendor:raylib\")" ~needle:"not resolved"; (* Keywords resolve against an enum and against nothing else. *) rejects_check "a keyword needs an enum" - "(defn g [x i32]) (defn f [] (g :space))" ~needle:"is expected here"; + "(defn g [x i32] ()) (defn f [] () (g :space))" ~needle:"is expected here"; rejects_check "a keyword with no expectation" - "(defn f [] (print (i64 :space)))" ~needle:"no keyword type"; + "(defn f [] () (print (i64 :space)))" ~needle:"no keyword type"; rejects_check "a keyword that is not a member" - "(defenum Key [space 32]) (defn g [k Key]) (defn f [] (g :spcae))" + "(defenum Key [space 32]) (defn g [k Key] ()) (defn f [] () (g :spcae))" ~needle:"has no member :spcae"; accepts "a keyword that is a member" - "(defenum Key [space 32 r 82]) (defn g [k Key]) (defn f [] (g :r))"; + "(defenum Key [space 32 r 82]) (defn g [k Key] ()) (defn f [] () (g :r))"; (* Converting an enum, explicitly, in both directions. The point of the conversion is that it is written at the site: a bare integer still does not fit an enum parameter, so the checked property — a typo is an error @@ -886,11 +890,11 @@ let () = accepts "an enum converts to a float, through its i32" "(defenum Key [space 32]) (defn f [k Key] f32 (f32 k))"; accepts "an integer converts to an enum" - "(defenum Key [space 32]) (defn g [k Key]) (defn f [i i32] (g (Key i)))"; + "(defenum Key [space 32]) (defn g [k Key] ()) (defn f [i i32] () (g (Key i)))"; accepts "a value that is no declared member converts" - "(defenum Key [space 32]) (defn g [k Key]) (defn f [] (g (Key 999)))"; + "(defenum Key [space 32]) (defn g [k Key] ()) (defn f [] () (g (Key 999)))"; rejects_check "an integer still does not fit an enum on its own" - "(defenum Key [space 32]) (defn g [k Key]) (defn f [i i32] (g i))" + "(defenum Key [space 32]) (defn g [k Key] ()) (defn f [i i32] () (g i))" ~needle:"expected Key"; rejects_check "an enum does not convert to another enum" "(defenum A [x 1]) (defenum B [y 1]) (defn f [a A] B (B a))" @@ -905,8 +909,8 @@ let () = accepts "an enum is a return type" "(defenum Key [space 32]) (defn f [i i32] Key (Key i))"; accepts "an enum conversion at the head of a body is not a return type" - "(defenum Key [space 32]) (defn g [k Key]) \ - (defn f [] (Key 1) (g :space))"; + "(defenum Key [space 32]) (defn g [k Key] ()) \ + (defn f [] () (Key 1) (g :space))"; rejects_check "an enum conversion takes one argument" "(defenum Key [space 32]) (defn f [] Key (Key 1 2))" ~needle:"1 argument"; @@ -952,8 +956,8 @@ let () = An fn takes its parameter types from the position it is written in, and a defn's body that just answers one says nothing about them. *) rejects_check "an fn with nothing to say what it takes" - "(defn f [] (fn [x] x))" ~needle:"nothing here says what this fn"; - rejects_check "type variables are milestone 5" "(defn f [x a])" + "(defn f [] () (fn [x] x))" ~needle:"nothing here says what this fn"; + rejects_check "type variables are milestone 5" "(defn f [x a] ())" ~needle:"milestone 5"; (* The other half: a name in value position now *works*, and the arity is checked against the function it names. *) @@ -998,13 +1002,13 @@ let () = accepts "handler-bind over a struct condition" "(defstruct C [id i32]) (defvar n i64)\n\ - (defn f [] (handler-bind [(C [c] (set n 1))] (signal (C {.id 2}))))"; + (defn f [] () (handler-bind [(C [c] (set n 1))] (signal (C {.id 2}))))"; (* Matching is by type and there is no hierarchy, so a condition has to be a struct — an integer would have nothing to match against. *) rejects_check "signalling a non-struct" - "(defn f [] (signal 1))" ~needle:"a condition is a struct"; + "(defn f [] () (signal 1))" ~needle:"a condition is a struct"; rejects_check "erroring with a non-struct" - "(defn f [] (error 1))" ~needle:"a condition is a struct"; + "(defn f [] () (error 1))" ~needle:"a condition is a struct"; (* §2: error is Never, so it unifies with anything — including the position where a value of some other type was expected. That is what makes it usable as a restart-case body's fall-through. *) @@ -1021,7 +1025,7 @@ let () = rather than as an unknown name. *) rejects_check "a handler capturing a local" "(defstruct C [id i32])\n\ - (defn f [] (let [n 0] (handler-bind [(C [c] (set n 1))] (signal (C {.id 2})))))" + (defn f [] () (let [n 0] (handler-bind [(C [c] (set n 1))] (signal (C {.id 2})))))" ~needle:"a handler cannot see n"; (* The frames are popped on the way out of the body, so an early exit would leave them on the stack pointing into a function that has gone. *) @@ -1033,7 +1037,7 @@ let () = into a lookup and no place form for one. Refused with that reason rather than as a milestone that will never arrive. *) rejects_check "a map entry as a place" - "(defn f [] (set (get m 1) 2))" + "(defn f [] () (set (get m 1) 2))" ~needle:"a map is written with (put m k v)"; (* ── restart-case and invoke-restart, §3 to §6 ─────────────────── *) @@ -1068,15 +1072,15 @@ let () = accepts "a restart with parameters" "(defn f [] i32 (restart-case 1 (skip [n i32] n)))"; accepts "invoke-restart with arguments" - "(defn f [] (invoke-restart 'skip 1))"; + "(defn f [] () (invoke-restart 'skip 1))"; rejects_check "a restart parameter outside its clause" "(defn f [] i32 (+ (restart-case 1 (skip [n i32] n)) n))" ~needle:"unknown name n"; rejects_check "a restart argument that is not a value" - "(defn f [] (invoke-restart 'skip (println \"\")))" + "(defn f [] () (invoke-restart 'skip (println \"\")))" ~needle:"a restart argument must be a value"; rejects_check "invoke-restart on an unquoted name" - "(defn f [] (invoke-restart skip))" + "(defn f [] () (invoke-restart skip))" ~needle:"a quoted restart name and then its arguments"; (* §5 runs the defers on the way out, so a defer is already the cleanup path a transfer uses. One that starts its own transfer has no answer. *) @@ -1090,9 +1094,9 @@ let () = (fun (name, src) -> rejects_check (name ^ " is still unimplemented") src ~needle:"not implemented yet") - [ "handler-case", "(defn f [] (handler-case 1))"; - "find-restart", "(defn f [] (find-restart 'skip))"; - "compute-restarts", "(defn f [] (compute-restarts))" ]; + [ "handler-case", "(defn f [] () (handler-case 1))"; + "find-restart", "(defn f [] () (find-restart 'skip))"; + "compute-restarts", "(defn f [] () (compute-restarts))" ]; (* ── Destructuring ─────────────────────────────────────────────── *) @@ -1224,7 +1228,7 @@ let () = [ "a defn parameter", pt ^ "(defn f [{:keys [x]} Point] i32 x)"; "a defstruct field", "(defstruct S [[a b] i32])"; "an fn parameter", "(defn f [] i32 (let [g (fn [[a b]] a)] 0))"; - "a dotimes counter", "(defn f [] (dotimes [[a b] 3] 0))"; + "a dotimes counter", "(defn f [] () (dotimes [[a b] 3] 0))"; "a declare parameter", pt ^ "(declare g [{:keys [x]} Point] \"G\")" ]; (* ── match over an enum ────────────────────────────────────────── *) @@ -1548,9 +1552,9 @@ let () = let chain = synth "(defmacro m [args] `(do))\n\ - (defn a [] Unit (m))\n\ - (defn b [] Unit (a))\n\ - (defn c [] Unit (do))\n" + (defn a [] () (m))\n\ + (defn b [] () (a))\n\ + (defn c [] () (do))\n" in check "the reduction is transitive" (names_of (Macro.reduce chain) = [ "m"; "c" ]); diff --git a/test/test_session.ml b/test/test_session.ml index 7339dbd..94b60e9 100644 --- a/test/test_session.ml +++ b/test/test_session.ml @@ -218,7 +218,7 @@ let () = "not a package" — and the failure is the silent one above: the form splices as a bare [step] and the running program keeps the one it had. *) let t2, _ = Session.create ~file:"programs/sand-headless.flan" () in - (match Session.eval ~origin:"../sand.flan" t2 "(defn step [] Unit (do))" with + (match Session.eval ~origin:"../sand.flan" t2 "(defn step [] () (do))" with | c -> if c.Session.fns <> [ "sand/step" ] then fail "a form from a single-file package reported %s, wanted sand/step" @@ -226,7 +226,7 @@ let () = | exception Loc.Error (_, m) -> fail "redefining sand/step: %s" m); (* And a file that is not a package keeps its names as written. *) - (match Session.eval ~origin:"../sand.flan" t "(defn game-draw [] Unit (do))" with + (match Session.eval ~origin:"../sand.flan" t "(defn game-draw [] () (do))" with | c -> if c.Session.fns <> [ "game-draw" ] then fail "a form from the program's own file reported %s" diff --git a/tools/unit-return.py b/tools/unit-return.py new file mode 100755 index 0000000..84f2376 --- /dev/null +++ b/tools/unit-return.py @@ -0,0 +1,536 @@ +#!/usr/bin/env python3 +"""Give every `defn` an explicit return type, and rewrite `Unit` as `()`. + +`(defn f [x i32] body)` becomes `(defn f [x i32] () body)`, and a return type +already written as `Unit` -- or a `Unit` anywhere else a type is spelled, as in +`(Fn [i32] Unit)` -- becomes `()`. + +Two rules, one pass, because both are the same change: the slot after the +parameters is now unconditionally a type, so a function that returns nothing +has to say so, and the thing it says is `()`. + +Deciding whether a `defn` already has a return type is the whole difficulty, +and this script does it the way `lib/parse.ml` did before the slot became +mandatory: a form in that position is the return type when it is a *type form* +and it is not the entire body. `is_type_form` below is a transcription of the +one in parse.ml, deliberately faithful rather than improved -- being identical +to the parser it is replacing is what makes the sweep meaning-preserving. The +type names it needs come from the file's own declarations, from the prelude's +(read out of lib/prelude.ml), and from the builtin list. + +Re-runnable: a `defn` whose slot is already filled is left alone, and `()` is +itself a type form, so converting a converted file is a no-op. Parallel +branches that wrote Flan in the old spelling want this pass at merge. + + tools/unit-return.py ... # rewrite .flan in place + tools/unit-return.py --check ... # report, change nothing + tools/unit-return.py --in-strings ... # Flan inside "..." literals + tools/unit-return.py --raw-ml lib/prelude.ml # Flan in a {flan|...|flan} block + tools/unit-return.py --in-html web/index.html # Flan in
 blocks
+
+A directory is walked for `.flan` files only. Anything else is named on the
+command line with the mode that says how its Flan is embedded, because a blind
+scan of an OCaml or HTML file would read its punctuation as Flan.
+
+`-v` logs every `defn` seen and what was decided about it, which is the only
+practical way to review a sweep this size.
+
+**Read the diff of every non-`.flan` file.** A snippet split across OCaml
+string concatenation -- `decls ^ "(defn f [s [u8]] Cursor ...)"` -- is scanned
+one literal at a time, and the names the other literal declared would be
+invisible. The embedded modes work around it by pooling every fragment's type
+declarations across the whole file, and counting a pooled name only as a bare
+symbol, exactly as the prelude's types count: as a list head it would eat
+`(Some 1)` and `(Rune {.code 65})` as return types, which is the misparse this
+change exists to remove. That is sound because no user type takes arguments --
+only the builtin constructors do, and they are known already -- but it is a
+pool and not the real scope, so read the diff.
+"""
+
+import sys, os, re
+
+DELIM = set('()[]{}";`~ \t\n\r,')
+OPENERS = {'(': ')', '[': ']', '{': '}'}
+CLOSERS = {')', ']', '}'}
+
+# lib/parse.ml, [primitives] and [builtin_types]. Present in [types] under
+# their plain names, so they count as type forms in every position.
+BUILTINS = {
+    "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64",
+    "f32", "f64", "bool", "string", "Unit", "Never",
+    "Ptr", "Option", "Result", "Vec", "Map", "Handle", "Fn",
+}
+
+NUMERIC = re.compile(r'^[-+]?[0-9]')
+
+
+class Atom:
+    def __init__(self, start, end, text):
+        self.start, self.end, self.text = start, end, text
+        self.tok = start        # never moves; `start` may slide onto a quote sigil
+        self.items = []
+
+    @property
+    def open_char(self):
+        return None
+
+    def sym(self):
+        """The symbol name, or None if this atom is not one.
+
+        A string, a keyword, a character literal and a number are all atoms and
+        none of them is a symbol -- parse.ml's [is_type_form] answers false for
+        every one of them, through its catch-all arm.
+        """
+        t = self.text
+        if not t or t[0] in '":\\' or NUMERIC.match(t):
+            return None
+        return t
+
+
+class Seq:
+    def __init__(self, open_char, start):
+        self.open_char, self.start = open_char, start
+        self.end = start
+        self.items = []
+        self.closed = False
+
+    def sym(self):
+        return None
+
+
+def lex_forms(src, i, end, stop=None):
+    """Read forms from src[i:end]. Returns (items, next_index, closed).
+
+    `closed` is False when the text ran out before the enclosing delimiter did,
+    which is how a fragment that holds only part of a form is recognised --
+    `"(defn step [] i64\\n"`, one line of a snippet built by concatenation.
+
+    The lexing rules mirror lib/reader.ml. A quote/quasiquote/unquote prefix is
+    folded into the form it applies to, so a quoted value stays one element.
+    """
+    items = []
+    n = end
+    pending_prefix = None
+
+    def push(node):
+        nonlocal pending_prefix
+        if pending_prefix is not None:
+            node.start = pending_prefix
+            pending_prefix = None
+        items.append(node)
+
+    while i < n:
+        c = src[i]
+        if c in ' \t\n\r,':
+            i += 1
+        elif c == ';':                                   # line comment
+            while i < n and src[i] != '\n':
+                i += 1
+        elif c == '"':                                   # string literal
+            j = i + 1
+            while j < n and src[j] != '"':
+                j += 2 if src[j] == '\\' else 1
+            j = min(j + 1, n)
+            push(Atom(i, j, src[i:j]))
+            i = j
+        elif c == '\\':                                  # character literal
+            j = i + 1
+            if j < n:
+                j += 1
+                while j < n and src[j] not in DELIM:
+                    j += 1
+            push(Atom(i, j, src[i:j]))
+            i = j
+        elif c in "'`~":                                 # quote sugar
+            if pending_prefix is None:
+                pending_prefix = i
+            i += 2 if (c == '~' and i + 1 < n and src[i + 1] == '@') else 1
+        elif c in OPENERS:
+            node = Seq(c, i)
+            node.items, i, node.closed = lex_forms(src, i + 1, n, OPENERS[c])
+            node.end = i
+            push(node)
+        elif c in CLOSERS:
+            return items, i + 1, True
+        else:                                            # symbol or keyword
+            j = i
+            while j < n and src[j] not in DELIM:
+                j += 1
+            if j == i:
+                j = i + 1
+            push(Atom(i, j, src[i:j]))
+            i = j
+    return items, n, False
+
+
+def head(node):
+    """The head symbol of a `(...)` form, or None."""
+    if getattr(node, 'open_char', None) != '(' or not node.items:
+        return None
+    return node.items[0].sym()
+
+
+# ── the type sets, as lib/parse.ml collects them ──────────────────────
+
+class Types:
+    """What `is_type_form` consults.
+
+    Three sets, kept apart exactly as parse.ml keeps them, because which
+    positions a name counts in depends on where it came from:
+
+    `names` -- builtins and the file's own defstruct/defunion/defalias. Count
+    as a bare symbol *and* as a list head, since `(Option f64)` is a type.
+
+    `enums`, `prelude` -- enum names and the prelude's types. Count only as a
+    bare symbol. As a list head they would eat `(Key 1)` and `(Rune {.code 65})`
+    -- a conversion and a constructor -- as return types, which is the silent
+    misparse this whole change exists to remove.
+
+    `aliases` -- import aliases, for `rl/Vector2`.
+    """
+    def __init__(self, prelude_names, prelude_enums):
+        self.names = set(BUILTINS)
+        self.enums = set(prelude_enums)
+        self.prelude = set(prelude_names)
+        self.aliases = set()
+
+    def scan(self, forms):
+        """Add what a program's top-level declarations introduce."""
+        for f in forms:
+            h = head(f)
+            if h in ('defstruct', 'defunion', 'defalias') and len(f.items) == 3:
+                n = f.items[1].sym()
+                if n:
+                    self.names.add(n)
+            elif h == 'defenum' and len(f.items) == 3:
+                n = f.items[1].sym()
+                if n:
+                    self.enums.add(n)
+            elif h == 'import' and len(f.items) == 3:
+                a = f.items[1].sym()
+                if a:
+                    self.aliases.add(a)
+
+    def copy(self):
+        t = Types(self.prelude, self.enums)
+        t.names = set(self.names)
+        t.aliases = set(self.aliases)
+        return t
+
+    def qualified(self, s):
+        i = s.find('/')
+        if i < 0:
+            return False
+        alias, name = s[:i], s[i + 1:]
+        return alias in self.aliases and name[:1].isupper()
+
+    def is_type_form(self, f):
+        oc = getattr(f, 'open_char', None)
+        if oc == '(' and not f.items:
+            return True                      # () is unit
+        if oc in ('[', '{'):
+            return True                      # [T], [n T] and {K V} are only types
+        if oc == '(':
+            h = head(f)
+            return bool(h) and (h in self.names or self.qualified(h))
+        s = f.sym()
+        if s is None:
+            return False
+        return s in self.names or s in self.enums or s in self.prelude \
+            or self.qualified(s)
+
+
+def prelude_type_names(root):
+    """The prelude's type names, from its {flan|...|flan} block.
+
+    parse.ml's [prelude_types] does the same walk over the same text; reading
+    the file keeps the two from drifting apart by hand.
+    """
+    path = os.path.join(root, 'lib', 'prelude.ml')
+    try:
+        with open(path, encoding='utf-8') as fh:
+            src = fh.read()
+    except OSError:
+        return set(), set()
+    body = flan_block(src)
+    if body is None:
+        return set(), set()
+    forms, _, _ = lex_forms(body, 0, len(body))
+    names, enums = set(), set()
+    for f in forms:
+        h = head(f)
+        if len(f.items) != 3:
+            continue
+        n = f.items[1].sym()
+        if not n:
+            continue
+        if h in ('defstruct', 'defunion', 'defalias'):
+            names.add(n)
+        elif h == 'defenum':
+            enums.add(n)
+    return names, enums
+
+
+def flan_block(src):
+    i = src.find('{flan|')
+    j = src.rfind('|flan}')
+    if i < 0 or j < i:
+        return None
+    return src[i + len('{flan|'):j]
+
+
+# ── the rewrite ───────────────────────────────────────────────────────
+
+def plan(src, types, label, log, base_line=1):
+    """Edits for one program's worth of Flan. Returns [(start, end, text)]."""
+    forms, _, _ = lex_forms(src, 0, len(src))
+    types = types.copy()
+    types.scan(forms)
+    edits = []
+
+    def line_of(off):
+        return base_line + src.count('\n', 0, off)
+
+    def walk(node, nested):
+        if getattr(node, 'open_char', None) is None:
+            # Only inside a form. Every type position is -- (Fn [i32] Unit),
+            # [Unit], the slot after a defn's parameters -- and a bare top-level
+            # `Unit` is not Flan at all. The guard is what keeps this pass off
+            # an OCaml literal that happens to spell the word, as the checker's
+            # own pattern `Tname "Unit"` does.
+            if nested and node.sym() == 'Unit':
+                edits.append((node.tok, node.end, '()'))
+            return
+        if head(node) == 'defn':
+            decide(node)
+        for it in node.items:
+            walk(it, True)
+
+    def decide(node):
+        items = node.items
+        # (defn name [params] ...). Anything else -- a metadata sigil, a
+        # malformed form -- is left alone and reported, because guessing at a
+        # shape the parser does not accept is how a sweep corrupts a file.
+        if not node.closed:
+            log.append("%s:%d: skipped, the form is cut off here -- a fragment "
+                       "of a snippet built by concatenation"
+                       % (label, line_of(node.start)))
+            return
+        if len(items) < 3 or getattr(items[2], 'open_char', None) != '[':
+            log.append("%s:%d: skipped, not (defn name [params] ...)"
+                       % (label, line_of(node.start)))
+            return
+        name = items[1].sym() or '?'
+        rest = items[3:]
+        # parse.ml's guard: a single remaining form is the body, not the
+        # return type -- [(defn f [] i32)] was a function returning Unit whose
+        # body is the name [i32]. The exception is a lone [()], which is what
+        # this script itself writes for a function with no body, and which was
+        # never a legal body form. Without it a second run would fill the slot
+        # again, and a re-runnable sweep is the point.
+        lone_unit = len(rest) == 1 and getattr(rest[0], 'open_char', None) == '(' \
+            and not rest[0].items
+        if rest and (len(rest) >= 2 or lone_unit) and types.is_type_form(rest[0]):
+            log.append("%s:%d: %s kept %s"
+                       % (label, line_of(node.start), name,
+                          src[rest[0].start:rest[0].end].replace('\n', ' ')))
+            return
+        edits.append((items[2].end, items[2].end, ' ()'))
+        log.append("%s:%d: %s filled ()" % (label, line_of(node.start), name))
+
+    for f in forms:
+        walk(f, False)
+    return edits
+
+
+def apply(src, edits):
+    if not edits:
+        return src, 0
+    out = []
+    last = 0
+    for start, end, text in sorted(edits):
+        out.append(src[last:start])
+        out.append(text)
+        last = end
+    out.append(src[last:])
+    return ''.join(out), len(edits)
+
+
+def mask_ocaml_escapes(body):
+    """Blank out OCaml escapes so the Flan lexer cannot trip on them.
+
+    Length-preserving, so offsets into the masked text index the original. A
+    `\\"` must not end a Flan string, and the `\\n\\` line continuations the
+    tests wrap their snippets with must not read as Flan character literals.
+
+    An escaped newline becomes a real one rather than a blank: a `;` comment
+    runs to end of line, so flattening `\\n` to spaces would let one comment
+    swallow the rest of the snippet.
+    """
+    chars = list(body)
+    i = 0
+    while i < len(chars) - 1:
+        if chars[i] == '\\':
+            c = chars[i + 1]
+            chars[i] = ' '
+            chars[i + 1] = c if c in '\n\t' else ('\n' if c == 'n' else ' ')
+            i += 2
+        else:
+            i += 1
+    return ''.join(chars)
+
+
+def pool(types, fragments):
+    """Fold every fragment's declarations into the bare-symbol-only sets.
+
+    One file's Flan is written in pieces -- concatenated OCaml literals, one
+    
 per section -- and a piece does not see the piece that declared its
+    types. Pooling gives it back. See the module docstring for why pooled names
+    count only in bare-symbol position.
+    """
+    for body in fragments:
+        forms, _, _ = lex_forms(body, 0, len(body))
+        seen = types.copy()
+        seen.names = set()
+        seen.enums = set()
+        seen.scan(forms)
+        types.prelude |= seen.names
+        types.enums |= seen.enums
+        types.aliases |= seen.aliases
+    return types
+
+
+def convert_flan(src, types, label, log):
+    return apply(src, plan(src, types, label, log))
+
+
+def ocaml_literals(src):
+    """(offset, masked body) for every OCaml string literal in `src`."""
+    out = []
+    i, n = 0, len(src)
+    while i < n:
+        c = src[i]
+        if c == '"':
+            j = i + 1
+            while j < n and src[j] != '"':
+                j += 2 if src[j] == '\\' else 1
+            out.append((i + 1, mask_ocaml_escapes(src[i + 1:j])))
+            i = j + 1
+        elif c == '(' and i + 1 < n and src[i + 1] == '*':   # OCaml comment
+            i += 2
+        else:
+            i += 1
+    return out
+
+
+def convert_in_strings(src, types, label, log):
+    """Flan inside ordinary OCaml `"..."` literals, as the tests write it."""
+    lits = ocaml_literals(src)
+    pool(types, [body for _, body in lits])
+    edits = []
+    for off, body in lits:
+        base = src.count('\n', 0, off) + 1
+        for s, e, t in plan(body, types, label, log, base):
+            edits.append((off + s, off + e, t))
+    return apply(src, edits)
+
+
+def convert_raw_ml(src, types, label, log):
+    """A whole {flan|...|flan} block, as lib/prelude.ml writes it."""
+    i = src.find('{flan|')
+    j = src.rfind('|flan}')
+    if i < 0 or j < i:
+        return src, 0
+    off = i + len('{flan|')
+    body = src[off:j]
+    base = src.count('\n', 0, off) + 1
+    edits = [(off + s, off + e, t) for s, e, t in plan(body, types, label, log, base)]
+    return apply(src, edits)
+
+
+CODE = re.compile(r'
(.*?)
', re.S) + + +def convert_in_html(src, types, label, log): + """Flan in
 blocks. HTML entities are left escaped: `<` lexes
+    as an ordinary atom and nothing this pass writes needs escaping."""
+    edits = []
+    pool(types, [m.group(1) for m in CODE.finditer(src)])
+    for m in CODE.finditer(src):
+        off = m.start(1)
+        base = src.count('\n', 0, off) + 1
+        for s, e, t in plan(m.group(1), types, label, log, base):
+            edits.append((off + s, off + e, t))
+    return apply(src, edits)
+
+
+MODES = {
+    '--in-strings': convert_in_strings,
+    '--raw-ml': convert_raw_ml,
+    '--in-html': convert_in_html,
+}
+
+
+def walk_paths(paths):
+    for p in paths:
+        if os.path.isdir(p):
+            for root, dirs, files in os.walk(p):
+                # vendor/ is not third-party: edn, raylib and agent are this
+                # repo's own packages, written in Flan, and they convert too.
+                dirs[:] = [d for d in dirs
+                           if d not in ('_build', '.git', 'node_modules')]
+                for f in sorted(files):
+                    if f.endswith('.flan'):
+                        yield os.path.join(root, f)
+        else:
+            yield p
+
+
+def main(argv):
+    check = '--check' in argv
+    verbose = '-v' in argv or '--verbose' in argv
+    modes = [m for m in MODES if m in argv]
+    if len(modes) > 1:
+        sys.stderr.write("pick one of %s\n" % ', '.join(MODES))
+        return 2
+    mode = modes[0] if modes else None
+    paths = [a for a in argv[1:] if not a.startswith('-')] or ['.']
+
+    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    pnames, penums = prelude_type_names(root)
+    if not pnames:
+        sys.stderr.write("warning: no prelude types found under %s -- a "
+                         "prelude type in return position may be mis-read\n" % root)
+
+    total_files = total_edits = 0
+    log = []
+    for path in walk_paths(paths):
+        if not path.endswith('.flan') and mode is None:
+            sys.stderr.write(
+                "%s: say how the Flan is embedded -- --in-strings for OCaml "
+                "string literals, --raw-ml for a {flan|...|flan} block, "
+                "--in-html for 
 blocks. A blind scan would read the "
+                "host language's punctuation as Flan.\n" % path)
+            return 2
+        with open(path, encoding='utf-8') as fh:
+            src = fh.read()
+        types = Types(pnames, penums)
+        fn = MODES[mode] if mode else convert_flan
+        new, n = fn(src, types, path, log)
+        if n:
+            total_files += 1
+            total_edits += n
+            print("%s: %d" % (path, n))
+            if not check:
+                with open(path, 'w', encoding='utf-8') as fh:
+                    fh.write(new)
+    if verbose:
+        for line in log:
+            print("  " + line)
+    verb = "would make" if check else "made"
+    print("%s %d edits across %d files" % (verb, total_edits, total_files))
+    return 1 if (check and total_edits) else 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
diff --git a/vendor/edn/edn.flan b/vendor/edn/edn.flan
index 11d91c1..c12af96 100644
--- a/vendor/edn/edn.flan
+++ b/vendor/edn/edn.flan
@@ -197,7 +197,7 @@
 ;;
 ;; The first failure wins: a later one would overwrite the offset that
 ;; explains the file, with an offset that is merely downstream of it.
-(defn fail [c (Ptr Cursor)  code i32  pos i32]
+(defn fail [c (Ptr Cursor)  code i32  pos i32] ()
   (when (= (.err c) err-none)
     (set (.err c) code)
     (set (.err-pos c) pos)))
@@ -256,7 +256,7 @@
 ;; Whitespace, commas, and `;` comments, which run to the newline or to the end
 ;; of input — a comment on the last line of a file with no trailing newline is
 ;; the case that decides whether the loop tests the length before the byte.
-(defn skip-trivia [c (Ptr Cursor)]
+(defn skip-trivia [c (Ptr Cursor)] ()
   (while (not (at-end? c))
     (let [b (at (.src c) (.pos c))]
       (cond
diff --git a/web/examples/arrays.flan b/web/examples/arrays.flan
index 747eff6..1529497 100644
--- a/web/examples/arrays.flan
+++ b/web/examples/arrays.flan
@@ -7,7 +7,7 @@
 ;; No initialiser means all-bytes-zero, so this is BSS and costs nothing.
 (defvar grid [rows [cols i32]])
 
-(defn main []
+(defn main [] ()
   (set (at grid 1 2) 7)
   (print (at grid 1 2)) (println "")           ; 7
   (print (len palette)) (println "")           ; 4
diff --git a/web/examples/boom.flan b/web/examples/boom.flan
index 7771015..09f2f83 100644
--- a/web/examples/boom.flan
+++ b/web/examples/boom.flan
@@ -7,6 +7,6 @@
     (use-placeholder [] -1)
     (retry           [] 7)))
 
-(defn main []
+(defn main [] ()
   (print (load 1))
   (println ""))
diff --git a/web/examples/bounds.flan b/web/examples/bounds.flan
index 853b734..3b4a365 100644
--- a/web/examples/bounds.flan
+++ b/web/examples/bounds.flan
@@ -3,7 +3,7 @@
 ;; (at xs 7) with a literal index does not reach the backend at all: check.ml
 ;; rejects it. This one goes through a local, so it is the runtime check that
 ;; catches it — the same message, and the program stops where it happened.
-(defn main []
+(defn main [] ()
   (let [i 7]
     (println "before")
     (print (at xs i))
diff --git a/web/examples/breakdemo.flan b/web/examples/breakdemo.flan
index 7f7ae55..d55cce7 100644
--- a/web/examples/breakdemo.flan
+++ b/web/examples/breakdemo.flan
@@ -9,7 +9,7 @@
     (use-placeholder [] -1)
     (retry           [] 7)))
 
-(defn main []
+(defn main [] ()
   (agent/start "/tmp/flan-breakdemo.sock")
   (print (load 1))
   (println ""))
diff --git a/web/examples/conds.flan b/web/examples/conds.flan
index 5537d56..b3f9728 100644
--- a/web/examples/conds.flan
+++ b/web/examples/conds.flan
@@ -2,11 +2,11 @@
 
 (defvar seen i64)
 
-(defn load-all []
+(defn load-all [] ()
   (signal (AssetMissing {.id 1}))     ; Unit — the caller carries on
   (signal (AssetMissing {.id 2})))
 
-(defn main []
+(defn main [] ()
   (load-all)                          ; no handler: a no-op
   (print seen) (println "")           ; 0
 
diff --git a/web/examples/control.flan b/web/examples/control.flan
index a99b6db..2820eaf 100644
--- a/web/examples/control.flan
+++ b/web/examples/control.flan
@@ -6,7 +6,7 @@
     (= n 0)  "zero"
     :else    "positive"))
 
-(defn countdown [n i32]
+(defn countdown [n i32] ()
   (let [i n]
     (while (> i 0)
       (print i)
@@ -20,7 +20,7 @@
       (return (Some (at s i)))))
   None)
 
-(defn main []
+(defn main [] ()
   (println (classify -3))
   (countdown 4)
   (unless false
diff --git a/web/examples/defer.flan b/web/examples/defer.flan
index d4d3a75..daa5c45 100644
--- a/web/examples/defer.flan
+++ b/web/examples/defer.flan
@@ -6,6 +6,6 @@
   (println "body")
   n)
 
-(defn main []
+(defn main [] ()
   (print (work 3))
   (println ""))
diff --git a/web/examples/enums.flan b/web/examples/enums.flan
index 4773ea5..6743251 100644
--- a/web/examples/enums.flan
+++ b/web/examples/enums.flan
@@ -7,7 +7,7 @@
     (= k :escape) "escape"
     :else         "an arrow"))
 
-(defn main []
+(defn main [] ()
   ;; :space resolves against the parameter's enum at compile time.
   ;; A typo is an error here, not a wrong number later.
   (println (key-name :space))
diff --git a/web/examples/ffi.flan b/web/examples/ffi.flan
index 2c7efb0..d34ca00 100644
--- a/web/examples/ffi.flan
+++ b/web/examples/ffi.flan
@@ -2,5 +2,5 @@
 ;; no aggregate crosses, so no wrapper is generated.
 (declare cos-f64 [x f64] f64 "cos")
 
-(defn main []
+(defn main [] ()
   (print (cos-f64 0.0)) (println ""))
diff --git a/web/examples/globals.flan b/web/examples/globals.flan
index 5b6a5d2..ea7583b 100644
--- a/web/examples/globals.flan
+++ b/web/examples/globals.flan
@@ -5,7 +5,7 @@
 (defconst cols 4)
 (defvar grid [rows [cols u32]])        ; BSS, rows*cols*4 bytes
 
-(defn main []
+(defn main [] ()
   (print cell-size) (println "")
   (print gravity) (println "")
   (print current-color) (println "")
diff --git a/web/examples/hello.flan b/web/examples/hello.flan
index fcef751..9946bc7 100644
--- a/web/examples/hello.flan
+++ b/web/examples/hello.flan
@@ -1,2 +1,2 @@
-(defn main []
+(defn main [] ()
   (println "hello from flan"))
diff --git a/web/examples/option.flan b/web/examples/option.flan
index 97986f4..ea8ea91 100644
--- a/web/examples/option.flan
+++ b/web/examples/option.flan
@@ -4,7 +4,7 @@
 (defn doubled-first [s [i32]] (Option i32)
   (Some (* 2 (some (index-of-i32 s 15)))))
 
-(defn main []
+(defn main [] ()
   (match (doubled-first (slice nums 0 4))
     (Some i) (do (print i) (println ""))          ; 4
     None     (println "not found"))
diff --git a/web/examples/pkg.flan b/web/examples/pkg.flan
index d55033b..d749471 100644
--- a/web/examples/pkg.flan
+++ b/web/examples/pkg.flan
@@ -2,7 +2,7 @@
 ;; arrives qualified by the alias this import chose.
 (import g "geom")
 
-(defn main []
+(defn main [] ()
   (let [v (g/add (g/V2 {.x 3.0 .y 0.0})
                  (g/V2 {.x 0.0 .y 4.0}))]
     (print (g/length v))
diff --git a/web/examples/places.flan b/web/examples/places.flan
index 10ce89a..fbe4c17 100644
--- a/web/examples/places.flan
+++ b/web/examples/places.flan
@@ -5,7 +5,7 @@
 (defvar room [room-size i32])
 
 ;; `set` takes a fixed list of forms, not an extensible setf.
-(defn main []
+(defn main [] ()
   (let [e (Enemy {.hp 10 .name "slime"})
         p (addr e)]
     (set spawned (+ spawned 1))    ; a local or a defvar
diff --git a/web/examples/printing.flan b/web/examples/printing.flan
index 206cd79..6a8542b 100644
--- a/web/examples/printing.flan
+++ b/web/examples/printing.flan
@@ -4,7 +4,7 @@
 (defn look-up [k Key] (Option i32)
   (if (= k :space) (Some 32) None))
 
-(defn main []
+(defn main [] ()
   (println 42)                               ; an i32, uncast
   (println 1.5)
   (println (Enemy {.hp 3 .name "wisp" .key :left}))
diff --git a/web/examples/restart.flan b/web/examples/restart.flan
index 564f8c8..24d746c 100644
--- a/web/examples/restart.flan
+++ b/web/examples/restart.flan
@@ -15,7 +15,7 @@
     (use-placeholder [] -1)
     (retry           [] 7)))
 
-(defn main []
+(defn main [] ()
   (print (fetch 1)) (println "")          ; 101 — nothing handled it
 
   (handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))]
diff --git a/web/examples/shimdemo.flan b/web/examples/shimdemo.flan
index 9863eae..aef8782 100644
--- a/web/examples/shimdemo.flan
+++ b/web/examples/shimdemo.flan
@@ -2,6 +2,6 @@
 
 (declare-c get-mouse-position [] Vector2 "GetMousePosition")
 
-(defn main []
+(defn main [] ()
   (print (.x (get-mouse-position)))
   (println ""))
diff --git a/web/examples/structs.flan b/web/examples/structs.flan
index 278377d..299e744 100644
--- a/web/examples/structs.flan
+++ b/web/examples/structs.flan
@@ -7,10 +7,10 @@
     (at (.src c) (.pos c))
     0))
 
-(defn advance [c (Ptr Cursor)]
+(defn advance [c (Ptr Cursor)] ()
   (set (.pos c) (+ (.pos c) 1)))   ; field access derefs one level
 
-(defn main []
+(defn main [] ()
   (let [c (Cursor {.src (bytes "hi")})]   ; pos omitted, so pos is 0
     (print (peek (addr c))) (println "")
     (advance (addr c))
diff --git a/web/index.html b/web/index.html
index 4068d17..a2283cd 100644
--- a/web/index.html
+++ b/web/index.html
@@ -337,7 +337,7 @@ produced. run builds to a temporary file and execs it.

The smallest program:

-
(defn main []
+
(defn main [] ()
   (println "hello from flan"))

The entry point is (defn main [args [string]] i32). Both the parameter @@ -380,7 +380,7 @@ heap is involved.

(defvar room [room-size i32]) ;; `set` takes a fixed list of forms, not an extensible setf. -(defn main [] +(defn main [] () (let [e (Enemy {.hp 10 .name "slime"}) p (addr e)] (set spawned (+ spawned 1)) ; a local or a defvar @@ -411,7 +411,7 @@ wisp ;; (at xs 7) with a literal index does not reach the backend at all: check.ml ;; rejects it. This one goes through a local, so it is the runtime check that ;; catches it — the same message, and the program stops where it happened. -(defn main [] +(defn main [] () (let [i 7] (println "before") (print (at xs i)) @@ -502,10 +502,10 @@ its fields, and omitted fields are zeroed.

(at (.src c) (.pos c)) 0)) -(defn advance [c (Ptr Cursor)] +(defn advance [c (Ptr Cursor)] () (set (.pos c) (+ (.pos c) 1))) ; field access derefs one level -(defn main [] +(defn main [] () (let [c (Cursor {.src (bytes "hi")})] ; pos omitted, so pos is 0 (print (peek (addr c))) (println "") (advance (addr c)) @@ -534,7 +534,7 @@ typo is an error there rather than a wrong number later.

(= k :escape) "escape" :else "an arrow")) -(defn main [] +(defn main [] () ;; :space resolves against the parameter's enum at compile time. ;; A typo is an error here, not a wrong number later. (println (key-name :space)) @@ -567,7 +567,7 @@ functions need no forward declaration. Globals come in two kinds:

(defconst cols 4) (defvar grid [rows [cols u32]]) ; BSS, rows*cols*4 bytes -(defn main [] +(defn main [] () (print cell-size) (println "") (print gravity) (println "") (print current-color) (println "") @@ -601,7 +601,7 @@ whose type matters is named at the top level rather than written inline.

(= n 0) "zero" :else "positive")) -(defn countdown [n i32] +(defn countdown [n i32] () (let [i n] (while (> i 0) (print i) @@ -615,7 +615,7 @@ whose type matters is named at the top level rather than written inline.

(return (Some (at s i))))) None) -(defn main [] +(defn main [] () (println (classify -3)) (countdown 4) (unless false @@ -655,7 +655,7 @@ on nothing else today. some unwraps Some and early-ret (defn doubled-first [s [i32]] (Option i32) (Some (* 2 (some (index-of-i32 s 15))))) -(defn main [] +(defn main [] () (match (doubled-first (slice nums 0 4)) (Some i) (do (print i) (println "")) ; 4 None (println "not found")) @@ -680,7 +680,7 @@ has not executed yet and must not fire.

(println "body") n) -(defn main [] +(defn main [] () (print (work 3)) (println ""))
@@ -711,7 +711,7 @@ It is a place: (set (at grid r c) v) and (addr (at grid r c)) ;; No initialiser means all-bytes-zero, so this is BSS and costs nothing. (defvar grid [rows [cols i32]]) -(defn main [] +(defn main [] () (set (at grid 1 2) 7) (print (at grid 1 2)) (println "") ; 7 (print (len palette)) (println "") ; 4 @@ -750,7 +750,7 @@ user-supplied printer to choose between.

(defn look-up [k Key] (Option i32) (if (= k :space) (Some 32) None)) -(defn main [] +(defn main [] () (println 42) ; an i32, uncast (println 1.5) (println (Enemy {.hp 3 .name "wisp" .key :left})) @@ -859,7 +859,7 @@ and no ceremony.

;; arrives qualified by the alias this import chose. (import g "geom") -(defn main [] +(defn main [] () (let [v (g/add (g/V2 {.x 3.0 .y 0.0}) (g/V2 {.x 0.0 .y 4.0}))] (print (g/length v)) @@ -933,11 +933,11 @@ normally leaves the signaller to carry on — the accumulation case:

(defvar seen i64) -(defn load-all [] +(defn load-all [] () (signal (AssetMissing {.id 1})) ; Unit — the caller carries on (signal (AssetMissing {.id 2}))) -(defn main [] +(defn main [] () (load-all) ; no handler: a no-op (print seen) (println "") ; 0 @@ -971,7 +971,7 @@ first, before the clause body starts.

(use-placeholder [] -1) (retry [] 7))) -(defn main [] +(defn main [] () (print (fetch 1)) (println "") ; 101 — nothing handled it (handler-bind [(AssetMissing [c] (invoke-restart 'use-placeholder))] @@ -1060,7 +1060,7 @@ and an exit status of 134:

(use-placeholder [] -1) (retry [] 7))) -(defn main [] +(defn main [] () (print (load 1)) (println ""))
@@ -1083,7 +1083,7 @@ is generated; a Flan string crosses as ptr+len, exactly as it is stored.

(declare cos-f64 [x f64] f64 "cos")
 
-(defn main []
+(defn main [] ()
   (print (cos-f64 0.0)) (println ""))
1