Flan's tagged sum has been spelled defunion since it landed, which was accurate right up until the language wanted C's untagged union as well. Both cannot be called the same thing, and the tagged one is the one with an alternative name that says what it is: a case, its fields, and a tag that steers which case is live is a data type, not a union. So the form is defdata everywhere -- the parser, the AST, the checker, both backends, the prelude's Form, the editor's font-locking and imenu, the docs and every .flan file in the tree. The internal vocabulary moves with it: Tast.union is Tast.data, uname is dname, the tables the checker and the emitter keep are datas. Leaving them would have inverted the words permanently, with surface defunion meaning one thing and env.unions meaning the other, which is exactly the kind of drift the comments in those files exist to prevent. What did not move is case, variant and vfields: a tagged sum still has cases, and it still has one live at a time. defunion is not kept as an alias. An alias would compile the day the untagged form lands and mean the opposite of what it used to -- the same silent misparse that made defn's return type mandatory, and worse, because the reader would have no reason to look. The old spelling is a named refusal instead, parse/defunion-renamed, which says what it is now called and that the name is reserved for something else. It fires on the head alone, so (defunion U [A B]) -- which would otherwise have parsed cleanly as one field A of type B -- is refused with the rest.
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)])
|
|
|
|
(defdata 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))
|