The sketch's condition syntax, checked against the parser rather than recalled

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.
This commit is contained in:
Joseph Ferano 2026-09-14 07:33:07 +07:00
parent d94e864dba
commit 0aebef62f3
3 changed files with 31 additions and 16 deletions

View File

@ -194,8 +194,10 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one.
- Types are mandatory; *inference* makes them feel optional. Annotate function
signatures, infer locals — Odin/Zig/Rust ergonomics.
- Signatures are annotated as inline name/type pairs, as in ~let~ and
~defstruct~: ~(defn area [s Shape] f32 ...)~. No separate ~declare~ form —
- Signatures are annotated as inline name/type pairs, as in ~defstruct~ and a
~restart-case~ clause: ~(defn area [s Shape] f32 ...)~. A ~let~ is not one of
them — a local is inferred from its initialiser and takes no annotation at
all. No separate ~declare~ form —
~declare~ is kept only where there is no body (forward declarations, FFI).
- Annotations at function boundaries are unavoidable, because compile-time
overloading is incompatible with full inference. Locals are inferred.

View File

@ -166,6 +166,12 @@ instead:
(defn largest [xs [$t] gt (Fn [$t $t] bool)] (Option $t) ...)
```
The value handed to such a parameter is a named `defn`. An `fn` cannot be written
inline into it, because the generic body is checked with nothing substituted and
there is no concrete type yet for the `fn`'s own parameters to come from; that
restriction lifts at a monomorphic call site, where `reduce`'s and `filter`'s
callbacks are ordinary inline `fn`s.
The alternatives to predicates — compile-time interfaces, or intrinsics
restricted to primitives — remain deliberately deferred until the base checker is
stable (build sequence milestone 4). The ceiling is that nobody can supply a

View File

@ -2,11 +2,13 @@
;;
;; Rules held here:
;; - every type notation reads as exactly ONE data item
;; - types are inline name/type pairs, as in `let` and `defstruct`
;; - 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
;; - lowercase type names are variables, Capitalized are concrete
;; - no `!` convention (nothing is immutable), no `->`, no sigils
;; - 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).
@ -91,8 +93,8 @@
;; >". 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 the fn can be written where it is
;; used, as `centroid` does below.
;; 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.
@ -151,7 +153,9 @@
;; 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])
;; 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
@ -168,10 +172,14 @@
;; 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))]
;; 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.
@ -189,10 +197,9 @@
(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))]
(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)