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>
397 lines
26 KiB
Markdown
397 lines
26 KiB
Markdown
# 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 (J1–J8 are preconditions, not rulings).
|