flan/syntax-sketch.flan
Joseph Ferano 5ea0bcae84 Remove nth, the alias that was not one
nth and at were documented as the same operation, and as reads they were:
check.ml matched "at" | "nth" in one arm. But a place is recovered in two
other spots -- parse.ml for (set ...) and place_of_expr for (addr ...) --
and both match only Sym "at". So (set (nth a i) x) and (addr (nth a i))
were refused while the at forms worked.

Two names said to be identical that disagree about writing is worse than
one name, and the asymmetry is not worth fixing in three places to keep a
synonym. at is the indexing operation; nth is gone.

The six call sites were all reads, so they rewrite directly. get/put stay
the Map pair: get returns (Option V) and is deliberately not a place.

nth-gone.flan pins the removal -- it has to fail as a name nobody defined,
not quietly resolve to at again.

destructure~nth is compiler-generated and unrelated.
2026-09-12 04:46:41 +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)) (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 Unit)
(for [p paths]
(try (await (load-texture-async p))))
(Ok unit))