flan/syntax-sketch.flan
2026-09-10 14:40:34 +07:00

199 lines
9.1 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. There is no
;; map literal — a map is built with make-map and an allocator.
;; (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)) (nth 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 Unit)
(for [p paths]
(try (await (load-texture-async p))))
(Ok unit))