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.
201 lines
9.3 KiB
Plaintext
201 lines
9.3 KiB
Plaintext
;; Syntax sketch. Not final — illustrates the decisions in plan.org.
|
|
;;
|
|
;; Rules held here:
|
|
;; - every type notation reads as exactly ONE data item
|
|
;; - types are inline name/type pairs, as in `let` and `defstruct`
|
|
;; - an omitted return type means Unit (a real zero-sized type, not C's void)
|
|
;; - lowercase type names are variables, Capitalized are concrete
|
|
;; - no `!` convention (nothing is immutable), no `->`, no sigils
|
|
;;
|
|
;; Normative references: spec-memory.md (ownership, containers, places,
|
|
;; generics, function values) and spec-conditions.md (restart semantics).
|
|
|
|
(import rl "vendor:raylib") ; directory = package, declaration optional;
|
|
; imports are always qualified rl/foo
|
|
|
|
;; ── Type notation ─────────────────────────────────────────────────────
|
|
;; [4 f32] fixed array — a value, copies on assignment
|
|
;; [f32] slice, ptr+len — a NON-OWNING view, copies the view only
|
|
;; (Vec f32) owning growable, ptr+len+cap — MOVE-ONLY, carries allocator
|
|
;; {string i32} owning hashmap — move-only, shorthand for (Map string i32)
|
|
;;
|
|
;; Braces are read by position: in a TYPE position {K V} is a map type; in a
|
|
;; VALUE position {.field v ...} is a struct or condition literal — a field
|
|
;; label is a dot, and the colon is left for keys. There is no map literal yet;
|
|
;; a map is built with make-map and an allocator, and when a literal arrives it
|
|
;; takes {:key value}, which is why the dot is what struct construction uses.
|
|
;; (Ptr World) pointer
|
|
;; (Fn [f32] bool) function pointer, no captured environment
|
|
;; (Option a) union from the stdlib
|
|
;; (Handle a) generational handle into a pool
|
|
;;
|
|
;; A struct is a value type iff all its fields are. One Vec field makes it
|
|
;; move-only. Copying an owning container is always explicit: (clone v).
|
|
|
|
(defalias Vec2 [2 f32])
|
|
(defalias Vec4 [4 f32])
|
|
|
|
;; ── Structs are value types with C layout, no header word ─────────────
|
|
(defstruct Enemy
|
|
[pos Vec2
|
|
vel Vec2
|
|
hp i32
|
|
spr (Handle Texture)])
|
|
|
|
(defunion Shape
|
|
[(Circle [r f32])
|
|
(Rect [w f32 h f32])])
|
|
|
|
;; ── Locals inferred; only signatures are annotated ───────────────────
|
|
(defn area [s Shape] f32
|
|
(match s
|
|
(Circle r) (* PI r r)
|
|
(Rect w h) (* w h)))
|
|
|
|
;; ── Lowercase = type variable. Monomorphised at each call site ────────
|
|
;; There are no type classes, so `a` supports only what EVERY type supports.
|
|
;; Ordering is not that — it is passed in as a function value. Type arguments
|
|
;; are inferred from the argument types; there is no explicit instantiation.
|
|
;; The inner `fn` captures `gt`, a parameter: legal because it does not outlive
|
|
;; this frame (spec-memory.md, non-escaping fn).
|
|
(defn largest [xs [a] gt (Fn [a a] bool)] (Option a)
|
|
(if (> (len xs) 0)
|
|
(Some (reduce (fn [x y] (if (gt x y) x y)) (at xs 0) xs))
|
|
None))
|
|
|
|
;; (largest hps >) — `>` at i32 is an ordinary function value
|
|
;; (largest es (fn [x y] (> (.hp x) (.hp y))))
|
|
|
|
;; Parameters are immutable values; pass a pointer to mutate. `[Enemy]` is a
|
|
;; borrowed slice — centroid neither owns nor frees the storage.
|
|
(defn centroid [es [Enemy]] Vec2
|
|
(/ (reduce (fn [acc e] (+ acc (.pos e))) [0 0] es)
|
|
(f32 (len es))))
|
|
|
|
;; ── Handles, not pointers, for anything cross-referenced ──────────────
|
|
;; Pattern bindings bind VALUES, so matching a struct out of a pool would give
|
|
;; 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] ()
|
|
(match (resolve w h)
|
|
(Some e) (set (.hp e) (- (.hp e) amount)) ; e : (Ptr Enemy), field derefs
|
|
None (log "stale enemy handle")))
|
|
|
|
;; ── Error handling is layered ─────────────────────────────────────────
|
|
;; Option expected absence: lookup miss, empty collection, end of stream
|
|
;; Result failure that belongs in the signature; error set inferred
|
|
;; Condition failure where the CALLER owns the recovery policy
|
|
;;
|
|
;; Rule: if you can name the one correct recovery at the point of failure,
|
|
;; return a Result. If the answer is "depends who's calling", signal.
|
|
|
|
;; `some` unwraps Some, else early-returns None.
|
|
(defn player-weapon [w (Ptr World)] (Option Weapon)
|
|
(let [p (some (find-player w))
|
|
s (some (slot (.inventory p) 3))]
|
|
(Some (.weapon s))))
|
|
|
|
;; `try` unwraps Ok, else early-returns Err, widening this function's error
|
|
;; set. Option→Result conversion is explicit — no implicit From, no anyhow.
|
|
(defn load-config [path string] (Result Config)
|
|
(let [text (try (read-file path))
|
|
table (try (parse-toml text))
|
|
port (try (ok-or (get table "port")
|
|
(MissingKey {.key "port"})))]
|
|
(Ok (Config {.port port}))))
|
|
|
|
;; errdefer runs only on the Result failure path — NOT on a restart transfer
|
|
;; (spec-conditions.md §5). Pairs with explicit allocation.
|
|
(defn load-atlas [path string] (Result Atlas)
|
|
(let [buf (alloc-image context/allocator)]
|
|
(errdefer (free buf))
|
|
(try (decode-png path buf))
|
|
(Ok (Atlas {.image buf}))))
|
|
|
|
;; ── Conditions: handlers run on the signalling frame, nothing unwinds ─
|
|
;; load-texture cannot know the right recovery — an editor wants a placeholder,
|
|
;; a release build wants to abort, a hot-reload session wants to retry after the
|
|
;; file is fixed on disk. So it offers a menu and the caller chooses.
|
|
(defcondition AssetMissing [path string])
|
|
|
|
;; `signal` has type Unit and RETURNS if every handler returns normally, so the
|
|
;; fall-through path of a restart-case in value position must still produce the
|
|
;; type. `abort` has type Never, which unifies with (Handle Texture).
|
|
;; Every clause body and the restart-case body share one type.
|
|
(defn load-texture [path string] (Handle Texture)
|
|
(if (file-exists? path)
|
|
(rl/load-texture path)
|
|
(restart-case
|
|
(do (signal (AssetMissing {.path path}))
|
|
(abort "unhandled AssetMissing"))
|
|
(use-placeholder [] placeholder-texture)
|
|
(retry [] (load-texture path)))))
|
|
|
|
;; 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
|
|
(handler-bind [AssetMissing (fn [c]
|
|
(log "missing asset:" (.path c))
|
|
(invoke-restart 'use-placeholder))]
|
|
(parse-level (slurp path))))
|
|
|
|
;; A handler that returns normally does not unwind, so the signaller carries on.
|
|
;; That is error accumulation with no monad or applicative. Restarts go at the
|
|
;; resync point — once — not in every function below it.
|
|
;; The result is an owning (Vec Form): it is pushed to, and it is returned by
|
|
;; move, so the caller owns it.
|
|
(defn parse-all [p (Ptr Parser)] (Vec Form)
|
|
(let [forms (make-vec Form)]
|
|
(until (at-end? p)
|
|
(restart-case
|
|
(push forms (parse-form p))
|
|
(skip-form [] (skip-to-next-delimiter p))))
|
|
forms))
|
|
|
|
(defn collect-parse-errors [src string] (Result Ast)
|
|
(let [errors (make-vec ParseError)]
|
|
(handler-bind [ParseError (fn [c]
|
|
(push errors c) ; value struct: copies out of
|
|
; the signalling frame
|
|
(invoke-restart 'skip-form))]
|
|
(let [ast (parse-all (parser src))]
|
|
(if (zero? (len errors))
|
|
(Ok ast)
|
|
(Err (Errors errors))))))) ; errors moves into the Err
|
|
|
|
;; ── Allocators. context/temp resets each frame; nothing freed by hand ─
|
|
;; `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] ()
|
|
(with-allocator context/temp
|
|
(->> (as-slice (.enemies w))
|
|
(filter (fn [e] (on-screen? (.pos e))))
|
|
(each (fn [e] (rl/draw-texture (.spr e) (.pos e))))))
|
|
(free-all context/temp))
|
|
|
|
;; ── defer for explicit resources ──────────────────────────────────────
|
|
;; defer DOES run when a restart transfer passes through this frame.
|
|
(defn save-world [w (Ptr World) path string] ()
|
|
(let [f (open path :write)]
|
|
(defer (close f))
|
|
(write-bytes f (serialize w))))
|
|
|
|
;; ── Fixed arrays: component-wise ops and swizzles, no library ─────────
|
|
(defn reflect [v Vec4 n Vec4] Vec4
|
|
(- v (* 2.0 (dot v n) n)))
|
|
|
|
(defn to-2d [v Vec4] Vec2 (.xy v))
|
|
|
|
;; ── Later: async as a state-machine transform, not fibers ─────────────
|
|
;; The handler and restart stacks live in the task state, not thread-local.
|
|
;;
|
|
;; 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 ())
|
|
(for [p paths]
|
|
(try (await (load-texture-async p))))
|
|
(Ok unit))
|