;; 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` ;; - the return type is always written; () is unit, a real zero-sized type ;; rather than 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 () 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))