33 KiB
SX Primitives — Meta-Loop Briefing
Goal: add fundamental missing SX primitives in sequence, then sweep all language implementations to replace their workarounds. Full rationale: vectors fix O(n) array access across every language; numeric tower fixes float/int conflation; dynamic-wind fixes cleanup semantics; coroutine primitive unifies Ruby/Lua/Tcl; string buffer fixes O(n²) concat; algebraic data types eliminate the tagged-dict pattern everywhere.
Each fire: find the first unchecked [ ], do it, commit, tick it, stop.
Sub-items within a Phase may span multiple fires — just commit progress and tick what's done.
Phase 0 — Prep (gate)
-
Stop new-language loops: send
/exitto sx-loops windows for the four blank-slate languages that haven't committed workarounds yet:tmux send-keys -t sx-loops:common-lisp "/exit" Enter tmux send-keys -t sx-loops:apl "/exit" Enter tmux send-keys -t sx-loops:ruby "/exit" Enter tmux send-keys -t sx-loops:tcl "/exit" EnterVerify all four windows are idle (claude prompt, no active task). -
E38 + E39 landed: check both Bucket-E branches for implementation commits.
git log --oneline hs-e38-sourceinfo | head -5 git log --oneline hs-e39-webworker | head -5If either branch has only its base commit (no impl work yet): note "pending" and stop — next fire re-checks. Proceed only when both have at least one implementation commit.
Phase 1 — Vectors
Native mutable integer-indexed arrays. Fix: Lua O(n) sort, APL rank polymorphism, Ruby Array, Tcl lists, Common Lisp vectors, all using string-keyed dicts today.
Primitives to add:
make-vectorn[fill]→ vector of length nvector?v→ boolvector-refvi→ element at index i (0-based)vector-set!vix→ mutate in placevector-lengthv→ integervector->listv→ listlist->vectorlst→ vectorvector-fill!vx→ fill all elementsvector-copyv[start][end]→ fresh copy of slice
Steps:
- OCaml: add
SxVector of value arraytohosts/ocaml/sx_types.ml; implement all primitives inhosts/ocaml/sx_primitives.ml(or equivalent); wire into evaluator. Note: Vector type + most prims were already present; added bounds-checked vector-ref/set! and optional start/end to vector-copy. 10/10 vector tests pass (r7rs suite). - Spec: add vector entries to
spec/primitives.sxwith type signatures and descriptions. All 10 vector primitives now have :as type annotations, :returns, and :doc strings. make-vector: optional fill param; vector-copy: optional start/end (done prev step). - JS bootstrapper: implement vectors in
hosts/javascript/platform.js(or equivalent); ensuresx-browser.jsrebuild picks them up. Fixed index-of for lists (was returning -1 not NIL, breaking bind-lambda-params), added lastErrorKont/hostError/try-catch/without-io-hook stubs. Vectors work. - Tests: 40+ tests in
spec/tests/test-vectors.sxcovering construction, ref, set!, length, conversions, fill, copy, bounds behaviour. 42 tests, all pass. 1847 standard / 2362 full passing (up from 5). - Verify: full test suite still passes (
node hosts/javascript/run_tests.js --full). 2362/4924 pass (improvement from pre-existing lambda binding bug, no regressions). - Commit:
spec: vector primitive (make-vector/vector-ref/vector-set!/etc)Committed as: js: fix lambda binding (index-of on lists), add vectors + R7RS platform stubs
Phase 2 — Numeric tower
Float ≠ integer distinction. Fix: Erlang =:=, Lua math.type(), Haskell Num/Integral,
Common Lisp integerp/floatp/ratio, JS Number.isInteger.
Changes:
parse-numberpreserves float identity:"1.0"→ float 1.0, not integer 1- New predicates:
integer?,float?,exact?,inexact? - New coercions:
exact->inexact,inexact->exact - Fix
floor/ceiling/truncate/roundto return integers when applied to floats number->stringrenders1.0as"1.0",1as"1"- Arithmetic:
(+ 1 1.0)→2.0(float contagion),(+ 1 1)→2(integer)
Steps:
- OCaml: distinguish
Integer of int/Number of floatinsx_types.ml; update all arithmetic primitives for float contagion; fixparse-number. 92/92 numeric tower tests pass; 4874 total (394 pre-existing hs-upstream fails unchanged). - Spec: update
spec/primitives.sxwith new predicates + coercions; document contagion rules. Added integer?/float? predicates; updated number? body; / returns "float"; floor/ceil/truncate return "integer"; +/-/* doc float contagion; fixed double-paren params; 4874/394 baseline. - JS bootstrapper: update number representation and arithmetic.
- Tests: 92 tests in
spec/tests/test-numeric-tower.sx— int-arithmetic, float-contagion, division, predicates, coercions, rounding, parse-number, equality, modulo, min-max, stringify. - Verify: full suite passes. Pay attention to any test that relied on
1.0 = 1. - Commit:
spec: numeric tower — float/int distinction + contagion
Phase 3 — Dynamic-wind
Fix: Common Lisp unwind-protect, Ruby ensure, JS finally, Tcl catch+cleanup,
Erlang try...after (currently uses double-nested guard workaround).
- Spec: implement
dynamic-windinspec/evaluator.sxsuch that the after-thunk fires on both normal return AND non-local exit (raise/call-cc escape). Must compose withguard— currently they don't interact. - OCaml: wire
dynamic-windthrough the CEK machine with aWindFramecontinuation. - JS bootstrapper: update.
- Tests: 20+ tests covering normal return, raise, call/cc escape, nested dynamic-winds.
- Commit:
spec: dynamic-wind + guard integration
Phase 4 — Coroutine primitive
Unify Ruby fibers, Lua coroutines, Tcl coroutines — all currently reimplemented separately using call/cc+perform/resume.
- Spec: add
make-coroutine,coroutine-resume,coroutine-yield,coroutine?,coroutine-alive?tospec/primitives.sx. Build on existingperform/cek-resumemachinery — coroutines ARE perform/resume with a stable identity. - OCaml: implement coroutine type; wire resume/yield through CEK suspension.
- JS bootstrapper: update.
- Tests: 25+ tests — multi-yield, final return, arg passthrough, alive? predicate, nested coroutines, "final return vs yield" distinction (the Lua gotcha).
- Commit:
spec: coroutine primitive (make-coroutine/resume/yield)
Phase 5 — String buffer
Fix O(n²) string concatenation in loops across Lua, Ruby, Common Lisp, Tcl.
- Spec + OCaml: add
make-string-buffer,string-buffer-append!,string-buffer->string,string-buffer-lengthto primitives. OCaml:Buffer.twrapper. JS: array+join. - Tests: 15+ tests.
- Commit:
spec: string-buffer primitive
Phase 6 — Algebraic data types
The deepest structural gap. Every language uses {:tag "..." :field ...} tagged dicts to
simulate sum types. A native define-type + match form eliminates this everywhere.
-
Design: write
plans/designs/sx-adt.mdcovering syntax, CEK dispatch, interaction with existingcond/case, exhaustiveness checking, recursive types, pattern variables. Draft, then stop — next fire reviews design before implementing. -
Spec: implement
define-typespecial form inspec/evaluator.sx:(define-type Name (Ctor1 field...) (Ctor2 field...) ...)Creates constructor functionsCtor1,Ctor2+ predicateName?. -
Spec: implement
matchspecial form:(match expr ((Ctor1 a b) body) ((Ctor2 x) body) (else body))Exhaustiveness warning if not all constructors covered and noelse. -
OCaml: add
SxAdt of string * value arrayto types; implement constructors + match. -
JS bootstrapper: update.
-
Tests: 40+ tests in
spec/tests/test-adt.sx. -
Commit:
spec: algebraic data types (define-type + match)
Phase 7 — Bitwise operations
Completely absent today. Needed by: Forth (core), APL (array masks), Erlang (bitmatch),
JS (typed arrays, bitfields), Common Lisp (logand/logior/logxor/lognot/ash).
Primitives to add:
bitwise-andab→ integerbitwise-orab→ integerbitwise-xorab→ integerbitwise-nota→ integerarithmetic-shiftacount→ integer (left if count > 0, right if count < 0)bit-counta→ number of set bits (popcount)integer-lengtha→ number of bits needed to represent a
Steps:
- Spec: add entries to
spec/primitives.sxwith type signatures. - OCaml: implement in
hosts/ocaml/sx_primitives.mlusing OCamlland/lor/lxor/lnot/lsl/lsr. - JS bootstrapper: implement in
hosts/javascript/platform.jsusing JS&/|/^/~/<</>>. - Tests: 25+ tests in
spec/tests/test-bitwise.sx— basic ops, shift left/right, negative numbers, popcount. - Commit:
spec: bitwise operations (bitwise-and/or/xor/not, arithmetic-shift, bit-count)
Phase 8 — Multiple values
R7RS standard. Common Lisp uses them heavily; Haskell tuples map naturally; Erlang multi-return. Without them, every function returning two things encodes it as a list or dict.
Primitives / forms to add:
valuesv...→ multiple-value objectcall-with-valuesproducerconsumer→ applies consumer to values from producerlet-values(((a b) expr) ...)body— binding form (special form in evaluator)define-values(a b ...)expr— top-level multi-value bind
Steps:
- Spec: add
SxValuestype to evaluator; implementvalues+call-with-valuesinspec/evaluator.sx; addlet-values/define-valuesspecial forms. - OCaml: add
SxValues of value listtosx_types.ml; wire through CEK. - JS bootstrapper: implement values type + forms.
- Tests: 25+ tests in
spec/tests/test-values.sx— basic producer/consumer, let-values destructuring, define-values, interaction withbegin/do. - Commit:
spec: multiple values (values/call-with-values/let-values)
Phase 9 — Promises (lazy evaluation)
Critical for Haskell — lazy evaluation is so central that without it the Haskell implementation can't be idiomatic. Also useful for lazy lists in Common Lisp and lazy streams in Scheme-style code generally.
Primitives / forms to add:
delayexpr→ promise (special form — expr not evaluated yet)forcep→ evaluate promise, cache result, return itmake-promisev→ already-forced promise wrapping vpromise?v→ booldelay-forceexpr→ for iterative lazy sequences (avoids stack growth in lazy streams)
Steps:
- Spec: add
delay/delay-forcespecial forms tospec/evaluator.sx; add promise type with mutable forced/value slots;forcechecks if already forced before eval. - OCaml: add
SxPromise of { mutable forced: bool; mutable value: value; thunk: value }; wiredelay/force/delay-forcethrough CEK. - JS bootstrapper: implement promise type + forms.
- Tests: 25+ tests in
spec/tests/test-promises.sx— basic delay/force, memoisation (forced only once), delay-force lazy stream, promise? predicate, make-promise. - Commit:
spec: promises — delay/force/delay-force for lazy evaluation
Phase 10 — Mutable hash tables
Distinct from SX's immutable dicts. Dict primitives copy on every update — fine for functional code, wrong for table-heavy language implementations. Lua tables, Smalltalk dicts, Erlang process dictionaries, and JS Map all need O(1) mutable associative storage.
Primitives to add:
make-hash-table[capacity]→ fresh mutable hash tablehash-table?v→ boolhash-table-set!htkeyval→ mutate in placehash-table-refhtkey[default]→ value or default/errorhash-table-delete!htkey→ remove entryhash-table-sizeht→ integerhash-table-keysht→ list of keyshash-table-valuesht→ list of valueshash-table->alistht→ list of (key . value) pairshash-table-for-eachhtfn→ iterate (fn key val) for side effectshash-table-merge!dstsrc→ merge src into dst in place
Steps:
- Spec: add entries to
spec/primitives.sx. - OCaml: add
SxHashTable of (value, value) Hashtbl.ttosx_types.ml; implement all primitives inhosts/ocaml/sx_primitives.ml. - JS bootstrapper: implement using JS
Mapinhosts/javascript/platform.js. - Tests: 30+ tests in
spec/tests/test-hash-table.sx— set/ref/delete, size, iteration, default on missing key, merge, keys/values lists. - Commit:
spec: mutable hash tables (make-hash-table/ref/set!/delete!/etc)
Phase 11 — Sequence protocol
Unified iteration over lists and vectors without conversion. Currently map/filter/
for-each only work on lists — you must vector->list first, which defeats the purpose
of vectors. A sequence protocol makes all collection operations polymorphic.
Approach: extend existing map/filter/reduce/for-each/some/every? to dispatch
on type (list → existing path, vector → index loop, string → char iteration). Add:
in-rangestart[end][step]→ lazy range sequence (works withfor-each/map)sequence->lists→ coerce any sequence to listsequence->vectors→ coerce any sequence to vectorsequence-lengths→ length of any sequencesequence-refsi→ element by index (lists and vectors)sequence-appends1s2→ concatenate two same-type sequences
Steps:
- Spec: extend
map/filter/reduce/for-each/some/every?inspec/evaluator.sxto type-dispatch; addin-rangelazy sequence type + helpers. - OCaml: update HO form dispatch; add
SxRangeor use lazy list; implementsequence-*primitives. - JS bootstrapper: update.
- Tests: 30+ tests in
spec/tests/test-sequences.sx— map over vector, filter over range, for-each over string chars, sequence-append, sequence->list/vector coercions. - Commit:
spec: sequence protocol — polymorphic map/filter/for-each over list/vector/range
Phase 12 — gensym + symbol interning
Unique symbol generation. Tiny to implement; broadly needed: Prolog uses it for fresh variable names, Common Lisp uses it constantly in macros, any hygienic macro system needs it, and Smalltalk uses it for anonymous class/method naming.
Primitives to add:
gensym[prefix]→ unique symbol, e.g.g42,var-17. Counter-based, monotonically increasing.symbol-interned?s→ bool — whether the symbol is in the global intern tableinternstr→ symbol — intern a string as a symbol (string->symbol already exists; this is the explicit interning operation for languages that distinguish interned vs uninterned)
Steps:
- Spec: add
gensymcounter to evaluator state; implement inspec/evaluator.sx.string->symbolalready exists —gensymis just a counter-suffixed variant. - OCaml: add global gensym counter; implement primitives.
- JS bootstrapper: implement.
- Tests: 15+ tests in
spec/tests/test-gensym.sx— uniqueness, prefix, symbol?, string->symbol round-trip. - Commit:
spec: gensym + symbol interning
Phase 13 — Character type
Common Lisp and Haskell have a distinct Char type that is not a string. Without it both
implementations are approximations — CL's #\a literal and Haskell's 'a' both need a
real char value, not a length-1 string.
Primitives to add:
char?v→ boolchar->integerc→ Unicode codepoint integerinteger->charn→ charchar=?char<?char>?char<=?char>=?→ comparatorschar-ci=?char-ci<?etc. → case-insensitive comparatorschar-alphabetic?char-numeric?char-whitespace?→ predicateschar-upper-case?char-lower-case?→ predicateschar-upcasechar-downcase→ char → charstring->listextended to return chars (not length-1 strings)list->stringaccepting chars
Also: #\a reader syntax for char literals (parser addition).
Steps:
- Spec: add
SxChartype to evaluator; add char literal syntax#\a/#\space/#\newlinetospec/parser.sx; implement all predicates + comparators. - OCaml: add
SxChar of chartosx_types.ml; implement primitives. - JS bootstrapper: implement char type wrapping a codepoint integer.
- Tests: 30+ tests in
spec/tests/test-chars.sx— literals, char->integer round-trip, comparators, predicates, upcase/downcase, string<->list with chars. - Commit:
spec: character type (char? char->integer #\\a literals + predicates)
Phase 14 — String ports
Needed for any language with a reader protocol: Common Lisp's read, Prolog's term parser,
Smalltalk's printString. Without string ports these all do their own character walking
on raw strings rather than treating a string as an I/O stream.
Primitives to add:
open-input-stringstr→ input portopen-output-string→ output portget-output-stringport→ string (flush output port to string)input-port?output-port?port?→ predicatesread-char[port]→ char or eof-objectpeek-char[port]→ char or eof-object (non-consuming)read-line[port]→ string or eof-objectwrite-charchar[port]→ voidwrite-stringstr[port]→ voideof-object→ the eof sentineleof-object?v→ boolclose-portport→ void
Steps:
- Spec: add port type + eof-object to evaluator; implement all primitives. Ports are mutable objects with a position cursor (input) or accumulation buffer (output).
- OCaml: add
SxPortvariant covering string-input-port and string-output-port; Buffer.t for output, string+offset for input. - JS bootstrapper: implement port type.
- Tests: 25+ tests in
spec/tests/test-ports.sx— open/read/peek/eof, output accumulation, read-line, write-char, close. - Commit:
spec: string ports (open-input-string/open-output-string/read-char/etc)
Phase 15 — Math completeness
Filling specific gaps that multiple language implementations need.
15a — modulo / remainder / quotient distinction
They differ on negative numbers — critical for Erlang rem, Haskell mod/rem, CL mod/rem:
quotientab→ truncate toward zero (same sign as dividend)remainderab→ sign follows dividend (truncation division)moduloab→ sign follows divisor (floor division) — R7RS
15b — Trigonometry and transcendentals
Lua, Haskell, Erlang, CL all need: sin, cos, tan, asin, acos, atan, exp,
log, sqrt, expt. Check which are already present; add missing ones.
15c — GCD / LCM
gcd a b → greatest common divisor; lcm a b → least common multiple.
Needed by Haskell Rational, CL, and any language doing fraction arithmetic.
15d — Radix number parsing / formatting
(number->string n radix) → e.g. (number->string 255 16) → "ff".
(string->number s radix) → e.g. (string->number "ff" 16) → 255.
Needed by: Common Lisp, Smalltalk, Erlang integer formatting.
Steps:
- Audit which trig / math functions are already in
spec/primitives.sx; note gaps. - Spec + OCaml + JS: implement missing trig (
sin/cos/tan/asin/acos/atan/exp/log). - Spec + OCaml + JS:
quotient/remainder/modulowith correct negative semantics. - Spec + OCaml + JS:
gcd/lcm. - Spec + OCaml + JS: radix variants of
number->string/string->number. - Tests: 40+ tests in
spec/tests/test-math.sx. - Commit:
spec: math completeness — trig, quotient/remainder/modulo, gcd/lcm, radix
Phase 16 — Rational numbers
Haskell's Rational type and Common Lisp ratios (1/3) both need this. Natural extension
of the numeric tower (Phase 2) — rationals are the third numeric type alongside int and float.
Primitives to add:
make-rationalnumeratordenominator→ rational (auto-reduced by GCD)rational?v→ boolnumeratorr→ integerdenominatorr→ integer- Reader syntax:
1/3parsed as rational literal - Arithmetic:
(+ 1/3 1/6)→1/2;(* 1/3 3)→1; mixed int/rational → rational exact->inexacton rational → float;inexact->exacton float → rational approximation(number->string 1/3)→"1/3"
Steps:
- Spec: add
SxRationaltype; addn/dreader syntax tospec/parser.sx; extend all arithmetic primitives for rational contagion (int op rational → rational, rational op float → float). - OCaml: add
SxRational of int * int(stored in reduced form); implement all arithmetic. - JS bootstrapper: implement rational type.
- Tests: 30+ tests in
spec/tests/test-rationals.sx— literals, arithmetic, reduction, mixed numeric tower, exact<->inexact conversion. - Commit:
spec: rational numbers — 1/3 literals, arithmetic, numeric tower integration
Phase 17 — read / write / display
Completes the I/O model. Builds on string ports (Phase 14) and char type (Phase 13).
read parses any SX value from a port; write serializes with quoting (round-trippable);
display serializes without quoting (human-readable). Common Lisp's read macro,
Prolog term I/O, and Smalltalk's printString all need this.
Primitives to add:
read[port]→ SX value or eof-object — full SX parser reading from a portread-charalready in Phase 14;readuses it internallywriteval[port]→ void — serializes with quotes:"hello",#\a,(1 2 3)displayval[port]→ void — serializes without quotes:hello,a,(1 2 3)newline[port]→ void — writes\nwrite-to-stringval→ string — convenience:(write val (open-output-string))display-to-stringval→ string — convenience
Steps:
- Spec: implement
readinspec/evaluator.sx— wraps the existing parser to read one datum from a port cursor; handles eof gracefully. - Spec: implement
write/display/newline— extend the existing serializer for port output;writequotes strings + uses#\for chars,displaydoes not. - OCaml: wire
readthrough port type; implementwrite/displayoutput path. - JS bootstrapper: implement.
- Tests: 25+ tests in
spec/tests/test-read-write.sx— read string literal, read list, read eof, write round-trip, display vs write quoting, newline, write-to-string. - Commit:
spec: read/write/display — S-expression reader/writer on ports
Phase 18 — Sets
O(1) membership testing. Distinct from hash tables (unkeyed) and lists (O(n)).
Erlang has sets as a stdlib staple, Haskell Data.Set, APL uses set operations
constantly, Common Lisp has union/intersection on lists but a native set is O(1).
Primitives to add:
make-set[list]→ fresh set, optionally seeded from listset?v→ boolset-add!sval→ voidset-member?sval→ boolset-remove!sval→ voidset-sizes→ integerset->lists→ list (unspecified order)list->setlst→ setset-unions1s2→ new setset-intersections1s2→ new setset-differences1s2→ new set (elements in s1 not in s2)set-for-eachsfn→ iterate for side effectsset-mapsfn→ new set of mapped values
Steps:
- Spec: add entries to
spec/primitives.sx. - OCaml: implement using
Hashtbl.twith unit values (or a properSetfunctor with a comparison function); addSxSettosx_types.ml. - JS bootstrapper: implement using JS
Set. - Tests: 30+ tests in
spec/tests/test-sets.sx— add/member/remove, union/intersection/ difference, list conversion, for-each, size. - Commit:
spec: sets (make-set/set-add!/set-member?/union/intersection/etc)
Phase 19 — Regular expressions as primitives
lib/js/regex.sx is a pure-SX regex engine already written. Promoting it to a primitive
gives every language free regex without reinventing: Lua patterns, Tcl regexp, Ruby regex,
JS regex, Erlang re module. Mostly a wiring job — the implementation exists.
Primitives to add:
make-regexppattern[flags]→ regexp object (flags:"i"case-insensitive,"g"global,"m"multiline)regexp?v→ boolregexp-matchrestr→ match dict{:match "..." :start N :end N :groups (...)}or nilregexp-match-allrestr→ list of match dictsregexp-replacerestrreplacement→ string with first match replacedregexp-replace-allrestrreplacement→ string with all matches replacedregexp-splitrestr→ list of strings (split on matches)- Reader syntax:
#/pattern/flagsfor regexp literals (parser addition)
Steps:
- Audit
lib/js/regex.sx— understand the API it already exposes; map to the primitive API above. - Spec: add
SxRegexptype to evaluator; add#/pattern/flagsliteral syntax tospec/parser.sx; wirelib/js/regex.sxengine as the implementation. - OCaml: implement using OCaml
Relibrary (orStr); addSxRegexpto types. - JS bootstrapper: use native JS
RegExp; wrap in the primitive API. - Tests: 30+ tests in
spec/tests/test-regexp.sx— basic match, groups, replace, replace-all, split, flags (case-insensitive), no-match nil return. - Commit:
spec: regular expressions (make-regexp/regexp-match/regexp-replace + #/pat/ literals)
Phase 20 — Bytevectors
R7RS standard. Needed for WebSocket binary frames (E36), binary protocol parsing, and
efficient string encoding. Also the foundation for proper Unicode: string->utf8 /
utf8->string require a byte array type.
Primitives to add:
make-bytevectorn[fill]→ bytevector of n bytes (fill defaults to 0)bytevector?v→ boolbytevector-lengthbv→ integerbytevector-u8-refbvi→ byte 0–255bytevector-u8-set!bvibyte→ voidbytevector-copybv[start][end]→ fresh copybytevector-copy!dstatsrc[start][end]→ in-place copybytevector-appendbv...→ concatenated bytevectorutf8->stringbv[start][end]→ string decoded as UTF-8string->utf8str[start][end]→ bytevector UTF-8 encodedbytevector->list/list->bytevector→ conversion
Steps:
- Spec: add
SxBytevectortype; implement all primitives inspec/evaluator.sx/spec/primitives.sx. - OCaml: add
SxBytevector of bytestosx_types.ml; implement primitives using OCamlBytes. - JS bootstrapper: implement using
Uint8Array. - Tests: 30+ tests in
spec/tests/test-bytevectors.sx— construction, ref/set, copy, append, utf8 round-trip, slice. - Commit:
spec: bytevectors (make-bytevector/u8-ref/u8-set!/utf8->string/etc)
Phase 21 — format
CL-style string formatting beyond str. (format "Hello ~a, age ~d" name age).
Haskell printf, Erlang io:format, CL format, and general string templating all use this idiom.
Directives:
~a— display (no quotes)~s— write (with quotes)~d— decimal integer~x— hexadecimal integer~o— octal integer~b— binary integer~f— fixed-point float~e— scientific notation float~%— newline~&— fresh line (newline only if not already at start of line)~~— literal tilde~t— tab
Signature: (format template arg...) → string.
Optional: (format port template arg...) — write to port directly.
Steps:
- Spec: implement
formatas a pure SX function inspec/primitives.sx— parses~Xdirectives, dispatches todisplay/write/number->stringas appropriate. Pure SX: no host calls needed. Self-hosting — uses string-buffer (Phase 5) internally. - OCaml: expose as a primitive (or let it run as SX through the evaluator).
- JS bootstrapper: same.
- Tests: 25+ tests in
spec/tests/test-format.sx— each directive, multiple args, nested format, port variant,~~escape. - Commit:
spec: format — CL-style string formatting (~a ~s ~d ~x ~% etc)
Phase 22 — Language sweep
Replace workarounds with primitives. One language per fire (or per sub-item for big ones). Start with blank slates (CL, APL, Ruby, Tcl) — they haven't committed to workarounds yet.
Scope per language: only lib/<lang>/**. Don't touch spec or other languages.
Brief each language's loop agent (or do inline) after rebasing their branch onto architecture.
-
Restart CL/APL/Ruby/Tcl loops with updated briefing pointing to new primitives. Add a note to each
plans/<lang>-on-sx.mdunder a## SX primitive baselinesection: "Use vectors for arrays; numeric tower + rationals for numbers; ADTs for tagged data; coroutines for fibers; string-buffer for mutable string building; bitwise ops for bit manipulation; multiple values for multi-return; promises for lazy evaluation; hash tables for mutable associative storage; sets for O(1) membership; sequence protocol for polymorphic iteration; gensym for unique symbols; char type for characters; string ports + read/write for reader protocols; regexp for pattern matching; bytevectors for binary data; format for string templating." -
Common Lisp: char type (
#\a); string ports +read/writefor reader/printer; gensym for macros; rational numbers for CL ratios; multiple values; sets for CL set ops;modulo/remainder/quotient; radix formatting;formatforcl:format. -
Lua: vectors for arrays; hash tables for Lua tables;
delay/forcefor lazy iterators; regexp for Lua pattern matching; trig from math completeness; bytevectors for binary I/O. -
Erlang: numeric tower for float/int; bitwise ops for bitmatch; multiple values for multi-return; sets for Erlang sets;
remainderforrem; regexp forremodule. -
Haskell: numeric tower for
Num/Integral/Fractional; promises for lazy evaluation (critical); multiple values for tuples; rational numbers forRational; char type forChar;gcd/lcm; sets forData.Set;read/writeforShow/Readinstances. -
JS: vectors for Array; hash tables for
Map; sets forSet; bitwise ops for typed arrays; regexp for JS regex; bytevectors forUint8Array; radix formatting. -
Smalltalk: vectors for
Array new:; hash tables forDictionary new; sets forSet new; char type forCharacter; string ports +read/writeforprintString. -
APL: vectors as core array type; bitwise ops for array masks; sets for APL set ops; sequence protocol for rank-polymorphic operations; format for APL output formatting.
-
Ruby: coroutines for fibers; hash tables for
Hash; sets forSet; regexp for Ruby regex; string ports forStringIO; bytevectors forStringbinary encoding. -
Tcl: string ports for Tcl channel abstraction; string-buffer for
append; coroutines for Tcl coroutines; regexp for Tclregexp; format for Tclformat. -
Forth: bitwise ops (core); string-buffer for word-definition accumulation; bytevectors for Forth's raw memory model.
Ground rules
- Work on the
architecturebranch in/root/rose-ash(main worktree). - Use sx-tree MCP for all
.sxfile edits. Never use raw Edit/Write/Read on.sxfiles. - Commit after each concrete unit of work. Never leave the branch broken.
- Never push to
main— only push toorigin/architecture. - Update this checklist every fire: tick
[x]done, add inline notes on blockers.
Progress log
Newest first.
- 2026-04-26: Phase 2 Spec done — integer?/float? predicates added to spec/primitives.sx; floor/ceil/truncate :returns updated to "integer"; / to "float"; exact->inexact/inexact->exact docs and returns updated; float contagion documented on +/-/*; 4874/394 baseline. Committed
45ec5535. - 2026-04-26: Phase 2 OCaml+Tests done —
Integer of int/Number of floatin sx_types.ml; float contagion across all arithmetic; floor/truncate/round → Integer; integer?/float?/exact?/inexact?/exact->inexact/inexact->exact; 92/92 numeric tower tests pass; 4874 total (394 pre-existing unchanged). Committedc70bbdeb. - 2026-04-26: Phase 1 complete — JS step done. Fixed fundamental lambda binding bug (index-of on arrays returned -1 not NIL, making bind-lambda-params mis-fire &rest branch). Added lastErrorKont/hostError/try-catch stubs. 42/42 vector tests pass. 1847 std / 2362 full passing (up from 5). Committed.
- 2026-04-25: Phase 1 spec step done — all 10 vector primitives in spec/primitives.sx have full :as type annotations, :returns, :doc; make-vector optional fill param added.
- 2026-04-25: Phase 1 OCaml step done — bounds-checked vector-ref/set!, vector-copy now accepts optional start/end, spec/primitives.sx doc updated. 10/10 r7rs vector tests pass, 4747 total (394 pre-existing hs-upstream fails unchanged).
- 2026-04-25: Phase 0 complete — stopped CL/APL/Ruby/Tcl loops (all 4 idle at shell); confirmed E38 (tokenizer :end/:line) and E39 (WebWorker stub) both have implementation commits.