Files
rose-ash/plans/sx-review/RULINGS.md
giles 4f766ea4f1 plans: SX review master remediation plan + evidence
Consolidates the three-lane review (core K01-K110, hosts J*/C*/JS*/P*/S*,
conformance F1-F15) into plans/sx-review/:
- PLAN.md — 15 workstreams, phased execution, full per-finding coverage
  ledger (every ~213 finding-instances mapped to a workstream + status)
- RULINGS.md — 40 draft normative rulings (Phase-0 gate)
- core.md / hosts.md / conformance.md — the lane evidence files

dc7aa709 quick-wins batch marked DONE in the ledger; K01 (guard re-raise
hang), S1 (live HTTP crash), K03 (shift-k), and W14 (test gate) flagged as
the highest-value open work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 21:28:41 +00:00

397 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SX RULINGS — normative decisions on every ambiguity surfaced by the 2026-07-03 review
DRAFT for ratification. Each ruling: STATUS `PROPOSED` → flip to `RATIFIED` / `REJECTED` /
`AMENDED: <text>`. Once ratified, this file moves to `spec/RULINGS.md` and becomes the
authority the conformance batteries pin against. Evidence citations: core.md finding names,
hosts.md J/C/JS/P/S codes, conformance.md F codes.
**Default posture used for recommendations** (override per-ruling as you see fit):
1. Prefer an ERROR over any silent behavior (silent drop/no-op/misparse caused the worst findings).
2. Prefer R7RS/standard semantics where churn is low; prefer current-behavior-plus-documentation
where churn is high and behavior is defensible.
3. Every ruling lands with conformance rows that run on BOTH production kernels (native + WASM).
**Companion decisions (not language rulings, restated for context):**
- D1 host lineup — recommended: kernel family (native OCaml + WASM) are the only evaluator
targets; hosts/javascript and hosts/python standalone retired; shared/sx/parser.py shrunk to a
wire-subset with a parity suite. Rulings below marked [D1] simplify to kernel-only if ratified.
- D3 gate — recommended: native corpus green (hs-upstream skip-listed) + same corpus on WASM +
cross-kernel differential battery + CEK-vs-JIT differential (when JIT on) + sx_ref.ml regen diff.
---
## A. Bindings & scope
### R1. `set!` on an unbound name
- Current: silently creates a root binding (tested intent, test-scope.sx:196) — but BOTH spec docs
say error (eval-rules.sx:112, special-forms.sx:141), and under JIT it writes a different global
table than the interpreter (split brain).
- RECOMMENDATION: **ERROR** ("set!: <name> is not bound — use define"). Typo'd set! is a bug-hider;
the docs already promise this. Flip test-scope.sx:196; sweep the corpus for reliance (expected
small — the idiom is define-then-set!). Either way the JIT/interpreter split MUST die.
- Churn: low-medium. Findings: core set!-unbound; hosts J-globals split. STATUS: PROPOSED
### R2. The ~60 special-form/HO names (`map`, `filter`, `bind`, `match`, `do`, `case`, `->`, …)
- Current: `define`/`let`/`defmacro` of these names is silently accepted but ignored in call
position (CEK); the VM honors them (J8) — worst of both worlds.
- RECOMMENDATION: **reserved words**`define`/`let`/`set!`/`defmacro` of any dispatch-table name
is a load-time ERROR. Publish the list in spec. Align the VM. (Full lexical honoring is more
Schemely but taxes every list-head dispatch and rescues little real code.)
- Churn: low (error surfaces existing dead definitions). Findings: core unshadowable-names; J8. STATUS: PROPOSED
### R3. `let` semantics
- Current: sequential (`let*`), body = implicit begin, on BOTH engines (tested intent). CLAUDE.md
island rules claim the opposite (describes a dead evaluator).
- RECOMMENDATION: **ratify current behavior**: `let``let*`; body sequences. Fix CLAUDE.md.
Document (or forbid) the observed letrec-ish quirk that binding-init lambdas capture the shared
frame (`(let ((f (fn () a)) (a 5)) (f))` → 5).
- Churn: zero (docs only). Findings: core let-docs; hosts handoff let-sequential. STATUS: PROPOSED
### R4. `letrec`
- Current: parallel (all inits evaluated, then bound); read-before-init yields nil silently; PLUS
two outright bugs (names injected into foreign lambdas' closures = global contamination;
named-let loop name leaks into and clobbers the enclosing frame).
- RECOMMENDATION: **letrec\* semantics** (sequential init) with ERROR on read-before-init
(pre-bind to an "uninitialized" sentinel that faults on read). Named-let binds its loop name in
a fresh frame, invisible after the form. The closure-injection and frame-leak are bugs to fix
regardless of ruling.
- Churn: low. Findings: core letrec-parallel/-injection/named-let. STATUS: PROPOSED
### R5. Component `&key` conventions
- Current: `:flag false` is coerced to nil (indistinguishable from omitted); trailing keyword with
no value silently binds nil; `&key` on plain fn/defmacro silently misbinds.
- RECOMMENDATION: `false` is a legal &key value (bind via has-key, not `(or …)`); trailing keyword
without a value = ERROR; `&key` in fn/defmacro either implemented identically to components or
ERROR at definition (recommend: implement — one binding path for all three).
- Churn: low. Findings: core &key-false / trailing-kw / defmacro-&key. STATUS: PROPOSED
## B. Errors & conditions
### R6. Handler installation semantics
- Current: a handler runs with ITSELF still installed → any raise/error inside a guard clause or
handler-bind handler loops forever (crit 1; WASM-verified).
- RECOMMENDATION: **R7RS/CL semantics** — handlers run with the OUTER handler set; guard clause
bodies evaluate after the escape (the no-match auto-reraise path already does this correctly —
make it the only path).
- Churn: zero for correct code (only un-hangs broken cases). Findings: crit 1, guard family. STATUS: PROPOSED
### R7. What is catchable
- Current: only guest `(raise …)` reaches guard; host primitive errors, undefined-symbol, arity
errors, and strict type errors all blow through every handler.
- RECOMMENDATION: **everything is a condition.** Host/primitive/strict/undefined-symbol errors are
raised as structured condition dicts ({:type :message :op …}) through the same channel guard
sees. Reserve a non-catchable class only for kernel panics.
- Churn: low-medium (code that "relied" on uncatchability is unlikely). Findings: core
host-errors-uncatchable, strict-uncatchable; enables sane server error pages. STATUS: PROPOSED
### R8. `raise-continuable` / `signal-condition`
- RECOMMENDATION: ratify R7RS: handler's value returns to the signal site (the current
whole-program-result behavior is crit 2's frame-key bug, not a semantic choice). STATUS: PROPOSED
## C. Special forms
### R9. `cond` grammar — kill the dual-mode heuristic
- Current: flat pairs documented; undocumented Scheme clause mode auto-detected iff every arg is a
2-element list → silent side-effect drops, mode flips, wrong values (core cond-ambiguity).
- RECOMMENDATION: **flat pairs only**: `(cond t1 r1 t2 r2 … :else d)`. Multi-expression results
use explicit `(do …)`. Support arrow as a flat triple `t => receiver`. A clause-shaped arg list
as a test position is just evaluated — no mode detection ever. Migrate the cond-arrow suite
(test-r7rs.sx:135-145) and any clause-mode usage (sweep needed).
- Churn: medium (sweep + migrate clause-mode call sites). Findings: core cond-ambiguity,
guard-multi-expr (inherits). STATUS: PROPOSED
### R10. `case`
- RECOMMENDATION: ratify the flat evaluated-datums form and document it (vals ARE evaluated,
first-match, structural `=`); `:else`/`else` legal ONLY in final position (else ERROR); Scheme
datum-list clause syntax → clear parse-time ERROR ("use flat pairs"). Keyword/string punning
follows R21 and gets documented.
- Churn: low. Findings: core case-else-position / case-dialect. STATUS: PROPOSED
### R11. `do`
- Current: `do` is a begin-alias EXCEPT when its first form's head is a list — then it's a Scheme
do-loop → IIFE misparse.
- RECOMMENDATION: **`do` = begin alias, always.** Scheme do-loop moves to a distinct name
(`do-loop`) or is dropped (named let covers it). Kills the heuristic.
- Churn: low (sweep for real do-loop usage; expected rare). Findings: core do-IIFE. STATUS: PROPOSED
### R12. Quasiquote
- RECOMMENDATION, four sub-rulings:
a. `unquote-splicing` becomes an alias of `splice-unquote` (one-line; kills the silent
zero-splice trap; rename the misleadingly-named tests).
b. Implement standard **depth tracking** (nested quasiquote raises quote depth; `,,x` works).
Hosts agree current shallow behavior is consistent-but-nonstandard — fix at spec level.
c. Quasiquote **traverses dict literals** (`{:k ,v}` works).
d. Splicing a non-list and malformed splice arity → ERROR.
- Churn: low (b is the only subtle one). Findings: core qq-longhand/-depth/-dicts/-splice-nonlist. STATUS: PROPOSED
### R13. Threading `->` / `->>`
- RECOMMENDATION: (a) steps evaluate in CEK frames (bug: guard/IO broken through threading);
(b) a lambda literal as a step = expand-time ERROR; (c) keyword step sugar: `(-> x :k :j)`
`(-> x (get :k) (get :j))` — cheap, expected, kills the `Not callable: nil` trap; (d) remove the
dead `|>` dispatch branch (parser rejects `|` anyway); (e) fix reduce-seeding via R15.
- Churn: low. Findings: core threading-nested-CEK/-lambda-literal/|>-dead/keywords-as-getters. STATUS: PROPOSED
### R14. `match`
- RECOMMENDATION: `(pattern (when cond))` guard clauses either implemented or ERROR — never
silently read as a structural pattern (current). Recommend: implement (small, high value).
Document let-match as dict-destructuring-only with a clear error for list patterns.
- Churn: low. Findings: core match-guards. STATUS: PROPOSED
## D. Calling convention
### R15. Higher-order forms
- RECOMMENDATION, six sub-rulings:
a. Arg-order swap happens ONLY when exactly one argument is callable (components count as
callable); both-callable or neither → ERROR "map: cannot determine function/collection".
b. `(reduce f coll)` (2-arg) = Clojure-style fold (first element as init, empty coll → error
unless f has identity? keep simple: empty → ERROR); `(reduce init f coll)` and threaded
`(-> init (reduce f coll))` work via the one-callable rule in (a).
c. Data-first with extra args = ERROR (today silently dropped).
d. Multi-collection map coerces every collection with seq-to-list (strings/vectors), zips to
shortest (already); map over a dict iterates `(k v)` pairs.
e. HO names are first-class: `map` etc. in value position resolve to real closures so
`(define f map)` / `(apply map …)` work.
f. Zero/one-arg HO calls = arity ERROR (today silently `()`).
Also fix the O(n²) accumulation (implementation, not semantics).
- Churn: medium — (a) changes behavior for ambiguous calls, sweep needed. Findings: core reduce-2arg /
reduce-swap / swap-drops-args / HO-not-first-class / ho-cryptic-errors / multi-coll / zero-arg;
J7 (VM parity). STATUS: PROPOSED
### R16. `apply`
- Current: native never spreads; WASM spreads 2-arg; test runner has a third behavior (three-way
divergence, F-3 + core corrected finding).
- RECOMMENDATION: **R7RS**: `(apply f a b … rest-list)` spreads, leading args prepended. All
surfaces align; strict checks fire through apply (R25).
- Churn: low (today it mostly errors). STATUS: PROPOSED
### R17. Arity checking (too-few args)
- Current: missing params silently nil-fill (this is load-bearing: 1-arg `(assert x)` works only
via nil-fill); too-many errors.
- RECOMMENDATION: **ERROR on too-few** as well, with `&optional`/`&key`/`&rest` as the explicit
mechanisms. Sweep required (harness `assert`, any nil-fill reliance). If the sweep turns up
heavy reliance, fallback position: keep nil-fill but document it loudly and make strict mode
error. Primary recommendation stands: error.
- Churn: **high** — flagged as the riskiest ruling; do the sweep before ratifying. Findings: core
strict-too-few / harness-assert nit. STATUS: PROPOSED
## E. Keywords, equality, types
### R18 (=R21 referenced above). Keywords
- RECOMMENDATION: ratify current model — keywords self-evaluate to their string name; keyword-ness
exists only in unevaluated AST. Consequences made explicit: `(keyword-name :k)` needs a quote;
`"keyword"` is REMOVED from the strict type system; case/dict punning documented. NOT callable
(R13c covers the getter idiom).
- Churn: zero (docs + removing a dead type branch). STATUS: PROPOSED
### R19. Equality
- RECOMMENDATION (low-churn variant, chosen deliberately over full R7RS split):
a. `=` stays deep structural equality (alias equal?) — ubiquitous in the corpus; add the missing
**Char arm** (today `(= #\a #\a)` → false) and any other missing type arms; document that
`(= 1 1.0)` → true (numeric value equality inside =).
b. Add real `eqv?` (identity + exact numeric/char equality) and `eq?` (alias identical?) as
kernel primitives — they are spec-declared today but implemented NOWHERE.
c. Comparisons `< > <= >=` become n-ary chained (R7RS); `=` stays 2+-ary deep.
d. If content-addressing ever needs exactness-distinguishing equality, that's `eqv?`, not `=`.
- Churn: low. Findings: core eq?/eqv?-missing, =-binary, char-equality; P5. STATUS: PROPOSED
### R20. Strict typing
- RECOMMENDATION: (a) checks move to the continue-with-call/vm_call chokepoints → HO callbacks,
apply, components, => receivers all covered; (b) unknown type name at declaration = ERROR;
(c) `"component"` becomes a real type branch; `"keyword"` removed (R18); (d) `(:as type)` param
annotations become the declaration channel (deprecate the name-keyed global dict, which is
trivially evaded and inherited by shadowers); (e) strict errors are catchable conditions (R7);
(f) set-prim-param-types! merges and validates; (g) return types: explicitly out of scope now.
- Churn: low-medium. Findings: core strict-* family (8 findings). STATUS: PROPOSED
## F. Numbers
### R21. Integer model & overflow
- Current: native = int63 with overflow-promote-to-float on + and * but silent WRAP on expt;
WASM = 32-bit silent wrap (F-1 — production browsers!); JS bundle = float64.
- RECOMMENDATION: spec defines SX integers as **exact within ±2^53** (the portable range);
arithmetic that exceeds the host's exact range **promotes to float** (never wraps) — `expt`
included. WASM must be fixed to match (js_of_ocaml int64/boxed or explicit overflow checks) —
hosts lane feasibility-checks the mechanism; silent 32-bit wrap is a bug under any ruling.
Values beyond 2^53 must not be trusted exact across the wire.
- Churn: low at spec level; WASM fix is real hosts work. Findings: F-1, P4, core expt. STATUS: PROPOSED
### R22. Division & zero
- RECOMMENDATION: integer `/`, `mod`, `quotient`, `remainder` by zero = catchable SX condition
(today: raw OCaml Division_by_zero for mod/quotient, silent `inf` for /); float ops keep IEEE
(inf/nan). `/` doc fixed: returns int when exact, float otherwise (current behavior ratified).
- Churn: low. Findings: core div-by-zero, /-doc. STATUS: PROPOSED
### R23. Float text & wire
- RECOMMENDATION: **shortest-round-trip printing everywhere** (native `%g` 6-sig-digit printing is
a wire-corruption bug — P1); `inf`/`-inf`/`nan` are THE wire tokens on all hosts (P10); `round`
stays half-away-from-zero, documented (R7RS banker's rejected: churn without benefit);
`inexact->exact` rounding behavior kept + documented; `str 1.0` → keep `"1"` but canonical/wire
serializers must preserve the float/int distinction (`1.0` serializes as `1.0`).
- Churn: low. Findings: P1, P10, core round/float-rendering; canonical CID determinism. STATUS: PROPOSED
### R24. Rationals
- RECOMMENDATION: `string->number` parses `"1/2"`; `(/ 1 3)` stays float (rationals remain opt-in
via make-rational) — documented; radix arg restored by fixing the r7rs.sx shadow (C2).
- Churn: low. STATUS: PROPOSED
## G. Strings
### R25. Unit semantics
- Current: native counts UTF-8 bytes (substring can split codepoints → invalid UTF-8); JS counts
UTF-16 units; constructors are codepoint-aware. Project style mandates UTF-8 text everywhere.
- RECOMMENDATION: **codepoint semantics** for length/substring/index/ref at the spec level; kernel
implements UTF-8-aware ops. Accept the perf cost (or add byte-* variants for hot paths later).
- Churn: medium (kernel work + any code relying on byte counts). Findings: core UTF-8 family, P6. STATUS: PROPOSED
### R26. Case mapping
- RECOMMENDATION: kernel `upcase`/`downcase`/`upper`/`lower` are **ASCII-only, documented** (full
Unicode case tables deferred; JS's full-Unicode behavior dies with D1). Aliases exist on all
surfaces (P11).
- Churn: zero. STATUS: PROPOSED
### R27. `split` and escapes
- RECOMMENDATION: `split` = literal substring separator, keeps empties, empty separator → chars
(ratifies native; pin with the multi-char test that history shows is needed). String escape
table is normative: `\n \t \r \\ \" \uXXXX(validated: 4 hex digits, scalar value, else ERROR)`;
**unknown escape = parse ERROR** (kills the native-keeps-backslash vs guest-drops-it silent
divergence, C25 direction fight).
- Churn: low. Findings: core split note, \u family, unknown-escape divergence; C25. STATUS: PROPOSED
## H. Collections, nil, dicts
### R28. nil vs empty list
- Current: distinct values in the reader/serializer; `(cons 1 nil)``(1)` on native (nil-as-
empty in constructors); read ops inconsistent (`first nil` → nil but `reverse nil` → error).
- RECOMMENDATION: nil and `()` remain **distinct values**; collection READ ops uniformly
**nil-pun** (treat nil as empty: first/rest/nth/last/reverse/len/empty? all accept nil);
constructors keep nil-as-empty seeding (cons/append onto nil). `nil?``empty?` preserved.
- Churn: low (only un-errors cases). Findings: core nil-tolerance; P7/P8 arms. STATUS: PROPOSED
### R29. Dict ordering
- RECOMMENDATION: **insertion order preserved** — iteration, keys/vals, and serialization (OCaml
Hashtbl replaced with an insertion-indexed structure; keys-reversed bug dies). CANONICAL form
always sorts keys independently (already true in the CBOR/CID layer). Duplicate literal keys:
last-wins, documented.
- EMPIRICAL NOTE (quick-wins batch, 2026-07-03): an interim sorted-keys change broke 4 render
tests — attr emission order flows through dict_keys and the tests PIN source-order attributes
(`width` before `height` etc.). So the current reverse-ish order is load-bearing for render;
any change here must land together with the render-attr ordering contract. Reverted; do not
change keys order except via this ruling.
- Churn: medium (kernel dict rework) but pays across wire/golden/cache findings C27/P9/core-keys. STATUS: PROPOSED
### R30. Small-primitive contract fixes (spec already says; hosts violate)
- RECOMMENDATION: ratify the spec text and fix: `contains?` on dicts = key check; `merge` skips
nil; `into` native on the kernel; `sort` takes an optional comparator, compares int/float
numerically, stable; `get` returns a STORED nil (default only when key absent); `zip-pairs` =
sliding window per spec (kernel currently chunks); `(max)`/`(min)` zero-arg = ERROR.
- Churn: low each. Findings: core contains?/sort/keys; P2/P3/P8/P12; JS `get` arm. STATUS: PROPOSED
### R31. `append!` and mutation
- Current: silently no-ops on ANY derived list (map/filter/rest/reverse output) — worst silent-
data-loss finding in the primitives sweep.
- RECOMMENDATION: `append!` **ERRORS on non-mutable lists** immediately (honest), and is
deprecated in favor of persistent `append` + a real mutable vector/buffer for accumulator
idioms. Sweep the corpus (it's a known accumulator idiom in loops).
- Churn: medium (idiom sweep). Findings: core append!. STATUS: PROPOSED
## I. Parser & wire
### R32. One token grammar
- RECOMMENDATION: publish the normative ident/number classifier in spec/parser.sx and make every
surface bind THE SAME table (today: four divergent tables → same source, different ASTs).
Specific token rulings: maximal-munch then classify (`1+`, `a,b` are symbols — ratifies native);
hex/binary/octal `#x/#o/#b`-style and `0x10` accepted, documented; `inf`/`nan`/`-inf` are number
literals (reserved, not idents); `1e` and other malformed numbers = parse ERROR (never nil);
unicode identifiers **allowed** (UTF-8 letters — the docs mandate UTF-8 text; native reader
extends its charset); `$`/`|` NOT ident chars; `.` IS a valid symbol (ratifies native; JS4 dies
with D1); `#t`/`#f` = boolean literals on all surfaces.
- Churn: medium (native reader charset + guest table sync). Findings: core parser-divergence
family; C1b (unicode symbol kills server — fixed by charset + C1 try-wrap); JS4. STATUS: PROPOSED
### R33. Reader extensibility & comments
- RECOMMENDATION: implement the `#name` reader-macro registry on the kernel (spec documents it;
only JS has it today) — small, and sx-pub extensibility wants it. `#;` datum comment valid
before `)` and at EOF (standard). `#|…|` stays a RAW STRING (documented loudly as not-a-block-
comment); no block comments.
- Churn: low. Findings: core reader-macro/datum-comment/raw-string. STATUS: PROPOSED
### R34. Dict literals & serializer round-trip
- RECOMMENDATION: dict literal keys must be keyword/string/symbol — anything else is a parse ERROR
on every parser (guest currently stringifies `{1 2}` silently); odd form count gets a "dict
needs key-value pairs" error. Serializer: dict keys escaped/round-trippable (today unparseable
output for non-ident keys — also a CID hazard); chars serialize by codepoint (`#\é` readable
back once R25 lands); PROPERTY TEST: `parse(serialize(x)) = x` for the full value lattice, run
on both kernels.
- Churn: low. Findings: core serializer-dict-keys / multibyte-chars / dict-edges. STATUS: PROPOSED
### R35. Canonical form & CIDs (sx-pub-critical)
- RECOMMENDATION: the **native CBOR/CID path is normative** (key-sorted, verified native==WASM,
F-3). The canonical TEXT form is defined as: sorted keys, shortest-round-trip floats with
preserved int/float distinction, fully-escaped strings, and is a fixed point
(canonical(parse(canonical(x))) = canonical(x)) — property-tested cross-kernel. spec/canonical.sx
either becomes a tested mirror of the native path (fix its runner-only helpers) or is deleted;
two silently-diverging implementations is the one unacceptable state.
- Churn: low-medium. Findings: core canonical family; F-3; P9/C27 (via R29). STATUS: PROPOSED
### R40. Primitive naming & small-default unification (answers the hosts handoff list)
- RECOMMENDATION: one canonical name registry in spec/primitives.sx; per-host aliases die (with
D1 most of these resolve to "make it native on the kernel"): `json-encode`/`json-parse` are
KERNEL primitives (not IO-bridge helpers — today unavailable sandboxed); `regex-*` is the
canonical family name; `parse`/`sx-parse``sx-parse` canonical, `parse` alias documented;
1-arg `(range n)` = 0..n-1 (ratifies native); `parse-int`/`string->number` on failure → nil
(ratifies native, never 0); `format` and the stdlib move for real (the primitives.sx header
claims a stdlib migration that never happened — make the header true or revert it) and
spec/stdlib.sx loads in production (today `format` is unresolved on the server).
- Churn: low. Findings: F-9 naming splits, P7 arms, core spec-drift / stdlib-header. STATUS: PROPOSED
## J. Render contracts
### R36. Attribute contract (all four adapters)
- RECOMMENDATION: one contract, HTML-mode's as base: boolean-registry attrs — false/nil omit,
anything else emits bare name (SX truthiness, documented footgun stands); non-boolean attrs —
value stringified INCLUDING `"true"`/`"false"` (DOM adapter aligns — C19/core, found by both
lanes); attribute NAMES validated `[A-Za-z_:][A-Za-z0-9_:.-]*` else ERROR (kills spread-dict
injection); nil attr value omits the attribute.
- Churn: low. Findings: core attr-name-injection / bool-footguns / dom-html-parity; C19. STATUS: PROPOSED
### R37. Raw-text elements & voids
- RECOMMENDATION: `<script>`/`<style>` children are NOT entity-escaped; instead the renderer
ERRORS if content contains `</script`/`</style` (raw! unchanged as the explicit bypass) — HTML-
correct and injection-safe, and stops corrupting legitimate inline JS/CSS. Void-element registry
completed (area/base/embed/param/track added to HTML_TAGS); children passed to a void element =
ERROR (today silently dropped). Component render gets a depth limit (default 512) with a clear
error. `is-render-expr?` either wired in (html:/custom elements) or deleted.
- Churn: low. Findings: core script-style / void-elements / recursive-component / is-render-expr. STATUS: PROPOSED
### R38. aser wire format
- RECOMMENDATION: list-valued keyword args serialize quoted (`:items (quote (…))`) so the wire
form re-evaluates to the same value; contract documented: components unexpanded, control flow
evaluated, all VALUES must round-trip through parse+eval. Property-test aser output re-evaluation.
- Churn: low. Findings: core aser-list-kwargs; S14 (deep-nesting parity — add the depth-2 test). STATUS: PROPOSED
## K. Signals (spec-level semantics)
### R39. Reactive semantics
- RECOMMENDATION: (a) change detection by `=` (deep equality — needs R19's Char arm etc.), not
physical identity — kills the every-reset-notifies behavior; (b) `batch` is unwind-safe
(depth decremented on any exit — today one throw wedges all reactivity forever); (c) notify is
glitch-free: two-phase (mark dirty → recompute in topological order) so diamonds recompute once;
(d) dispose-computed actually unsubscribes (bug); (e) effect cleanup cleared after invocation
(bug). Ratify as the documented reactive contract with tests (today: zero coverage of these).
- Churn: low-medium (topological notify is the real work). Findings: core signals family (5). STATUS: PROPOSED
---
## Ratification checklist
1. Flip each STATUS; AMENDED rulings get their text edited in place.
2. High-churn rulings needing a pre-ratification sweep: **R17 (arity)**, R9 (cond clause-mode
usage), R31 (append! idiom), R15a (ambiguous HO calls).
3. On ratification: move to spec/RULINGS.md; every ruling becomes (a) a conformance battery row
(native + WASM), (b) a fix ticket if behavior changes, (c) a docs line in CLAUDE.md's rewrite.
4. Rulings deliberately NOT made here (need your call, no strong recommendation):
- Whether rationals should ever be the default result of exact `/` (R24 keeps float).
- Whether to pursue full-Unicode case mapping (R26 defers).
- Whether `do-loop` (R11) is worth keeping at all vs deleting Scheme do entirely.
- JIT re-enable timeline (J1J8 are preconditions, not rulings).