handler-bind takes (Type [name] body ...) and has since it was written; the sketch paired a type with an fn, which is the shape parse.ml names in its own refusal message. load-level had two return types. And there is no defcondition anywhere in the tree -- a condition type is an ordinary struct, which is what both spec-conditions.md and conditions.org say, so the one form in this file that introduced one was inventing it. The header's rules went with them: lowercase-is-a-type-variable and "no sigils" are both the pre-$t spelling, and let never took an annotation.
242 lines
12 KiB
Plaintext
242 lines
12 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 `defstruct` and a restart-case
|
|
;; clause. NOT in `let`: a local is inferred and takes no annotation
|
|
;; - the return type is always written; () is unit, a real zero-sized type
|
|
;; rather than C's void
|
|
;; - a type VARIABLE is $t; every other type name is concrete, whatever its
|
|
;; case. Lowercase-is-a-variable was the first spelling and is gone
|
|
;; - no `!` convention (nothing is immutable), and no `->`
|
|
;;
|
|
;; 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
|
|
;; (Map string i32) owning hashmap — move-only
|
|
;;
|
|
;; Braces are NOT a type. {K V} used to be a second spelling of (Map K V) and
|
|
;; was withdrawn: the brace's value and type meanings do not correspond the way
|
|
;; the bracket's do, and {} in type position is wanted for anonymous struct
|
|
;; types, {.x f32 .y f32}. 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 map-new and an allocator,
|
|
;; and when a literal arrives it takes {:key value}, which is why the dot is
|
|
;; what struct construction uses. A defn's constraint map, {:where (ordered?
|
|
;; $t)}, is the other brace form, and it sits after the return type.
|
|
;; (Ptr World) pointer
|
|
;; (Fn [f32] bool) function pointer, no captured environment
|
|
;; (Option $t) union from the stdlib
|
|
;; (Handle $t) 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)))
|
|
|
|
;; ── $t binds a type variable. Monomorphised at each call site ─────────
|
|
;; The sigil is on the type, everywhere a type goes: [$t], (Fn [$t $t] bool),
|
|
;; (Option $t). Bare t is the same variable where a type's NAME is an argument
|
|
;; in expression position — (vec-new t), (map-new t i32), the cast (t x).
|
|
;; There are no type classes, so $t supports only what EVERY type supports, and
|
|
;; the body is checked abstractly, so an unsupported operation is an error here
|
|
;; rather than at the first call site that happened to instantiate it. Ordering
|
|
;; is not supported, so it is passed in as a function value. Type arguments are
|
|
;; inferred from the argument types; there is no explicit instantiation.
|
|
(defn largest [xs [$t] gt (Fn [$t $t] bool)] (Option $t)
|
|
{:where (copyable? $t)}
|
|
(if (> (len xs) 0)
|
|
(let [best (at xs 0)]
|
|
(dotimes [i (len xs)]
|
|
(when (gt (at xs i) best) (set best (at xs i))))
|
|
(Some best))
|
|
None))
|
|
|
|
;; A {:where ...} clause admits the operator instead of taking it as an
|
|
;; argument. Five predicates — ordered? equal? hashable? numeric? copyable? —
|
|
;; and each instantiation is checked against the ones the signature declares.
|
|
(defn smallest [xs [$t]] (Option $t)
|
|
{:where (ordered? $t)}
|
|
(if (> (len xs) 0)
|
|
(let [m (at xs 0)]
|
|
(dotimes [i (len xs)] (set m (min m (at xs i))))
|
|
(Some m))
|
|
None))
|
|
|
|
;; (largest hps taller) — a top-level defn is an ordinary function value.
|
|
;; An OPERATOR is not: `>` is not a name, and (largest hps >) is "unknown name
|
|
;; >". Nor can an `fn` be written inline into a (Fn [$t $t] bool) argument: the
|
|
;; generic body is checked with nothing substituted, so there is no type for the
|
|
;; fn's own parameters to come from yet. Inside a generic the callback is a
|
|
;; named defn; at a monomorphic call site, where the types are already
|
|
;; concrete, the fn can be written inline where it is used.
|
|
|
|
;; Parameters are immutable values; pass a pointer to mutate. `[Enemy]` is a
|
|
;; borrowed slice — centroid neither owns nor frees the storage.
|
|
;; This one is still a sketch of where the syntax is going and does not compile
|
|
;; today, on two counts worth naming rather than leaving to be discovered:
|
|
;; component-wise `+` and `/` over a fixed array are planned and not built, and
|
|
;; the prelude's `reduce` is (reduce s init f) with its accumulator at the
|
|
;; ELEMENT type, so it cannot fold an [Enemy] into a Vec2. Written against what
|
|
;; exists, this is a `dotimes` accumulating into a local.
|
|
(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.
|
|
;; A condition type is an ordinary struct — there is no defcondition, and no
|
|
;; class hierarchy to put one in. Matching is by type plus a predicate.
|
|
(defstruct 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.
|
|
;; A handler clause is (Type [name] body ...) — the type, then the one binding,
|
|
;; then the body. It is not a type paired with an `fn`, and a handler closes
|
|
;; over nothing: it is lifted into its own function, so a value it wants to keep
|
|
;; goes on the condition or into a global.
|
|
(defn load-level [path string] Level
|
|
(handler-bind [(AssetMissing [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 [c]
|
|
(push errors c) ; value struct: copies out of
|
|
(invoke-restart 'skip-form))] ; the signalling frame
|
|
(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))
|