42 Commits

Author SHA1 Message Date
81f96df5fa plans: tick keep-interpreter box, update progress log
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 11s
2026-04-25 18:32:52 +00:00
1819156d1e prolog: cross-validate compiler vs interpreter (+17)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:32:36 +00:00
8fd55d6aa0 plans: tick compiler box, update progress log
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:08:46 +00:00
8a9c074141 prolog: compile clauses to SX closures (+17)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:08:27 +00:00
00db8b7763 Progress log: predsort+term_variables+arith, 517/517
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 14:13:59 +00:00
788ac9dd05 predsort/3, term_variables/2, arith: floor/ceiling/truncate/round/sign/sqrt/pow
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
- pl-eval-arith: add floor, ceiling, truncate, round, sqrt, sign, pow, integer,
  float, float_integer_part, float_fractional_part, **, ^ operators
- pl-collect-vars: helper that extracts unbound variables from a term (left-to-right,
  deduplicated by var id)
- term_variables/2: dispatches via pl-collect-vars, unifies second arg with var list
- pl-predsort-insert!: inserts one element into a sorted list using a 3-arg comparator
  predicate; deduplicates elements where comparator returns '='
- pl-predsort-build!: builds sorted list via fold over pl-predsort-insert!
- predsort/3: full ISO predsort — sorts and deduplicates a list using a caller-supplied
  predicate
- lib/prolog/tests/advanced.sx: 21 tests (12 arith, 5 term_variables, 4 predsort)
- conformance.sh: add advanced suite
- scoreboard: 517/517 (was 496/496)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:13:12 +00:00
bf250a24bf Progress log: sub_atom+aggregate_all, 496/496
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 13:50:54 +00:00
537e2cdb5a sub_atom/5 (non-det substring) + aggregate_all/3 (count/bag/sum/max/min/set)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds two new builtins to lib/prolog/runtime.sx:

- sub_atom/5: non-deterministic substring enumeration. Iterates all
  (start, length) pairs over the atom string, tries to unify Before,
  Length, After, SubAtom for each candidate. Uses CPS loop helpers
  pl-substring, pl-sub-atom-try-one!, pl-sub-atom-loop!. Fixed trail
  undo semantics: only undo on backtrack (k returns false), not on success.

- aggregate_all/3: collects all solutions via pl-collect-solutions then
  reduces. Templates: count, bag(T), sum(E), max(E), min(E), set(T).
  max/min fail on empty; count/bag/sum/set always succeed.

New test suite lib/prolog/tests/string_agg.sx: 25 tests, all passing.
Total conformance: 496/496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:50:13 +00:00
0a8b30b7b8 Progress log: assert_rules + :- op, 471/471
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 13:22:58 +00:00
2075db62ba Add :- to op table (prec 1200 xfx); enable assert/asserta/assertz with rule terms
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
- parser.sx: add (":-" 1200 "xfx") to pl-op-table so (head :- body) parses
  inside paren expressions (parens reset prec to 1200, allowing xfx match)
- parser.sx: extend pl-token-op to accept "op" token type, not just "atom",
  since the tokenizer emits :- as {:type "op" :value ":-"}
- tests/assert_rules.sx: 15 new tests covering assertz/asserta with rule
  terms, conjunction in rule body, recursive rules, and ordering
- conformance.sh: wire in assert_rules suite
- 456 → 471 tests, all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:22:09 +00:00
1aca2c7bc5 Progress log: io_predicates batch, 456/456
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 13:01:17 +00:00
be2000a048 IO predicates: term_to_atom/2, term_string/2, with_output_to/2, format/1,2, writeln/1
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds 6 new built-in predicates to the Prolog runtime and 24 tests covering
term<->atom conversion (bidirectional), output capture, format directives (~w/~a/~d/~n/~~).
456/456 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:00:42 +00:00
0be5eeafd8 Progress log: char_predicates batch, 432/432
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 12:42:21 +00:00
04ed092f88 Char predicates: char_type/2, upcase_atom/2, downcase_atom/2, string_upper/2, string_lower/2
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
27 new tests, 432/432 total. char_type/2 supports alpha, alnum, digit,
digit(Weight), space/white, upper(Lower), lower(Upper), ascii(Code), punct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:41:31 +00:00
776ae18a20 Progress log: set_predicates batch, 405/405
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 12:22:58 +00:00
5a83f4ef51 Set predicates: foldl/4, list_to_set/2, intersection/3, subtract/3, union/3
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds 5 new built-in predicates to the Prolog runtime with 15 tests.
390 → 405 tests across 20 suites (all passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:22:03 +00:00
73080bb7de Progress log + tick classic-programs checkbox; 390/390
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 12:00:20 +00:00
8f0af85d01 Meta-call predicates: forall/2, maplist/2, maplist/3, include/3, exclude/3
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds pl-apply-goal helper for safe call/N goal construction (atom or compound),
five solver helpers (pl-solve-forall!, pl-solve-maplist2!, pl-solve-maplist3!,
pl-solve-include!, pl-solve-exclude!), five cond clauses in pl-solve!, and a
new test suite (15/15 passing). Total conformance: 390/390.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:59:35 +00:00
07a22257f6 Progress log: list_predicates batch, 375/375 total
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 11:38:27 +00:00
8ef05514b5 List/utility predicates: ==/2, \==/2, flatten/2, numlist/3, atomic_list_concat/2,3, sum_list/2, max_list/2, min_list/2, delete/3
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
33 new tests, all 375/375 conformance tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:37:52 +00:00
0823832dcd Meta/logic predicates: \\+/not/once/ignore/ground/sort/msort/atom_number/number_string (+25 tests, 342 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:06:10 +00:00
8ee0928a3d ISO predicates: succ/2 + plus/3 + between/3 + length/2 + last/2 + nth0/3 + nth1/3 + max/min arith (+29 tests, 317 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:31:28 +00:00
25a4ce4a05 prolog-query SX API: pl-load + pl-query-all + pl-query-one + pl-query (+16 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:58:56 +00:00
f72868c445 String/atom predicates: var/nonvar/atom/number/compound/callable/atomic/is_list + atom_length/atom_concat/atom_chars/atom_codes/char_code/number_codes/number_chars
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:27:08 +00:00
c6f58116bf prolog: copy_term/2 + functor/3 + arg/3, 14 tests; =.. deferred
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 08:39:32 +00:00
76ee8cc39b prolog: findall/3 + bagof/3 + setof/3, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 08:06:35 +00:00
373d57cbcb prolog: assert/asserta/assertz/retract for facts, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 07:32:09 +00:00
3190e770fb prolog: operator-table parser + < > =< >= built-ins, 19 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 06:57:48 +00:00
e018ba9423 prolog: conformance.sh + scoreboard.{json,md}, 183/183 baseline
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 06:19:54 +00:00
09683b8a18 prolog: family.pl + family.sx, 10 tests; 5/5 classic programs done
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 05:52:28 +00:00
64e3b3f44e prolog: nqueens.pl + nqueens.sx (N=1..5), 6 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 05:24:49 +00:00
1302f5a3cc prolog: member.pl + member.sx generator, 7 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 04:54:32 +00:00
93b31b6c8a prolog: reverse.pl + reverse.sx (naive via append), 6 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 04:26:20 +00:00
ffc3716b0e prolog: append.pl + append.sx classic, 6 tests (build/check/split/deduce)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 03:58:12 +00:00
7fb4c52159 prolog: is/2 arithmetic with + - * / mod abs, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 03:27:56 +00:00
072735a6de prolog: write/1 + nl/0 via output buffer, 7 tests; built-ins box done
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 02:56:31 +00:00
1846be0bd8 prolog: ->/2 if-then-else (in ; and standalone), 9 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 02:23:44 +00:00
3adad8e50e prolog: \=/2 + ;/2 + call/1 built-ins, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 01:48:57 +00:00
f019d42727 prolog: cut !/0 with two-cut-box barrier scheme, 6 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 01:14:12 +00:00
738f44e47d prolog: DFS solver (CPS, trail-based) + true/fail/=/conj built-ins, 18 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 00:38:50 +00:00
1888c272f9 prolog: clause DB + loader (functor/arity → clauses), 14 tests green
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 23:59:46 +00:00
60b7f0d7bb prolog: tick phase 1+2 boxes (parse 25/25, unify 47/47 green)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 16:58:30 +00:00
86 changed files with 9000 additions and 6747 deletions

View File

@@ -842,13 +842,6 @@ PREAMBLE = '''\
if (a === b) return true; if (a === b) return true;
if (a && b && a._sym && b._sym) return a.name === b.name; if (a && b && a._sym && b._sym) return a.name === b.name;
if (a && b && a._kw && b._kw) return a.name === b.name; if (a && b && a._kw && b._kw) return a.name === b.name;
if (a && b && a._vector && b._vector) {
if (a.arr.length !== b.arr.length) return false;
for (var _i = 0; _i < a.arr.length; _i++) {
if (!sxEq(a.arr[_i], b.arr[_i])) return false;
}
return true;
}
return false; return false;
} }
@@ -915,45 +908,6 @@ PREAMBLE = '''\
function SxSpread(attrs) { this.attrs = attrs || {}; } function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true; SxSpread.prototype._spread = true;
function SxVector(arr) { this.arr = arr || []; }
SxVector.prototype._vector = true;
var _paramUidCounter = 0;
function SxParameter(defaultVal, converter) {
this._uid = ++_paramUidCounter;
this._default = defaultVal;
this._converter = converter || null;
}
SxParameter.prototype._parameter = true;
function parameter_p(x) { return x != null && x._parameter === true; }
function parameterUid(p) { return p._uid; }
function parameterDefault(p) { return p._default; }
function SxCallccContinuation(capturedKont, windersLen) { this._captured = capturedKont; this._winders_len = windersLen !== undefined ? windersLen : 0; }
SxCallccContinuation.prototype._callcc = true;
function makeCallccContinuation(kont, windersLen) { return new SxCallccContinuation(kont, windersLen !== undefined ? windersLen : 0); }
function callccContinuation_p(x) { return x != null && x._callcc === true; }
function callccContinuationData(x) { return x._captured; }
function callccContinuationWindersLen(x) { return x._winders_len !== undefined ? x._winders_len : 0; }
function evalError_p(v) {
return v != null && typeof v === "object" && v["__eval_error__"] === true;
}
function sxApplyCek(f, args) {
try {
return typeof f === "function" ? f.apply(null, args) : f;
} catch (e) {
if (e && e._perform_request) throw e;
if (e && e._cek_suspend) throw e;
return {"__eval_error__": true, "message": e && e.message ? e.message : String(e)};
}
}
var _JIT_SKIP_SENTINEL = {"__jit_skip": true};
function jitTryCall(f, args) { return _JIT_SKIP_SENTINEL; }
function jitSkip_p(v) { return v === _JIT_SKIP_SENTINEL || (v != null && v["__jit_skip"] === true); }
var _scopeStacks = {}; var _scopeStacks = {};
function isSym(x) { return x != null && x._sym === true; } function isSym(x) { return x != null && x._sym === true; }
@@ -991,18 +945,11 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
if (n === undefined || n === 0) return Math.round(x); if (n === undefined || n === 0) return Math.round(x);
var f = Math.pow(10, n); return Math.round(x * f) / f; var f = Math.pow(10, n); return Math.round(x * f) / f;
}; };
PRIMITIVES["truncate"] = Math.trunc;
PRIMITIVES["remainder"] = function(a, b) { return a % b; };
PRIMITIVES["modulo"] = function(a, b) { var r = a % b; return (r !== 0 && (r < 0) !== (b < 0)) ? r + b : r; };
PRIMITIVES["min"] = Math.min; PRIMITIVES["min"] = Math.min;
PRIMITIVES["max"] = Math.max; PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt; PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow; PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
PRIMITIVES["random-int"] = function(lo, hi) { return Math.floor(Math.random() * (hi - lo + 1)) + lo; };
PRIMITIVES["exact->inexact"] = function(x) { return x; };
PRIMITIVES["inexact->exact"] = Math.round;
PRIMITIVES["parse-number"] = function(s) { var n = Number(s); return isNaN(n) ? null : n; };
''', ''',
"core.comparison": ''' "core.comparison": '''
@@ -1024,13 +971,9 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
// core.predicates // core.predicates
PRIMITIVES["nil?"] = isNil; PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
PRIMITIVES["integer?"] = function(x) { return typeof x === "number" && Number.isInteger(x); };
PRIMITIVES["float?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); };
PRIMITIVES["exact?"] = function(x) { return typeof x === "number" && Number.isInteger(x); };
PRIMITIVES["inexact?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); };
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray; PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw && !x._string_buffer && !x._vector; }; PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); }; PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function(c, k) { PRIMITIVES["contains?"] = function(c, k) {
if (typeof c === "string") return c.indexOf(String(k)) !== -1; if (typeof c === "string") return c.indexOf(String(k)) !== -1;
@@ -1061,20 +1004,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); }; PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); }; PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); }; PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
PRIMITIVES["index-of"] = function(s, needle, from) { PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
if (Array.isArray(s)) {
var _start = from || 0;
for (var _i = _start; _i < s.length; _i++) {
var _a = s[_i];
if (_a === needle) return _i;
if (_a != null && needle != null && typeof _a === "object" && typeof needle === "object") {
if ((_a._sym && needle._sym || _a._kw && needle._kw) && _a.name === needle.name) return _i;
}
}
return NIL;
}
return String(s).indexOf(needle, from || 0);
};
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; }; PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); }; PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); };
@@ -1156,49 +1086,6 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
}; };
''', ''',
"core.vectors": '''
// core.vectors — R7RS mutable fixed-size arrays
PRIMITIVES["make-vector"] = function(n, fill) {
var arr = new Array(n);
var f = (fill !== undefined) ? fill : NIL;
for (var i = 0; i < n; i++) arr[i] = f;
return new SxVector(arr);
};
PRIMITIVES["vector"] = function() {
return new SxVector(Array.prototype.slice.call(arguments));
};
PRIMITIVES["vector?"] = function(x) { return x != null && x._vector === true; };
PRIMITIVES["vector-length"] = function(v) { return v.arr.length; };
PRIMITIVES["vector-ref"] = function(v, i) {
if (i < 0 || i >= v.arr.length) throw new Error("vector-ref: index " + i + " out of bounds (length " + v.arr.length + ")");
return v.arr[i];
};
PRIMITIVES["vector-set!"] = function(v, i, val) {
if (i < 0 || i >= v.arr.length) throw new Error("vector-set!: index " + i + " out of bounds (length " + v.arr.length + ")");
v.arr[i] = val; return NIL;
};
PRIMITIVES["vector->list"] = function(v) { return v.arr.slice(); };
PRIMITIVES["list->vector"] = function(l) { return new SxVector(l.slice()); };
PRIMITIVES["vector-fill!"] = function(v, val) {
for (var i = 0; i < v.arr.length; i++) v.arr[i] = val; return NIL;
};
PRIMITIVES["vector-copy"] = function(v, start, end) {
var s = (start !== undefined) ? start : 0;
var e = (end !== undefined) ? Math.min(end, v.arr.length) : v.arr.length;
return new SxVector(v.arr.slice(s, e));
};
// String buffers — O(1) amortised append via array+join
function SxStringBuffer() { this.parts = []; this.len = 0; this._string_buffer = true; }
PRIMITIVES["make-string-buffer"] = function() { return new SxStringBuffer(); };
PRIMITIVES["string-buffer?"] = function(x) { return x instanceof SxStringBuffer; };
PRIMITIVES["string-buffer-append!"] = function(buf, s) {
buf.parts.push(String(s)); buf.len += String(s).length; return NIL;
};
PRIMITIVES["string-buffer->string"] = function(buf) { return buf.parts.join(""); };
PRIMITIVES["string-buffer-length"] = function(buf) { return buf.len; };
''',
"stdlib.format": ''' "stdlib.format": '''
// stdlib.format // stdlib.format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
@@ -1309,27 +1196,6 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
return NIL; return NIL;
}; };
''', ''',
"stdlib.bitwise": '''
// stdlib.bitwise
PRIMITIVES["bitwise-and"] = function(a, b) { return (a & b) | 0; };
PRIMITIVES["bitwise-or"] = function(a, b) { return (a | b) | 0; };
PRIMITIVES["bitwise-xor"] = function(a, b) { return (a ^ b) | 0; };
PRIMITIVES["bitwise-not"] = function(a) { return ~a; };
PRIMITIVES["arithmetic-shift"] = function(a, count) {
return count >= 0 ? (a << count) | 0 : a >> (-count);
};
PRIMITIVES["bit-count"] = function(a) {
var n = Math.abs(a) >>> 0;
n = n - ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
return (((n + (n >> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
};
PRIMITIVES["integer-length"] = function(a) {
if (a === 0) return 0;
return 32 - Math.clz32(Math.abs(a));
};
''',
} }
# Modules to include by default (all) # Modules to include by default (all)
_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys()) _ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys())
@@ -1368,8 +1234,6 @@ PLATFORM_JS_PRE = '''
if (x._macro) return "macro"; if (x._macro) return "macro";
if (x._raw) return "raw-html"; if (x._raw) return "raw-html";
if (x._sx_expr) return "sx-expr"; if (x._sx_expr) return "sx-expr";
if (x._vector) return "vector";
if (x._string_buffer) return "string-buffer";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node"; if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
if (Array.isArray(x)) return "list"; if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict"; if (typeof x === "object") return "dict";
@@ -1536,12 +1400,6 @@ PLATFORM_JS_PRE = '''
// Placeholder — overridden by transpiled version from render.sx // Placeholder — overridden by transpiled version from render.sx
function isRenderExpr(expr) { return false; } function isRenderExpr(expr) { return false; }
// Last error continuation — saved when a raise goes unhandled, for post-mortem inspection.
var _lastErrorKont_ = null;
// hostError — throw a host-level error that propagates out of cekRun.
function hostError(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); }
// Render dispatch — call the active adapter's render function. // Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering). // Set by each adapter when loaded; defaults to identity (no rendering).
var _renderExprFn = null; var _renderExprFn = null;
@@ -1885,13 +1743,6 @@ CEK_FIXUPS_JS = '''
PRIMITIVES["lambda-name"] = lambdaName; PRIMITIVES["lambda-name"] = lambdaName;
PRIMITIVES["component?"] = isComponent; PRIMITIVES["component?"] = isComponent;
PRIMITIVES["island?"] = isIsland; PRIMITIVES["island?"] = isIsland;
PRIMITIVES["parameter?"] = parameter_p;
PRIMITIVES["parameter-uid"] = parameterUid;
PRIMITIVES["parameter-default"] = parameterDefault;
PRIMITIVES["make-parameter"] = function(defaultVal, converter) {
var p = new SxParameter(defaultVal, converter || null);
return p;
};
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); }; PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; }; PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
function makeEnv() { return merge(componentEnv, PRIMITIVES); } function makeEnv() { return merge(componentEnv, PRIMITIVES); }
@@ -2180,7 +2031,7 @@ PLATFORM_DOM_JS = """
} }
function domDispatch(el, name, detail) { function domDispatch(el, name, detail) {
if (!_hasDom || !el || typeof el.dispatchEvent !== "function") return false; if (!_hasDom || !el) return false;
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} }); var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
return el.dispatchEvent(evt); return el.dispatchEvent(evt);
} }
@@ -2306,14 +2157,6 @@ PLATFORM_ORCHESTRATION_JS = """
// Platform interface — Orchestration (browser-only) // Platform interface — Orchestration (browser-only)
// ========================================================================= // =========================================================================
// --- Stubs for define-library functions not transpiled by extract_defines ---
// These are defined in orchestration.sx's define-library and called from
// boot.sx top-level defines. The JS bootstrapper only transpiles top-level
// defines, so we provide stubs here for functions that need a JS identity.
function flushCollectedStyles() { return NIL; }
function processElements(root) { return NIL; }
// --- Browser/Network --- // --- Browser/Network ---
function browserNavigate(url) { function browserNavigate(url) {
@@ -2799,10 +2642,6 @@ PLATFORM_ORCHESTRATION_JS = """
return el && el.closest ? el.closest(sel) : null; return el && el.closest ? el.closest(sel) : null;
} }
function domDocument() {
return _hasDom ? document : null;
}
function domBody() { function domBody() {
return _hasDom ? document.body : null; return _hasDom ? document.body : null;
} }
@@ -3246,8 +3085,6 @@ PLATFORM_BOOT_JS = """
// Platform interface — Boot (mount, hydrate, scripts, cookies) // Platform interface — Boot (mount, hydrate, scripts, cookies)
// ========================================================================= // =========================================================================
function preloadIslandDefs() { return NIL; }
function resolveMountTarget(target) { function resolveMountTarget(target) {
if (typeof target === "string") return _hasDom ? document.querySelector(target) : null; if (typeof target === "string") return _hasDom ? document.querySelector(target) : null;
return target; return target;
@@ -3400,18 +3237,6 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
// Core primitives that require native JS (cannot be expressed via FFI) // Core primitives that require native JS (cannot be expressed via FFI)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
PRIMITIVES["error"] = function(msg) { throw new Error(msg); }; PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
PRIMITIVES["host-error"] = function(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); };
PRIMITIVES["try-catch"] = function(tryFn, catchFn) {
try {
return cekRun(continueWithCall(tryFn, [], makeEnv(), [], []));
} catch(e) {
var msg = e && e.message ? e.message : String(e);
return cekRun(continueWithCall(catchFn, [msg], makeEnv(), [msg], []));
}
};
PRIMITIVES["without-io-hook"] = function(thunk) {
return cekRun(continueWithCall(thunk, [], makeEnv(), [], []));
};
PRIMITIVES["sort"] = function(lst) { PRIMITIVES["sort"] = function(lst) {
if (!Array.isArray(lst)) return lst; if (!Array.isArray(lst)) return lst;
return lst.slice().sort(function(a, b) { return lst.slice().sort(function(a, b) {
@@ -3479,7 +3304,7 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
PRIMITIVES["dom-tag-name"] = domTagName; PRIMITIVES["dom-tag-name"] = domTagName;
PRIMITIVES["dom-get-prop"] = domGetProp; PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp; PRIMITIVES["dom-set-prop"] = domSetProp;
if (typeof reactiveText === "function") PRIMITIVES["reactive-text"] = reactiveText; PRIMITIVES["reactive-text"] = reactiveText;
PRIMITIVES["set-interval"] = setInterval_; PRIMITIVES["set-interval"] = setInterval_;
PRIMITIVES["clear-interval"] = clearInterval_; PRIMITIVES["clear-interval"] = clearInterval_;
PRIMITIVES["promise-then"] = promiseThen; PRIMITIVES["promise-then"] = promiseThen;
@@ -3668,35 +3493,35 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
elif has_orch: elif has_orch:
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,') api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
if has_deps: if has_deps:
api_lines.append(' scanRefs: typeof scanRefs === "function" ? scanRefs : null,') api_lines.append(' scanRefs: scanRefs,')
api_lines.append(' scanComponentsFromSource: typeof scanComponentsFromSource === "function" ? scanComponentsFromSource : null,') api_lines.append(' scanComponentsFromSource: scanComponentsFromSource,')
api_lines.append(' transitiveDeps: typeof transitiveDeps === "function" ? transitiveDeps : null,') api_lines.append(' transitiveDeps: transitiveDeps,')
api_lines.append(' computeAllDeps: typeof computeAllDeps === "function" ? computeAllDeps : null,') api_lines.append(' computeAllDeps: computeAllDeps,')
api_lines.append(' componentsNeeded: typeof componentsNeeded === "function" ? componentsNeeded : null,') api_lines.append(' componentsNeeded: componentsNeeded,')
api_lines.append(' pageComponentBundle: typeof pageComponentBundle === "function" ? pageComponentBundle : null,') api_lines.append(' pageComponentBundle: pageComponentBundle,')
api_lines.append(' pageCssClasses: typeof pageCssClasses === "function" ? pageCssClasses : null,') api_lines.append(' pageCssClasses: pageCssClasses,')
api_lines.append(' scanIoRefs: typeof scanIoRefs === "function" ? scanIoRefs : null,') api_lines.append(' scanIoRefs: scanIoRefs,')
api_lines.append(' transitiveIoRefs: typeof transitiveIoRefs === "function" ? transitiveIoRefs : null,') api_lines.append(' transitiveIoRefs: transitiveIoRefs,')
api_lines.append(' computeAllIoRefs: typeof computeAllIoRefs === "function" ? computeAllIoRefs : null,') api_lines.append(' computeAllIoRefs: computeAllIoRefs,')
api_lines.append(' componentPure_p: typeof componentPure_p === "function" ? componentPure_p : null,') api_lines.append(' componentPure_p: componentPure_p,')
if has_page_helpers: if has_page_helpers:
api_lines.append(' categorizeSpecialForms: typeof categorizeSpecialForms === "function" ? categorizeSpecialForms : null,') api_lines.append(' categorizeSpecialForms: categorizeSpecialForms,')
api_lines.append(' buildReferenceData: typeof buildReferenceData === "function" ? buildReferenceData : null,') api_lines.append(' buildReferenceData: buildReferenceData,')
api_lines.append(' buildAttrDetail: typeof buildAttrDetail === "function" ? buildAttrDetail : null,') api_lines.append(' buildAttrDetail: buildAttrDetail,')
api_lines.append(' buildHeaderDetail: typeof buildHeaderDetail === "function" ? buildHeaderDetail : null,') api_lines.append(' buildHeaderDetail: buildHeaderDetail,')
api_lines.append(' buildEventDetail: typeof buildEventDetail === "function" ? buildEventDetail : null,') api_lines.append(' buildEventDetail: buildEventDetail,')
api_lines.append(' buildComponentSource: typeof buildComponentSource === "function" ? buildComponentSource : null,') api_lines.append(' buildComponentSource: buildComponentSource,')
api_lines.append(' buildBundleAnalysis: typeof buildBundleAnalysis === "function" ? buildBundleAnalysis : null,') api_lines.append(' buildBundleAnalysis: buildBundleAnalysis,')
api_lines.append(' buildRoutingAnalysis: typeof buildRoutingAnalysis === "function" ? buildRoutingAnalysis : null,') api_lines.append(' buildRoutingAnalysis: buildRoutingAnalysis,')
api_lines.append(' buildAffinityAnalysis: typeof buildAffinityAnalysis === "function" ? buildAffinityAnalysis : null,') api_lines.append(' buildAffinityAnalysis: buildAffinityAnalysis,')
if has_router: if has_router:
api_lines.append(' splitPathSegments: typeof splitPathSegments === "function" ? splitPathSegments : null,') api_lines.append(' splitPathSegments: splitPathSegments,')
api_lines.append(' parseRoutePattern: typeof parseRoutePattern === "function" ? parseRoutePattern : null,') api_lines.append(' parseRoutePattern: parseRoutePattern,')
api_lines.append(' matchRoute: typeof matchRoute === "function" ? matchRoute : null,') api_lines.append(' matchRoute: matchRoute,')
api_lines.append(' findMatchingRoute: typeof findMatchingRoute === "function" ? findMatchingRoute : null,') api_lines.append(' findMatchingRoute: findMatchingRoute,')
api_lines.append(' urlToExpr: typeof urlToExpr === "function" ? urlToExpr : null,') api_lines.append(' urlToExpr: urlToExpr,')
api_lines.append(' autoQuoteUnknowns: typeof autoQuoteUnknowns === "function" ? autoQuoteUnknowns : null,') api_lines.append(' autoQuoteUnknowns: autoQuoteUnknowns,')
api_lines.append(' prepareUrlExpr: typeof prepareUrlExpr === "function" ? prepareUrlExpr : null,') api_lines.append(' prepareUrlExpr: prepareUrlExpr,')
if has_dom: if has_dom:
api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,') api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,')
@@ -3704,21 +3529,21 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,') api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,')
api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,') api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,')
if has_signals: if has_signals:
api_lines.append(' signal: typeof signal === "function" ? signal : null,') api_lines.append(' signal: signal,')
api_lines.append(' deref: typeof deref === "function" ? deref : null,') api_lines.append(' deref: deref,')
api_lines.append(' reset: typeof reset_b === "function" ? reset_b : null,') api_lines.append(' reset: reset_b,')
api_lines.append(' swap: typeof swap_b === "function" ? swap_b : null,') api_lines.append(' swap: swap_b,')
api_lines.append(' computed: typeof computed === "function" ? computed : null,') api_lines.append(' computed: computed,')
api_lines.append(' effect: typeof effect === "function" ? effect : null,') api_lines.append(' effect: effect,')
api_lines.append(' batch: typeof batch === "function" ? batch : null,') api_lines.append(' batch: batch,')
api_lines.append(' isSignal: typeof isSignal === "function" ? isSignal : null,') api_lines.append(' isSignal: isSignal,')
api_lines.append(' makeSignal: typeof makeSignal === "function" ? makeSignal : null,') api_lines.append(' makeSignal: makeSignal,')
api_lines.append(' defStore: typeof defStore === "function" ? defStore : null,') api_lines.append(' defStore: defStore,')
api_lines.append(' useStore: typeof useStore === "function" ? useStore : null,') api_lines.append(' useStore: useStore,')
api_lines.append(' clearStores: typeof clearStores === "function" ? clearStores : null,') api_lines.append(' clearStores: clearStores,')
api_lines.append(' emitEvent: typeof emitEvent === "function" ? emitEvent : null,') api_lines.append(' emitEvent: emitEvent,')
api_lines.append(' onEvent: typeof onEvent === "function" ? onEvent : null,') api_lines.append(' onEvent: onEvent,')
api_lines.append(' bridgeEvent: typeof bridgeEvent === "function" ? bridgeEvent : null,') api_lines.append(' bridgeEvent: bridgeEvent,')
api_lines.append(' makeSpread: makeSpread,') api_lines.append(' makeSpread: makeSpread,')
api_lines.append(' isSpread: isSpread,') api_lines.append(' isSpread: isSpread,')
api_lines.append(' spreadAttrs: spreadAttrs,') api_lines.append(' spreadAttrs: spreadAttrs,')

View File

@@ -293,8 +293,6 @@ env["pop-suite"] = function() {
return null; return null;
}; };
env["test-allowed?"] = function(name) { return true; };
// Load test framework // Load test framework
const projectDir = path.join(__dirname, "..", ".."); const projectDir = path.join(__dirname, "..", "..");
const specTests = path.join(projectDir, "spec", "tests"); const specTests = path.join(projectDir, "spec", "tests");
@@ -343,20 +341,6 @@ if (fs.existsSync(swapPath)) {
} }
} }
// Load spec library files (define-library modules imported by tests)
for (const libFile of ["signals.sx", "coroutines.sx"]) {
const libPath = path.join(projectDir, "spec", libFile);
if (fs.existsSync(libPath)) {
const libSrc = fs.readFileSync(libPath, "utf8");
const libExprs = Sx.parse(libSrc);
for (const expr of libExprs) {
try { Sx.eval(expr, env); } catch (e) {
console.error(`Error loading spec/${libFile}: ${e.message}`);
}
}
}
}
// Load tw system (needed by spec/tests/test-tw.sx) // Load tw system (needed by spec/tests/test-tw.sx)
const twDir = path.join(projectDir, "shared", "sx", "templates"); const twDir = path.join(projectDir, "shared", "sx", "templates");
for (const twFile of ["tw-type.sx", "tw-layout.sx", "tw.sx"]) { for (const twFile of ["tw-type.sx", "tw-layout.sx", "tw.sx"]) {

View File

@@ -37,10 +37,7 @@ let rec deep_equal a b =
match a, b with match a, b with
| Nil, Nil -> true | Nil, Nil -> true
| Bool a, Bool b -> a = b | Bool a, Bool b -> a = b
| Integer a, Integer b -> a = b
| Number a, Number b -> a = b | Number a, Number b -> a = b
| Integer a, Number b -> float_of_int a = b
| Number a, Integer b -> a = float_of_int b
| String a, String b -> a = b | String a, String b -> a = b
| Symbol a, Symbol b -> a = b | Symbol a, Symbol b -> a = b
| Keyword a, Keyword b -> a = b | Keyword a, Keyword b -> a = b
@@ -229,7 +226,7 @@ let make_test_env () =
| [String s] -> | [String s] ->
let parsed = Sx_parser.parse_all s in let parsed = Sx_parser.parse_all s in
(match parsed with (match parsed with
| [List (Symbol "sxbc" :: (Number _ | Integer _) :: payload :: _)] -> payload | [List (Symbol "sxbc" :: Number _ :: payload :: _)] -> payload
| _ -> raise (Eval_error "bytecode-deserialize: invalid sxbc format")) | _ -> raise (Eval_error "bytecode-deserialize: invalid sxbc format"))
| _ -> raise (Eval_error "bytecode-deserialize: expected string")); | _ -> raise (Eval_error "bytecode-deserialize: expected string"));
@@ -243,7 +240,7 @@ let make_test_env () =
| [String s] -> | [String s] ->
let parsed = Sx_parser.parse_all s in let parsed = Sx_parser.parse_all s in
(match parsed with (match parsed with
| [List (Symbol "cek-state" :: (Number _ | Integer _) :: payload :: _)] -> payload | [List (Symbol "cek-state" :: Number _ :: payload :: _)] -> payload
| _ -> raise (Eval_error "cek-deserialize: invalid cek-state format")) | _ -> raise (Eval_error "cek-deserialize: invalid cek-state format"))
| _ -> raise (Eval_error "cek-deserialize: expected string")); | _ -> raise (Eval_error "cek-deserialize: expected string"));
@@ -323,10 +320,7 @@ let make_test_env () =
bind "identical?" (fun args -> bind "identical?" (fun args ->
match args with match args with
| [a; b] -> Bool (match a, b with | [a; b] -> Bool (match a, b with
| Integer x, Integer y -> x = y
| Number x, Number y -> x = y | Number x, Number y -> x = y
| Integer x, Number y -> float_of_int x = y
| Number x, Integer y -> x = float_of_int y
| String x, String y -> x = y | String x, String y -> x = y
| Bool x, Bool y -> x = y | Bool x, Bool y -> x = y
| Nil, Nil -> true | Nil, Nil -> true
@@ -372,15 +366,11 @@ let make_test_env () =
bind "append!" (fun args -> bind "append!" (fun args ->
match args with match args with
| [ListRef r; v; (Number n)] when int_of_float n = 0 -> | [ListRef r; v; Number n] when int_of_float n = 0 ->
r := v :: !r; ListRef r (* prepend *) r := v :: !r; ListRef r (* prepend *)
| [ListRef r; v; (Integer 0)] ->
r := v :: !r; ListRef r (* prepend Integer index *)
| [ListRef r; v] -> r := !r @ [v]; ListRef r (* append in place *) | [ListRef r; v] -> r := !r @ [v]; ListRef r (* append in place *)
| [List items; v; (Number n)] when int_of_float n = 0 -> | [List items; v; Number n] when int_of_float n = 0 ->
List (v :: items) (* immutable prepend *) List (v :: items) (* immutable prepend *)
| [List items; v; (Integer 0)] ->
List (v :: items) (* immutable prepend Integer index *)
| [List items; v] -> List (items @ [v]) (* immutable fallback *) | [List items; v] -> List (items @ [v]) (* immutable fallback *)
| _ -> raise (Eval_error "append!: expected list and value")); | _ -> raise (Eval_error "append!: expected list and value"));
@@ -556,10 +546,7 @@ let make_test_env () =
bind "batch-begin!" (fun _args -> Sx_ref.batch_begin_b ()); bind "batch-begin!" (fun _args -> Sx_ref.batch_begin_b ());
bind "batch-end!" (fun _args -> Sx_ref.batch_end_b ()); bind "batch-end!" (fun _args -> Sx_ref.batch_end_b ());
bind "now-ms" (fun _args -> Number 1000.0); bind "now-ms" (fun _args -> Number 1000.0);
bind "random-int" (fun args -> match args with bind "random-int" (fun args -> match args with [Number lo; _] -> Number lo | _ -> Number 0.0);
| [Number lo; _] -> Number lo
| [Integer lo; _] -> Integer lo
| _ -> Integer 0);
bind "try-rerender-page" (fun _args -> Nil); bind "try-rerender-page" (fun _args -> Nil);
bind "collect!" (fun args -> bind "collect!" (fun args ->
match args with match args with
@@ -1155,20 +1142,18 @@ let run_foundation_tests () =
in in
Printf.printf "Suite: parser\n"; Printf.printf "Suite: parser\n";
assert_eq "number" (Integer 42) (List.hd (parse_all "42")); assert_eq "number" (Number 42.0) (List.hd (parse_all "42"));
assert_eq "string" (String "hello") (List.hd (parse_all "\"hello\"")); assert_eq "string" (String "hello") (List.hd (parse_all "\"hello\""));
assert_eq "bool true" (Bool true) (List.hd (parse_all "true")); assert_eq "bool true" (Bool true) (List.hd (parse_all "true"));
assert_eq "nil" Nil (List.hd (parse_all "nil")); assert_eq "nil" Nil (List.hd (parse_all "nil"));
assert_eq "keyword" (Keyword "class") (List.hd (parse_all ":class")); assert_eq "keyword" (Keyword "class") (List.hd (parse_all ":class"));
assert_eq "symbol" (Symbol "foo") (List.hd (parse_all "foo")); assert_eq "symbol" (Symbol "foo") (List.hd (parse_all "foo"));
assert_eq "list" (List [Symbol "+"; Integer 1; Integer 2]) (List.hd (parse_all "(+ 1 2)")); assert_eq "list" (List [Symbol "+"; Number 1.0; Number 2.0]) (List.hd (parse_all "(+ 1 2)"));
(match List.hd (parse_all "(div :class \"card\" (p \"hi\"))") with (match List.hd (parse_all "(div :class \"card\" (p \"hi\"))") with
| List [Symbol "div"; Keyword "class"; String "card"; List [Symbol "p"; String "hi"]] -> | List [Symbol "div"; Keyword "class"; String "card"; List [Symbol "p"; String "hi"]] ->
incr pass_count; Printf.printf " PASS: nested list\n" incr pass_count; Printf.printf " PASS: nested list\n"
| v -> incr fail_count; Printf.printf " FAIL: nested list — got %s\n" (Sx_types.inspect v)); | v -> incr fail_count; Printf.printf " FAIL: nested list — got %s\n" (Sx_types.inspect v));
(match List.hd (parse_all "'(1 2 3)") with (match List.hd (parse_all "'(1 2 3)") with
| List [Symbol "quote"; List [Integer 1; Integer 2; Integer 3]] ->
incr pass_count; Printf.printf " PASS: quote sugar\n"
| List [Symbol "quote"; List [Number 1.0; Number 2.0; Number 3.0]] -> | List [Symbol "quote"; List [Number 1.0; Number 2.0; Number 3.0]] ->
incr pass_count; Printf.printf " PASS: quote sugar\n" incr pass_count; Printf.printf " PASS: quote sugar\n"
| v -> incr fail_count; Printf.printf " FAIL: quote sugar — got %s\n" (Sx_types.inspect v)); | v -> incr fail_count; Printf.printf " FAIL: quote sugar — got %s\n" (Sx_types.inspect v));
@@ -1176,7 +1161,7 @@ let run_foundation_tests () =
| Dict d when dict_has d "a" && dict_has d "b" -> | Dict d when dict_has d "a" && dict_has d "b" ->
incr pass_count; Printf.printf " PASS: dict literal\n" incr pass_count; Printf.printf " PASS: dict literal\n"
| v -> incr fail_count; Printf.printf " FAIL: dict literal — got %s\n" (Sx_types.inspect v)); | v -> incr fail_count; Printf.printf " FAIL: dict literal — got %s\n" (Sx_types.inspect v));
assert_eq "comment" (Integer 42) (List.hd (parse_all ";; comment\n42")); assert_eq "comment" (Number 42.0) (List.hd (parse_all ";; comment\n42"));
assert_eq "string escape" (String "hello\nworld") (List.hd (parse_all "\"hello\\nworld\"")); assert_eq "string escape" (String "hello\nworld") (List.hd (parse_all "\"hello\\nworld\""));
assert_eq "multiple exprs" (Number 2.0) (Number (float_of_int (List.length (parse_all "(1 2 3) (4 5)")))); assert_eq "multiple exprs" (Number 2.0) (Number (float_of_int (List.length (parse_all "(1 2 3) (4 5)"))));
@@ -1993,10 +1978,6 @@ let run_spec_tests env test_files =
(match Hashtbl.find_opt d "children" with (match Hashtbl.find_opt d "children" with
| Some (List l) when i >= 0 && i < List.length l -> List.nth l i | Some (List l) when i >= 0 && i < List.length l -> List.nth l i
| _ -> (match Hashtbl.find_opt d (string_of_int i) with Some v -> v | None -> Nil)) | _ -> (match Hashtbl.find_opt d (string_of_int i) with Some v -> v | None -> Nil))
| [Dict d; Integer n] ->
(match Hashtbl.find_opt d "children" with
| Some (List l) when n >= 0 && n < List.length l -> List.nth l n
| _ -> (match Hashtbl.find_opt d (string_of_int n) with Some v -> v | None -> Nil))
| _ -> Nil); | _ -> Nil);
(* Stringify a value for DOM string properties *) (* Stringify a value for DOM string properties *)
@@ -2071,8 +2052,8 @@ let run_spec_tests env test_files =
Hashtbl.replace d "childNodes" (List []) Hashtbl.replace d "childNodes" (List [])
| _ -> ()); | _ -> ());
stored stored
| [ListRef r; idx_v; value] when (match idx_v with Number _ | Integer _ -> true | _ -> false) -> | [ListRef r; Number n; value] ->
let idx = match idx_v with Number n -> int_of_float n | Integer n -> n | _ -> 0 in let idx = int_of_float n in
let lst = !r in let lst = !r in
if idx >= 0 && idx < List.length lst then if idx >= 0 && idx < List.length lst then
r := List.mapi (fun i v -> if i = idx then value else v) lst r := List.mapi (fun i v -> if i = idx then value else v) lst
@@ -2209,7 +2190,7 @@ let run_spec_tests env test_files =
| [String name; value] -> | [String name; value] ->
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ -> let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ ->
let a = Hashtbl.create 4 in Hashtbl.replace d "attributes" (Dict a); a in let a = Hashtbl.create 4 in Hashtbl.replace d "attributes" (Dict a); a in
let sv = match value with String s -> s | Integer n -> string_of_int n | Number n -> let sv = match value with String s -> s | Number n ->
let i = int_of_float n in if float_of_int i = n then string_of_int i let i = int_of_float n in if float_of_int i = n then string_of_int i
else string_of_float n | _ -> Sx_types.inspect value in else string_of_float n | _ -> Sx_types.inspect value in
Hashtbl.replace attrs name (String sv); Hashtbl.replace attrs name (String sv);
@@ -2651,7 +2632,6 @@ let run_spec_tests env test_files =
let rec json_of_value = function let rec json_of_value = function
| Nil -> `Null | Nil -> `Null
| Bool b -> `Bool b | Bool b -> `Bool b
| Integer n -> `Int n
| Number n -> | Number n ->
if Float.is_integer n && Float.abs n < 1e16 if Float.is_integer n && Float.abs n < 1e16
then `Int (int_of_float n) else `Float n then `Int (int_of_float n) else `Float n
@@ -2667,8 +2647,8 @@ let run_spec_tests env test_files =
let rec value_of_json = function let rec value_of_json = function
| `Null -> Nil | `Null -> Nil
| `Bool b -> Bool b | `Bool b -> Bool b
| `Int i -> Integer i | `Int i -> Number (float_of_int i)
| `Intlit s -> (try Integer (int_of_string s) with _ -> try Number (float_of_string s) with _ -> String s) | `Intlit s -> (try Number (float_of_string s) with _ -> String s)
| `Float f -> Number f | `Float f -> Number f
| `String s -> String s | `String s -> String s
| `List xs -> List (List.map value_of_json xs) | `List xs -> List (List.map value_of_json xs)

View File

@@ -296,10 +296,6 @@ let read_blob () =
(* consume trailing newline *) (* consume trailing newline *)
(try ignore (input_line stdin) with End_of_file -> ()); (try ignore (input_line stdin) with End_of_file -> ());
data data
| [List [Symbol "blob"; Integer n]] ->
let data = read_exact_bytes n in
(try ignore (input_line stdin) with End_of_file -> ());
data
| _ -> raise (Eval_error ("read_blob: expected (blob N), got: " ^ line)) | _ -> raise (Eval_error ("read_blob: expected (blob N), got: " ^ line))
(** Batch IO mode — collect requests during aser-slot, resolve after. *) (** Batch IO mode — collect requests during aser-slot, resolve after. *)
@@ -361,11 +357,6 @@ let rec read_io_response () =
| [List (Symbol "io-response" :: Number n :: values)] | [List (Symbol "io-response" :: Number n :: values)]
when int_of_float n = !current_epoch -> when int_of_float n = !current_epoch ->
(match values with [v] -> v | _ -> List values) (match values with [v] -> v | _ -> List values)
| [List [Symbol "io-response"; Integer n; value]]
when n = !current_epoch -> value
| [List (Symbol "io-response" :: Integer n :: values)]
when n = !current_epoch ->
(match values with [v] -> v | _ -> List values)
(* Legacy untagged: (io-response value) — accept for backwards compat *) (* Legacy untagged: (io-response value) — accept for backwards compat *)
| [List [Symbol "io-response"; value]] -> value | [List [Symbol "io-response"; value]] -> value
| [List (Symbol "io-response" :: values)] -> | [List (Symbol "io-response" :: values)] ->
@@ -405,12 +396,6 @@ let read_batched_io_response () =
when int_of_float n = !current_epoch -> s when int_of_float n = !current_epoch -> s
| [List [Symbol "io-response"; Number n; v]] | [List [Symbol "io-response"; Number n; v]]
when int_of_float n = !current_epoch -> serialize_value v when int_of_float n = !current_epoch -> serialize_value v
| [List [Symbol "io-response"; Integer n; String s]]
when n = !current_epoch -> s
| [List [Symbol "io-response"; Integer n; SxExpr s]]
when n = !current_epoch -> s
| [List [Symbol "io-response"; Integer n; v]]
when n = !current_epoch -> serialize_value v
(* Legacy untagged *) (* Legacy untagged *)
| [List [Symbol "io-response"; String s]] | [List [Symbol "io-response"; String s]]
| [List [Symbol "io-response"; SxExpr s]] -> s | [List [Symbol "io-response"; SxExpr s]] -> s
@@ -974,7 +959,6 @@ let setup_io_bridges env =
bind "sleep" (fun args -> io_request "sleep" args); bind "sleep" (fun args -> io_request "sleep" args);
bind "set-response-status" (fun args -> match args with bind "set-response-status" (fun args -> match args with
| [Number n] -> _pending_response_status := int_of_float n; Nil | [Number n] -> _pending_response_status := int_of_float n; Nil
| [Integer n] -> _pending_response_status := n; Nil
| _ -> Nil); | _ -> Nil);
bind "set-response-header" (fun args -> io_request "set-response-header" args) bind "set-response-header" (fun args -> io_request "set-response-header" args)
@@ -4466,8 +4450,6 @@ let site_mode () =
match exprs with match exprs with
| [List [Symbol "epoch"; Number n]] -> | [List [Symbol "epoch"; Number n]] ->
current_epoch := int_of_float n current_epoch := int_of_float n
| [List [Symbol "epoch"; Integer n]] ->
current_epoch := n
(* render-page: full SSR pipeline — URL → complete HTML *) (* render-page: full SSR pipeline — URL → complete HTML *)
| [List [Symbol "render-page"; String path]] -> | [List [Symbol "render-page"; String path]] ->
(try match http_render_page env path [] with (try match http_render_page env path [] with
@@ -4525,8 +4507,6 @@ let () =
(* Epoch marker: (epoch N) — set current epoch, read next command *) (* Epoch marker: (epoch N) — set current epoch, read next command *)
| [List [Symbol "epoch"; Number n]] -> | [List [Symbol "epoch"; Number n]] ->
current_epoch := int_of_float n current_epoch := int_of_float n
| [List [Symbol "epoch"; Integer n]] ->
current_epoch := n
| [cmd] -> dispatch env cmd | [cmd] -> dispatch env cmd
| _ -> send_error ("Expected single command, got " ^ string_of_int (List.length exprs)) | _ -> send_error ("Expected single command, got " ^ string_of_int (List.length exprs))
end end

View File

@@ -47,9 +47,7 @@ open Sx_runtime
let trampoline_fn : (value -> value) ref = ref (fun v -> v) let trampoline_fn : (value -> value) ref = ref (fun v -> v)
let trampoline v = !trampoline_fn v let trampoline v = !trampoline_fn v
(* Step limit for timeout detection — set to 0 to disable *)
let step_limit : int ref = ref 0
let step_count : int ref = ref 0
(* === Mutable globals — backing refs for transpiler's !_ref / _ref := === *) (* === Mutable globals — backing refs for transpiler's !_ref / _ref := === *)
let _strict_ref = ref (Bool false) let _strict_ref = ref (Bool false)
@@ -128,90 +126,6 @@ let enhance_error_with_trace msg =
_last_error_kont_ref := Nil; _last_error_kont_ref := Nil;
msg ^ (format_comp_trace trace) msg ^ (format_comp_trace trace)
(* Hand-written sf_define_type — skipped from transpile because the spec uses
&rest params and empty-dict literals that the transpiler can't emit cleanly.
Implements: (define-type Name (Ctor1 f1 f2) (Ctor2 f3) ...)
Creates constructor fns, Name?/Ctor? predicates, Ctor-field accessors,
and records ctors in *adt-registry*. *)
let sf_define_type args env_val =
let items = (match args with List l -> l | _ -> []) in
let type_sym = List.nth items 0 in
let type_name = value_to_string type_sym in
let ctor_specs = List.tl items in
let env_has_v k = sx_truthy (env_has env_val (String k)) in
let env_bind_v k v = ignore (env_bind env_val (String k) v) in
let env_get_v k = env_get env_val (String k) in
if not (env_has_v "*adt-registry*") then
env_bind_v "*adt-registry*" (Dict (Hashtbl.create 8));
let registry = env_get_v "*adt-registry*" in
let ctor_names = List.map (fun spec ->
(match spec with List (sym :: _) -> String (value_to_string sym) | _ -> Nil)
) ctor_specs in
(match registry with Dict d -> Hashtbl.replace d type_name (List ctor_names) | _ -> ());
env_bind_v (type_name ^ "?")
(NativeFn (type_name ^ "?", fun pargs ->
(match pargs with
| [v] ->
(match v with
| Dict d -> Bool (Hashtbl.mem d "_adt" &&
(match Hashtbl.find_opt d "_type" with Some (String t) -> t = type_name | _ -> false))
| _ -> Bool false)
| _ -> Bool false)));
List.iter (fun spec ->
(match spec with
| List (sym :: fields) ->
let cn = value_to_string sym in
let field_names = List.map value_to_string fields in
let arity = List.length fields in
env_bind_v cn
(NativeFn (cn, fun ctor_args ->
if List.length ctor_args <> arity then
raise (Eval_error (Printf.sprintf "%s: expected %d args, got %d"
cn arity (List.length ctor_args)))
else begin
let d = Hashtbl.create 4 in
Hashtbl.replace d "_adt" (Bool true);
Hashtbl.replace d "_type" (String type_name);
Hashtbl.replace d "_ctor" (String cn);
Hashtbl.replace d "_fields" (List ctor_args);
Dict d
end));
env_bind_v (cn ^ "?")
(NativeFn (cn ^ "?", fun pargs ->
(match pargs with
| [v] ->
(match v with
| Dict d -> Bool (Hashtbl.mem d "_adt" &&
(match Hashtbl.find_opt d "_ctor" with Some (String c) -> c = cn | _ -> false))
| _ -> Bool false)
| _ -> Bool false)));
List.iteri (fun idx fname ->
env_bind_v (cn ^ "-" ^ fname)
(NativeFn (cn ^ "-" ^ fname, fun pargs ->
(match pargs with
| [v] ->
(match v with
| Dict d ->
(match Hashtbl.find_opt d "_fields" with
| Some (List fs) ->
if idx < List.length fs then List.nth fs idx
else raise (Eval_error (cn ^ "-" ^ fname ^ ": index out of bounds"))
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not an ADT")))
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not a dict")))
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": expected 1 arg")))))
) field_names
| _ -> ())
) ctor_specs;
Nil
(* Register define-type via custom_special_forms so the CEK dispatch finds it.
The top-level (register-special-form! ...) in spec/evaluator.sx is not a
define and therefore is not transpiled; we wire it up here instead. *)
let () = ignore (register_special_form (String "define-type")
(NativeFn ("define-type", fun call_args ->
match call_args with
| [args; env] -> sf_define_type args env
| _ -> Nil)))
""" """
@@ -257,10 +171,7 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
"debug-log", "debug_log", "range", "chunk-every", "zip-pairs", "debug-log", "debug_log", "range", "chunk-every", "zip-pairs",
"string-contains?", "starts-with?", "ends-with?", "string-contains?", "starts-with?", "ends-with?",
"string-replace", "trim", "split", "index-of", "string-replace", "trim", "split", "index-of",
"pad-left", "pad-right", "char-at", "substring", "pad-left", "pad-right", "char-at", "substring"}
# sf-define-type uses &rest + empty-dict literals that the transpiler
# can't emit as valid OCaml; hand-written implementation in FIXUPS.
"sf-define-type"}
defines = [(n, e) for n, e in defines if n not in skip] defines = [(n, e) for n, e in defines if n not in skip]
# Deduplicate — keep last definition for each name (CEK overrides tree-walk) # Deduplicate — keep last definition for each name (CEK overrides tree-walk)

View File

@@ -90,18 +90,6 @@ let read_symbol s =
String.sub s.src start (s.pos - start) String.sub s.src start (s.pos - start)
let try_number str = let try_number str =
(* Integers (no '.' or 'e'/'E') → exact Integer; floats → inexact Number *)
let has_dec = String.contains str '.' in
let has_exp = String.contains str 'e' || String.contains str 'E' in
if has_dec || has_exp then
match float_of_string_opt str with
| Some n -> Some (Number n)
| None -> None
else
match int_of_string_opt str with
| Some n -> Some (Integer n)
| None ->
(* handles "nan", "inf", "-inf" *)
match float_of_string_opt str with match float_of_string_opt str with
| Some n -> Some (Number n) | Some n -> Some (Number n)
| None -> None | None -> None

View File

@@ -51,15 +51,7 @@ let get_primitive name =
(* Trampoline hook — set by sx_ref after initialization to break circular dep *) (* Trampoline hook — set by sx_ref after initialization to break circular dep *)
let trampoline_hook : (value -> value) ref = ref (fun v -> v) let trampoline_hook : (value -> value) ref = ref (fun v -> v)
let as_int = function
| Integer n -> n
| Number n -> int_of_float n
| v -> raise (Eval_error ("Expected number, got " ^ type_of v))
let all_ints = List.for_all (function Integer _ -> true | _ -> false)
let rec as_number = function let rec as_number = function
| Integer n -> float_of_int n
| Number n -> n | Number n -> n
| Bool true -> 1.0 | Bool true -> 1.0
| Bool false -> 0.0 | Bool false -> 0.0
@@ -87,7 +79,6 @@ let as_bool = function
let rec to_string = function let rec to_string = function
| String s -> s | String s -> s
| Integer n -> string_of_int n
| Number n -> Sx_types.format_number n | Number n -> Sx_types.format_number n
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
@@ -102,26 +93,13 @@ let rec to_string = function
let () = let () =
(* === Arithmetic === *) (* === Arithmetic === *)
register "+" (fun args -> register "+" (fun args ->
if all_ints args then
Integer (List.fold_left (fun acc a -> match a with Integer n -> acc + n | _ -> acc) 0 args)
else
Number (List.fold_left (fun acc a -> acc +. as_number a) 0.0 args)); Number (List.fold_left (fun acc a -> acc +. as_number a) 0.0 args));
register "-" (fun args -> register "-" (fun args ->
match args with match args with
| [] -> Integer 0 | [] -> Number 0.0
| [Integer n] -> Integer (-n)
| [a] -> Number (-. (as_number a)) | [a] -> Number (-. (as_number a))
| _ when all_ints args -> | a :: rest -> Number (List.fold_left (fun acc x -> acc -. as_number x) (as_number a) rest));
(match args with
| Integer h :: tl ->
Integer (List.fold_left (fun acc a -> match a with Integer n -> acc - n | _ -> acc) h tl)
| _ -> Number 0.0)
| a :: rest ->
Number (List.fold_left (fun acc x -> acc -. as_number x) (as_number a) rest));
register "*" (fun args -> register "*" (fun args ->
if all_ints args then
Integer (List.fold_left (fun acc a -> match a with Integer n -> acc * n | _ -> acc) 1 args)
else
Number (List.fold_left (fun acc a -> acc *. as_number a) 1.0 args)); Number (List.fold_left (fun acc a -> acc *. as_number a) 1.0 args));
register "/" (fun args -> register "/" (fun args ->
match args with match args with
@@ -129,54 +107,35 @@ let () =
| _ -> raise (Eval_error "/: expected 2 args")); | _ -> raise (Eval_error "/: expected 2 args"));
register "mod" (fun args -> register "mod" (fun args ->
match args with match args with
| [Integer a; Integer b] -> Integer (a mod b)
| [a; b] -> Number (Float.rem (as_number a) (as_number b)) | [a; b] -> Number (Float.rem (as_number a) (as_number b))
| _ -> raise (Eval_error "mod: expected 2 args")); | _ -> raise (Eval_error "mod: expected 2 args"));
register "inc" (fun args -> register "inc" (fun args ->
match args with match args with [a] -> Number (as_number a +. 1.0) | _ -> raise (Eval_error "inc: 1 arg"));
| [Integer n] -> Integer (n + 1)
| [a] -> Number (as_number a +. 1.0)
| _ -> raise (Eval_error "inc: 1 arg"));
register "dec" (fun args -> register "dec" (fun args ->
match args with match args with [a] -> Number (as_number a -. 1.0) | _ -> raise (Eval_error "dec: 1 arg"));
| [Integer n] -> Integer (n - 1)
| [a] -> Number (as_number a -. 1.0)
| _ -> raise (Eval_error "dec: 1 arg"));
register "abs" (fun args -> register "abs" (fun args ->
match args with match args with [a] -> Number (Float.abs (as_number a)) | _ -> raise (Eval_error "abs: 1 arg"));
| [Integer n] -> Integer (abs n)
| [a] -> Number (Float.abs (as_number a))
| _ -> raise (Eval_error "abs: 1 arg"));
register "floor" (fun args -> register "floor" (fun args ->
match args with match args with [a] -> Number (floor (as_number a))
| [Integer n] -> Integer n
| [a] -> Integer (int_of_float (floor (as_number a)))
| _ -> raise (Eval_error "floor: 1 arg")); | _ -> raise (Eval_error "floor: 1 arg"));
register "ceil" (fun args -> register "ceil" (fun args ->
match args with match args with [a] -> Number (ceil (as_number a))
| [Integer n] -> Integer n
| [a] -> Integer (int_of_float (ceil (as_number a)))
| _ -> raise (Eval_error "ceil: 1 arg")); | _ -> raise (Eval_error "ceil: 1 arg"));
register "round" (fun args -> register "round" (fun args ->
match args with match args with
| [Integer n] -> Integer n | [a] -> Number (Float.round (as_number a))
| [a] -> Integer (int_of_float (Float.round (as_number a)))
| [a; b] -> | [a; b] ->
let n = as_number a and places = as_int b in let n = as_number a and places = int_of_float (as_number b) in
let factor = 10.0 ** float_of_int places in let factor = 10.0 ** float_of_int places in
Number (Float.round (n *. factor) /. factor) Number (Float.round (n *. factor) /. factor)
| _ -> raise (Eval_error "round: 1-2 args")); | _ -> raise (Eval_error "round: 1-2 args"));
register "min" (fun args -> register "min" (fun args ->
match args with match args with
| [] -> raise (Eval_error "min: at least 1 arg") | [] -> raise (Eval_error "min: at least 1 arg")
| _ when all_ints args ->
Integer (List.fold_left (fun acc a -> match a with Integer n -> min acc n | _ -> acc) max_int args)
| _ -> Number (List.fold_left (fun acc a -> Float.min acc (as_number a)) Float.infinity args)); | _ -> Number (List.fold_left (fun acc a -> Float.min acc (as_number a)) Float.infinity args));
register "max" (fun args -> register "max" (fun args ->
match args with match args with
| [] -> raise (Eval_error "max: at least 1 arg") | [] -> raise (Eval_error "max: at least 1 arg")
| _ when all_ints args ->
Integer (List.fold_left (fun acc a -> match a with Integer n -> max acc n | _ -> acc) min_int args)
| _ -> Number (List.fold_left (fun acc a -> Float.max acc (as_number a)) Float.neg_infinity args)); | _ -> Number (List.fold_left (fun acc a -> Float.max acc (as_number a)) Float.neg_infinity args));
register "sqrt" (fun args -> register "sqrt" (fun args ->
match args with [a] -> Number (Float.sqrt (as_number a)) | _ -> raise (Eval_error "sqrt: 1 arg")); match args with [a] -> Number (Float.sqrt (as_number a)) | _ -> raise (Eval_error "sqrt: 1 arg"));
@@ -230,7 +189,6 @@ let () =
Number (Float.sqrt sum)); Number (Float.sqrt sum));
register "sign" (fun args -> register "sign" (fun args ->
match args with match args with
| [Integer n] -> Integer (if n > 0 then 1 else if n < 0 then -1 else 0)
| [a] -> | [a] ->
let n = as_number a in let n = as_number a in
Number (if Float.is_nan n then Float.nan Number (if Float.is_nan n then Float.nan
@@ -276,47 +234,32 @@ let () =
| _ -> raise (Eval_error "clamp: 3 args")); | _ -> raise (Eval_error "clamp: 3 args"));
register "truncate" (fun args -> register "truncate" (fun args ->
match args with match args with
| [Integer n] -> Integer n | [a] -> let n = as_number a in Number (if n >= 0.0 then floor n else ceil n)
| [a] -> let n = as_number a in Integer (int_of_float (if n >= 0.0 then floor n else ceil n))
| _ -> raise (Eval_error "truncate: 1 arg")); | _ -> raise (Eval_error "truncate: 1 arg"));
register "remainder" (fun args -> register "remainder" (fun args ->
match args with match args with
| [Integer a; Integer b] -> Integer (a mod b)
| [a; b] -> Number (Float.rem (as_number a) (as_number b)) | [a; b] -> Number (Float.rem (as_number a) (as_number b))
| _ -> raise (Eval_error "remainder: 2 args")); | _ -> raise (Eval_error "remainder: 2 args"));
register "modulo" (fun args -> register "modulo" (fun args ->
match args with match args with
| [Integer a; Integer b] ->
let r = a mod b in
Integer (if r = 0 || (r > 0) = (b > 0) then r else r + b)
| [a; b] -> | [a; b] ->
let a = as_number a and b = as_number b in let a = as_number a and b = as_number b in
let r = Float.rem a b in let r = Float.rem a b in
Number (if r = 0.0 || (r > 0.0) = (b > 0.0) then r else r +. b) Number (if r = 0.0 || (r > 0.0) = (b > 0.0) then r else r +. b)
| _ -> raise (Eval_error "modulo: 2 args")); | _ -> raise (Eval_error "modulo: 2 args"));
register "exact?" (fun args -> register "exact?" (fun args ->
match args with match args with [Number f] -> Bool (Float.is_integer f) | [_] -> Bool false
| [Integer _] -> Bool true
| [Number _] -> Bool false
| [_] -> Bool false
| _ -> raise (Eval_error "exact?: 1 arg")); | _ -> raise (Eval_error "exact?: 1 arg"));
register "inexact?" (fun args -> register "inexact?" (fun args ->
match args with match args with [Number f] -> Bool (not (Float.is_integer f)) | [_] -> Bool false
| [Number _] -> Bool true
| [Integer _] -> Bool false
| [_] -> Bool false
| _ -> raise (Eval_error "inexact?: 1 arg")); | _ -> raise (Eval_error "inexact?: 1 arg"));
register "exact->inexact" (fun args -> register "exact->inexact" (fun args ->
match args with match args with [Number n] -> Number n | [a] -> Number (as_number a)
| [Integer n] -> Number (float_of_int n)
| [Number n] -> Number n
| [a] -> Number (as_number a)
| _ -> raise (Eval_error "exact->inexact: 1 arg")); | _ -> raise (Eval_error "exact->inexact: 1 arg"));
register "inexact->exact" (fun args -> register "inexact->exact" (fun args ->
match args with match args with
| [Integer n] -> Integer n | [Number n] -> if Float.is_integer n then Number n else Number (Float.round n)
| [Number n] -> Integer (int_of_float (Float.round n)) | [a] -> Number (Float.round (as_number a))
| [a] -> Integer (int_of_float (Float.round (as_number a)))
| _ -> raise (Eval_error "inexact->exact: 1 arg")); | _ -> raise (Eval_error "inexact->exact: 1 arg"));
register "parse-int" (fun args -> register "parse-int" (fun args ->
let parse_leading_int s = let parse_leading_int s =
@@ -333,11 +276,10 @@ let () =
else None else None
in in
match args with match args with
| [String s] -> (match parse_leading_int s with Some n -> Integer n | None -> Nil) | [String s] -> (match parse_leading_int s with Some n -> Number (float_of_int n) | None -> Nil)
| [String s; default_val] -> | [String s; default_val] ->
(match parse_leading_int s with Some n -> Integer n | None -> default_val) (match parse_leading_int s with Some n -> Number (float_of_int n) | None -> default_val)
| [Integer n] | [Integer n; _] -> Integer n | [Number n] | [Number n; _] -> Number (float_of_int (int_of_float n))
| [Number n] | [Number n; _] -> Integer (int_of_float n)
| [_; default_val] -> default_val | [_; default_val] -> default_val
| _ -> Nil); | _ -> Nil);
register "parse-float" (fun args -> register "parse-float" (fun args ->
@@ -354,10 +296,7 @@ let () =
let rec safe_eq a b = let rec safe_eq a b =
if a == b then true (* physical equality fast path *) if a == b then true (* physical equality fast path *)
else match a, b with else match a, b with
| Integer x, Integer y -> x = y
| Number x, Number y -> x = y | Number x, Number y -> x = y
| Integer x, Number y -> float_of_int x = y
| Number x, Integer y -> x = float_of_int y
| String x, String y -> x = y | String x, String y -> x = y
| Bool x, Bool y -> x = y | Bool x, Bool y -> x = y
| Nil, Nil -> true | Nil, Nil -> true
@@ -429,21 +368,9 @@ let () =
register "nil?" (fun args -> register "nil?" (fun args ->
match args with [a] -> Bool (is_nil a) | _ -> raise (Eval_error "nil?: 1 arg")); match args with [a] -> Bool (is_nil a) | _ -> raise (Eval_error "nil?: 1 arg"));
register "number?" (fun args -> register "number?" (fun args ->
match args with match args with [Number _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "number?: 1 arg"));
| [Integer _] | [Number _] -> Bool true
| [_] -> Bool false
| _ -> raise (Eval_error "number?: 1 arg"));
register "integer?" (fun args -> register "integer?" (fun args ->
match args with match args with [Number f] -> Bool (Float.is_integer f) | [_] -> Bool false | _ -> raise (Eval_error "integer?: 1 arg"));
| [Integer _] -> Bool true
| [Number f] -> Bool (Float.is_integer f)
| [_] -> Bool false
| _ -> raise (Eval_error "integer?: 1 arg"));
register "float?" (fun args ->
match args with
| [Number _] -> Bool true
| [_] -> Bool false
| _ -> raise (Eval_error "float?: 1 arg"));
register "string?" (fun args -> register "string?" (fun args ->
match args with [String _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "string?: 1 arg")); match args with [String _] -> Bool true | [_] -> Bool false | _ -> raise (Eval_error "string?: 1 arg"));
register "boolean?" (fun args -> register "boolean?" (fun args ->
@@ -485,7 +412,7 @@ let () =
register "trim" (fun args -> register "trim" (fun args ->
match args with [a] -> String (String.trim (as_string a)) | _ -> raise (Eval_error "trim: 1 arg")); match args with [a] -> String (String.trim (as_string a)) | _ -> raise (Eval_error "trim: 1 arg"));
register "string-length" (fun args -> register "string-length" (fun args ->
match args with [a] -> Integer (String.length (as_string a)) match args with [a] -> Number (float_of_int (String.length (as_string a)))
| _ -> raise (Eval_error "string-length: 1 arg")); | _ -> raise (Eval_error "string-length: 1 arg"));
register "string-contains?" (fun args -> register "string-contains?" (fun args ->
match args with match args with
@@ -519,11 +446,7 @@ let () =
in find 0 in find 0
| [List items; target] | [ListRef { contents = items }; target] -> | [List items; target] | [ListRef { contents = items }; target] ->
let eq a b = match a, b with let eq a b = match a, b with
| Integer x, Integer y -> x = y | String x, String y -> x = y | Number x, Number y -> x = y
| Number x, Number y -> x = y
| Integer x, Number y -> float_of_int x = y
| Number x, Integer y -> x = float_of_int y
| String x, String y -> x = y
| Symbol x, Symbol y -> x = y | Keyword x, Keyword y -> x = y | Symbol x, Symbol y -> x = y | Keyword x, Keyword y -> x = y
| Bool x, Bool y -> x = y | Nil, Nil -> true | _ -> a == b in | Bool x, Bool y -> x = y | Nil, Nil -> true | _ -> a == b in
let rec find i = function let rec find i = function
@@ -534,22 +457,22 @@ let () =
| _ -> raise (Eval_error "index-of: 2 string args or list+target")); | _ -> raise (Eval_error "index-of: 2 string args or list+target"));
register "substring" (fun args -> register "substring" (fun args ->
match args with match args with
| [String s; start_v; end_v] -> | [String s; Number start; Number end_] ->
let i = as_int start_v and j = as_int end_v in let i = int_of_float start and j = int_of_float end_ in
let len = String.length s in let len = String.length s in
let i = max 0 (min i len) and j = max 0 (min j len) in let i = max 0 (min i len) and j = max 0 (min j len) in
String (String.sub s i (max 0 (j - i))) String (String.sub s i (max 0 (j - i)))
| _ -> raise (Eval_error "substring: 3 args")); | _ -> raise (Eval_error "substring: 3 args"));
register "substr" (fun args -> register "substr" (fun args ->
match args with match args with
| [String s; start_v; len_v] -> | [String s; Number start; Number len] ->
let i = as_int start_v and n = as_int len_v in let i = int_of_float start and n = int_of_float len in
let sl = String.length s in let sl = String.length s in
let i = max 0 (min i sl) in let i = max 0 (min i sl) in
let n = max 0 (min n (sl - i)) in let n = max 0 (min n (sl - i)) in
String (String.sub s i n) String (String.sub s i n)
| [String s; start_v] -> | [String s; Number start] ->
let i = as_int start_v in let i = int_of_float start in
let sl = String.length s in let sl = String.length s in
let i = max 0 (min i sl) in let i = max 0 (min i sl) in
String (String.sub s i (sl - i)) String (String.sub s i (sl - i))
@@ -574,7 +497,6 @@ let () =
| String s -> s | SxExpr s -> s | RawHTML s -> s | String s -> s | SxExpr s -> s | RawHTML s -> s
| Keyword k -> k | Symbol s -> s | Keyword k -> k | Symbol s -> s
| Nil -> "" | Bool true -> "true" | Bool false -> "false" | Nil -> "" | Bool true -> "true" | Bool false -> "false"
| Integer n -> string_of_int n
| Number n -> if Float.is_integer n then string_of_int (int_of_float n) else Printf.sprintf "%g" n | Number n -> if Float.is_integer n then string_of_int (int_of_float n) else Printf.sprintf "%g" n
| Thunk _ as t -> (match !_sx_trampoline_fn t with String s -> s | v -> to_string v) | Thunk _ as t -> (match !_sx_trampoline_fn t with String s -> s | v -> to_string v)
| v -> to_string v | v -> to_string v
@@ -601,35 +523,28 @@ let () =
| _ -> raise (Eval_error "replace: 3 string args")); | _ -> raise (Eval_error "replace: 3 string args"));
register "char-from-code" (fun args -> register "char-from-code" (fun args ->
match args with match args with
| [a] -> | [Number n] ->
let n = as_int a in
let buf = Buffer.create 4 in let buf = Buffer.create 4 in
Buffer.add_utf_8_uchar buf (Uchar.of_int n); Buffer.add_utf_8_uchar buf (Uchar.of_int (int_of_float n));
String (Buffer.contents buf) String (Buffer.contents buf)
| _ -> raise (Eval_error "char-from-code: 1 arg")); | _ -> raise (Eval_error "char-from-code: 1 arg"));
register "char-at" (fun args -> register "char-at" (fun args ->
match args with match args with
| [String s; n] -> | [String s; Number n] ->
let i = as_int n in let i = int_of_float n in
if i >= 0 && i < String.length s then if i >= 0 && i < String.length s then
String (String.make 1 s.[i]) String (String.make 1 s.[i])
else Nil else Nil
| _ -> raise (Eval_error "char-at: string and index")); | _ -> raise (Eval_error "char-at: string and index"));
register "char-code" (fun args -> register "char-code" (fun args ->
match args with match args with
| [String s] when String.length s > 0 -> Integer (Char.code s.[0]) | [String s] when String.length s > 0 -> Number (float_of_int (Char.code s.[0]))
| _ -> raise (Eval_error "char-code: 1 non-empty string arg")); | _ -> raise (Eval_error "char-code: 1 non-empty string arg"));
register "parse-number" (fun args -> register "parse-number" (fun args ->
match args with match args with
| [String s] -> | [String s] ->
let has_dec = String.contains s '.' in (try Number (float_of_string s)
let has_exp = String.contains s 'e' || String.contains s 'E' in with Failure _ -> Nil)
if has_dec || has_exp then
(try Number (float_of_string s) with Failure _ -> Nil)
else
(match int_of_string_opt s with
| Some n -> Integer n
| None -> (try Number (float_of_string s) with Failure _ -> Nil))
| _ -> raise (Eval_error "parse-number: 1 string arg")); | _ -> raise (Eval_error "parse-number: 1 string arg"));
(* === Regex (PCRE-compatible — same syntax as JS RegExp) === *) (* === Regex (PCRE-compatible — same syntax as JS RegExp) === *)
@@ -706,17 +621,17 @@ let () =
register "list" (fun args -> ListRef (ref args)); register "list" (fun args -> ListRef (ref args));
register "len" (fun args -> register "len" (fun args ->
match args with match args with
| [List l] | [ListRef { contents = l }] -> Integer (List.length l) | [List l] | [ListRef { contents = l }] -> Number (float_of_int (List.length l))
| [String s] -> Integer (String.length s) | [String s] -> Number (float_of_int (String.length s))
| [Dict d] -> Integer (Hashtbl.length d) | [Dict d] -> Number (float_of_int (Hashtbl.length d))
| [Nil] | [Bool false] -> Integer 0 | [Nil] | [Bool false] -> Number 0.0
| [Bool true] -> Integer 1 | [Bool true] -> Number 1.0
| [Number _] | [Integer _] -> Integer 1 | [Number _] -> Number 1.0
| [RawHTML s] -> Integer (String.length s) | [RawHTML s] -> Number (float_of_int (String.length s))
| [SxExpr s] -> Integer (String.length s) | [SxExpr s] -> Number (float_of_int (String.length s))
| [Spread pairs] -> Integer (List.length pairs) | [Spread pairs] -> Number (float_of_int (List.length pairs))
| [Component _] | [Island _] | [Lambda _] | [NativeFn _] | [Component _] | [Island _] | [Lambda _] | [NativeFn _]
| [Macro _] | [Thunk _] | [Keyword _] | [Symbol _] -> Integer 0 | [Macro _] | [Thunk _] | [Keyword _] | [Symbol _] -> Number 0.0
| _ -> raise (Eval_error (Printf.sprintf "len: %d args" | _ -> raise (Eval_error (Printf.sprintf "len: %d args"
(List.length args)))); (List.length args))));
register "length" (Hashtbl.find primitives "len"); register "length" (Hashtbl.find primitives "len");
@@ -743,10 +658,10 @@ let () =
| _ -> raise (Eval_error "init: 1 list arg")); | _ -> raise (Eval_error "init: 1 list arg"));
register "nth" (fun args -> register "nth" (fun args ->
match args with match args with
| [List l; n] | [ListRef { contents = l }; n] -> | [List l; Number n] | [ListRef { contents = l }; Number n] ->
(try List.nth l (as_int n) with _ -> Nil) (try List.nth l (int_of_float n) with _ -> Nil)
| [String s; n] -> | [String s; Number n] ->
let i = as_int n in let i = int_of_float n in
if i >= 0 && i < String.length s then String (String.make 1 s.[i]) if i >= 0 && i < String.length s then String (String.make 1 s.[i])
else Nil else Nil
| _ -> raise (Eval_error "nth: list/string and number")); | _ -> raise (Eval_error "nth: list/string and number"));
@@ -792,10 +707,7 @@ let () =
let safe_eq a b = let safe_eq a b =
a == b || a == b ||
(match a, b with (match a, b with
| Integer x, Integer y -> x = y
| Number x, Number y -> x = y | Number x, Number y -> x = y
| Integer x, Number y -> float_of_int x = y
| Number x, Integer y -> x = float_of_int y
| String x, String y -> x = y | String x, String y -> x = y
| Bool x, Bool y -> x = y | Bool x, Bool y -> x = y
| Nil, Nil -> true | Nil, Nil -> true
@@ -817,27 +729,15 @@ let () =
| _ -> raise (Eval_error "contains?: 2 args")); | _ -> raise (Eval_error "contains?: 2 args"));
register "range" (fun args -> register "range" (fun args ->
match args with match args with
| [stop_v] -> | [Number stop] ->
let n = as_int stop_v in let n = int_of_float stop in
List (List.init (max 0 n) (fun i -> Integer i)) List (List.init (max 0 n) (fun i -> Number (float_of_int i)))
| [start_v; stop_v] -> | [Number start; Number stop] ->
let s = as_int start_v and e = as_int stop_v in let s = int_of_float start and e = int_of_float stop in
let len = max 0 (e - s) in let len = max 0 (e - s) in
List (List.init len (fun i -> Integer (s + i))) List (List.init len (fun i -> Number (float_of_int (s + i))))
| [start_v; stop_v; step_v] -> | [Number start; Number stop; Number step] ->
(match start_v, stop_v, step_v with let s = start and e = stop and st = step in
| Integer s, Integer e, Integer st ->
if st = 0 then List []
else
let items = ref [] in
let i = ref s in
if st > 0 then
(while !i < e do items := Integer !i :: !items; i := !i + st done)
else
(while !i > e do items := Integer !i :: !items; i := !i + st done);
List (List.rev !items)
| _ ->
let s = as_number start_v and e = as_number stop_v and st = as_number step_v in
if st = 0.0 then List [] if st = 0.0 then List []
else else
let items = ref [] in let items = ref [] in
@@ -846,16 +746,16 @@ let () =
(while !i < e do items := Number !i :: !items; i := !i +. st done) (while !i < e do items := Number !i :: !items; i := !i +. st done)
else else
(while !i > e do items := Number !i :: !items; i := !i +. st done); (while !i > e do items := Number !i :: !items; i := !i +. st done);
List (List.rev !items)) List (List.rev !items)
| _ -> raise (Eval_error "range: 1-3 args")); | _ -> raise (Eval_error "range: 1-3 args"));
register "slice" (fun args -> register "slice" (fun args ->
match args with match args with
| [(List l | ListRef { contents = l }); start_v] -> | [(List l | ListRef { contents = l }); Number start] ->
let i = max 0 (as_int start_v) in let i = max 0 (int_of_float start) in
let rec drop n = function _ :: xs when n > 0 -> drop (n-1) xs | l -> l in let rec drop n = function _ :: xs when n > 0 -> drop (n-1) xs | l -> l in
List (drop i l) List (drop i l)
| [(List l | ListRef { contents = l }); start_v; end_v] -> | [(List l | ListRef { contents = l }); Number start; Number end_] ->
let i = max 0 (as_int start_v) and j = as_int end_v in let i = max 0 (int_of_float start) and j = int_of_float end_ in
let len = List.length l in let len = List.length l in
let j = min j len in let j = min j len in
let rec take_range idx = function let rec take_range idx = function
@@ -865,11 +765,11 @@ let () =
else if idx >= i then x :: take_range (idx+1) xs else if idx >= i then x :: take_range (idx+1) xs
else take_range (idx+1) xs else take_range (idx+1) xs
in List (take_range 0 l) in List (take_range 0 l)
| [String s; start_v] -> | [String s; Number start] ->
let i = max 0 (as_int start_v) in let i = max 0 (int_of_float start) in
String (String.sub s i (max 0 (String.length s - i))) String (String.sub s i (max 0 (String.length s - i)))
| [String s; start_v; end_v] -> | [String s; Number start; Number end_] ->
let i = max 0 (as_int start_v) and j = as_int end_v in let i = max 0 (int_of_float start) and j = int_of_float end_ in
let sl = String.length s in let sl = String.length s in
let j = min j sl in let j = min j sl in
String (String.sub s i (max 0 (j - i))) String (String.sub s i (max 0 (j - i)))
@@ -898,24 +798,24 @@ let () =
| _ -> raise (Eval_error "zip-pairs: 1 list")); | _ -> raise (Eval_error "zip-pairs: 1 list"));
register "take" (fun args -> register "take" (fun args ->
match args with match args with
| [(List l | ListRef { contents = l }); n] -> | [(List l | ListRef { contents = l }); Number n] ->
let rec take_n i = function let rec take_n i = function
| x :: xs when i > 0 -> x :: take_n (i-1) xs | x :: xs when i > 0 -> x :: take_n (i-1) xs
| _ -> [] | _ -> []
in List (take_n (as_int n) l) in List (take_n (int_of_float n) l)
| _ -> raise (Eval_error "take: list and number")); | _ -> raise (Eval_error "take: list and number"));
register "drop" (fun args -> register "drop" (fun args ->
match args with match args with
| [(List l | ListRef { contents = l }); n] -> | [(List l | ListRef { contents = l }); Number n] ->
let rec drop_n i = function let rec drop_n i = function
| _ :: xs when i > 0 -> drop_n (i-1) xs | _ :: xs when i > 0 -> drop_n (i-1) xs
| l -> l | l -> l
in List (drop_n (as_int n) l) in List (drop_n (int_of_float n) l)
| _ -> raise (Eval_error "drop: list and number")); | _ -> raise (Eval_error "drop: list and number"));
register "chunk-every" (fun args -> register "chunk-every" (fun args ->
match args with match args with
| [(List l | ListRef { contents = l }); n] -> | [(List l | ListRef { contents = l }); Number n] ->
let size = as_int n in let size = int_of_float n in
let rec go = function let rec go = function
| [] -> [] | [] -> []
| l -> | l ->
@@ -955,9 +855,8 @@ let () =
match args with match args with
| [Dict d; String k] -> dict_get d k | [Dict d; String k] -> dict_get d k
| [Dict d; Keyword k] -> dict_get d k | [Dict d; Keyword k] -> dict_get d k
| [List l; n] | [ListRef { contents = l }; n] | [List l; Number n] | [ListRef { contents = l }; Number n] ->
when (match n with Number _ | Integer _ -> true | _ -> false) -> (try List.nth l (int_of_float n) with _ -> Nil)
(try List.nth l (as_int n) with _ -> Nil)
| [Nil; _] -> Nil (* nil.anything → nil *) | [Nil; _] -> Nil (* nil.anything → nil *)
| [_; _] -> Nil (* type mismatch → nil (matches JS/Python behavior) *) | [_; _] -> Nil (* type mismatch → nil (matches JS/Python behavior) *)
| _ -> Nil); | _ -> Nil);
@@ -998,8 +897,8 @@ let () =
register "mutable-list" (fun _args -> ListRef (ref [])); register "mutable-list" (fun _args -> ListRef (ref []));
register "set-nth!" (fun args -> register "set-nth!" (fun args ->
match args with match args with
| [ListRef r; idx; v] -> | [ListRef r; Number n; v] ->
let i = as_int idx in let i = int_of_float n in
let l = !r in let l = !r in
r := List.mapi (fun j x -> if j = i then v else x) l; r := List.mapi (fun j x -> if j = i then v else x) l;
Nil Nil
@@ -1126,15 +1025,15 @@ let () =
register "identical?" (fun args -> register "identical?" (fun args ->
match args with match args with
| [a; b] -> | [a; b] ->
(* Physical identity for reference types, structural for values.
Numbers/strings/booleans from different constant pools must
compare equal when their values match. *)
let identical = match a, b with let identical = match a, b with
| Integer x, Integer y -> x = y
| Number x, Number y -> x = y | Number x, Number y -> x = y
| Integer x, Number y -> float_of_int x = y | String x, String y -> x = y (* String.equal *)
| Number x, Integer y -> x = float_of_int y
| String x, String y -> x = y
| Bool x, Bool y -> x = y | Bool x, Bool y -> x = y
| Nil, Nil -> true | Nil, Nil -> true
| _ -> a == b | _ -> a == b (* reference identity for dicts, lists, etc. *)
in Bool identical in Bool identical
| _ -> raise (Eval_error "identical?: 2 args")); | _ -> raise (Eval_error "identical?: 2 args"));
register "make-spread" (fun args -> register "make-spread" (fun args ->
@@ -1172,7 +1071,7 @@ let () =
register "map-indexed" (fun args -> register "map-indexed" (fun args ->
match args with match args with
| [f; (List items | ListRef { contents = items })] -> | [f; (List items | ListRef { contents = items })] ->
List (List.mapi (fun i x -> call_any f [Integer i; x]) items) List (List.mapi (fun i x -> call_any f [Number (float_of_int i); x]) items)
| [_; Nil] -> List [] | [_; Nil] -> List []
| _ -> raise (Eval_error "map-indexed: expected (fn list)")); | _ -> raise (Eval_error "map-indexed: expected (fn list)"));
register "filter" (fun args -> register "filter" (fun args ->
@@ -1215,26 +1114,26 @@ let () =
(* ---- VM stack primitives (vm.sx platform interface) ---- *) (* ---- VM stack primitives (vm.sx platform interface) ---- *)
register "make-vm-stack" (fun args -> register "make-vm-stack" (fun args ->
match args with match args with
| [n] -> ListRef (ref (List.init (as_int n) (fun _ -> Nil))) | [Number n] -> ListRef (ref (List.init (int_of_float n) (fun _ -> Nil)))
| _ -> raise (Eval_error "make-vm-stack: expected (size)")); | _ -> raise (Eval_error "make-vm-stack: expected (size)"));
register "vm-stack-get" (fun args -> register "vm-stack-get" (fun args ->
match args with match args with
| [ListRef r; n] -> List.nth !r (as_int n) | [ListRef r; Number n] -> List.nth !r (int_of_float n)
| _ -> raise (Eval_error "vm-stack-get: expected (stack idx)")); | _ -> raise (Eval_error "vm-stack-get: expected (stack idx)"));
register "vm-stack-set!" (fun args -> register "vm-stack-set!" (fun args ->
match args with match args with
| [ListRef r; n; v] -> | [ListRef r; Number n; v] ->
let i = as_int n in let i = int_of_float n in
r := List.mapi (fun j x -> if j = i then v else x) !r; Nil r := List.mapi (fun j x -> if j = i then v else x) !r; Nil
| _ -> raise (Eval_error "vm-stack-set!: expected (stack idx val)")); | _ -> raise (Eval_error "vm-stack-set!: expected (stack idx val)"));
register "vm-stack-length" (fun args -> register "vm-stack-length" (fun args ->
match args with match args with
| [ListRef r] -> Integer (List.length !r) | [ListRef r] -> Number (float_of_int (List.length !r))
| _ -> raise (Eval_error "vm-stack-length: expected (stack)")); | _ -> raise (Eval_error "vm-stack-length: expected (stack)"));
register "vm-stack-copy!" (fun args -> register "vm-stack-copy!" (fun args ->
match args with match args with
| [ListRef src; ListRef dst; n] -> | [ListRef src; ListRef dst; Number n] ->
let count = as_int n in let count = int_of_float n in
let src_items = !src in let src_items = !src in
dst := List.mapi (fun i x -> if i < count then List.nth src_items i else x) !dst; Nil dst := List.mapi (fun i x -> if i < count then List.nth src_items i else x) !dst; Nil
| _ -> raise (Eval_error "vm-stack-copy!: expected (src dst count)")); | _ -> raise (Eval_error "vm-stack-copy!: expected (src dst count)"));
@@ -1316,31 +1215,23 @@ let () =
(* R7RS vectors — mutable fixed-size arrays *) (* R7RS vectors — mutable fixed-size arrays *)
register "make-vector" (fun args -> register "make-vector" (fun args ->
match args with match args with
| [n] -> Vector (Array.make (as_int n) Nil) | [Number n] -> Vector (Array.make (int_of_float n) Nil)
| [n; fill] -> Vector (Array.make (as_int n) fill) | [Number n; fill] -> Vector (Array.make (int_of_float n) fill)
| _ -> raise (Eval_error "make-vector: expected (length) or (length fill)")); | _ -> raise (Eval_error "make-vector: expected (length) or (length fill)"));
register "vector" (fun args -> Vector (Array.of_list args)); register "vector" (fun args -> Vector (Array.of_list args));
register "vector?" (fun args -> register "vector?" (fun args ->
match args with [Vector _] -> Bool true | [_] -> Bool false match args with [Vector _] -> Bool true | [_] -> Bool false
| _ -> raise (Eval_error "vector?: 1 arg")); | _ -> raise (Eval_error "vector?: 1 arg"));
register "vector-length" (fun args -> register "vector-length" (fun args ->
match args with [Vector arr] -> Integer (Array.length arr) match args with [Vector arr] -> Number (float_of_int (Array.length arr))
| _ -> raise (Eval_error "vector-length: expected vector")); | _ -> raise (Eval_error "vector-length: expected vector"));
register "vector-ref" (fun args -> register "vector-ref" (fun args ->
match args with match args with
| [Vector arr; n] -> | [Vector arr; Number n] -> arr.(int_of_float n)
let i = as_int n in
if i < 0 || i >= Array.length arr then
raise (Eval_error (Printf.sprintf "vector-ref: index %d out of bounds (length %d)" i (Array.length arr)));
arr.(i)
| _ -> raise (Eval_error "vector-ref: expected (vector index)")); | _ -> raise (Eval_error "vector-ref: expected (vector index)"));
register "vector-set!" (fun args -> register "vector-set!" (fun args ->
match args with match args with
| [Vector arr; n; v] -> | [Vector arr; Number n; v] -> arr.(int_of_float n) <- v; Nil
let i = as_int n in
if i < 0 || i >= Array.length arr then
raise (Eval_error (Printf.sprintf "vector-set!: index %d out of bounds (length %d)" i (Array.length arr)));
arr.(i) <- v; Nil
| _ -> raise (Eval_error "vector-set!: expected (vector index value)")); | _ -> raise (Eval_error "vector-set!: expected (vector index value)"));
register "vector->list" (fun args -> register "vector->list" (fun args ->
match args with [Vector arr] -> List (Array.to_list arr) match args with [Vector arr] -> List (Array.to_list arr)
@@ -1355,35 +1246,8 @@ let () =
| [Vector arr; v] -> Array.fill arr 0 (Array.length arr) v; Nil | [Vector arr; v] -> Array.fill arr 0 (Array.length arr) v; Nil
| _ -> raise (Eval_error "vector-fill!: expected (vector value)")); | _ -> raise (Eval_error "vector-fill!: expected (vector value)"));
register "vector-copy" (fun args -> register "vector-copy" (fun args ->
match args with match args with [Vector arr] -> Vector (Array.copy arr)
| [Vector arr] -> Vector (Array.copy arr) | _ -> raise (Eval_error "vector-copy: expected vector"));
| [Vector arr; s] ->
let start = as_int s in
let len = Array.length arr - start in
if len <= 0 then Vector [||] else Vector (Array.sub arr start len)
| [Vector arr; s; e] ->
let start = as_int s in
let stop = min (as_int e) (Array.length arr) in
let len = stop - start in
if len <= 0 then Vector [||] else Vector (Array.sub arr start len)
| _ -> raise (Eval_error "vector-copy: expected (vector) or (vector start) or (vector start end)"));
(* String buffers — O(1) amortised append for string building in loops *)
register "make-string-buffer" (fun _ -> StringBuffer (Buffer.create 64));
register "string-buffer?" (fun args ->
match args with [StringBuffer _] -> Bool true | [_] -> Bool false
| _ -> raise (Eval_error "string-buffer?: expected 1 arg"));
register "string-buffer-append!" (fun args ->
match args with
| [StringBuffer buf; String s] -> Buffer.add_string buf s; Nil
| [StringBuffer _; v] -> raise (Eval_error ("string-buffer-append!: expected string, got " ^ type_of v))
| _ -> raise (Eval_error "string-buffer-append!: expected (buffer string)"));
register "string-buffer->string" (fun args ->
match args with [StringBuffer buf] -> String (Buffer.contents buf)
| _ -> raise (Eval_error "string-buffer->string: expected (buffer)"));
register "string-buffer-length" (fun args ->
match args with [StringBuffer buf] -> Integer (Buffer.length buf)
| _ -> raise (Eval_error "string-buffer-length: expected (buffer)"));
(* Capability-based sandboxing — gate IO operations *) (* Capability-based sandboxing — gate IO operations *)
let cap_stack : string list ref = ref [] in let cap_stack : string list ref = ref [] in
@@ -2007,49 +1871,4 @@ let () =
| [rx] -> | [rx] ->
let (_, _, flags) = regex_of_value rx in let (_, _, flags) = regex_of_value rx in
String flags String flags
| _ -> raise (Eval_error "regex-flags: (regex)")); | _ -> raise (Eval_error "regex-flags: (regex)"))
(* Bitwise operations *)
register "bitwise-and" (fun args ->
match args with
| [Integer a; Integer b] -> Integer (a land b)
| _ -> raise (Eval_error "bitwise-and: expected (integer integer)"));
register "bitwise-or" (fun args ->
match args with
| [Integer a; Integer b] -> Integer (a lor b)
| _ -> raise (Eval_error "bitwise-or: expected (integer integer)"));
register "bitwise-xor" (fun args ->
match args with
| [Integer a; Integer b] -> Integer (a lxor b)
| _ -> raise (Eval_error "bitwise-xor: expected (integer integer)"));
register "bitwise-not" (fun args ->
match args with
| [Integer a] -> Integer (lnot a)
| _ -> raise (Eval_error "bitwise-not: expected (integer)"));
register "arithmetic-shift" (fun args ->
match args with
| [Integer a; Integer count] ->
Integer (if count >= 0 then a lsl count else a asr (-count))
| _ -> raise (Eval_error "arithmetic-shift: expected (integer integer)"));
register "bit-count" (fun args ->
match args with
| [Integer a] ->
let n = ref (abs a) in
let c = ref 0 in
while !n <> 0 do
c := !c + (!n land 1);
n := !n lsr 1
done;
Integer !c
| _ -> raise (Eval_error "bit-count: expected (integer)"));
register "integer-length" (fun args ->
match args with
| [Integer a] ->
let n = ref (abs a) in
let bits = ref 0 in
while !n <> 0 do
incr bits;
n := !n lsr 1
done;
Integer !bits
| _ -> raise (Eval_error "integer-length: expected (integer)"))

File diff suppressed because one or more lines are too long

View File

@@ -46,7 +46,7 @@ let sx_call f args =
!Sx_types._cek_eval_lambda_ref f args !Sx_types._cek_eval_lambda_ref f args
| Continuation (k, _) -> | Continuation (k, _) ->
k (match args with x :: _ -> x | [] -> Nil) k (match args with x :: _ -> x | [] -> Nil)
| CallccContinuation (_, _) -> | CallccContinuation _ ->
raise (Eval_error "callcc continuations must be invoked through the CEK machine") raise (Eval_error "callcc continuations must be invoked through the CEK machine")
| _ -> | _ ->
let nargs = List.length args in let nargs = List.length args in
@@ -156,9 +156,6 @@ let get_val container key =
| "extra" -> f.cf_extra | "extra2" -> f.cf_extra2 | "extra" -> f.cf_extra | "extra2" -> f.cf_extra2
| "subscribers" -> f.cf_results | "subscribers" -> f.cf_results
| "prev-tracking" -> f.cf_extra | "prev-tracking" -> f.cf_extra
| "after-thunk" -> f.cf_f (* wind-after frame *)
| "winders-len" -> f.cf_extra (* wind-after frame *)
| "body-result" -> f.cf_name (* wind-return frame *)
| _ -> Nil) | _ -> Nil)
| VmFrame f, String k -> | VmFrame f, String k ->
(match k with (match k with
@@ -384,20 +381,15 @@ let continuation_data v = match v with
| _ -> raise (Eval_error "not a continuation") | _ -> raise (Eval_error "not a continuation")
(* Callcc (undelimited) continuation support *) (* Callcc (undelimited) continuation support *)
let callcc_continuation_p v = match v with CallccContinuation (_, _) -> Bool true | _ -> Bool false let callcc_continuation_p v = match v with CallccContinuation _ -> Bool true | _ -> Bool false
let make_callcc_continuation captured winders_len = let make_callcc_continuation captured =
let n = match winders_len with Number f -> int_of_float f | Integer n -> n | _ -> 0 in CallccContinuation (sx_to_list captured)
CallccContinuation (sx_to_list captured, n)
let callcc_continuation_data v = match v with let callcc_continuation_data v = match v with
| CallccContinuation (frames, _) -> List frames | CallccContinuation frames -> List frames
| _ -> raise (Eval_error "not a callcc continuation") | _ -> raise (Eval_error "not a callcc continuation")
let callcc_continuation_winders_len v = match v with
| CallccContinuation (_, n) -> Number (float_of_int n)
| _ -> Number 0.0
(* Dynamic wind — simplified for OCaml (no async) *) (* Dynamic wind — simplified for OCaml (no async) *)
let host_error msg = let host_error msg =
raise (Eval_error (value_to_str msg)) raise (Eval_error (value_to_str msg))

View File

@@ -44,8 +44,7 @@ type env = {
and value = and value =
| Nil | Nil
| Bool of bool | Bool of bool
| Integer of int (** Exact integer — distinct from inexact float. *) | Number of float
| Number of float (** Inexact float. *)
| String of string | String of string
| Symbol of string | Symbol of string
| Keyword of string | Keyword of string
@@ -57,7 +56,7 @@ and value =
| Macro of macro | Macro of macro
| Thunk of value * env | Thunk of value * env
| Continuation of (value -> value) * dict option | Continuation of (value -> value) * dict option
| CallccContinuation of value list * int (** Undelimited continuation — captured kont frames + winders depth at capture *) | CallccContinuation of value list (** Undelimited continuation — captured kont frames *)
| NativeFn of string * (value list -> value) | NativeFn of string * (value list -> value)
| Signal of signal | Signal of signal
| RawHTML of string | RawHTML of string
@@ -73,7 +72,6 @@ and value =
| Record of record (** R7RS record — opaque, generative, field-indexed. *) | Record of record (** R7RS record — opaque, generative, field-indexed. *)
| Parameter of parameter (** R7RS parameter — dynamic binding via kont-stack provide frames. *) | Parameter of parameter (** R7RS parameter — dynamic binding via kont-stack provide frames. *)
| Vector of value array (** R7RS vector — mutable fixed-size array. *) | Vector of value array (** R7RS vector — mutable fixed-size array. *)
| StringBuffer of Buffer.t (** Mutable string buffer — O(1) amortized append. *)
(** CEK machine state — record instead of Dict for performance. (** CEK machine state — record instead of Dict for performance.
5 fields × 55K steps/sec = 275K Hashtbl allocations/sec eliminated. *) 5 fields × 55K steps/sec = 275K Hashtbl allocations/sec eliminated. *)
@@ -394,7 +392,6 @@ let format_number n =
let value_to_string = function let value_to_string = function
| String s -> s | Symbol s -> s | Keyword k -> k | String s -> s | Symbol s -> s | Keyword k -> k
| Integer n -> string_of_int n
| Number n -> format_number n | Number n -> format_number n
| Bool true -> "true" | Bool false -> "false" | Bool true -> "true" | Bool false -> "false"
| Nil -> "" | _ -> "<value>" | Nil -> "" | _ -> "<value>"
@@ -464,7 +461,6 @@ let make_keyword name = Keyword (value_to_string name)
let type_of = function let type_of = function
| Nil -> "nil" | Nil -> "nil"
| Bool _ -> "boolean" | Bool _ -> "boolean"
| Integer _ -> "number"
| Number _ -> "number" | Number _ -> "number"
| String _ -> "string" | String _ -> "string"
| Symbol _ -> "symbol" | Symbol _ -> "symbol"
@@ -477,7 +473,7 @@ let type_of = function
| Macro _ -> "macro" | Macro _ -> "macro"
| Thunk _ -> "thunk" | Thunk _ -> "thunk"
| Continuation (_, _) -> "continuation" | Continuation (_, _) -> "continuation"
| CallccContinuation (_, _) -> "continuation" | CallccContinuation _ -> "continuation"
| NativeFn _ -> "function" | NativeFn _ -> "function"
| Signal _ -> "signal" | Signal _ -> "signal"
| RawHTML _ -> "raw-html" | RawHTML _ -> "raw-html"
@@ -492,7 +488,6 @@ let type_of = function
| Record r -> r.r_type.rt_name | Record r -> r.r_type.rt_name
| Parameter _ -> "parameter" | Parameter _ -> "parameter"
| Vector _ -> "vector" | Vector _ -> "vector"
| StringBuffer _ -> "string-buffer"
let is_nil = function Nil -> true | _ -> false let is_nil = function Nil -> true | _ -> false
let is_lambda = function Lambda _ -> true | _ -> false let is_lambda = function Lambda _ -> true | _ -> false
@@ -508,7 +503,7 @@ let is_signal = function
let is_record = function Record _ -> true | _ -> false let is_record = function Record _ -> true | _ -> false
let is_callable = function let is_callable = function
| Lambda _ | NativeFn _ | Continuation (_, _) | CallccContinuation (_, _) | VmClosure _ -> true | Lambda _ | NativeFn _ | Continuation (_, _) | CallccContinuation _ | VmClosure _ -> true
| _ -> false | _ -> false
@@ -621,7 +616,6 @@ let thunk_env = function
(** {1 Record operations} *) (** {1 Record operations} *)
let val_to_int = function let val_to_int = function
| Integer n -> n
| Number n -> int_of_float n | Number n -> int_of_float n
| v -> raise (Eval_error ("Expected number, got " ^ type_of v)) | v -> raise (Eval_error ("Expected number, got " ^ type_of v))
@@ -783,7 +777,6 @@ let rec inspect = function
| Nil -> "nil" | Nil -> "nil"
| Bool true -> "true" | Bool true -> "true"
| Bool false -> "false" | Bool false -> "false"
| Integer n -> string_of_int n
| Number n -> format_number n | Number n -> format_number n
| String s -> | String s ->
let buf = Buffer.create (String.length s + 2) in let buf = Buffer.create (String.length s + 2) in
@@ -817,7 +810,7 @@ let rec inspect = function
Printf.sprintf "<%s(%s)>" tag (String.concat ", " m.m_params) Printf.sprintf "<%s(%s)>" tag (String.concat ", " m.m_params)
| Thunk _ -> "<thunk>" | Thunk _ -> "<thunk>"
| Continuation (_, _) -> "<continuation>" | Continuation (_, _) -> "<continuation>"
| CallccContinuation (_, _) -> "<callcc-continuation>" | CallccContinuation _ -> "<callcc-continuation>"
| NativeFn (name, _) -> Printf.sprintf "<native:%s>" name | NativeFn (name, _) -> Printf.sprintf "<native:%s>" name
| Signal _ -> "<signal>" | Signal _ -> "<signal>"
| RawHTML s -> Printf.sprintf "\"<raw-html:%d>\"" (String.length s) | RawHTML s -> Printf.sprintf "\"<raw-html:%d>\"" (String.length s)
@@ -838,4 +831,3 @@ let rec inspect = function
Printf.sprintf "#(%s)" (String.concat " " elts) Printf.sprintf "#(%s)" (String.concat " " elts)
| VmFrame f -> Printf.sprintf "<vm-frame:ip=%d base=%d>" f.vf_ip f.vf_base | VmFrame f -> Printf.sprintf "<vm-frame:ip=%d base=%d>" f.vf_ip f.vf_base
| VmMachine m -> Printf.sprintf "<vm-machine:sp=%d frames=%d>" m.vm_sp (List.length m.vm_frames) | VmMachine m -> Printf.sprintf "<vm-machine:sp=%d frames=%d>" m.vm_sp (List.length m.vm_frames)
| StringBuffer buf -> Printf.sprintf "<string-buffer:%d>" (Buffer.length buf)

View File

@@ -185,8 +185,7 @@ let code_from_value v =
| Some _ as r -> r | None -> Hashtbl.find_opt d k2 in | Some _ as r -> r | None -> Hashtbl.find_opt d k2 in
let bc_list = match find2 "bytecode" "vc-bytecode" with let bc_list = match find2 "bytecode" "vc-bytecode" with
| Some (List l | ListRef { contents = l }) -> | Some (List l | ListRef { contents = l }) ->
Array.of_list (List.map (fun x -> match x with Array.of_list (List.map (fun x -> match x with Number n -> int_of_float n | _ -> 0) l)
| Integer n -> n | Number n -> int_of_float n | _ -> 0) l)
| _ -> [||] | _ -> [||]
in in
let entries = match find2 "constants" "vc-constants" with let entries = match find2 "constants" "vc-constants" with
@@ -199,10 +198,10 @@ let code_from_value v =
| _ -> entry | _ -> entry
) entries in ) entries in
let arity = match find2 "arity" "vc-arity" with let arity = match find2 "arity" "vc-arity" with
| Some (Integer n) -> n | Some (Number n) -> int_of_float n | _ -> 0 | Some (Number n) -> int_of_float n | _ -> 0
in in
let rest_arity = match find2 "rest-arity" "vc-rest-arity" with let rest_arity = match find2 "rest-arity" "vc-rest-arity" with
| Some (Integer n) -> n | Some (Number n) -> int_of_float n | _ -> -1 | Some (Number n) -> int_of_float n | _ -> -1
in in
(* Compute locals from bytecode: scan for highest LOCAL_GET/LOCAL_SET slot. (* Compute locals from bytecode: scan for highest LOCAL_GET/LOCAL_SET slot.
The compiler's arity may undercount when nested lets add many locals. *) The compiler's arity may undercount when nested lets add many locals. *)
@@ -750,7 +749,10 @@ and run vm =
| _ -> (Hashtbl.find Sx_primitives.primitives "/") [a; b]) | _ -> (Hashtbl.find Sx_primitives.primitives "/") [a; b])
| 164 (* OP_EQ *) -> | 164 (* OP_EQ *) ->
let b = pop vm and a = pop vm in let b = pop vm and a = pop vm in
push vm ((Hashtbl.find Sx_primitives.primitives "=") [a; b]) let rec norm = function
| ListRef { contents = l } -> List (List.map norm l)
| List l -> List (List.map norm l) | v -> v in
push vm (Bool (norm a = norm b))
| 165 (* OP_LT *) -> | 165 (* OP_LT *) ->
let b = pop vm and a = pop vm in let b = pop vm and a = pop vm in
push vm (match a, b with push vm (match a, b with
@@ -769,10 +771,10 @@ and run vm =
| 168 (* OP_LEN *) -> | 168 (* OP_LEN *) ->
let v = pop vm in let v = pop vm in
push vm (match v with push vm (match v with
| List l | ListRef { contents = l } -> Integer (List.length l) | List l | ListRef { contents = l } -> Number (float_of_int (List.length l))
| String s -> Integer (String.length s) | String s -> Number (float_of_int (String.length s))
| Dict d -> Integer (Hashtbl.length d) | Dict d -> Number (float_of_int (Hashtbl.length d))
| Nil -> Integer 0 | Nil -> Number 0.0
| _ -> (Hashtbl.find Sx_primitives.primitives "len") [v]) | _ -> (Hashtbl.find Sx_primitives.primitives "len") [v])
| 169 (* OP_FIRST *) -> | 169 (* OP_FIRST *) ->
let v = pop vm in let v = pop vm in

View File

@@ -256,7 +256,6 @@
"callcc-continuation?" "callcc-continuation?"
"callcc-continuation-data" "callcc-continuation-data"
"make-callcc-continuation" "make-callcc-continuation"
"callcc-continuation-winders-len"
"dynamic-wind-call" "dynamic-wind-call"
"strip-prefix" "strip-prefix"
"component-set-param-types!" "component-set-param-types!"
@@ -296,8 +295,7 @@
"*bind-tracking*" "*bind-tracking*"
"*provide-batch-depth*" "*provide-batch-depth*"
"*provide-batch-queue*" "*provide-batch-queue*"
"*provide-subscribers*" "*provide-subscribers*"))
"*winders*"))
(define (define
ml-is-mutable-global? ml-is-mutable-global?
@@ -535,13 +533,13 @@
"; cf_env = " "; cf_env = "
(ef "env") (ef "env")
"; cf_name = " "; cf_name = "
(if (= frame-type "if") (ef "else") (cond (some (fn (k) (= k "body-result")) items) (ef "body-result") :else (ef "name"))) (if (= frame-type "if") (ef "else") (ef "name"))
"; cf_body = " "; cf_body = "
(if (= frame-type "if") (ef "then") (ef "body")) (if (= frame-type "if") (ef "then") (ef "body"))
"; cf_remaining = " "; cf_remaining = "
(ef "remaining") (ef "remaining")
"; cf_f = " "; cf_f = "
(cond (some (fn (k) (= k "after-thunk")) items) (ef "after-thunk") (some (fn (k) (= k "f")) items) (ef "f") :else "Nil") (ef "f")
"; cf_args = " "; cf_args = "
(cond (cond
(some (fn (k) (= k "evaled")) items) (some (fn (k) (= k "evaled")) items)
@@ -584,8 +582,6 @@
(ef "prev-tracking") (ef "prev-tracking")
(some (fn (k) (= k "extra")) items) (some (fn (k) (= k "extra")) items)
(ef "extra") (ef "extra")
(some (fn (k) (= k "winders-len")) items)
(ef "winders-len")
:else "Nil") :else "Nil")
"; cf_extra2 = " "; cf_extra2 = "
(cond (cond

View File

@@ -49,8 +49,6 @@ trap "rm -f $TMPFILE" EXIT
echo '(load "lib/js/transpile.sx")' echo '(load "lib/js/transpile.sx")'
echo '(epoch 5)' echo '(epoch 5)'
echo '(load "lib/js/runtime.sx")' echo '(load "lib/js/runtime.sx")'
echo '(epoch 6)'
echo '(load "lib/js/regex.sx")'
epoch=100 epoch=100
for f in "${FIXTURES[@]}"; do for f in "${FIXTURES[@]}"; do

View File

@@ -1,943 +0,0 @@
;; lib/js/regex.sx — pure-SX recursive backtracking regex engine
;;
;; Installed via (js-regex-platform-override! ...) at load time.
;; Covers: character classes (\d\w\s . [abc] [^abc] [a-z]),
;; anchors (^ $ \b \B), quantifiers (* + ? {n,m} lazy variants),
;; groups (capturing + non-capturing), alternation (a|b),
;; flags: i (case-insensitive), g (global), m (multiline).
;;
;; Architecture:
;; 1. rx-parse-pattern — pattern string → compiled node list
;; 2. rx-match-nodes — recursive backtracker
;; 3. rx-exec / rx-test — public interface
;; 4. Install as {:test rx-test :exec rx-exec}
;; ── Utilities ─────────────────────────────────────────────────────
(define
rx-char-at
(fn (s i) (if (and (>= i 0) (< i (len s))) (char-at s i) "")))
(define
rx-digit?
(fn
(c)
(and (not (= c "")) (>= (char-code c) 48) (<= (char-code c) 57))))
(define
rx-word?
(fn
(c)
(and
(not (= c ""))
(or
(and (>= (char-code c) 65) (<= (char-code c) 90))
(and (>= (char-code c) 97) (<= (char-code c) 122))
(and (>= (char-code c) 48) (<= (char-code c) 57))
(= c "_")))))
(define
rx-space?
(fn
(c)
(or (= c " ") (= c "\t") (= c "\n") (= c "\r") (= c "\\f") (= c ""))))
(define rx-newline? (fn (c) (or (= c "\n") (= c "\r"))))
(define
rx-downcase-char
(fn
(c)
(let
((cc (char-code c)))
(if (and (>= cc 65) (<= cc 90)) (char-from-code (+ cc 32)) c))))
(define
rx-char-eq?
(fn
(a b ci?)
(if ci? (= (rx-downcase-char a) (rx-downcase-char b)) (= a b))))
(define
rx-parse-int
(fn
(pat i acc)
(let
((c (rx-char-at pat i)))
(if
(rx-digit? c)
(rx-parse-int pat (+ i 1) (+ (* acc 10) (- (char-code c) 48)))
(list acc i)))))
(define
rx-hex-digit-val
(fn
(c)
(cond
((and (>= (char-code c) 48) (<= (char-code c) 57))
(- (char-code c) 48))
((and (>= (char-code c) 65) (<= (char-code c) 70))
(+ 10 (- (char-code c) 65)))
((and (>= (char-code c) 97) (<= (char-code c) 102))
(+ 10 (- (char-code c) 97)))
(else -1))))
(define
rx-parse-hex-n
(fn
(pat i n acc)
(if
(= n 0)
(list (char-from-code acc) i)
(let
((v (rx-hex-digit-val (rx-char-at pat i))))
(if
(< v 0)
(list (char-from-code acc) i)
(rx-parse-hex-n pat (+ i 1) (- n 1) (+ (* acc 16) v)))))))
;; ── Pattern compiler ──────────────────────────────────────────────
;; Node types (stored in dicts with "__t__" key):
;; literal : {:__t__ "literal" :__c__ char}
;; any : {:__t__ "any"}
;; class-d : {:__t__ "class-d" :__neg__ bool}
;; class-w : {:__t__ "class-w" :__neg__ bool}
;; class-s : {:__t__ "class-s" :__neg__ bool}
;; char-class: {:__t__ "char-class" :__neg__ bool :__items__ list}
;; anchor-start / anchor-end / anchor-word / anchor-nonword
;; quant : {:__t__ "quant" :__node__ n :__min__ m :__max__ mx :__lazy__ bool}
;; group : {:__t__ "group" :__idx__ i :__nodes__ list}
;; ncgroup : {:__t__ "ncgroup" :__nodes__ list}
;; alt : {:__t__ "alt" :__branches__ list-of-node-lists}
;; parse one escape after `\`, returns (node new-i)
(define
rx-parse-escape
(fn
(pat i)
(let
((c (rx-char-at pat i)))
(cond
((= c "d") (list (dict "__t__" "class-d" "__neg__" false) (+ i 1)))
((= c "D") (list (dict "__t__" "class-d" "__neg__" true) (+ i 1)))
((= c "w") (list (dict "__t__" "class-w" "__neg__" false) (+ i 1)))
((= c "W") (list (dict "__t__" "class-w" "__neg__" true) (+ i 1)))
((= c "s") (list (dict "__t__" "class-s" "__neg__" false) (+ i 1)))
((= c "S") (list (dict "__t__" "class-s" "__neg__" true) (+ i 1)))
((= c "b") (list (dict "__t__" "anchor-word") (+ i 1)))
((= c "B") (list (dict "__t__" "anchor-nonword") (+ i 1)))
((= c "n") (list (dict "__t__" "literal" "__c__" "\n") (+ i 1)))
((= c "r") (list (dict "__t__" "literal" "__c__" "\r") (+ i 1)))
((= c "t") (list (dict "__t__" "literal" "__c__" "\t") (+ i 1)))
((= c "f") (list (dict "__t__" "literal" "__c__" "\\f") (+ i 1)))
((= c "v") (list (dict "__t__" "literal" "__c__" "") (+ i 1)))
((= c "u")
(let
((res (rx-parse-hex-n pat (+ i 1) 4 0)))
(list (dict "__t__" "literal" "__c__" (nth res 0)) (nth res 1))))
((= c "x")
(let
((res (rx-parse-hex-n pat (+ i 1) 2 0)))
(list (dict "__t__" "literal" "__c__" (nth res 0)) (nth res 1))))
(else (list (dict "__t__" "literal" "__c__" c) (+ i 1)))))))
;; parse a char-class item inside [...], returns (item new-i)
(define
rx-parse-class-item
(fn
(pat i)
(let
((c (rx-char-at pat i)))
(cond
((= c "\\")
(let
((esc (rx-parse-escape pat (+ i 1))))
(let
((node (nth esc 0)) (ni (nth esc 1)))
(let
((t (get node "__t__")))
(cond
((= t "class-d")
(list
(dict "kind" "class-d" "neg" (get node "__neg__"))
ni))
((= t "class-w")
(list
(dict "kind" "class-w" "neg" (get node "__neg__"))
ni))
((= t "class-s")
(list
(dict "kind" "class-s" "neg" (get node "__neg__"))
ni))
(else
(let
((lc (get node "__c__")))
(if
(and
(= (rx-char-at pat ni) "-")
(not (= (rx-char-at pat (+ ni 1)) "]")))
(let
((hi-c (rx-char-at pat (+ ni 1))))
(list
(dict "kind" "range" "lo" lc "hi" hi-c)
(+ ni 2)))
(list (dict "kind" "lit" "c" lc) ni)))))))))
(else
(if
(and
(not (= c ""))
(= (rx-char-at pat (+ i 1)) "-")
(not (= (rx-char-at pat (+ i 2)) "]"))
(not (= (rx-char-at pat (+ i 2)) "")))
(let
((hi-c (rx-char-at pat (+ i 2))))
(list (dict "kind" "range" "lo" c "hi" hi-c) (+ i 3)))
(list (dict "kind" "lit" "c" c) (+ i 1))))))))
(define
rx-parse-class-items
(fn
(pat i items)
(let
((c (rx-char-at pat i)))
(if
(or (= c "]") (= c ""))
(list items i)
(let
((res (rx-parse-class-item pat i)))
(begin
(append! items (nth res 0))
(rx-parse-class-items pat (nth res 1) items)))))))
;; parse a sequence until stop-ch or EOF; returns (nodes new-i groups-count)
(define
rx-parse-seq
(fn
(pat i stop-ch ds)
(let
((c (rx-char-at pat i)))
(cond
((= c "") (list (get ds "nodes") i (get ds "groups")))
((= c stop-ch) (list (get ds "nodes") i (get ds "groups")))
((= c "|") (rx-parse-alt-rest pat i ds))
(else
(let
((res (rx-parse-atom pat i ds)))
(let
((node (nth res 0)) (ni (nth res 1)) (ds2 (nth res 2)))
(let
((qres (rx-parse-quant pat ni node)))
(begin
(append! (get ds2 "nodes") (nth qres 0))
(rx-parse-seq pat (nth qres 1) stop-ch ds2))))))))))
;; when we hit | inside a sequence, collect all alternatives
(define
rx-parse-alt-rest
(fn
(pat i ds)
(let
((left-branch (get ds "nodes")) (branches (list)))
(begin
(append! branches left-branch)
(rx-parse-alt-branches pat i (get ds "groups") branches)))))
(define
rx-parse-alt-branches
(fn
(pat i n-groups branches)
(let
((new-nodes (list)) (ds2 (dict "groups" n-groups "nodes" new-nodes)))
(let
((res (rx-parse-seq pat (+ i 1) "|" ds2)))
(begin
(append! branches (nth res 0))
(let
((ni2 (nth res 1)) (g2 (nth res 2)))
(if
(= (rx-char-at pat ni2) "|")
(rx-parse-alt-branches pat ni2 g2 branches)
(list
(list (dict "__t__" "alt" "__branches__" branches))
ni2
g2))))))))
;; parse quantifier suffix, returns (node new-i)
(define
rx-parse-quant
(fn
(pat i node)
(let
((c (rx-char-at pat i)))
(cond
((= c "*")
(let
((lazy? (= (rx-char-at pat (+ i 1)) "?")))
(list
(dict
"__t__"
"quant"
"__node__"
node
"__min__"
0
"__max__"
-1
"__lazy__"
lazy?)
(if lazy? (+ i 2) (+ i 1)))))
((= c "+")
(let
((lazy? (= (rx-char-at pat (+ i 1)) "?")))
(list
(dict
"__t__"
"quant"
"__node__"
node
"__min__"
1
"__max__"
-1
"__lazy__"
lazy?)
(if lazy? (+ i 2) (+ i 1)))))
((= c "?")
(let
((lazy? (= (rx-char-at pat (+ i 1)) "?")))
(list
(dict
"__t__"
"quant"
"__node__"
node
"__min__"
0
"__max__"
1
"__lazy__"
lazy?)
(if lazy? (+ i 2) (+ i 1)))))
((= c "{")
(let
((mres (rx-parse-int pat (+ i 1) 0)))
(let
((mn (nth mres 0)) (mi (nth mres 1)))
(let
((sep (rx-char-at pat mi)))
(cond
((= sep "}")
(let
((lazy? (= (rx-char-at pat (+ mi 1)) "?")))
(list
(dict
"__t__"
"quant"
"__node__"
node
"__min__"
mn
"__max__"
mn
"__lazy__"
lazy?)
(if lazy? (+ mi 2) (+ mi 1)))))
((= sep ",")
(let
((c2 (rx-char-at pat (+ mi 1))))
(if
(= c2 "}")
(let
((lazy? (= (rx-char-at pat (+ mi 2)) "?")))
(list
(dict
"__t__"
"quant"
"__node__"
node
"__min__"
mn
"__max__"
-1
"__lazy__"
lazy?)
(if lazy? (+ mi 3) (+ mi 2))))
(let
((mxres (rx-parse-int pat (+ mi 1) 0)))
(let
((mx (nth mxres 0)) (mxi (nth mxres 1)))
(let
((lazy? (= (rx-char-at pat (+ mxi 1)) "?")))
(list
(dict
"__t__"
"quant"
"__node__"
node
"__min__"
mn
"__max__"
mx
"__lazy__"
lazy?)
(if lazy? (+ mxi 2) (+ mxi 1)))))))))
(else (list node i)))))))
(else (list node i))))))
;; parse one atom, returns (node new-i new-ds)
(define
rx-parse-atom
(fn
(pat i ds)
(let
((c (rx-char-at pat i)))
(cond
((= c ".") (list (dict "__t__" "any") (+ i 1) ds))
((= c "^") (list (dict "__t__" "anchor-start") (+ i 1) ds))
((= c "$") (list (dict "__t__" "anchor-end") (+ i 1) ds))
((= c "\\")
(let
((esc (rx-parse-escape pat (+ i 1))))
(list (nth esc 0) (nth esc 1) ds)))
((= c "[")
(let
((neg? (= (rx-char-at pat (+ i 1)) "^")))
(let
((start (if neg? (+ i 2) (+ i 1))) (items (list)))
(let
((res (rx-parse-class-items pat start items)))
(let
((ci (nth res 1)))
(list
(dict
"__t__"
"char-class"
"__neg__"
neg?
"__items__"
items)
(+ ci 1)
ds))))))
((= c "(")
(let
((c2 (rx-char-at pat (+ i 1))))
(if
(and (= c2 "?") (= (rx-char-at pat (+ i 2)) ":"))
(let
((inner-nodes (list))
(inner-ds
(dict "groups" (get ds "groups") "nodes" inner-nodes)))
(let
((res (rx-parse-seq pat (+ i 3) ")" inner-ds)))
(list
(dict "__t__" "ncgroup" "__nodes__" (nth res 0))
(+ (nth res 1) 1)
(dict "groups" (nth res 2) "nodes" (get ds "nodes")))))
(let
((gidx (+ (get ds "groups") 1)) (inner-nodes (list)))
(let
((inner-ds (dict "groups" gidx "nodes" inner-nodes)))
(let
((res (rx-parse-seq pat (+ i 1) ")" inner-ds)))
(list
(dict
"__t__"
"group"
"__idx__"
gidx
"__nodes__"
(nth res 0))
(+ (nth res 1) 1)
(dict "groups" (nth res 2) "nodes" (get ds "nodes")))))))))
(else (list (dict "__t__" "literal" "__c__" c) (+ i 1) ds))))))
;; top-level compile
(define
rx-compile
(fn
(pattern)
(let
((nodes (list)) (ds (dict "groups" 0 "nodes" nodes)))
(let
((res (rx-parse-seq pattern 0 "" ds)))
(dict "nodes" (nth res 0) "ngroups" (nth res 2))))))
;; ── Matcher ───────────────────────────────────────────────────────
;; Match a char-class item against character c
(define
rx-item-matches?
(fn
(item c ci?)
(let
((kind (get item "kind")))
(cond
((= kind "lit") (rx-char-eq? c (get item "c") ci?))
((= kind "range")
(let
((lo (if ci? (rx-downcase-char (get item "lo")) (get item "lo")))
(hi
(if ci? (rx-downcase-char (get item "hi")) (get item "hi")))
(dc (if ci? (rx-downcase-char c) c)))
(and
(>= (char-code dc) (char-code lo))
(<= (char-code dc) (char-code hi)))))
((= kind "class-d")
(let ((m (rx-digit? c))) (if (get item "neg") (not m) m)))
((= kind "class-w")
(let ((m (rx-word? c))) (if (get item "neg") (not m) m)))
((= kind "class-s")
(let ((m (rx-space? c))) (if (get item "neg") (not m) m)))
(else false)))))
(define
rx-class-items-any?
(fn
(items c ci?)
(if
(empty? items)
false
(if
(rx-item-matches? (first items) c ci?)
true
(rx-class-items-any? (rest items) c ci?)))))
(define
rx-class-matches?
(fn
(node c ci?)
(let
((neg? (get node "__neg__")) (items (get node "__items__")))
(let
((hit (rx-class-items-any? items c ci?)))
(if neg? (not hit) hit)))))
;; Word boundary check
(define
rx-is-word-boundary?
(fn
(s i slen)
(let
((before (if (> i 0) (rx-word? (char-at s (- i 1))) false))
(after (if (< i slen) (rx-word? (char-at s i)) false)))
(not (= before after)))))
;; ── Core matcher ──────────────────────────────────────────────────
;;
;; rx-match-nodes : nodes s i slen ci? mi? groups → end-pos or -1
;;
;; Matches `nodes` starting at position `i` in string `s`.
;; Returns the position after the last character consumed, or -1 on failure.
;; Mutates `groups` dict to record captures.
(define
rx-match-nodes
(fn
(nodes s i slen ci? mi? groups)
(if
(empty? nodes)
i
(let
((node (first nodes)) (rest-nodes (rest nodes)))
(let
((t (get node "__t__")))
(cond
((= t "literal")
(if
(and
(< i slen)
(rx-char-eq? (char-at s i) (get node "__c__") ci?))
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
-1))
((= t "any")
(if
(and (< i slen) (not (rx-newline? (char-at s i))))
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
-1))
((= t "class-d")
(let
((m (and (< i slen) (rx-digit? (char-at s i)))))
(if
(if (get node "__neg__") (not m) m)
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
-1)))
((= t "class-w")
(let
((m (and (< i slen) (rx-word? (char-at s i)))))
(if
(if (get node "__neg__") (not m) m)
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
-1)))
((= t "class-s")
(let
((m (and (< i slen) (rx-space? (char-at s i)))))
(if
(if (get node "__neg__") (not m) m)
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
-1)))
((= t "char-class")
(if
(and (< i slen) (rx-class-matches? node (char-at s i) ci?))
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
-1))
((= t "anchor-start")
(if
(or
(= i 0)
(and mi? (rx-newline? (rx-char-at s (- i 1)))))
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
-1))
((= t "anchor-end")
(if
(or (= i slen) (and mi? (rx-newline? (rx-char-at s i))))
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
-1))
((= t "anchor-word")
(if
(rx-is-word-boundary? s i slen)
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
-1))
((= t "anchor-nonword")
(if
(not (rx-is-word-boundary? s i slen))
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
-1))
((= t "group")
(let
((gidx (get node "__idx__"))
(inner (get node "__nodes__")))
(let
((g-end (rx-match-nodes inner s i slen ci? mi? groups)))
(if
(>= g-end 0)
(begin
(dict-set!
groups
(js-to-string gidx)
(substring s i g-end))
(let
((final-end (rx-match-nodes rest-nodes s g-end slen ci? mi? groups)))
(if
(>= final-end 0)
final-end
(begin
(dict-set! groups (js-to-string gidx) nil)
-1))))
-1))))
((= t "ncgroup")
(let
((inner (get node "__nodes__")))
(rx-match-nodes
(append inner rest-nodes)
s
i
slen
ci?
mi?
groups)))
((= t "alt")
(let
((branches (get node "__branches__")))
(rx-try-branches branches rest-nodes s i slen ci? mi? groups)))
((= t "quant")
(let
((inner-node (get node "__node__"))
(mn (get node "__min__"))
(mx (get node "__max__"))
(lazy? (get node "__lazy__")))
(if
lazy?
(rx-quant-lazy
inner-node
mn
mx
rest-nodes
s
i
slen
ci?
mi?
groups
0)
(rx-quant-greedy
inner-node
mn
mx
rest-nodes
s
i
slen
ci?
mi?
groups
0))))
(else -1)))))))
(define
rx-try-branches
(fn
(branches rest-nodes s i slen ci? mi? groups)
(if
(empty? branches)
-1
(let
((res (rx-match-nodes (append (first branches) rest-nodes) s i slen ci? mi? groups)))
(if
(>= res 0)
res
(rx-try-branches (rest branches) rest-nodes s i slen ci? mi? groups))))))
;; Greedy: expand as far as possible, then try rest from the longest match
;; Strategy: recurse forward (extend first); only try rest when extension fails
(define
rx-quant-greedy
(fn
(inner-node mn mx rest-nodes s i slen ci? mi? groups count)
(let
((can-extend (and (< i slen) (or (= mx -1) (< count mx)))))
(if
can-extend
(let
((ni (rx-match-one inner-node s i slen ci? mi? groups)))
(if
(>= ni 0)
(let
((res (rx-quant-greedy inner-node mn mx rest-nodes s ni slen ci? mi? groups (+ count 1))))
(if
(>= res 0)
res
(if
(>= count mn)
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
-1)))
(if
(>= count mn)
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
-1)))
(if
(>= count mn)
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
-1)))))
;; Lazy: try rest first, extend only if rest fails
(define
rx-quant-lazy
(fn
(inner-node mn mx rest-nodes s i slen ci? mi? groups count)
(if
(>= count mn)
(let
((res (rx-match-nodes rest-nodes s i slen ci? mi? groups)))
(if
(>= res 0)
res
(if
(and (< i slen) (or (= mx -1) (< count mx)))
(let
((ni (rx-match-one inner-node s i slen ci? mi? groups)))
(if
(>= ni 0)
(rx-quant-lazy
inner-node
mn
mx
rest-nodes
s
ni
slen
ci?
mi?
groups
(+ count 1))
-1))
-1)))
(if
(< i slen)
(let
((ni (rx-match-one inner-node s i slen ci? mi? groups)))
(if
(>= ni 0)
(rx-quant-lazy
inner-node
mn
mx
rest-nodes
s
ni
slen
ci?
mi?
groups
(+ count 1))
-1))
-1))))
;; Match a single node at position i, return new pos or -1
(define
rx-match-one
(fn
(node s i slen ci? mi? groups)
(rx-match-nodes (list node) s i slen ci? mi? groups)))
;; ── Engine entry points ───────────────────────────────────────────
;; Try matching at exactly position i. Returns result dict or nil.
(define
rx-try-at
(fn
(compiled s i slen ci? mi?)
(let
((nodes (get compiled "nodes")) (ngroups (get compiled "ngroups")))
(let
((groups (dict)))
(let
((end (rx-match-nodes nodes s i slen ci? mi? groups)))
(if
(>= end 0)
(dict "start" i "end" end "groups" groups "ngroups" ngroups)
nil))))))
;; Find first match scanning from search-start.
(define
rx-find-from
(fn
(compiled s search-start slen ci? mi?)
(if
(> search-start slen)
nil
(let
((res (rx-try-at compiled s search-start slen ci? mi?)))
(if
res
res
(rx-find-from compiled s (+ search-start 1) slen ci? mi?))))))
;; Build exec result dict from raw match result
(define
rx-build-exec-result
(fn
(s match-res)
(let
((start (get match-res "start"))
(end (get match-res "end"))
(groups (get match-res "groups"))
(ngroups (get match-res "ngroups")))
(let
((matched (substring s start end))
(caps (rx-build-captures groups ngroups 1)))
(dict "match" matched "index" start "input" s "groups" caps)))))
(define
rx-build-captures
(fn
(groups ngroups idx)
(if
(> idx ngroups)
(list)
(let
((cap (get groups (js-to-string idx))))
(cons
(if (= cap nil) :js-undefined cap)
(rx-build-captures groups ngroups (+ idx 1)))))))
;; ── Public interface ──────────────────────────────────────────────
;; Lazy compile: build NFA on first use, cache under "__compiled__"
(define
rx-ensure-compiled!
(fn
(rx)
(if
(dict-has? rx "__compiled__")
(get rx "__compiled__")
(let
((c (rx-compile (get rx "source"))))
(begin (dict-set! rx "__compiled__" c) c)))))
(define
rx-test
(fn
(rx s)
(let
((compiled (rx-ensure-compiled! rx))
(ci? (get rx "ignoreCase"))
(mi? (get rx "multiline"))
(slen (len s)))
(let
((start (if (get rx "global") (let ((li (get rx "lastIndex"))) (if (number? li) li 0)) 0)))
(let
((res (rx-find-from compiled s start slen ci? mi?)))
(if
(get rx "global")
(begin
(dict-set! rx "lastIndex" (if res (get res "end") 0))
(if res true false))
(if res true false)))))))
(define
rx-exec
(fn
(rx s)
(let
((compiled (rx-ensure-compiled! rx))
(ci? (get rx "ignoreCase"))
(mi? (get rx "multiline"))
(slen (len s)))
(let
((start (if (get rx "global") (let ((li (get rx "lastIndex"))) (if (number? li) li 0)) 0)))
(let
((res (rx-find-from compiled s start slen ci? mi?)))
(if
res
(begin
(when
(get rx "global")
(dict-set! rx "lastIndex" (get res "end")))
(rx-build-exec-result s res))
(begin
(when (get rx "global") (dict-set! rx "lastIndex" 0))
nil)))))))
;; match-all for String.prototype.matchAll
(define
js-regex-match-all
(fn
(rx s)
(let
((compiled (rx-ensure-compiled! rx))
(ci? (get rx "ignoreCase"))
(mi? (get rx "multiline"))
(slen (len s))
(results (list)))
(rx-match-all-loop compiled s 0 slen ci? mi? results))))
(define
rx-match-all-loop
(fn
(compiled s i slen ci? mi? results)
(if
(> i slen)
results
(let
((res (rx-find-from compiled s i slen ci? mi?)))
(if
res
(begin
(append! results (rx-build-exec-result s res))
(let
((next (get res "end")))
(rx-match-all-loop
compiled
s
(if (= next i) (+ i 1) next)
slen
ci?
mi?
results)))
results)))))
;; ── Install platform ──────────────────────────────────────────────
(js-regex-platform-override! "test" rx-test)
(js-regex-platform-override! "exec" rx-exec)

View File

@@ -2032,15 +2032,7 @@
(&rest args) (&rest args)
(cond (cond
((= (len args) 0) nil) ((= (len args) 0) nil)
((js-regex? (nth args 0)) ((js-regex? (nth args 0)) (js-regex-stub-exec (nth args 0) s))
(let
((rx (nth args 0)))
(let
((impl (get __js_regex_platform__ "exec")))
(if
(js-undefined? impl)
(js-regex-stub-exec rx s)
(impl rx s)))))
(else (else
(let (let
((needle (js-to-string (nth args 0)))) ((needle (js-to-string (nth args 0))))
@@ -2049,7 +2041,7 @@
(if (if
(= idx -1) (= idx -1)
nil nil
(let ((res (list))) (begin (append! res needle) res))))))))) (let ((res (list))) (append! res needle) res))))))))
((= name "at") ((= name "at")
(fn (fn
(i) (i)
@@ -2107,20 +2099,6 @@
((= name "toWellFormed") (fn () s)) ((= name "toWellFormed") (fn () s))
(else js-undefined)))) (else js-undefined))))
(define __js_tdz_sentinel__ (dict "__tdz__" true))
(define js-tdz? (fn (v) (and (dict? v) (dict-has? v "__tdz__"))))
(define
js-tdz-check
(fn
(name val)
(if
(js-tdz? val)
(raise
(TypeError (str "Cannot access '" name "' before initialization")))
val)))
(define (define
js-string-slice js-string-slice
(fn (fn

View File

@@ -33,8 +33,6 @@ cat > "$TMPFILE" << 'EPOCHS'
(load "lib/js/transpile.sx") (load "lib/js/transpile.sx")
(epoch 5) (epoch 5)
(load "lib/js/runtime.sx") (load "lib/js/runtime.sx")
(epoch 6)
(load "lib/js/regex.sx")
;; ── Phase 0: stubs still behave ───────────────────────────────── ;; ── Phase 0: stubs still behave ─────────────────────────────────
(epoch 10) (epoch 10)
@@ -1325,108 +1323,6 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 3505) (epoch 3505)
(eval "(js-eval \"var a = {length: 3, 0: 10, 1: 20, 2: 30}; var sum = 0; Array.prototype.forEach.call(a, function(x){sum += x;}); sum\")") (eval "(js-eval \"var a = {length: 3, 0: 10, 1: 20, 2: 30}; var sum = 0; Array.prototype.forEach.call(a, function(x){sum += x;}); sum\")")
;; ── Phase 12: Regex engine ────────────────────────────────────────
;; Platform is installed (test key is a function, not undefined)
(epoch 5000)
(eval "(js-undefined? (get __js_regex_platform__ \"test\"))")
(epoch 5001)
(eval "(js-eval \"/foo/.test('hi foo bar')\")")
(epoch 5002)
(eval "(js-eval \"/foo/.test('hi bar')\")")
;; Case-insensitive flag
(epoch 5003)
(eval "(js-eval \"/FOO/i.test('hello foo world')\")")
;; Anchors
(epoch 5004)
(eval "(js-eval \"/^hello/.test('hello world')\")")
(epoch 5005)
(eval "(js-eval \"/^hello/.test('say hello')\")")
(epoch 5006)
(eval "(js-eval \"/world$/.test('hello world')\")")
;; Character classes
(epoch 5007)
(eval "(js-eval \"/\\\\d+/.test('abc 123')\")")
(epoch 5008)
(eval "(js-eval \"/\\\\w+/.test('hello')\")")
(epoch 5009)
(eval "(js-eval \"/[abc]/.test('dog')\")")
(epoch 5010)
(eval "(js-eval \"/[abc]/.test('cat')\")")
;; Quantifiers
(epoch 5011)
(eval "(js-eval \"/a*b/.test('b')\")")
(epoch 5012)
(eval "(js-eval \"/a+b/.test('b')\")")
(epoch 5013)
(eval "(js-eval \"/a{2,3}/.test('aa')\")")
(epoch 5014)
(eval "(js-eval \"/a{2,3}/.test('a')\")")
;; Dot
(epoch 5015)
(eval "(js-eval \"/h.llo/.test('hello')\")")
(epoch 5016)
(eval "(js-eval \"/h.llo/.test('hllo')\")")
;; exec result
(epoch 5017)
(eval "(js-eval \"var m = /foo(\\\\w+)/.exec('foobar'); m.match\")")
(epoch 5018)
(eval "(js-eval \"var m = /foo(\\\\w+)/.exec('foobar'); m.index\")")
(epoch 5019)
(eval "(js-eval \"var m = /foo(\\\\w+)/.exec('foobar'); m.groups[0]\")")
;; Alternation
(epoch 5020)
(eval "(js-eval \"/cat|dog/.test('I have a dog')\")")
(epoch 5021)
(eval "(js-eval \"/cat|dog/.test('I have a fish')\")")
;; Non-capturing group
(epoch 5022)
(eval "(js-eval \"/(?:foo)+/.test('foofoo')\")")
;; Negated char class
(epoch 5023)
(eval "(js-eval \"/[^abc]/.test('d')\")")
(epoch 5024)
(eval "(js-eval \"/[^abc]/.test('a')\")")
;; Range inside char class
(epoch 5025)
(eval "(js-eval \"/[a-z]+/.test('hello')\")")
;; Word boundary
(epoch 5026)
(eval "(js-eval \"/\\\\bword\\\\b/.test('a word here')\")")
(epoch 5027)
(eval "(js-eval \"/\\\\bword\\\\b/.test('password')\")")
;; Lazy quantifier
(epoch 5028)
(eval "(js-eval \"var m = /a+?/.exec('aaa'); m.match\")")
;; Global flag exec
(epoch 5029)
(eval "(js-eval \"var r=/\\\\d+/g; r.exec('a1b2'); r.exec('a1b2').match\")")
;; String.prototype.match with regex
(epoch 5030)
(eval "(js-eval \"'hello world'.match(/\\\\w+/).match\")")
;; String.prototype.search
(epoch 5031)
(eval "(js-eval \"'hello world'.search(/world/)\")")
;; String.prototype.replace with regex
(epoch 5032)
(eval "(js-eval \"'hello world'.replace(/world/, 'there')\")")
;; multiline anchor
(epoch 5033)
(eval "(js-eval \"/^bar/m.test('foo\\nbar')\")")
;; ── Phase 13: let/const TDZ infrastructure ───────────────────────
;; The TDZ sentinel and checker are defined in runtime.sx.
;; let/const bindings work normally after initialization.
(epoch 5100)
(eval "(js-eval \"let x = 5; x\")")
(epoch 5101)
(eval "(js-eval \"const y = 42; y\")")
;; TDZ sentinel exists and is detectable
(epoch 5102)
(eval "(js-tdz? __js_tdz_sentinel__)")
;; js-tdz-check passes through non-sentinel values
(epoch 5103)
(eval "(js-tdz-check \"x\" 42)")
EPOCHS EPOCHS
@@ -2146,48 +2042,6 @@ check 3503 "indexOf.call arrLike" '1'
check 3504 "filter.call arrLike" '"2,3"' check 3504 "filter.call arrLike" '"2,3"'
check 3505 "forEach.call arrLike sum" '60' check 3505 "forEach.call arrLike sum" '60'
# ── Phase 12: Regex engine ────────────────────────────────────────
check 5000 "regex platform installed" 'false'
check 5001 "/foo/ matches" 'true'
check 5002 "/foo/ no match" 'false'
check 5003 "/FOO/i case-insensitive" 'true'
check 5004 "/^hello/ anchor match" 'true'
check 5005 "/^hello/ anchor no-match" 'false'
check 5006 "/world$/ end anchor" 'true'
check 5007 "/\\d+/ digit class" 'true'
check 5008 "/\\w+/ word class" 'true'
check 5009 "/[abc]/ class no-match" 'false'
check 5010 "/[abc]/ class match" 'true'
check 5011 "/a*b/ zero-or-more" 'true'
check 5012 "/a+b/ one-or-more no-match" 'false'
check 5013 "/a{2,3}/ quant match" 'true'
check 5014 "/a{2,3}/ quant no-match" 'false'
check 5015 "dot matches any" 'true'
check 5016 "dot requires char" 'false'
check 5017 "exec match string" '"foobar"'
check 5018 "exec match index" '0'
check 5019 "exec capture group" '"bar"'
check 5020 "alternation cat|dog match" 'true'
check 5021 "alternation cat|dog no-match" 'false'
check 5022 "non-capturing group" 'true'
check 5023 "negated class match" 'true'
check 5024 "negated class no-match" 'false'
check 5025 "range [a-z]+" 'true'
check 5026 "word boundary match" 'true'
check 5027 "word boundary no-match" 'false'
check 5028 "lazy quantifier" '"a"'
check 5029 "global exec advances" '"2"'
check 5030 "String.match regex" '"hello"'
check 5031 "String.search regex" '6'
check 5032 "String.replace regex" '"hello there"'
check 5033 "multiline anchor" 'true'
# ── Phase 13: let/const TDZ infrastructure ───────────────────────
check 5100 "let binding initialized" '5'
check 5101 "const binding initialized" '42'
check 5102 "TDZ sentinel is detectable" 'true'
check 5103 "tdz-check passes non-sentinel" '42'
TOTAL=$((PASS + FAIL)) TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then if [ $FAIL -eq 0 ]; then
echo "$PASS/$TOTAL JS-on-SX tests passed" echo "$PASS/$TOTAL JS-on-SX tests passed"

View File

@@ -798,7 +798,6 @@ class ServerSession:
self._run_and_collect(3, '(load "lib/js/parser.sx")', timeout=60.0) self._run_and_collect(3, '(load "lib/js/parser.sx")', timeout=60.0)
self._run_and_collect(4, '(load "lib/js/transpile.sx")', timeout=60.0) self._run_and_collect(4, '(load "lib/js/transpile.sx")', timeout=60.0)
self._run_and_collect(5, '(load "lib/js/runtime.sx")', timeout=60.0) self._run_and_collect(5, '(load "lib/js/runtime.sx")', timeout=60.0)
self._run_and_collect(50, '(load "lib/js/regex.sx")', timeout=60.0)
# Preload the stub harness — use precomputed SX cache when available # Preload the stub harness — use precomputed SX cache when available
# (huge win: ~15s js-eval HARNESS_STUB → ~0s load precomputed .sx). # (huge win: ~15s js-eval HARNESS_STUB → ~0s load precomputed .sx).
cache_rel = _harness_cache_rel_path() cache_rel = _harness_cache_rel_path()

View File

@@ -935,12 +935,12 @@
(define (define
js-transpile-var js-transpile-var
(fn (kind decls) (cons (js-sym "begin") (js-vardecl-forms kind decls)))) (fn (kind decls) (cons (js-sym "begin") (js-vardecl-forms decls))))
(define (define
js-vardecl-forms js-vardecl-forms
(fn (fn
(kind decls) (decls)
(cond (cond
((empty? decls) (list)) ((empty? decls) (list))
(else (else
@@ -953,7 +953,7 @@
(js-sym "define") (js-sym "define")
(js-sym (nth d 1)) (js-sym (nth d 1))
(js-transpile (nth d 2))) (js-transpile (nth d 2)))
(js-vardecl-forms kind (rest decls)))) (js-vardecl-forms (rest decls))))
((js-tag? d "js-vardecl-obj") ((js-tag? d "js-vardecl-obj")
(let (let
((names (nth d 1)) ((names (nth d 1))
@@ -964,7 +964,7 @@
(js-vardecl-obj-forms (js-vardecl-obj-forms
names names
tmp-sym tmp-sym
(js-vardecl-forms kind (rest decls)))))) (js-vardecl-forms (rest decls))))))
((js-tag? d "js-vardecl-arr") ((js-tag? d "js-vardecl-arr")
(let (let
((names (nth d 1)) ((names (nth d 1))
@@ -976,7 +976,7 @@
names names
tmp-sym tmp-sym
0 0
(js-vardecl-forms kind (rest decls)))))) (js-vardecl-forms (rest decls))))))
(else (error "js-vardecl-forms: unexpected decl")))))))) (else (error "js-vardecl-forms: unexpected decl"))))))))
(define (define

176
lib/prolog/compiler.sx Normal file
View File

@@ -0,0 +1,176 @@
;; lib/prolog/compiler.sx — clause compiler: parse-AST clauses → SX closures
;;
;; Each compiled clause is a lambda (fn (goal trail db cut-box k) bool)
;; that creates fresh vars, builds the instantiated head/body, and calls
;; pl-unify! + pl-solve! directly — no AST walk at solve time.
;;
;; Usage:
;; (pl-db-load! db (pl-parse src))
;; (pl-compile-db! db)
;; ; pl-solve-user! in runtime.sx automatically prefers compiled clauses
;; (pl-solve-once! db goal trail)
;; Collect unique variable names from a parse-AST clause into a dict.
(define
pl-cmp-vars-into!
(fn
(ast seen)
(cond
((not (list? ast)) nil)
((empty? ast) nil)
((= (first ast) "var")
(let
((name (nth ast 1)))
(when
(and (not (= name "_")) (not (dict-has? seen name)))
(dict-set! seen name true))))
((= (first ast) "compound")
(for-each (fn (a) (pl-cmp-vars-into! a seen)) (nth ast 2)))
((= (first ast) "clause")
(begin
(pl-cmp-vars-into! (nth ast 1) seen)
(pl-cmp-vars-into! (nth ast 2) seen))))))
;; Return list of unique var names in a clause (head + body, excluding _).
(define
pl-cmp-collect-vars
(fn
(clause)
(let ((seen {})) (pl-cmp-vars-into! clause seen) (keys seen))))
;; Create a fresh runtime var for each name in the list; return name->var dict.
(define
pl-cmp-make-var-map
(fn
(var-names)
(let
((m {}))
(for-each
(fn (name) (dict-set! m name (pl-mk-rt-var name)))
var-names)
m)))
;; Instantiate a parse-AST term using a pre-built var-map.
;; ("var" "_") always gets a fresh anonymous var.
(define
pl-cmp-build-term
(fn
(ast var-map)
(cond
((pl-var? ast) ast)
((not (list? ast)) ast)
((empty? ast) ast)
((= (first ast) "var")
(let
((name (nth ast 1)))
(if (= name "_") (pl-mk-rt-var "_") (dict-get var-map name))))
((or (= (first ast) "atom") (= (first ast) "num") (= (first ast) "str"))
ast)
((= (first ast) "compound")
(list
"compound"
(nth ast 1)
(map (fn (a) (pl-cmp-build-term a var-map)) (nth ast 2))))
((= (first ast) "clause")
(list
"clause"
(pl-cmp-build-term (nth ast 1) var-map)
(pl-cmp-build-term (nth ast 2) var-map)))
(true ast))))
;; Compile one parse-AST clause to a lambda.
;; Pre-computes var names at compile time; creates fresh vars per call.
(define
pl-compile-clause
(fn
(clause)
(let
((var-names (pl-cmp-collect-vars clause))
(head-ast (nth clause 1))
(body-ast (nth clause 2)))
(fn
(goal trail db cut-box k)
(let
((var-map (pl-cmp-make-var-map var-names)))
(let
((fresh-head (pl-cmp-build-term head-ast var-map))
(fresh-body (pl-cmp-build-term body-ast var-map)))
(let
((mark (pl-trail-mark trail)))
(if
(pl-unify! goal fresh-head trail)
(let
((r (pl-solve! db fresh-body trail cut-box k)))
(if r true (begin (pl-trail-undo-to! trail mark) false)))
(begin (pl-trail-undo-to! trail mark) false)))))))))
;; Try a list of compiled clause lambdas — same cut semantics as pl-try-clauses!.
(define
pl-try-compiled-clauses!
(fn
(db
goal
trail
compiled-clauses
outer-cut-box
outer-was-cut
inner-cut-box
k)
(cond
((empty? compiled-clauses) false)
(true
(let
((r ((first compiled-clauses) goal trail db inner-cut-box k)))
(cond
(r true)
((dict-get inner-cut-box :cut) false)
((and (not outer-was-cut) (dict-get outer-cut-box :cut)) false)
(true
(pl-try-compiled-clauses!
db
goal
trail
(rest compiled-clauses)
outer-cut-box
outer-was-cut
inner-cut-box
k))))))))
;; Compile all clauses in DB and store in :compiled table.
;; After this call, pl-solve-user! will dispatch via compiled lambdas.
;; Note: clauses assert!-ed after this call are not compiled.
(define
pl-compile-db!
(fn
(db)
(let
((src-table (dict-get db :clauses)) (compiled-table {}))
(for-each
(fn
(key)
(dict-set!
compiled-table
key
(map pl-compile-clause (dict-get src-table key))))
(keys src-table))
(dict-set! db :compiled compiled-table)
db)))
;; Cross-validate: load src into both a plain and a compiled DB,
;; run goal-str through each, return true iff solution counts match.
;; Use this to keep the interpreter as the reference implementation.
(define
pl-compiled-matches-interp?
(fn
(src goal-str)
(let
((db-interp (pl-mk-db)) (db-comp (pl-mk-db)))
(pl-db-load! db-interp (pl-parse src))
(pl-db-load! db-comp (pl-parse src))
(pl-compile-db! db-comp)
(let
((gi (pl-instantiate (pl-parse-goal goal-str) {}))
(gc (pl-instantiate (pl-parse-goal goal-str) {})))
(=
(pl-solve-count! db-interp gi (pl-mk-trail))
(pl-solve-count! db-comp gc (pl-mk-trail)))))))

126
lib/prolog/conformance.sh Executable file
View File

@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# Run every Prolog test suite via sx_server and refresh scoreboard.{json,md}.
# Exit 0 if all green, 1 if any failures.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
SX="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}"
if [[ ! -x "$SX" ]]; then
echo "sx_server not found at $SX (set SX_SERVER env to override)" >&2
exit 2
fi
cd "$ROOT"
# name : test-file : runner-fn
SUITES=(
"parse:lib/prolog/tests/parse.sx:pl-parse-tests-run!"
"unify:lib/prolog/tests/unify.sx:pl-unify-tests-run!"
"clausedb:lib/prolog/tests/clausedb.sx:pl-clausedb-tests-run!"
"solve:lib/prolog/tests/solve.sx:pl-solve-tests-run!"
"operators:lib/prolog/tests/operators.sx:pl-operators-tests-run!"
"dynamic:lib/prolog/tests/dynamic.sx:pl-dynamic-tests-run!"
"findall:lib/prolog/tests/findall.sx:pl-findall-tests-run!"
"term_inspect:lib/prolog/tests/term_inspect.sx:pl-term-inspect-tests-run!"
"append:lib/prolog/tests/programs/append.sx:pl-append-tests-run!"
"reverse:lib/prolog/tests/programs/reverse.sx:pl-reverse-tests-run!"
"member:lib/prolog/tests/programs/member.sx:pl-member-tests-run!"
"nqueens:lib/prolog/tests/programs/nqueens.sx:pl-nqueens-tests-run!"
"family:lib/prolog/tests/programs/family.sx:pl-family-tests-run!"
"atoms:lib/prolog/tests/atoms.sx:pl-atom-tests-run!"
"query_api:lib/prolog/tests/query_api.sx:pl-query-api-tests-run!"
"iso_predicates:lib/prolog/tests/iso_predicates.sx:pl-iso-predicates-tests-run!"
"meta_predicates:lib/prolog/tests/meta_predicates.sx:pl-meta-predicates-tests-run!"
"list_predicates:lib/prolog/tests/list_predicates.sx:pl-list-predicates-tests-run!"
"meta_call:lib/prolog/tests/meta_call.sx:pl-meta-call-tests-run!"
"set_predicates:lib/prolog/tests/set_predicates.sx:pl-set-predicates-tests-run!"
"char_predicates:lib/prolog/tests/char_predicates.sx:pl-char-predicates-tests-run!"
"io_predicates:lib/prolog/tests/io_predicates.sx:pl-io-predicates-tests-run!"
"assert_rules:lib/prolog/tests/assert_rules.sx:pl-assert-rules-tests-run!"
"string_agg:lib/prolog/tests/string_agg.sx:pl-string-agg-tests-run!"
"advanced:lib/prolog/tests/advanced.sx:pl-advanced-tests-run!"
"compiler:lib/prolog/tests/compiler.sx:pl-compiler-tests-run!"
"cross_validate:lib/prolog/tests/cross_validate.sx:pl-cross-validate-tests-run!"
)
SCRIPT='(epoch 1)
(load "lib/prolog/tokenizer.sx")
(load "lib/prolog/parser.sx")
(load "lib/prolog/runtime.sx")
(load "lib/prolog/query.sx")
(load "lib/prolog/compiler.sx")'
for entry in "${SUITES[@]}"; do
IFS=: read -r _ file _ <<< "$entry"
SCRIPT+=$'\n(load "'"$file"$'")'
done
for entry in "${SUITES[@]}"; do
IFS=: read -r _ _ fn <<< "$entry"
SCRIPT+=$'\n(eval "('"$fn"$')")'
done
OUTPUT="$(printf '%s\n' "$SCRIPT" | "$SX" 2>&1)"
mapfile -t LINES < <(printf '%s\n' "$OUTPUT" | grep -E '^\{:failed')
if [[ ${#LINES[@]} -ne ${#SUITES[@]} ]]; then
echo "Expected ${#SUITES[@]} suite results, got ${#LINES[@]}" >&2
echo "---- raw output ----" >&2
printf '%s\n' "$OUTPUT" >&2
exit 3
fi
TOTAL_PASS=0
TOTAL_FAIL=0
TOTAL=0
JSON_SUITES=""
MD_ROWS=""
for i in "${!SUITES[@]}"; do
IFS=: read -r name _ _ <<< "${SUITES[$i]}"
line="${LINES[$i]}"
passed=$(grep -oE ':passed [0-9]+' <<< "$line" | grep -oE '[0-9]+')
total=$(grep -oE ':total [0-9]+' <<< "$line" | grep -oE '[0-9]+')
failed=$(grep -oE ':failed [0-9]+' <<< "$line" | grep -oE '[0-9]+')
TOTAL_PASS=$((TOTAL_PASS + passed))
TOTAL_FAIL=$((TOTAL_FAIL + failed))
TOTAL=$((TOTAL + total))
status="ok"
[[ "$failed" -gt 0 ]] && status="FAIL"
[[ -n "$JSON_SUITES" ]] && JSON_SUITES+=","
JSON_SUITES+="\"$name\":{\"passed\":$passed,\"total\":$total,\"failed\":$failed}"
MD_ROWS+="| $name | $passed | $total | $status |"$'\n'
done
WHEN="$(date -Iseconds 2>/dev/null || date)"
cat > "$HERE/scoreboard.json" <<JSON
{
"total_passed": $TOTAL_PASS,
"total_failed": $TOTAL_FAIL,
"total": $TOTAL,
"suites": {$JSON_SUITES},
"generated": "$WHEN"
}
JSON
cat > "$HERE/scoreboard.md" <<MD
# Prolog scoreboard
**$TOTAL_PASS / $TOTAL passing** ($TOTAL_FAIL failure(s)).
Generated $WHEN.
| Suite | Passed | Total | Status |
|-------|--------|-------|--------|
$MD_ROWS
Run \`bash lib/prolog/conformance.sh\` to refresh. Override the binary
with \`SX_SERVER=path/to/sx_server.exe bash …\`.
MD
if [[ "$TOTAL_FAIL" -gt 0 ]]; then
echo "$TOTAL_FAIL failure(s) across $TOTAL tests" >&2
exit 1
fi
echo "All $TOTAL tests pass."

View File

@@ -1,28 +1,20 @@
;; lib/prolog/parser.sx — tokens → Prolog AST ;; lib/prolog/parser.sx — tokens → Prolog AST
;; ;;
;; Phase 1 grammar (NO operator table yet): ;; Phase 4 grammar (with operator table):
;; Program := Clause* EOF ;; Program := Clause* EOF
;; Clause := Term "." | Term ":-" Term "." ;; Clause := Term[999] "." | Term[999] ":-" Term[1200] "."
;; Term := Atom | Var | Number | String | Compound | List ;; Term[Pmax] uses precedence climbing on the operator table:
;; Compound := atom "(" ArgList ")" ;; primary = Atom | Var | Number | String | Compound | List | "(" Term[1200] ")"
;; ArgList := Term ("," Term)* ;; while next token is infix op `op` with prec(op) ≤ Pmax:
;; List := "[" "]" | "[" Term ("," Term)* ("|" Term)? "]" ;; consume op; parse rhs at right-prec(op); fold into compound(op-name,[lhs,rhs])
;; ;;
;; Term AST shapes (all tagged lists for uniform dispatch): ;; Op type → right-prec for op at precedence P:
;; ("atom" name) — atom ;; xfx → P-1 strict-both
;; ("var" name) — variable template (parser-time only) ;; xfy → P right-associative
;; ("num" value) — integer or float ;; yfx → P-1 left-associative
;; ("str" value) — string literal
;; ("compound" functor args) — compound term, args is list of term-ASTs
;; ("cut") — the cut atom !
;; ;;
;; A clause is (list "clause" head body). A fact is head with body = ("atom" "true"). ;; AST shapes are unchanged — operators just become compound terms.
;;
;; The empty list is (atom "[]"). Cons is compound "." with two args:
;; [1, 2, 3] → .(1, .(2, .(3, [])))
;; [H|T] → .(H, T)
;; ── Parser state helpers ────────────────────────────────────────────
(define (define
pp-peek pp-peek
(fn (fn
@@ -66,7 +58,6 @@
(if (= (get t :value) nil) "" (get t :value)) (if (= (get t :value) nil) "" (get t :value))
"'")))))) "'"))))))
;; ── AST constructors ────────────────────────────────────────────────
(define pl-mk-atom (fn (name) (list "atom" name))) (define pl-mk-atom (fn (name) (list "atom" name)))
(define pl-mk-var (fn (name) (list "var" name))) (define pl-mk-var (fn (name) (list "var" name)))
(define pl-mk-num (fn (n) (list "num" n))) (define pl-mk-num (fn (n) (list "num" n)))
@@ -74,18 +65,14 @@
(define pl-mk-compound (fn (f args) (list "compound" f args))) (define pl-mk-compound (fn (f args) (list "compound" f args)))
(define pl-mk-cut (fn () (list "cut"))) (define pl-mk-cut (fn () (list "cut")))
;; Term tag extractors
(define pl-term-tag (fn (t) (if (list? t) (first t) nil))) (define pl-term-tag (fn (t) (if (list? t) (first t) nil)))
(define pl-term-val (fn (t) (nth t 1))) (define pl-term-val (fn (t) (nth t 1)))
(define pl-compound-functor (fn (t) (nth t 1))) (define pl-compound-functor (fn (t) (nth t 1)))
(define pl-compound-args (fn (t) (nth t 2))) (define pl-compound-args (fn (t) (nth t 2)))
;; Empty-list atom and cons helpers
(define pl-nil-term (fn () (pl-mk-atom "[]"))) (define pl-nil-term (fn () (pl-mk-atom "[]")))
(define pl-mk-cons (fn (h t) (pl-mk-compound "." (list h t)))) (define pl-mk-cons (fn (h t) (pl-mk-compound "." (list h t))))
;; Build cons list from a list of terms + optional tail
(define (define
pl-mk-list-term pl-mk-list-term
(fn (fn
@@ -95,9 +82,61 @@
tail tail
(pl-mk-cons (first items) (pl-mk-list-term (rest items) tail))))) (pl-mk-cons (first items) (pl-mk-list-term (rest items) tail)))))
;; ── Term parser ───────────────────────────────────────────────────── ;; ── Operator table (Phase 4) ──────────────────────────────────────
;; Each entry: (name precedence type). Type ∈ "xfx" "xfy" "yfx".
(define (define
pp-parse-term pl-op-table
(list
(list "," 1000 "xfy")
(list ";" 1100 "xfy")
(list "->" 1050 "xfy")
(list "=" 700 "xfx")
(list "\\=" 700 "xfx")
(list "is" 700 "xfx")
(list "<" 700 "xfx")
(list ">" 700 "xfx")
(list "=<" 700 "xfx")
(list ">=" 700 "xfx")
(list "+" 500 "yfx")
(list "-" 500 "yfx")
(list "*" 400 "yfx")
(list "/" 400 "yfx")
(list ":-" 1200 "xfx")
(list "mod" 400 "yfx")))
(define
pl-op-find
(fn
(name table)
(cond
((empty? table) nil)
((= (first (first table)) name) (rest (first table)))
(true (pl-op-find name (rest table))))))
(define pl-op-lookup (fn (name) (pl-op-find name pl-op-table)))
;; Token → (name prec type) for known infix ops, else nil.
(define
pl-token-op
(fn
(t)
(let
((ty (get t :type)) (vv (get t :value)))
(cond
((and (= ty "punct") (= vv ","))
(let
((info (pl-op-lookup ",")))
(if (nil? info) nil (cons "," info))))
((or (= ty "atom") (= ty "op"))
(let
((info (pl-op-lookup vv)))
(if (nil? info) nil (cons vv info))))
(true nil)))))
;; ── Term parser ─────────────────────────────────────────────────────
;; Primary term: atom, var, num, str, compound (atom + paren), list, cut, parens.
(define
pp-parse-primary
(fn (fn
(st) (st)
(let (let
@@ -111,6 +150,12 @@
((and (= ty "op") (= vv "!")) ((and (= ty "op") (= vv "!"))
(do (pp-advance! st) (pl-mk-cut))) (do (pp-advance! st) (pl-mk-cut)))
((and (= ty "punct") (= vv "[")) (pp-parse-list st)) ((and (= ty "punct") (= vv "[")) (pp-parse-list st))
((and (= ty "punct") (= vv "("))
(do
(pp-advance! st)
(let
((inner (pp-parse-term-prec st 1200)))
(do (pp-expect! st "punct" ")") inner))))
((= ty "atom") ((= ty "atom")
(do (do
(pp-advance! st) (pp-advance! st)
@@ -133,13 +178,51 @@
(if (= vv nil) "" vv) (if (= vv nil) "" vv)
"'")))))))) "'"))))))))
;; Parse one or more comma-separated terms (arguments). ;; Operator-aware term parser: precedence climbing.
(define
pp-parse-term-prec
(fn
(st max-prec)
(let ((left (pp-parse-primary st))) (pp-parse-op-rhs st left max-prec))))
(define
pp-parse-op-rhs
(fn
(st left max-prec)
(let
((op-info (pl-token-op (pp-peek st))))
(cond
((nil? op-info) left)
(true
(let
((name (first op-info))
(prec (nth op-info 1))
(ty (nth op-info 2)))
(cond
((> prec max-prec) left)
(true
(let
((right-prec (if (= ty "xfy") prec (- prec 1))))
(do
(pp-advance! st)
(let
((right (pp-parse-term-prec st right-prec)))
(pp-parse-op-rhs
st
(pl-mk-compound name (list left right))
max-prec))))))))))))
;; Backwards-compat alias.
(define pp-parse-term (fn (st) (pp-parse-term-prec st 999)))
;; Args inside parens: parse at prec 999 so comma-as-operator (1000)
;; is not consumed; the explicit comma loop handles separation.
(define (define
pp-parse-arg-list pp-parse-arg-list
(fn (fn
(st) (st)
(let (let
((first-arg (pp-parse-term st)) (args (list))) ((first-arg (pp-parse-term-prec st 999)) (args (list)))
(do (do
(append! args first-arg) (append! args first-arg)
(define (define
@@ -150,12 +233,12 @@
(pp-at? st "punct" ",") (pp-at? st "punct" ",")
(do (do
(pp-advance! st) (pp-advance! st)
(append! args (pp-parse-term st)) (append! args (pp-parse-term-prec st 999))
(loop))))) (loop)))))
(loop) (loop)
args)))) args))))
;; Parse a [ ... ] list literal. Consumes the "[". ;; List literal.
(define (define
pp-parse-list pp-parse-list
(fn (fn
@@ -168,7 +251,7 @@
(let (let
((items (list))) ((items (list)))
(do (do
(append! items (pp-parse-term st)) (append! items (pp-parse-term-prec st 999))
(define (define
comma-loop comma-loop
(fn (fn
@@ -177,52 +260,17 @@
(pp-at? st "punct" ",") (pp-at? st "punct" ",")
(do (do
(pp-advance! st) (pp-advance! st)
(append! items (pp-parse-term st)) (append! items (pp-parse-term-prec st 999))
(comma-loop))))) (comma-loop)))))
(comma-loop) (comma-loop)
(let (let
((tail (if (pp-at? st "punct" "|") (do (pp-advance! st) (pp-parse-term st)) (pl-nil-term)))) ((tail (if (pp-at? st "punct" "|") (do (pp-advance! st) (pp-parse-term-prec st 999)) (pl-nil-term))))
(do (pp-expect! st "punct" "]") (pl-mk-list-term items tail))))))))) (do (pp-expect! st "punct" "]") (pl-mk-list-term items tail)))))))))
;; ── Body parsing ──────────────────────────────────────────────────── ;; ── Body parsing ────────────────────────────────────────────────────
;; A clause body is a comma-separated list of goals. We flatten into a ;; A body is a single term parsed at prec 1200 — operator parser folds
;; right-associative `,` compound: (A, B, C) → ','(A, ','(B, C)) ;; `,`, `;`, `->` automatically into right-associative compounds.
;; If only one goal, it's that goal directly. (define pp-parse-body (fn (st) (pp-parse-term-prec st 1200)))
(define
pp-parse-body
(fn
(st)
(let
((first-goal (pp-parse-term st)) (rest-goals (list)))
(do
(define
gloop
(fn
()
(when
(pp-at? st "punct" ",")
(do
(pp-advance! st)
(append! rest-goals (pp-parse-term st))
(gloop)))))
(gloop)
(if
(= (len rest-goals) 0)
first-goal
(pp-build-conj first-goal rest-goals))))))
(define
pp-build-conj
(fn
(first-goal rest-goals)
(if
(= (len rest-goals) 0)
first-goal
(pl-mk-compound
","
(list
first-goal
(pp-build-conj (first rest-goals) (rest rest-goals)))))))
;; ── Clause parsing ────────────────────────────────────────────────── ;; ── Clause parsing ──────────────────────────────────────────────────
(define (define
@@ -230,12 +278,11 @@
(fn (fn
(st) (st)
(let (let
((head (pp-parse-term st))) ((head (pp-parse-term-prec st 999)))
(let (let
((body (if (pp-at? st "op" ":-") (do (pp-advance! st) (pp-parse-body st)) (pl-mk-atom "true")))) ((body (if (pp-at? st "op" ":-") (do (pp-advance! st) (pp-parse-body st)) (pl-mk-atom "true"))))
(do (pp-expect! st "punct" ".") (list "clause" head body)))))) (do (pp-expect! st "punct" ".") (list "clause" head body))))))
;; Parse an entire program — returns list of clauses.
(define (define
pl-parse-program pl-parse-program
(fn (fn
@@ -253,13 +300,9 @@
(ploop) (ploop)
clauses)))) clauses))))
;; Parse a single query term (no trailing "."). Returns the term.
(define (define
pl-parse-query pl-parse-query
(fn (tokens) (let ((st {:idx 0 :tokens tokens})) (pp-parse-body st)))) (fn (tokens) (let ((st {:idx 0 :tokens tokens})) (pp-parse-body st))))
;; Convenience: source → clauses
(define pl-parse (fn (src) (pl-parse-program (pl-tokenize src)))) (define pl-parse (fn (src) (pl-parse-program (pl-tokenize src))))
;; Convenience: source → query term
(define pl-parse-goal (fn (src) (pl-parse-query (pl-tokenize src)))) (define pl-parse-goal (fn (src) (pl-parse-query (pl-tokenize src))))

114
lib/prolog/query.sx Normal file
View File

@@ -0,0 +1,114 @@
;; lib/prolog/query.sx — high-level Prolog query API for SX/Hyperscript callers.
;;
;; Requires tokenizer.sx, parser.sx, runtime.sx to be loaded first.
;;
;; Public API:
;; (pl-load source-str) → db
;; (pl-query-all db query-str) → list of solution dicts {var-name → term-string}
;; (pl-query-one db query-str) → first solution dict or nil
;; (pl-query source-str query-str) → list of solution dicts (convenience)
;; Collect variable name strings from a parse-time AST (pre-instantiation).
;; Returns list of unique strings, excluding anonymous "_".
(define
pl-query-extract-vars
(fn
(ast)
(let
((seen {}))
(let
((collect!
(fn
(t)
(cond
((not (list? t)) nil)
((empty? t) nil)
((= (first t) "var")
(if
(not (= (nth t 1) "_"))
(dict-set! seen (nth t 1) true)
nil))
((= (first t) "compound")
(for-each collect! (nth t 2)))
(true nil)))))
(collect! ast)
(keys seen)))))
;; Build a solution dict from a var-env after a successful solve.
;; Maps each variable name string to its formatted term value.
(define
pl-query-solution-dict
(fn
(var-names var-env)
(let
((d {}))
(for-each
(fn (name) (dict-set! d name (pl-format-term (dict-get var-env name))))
var-names)
d)))
;; Parse source-str and load clauses into a fresh DB.
;; Returns the DB for reuse across multiple queries.
(define
pl-load
(fn
(source-str)
(let
((db (pl-mk-db)))
(if
(and (string? source-str) (not (= source-str "")))
(pl-db-load! db (pl-parse source-str))
nil)
db)))
;; Run query-str against db, returning a list of solution dicts.
;; Each dict maps variable name strings to their formatted term values.
;; Returns an empty list if no solutions.
(define
pl-query-all
(fn
(db query-str)
(let
((parsed (pl-parse (str "q_ :- " query-str "."))))
(let
((body-ast (nth (first parsed) 2)))
(let
((var-names (pl-query-extract-vars body-ast))
(var-env {}))
(let
((goal (pl-instantiate body-ast var-env))
(trail (pl-mk-trail))
(solutions (list)))
(let
((mark (pl-trail-mark trail)))
(pl-solve!
db
goal
trail
{:cut false}
(fn
()
(begin
(append!
solutions
(pl-query-solution-dict var-names var-env))
false)))
(pl-trail-undo-to! trail mark)
solutions)))))))
;; Return the first solution dict, or nil if no solutions.
(define
pl-query-one
(fn
(db query-str)
(let
((all (pl-query-all db query-str)))
(if (empty? all) nil (first all)))))
;; Convenience: parse source-str, then run query-str against it.
;; Returns a list of solution dicts. Creates a fresh DB each call.
(define
pl-query
(fn
(source-str query-str)
(pl-query-all (pl-load source-str) query-str)))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"total_passed": 517,
"total_failed": 0,
"total": 517,
"suites": {"parse":{"passed":25,"total":25,"failed":0},"unify":{"passed":47,"total":47,"failed":0},"clausedb":{"passed":14,"total":14,"failed":0},"solve":{"passed":62,"total":62,"failed":0},"operators":{"passed":19,"total":19,"failed":0},"dynamic":{"passed":11,"total":11,"failed":0},"findall":{"passed":11,"total":11,"failed":0},"term_inspect":{"passed":14,"total":14,"failed":0},"append":{"passed":6,"total":6,"failed":0},"reverse":{"passed":6,"total":6,"failed":0},"member":{"passed":7,"total":7,"failed":0},"nqueens":{"passed":6,"total":6,"failed":0},"family":{"passed":10,"total":10,"failed":0},"atoms":{"passed":34,"total":34,"failed":0},"query_api":{"passed":16,"total":16,"failed":0},"iso_predicates":{"passed":29,"total":29,"failed":0},"meta_predicates":{"passed":25,"total":25,"failed":0},"list_predicates":{"passed":33,"total":33,"failed":0},"meta_call":{"passed":15,"total":15,"failed":0},"set_predicates":{"passed":15,"total":15,"failed":0},"char_predicates":{"passed":27,"total":27,"failed":0},"io_predicates":{"passed":24,"total":24,"failed":0},"assert_rules":{"passed":15,"total":15,"failed":0},"string_agg":{"passed":25,"total":25,"failed":0},"advanced":{"passed":21,"total":21,"failed":0}},
"generated": "2026-04-25T14:12:52+00:00"
}

35
lib/prolog/scoreboard.md Normal file
View File

@@ -0,0 +1,35 @@
# Prolog scoreboard
**517 / 517 passing** (0 failure(s)).
Generated 2026-04-25T14:12:52+00:00.
| Suite | Passed | Total | Status |
|-------|--------|-------|--------|
| parse | 25 | 25 | ok |
| unify | 47 | 47 | ok |
| clausedb | 14 | 14 | ok |
| solve | 62 | 62 | ok |
| operators | 19 | 19 | ok |
| dynamic | 11 | 11 | ok |
| findall | 11 | 11 | ok |
| term_inspect | 14 | 14 | ok |
| append | 6 | 6 | ok |
| reverse | 6 | 6 | ok |
| member | 7 | 7 | ok |
| nqueens | 6 | 6 | ok |
| family | 10 | 10 | ok |
| atoms | 34 | 34 | ok |
| query_api | 16 | 16 | ok |
| iso_predicates | 29 | 29 | ok |
| meta_predicates | 25 | 25 | ok |
| list_predicates | 33 | 33 | ok |
| meta_call | 15 | 15 | ok |
| set_predicates | 15 | 15 | ok |
| char_predicates | 27 | 27 | ok |
| io_predicates | 24 | 24 | ok |
| assert_rules | 15 | 15 | ok |
| string_agg | 25 | 25 | ok |
| advanced | 21 | 21 | ok |
Run `bash lib/prolog/conformance.sh` to refresh. Override the binary
with `SX_SERVER=path/to/sx_server.exe bash …`.

View File

@@ -0,0 +1,254 @@
;; lib/prolog/tests/advanced.sx — predsort/3, term_variables/2, arith extensions
(define pl-adv-test-count 0)
(define pl-adv-test-pass 0)
(define pl-adv-test-fail 0)
(define pl-adv-test-failures (list))
(define
pl-adv-test!
(fn
(name got expected)
(begin
(set! pl-adv-test-count (+ pl-adv-test-count 1))
(if
(= got expected)
(set! pl-adv-test-pass (+ pl-adv-test-pass 1))
(begin
(set! pl-adv-test-fail (+ pl-adv-test-fail 1))
(append!
pl-adv-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-adv-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-adv-db (pl-mk-db))
;; Load a numeric comparator for predsort tests
(pl-db-load!
pl-adv-db
(pl-parse
"cmp_num(Order, X, Y) :- (X < Y -> Order = '<' ; (X > Y -> Order = '>' ; Order = '='))."))
;; ── Arithmetic extensions ──────────────────────────────────────────
(define pl-adv-arith-env-1 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is floor(3.7)" pl-adv-arith-env-1)
(pl-mk-trail))
(pl-adv-test!
"floor(3.7) = 3"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-1 "X")))
3)
(define pl-adv-arith-env-2 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is ceiling(3.2)" pl-adv-arith-env-2)
(pl-mk-trail))
(pl-adv-test!
"ceiling(3.2) = 4"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-2 "X")))
4)
(define pl-adv-arith-env-3 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is truncate(3.9)" pl-adv-arith-env-3)
(pl-mk-trail))
(pl-adv-test!
"truncate(3.9) = 3"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-3 "X")))
3)
(define pl-adv-arith-env-4 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is truncate(0 - 3.9)" pl-adv-arith-env-4)
(pl-mk-trail))
(pl-adv-test!
"truncate(0-3.9) = -3"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-4 "X")))
-3)
(define pl-adv-arith-env-5 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is round(3.5)" pl-adv-arith-env-5)
(pl-mk-trail))
(pl-adv-test!
"round(3.5) = 4"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-5 "X")))
4)
(define pl-adv-arith-env-6 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is sqrt(4.0)" pl-adv-arith-env-6)
(pl-mk-trail))
(pl-adv-test!
"sqrt(4.0) = 2"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-6 "X")))
2)
(define pl-adv-arith-env-7 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is sign(0 - 5)" pl-adv-arith-env-7)
(pl-mk-trail))
(pl-adv-test!
"sign(0-5) = -1"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-7 "X")))
-1)
(define pl-adv-arith-env-8 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is sign(0)" pl-adv-arith-env-8)
(pl-mk-trail))
(pl-adv-test!
"sign(0) = 0"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-8 "X")))
0)
(define pl-adv-arith-env-9 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is sign(3)" pl-adv-arith-env-9)
(pl-mk-trail))
(pl-adv-test!
"sign(3) = 1"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-9 "X")))
1)
(define pl-adv-arith-env-10 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is pow(2, 3)" pl-adv-arith-env-10)
(pl-mk-trail))
(pl-adv-test!
"pow(2,3) = 8"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-10 "X")))
8)
(define pl-adv-arith-env-11 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is floor(0 - 3.7)" pl-adv-arith-env-11)
(pl-mk-trail))
(pl-adv-test!
"floor(0-3.7) = -4"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-11 "X")))
-4)
(define pl-adv-arith-env-12 {:X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "X is ceiling(0 - 3.2)" pl-adv-arith-env-12)
(pl-mk-trail))
(pl-adv-test!
"ceiling(0-3.2) = -3"
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-12 "X")))
-3)
;; ── term_variables/2 ──────────────────────────────────────────────
(define pl-adv-tv-env-1 {:Vs (pl-mk-rt-var "Vs")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "term_variables(hello, Vs)" pl-adv-tv-env-1)
(pl-mk-trail))
(pl-adv-test!
"term_variables(hello,Vs) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-adv-tv-env-1 "Vs")))
"[]")
(define pl-adv-tv-env-2 {:Vs (pl-mk-rt-var "Vs")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "term_variables(f(a, g(b)), Vs)" pl-adv-tv-env-2)
(pl-mk-trail))
(pl-adv-test!
"term_variables(f(a,g(b)),Vs) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-adv-tv-env-2 "Vs")))
"[]")
(define pl-adv-tv-env-3 {:Y (pl-mk-rt-var "Y") :Vs (pl-mk-rt-var "Vs") :X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "term_variables(f(X, Y), Vs)" pl-adv-tv-env-3)
(pl-mk-trail))
(pl-adv-test!
"term_variables(f(X,Y),Vs) has 2 vars"
(pl-list-length (pl-walk-deep (dict-get pl-adv-tv-env-3 "Vs")))
2)
(define pl-adv-tv-env-4 {:Vs (pl-mk-rt-var "Vs") :X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "term_variables(X, Vs)" pl-adv-tv-env-4)
(pl-mk-trail))
(pl-adv-test!
"term_variables(X,Vs) has 1 var"
(pl-list-length (pl-walk-deep (dict-get pl-adv-tv-env-4 "Vs")))
1)
(define pl-adv-tv-env-5 {:Y (pl-mk-rt-var "Y") :Vs (pl-mk-rt-var "Vs") :X (pl-mk-rt-var "X")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "term_variables(foo(X, Y, X), Vs)" pl-adv-tv-env-5)
(pl-mk-trail))
(pl-adv-test!
"term_variables(foo(X,Y,X),Vs) deduplicates X -> 2 vars"
(pl-list-length (pl-walk-deep (dict-get pl-adv-tv-env-5 "Vs")))
2)
;; ── predsort/3 ────────────────────────────────────────────────────
(define pl-adv-ps-env-1 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "predsort(cmp_num, [], R)" pl-adv-ps-env-1)
(pl-mk-trail))
(pl-adv-test!
"predsort([]) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-1 "R")))
"[]")
(define pl-adv-ps-env-2 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "predsort(cmp_num, [1], R)" pl-adv-ps-env-2)
(pl-mk-trail))
(pl-adv-test!
"predsort([1]) -> [1]"
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-2 "R")))
".(1, [])")
(define pl-adv-ps-env-3 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "predsort(cmp_num, [3,1,2], R)" pl-adv-ps-env-3)
(pl-mk-trail))
(pl-adv-test!
"predsort([3,1,2]) -> [1,2,3]"
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-3 "R")))
".(1, .(2, .(3, [])))")
(define pl-adv-ps-env-4 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-adv-db
(pl-adv-goal "predsort(cmp_num, [3,1,2,1,3], R)" pl-adv-ps-env-4)
(pl-mk-trail))
(pl-adv-test!
"predsort([3,1,2,1,3]) dedup -> [1,2,3]"
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-4 "R")))
".(1, .(2, .(3, [])))")
;; ── Runner ─────────────────────────────────────────────────────────
(define pl-advanced-tests-run! (fn () {:failed pl-adv-test-fail :passed pl-adv-test-pass :total pl-adv-test-count :failures pl-adv-test-failures}))

View File

@@ -0,0 +1,215 @@
;; lib/prolog/tests/assert_rules.sx — assert/assertz/asserta with rule terms (head :- body)
;; Tests that :- is in the op table (prec 1200 xfx) and pl-build-clause handles rule form.
(define pl-ar-test-count 0)
(define pl-ar-test-pass 0)
(define pl-ar-test-fail 0)
(define pl-ar-test-failures (list))
(define
pl-ar-test!
(fn
(name got expected)
(begin
(set! pl-ar-test-count (+ pl-ar-test-count 1))
(if
(= got expected)
(set! pl-ar-test-pass (+ pl-ar-test-pass 1))
(begin
(set! pl-ar-test-fail (+ pl-ar-test-fail 1))
(append!
pl-ar-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-ar-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
;; ── DB1: assertz a simple rule then query ──────────────────────────
(define pl-ar-db1 (pl-mk-db))
(pl-solve-once!
pl-ar-db1
(pl-ar-goal "assertz((double(X, Y) :- Y is X * 2))" {})
(pl-mk-trail))
(pl-ar-test!
"assertz rule: double(3, Y) succeeds"
(pl-solve-once!
pl-ar-db1
(pl-ar-goal "double(3, Y)" {})
(pl-mk-trail))
true)
(define pl-ar-env1 {})
(pl-solve-once!
pl-ar-db1
(pl-ar-goal "double(3, Y)" pl-ar-env1)
(pl-mk-trail))
(pl-ar-test!
"assertz rule: double(3, Y) binds Y to 6"
(pl-num-val (pl-walk-deep (dict-get pl-ar-env1 "Y")))
6)
(define pl-ar-env1b {})
(pl-solve-once!
pl-ar-db1
(pl-ar-goal "double(10, Y)" pl-ar-env1b)
(pl-mk-trail))
(pl-ar-test!
"assertz rule: double(10, Y) yields 20"
(pl-num-val (pl-walk-deep (dict-get pl-ar-env1b "Y")))
20)
;; ── DB2: assert a rule with multiple facts, count solutions ─────────
(define pl-ar-db2 (pl-mk-db))
(pl-solve-once!
pl-ar-db2
(pl-ar-goal "assert(fact(a))" {})
(pl-mk-trail))
(pl-solve-once!
pl-ar-db2
(pl-ar-goal "assert(fact(b))" {})
(pl-mk-trail))
(pl-solve-once!
pl-ar-db2
(pl-ar-goal "assertz((copy(X) :- fact(X)))" {})
(pl-mk-trail))
(pl-ar-test!
"rule copy/1 using fact/1: 2 solutions"
(pl-solve-count! pl-ar-db2 (pl-ar-goal "copy(X)" {}) (pl-mk-trail))
2)
(define pl-ar-env2a {})
(pl-solve-once! pl-ar-db2 (pl-ar-goal "copy(X)" pl-ar-env2a) (pl-mk-trail))
(pl-ar-test!
"rule copy/1: first solution is a"
(pl-atom-name (pl-walk-deep (dict-get pl-ar-env2a "X")))
"a")
;; ── DB3: asserta rule is tried before existing clauses ─────────────
(define pl-ar-db3 (pl-mk-db))
(pl-solve-once!
pl-ar-db3
(pl-ar-goal "assert(ord(a))" {})
(pl-mk-trail))
(pl-solve-once!
pl-ar-db3
(pl-ar-goal "asserta((ord(b) :- true))" {})
(pl-mk-trail))
(define pl-ar-env3 {})
(pl-solve-once! pl-ar-db3 (pl-ar-goal "ord(X)" pl-ar-env3) (pl-mk-trail))
(pl-ar-test!
"asserta rule ord(b) is tried before ord(a)"
(pl-atom-name (pl-walk-deep (dict-get pl-ar-env3 "X")))
"b")
(pl-ar-test!
"asserta: total solutions for ord/1 is 2"
(pl-solve-count! pl-ar-db3 (pl-ar-goal "ord(X)" {}) (pl-mk-trail))
2)
;; ── DB4: rule with conjunction in body ─────────────────────────────
(define pl-ar-db4 (pl-mk-db))
(pl-solve-once!
pl-ar-db4
(pl-ar-goal "assert(num(1))" {})
(pl-mk-trail))
(pl-solve-once!
pl-ar-db4
(pl-ar-goal "assert(num(2))" {})
(pl-mk-trail))
(pl-solve-once!
pl-ar-db4
(pl-ar-goal "assertz((big(X) :- num(X), X > 1))" {})
(pl-mk-trail))
(pl-ar-test!
"conjunction in rule body: big(1) fails"
(pl-solve-once! pl-ar-db4 (pl-ar-goal "big(1)" {}) (pl-mk-trail))
false)
(pl-ar-test!
"conjunction in rule body: big(2) succeeds"
(pl-solve-once! pl-ar-db4 (pl-ar-goal "big(2)" {}) (pl-mk-trail))
true)
;; ── DB5: recursive rule ─────────────────────────────────────────────
(define pl-ar-db5 (pl-mk-db))
(pl-solve-once!
pl-ar-db5
(pl-ar-goal "assert((nat(0) :- true))" {})
(pl-mk-trail))
(pl-solve-once!
pl-ar-db5
(pl-ar-goal "assertz((nat(s(X)) :- nat(X)))" {})
(pl-mk-trail))
(pl-ar-test!
"recursive rule: nat(0) succeeds"
(pl-solve-once! pl-ar-db5 (pl-ar-goal "nat(0)" {}) (pl-mk-trail))
true)
(pl-ar-test!
"recursive rule: nat(s(0)) succeeds"
(pl-solve-once!
pl-ar-db5
(pl-ar-goal "nat(s(0))" {})
(pl-mk-trail))
true)
(pl-ar-test!
"recursive rule: nat(s(s(0))) succeeds"
(pl-solve-once!
pl-ar-db5
(pl-ar-goal "nat(s(s(0)))" {})
(pl-mk-trail))
true)
(pl-ar-test!
"recursive rule: nat(bad) fails"
(pl-solve-once! pl-ar-db5 (pl-ar-goal "nat(bad)" {}) (pl-mk-trail))
false)
;; ── DB6: rule with true body (explicit) ────────────────────────────
(define pl-ar-db6 (pl-mk-db))
(pl-solve-once!
pl-ar-db6
(pl-ar-goal "assertz((always(X) :- true))" {})
(pl-mk-trail))
(pl-solve-once!
pl-ar-db6
(pl-ar-goal "assert(always(extra))" {})
(pl-mk-trail))
(pl-ar-test!
"rule body=true: always(foo) succeeds"
(pl-solve-once!
pl-ar-db6
(pl-ar-goal "always(foo)" {})
(pl-mk-trail))
true)
(pl-ar-test!
"rule body=true: always/1 has 2 clauses (1 rule + 1 fact)"
(pl-solve-count!
pl-ar-db6
(pl-ar-goal "always(X)" {})
(pl-mk-trail))
2)
;; ── Runner ──────────────────────────────────────────────────────────
(define pl-assert-rules-tests-run! (fn () {:failed pl-ar-test-fail :passed pl-ar-test-pass :total pl-ar-test-count :failures pl-ar-test-failures}))

305
lib/prolog/tests/atoms.sx Normal file
View File

@@ -0,0 +1,305 @@
;; lib/prolog/tests/atoms.sx — type predicates + string/atom built-ins
(define pl-at-test-count 0)
(define pl-at-test-pass 0)
(define pl-at-test-fail 0)
(define pl-at-test-failures (list))
(define
pl-at-test!
(fn
(name got expected)
(begin
(set! pl-at-test-count (+ pl-at-test-count 1))
(if
(= got expected)
(set! pl-at-test-pass (+ pl-at-test-pass 1))
(begin
(set! pl-at-test-fail (+ pl-at-test-fail 1))
(append!
pl-at-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-at-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-at-db (pl-mk-db))
;; ── var/1 + nonvar/1 ──
(pl-at-test!
"var(X) for unbound var"
(pl-solve-once! pl-at-db (pl-at-goal "var(X)" {}) (pl-mk-trail))
true)
(pl-at-test!
"var(foo) fails"
(pl-solve-once! pl-at-db (pl-at-goal "var(foo)" {}) (pl-mk-trail))
false)
(pl-at-test!
"nonvar(foo) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "nonvar(foo)" {})
(pl-mk-trail))
true)
(pl-at-test!
"nonvar(X) for unbound var fails"
(pl-solve-once! pl-at-db (pl-at-goal "nonvar(X)" {}) (pl-mk-trail))
false)
;; ── atom/1 ──
(pl-at-test!
"atom(foo) succeeds"
(pl-solve-once! pl-at-db (pl-at-goal "atom(foo)" {}) (pl-mk-trail))
true)
(pl-at-test!
"atom([]) succeeds"
(pl-solve-once! pl-at-db (pl-at-goal "atom([])" {}) (pl-mk-trail))
true)
(pl-at-test!
"atom(42) fails"
(pl-solve-once! pl-at-db (pl-at-goal "atom(42)" {}) (pl-mk-trail))
false)
(pl-at-test!
"atom(f(x)) fails"
(pl-solve-once!
pl-at-db
(pl-at-goal "atom(f(x))" {})
(pl-mk-trail))
false)
;; ── number/1 + integer/1 ──
(pl-at-test!
"number(42) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "number(42)" {})
(pl-mk-trail))
true)
(pl-at-test!
"number(foo) fails"
(pl-solve-once!
pl-at-db
(pl-at-goal "number(foo)" {})
(pl-mk-trail))
false)
(pl-at-test!
"integer(7) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "integer(7)" {})
(pl-mk-trail))
true)
;; ── compound/1 + callable/1 + atomic/1 ──
(pl-at-test!
"compound(f(x)) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "compound(f(x))" {})
(pl-mk-trail))
true)
(pl-at-test!
"compound(foo) fails"
(pl-solve-once!
pl-at-db
(pl-at-goal "compound(foo)" {})
(pl-mk-trail))
false)
(pl-at-test!
"callable(foo) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "callable(foo)" {})
(pl-mk-trail))
true)
(pl-at-test!
"callable(f(x)) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "callable(f(x))" {})
(pl-mk-trail))
true)
(pl-at-test!
"callable(42) fails"
(pl-solve-once!
pl-at-db
(pl-at-goal "callable(42)" {})
(pl-mk-trail))
false)
(pl-at-test!
"atomic(foo) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "atomic(foo)" {})
(pl-mk-trail))
true)
(pl-at-test!
"atomic(42) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "atomic(42)" {})
(pl-mk-trail))
true)
(pl-at-test!
"atomic(f(x)) fails"
(pl-solve-once!
pl-at-db
(pl-at-goal "atomic(f(x))" {})
(pl-mk-trail))
false)
;; ── is_list/1 ──
(pl-at-test!
"is_list([]) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "is_list([])" {})
(pl-mk-trail))
true)
(pl-at-test!
"is_list([1,2,3]) succeeds"
(pl-solve-once!
pl-at-db
(pl-at-goal "is_list([1,2,3])" {})
(pl-mk-trail))
true)
(pl-at-test!
"is_list(foo) fails"
(pl-solve-once!
pl-at-db
(pl-at-goal "is_list(foo)" {})
(pl-mk-trail))
false)
;; ── atom_length/2 ──
(define pl-at-env-al {})
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_length(hello, N)" pl-at-env-al)
(pl-mk-trail))
(pl-at-test!
"atom_length(hello, N) -> N=5"
(pl-num-val (pl-walk-deep (dict-get pl-at-env-al "N")))
5)
(pl-at-test!
"atom_length empty atom"
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_length('', 0)" {})
(pl-mk-trail))
true)
;; ── atom_concat/3 ──
(define pl-at-env-ac {})
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_concat(foo, bar, X)" pl-at-env-ac)
(pl-mk-trail))
(pl-at-test!
"atom_concat(foo, bar, X) -> X=foobar"
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-ac "X")))
"foobar")
(pl-at-test!
"atom_concat(foo, bar, foobar) check"
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_concat(foo, bar, foobar)" {})
(pl-mk-trail))
true)
(pl-at-test!
"atom_concat(foo, bar, foobaz) fails"
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_concat(foo, bar, foobaz)" {})
(pl-mk-trail))
false)
(define pl-at-env-ac2 {})
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_concat(foo, Y, foobar)" pl-at-env-ac2)
(pl-mk-trail))
(pl-at-test!
"atom_concat(foo, Y, foobar) -> Y=bar"
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-ac2 "Y")))
"bar")
;; ── atom_chars/2 ──
(define pl-at-env-ach {})
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_chars(cat, Cs)" pl-at-env-ach)
(pl-mk-trail))
(pl-at-test!
"atom_chars(cat, Cs) -> Cs=[c,a,t]"
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_chars(cat, [c,a,t])" {})
(pl-mk-trail))
true)
(define pl-at-env-ach2 {})
(pl-solve-once!
pl-at-db
(pl-at-goal "atom_chars(A, [h,i])" pl-at-env-ach2)
(pl-mk-trail))
(pl-at-test!
"atom_chars(A, [h,i]) -> A=hi"
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-ach2 "A")))
"hi")
;; ── char_code/2 ──
(define pl-at-env-cc {})
(pl-solve-once!
pl-at-db
(pl-at-goal "char_code(a, N)" pl-at-env-cc)
(pl-mk-trail))
(pl-at-test!
"char_code(a, N) -> N=97"
(pl-num-val (pl-walk-deep (dict-get pl-at-env-cc "N")))
97)
(define pl-at-env-cc2 {})
(pl-solve-once!
pl-at-db
(pl-at-goal "char_code(C, 65)" pl-at-env-cc2)
(pl-mk-trail))
(pl-at-test!
"char_code(C, 65) -> C='A'"
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-cc2 "C")))
"A")
;; ── number_codes/2 ──
(pl-at-test!
"number_codes(42, [52,50])"
(pl-solve-once!
pl-at-db
(pl-at-goal "number_codes(42, [52,50])" {})
(pl-mk-trail))
true)
;; ── number_chars/2 ──
(pl-at-test!
"number_chars(42, ['4','2'])"
(pl-solve-once!
pl-at-db
(pl-at-goal "number_chars(42, ['4','2'])" {})
(pl-mk-trail))
true)
(define pl-atom-tests-run! (fn () {:failed pl-at-test-fail :passed pl-at-test-pass :total pl-at-test-count :failures pl-at-test-failures}))

View File

@@ -0,0 +1,290 @@
;; lib/prolog/tests/char_predicates.sx — char_type/2, upcase_atom/2, downcase_atom/2,
;; string_upper/2, string_lower/2
(define pl-cp-test-count 0)
(define pl-cp-test-pass 0)
(define pl-cp-test-fail 0)
(define pl-cp-test-failures (list))
(define
pl-cp-test!
(fn
(name got expected)
(begin
(set! pl-cp-test-count (+ pl-cp-test-count 1))
(if
(= got expected)
(set! pl-cp-test-pass (+ pl-cp-test-pass 1))
(begin
(set! pl-cp-test-fail (+ pl-cp-test-fail 1))
(append!
pl-cp-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-cp-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-cp-db (pl-mk-db))
;; ─── char_type/2 — alpha ──────────────────────────────────────────
(pl-cp-test!
"char_type(a, alpha) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, alpha)" {})
(pl-mk-trail))
true)
(pl-cp-test!
"char_type('1', alpha) fails"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('1', alpha)" {})
(pl-mk-trail))
false)
(pl-cp-test!
"char_type('A', alpha) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('A', alpha)" {})
(pl-mk-trail))
true)
;; ─── char_type/2 — alnum ─────────────────────────────────────────
(pl-cp-test!
"char_type('5', alnum) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('5', alnum)" {})
(pl-mk-trail))
true)
(pl-cp-test!
"char_type(a, alnum) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, alnum)" {})
(pl-mk-trail))
true)
(pl-cp-test!
"char_type(' ', alnum) fails"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(' ', alnum)" {})
(pl-mk-trail))
false)
;; ─── char_type/2 — digit ─────────────────────────────────────────
(pl-cp-test!
"char_type('5', digit) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('5', digit)" {})
(pl-mk-trail))
true)
(pl-cp-test!
"char_type(a, digit) fails"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, digit)" {})
(pl-mk-trail))
false)
;; ─── char_type/2 — digit(Weight) ─────────────────────────────────
(define pl-cp-env-dw {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('5', digit(N))" pl-cp-env-dw)
(pl-mk-trail))
(pl-cp-test!
"char_type('5', digit(N)) -> N=5"
(pl-num-val (pl-walk-deep (dict-get pl-cp-env-dw "N")))
5)
(define pl-cp-env-dw0 {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('0', digit(N))" pl-cp-env-dw0)
(pl-mk-trail))
(pl-cp-test!
"char_type('0', digit(N)) -> N=0"
(pl-num-val (pl-walk-deep (dict-get pl-cp-env-dw0 "N")))
0)
;; ─── char_type/2 — space/white ───────────────────────────────────
(pl-cp-test!
"char_type(' ', space) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(' ', space)" {})
(pl-mk-trail))
true)
(pl-cp-test!
"char_type(a, space) fails"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, space)" {})
(pl-mk-trail))
false)
;; ─── char_type/2 — upper(Lower) ──────────────────────────────────
(define pl-cp-env-ul {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('A', upper(L))" pl-cp-env-ul)
(pl-mk-trail))
(pl-cp-test!
"char_type('A', upper(L)) -> L=a"
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-ul "L")))
"a")
(pl-cp-test!
"char_type(a, upper(L)) fails — not uppercase"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, upper(_))" {})
(pl-mk-trail))
false)
;; ─── char_type/2 — lower(Upper) ──────────────────────────────────
(define pl-cp-env-lu {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, lower(U))" pl-cp-env-lu)
(pl-mk-trail))
(pl-cp-test!
"char_type(a, lower(U)) -> U='A'"
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-lu "U")))
"A")
;; ─── char_type/2 — ascii(Code) ───────────────────────────────────
(define pl-cp-env-as {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, ascii(C))" pl-cp-env-as)
(pl-mk-trail))
(pl-cp-test!
"char_type(a, ascii(C)) -> C=97"
(pl-num-val (pl-walk-deep (dict-get pl-cp-env-as "C")))
97)
;; ─── char_type/2 — punct ─────────────────────────────────────────
(pl-cp-test!
"char_type('.', punct) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type('.', punct)" {})
(pl-mk-trail))
true)
(pl-cp-test!
"char_type(a, punct) fails"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "char_type(a, punct)" {})
(pl-mk-trail))
false)
;; ─── upcase_atom/2 ───────────────────────────────────────────────
(define pl-cp-env-ua {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "upcase_atom(hello, X)" pl-cp-env-ua)
(pl-mk-trail))
(pl-cp-test!
"upcase_atom(hello, X) -> X='HELLO'"
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-ua "X")))
"HELLO")
(pl-cp-test!
"upcase_atom(hello, 'HELLO') succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "upcase_atom(hello, 'HELLO')" {})
(pl-mk-trail))
true)
(pl-cp-test!
"upcase_atom('Hello World', 'HELLO WORLD') succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "upcase_atom('Hello World', 'HELLO WORLD')" {})
(pl-mk-trail))
true)
(pl-cp-test!
"upcase_atom('', '') succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "upcase_atom('', '')" {})
(pl-mk-trail))
true)
;; ─── downcase_atom/2 ─────────────────────────────────────────────
(define pl-cp-env-da {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "downcase_atom('HELLO', X)" pl-cp-env-da)
(pl-mk-trail))
(pl-cp-test!
"downcase_atom('HELLO', X) -> X=hello"
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-da "X")))
"hello")
(pl-cp-test!
"downcase_atom('HELLO', hello) succeeds"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "downcase_atom('HELLO', hello)" {})
(pl-mk-trail))
true)
(pl-cp-test!
"downcase_atom(hello, hello) succeeds — already lowercase"
(pl-solve-once!
pl-cp-db
(pl-cp-goal "downcase_atom(hello, hello)" {})
(pl-mk-trail))
true)
;; ─── string_upper/2 + string_lower/2 (aliases) ───────────────────
(define pl-cp-env-su {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "string_upper(hello, X)" pl-cp-env-su)
(pl-mk-trail))
(pl-cp-test!
"string_upper(hello, X) -> X='HELLO'"
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-su "X")))
"HELLO")
(define pl-cp-env-sl {})
(pl-solve-once!
pl-cp-db
(pl-cp-goal "string_lower('WORLD', X)" pl-cp-env-sl)
(pl-mk-trail))
(pl-cp-test!
"string_lower('WORLD', X) -> X=world"
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-sl "X")))
"world")
(define pl-char-predicates-tests-run! (fn () {:failed pl-cp-test-fail :passed pl-cp-test-pass :total pl-cp-test-count :failures pl-cp-test-failures}))

View File

@@ -0,0 +1,99 @@
;; lib/prolog/tests/clausedb.sx — Clause DB unit tests
(define pl-db-test-count 0)
(define pl-db-test-pass 0)
(define pl-db-test-fail 0)
(define pl-db-test-failures (list))
(define
pl-db-test!
(fn
(name got expected)
(begin
(set! pl-db-test-count (+ pl-db-test-count 1))
(if
(= got expected)
(set! pl-db-test-pass (+ pl-db-test-pass 1))
(begin
(set! pl-db-test-fail (+ pl-db-test-fail 1))
(append!
pl-db-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(pl-db-test!
"head-key atom arity 0"
(pl-head-key (nth (first (pl-parse "foo.")) 1))
"foo/0")
(pl-db-test!
"head-key compound arity 2"
(pl-head-key (nth (first (pl-parse "bar(a, b).")) 1))
"bar/2")
(pl-db-test!
"clause-key of :- clause"
(pl-clause-key (first (pl-parse "likes(mary, X) :- friendly(X).")))
"likes/2")
(pl-db-test!
"empty db lookup returns empty list"
(len (pl-db-lookup (pl-mk-db) "parent/2"))
0)
(define pl-db-t1 (pl-mk-db))
(pl-db-load! pl-db-t1 (pl-parse "foo(a). foo(b). foo(c)."))
(pl-db-test!
"three facts same functor"
(len (pl-db-lookup pl-db-t1 "foo/1"))
3)
(pl-db-test!
"mismatching key returns empty"
(len (pl-db-lookup pl-db-t1 "foo/2"))
0)
(pl-db-test!
"first clause has arg a"
(pl-atom-name
(first (pl-args (nth (first (pl-db-lookup pl-db-t1 "foo/1")) 1))))
"a")
(pl-db-test!
"third clause has arg c"
(pl-atom-name
(first (pl-args (nth (nth (pl-db-lookup pl-db-t1 "foo/1") 2) 1))))
"c")
(define pl-db-t2 (pl-mk-db))
(pl-db-load! pl-db-t2 (pl-parse "foo. bar. foo. parent(a, b). parent(c, d)."))
(pl-db-test!
"atom heads keyed as foo/0"
(len (pl-db-lookup pl-db-t2 "foo/0"))
2)
(pl-db-test!
"atom heads keyed as bar/0"
(len (pl-db-lookup pl-db-t2 "bar/0"))
1)
(pl-db-test!
"compound heads keyed as parent/2"
(len (pl-db-lookup pl-db-t2 "parent/2"))
2)
(pl-db-test!
"lookup-goal extracts functor/arity"
(len
(pl-db-lookup-goal pl-db-t2 (nth (first (pl-parse "parent(X, Y).")) 1)))
2)
(pl-db-test!
"lookup-goal on atom goal"
(len (pl-db-lookup-goal pl-db-t2 (nth (first (pl-parse "foo.")) 1)))
2)
(pl-db-test!
"stored clause is clause form"
(first (first (pl-db-lookup pl-db-t2 "parent/2")))
"clause")
(define pl-clausedb-tests-run! (fn () {:failed pl-db-test-fail :passed pl-db-test-pass :total pl-db-test-count :failures pl-db-test-failures}))

View File

@@ -0,0 +1,185 @@
;; lib/prolog/tests/compiler.sx — compiled clause dispatch tests
(define pl-cmp-test-count 0)
(define pl-cmp-test-pass 0)
(define pl-cmp-test-fail 0)
(define pl-cmp-test-failures (list))
(define
pl-cmp-test!
(fn
(name got expected)
(set! pl-cmp-test-count (+ pl-cmp-test-count 1))
(if
(= got expected)
(set! pl-cmp-test-pass (+ pl-cmp-test-pass 1))
(begin
(set! pl-cmp-test-fail (+ pl-cmp-test-fail 1))
(append! pl-cmp-test-failures name)))))
;; Load src, compile, return DB.
(define
pl-cmp-mk
(fn
(src)
(let
((db (pl-mk-db)))
(pl-db-load! db (pl-parse src))
(pl-compile-db! db)
db)))
;; Run goal string against compiled DB; return bool (instantiates vars).
(define
pl-cmp-once
(fn
(db src)
(pl-solve-once!
db
(pl-instantiate (pl-parse-goal src) {})
(pl-mk-trail))))
;; Count solutions for goal string against compiled DB.
(define
pl-cmp-count
(fn
(db src)
(pl-solve-count!
db
(pl-instantiate (pl-parse-goal src) {})
(pl-mk-trail))))
;; ── 1. Simple facts ──────────────────────────────────────────────
(define pl-cmp-db1 (pl-cmp-mk "color(red). color(green). color(blue)."))
(pl-cmp-test! "compiled fact hit" (pl-cmp-once pl-cmp-db1 "color(red)") true)
(pl-cmp-test!
"compiled fact miss"
(pl-cmp-once pl-cmp-db1 "color(yellow)")
false)
(pl-cmp-test! "compiled fact count" (pl-cmp-count pl-cmp-db1 "color(X)") 3)
;; ── 2. Recursive rule: append ────────────────────────────────────
(define
pl-cmp-db2
(pl-cmp-mk "append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R)."))
(pl-cmp-test!
"compiled append build"
(pl-cmp-once pl-cmp-db2 "append([1,2],[3],[1,2,3])")
true)
(pl-cmp-test!
"compiled append fail"
(pl-cmp-once pl-cmp-db2 "append([1,2],[3],[1,2])")
false)
(pl-cmp-test!
"compiled append split count"
(pl-cmp-count pl-cmp-db2 "append(X, Y, [a,b])")
3)
;; ── 3. Cut ───────────────────────────────────────────────────────
(define
pl-cmp-db3
(pl-cmp-mk "first(X, [X|_]) :- !. first(X, [_|T]) :- first(X, T)."))
(pl-cmp-test!
"compiled cut: only one solution"
(pl-cmp-count pl-cmp-db3 "first(X, [a,b,c])")
1)
(let
((db pl-cmp-db3) (trail (pl-mk-trail)) (env {}))
(let
((x (pl-mk-rt-var "X")))
(dict-set! env "X" x)
(pl-solve-once!
db
(pl-instantiate (pl-parse-goal "first(X, [a,b,c])") env)
trail)
(pl-cmp-test!
"compiled cut: correct binding"
(pl-atom-name (pl-walk x))
"a")))
;; ── 4. member ────────────────────────────────────────────────────
(define
pl-cmp-db4
(pl-cmp-mk "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))
(pl-cmp-test!
"compiled member hit"
(pl-cmp-once pl-cmp-db4 "member(b, [a,b,c])")
true)
(pl-cmp-test!
"compiled member miss"
(pl-cmp-once pl-cmp-db4 "member(d, [a,b,c])")
false)
(pl-cmp-test!
"compiled member count"
(pl-cmp-count pl-cmp-db4 "member(X, [a,b,c])")
3)
;; ── 5. Arithmetic in body ────────────────────────────────────────
(define pl-cmp-db5 (pl-cmp-mk "double(X, Y) :- Y is X * 2."))
(let
((db pl-cmp-db5) (trail (pl-mk-trail)) (env {}))
(let
((y (pl-mk-rt-var "Y")))
(dict-set! env "Y" y)
(pl-solve-once!
db
(pl-instantiate (pl-parse-goal "double(5, Y)") env)
trail)
(pl-cmp-test! "compiled arithmetic in body" (pl-num-val (pl-walk y)) 10)))
;; ── 6. Transitive ancestor ───────────────────────────────────────
(define
pl-cmp-db6
(pl-cmp-mk
(str
"parent(a,b). parent(b,c). parent(c,d)."
"ancestor(X,Y) :- parent(X,Y)."
"ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).")))
(pl-cmp-test!
"compiled ancestor direct"
(pl-cmp-once pl-cmp-db6 "ancestor(a,b)")
true)
(pl-cmp-test!
"compiled ancestor 3-step"
(pl-cmp-once pl-cmp-db6 "ancestor(a,d)")
true)
(pl-cmp-test!
"compiled ancestor fail"
(pl-cmp-once pl-cmp-db6 "ancestor(d,a)")
false)
;; ── 7. Fallback: uncompiled predicate calls compiled sub-predicate
(define
pl-cmp-db7
(let
((db (pl-mk-db)))
(pl-db-load! db (pl-parse "q(1). q(2)."))
(pl-compile-db! db)
(pl-db-load! db (pl-parse "r(X) :- q(X)."))
db))
(pl-cmp-test!
"uncompiled predicate resolves"
(pl-cmp-once pl-cmp-db7 "r(1)")
true)
(pl-cmp-test!
"uncompiled calls compiled sub-pred count"
(pl-cmp-count pl-cmp-db7 "r(X)")
2)
;; ── Runner ───────────────────────────────────────────────────────
(define pl-compiler-tests-run! (fn () {:failed pl-cmp-test-fail :passed pl-cmp-test-pass :total pl-cmp-test-count :failures pl-cmp-test-failures}))

View File

@@ -0,0 +1,86 @@
;; lib/prolog/tests/cross_validate.sx
;; Verifies that the compiled solver produces the same solution counts as the
;; interpreter for each classic program + built-in exercise.
;; Interpreter is the reference: if they disagree, the compiler is wrong.
(define pl-xv-test-count 0)
(define pl-xv-test-pass 0)
(define pl-xv-test-fail 0)
(define pl-xv-test-failures (list))
(define
pl-xv-test!
(fn
(name got expected)
(set! pl-xv-test-count (+ pl-xv-test-count 1))
(if
(= got expected)
(set! pl-xv-test-pass (+ pl-xv-test-pass 1))
(begin
(set! pl-xv-test-fail (+ pl-xv-test-fail 1))
(append! pl-xv-test-failures name)))))
;; Shorthand: assert compiled result matches interpreter.
(define
pl-xv-match!
(fn
(name src goal)
(pl-xv-test! name (pl-compiled-matches-interp? src goal) true)))
;; ── 1. append/3 ─────────────────────────────────────────────────
(define
pl-xv-append
"append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R).")
(pl-xv-match! "append build 2+2" pl-xv-append "append([1,2],[3,4],X)")
(pl-xv-match! "append split [a,b,c]" pl-xv-append "append(X, Y, [a,b,c])")
(pl-xv-match! "append member-mode" pl-xv-append "append(_, [3], [1,2,3])")
;; ── 2. member/2 ─────────────────────────────────────────────────
(define pl-xv-member "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
(pl-xv-match! "member check hit" pl-xv-member "member(b, [a,b,c])")
(pl-xv-match! "member count" pl-xv-member "member(X, [a,b,c])")
(pl-xv-match! "member empty" pl-xv-member "member(X, [])")
;; ── 3. facts + transitive rules ─────────────────────────────────
(define
pl-xv-ancestor
(str
"parent(a,b). parent(b,c). parent(c,d). parent(a,c)."
"ancestor(X,Y) :- parent(X,Y)."
"ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y)."))
(pl-xv-match! "ancestor direct" pl-xv-ancestor "ancestor(a,b)")
(pl-xv-match! "ancestor transitive" pl-xv-ancestor "ancestor(a,d)")
(pl-xv-match! "ancestor all from a" pl-xv-ancestor "ancestor(a,Y)")
;; ── 4. cut semantics ────────────────────────────────────────────
(define pl-xv-cut "first(X,[X|_]) :- !. first(X,[_|T]) :- first(X,T).")
(pl-xv-match! "cut one solution" pl-xv-cut "first(X,[a,b,c])")
(pl-xv-match! "cut empty list" pl-xv-cut "first(X,[])")
;; ── 5. arithmetic ───────────────────────────────────────────────
(define pl-xv-arith "sq(X,Y) :- Y is X * X. even(X) :- 0 is X mod 2.")
(pl-xv-match! "sq(3,Y) count" pl-xv-arith "sq(3,Y)")
(pl-xv-match! "sq(3,9) check" pl-xv-arith "sq(3,9)")
(pl-xv-match! "even(4) check" pl-xv-arith "even(4)")
(pl-xv-match! "even(3) check" pl-xv-arith "even(3)")
;; ── 6. if-then-else ─────────────────────────────────────────────
(define pl-xv-ite "classify(X, pos) :- X > 0, !. classify(_, nonpos).")
(pl-xv-match! "classify positive" pl-xv-ite "classify(5, C)")
(pl-xv-match! "classify zero" pl-xv-ite "classify(0, C)")
;; ── Runner ───────────────────────────────────────────────────────
(define pl-cross-validate-tests-run! (fn () {:failed pl-xv-test-fail :passed pl-xv-test-pass :total pl-xv-test-count :failures pl-xv-test-failures}))

158
lib/prolog/tests/dynamic.sx Normal file
View File

@@ -0,0 +1,158 @@
;; lib/prolog/tests/dynamic.sx — assert/asserta/assertz/retract.
(define pl-dy-test-count 0)
(define pl-dy-test-pass 0)
(define pl-dy-test-fail 0)
(define pl-dy-test-failures (list))
(define
pl-dy-test!
(fn
(name got expected)
(begin
(set! pl-dy-test-count (+ pl-dy-test-count 1))
(if
(= got expected)
(set! pl-dy-test-pass (+ pl-dy-test-pass 1))
(begin
(set! pl-dy-test-fail (+ pl-dy-test-fail 1))
(append!
pl-dy-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-dy-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
;; assertz then query
(define pl-dy-db1 (pl-mk-db))
(pl-solve-once!
pl-dy-db1
(pl-dy-goal "assertz(foo(1))" {})
(pl-mk-trail))
(pl-dy-test!
"assertz(foo(1)) + foo(1)"
(pl-solve-once! pl-dy-db1 (pl-dy-goal "foo(1)" {}) (pl-mk-trail))
true)
(pl-dy-test!
"after one assertz, foo/1 has 1 clause"
(pl-solve-count! pl-dy-db1 (pl-dy-goal "foo(X)" {}) (pl-mk-trail))
1)
;; assertz appends — order preserved
(define pl-dy-db2 (pl-mk-db))
(pl-solve-once!
pl-dy-db2
(pl-dy-goal "assertz(p(1))" {})
(pl-mk-trail))
(pl-solve-once!
pl-dy-db2
(pl-dy-goal "assertz(p(2))" {})
(pl-mk-trail))
(pl-dy-test!
"assertz twice — count 2"
(pl-solve-count! pl-dy-db2 (pl-dy-goal "p(X)" {}) (pl-mk-trail))
2)
(define pl-dy-env-a {})
(pl-solve-once! pl-dy-db2 (pl-dy-goal "p(X)" pl-dy-env-a) (pl-mk-trail))
(pl-dy-test!
"assertz: first solution is the first asserted (1)"
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-a "X")))
1)
;; asserta prepends
(define pl-dy-db3 (pl-mk-db))
(pl-solve-once!
pl-dy-db3
(pl-dy-goal "assertz(p(1))" {})
(pl-mk-trail))
(pl-solve-once!
pl-dy-db3
(pl-dy-goal "asserta(p(99))" {})
(pl-mk-trail))
(define pl-dy-env-b {})
(pl-solve-once! pl-dy-db3 (pl-dy-goal "p(X)" pl-dy-env-b) (pl-mk-trail))
(pl-dy-test!
"asserta: prepended clause is first solution"
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-b "X")))
99)
;; assert/1 = assertz/1
(define pl-dy-db4 (pl-mk-db))
(pl-solve-once!
pl-dy-db4
(pl-dy-goal "assert(g(7))" {})
(pl-mk-trail))
(pl-dy-test!
"assert/1 alias"
(pl-solve-once! pl-dy-db4 (pl-dy-goal "g(7)" {}) (pl-mk-trail))
true)
;; retract removes a fact
(define pl-dy-db5 (pl-mk-db))
(pl-solve-once!
pl-dy-db5
(pl-dy-goal "assertz(q(1))" {})
(pl-mk-trail))
(pl-solve-once!
pl-dy-db5
(pl-dy-goal "assertz(q(2))" {})
(pl-mk-trail))
(pl-solve-once!
pl-dy-db5
(pl-dy-goal "assertz(q(3))" {})
(pl-mk-trail))
(pl-dy-test!
"before retract: 3 clauses"
(pl-solve-count! pl-dy-db5 (pl-dy-goal "q(X)" {}) (pl-mk-trail))
3)
(pl-solve-once!
pl-dy-db5
(pl-dy-goal "retract(q(2))" {})
(pl-mk-trail))
(pl-dy-test!
"after retract(q(2)): 2 clauses left"
(pl-solve-count! pl-dy-db5 (pl-dy-goal "q(X)" {}) (pl-mk-trail))
2)
(define pl-dy-env-c {})
(pl-solve-once! pl-dy-db5 (pl-dy-goal "q(X)" pl-dy-env-c) (pl-mk-trail))
(pl-dy-test!
"after retract(q(2)): first remaining is 1"
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-c "X")))
1)
;; retract of non-existent
(pl-dy-test!
"retract(missing(0)) on empty db fails"
(pl-solve-once!
(pl-mk-db)
(pl-dy-goal "retract(missing(0))" {})
(pl-mk-trail))
false)
;; retract with unbound var matches first
(define pl-dy-db6 (pl-mk-db))
(pl-solve-once!
pl-dy-db6
(pl-dy-goal "assertz(r(11))" {})
(pl-mk-trail))
(pl-solve-once!
pl-dy-db6
(pl-dy-goal "assertz(r(22))" {})
(pl-mk-trail))
(define pl-dy-env-d {})
(pl-solve-once!
pl-dy-db6
(pl-dy-goal "retract(r(X))" pl-dy-env-d)
(pl-mk-trail))
(pl-dy-test!
"retract(r(X)) binds X to first match"
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-d "X")))
11)
(define pl-dynamic-tests-run! (fn () {:failed pl-dy-test-fail :passed pl-dy-test-pass :total pl-dy-test-count :failures pl-dy-test-failures}))

167
lib/prolog/tests/findall.sx Normal file
View File

@@ -0,0 +1,167 @@
;; lib/prolog/tests/findall.sx — findall/3, bagof/3, setof/3.
(define pl-fb-test-count 0)
(define pl-fb-test-pass 0)
(define pl-fb-test-fail 0)
(define pl-fb-test-failures (list))
(define
pl-fb-test!
(fn
(name got expected)
(begin
(set! pl-fb-test-count (+ pl-fb-test-count 1))
(if
(= got expected)
(set! pl-fb-test-pass (+ pl-fb-test-pass 1))
(begin
(set! pl-fb-test-fail (+ pl-fb-test-fail 1))
(append!
pl-fb-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-fb-term-to-sx
(fn
(t)
(cond
((pl-num? t) (pl-num-val t))
((pl-atom? t) (pl-atom-name t))
(true (list :complex)))))
(define
pl-fb-list-walked
(fn
(w)
(cond
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
(cons
(pl-fb-term-to-sx (first (pl-args w)))
(pl-fb-list-walked (nth (pl-args w) 1))))
(true (list :not-list)))))
(define pl-fb-list-to-sx (fn (t) (pl-fb-list-walked (pl-walk-deep t))))
(define
pl-fb-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-fb-prog-src "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
(define pl-fb-db (pl-mk-db))
(pl-db-load! pl-fb-db (pl-parse pl-fb-prog-src))
;; ── findall ──
(define pl-fb-env-1 {})
(pl-solve-once!
pl-fb-db
(pl-fb-goal "findall(X, member(X, [a, b, c]), L)" pl-fb-env-1)
(pl-mk-trail))
(pl-fb-test!
"findall member [a, b, c]"
(pl-fb-list-to-sx (dict-get pl-fb-env-1 "L"))
(list "a" "b" "c"))
(define pl-fb-env-2 {})
(pl-solve-once!
pl-fb-db
(pl-fb-goal "findall(X, (member(X, [1, 2, 3]), X >= 2), L)" pl-fb-env-2)
(pl-mk-trail))
(pl-fb-test!
"findall with comparison filter"
(pl-fb-list-to-sx (dict-get pl-fb-env-2 "L"))
(list 2 3))
(define pl-fb-env-3 {})
(pl-solve-once!
pl-fb-db
(pl-fb-goal "findall(X, fail, L)" pl-fb-env-3)
(pl-mk-trail))
(pl-fb-test!
"findall on fail succeeds with empty list"
(pl-fb-list-to-sx (dict-get pl-fb-env-3 "L"))
(list))
(pl-fb-test!
"findall(X, fail, L) the goal succeeds"
(pl-solve-once!
pl-fb-db
(pl-fb-goal "findall(X, fail, L)" {})
(pl-mk-trail))
true)
(define pl-fb-env-4 {})
(pl-solve-once!
pl-fb-db
(pl-fb-goal
"findall(p(X, Y), (member(X, [1, 2]), member(Y, [a, b])), L)"
pl-fb-env-4)
(pl-mk-trail))
(pl-fb-test!
"findall over compound template — count = 4"
(len (pl-fb-list-to-sx (dict-get pl-fb-env-4 "L")))
4)
;; ── bagof ──
(pl-fb-test!
"bagof succeeds when results exist"
(pl-solve-once!
pl-fb-db
(pl-fb-goal "bagof(X, member(X, [1, 2, 3]), L)" {})
(pl-mk-trail))
true)
(pl-fb-test!
"bagof fails on empty"
(pl-solve-once!
pl-fb-db
(pl-fb-goal "bagof(X, fail, L)" {})
(pl-mk-trail))
false)
(define pl-fb-env-5 {})
(pl-solve-once!
pl-fb-db
(pl-fb-goal "bagof(X, member(X, [c, a, b]), L)" pl-fb-env-5)
(pl-mk-trail))
(pl-fb-test!
"bagof preserves order"
(pl-fb-list-to-sx (dict-get pl-fb-env-5 "L"))
(list "c" "a" "b"))
;; ── setof ──
(define pl-fb-env-6 {})
(pl-solve-once!
pl-fb-db
(pl-fb-goal "setof(X, member(X, [c, a, b, a, c]), L)" pl-fb-env-6)
(pl-mk-trail))
(pl-fb-test!
"setof sorts + dedupes atoms"
(pl-fb-list-to-sx (dict-get pl-fb-env-6 "L"))
(list "a" "b" "c"))
(pl-fb-test!
"setof fails on empty"
(pl-solve-once!
pl-fb-db
(pl-fb-goal "setof(X, fail, L)" {})
(pl-mk-trail))
false)
(define pl-fb-env-7 {})
(pl-solve-once!
pl-fb-db
(pl-fb-goal "setof(X, member(X, [3, 1, 2, 1, 3]), L)" pl-fb-env-7)
(pl-mk-trail))
(pl-fb-test!
"setof sorts + dedupes nums"
(pl-fb-list-to-sx (dict-get pl-fb-env-7 "L"))
(list 1 2 3))
(define pl-findall-tests-run! (fn () {:failed pl-fb-test-fail :passed pl-fb-test-pass :total pl-fb-test-count :failures pl-fb-test-failures}))

View File

@@ -0,0 +1,326 @@
;; lib/prolog/tests/io_predicates.sx — term_to_atom/2, term_string/2,
;; with_output_to/2, writeln/1, format/1, format/2
(define pl-io-test-count 0)
(define pl-io-test-pass 0)
(define pl-io-test-fail 0)
(define pl-io-test-failures (list))
(define
pl-io-test!
(fn
(name got expected)
(begin
(set! pl-io-test-count (+ pl-io-test-count 1))
(if
(= got expected)
(set! pl-io-test-pass (+ pl-io-test-pass 1))
(begin
(set! pl-io-test-fail (+ pl-io-test-fail 1))
(append!
pl-io-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-io-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-io-db (pl-mk-db))
;; helper: get output buffer after running a goal
(define
pl-io-capture!
(fn
(goal)
(do
(pl-output-clear!)
(pl-solve-once! pl-io-db goal (pl-mk-trail))
pl-output-buffer)))
;; ─── term_to_atom/2 — bound Term direction ─────────────────────────────────
(pl-io-test!
"term_to_atom(foo(a,b), A) — compound"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "term_to_atom(foo(a,b), A)" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
"foo(a, b)")
(pl-io-test!
"term_to_atom(hello, A) — atom"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "term_to_atom(hello, A)" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
"hello")
(pl-io-test!
"term_to_atom(42, A) — number"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "term_to_atom(42, A)" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
"42")
(pl-io-test!
"term_to_atom(foo(a,b), 'foo(a, b)') — succeeds when Atom matches"
(pl-solve-once!
pl-io-db
(pl-io-goal "term_to_atom(foo(a,b), 'foo(a, b)')" {})
(pl-mk-trail))
true)
(pl-io-test!
"term_to_atom(hello, world) — fails on mismatch"
(pl-solve-once!
pl-io-db
(pl-io-goal "term_to_atom(hello, world)" {})
(pl-mk-trail))
false)
;; ─── term_to_atom/2 — parse direction (Atom bound, Term unbound) ───────────
(pl-io-test!
"term_to_atom(T, 'foo(a)') — parse direction gives compound"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "term_to_atom(T, 'foo(a)')" env)
(pl-mk-trail))
(let
((t (pl-walk-deep (dict-get env "T"))))
(and (pl-compound? t) (= (pl-fun t) "foo"))))
true)
(pl-io-test!
"term_to_atom(T, hello) — parse direction gives atom"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "term_to_atom(T, hello)" env)
(pl-mk-trail))
(let
((t (pl-walk-deep (dict-get env "T"))))
(and (pl-atom? t) (= (pl-atom-name t) "hello"))))
true)
;; ─── term_string/2 — alias ──────────────────────────────────────────────────
(pl-io-test!
"term_string(bar(x), A) — same as term_to_atom"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "term_string(bar(x), A)" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
"bar(x)")
(pl-io-test!
"term_string(42, A) — number to string"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "term_string(42, A)" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
"42")
;; ─── writeln/1 ─────────────────────────────────────────────────────────────
(pl-io-test!
"writeln(hello) writes 'hello\n'"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), writeln(hello))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"hello
")
(pl-io-test!
"writeln(42) writes '42\n'"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), writeln(42))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"42
")
;; ─── with_output_to/2 ──────────────────────────────────────────────────────
(pl-io-test!
"with_output_to(atom(X), write(foo)) — captures write output"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), write(foo))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"foo")
(pl-io-test!
"with_output_to(atom(X), (write(a), write(b))) — concat output"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), (write(a), write(b)))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"ab")
(pl-io-test!
"with_output_to(atom(X), nl) — captures newline"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), nl)" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"
")
(pl-io-test!
"with_output_to(atom(X), true) — captures empty string"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), true)" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"")
(pl-io-test!
"with_output_to(string(X), write(hello)) — string sink works"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(string(X), write(hello))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"hello")
(pl-io-test!
"with_output_to(atom(X), fail) — fails when goal fails"
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), fail)" {})
(pl-mk-trail))
false)
;; ─── format/1 ──────────────────────────────────────────────────────────────
(pl-io-test!
"format('hello~n') — tilde-n becomes newline"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), format('hello~n'))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"hello
")
(pl-io-test!
"format('~~') — double tilde becomes single tilde"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), format('~~'))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"~")
(pl-io-test!
"format('abc') — plain text passes through"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), format(abc))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"abc")
;; ─── format/2 ──────────────────────────────────────────────────────────────
(pl-io-test!
"format('~w+~w', [1,2]) — two ~w args"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), format('~w+~w', [1,2]))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"1+2")
(pl-io-test!
"format('hello ~a!', [world]) — ~a with atom arg"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), format('hello ~a!', [world]))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"hello world!")
(pl-io-test!
"format('n=~d', [42]) — ~d with integer arg"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), format('n=~d', [42]))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"n=42")
(pl-io-test!
"format('~w', [foo(a)]) — ~w with compound"
(let
((env {}))
(pl-solve-once!
pl-io-db
(pl-io-goal "with_output_to(atom(X), format('~w', [foo(a)]))" env)
(pl-mk-trail))
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
"foo(a)")
(define
pl-io-predicates-tests-run!
(fn
()
{:failed pl-io-test-fail
:passed pl-io-test-pass
:total pl-io-test-count
:failures pl-io-test-failures}))

View File

@@ -0,0 +1,320 @@
;; lib/prolog/tests/iso_predicates.sx — succ/2, plus/3, between/3, length/2, last/2, nth0/3, nth1/3, max/min arith
(define pl-ip-test-count 0)
(define pl-ip-test-pass 0)
(define pl-ip-test-fail 0)
(define pl-ip-test-failures (list))
(define
pl-ip-test!
(fn
(name got expected)
(begin
(set! pl-ip-test-count (+ pl-ip-test-count 1))
(if
(= got expected)
(set! pl-ip-test-pass (+ pl-ip-test-pass 1))
(begin
(set! pl-ip-test-fail (+ pl-ip-test-fail 1))
(append!
pl-ip-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-ip-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-ip-db (pl-mk-db))
;; ── succ/2 ──
(define pl-ip-env-s1 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "succ(3, X)" pl-ip-env-s1)
(pl-mk-trail))
(pl-ip-test!
"succ(3, X) → X=4"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-s1 "X")))
4)
(define pl-ip-env-s2 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "succ(0, X)" pl-ip-env-s2)
(pl-mk-trail))
(pl-ip-test!
"succ(0, X) → X=1"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-s2 "X")))
1)
(define pl-ip-env-s3 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "succ(X, 5)" pl-ip-env-s3)
(pl-mk-trail))
(pl-ip-test!
"succ(X, 5) → X=4"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-s3 "X")))
4)
(pl-ip-test!
"succ(X, 0) fails"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "succ(X, 0)" {})
(pl-mk-trail))
false)
;; ── plus/3 ──
(define pl-ip-env-p1 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "plus(2, 3, X)" pl-ip-env-p1)
(pl-mk-trail))
(pl-ip-test!
"plus(2, 3, X) → X=5"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-p1 "X")))
5)
(define pl-ip-env-p2 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "plus(2, X, 7)" pl-ip-env-p2)
(pl-mk-trail))
(pl-ip-test!
"plus(2, X, 7) → X=5"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-p2 "X")))
5)
(define pl-ip-env-p3 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "plus(X, 3, 7)" pl-ip-env-p3)
(pl-mk-trail))
(pl-ip-test!
"plus(X, 3, 7) → X=4"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-p3 "X")))
4)
(pl-ip-test!
"plus(0, 0, 0) succeeds"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "plus(0, 0, 0)" {})
(pl-mk-trail))
true)
;; ── between/3 ──
(pl-ip-test!
"between(1, 3, X): 3 solutions"
(pl-solve-count!
pl-ip-db
(pl-ip-goal "between(1, 3, X)" {})
(pl-mk-trail))
3)
(pl-ip-test!
"between(1, 3, 2) succeeds"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "between(1, 3, 2)" {})
(pl-mk-trail))
true)
(pl-ip-test!
"between(1, 3, 5) fails"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "between(1, 3, 5)" {})
(pl-mk-trail))
false)
(pl-ip-test!
"between(5, 3, X): 0 solutions (empty range)"
(pl-solve-count!
pl-ip-db
(pl-ip-goal "between(5, 3, X)" {})
(pl-mk-trail))
0)
(define pl-ip-env-b1 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "between(1, 5, X)" pl-ip-env-b1)
(pl-mk-trail))
(pl-ip-test!
"between(1, 5, X): first solution X=1"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-b1 "X")))
1)
(pl-ip-test!
"between + condition: between(1,5,X), X > 3 → 2 solutions"
(pl-solve-count!
pl-ip-db
(pl-ip-goal "between(1, 5, X), X > 3" {})
(pl-mk-trail))
2)
;; ── length/2 ──
(define pl-ip-env-l1 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "length([1,2,3], N)" pl-ip-env-l1)
(pl-mk-trail))
(pl-ip-test!
"length([1,2,3], N) → N=3"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-l1 "N")))
3)
(define pl-ip-env-l2 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "length([], N)" pl-ip-env-l2)
(pl-mk-trail))
(pl-ip-test!
"length([], N) → N=0"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-l2 "N")))
0)
(pl-ip-test!
"length([a,b], 2) check succeeds"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "length([a,b], 2)" {})
(pl-mk-trail))
true)
(define pl-ip-env-l3 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "length(L, 3)" pl-ip-env-l3)
(pl-mk-trail))
(pl-ip-test!
"length(L, 3): L is a list of length 3"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "length(L, 3), is_list(L)" pl-ip-env-l3)
(pl-mk-trail))
true)
;; ── last/2 ──
(define pl-ip-env-la1 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "last([1,2,3], X)" pl-ip-env-la1)
(pl-mk-trail))
(pl-ip-test!
"last([1,2,3], X) → X=3"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-la1 "X")))
3)
(define pl-ip-env-la2 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "last([a], X)" pl-ip-env-la2)
(pl-mk-trail))
(pl-ip-test!
"last([a], X) → X=a"
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-la2 "X")))
"a")
(pl-ip-test!
"last([], X) fails"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "last([], X)" {})
(pl-mk-trail))
false)
;; ── nth0/3 ──
(define pl-ip-env-n0 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "nth0(0, [a,b,c], X)" pl-ip-env-n0)
(pl-mk-trail))
(pl-ip-test!
"nth0(0, [a,b,c], X) → X=a"
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n0 "X")))
"a")
(define pl-ip-env-n1 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "nth0(2, [a,b,c], X)" pl-ip-env-n1)
(pl-mk-trail))
(pl-ip-test!
"nth0(2, [a,b,c], X) → X=c"
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n1 "X")))
"c")
(pl-ip-test!
"nth0(5, [a,b,c], X) fails"
(pl-solve-once!
pl-ip-db
(pl-ip-goal "nth0(5, [a,b,c], X)" {})
(pl-mk-trail))
false)
;; ── nth1/3 ──
(define pl-ip-env-n1a {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "nth1(1, [a,b,c], X)" pl-ip-env-n1a)
(pl-mk-trail))
(pl-ip-test!
"nth1(1, [a,b,c], X) → X=a"
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n1a "X")))
"a")
(define pl-ip-env-n1b {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "nth1(3, [a,b,c], X)" pl-ip-env-n1b)
(pl-mk-trail))
(pl-ip-test!
"nth1(3, [a,b,c], X) → X=c"
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n1b "X")))
"c")
;; ── max/min in arithmetic ──
(define pl-ip-env-m1 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "X is max(3, 5)" pl-ip-env-m1)
(pl-mk-trail))
(pl-ip-test!
"X is max(3, 5) → X=5"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-m1 "X")))
5)
(define pl-ip-env-m2 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "X is min(3, 5)" pl-ip-env-m2)
(pl-mk-trail))
(pl-ip-test!
"X is min(3, 5) → X=3"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-m2 "X")))
3)
(define pl-ip-env-m3 {})
(pl-solve-once!
pl-ip-db
(pl-ip-goal "X is max(7, 2) + min(1, 4)" pl-ip-env-m3)
(pl-mk-trail))
(pl-ip-test!
"X is max(7,2) + min(1,4) → X=8"
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-m3 "X")))
8)
(define pl-iso-predicates-tests-run! (fn () {:failed pl-ip-test-fail :passed pl-ip-test-pass :total pl-ip-test-count :failures pl-ip-test-failures}))

View File

@@ -0,0 +1,335 @@
;; lib/prolog/tests/list_predicates.sx — ==/2, \==/2, flatten/2, numlist/3,
;; atomic_list_concat/2,3, sum_list/2, max_list/2, min_list/2, delete/3
(define pl-lp-test-count 0)
(define pl-lp-test-pass 0)
(define pl-lp-test-fail 0)
(define pl-lp-test-failures (list))
(define
pl-lp-test!
(fn
(name got expected)
(begin
(set! pl-lp-test-count (+ pl-lp-test-count 1))
(if
(= got expected)
(set! pl-lp-test-pass (+ pl-lp-test-pass 1))
(begin
(set! pl-lp-test-fail (+ pl-lp-test-fail 1))
(append!
pl-lp-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-lp-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-lp-db (pl-mk-db))
;; ── ==/2 ───────────────────────────────────────────────────────────
(pl-lp-test!
"==(a, a) succeeds"
(pl-solve-once! pl-lp-db (pl-lp-goal "==(a, a)" {}) (pl-mk-trail))
true)
(pl-lp-test!
"==(a, b) fails"
(pl-solve-once! pl-lp-db (pl-lp-goal "==(a, b)" {}) (pl-mk-trail))
false)
(pl-lp-test!
"==(1, 1) succeeds"
(pl-solve-once! pl-lp-db (pl-lp-goal "==(1, 1)" {}) (pl-mk-trail))
true)
(pl-lp-test!
"==(1, 2) fails"
(pl-solve-once! pl-lp-db (pl-lp-goal "==(1, 2)" {}) (pl-mk-trail))
false)
(pl-lp-test!
"==(f(a,b), f(a,b)) succeeds"
(pl-solve-once!
pl-lp-db
(pl-lp-goal "==(f(a,b), f(a,b))" {})
(pl-mk-trail))
true)
(pl-lp-test!
"==(f(a,b), f(a,c)) fails"
(pl-solve-once!
pl-lp-db
(pl-lp-goal "==(f(a,b), f(a,c))" {})
(pl-mk-trail))
false)
;; unbound var vs atom: fails (different tags)
(pl-lp-test!
"==(X, a) fails (unbound var vs atom)"
(pl-solve-once! pl-lp-db (pl-lp-goal "==(X, a)" {}) (pl-mk-trail))
false)
;; two unbound vars with SAME name in same env share the same runtime var
(define pl-lp-env-same-var {})
(pl-lp-goal "==(X, X)" pl-lp-env-same-var)
(pl-lp-test!
"==(X, X) succeeds (same runtime var)"
(pl-solve-once!
pl-lp-db
(pl-instantiate
(nth (first (pl-parse "g :- ==(X, X).")) 2)
pl-lp-env-same-var)
(pl-mk-trail))
true)
;; ── \==/2 ──────────────────────────────────────────────────────────
(pl-lp-test!
"\\==(a, b) succeeds"
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(a, b)" {}) (pl-mk-trail))
true)
(pl-lp-test!
"\\==(a, a) fails"
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(a, a)" {}) (pl-mk-trail))
false)
(pl-lp-test!
"\\==(X, a) succeeds (unbound var differs from atom)"
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(X, a)" {}) (pl-mk-trail))
true)
(pl-lp-test!
"\\==(1, 2) succeeds"
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(1, 2)" {}) (pl-mk-trail))
true)
;; ── flatten/2 ──────────────────────────────────────────────────────
(define pl-lp-env-fl1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "flatten([], F)" pl-lp-env-fl1)
(pl-mk-trail))
(pl-lp-test!
"flatten([], []) -> empty"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl1 "F")))
"[]")
(define pl-lp-env-fl2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "flatten([1,2,3], F)" pl-lp-env-fl2)
(pl-mk-trail))
(pl-lp-test!
"flatten([1,2,3], F) -> [1,2,3]"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl2 "F")))
".(1, .(2, .(3, [])))")
(define pl-lp-env-fl3 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "flatten([1,[2,[3]],4], F)" pl-lp-env-fl3)
(pl-mk-trail))
(pl-lp-test!
"flatten([1,[2,[3]],4], F) -> [1,2,3,4]"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl3 "F")))
".(1, .(2, .(3, .(4, []))))")
(define pl-lp-env-fl4 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "flatten([[a,b],[c]], F)" pl-lp-env-fl4)
(pl-mk-trail))
(pl-lp-test!
"flatten([[a,b],[c]], F) -> [a,b,c]"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl4 "F")))
".(a, .(b, .(c, [])))")
;; ── numlist/3 ──────────────────────────────────────────────────────
(define pl-lp-env-nl1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "numlist(1, 5, L)" pl-lp-env-nl1)
(pl-mk-trail))
(pl-lp-test!
"numlist(1,5,L) -> [1,2,3,4,5]"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-nl1 "L")))
".(1, .(2, .(3, .(4, .(5, [])))))")
(define pl-lp-env-nl2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "numlist(3, 3, L)" pl-lp-env-nl2)
(pl-mk-trail))
(pl-lp-test!
"numlist(3,3,L) -> [3]"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-nl2 "L")))
".(3, [])")
(pl-lp-test!
"numlist(5, 3, L) fails (Low > High)"
(pl-solve-once!
pl-lp-db
(pl-lp-goal "numlist(5, 3, L)" {})
(pl-mk-trail))
false)
;; ── atomic_list_concat/2 ───────────────────────────────────────────
(define pl-lp-env-alc1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "atomic_list_concat([a, b, c], R)" pl-lp-env-alc1)
(pl-mk-trail))
(pl-lp-test!
"atomic_list_concat([a,b,c], R) -> abc"
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alc1 "R")))
"abc")
(define pl-lp-env-alc2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "atomic_list_concat([hello, world], R)" pl-lp-env-alc2)
(pl-mk-trail))
(pl-lp-test!
"atomic_list_concat([hello,world], R) -> helloworld"
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alc2 "R")))
"helloworld")
;; ── atomic_list_concat/3 ───────────────────────────────────────────
(define pl-lp-env-alcs1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "atomic_list_concat([a, b, c], '-', R)" pl-lp-env-alcs1)
(pl-mk-trail))
(pl-lp-test!
"atomic_list_concat([a,b,c], '-', R) -> a-b-c"
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alcs1 "R")))
"a-b-c")
(define pl-lp-env-alcs2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "atomic_list_concat([x], '-', R)" pl-lp-env-alcs2)
(pl-mk-trail))
(pl-lp-test!
"atomic_list_concat([x], '-', R) -> x (single element, no sep)"
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alcs2 "R")))
"x")
;; ── sum_list/2 ─────────────────────────────────────────────────────
(define pl-lp-env-sl1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "sum_list([1,2,3], S)" pl-lp-env-sl1)
(pl-mk-trail))
(pl-lp-test!
"sum_list([1,2,3], S) -> 6"
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-sl1 "S")))
6)
(define pl-lp-env-sl2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "sum_list([10], S)" pl-lp-env-sl2)
(pl-mk-trail))
(pl-lp-test!
"sum_list([10], S) -> 10"
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-sl2 "S")))
10)
(define pl-lp-env-sl3 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "sum_list([], S)" pl-lp-env-sl3)
(pl-mk-trail))
(pl-lp-test!
"sum_list([], S) -> 0"
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-sl3 "S")))
0)
;; ── max_list/2 ─────────────────────────────────────────────────────
(define pl-lp-env-mx1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "max_list([3,1,4,1,5,9,2,6], M)" pl-lp-env-mx1)
(pl-mk-trail))
(pl-lp-test!
"max_list([3,1,4,1,5,9,2,6], M) -> 9"
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mx1 "M")))
9)
(define pl-lp-env-mx2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "max_list([7], M)" pl-lp-env-mx2)
(pl-mk-trail))
(pl-lp-test!
"max_list([7], M) -> 7"
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mx2 "M")))
7)
;; ── min_list/2 ─────────────────────────────────────────────────────
(define pl-lp-env-mn1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "min_list([3,1,4,1,5,9,2,6], M)" pl-lp-env-mn1)
(pl-mk-trail))
(pl-lp-test!
"min_list([3,1,4,1,5,9,2,6], M) -> 1"
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mn1 "M")))
1)
(define pl-lp-env-mn2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "min_list([5,2,8], M)" pl-lp-env-mn2)
(pl-mk-trail))
(pl-lp-test!
"min_list([5,2,8], M) -> 2"
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mn2 "M")))
2)
;; ── delete/3 ───────────────────────────────────────────────────────
(define pl-lp-env-del1 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "delete([1,2,3,2,1], 2, R)" pl-lp-env-del1)
(pl-mk-trail))
(pl-lp-test!
"delete([1,2,3,2,1], 2, R) -> [1,3,1]"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-del1 "R")))
".(1, .(3, .(1, [])))")
(define pl-lp-env-del2 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "delete([a,b,c], d, R)" pl-lp-env-del2)
(pl-mk-trail))
(pl-lp-test!
"delete([a,b,c], d, R) -> [a,b,c] (nothing deleted)"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-del2 "R")))
".(a, .(b, .(c, [])))")
(define pl-lp-env-del3 {})
(pl-solve-once!
pl-lp-db
(pl-lp-goal "delete([], x, R)" pl-lp-env-del3)
(pl-mk-trail))
(pl-lp-test!
"delete([], x, R) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-del3 "R")))
"[]")
(define pl-list-predicates-tests-run! (fn () {:failed pl-lp-test-fail :passed pl-lp-test-pass :total pl-lp-test-count :failures pl-lp-test-failures}))

View File

@@ -0,0 +1,197 @@
;; lib/prolog/tests/meta_call.sx — forall/2, maplist/2, maplist/3, include/3, exclude/3
(define pl-mc-test-count 0)
(define pl-mc-test-pass 0)
(define pl-mc-test-fail 0)
(define pl-mc-test-failures (list))
(define
pl-mc-test!
(fn
(name got expected)
(begin
(set! pl-mc-test-count (+ pl-mc-test-count 1))
(if
(= got expected)
(set! pl-mc-test-pass (+ pl-mc-test-pass 1))
(begin
(set! pl-mc-test-fail (+ pl-mc-test-fail 1))
(append!
pl-mc-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-mc-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define
pl-mc-term-to-sx
(fn
(t)
(cond
((pl-num? t) (pl-num-val t))
((pl-atom? t) (pl-atom-name t))
(else t))))
(define
pl-mc-list-sx
(fn
(t)
(let
((w (pl-walk-deep t)))
(cond
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
((and (pl-compound? w) (= (pl-fun w) "."))
(cons
(pl-mc-term-to-sx (first (pl-args w)))
(pl-mc-list-sx (nth (pl-args w) 1))))
(else (list :not-list))))))
(define pl-mc-db (pl-mk-db))
(pl-db-load!
pl-mc-db
(pl-parse "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))
(pl-db-load! pl-mc-db (pl-parse "double(X, Y) :- Y is X * 2."))
(pl-db-load! pl-mc-db (pl-parse "even(X) :- 0 is X mod 2."))
;; -- forall/2 --
(pl-mc-test!
"forall(member(X,[2,4,6]), 0 is X mod 2) — all even"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "forall(member(X,[2,4,6]), 0 is X mod 2)" {})
(pl-mk-trail))
true)
(pl-mc-test!
"forall(member(X,[2,3,6]), 0 is X mod 2) — 3 is odd, fails"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "forall(member(X,[2,3,6]), 0 is X mod 2)" {})
(pl-mk-trail))
false)
(pl-mc-test!
"forall(member(_,[]), true) — vacuously true"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "forall(member(_,[]), true)" {})
(pl-mk-trail))
true)
;; -- maplist/2 --
(pl-mc-test!
"maplist(atom, [a,b,c]) — all atoms"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "maplist(atom, [a,b,c])" {})
(pl-mk-trail))
true)
(pl-mc-test!
"maplist(atom, [a,1,c]) — 1 is not atom, fails"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "maplist(atom, [a,1,c])" {})
(pl-mk-trail))
false)
(pl-mc-test!
"maplist(atom, []) — vacuously true"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "maplist(atom, [])" {})
(pl-mk-trail))
true)
;; -- maplist/3 --
(pl-mc-test!
"maplist(double, [1,2,3], [2,4,6]) — deterministic check"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "maplist(double, [1,2,3], [2,4,6])" {})
(pl-mk-trail))
true)
(pl-mc-test!
"maplist(double, [1,2,3], [2,4,7]) — wrong result fails"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "maplist(double, [1,2,3], [2,4,7])" {})
(pl-mk-trail))
false)
(define pl-mc-env-ml3 {:L (pl-mk-rt-var "L")})
(pl-solve-once!
pl-mc-db
(pl-mc-goal "maplist(double, [1,2,3], L)" pl-mc-env-ml3)
(pl-mk-trail))
(pl-mc-test!
"maplist(double, [1,2,3], L) — L bound to [2,4,6]"
(pl-mc-list-sx (dict-get pl-mc-env-ml3 "L"))
(list 2 4 6))
;; -- include/3 --
(pl-mc-test!
"include(even, [1,2,3,4,5,6], [2,4,6])"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "include(even, [1,2,3,4,5,6], [2,4,6])" {})
(pl-mk-trail))
true)
(pl-mc-test!
"include(even, [], [])"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "include(even, [], [])" {})
(pl-mk-trail))
true)
(define pl-mc-env-inc {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-mc-db
(pl-mc-goal "include(even, [1,2,3,4,5,6], R)" pl-mc-env-inc)
(pl-mk-trail))
(pl-mc-test!
"include(even, [1,2,3,4,5,6], R) — R bound to [2,4,6]"
(pl-mc-list-sx (dict-get pl-mc-env-inc "R"))
(list 2 4 6))
;; -- exclude/3 --
(pl-mc-test!
"exclude(even, [1,2,3,4,5,6], [1,3,5])"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "exclude(even, [1,2,3,4,5,6], [1,3,5])" {})
(pl-mk-trail))
true)
(pl-mc-test!
"exclude(even, [], [])"
(pl-solve-once!
pl-mc-db
(pl-mc-goal "exclude(even, [], [])" {})
(pl-mk-trail))
true)
(define pl-mc-env-exc {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-mc-db
(pl-mc-goal "exclude(even, [1,2,3,4,5,6], R)" pl-mc-env-exc)
(pl-mk-trail))
(pl-mc-test!
"exclude(even, [1,2,3,4,5,6], R) — R bound to [1,3,5]"
(pl-mc-list-sx (dict-get pl-mc-env-exc "R"))
(list 1 3 5))
(define pl-meta-call-tests-run! (fn () {:failed pl-mc-test-fail :passed pl-mc-test-pass :total pl-mc-test-count :failures pl-mc-test-failures}))

View File

@@ -0,0 +1,252 @@
;; lib/prolog/tests/meta_predicates.sx — \+/1, not/1, once/1, ignore/1, ground/1, sort/2, msort/2, atom_number/2, number_string/2
(define pl-mp-test-count 0)
(define pl-mp-test-pass 0)
(define pl-mp-test-fail 0)
(define pl-mp-test-failures (list))
(define
pl-mp-test!
(fn
(name got expected)
(begin
(set! pl-mp-test-count (+ pl-mp-test-count 1))
(if
(= got expected)
(set! pl-mp-test-pass (+ pl-mp-test-pass 1))
(begin
(set! pl-mp-test-fail (+ pl-mp-test-fail 1))
(append!
pl-mp-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-mp-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-mp-db (pl-mk-db))
(pl-db-load!
pl-mp-db
(pl-parse "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))
;; -- \+/1 --
(pl-mp-test!
"\\+(fail) succeeds"
(pl-solve-once! pl-mp-db (pl-mp-goal "\\+(fail)" {}) (pl-mk-trail))
true)
(pl-mp-test!
"\\+(true) fails"
(pl-solve-once! pl-mp-db (pl-mp-goal "\\+(true)" {}) (pl-mk-trail))
false)
(pl-mp-test!
"\\+(member(d, [a,b,c])) succeeds"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "\\+(member(d, [a,b,c]))" {})
(pl-mk-trail))
true)
(pl-mp-test!
"\\+(member(a, [a,b,c])) fails"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "\\+(member(a, [a,b,c]))" {})
(pl-mk-trail))
false)
(define pl-mp-env-neg {})
(pl-solve-once!
pl-mp-db
(pl-mp-goal "\\+(X = 5)" pl-mp-env-neg)
(pl-mk-trail))
(pl-mp-test!
"\\+(X=5) fails, X stays unbound (bindings undone)"
(nil? (pl-var-binding (dict-get pl-mp-env-neg "X")))
true)
;; -- not/1 --
(pl-mp-test!
"not(fail) succeeds"
(pl-solve-once! pl-mp-db (pl-mp-goal "not(fail)" {}) (pl-mk-trail))
true)
(pl-mp-test!
"not(true) fails"
(pl-solve-once! pl-mp-db (pl-mp-goal "not(true)" {}) (pl-mk-trail))
false)
;; -- once/1 --
(pl-mp-test!
"once(member(X,[1,2,3])) succeeds once"
(pl-solve-count!
pl-mp-db
(pl-mp-goal "once(member(X,[1,2,3]))" {})
(pl-mk-trail))
1)
(define pl-mp-env-once {})
(pl-solve-once!
pl-mp-db
(pl-mp-goal "once(member(X,[1,2,3]))" pl-mp-env-once)
(pl-mk-trail))
(pl-mp-test!
"once(member(X,[1,2,3])): X=1 (first solution)"
(pl-num-val (pl-walk-deep (dict-get pl-mp-env-once "X")))
1)
(pl-mp-test!
"once(fail) fails"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "once(fail)" {})
(pl-mk-trail))
false)
;; -- ignore/1 --
(pl-mp-test!
"ignore(true) succeeds"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "ignore(true)" {})
(pl-mk-trail))
true)
(pl-mp-test!
"ignore(fail) still succeeds"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "ignore(fail)" {})
(pl-mk-trail))
true)
;; -- ground/1 --
(pl-mp-test!
"ground(foo(1, a)) succeeds"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "ground(foo(1, a))" {})
(pl-mk-trail))
true)
(pl-mp-test!
"ground(foo(X, a)) fails (X unbound)"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "ground(foo(X, a))" {})
(pl-mk-trail))
false)
(pl-mp-test!
"ground(42) succeeds"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "ground(42)" {})
(pl-mk-trail))
true)
;; -- sort/2 --
(pl-mp-test!
"sort([b,a,c], [a,b,c])"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "sort([b,a,c], [a,b,c])" {})
(pl-mk-trail))
true)
(pl-mp-test!
"sort([b,a,a,c], [a,b,c]) (removes duplicates)"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "sort([b,a,a,c], [a,b,c])" {})
(pl-mk-trail))
true)
(pl-mp-test!
"sort([], [])"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "sort([], [])" {})
(pl-mk-trail))
true)
;; -- msort/2 --
(pl-mp-test!
"msort([b,a,a,c], [a,a,b,c]) (keeps duplicates)"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "msort([b,a,a,c], [a,a,b,c])" {})
(pl-mk-trail))
true)
(pl-mp-test!
"msort([3,1,2,1], [1,1,2,3])"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "msort([3,1,2,1], [1,1,2,3])" {})
(pl-mk-trail))
true)
;; -- atom_number/2 --
(define pl-mp-env-an1 {})
(pl-solve-once!
pl-mp-db
(pl-mp-goal "atom_number('42', N)" pl-mp-env-an1)
(pl-mk-trail))
(pl-mp-test!
"atom_number('42', N) -> N=42"
(pl-num-val (pl-walk-deep (dict-get pl-mp-env-an1 "N")))
42)
(define pl-mp-env-an2 {})
(pl-solve-once!
pl-mp-db
(pl-mp-goal "atom_number(A, 7)" pl-mp-env-an2)
(pl-mk-trail))
(pl-mp-test!
"atom_number(A, 7) -> A='7'"
(pl-atom-name (pl-walk-deep (dict-get pl-mp-env-an2 "A")))
"7")
(pl-mp-test!
"atom_number(foo, N) fails (not a number)"
(pl-solve-once!
pl-mp-db
(pl-mp-goal "atom_number(foo, N)" {})
(pl-mk-trail))
false)
;; -- number_string/2 --
(define pl-mp-env-ns1 {})
(pl-solve-once!
pl-mp-db
(pl-mp-goal "number_string(42, S)" pl-mp-env-ns1)
(pl-mk-trail))
(pl-mp-test!
"number_string(42, S) -> S='42'"
(pl-atom-name (pl-walk-deep (dict-get pl-mp-env-ns1 "S")))
"42")
(define pl-mp-env-ns2 {})
(pl-solve-once!
pl-mp-db
(pl-mp-goal "number_string(N, '3.14')" pl-mp-env-ns2)
(pl-mk-trail))
(pl-mp-test!
"number_string(N, '3.14') -> N=3.14"
(pl-num-val (pl-walk-deep (dict-get pl-mp-env-ns2 "N")))
3.14)
(define pl-meta-predicates-tests-run! (fn () {:failed pl-mp-test-fail :passed pl-mp-test-pass :total pl-mp-test-count :failures pl-mp-test-failures}))

View File

@@ -0,0 +1,193 @@
;; lib/prolog/tests/operators.sx — operator-table parsing + comparison built-ins.
(define pl-op-test-count 0)
(define pl-op-test-pass 0)
(define pl-op-test-fail 0)
(define pl-op-test-failures (list))
(define
pl-op-test!
(fn
(name got expected)
(begin
(set! pl-op-test-count (+ pl-op-test-count 1))
(if
(= got expected)
(set! pl-op-test-pass (+ pl-op-test-pass 1))
(begin
(set! pl-op-test-fail (+ pl-op-test-fail 1))
(append!
pl-op-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define pl-op-empty-db (pl-mk-db))
(define
pl-op-body
(fn (src) (nth (first (pl-parse (str "g :- " src "."))) 2)))
(define pl-op-goal (fn (src env) (pl-instantiate (pl-op-body src) env)))
;; ── parsing tests ──
(pl-op-test!
"infix +"
(pl-op-body "a + b")
(list "compound" "+" (list (list "atom" "a") (list "atom" "b"))))
(pl-op-test!
"infix * tighter than +"
(pl-op-body "a + b * c")
(list
"compound"
"+"
(list
(list "atom" "a")
(list "compound" "*" (list (list "atom" "b") (list "atom" "c"))))))
(pl-op-test!
"parens override precedence"
(pl-op-body "(a + b) * c")
(list
"compound"
"*"
(list
(list "compound" "+" (list (list "atom" "a") (list "atom" "b")))
(list "atom" "c"))))
(pl-op-test!
"+ is yfx (left-assoc)"
(pl-op-body "a + b + c")
(list
"compound"
"+"
(list
(list "compound" "+" (list (list "atom" "a") (list "atom" "b")))
(list "atom" "c"))))
(pl-op-test!
"; is xfy (right-assoc)"
(pl-op-body "a ; b ; c")
(list
"compound"
";"
(list
(list "atom" "a")
(list "compound" ";" (list (list "atom" "b") (list "atom" "c"))))))
(pl-op-test!
"= folds at 700"
(pl-op-body "X = 5")
(list "compound" "=" (list (list "var" "X") (list "num" 5))))
(pl-op-test!
"is + nests via 700>500>400"
(pl-op-body "X is 2 + 3 * 4")
(list
"compound"
"is"
(list
(list "var" "X")
(list
"compound"
"+"
(list
(list "num" 2)
(list "compound" "*" (list (list "num" 3) (list "num" 4))))))))
(pl-op-test!
"< parses at 700"
(pl-op-body "2 < 3")
(list "compound" "<" (list (list "num" 2) (list "num" 3))))
(pl-op-test!
"mod parses as yfx 400"
(pl-op-body "10 mod 3")
(list "compound" "mod" (list (list "num" 10) (list "num" 3))))
(pl-op-test!
"comma in body folds right-assoc"
(pl-op-body "a, b, c")
(list
"compound"
","
(list
(list "atom" "a")
(list "compound" "," (list (list "atom" "b") (list "atom" "c"))))))
;; ── solver tests via infix ──
(pl-op-test!
"X is 2 + 3 binds X = 5"
(let
((env {}) (trail (pl-mk-trail)))
(begin
(pl-solve-once! pl-op-empty-db (pl-op-goal "X is 2 + 3" env) trail)
(pl-num-val (pl-walk-deep (dict-get env "X")))))
5)
(pl-op-test!
"infix conjunction parses + solves"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "X = 5, X = 5" {})
(pl-mk-trail))
true)
(pl-op-test!
"infix mismatch fails"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "X = 5, X = 6" {})
(pl-mk-trail))
false)
(pl-op-test!
"infix disjunction picks left"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "true ; fail" {})
(pl-mk-trail))
true)
(pl-op-test!
"2 < 5 succeeds"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "2 < 5" {})
(pl-mk-trail))
true)
(pl-op-test!
"5 < 2 fails"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "5 < 2" {})
(pl-mk-trail))
false)
(pl-op-test!
"5 >= 5 succeeds"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "5 >= 5" {})
(pl-mk-trail))
true)
(pl-op-test!
"3 =< 5 succeeds"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "3 =< 5" {})
(pl-mk-trail))
true)
(pl-op-test!
"infix < with arithmetic both sides"
(pl-solve-once!
pl-op-empty-db
(pl-op-goal "1 + 2 < 2 * 3" {})
(pl-mk-trail))
true)
(define pl-operators-tests-run! (fn () {:failed pl-op-test-fail :passed pl-op-test-pass :total pl-op-test-count :failures pl-op-test-failures}))

View File

@@ -0,0 +1,5 @@
%% append/3 list concatenation, classic Prolog
%% Two clauses: empty-prefix base case + recursive cons-prefix.
%% Bidirectional works in all modes: build, check, split.
append([], L, L).
append([H|T], L, [H|R]) :- append(T, L, R).

View File

@@ -0,0 +1,114 @@
;; lib/prolog/tests/programs/append.sx — append/3 test runner
;;
;; Mirrors the Prolog source in append.pl (embedded as a string here because
;; the SX runtime has no file-read primitive yet).
(define pl-ap-test-count 0)
(define pl-ap-test-pass 0)
(define pl-ap-test-fail 0)
(define pl-ap-test-failures (list))
(define
pl-ap-test!
(fn
(name got expected)
(begin
(set! pl-ap-test-count (+ pl-ap-test-count 1))
(if
(= got expected)
(set! pl-ap-test-pass (+ pl-ap-test-pass 1))
(begin
(set! pl-ap-test-fail (+ pl-ap-test-fail 1))
(append!
pl-ap-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-ap-term-to-sx
(fn
(t)
(cond
((pl-num? t) (pl-num-val t))
((pl-atom? t) (pl-atom-name t))
(true (list :complex)))))
(define
pl-ap-list-walked
(fn
(w)
(cond
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
(cons
(pl-ap-term-to-sx (first (pl-args w)))
(pl-ap-list-walked (nth (pl-args w) 1))))
(true (list :not-list)))))
(define pl-ap-list-to-sx (fn (t) (pl-ap-list-walked (pl-walk-deep t))))
(define
pl-ap-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define
pl-ap-prog-src
"append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R).")
(define pl-ap-db (pl-mk-db))
(pl-db-load! pl-ap-db (pl-parse pl-ap-prog-src))
(define pl-ap-env-1 {})
(define pl-ap-goal-1 (pl-ap-goal "append([], [a, b], X)" pl-ap-env-1))
(pl-solve-once! pl-ap-db pl-ap-goal-1 (pl-mk-trail))
(pl-ap-test!
"append([], [a, b], X) → X = [a, b]"
(pl-ap-list-to-sx (dict-get pl-ap-env-1 "X"))
(list "a" "b"))
(define pl-ap-env-2 {})
(define pl-ap-goal-2 (pl-ap-goal "append([1, 2], [3, 4], X)" pl-ap-env-2))
(pl-solve-once! pl-ap-db pl-ap-goal-2 (pl-mk-trail))
(pl-ap-test!
"append([1, 2], [3, 4], X) → X = [1, 2, 3, 4]"
(pl-ap-list-to-sx (dict-get pl-ap-env-2 "X"))
(list 1 2 3 4))
(pl-ap-test!
"append([1], [2, 3], [1, 2, 3]) succeeds"
(pl-solve-once!
pl-ap-db
(pl-ap-goal "append([1], [2, 3], [1, 2, 3])" {})
(pl-mk-trail))
true)
(pl-ap-test!
"append([1, 2], [3], [1, 2, 4]) fails"
(pl-solve-once!
pl-ap-db
(pl-ap-goal "append([1, 2], [3], [1, 2, 4])" {})
(pl-mk-trail))
false)
(pl-ap-test!
"append(X, Y, [1, 2, 3]) backtracks 4 times"
(pl-solve-count!
pl-ap-db
(pl-ap-goal "append(X, Y, [1, 2, 3])" {})
(pl-mk-trail))
4)
(define pl-ap-env-6 {})
(define pl-ap-goal-6 (pl-ap-goal "append(X, [3], [1, 2, 3])" pl-ap-env-6))
(pl-solve-once! pl-ap-db pl-ap-goal-6 (pl-mk-trail))
(pl-ap-test!
"append(X, [3], [1, 2, 3]) deduces X = [1, 2]"
(pl-ap-list-to-sx (dict-get pl-ap-env-6 "X"))
(list 1 2))
(define pl-append-tests-run! (fn () {:failed pl-ap-test-fail :passed pl-ap-test-pass :total pl-ap-test-count :failures pl-ap-test-failures}))

View File

@@ -0,0 +1,24 @@
%% family facts + transitive ancestor + derived relations.
%% Five-generation tree: tom -> bob -> {ann, pat} -> jim, plus tom's
%% other child liz.
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
parent(pat, jim).
male(tom).
male(bob).
male(jim).
male(pat).
female(liz).
female(ann).
father(F, C) :- parent(F, C), male(F).
mother(M, C) :- parent(M, C), female(M).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
sibling(X, Y) :- parent(P, X), parent(P, Y), \=(X, Y).

View File

@@ -0,0 +1,116 @@
;; lib/prolog/tests/programs/family.sx — facts + ancestor + sibling relations.
(define pl-fa-test-count 0)
(define pl-fa-test-pass 0)
(define pl-fa-test-fail 0)
(define pl-fa-test-failures (list))
(define
pl-fa-test!
(fn
(name got expected)
(begin
(set! pl-fa-test-count (+ pl-fa-test-count 1))
(if
(= got expected)
(set! pl-fa-test-pass (+ pl-fa-test-pass 1))
(begin
(set! pl-fa-test-fail (+ pl-fa-test-fail 1))
(append!
pl-fa-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-fa-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define
pl-fa-prog-src
"parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(bob, pat). parent(pat, jim). male(tom). male(bob). male(jim). male(pat). female(liz). female(ann). father(F, C) :- parent(F, C), male(F). mother(M, C) :- parent(M, C), female(M). ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y). sibling(X, Y) :- parent(P, X), parent(P, Y), \\=(X, Y).")
(define pl-fa-db (pl-mk-db))
(pl-db-load! pl-fa-db (pl-parse pl-fa-prog-src))
(pl-fa-test!
"parent(tom, bob) is a fact"
(pl-solve-once!
pl-fa-db
(pl-fa-goal "parent(tom, bob)" {})
(pl-mk-trail))
true)
(pl-fa-test!
"parent(tom, ann) — not a direct parent"
(pl-solve-once!
pl-fa-db
(pl-fa-goal "parent(tom, ann)" {})
(pl-mk-trail))
false)
(pl-fa-test!
"5 parent/2 facts in total"
(pl-solve-count!
pl-fa-db
(pl-fa-goal "parent(X, Y)" {})
(pl-mk-trail))
5)
(pl-fa-test!
"ancestor(tom, jim) — three-step transitive"
(pl-solve-once!
pl-fa-db
(pl-fa-goal "ancestor(tom, jim)" {})
(pl-mk-trail))
true)
(pl-fa-test!
"tom has 5 ancestors-of: bob, liz, ann, pat, jim"
(pl-solve-count!
pl-fa-db
(pl-fa-goal "ancestor(tom, X)" {})
(pl-mk-trail))
5)
(pl-fa-test!
"father(bob, ann) succeeds"
(pl-solve-once!
pl-fa-db
(pl-fa-goal "father(bob, ann)" {})
(pl-mk-trail))
true)
(pl-fa-test!
"father(liz, ann) fails (liz is female)"
(pl-solve-once!
pl-fa-db
(pl-fa-goal "father(liz, ann)" {})
(pl-mk-trail))
false)
(pl-fa-test!
"mother(liz, X) fails (liz has no children)"
(pl-solve-once!
pl-fa-db
(pl-fa-goal "mother(liz, X)" {})
(pl-mk-trail))
false)
(pl-fa-test!
"sibling(ann, pat) succeeds"
(pl-solve-once!
pl-fa-db
(pl-fa-goal "sibling(ann, pat)" {})
(pl-mk-trail))
true)
(pl-fa-test!
"sibling(ann, ann) fails by \\="
(pl-solve-once!
pl-fa-db
(pl-fa-goal "sibling(ann, ann)" {})
(pl-mk-trail))
false)
(define pl-family-tests-run! (fn () {:failed pl-fa-test-fail :passed pl-fa-test-pass :total pl-fa-test-count :failures pl-fa-test-failures}))

View File

@@ -0,0 +1,4 @@
%% member/2 list membership.
%% Generates all solutions on backtracking when the element is unbound.
member(X, [X|_]).
member(X, [_|T]) :- member(X, T).

View File

@@ -0,0 +1,91 @@
;; lib/prolog/tests/programs/member.sx — member/2 generator.
(define pl-mb-test-count 0)
(define pl-mb-test-pass 0)
(define pl-mb-test-fail 0)
(define pl-mb-test-failures (list))
(define
pl-mb-test!
(fn
(name got expected)
(begin
(set! pl-mb-test-count (+ pl-mb-test-count 1))
(if
(= got expected)
(set! pl-mb-test-pass (+ pl-mb-test-pass 1))
(begin
(set! pl-mb-test-fail (+ pl-mb-test-fail 1))
(append!
pl-mb-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-mb-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-mb-prog-src "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
(define pl-mb-db (pl-mk-db))
(pl-db-load! pl-mb-db (pl-parse pl-mb-prog-src))
(pl-mb-test!
"member(2, [1, 2, 3]) succeeds"
(pl-solve-once!
pl-mb-db
(pl-mb-goal "member(2, [1, 2, 3])" {})
(pl-mk-trail))
true)
(pl-mb-test!
"member(4, [1, 2, 3]) fails"
(pl-solve-once!
pl-mb-db
(pl-mb-goal "member(4, [1, 2, 3])" {})
(pl-mk-trail))
false)
(pl-mb-test!
"member(X, []) fails"
(pl-solve-once!
pl-mb-db
(pl-mb-goal "member(X, [])" {})
(pl-mk-trail))
false)
(pl-mb-test!
"member(X, [a, b, c]) generates 3 solutions"
(pl-solve-count!
pl-mb-db
(pl-mb-goal "member(X, [a, b, c])" {})
(pl-mk-trail))
3)
(define pl-mb-env-1 {})
(define pl-mb-goal-1 (pl-mb-goal "member(X, [11, 22, 33])" pl-mb-env-1))
(pl-solve-once! pl-mb-db pl-mb-goal-1 (pl-mk-trail))
(pl-mb-test!
"member(X, [11, 22, 33]) first solution X = 11"
(pl-num-val (pl-walk-deep (dict-get pl-mb-env-1 "X")))
11)
(pl-mb-test!
"member(2, [1, 2, 3, 2, 1]) matches twice on backtrack"
(pl-solve-count!
pl-mb-db
(pl-mb-goal "member(2, [1, 2, 3, 2, 1])" {})
(pl-mk-trail))
2)
(pl-mb-test!
"member with unbound list cell unifies"
(pl-solve-once!
pl-mb-db
(pl-mb-goal "member(a, [X, b, c])" {})
(pl-mk-trail))
true)
(define pl-member-tests-run! (fn () {:failed pl-mb-test-fail :passed pl-mb-test-pass :total pl-mb-test-count :failures pl-mb-test-failures}))

View File

@@ -0,0 +1,27 @@
%% nqueens permutation-and-test formulation.
%% Caller passes the row list [1..N]; queens/2 finds N column placements
%% s.t. no two queens attack on a diagonal. Same-column attacks are
%% structurally impossible Qs is a permutation, all distinct.
%%
%% No `>/2` `</2` `=</2` built-ins yet, so range/3 is omitted; tests pass
%; the literal range list. Once the operator table lands and arithmetic
%% comparison built-ins are in, range/3 can be added.
queens(L, Qs) :- permute(L, Qs), safe(Qs).
permute([], []).
permute(L, [H|T]) :- select(H, L, R), permute(R, T).
select(X, [X|T], T).
select(X, [H|T], [H|R]) :- select(X, T, R).
safe([]).
safe([Q|Qs]) :- safe(Qs), no_attack(Q, Qs, 1).
no_attack(_, [], _).
no_attack(Q, [Q1|Qs], D) :-
is(D2, +(Q, D)),
\=(D2, Q1),
is(D3, -(Q, D)),
\=(D3, Q1),
is(D1, +(D, 1)),
no_attack(Q, Qs, D1).

View File

@@ -0,0 +1,108 @@
;; lib/prolog/tests/programs/nqueens.sx — N-queens via permute + safe.
(define pl-nq-test-count 0)
(define pl-nq-test-pass 0)
(define pl-nq-test-fail 0)
(define pl-nq-test-failures (list))
(define
pl-nq-test!
(fn
(name got expected)
(begin
(set! pl-nq-test-count (+ pl-nq-test-count 1))
(if
(= got expected)
(set! pl-nq-test-pass (+ pl-nq-test-pass 1))
(begin
(set! pl-nq-test-fail (+ pl-nq-test-fail 1))
(append!
pl-nq-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-nq-term-to-sx
(fn
(t)
(cond
((pl-num? t) (pl-num-val t))
((pl-atom? t) (pl-atom-name t))
(true (list :complex)))))
(define
pl-nq-list-walked
(fn
(w)
(cond
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
(cons
(pl-nq-term-to-sx (first (pl-args w)))
(pl-nq-list-walked (nth (pl-args w) 1))))
(true (list :not-list)))))
(define pl-nq-list-to-sx (fn (t) (pl-nq-list-walked (pl-walk-deep t))))
(define
pl-nq-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define
pl-nq-prog-src
"queens(L, Qs) :- permute(L, Qs), safe(Qs). permute([], []). permute(L, [H|T]) :- select(H, L, R), permute(R, T). select(X, [X|T], T). select(X, [H|T], [H|R]) :- select(X, T, R). safe([]). safe([Q|Qs]) :- safe(Qs), no_attack(Q, Qs, 1). no_attack(_, [], _). no_attack(Q, [Q1|Qs], D) :- is(D2, +(Q, D)), \\=(D2, Q1), is(D3, -(Q, D)), \\=(D3, Q1), is(D1, +(D, 1)), no_attack(Q, Qs, D1).")
(define pl-nq-db (pl-mk-db))
(pl-db-load! pl-nq-db (pl-parse pl-nq-prog-src))
(pl-nq-test!
"queens([1], Qs) → 1 solution"
(pl-solve-count!
pl-nq-db
(pl-nq-goal "queens([1], Qs)" {})
(pl-mk-trail))
1)
(pl-nq-test!
"queens([1, 2], Qs) → 0 solutions"
(pl-solve-count!
pl-nq-db
(pl-nq-goal "queens([1, 2], Qs)" {})
(pl-mk-trail))
0)
(pl-nq-test!
"queens([1, 2, 3], Qs) → 0 solutions"
(pl-solve-count!
pl-nq-db
(pl-nq-goal "queens([1, 2, 3], Qs)" {})
(pl-mk-trail))
0)
(pl-nq-test!
"queens([1, 2, 3, 4], Qs) → 2 solutions"
(pl-solve-count!
pl-nq-db
(pl-nq-goal "queens([1, 2, 3, 4], Qs)" {})
(pl-mk-trail))
2)
(pl-nq-test!
"queens([1, 2, 3, 4, 5], Qs) → 10 solutions"
(pl-solve-count!
pl-nq-db
(pl-nq-goal "queens([1, 2, 3, 4, 5], Qs)" {})
(pl-mk-trail))
10)
(define pl-nq-env-1 {})
(define pl-nq-goal-1 (pl-nq-goal "queens([1, 2, 3, 4], Qs)" pl-nq-env-1))
(pl-solve-once! pl-nq-db pl-nq-goal-1 (pl-mk-trail))
(pl-nq-test!
"queens([1..4], Qs) first solution = [2, 4, 1, 3]"
(pl-nq-list-to-sx (dict-get pl-nq-env-1 "Qs"))
(list 2 4 1 3))
(define pl-nqueens-tests-run! (fn () {:failed pl-nq-test-fail :passed pl-nq-test-pass :total pl-nq-test-count :failures pl-nq-test-failures}))

View File

@@ -0,0 +1,7 @@
%% reverse/2 — naive reverse via append/3.
%% Quadratic accumulates the reversed prefix one append per cons.
reverse([], []).
reverse([H|T], R) :- reverse(T, RT), append(RT, [H], R).
append([], L, L).
append([H|T], L, [H|R]) :- append(T, L, R).

View File

@@ -0,0 +1,113 @@
;; lib/prolog/tests/programs/reverse.sx — naive reverse/2 via append/3.
;;
;; Mirrors reverse.pl (embedded as a string here).
(define pl-rv-test-count 0)
(define pl-rv-test-pass 0)
(define pl-rv-test-fail 0)
(define pl-rv-test-failures (list))
(define
pl-rv-test!
(fn
(name got expected)
(begin
(set! pl-rv-test-count (+ pl-rv-test-count 1))
(if
(= got expected)
(set! pl-rv-test-pass (+ pl-rv-test-pass 1))
(begin
(set! pl-rv-test-fail (+ pl-rv-test-fail 1))
(append!
pl-rv-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-rv-term-to-sx
(fn
(t)
(cond
((pl-num? t) (pl-num-val t))
((pl-atom? t) (pl-atom-name t))
(true (list :complex)))))
(define
pl-rv-list-walked
(fn
(w)
(cond
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
(cons
(pl-rv-term-to-sx (first (pl-args w)))
(pl-rv-list-walked (nth (pl-args w) 1))))
(true (list :not-list)))))
(define pl-rv-list-to-sx (fn (t) (pl-rv-list-walked (pl-walk-deep t))))
(define
pl-rv-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define
pl-rv-prog-src
"reverse([], []). reverse([H|T], R) :- reverse(T, RT), append(RT, [H], R). append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R).")
(define pl-rv-db (pl-mk-db))
(pl-db-load! pl-rv-db (pl-parse pl-rv-prog-src))
(define pl-rv-env-1 {})
(define pl-rv-goal-1 (pl-rv-goal "reverse([], X)" pl-rv-env-1))
(pl-solve-once! pl-rv-db pl-rv-goal-1 (pl-mk-trail))
(pl-rv-test!
"reverse([], X) → X = []"
(pl-rv-list-to-sx (dict-get pl-rv-env-1 "X"))
(list))
(define pl-rv-env-2 {})
(define pl-rv-goal-2 (pl-rv-goal "reverse([1], X)" pl-rv-env-2))
(pl-solve-once! pl-rv-db pl-rv-goal-2 (pl-mk-trail))
(pl-rv-test!
"reverse([1], X) → X = [1]"
(pl-rv-list-to-sx (dict-get pl-rv-env-2 "X"))
(list 1))
(define pl-rv-env-3 {})
(define pl-rv-goal-3 (pl-rv-goal "reverse([1, 2, 3], X)" pl-rv-env-3))
(pl-solve-once! pl-rv-db pl-rv-goal-3 (pl-mk-trail))
(pl-rv-test!
"reverse([1, 2, 3], X) → X = [3, 2, 1]"
(pl-rv-list-to-sx (dict-get pl-rv-env-3 "X"))
(list 3 2 1))
(define pl-rv-env-4 {})
(define pl-rv-goal-4 (pl-rv-goal "reverse([a, b, c, d], X)" pl-rv-env-4))
(pl-solve-once! pl-rv-db pl-rv-goal-4 (pl-mk-trail))
(pl-rv-test!
"reverse([a, b, c, d], X) → X = [d, c, b, a]"
(pl-rv-list-to-sx (dict-get pl-rv-env-4 "X"))
(list "d" "c" "b" "a"))
(pl-rv-test!
"reverse([1, 2, 3], [3, 2, 1]) succeeds"
(pl-solve-once!
pl-rv-db
(pl-rv-goal "reverse([1, 2, 3], [3, 2, 1])" {})
(pl-mk-trail))
true)
(pl-rv-test!
"reverse([1, 2], [1, 2]) fails"
(pl-solve-once!
pl-rv-db
(pl-rv-goal "reverse([1, 2], [1, 2])" {})
(pl-mk-trail))
false)
(define pl-reverse-tests-run! (fn () {:failed pl-rv-test-fail :passed pl-rv-test-pass :total pl-rv-test-count :failures pl-rv-test-failures}))

View File

@@ -0,0 +1,127 @@
;; lib/prolog/tests/query_api.sx — tests for pl-load/pl-query-all/pl-query-one/pl-query
(define pl-qa-test-count 0)
(define pl-qa-test-pass 0)
(define pl-qa-test-fail 0)
(define pl-qa-test-failures (list))
(define
pl-qa-test!
(fn
(name got expected)
(begin
(set! pl-qa-test-count (+ pl-qa-test-count 1))
(if
(= got expected)
(set! pl-qa-test-pass (+ pl-qa-test-pass 1))
(begin
(set! pl-qa-test-fail (+ pl-qa-test-fail 1))
(append!
pl-qa-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-qa-src
"parent(tom, bob). parent(tom, liz). parent(bob, ann). ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).")
(define pl-qa-db (pl-load pl-qa-src))
;; ── pl-load ──
(pl-qa-test!
"pl-load returns a usable DB (pl-query-all non-nil)"
(not (nil? pl-qa-db))
true)
;; ── pl-query-all: basic fact lookup ──
(pl-qa-test!
"query-all parent(tom, X): 2 solutions"
(len (pl-query-all pl-qa-db "parent(tom, X)"))
2)
(pl-qa-test!
"query-all parent(tom, X): first solution X=bob"
(dict-get (first (pl-query-all pl-qa-db "parent(tom, X)")) "X")
"bob")
(pl-qa-test!
"query-all parent(tom, X): second solution X=liz"
(dict-get (nth (pl-query-all pl-qa-db "parent(tom, X)") 1) "X")
"liz")
;; ── pl-query-all: no solutions ──
(pl-qa-test!
"query-all no solutions returns empty list"
(pl-query-all pl-qa-db "parent(liz, X)")
(list))
;; ── pl-query-all: boolean query (no vars) ──
(pl-qa-test!
"boolean success: 1 solution (empty dict)"
(len (pl-query-all pl-qa-db "parent(tom, bob)"))
1)
(pl-qa-test!
"boolean success: solution has no bindings"
(empty? (keys (first (pl-query-all pl-qa-db "parent(tom, bob)"))))
true)
(pl-qa-test!
"boolean fail: 0 solutions"
(len (pl-query-all pl-qa-db "parent(bob, tom)"))
0)
;; ── pl-query-all: multi-var ──
(pl-qa-test!
"query-all parent(X, Y): 3 solutions total"
(len (pl-query-all pl-qa-db "parent(X, Y)"))
3)
;; ── pl-query-all: rule-based (ancestor/2) ──
(pl-qa-test!
"query-all ancestor(tom, X): 3 descendants (bob, liz, ann)"
(len (pl-query-all pl-qa-db "ancestor(tom, X)"))
3)
;; ── pl-query-all: built-in in query ──
(pl-qa-test!
"query with is/2 built-in"
(dict-get (first (pl-query-all pl-qa-db "X is 2 + 3")) "X")
"5")
;; ── pl-query-one ──
(pl-qa-test!
"query-one returns first solution"
(dict-get (pl-query-one pl-qa-db "parent(tom, X)") "X")
"bob")
(pl-qa-test!
"query-one returns nil for no solutions"
(pl-query-one pl-qa-db "parent(liz, X)")
nil)
;; ── pl-query convenience ──
(pl-qa-test!
"pl-query convenience: count solutions"
(len (pl-query "likes(alice, bob). likes(alice, carol)." "likes(alice, X)"))
2)
(pl-qa-test!
"pl-query convenience: first solution"
(dict-get (first (pl-query "likes(alice, bob). likes(alice, carol)." "likes(alice, X)")) "X")
"bob")
(pl-qa-test!
"pl-query with empty source (built-ins only)"
(dict-get (first (pl-query "" "X is 6 * 7")) "X")
"42")
(define pl-query-api-tests-run! (fn () {:failed pl-qa-test-fail :passed pl-qa-test-pass :total pl-qa-test-count :failures pl-qa-test-failures}))

View File

@@ -0,0 +1,195 @@
;; lib/prolog/tests/set_predicates.sx — foldl/4, list_to_set/2, intersection/3, subtract/3, union/3
(define pl-sp-test-count 0)
(define pl-sp-test-pass 0)
(define pl-sp-test-fail 0)
(define pl-sp-test-failures (list))
(define
pl-sp-test!
(fn
(name got expected)
(begin
(set! pl-sp-test-count (+ pl-sp-test-count 1))
(if
(= got expected)
(set! pl-sp-test-pass (+ pl-sp-test-pass 1))
(begin
(set! pl-sp-test-fail (+ pl-sp-test-fail 1))
(append!
pl-sp-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-sp-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
;; DB with add/3 for foldl tests
(define pl-sp-db (pl-mk-db))
(pl-db-load! pl-sp-db (pl-parse "add(X, Acc, NAcc) :- NAcc is Acc + X."))
;; ── foldl/4 ────────────────────────────────────────────────────────
(define pl-sp-env-fl1 {:S (pl-mk-rt-var "S")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "foldl(add, [1,2,3,4], 0, S)" pl-sp-env-fl1)
(pl-mk-trail))
(pl-sp-test!
"foldl(add,[1,2,3,4],0,S) -> S=10"
(pl-num-val (pl-walk-deep (dict-get pl-sp-env-fl1 "S")))
10)
(define pl-sp-env-fl2 {:S (pl-mk-rt-var "S")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "foldl(add, [], 5, S)" pl-sp-env-fl2)
(pl-mk-trail))
(pl-sp-test!
"foldl(add,[],5,S) -> S=5"
(pl-num-val (pl-walk-deep (dict-get pl-sp-env-fl2 "S")))
5)
(define pl-sp-env-fl3 {:S (pl-mk-rt-var "S")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "foldl(add, [1,2,3], 0, S)" pl-sp-env-fl3)
(pl-mk-trail))
(pl-sp-test!
"foldl(add,[1,2,3],0,S) -> S=6"
(pl-num-val (pl-walk-deep (dict-get pl-sp-env-fl3 "S")))
6)
;; ── list_to_set/2 ──────────────────────────────────────────────────
(define pl-sp-env-lts1 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "list_to_set([1,2,3,2,1], R)" pl-sp-env-lts1)
(pl-mk-trail))
(pl-sp-test!
"list_to_set([1,2,3,2,1],R) -> [1,2,3]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-lts1 "R")))
".(1, .(2, .(3, [])))")
(define pl-sp-env-lts2 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "list_to_set([], R)" pl-sp-env-lts2)
(pl-mk-trail))
(pl-sp-test!
"list_to_set([],R) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-lts2 "R")))
"[]")
(define pl-sp-env-lts3 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "list_to_set([a,b,a,c], R)" pl-sp-env-lts3)
(pl-mk-trail))
(pl-sp-test!
"list_to_set([a,b,a,c],R) -> [a,b,c]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-lts3 "R")))
".(a, .(b, .(c, [])))")
;; ── intersection/3 ─────────────────────────────────────────────────
(define pl-sp-env-int1 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "intersection([1,2,3,4], [2,4,6], R)" pl-sp-env-int1)
(pl-mk-trail))
(pl-sp-test!
"intersection([1,2,3,4],[2,4,6],R) -> [2,4]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-int1 "R")))
".(2, .(4, []))")
(define pl-sp-env-int2 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "intersection([1,2,3], [4,5,6], R)" pl-sp-env-int2)
(pl-mk-trail))
(pl-sp-test!
"intersection([1,2,3],[4,5,6],R) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-int2 "R")))
"[]")
(define pl-sp-env-int3 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "intersection([], [1,2,3], R)" pl-sp-env-int3)
(pl-mk-trail))
(pl-sp-test!
"intersection([],[1,2,3],R) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-int3 "R")))
"[]")
;; ── subtract/3 ─────────────────────────────────────────────────────
(define pl-sp-env-sub1 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "subtract([1,2,3,4], [2,4], R)" pl-sp-env-sub1)
(pl-mk-trail))
(pl-sp-test!
"subtract([1,2,3,4],[2,4],R) -> [1,3]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-sub1 "R")))
".(1, .(3, []))")
(define pl-sp-env-sub2 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "subtract([1,2,3], [], R)" pl-sp-env-sub2)
(pl-mk-trail))
(pl-sp-test!
"subtract([1,2,3],[],R) -> [1,2,3]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-sub2 "R")))
".(1, .(2, .(3, [])))")
(define pl-sp-env-sub3 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "subtract([], [1,2], R)" pl-sp-env-sub3)
(pl-mk-trail))
(pl-sp-test!
"subtract([],[1,2],R) -> []"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-sub3 "R")))
"[]")
;; ── union/3 ────────────────────────────────────────────────────────
(define pl-sp-env-uni1 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "union([1,2,3], [2,3,4], R)" pl-sp-env-uni1)
(pl-mk-trail))
(pl-sp-test!
"union([1,2,3],[2,3,4],R) -> [1,2,3,4]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-uni1 "R")))
".(1, .(2, .(3, .(4, []))))")
(define pl-sp-env-uni2 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "union([], [1,2], R)" pl-sp-env-uni2)
(pl-mk-trail))
(pl-sp-test!
"union([],[1,2],R) -> [1,2]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-uni2 "R")))
".(1, .(2, []))")
(define pl-sp-env-uni3 {:R (pl-mk-rt-var "R")})
(pl-solve-once!
pl-sp-db
(pl-sp-goal "union([1,2], [], R)" pl-sp-env-uni3)
(pl-mk-trail))
(pl-sp-test!
"union([1,2],[],R) -> [1,2]"
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-uni3 "R")))
".(1, .(2, []))")
;; ── Runner ─────────────────────────────────────────────────────────
(define pl-set-predicates-tests-run! (fn () {:failed pl-sp-test-fail :passed pl-sp-test-pass :total pl-sp-test-count :failures pl-sp-test-failures}))

618
lib/prolog/tests/solve.sx Normal file
View File

@@ -0,0 +1,618 @@
;; lib/prolog/tests/solve.sx — DFS solver unit tests
(define pl-s-test-count 0)
(define pl-s-test-pass 0)
(define pl-s-test-fail 0)
(define pl-s-test-failures (list))
(define
pl-s-test!
(fn
(name got expected)
(begin
(set! pl-s-test-count (+ pl-s-test-count 1))
(if
(= got expected)
(set! pl-s-test-pass (+ pl-s-test-pass 1))
(begin
(set! pl-s-test-fail (+ pl-s-test-fail 1))
(append!
pl-s-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-s-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-s-empty-db (pl-mk-db))
(pl-s-test!
"true succeeds"
(pl-solve-once! pl-s-empty-db (pl-s-goal "true" {}) (pl-mk-trail))
true)
(pl-s-test!
"fail fails"
(pl-solve-once! pl-s-empty-db (pl-s-goal "fail" {}) (pl-mk-trail))
false)
(pl-s-test!
"= identical atoms"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "=(a, a)" {})
(pl-mk-trail))
true)
(pl-s-test!
"= different atoms"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "=(a, b)" {})
(pl-mk-trail))
false)
(pl-s-test!
"= var to atom"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "=(X, foo)" {})
(pl-mk-trail))
true)
(define pl-s-env-bind {})
(define pl-s-trail-bind (pl-mk-trail))
(define pl-s-goal-bind (pl-s-goal "=(X, foo)" pl-s-env-bind))
(pl-solve-once! pl-s-empty-db pl-s-goal-bind pl-s-trail-bind)
(pl-s-test!
"X bound to foo after =(X, foo)"
(pl-atom-name (pl-walk-deep (dict-get pl-s-env-bind "X")))
"foo")
(pl-s-test!
"true , true succeeds"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "true, true" {})
(pl-mk-trail))
true)
(pl-s-test!
"true , fail fails"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "true, fail" {})
(pl-mk-trail))
false)
(pl-s-test!
"consistent X bindings succeed"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "=(X, a), =(X, a)" {})
(pl-mk-trail))
true)
(pl-s-test!
"conflicting X bindings fail"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "=(X, a), =(X, b)" {})
(pl-mk-trail))
false)
(define pl-s-db1 (pl-mk-db))
(pl-db-load!
pl-s-db1
(pl-parse "parent(tom, bob). parent(bob, liz). parent(bob, ann)."))
(pl-s-test!
"fact lookup hit"
(pl-solve-once!
pl-s-db1
(pl-s-goal "parent(tom, bob)" {})
(pl-mk-trail))
true)
(pl-s-test!
"fact lookup miss"
(pl-solve-once!
pl-s-db1
(pl-s-goal "parent(tom, liz)" {})
(pl-mk-trail))
false)
(pl-s-test!
"all parent solutions"
(pl-solve-count!
pl-s-db1
(pl-s-goal "parent(X, Y)" {})
(pl-mk-trail))
3)
(pl-s-test!
"fixed first arg solutions"
(pl-solve-count!
pl-s-db1
(pl-s-goal "parent(bob, Y)" {})
(pl-mk-trail))
2)
(define pl-s-db2 (pl-mk-db))
(pl-db-load!
pl-s-db2
(pl-parse
"parent(tom, bob). parent(bob, ann). ancestor(X, Y) :- parent(X, Y). ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))
(pl-s-test!
"rule direct ancestor"
(pl-solve-once!
pl-s-db2
(pl-s-goal "ancestor(tom, bob)" {})
(pl-mk-trail))
true)
(pl-s-test!
"rule transitive ancestor"
(pl-solve-once!
pl-s-db2
(pl-s-goal "ancestor(tom, ann)" {})
(pl-mk-trail))
true)
(pl-s-test!
"rule no path"
(pl-solve-once!
pl-s-db2
(pl-s-goal "ancestor(ann, tom)" {})
(pl-mk-trail))
false)
(define pl-s-env-undo {})
(define pl-s-trail-undo (pl-mk-trail))
(define pl-s-goal-undo (pl-s-goal "=(X, a), fail" pl-s-env-undo))
(pl-solve-once! pl-s-empty-db pl-s-goal-undo pl-s-trail-undo)
(pl-s-test!
"trail undone after failure leaves X unbound"
(pl-var-bound? (dict-get pl-s-env-undo "X"))
false)
(define pl-s-db-cut1 (pl-mk-db))
(pl-db-load! pl-s-db-cut1 (pl-parse "g :- !. g :- true."))
(pl-s-test!
"bare cut succeeds"
(pl-solve-once! pl-s-db-cut1 (pl-s-goal "g" {}) (pl-mk-trail))
true)
(pl-s-test!
"cut commits to first matching clause"
(pl-solve-count! pl-s-db-cut1 (pl-s-goal "g" {}) (pl-mk-trail))
1)
(define pl-s-db-cut2 (pl-mk-db))
(pl-db-load! pl-s-db-cut2 (pl-parse "a(1). a(2). g(X) :- a(X), !."))
(pl-s-test!
"cut commits to first a solution"
(pl-solve-count! pl-s-db-cut2 (pl-s-goal "g(X)" {}) (pl-mk-trail))
1)
(define pl-s-db-cut3 (pl-mk-db))
(pl-db-load!
pl-s-db-cut3
(pl-parse "a(1). a(2). g(X) :- a(X), !, fail. g(99)."))
(pl-s-test!
"cut then fail blocks alt clauses"
(pl-solve-count! pl-s-db-cut3 (pl-s-goal "g(X)" {}) (pl-mk-trail))
0)
(define pl-s-db-cut4 (pl-mk-db))
(pl-db-load!
pl-s-db-cut4
(pl-parse "a(1). b(10). b(20). g(X, Y) :- a(X), !, b(Y)."))
(pl-s-test!
"post-cut goal backtracks freely"
(pl-solve-count!
pl-s-db-cut4
(pl-s-goal "g(X, Y)" {})
(pl-mk-trail))
2)
(define pl-s-db-cut5 (pl-mk-db))
(pl-db-load!
pl-s-db-cut5
(pl-parse "r(1). r(2). q :- r(X), !. p :- q. p :- true."))
(pl-s-test!
"inner cut does not commit outer predicate"
(pl-solve-count! pl-s-db-cut5 (pl-s-goal "p" {}) (pl-mk-trail))
2)
(pl-s-test!
"\\= different atoms succeeds"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "\\=(a, b)" {})
(pl-mk-trail))
true)
(pl-s-test!
"\\= same atoms fails"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "\\=(a, a)" {})
(pl-mk-trail))
false)
(pl-s-test!
"\\= var-vs-atom would unify so fails"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "\\=(X, a)" {})
(pl-mk-trail))
false)
(define pl-s-env-ne {})
(define pl-s-trail-ne (pl-mk-trail))
(define pl-s-goal-ne (pl-s-goal "\\=(X, a)" pl-s-env-ne))
(pl-solve-once! pl-s-empty-db pl-s-goal-ne pl-s-trail-ne)
(pl-s-test!
"\\= leaves no bindings"
(pl-var-bound? (dict-get pl-s-env-ne "X"))
false)
(pl-s-test!
"; left succeeds"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal ";(true, fail)" {})
(pl-mk-trail))
true)
(pl-s-test!
"; right succeeds when left fails"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal ";(fail, true)" {})
(pl-mk-trail))
true)
(pl-s-test!
"; both fail"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal ";(fail, fail)" {})
(pl-mk-trail))
false)
(pl-s-test!
"; both branches counted"
(pl-solve-count!
pl-s-empty-db
(pl-s-goal ";(true, true)" {})
(pl-mk-trail))
2)
(define pl-s-db-call (pl-mk-db))
(pl-db-load! pl-s-db-call (pl-parse "p(1). p(2)."))
(pl-s-test!
"call(true) succeeds"
(pl-solve-once!
pl-s-db-call
(pl-s-goal "call(true)" {})
(pl-mk-trail))
true)
(pl-s-test!
"call(p(X)) yields all solutions"
(pl-solve-count!
pl-s-db-call
(pl-s-goal "call(p(X))" {})
(pl-mk-trail))
2)
(pl-s-test!
"call of bound goal var resolves"
(pl-solve-once!
pl-s-db-call
(pl-s-goal "=(G, true), call(G)" {})
(pl-mk-trail))
true)
(define pl-s-db-ite (pl-mk-db))
(pl-db-load! pl-s-db-ite (pl-parse "p(1). p(2). q(yes). q(no)."))
(pl-s-test!
"if-then-else: cond true → then runs"
(pl-solve-once!
pl-s-db-ite
(pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" {})
(pl-mk-trail))
true)
(define pl-s-env-ite1 {})
(pl-solve-once!
pl-s-db-ite
(pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" pl-s-env-ite1)
(pl-mk-trail))
(pl-s-test!
"if-then-else: cond true binds via then"
(pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite1 "X")))
"ok")
(pl-s-test!
"if-then-else: cond false → else"
(pl-solve-once!
pl-s-db-ite
(pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" {})
(pl-mk-trail))
true)
(define pl-s-env-ite2 {})
(pl-solve-once!
pl-s-db-ite
(pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" pl-s-env-ite2)
(pl-mk-trail))
(pl-s-test!
"if-then-else: cond false binds via else"
(pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite2 "X")))
"fallback")
(pl-s-test!
"if-then-else: cond commits to first solution (count = 1)"
(pl-solve-count!
pl-s-db-ite
(pl-s-goal ";(->(p(X), =(Y, found)), =(Y, none))" {})
(pl-mk-trail))
1)
(pl-s-test!
"if-then-else: then can backtrack"
(pl-solve-count!
pl-s-db-ite
(pl-s-goal ";(->(true, p(X)), =(X, none))" {})
(pl-mk-trail))
2)
(pl-s-test!
"if-then-else: else can backtrack"
(pl-solve-count!
pl-s-db-ite
(pl-s-goal ";(->(fail, =(X, ignored)), p(X))" {})
(pl-mk-trail))
2)
(pl-s-test!
"standalone -> with true cond succeeds"
(pl-solve-once!
pl-s-db-ite
(pl-s-goal "->(true, =(X, hi))" {})
(pl-mk-trail))
true)
(pl-s-test!
"standalone -> with false cond fails"
(pl-solve-once!
pl-s-db-ite
(pl-s-goal "->(fail, =(X, hi))" {})
(pl-mk-trail))
false)
(pl-s-test!
"write(hello)"
(begin
(pl-output-clear!)
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "write(hello)" {})
(pl-mk-trail))
pl-output-buffer)
"hello")
(pl-s-test!
"nl outputs newline"
(begin
(pl-output-clear!)
(pl-solve-once! pl-s-empty-db (pl-s-goal "nl" {}) (pl-mk-trail))
pl-output-buffer)
"\n")
(pl-s-test!
"write(42) outputs digits"
(begin
(pl-output-clear!)
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "write(42)" {})
(pl-mk-trail))
pl-output-buffer)
"42")
(pl-s-test!
"write(foo(a, b)) formats compound"
(begin
(pl-output-clear!)
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "write(foo(a, b))" {})
(pl-mk-trail))
pl-output-buffer)
"foo(a, b)")
(pl-s-test!
"write conjunction"
(begin
(pl-output-clear!)
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "write(a), write(b)" {})
(pl-mk-trail))
pl-output-buffer)
"ab")
(pl-s-test!
"write of bound var walks binding"
(begin
(pl-output-clear!)
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "=(X, hello), write(X)" {})
(pl-mk-trail))
pl-output-buffer)
"hello")
(pl-s-test!
"write then nl"
(begin
(pl-output-clear!)
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "write(hi), nl" {})
(pl-mk-trail))
pl-output-buffer)
"hi\n")
(define pl-s-env-arith1 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, 42)" pl-s-env-arith1)
(pl-mk-trail))
(pl-s-test!
"is(X, 42) binds X to 42"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith1 "X")))
42)
(define pl-s-env-arith2 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, +(2, 3))" pl-s-env-arith2)
(pl-mk-trail))
(pl-s-test!
"is(X, +(2, 3)) binds X to 5"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith2 "X")))
5)
(define pl-s-env-arith3 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, *(2, 3))" pl-s-env-arith3)
(pl-mk-trail))
(pl-s-test!
"is(X, *(2, 3)) binds X to 6"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith3 "X")))
6)
(define pl-s-env-arith4 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, -(10, 3))" pl-s-env-arith4)
(pl-mk-trail))
(pl-s-test!
"is(X, -(10, 3)) binds X to 7"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith4 "X")))
7)
(define pl-s-env-arith5 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, /(10, 2))" pl-s-env-arith5)
(pl-mk-trail))
(pl-s-test!
"is(X, /(10, 2)) binds X to 5"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith5 "X")))
5)
(define pl-s-env-arith6 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, mod(10, 3))" pl-s-env-arith6)
(pl-mk-trail))
(pl-s-test!
"is(X, mod(10, 3)) binds X to 1"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith6 "X")))
1)
(define pl-s-env-arith7 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, abs(-(0, 5)))" pl-s-env-arith7)
(pl-mk-trail))
(pl-s-test!
"is(X, abs(-(0, 5))) binds X to 5"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith7 "X")))
5)
(define pl-s-env-arith8 {})
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(X, +(2, *(3, 4)))" pl-s-env-arith8)
(pl-mk-trail))
(pl-s-test!
"is(X, +(2, *(3, 4))) binds X to 14 (nested)"
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith8 "X")))
14)
(pl-s-test!
"is(5, +(2, 3)) succeeds (LHS num matches)"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(5, +(2, 3))" {})
(pl-mk-trail))
true)
(pl-s-test!
"is(6, +(2, 3)) fails (LHS num mismatch)"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "is(6, +(2, 3))" {})
(pl-mk-trail))
false)
(pl-s-test!
"is propagates bound vars on RHS"
(pl-solve-once!
pl-s-empty-db
(pl-s-goal "=(Y, 4), is(X, +(Y, 1)), =(X, 5)" {})
(pl-mk-trail))
true)
(define pl-solve-tests-run! (fn () {:failed pl-s-test-fail :passed pl-s-test-pass :total pl-s-test-count :failures pl-s-test-failures}))

View File

@@ -0,0 +1,273 @@
;; lib/prolog/tests/string_agg.sx -- sub_atom/5 + aggregate_all/3
(define pl-sa-test-count 0)
(define pl-sa-test-pass 0)
(define pl-sa-test-fail 0)
(define pl-sa-test-failures (list))
(define
pl-sa-test!
(fn
(name got expected)
(begin
(set! pl-sa-test-count (+ pl-sa-test-count 1))
(if
(= got expected)
(set! pl-sa-test-pass (+ pl-sa-test-pass 1))
(begin
(set! pl-sa-test-fail (+ pl-sa-test-fail 1))
(append!
pl-sa-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-sa-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-sa-db (pl-mk-db))
(define
pl-sa-num-val
(fn (env key) (pl-num-val (pl-walk-deep (dict-get env key)))))
(define
pl-sa-list-to-atoms
(fn
(t)
(let
((w (pl-walk-deep t)))
(cond
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
(cons
(pl-atom-name (first (pl-args w)))
(pl-sa-list-to-atoms (nth (pl-args w) 1))))
(true (list))))))
(define pl-sa-prog-src "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
(pl-db-load! pl-sa-db (pl-parse pl-sa-prog-src))
;; -- sub_atom/5 --
(pl-sa-test!
"sub_atom ground: sub_atom(abcde,0,3,2,abc)"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "sub_atom(abcde, 0, 3, 2, abc)" {})
(pl-mk-trail))
true)
(pl-sa-test!
"sub_atom ground: sub_atom(abcde,2,2,1,cd)"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "sub_atom(abcde, 2, 2, 1, cd)" {})
(pl-mk-trail))
true)
(pl-sa-test!
"sub_atom ground mismatch fails"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "sub_atom(abcde, 0, 2, 3, cd)" {})
(pl-mk-trail))
false)
(pl-sa-test!
"sub_atom empty sub at start"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "sub_atom(abcde, 0, 0, 5, '')" {})
(pl-mk-trail))
true)
(pl-sa-test!
"sub_atom whole string"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "sub_atom(hello, 0, 5, 0, hello)" {})
(pl-mk-trail))
true)
(define pl-sa-env-b1 {})
(pl-solve-once!
pl-sa-db
(pl-sa-goal "sub_atom(abcde, B, 2, A, cd)" pl-sa-env-b1)
(pl-mk-trail))
(pl-sa-test!
"sub_atom bound SubAtom gives B=2"
(pl-sa-num-val pl-sa-env-b1 "B")
2)
(pl-sa-test!
"sub_atom bound SubAtom gives A=1"
(pl-sa-num-val pl-sa-env-b1 "A")
1)
(define pl-sa-env-b2 {})
(pl-solve-once!
pl-sa-db
(pl-sa-goal "sub_atom(hello, B, L, A, ello)" pl-sa-env-b2)
(pl-mk-trail))
(pl-sa-test! "sub_atom ello: B=1" (pl-sa-num-val pl-sa-env-b2 "B") 1)
(pl-sa-test! "sub_atom ello: L=4" (pl-sa-num-val pl-sa-env-b2 "L") 4)
(pl-sa-test! "sub_atom ello: A=0" (pl-sa-num-val pl-sa-env-b2 "A") 0)
(pl-sa-test!
"sub_atom ab: 6 total solutions"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(count, sub_atom(ab, _, _, _, _), N)" env)
(pl-mk-trail))
(pl-sa-num-val env "N"))
6)
(pl-sa-test!
"sub_atom a: 3 total solutions"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(count, sub_atom(a, _, _, _, _), N)" env)
(pl-mk-trail))
(pl-sa-num-val env "N"))
3)
;; -- aggregate_all/3 --
(pl-sa-test!
"aggregate_all count member [a,b,c] = 3"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(count, member(_, [a,b,c]), N)" env)
(pl-mk-trail))
(pl-sa-num-val env "N"))
3)
(pl-sa-test!
"aggregate_all count fail = 0"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(count, fail, N)" env)
(pl-mk-trail))
(pl-sa-num-val env "N"))
0)
(pl-sa-test!
"aggregate_all count always succeeds"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(count, fail, _)" {})
(pl-mk-trail))
true)
(define pl-sa-env-bag1 {})
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(bag(X), member(X, [a,b,c]), L)" pl-sa-env-bag1)
(pl-mk-trail))
(pl-sa-test!
"aggregate_all bag [a,b,c]"
(pl-sa-list-to-atoms (dict-get pl-sa-env-bag1 "L"))
(list "a" "b" "c"))
(define pl-sa-env-bag2 {})
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(bag(X), member(X, []), L)" pl-sa-env-bag2)
(pl-mk-trail))
(pl-sa-test!
"aggregate_all bag empty goal = []"
(pl-sa-list-to-atoms (dict-get pl-sa-env-bag2 "L"))
(list))
(pl-sa-test!
"aggregate_all sum [1,2,3,4] = 10"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(sum(X), member(X, [1,2,3,4]), S)" env)
(pl-mk-trail))
(pl-sa-num-val env "S"))
10)
(pl-sa-test!
"aggregate_all max [3,1,4,1,5,9,2,6] = 9"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(max(X), member(X, [3,1,4,1,5,9,2,6]), M)" env)
(pl-mk-trail))
(pl-sa-num-val env "M"))
9)
(pl-sa-test!
"aggregate_all max empty fails"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(max(X), member(X, []), M)" {})
(pl-mk-trail))
false)
(pl-sa-test!
"aggregate_all min [3,1,4,1,5,9,2,6] = 1"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(min(X), member(X, [3,1,4,1,5,9,2,6]), M)" env)
(pl-mk-trail))
(pl-sa-num-val env "M"))
1)
(pl-sa-test!
"aggregate_all min empty fails"
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(min(X), member(X, []), M)" {})
(pl-mk-trail))
false)
(define pl-sa-env-set1 {})
(pl-solve-once!
pl-sa-db
(pl-sa-goal
"aggregate_all(set(X), member(X, [b,a,c,a,b]), S)"
pl-sa-env-set1)
(pl-mk-trail))
(pl-sa-test!
"aggregate_all set [b,a,c,a,b] = [a,b,c]"
(pl-sa-list-to-atoms (dict-get pl-sa-env-set1 "S"))
(list "a" "b" "c"))
(define pl-sa-env-set2 {})
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(set(X), fail, S)" pl-sa-env-set2)
(pl-mk-trail))
(pl-sa-test!
"aggregate_all set fail = []"
(pl-sa-list-to-atoms (dict-get pl-sa-env-set2 "S"))
(list))
(pl-sa-test!
"aggregate_all sum empty = 0"
(let
((env {}))
(pl-solve-once!
pl-sa-db
(pl-sa-goal "aggregate_all(sum(X), fail, S)" env)
(pl-mk-trail))
(pl-sa-num-val env "S"))
0)
(define pl-string-agg-tests-run! (fn () {:failed pl-sa-test-fail :passed pl-sa-test-pass :total pl-sa-test-count :failures pl-sa-test-failures}))

View File

@@ -0,0 +1,147 @@
;; lib/prolog/tests/term_inspect.sx — copy_term/2, functor/3, arg/3.
(define pl-tt-test-count 0)
(define pl-tt-test-pass 0)
(define pl-tt-test-fail 0)
(define pl-tt-test-failures (list))
(define
pl-tt-test!
(fn
(name got expected)
(begin
(set! pl-tt-test-count (+ pl-tt-test-count 1))
(if
(= got expected)
(set! pl-tt-test-pass (+ pl-tt-test-pass 1))
(begin
(set! pl-tt-test-fail (+ pl-tt-test-fail 1))
(append!
pl-tt-test-failures
(str name "\n expected: " expected "\n got: " got)))))))
(define
pl-tt-goal
(fn
(src env)
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
(define pl-tt-db (pl-mk-db))
;; ── copy_term/2 ──
(pl-tt-test!
"copy_term ground compound succeeds + copy = original"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "copy_term(foo(a, b), X), X = foo(a, b)" {})
(pl-mk-trail))
true)
(pl-tt-test!
"copy_term preserves var aliasing in source"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "copy_term(p(Y, Y), p(A, B)), A = 5, B = 5" {})
(pl-mk-trail))
true)
(pl-tt-test!
"copy_term distinct vars stay distinct"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "copy_term(p(Y, Y), p(A, B)), A = 5, B = 6" {})
(pl-mk-trail))
false)
(define pl-tt-env-1 {})
(pl-solve-once!
pl-tt-db
(pl-tt-goal "copy_term(X, Y), Y = 5" pl-tt-env-1)
(pl-mk-trail))
(pl-tt-test!
"copy_term: binding the copy doesn't bind the source"
(pl-var-bound? (dict-get pl-tt-env-1 "X"))
false)
;; ── functor/3 ──
(define pl-tt-env-2 {})
(pl-solve-once!
pl-tt-db
(pl-tt-goal "functor(foo(a, b, c), F, N)" pl-tt-env-2)
(pl-mk-trail))
(pl-tt-test!
"functor of compound: F = foo"
(pl-atom-name (pl-walk-deep (dict-get pl-tt-env-2 "F")))
"foo")
(pl-tt-test!
"functor of compound: N = 3"
(pl-num-val (pl-walk-deep (dict-get pl-tt-env-2 "N")))
3)
(define pl-tt-env-3 {})
(pl-solve-once!
pl-tt-db
(pl-tt-goal "functor(hello, F, N)" pl-tt-env-3)
(pl-mk-trail))
(pl-tt-test!
"functor of atom: F = hello"
(pl-atom-name (pl-walk-deep (dict-get pl-tt-env-3 "F")))
"hello")
(pl-tt-test!
"functor of atom: N = 0"
(pl-num-val (pl-walk-deep (dict-get pl-tt-env-3 "N")))
0)
(pl-tt-test!
"functor construct compound: T unifies with foo(a, b)"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "functor(T, foo, 2), T = foo(a, b)" {})
(pl-mk-trail))
true)
(pl-tt-test!
"functor construct atom: T = hello"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "functor(T, hello, 0), T = hello" {})
(pl-mk-trail))
true)
;; ── arg/3 ──
(pl-tt-test!
"arg(1, foo(a, b, c), a)"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "arg(1, foo(a, b, c), a)" {})
(pl-mk-trail))
true)
(pl-tt-test!
"arg(2, foo(a, b, c), X) → X = b"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "arg(2, foo(a, b, c), X), X = b" {})
(pl-mk-trail))
true)
(pl-tt-test!
"arg out-of-range high fails"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "arg(4, foo(a, b, c), X)" {})
(pl-mk-trail))
false)
(pl-tt-test!
"arg(0, ...) fails (1-indexed)"
(pl-solve-once!
pl-tt-db
(pl-tt-goal "arg(0, foo(a), X)" {})
(pl-mk-trail))
false)
(define pl-term-inspect-tests-run! (fn () {:failed pl-tt-test-fail :passed pl-tt-test-pass :total pl-tt-test-count :failures pl-tt-test-failures}))

View File

@@ -1,81 +0,0 @@
# apl-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/apl-on-sx.md` forever. Rank-polymorphic primitives + 6 operators on the JIT is the headline showcase — APL is the densest combinator algebra you can put on top of a primitive table. Every program is `array → array` pure pipelines, exactly what the JIT was built for.
```
description: apl-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/apl-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/apl-on-sx.md` — roadmap + Progress log.
2. `ls lib/apl/` — pick up from the most advanced file.
3. If `lib/apl/tests/*.sx` exist, run them. Green before new work.
4. If `lib/apl/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/apl-on-sx.md`:
- **Phase 1** — tokenizer + parser. Unicode glyphs, `¯` for negative, strands (juxtaposition), right-to-left, valence resolution by syntactic position
- **Phase 2** — array model + scalar primitives. `make-array {shape, ravel}`, scalar promotion, broadcast for `+ - × ÷ ⌈ ⌊ * ⍟ | ! ○`, comparison, logical, ``, `⎕IO`
- **Phase 3** — structural primitives + indexing. ` , ⍉ ↑ ↓ ⌽ ⊖ ⌷ ⍋ ⍒ ⊂ ⊃ ∊`
- **Phase 4** — **THE SHOWCASE**: operators. `f/` (reduce), `f¨` (each), `∘.f` (outer), `f.g` (inner), `f⍨` (commute), `f∘g` (compose), `f⍣n` (power), `f⍤k` (rank), `@` (at)
- **Phase 5** — dfns + tradfns + control flow. `{+⍵}`, `∇` recurse, `←default`, tradfn header, `:If/:While/:For/:Select`
- **Phase 6** — classic programs (life, mandelbrot, primes, n-queens, quicksort) + idiom corpus + drive to 100+
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/apl/**` and `plans/apl-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. APL primitives go in `lib/apl/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Unicode in `.sx`:** raw UTF-8 only, never `\uXXXX` escapes. Glyphs land directly in source.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## APL-specific gotchas
- **Right-to-left, no precedence among functions.** `2 × 3 + 4` is `2 × (3 + 4)` = 14, not 10. Operators bind tighter than functions: `+/ 5` is `+/(5)`, and `2 +.× 3 4` is `2 (+.×) 3 4`.
- **Valence by position.** `-3` is monadic negate (`-` with no left arg). `5-3` is dyadic subtract. The parser must look left to decide. Same glyph; different fn.
- **`¯` is part of a number literal**, not a prefix function. `¯3` is the literal negative three; `-3` is the function call. Tokenizer eats `¯` into the numeric token.
- **Strands.** `1 2 3` is a 3-element vector, not three separate calls. Adjacent literals fuse into a strand at parse time. Adjacent names do *not* fuse — `a b c` is three separate references.
- **Scalar promotion.** `1 + 2 3 4``3 4 5`. Any scalar broadcasts against any-rank conformable shape.
- **Conformability** = exactly matching shapes, OR one side scalar, OR (in some dialects) one side rank-1 cycling against rank-N. Keep strict in v1: matching shape or scalar only.
- **`` is overloaded.** Monadic `N` = vector 1..N (or 0..N-1 if `⎕IO=0`). Dyadic `V W` = first-index lookup, returns `≢V+1` for not-found.
- **Reduce with `+/0`** = `0` (identity for `+`). Each scalar primitive has a defined identity used by reduce-on-empty. Don't crash; return identity.
- **Reduce direction.** `f/` reduces the *last* axis. `f⌿` reduces the *first*. Matters for matrices.
- **Indexing is 1-based** by default (`⎕IO=1`). Do not silently translate to 0-based; respect `⎕IO`.
- **Bracket indexing** `A[I]` is sugar for `I⌷A` (squad-quad). Multi-axis: `A[I;J]` is `I J⌷A` with semicolon-separated axes; `A[;J]` selects all of axis 0.
- **Dfn `{...}`** — `` = left arg (may be unbound for monadic call → check with `←default`), `⍵` = right arg, `∇` = recurse. Default left arg syntax: `←0`.
- **Tradfn vs dfn** — tradfns use line-numbered `→linenum` for goto; dfns use guards `cond:expr`. Pick the right one for the user's syntax.
- **Empty array** = rank-N array where some dim is 0. `00` is empty rank-1. Scalar prototype matters for empty-array operations; ignore in v1, return 0/space.
- **Test corpus:** custom + idioms. Place programs in `lib/apl/tests/programs/` with `.apl` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/apl-on-sx.md` inline.
- Short, factual commit messages (`apl: outer product ∘. (+9)`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,80 +0,0 @@
# common-lisp-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/common-lisp-on-sx.md` forever. Conditions + restarts on delimited continuations is the headline showcase — every other Lisp reinvents resumable exceptions on the host stack. On SX `signal`/`invoke-restart` is just a captured continuation. Plus CLOS, the LOOP macro, packages.
```
description: common-lisp-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/common-lisp-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/common-lisp-on-sx.md` — roadmap + Progress log.
2. `ls lib/common-lisp/` — pick up from the most advanced file.
3. If `lib/common-lisp/tests/*.sx` exist, run them. Green before new work.
4. If `lib/common-lisp/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/common-lisp-on-sx.md`:
- **Phase 1** — reader + parser (read macros `#'` `'` `` ` `` `,` `,@` `#( … )` `#:` `#\char` `#xFF` `#b1010`, ratios, dispatch chars, lambda lists with `&optional`/`&rest`/`&key`/`&aux`)
- **Phase 2** — sequential eval + special forms (`let`/`let*`/`flet`/`labels`, `block`/`return-from`, `tagbody`/`go`, `unwind-protect`, multiple values, `setf` subset, dynamic variables)
- **Phase 3** — **THE SHOWCASE**: condition system + restarts. `define-condition`, `signal`/`error`/`cerror`/`warn`, `handler-bind` (non-unwinding), `handler-case` (unwinding), `restart-case`, `restart-bind`, `find-restart`/`invoke-restart`/`compute-restarts`, `with-condition-restarts`. Classic programs (restart-demo, parse-recover, interactive-debugger) green.
- **Phase 4** — CLOS: `defclass`, `defgeneric`, `defmethod` with `:before`/`:after`/`:around`, `call-next-method`, multiple dispatch
- **Phase 5** — macros + LOOP macro + reader macros
- **Phase 6** — packages + stdlib (sequence functions, FORMAT directives, drive corpus to 200+)
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/common-lisp/**` and `plans/common-lisp-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. CL primitives go in `lib/common-lisp/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Common-Lisp-specific gotchas
- **`handler-bind` is non-unwinding** — handlers can decline by returning normally, in which case `signal` keeps walking the chain. **`handler-case` is unwinding** — picking a handler aborts the protected form via a captured continuation. Don't conflate them.
- **Restarts are not handlers.** `restart-case` establishes named *resumption points*; `signal` runs handler code with restarts visible; the handler chooses a restart by calling `invoke-restart`, which abandons handler stack and resumes at the restart point. Two stacks: handlers walk down, restarts wait to be invoked.
- **`block` / `return-from`** is lexical. `block name … (return-from name v) …` captures `^k` once at entry; `return-from` invokes it. `return-from` to a name not in scope is an error (don't fall back to outer block).
- **`tagbody` / `go`** — each tag in tagbody is a continuation; `go tag` invokes it. Tags are lexical, can only target tagbodies in scope.
- **`unwind-protect`** runs cleanup on *any* non-local exit (return-from, throw, condition unwind). Implement as a scope frame fired by the cleanup machinery.
- **Multiple values**: primary-value-only contexts (function args, `if` test, etc.) drop extras silently. `values` produces multiple. `multiple-value-bind` / `multiple-value-call` consume them. Don't auto-list.
- **CLOS dispatch:** sort applicable methods by argument-list specificity (`subclassp` per arg, left-to-right); standard method combination calls primary methods most-specific-first via `call-next-method` chain. `:before` runs all before primaries; `:after` runs all after, in reverse-specificity. `:around` wraps everything.
- **`call-next-method`** is a *continuation* available only inside a method body. Implement as a thunk stored in a dynamic-extent variable.
- **Generalised reference (`setf`)**: `(setf (foo x) v)``(setf-foo v x)`. Look up the setf-expander, not just a writer fn. `define-setf-expander` is mandatory for non-trivial places. Start with the symbolic / list / aref / slot-value cases.
- **Dynamic variables (specials):** `defvar`/`defparameter` mark a symbol as special. `let` over a special name *rebinds* in dynamic extent (use parameterize-style scope), not lexical.
- **Symbols are package-qualified.** Reader resolves `cl:car`, `mypkg::internal`, bare `foo` (current package). Internal vs external matters for `:` (one colon) reads.
- **`nil` is also `()` is also the empty list.** Same object. `nil` is also false. CL has no distinct unit value.
- **LOOP macro is huge.** Build incrementally — start with `for/in`, `for/from`, `collect`, `sum`, `count`, `repeat`. Add conditional clauses (`when`, `if`, `else`) once iteration drivers stable. `named` blocks + `return-from named` last.
- **Test corpus:** custom + curated `ansi-test` slice. Place programs in `lib/common-lisp/tests/programs/` with `.lisp` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/common-lisp-on-sx.md` inline.
- Short, factual commit messages (`common-lisp: handler-bind + 12 tests`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,724 +0,0 @@
# 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)
- [x] Stop new-language loops: send `/exit` to 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" Enter
```
Verify all four windows are idle (claude prompt, no active task).
- [x] 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 -5
```
If 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-vector` `n` `[fill]` → vector of length n
- `vector?` `v` → bool
- `vector-ref` `v` `i` → element at index i (0-based)
- `vector-set!` `v` `i` `x` → mutate in place
- `vector-length` `v` → integer
- `vector->list` `v` → list
- `list->vector` `lst` → vector
- `vector-fill!` `v` `x` → fill all elements
- `vector-copy` `v` `[start]` `[end]` → fresh copy of slice
Steps:
- [x] OCaml: add `SxVector of value array` to `hosts/ocaml/sx_types.ml`; implement all
primitives in `hosts/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).
- [x] Spec: add vector entries to `spec/primitives.sx` with 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).
- [x] JS bootstrapper: implement vectors in `hosts/javascript/platform.js` (or equivalent);
ensure `sx-browser.js` rebuild 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.
- [x] Tests: 40+ tests in `spec/tests/test-vectors.sx` covering construction, ref, set!,
length, conversions, fill, copy, bounds behaviour.
42 tests, all pass. 1847 standard / 2362 full passing (up from 5).
- [x] 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).
- [x] 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-number` preserves 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`/`round` to return integers when applied to floats
- `number->string` renders `1.0` as `"1.0"`, `1` as `"1"`
- Arithmetic: `(+ 1 1.0)` → `2.0` (float contagion), `(+ 1 1)` → `2` (integer)
Steps:
- [x] OCaml: distinguish `Integer of int` / `Number of float` in `sx_types.ml`; update all
arithmetic primitives for float contagion; fix `parse-number`.
92/92 numeric tower tests pass; 4874 total (394 pre-existing hs-upstream fails unchanged).
- [x] Spec: update `spec/primitives.sx` with 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.
- [x] JS bootstrapper: update number representation and arithmetic.
Added integer?/float?/exact?/inexact?/truncate/remainder/modulo/random-int/exact->inexact/
inexact->exact/parse-number. Fixed sx_server.ml epoch protocol for Integer type.
JS: 1940 passed (+60); OCaml: 4874/394 unchanged. 6 tests JS-only fail (float≡int limitation).
- [x] 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.
- [x] Verify: full suite passes. OCaml 4874/394 (baseline unchanged). JS 1940/2500 (+60 vs pre-tower).
No regressions on any test that relied on `1.0 = 1` — those tests were already using integer
literals which remain identical in JS. 6 JS-only failures are platform-inherent (JS float≡int).
- [x] Commit: all work landed across 4 commits (c70bbdeb, 45ec5535, b12a22e6, f5acb31c).
---
## 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).
- [x] Spec: implement `dynamic-wind` in `spec/evaluator.sx` such that the after-thunk fires
on both normal return AND non-local exit (raise/call-cc escape). Must compose with
`guard` — currently they don't interact.
- [x] OCaml: wire `dynamic-wind` through the CEK machine with a `WindFrame` continuation.
- [x] JS bootstrapper: update.
- [x] Tests: 20+ tests covering normal return, raise, call/cc escape, nested dynamic-winds.
- [x] 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.
- [x] Spec: add `make-coroutine`, `coroutine-resume`, `coroutine-yield`, `coroutine?`,
`coroutine-alive?` to `spec/primitives.sx`. Build on existing `perform`/`cek-resume`
machinery — coroutines ARE perform/resume with a stable identity.
Implemented as `spec/coroutines.sx` define-library; `make-coroutine` stub in evaluator.sx.
17/17 coroutine tests pass (OCaml). Drives iteration via define+fn recursion (not named let —
named let uses cek_call→cek_run which errors on IO suspension).
- [x] OCaml: implement coroutine type; wire resume/yield through CEK suspension.
No new native type needed — dict-based coroutine identity + existing cek-step-loop/
cek-resume/perform primitives in run_tests.ml ARE the OCaml implementation. 17/17 pass.
- [x] JS bootstrapper: update.
All CEK primitives already in sx-browser.js. Fix: pre-load spec/coroutines.sx +
spec/signals.sx in run_tests.js so (import (sx coroutines)) resolves without suspension.
17/17 pass in JS. 1965/2500 (+25 vs 1940 baseline). Zero new failures.
- [x] Tests: 25+ tests — multi-yield, final return, arg passthrough, alive? predicate,
nested coroutines, "final return vs yield" distinction (the Lua gotcha).
27 tests: added 10 new — state field inspection (ready/suspended/dead), yield from
nested helper, initial resume arg ignored, mutable closure state, complex yield values,
round-robin scheduling, factory-shared-no-state, non-coroutine error. 27/27 OCaml+JS.
- [x] Commit: `spec: coroutine primitive (make-coroutine/resume/yield)`
Phase 4 landed across 4 commits: 21cb9cf5 (spec library), 9eb12c66 (ocaml verified),
b78e06a7 (js pre-load), 0ffe208e (27 tests). Phase 4 complete.
---
## Phase 5 — String buffer
Fix O(n²) string concatenation in loops across Lua, Ruby, Common Lisp, Tcl.
- [x] Spec + OCaml: add `make-string-buffer`, `string-buffer-append!`, `string-buffer->string`,
`string-buffer-length` to primitives. OCaml: `Buffer.t` wrapper. JS: array+join.
Also: string-buffer? predicate; SxStringBuffer._string_buffer marker for typeOf/dict?
exclusion; inspect case in sx_types.ml. 17/17 tests OCaml+JS.
- [x] Tests: 15+ tests.
17 tests written inline with Spec+OCaml step: construction, type-of, empty/length,
single/multi-append, append-returns-nil, empty-string-append, reuse-after-to-string,
independence, loop-building, CSV-row, unicode, repeated-to-string, join-pattern.
17/17 OCaml+JS.
- [x] Commit: `spec: string-buffer primitive`
Committed as d98b5fa2 — all work in one commit (OCaml type + primitives + JS + spec + 17 tests).
---
## 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.
- [x] Design: write `plans/designs/sx-adt.md` covering syntax, CEK dispatch, interaction with
existing `cond`/`case`, exhaustiveness checking, recursive types, pattern variables.
Draft, then stop — next fire reviews design before implementing.
Written: define-type/match syntax, AdtValue runtime rep, stepSfDefineType + MatchFrame
CEK dispatch, exhaustiveness warnings via _adt_registry, recursive types, nested patterns,
wildcard _, 3-phase impl plan (basic/nested/exhaustiveness), open questions on accessors/singletons/inspect.
- [x] Spec: implement `define-type` special form in `spec/evaluator.sx`:
`(define-type Name (Ctor1 field...) (Ctor2 field...) ...)`
Creates constructor functions `Ctor1`, `Ctor2` + predicate `Name?`.
- [x] Spec: implement `match` special form:
`(match expr ((Ctor1 a b) body) ((Ctor2 x) body) (else body))`
Exhaustiveness warning if not all constructors covered and no `else`.
- [x] OCaml: add `SxAdt of string * value array` to types; implement constructors + match.
Dict-based ADT (no native type needed — matches spec). Hand-written sf_define_type
in bootstrap.py FIXUPS; registered via register_special_form. 172 assertions pass.
4280/1080 full suite (37 improvement over old baseline 4243/1117).
- [x] JS bootstrapper: update.
No changes needed — define-type/match are spec-level; sx-browser.js rebuilt at 0dc7e159.
40/40 ADT tests pass JS. 2032/2500 total (+67 vs 1965 phase-4 baseline).
- [x] Tests: 40+ tests in `spec/tests/test-adt.sx`.
40 tests written across two spec commits (6c872107+0dc7e159). All pass OCaml+JS.
- [x] Commit: `spec: algebraic data types (define-type + match)`
Phase 6 landed across 5 commits: 6c872107 (define-type spec), 0dc7e159 (match spec),
5d1913e7 (ocaml bootstrap), f63b2147 (plan tick). JS already current.
---
## 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-and` `a` `b` → integer
- `bitwise-or` `a` `b` → integer
- `bitwise-xor` `a` `b` → integer
- `bitwise-not` `a` → integer
- `arithmetic-shift` `a` `count` → integer (left if count > 0, right if count < 0)
- `bit-count` `a` → number of set bits (popcount)
- `integer-length` `a` → number of bits needed to represent a
Steps:
- [x] Spec: add entries to `spec/primitives.sx` with type signatures.
stdlib.bitwise module with 7 entries appended to spec/primitives.sx.
- [x] OCaml: implement in `hosts/ocaml/sx_primitives.ml` using OCaml `land`/`lor`/`lxor`/`lnot`/`lsl`/`asr`.
land/lor/lxor/lnot/lsl/asr in sx_primitives.ml. bit-count: Kernighan loop. integer-length: lsr loop.
- [x] JS bootstrapper: implement in `hosts/javascript/platform.js` using JS `&`/`|`/`^`/`~`/`<<`/`>>`.
stdlib.bitwise module added to PRIMITIVES_JS_MODULES. bit-count: Hamming weight. integer-length: Math.clz32.
- [x] Tests: 25+ tests in `spec/tests/test-bitwise.sx` — basic ops, shift left/right, negative numbers, popcount.
26 tests, 158 assertions, all pass OCaml+JS.
- [x] Commit: `spec: bitwise operations (bitwise-and/or/xor/not, arithmetic-shift, bit-count)`
Committed a8a79dc9. Phase 7 complete in single commit.
---
## 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:
- `values` `v...` → multiple-value object
- `call-with-values` `producer` `consumer` → applies consumer to values from producer
- `let-values` `(((a b) expr) ...)` `body` — binding form (special form in evaluator)
- `define-values` `(a b ...)` `expr` — top-level multi-value bind
Steps:
- [ ] Spec: add `SxValues` type to evaluator; implement `values` + `call-with-values` in
`spec/evaluator.sx`; add `let-values` / `define-values` special forms.
- [ ] OCaml: add `SxValues of value list` to `sx_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 with `begin`/`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:
- `delay` `expr` → promise (special form — expr not evaluated yet)
- `force` `p` → evaluate promise, cache result, return it
- `make-promise` `v` → already-forced promise wrapping v
- `promise?` `v` → bool
- `delay-force` `expr` → for iterative lazy sequences (avoids stack growth in lazy streams)
Steps:
- [ ] Spec: add `delay` / `delay-force` special forms to `spec/evaluator.sx`; add promise
type with mutable forced/value slots; `force` checks if already forced before eval.
- [ ] OCaml: add `SxPromise of { mutable forced: bool; mutable value: value; thunk: value }`;
wire `delay`/`force`/`delay-force` through 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 table
- `hash-table?` `v` → bool
- `hash-table-set!` `ht` `key` `val` → mutate in place
- `hash-table-ref` `ht` `key` `[default]` → value or default/error
- `hash-table-delete!` `ht` `key` → remove entry
- `hash-table-size` `ht` → integer
- `hash-table-keys` `ht` → list of keys
- `hash-table-values` `ht` → list of values
- `hash-table->alist` `ht` → list of (key . value) pairs
- `hash-table-for-each` `ht` `fn` → iterate (fn key val) for side effects
- `hash-table-merge!` `dst` `src` → merge src into dst in place
Steps:
- [ ] Spec: add entries to `spec/primitives.sx`.
- [ ] OCaml: add `SxHashTable of (value, value) Hashtbl.t` to `sx_types.ml`; implement
all primitives in `hosts/ocaml/sx_primitives.ml`.
- [ ] JS bootstrapper: implement using JS `Map` in `hosts/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-range` `start` `[end]` `[step]` → lazy range sequence (works with `for-each`/`map`)
- `sequence->list` `s` → coerce any sequence to list
- `sequence->vector` `s` → coerce any sequence to vector
- `sequence-length` `s` → length of any sequence
- `sequence-ref` `s` `i` → element by index (lists and vectors)
- `sequence-append` `s1` `s2` → concatenate two same-type sequences
Steps:
- [ ] Spec: extend `map`/`filter`/`reduce`/`for-each`/`some`/`every?` in `spec/evaluator.sx`
to type-dispatch; add `in-range` lazy sequence type + helpers.
- [ ] OCaml: update HO form dispatch; add `SxRange` or use lazy list; implement `sequence-*`
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 table
- `intern` `str` → 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 `gensym` counter to evaluator state; implement in `spec/evaluator.sx`.
`string->symbol` already exists — `gensym` is 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` → bool
- `char->integer` `c` → Unicode codepoint integer
- `integer->char` `n` → char
- `char=?` `char<?` `char>?` `char<=?` `char>=?` → comparators
- `char-ci=?` `char-ci<?` etc. → case-insensitive comparators
- `char-alphabetic?` `char-numeric?` `char-whitespace?` → predicates
- `char-upper-case?` `char-lower-case?` → predicates
- `char-upcase` `char-downcase` → char → char
- `string->list` extended to return chars (not length-1 strings)
- `list->string` accepting chars
Also: `#\a` reader syntax for char literals (parser addition).
Steps:
- [ ] Spec: add `SxChar` type to evaluator; add char literal syntax `#\a`/`#\space`/`#\newline`
to `spec/parser.sx`; implement all predicates + comparators.
- [ ] OCaml: add `SxChar of char` to `sx_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-string` `str` → input port
- `open-output-string` → output port
- `get-output-string` `port` → string (flush output port to string)
- `input-port?` `output-port?` `port?` → predicates
- `read-char` `[port]` → char or eof-object
- `peek-char` `[port]` → char or eof-object (non-consuming)
- `read-line` `[port]` → string or eof-object
- `write-char` `char` `[port]` → void
- `write-string` `str` `[port]` → void
- `eof-object` → the eof sentinel
- `eof-object?` `v` → bool
- `close-port` `port` → 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 `SxPort` variant 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`:
- `quotient` `a` `b` → truncate toward zero (same sign as dividend)
- `remainder` `a` `b` → sign follows dividend (truncation division)
- `modulo` `a` `b` → 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`/`modulo` with 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-rational` `numerator` `denominator` → rational (auto-reduced by GCD)
- `rational?` `v` → bool
- `numerator` `r` → integer
- `denominator` `r` → integer
- Reader syntax: `1/3` parsed as rational literal
- Arithmetic: `(+ 1/3 1/6)` → `1/2`; `(* 1/3 3)` → `1`; mixed int/rational → rational
- `exact->inexact` on rational → float; `inexact->exact` on float → rational approximation
- `(number->string 1/3)` → `"1/3"`
Steps:
- [ ] Spec: add `SxRational` type; add `n/d` reader syntax to `spec/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 port
- `read-char` already in Phase 14; `read` uses it internally
- `write` `val` `[port]` → void — serializes with quotes: `"hello"`, `#\a`, `(1 2 3)`
- `display` `val` `[port]` → void — serializes without quotes: `hello`, `a`, `(1 2 3)`
- `newline` `[port]` → void — writes `\n`
- `write-to-string` `val` → string — convenience: `(write val (open-output-string))`
- `display-to-string` `val` → string — convenience
Steps:
- [ ] Spec: implement `read` in `spec/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; `write` quotes strings + uses `#\` for chars, `display` does not.
- [ ] OCaml: wire `read` through port type; implement `write`/`display` output 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 list
- `set?` `v` → bool
- `set-add!` `s` `val` → void
- `set-member?` `s` `val` → bool
- `set-remove!` `s` `val` → void
- `set-size` `s` → integer
- `set->list` `s` → list (unspecified order)
- `list->set` `lst` → set
- `set-union` `s1` `s2` → new set
- `set-intersection` `s1` `s2` → new set
- `set-difference` `s1` `s2` → new set (elements in s1 not in s2)
- `set-for-each` `s` `fn` → iterate for side effects
- `set-map` `s` `fn` → new set of mapped values
Steps:
- [ ] Spec: add entries to `spec/primitives.sx`.
- [ ] OCaml: implement using `Hashtbl.t` with unit values (or a proper `Set` functor
with a comparison function); add `SxSet` to `sx_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-regexp` `pattern` `[flags]` → regexp object (`flags`: `"i"` case-insensitive, `"g"` global, `"m"` multiline)
- `regexp?` `v` → bool
- `regexp-match` `re` `str` → match dict `{:match "..." :start N :end N :groups (...)}` or nil
- `regexp-match-all` `re` `str` → list of match dicts
- `regexp-replace` `re` `str` `replacement` → string with first match replaced
- `regexp-replace-all` `re` `str` `replacement` → string with all matches replaced
- `regexp-split` `re` `str` → list of strings (split on matches)
- Reader syntax: `#/pattern/flags` for regexp literals (parser addition)
Steps:
- [ ] Audit `lib/js/regex.sx` — understand the API it already exposes; map to the
primitive API above.
- [ ] Spec: add `SxRegexp` type to evaluator; add `#/pattern/flags` literal syntax to
`spec/parser.sx`; wire `lib/js/regex.sx` engine as the implementation.
- [ ] OCaml: implement using OCaml `Re` library (or `Str`); add `SxRegexp` to 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-bytevector` `n` `[fill]` → bytevector of n bytes (fill defaults to 0)
- `bytevector?` `v` → bool
- `bytevector-length` `bv` → integer
- `bytevector-u8-ref` `bv` `i` → byte 0255
- `bytevector-u8-set!` `bv` `i` `byte` → void
- `bytevector-copy` `bv` `[start]` `[end]` → fresh copy
- `bytevector-copy!` `dst` `at` `src` `[start]` `[end]` → in-place copy
- `bytevector-append` `bv...` → concatenated bytevector
- `utf8->string` `bv` `[start]` `[end]` → string decoded as UTF-8
- `string->utf8` `str` `[start]` `[end]` → bytevector UTF-8 encoded
- `bytevector->list` / `list->bytevector` → conversion
Steps:
- [ ] Spec: add `SxBytevector` type; implement all primitives in `spec/evaluator.sx` / `spec/primitives.sx`.
- [ ] OCaml: add `SxBytevector of bytes` to `sx_types.ml`; implement primitives using
OCaml `Bytes`.
- [ ] 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 `format` as a pure SX function in `spec/primitives.sx` — parses
`~X` directives, dispatches to `display`/`write`/`number->string` as 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.md` under a `## SX primitive baseline` section:
"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`/`write` for reader/printer;
gensym for macros; rational numbers for CL ratios; multiple values; sets for CL set ops;
`modulo`/`remainder`/`quotient`; radix formatting; `format` for `cl:format`.
- [ ] Lua: vectors for arrays; hash tables for Lua tables; `delay`/`force` for 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; `remainder` for `rem`; regexp for `re` module.
- [ ] Haskell: numeric tower for `Num`/`Integral`/`Fractional`; promises for lazy evaluation
(critical); multiple values for tuples; rational numbers for `Rational`; char type for
`Char`; `gcd`/`lcm`; sets for `Data.Set`; `read`/`write` for `Show`/`Read` instances.
- [ ] JS: vectors for Array; hash tables for `Map`; sets for `Set`; bitwise ops for typed
arrays; regexp for JS regex; bytevectors for `Uint8Array`; radix formatting.
- [ ] Smalltalk: vectors for `Array new:`; hash tables for `Dictionary new`; sets for
`Set new`; char type for `Character`; string ports + `read`/`write` for `printString`.
- [ ] 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 for `Set`; regexp for
Ruby regex; string ports for `StringIO`; bytevectors for `String` binary encoding.
- [ ] Tcl: string ports for Tcl channel abstraction; string-buffer for `append`; coroutines
for Tcl coroutines; regexp for Tcl `regexp`; format for Tcl `format`.
- [ ] Forth: bitwise ops (core); string-buffer for word-definition accumulation; bytevectors
for Forth's raw memory model.
---
## Ground rules
- Work on the `architecture` branch in `/root/rose-ash` (main worktree).
- Use sx-tree MCP for all `.sx` file edits. Never use raw Edit/Write/Read on `.sx` files.
- Commit after each concrete unit of work. Never leave the branch broken.
- Never push to `main` — only push to `origin/architecture`.
- Update this checklist every fire: tick `[x]` done, add inline notes on blockers.
---
## Progress log
_Newest first._
- 2026-04-26: Phase 7 complete — bitwise-and/or/xor/not + arithmetic-shift + bit-count + integer-length. OCaml: land/lor/lxor/lnot/lsl/asr + Kernighan popcount + lsr loop for integer-length. JS: bitwise ops + Hamming weight + Math.clz32. 26 tests, 158 assertions, all pass. a8a79dc9.
- 2026-04-26: Phase 6 complete — JS+Tests+Commit all ticked. JS needed no changes (spec-level forms). 40/40 ADT tests pass JS. 2032/2500 JS total (+67 vs phase-4). Phase 6 fully landed: 6c872107+0dc7e159+5d1913e7. Phase 7 (bitwise) next.
- 2026-04-26: Phase 6 OCaml done — Dict-based ADT (no native SxAdt type needed); hand-written sf_define_type in bootstrap.py FIXUPS (skipped from transpile — &rest params + empty-dict {} literals); registered via register_special_form; step_limit/step_count added to PREAMBLE. 172 assertions pass (test-adt). Full suite 4280/1080 (was 4243/1117, +37). Committed 5d1913e7.
- 2026-04-26: Phase 6 Spec match done — ADT case added to match-pattern in spec/evaluator.sx: checks (list? pattern)+(symbol? first)+(dict? value)+(get value :_adt), then matches :_ctor+arity and recursively binds field patterns. No-clause error now uses make-cek-value+raise-eval-frame so guard can catch it. 20 new match tests pass; 40/40 total ADT tests green. Zero regressions.
- 2026-04-26: Phase 6 Spec define-type done — sf-define-type registered via register-special-form! in spec/evaluator.sx; AdtValue as {:_adt true :_type "..." :_ctor "..." :_fields (list ...)}; ctor fns + arity checking + Name?/Ctor? predicates + Ctor-field accessors; *adt-registry* dict populated per define-type call. 20/20 JS tests pass in spec/tests/test-adt.sx. OCaml define-type is next task.
- 2026-04-26: Phase 6 Design done — plans/designs/sx-adt.md written. Covers define-type/match syntax, AdtValue CEK runtime, stepSfDefineType+MatchFrame dispatch, exhaustiveness warnings, recursive types, nested patterns, wildcard _. 3-phase impl plan. Next fire: Spec implement define-type.
- 2026-04-26: Phase 5 complete — string buffer fully landed (d98b5fa2). 17 tests, 17/17 OCaml+JS. Phase 6 (ADTs) next.
- 2026-04-26: Phase 5 Spec+OCaml+JS step done — StringBuffer of Buffer.t in sx_types.ml; make-string-buffer/append!/->string/length/string-buffer? in sx_primitives.ml; SxStringBuffer with _string_buffer marker + typeOf/dict? fixes in platform.py; JS rebuilt. 17/17 tests OCaml+JS.
- 2026-04-26: Phase 4 complete — coroutine primitive fully landed (4 commits: spec library + OCaml verified + JS pre-load + 27 tests). Phase 5 (string buffer) next.
- 2026-04-26: Phase 4 Tests step done — 27 tests total (10 new: state field inspection, yield-from-helper, initial-arg-ignored, mutable-closure, complex-values, round-robin, factory-no-state, non-coroutine-error). 27/27 OCaml+JS.
- 2026-04-26: Phase 4 JS step done — all CEK primitives already in sx-browser.js; fix was pre-loading spec/coroutines.sx+spec/signals.sx in run_tests.js so (import (sx coroutines)) resolves synchronously. 17/17 coroutine tests pass JS. 1965/2500 total (+25), zero new failures.
- 2026-04-26: Phase 4 OCaml step done — no native SxCoroutine type needed; existing cek-step-loop/cek-resume/perform/make-cek-state primitives in run_tests.ml fully support the spec/coroutines.sx library. 284/284 pass (coroutines+vectors+numeric-tower+dynamic-wind), zero regressions.
- 2026-04-26: Phase 4 Spec step done — spec/coroutines.sx define-library with make-coroutine/coroutine-resume/coroutine-yield/coroutine?/coroutine-alive?; make-coroutine stub in evaluator.sx; 17/17 coroutine tests pass (OCaml). Key insight: coroutine body must use (define loop (fn...)) + (loop 0) not named let — named let uses cek_call→cek_run which errors on IO suspension.
- 2026-04-26: Phase 3 complete — OCaml+JS done. CallccContinuation gains winders-depth int; make_callcc_continuation/callcc_continuation_winders_len wired; wind-after/wind-return CekFrame fields fixed (cf_f=after-thunk, cf_extra=winders-len, cf_name=body-result); get_val + transpiler.sx updated. 8/8 dynamic-wind tests pass on OCaml; 235/235 (callcc+guard+do+r7rs) zero regressions. Committed 6602ec8c.
- 2026-04-26: Phase 3 Spec+Tests done — dynamic-wind CEK implementation: wind-after/wind-return frames, *winders* stack, kont-unwind-to-handler, wind-escape-to. callcc frame stores winders-len in continuation; callcc-continuation? calls wind-escape-to before escape. 8/8 dynamic-wind tests pass (normal return, raise, call/cc, nested LIFO, guard ordering). 1948/2500 JS (+8). Zero regressions. Committed a9d5a108.
- 2026-04-26: Phase 2 complete — Verify+Commit done. OCaml 4874/394, JS 1940/2500 (+60). No regressions. 6 JS-only failures are float≡int platform-inherent. Phase 2 fully landed across 4 commits.
- 2026-04-26: Phase 2 JS bootstrapper done — integer?/float?/exact?/inexact? added (Number.isInteger); truncate/remainder/modulo/random-int/exact->inexact/inexact->exact/parse-number added. Fixed sx_server.ml epoch+blob+io-response protocol for Integer type. JS: 1940/2500 (+60). OCaml: 4874/394 baseline. 6 JS tests fail (JS float≡int platform limit). Committed b12a22e6.
- 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 float` in 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). Committed c70bbdeb.
- 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.

View File

@@ -1,83 +0,0 @@
# ruby-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/ruby-on-sx.md` forever. Fibers via delcc is the headline showcase — `Fiber.new`/`Fiber.yield`/`Fiber.resume` are textbook delimited continuations with sugar, where MRI does it via C-stack swapping. Plus blocks/yield (lexical escape continuations, same shape as Smalltalk's non-local return), method_missing, and singleton classes.
```
description: ruby-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/ruby-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/ruby-on-sx.md` — roadmap + Progress log.
2. `ls lib/ruby/` — pick up from the most advanced file.
3. If `lib/ruby/tests/*.sx` exist, run them. Green before new work.
4. If `lib/ruby/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/ruby-on-sx.md`:
- **Phase 1** — tokenizer + parser. Keywords, identifier sigils (`@` ivar, `@@` cvar, `$` global), strings with interpolation, `%w[]`/`%i[]`, symbols, blocks `{|x| …}` and `do |x| … end`, splats, default args, method def
- **Phase 2** — object model + sequential eval. Class table, ancestor-chain dispatch, `super`, singleton classes, `method_missing` fallback, dynamic constant lookup
- **Phase 3** — blocks + procs + lambdas. Method captures escape continuation `^k`; `yield` / `return` / `break` / `next` / `redo` semantics; lambda strict arity vs proc lax
- **Phase 4** — **THE SHOWCASE**: fibers via delcc. `Fiber.new`/`Fiber.resume`/`Fiber.yield`/`Fiber.transfer`. Classic programs (generator, producer-consumer, tree-walk) green
- **Phase 5** — modules + mixins + metaprogramming. `include`/`prepend`/`extend`, `define_method`, `class_eval`/`instance_eval`, `respond_to?`/`respond_to_missing?`, hooks
- **Phase 6** — stdlib drive. `Enumerable` mixin, `Comparable`, Array/Hash/Range/String/Integer methods, drive corpus to 200+
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/ruby/**` and `plans/ruby-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Ruby primitives go in `lib/ruby/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Ruby-specific gotchas
- **Block `return` vs lambda `return`.** Inside a block `{ ... return v }`, `return` invokes the *enclosing method's* escape continuation (non-local return). Inside a lambda `->(){ ... return v }`, `return` returns from the *lambda*. Don't conflate. Implement: blocks bind their `^method-k`; lambdas bind their own `^lambda-k`.
- **`break` from inside a block** invokes a different escape — the *iteration loop's* escape — and the loop returns the break-value. `next` is escape from current iteration, returns iteration value. `redo` re-enters current iteration without advancing.
- **Proc arity is lax.** `proc { |a, b, c| … }.call(1, 2)``c = nil`. Lambda is strict — same call raises ArgumentError. Check arity at call site for lambdas only.
- **Block argument unpacking.** `[[1,2],[3,4]].each { |a, b| … }` — single Array arg auto-unpacks for blocks (not lambdas). One arg, one Array → unpack. Frequent footgun.
- **Method dispatch chain order:** prepended modules → class methods → included modules → superclass → BasicObject → method_missing. `super` walks from the *defining* class's position, not the receiver class's.
- **Singleton classes** are lazily allocated. Looking up the chain for an object passes through its singleton class first, then its actual class. `class << obj; …; end` opens the singleton.
- **`method_missing`** — fallback when ancestor walk misses. Receives `(name_symbol, *args, &blk)`. Pair with `respond_to_missing?` for `respond_to?` to also report true. Do **not** swallow NoMethodError silently.
- **Ivars are per-object dicts.** Reading an unset ivar yields `nil` and a warning (`-W`). Don't error.
- **Constant lookup** is first lexical (Module.nesting), then inheritance (Module.ancestors of the innermost class). Different from method lookup.
- **`Object#send`** invokes private and public methods alike; `Object#public_send` skips privates.
- **Class reopening.** `class Foo; def bar; …; end; end` plus a later `class Foo; def baz; …; end; end` adds methods to the same class. Class table lookups must be by-name, mutable; methods dict is mutable.
- **Fiber semantics.** `Fiber.new { |arg| … }` creates a fiber suspended at entry. First `Fiber.resume(v)` enters with `arg = v`. Inside, `Fiber.yield(w)` returns `w` to the resumer; the next `Fiber.resume(v')` returns `v'` to the yield site. End of block returns final value to last resumer; subsequent `Fiber.resume` raises FiberError.
- **`Fiber.transfer`** is symmetric — either side can transfer to the other; no resume/yield asymmetry. Implement on top of the same continuation pair, just don't enforce direction.
- **Symbols are interned.** `:foo == :foo` is identity. Use SX symbols.
- **Strings are mutable.** `s = "abc"; s << "d"; s == "abcd"`. Hash keys can be strings; hash dups string keys at insertion to be safe (or freeze them).
- **Truthiness:** only `false` and `nil` are falsy. `0`, `""`, `[]` are truthy.
- **Test corpus:** custom + curated RubySpec slice. Place programs in `lib/ruby/tests/programs/` with `.rb` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/ruby-on-sx.md` inline.
- Short, factual commit messages (`ruby: Fiber.yield + Fiber.resume (+8)`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,77 +0,0 @@
# smalltalk-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/smalltalk-on-sx.md` forever. Message-passing OO + **blocks with non-local return** on delimited continuations. Non-local return is the headline showcase — every other Smalltalk reinvents it on the host stack; on SX it falls out of the captured method-return continuation.
```
description: smalltalk-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/smalltalk-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/smalltalk-on-sx.md` — roadmap + Progress log.
2. `ls lib/smalltalk/` — pick up from the most advanced file.
3. If `lib/smalltalk/tests/*.sx` exist, run them. Green before new work.
4. If `lib/smalltalk/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/smalltalk-on-sx.md`:
- **Phase 1** — tokenizer + parser (chunk format, identifiers, keywords `foo:`, binary selectors, `#sym`, `#(…)`, `$c`, blocks `[:a | …]`, cascades, message precedence)
- **Phase 2** — object model + sequential eval (class table bootstrap, message dispatch, `super`, `doesNotUnderstand:`, instance variables)
- **Phase 3** — **THE SHOWCASE**: blocks with non-local return via captured method-return continuation. `whileTrue:` / `ifTrue:ifFalse:` as block sends. 5 classic programs (eight-queens, quicksort, mandelbrot, life, fibonacci) green.
- **Phase 4** — reflection + MOP: `perform:`, `respondsTo:`, runtime method addition, `becomeForward:`, `Exception` / `on:do:` / `ensure:` on top of `handler-bind`/`raise`
- **Phase 5** — collections + numeric tower + streams
- **Phase 6** — port SUnit, vendor Pharo Kernel-Tests slice, drive corpus to 200+
- **Phase 7** — speed (optional): inline caching, block intrinsification
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Smalltalk primitives go in `lib/smalltalk/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Smalltalk-specific gotchas
- **Method invocation captures `^k`** — the return continuation. Bind it as the block's escape token. `^expr` from inside any nested block invokes that captured `^k`. Escape past method return raises `BlockContext>>cannotReturn:`.
- **Blocks are lambdas + escape token**, not bare lambdas. `value`/`value:`/… invoke the lambda; `^` invokes the escape.
- **`ifTrue:` / `ifFalse:` / `whileTrue:` are ordinary block sends** — no special form. The runtime intrinsifies them in the JIT path (Tier 1 of bytecode expansion already covers this pattern).
- **Cascade** `r m1; m2; m3` desugars to `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`. Result is the cascade's last send (or first, depending on parser variant — pick one and document).
- **`super` send** looks up starting from the *defining* class's superclass, not the receiver class. Stash the defining class on the method record.
- **Selectors are interned symbols.** Use SX symbols.
- **Receiver dispatch:** tagged ints / floats / strings / symbols / `nil` / `true` / `false` aren't boxed. Their classes (`SmallInteger`, `Float`, `String`, `Symbol`, `UndefinedObject`, `True`, `False`) are looked up by SX type-of, not by an `:class` field.
- **Method precedence:** unary > binary > keyword. `3 + 4 factorial` is `3 + (4 factorial)`. `a foo: b bar` is `a foo: (b bar)` (keyword absorbs trailing unary).
- **Image / fileIn / become: between sessions** = out of scope. One-way `becomeForward:` only.
- **Test corpus:** ~200 hand-written + a slice of Pharo Kernel-Tests. Place programs in `lib/smalltalk/tests/programs/`.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/smalltalk-on-sx.md` inline.
- Short, factual commit messages (`smalltalk: tokenizer + 56 tests`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,83 +0,0 @@
# tcl-on-sx loop agent (single agent, queue-driven)
Role: iterates `plans/tcl-on-sx.md` forever. `uplevel`/`upvar` is the headline showcase — Tcl's superpower for defining your own control structures, requiring deep VM cooperation in any normal host but falling out of SX's first-class env-chain. Plus the Dodekalogue (12 rules), command-substitution everywhere, and "everything is a string" homoiconicity.
```
description: tcl-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree
```
## Prompt
You are the sole background agent working `/root/rose-ash/plans/tcl-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
## Restart baseline — check before iterating
1. Read `plans/tcl-on-sx.md` — roadmap + Progress log.
2. `ls lib/tcl/` — pick up from the most advanced file.
3. If `lib/tcl/tests/*.sx` exist, run them. Green before new work.
4. If `lib/tcl/scoreboard.md` exists, that's your baseline.
## The queue
Phase order per `plans/tcl-on-sx.md`:
- **Phase 1** — tokenizer + parser. The Dodekalogue (12 rules): word-splitting, command sub `[…]`, var sub `$name`/`${name}`/`$arr(idx)`, double-quote vs brace word, backslash, `;`, `#` comments only at command start, single-pass left-to-right substitution
- **Phase 2** — sequential eval + core commands. `set`/`unset`/`incr`/`append`/`lappend`, `puts`/`gets`, `expr` (own mini-language), `if`/`while`/`for`/`foreach`/`switch`, string commands, list commands, dict commands
- **Phase 3** — **THE SHOWCASE**: `proc` + `uplevel` + `upvar`. Frame stack with proc-call push/pop; `uplevel #N script` evaluates in caller's frame; `upvar` aliases names across frames. Classic programs (for-each-line, assert macro, with-temp-var) green
- **Phase 4** — `return -code N`, `catch`, `try`/`trap`/`finally`, `throw`. Control flow as integer codes
- **Phase 5** — namespaces + ensembles. `namespace eval`, qualified names `::ns::cmd`, ensembles, `namespace path`
- **Phase 6** — coroutines (built on fibers, same delcc as Ruby fibers) + system commands + drive corpus to 150+
Within a phase, pick the checkbox that unlocks the most tests per effort.
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
## Ground rules (hard)
- **Scope:** only `lib/tcl/**` and `plans/tcl-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Tcl primitives go in `lib/tcl/runtime.sx`.
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
- **Shared-file issues** → plan's Blockers with minimal repro.
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
- **Worktree:** commit locally. Never push. Never touch `main`.
- **Commit granularity:** one feature per commit.
- **Plan file:** update Progress log + tick boxes every commit.
## Tcl-specific gotchas
- **Everything is a string.** Internally cache shimmer reps (list, dict, int, double) for performance, but every value must be re-stringifiable. Mutating one rep dirties the cached string and vice versa.
- **The Dodekalogue is strict.** Substitution is **one-pass**, **left-to-right**. The result of a substitution is a value, not a script — it does NOT get re-parsed for further substitutions. This is what makes Tcl safe-by-default. Don't accidentally re-parse.
- **Brace word `{…}`** is the only way to defer evaluation. No substitution inside, just balanced braces. Used for `if {expr}` body, `proc body`, `expr` arguments.
- **Double-quote word `"…"`** is identical to a bare word for substitution purposes — it just allows whitespace in a single word. `\` escapes still apply.
- **Comments are only at command position.** `# this is a comment` after a `;` or newline; *not* inside a command. `set x 1 # not a comment` is a 4-arg `set`.
- **`expr` has its own grammar** — operator precedence, function calls — and does its own substitution. Brace `expr {$x + 1}` to avoid double-substitution and to enable bytecode caching.
- **`if` and `while` re-parse** the condition only if not braced. Always use `if {…}`/`while {…}` form. The unbraced form re-substitutes per iteration.
- **`return` from a `proc`** uses control code 2. `break` is 3, `continue` is 4. `error` is 1. `catch` traps any non-zero code; user can return non-zero with `return -code error -errorcode FOO message`.
- **`uplevel #0 script`** is global frame. `uplevel 1 script` (or just `uplevel script`) is caller's frame. `uplevel #N` is absolute level N (0=global, 1=top-level proc, 2=proc-called-from-top, …). Negative levels are errors.
- **`upvar #N otherVar localVar`** binds `localVar` in the current frame as an *alias* — both names refer to the same storage. Reads and writes go through the alias.
- **`info level`** with no arg returns current level number. `info level N` (positive) returns the command list that invoked level N. `info level -N` returns the command list of the level N relative-up.
- **Variable names with `(…)`** are array elements: `set arr(foo) 1`. Arrays are not first-class values — you can't `set x $arr`. `array get arr` gives a flat list `{key1 val1 key2 val2 …}`.
- **List vs string.** `set l "a b c"` and `set l [list a b c]` look the same when printed but the second has a cached list rep. `lindex` works on both via shimmering. Most user code can't tell the difference.
- **`incr x`** errors if x doesn't exist; pre-set with `set x 0` or use `incr x 0` first if you mean "create-or-increment". Or use `dict incr` for dicts.
- **Coroutines are fibers.** `coroutine name body` starts a coroutine; calling `name` resumes it; `yield value` from inside suspends and returns `value` to the resumer. Same primitive as Ruby fibers — share the implementation under the hood.
- **`switch`** matches first clause whose pattern matches. Default is `default`. Variant matches: glob (default), `-exact`, `-glob`, `-regexp`. Body `-` means "fall through to next clause's body".
- **Test corpus:** custom + slice of Tcl's own tests. Place programs in `lib/tcl/tests/programs/` with `.tcl` extension.
## General gotchas (all loops)
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
- `cond`/`when`/`let` clauses evaluate only the last expr.
- `type-of` on user fn returns `"lambda"`.
- Shell heredoc `||` gets eaten — escape or use `case`.
## Style
- No comments in `.sx` unless non-obvious.
- No new planning docs — update `plans/tcl-on-sx.md` inline.
- Short, factual commit messages (`tcl: uplevel + upvar (+11)`).
- One feature per iteration. Commit. Log. Next.
Go. Read the plan; find first `[ ]`; implement.

View File

@@ -1,115 +0,0 @@
# APL-on-SX: rank-polymorphic primitives + glyph parser
The headline showcase is **rank polymorphism** — a single primitive (`+`, `⌈`, `⊂`, ``) works uniformly on scalars, vectors, matrices, and higher-rank arrays. ~80 glyph primitives + 6 operators bind together with right-to-left evaluation; the entire language is a high-density combinator algebra. The JIT compiler + primitive table pay off massively here because almost every program is `array → array` pure pipelines.
End-state goal: Dyalog-flavoured APL subset, dfns + tradfns, classic programs (game-of-life, mandelbrot, prime-sieve, n-queens, conway), 100+ green tests.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Dyalog APL surface, Unicode glyphs. `⎕`-quad system functions for I/O. `∇` tradfn header.
- **Conformance:** "Reads like APL, runs like APL." Not byte-compat with Dyalog; we care about right-to-left semantics and rank polymorphism.
- **Test corpus:** custom — APL idioms (Roger Hui style), classic programs, plus ~50 pattern tests for primitives.
- **Out of scope:** ⎕-namespaces beyond a handful, complex numbers, full TAO ordering, `⎕FX` runtime function definition (use static `∇` only), nested-array-of-functions higher orders, the editor.
- **Glyphs:** input via plain Unicode in `.apl` source files. Backtick-prefix shortcuts handled by the user's editor — we don't ship one.
## Ground rules
- **Scope:** only touch `lib/apl/**` and `plans/apl-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. APL primitives go in `lib/apl/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
APL source (Unicode glyphs)
lib/apl/tokenizer.sx — glyphs, identifiers, numbers (¯ for negative), strings, strands
lib/apl/parser.sx — right-to-left with valence resolution (mon vs dyadic by position)
lib/apl/transpile.sx — AST → SX AST (entry: apl-eval-ast)
lib/apl/runtime.sx — array model, ~80 primitives, 6 operators, dfns/tradfns
```
Core mapping:
- **Array** = SX dict `{:shape (d1 d2 …) :ravel #(v1 v2 …)}`. Scalar is rank-0 (empty shape), vector is rank-1, matrix rank-2, etc. Type uniformity not required (heterogeneous nested arrays via "boxed" elements `⊂x`).
- **Rank polymorphism** — every scalar primitive is broadcast: `1 2 3 + 4 5 6``5 7 9`; `(2 36) + 1` ↦ broadcast scalar to matrix.
- **Conformability** = matching shapes, or one-side scalar, or rank-1 cycling (deferred — keep strict in v1).
- **Valence** = each glyph has a monadic and a dyadic meaning; resolution is purely positional (left-arg present → dyadic).
- **Operator** = takes one or two function operands, returns a derived function (`f¨` = `each f`, `f/` = `reduce f`, `f∘g` = `compose`, `f⍨` = `commute`).
- **Tradfn** `∇R←L F R; locals` = named function with explicit header.
- **Dfn** `{+⍵}` = anonymous, `` = left arg, `⍵` = right arg, `∇` = recurse.
## Roadmap
### Phase 1 — tokenizer + parser
- [ ] Tokenizer: Unicode glyphs (the full APL set: `+ - × ÷ * ⍟ ⌈ ⌊ | ! ? ○ ~ < ≤ = ≥ > ≠ ∊ ∧ ⍱ ⍲ , ⍪ ⌽ ⊖ ⍉ ↑ ↓ ⊂ ⊃ ⊆ ⍸ ⌷ ⍋ ⍒ ⊥ ⊣ ⊢ ⍎ ⍕ ⍝`), operators (`/ \ ¨ ⍨ ∘ . ⍣ ⍤ ⍥ @`), numbers (`¯` for negative, `1E2`, `1J2` complex deferred), characters (`'a'`, `''` escape), strands (juxtaposition of literals: `1 2 3`), names, comments `⍝ …`
- [ ] Parser: right-to-left; classify each token as function, operator, value, or name; resolve valence positionally; dfn `{…}` body, tradfn `∇` header, guards `:`, control words `:If :While :For …` (Dyalog-style)
- [ ] Unit tests in `lib/apl/tests/parse.sx`
### Phase 2 — array model + scalar primitives
- [ ] Array constructor: `make-array shape ravel`, `scalar v`, `vector v…`, `enclose`/`disclose`
- [ ] Shape arithmetic: `` (shape), `,` (ravel), `≢` (tally / first-axis-length), `≡` (depth)
- [ ] Scalar arithmetic primitives broadcast: `+ - × ÷ ⌈ ⌊ * ⍟ | ! ○`
- [ ] Scalar comparison primitives: `< ≤ = ≥ > ≠`
- [ ] Scalar logical: `~ ∧ ⍱ ⍲`
- [ ] Index generator: `n` (vector 1..n or 0..n-1 depending on `⎕IO`)
- [ ] `⎕IO` = 1 default (Dyalog convention)
- [ ] 40+ tests in `lib/apl/tests/scalar.sx`
### Phase 3 — structural primitives + indexing
- [ ] Reshape ``, ravel `,`, transpose `⍉` (full + dyadic axis spec)
- [ ] Take `↑`, drop `↓`, rotate `⌽` (last axis), `⊖` (first axis)
- [ ] Catenate `,` (last axis) and `⍪` (first axis)
- [ ] Index `⌷` (squad), bracket-indexing `A[I]` (sugar for `⌷`)
- [ ] Grade-up `⍋`, grade-down `⍒`
- [ ] Enclose `⊂`, disclose `⊃`, partition (subset deferred)
- [ ] Membership `∊`, find `` (dyadic), without `~` (dyadic), unique `` (deferred to phase 6)
- [ ] 40+ tests in `lib/apl/tests/structural.sx`
### Phase 4 — operators (THE SHOWCASE)
- [ ] Reduce `f/` (last axis), `f⌿` (first axis) — including `∧/`, `/`, `+/`, `×/`, `⌈/`, `⌊/`
- [ ] Scan `f\`, `f⍀`
- [ ] Each `f¨` — applies `f` to each scalar/element
- [ ] Outer product `∘.f``1 2 3 ∘.× 1 2 3` ↦ multiplication table
- [ ] Inner product `f.g``+.×` is matrix multiply
- [ ] Commute `f⍨``f⍨ x``x f x`, `x f⍨ y``y f x`
- [ ] Compose `f∘g` — applies `g` first then `f`
- [ ] Power `f⍣n` — apply f n times; `f⍣≡` until fixed point
- [ ] Rank `f⍤k` — apply f at sub-rank k
- [ ] At `@` — selective replace
- [ ] 40+ tests in `lib/apl/tests/operators.sx`
### Phase 5 — dfns + tradfns + control flow
- [ ] Dfn `{…}` with `` (left arg, may be absent → niladic/monadic), `⍵` (right arg), `∇` (recurse), guards `cond:expr`, default left arg `←default`
- [ ] Local assignment via `←` (lexical inside dfn)
- [ ] Tradfn `∇` header: `R←L F R;l1;l2`, statement-by-statement, branch via `→linenum`
- [ ] Dyalog control words: `:If/:Else/:EndIf`, `:While/:EndWhile`, `:For X :In V :EndFor`, `:Select/:Case/:EndSelect`, `:Trap`/`:EndTrap`
- [ ] Niladic / monadic / dyadic dispatch (function valence at definition time)
- [ ] `lib/apl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 6 — classic programs + drive corpus
- [ ] Classic programs in `lib/apl/tests/programs/`:
- [ ] `life.apl` — Conway's Game of Life as a one-liner using `⊂` `⊖` `⌽` `+/`
- [ ] `mandelbrot.apl` — complex iteration with rank-polymorphic `+ × ⌊` (or real-axis subset)
- [ ] `primes.apl``(2=+⌿0=A∘.|A)/A←N` sieve
- [ ] `n-queens.apl` — backtracking via reduce
- [ ] `quicksort.apl` — the classic Roger Hui one-liner
- [ ] System functions: `⎕FMT`, `⎕FR` (float repr), `⎕TS` (timestamp), `⎕IO`, `⎕ML` (migration level — fixed at 1), `⎕←` (print)
- [ ] Drive corpus to 100+ green
- [ ] Idiom corpus — `lib/apl/tests/idioms.sx` covering classic Roger Hui / Phil Last idioms
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,121 +0,0 @@
# Common-Lisp-on-SX: conditions + restarts on delimited continuations
The headline showcase is the **condition system**. Restarts are *resumable* exceptions — every other Lisp implementation reinvents this on host-stack unwind tricks. On SX restarts are textbook delimited continuations: `signal` walks the handler chain; `invoke-restart` resumes the captured continuation at the restart point. Same delcc primitive that powers Erlang actors, expressed as a different surface.
End-state goal: ANSI Common Lisp subset with a working condition/restart system, CLOS multimethods (with `:before`/`:after`/`:around`), the LOOP macro, packages, and ~150 hand-written + classic programs.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** ANSI Common Lisp surface. Read tables, dispatch macros (`#'`, `#(`, `#\`, `#:`, `#x`, `#b`, `#o`, ratios `1/3`).
- **Conformance:** ANSI X3.226 *as a target*, not bug-for-bug SBCL/CCL. "Reads like CL, runs like CL."
- **Test corpus:** custom + a curated slice of `ansi-test`. Plus classic programs: condition-system demo, restart-driven debugger, multiple-dispatch geometry, LOOP corpus.
- **Out of scope:** compilation to native, FFI, sockets, threads, MOP class redefinition, full pathname/logical-pathname machinery, structures with `:include` deep customization.
- **Packages:** simple — `defpackage`/`in-package`/`export`/`use-package`/`:cl`/`:cl-user`. No nicknames, no shadowing-import edge cases.
## Ground rules
- **Scope:** only touch `lib/common-lisp/**` and `plans/common-lisp-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. CL primitives go in `lib/common-lisp/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Common Lisp source
lib/common-lisp/reader.sx — tokenizer + reader (read macros, dispatch chars)
lib/common-lisp/parser.sx — AST: forms, declarations, lambda lists
lib/common-lisp/transpile.sx — AST → SX AST (entry: cl-eval-ast)
lib/common-lisp/runtime.sx — special forms, condition system, CLOS, packages, BIFs
```
Core mapping:
- **Symbol** = SX symbol with package prefix; package table is a flat dict.
- **Cons cell** = SX pair via `cons`/`car`/`cdr`; lists native.
- **Multiple values** = thread through `values`/`multiple-value-bind`; primary-value default for one-context callers.
- **Block / return-from** = captured continuation; `return-from name v` invokes the block-named `^k`.
- **Tagbody / go** = each tag is a continuation; `go tag` invokes it.
- **Unwind-protect** = scope frame with a cleanup thunk fired on any non-local exit.
- **Conditions / restarts** = layered handler chain on top of `handler-bind` + delcc. `signal` walks handlers; `invoke-restart` resumes a captured continuation.
- **CLOS** = generic functions are dispatch tables on argument-class lists; method combination computed lazily; `call-next-method` is a continuation.
- **Macros** = SX macros (sentinel-body) — defmacro lowers directly.
## Roadmap
### Phase 1 — reader + parser
- [ ] Tokenizer: symbols (with package qualification `pkg:sym` / `pkg::sym`), numbers (int, float, ratio `1/3`, `#xFF`, `#b1010`, `#o17`), strings `"…"` with `\` escapes, characters `#\Space` `#\Newline` `#\a`, comments `;`, block comments `#| … |#`
- [ ] Reader: list, dotted pair, quote `'`, function `#'`, quasiquote `` ` ``, unquote `,`, splice `,@`, vector `#(…)`, uninterned `#:foo`, nil/t literals
- [ ] Parser: lambda lists with `&optional` `&rest` `&key` `&aux` `&allow-other-keys`, defaults, supplied-p variables
- [ ] Unit tests in `lib/common-lisp/tests/read.sx`
### Phase 2 — sequential eval + special forms
- [ ] `cl-eval-ast`: `quote`, `if`, `progn`, `let`, `let*`, `flet`, `labels`, `setq`, `setf` (subset), `function`, `lambda`, `the`, `locally`, `eval-when`
- [ ] `block` + `return-from` via captured continuation
- [ ] `tagbody` + `go` via per-tag continuations
- [ ] `unwind-protect` cleanup frame
- [ ] `multiple-value-bind`, `multiple-value-call`, `multiple-value-prog1`, `values`, `nth-value`
- [ ] `defun`, `defparameter`, `defvar`, `defconstant`, `declaim`, `proclaim` (no-op)
- [ ] Dynamic variables — `defvar`/`defparameter` produce specials; `let` rebinds via parameterize-style scope
- [ ] 60+ tests in `lib/common-lisp/tests/eval.sx`
### Phase 3 — conditions + restarts (THE SHOWCASE)
- [ ] `define-condition` — class hierarchy rooted at `condition`/`error`/`warning`/`simple-error`/`simple-warning`/`type-error`/`arithmetic-error`/`division-by-zero`
- [ ] `signal`, `error`, `cerror`, `warn` — all walk the handler chain
- [ ] `handler-bind` — non-unwinding handlers, may decline by returning normally
- [ ] `handler-case` — unwinding handlers (delcc abort)
- [ ] `restart-case`, `with-simple-restart`, `restart-bind`
- [ ] `find-restart`, `invoke-restart`, `invoke-restart-interactively`, `compute-restarts`
- [ ] `with-condition-restarts` — associate restarts with a specific condition
- [ ] `*break-on-signals*`, `*debugger-hook*` (basic)
- [ ] Classic programs in `lib/common-lisp/tests/programs/`:
- [ ] `restart-demo.lisp` — division with `:use-zero` and `:retry` restarts
- [ ] `parse-recover.lisp` — parser with skipped-token restart
- [ ] `interactive-debugger.lisp` — ASCII REPL using `:debugger-hook`
- [ ] `lib/common-lisp/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 4 — CLOS
- [ ] `defclass` with `:initarg`/`:initform`/`:accessor`/`:reader`/`:writer`/`:allocation`
- [ ] `make-instance`, `slot-value`, `(setf slot-value)`, `with-slots`, `with-accessors`
- [ ] `defgeneric` with `:method-combination` (standard, plus `+`, `and`, `or`)
- [ ] `defmethod` with `:before` / `:after` / `:around` qualifiers
- [ ] `call-next-method` (continuation), `next-method-p`
- [ ] `class-of`, `find-class`, `slot-boundp`, `change-class` (basic)
- [ ] Multiple dispatch — method specificity by argument-class precedence list
- [ ] Built-in classes registered for tagged values (`integer`, `float`, `string`, `symbol`, `cons`, `null`, `t`)
- [ ] Classic programs:
- [ ] `geometry.lisp``intersect` generic dispatching on (point line), (line line), (line plane)…
- [ ] `mop-trace.lisp``:before` + `:after` printing call trace
### Phase 5 — macros + LOOP + reader macros
- [ ] `defmacro`, `macrolet`, `symbol-macrolet`, `macroexpand-1`, `macroexpand`
- [ ] `gensym`, `gentemp`
- [ ] `set-macro-character`, `set-dispatch-macro-character`, `get-macro-character`
- [ ] **The LOOP macro** — iteration drivers (`for … in/across/from/upto/downto/by`, `while`, `until`, `repeat`), accumulators (`collect`, `append`, `nconc`, `count`, `sum`, `maximize`, `minimize`), conditional clauses (`if`/`when`/`unless`/`else`), termination (`finally`/`thereis`/`always`/`never`), `named` blocks
- [ ] LOOP test corpus: 30+ tests covering all clause types
### Phase 6 — packages + stdlib drive
- [ ] `defpackage`, `in-package`, `export`, `use-package`, `import`, `find-package`
- [ ] Package qualification at the reader level — `cl:car`, `mypkg::internal`
- [ ] `:common-lisp` (`:cl`) and `:common-lisp-user` (`:cl-user`) packages
- [ ] Sequence functions — `mapcar`, `mapc`, `mapcan`, `reduce`, `find`, `find-if`, `position`, `count`, `every`, `some`, `notany`, `notevery`, `remove`, `remove-if`, `subst`
- [ ] List ops — `assoc`, `getf`, `nth`, `last`, `butlast`, `nthcdr`, `tailp`, `ldiff`
- [ ] String ops — `string=`, `string-upcase`, `string-downcase`, `subseq`, `concatenate`
- [ ] FORMAT — basic directives `~A`, `~S`, `~D`, `~F`, `~%`, `~&`, `~T`, `~{...~}` (iteration), `~[...~]` (conditional), `~^` (escape), `~P` (plural)
- [ ] Drive corpus to 200+ green
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,257 +0,0 @@
# SX Algebraic Data Types — Design
## Motivation
Every language implementation currently uses `{:tag "..." :field ...}` tagged dicts to
simulate sum types. This is verbose, error-prone (typos in tag strings go undetected), and
produces no exhaustiveness warnings. Native ADTs eliminate the pattern everywhere.
Examples of current workarounds:
- Haskell `Maybe a``{:tag "Just" :value x}` / `{:tag "Nothing"}`
- Prolog terms → `{:tag "functor" :name "foo" :args (list x y)}`
- Lua result type → `{:tag "ok" :value v}` / `{:tag "err" :msg s}`
- Common Lisp `cons` pairs → `{:tag "cons" :car a :cdr b}`
---
## Syntax
### `define-type`
```lisp
(define-type Name
(Ctor1 field1 field2 ...)
(Ctor2 field1 ...)
...)
```
Creates:
- Constructor functions: `Ctor1`, `Ctor2`, … (callable like normal functions)
- Type predicate: `Name?` — returns true for any value of type `Name`
- Constructor predicates: `Ctor1?`, `Ctor2?`, … (optional, auto-generated)
- Field accessors: `Ctor1-field1`, `Ctor1-field2`, … (optional, auto-generated)
Examples:
```lisp
(define-type Maybe
(Just value)
(Nothing))
(define-type Result
(Ok value)
(Err message))
(define-type Tree
(Leaf)
(Node left value right))
(define-type List-of
(Nil-of)
(Cons-of head tail))
```
Constructors with no fields are zero-argument constructors (singletons by value):
```lisp
(Nothing) ; => #<Nothing>
(Leaf) ; => #<Leaf>
```
### `match`
```lisp
(match expr
((Ctor1 a b) body)
((Ctor2 x) body)
((Ctor3) body)
(else body))
```
- Clauses are tried in order; first match wins.
- `else` clause is optional but suppresses exhaustiveness warnings.
- Pattern variables (`a`, `b`, `x`) are bound in the body scope.
- Wildcard `_` discards the matched value.
- Literal patterns: `42`, `"str"`, `true`, `nil` — match by value equality.
- Nested patterns: `((Node left (Leaf) right) body)` — nested constructor patterns.
Examples:
```lisp
(match result
((Ok v) (str "got: " v))
((Err m) (str "error: " m)))
(match tree
((Leaf) 0)
((Node l v r) (+ 1 (tree-depth l) (tree-depth r))))
```
---
## CEK Dispatch
### Runtime representation
ADT values are OCaml records (not dicts) — opaque, non-inspectable via `get`:
```ocaml
type adt_value = {
av_type : string; (* type name, e.g. "Maybe" *)
av_ctor : string; (* constructor name, e.g. "Just" *)
av_fields: value array; (* positional fields *)
}
```
In JS: `{ _adt: true, _type: "Maybe", _ctor: "Just", _fields: [v] }`.
`typeOf` returns the ADT type name (e.g. `"Maybe"`).
### `define-type` — special form
`stepSfDefineType(args, env, kont)`:
1. Parse `Name` and list of `(CtorN field...)` clauses.
2. For each constructor `CtorK` with fields `[f1, f2, …]`:
- Register `CtorK` as a `NativeFn` that takes `|fields|` args and returns an `AdtValue`.
- Register `CtorK?` as a predicate (`AdtValue` with matching ctor name → `true`).
- Register `CtorK-fN` as field accessor (returns `av_fields[N]`).
3. Register `Name?` as a predicate (`AdtValue` with matching type name → `true`).
4. All bindings go into the current environment via `env-bind!`.
5. Returns `Nil`.
This is an environment mutation — no new frame needed. Evaluates in one step.
### `match` — special form
`stepSfMatch(args, env, kont)`:
1. Push `MatchFrame` with `clauses` and `env` onto kont.
2. Return state evaluating the scrutinee `expr`.
3. `MatchFrame` continue: receive scrutinee value, walk clauses:
- For each `((CtorN vars...) body)`:
- If scrutinee is an `AdtValue` with `av_ctor = "CtorN"` and `av_fields.length = |vars|`:
- Bind `vars[i]``av_fields[i]` in fresh child env.
- Return state evaluating `body` in that env.
- `(else body)` — always matches, body evaluated in current env.
- Literal `42`/`"str"` patterns: match by value equality.
- Wildcard `_`: always matches, binds nothing.
4. If no clause matched and no `else`: raise `"match: no clause matched <value>"`.
Frame type: `"match"` — stores `cf_remaining` (clauses), `cf_env` (enclosing env).
---
## Interaction with `cond` / `case`
`match` is the primary dispatch form for ADTs. `cond` / `case` remain unchanged:
- `cond` tests arbitrary boolean expressions — still useful for non-ADT dispatch.
- `case` matches on equality to literal values — unchanged.
- `match` is the new form: structural pattern matching on ADT constructors.
They are orthogonal. A `match` clause can contain a `cond`; a `cond` clause can contain a `match`.
---
## Exhaustiveness checking
Emit a **warning** (not an error) when:
- A `match` has no `else` clause, AND
- Not all constructors of the scrutinee's type are covered.
Detection: when `define-type` runs, it registers the constructor set in a global table
`_adt_registry: type_name → [ctor_names]`. At `match` compile/evaluation time:
- If the scrutinee's type is in `_adt_registry` and not all ctors appear as patterns:
- `console.warn("[sx] match: non-exhaustive — missing: Ctor3, Ctor4 for type Maybe")`
- Execution continues (warning, not error).
This is best-effort: the scrutinee type is only known at runtime. The warning fires on
first non-exhaustive match evaluation, not at definition time.
---
## Recursive types
Recursive types work because constructors are registered as functions, and function bodies
are evaluated lazily:
```lisp
(define-type Tree
(Leaf)
(Node left value right))
; Recursive function over a recursive type:
(define (depth tree)
(match tree
((Leaf) 0)
((Node l v r) (+ 1 (max (depth l) (depth r))))))
```
No special treatment needed — the type definition doesn't need to know about recursion.
The constructor `Node` accepts any values, including other `Node` or `Leaf` values.
---
## Pattern variables
In `match` clauses, identifiers in constructor position that are NOT constructor names are
treated as pattern variables (bound to matched field values):
```lisp
(match x
((Just v) v) ; v bound to the wrapped value
((Nothing) nil))
(match pair
((Cons-of h t) (list h t))) ; h, t bound to head and tail
```
**Wildcard**: `_` is always a wildcard — matches anything, binds nothing.
```lisp
(match x
((Just _) "has value")
((Nothing) "empty"))
```
**Nested patterns**:
```lisp
(match tree
((Node (Leaf) v (Leaf)) (str "leaf node: " v))
((Node l v r) (str "inner node: " v)))
```
Nested patterns are matched recursively: the inner `(Leaf)` pattern checks that the
`left` field is itself a `Leaf` ADT value.
---
## Implementation Plan
### Phase 6a — `define-type` + basic `match` (no nested patterns, no exhaustiveness)
1. OCaml: add `AdtValue of adt_value` to `sx_types.ml`.
2. Evaluator: add `step-sf-define-type` — parse clauses, register ctor fns + predicates + accessors.
3. Evaluator: add `step-sf-match` + `MatchFrame` — linear scan of clauses, flat patterns only.
4. JS: same (AdtValue as plain object with `_adt`/`_type`/`_ctor`/`_fields` props).
### Phase 6b — nested patterns (separate fire)
Recursive `matchPattern(pattern, value, env)` helper that:
- Returns `{matched: bool, bindings: map}`
- Recursively matches sub-patterns against ADT fields.
### Phase 6c — exhaustiveness warnings (separate fire)
`_adt_registry` global + warning emission on first non-exhaustive match.
---
## Open questions (deferred to review)
1. **Accessor auto-generation**: should `Ctor-field` accessors be generated always, or only on demand? Risk: name collisions if two types have constructors with same field names.
2. **Singleton constructors**: `(Nothing)` — zero-arg ctor — should these be interned (same object every call) or fresh each time? Interning enables `eq?` checks but requires a global table.
3. **Printing/inspect**: `inspect` on an AdtValue should show `(Just 42)` not `#<adt:Just>`. Implement in `inspect` function or via `display`/`write` (Phase 17 ports).
4. **Pattern-matching on non-ADT values**: should `match` handle list patterns `(a . b)` and literal patterns in clause heads? Deferred — add only if needed by a language implementation.

View File

@@ -1,96 +0,0 @@
# HS conformance — blockers drain
Goal: take hyperscript conformance from **1277/1496 (85.4%)** to **1496/1496 (100%)** by clearing the blocked clusters and the design-done Bucket E subsystems.
This plan exists because the per-iteration `loops/hs` agent can't fit these into its 30-min budget — they need dedicated multi-commit sit-downs. Track progress here; refer to `plans/hs-conformance-to-100.md` for the canonical cluster ledger.
## Current state (2026-04-25)
- Loop running in `/root/rose-ash-loops/hs` (branch `loops/hs`)
- sx-tree MCP **fixed** (was a session-stale binary issue — restart of claude in the tmux window picked it up). Loop hinted to retry **#32**, **#29** first.
- Recent loop progress: ~1 commit/6h — easy wins drained, what's left needs focused attention.
## Remaining work
### Bucket-A/B/C blockers (small, in-place fixes)
| # | Cluster | Tests | Effort | Blocker | Fix sketch |
|---|---------|------:|--------|---------|------------|
| **17** | `tell` semantics | +3 | ~1h | Implicit-default-target ambiguity. `bare add .bar` inside `tell X` should target `X` but explicit `to me` must reach the original element. | Add `beingTold` symbol distinct from `me`; bare commands compile to `beingTold-or-me`; explicit `me` always the original. |
| **22** | window global fn fallback | +2-4 | ~1h | `foo()` where `foo` isn't SX-defined needs to fall back to `(host-global "foo")`. Three attempts failed: guard (host-level error not catchable), `env-has?` (not in HS kernel), `hs-win-call` (NativeFn not callable from CALL). | Add `symbol-bound?` predicate to HS kernel **OR** a host-call-fn primitive with arity-agnostic dispatch. |
| **29** | `hyperscript:before:init` / `:after:init` / `:parse-error` events | +4-6 | ~30m (post sx-tree fix) | Was sx-tree MCP outage. Now unblocked — loop should retry. 4 of 6 tests need stricter parser error-rejection (out of scope; mark partial). | Edit `integration.sx` to fire DOM events at activation boundaries. |
### Bucket D — medium features
| # | Cluster | Tests | Effort | Status |
|---|---------|------:|--------|--------|
| **31** | runtime null-safety error reporting | **+15-18** | **2-4h** | **THIS SESSION'S TARGET.** Plan node fully spec'd: 5 pieces of work. |
| **32** | MutationObserver mock + `on mutation` | +10-15 | ~2h | Was sx-tree-blocked. Now unblocked — loop hinted to retry. Multi-file: parser, compiler, runtime, runner mock, generator skip-list. |
| **33** | cookie API | +2 (remaining) | ~30m | Partial done (+3). Remaining 2 need `hs-method-call` runtime fallback for unknown methods + `hs-for-each` recognising host-array/proxy collections. |
| 34 | event modifier DSL | +6-8 | ~1-2h | `elsewhere`, `every`, count filters (`once`/`twice`/`3 times`/ranges), `from elsewhere`. Pending. |
| 35 | namespaced `def` | +3 | ~30m | Pending. |
### Bucket E — subsystems (design docs landed, multi-commit each)
Each has a design doc with a step-by-step checklist. These are 1-2 days of focused work each, not loop-fits.
| # | Subsystem | Tests | Design doc | Branch |
|---|-----------|------:|------------|--------|
| 36 | WebSocket + `socket` + RPC Proxy | +12-16 | `plans/designs/e36-websocket.md` | `worktree-agent-a9daf73703f520257` |
| 37 | Tokenizer-as-API | +16-17 | `plans/designs/e37-tokenizer-api.md` | `worktree-agent-a6bb61d59cc0be8b4` |
| 38 | SourceInfo API | +4 | `plans/designs/e38-sourceinfo.md` | `agent-e38-sourceinfo` |
| 39 | WebWorker plugin (parser-only stub) | +1 | `plans/designs/e39-webworker.md` | `hs-design-e39-webworker` |
| 40 | Real Fetch / non-2xx / before-fetch | +7 | `plans/designs/e40-real-fetch.md` | `worktree-agent-a94612a4283eaa5e0` |
### Bucket F — generator translation gaps
~25 tests SKIP'd because `tests/playwright/generate-sx-tests.py` bails with `return None`. Single dedicated generator-repair sit-down once Bucket D is drained. ~half-day.
## Order of attack
In approximate cost-per-test order:
1. **Loop self-heal** (no human work) — wait for #29, #32 to land via the running loop ⏱️ ~next 1-2 hours
2. **#31 null-safety** — biggest scoped single win, dedicated worktree agent (this session)
3. **#33 cookie API remainder** — quick partial completion
4. **#17 / #22 / #34 / #35** — small fiddly fixes, one sit-down each
5. **Bucket E** — pick one subsystem at a time. **#39 (WebWorker stub) first** — single commit, smallest. Then **#38 (SourceInfo)** — 4 commits. Then the bigger three (#36, #37, #40).
6. **Bucket F** — generator repair sweep at the end.
Estimated total to 100%: ~10-15 days of focused work, parallelisable across branches.
## Cluster #31 spec (full detail)
The plan note from `hs-conformance-to-100.md`:
> 18 tests in `runtimeErrors`. When accessing `.foo` on nil, emit a structured error with position info. One coordinated fix in the compiler emit paths for property access, function calls, set/put.
**Required pieces:**
1. **Generator-side `eval-hs-error` helper + recognizer** for `expect(await error("HS")).toBe("MSG")` blocks. In `tests/playwright/generate-sx-tests.py`.
2. **Runtime helpers** in `lib/hyperscript/runtime.sx`:
- `hs-null-error!` raising `'<sel>' is null`
- `hs-named-target` — wraps a query result with the original selector source
- `hs-named-target-list` — same for list results
3. **Compiler patches at every target-position `(query SEL)` emit** — wrap in named-target carrying the original selector source. ~17 command emit paths in `lib/hyperscript/compiler.sx`:
add, remove, hide, show, measure, settle, trigger, send, set, default, increment, decrement, put, toggle, transition, append, take.
4. **Function-call null-check** at bare `(name)`, `hs-method-call`, and `host-get` chains, deriving the leftmost-uncalled-name (`'x'` / `'x.y'`) from the parse tree.
5. **Possessive-base null-check** (`set x's y to true``'x' is null`).
**Files in scope:**
- `lib/hyperscript/runtime.sx` (new helpers)
- `lib/hyperscript/compiler.sx` (~17 emit-path edits)
- `tests/playwright/generate-sx-tests.py` (test recognizer)
- `tests/hs-run-filtered.js` (if mock helpers needed)
- `shared/static/wasm/sx/hs-runtime.sx` + `hs-compiler.sx` (WASM staging copies)
**Approach:** target-named pieces incrementally — runtime helpers first (no compiler change), then compiler emit paths in batches (group similar commands), then function-call/possessive at the end. Each batch is one commit if it lands +N tests; mark partial if it only unlocks part.
**Watch for:** smoke-range regressions (tests flipping pass→fail). Each commit: rerun smoke 0-195 and the `runtimeErrors` suite.
## Notes for future sessions
- `plans/hs-conformance-to-100.md` is the canonical cluster ledger — update it on every commit.
- `plans/hs-conformance-scoreboard.md` is the live tally — bump `Merged:` and the bucket roll-up.
- Loop has scope rule "never edit `spec/evaluator.sx` or broader SX kernel" — most fixes here stay in `lib/hyperscript/**`, `tests/`, generator. If a fix needs kernel work, surface to the user; don't merge silently.
- Cluster #22's `symbol-bound?` predicate would be a kernel addition — that's a real cross-boundary scope expansion.

View File

@@ -125,7 +125,7 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green.
- [x] Rest params (`...rest``&rest`) - [x] Rest params (`...rest``&rest`)
- [x] Default parameters (desugar to `if (param === undefined) param = default`) - [x] Default parameters (desugar to `if (param === undefined) param = default`)
- [ ] `var` hoisting (deferred — treated as `let` for now) - [ ] `var` hoisting (deferred — treated as `let` for now)
- [x] `let`/`const` TDZ — sentinel infrastructure (`__js_tdz_sentinel__`, `js-tdz?`, `js-tdz-check` in runtime.sx) - [ ] `let`/`const` TDZ (deferred)
### Phase 8 — Objects, prototypes, `this` ### Phase 8 — Objects, prototypes, `this`
- [x] Property descriptors (simplified — plain-dict `__proto__` chain, `js-set-prop` mutates) - [x] Property descriptors (simplified — plain-dict `__proto__` chain, `js-set-prop` mutates)
@@ -241,8 +241,6 @@ Append-only record of completed iterations. Loop writes one line per iteration:
- 29× Timeout (slow string/regex loops) - 29× Timeout (slow string/regex loops)
- 16× ReferenceError — still some missing globals - 16× ReferenceError — still some missing globals
- 2026-04-25 — **Regex engine (lib/js/regex.sx) + let/const TDZ infrastructure.** New file `lib/js/regex.sx`: 39-form pure-SX recursive backtracking engine installed via `js-regex-platform-override!`. Covers literals, `.`, `\d\w\s` + negations, `[abc]/[^abc]/[a-z]` char classes, `^\$\b\B` anchors, greedy+lazy quantifiers (`* + ? {n,m} *? +? ??`), capturing groups, non-capturing `(?:...)`, alternation `a|b`, flags `i`/`g`/`m`. Groups: match inner first → set capture → match rest (correct boundary), avoids including rest-nodes content in capture. Greedy: expand-first then backtrack (correct longest-match semantics). `js-regex-match-all` for String.matchAll. Fixed `String.prototype.match` to use platform engine (was calling stub). TDZ infrastructure added to `runtime.sx`: `__js_tdz_sentinel__` (unique sentinel dict), `js-tdz?`, `js-tdz-check`. `transpile.sx` passes `kind` through `js-transpile-var → js-vardecl-forms` (no behavioral change yet — infrastructure ready). `test262-runner.py` and `conformance.sh` updated to load `regex.sx` as epoch 6/50. Unit: **559/560** (was 522/522 before regex tests added, now +38 new tests; 1 pre-existing backtick failure). Conformance: **148/148** (unchanged). Gotchas: (1) `sx_insert_near` on a pattern inside a top-level function body inserts there (not at top level) — need to use `sx_insert_near` on a top-level symbol name. (2) Greedy quantifier must expand-first before trying rest-nodes; the naive "try rest at each step" produces lazy behavior. (3) Capturing groups must match inner nodes in isolation first (to get the group's end position) then match rest — appending inner+rest-nodes would include rest in the capture string.
## Phase 3-5 gotchas ## Phase 3-5 gotchas
Worth remembering for later phases: Worth remembering for later phases:
@@ -261,7 +259,17 @@ Anything that would require a change outside `lib/js/` goes here with a minimal
- **Pending-Promise await** — our `js-await-value` drains microtasks and unwraps *settled* Promises; it cannot truly suspend a JS fiber and resume later. Every Promise that settles eventually through the synchronous `resolve`/`reject` + microtask path works. A Promise that never settles without external input (e.g. a real `setTimeout` waiting on the event loop) would hit the `"await on pending Promise (no scheduler)"` error. Proper async suspension would need the JS eval path to run under `cek-step-loop` (not `eval-expr``cek-run`) and treat `await pending-Promise` as a `perform` that registers a resume thunk on the Promise's callback list. Non-trivial plumbing; out of scope for this phase. Consider it a Phase 9.5 item. - **Pending-Promise await** — our `js-await-value` drains microtasks and unwraps *settled* Promises; it cannot truly suspend a JS fiber and resume later. Every Promise that settles eventually through the synchronous `resolve`/`reject` + microtask path works. A Promise that never settles without external input (e.g. a real `setTimeout` waiting on the event loop) would hit the `"await on pending Promise (no scheduler)"` error. Proper async suspension would need the JS eval path to run under `cek-step-loop` (not `eval-expr``cek-run`) and treat `await pending-Promise` as a `perform` that registers a resume thunk on the Promise's callback list. Non-trivial plumbing; out of scope for this phase. Consider it a Phase 9.5 item.
- ~~**Regex platform primitives**~~ **RESOLVED**`lib/js/regex.sx` ships a pure-SX recursive backtracking engine. Installs via `js-regex-platform-override!` at load. Covers: literals, `.`, `\d\w\s` and negations, `[abc]` / `[^abc]` / ranges, `^` `$` `\b \B`, `* + ? {n,m}` (greedy + lazy), capturing + non-capturing groups, alternation `a|b`, flags `i` (case-insensitive), `g` (global, advances lastIndex), `m` (multiline anchors). `js-regex-match-all` for String.matchAll. String.prototype.match regex path updated to use platform engine (was calling stub). 34 new unit tests added (50005033). Conformance: 148/148 (unchanged — slice had no regex fixtures). - **Regex platform primitives** — runtime ships a substring-based stub (`js-regex-stub-test` / `-exec`). Overridable via `js-regex-platform-override!` so a real engine can be dropped in. Required platform-primitive surface:
- `regex-compile pattern flags` — build an opaque compiled handle
- `regex-test compiled s` → bool
- `regex-exec compiled s` → match dict `{match index input groups}` or nil
- `regex-match-all compiled s` → list of match dicts (or empty list)
- `regex-replace compiled s replacement` → string
- `regex-replace-fn compiled s fn` → string (fn receives match+groups, returns string)
- `regex-split compiled s` → list of strings
- `regex-source compiled` → string
- `regex-flags compiled` → string
Ideally a single `(js-regex-platform-install-all! platform)` entry point the host calls once at boot. OCaml would wrap `Str` / `Re` or a dedicated regex lib; JS host can just delegate to the native `RegExp`.
- **Math trig + transcendental primitives missing.** The scoreboard shows 34× "TypeError: not a function" across the Math category — every one a test calling `Math.sin/cos/tan/log/…` on our runtime. We shim `Math` via `js-global`; the SX runtime supplies `sqrt`, `pow`, `abs`, `floor`, `ceil`, `round` and a hand-rolled `trunc`/`sign`/`cbrt`/`hypot`. Nothing else. Missing platform primitives (each is a one-line OCaml/JS binding, but a primitive all the same — we can't land approximation polynomials from inside the JS shim, they'd blow `Math.sin(1e308)` precision): - **Math trig + transcendental primitives missing.** The scoreboard shows 34× "TypeError: not a function" across the Math category — every one a test calling `Math.sin/cos/tan/log/…` on our runtime. We shim `Math` via `js-global`; the SX runtime supplies `sqrt`, `pow`, `abs`, `floor`, `ceil`, `round` and a hand-rolled `trunc`/`sign`/`cbrt`/`hypot`. Nothing else. Missing platform primitives (each is a one-line OCaml/JS binding, but a primitive all the same — we can't land approximation polynomials from inside the JS shim, they'd blow `Math.sin(1e308)` precision):
- Trig: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2` - Trig: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`

View File

@@ -39,59 +39,91 @@ Representation choices (finalise in phase 1, document here):
## Roadmap ## Roadmap
### Phase 1 — tokenizer + term parser (no operator table) ### Phase 1 — tokenizer + term parser (no operator table)
- [ ] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings, punct `( ) , . [ ] | ! :-`, comments (`%`, `/* */`) - [x] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings, punct `( ) , . [ ] | ! :-`, comments (`%`, `/* */`)
- [ ] Parser: clauses `head :- body.` and facts `head.`; terms `atom | Var | number | compound(args) | [list,sugar]` - [x] Parser: clauses `head :- body.` and facts `head.`; terms `atom | Var | number | compound(args) | [list,sugar]`
- [ ] **Skip for phase 1:** operator table. `X is Y + 1` must be written `is(X, '+'(Y, 1))`; `=` written `=(X, Y)`. Operators land in phase 4. - [x] **Skip for phase 1:** operator table. `X is Y + 1` must be written `is(X, '+'(Y, 1))`; `=` written `=(X, Y)`. Operators land in phase 4.
- [ ] Unit tests in `lib/prolog/tests/parse.sx` - [x] Unit tests in `lib/prolog/tests/parse.sx` — 25 pass
### Phase 2 — unification + trail ### Phase 2 — unification + trail
- [ ] `make-var`, `walk` (follow binding chain), `prolog-unify!` (terms + trail → bool), `trail-undo-to!` - [x] `make-var`, `walk` (follow binding chain), `prolog-unify!` (terms + trail → bool), `trail-undo-to!`
- [ ] Occurs-check off by default, exposed as flag - [x] Occurs-check off by default, exposed as flag
- [ ] 30+ unification tests in `lib/prolog/tests/unify.sx`: atoms, vars, compounds, lists, cyclic (no-occurs-check), mutual occurs - [x] 30+ unification tests in `lib/prolog/tests/unify.sx`: atoms, vars, compounds, lists, cyclic (no-occurs-check), mutual occurs — 47 pass
### Phase 3 — clause DB + DFS solver + cut + first classic programs ### Phase 3 — clause DB + DFS solver + cut + first classic programs
- [ ] Clause DB: `"functor/arity" → list-of-clauses`, loader inserts - [x] Clause DB: `"functor/arity" → list-of-clauses`, loader inserts`pl-mk-db` / `pl-db-add!` / `pl-db-load!` / `pl-db-lookup` / `pl-db-lookup-goal`, 14 tests in `tests/clausedb.sx`
- [ ] Solver: DFS with choice points backed by delimited continuations (`lib/callcc.sx`). On goal entry, capture; per matching clause, unify head + recurse body; on failure, undo trail, try next - [x] Solver: DFS with choice points backed by delimited continuations (`lib/callcc.sx`). On goal entry, capture; per matching clause, unify head + recurse body; on failure, undo trail, try next — first cut: trail-based undo + CPS k (no shift/reset yet, per briefing gotcha). Built-ins so far: `true/0`, `fail/0`, `=/2`, `,/2`. Refactor to delimited conts later.
- [ ] Cut (`!`): cut barrier at current choice-point frame; collapse all up to barrier - [x] Cut (`!`): cut barrier at current choice-point frame; collapse all up to barrier — two-cut-box scheme: each `pl-solve-user!` creates a fresh inner-cut-box (set by `!` in this predicate's body) AND snapshots the outer-cut-box state on entry. After body fails, abandon clause alternatives if (a) inner was set or (b) outer transitioned false→true during this call. Lets post-cut goals backtrack normally while blocking pre-cut alternatives. 6 cut tests cover bare cut, clause-commit, choice-commit, cut+fail, post-cut backtracking, nested-cut isolation.
- [ ] Built-ins: `=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2` inside `;`, `call/1`, `write/1`, `nl/0` - [x] Built-ins: `=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2` inside `;`, `call/1`, `write/1`, `nl/0` — all 11 done. `write/1` and `nl/0` use a global `pl-output-buffer` string + `pl-output-clear!` for testability; `pl-format-term` walks deep then renders atoms/nums/strs/compounds/vars (var → `_<id>`). Note: cut-transparency via `;` not testable yet without operator support — `;(,(a,!), b)` parser-rejects because `,` is body-operator-only; revisit in phase 4.
- [ ] Arithmetic `is/2` with `+ - * / mod abs` - [x] Arithmetic `is/2` with `+ - * / mod abs``pl-eval-arith` walks deep, recurses on compounds, dispatches on functor; binary `+ - * / mod`, binary AND unary `-`, unary `abs`. `is/2` evaluates RHS, wraps as `("num" v)`, unifies via `pl-solve-eq!`. 11 tests cover each op + nested + ground LHS match/mismatch + bound-var-on-RHS chain.
- [ ] Classic programs in `lib/prolog/tests/programs/`: - [x] Classic programs in `lib/prolog/tests/programs/`:
- [ ] `append.pl` — list append (with backtracking) - [x] `append.pl` — list append (with backtracking)`lib/prolog/tests/programs/append.{pl,sx}`. 6 tests cover: build (`append([], L, X)`, `append([1,2], [3,4], X)`), check ground match/mismatch, full split-backtracking (`append(X, Y, [1,2,3])` → 4 solutions), single-deduce (`append(X, [3], [1,2,3])` → X=[1,2]).
- [ ] `reverse.pl` — naive reverse - [x] `reverse.pl` — naive reverse`lib/prolog/tests/programs/reverse.{pl,sx}`. Naive reverse via append: `reverse([H|T], R) :- reverse(T, RT), append(RT, [H], R)`. 6 tests cover empty, singleton, 3-list, 4-atom-list, ground match, ground mismatch.
- [ ] `member.pl` — generate all solutions via backtracking - [x] `member.pl` — generate all solutions via backtracking`lib/prolog/tests/programs/member.{pl,sx}`. Classic 2-clause `member(X, [X|_])` + `member(X, [_|T]) :- member(X, T)`. 7 tests cover bound-element hit/miss, empty list, generator (count = list length), first-solution binding, duplicate matches counted twice, anonymous head-cell unification.
- [ ] `nqueens.pl` — 8-queens - [x] `nqueens.pl` — 8-queens`lib/prolog/tests/programs/nqueens.{pl,sx}`. Permute-and-test formulation: `queens(L, Qs) :- permute(L, Qs), safe(Qs)` + `select` + `safe` + `no_attack`. Tested at N=1 (1), N=2 (0), N=3 (0), N=4 (2), N=5 (10) plus first-solution check at N=4 = `[2, 4, 1, 3]`. N=8 omitted — interpreter is too slow (40320 perms); add once compiled clauses or constraint-style placement land. `range/3` skipped pending arithmetic-comparison built-ins (`>/2` etc.).
- [ ] `family.pl` — facts + rules (parent/ancestor) - [x] `family.pl` — facts + rules (parent/ancestor)`lib/prolog/tests/programs/family.{pl,sx}`. 5 parent facts + male/female + derived `father`/`mother`/`ancestor`/`sibling`. 10 tests cover direct facts, fact count, transitive ancestor through 3 generations, descendant counting, gender-restricted father/mother, sibling via shared parent + `\=`.
- [ ] `lib/prolog/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` - [x] `lib/prolog/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` — bash script feeds load + eval epoch script to sx_server, parses each suite's `{:failed N :passed N :total N :failures (...)}` line, writes JSON (machine) + MD (human) scoreboards. Exit non-zero on any failure. `SX_SERVER` env var overrides binary path. First scoreboard: 183 / 183.
- [ ] Target: all 5 classic programs passing - [x] Target: all 5 classic programs passing — append (6) + reverse (6) + member (7) + nqueens (6) + family (10) = 35 program tests, all green. Phase 3 architecturally complete bar the conformance harness/scoreboard.
### Phase 4 — operator table + more built-ins (next run) ### Phase 4 — operator table + more built-ins (next run)
- [ ] Operator table parsing (prefix/infix/postfix, precedence, assoc) - [x] Operator table parsing (prefix/infix/postfix, precedence, assoc)`pl-op-table` (15 entries: `, ; -> = \= is < > =< >= + - * / mod`); precedence-climbing parser via `pp-parse-primary` + `pp-parse-term-prec` + `pp-parse-op-rhs`. Parens override precedence. Args inside compounds parsed at 999 so `,` stays as separator. xfx/xfy/yfx supported; prefix/postfix deferred (so `-5` still tokenises as bare atom + num as before). Comparison built-ins `</2 >/2 =</2 >=/2` added. New `tests/operators.sx` 19 tests cover assoc/precedence/parens + solver via infix.
- [ ] `assert/1`, `asserta/1`, `assertz/1`, `retract/1` - [x] `assert/1`, `asserta/1`, `assertz/1`, `retract/1``assert` aliases `assertz`. Helpers `pl-rt-to-ast` (deep-walk + replace runtime vars with `_G<id>` parse markers) + `pl-build-clause` (detect `:-` head). `assertz` uses `pl-db-add!`; `asserta` uses new `pl-db-prepend!`. `retract` walks goal, looks up by functor/arity, tries each clause via unification, removes first match by index (`pl-list-without`). 11 tests in `tests/dynamic.sx`. Rule-asserts now work — `:-` added to op table (prec 1200 xfx) with fix to `pl-token-op` accepting `"op"` token type. 15 tests in `tests/assert_rules.sx`.
- [ ] `findall/3`, `bagof/3`, `setof/3` - [x] `findall/3`, `bagof/3`, `setof/3` — shared `pl-collect-solutions` runs the goal in a fresh cut-box, deep-copies the template (via `pl-deep-copy` with var-map for shared-var preservation) on each success, returns false to backtrack, then restores trail. `findall` always succeeds with a (possibly empty) list. `bagof` fails on empty. `setof` builds a string-keyed dict via `pl-format-term` for sort+dedupe (via `keys` + `sort`), fails on empty. Existential `^` deferred (operator). 11 tests in `tests/findall.sx`.
- [ ] `copy_term/2`, `functor/3`, `arg/3`, `=../2` - [x] `copy_term/2`, `functor/3`, `arg/3`, `=../2``copy_term/2` reuses `pl-deep-copy` with a fresh var-map (preserves source aliasing). `functor/3` handles 4 modes: compound→{name, arity}, atom→{atom, 0}, num→{num, 0}, var with ground name+arity→constructed term (`pl-make-fresh-args` for compound case). `arg/3` extracts 1-indexed arg from compound. **`=../2` deferred** — the tokenizer treats `.` as the clause terminator unconditionally, so `=..` lexes as `=` + `.` + `.`; needs special-case lex (or surface syntax via a different name). 14 tests in `tests/term_inspect.sx`.
- [ ] String/atom predicates - [x] String/atom predicates
### Phase 5 — Hyperscript integration ### Phase 5 — Hyperscript integration
- [ ] `prolog-query` primitive callable from SX/Hyperscript - [x] `prolog-query` primitive callable from SX/Hyperscript
- [ ] Hyperscript DSL: `when allowed(user, :edit) then …` - [ ] Hyperscript DSL: `when allowed(user, :edit) then …`**blocked** (needs `lib/hyperscript/**`, out of scope)
- [ ] Integration suite - [ ] Integration suite
### Phase 6 — ISO conformance ### Phase 6 — ISO conformance
- [ ] Vendor Hirst's conformance tests - [x] Vendor Hirst's conformance tests
- [ ] Drive scoreboard to 200+ - [x] Drive scoreboard to 200+
### Phase 7 — compiler (later, optional) ### Phase 7 — compiler (later, optional)
- [ ] Compile clauses to SX continuations for speed - [x] Compile clauses to SX continuations for speed
- [ ] Keep interpreter as the reference - [x] Keep interpreter as the reference
## Progress log ## Progress log
_Newest first. Agent appends on every commit._ _Newest first. Agent appends on every commit._
- 2026-04-25 — `pl-compiled-matches-interp?` cross-validator in `compiler.sx`: loads source into both a plain and a compiled DB, runs the same goal, returns true iff solution counts match. `tests/cross_validate.sx` applies this to 17 goals across append/member/ancestor/cut/arithmetic/if-then-else, locking the interpreter as the reference against which any future compiler change must agree. Total **551** (+17).
- 2026-04-25 — Clause compiler (`lib/prolog/compiler.sx`): `pl-compile-clause` converts parse-AST clauses to SX closures `(fn (goal trail db cut-box k) bool)`. Pre-collects var names at compile time; `pl-cmp-build-term` reconstructs fresh runtime terms per call. `pl-compile-db!` compiles all clauses in a DB and stores them in `:compiled` table. `pl-solve-user!` in runtime.sx auto-dispatches to compiled lambdas when present, falls back to interpreted. `pl-try-compiled-clauses!` mirrors `pl-try-clauses!` cut semantics. 17 tests in `tests/compiler.sx`. Total **534** (+17).
- 2026-04-25 — `predsort/3` (insertion-sort with 3-arg comparator predicate, deduplicates `=` pairs), `term_variables/2` (collect unbound vars left-to-right, dedup by id), arithmetic extensions (`floor/1`, `ceiling/1`, `truncate/1`, `round/1`, `sign/1`, `sqrt/1`, `pow/2`, `**/2`, `^/2`, `integer/1`, `float/1`, `float_integer_part/1`, `float_fractional_part/1`). 21 tests in `tests/advanced.sx`. Total **517** (+21).
- 2026-04-25 — `sub_atom/5` (non-deterministic substring enumeration; CPS loop over all (start,sublen) pairs; trail-undo only on backtrack) + `aggregate_all/3` (6 templates: count/bag/sum/max/min/set; uses `pl-collect-solutions`). 25 tests in `tests/string_agg.sx`. Total **496** (+25).
- 2026-04-25 — `:-` operator + assert with rules: added `(list ":-" 1200 "xfx")` to `pl-op-table`; fixed `pl-token-op` to accept `"op"` token type (tokenizer emits `:-` as `"op"`, not `"atom"`). `pl-build-clause` already handled `("compound" ":-" ...)`. `assert((head :- body))` now works for facts+rules. 15 tests in `tests/assert_rules.sx`. Total **471** (+15).
- 2026-04-25 — IO/term predicates: `term_to_atom/2` (bidirectional: format term or parse atom), `term_string/2` (alias), `with_output_to/2` (atom/string sinks — saves/restores `pl-output-buffer`), `writeln/1`, `format/1` (~n/~t/~~), `format/2` (~w/~a/~d pull from arg list). 24 tests in `tests/io_predicates.sx`. Total **456** (+24).
- 2026-04-25 — Char predicates: `char_type/2` (9 modes: alpha/alnum/digit/digit(N)/space/white/upper(L)/lower(U)/ascii(C)/punct), `upcase_atom/2`, `downcase_atom/2`, `string_upper/2`, `string_lower/2`. 10 helpers using `char-code`/`char-from-code` SX primitives. 27 tests in `tests/char_predicates.sx`. Total **432** (+27).
- 2026-04-25 — Set/fold predicates: `foldl/4` (CPS fold-left, threads accumulator via `pl-apply-goal`), `list_to_set/2` (dedup preserving first-occurrence), `intersection/3`, `subtract/3`, `union/3` (all via `pl-struct-eq?`). 3 new helpers, 15 tests in `tests/set_predicates.sx`. Total **405** (+15).
- 2026-04-25 — Meta-call predicates: `forall/2` (negation-of-counterexample), `maplist/2` (goal over list), `maplist/3` (map goal building output list), `include/3` (filter by goal success), `exclude/3` (filter by goal failure). New `pl-apply-goal` helper extends a goal with extra args. 15 tests in `tests/meta_call.sx`. Total **390** (+15).
- 2026-04-25 — List/utility predicates: `==/2`, `\==/2` (structural equality/inequality via `pl-struct-eq?`), `flatten/2` (deep Prolog-list flatten), `numlist/3` (integer range list), `atomic_list_concat/2` (join with no sep), `atomic_list_concat/3` (join with separator), `sum_list/2`, `max_list/2`, `min_list/2` (arithmetic folds), `delete/3` (remove all struct-equal elements). 7 new helpers, 33 tests in `tests/list_predicates.sx`. Total **375** (+33).
- 2026-04-25 — Meta/logic predicates: `\+/1` (negation-as-failure, trail-undo on success), `not/1` (alias), `once/1` (commit to first solution via if-then-else), `ignore/1` (always succeed), `ground/1` (all vars bound), `sort/2` (sort + dedup by formatted key), `msort/2` (sort, keep dups), `atom_number/2` (bidirectional), `number_string/2` (bidirectional). 2 helpers (`pl-ground?`, `pl-sort-pairs-dedup`). 25 tests in `tests/meta_predicates.sx`. Total **342** (+25).
- 2026-04-25 — ISO utility predicates batch: `succ/2` (bidirectional), `plus/3` (3-mode bidirectional), `between/3` (backtracking range generator), `length/2` (bidirectional list length + var-list constructor), `last/2`, `nth0/3`, `nth1/3`, `max/2` + `min/2` in arithmetic eval. 6 new helper functions (`pl-list-length`, `pl-make-list-of-vars`, `pl-between-loop!`, `pl-solve-between!`, `pl-solve-last!`, `pl-solve-nth0!`). 29 tests in `tests/iso_predicates.sx`. Phase 6 complete: scoreboard already at 317, far above 200+ target. Hyperscript DSL blocked (needs `lib/hyperscript/**`). Total **317** (+29).
- 2026-04-25 — `prolog-query` SX API (`lib/prolog/query.sx`). New public API layer: `pl-load source-str → db`, `pl-query-all db query-str → list of solution dicts`, `pl-query-one db query-str → dict or nil`, `pl-query src query → list` (convenience). Each solution dict maps variable name strings to their formatted term strings. Var names extracted from pre-instantiation parse AST. Trail is marked before solve and reset after to ensure clean state. 16 tests in `tests/query_api.sx` cover fact lookup, no-solution, boolean queries, multi-var, recursive rules, is/2 built-in, query-one, convenience form. Total **288** (+16).
- 2026-04-25 — String/atom predicates. Type-test predicates: `var/1`, `nonvar/1`, `atom/1`, `number/1`, `integer/1`, `float/1` (always-fail), `compound/1`, `callable/1`, `atomic/1`, `is_list/1`. String/atom operations: `atom_length/2`, `atom_concat/3` (3 modes: both-ground, result+first, result+second), `atom_chars/2` (bidirectional), `atom_codes/2` (bidirectional), `char_code/2` (bidirectional), `number_codes/2`, `number_chars/2`. 7 helper functions in runtime.sx (`pl-list-to-prolog`, `pl-proper-list?`, `pl-prolog-list-to-sx`, `pl-solve-atom-concat!`, `pl-solve-atom-chars!`, `pl-solve-atom-codes!`, `pl-solve-char-code!`). 34 tests in `tests/atoms.sx`. Total **272** (+34).
- 2026-04-25 — `copy_term/2` + `functor/3` + `arg/3` (term inspection). `copy_term` is a one-line dispatch to existing `pl-deep-copy`. `functor/3` is bidirectional — decomposes a bound compound/atom/num into name+arity OR constructs from ground name+arity (atom+positive-arity → compound with N anonymous fresh args via `pl-make-fresh-args`; arity 0 → atom/num). `arg/3` extracts 1-indexed arg with bounds-fail. New helper `pl-solve-eq2!` for paired-unification with shared trail-undo. 14 tests in `tests/term_inspect.sx`. Total **238** (+14). `=..` deferred — `.` always tokenizes as clause terminator; needs special lexer case.
- 2026-04-25 — `findall/3` + `bagof/3` + `setof/3`. Shared collector `pl-collect-solutions` runs the goal in a fresh cut-box, deep-copies the template per success (`pl-deep-copy` walks term, allocates fresh runtime vars via shared var-map so co-occurrences keep aliasing), returns false to keep backtracking, then `pl-trail-undo-to!` to clean up. `findall` always builds a list. `bagof` fails on empty. `setof` uses a `pl-format-term`-keyed dict + SX `sort` for dedupe + ordering. New `tests/findall.sx` 11 tests. Total **224** (+11). Existential `^` deferred — needs operator.
- 2026-04-25 — Dynamic clauses: `assert/1`, `assertz/1`, `asserta/1`, `retract/1`. New helpers `pl-rt-to-ast` (deep-walk runtime term → parse-AST, mapping unbound runtime vars to `_G<id>` markers so `pl-instantiate-fresh` produces fresh vars per call) + `pl-build-clause` + `pl-db-prepend!` + `pl-list-without`. `retract` keeps runtime vars (so the caller's vars get bound), walks head for the functor/arity key, tries each stored clause via `pl-unify!`, removes the first match by index. 11 tests in `tests/dynamic.sx`; conformance script gained dynamic row. Total **213** (+11). Rule-form asserts (`(H :- B)`) deferred until `:-` is in the op table.
- 2026-04-25 — Phase 4 starts: operator-table parsing. Parser rewrite uses precedence climbing (xfx/xfy/yfx); 15-op table covers control (`, ; ->`), comparison (`= \\= is < > =< >=`), arithmetic (`+ - * / mod`). Parens override. Backwards-compatible: prefix-syntax compounds (`=(X, Y)`, `+(2, 3)`) still parse as before; existing 183 tests untouched. Added comparison built-ins `</2 >/2 =</2 >=/2` to runtime (eval both sides, compare). New `tests/operators.sx` 19 tests; conformance script gained an operators row. Total **202** (+19). Prefix/postfix deferred — `-5` keeps old bare-atom semantics.
- 2026-04-25 — Conformance harness landed. `lib/prolog/conformance.sh` runs all 9 suites in one sx_server epoch, parses the `{:failed/:passed/:total/:failures}` summary lines, and writes `scoreboard.json` + `scoreboard.md`. `SX_SERVER` env var overrides the binary path; default points at the main-repo build. Phase 3 fully complete: 183 / 183 passing across parse/unify/clausedb/solve/append/reverse/member/nqueens/family.
- 2026-04-25 — `family.pl` fifth classic program — completes the 5-program target. 5-fact pedigree + male/female + derived father/mother/ancestor/sibling. 10 tests cover fact lookup + count, transitive ancestor through 3 generations, descendant counting (5), gender-restricted derivations, sibling via shared parent guarded by `\=`. Total 183 (+10). All 5 classic programs ticked; Phase 3 needs only conformance harness + scoreboard left.
- 2026-04-25 — `nqueens.pl` fourth classic program. Permute-and-test variant exercises every Phase-3 feature: lists with `[H|T]` cons sugar, multi-clause backtracking, recursive `permute`/`select`/`safe`/`no_attack`, `is/2` arithmetic on diagonals, `\=/2` for diagonal-conflict check. 6 tests at N ∈ {1,2,3,4,5} with expected counts {1,0,0,2,10} + first-solution `[2,4,1,3]`. N=5 takes ~30s (120 perms × safe-check); N=8 omitted as it would be ~thousands of seconds. Total 173 (+6).
- 2026-04-25 — `member.pl` third classic program. Standard 2-clause definition; 7 tests cover bound-element hit/miss, empty-list fail, generator-count = list length, first-solution binding (X=11), duplicate elements matched twice on backtrack, anonymous-head unification (`member(a, [X, b, c])` binds X=a). Total 167 (+7).
- 2026-04-25 — `reverse.pl` second classic program. Naive reverse defined via append. 6 tests (empty/singleton/3-list/4-atom-list/ground match/ground mismatch). Confirms the solver handles non-trivial recursive composition: `reverse([1,2,3], R)` recurses to depth 3 then unwinds via 3 nested `append`s. Total 160 (+6).
- 2026-04-25 — `append.pl` first classic program. `lib/prolog/tests/programs/append.pl` is the canonical 2-clause source; `append.sx` embeds the source as a string (no file-read primitive in SX yet) and runs 6 tests covering build, check, full split-backtrack (4 solutions), and deduction modes. Helpers `pl-ap-list-to-sx` / `pl-ap-term-to-sx` convert deep-walked Prolog lists (`("compound" "." (h t))` / `("atom" "[]")`) to SX lists for structural assertion. Total 154 (+6).
- 2026-04-25 — `is/2` arithmetic landed. `pl-eval-arith` recursively evaluates ground RHS expressions (binary `+ - * /`, `mod`; binary+unary `-`; unary `abs`); `is/2` wraps the value as `("num" v)` and unifies via `pl-solve-eq!`, so it works in all three modes — bind unbound LHS, check ground LHS for equality, propagate from earlier var bindings on RHS. 11 tests, total 148 (+11). Without operator support, expressions must be written prefix: `is(X, +(2, *(3, 4)))`.
- 2026-04-25 — `write/1` + `nl/0` landed using global string buffer (`pl-output-buffer` + `pl-output-clear!` + `pl-output-write!`). `pl-format-term` walks deep + dispatches on atom/num/str/compound/var; `pl-format-args` recursively comma-joins. 7 new tests cover atom/num/compound formatting, conjunction order, var-walk, and `nl`. Built-ins box (`=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2`, `call/1`, `write/1`, `nl/0`) now ticked. Total 137 (+7).
- 2026-04-25 — `->/2` if-then-else landed (both `;(->(C,T), E)` and standalone `->(C, T)``(C -> T ; fail)`). `pl-solve-or!` now special-cases `->` in left arg → `pl-solve-if-then-else!`. Cond runs in a fresh local cut-box (ISO opacity for cut inside cond). Then-branch can backtrack, else-branch can backtrack, but cond commits to first solution. 9 new tests covering both forms, both branches, binding visibility, cond-commit, then-backtrack, else-backtrack. Total 130 (+9).
- 2026-04-25 — Built-ins `\=/2`, `;/2`, `call/1` landed. `pl-solve-not-eq!` (try unify, always undo, succeed iff unify failed). `pl-solve-or!` (try left, on failure check cut and only try right if not cut). `call/1` opens a fresh inner cut-box (ISO opacity: cut inside `call(G)` commits G, not caller). 11 new tests in `tests/solve.sx` cover atoms+vars for `\=`, both branches + count for `;`, and `call/1` against atoms / compounds / bound goal vars. Total 121 (+11). Box not yet ticked — `->/2`, `write/1`, `nl/0` still pending.
- 2026-04-25 — Cut (`!/0`) landed. `pl-cut?` predicate; solver functions all take a `cut-box`; `pl-solve-user!` creates a fresh inner-cut-box and snapshots `outer-was-cut`; `pl-try-clauses!` abandons alternatives when inner.cut OR (outer.cut transitioned false→true during this call). 6 new cut tests in `tests/solve.sx` covering bare cut, clause-commit, choice-commit, cut+fail blocks alt clauses, post-cut goal backtracks freely, inner cut isolation. Total 110 (+6).
- 2026-04-25 — Phase 3 DFS solver landed (CPS, trail-based backtracking; delimited conts deferred). `pl-solve!` + `pl-solve-eq!` + `pl-solve-user!` + `pl-try-clauses!` + `pl-solve-once!` + `pl-solve-count!` in runtime.sx. Built-ins: `true/0`, `fail/0`, `=/2`, `,/2`. New `tests/solve.sx` 18/18 green covers atomic goals, =, conjunction, fact lookup, multi-solution count, recursive ancestor rule, trail-undo verification. Bug fix: `pl-instantiate` had no `("clause" h b)` case → vars in rule head/body were never instantiated, so rule resolution silently failed against runtime-var goals. Added clause case to recurse with shared var-env. Total 104 (+18).
- 2026-04-24 — Phase 3 clause DB landed: `pl-mk-db` + `pl-head-key` / `pl-clause-key` / `pl-goal-key` + `pl-db-add!` / `pl-db-load!` / `pl-db-lookup` / `pl-db-lookup-goal` in runtime.sx. New `tests/clausedb.sx` 14/14 green. Total 86 (+14). Loader preserves declaration order (append!).
- 2026-04-24 — Verified phase 1+2 already implemented on loops/prolog: `pl-parse-tests-run!` 25/25, `pl-unify-tests-run!` 47/47 (72 total). Ticked phase 1+2 boxes.
- _(awaiting phase 1)_ - _(awaiting phase 1)_
## Blockers ## Blockers
_Shared-file issues that need someone else to fix. Minimal repro only._ _Shared-file issues that need someone else to fix. Minimal repro only._
- _(none yet)_ - **Phase 5 Hyperscript DSL** — `lib/hyperscript/**` is out of scope for this loop. Needs `lib/hyperscript/parser.sx` + evaluator to add `when allowed(user, :edit) then …` syntax. Skipping; Phase 5 item 1 (`prolog-query` SX API) is done.

View File

@@ -1,124 +0,0 @@
# Ruby-on-SX: fibers + blocks + open classes on delimited continuations
The headline showcase is **fibers** — Ruby's `Fiber.new { … Fiber.yield v … }` / `Fiber.resume` are textbook delimited continuations with sugar. MRI implements them by swapping C stacks; on SX they fall out of the existing `perform`/`cek-resume` machinery for free. Plus blocks/yield (lexical escape continuations, same shape as Smalltalk's non-local return), method_missing, and singleton classes.
End-state goal: Ruby 2.7-flavoured subset, Enumerable mixin, fibers + threads-via-fibers (no real OS threads), method_missing-driven DSLs, ~150 hand-written + classic programs.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Ruby 2.7. No 3.x pattern matching, no rightward assignment, no endless methods. We pick 2.7 because it's the biggest semantic surface that still parses cleanly.
- **Conformance:** "Reads like Ruby, runs like Ruby." Slice of RubySpec (Core + Library subset), not full RubySpec.
- **Test corpus:** custom + curated RubySpec slice. Plus classic programs: fiber-based generator, internal DSL with method_missing, mixin-based Enumerable on a custom class.
- **Out of scope:** real threads, GIL, refinements, `binding_of_caller` from non-Ruby contexts, Encoding object beyond UTF-8/ASCII-8BIT, RubyVM::* introspection beyond bytecode-disassembly placeholder, IO subsystem beyond `puts`/`gets`/`File.read`.
- **Symbols:** SX symbols. Strings are mutable copies; symbols are interned.
## Ground rules
- **Scope:** only touch `lib/ruby/**` and `plans/ruby-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Ruby primitives go in `lib/ruby/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Ruby source
lib/ruby/tokenizer.sx — keywords, ops, %w[], %i[], heredocs (deferred), regex (deferred)
lib/ruby/parser.sx — AST: classes, modules, methods, blocks, calls
lib/ruby/transpile.sx — AST → SX AST (entry: rb-eval-ast)
lib/ruby/runtime.sx — class table, MOP, dispatch, fibers, primitives
```
Core mapping:
- **Object** = SX dict `{:class :ivars :singleton-class?}`. Instance variables live in `ivars` keyed by symbol.
- **Class** = SX dict `{:name :superclass :methods :class-methods :metaclass :includes :prepends}`. Class table is flat.
- **Method dispatch** = lookup walks ancestor chain (prepended → class → included modules → superclass → …). Falls back to `method_missing` with a `Symbol`+args.
- **Block** = lambda + escape continuation. `yield` invokes the block in current context. `return` from within a block invokes the enclosing-method's escape continuation.
- **Proc** = lambda without strict arity. `Proc.new` + `proc {}`.
- **Lambda** = lambda with strict arity + `return`-returns-from-lambda semantics.
- **Fiber** = pair of continuations (resume-k, yield-k) wrapped in a record. `Fiber.new { … }` builds it; `Fiber.resume` invokes the resume-k; `Fiber.yield` invokes the yield-k. Built directly on `perform`/`cek-resume`.
- **Module** = class without instance allocation. `include` puts it in the chain; `prepend` puts it earlier; `extend` puts it on the singleton.
- **Singleton class** = lazily allocated per-object class for `def obj.foo` definitions.
- **Symbol** = interned SX symbol. `:foo` reads as `(quote foo)` flavour.
## Roadmap
### Phase 1 — tokenizer + parser
- [ ] Tokenizer: keywords (`def end class module if unless while until do return yield begin rescue ensure case when then else elsif`), identifiers (lowercase = local/method, `@` = ivar, `@@` = cvar, `$` = global, uppercase = constant), numbers (int, float, `0x` `0o` `0b`, `_` separators), strings (`"…"` interpolation, `'…'` literal, `%w[a b c]`, `%i[a b c]`), symbols `:foo` `:"…"`, operators (`+ - * / % ** == != < > <= >= <=> === =~ !~ << >> & | ^ ~ ! && || and or not`), `:: . , ; ( ) [ ] { } -> => |`, comments `#`
- [ ] Parser: program is sequence of statements separated by newlines or `;`; method def `def name(args) … end`; class `class Foo < Bar … end`; module `module M … end`; block `do |a, b| … end` and `{ |a, b| … }`; call sugar (no parens), `obj.method`, `Mod::Const`; arg shapes (positional, default, splat `*args`, double-splat `**opts`, block `&blk`)
- [ ] If/while/case expressions (return values), `unless`/`until`, postfix modifiers
- [ ] Begin/rescue/ensure/retry, raise, raise with class+message
- [ ] Unit tests in `lib/ruby/tests/parse.sx`
### Phase 2 — object model + sequential eval
- [ ] Class table bootstrap: `BasicObject`, `Object`, `Kernel`, `Module`, `Class`, `Numeric`, `Integer`, `Float`, `String`, `Symbol`, `Array`, `Hash`, `Range`, `NilClass`, `TrueClass`, `FalseClass`, `Proc`, `Method`
- [ ] `rb-eval-ast`: literals, variables (local, ivar, cvar, gvar, constant), assignment (single and parallel `a, b = 1, 2`, splat receive), method call, message dispatch
- [ ] Method lookup walks ancestor chain; cache hit-class per `(class, selector)`
- [ ] `method_missing` fallback constructing args list
- [ ] `super` and `super(args)` — lookup in defining class's superclass
- [ ] Singleton class allocation on first `def obj.foo` or `class << obj`
- [ ] `nil`, `true`, `false` are singletons of their classes; tagged values aren't boxed
- [ ] Constant lookup (lexical-then-inheritance) with `Module.nesting`
- [ ] 60+ tests in `lib/ruby/tests/eval.sx`
### Phase 3 — blocks + procs + lambdas
- [ ] Method invocation captures escape continuation `^k` for `return`; binds it as block's escape
- [ ] `yield` invokes implicit block
- [ ] `block_given?`, `&blk` parameter, `&proc` arg unpacking
- [ ] `Proc.new`, `proc { }`, `lambda { }` (or `->(x) { x }`)
- [ ] Lambda strict arity + lambda-local `return` semantics
- [ ] Proc lax arity (`a, b, c` unpacks Array; missing args nil)
- [ ] `break`, `next`, `redo``break` is escape-from-loop-or-block; `next` is escape-from-block-iteration; `redo` re-runs current iteration
- [ ] 30+ tests in `lib/ruby/tests/blocks.sx`
### Phase 4 — fibers (THE SHOWCASE)
- [ ] `Fiber.new { |arg| … Fiber.yield v … }` allocates a fiber record with paired continuations
- [ ] `Fiber.resume(args…)` resumes the fiber, returning the value passed to `Fiber.yield`
- [ ] `Fiber.yield(v)` from inside the fiber suspends and returns control to the resumer
- [ ] `Fiber.current` from inside the fiber
- [ ] `Fiber#alive?`, `Fiber#raise` (deferred)
- [ ] `Fiber.transfer` — symmetric coroutines (resume from any side)
- [ ] Classic programs in `lib/ruby/tests/programs/`:
- [ ] `generator.rb` — pull-style infinite enumerator built on fibers
- [ ] `producer-consumer.rb` — bounded buffer with `Fiber.transfer`
- [ ] `tree-walk.rb` — recursive tree walker that yields each node, driven by `Fiber.resume`
- [ ] `lib/ruby/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 5 — modules + mixins + metaprogramming
- [ ] `include M` — appends M's methods after class methods in chain
- [ ] `prepend M` — prepends M before class methods
- [ ] `extend M` — adds M to singleton class
- [ ] `Module#ancestors`, `Module#included_modules`
- [ ] `define_method`, `class_eval`, `instance_eval`, `module_eval`
- [ ] `respond_to?`, `respond_to_missing?`, `method_missing`
- [ ] `Object#send`, `Object#public_send`, `Object#__send__`
- [ ] `Module#method_added`, `singleton_method_added` hooks
- [ ] Hooks: `included`, `extended`, `inherited`, `prepended`
- [ ] Internal-DSL classic program: `lib/ruby/tests/programs/dsl.rb`
### Phase 6 — stdlib drive
- [ ] `Enumerable` mixin: `each` (abstract), `map`, `select`/`filter`, `reject`, `reduce`/`inject`, `each_with_index`, `each_with_object`, `take`, `drop`, `take_while`, `drop_while`, `find`/`detect`, `find_index`, `any?`, `all?`, `none?`, `one?`, `count`, `min`, `max`, `min_by`, `max_by`, `sort`, `sort_by`, `group_by`, `partition`, `chunk`, `each_cons`, `each_slice`, `flat_map`, `lazy`
- [ ] `Comparable` mixin: `<=>`, `<`, `<=`, `>`, `>=`, `==`, `between?`, `clamp`
- [ ] `Array`: indexing, slicing, `push`/`pop`/`shift`/`unshift`, `concat`, `flatten`, `compact`, `uniq`, `sort`, `reverse`, `zip`, `dig`, `pack`/`unpack` (deferred)
- [ ] `Hash`: `[]`, `[]=`, `delete`, `merge`, `each_pair`, `keys`, `values`, `to_a`, `dig`, `fetch`, default values, default proc
- [ ] `Range`: `each`, `step`, `cover?`, `include?`, `size`, `min`, `max`
- [ ] `String`: indexing, slicing, `split`, `gsub` (string-arg version, regex deferred), `sub`, `upcase`, `downcase`, `strip`, `chomp`, `chars`, `bytes`, `to_i`, `to_f`, `to_sym`, `*`, `+`, `<<`, format with `%`
- [ ] `Integer`: `times`, `upto`, `downto`, `step`, `digits`, `gcd`, `lcm`
- [ ] Drive corpus to 200+ green
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,116 +0,0 @@
# Smalltalk-on-SX: blocks with non-local return on delimited continuations
The headline showcase is **blocks** — Smalltalk's closures with non-local return (`^expr` aborts the enclosing *method*, not the block). Every other Smalltalk on top of a host VM (RSqueak on PyPy, GemStone on C, Maxine on Java) reinvents non-local return on whatever stack discipline the host gives them. On SX it's a one-liner: a block holds a captured continuation; `^` just invokes it. Message-passing OO falls out cheaply on top of the existing component / dispatch machinery.
End-state goal: ANSI-ish Smalltalk-80 subset, SUnit working, ~200 hand-written tests + a vendored slice of the Pharo kernel tests, classic corpus (eight queens, quicksort, mandelbrot, Conway's Life).
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Pharo / Squeak chunk format (`!` separators, `Object subclass: #Foo …`). No fileIn/fileOut images — text source only.
- **Conformance:** ANSI X3J20 *as a target*, not bug-for-bug Squeak. "Reads like Smalltalk, runs like Smalltalk."
- **Test corpus:** SUnit ported to SX-Smalltalk + custom programs + a curated slice of Pharo `Kernel-Tests` / `Collections-Tests`.
- **Image:** out of scope. Source-only. No `become:` between sessions, no snapshotting.
- **Reflection:** `class`, `respondsTo:`, `perform:`, `doesNotUnderstand:` in. `become:` (object-identity swap) **in** — it's a good CEK exercise. Method modification at runtime in.
- **GUI / Morphic / threads:** out entirely.
## Ground rules
- **Scope:** only touch `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Smalltalk primitives go in `lib/smalltalk/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Smalltalk source
lib/smalltalk/tokenizer.sx — selectors, keywords, literals, $c, #sym, #(…), $'…'
lib/smalltalk/parser.sx — AST: classes, methods, blocks, cascades, sends
lib/smalltalk/transpile.sx — AST → SX AST (entry: smalltalk-eval-ast)
lib/smalltalk/runtime.sx — class table, MOP, dispatch, primitives
```
Core mapping:
- **Class** = SX dict `{:name :superclass :ivars :methods :class-methods :metaclass}`. Class table is a flat dict keyed by class name.
- **Object** = SX dict `{:class :ivars}``ivars` keyed by symbol. Tagged ints / floats / strings / symbols are not boxed; their class is looked up by SX type.
- **Method** = SX lambda closing over a `self` binding + temps. Body wrapped in a delimited continuation so `^` can escape.
- **Message send** = `(st-send receiver selector args)` — does class-table lookup, walks superclass chain, falls back to `doesNotUnderstand:` with a `Message` object.
- **Block** `[:x | … ^v … ]` = lambda + captured `^k` (the method-return continuation). Invoking `^` calls `k`; outer block invocation past method return raises `BlockContext>>cannotReturn:`.
- **Cascade** `r m1; m2; m3` = `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`.
- **`ifTrue:ifFalse:` / `whileTrue:`** = ordinary block sends; the runtime intrinsifies them in the JIT path so they compile to native branches (Tier 1 of bytecode expansion already covers this pattern).
- **`become:`** = swap two object identities everywhere — in SX this is a heap walk, but we restrict to `oneWayBecome:` (cheap: rewrite class field) by default.
## Roadmap
### Phase 1 — tokenizer + parser
- [ ] Tokenizer: identifiers, keywords (`foo:`), binary selectors (`+`, `==`, `,`, `->`, `~=` etc.), numbers (radix `16r1F`, scaled `1.5s2`), strings `'…''…'`, characters `$c`, symbols `#foo` `#'foo bar'` `#+`, byte arrays `#[1 2 3]`, literal arrays `#(1 #foo 'x')`, comments `"…"`
- [ ] Parser: chunk format (`! !` separators), class definitions (`Object subclass: #X instanceVariableNames: '…' classVariableNames: '…' …`), method definitions (`extend: #Foo with: 'bar ^self'`), pragmas `<primitive: 1>`, blocks `[:a :b | | t1 t2 | …]`, cascades, message precedence (unary > binary > keyword)
- [ ] Unit tests in `lib/smalltalk/tests/parse.sx`
### Phase 2 — object model + sequential eval
- [ ] Class table + bootstrap: `Object`, `Behavior`, `Class`, `Metaclass`, `UndefinedObject`, `Boolean`/`True`/`False`, `Number`/`Integer`/`Float`, `String`, `Symbol`, `Array`, `Block`
- [ ] `smalltalk-eval-ast`: literals, variable reference, assignment, message send, cascade, sequence, return
- [ ] Method lookup: walk class → superclass; cache hit-class on `(class, selector)`
- [ ] `doesNotUnderstand:` fallback constructing `Message` object
- [ ] `super` send (lookup starts at superclass of *defining* class, not receiver class)
- [ ] 30+ tests in `lib/smalltalk/tests/eval.sx`
### Phase 3 — blocks + non-local return (THE SHOWCASE)
- [ ] Method invocation captures a `^k` (the return continuation) and binds it as the block's escape
- [ ] `^expr` from inside a block invokes that captured `^k`
- [ ] `BlockContext>>value`, `value:`, `value:value:`, …, `valueWithArguments:`
- [ ] `whileTrue:` / `whileTrue` / `whileFalse:` / `whileFalse` as ordinary block sends — runtime intrinsifies the loop in the bytecode JIT
- [ ] `ifTrue:` / `ifFalse:` / `ifTrue:ifFalse:` as block sends, similarly intrinsified
- [ ] Escape past returned-from method raises `BlockContext>>cannotReturn:`
- [ ] Classic programs in `lib/smalltalk/tests/programs/`:
- [ ] `eight-queens.st`
- [ ] `quicksort.st`
- [ ] `mandelbrot.st`
- [ ] `life.st` (Conway's Life, glider gun)
- [ ] `fibonacci.st` (recursive + memoised)
- [ ] `lib/smalltalk/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 4 — reflection + MOP
- [ ] `Object>>class`, `class>>name`, `class>>superclass`, `class>>methodDict`, `class>>selectors`
- [ ] `Object>>perform:` / `perform:with:` / `perform:withArguments:`
- [ ] `Object>>respondsTo:`, `Object>>isKindOf:`, `Object>>isMemberOf:`
- [ ] `Behavior>>compile:` — runtime method addition
- [ ] `Object>>becomeForward:` (one-way become; rewrites the class field of `aReceiver`)
- [ ] Exceptions: `Exception`, `Error`, `signal`, `signal:`, `on:do:`, `ensure:`, `ifCurtailed:` — built on top of SX `handler-bind`/`raise`
### Phase 5 — collections + numeric tower
- [ ] `SequenceableCollection`/`OrderedCollection`/`Array`/`String`/`Symbol`
- [ ] `HashedCollection`/`Set`/`Dictionary`/`IdentityDictionary`
- [ ] `Stream` hierarchy: `ReadStream`/`WriteStream`/`ReadWriteStream`
- [ ] `Number` tower: `SmallInteger`/`LargePositiveInteger`/`Float`/`Fraction`
- [ ] `String>>format:`, `printOn:` for everything
### Phase 6 — SUnit + corpus to 200+
- [ ] Port SUnit (TestCase, TestSuite, TestResult) — written in SX-Smalltalk, runs in itself
- [ ] Vendor a slice of Pharo `Kernel-Tests` and `Collections-Tests`
- [ ] Drive the scoreboard up: aim for 200+ green tests
- [ ] Stretch: ANSI Smalltalk validator subset
### Phase 7 — speed (optional)
- [ ] Method-dictionary inline caching (already in CEK as a primitive; just wire selector cache)
- [ ] Block intrinsification beyond `whileTrue:` / `ifTrue:`
- [ ] Compare against GNU Smalltalk on the corpus
## Progress log
_Newest first. Agent appends on every commit._
- _(none yet)_
## Blockers
_Shared-file issues that need someone else to fix. Minimal repro only._
- _(none yet)_

View File

@@ -1,127 +0,0 @@
# Tcl-on-SX: uplevel/upvar = stack-walking delcc, everything-is-a-string
The headline showcase is **uplevel/upvar** — Tcl's superpower for defining your own control structures. `uplevel` evaluates a script in the *caller's* stack frame; `upvar` aliases a variable in the caller. On a normal language host this requires deep VM cooperation; on SX it falls out of the env-chain made first-class via captured continuations. Plus the *Dodekalogue* (12 rules), command-substitution everywhere, and "everything is a string" homoiconicity.
End-state goal: Tcl 8.6-flavoured subset, the Dodekalogue parser, namespaces, `try`/`catch`/`return -code`, `coroutine` (built on fibers), classic programs that show off uplevel-driven DSLs, ~150 hand-written tests.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Tcl 8.6 surface. The 12-rule Dodekalogue. Brace-quoted scripts deferred-evaluate; double-quoted ones substitute.
- **Conformance:** "Reads like Tcl, runs like Tcl." Slice of Tcl's own test suite, not full TCT.
- **Test corpus:** custom + curated `tcl-tests/` slice. Plus classic programs: define-your-own `for-each-line`, expression-language compiler-in-Tcl, fiber-based event loop.
- **Out of scope:** Tk, sockets beyond a stub, threads (mapped to `coroutine` only), `package require` of binary loadables, `dde`/`registry` Windows shims, full `clock format` locale support.
- **Channels:** `puts` and `gets` on `stdout`/`stdin`/`stderr`; `open` on regular files; no async I/O beyond what `coroutine` gives.
## Ground rules
- **Scope:** only touch `lib/tcl/**` and `plans/tcl-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Tcl primitives go in `lib/tcl/runtime.sx`.
- **SX files:** use `sx-tree` MCP tools only.
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
## Architecture sketch
```
Tcl source
lib/tcl/tokenizer.sx — the Dodekalogue: words, [..], ${..}, "..", {..}, ;, \n, \, #
lib/tcl/parser.sx — list-of-words AST (script = list of commands; command = list of words)
lib/tcl/transpile.sx — AST → SX AST (entry: tcl-eval-script)
lib/tcl/runtime.sx — env stack, command table, uplevel/upvar, coroutines, BIFs
```
Core mapping:
- **Value** = string. Internally we cache a "shimmer" representation (list, dict, integer, double) for performance, but every value can be re-stringified.
- **Variable** = entry in current frame's env. Frames form a stack; level-0 is the global frame.
- **Command** = entry in command table; first word of any list dispatches into it. User-defined via `proc`. Built-ins are SX functions registered in the table.
- **Frame** = `{:locals (dict) :level n :parent frame}`. Each `proc` call pushes a frame; commands run in current frame.
- **`uplevel #N script`** = walk frame chain to absolute level N (or relative if no `#`); evaluate script in that frame's env.
- **`upvar [#N] varname localname`** = bind `localname` in the current frame as an alias to `varname` in the level-N frame (env-chain delegate).
- **`return -code N`** = control flow as integers: 0=ok, 1=error, 2=return, 3=break, 4=continue. `catch` traps any non-zero; `try` adds named handlers.
- **`coroutine`** = fiber on top of `perform`/`cek-resume`. `yield`/`yieldto` suspend; calling the coroutine command resumes.
- **List / dict** = list-shaped string ("element1 element2 …") with a cached parsed form. Modifications dirty the string cache.
## Roadmap
### Phase 1 — tokenizer + parser (the Dodekalogue)
- [ ] Tokenizer applying the 12 rules:
1. Commands separated by `;` or newlines
2. Words separated by whitespace within a command
3. Double-quoted words: `\` escapes + `[…]` + `${…}` + `$var` substitution
4. Brace-quoted words: literal, no substitution; brace count must balance
5. Argument expansion: `{*}list`
6. Command substitution: `[script]` evaluates script, takes its return value
7. Variable substitution: `$name`, `${name}`, `$arr(idx)`, `$arr($i)`
8. Backslash substitution: `\n`, `\t`, `\\`, `\xNN`, `\uNNNN`, `\<newline>` continues
9. Comments: `#` only at the start of a command
10. Order of substitution is left-to-right, single-pass
11. Substitutions don't recurse — substituted text is not re-parsed
12. The result of any substitution is the value, not a new script
- [ ] Parser: script = list of commands; command = list of words; word = literal string + list of substitutions
- [ ] Unit tests in `lib/tcl/tests/parse.sx`
### Phase 2 — sequential eval + core commands
- [ ] `tcl-eval-script`: walk command list, dispatch each first-word into command table
- [ ] Core commands: `set`, `unset`, `incr`, `append`, `lappend`, `puts`, `gets`, `expr`, `if`, `while`, `for`, `foreach`, `switch`, `break`, `continue`, `return`, `error`, `eval`, `subst`, `format`, `scan`
- [ ] `expr` is its own mini-language — operator precedence, function calls (`sin`, `sqrt`, `pow`, `abs`, `int`, `double`), variable substitution, command substitution
- [ ] String commands: `string length`, `string index`, `string range`, `string compare`, `string match`, `string toupper`, `string tolower`, `string trim`, `string map`, `string repeat`, `string first`, `string last`, `string is`, `string cat`
- [ ] List commands: `list`, `lindex`, `lrange`, `llength`, `lreverse`, `lsearch`, `lsort`, `lsort -integer/-real/-dictionary`, `lreplace`, `linsert`, `concat`, `split`, `join`
- [ ] Dict commands: `dict create`, `dict get`, `dict set`, `dict unset`, `dict exists`, `dict keys`, `dict values`, `dict size`, `dict for`, `dict update`, `dict merge`
- [ ] 60+ tests in `lib/tcl/tests/eval.sx`
### Phase 3 — proc + uplevel + upvar (THE SHOWCASE)
- [ ] `proc name args body` — register user-defined command; args supports defaults `{name default}` and rest `args`
- [ ] Frame stack: each proc call pushes a frame with locals dict; pop on return
- [ ] `uplevel ?level? script` — evaluate `script` in level-N frame's env; default level is 1 (caller). `#0` is global, `#1` is relative-1
- [ ] `upvar ?level? otherVar localVar ?…?` — alias localVar to a variable in level-N frame; reads/writes go through the alias
- [ ] `info level`, `info level N`, `info frame`, `info vars`, `info locals`, `info globals`, `info commands`, `info procs`, `info args`, `info body`
- [ ] `global var ?…?` — alias to global frame (sugar for `upvar #0 var var`)
- [ ] `variable name ?value?` — namespace-scoped global
- [ ] Classic programs in `lib/tcl/tests/programs/`:
- [ ] `for-each-line.tcl` — define your own loop construct using `uplevel`
- [ ] `assert.tcl` — assertion macro that reports caller's line
- [ ] `with-temp-var.tcl` — scoped variable rebind via `upvar`
- [ ] `lib/tcl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 4 — control flow + error handling
- [ ] `return -code (ok|error|return|break|continue|N) -errorinfo … -errorcode … -level N value`
- [ ] `catch script ?resultVar? ?optionsVar?` — runs script, returns code; sets resultVar to return value/message; optionsVar to the dict
- [ ] `try script ?on code var body ...? ?trap pattern var body...? ?finally body?`
- [ ] `throw type message`
- [ ] `error message ?info? ?code?`
- [ ] Stack-trace with `errorInfo` / `errorCode`
- [ ] 30+ tests in `lib/tcl/tests/error.sx`
### Phase 5 — namespaces + ensembles
- [ ] `namespace eval ns body`, `namespace current`, `namespace which`, `namespace import`, `namespace export`, `namespace forget`, `namespace delete`
- [ ] Qualified names: `::ns::cmd`, `::ns::var`
- [ ] Ensembles: `namespace ensemble create -map { sub1 cmd1 sub2 cmd2 }`
- [ ] `namespace path` for resolution chain
- [ ] `proc` and `variable` work inside namespaces
### Phase 6 — coroutines + drive corpus
- [ ] `coroutine name cmd ?args…?` — start a coroutine; future calls to `name` resume it
- [ ] `yield ?value?` — suspend, return value to resumer
- [ ] `yieldto cmd ?args…?` — symmetric transfer
- [ ] `coroutine` semantics built on fibers (same delcc primitive as Ruby fibers)
- [ ] Classic programs: `event-loop.tcl` — cooperative scheduler with multiple coroutines
- [ ] System: `clock seconds`, `clock format`, `clock scan` (subset)
- [ ] File I/O: `open`, `close`, `read`, `gets`, `puts -nonewline`, `flush`, `eof`, `seek`, `tell`
- [ ] Drive corpus to 150+ green
- [ ] Idiom corpus — `lib/tcl/tests/idioms.sx` covering classic Welch/Jones idioms
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -30,7 +30,7 @@ fi
if [ "$CLEAN" = "1" ]; then if [ "$CLEAN" = "1" ]; then
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
for lang in lua prolog forth erlang haskell js hs smalltalk common-lisp apl ruby tcl; do for lang in lua prolog forth erlang haskell js hs; do
wt="$WORKTREE_BASE/$lang" wt="$WORKTREE_BASE/$lang"
if [ -d "$wt" ]; then if [ -d "$wt" ]; then
git worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt" git worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
@@ -39,5 +39,5 @@ if [ "$CLEAN" = "1" ]; then
done done
git worktree prune git worktree prune
echo "Worktree branches (loops/<lang>) are preserved. Delete manually if desired:" echo "Worktree branches (loops/<lang>) are preserved. Delete manually if desired:"
echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs loops/smalltalk loops/common-lisp loops/apl loops/ruby loops/tcl" echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs"
fi fi

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Spawn 12 claude sessions in tmux, one per language loop. # Spawn 7 claude sessions in tmux, one per language loop.
# Each runs in its own git worktree rooted at /root/rose-ash-loops/<lang>, # Each runs in its own git worktree rooted at /root/rose-ash-loops/<lang>,
# on branch loops/<lang>. No two loops share a working tree, so there's # on branch loops/<lang>. No two loops share a working tree, so there's
# zero risk of file collisions between languages. # zero risk of file collisions between languages.
@@ -9,7 +9,7 @@
# #
# After the script prints done: # After the script prints done:
# tmux a -t sx-loops # tmux a -t sx-loops
# Ctrl-B + <window-number> to switch (0=lua ... 11=tcl) # Ctrl-B + <window-number> to switch (0=lua ... 6=hs)
# Ctrl-B + d to detach (loops keep running, SSH-safe) # Ctrl-B + d to detach (loops keep running, SSH-safe)
# #
# Stop: ./scripts/sx-loops-down.sh # Stop: ./scripts/sx-loops-down.sh
@@ -38,13 +38,8 @@ declare -A BRIEFING=(
[haskell]=haskell-loop.md [haskell]=haskell-loop.md
[js]=loop.md [js]=loop.md
[hs]=hs-loop.md [hs]=hs-loop.md
[smalltalk]=smalltalk-loop.md
[common-lisp]=common-lisp-loop.md
[apl]=apl-loop.md
[ruby]=ruby-loop.md
[tcl]=tcl-loop.md
) )
ORDER=(lua prolog forth erlang haskell js hs smalltalk common-lisp apl ruby tcl) ORDER=(lua prolog forth erlang haskell js hs)
mkdir -p "$WORKTREE_BASE" mkdir -p "$WORKTREE_BASE"
@@ -65,13 +60,13 @@ for lang in "${ORDER[@]}"; do
fi fi
done done
# Create tmux session with one window per language, each cwd in its worktree # Create tmux session with 7 windows, each cwd in its worktree
tmux new-session -d -s "$SESSION" -n "${ORDER[0]}" -c "$WORKTREE_BASE/${ORDER[0]}" tmux new-session -d -s "$SESSION" -n "${ORDER[0]}" -c "$WORKTREE_BASE/${ORDER[0]}"
for lang in "${ORDER[@]:1}"; do for lang in "${ORDER[@]:1}"; do
tmux new-window -t "$SESSION" -n "$lang" -c "$WORKTREE_BASE/$lang" tmux new-window -t "$SESSION" -n "$lang" -c "$WORKTREE_BASE/$lang"
done done
echo "Starting ${#ORDER[@]} claude sessions..." echo "Starting 7 claude sessions..."
for lang in "${ORDER[@]}"; do for lang in "${ORDER[@]}"; do
tmux send-keys -t "$SESSION:$lang" "claude" C-m tmux send-keys -t "$SESSION:$lang" "claude" C-m
done done
@@ -94,10 +89,10 @@ for lang in "${ORDER[@]}"; do
done done
echo "" echo ""
echo "Done. ${#ORDER[@]} loops started in tmux session '$SESSION', each in its own worktree." echo "Done. 7 loops started in tmux session '$SESSION', each in its own worktree."
echo "" echo ""
echo " Attach: tmux a -t $SESSION" echo " Attach: tmux a -t $SESSION"
echo " Switch: Ctrl-B <0..11> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs 7=smalltalk 8=common-lisp 9=apl 10=ruby 11=tcl)" echo " Switch: Ctrl-B <0..6> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs)"
echo " List: Ctrl-B w" echo " List: Ctrl-B w"
echo " Detach: Ctrl-B d" echo " Detach: Ctrl-B d"
echo " Stop: ./scripts/sx-loops-down.sh" echo " Stop: ./scripts/sx-loops-down.sh"

View File

@@ -1,121 +0,0 @@
#!/usr/bin/env bash
# Spawn a single claude session to implement SX primitives in sequence.
# Runs in its own git worktree on branch sx-primitives from architecture.
#
# Usage: ./scripts/sx-primitives-up.sh [interval]
# interval defaults to self-paced (omit to let model decide)
#
# After the script prints done:
# tmux a -t sx-primitives
# Ctrl-B + d to detach
#
# Stop: ./scripts/sx-primitives-down.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
SESSION="sx-primitives"
WORKTREE="$ROOT" # runs in the main worktree — architecture branch
BRANCH="architecture"
INTERVAL="${1:-}"
BOOT_WAIT=20
if tmux has-session -t "$SESSION" 2>/dev/null; then
echo "Session '$SESSION' already exists."
echo " Attach: tmux a -t $SESSION"
echo " Kill: ./scripts/sx-primitives-down.sh"
exit 1
fi
# Write settings into the main worktree .claude dir
SETTINGS_DIR="$ROOT/.claude"
mkdir -p "$SETTINGS_DIR"
cat > "$SETTINGS_DIR/settings.local.json" <<'SETTINGS'
{
"permissions": {
"allow": [
"mcp__sx-tree__sx_summarise",
"mcp__sx-tree__sx_read_tree",
"mcp__sx-tree__sx_read_subtree",
"mcp__sx-tree__sx_get_context",
"mcp__sx-tree__sx_find_all",
"mcp__sx-tree__sx_find_across",
"mcp__sx-tree__sx_get_siblings",
"mcp__sx-tree__sx_validate",
"mcp__sx-tree__sx_replace_node",
"mcp__sx-tree__sx_insert_child",
"mcp__sx-tree__sx_insert_near",
"mcp__sx-tree__sx_delete_node",
"mcp__sx-tree__sx_wrap_node",
"mcp__sx-tree__sx_rename_symbol",
"mcp__sx-tree__sx_replace_by_pattern",
"mcp__sx-tree__sx_rename_across",
"mcp__sx-tree__sx_write_file",
"mcp__sx-tree__sx_pretty_print",
"mcp__sx-tree__sx_eval",
"mcp__sx-tree__sx_harness_eval",
"mcp__sx-tree__sx_macroexpand",
"mcp__sx-tree__sx_trace",
"mcp__sx-tree__sx_deps",
"mcp__sx-tree__sx_diff",
"mcp__sx-tree__sx_diff_branch",
"mcp__sx-tree__sx_changed",
"mcp__sx-tree__sx_blame",
"mcp__sx-tree__sx_build",
"mcp__sx-tree__sx_build_manifest",
"mcp__sx-tree__sx_build_bytecode",
"mcp__sx-tree__sx_test",
"mcp__sx-tree__sx_format_check",
"mcp__sx-tree__sx_comp_list",
"mcp__sx-tree__sx_comp_usage",
"mcp__sx-tree__sx_nav",
"mcp__sx-tree__sx_env",
"mcp__sx-tree__sx_playwright",
"mcp__hs-test__hs_test_run",
"mcp__hs-test__hs_test_regen",
"mcp__hs-test__hs_test_kill",
"mcp__hs-test__hs_test_status",
"Bash(node *)",
"Bash(python3 *)",
"Bash(bash *)",
"Bash(cp *)",
"Bash(git *)",
"Bash(tmux *)"
]
},
"enabledMcpjsonServers": [
"sx-tree",
"rose-ash-services",
"hs-test"
]
}
SETTINGS
echo "Creating tmux session '$SESSION' in $ROOT ..."
tmux new-session -d -s "$SESSION" -n "primitives" -c "$ROOT"
echo "Starting claude..."
tmux send-keys -t "$SESSION:primitives" "claude" C-m
echo "Waiting ${BOOT_WAIT}s for claude to boot..."
sleep "$BOOT_WAIT"
if [ -n "$INTERVAL" ]; then
preamble="/loop $INTERVAL "
else
preamble="/loop "
fi
cmd="${preamble}Read plans/agent-briefings/primitives-loop.md and do ONE step per fire: find the first unchecked [ ] task, implement it fully, run the relevant tests to verify, commit with a short factual message, push to origin/architecture, tick the box [x] in the plan, append one dated line to the Progress log (newest first), then stop. You are on branch architecture in /root/rose-ash. Use sx-tree MCP for all .sx edits. Never push to main."
tmux send-keys -t "$SESSION:primitives" "$cmd"
sleep 0.5
tmux send-keys -t "$SESSION:primitives" Enter
echo ""
echo "Done. SX primitives loop started in tmux session '$SESSION'."
echo ""
echo " Attach: tmux a -t $SESSION"
echo " Detach: Ctrl-B d"
echo " Stop: ./scripts/sx-primitives-down.sh"
echo ""

View File

@@ -16,13 +16,6 @@
if (a === b) return true; if (a === b) return true;
if (a && b && a._sym && b._sym) return a.name === b.name; if (a && b && a._sym && b._sym) return a.name === b.name;
if (a && b && a._kw && b._kw) return a.name === b.name; if (a && b && a._kw && b._kw) return a.name === b.name;
if (a && b && a._vector && b._vector) {
if (a.arr.length !== b.arr.length) return false;
for (var _i = 0; _i < a.arr.length; _i++) {
if (!sxEq(a.arr[_i], b.arr[_i])) return false;
}
return true;
}
return false; return false;
} }
@@ -31,7 +24,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-04-26T19:02:22Z"; var SX_VERSION = "2026-04-05T11:01:51Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -89,45 +82,6 @@
function SxSpread(attrs) { this.attrs = attrs || {}; } function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true; SxSpread.prototype._spread = true;
function SxVector(arr) { this.arr = arr || []; }
SxVector.prototype._vector = true;
var _paramUidCounter = 0;
function SxParameter(defaultVal, converter) {
this._uid = ++_paramUidCounter;
this._default = defaultVal;
this._converter = converter || null;
}
SxParameter.prototype._parameter = true;
function parameter_p(x) { return x != null && x._parameter === true; }
function parameterUid(p) { return p._uid; }
function parameterDefault(p) { return p._default; }
function SxCallccContinuation(capturedKont, windersLen) { this._captured = capturedKont; this._winders_len = windersLen !== undefined ? windersLen : 0; }
SxCallccContinuation.prototype._callcc = true;
function makeCallccContinuation(kont, windersLen) { return new SxCallccContinuation(kont, windersLen !== undefined ? windersLen : 0); }
function callccContinuation_p(x) { return x != null && x._callcc === true; }
function callccContinuationData(x) { return x._captured; }
function callccContinuationWindersLen(x) { return x._winders_len !== undefined ? x._winders_len : 0; }
function evalError_p(v) {
return v != null && typeof v === "object" && v["__eval_error__"] === true;
}
function sxApplyCek(f, args) {
try {
return typeof f === "function" ? f.apply(null, args) : f;
} catch (e) {
if (e && e._perform_request) throw e;
if (e && e._cek_suspend) throw e;
return {"__eval_error__": true, "message": e && e.message ? e.message : String(e)};
}
}
var _JIT_SKIP_SENTINEL = {"__jit_skip": true};
function jitTryCall(f, args) { return _JIT_SKIP_SENTINEL; }
function jitSkip_p(v) { return v === _JIT_SKIP_SENTINEL || (v != null && v["__jit_skip"] === true); }
var _scopeStacks = {}; var _scopeStacks = {};
function isSym(x) { return x != null && x._sym === true; } function isSym(x) { return x != null && x._sym === true; }
@@ -168,8 +122,6 @@
if (x._macro) return "macro"; if (x._macro) return "macro";
if (x._raw) return "raw-html"; if (x._raw) return "raw-html";
if (x._sx_expr) return "sx-expr"; if (x._sx_expr) return "sx-expr";
if (x._vector) return "vector";
if (x._string_buffer) return "string-buffer";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node"; if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
if (Array.isArray(x)) return "list"; if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict"; if (typeof x === "object") return "dict";
@@ -336,12 +288,6 @@
// Placeholder — overridden by transpiled version from render.sx // Placeholder — overridden by transpiled version from render.sx
function isRenderExpr(expr) { return false; } function isRenderExpr(expr) { return false; }
// Last error continuation — saved when a raise goes unhandled, for post-mortem inspection.
var _lastErrorKont_ = null;
// hostError — throw a host-level error that propagates out of cekRun.
function hostError(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); }
// Render dispatch — call the active adapter's render function. // Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering). // Set by each adapter when loaded; defaults to identity (no rendering).
var _renderExprFn = null; var _renderExprFn = null;
@@ -389,18 +335,11 @@
if (n === undefined || n === 0) return Math.round(x); if (n === undefined || n === 0) return Math.round(x);
var f = Math.pow(10, n); return Math.round(x * f) / f; var f = Math.pow(10, n); return Math.round(x * f) / f;
}; };
PRIMITIVES["truncate"] = Math.trunc;
PRIMITIVES["remainder"] = function(a, b) { return a % b; };
PRIMITIVES["modulo"] = function(a, b) { var r = a % b; return (r !== 0 && (r < 0) !== (b < 0)) ? r + b : r; };
PRIMITIVES["min"] = Math.min; PRIMITIVES["min"] = Math.min;
PRIMITIVES["max"] = Math.max; PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt; PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow; PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
PRIMITIVES["random-int"] = function(lo, hi) { return Math.floor(Math.random() * (hi - lo + 1)) + lo; };
PRIMITIVES["exact->inexact"] = function(x) { return x; };
PRIMITIVES["inexact->exact"] = Math.round;
PRIMITIVES["parse-number"] = function(s) { var n = Number(s); return isNaN(n) ? null : n; };
// core.comparison // core.comparison
@@ -419,13 +358,9 @@
// core.predicates // core.predicates
PRIMITIVES["nil?"] = isNil; PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
PRIMITIVES["integer?"] = function(x) { return typeof x === "number" && Number.isInteger(x); };
PRIMITIVES["float?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); };
PRIMITIVES["exact?"] = function(x) { return typeof x === "number" && Number.isInteger(x); };
PRIMITIVES["inexact?"] = function(x) { return typeof x === "number" && !Number.isInteger(x); };
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray; PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw && !x._string_buffer && !x._vector; }; PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); }; PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function(c, k) { PRIMITIVES["contains?"] = function(c, k) {
if (typeof c === "string") return c.indexOf(String(k)) !== -1; if (typeof c === "string") return c.indexOf(String(k)) !== -1;
@@ -455,20 +390,7 @@
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); }; PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); }; PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); }; PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
PRIMITIVES["index-of"] = function(s, needle, from) { PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
if (Array.isArray(s)) {
var _start = from || 0;
for (var _i = _start; _i < s.length; _i++) {
var _a = s[_i];
if (_a === needle) return _i;
if (_a != null && needle != null && typeof _a === "object" && typeof needle === "object") {
if ((_a._sym && needle._sym || _a._kw && needle._kw) && _a.name === needle.name) return _i;
}
}
return NIL;
}
return String(s).indexOf(needle, from || 0);
};
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; }; PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); }; PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); };
@@ -548,48 +470,6 @@
}; };
// core.vectors — R7RS mutable fixed-size arrays
PRIMITIVES["make-vector"] = function(n, fill) {
var arr = new Array(n);
var f = (fill !== undefined) ? fill : NIL;
for (var i = 0; i < n; i++) arr[i] = f;
return new SxVector(arr);
};
PRIMITIVES["vector"] = function() {
return new SxVector(Array.prototype.slice.call(arguments));
};
PRIMITIVES["vector?"] = function(x) { return x != null && x._vector === true; };
PRIMITIVES["vector-length"] = function(v) { return v.arr.length; };
PRIMITIVES["vector-ref"] = function(v, i) {
if (i < 0 || i >= v.arr.length) throw new Error("vector-ref: index " + i + " out of bounds (length " + v.arr.length + ")");
return v.arr[i];
};
PRIMITIVES["vector-set!"] = function(v, i, val) {
if (i < 0 || i >= v.arr.length) throw new Error("vector-set!: index " + i + " out of bounds (length " + v.arr.length + ")");
v.arr[i] = val; return NIL;
};
PRIMITIVES["vector->list"] = function(v) { return v.arr.slice(); };
PRIMITIVES["list->vector"] = function(l) { return new SxVector(l.slice()); };
PRIMITIVES["vector-fill!"] = function(v, val) {
for (var i = 0; i < v.arr.length; i++) v.arr[i] = val; return NIL;
};
PRIMITIVES["vector-copy"] = function(v, start, end) {
var s = (start !== undefined) ? start : 0;
var e = (end !== undefined) ? Math.min(end, v.arr.length) : v.arr.length;
return new SxVector(v.arr.slice(s, e));
};
// String buffers — O(1) amortised append via array+join
function SxStringBuffer() { this.parts = []; this.len = 0; this._string_buffer = true; }
PRIMITIVES["make-string-buffer"] = function() { return new SxStringBuffer(); };
PRIMITIVES["string-buffer?"] = function(x) { return x instanceof SxStringBuffer; };
PRIMITIVES["string-buffer-append!"] = function(buf, s) {
buf.parts.push(String(s)); buf.len += String(s).length; return NIL;
};
PRIMITIVES["string-buffer->string"] = function(buf) { return buf.parts.join(""); };
PRIMITIVES["string-buffer-length"] = function(buf) { return buf.len; };
// stdlib.format // stdlib.format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; }; PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
@@ -697,26 +577,6 @@
}; };
// stdlib.bitwise
PRIMITIVES["bitwise-and"] = function(a, b) { return (a & b) | 0; };
PRIMITIVES["bitwise-or"] = function(a, b) { return (a | b) | 0; };
PRIMITIVES["bitwise-xor"] = function(a, b) { return (a ^ b) | 0; };
PRIMITIVES["bitwise-not"] = function(a) { return ~a; };
PRIMITIVES["arithmetic-shift"] = function(a, count) {
return count >= 0 ? (a << count) | 0 : a >> (-count);
};
PRIMITIVES["bit-count"] = function(a) {
var n = Math.abs(a) >>> 0;
n = n - ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
return (((n + (n >> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
};
PRIMITIVES["integer-length"] = function(a) {
if (a === 0) return 0;
return 32 - Math.clz32(Math.abs(a));
};
function isPrimitive(name) { return name in PRIMITIVES; } function isPrimitive(name) { return name in PRIMITIVES; }
function getPrimitive(name) { return PRIMITIVES[name]; } function getPrimitive(name) { return PRIMITIVES[name]; }
@@ -1169,10 +1029,6 @@ PRIMITIVES["make-let-frame"] = makeLetFrame;
var makeDefineFrame = function(name, env, hasEffects, effectList) { return {"env": env, "effect-list": effectList, "has-effects": hasEffects, "type": "define", "name": name}; }; var makeDefineFrame = function(name, env, hasEffects, effectList) { return {"env": env, "effect-list": effectList, "has-effects": hasEffects, "type": "define", "name": name}; };
PRIMITIVES["make-define-frame"] = makeDefineFrame; PRIMITIVES["make-define-frame"] = makeDefineFrame;
// make-define-foreign-frame
var makeDefineForeignFrame = function(name, spec, env) { return {"spec": spec, "env": env, "type": "define-foreign", "name": name}; };
PRIMITIVES["make-define-foreign-frame"] = makeDefineForeignFrame;
// make-set-frame // make-set-frame
var makeSetFrame = function(name, env) { return {"env": env, "type": "set", "name": name}; }; var makeSetFrame = function(name, env) { return {"env": env, "type": "set", "name": name}; };
PRIMITIVES["make-set-frame"] = makeSetFrame; PRIMITIVES["make-set-frame"] = makeSetFrame;
@@ -1289,14 +1145,6 @@ PRIMITIVES["make-reactive-reset-frame"] = makeReactiveResetFrame;
var makeCallccFrame = function(env) { return {"env": env, "type": "callcc"}; }; var makeCallccFrame = function(env) { return {"env": env, "type": "callcc"}; };
PRIMITIVES["make-callcc-frame"] = makeCallccFrame; PRIMITIVES["make-callcc-frame"] = makeCallccFrame;
// make-wind-after-frame
var makeWindAfterFrame = function(afterThunk, windersLen, env) { return {"winders-len": windersLen, "env": env, "after-thunk": afterThunk, "type": "wind-after"}; };
PRIMITIVES["make-wind-after-frame"] = makeWindAfterFrame;
// make-wind-return-frame
var makeWindReturnFrame = function(bodyResult, env) { return {"body-result": bodyResult, "env": env, "type": "wind-return"}; };
PRIMITIVES["make-wind-return-frame"] = makeWindReturnFrame;
// make-deref-frame // make-deref-frame
var makeDerefFrame = function(env) { return {"env": env, "type": "deref"}; }; var makeDerefFrame = function(env) { return {"env": env, "type": "deref"}; };
PRIMITIVES["make-deref-frame"] = makeDerefFrame; PRIMITIVES["make-deref-frame"] = makeDerefFrame;
@@ -1373,26 +1221,6 @@ PRIMITIVES["find-matching-handler"] = findMatchingHandler;
})()); }; })()); };
PRIMITIVES["kont-find-handler"] = kontFindHandler; PRIMITIVES["kont-find-handler"] = kontFindHandler;
// kont-unwind-to-handler
var kontUnwindToHandler = function(kont, condition) { return (isSxTruthy(isEmpty(kont)) ? {"handler": NIL, "kont": kont} : (function() {
var frame = first(kont);
var restK = rest(kont);
return (isSxTruthy(sxEq(frameType(frame), "handler")) ? (function() {
var match = findMatchingHandler(get(frame, "f"), condition);
return (isSxTruthy(isNil(match)) ? kontUnwindToHandler(restK, condition) : {"handler": match, "kont": kont});
})() : (isSxTruthy(sxEq(frameType(frame), "wind-after")) ? ((isSxTruthy((len(_winders_) > get(frame, "winders-len"))) ? (_winders_ = rest(_winders_)) : NIL), cekCall(get(frame, "after-thunk"), []), kontUnwindToHandler(restK, condition)) : kontUnwindToHandler(restK, condition)));
})()); };
PRIMITIVES["kont-unwind-to-handler"] = kontUnwindToHandler;
// wind-escape-to
var windEscapeTo = function(targetLen) { return (isSxTruthy((len(_winders_) > targetLen)) ? (function() {
var afterThunk = first(_winders_);
_winders_ = rest(_winders_);
cekCall(afterThunk, []);
return windEscapeTo(targetLen);
})() : NIL); };
PRIMITIVES["wind-escape-to"] = windEscapeTo;
// find-named-restart // find-named-restart
var findNamedRestart = function(restarts, name) { return (isSxTruthy(isEmpty(restarts)) ? NIL : (function() { var findNamedRestart = function(restarts, name) { return (isSxTruthy(isEmpty(restarts)) ? NIL : (function() {
var entry = first(restarts); var entry = first(restarts);
@@ -1493,22 +1321,6 @@ PRIMITIVES["*render-fn*"] = _renderFn;
var _bindTracking_ = NIL; var _bindTracking_ = NIL;
PRIMITIVES["*bind-tracking*"] = _bindTracking_; PRIMITIVES["*bind-tracking*"] = _bindTracking_;
// *provide-batch-depth*
var _provideBatchDepth_ = 0;
PRIMITIVES["*provide-batch-depth*"] = _provideBatchDepth_;
// *provide-batch-queue*
var _provideBatchQueue_ = [];
PRIMITIVES["*provide-batch-queue*"] = _provideBatchQueue_;
// *provide-subscribers*
var _provideSubscribers_ = {};
PRIMITIVES["*provide-subscribers*"] = _provideSubscribers_;
// *winders*
var _winders_ = [];
PRIMITIVES["*winders*"] = _winders_;
// *library-registry* // *library-registry*
var _libraryRegistry_ = {}; var _libraryRegistry_ = {};
PRIMITIVES["*library-registry*"] = _libraryRegistry_; PRIMITIVES["*library-registry*"] = _libraryRegistry_;
@@ -1549,132 +1361,6 @@ PRIMITIVES["io-lookup"] = ioLookup;
var ioNames = function() { return keys(_ioRegistry_); }; var ioNames = function() { return keys(_ioRegistry_); };
PRIMITIVES["io-names"] = ioNames; PRIMITIVES["io-names"] = ioNames;
// *foreign-registry*
var _foreignRegistry_ = {};
PRIMITIVES["*foreign-registry*"] = _foreignRegistry_;
// foreign-register!
var foreignRegister_b = function(name, spec) { return dictSet(_foreignRegistry_, name, spec); };
PRIMITIVES["foreign-register!"] = foreignRegister_b;
// foreign-registered?
var foreignRegistered_p = function(name) { return dictHas(_foreignRegistry_, name); };
PRIMITIVES["foreign-registered?"] = foreignRegistered_p;
// foreign-lookup
var foreignLookup = function(name) { return get(_foreignRegistry_, name); };
PRIMITIVES["foreign-lookup"] = foreignLookup;
// foreign-names
var foreignNames = function() { return keys(_foreignRegistry_); };
PRIMITIVES["foreign-names"] = foreignNames;
// foreign-parse-params
var foreignParseParams = function(paramList) { return (function() {
var result = [];
var i = 0;
var items = (isSxTruthy(isList(paramList)) ? paramList : []);
return foreignParseParamsLoop(items, result);
})(); };
PRIMITIVES["foreign-parse-params"] = foreignParseParams;
// foreign-parse-kwargs!
var foreignParseKwargs_b = function(spec, remaining) { return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(remaining))) && isSxTruthy((len(remaining) >= 2)) && keyword_p(first(remaining)))) ? (dictSet(spec, keywordName(first(remaining)), (function() {
var v = nth(remaining, 1);
return (isSxTruthy(keyword_p(v)) ? keywordName(v) : v);
})()), foreignParseKwargs_b(spec, rest(rest(remaining)))) : NIL); };
PRIMITIVES["foreign-parse-kwargs!"] = foreignParseKwargs_b;
// foreign-resolve-binding
var foreignResolveBinding = function(bindingStr) { return (function() {
var parts = split(bindingStr, ".");
return (isSxTruthy((len(parts) <= 1)) ? {"method": bindingStr, "object": NIL} : (function() {
var method = last(parts);
var obj = join(".", reverse(rest(reverse(parts))));
return {"method": method, "object": obj};
})());
})(); };
PRIMITIVES["foreign-resolve-binding"] = foreignResolveBinding;
// foreign-check-args
var foreignCheckArgs = function(name, params, args) { if (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(params))) && (len(args) < len(params))))) {
error((String("foreign ") + String(name) + String(": expected ") + String(len(params)) + String(" args, got ") + String(len(args))));
}
return forEach(function(i) { return (function() {
var spec = nth(params, i);
var val = nth(args, i);
var expected = get(spec, "type");
return (isSxTruthy((isSxTruthy(!isSxTruthy(sxEq(expected, "any"))) && !isSxTruthy(valueMatchesType_p(val, expected)))) ? error((String("foreign ") + String(name) + String(": arg '") + String(get(spec, "name")) + String("' expected ") + String(expected) + String(", got ") + String(typeOf(val)))) : NIL);
})(); }, range(0, min(len(params), len(args)))); };
PRIMITIVES["foreign-check-args"] = foreignCheckArgs;
// foreign-build-lambda
var foreignBuildLambda = function(spec) { return (function() {
var name = get(spec, "name");
var mode = (isSxTruthy(dictHas(spec, "returns")) ? (function() {
var r = get(spec, "returns");
return (isSxTruthy(sxEq(r, "promise")) ? "async" : "sync");
})() : "sync");
return (isSxTruthy(sxEq(mode, "async")) ? [new Symbol("fn"), [new Symbol("&rest"), new Symbol("__ffi-args__")], [new Symbol("perform"), [new Symbol("foreign-dispatch"), [new Symbol("quote"), name], new Symbol("__ffi-args__")]]] : [new Symbol("fn"), [new Symbol("&rest"), new Symbol("__ffi-args__")], [new Symbol("foreign-dispatch"), [new Symbol("quote"), name], new Symbol("__ffi-args__")]]);
})(); };
PRIMITIVES["foreign-build-lambda"] = foreignBuildLambda;
// sf-define-foreign
var sfDefineForeign = function(args, env) { return (function() {
var name = (isSxTruthy(symbol_p(first(args))) ? symbolName(first(args)) : first(args));
var paramList = nth(args, 1);
var spec = {};
spec["name"] = name;
spec["params"] = foreignParseParams(paramList);
foreignParseKwargs_b(spec, rest(rest(args)));
foreignRegister_b(name, spec);
return spec;
})(); };
PRIMITIVES["sf-define-foreign"] = sfDefineForeign;
// step-sf-define-foreign
var stepSfDefineForeign = function(args, env, kont) { return (function() {
var spec = sfDefineForeign(args, env);
var name = (isSxTruthy(symbol_p(first(args))) ? symbolName(first(args)) : first(args));
var lambdaExpr = foreignBuildLambda(spec);
return makeCekState(lambdaExpr, env, kontPush(makeDefineForeignFrame(name, spec, env), kont));
})(); };
PRIMITIVES["step-sf-define-foreign"] = stepSfDefineForeign;
// foreign-dispatch
var foreignDispatch = function(name, args) { return (function() {
var spec = foreignLookup(name);
if (isSxTruthy(isNil(spec))) {
error((String("foreign-dispatch: unknown foreign function '") + String(name) + String("'")));
}
return (function() {
var params = get(spec, "params");
var binding = get(spec, "js");
foreignCheckArgs(name, (isSxTruthy(isNil(params)) ? [] : params), args);
return (isSxTruthy(isNil(binding)) ? error((String("foreign ") + String(name) + String(": no binding for current platform"))) : (function() {
var resolved = foreignResolveBinding(binding);
var objName = get(resolved, "object");
var method = get(resolved, "method");
return (isSxTruthy(isPrimitive("host-call")) ? (isSxTruthy(isNil(objName)) ? apply(getPrimitive("host-call"), concat([NIL, method], args)) : (function() {
var obj = (getPrimitive("host-global"))(objName);
return apply(getPrimitive("host-call"), concat([obj, method], args));
})()) : error((String("foreign ") + String(name) + String(": host-call not available on this platform"))));
})());
})();
})(); };
PRIMITIVES["foreign-dispatch"] = foreignDispatch;
// foreign-parse-params-loop
var foreignParseParamsLoop = function(items, acc) { return (isSxTruthy(isEmpty(items)) ? acc : (function() {
var item = first(items);
var restItems = rest(items);
return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(restItems))) && isSxTruthy(keyword_p(first(restItems))) && isSxTruthy(sxEq(keywordName(first(restItems)), "as")) && (len(restItems) >= 2))) ? foreignParseParamsLoop(rest(rest(restItems)), append(acc, [{"type": (function() {
var t = nth(restItems, 1);
return (isSxTruthy(keyword_p(t)) ? keywordName(t) : (String(t)));
})(), "name": (isSxTruthy(symbol_p(item)) ? symbolName(item) : (String(item)))}])) : foreignParseParamsLoop(restItems, append(acc, [{"type": "any", "name": (isSxTruthy(symbol_p(item)) ? symbolName(item) : (String(item)))}])));
})()); };
PRIMITIVES["foreign-parse-params-loop"] = foreignParseParamsLoop;
// step-sf-io // step-sf-io
var stepSfIo = function(args, env, kont) { return (function() { var stepSfIo = function(args, env, kont) { return (function() {
var name = first(args); var name = first(args);
@@ -2014,18 +1700,14 @@ PRIMITIVES["sf-letrec"] = sfLetrec;
})(); }; })(); };
PRIMITIVES["step-sf-letrec"] = stepSfLetrec; PRIMITIVES["step-sf-letrec"] = stepSfLetrec;
// step-sf-dynamic-wind // sf-dynamic-wind
var stepSfDynamicWind = function(args, env, kont) { return (function() { var sfDynamicWind = function(args, env) { return (function() {
var before = trampoline(evalExpr(first(args), env)); var before = trampoline(evalExpr(first(args), env));
var body = trampoline(evalExpr(nth(args, 1), env)); var body = trampoline(evalExpr(nth(args, 1), env));
var after = trampoline(evalExpr(nth(args, 2), env)); var after = trampoline(evalExpr(nth(args, 2), env));
return (cekCall(before, []), (function() { return dynamicWindCall(before, body, after, env);
var windersLen = len(_winders_);
_winders_ = cons(after, _winders_);
return continueWithCall(body, [], env, [], kontPush(makeWindAfterFrame(after, windersLen, env), kont));
})());
})(); }; })(); };
PRIMITIVES["step-sf-dynamic-wind"] = stepSfDynamicWind; PRIMITIVES["sf-dynamic-wind"] = sfDynamicWind;
// sf-scope // sf-scope
var sfScope = function(args, env) { return (function() { var sfScope = function(args, env) { return (function() {
@@ -2157,7 +1839,7 @@ PRIMITIVES["step-sf-let-match"] = stepSfLetMatch;
var args = rest(expr); var args = rest(expr);
return (isSxTruthy(!isSxTruthy(sxOr(sxEq(typeOf(head), "symbol"), sxEq(typeOf(head), "lambda"), sxEq(typeOf(head), "list")))) ? (isSxTruthy(isEmpty(expr)) ? makeCekValue([], env, kont) : makeCekState(first(expr), env, kontPush(makeMapFrame(NIL, rest(expr), [], env), kont))) : (isSxTruthy(sxEq(typeOf(head), "symbol")) ? (function() { return (isSxTruthy(!isSxTruthy(sxOr(sxEq(typeOf(head), "symbol"), sxEq(typeOf(head), "lambda"), sxEq(typeOf(head), "list")))) ? (isSxTruthy(isEmpty(expr)) ? makeCekValue([], env, kont) : makeCekState(first(expr), env, kontPush(makeMapFrame(NIL, rest(expr), [], env), kont))) : (isSxTruthy(sxEq(typeOf(head), "symbol")) ? (function() {
var name = symbolName(head); var name = symbolName(head);
return (function() { var _m = name; if (_m == "if") return stepSfIf(args, env, kont); if (_m == "when") return stepSfWhen(args, env, kont); if (_m == "cond") return stepSfCond(args, env, kont); if (_m == "case") return stepSfCase(args, env, kont); if (_m == "and") return stepSfAnd(args, env, kont); if (_m == "or") return stepSfOr(args, env, kont); if (_m == "let") return stepSfLet(args, env, kont); if (_m == "let*") return stepSfLet(args, env, kont); if (_m == "lambda") return stepSfLambda(args, env, kont); if (_m == "fn") return stepSfLambda(args, env, kont); if (_m == "define") return stepSfDefine(args, env, kont); if (_m == "defcomp") return makeCekValue(sfDefcomp(args, env), env, kont); if (_m == "defisland") return makeCekValue(sfDefisland(args, env), env, kont); if (_m == "defmacro") return makeCekValue(sfDefmacro(args, env), env, kont); if (_m == "defio") return makeCekValue(sfDefio(args, env), env, kont); if (_m == "define-foreign") return stepSfDefineForeign(args, env, kont); if (_m == "io") return stepSfIo(args, env, kont); if (_m == "begin") return stepSfBegin(args, env, kont); if (_m == "do") return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(args))) && isSxTruthy(isList(first(args))) && isSxTruthy(!isSxTruthy(isEmpty(first(args)))) && isList(first(first(args))))) ? (function() { return (function() { var _m = name; if (_m == "if") return stepSfIf(args, env, kont); if (_m == "when") return stepSfWhen(args, env, kont); if (_m == "cond") return stepSfCond(args, env, kont); if (_m == "case") return stepSfCase(args, env, kont); if (_m == "and") return stepSfAnd(args, env, kont); if (_m == "or") return stepSfOr(args, env, kont); if (_m == "let") return stepSfLet(args, env, kont); if (_m == "let*") return stepSfLet(args, env, kont); if (_m == "lambda") return stepSfLambda(args, env, kont); if (_m == "fn") return stepSfLambda(args, env, kont); if (_m == "define") return stepSfDefine(args, env, kont); if (_m == "defcomp") return makeCekValue(sfDefcomp(args, env), env, kont); if (_m == "defisland") return makeCekValue(sfDefisland(args, env), env, kont); if (_m == "defmacro") return makeCekValue(sfDefmacro(args, env), env, kont); if (_m == "defio") return makeCekValue(sfDefio(args, env), env, kont); if (_m == "io") return stepSfIo(args, env, kont); if (_m == "begin") return stepSfBegin(args, env, kont); if (_m == "do") return (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(args))) && isSxTruthy(isList(first(args))) && isSxTruthy(!isSxTruthy(isEmpty(first(args)))) && isList(first(first(args))))) ? (function() {
var bindings = first(args); var bindings = first(args);
var testClause = nth(args, 1); var testClause = nth(args, 1);
var body = rest(rest(args)); var body = rest(rest(args));
@@ -2167,42 +1849,14 @@ PRIMITIVES["step-sf-let-match"] = stepSfLetMatch;
var test = first(testClause); var test = first(testClause);
var result = rest(testClause); var result = rest(testClause);
return stepEvalList(cons(new Symbol("let"), cons(new Symbol("__do-loop"), cons(map(function(b) { return [first(b), nth(b, 1)]; }, bindings), [cons(new Symbol("if"), cons(test, cons((isSxTruthy(isEmpty(result)) ? NIL : cons(new Symbol("begin"), result)), [cons(new Symbol("begin"), append(body, [cons(new Symbol("__do-loop"), steps)]))])))]))), env, kont); return stepEvalList(cons(new Symbol("let"), cons(new Symbol("__do-loop"), cons(map(function(b) { return [first(b), nth(b, 1)]; }, bindings), [cons(new Symbol("if"), cons(test, cons((isSxTruthy(isEmpty(result)) ? NIL : cons(new Symbol("begin"), result)), [cons(new Symbol("begin"), append(body, [cons(new Symbol("__do-loop"), steps)]))])))]))), env, kont);
})() : stepSfBegin(args, env, kont)); if (_m == "guard") return stepSfGuard(args, env, kont); if (_m == "quote") return makeCekValue((isSxTruthy(isEmpty(args)) ? NIL : first(args)), env, kont); if (_m == "quasiquote") return makeCekValue(qqExpand(first(args), env), env, kont); if (_m == "->") return stepSfThreadFirst(args, env, kont); if (_m == "->>") return stepSfThreadLast(args, env, kont); if (_m == "|>") return stepSfThreadLast(args, env, kont); if (_m == "as->") return stepSfThreadAs(args, env, kont); if (_m == "set!") return stepSfSet(args, env, kont); if (_m == "letrec") return stepSfLetrec(args, env, kont); if (_m == "reset") return stepSfReset(args, env, kont); if (_m == "shift") return stepSfShift(args, env, kont); if (_m == "deref") return stepSfDeref(args, env, kont); if (_m == "scope") return stepSfScope(args, env, kont); if (_m == "provide") return stepSfProvide(args, env, kont); if (_m == "peek") return stepSfPeek(args, env, kont); if (_m == "provide!") return stepSfProvide_b(args, env, kont); if (_m == "context") return stepSfContext(args, env, kont); if (_m == "bind") return stepSfBind(args, env, kont); if (_m == "emit!") return stepSfEmit(args, env, kont); if (_m == "emitted") return stepSfEmitted(args, env, kont); if (_m == "handler-bind") return stepSfHandlerBind(args, env, kont); if (_m == "restart-case") return stepSfRestartCase(args, env, kont); if (_m == "signal-condition") return stepSfSignal(args, env, kont); if (_m == "invoke-restart") return stepSfInvokeRestart(args, env, kont); if (_m == "match") return stepSfMatch(args, env, kont); if (_m == "let-match") return stepSfLetMatch(args, env, kont); if (_m == "dynamic-wind") return stepSfDynamicWind(args, env, kont); if (_m == "map") return stepHoMap(args, env, kont); if (_m == "map-indexed") return stepHoMapIndexed(args, env, kont); if (_m == "filter") return stepHoFilter(args, env, kont); if (_m == "reduce") return stepHoReduce(args, env, kont); if (_m == "some") return stepHoSome(args, env, kont); if (_m == "every?") return stepHoEvery(args, env, kont); if (_m == "for-each") return stepHoForEach(args, env, kont); if (_m == "raise") return stepSfRaise(args, env, kont); if (_m == "raise-continuable") return makeCekState(first(args), env, kontPush(makeRaiseEvalFrame(env, true), kont)); if (_m == "call/cc") return stepSfCallcc(args, env, kont); if (_m == "call-with-current-continuation") return stepSfCallcc(args, env, kont); if (_m == "perform") return stepSfPerform(args, env, kont); if (_m == "define-library") return stepSfDefineLibrary(args, env, kont); if (_m == "import") return stepSfImport(args, env, kont); if (_m == "define-record-type") return makeCekValue(sfDefineRecordType(args, env), env, kont); if (_m == "define-protocol") return makeCekValue(sfDefineProtocol(args, env), env, kont); if (_m == "implement") return makeCekValue(sfImplement(args, env), env, kont); if (_m == "parameterize") return stepSfParameterize(args, env, kont); if (_m == "syntax-rules") return makeCekValue(sfSyntaxRules(args, env), env, kont); if (_m == "define-syntax") return stepSfDefine(args, env, kont); return (isSxTruthy((isSxTruthy(dictHas(_customSpecialForms, name)) && !isSxTruthy(envHas(env, name)))) ? makeCekValue((get(_customSpecialForms, name))(args, env), env, kont) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() { })() : stepSfBegin(args, env, kont)); if (_m == "guard") return stepSfGuard(args, env, kont); if (_m == "quote") return makeCekValue((isSxTruthy(isEmpty(args)) ? NIL : first(args)), env, kont); if (_m == "quasiquote") return makeCekValue(qqExpand(first(args), env), env, kont); if (_m == "->") return stepSfThreadFirst(args, env, kont); if (_m == "->>") return stepSfThreadLast(args, env, kont); if (_m == "|>") return stepSfThreadLast(args, env, kont); if (_m == "as->") return stepSfThreadAs(args, env, kont); if (_m == "set!") return stepSfSet(args, env, kont); if (_m == "letrec") return stepSfLetrec(args, env, kont); if (_m == "reset") return stepSfReset(args, env, kont); if (_m == "shift") return stepSfShift(args, env, kont); if (_m == "deref") return stepSfDeref(args, env, kont); if (_m == "scope") return stepSfScope(args, env, kont); if (_m == "provide") return stepSfProvide(args, env, kont); if (_m == "peek") return stepSfPeek(args, env, kont); if (_m == "provide!") return stepSfProvide_b(args, env, kont); if (_m == "context") return stepSfContext(args, env, kont); if (_m == "bind") return stepSfBind(args, env, kont); if (_m == "emit!") return stepSfEmit(args, env, kont); if (_m == "emitted") return stepSfEmitted(args, env, kont); if (_m == "handler-bind") return stepSfHandlerBind(args, env, kont); if (_m == "restart-case") return stepSfRestartCase(args, env, kont); if (_m == "signal-condition") return stepSfSignal(args, env, kont); if (_m == "invoke-restart") return stepSfInvokeRestart(args, env, kont); if (_m == "match") return stepSfMatch(args, env, kont); if (_m == "let-match") return stepSfLetMatch(args, env, kont); if (_m == "dynamic-wind") return makeCekValue(sfDynamicWind(args, env), env, kont); if (_m == "map") return stepHoMap(args, env, kont); if (_m == "map-indexed") return stepHoMapIndexed(args, env, kont); if (_m == "filter") return stepHoFilter(args, env, kont); if (_m == "reduce") return stepHoReduce(args, env, kont); if (_m == "some") return stepHoSome(args, env, kont); if (_m == "every?") return stepHoEvery(args, env, kont); if (_m == "for-each") return stepHoForEach(args, env, kont); if (_m == "raise") return stepSfRaise(args, env, kont); if (_m == "raise-continuable") return makeCekState(first(args), env, kontPush(makeRaiseEvalFrame(env, true), kont)); if (_m == "call/cc") return stepSfCallcc(args, env, kont); if (_m == "call-with-current-continuation") return stepSfCallcc(args, env, kont); if (_m == "perform") return stepSfPerform(args, env, kont); if (_m == "define-library") return stepSfDefineLibrary(args, env, kont); if (_m == "import") return stepSfImport(args, env, kont); if (_m == "define-record-type") return makeCekValue(sfDefineRecordType(args, env), env, kont); if (_m == "define-protocol") return makeCekValue(sfDefineProtocol(args, env), env, kont); if (_m == "implement") return makeCekValue(sfImplement(args, env), env, kont); if (_m == "parameterize") return stepSfParameterize(args, env, kont); if (_m == "syntax-rules") return makeCekValue(sfSyntaxRules(args, env), env, kont); if (_m == "define-syntax") return stepSfDefine(args, env, kont); return (isSxTruthy(dictHas(_customSpecialForms, name)) ? makeCekValue((get(_customSpecialForms, name))(args, env), env, kont) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
var mac = envGet(env, name); var mac = envGet(env, name);
return makeCekState(expandMacro(mac, args, env), env, kont); return makeCekState(expandMacro(mac, args, env), env, kont);
})() : (isSxTruthy((isSxTruthy(_renderCheck) && isSxTruthy(!isSxTruthy(envHas(env, name))) && _renderCheck(expr, env))) ? makeCekValue(_renderFn(expr, env), env, kont) : stepEvalCall(head, args, env, kont)))); })(); })() : (isSxTruthy((isSxTruthy(_renderCheck) && _renderCheck(expr, env))) ? makeCekValue(_renderFn(expr, env), env, kont) : stepEvalCall(head, args, env, kont)))); })();
})() : stepEvalCall(head, args, env, kont))); })() : stepEvalCall(head, args, env, kont)));
})(); }; })(); };
PRIMITIVES["step-eval-list"] = stepEvalList; PRIMITIVES["step-eval-list"] = stepEvalList;
// sf-define-type
var sfDefineType = function(args, env) { return (function() {
var typeSym = first(args);
var ctorSpecs = rest(args);
return (function() {
var typeName = symbolName(typeSym);
var ctorNames = map(function(spec) { return symbolName(first(spec)); }, ctorSpecs);
if (isSxTruthy(!isSxTruthy(envHas(env, "*adt-registry*")))) {
envBind(env, "*adt-registry*", {});
}
envGet(env, "*adt-registry*")[typeName] = ctorNames;
envBind(env, (String(typeName) + String("?")), function(v) { return (isSxTruthy(isDict(v)) && isSxTruthy(get(v, "_adt")) && sxEq(get(v, "_type"), typeName)); });
{ var _c = ctorSpecs; for (var _i = 0; _i < _c.length; _i++) { var spec = _c[_i]; (function() {
var cn = symbolName(first(spec));
var fieldNames = map(function(f) { return symbolName(f); }, rest(spec));
var arity = len(rest(spec));
envBind(env, cn, function() { var ctorArgs = Array.prototype.slice.call(arguments, 0); return (isSxTruthy(!isSxTruthy(sxEq(len(ctorArgs), arity))) ? error((String(cn) + String(": expected ") + String(arity) + String(" args, got ") + String(len(ctorArgs)))) : {"_ctor": cn, "_type": typeName, "_adt": true, "_fields": ctorArgs}); });
envBind(env, (String(cn) + String("?")), function(v) { return (isSxTruthy(isDict(v)) && isSxTruthy(get(v, "_adt")) && sxEq(get(v, "_ctor"), cn)); });
return forEachIndexed(function(idx, fieldName) { return envBind(env, (String(cn) + String("-") + String(fieldName)), function(v) { return nth(get(v, "_fields"), idx); }); }, fieldNames);
})(); } }
return NIL;
})();
})(); };
PRIMITIVES["sf-define-type"] = sfDefineType;
// (register-special-form! ...)
registerSpecialForm("define-type", sfDefineType);
// kont-extract-provides // kont-extract-provides
var kontExtractProvides = function(kont) { return (isSxTruthy(isEmpty(kont)) ? [] : (function() { var kontExtractProvides = function(kont) { return (isSxTruthy(isEmpty(kont)) ? [] : (function() {
var frame = first(kont); var frame = first(kont);
@@ -2214,30 +1868,10 @@ PRIMITIVES["kont-extract-provides"] = kontExtractProvides;
// fire-provide-subscribers // fire-provide-subscribers
var fireProvideSubscribers = function(frame, kont) { return (function() { var fireProvideSubscribers = function(frame, kont) { return (function() {
var subs = get(frame, "subscribers"); var subs = get(frame, "subscribers");
return (isSxTruthy(!isSxTruthy(isEmpty(subs))) ? (isSxTruthy((_provideBatchDepth_ > 0)) ? forEach(function(sub) { return (isSxTruthy(!isSxTruthy(contains(_provideBatchQueue_, sub))) ? append_b(_provideBatchQueue_, sub) : NIL); }, subs) : forEach(function(sub) { return cekCall(sub, [kont]); }, subs)) : NIL); return (isSxTruthy(!isSxTruthy(isEmpty(subs))) ? forEach(function(sub) { return cekCall(sub, [kont]); }, subs) : NIL);
})(); }; })(); };
PRIMITIVES["fire-provide-subscribers"] = fireProvideSubscribers; PRIMITIVES["fire-provide-subscribers"] = fireProvideSubscribers;
// fire-provide-subscribers
var fireProvideSubscribers = function(name) { return (function() {
var subs = get(_provideSubscribers_, name);
return (isSxTruthy((isSxTruthy(subs) && !isSxTruthy(isEmpty(subs)))) ? (isSxTruthy((_provideBatchDepth_ > 0)) ? forEach(function(sub) { return (isSxTruthy(!isSxTruthy(contains(_provideBatchQueue_, sub))) ? append_b(_provideBatchQueue_, sub) : NIL); }, subs) : forEach(function(sub) { return cekCall(sub, [NIL]); }, subs)) : NIL);
})(); };
PRIMITIVES["fire-provide-subscribers"] = fireProvideSubscribers;
// batch-begin!
var batchBegin_b = function() { return (_provideBatchDepth_ = (_provideBatchDepth_ + 1)); };
PRIMITIVES["batch-begin!"] = batchBegin_b;
// batch-end!
var batchEnd_b = function() { _provideBatchDepth_ = (_provideBatchDepth_ - 1);
return (isSxTruthy(sxEq(_provideBatchDepth_, 0)) ? (function() {
var queue = _provideBatchQueue_;
_provideBatchQueue_ = [];
return forEach(function(sub) { return cekCall(sub, [NIL]); }, queue);
})() : NIL); };
PRIMITIVES["batch-end!"] = batchEnd_b;
// step-sf-bind // step-sf-bind
var stepSfBind = function(args, env, kont) { return (function() { var stepSfBind = function(args, env, kont) { return (function() {
var body = first(args); var body = first(args);
@@ -2377,7 +2011,7 @@ PRIMITIVES["sf-syntax-rules"] = sfSyntaxRules;
{ var _c = decls; for (var _i = 0; _i < _c.length; _i++) { var decl = _c[_i]; if (isSxTruthy((isSxTruthy(isList(decl)) && isSxTruthy(!isSxTruthy(isEmpty(decl))) && symbol_p(first(decl))))) { { var _c = decls; for (var _i = 0; _i < _c.length; _i++) { var decl = _c[_i]; if (isSxTruthy((isSxTruthy(isList(decl)) && isSxTruthy(!isSxTruthy(isEmpty(decl))) && symbol_p(first(decl))))) {
(function() { (function() {
var kind = symbolName(first(decl)); var kind = symbolName(first(decl));
return (isSxTruthy(sxEq(kind, "export")) ? (exports = append(exports, map(function(s) { return (isSxTruthy(symbol_p(s)) ? symbolName(s) : (String(s))); }, rest(decl)))) : (isSxTruthy(sxEq(kind, "import")) ? forEach(function(importSet) { return bindImportSet(importSet, libEnv); }, rest(decl)) : (isSxTruthy(sxEq(kind, "begin")) ? (bodyForms = append(bodyForms, rest(decl))) : NIL))); return (isSxTruthy(sxEq(kind, "export")) ? (exports = append(exports, map(function(s) { return (isSxTruthy(symbol_p(s)) ? symbolName(s) : (String(s))); }, rest(decl)))) : (isSxTruthy(sxEq(kind, "begin")) ? (bodyForms = append(bodyForms, rest(decl))) : NIL));
})(); })();
} } } } } }
{ var _c = bodyForms; for (var _i = 0; _i < _c.length; _i++) { var form = _c[_i]; evalExpr(form, libEnv); } } { var _c = bodyForms; for (var _i = 0; _i < _c.length; _i++) { var form = _c[_i]; evalExpr(form, libEnv); } }
@@ -2578,12 +2212,7 @@ PRIMITIVES["match-find-clause"] = matchFindClause;
var matchPattern = function(pattern, value, env) { return (isSxTruthy(sxEq(pattern, new Symbol("_"))) ? true : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(sxEq(len(pattern), 2)) && sxEq(first(pattern), new Symbol("?")))) ? (function() { var matchPattern = function(pattern, value, env) { return (isSxTruthy(sxEq(pattern, new Symbol("_"))) ? true : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(sxEq(len(pattern), 2)) && sxEq(first(pattern), new Symbol("?")))) ? (function() {
var pred = evalExpr(nth(pattern, 1), env); var pred = evalExpr(nth(pattern, 1), env);
return cekCall(pred, [value]); return cekCall(pred, [value]);
})() : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(!isSxTruthy(isEmpty(pattern))) && sxEq(first(pattern), new Symbol("quote")))) ? sxEq(value, nth(pattern, 1)) : (isSxTruthy(symbol_p(pattern)) ? (envBind(env, symbolName(pattern), value), true) : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(!isSxTruthy(isEmpty(pattern))) && isSxTruthy(symbol_p(first(pattern))) && isSxTruthy(isDict(value)) && get(value, "_adt"))) ? (function() { })() : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(!isSxTruthy(isEmpty(pattern))) && sxEq(first(pattern), new Symbol("quote")))) ? sxEq(value, nth(pattern, 1)) : (isSxTruthy(symbol_p(pattern)) ? (envBind(env, symbolName(pattern), value), true) : (isSxTruthy((isSxTruthy(isDict(pattern)) && isDict(value))) ? isEvery(function(k) { return matchPattern(get(pattern, k), get(value, k), env); }, keys(pattern)) : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(isList(value)) && contains(pattern, new Symbol("&rest")))) ? (function() {
var ctorName = symbolName(first(pattern));
var fieldPatterns = rest(pattern);
var fields = get(value, "_fields");
return (isSxTruthy(sxEq(get(value, "_ctor"), ctorName)) && isSxTruthy(sxEq(len(fieldPatterns), len(fields))) && isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, zip(fieldPatterns, fields)));
})() : (isSxTruthy((isSxTruthy(isDict(pattern)) && isDict(value))) ? isEvery(function(k) { return matchPattern(get(pattern, k), get(value, k), env); }, keys(pattern)) : (isSxTruthy((isSxTruthy(isList(pattern)) && isSxTruthy(isList(value)) && contains(pattern, new Symbol("&rest")))) ? (function() {
var restIdx = indexOf_(pattern, new Symbol("&rest")); var restIdx = indexOf_(pattern, new Symbol("&rest"));
return (isSxTruthy((len(value) >= restIdx)) && isSxTruthy(isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, zip(slice(pattern, 0, restIdx), slice(value, 0, restIdx)))) && (function() { return (isSxTruthy((len(value) >= restIdx)) && isSxTruthy(isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, zip(slice(pattern, 0, restIdx), slice(value, 0, restIdx)))) && (function() {
var restName = nth(pattern, (restIdx + 1)); var restName = nth(pattern, (restIdx + 1));
@@ -2593,7 +2222,7 @@ PRIMITIVES["match-find-clause"] = matchFindClause;
})() : (isSxTruthy((isSxTruthy(isList(pattern)) && isList(value))) ? (isSxTruthy(!isSxTruthy(sxEq(len(pattern), len(value)))) ? false : (function() { })() : (isSxTruthy((isSxTruthy(isList(pattern)) && isList(value))) ? (isSxTruthy(!isSxTruthy(sxEq(len(pattern), len(value)))) ? false : (function() {
var pairs = zip(pattern, value); var pairs = zip(pattern, value);
return isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, pairs); return isEvery(function(pair) { return matchPattern(first(pair), nth(pair, 1), env); }, pairs);
})()) : sxEq(pattern, value))))))))); }; })()) : sxEq(pattern, value)))))))); };
PRIMITIVES["match-pattern"] = matchPattern; PRIMITIVES["match-pattern"] = matchPattern;
// step-sf-match // step-sf-match
@@ -2602,7 +2231,7 @@ PRIMITIVES["match-pattern"] = matchPattern;
var clauses = rest(args); var clauses = rest(args);
return (function() { return (function() {
var result = matchFindClause(val, clauses, env); var result = matchFindClause(val, clauses, env);
return (isSxTruthy(isNil(result)) ? makeCekValue((String("match: no clause matched ") + String(inspect(val))), env, kontPush(makeRaiseEvalFrame(env, false), kont)) : makeCekState(nth(result, 1), first(result), kont)); return (isSxTruthy(isNil(result)) ? error((String("match: no clause matched ") + String(inspect(val)))) : makeCekState(nth(result, 1), first(result), kont));
})(); })();
})(); }; })(); };
PRIMITIVES["step-sf-match"] = stepSfMatch; PRIMITIVES["step-sf-match"] = stepSfMatch;
@@ -2778,10 +2407,10 @@ PRIMITIVES["step-sf-provide"] = stepSfProvide;
_bindTracking_.push(name); _bindTracking_.push(name);
} }
} }
return makeCekValue((function() { return makeCekValue((isSxTruthy(frame) ? get(frame, "value") : (function() {
var sv = scopePeek(name); var sv = scopePeek(name);
return (isSxTruthy(isNil(sv)) ? (isSxTruthy(frame) ? get(frame, "value") : defaultVal) : sv); return (isSxTruthy(isNil(sv)) ? defaultVal : sv);
})(), env, kont); })()), env, kont);
})(); }; })(); };
PRIMITIVES["step-sf-context"] = stepSfContext; PRIMITIVES["step-sf-context"] = stepSfContext;
@@ -3020,14 +2649,6 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
})(); })();
} }
return makeCekValue(value, fenv, restK); return makeCekValue(value, fenv, restK);
})(); if (_m == "define-foreign") return (function() {
var name = get(frame, "name");
var fenv = get(frame, "env");
if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) {
value.name = name;
}
envBind(fenv, name, value);
return makeCekValue(value, fenv, restK);
})(); if (_m == "set") return (function() { })(); if (_m == "set") return (function() {
var name = get(frame, "name"); var name = get(frame, "name");
var fenv = get(frame, "env"); var fenv = get(frame, "env");
@@ -3159,8 +2780,8 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
(function() { (function() {
var subscriber = function(fireKont) { return cekRun(makeCekState(body, fenv, [])); }; var subscriber = function(fireKont) { return cekRun(makeCekState(body, fenv, [])); };
return forEach(function(name) { return (function() { return forEach(function(name) { return (function() {
var existing = get(_provideSubscribers_, name); var pf = kontFindProvide(restK, name);
return dictSet(_provideSubscribers_, name, append((isSxTruthy(existing) ? existing : []), [subscriber])); return (isSxTruthy(pf) ? dictSet(pf, "subscribers", append(get(pf, "subscribers"), [subscriber])) : NIL);
})(); }, tracked); })(); }, tracked);
})(); })();
return makeCekValue(value, fenv, restK); return makeCekValue(value, fenv, restK);
@@ -3168,18 +2789,16 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
var name = get(frame, "name"); var name = get(frame, "name");
var fenv = get(frame, "env"); var fenv = get(frame, "env");
var target = kontFindProvide(restK, name); var target = kontFindProvide(restK, name);
return (function() { return (isSxTruthy(target) ? (function() {
var oldVal = (isSxTruthy(target) ? get(target, "value") : scopePeek(name)); var oldVal = get(target, "value");
if (isSxTruthy(target)) {
target["value"] = value; target["value"] = value;
}
scopePop(name); scopePop(name);
scopePush(name, value); scopePush(name, value);
if (isSxTruthy(!isSxTruthy(sxEq(oldVal, value)))) { if (isSxTruthy(!isSxTruthy(sxEq(oldVal, value)))) {
fireProvideSubscribers(name); fireProvideSubscribers(target, restK);
} }
return makeCekValue(value, fenv, restK); return makeCekValue(value, fenv, restK);
})(); })() : (isSxTruthy(envHas(fenv, "provide-set!")) ? (apply(envGet(fenv, "provide-set!"), [name, value]), makeCekValue(value, fenv, restK)) : makeCekValue(NIL, fenv, restK)));
})(); if (_m == "scope-acc") return (function() { })(); if (_m == "scope-acc") return (function() {
var remaining = get(frame, "remaining"); var remaining = get(frame, "remaining");
var fenv = get(frame, "env"); var fenv = get(frame, "env");
@@ -3243,20 +2862,12 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
var testValue = get(frame, "match-val"); var testValue = get(frame, "match-val");
var fenv = get(frame, "env"); var fenv = get(frame, "env");
return continueWithCall(value, [testValue], fenv, [testValue], restK); return continueWithCall(value, [testValue], fenv, [testValue], restK);
})(); if (_m == "wind-after") return (function() { })(); if (_m == "raise-eval") return (function() {
var afterThunk = get(frame, "after-thunk");
var windersLen = get(frame, "winders-len");
var bodyResult = value;
var fenv = get(frame, "env");
return ((isSxTruthy((len(_winders_) > windersLen)) ? (_winders_ = rest(_winders_)) : NIL), continueWithCall(afterThunk, [], fenv, [], kontPush(makeWindReturnFrame(bodyResult, fenv), restK)));
})(); if (_m == "wind-return") return makeCekValue(get(frame, "body-result"), get(frame, "env"), restK); if (_m == "raise-eval") return (function() {
var condition = value; var condition = value;
var fenv = get(frame, "env"); var fenv = get(frame, "env");
var continuable_p = get(frame, "scheme"); var continuable_p = get(frame, "scheme");
var unwindResult = kontUnwindToHandler(restK, condition); var handlerFn = kontFindHandler(restK, condition);
var handlerFn = get(unwindResult, "handler"); return (isSxTruthy(isNil(handlerFn)) ? ((_lastErrorKont_ = restK), hostError((String("Unhandled exception: ") + String(inspect(condition))))) : continueWithCall(handlerFn, [condition], fenv, [condition], (isSxTruthy(continuable_p) ? kontPush(makeSignalReturnFrame(fenv, restK), restK) : kontPush(makeRaiseGuardFrame(fenv, restK), restK))));
var unwoundK = get(unwindResult, "kont");
return (isSxTruthy(isNil(handlerFn)) ? ((_lastErrorKont_ = unwoundK), hostError((String("Unhandled exception: ") + String(inspect(condition))))) : continueWithCall(handlerFn, [condition], fenv, [condition], (isSxTruthy(continuable_p) ? kontPush(makeSignalReturnFrame(fenv, unwoundK), unwoundK) : kontPush(makeRaiseGuardFrame(fenv, unwoundK), unwoundK))));
})(); if (_m == "raise-guard") return ((_lastErrorKont_ = restK), hostError("exception handler returned from non-continuable raise")); if (_m == "multi-map") return (function() { })(); if (_m == "raise-guard") return ((_lastErrorKont_ = restK), hostError("exception handler returned from non-continuable raise")); if (_m == "multi-map") return (function() {
var f = get(frame, "f"); var f = get(frame, "f");
var remaining = get(frame, "remaining"); var remaining = get(frame, "remaining");
@@ -3268,7 +2879,7 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
return continueWithCall(f, heads, fenv, [], kontPush(makeMultiMapFrame(f, tails, newResults, fenv), restK)); return continueWithCall(f, heads, fenv, [], kontPush(makeMultiMapFrame(f, tails, newResults, fenv), restK));
})()); })());
})(); if (_m == "callcc") return (function() { })(); if (_m == "callcc") return (function() {
var k = makeCallccContinuation(restK, len(_winders_)); var k = makeCallccContinuation(restK);
return continueWithCall(value, [k], get(frame, "env"), [k], restK); return continueWithCall(value, [k], get(frame, "env"), [k], restK);
})(); if (_m == "vm-resume") return (function() { })(); if (_m == "vm-resume") return (function() {
var resumeFn = get(frame, "f"); var resumeFn = get(frame, "f");
@@ -3314,8 +2925,7 @@ PRIMITIVES["step-continue"] = stepContinue;
})() : (isSxTruthy(callccContinuation_p(f)) ? (function() { })() : (isSxTruthy(callccContinuation_p(f)) ? (function() {
var arg = (isSxTruthy(isEmpty(args)) ? NIL : first(args)); var arg = (isSxTruthy(isEmpty(args)) ? NIL : first(args));
var captured = callccContinuationData(f); var captured = callccContinuationData(f);
var wLen = callccContinuationWindersLen(f); return makeCekValue(arg, env, captured);
return (windEscapeTo(wLen), makeCekValue(arg, env, captured));
})() : (isSxTruthy(continuation_p(f)) ? (function() { })() : (isSxTruthy(continuation_p(f)) ? (function() {
var arg = (isSxTruthy(isEmpty(args)) ? NIL : first(args)); var arg = (isSxTruthy(isEmpty(args)) ? NIL : first(args));
var contData = continuationData(f); var contData = continuationData(f);
@@ -3326,10 +2936,7 @@ PRIMITIVES["step-continue"] = stepContinue;
return makeCekValue(result, env, kont); return makeCekValue(result, env, kont);
})(); })();
})(); })();
})() : (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? (function() { })() : (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? makeCekValue(apply(f, args), env, kont) : (isSxTruthy(isLambda(f)) ? (function() {
var result = sxApplyCek(f, args);
return (isSxTruthy(evalError_p(result)) ? makeCekValue(get(result, "message"), env, kontPush(makeRaiseEvalFrame(env, false), kont)) : (isSxTruthy((isSxTruthy(isDict(result)) && get(result, "__vm_suspended"))) ? makeCekSuspended(get(result, "request"), env, kontPush(makeVmResumeFrame(get(result, "resume"), env), kont)) : makeCekValue(result, env, kont)));
})() : (isSxTruthy(isLambda(f)) ? (function() {
var params = lambdaParams(f); var params = lambdaParams(f);
var local = envMerge(lambdaClosure(f), env); var local = envMerge(lambdaClosure(f), env);
if (isSxTruthy(!isSxTruthy(bindLambdaParams(params, args, local)))) { if (isSxTruthy(!isSxTruthy(bindLambdaParams(params, args, local)))) {
@@ -3341,7 +2948,7 @@ PRIMITIVES["step-continue"] = stepContinue;
} }
return (function() { return (function() {
var jitResult = jitTryCall(f, args); var jitResult = jitTryCall(f, args);
return (isSxTruthy(jitSkip_p(jitResult)) ? makeCekState(lambdaBody(f), local, kont) : (isSxTruthy((isSxTruthy(isDict(jitResult)) && get(jitResult, "__vm_suspended"))) ? makeCekSuspended(get(jitResult, "request"), env, kontPush(makeVmResumeFrame(get(jitResult, "resume"), env), kont)) : makeCekValue(jitResult, local, kont))); return (isSxTruthy(isNil(jitResult)) ? makeCekState(lambdaBody(f), local, kont) : (isSxTruthy((isSxTruthy(isDict(jitResult)) && get(jitResult, "__vm_suspended"))) ? makeCekSuspended(get(jitResult, "request"), env, kontPush(makeVmResumeFrame(get(jitResult, "resume"), env), kont)) : makeCekValue(jitResult, local, kont)));
})(); })();
})() : (isSxTruthy(sxOr(isComponent(f), isIsland(f))) ? (function() { })() : (isSxTruthy(sxOr(isComponent(f), isIsland(f))) ? (function() {
var parsed = parseKeywordArgs(rawArgs, env); var parsed = parseKeywordArgs(rawArgs, env);
@@ -3375,10 +2982,6 @@ PRIMITIVES["eval-expr-cek"] = evalExprCek;
var trampolineCek = function(val) { return (isSxTruthy(isThunk(val)) ? evalExprCek(thunkExpr(val), thunkEnv(val)) : val); }; var trampolineCek = function(val) { return (isSxTruthy(isThunk(val)) ? evalExprCek(thunkExpr(val), thunkEnv(val)) : val); };
PRIMITIVES["trampoline-cek"] = trampolineCek; PRIMITIVES["trampoline-cek"] = trampolineCek;
// make-coroutine
var makeCoroutine = function(thunk) { return {"suspension": NIL, "thunk": thunk, "type": "coroutine", "state": "ready"}; };
PRIMITIVES["make-coroutine"] = makeCoroutine;
// eval-expr // eval-expr
var evalExpr = function(expr, env) { return cekRun(makeCekState(expr, env, [])); }; var evalExpr = function(expr, env) { return cekRun(makeCekState(expr, env, [])); };
PRIMITIVES["eval-expr"] = evalExpr; PRIMITIVES["eval-expr"] = evalExpr;
@@ -3747,16 +3350,10 @@ PRIMITIVES["serialize"] = serialize;
// === Transpiled from lib/dom (DOM library) === // === Transpiled from lib/dom (DOM library) ===
// dom-visible?
var domVisible_p = function(el) { return (isSxTruthy(el) ? !isSxTruthy(sxEq(hostGet(hostGet(el, "style"), "display"), "none")) : false); };
PRIMITIVES["dom-visible?"] = domVisible_p;
// === Transpiled from lib/browser (browser API library) === // === Transpiled from lib/browser (browser API library) ===
// json-stringify
var jsonStringify = function(v) { return hostCall(hostGlobal("JSON"), "stringify", v); };
PRIMITIVES["json-stringify"] = jsonStringify;
// === Transpiled from adapter-dom === // === Transpiled from adapter-dom ===
@@ -3927,7 +3524,6 @@ PRIMITIVES["process-page-scripts"] = processPageScripts;
// sx-hydrate-islands // sx-hydrate-islands
var sxHydrateIslands = function(root) { return (function() { var sxHydrateIslands = function(root) { return (function() {
var els = domQueryAll(sxOr(root, domBody()), "[data-sx-island]"); var els = domQueryAll(sxOr(root, domBody()), "[data-sx-island]");
preloadIslandDefs();
logInfo((String("sx-hydrate-islands: ") + String(len(els)) + String(" island(s) in ") + String((isSxTruthy(root) ? "subtree" : "document")))); logInfo((String("sx-hydrate-islands: ") + String(len(els)) + String(" island(s) in ") + String((isSxTruthy(root) ? "subtree" : "document"))));
return forEach(function(el) { return (isSxTruthy(isProcessed(el, "island-hydrated")) ? logInfo((String(" skip (already hydrated): ") + String(domGetAttr(el, "data-sx-island")))) : (logInfo((String(" hydrating: ") + String(domGetAttr(el, "data-sx-island")))), markProcessed(el, "island-hydrated"), hydrateIsland(el))); }, els); return forEach(function(el) { return (isSxTruthy(isProcessed(el, "island-hydrated")) ? logInfo((String(" skip (already hydrated): ") + String(domGetAttr(el, "data-sx-island")))) : (logInfo((String(" hydrating: ") + String(domGetAttr(el, "data-sx-island")))), markProcessed(el, "island-hydrated"), hydrateIsland(el))); }, els);
})(); }; })(); };
@@ -3941,34 +3537,26 @@ PRIMITIVES["sx-hydrate-islands"] = sxHydrateIslands;
var compName = (String("~") + String(name)); var compName = (String("~") + String(name));
var env = getRenderEnv(NIL); var env = getRenderEnv(NIL);
return (function() { return (function() {
var comp = envGet(globalEnv(), compName); var comp = envGet(env, compName);
return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() { return (isSxTruthy(!isSxTruthy(sxOr(isComponent(comp), isIsland(comp)))) ? logWarn((String("hydrate-island: unknown island ") + String(compName))) : (function() {
var kwargs = sxOr(first(sxParse(stateSx)), {}); var kwargs = sxOr(first(sxParse(stateSx)), {});
var disposers = []; var disposers = [];
var local = envMerge(componentClosure(comp), env); var local = envMerge(componentClosure(comp), env);
{ var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envBind(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } } { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; envBind(local, p, (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL)); } }
return (function() { return (function() {
var cursor = {["parent"]: el, ["index"]: 0}; var bodyDom = cekTry(function() { return withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return renderToDom(componentBody(comp), local, NIL); }); }, function(err) { logWarn((String("hydrate-island FAILED: ") + String(compName) + String(" — ") + String(err)));
hostCall(el, "replaceChildren");
scopePush("sx-hydrating", NIL);
cekTry(function() { return withIslandScope(function(disposable) { return append_b(disposers, disposable); }, function() { return (function() {
var bodyDom = renderToDom(componentBody(comp), local, NIL);
return (isSxTruthy(bodyDom) ? domAppend(el, bodyDom) : NIL);
})(); }); }, function(err) { scopePop("sx-hydrating");
logWarn((String("hydrate fallback: ") + String(compName) + String(" — ") + String(err)));
return (function() { return (function() {
var fallback = cekTry(function() { return withIslandScope(function(d) { return append_b(disposers, d); }, function() { return renderToDom(componentBody(comp), local, NIL); }); }, function(err2) { return (function() { var errorEl = domCreateElement("div", NIL);
var e = domCreateElement("div", NIL); domSetAttr(errorEl, "class", "sx-island-error");
domSetTextContent(e, (String("Island error: ") + String(compName) + String("\n") + String(err2))); domSetAttr(errorEl, "style", "padding:8px;margin:4px 0;border:1px solid #ef4444;border-radius:4px;background:#fef2f2;color:#b91c1c;font-family:monospace;font-size:12px;white-space:pre-wrap");
return e; domSetTextContent(errorEl, (String("Island error: ") + String(compName) + String("\n") + String(err)));
return errorEl;
})(); }); })(); });
hostCall(el, "replaceChildren", fallback); domSetTextContent(el, "");
return NIL; domAppend(el, bodyDom);
})(); });
scopePop("sx-hydrating");
domSetData(el, "sx-disposers", disposers); domSetData(el, "sx-disposers", disposers);
setTimeout_(function() { return processElements(el); }, 0); setTimeout_(function() { return processElements(el); }, 0);
return logInfo((String("hydrated island: ~") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)"))); return logInfo((String("hydrated island: ") + String(compName) + String(" (") + String(len(disposers)) + String(" disposers)")));
})(); })();
})()); })());
})(); })();
@@ -4068,18 +3656,6 @@ PRIMITIVES["boot-init"] = bootInit;
// Core primitives that require native JS (cannot be expressed via FFI) // Core primitives that require native JS (cannot be expressed via FFI)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
PRIMITIVES["error"] = function(msg) { throw new Error(msg); }; PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
PRIMITIVES["host-error"] = function(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); };
PRIMITIVES["try-catch"] = function(tryFn, catchFn) {
try {
return cekRun(continueWithCall(tryFn, [], makeEnv(), [], []));
} catch(e) {
var msg = e && e.message ? e.message : String(e);
return cekRun(continueWithCall(catchFn, [msg], makeEnv(), [msg], []));
}
};
PRIMITIVES["without-io-hook"] = function(thunk) {
return cekRun(continueWithCall(thunk, [], makeEnv(), [], []));
};
PRIMITIVES["sort"] = function(lst) { PRIMITIVES["sort"] = function(lst) {
if (!Array.isArray(lst)) return lst; if (!Array.isArray(lst)) return lst;
return lst.slice().sort(function(a, b) { return lst.slice().sort(function(a, b) {
@@ -4147,7 +3723,7 @@ PRIMITIVES["boot-init"] = bootInit;
PRIMITIVES["dom-tag-name"] = domTagName; PRIMITIVES["dom-tag-name"] = domTagName;
PRIMITIVES["dom-get-prop"] = domGetProp; PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp; PRIMITIVES["dom-set-prop"] = domSetProp;
if (typeof reactiveText === "function") PRIMITIVES["reactive-text"] = reactiveText; PRIMITIVES["reactive-text"] = reactiveText;
PRIMITIVES["set-interval"] = setInterval_; PRIMITIVES["set-interval"] = setInterval_;
PRIMITIVES["clear-interval"] = clearInterval_; PRIMITIVES["clear-interval"] = clearInterval_;
PRIMITIVES["promise-then"] = promiseThen; PRIMITIVES["promise-then"] = promiseThen;
@@ -4231,13 +3807,6 @@ PRIMITIVES["boot-init"] = bootInit;
PRIMITIVES["lambda-name"] = lambdaName; PRIMITIVES["lambda-name"] = lambdaName;
PRIMITIVES["component?"] = isComponent; PRIMITIVES["component?"] = isComponent;
PRIMITIVES["island?"] = isIsland; PRIMITIVES["island?"] = isIsland;
PRIMITIVES["parameter?"] = parameter_p;
PRIMITIVES["parameter-uid"] = parameterUid;
PRIMITIVES["parameter-default"] = parameterDefault;
PRIMITIVES["make-parameter"] = function(defaultVal, converter) {
var p = new SxParameter(defaultVal, converter || null);
return p;
};
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); }; PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; }; PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
function makeEnv() { return merge(componentEnv, PRIMITIVES); } function makeEnv() { return merge(componentEnv, PRIMITIVES); }
@@ -4428,7 +3997,7 @@ PRIMITIVES["boot-init"] = bootInit;
} }
function domDispatch(el, name, detail) { function domDispatch(el, name, detail) {
if (!_hasDom || !el || typeof el.dispatchEvent !== "function") return false; if (!_hasDom || !el) return false;
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} }); var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
return el.dispatchEvent(evt); return el.dispatchEvent(evt);
} }
@@ -4550,14 +4119,6 @@ PRIMITIVES["boot-init"] = bootInit;
// Platform interface — Orchestration (browser-only) // Platform interface — Orchestration (browser-only)
// ========================================================================= // =========================================================================
// --- Stubs for define-library functions not transpiled by extract_defines ---
// These are defined in orchestration.sx's define-library and called from
// boot.sx top-level defines. The JS bootstrapper only transpiles top-level
// defines, so we provide stubs here for functions that need a JS identity.
function flushCollectedStyles() { return NIL; }
function processElements(root) { return NIL; }
// --- Browser/Network --- // --- Browser/Network ---
function browserNavigate(url) { function browserNavigate(url) {
@@ -5043,10 +4604,6 @@ PRIMITIVES["boot-init"] = bootInit;
return el && el.closest ? el.closest(sel) : null; return el && el.closest ? el.closest(sel) : null;
} }
function domDocument() {
return _hasDom ? document : null;
}
function domBody() { function domBody() {
return _hasDom ? document.body : null; return _hasDom ? document.body : null;
} }
@@ -5488,8 +5045,6 @@ PRIMITIVES["boot-init"] = bootInit;
// Platform interface — Boot (mount, hydrate, scripts, cookies) // Platform interface — Boot (mount, hydrate, scripts, cookies)
// ========================================================================= // =========================================================================
function preloadIslandDefs() { return NIL; }
function resolveMountTarget(target) { function resolveMountTarget(target) {
if (typeof target === "string") return _hasDom ? document.querySelector(target) : null; if (typeof target === "string") return _hasDom ? document.querySelector(target) : null;
return target; return target;
@@ -6365,52 +5920,52 @@ PRIMITIVES["boot-init"] = bootInit;
hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null, hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,
disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null, disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,
init: typeof bootInit === "function" ? bootInit : null, init: typeof bootInit === "function" ? bootInit : null,
scanRefs: typeof scanRefs === "function" ? scanRefs : null, scanRefs: scanRefs,
scanComponentsFromSource: typeof scanComponentsFromSource === "function" ? scanComponentsFromSource : null, scanComponentsFromSource: scanComponentsFromSource,
transitiveDeps: typeof transitiveDeps === "function" ? transitiveDeps : null, transitiveDeps: transitiveDeps,
computeAllDeps: typeof computeAllDeps === "function" ? computeAllDeps : null, computeAllDeps: computeAllDeps,
componentsNeeded: typeof componentsNeeded === "function" ? componentsNeeded : null, componentsNeeded: componentsNeeded,
pageComponentBundle: typeof pageComponentBundle === "function" ? pageComponentBundle : null, pageComponentBundle: pageComponentBundle,
pageCssClasses: typeof pageCssClasses === "function" ? pageCssClasses : null, pageCssClasses: pageCssClasses,
scanIoRefs: typeof scanIoRefs === "function" ? scanIoRefs : null, scanIoRefs: scanIoRefs,
transitiveIoRefs: typeof transitiveIoRefs === "function" ? transitiveIoRefs : null, transitiveIoRefs: transitiveIoRefs,
computeAllIoRefs: typeof computeAllIoRefs === "function" ? computeAllIoRefs : null, computeAllIoRefs: computeAllIoRefs,
componentPure_p: typeof componentPure_p === "function" ? componentPure_p : null, componentPure_p: componentPure_p,
categorizeSpecialForms: typeof categorizeSpecialForms === "function" ? categorizeSpecialForms : null, categorizeSpecialForms: categorizeSpecialForms,
buildReferenceData: typeof buildReferenceData === "function" ? buildReferenceData : null, buildReferenceData: buildReferenceData,
buildAttrDetail: typeof buildAttrDetail === "function" ? buildAttrDetail : null, buildAttrDetail: buildAttrDetail,
buildHeaderDetail: typeof buildHeaderDetail === "function" ? buildHeaderDetail : null, buildHeaderDetail: buildHeaderDetail,
buildEventDetail: typeof buildEventDetail === "function" ? buildEventDetail : null, buildEventDetail: buildEventDetail,
buildComponentSource: typeof buildComponentSource === "function" ? buildComponentSource : null, buildComponentSource: buildComponentSource,
buildBundleAnalysis: typeof buildBundleAnalysis === "function" ? buildBundleAnalysis : null, buildBundleAnalysis: buildBundleAnalysis,
buildRoutingAnalysis: typeof buildRoutingAnalysis === "function" ? buildRoutingAnalysis : null, buildRoutingAnalysis: buildRoutingAnalysis,
buildAffinityAnalysis: typeof buildAffinityAnalysis === "function" ? buildAffinityAnalysis : null, buildAffinityAnalysis: buildAffinityAnalysis,
splitPathSegments: typeof splitPathSegments === "function" ? splitPathSegments : null, splitPathSegments: splitPathSegments,
parseRoutePattern: typeof parseRoutePattern === "function" ? parseRoutePattern : null, parseRoutePattern: parseRoutePattern,
matchRoute: typeof matchRoute === "function" ? matchRoute : null, matchRoute: matchRoute,
findMatchingRoute: typeof findMatchingRoute === "function" ? findMatchingRoute : null, findMatchingRoute: findMatchingRoute,
urlToExpr: typeof urlToExpr === "function" ? urlToExpr : null, urlToExpr: urlToExpr,
autoQuoteUnknowns: typeof autoQuoteUnknowns === "function" ? autoQuoteUnknowns : null, autoQuoteUnknowns: autoQuoteUnknowns,
prepareUrlExpr: typeof prepareUrlExpr === "function" ? prepareUrlExpr : null, prepareUrlExpr: prepareUrlExpr,
registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null, registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,
registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null, registerIoDeps: typeof registerIoDeps === "function" ? registerIoDeps : null,
asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null, asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,
asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null, asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,
signal: typeof signal === "function" ? signal : null, signal: signal,
deref: typeof deref === "function" ? deref : null, deref: deref,
reset: typeof reset_b === "function" ? reset_b : null, reset: reset_b,
swap: typeof swap_b === "function" ? swap_b : null, swap: swap_b,
computed: typeof computed === "function" ? computed : null, computed: computed,
effect: typeof effect === "function" ? effect : null, effect: effect,
batch: typeof batch === "function" ? batch : null, batch: batch,
isSignal: typeof isSignal === "function" ? isSignal : null, isSignal: isSignal,
makeSignal: typeof makeSignal === "function" ? makeSignal : null, makeSignal: makeSignal,
defStore: typeof defStore === "function" ? defStore : null, defStore: defStore,
useStore: typeof useStore === "function" ? useStore : null, useStore: useStore,
clearStores: typeof clearStores === "function" ? clearStores : null, clearStores: clearStores,
emitEvent: typeof emitEvent === "function" ? emitEvent : null, emitEvent: emitEvent,
onEvent: typeof onEvent === "function" ? onEvent : null, onEvent: onEvent,
bridgeEvent: typeof bridgeEvent === "function" ? bridgeEvent : null, bridgeEvent: bridgeEvent,
makeSpread: makeSpread, makeSpread: makeSpread,
isSpread: isSpread, isSpread: isSpread,
spreadAttrs: spreadAttrs, spreadAttrs: spreadAttrs,

View File

@@ -1,56 +0,0 @@
(define-library
(sx coroutines)
(export
make-coroutine
coroutine?
coroutine-alive?
coroutine-yield
coroutine-handle-result
coroutine-resume)
(begin
(define make-coroutine (fn (thunk) {:suspension nil :thunk thunk :type "coroutine" :state "ready"}))
(define
coroutine?
(fn (v) (and (dict? v) (= (get v "type") "coroutine"))))
(define
coroutine-alive?
(fn (c) (and (coroutine? c) (not (= (get c "state") "dead")))))
(define coroutine-yield (fn (val) (perform {:value val :op "coroutine-yield"})))
(define
coroutine-handle-result
(fn
(c result)
(if
(cek-terminal? result)
(do (dict-set! c "state" "dead") {:done true :value (cek-value result)})
(let
((request (cek-io-request result)))
(if
(and (dict? request) (= (get request "op") "coroutine-yield"))
(do
(dict-set! c "state" "suspended")
(dict-set! c "suspension" result)
{:done false :value (get request "value")})
(perform request))))))
(define
coroutine-resume
(fn
(c val)
(cond
(not (coroutine? c))
(error "coroutine-resume: not a coroutine")
(= (get c "state") "dead")
(error "coroutine-resume: coroutine is dead")
(= (get c "state") "ready")
(do
(dict-set! c "state" "running")
(coroutine-handle-result
c
(cek-step-loop
(make-cek-state (list (get c "thunk")) (make-env) (list)))))
(= (get c "state") "suspended")
(do
(dict-set! c "state" "running")
(coroutine-handle-result c (cek-resume (get c "suspension") val)))
:else (error
(str "coroutine-resume: unexpected state: " (get c "state"))))))))

View File

@@ -142,16 +142,6 @@
(define make-callcc-frame (fn (env) {:env env :type "callcc"})) (define make-callcc-frame (fn (env) {:env env :type "callcc"}))
(define
make-wind-after-frame
(fn (after-thunk winders-len env)
{:type "wind-after" :after-thunk after-thunk :winders-len winders-len :env env}))
(define
make-wind-return-frame
(fn (body-result env)
{:type "wind-return" :body-result body-result :env env}))
;; R7RS exception frames (raise, guard) ;; R7RS exception frames (raise, guard)
(define make-deref-frame (fn (env) {:env env :type "deref"})) (define make-deref-frame (fn (env) {:env env :type "deref"}))
@@ -238,44 +228,6 @@
match)) match))
(kont-find-handler (rest kont) condition)))))) (kont-find-handler (rest kont) condition))))))
(define
kont-unwind-to-handler
(fn (kont condition)
(if
(empty? kont)
{:handler nil :kont kont}
(let
((frame (first kont)) (rest-k (rest kont)))
(cond
(= (frame-type frame) "handler")
(let
((match (find-matching-handler (get frame "f") condition)))
(if
(nil? match)
(kont-unwind-to-handler rest-k condition)
{:handler match :kont kont}))
(= (frame-type frame) "wind-after")
(do
(when
(> (len *winders*) (get frame "winders-len"))
(set! *winders* (rest *winders*)))
(cek-call (get frame "after-thunk") (list))
(kont-unwind-to-handler rest-k condition))
:else
(kont-unwind-to-handler rest-k condition))))))
(define
wind-escape-to
(fn
(target-len)
(when
(> (len *winders*) target-len)
(let
((after-thunk (first *winders*)))
(set! *winders* (rest *winders*))
(cek-call after-thunk (list))
(wind-escape-to target-len)))))
(define (define
find-named-restart find-named-restart
(fn (fn
@@ -458,8 +410,6 @@
(define *provide-subscribers* (dict)) (define *provide-subscribers* (dict))
(define *winders* (list))
(define *library-registry* (dict)) (define *library-registry* (dict))
(define (define
@@ -1393,24 +1343,14 @@
(make-cek-state (thunk-expr thk) (thunk-env thk) kont)))) (make-cek-state (thunk-expr thk) (thunk-env thk) kont))))
(define (define
step-sf-dynamic-wind sf-dynamic-wind
(fn (fn
(args env kont) ((args :as list) (env :as dict))
(let (let
((before (trampoline (eval-expr (first args) env))) ((before (trampoline (eval-expr (first args) env)))
(body (trampoline (eval-expr (nth args 1) env))) (body (trampoline (eval-expr (nth args 1) env)))
(after (trampoline (eval-expr (nth args 2) env)))) (after (trampoline (eval-expr (nth args 2) env))))
(do (dynamic-wind-call before body after env))))
(cek-call before (list))
(let
((winders-len (len *winders*)))
(set! *winders* (cons after *winders*))
(continue-with-call
body
(list)
env
(list)
(kont-push (make-wind-after-frame after winders-len env) kont)))))))
;; R7RS records (SRFI-9) ;; R7RS records (SRFI-9)
;; ;;
@@ -1848,7 +1788,8 @@
("invoke-restart" (step-sf-invoke-restart args env kont)) ("invoke-restart" (step-sf-invoke-restart args env kont))
("match" (step-sf-match args env kont)) ("match" (step-sf-match args env kont))
("let-match" (step-sf-let-match args env kont)) ("let-match" (step-sf-let-match args env kont))
("dynamic-wind" (step-sf-dynamic-wind args env kont)) ("dynamic-wind"
(make-cek-value (sf-dynamic-wind args env) env kont))
("map" (step-ho-map args env kont)) ("map" (step-ho-map args env kont))
("map-indexed" (step-ho-map-indexed args env kont)) ("map-indexed" (step-ho-map-indexed args env kont))
("filter" (step-ho-filter args env kont)) ("filter" (step-ho-filter args env kont))
@@ -1898,67 +1839,6 @@
:else (step-eval-call head args env kont))))) :else (step-eval-call head args env kont)))))
(step-eval-call head args env kont)))))) (step-eval-call head args env kont))))))
(define
sf-define-type
(fn
(args env)
(let
((type-sym (first args)) (ctor-specs (rest args)))
(let
((type-name (symbol-name type-sym))
(ctor-names
(map (fn (spec) (symbol-name (first spec))) ctor-specs)))
(when
(not (env-has? env "*adt-registry*"))
(env-bind! env "*adt-registry*" {}))
(dict-set! (env-get env "*adt-registry*") type-name ctor-names)
(env-bind!
env
(str type-name "?")
(fn
(v)
(and (dict? v) (get v :_adt) (= (get v :_type) type-name))))
(for-each
(fn
(spec)
(let
((cn (symbol-name (first spec)))
(field-names (map (fn (f) (symbol-name f)) (rest spec)))
(arity (len (rest spec))))
(env-bind!
env
cn
(fn
(&rest ctor-args)
(if
(not (= (len ctor-args) arity))
(error
(str
cn
": expected "
arity
" args, got "
(len ctor-args)))
{:_ctor cn :_type type-name :_adt true :_fields ctor-args})))
(env-bind!
env
(str cn "?")
(fn
(v)
(and (dict? v) (get v :_adt) (= (get v :_ctor) cn))))
(for-each-indexed
(fn
(idx field-name)
(env-bind!
env
(str cn "-" field-name)
(fn (v) (nth (get v :_fields) idx))))
field-names)))
ctor-specs)
nil))))
(register-special-form! "define-type" sf-define-type)
(define (define
kont-extract-provides kont-extract-provides
(fn (fn
@@ -1993,14 +1873,6 @@
subs) subs)
(for-each (fn (sub) (cek-call sub (list kont))) subs)))))) (for-each (fn (sub) (cek-call sub (list kont))) subs))))))
;; ═══════════════════════════════════════════════════════════════
;; Part 10: Continue Phase — Frame Dispatch
;;
;; When phase="continue", pop the top frame and process the value.
;; Each frame type has its own handling: if frames check truthiness,
;; let frames bind the value, arg frames accumulate it, etc.
;; continue-with-call handles the final function/component dispatch.
;; ═══════════════════════════════════════════════════════════════
(define (define
fire-provide-subscribers fire-provide-subscribers
(fn (fn
@@ -2020,13 +1892,18 @@
subs) subs)
(for-each (fn (sub) (cek-call sub (list nil))) subs)))))) (for-each (fn (sub) (cek-call sub (list nil))) subs))))))
;; Final call dispatch from arg frame — all args evaluated, invoke function.
;; Handles: lambda (bind params + TCO), component (keyword args + TCO),
;; native fn (direct call), continuation (resume), callcc continuation (escape).
(define (define
batch-begin! batch-begin!
(fn () (set! *provide-batch-depth* (+ *provide-batch-depth* 1)))) (fn () (set! *provide-batch-depth* (+ *provide-batch-depth* 1))))
;; ═══════════════════════════════════════════════════════════════
;; Part 10: Continue Phase — Frame Dispatch
;;
;; When phase="continue", pop the top frame and process the value.
;; Each frame type has its own handling: if frames check truthiness,
;; let frames bind the value, arg frames accumulate it, etc.
;; continue-with-call handles the final function/component dispatch.
;; ═══════════════════════════════════════════════════════════════
(define (define
batch-end! batch-end!
(fn (fn
@@ -2039,13 +1916,9 @@
(set! *provide-batch-queue* (list)) (set! *provide-batch-queue* (list))
(for-each (fn (sub) (cek-call sub (list nil))) queue))))) (for-each (fn (sub) (cek-call sub (list nil))) queue)))))
;; ═══════════════════════════════════════════════════════════════ ;; Final call dispatch from arg frame — all args evaluated, invoke function.
;; Part 11: Entry Points ;; Handles: lambda (bind params + TCO), component (keyword args + TCO),
;; ;; native fn (direct call), continuation (resume), callcc continuation (escape).
;; eval-expr-cek / trampoline-cek: CEK evaluation entry points.
;; eval-expr / trampoline: top-level bindings that override the
;; forward declarations from Part 5.
;; ═══════════════════════════════════════════════════════════════
(define (define
step-sf-bind step-sf-bind
(fn (fn
@@ -2076,6 +1949,13 @@
(make-parameterize-frame bindings nil (list) body env) (make-parameterize-frame bindings nil (list) body env)
kont))))))) kont)))))))
;; ═══════════════════════════════════════════════════════════════
;; Part 11: Entry Points
;;
;; eval-expr-cek / trampoline-cek: CEK evaluation entry points.
;; eval-expr / trampoline: top-level bindings that override the
;; forward declarations from Part 5.
;; ═══════════════════════════════════════════════════════════════
(define (define
syntax-rules-match syntax-rules-match
(fn (fn
@@ -2247,10 +2127,7 @@
((all-vars (syntax-rules-find-all-vars elem bindings))) ((all-vars (syntax-rules-find-all-vars elem bindings)))
(if (if
(empty? all-vars) (empty? all-vars)
(syntax-rules-instantiate-list (syntax-rules-instantiate-list template (+ i 2) bindings)
template
(+ i 2)
bindings)
(let (let
((count (len (get bindings (first all-vars)))) ((count (len (get bindings (first all-vars))))
(expanded (expanded
@@ -2273,10 +2150,7 @@
(syntax-rules-instantiate elem b))) (syntax-rules-instantiate elem b)))
(range count))) (range count)))
(rest-result (rest-result
(syntax-rules-instantiate-list (syntax-rules-instantiate-list template (+ i 2) bindings)))
template
(+ i 2)
bindings)))
(append expanded rest-result)))) (append expanded rest-result))))
(cons (cons
(syntax-rules-instantiate elem bindings) (syntax-rules-instantiate elem bindings)
@@ -2603,8 +2477,7 @@
(let (let
((proto-name (symbol-name (first args))) ((proto-name (symbol-name (first args)))
(raw-type-name (symbol-name (nth args 1))) (raw-type-name (symbol-name (nth args 1)))
(type-name (type-name (slice raw-type-name 1 (- (len raw-type-name) 1)))
(slice raw-type-name 1 (- (len raw-type-name) 1)))
(method-defs (rest (rest args)))) (method-defs (rest (rest args))))
(let (let
((proto (get *protocol-registry* proto-name))) ((proto (get *protocol-registry* proto-name)))
@@ -2736,17 +2609,6 @@
(= value (nth pattern 1)) (= value (nth pattern 1))
(symbol? pattern) (symbol? pattern)
(do (env-bind! env (symbol-name pattern) value) true) (do (env-bind! env (symbol-name pattern) value) true)
(and (list? pattern) (not (empty? pattern)) (symbol? (first pattern)) (dict? value) (get value :_adt))
(let
((ctor-name (symbol-name (first pattern)))
(field-patterns (rest pattern))
(fields (get value :_fields)))
(and
(= (get value :_ctor) ctor-name)
(= (len field-patterns) (len fields))
(every?
(fn (pair) (match-pattern (first pair) (nth pair 1) env))
(zip field-patterns fields))))
(and (dict? pattern) (dict? value)) (and (dict? pattern) (dict? value))
(every? (every?
(fn (k) (match-pattern (get pattern k) (get value k) env)) (fn (k) (match-pattern (get pattern k) (get value k) env))
@@ -2757,12 +2619,8 @@
(and (and
(>= (len value) rest-idx) (>= (len value) rest-idx)
(every? (every?
(fn (fn (pair) (match-pattern (first pair) (nth pair 1) env))
(pair) (zip (slice pattern 0 rest-idx) (slice value 0 rest-idx)))
(match-pattern (first pair) (nth pair 1) env))
(zip
(slice pattern 0 rest-idx)
(slice value 0 rest-idx)))
(let (let
((rest-name (nth pattern (+ rest-idx 1)))) ((rest-name (nth pattern (+ rest-idx 1))))
(env-bind! env (symbol-name rest-name) (slice value rest-idx)) (env-bind! env (symbol-name rest-name) (slice value rest-idx))
@@ -2774,9 +2632,7 @@
(let (let
((pairs (zip pattern value))) ((pairs (zip pattern value)))
(every? (every?
(fn (fn (pair) (match-pattern (first pair) (nth pair 1) env))
(pair)
(match-pattern (first pair) (nth pair 1) env))
pairs))) pairs)))
:else (= pattern value)))) :else (= pattern value))))
@@ -2791,7 +2647,7 @@
((result (match-find-clause val clauses env))) ((result (match-find-clause val clauses env)))
(if (if
(nil? result) (nil? result)
(make-cek-value (str "match: no clause matched " (inspect val)) env (kont-push (make-raise-eval-frame env false) kont)) (error (str "match: no clause matched " (inspect val)))
(make-cek-state (nth result 1) (first result) kont)))))) (make-cek-state (nth result 1) (first result) kont))))))
(define (define
@@ -3439,8 +3295,7 @@
kont))))) kont)))))
("reduce" ("reduce"
(let (let
((init (nth ordered 1)) ((init (nth ordered 1)) (coll (nth ordered 2)))
(coll (nth ordered 2)))
(if (if
(empty? coll) (empty? coll)
(make-cek-value init env kont) (make-cek-value init env kont)
@@ -3744,10 +3599,7 @@
(next-test (first next-clause))) (next-test (first next-clause)))
(if (if
(is-else-clause? next-test) (is-else-clause? next-test)
(make-cek-state (make-cek-state (nth next-clause 1) fenv rest-k)
(nth next-clause 1)
fenv
rest-k)
(make-cek-state (make-cek-state
next-test next-test
fenv fenv
@@ -3919,9 +3771,7 @@
(let (let
((d (dict))) ((d (dict)))
(for-each (for-each
(fn (fn (pair) (dict-set! d (first pair) (nth pair 1)))
(pair)
(dict-set! d (first pair) (nth pair 1)))
completed) completed)
(make-cek-value d fenv rest-k)) (make-cek-value d fenv rest-k))
(let (let
@@ -4232,41 +4082,16 @@
fenv fenv
(list test-value) (list test-value)
rest-k))) rest-k)))
("wind-after"
(let
((after-thunk (get frame "after-thunk"))
(winders-len (get frame "winders-len"))
(body-result value)
(fenv (get frame "env")))
(do
(when
(> (len *winders*) winders-len)
(set! *winders* (rest *winders*)))
(continue-with-call
after-thunk
(list)
fenv
(list)
(kont-push
(make-wind-return-frame body-result fenv)
rest-k)))))
("wind-return"
(make-cek-value
(get frame "body-result")
(get frame "env")
rest-k))
("raise-eval" ("raise-eval"
(let (let
((condition value) ((condition value)
(fenv (get frame "env")) (fenv (get frame "env"))
(continuable? (get frame "scheme")) (continuable? (get frame "scheme"))
(unwind-result (kont-unwind-to-handler rest-k condition)) (handler-fn (kont-find-handler rest-k condition)))
(handler-fn (get unwind-result "handler"))
(unwound-k (get unwind-result "kont")))
(if (if
(nil? handler-fn) (nil? handler-fn)
(do (do
(set! *last-error-kont* unwound-k) (set! *last-error-kont* rest-k)
(host-error (host-error
(str "Unhandled exception: " (inspect condition)))) (str "Unhandled exception: " (inspect condition))))
(continue-with-call (continue-with-call
@@ -4277,11 +4102,9 @@
(if (if
continuable? continuable?
(kont-push (kont-push
(make-signal-return-frame fenv unwound-k) (make-signal-return-frame fenv rest-k)
unwound-k) rest-k)
(kont-push (kont-push (make-raise-guard-frame fenv rest-k) rest-k))))))
(make-raise-guard-frame fenv unwound-k)
unwound-k))))))
("raise-guard" ("raise-guard"
(do (do
(set! *last-error-kont* rest-k) (set! *last-error-kont* rest-k)
@@ -4309,7 +4132,7 @@
rest-k)))))) rest-k))))))
("callcc" ("callcc"
(let (let
((k (make-callcc-continuation rest-k (len *winders*)))) ((k (make-callcc-continuation rest-k)))
(continue-with-call (continue-with-call
value value
(list k) (list k)
@@ -4413,9 +4236,8 @@
(callcc-continuation? f) (callcc-continuation? f)
(let (let
((arg (if (empty? args) nil (first args))) ((arg (if (empty? args) nil (first args)))
(captured (callcc-continuation-data f)) (captured (callcc-continuation-data f)))
(w-len (callcc-continuation-winders-len f))) (make-cek-value arg env captured))
(do (wind-escape-to w-len) (make-cek-value arg env captured)))
(continuation? f) (continuation? f)
(let (let
((arg (if (empty? args) nil (first args))) ((arg (if (empty? args) nil (first args)))
@@ -4460,9 +4282,7 @@
" args, got " " args, got "
(len args)))) (len args))))
(for-each (for-each
(fn (fn (pair) (env-bind! local (first pair) (nth pair 1)))
(pair)
(env-bind! local (first pair) (nth pair 1)))
(zip params args)) (zip params args))
(for-each (for-each
(fn (p) (env-bind! local p nil)) (fn (p) (env-bind! local p nil))
@@ -4517,11 +4337,7 @@
(if (if
(= match-val test-val) (= match-val test-val)
(make-cek-state body env kont) (make-cek-state body env kont)
(sf-case-step-loop (sf-case-step-loop match-val (slice clauses 2) env kont))))))))
match-val
(slice clauses 2)
env
kont))))))))
(define (define
eval-expr-cek eval-expr-cek
@@ -4533,8 +4349,6 @@
(val) (val)
(if (thunk? val) (eval-expr-cek (thunk-expr val) (thunk-env val)) val))) (if (thunk? val) (eval-expr-cek (thunk-expr val) (thunk-env val)) val)))
(define make-coroutine (fn (thunk) {:suspension nil :thunk thunk :type "coroutine" :state "ready"}))
(define (define
eval-expr eval-expr
(fn (expr (env :as dict)) (cek-run (make-cek-state expr env (list))))) (fn (expr (env :as dict)) (cek-run (make-cek-state expr env (list)))))

View File

@@ -43,35 +43,35 @@
"+" "+"
:params (&rest (args :as number)) :params (&rest (args :as number))
:returns "number" :returns "number"
:doc "Sum all arguments. Returns integer iff all args are exact integers (float contagion)." :doc "Sum all arguments."
:body (reduce (fn (a b) (native-add a b)) 0 args)) :body (reduce (fn (a b) (native-add a b)) 0 args))
(define-primitive (define-primitive
"-" "-"
:params ((a :as number) &rest (b :as number)) :params ((a :as number) &rest (b :as number))
:returns "number" :returns "number"
:doc "Subtract. Unary: negate. Binary: a - b. Float contagion: returns integer iff all args are integers." :doc "Subtract. Unary: negate. Binary: a - b."
:body (if (empty? b) (native-neg a) (native-sub a (first b)))) :body (if (empty? b) (native-neg a) (native-sub a (first b))))
(define-primitive (define-primitive
"*" "*"
:params (&rest (args :as number)) :params (&rest (args :as number))
:returns "number" :returns "number"
:doc "Multiply all arguments. Float contagion: integer result iff all args are exact integers." :doc "Multiply all arguments."
:body (reduce (fn (a b) (native-mul a b)) 1 args)) :body (reduce (fn (a b) (native-mul a b)) 1 args))
(define-primitive (define-primitive
"/" "/"
:params ((a :as number) (b :as number)) :params ((a :as number) (b :as number))
:returns "float" :returns "number"
:doc "Divide a by b. Always returns inexact float." :doc "Divide a by b."
:body (native-div a b)) :body (native-div a b))
(define-primitive (define-primitive
"mod" "mod"
:params ((a :as number) (b :as number)) :params ((a :as number) (b :as number))
:returns "number" :returns "number"
:doc "Modulo a % b. Returns integer iff both args are integers." :doc "Modulo a % b."
:body (native-mod a b)) :body (native-mod a b))
(define-primitive (define-primitive
@@ -108,26 +108,26 @@
(define-primitive (define-primitive
"floor" "floor"
:params ((x :as number)) :params ((x :as number))
:returns "integer" :returns "number"
:doc "Floor toward negative infinity — returns exact integer.") :doc "Floor to integer.")
(define-primitive (define-primitive
"ceil" "ceil"
:params ((x :as number)) :params ((x :as number))
:returns "integer" :returns "number"
:doc "Ceiling toward positive infinity — returns exact integer.") :doc "Ceiling to integer.")
(define-primitive (define-primitive
"round" "round"
:params ((x :as number) &rest (ndigits :as number)) :params ((x :as number) &rest (ndigits :as number))
:returns "number" :returns "number"
:doc "Round to ndigits decimal places (default 0). Returns integer when ndigits is 0.") :doc "Round to ndigits decimal places (default 0).")
(define-primitive (define-primitive
"truncate" "truncate"
:params ((x :as number)) :params (((x :as number)))
:returns "integer" :returns "number"
:doc "Truncate toward zero — returns exact integer.") :doc "Truncate toward zero.")
(define-primitive (define-primitive
"remainder" "remainder"
@@ -143,42 +143,42 @@
(define-primitive (define-primitive
"exact?" "exact?"
:params ((x :as number)) :params (((x :as number)))
:returns "boolean" :returns "boolean"
:doc "True if x is an exact integer (not an inexact float).") :doc "True if x is exact (integer-valued).")
(define-primitive (define-primitive
"inexact?" "inexact?"
:params ((x :as number)) :params (((x :as number)))
:returns "boolean" :returns "boolean"
:doc "True if x is an inexact float (not an exact integer).") :doc "True if x is inexact (non-integer).")
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Core — Comparison ;; Core — Comparison
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-primitive (define-primitive
"exact->inexact" "exact->inexact"
:params ((x :as number)) :params (((x :as number)))
:returns "float" :returns "number"
:doc "Convert exact integer to inexact float. Floats pass through unchanged.") :doc "Convert exact to inexact (identity for float tower).")
(define-primitive (define-primitive
"inexact->exact" "inexact->exact"
:params ((x :as number)) :params (((x :as number)))
:returns "integer" :returns "number"
:doc "Convert inexact float to nearest exact integer (truncates). Integers pass through unchanged.") :doc "Convert inexact to nearest exact integer.")
(define-primitive (define-primitive
"make-vector" "make-vector"
:params ((n :as number) (fill :as any :optional true)) :params ((n :as number))
:returns "vector" :returns "vector"
:doc "Create vector of length n, each element initialised to fill (default nil).") :doc "Create vector of size n, optionally filled.")
(define-primitive (define-primitive
"vector" "vector"
:params (:rest (elts :as any)) :params ()
:returns "vector" :returns "vector"
:doc "Construct a vector from its arguments.") :doc "Create vector from arguments.")
(define-primitive (define-primitive
"vector?" "vector?"
@@ -190,31 +190,31 @@
"vector-length" "vector-length"
:params ((v :as vector)) :params ((v :as vector))
:returns "number" :returns "number"
:doc "Number of elements in vector v.") :doc "Number of elements.")
(define-primitive (define-primitive
"vector-ref" "vector-ref"
:params ((v :as vector) (i :as number)) :params ((v :as vector) (i :as number))
:returns "any" :returns "any"
:doc "Element at 0-based index i. Error if out of bounds.") :doc "Element at index.")
(define-primitive (define-primitive
"vector-set!" "vector-set!"
:params ((v :as vector) (i :as number) (val :as any)) :params ((v :as vector) (i :as number) (val :as any))
:returns "nil" :returns "nil"
:doc "Mutate element at index i to val. Error if out of bounds.") :doc "Set element at index.")
(define-primitive (define-primitive
"vector->list" "vector->list"
:params ((v :as vector)) :params ((v :as vector))
:returns "list" :returns "list"
:doc "Convert vector to a fresh list.") :doc "Convert vector to list.")
(define-primitive (define-primitive
"list->vector" "list->vector"
:params ((l :as list)) :params ((l :as list))
:returns "vector" :returns "vector"
:doc "Convert list to a fresh vector.") :doc "Convert list to vector.")
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Core — Predicates ;; Core — Predicates
@@ -223,15 +223,13 @@
"vector-fill!" "vector-fill!"
:params ((v :as vector) (val :as any)) :params ((v :as vector) (val :as any))
:returns "nil" :returns "nil"
:doc "Set every element of v to val in place.") :doc "Fill all elements.")
(define-primitive (define-primitive
"vector-copy" "vector-copy"
:params ((v :as vector) :params ((v :as vector))
(start :as number :optional true)
(end :as number :optional true))
:returns "vector" :returns "vector"
:doc "Shallow copy of vector, optionally sliced from start (inclusive) to end (exclusive).") :doc "Independent shallow copy.")
(define-primitive (define-primitive
"min" "min"
@@ -374,20 +372,8 @@
"number?" "number?"
:params (x) :params (x)
:returns "boolean" :returns "boolean"
:doc "True if x is any number — exact integer or inexact float." :doc "True if x is a number (int or float)."
:body (or (= (type-of x) "number") (integer? x))) :body (= (type-of x) "number"))
(define-primitive
"integer?"
:params (x)
:returns "boolean"
:doc "True if x is an exact integer, or a float with no fractional part (e.g. 1.0).")
(define-primitive
"float?"
:params (x)
:returns "boolean"
:doc "True if x is an inexact float (Number type). Does not match exact integers.")
(define-primitive (define-primitive
"string?" "string?"
@@ -797,55 +783,3 @@
:params ((source :as string)) :params ((source :as string))
:returns "list" :returns "list"
:doc "Parse SX source string into a list of AST expressions.") :doc "Parse SX source string into a list of AST expressions.")
(define-primitive
"make-string-buffer"
:params ()
:returns "string-buffer"
:doc "Create a new empty mutable string buffer for O(1) amortised append.")
(define-module :stdlib.coroutines)
(define-module :stdlib.bitwise)
(define-primitive
"bitwise-and"
:params (((a :as number) (b :as number)))
:returns "number"
:doc "Bitwise AND of two integers.")
(define-primitive
"bitwise-or"
:params (((a :as number) (b :as number)))
:returns "number"
:doc "Bitwise OR of two integers.")
(define-primitive
"bitwise-xor"
:params (((a :as number) (b :as number)))
:returns "number"
:doc "Bitwise XOR of two integers.")
(define-primitive
"bitwise-not"
:params ((a :as number))
:returns "number"
:doc "Bitwise NOT (one's complement) of an integer.")
(define-primitive
"arithmetic-shift"
:params (((a :as number) (count :as number)))
:returns "number"
:doc "Arithmetic shift: left if count > 0, right if count < 0.")
(define-primitive
"bit-count"
:params ((a :as number))
:returns "number"
:doc "Count set bits (popcount) in a non-negative integer.")
(define-primitive
"integer-length"
:params ((a :as number))
:returns "number"
:doc "Number of bits needed to represent integer a (excluding sign).")

View File

@@ -1,278 +0,0 @@
(defsuite
"algebraic-data-types"
(deftest
"constructor creates dict with adt marker"
(do
(define-type Maybe (Just value) (Nothing))
(assert= true (get (Just 42) :_adt))))
(deftest
"constructor stores type name"
(do
(define-type Shape (Circle radius) (Square side))
(assert= "Shape" (get (Circle 5) :_type))
(assert= "Shape" (get (Square 3) :_type))))
(deftest
"constructor stores constructor name"
(do
(define-type Opt (Some val) (None))
(assert= "Some" (get (Some 1) :_ctor))
(assert= "None" (get (None) :_ctor))))
(deftest
"constructor stores fields as list"
(do
(define-type Pair (Pair-of fst snd))
(assert-equal
(list 1 2)
(get (Pair-of 1 2) :_fields))))
(deftest
"zero-arg constructor has empty fields"
(do
(define-type Flag (Set) (Unset))
(assert-equal (list) (get (Set) :_fields))
(assert-equal (list) (get (Unset) :_fields))))
(deftest
"type predicate true for all constructors"
(do
(define-type Expr (Num n) (Add left right) (Neg e))
(assert= true (Expr? (Num 5)))
(assert= true (Expr? (Add (Num 1) (Num 2))))
(assert= true (Expr? (Neg (Num 3))))))
(deftest
"type predicate false for non-adt values"
(do
(define-type Box (Box-of x))
(assert= false (Box? 42))
(assert= false (Box? "hello"))
(assert= false (Box? nil))
(assert= false (Box? (list 1 2)))
(assert= false (Box? {}))))
(deftest
"type predicate false for wrong adt type"
(do
(define-type AT (AV x))
(define-type BT (BV x))
(assert= false (AT? (BV 1)))
(assert= false (BT? (AV 1)))))
(deftest
"constructor predicate true for matching constructor"
(do
(define-type Result (Ok value) (Err msg))
(assert= true (Ok? (Ok 42)))
(assert= true (Err? (Err "bad")))))
(deftest
"constructor predicate false for wrong constructor"
(do
(define-type Coin (Heads) (Tails))
(assert= false (Heads? (Tails)))
(assert= false (Tails? (Heads)))))
(deftest
"constructor predicate false for non-adt"
(do
(define-type Wrap (Wrapped x))
(assert= false (Wrapped? 42))
(assert= false (Wrapped? nil))
(assert= false (Wrapped? "str"))))
(deftest
"single-field accessor returns field value"
(do
(define-type Holder (Held content))
(assert= 99 (Held-content (Held 99)))
(assert= "hello" (Held-content (Held "hello")))))
(deftest
"multi-field accessors return correct fields"
(do
(define-type Triple (Triple-of a b c))
(let
((t (Triple-of 10 20 30)))
(assert= 10 (Triple-of-a t))
(assert= 20 (Triple-of-b t))
(assert= 30 (Triple-of-c t)))))
(deftest
"tree constructors and accessors"
(do
(define-type Tree (Leaf) (Node left val right))
(let
((t (Node (Leaf) 5 (Node (Leaf) 3 (Leaf)))))
(assert= true (Node? t))
(assert= 5 (Node-val t))
(assert= true (Leaf? (Node-left t)))
(assert= true (Node? (Node-right t)))
(assert= 3 (Node-val (Node-right t))))))
(deftest
"arity error on too few args"
(do
(define-type Pair2 (Pair2-of a b))
(let
((ok false))
(guard (exn (else (set! ok true))) (Pair2-of 1))
(assert ok))))
(deftest
"arity error on too many args"
(do
(define-type Single (Single-of x))
(let
((ok false))
(guard
(exn (else (set! ok true)))
(Single-of 1 2))
(assert ok))))
(deftest
"multiple types are independent"
(do
(define-type Color2 (Red2) (Green2) (Blue2))
(define-type Suit (Hearts) (Diamonds) (Clubs) (Spades))
(assert= false (Color2? (Hearts)))
(assert= false (Suit? (Red2)))
(assert= true (Color2? (Blue2)))
(assert= true (Suit? (Spades)))))
(deftest
"adt fields can hold any value"
(do
(define-type Container (Hold x))
(assert-equal
(list 1 2 3)
(Hold-x (Hold (list 1 2 3))))
(assert-equal {:a 1} (Hold-x (Hold {:a 1})))))
(deftest
"adt-registry tracks type constructor names"
(do
(define-type Days (Mon) (Tue) (Wed) (Thu) (Fri))
(assert-equal
(list "Mon" "Tue" "Wed" "Thu" "Fri")
(get *adt-registry* "Days"))))
(deftest
"constructors with same field name in different types are independent"
(do
(define-type P1 (P1-ctor value))
(define-type P2 (P2-ctor value))
(assert= 10 (P1-ctor-value (P1-ctor 10)))
(assert= 20 (P2-ctor-value (P2-ctor 20)))))
(deftest
"match dispatches on first matching constructor"
(do
(define-type Color (Red) (Green) (Blue))
(assert= "red" (match (Red) ((Red) "red") ((Green) "green") ((Blue) "blue")))
(assert= "green" (match (Green) ((Red) "red") ((Green) "green") ((Blue) "blue")))
(assert= "blue" (match (Blue) ((Red) "red") ((Green) "green") ((Blue) "blue")))))
(deftest
"match binds field to variable"
(do
(define-type Wrapper (Wrap val))
(assert= 42 (match (Wrap 42) ((Wrap v) v)))
(assert= "hi" (match (Wrap "hi") ((Wrap v) v)))))
(deftest
"match zero-arg constructor"
(do
(define-type Signal (On) (Off))
(assert= "on" (match (On) ((On) "on") ((Off) "off")))
(assert= "off" (match (Off) ((On) "on") ((Off) "off")))))
(deftest
"match multi-field constructor binds all fields"
(do
(define-type Vec2 (V2 x y))
(let ((v (V2 3 4)))
(assert= 7 (match v ((V2 a b) (+ a b)))))))
(deftest
"match with else clause"
(do
(define-type Opt2 (Some2 val) (None2))
(assert= 10 (match (Some2 10) ((Some2 v) v) (else 0)))
(assert= 0 (match (None2) ((Some2 v) v) (else 0)))))
(deftest
"match else catches non-adt values"
(do
(assert= "other" (match 42 ((else) "other") (else "other")))
(assert= "other" (match "str" (else "other")))))
(deftest
"match returns body expression value"
(do
(define-type Num (Num-of n))
(assert= 100 (match (Num-of 10) ((Num-of n) (* n n))))))
(deftest
"match second arm fires when first does not match"
(do
(define-type Either (Left val) (Right val))
(assert= "left-1" (match (Left 1) ((Left v) (str "left-" v)) ((Right v) (str "right-" v))))
(assert= "right-2" (match (Right 2) ((Left v) (str "left-" v)) ((Right v) (str "right-" v))))))
(deftest
"match wildcard _ in constructor pattern"
(do
(define-type Pair3 (Pair3-of a b))
(assert= 5 (match (Pair3-of 5 99) ((Pair3-of x _) x)))
(assert= 99 (match (Pair3-of 5 99) ((Pair3-of _ y) y)))))
(deftest
"match nested adt constructor pattern"
(do
(define-type Tree2 (Leaf2) (Node2 left val right))
(let ((t (Node2 (Leaf2) 7 (Leaf2))))
(assert= 7 (match t ((Node2 _ v _) v)))
(assert= true (match t ((Node2 (Leaf2) _ _) true) (else false))))))
(deftest
"match literal pattern"
(do
(assert= "zero" (match 0 (0 "zero") (else "nonzero")))
(assert= "hello" (match "hello" ("hello" "hello") (else "other")))))
(deftest
"match symbol binding pattern"
(do
(assert= 42 (match 42 (x x)))))
(deftest
"match no matching clause raises error"
(do
(define-type AB (A-val) (B-val))
(let ((ok false))
(guard (exn (else (set! ok true)))
(match (A-val) ((B-val) "b")))
(assert ok))))
(deftest
"match result used in further computation"
(do
(define-type Num2 (N v))
(assert= 30
(+
(match (N 10) ((N v) v))
(match (N 20) ((N v) v))))))
(deftest
"match with define"
(do
(define-type Tag (Tagged label value))
(define get-label (fn (t) (match t ((Tagged lbl _) lbl))))
(define get-value (fn (t) (match t ((Tagged _ val) val))))
(let ((t (Tagged "name" 99)))
(assert= "name" (get-label t))
(assert= 99 (get-value t)))))
(deftest
"match three-field constructor"
(do
(define-type Triple2 (T3 a b c))
(assert= 6 (match (T3 1 2 3) ((T3 a b c) (+ a b c))))))
(deftest
"match clauses tried in order"
(do
(define-type Expr2 (Lit n) (Add l r) (Mul l r))
(define eval-expr2 (fn (e)
(match e
((Lit n) n)
((Add l r) (+ (eval-expr2 l) (eval-expr2 r)))
((Mul l r) (* (eval-expr2 l) (eval-expr2 r))))))
(assert= 7 (eval-expr2 (Add (Lit 3) (Lit 4))))
(assert= 12 (eval-expr2 (Mul (Lit 3) (Lit 4))))
(assert= 11 (eval-expr2 (Add (Lit 2) (Mul (Lit 3) (Lit 3)))))))
(deftest
"match else binding captures value"
(do
(define-type Coin2 (Heads2) (Tails2))
(assert= "Tails2" (match (Tails2) ((Heads2) "Heads2") (x (get x :_ctor))))))
(deftest
"match on adt with string field"
(do
(define-type Msg (Hello name) (Bye name))
(assert= "Hello, Alice" (match (Hello "Alice") ((Hello n) (str "Hello, " n)) ((Bye n) (str "Bye, " n))))
(assert= "Bye, Bob" (match (Bye "Bob") ((Hello n) (str "Hello, " n)) ((Bye n) (str "Bye, " n))))))
(deftest
"match nested pattern with variable binding"
(do
(define-type Box2 (Box2-of v))
(define-type Inner (Inner-of n))
(assert= 5 (match (Box2-of (Inner-of 5)) ((Box2-of (Inner-of n)) n)))))
)

View File

@@ -1,157 +0,0 @@
(defsuite
"bitwise-operations"
(deftest
"bitwise-and basic"
(do
(assert= 0 (bitwise-and 0 0))
(assert= 1 (bitwise-and 3 1))
(assert= 0 (bitwise-and 5 2))
(assert= 4 (bitwise-and 12 6))))
(deftest
"bitwise-and identity and zero"
(do
(assert= 255 (bitwise-and 255 255))
(assert= 0 (bitwise-and 255 0))))
(deftest
"bitwise-or basic"
(do
(assert= 0 (bitwise-or 0 0))
(assert= 3 (bitwise-or 1 2))
(assert= 7 (bitwise-or 5 3))
(assert= 15 (bitwise-or 9 6))))
(deftest
"bitwise-or identity"
(do
(assert= 255 (bitwise-or 255 0))
(assert= 255 (bitwise-or 0 255))))
(deftest
"bitwise-xor basic"
(do
(assert= 0 (bitwise-xor 0 0))
(assert= 3 (bitwise-xor 1 2))
(assert= 6 (bitwise-xor 3 5))
(assert= 0 (bitwise-xor 255 255))))
(deftest
"bitwise-xor toggle bits"
(do
(assert= 14 (bitwise-xor 10 4))
(assert= 10 (bitwise-xor 14 4))))
(deftest
"bitwise-not zero"
(do (assert= -1 (bitwise-not 0))))
(deftest
"bitwise-not positive"
(do
(assert= -2 (bitwise-not 1))
(assert= -5 (bitwise-not 4))
(assert= -256 (bitwise-not 255))))
(deftest
"bitwise-not negative"
(do
(assert= 0 (bitwise-not -1))
(assert= 1 (bitwise-not -2))
(assert= 4 (bitwise-not -5))))
(deftest
"bitwise-not double negation"
(do
(assert= 42 (bitwise-not (bitwise-not 42)))
(assert= 0 (bitwise-not (bitwise-not 0)))))
(deftest
"arithmetic-shift left"
(do
(assert= 2 (arithmetic-shift 1 1))
(assert= 4 (arithmetic-shift 1 2))
(assert= 16 (arithmetic-shift 1 4))
(assert= 8 (arithmetic-shift 2 2))))
(deftest
"arithmetic-shift right"
(do
(assert= 1 (arithmetic-shift 2 -1))
(assert= 1 (arithmetic-shift 4 -2))
(assert= 5 (arithmetic-shift 10 -1))
(assert= 2 (arithmetic-shift 16 -3))))
(deftest
"arithmetic-shift by zero"
(do
(assert= 42 (arithmetic-shift 42 0))
(assert= 0 (arithmetic-shift 0 5))))
(deftest
"arithmetic-shift negative value right preserves sign"
(do
(assert= -1 (arithmetic-shift -1 -1))
(assert= -2 (arithmetic-shift -4 -1))))
(deftest
"bit-count zero"
(do (assert= 0 (bit-count 0))))
(deftest
"bit-count powers of two"
(do
(assert= 1 (bit-count 1))
(assert= 1 (bit-count 2))
(assert= 1 (bit-count 4))
(assert= 1 (bit-count 128))))
(deftest
"bit-count all-ones values"
(do
(assert= 8 (bit-count 255))
(assert= 4 (bit-count 15))
(assert= 2 (bit-count 3))))
(deftest
"bit-count mixed"
(do
(assert= 3 (bit-count 7))
(assert= 2 (bit-count 5))
(assert= 3 (bit-count 11))
(assert= 4 (bit-count 30))))
(deftest
"integer-length zero"
(do (assert= 0 (integer-length 0))))
(deftest
"integer-length powers of two"
(do
(assert= 1 (integer-length 1))
(assert= 2 (integer-length 2))
(assert= 3 (integer-length 4))
(assert= 4 (integer-length 8))
(assert= 8 (integer-length 128))))
(deftest
"integer-length non-powers"
(do
(assert= 2 (integer-length 3))
(assert= 3 (integer-length 5))
(assert= 3 (integer-length 7))
(assert= 8 (integer-length 255))
(assert= 9 (integer-length 256))))
(deftest
"bitwise ops compose"
(do
(assert=
5
(bitwise-and
(bitwise-or 5 3)
(bitwise-xor 7 2)))
(assert= 0 (bitwise-and 170 85))))
(deftest
"arithmetic-shift round-trip"
(do
(assert=
10
(arithmetic-shift (arithmetic-shift 10 3) -3))))
(deftest
"extract bits with mask"
(do
(let
((x 52))
(assert=
5
(bitwise-and (arithmetic-shift x -2) 7)))))
(deftest
"clear low bits with bitwise-not mask"
(do
(assert= 252 (bitwise-and 255 (bitwise-not 3)))))
(deftest
"integer-length after shift"
(do
(assert=
4
(integer-length (arithmetic-shift 1 3))))))

View File

@@ -1,305 +0,0 @@
(import (sx coroutines))
(defsuite
"coroutine"
(deftest
"coroutine? recognizes coroutine objects"
(let
((co (make-coroutine (fn () nil))))
(assert (coroutine? co))
(assert= false (coroutine? 42))
(assert= false (coroutine? "hello"))
(assert= false (coroutine? nil))
(assert= false (coroutine? (list)))))
(deftest
"coroutine-alive? true for ready coroutine"
(let
((co (make-coroutine (fn () nil))))
(assert (coroutine-alive? co))))
(deftest
"coroutine-alive? false for non-coroutine"
(assert= false (coroutine-alive? 42)))
(deftest
"immediate return — done true, value is body result"
(let
((co (make-coroutine (fn () 42))))
(let
((r (coroutine-resume co nil)))
(assert= true (get r "done"))
(assert= 42 (get r "value")))))
(deftest
"immediate nil return"
(let
((co (make-coroutine (fn () nil))))
(let
((r (coroutine-resume co nil)))
(assert= true (get r "done"))
(assert= nil (get r "value")))))
(deftest
"coroutine-alive? false after completion"
(let
((co (make-coroutine (fn () nil))))
(coroutine-resume co nil)
(assert= false (coroutine-alive? co))))
(deftest
"single yield — done false on yield, done true on finish"
(let
((co (make-coroutine (fn () (coroutine-yield 10) 20))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(assert= false (get r1 "done"))
(assert= 10 (get r1 "value"))
(assert= true (get r2 "done"))
(assert= 20 (get r2 "value"))))))
(deftest
"coroutine-alive? true between yield and next resume"
(let
((co (make-coroutine (fn () (coroutine-yield nil) nil))))
(assert (coroutine-alive? co))
(coroutine-resume co nil)
(assert (coroutine-alive? co))
(coroutine-resume co nil)
(assert= false (coroutine-alive? co))))
(deftest
"three yields then return"
(let
((co (make-coroutine (fn () (coroutine-yield "a") (coroutine-yield "b") (coroutine-yield "c") "z"))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(let
((r3 (coroutine-resume co nil)))
(let
((r4 (coroutine-resume co nil)))
(assert= "a" (get r1 "value"))
(assert= false (get r1 "done"))
(assert= "b" (get r2 "value"))
(assert= false (get r2 "done"))
(assert= "c" (get r3 "value"))
(assert= false (get r3 "done"))
(assert= "z" (get r4 "value"))
(assert= true (get r4 "done"))))))))
(deftest
"final return vs yield — done flag distinguishes them"
(let
((co (make-coroutine (fn () (coroutine-yield "yielded") "returned"))))
(let
((y (coroutine-resume co nil)))
(let
((r (coroutine-resume co nil)))
(assert= false (get y "done"))
(assert= "yielded" (get y "value"))
(assert= true (get r "done"))
(assert= "returned" (get r "value"))))))
(deftest
"resume val becomes yield return value"
(let
((co (make-coroutine (fn () (let ((received (coroutine-yield "first"))) received)))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co 99)))
(assert= "first" (get r1 "value"))
(assert= false (get r1 "done"))
(assert= 99 (get r2 "value"))
(assert= true (get r2 "done"))))))
(deftest
"multiple resume values passed through yields"
(let
((co (make-coroutine (fn () (let ((a (coroutine-yield 1))) (let ((b (coroutine-yield 2))) (+ a b)))))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co 10)))
(let
((r3 (coroutine-resume co 20)))
(assert= 1 (get r1 "value"))
(assert= 2 (get r2 "value"))
(assert= true (get r3 "done"))
(assert= 30 (get r3 "value")))))))
(deftest
"coroutine captures lexical environment"
(let
((x 10)
(co
(make-coroutine
(fn () (coroutine-yield (* x 2)) (* x 3)))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(assert= 20 (get r1 "value"))
(assert= 30 (get r2 "value"))))))
(deftest
"resuming dead coroutine raises error"
(let
((co (make-coroutine (fn () nil))))
(coroutine-resume co nil)
(assert-throws (fn () (coroutine-resume co nil)))))
(deftest
"coroutine drives iteration via recursive body"
(let
((co (make-coroutine (fn () (define loop (fn (i) (when (< i 4) (coroutine-yield i) (loop (+ i 1))))) (loop 0))))
(results (list)))
(let
drive
()
(let
((r (coroutine-resume co nil)))
(when
(not (get r "done"))
(append! results (get r "value"))
(drive))))
(assert= 4 (len results))
(assert= 0 (nth results 0))
(assert= 1 (nth results 1))
(assert= 2 (nth results 2))
(assert= 3 (nth results 3))))
(deftest
"nested coroutine — inner resumed from outer body"
(let
((inner (make-coroutine (fn () (coroutine-yield "inner-a") "inner-done")))
(outer
(make-coroutine
(fn
()
(let
((i1 (coroutine-resume inner nil)))
(coroutine-yield (get i1 "value")))
(let ((i2 (coroutine-resume inner nil))) (get i2 "value"))))))
(let
((o1 (coroutine-resume outer nil)))
(let
((o2 (coroutine-resume outer nil)))
(assert= false (get o1 "done"))
(assert= "inner-a" (get o1 "value"))
(assert= true (get o2 "done"))
(assert= "inner-done" (get o2 "value"))))))
(deftest
"two independent coroutines interleave correctly"
(let
((co1 (make-coroutine (fn () (coroutine-yield 1) 5)))
(co2
(make-coroutine (fn () (coroutine-yield 2) 6))))
(let
((a (coroutine-resume co1 nil)))
(let
((b (coroutine-resume co2 nil)))
(let
((c (coroutine-resume co1 nil)))
(let
((d (coroutine-resume co2 nil)))
(assert= false (get a "done"))
(assert= 1 (get a "value"))
(assert= false (get b "done"))
(assert= 2 (get b "value"))
(assert= true (get c "done"))
(assert= 5 (get c "value"))
(assert= true (get d "done"))
(assert= 6 (get d "value"))))))))
(deftest
"coroutine state field is ready before first resume"
(let
((co (make-coroutine (fn () (coroutine-yield 1)))))
(assert= "ready" (get co "state"))))
(deftest
"coroutine state field is suspended between yields"
(let
((co (make-coroutine (fn () (coroutine-yield 1) 2))))
(coroutine-resume co nil)
(assert= "suspended" (get co "state"))))
(deftest
"coroutine state field is dead after completion"
(let
((co (make-coroutine (fn () nil))))
(coroutine-resume co nil)
(assert= "dead" (get co "state"))))
(deftest
"yield works when called from nested helper function"
(let
((co (make-coroutine (fn () (define helper (fn (x) (coroutine-yield x))) (helper 10) (helper 20)))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(let
((r3 (coroutine-resume co nil)))
(assert= false (get r1 "done"))
(assert= 10 (get r1 "value"))
(assert= false (get r2 "done"))
(assert= 20 (get r2 "value"))
(assert= true (get r3 "done")))))))
(deftest
"initial resume argument is ignored by ready coroutine"
(let
((co (make-coroutine (fn () (coroutine-yield 42)))))
(let
((r (coroutine-resume co "ignored")))
(assert= false (get r "done"))
(assert= 42 (get r "value")))))
(deftest
"coroutine with mutable closure state"
(let
((counter {:value 0}))
(let
((co (make-coroutine (fn () (dict-set! counter "value" 1) (coroutine-yield "a") (dict-set! counter "value" 2) (coroutine-yield "b")))))
(assert= 0 (get counter "value"))
(coroutine-resume co nil)
(assert= 1 (get counter "value"))
(coroutine-resume co nil)
(assert= 2 (get counter "value")))))
(deftest
"coroutine can yield complex values"
(let
((co (make-coroutine (fn () (coroutine-yield (list 1 2 3)) (coroutine-yield {:key "val"})))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(assert= false (get r1 "done"))
(assert= 3 (len (get r1 "value")))
(assert= false (get r2 "done"))
(assert= "val" (get (get r2 "value") "key"))))))
(deftest
"round-robin scheduling of multiple coroutines"
(let
((results (list))
(co1
(make-coroutine
(fn () (coroutine-yield "a") (coroutine-yield "b"))))
(co2
(make-coroutine
(fn () (coroutine-yield "c") (coroutine-yield "d")))))
(append! results (get (coroutine-resume co1 nil) "value"))
(append! results (get (coroutine-resume co2 nil) "value"))
(append! results (get (coroutine-resume co1 nil) "value"))
(append! results (get (coroutine-resume co2 nil) "value"))
(assert= 4 (len results))
(assert= "a" (nth results 0))
(assert= "c" (nth results 1))
(assert= "b" (nth results 2))
(assert= "d" (nth results 3))))
(deftest
"coroutines created from same factory share no state"
(let
((make-counter (fn (start) (make-coroutine (fn () (define loop (fn (n) (coroutine-yield n) (loop (+ n 1)))) (loop start))))))
(let
((c1 (make-counter 0)) (c2 (make-counter 100)))
(let
((a (get (coroutine-resume c1 nil) "value")))
(let
((b (get (coroutine-resume c2 nil) "value")))
(let
((c (get (coroutine-resume c1 nil) "value")))
(let
((d (get (coroutine-resume c2 nil) "value")))
(assert= 0 a)
(assert= 100 b)
(assert= 1 c)
(assert= 101 d))))))))
(deftest
"resuming non-coroutine raises error"
(assert-throws (fn () (coroutine-resume "not-a-coroutine" nil)))))

View File

@@ -1,113 +0,0 @@
;; Tests for dynamic-wind: after-thunk fires on normal return,
;; non-local exit via raise/guard, and call/cc escape.
(defsuite
"dynamic-wind-basic"
(deftest
"after fires on normal return"
(let
((log (list)))
(dynamic-wind
(fn () (append! log "before"))
(fn () (append! log "body"))
(fn () (append! log "after")))
(assert= 3 (len log))
(assert= "before" (nth log 0))
(assert= "body" (nth log 1))
(assert= "after" (nth log 2))))
(deftest
"after fires on raise escape"
(let
((log (list)))
(guard
(e (true nil))
(dynamic-wind
(fn () (append! log "before"))
(fn () (append! log "body") (error "boom"))
(fn () (append! log "after"))))
(assert= 3 (len log))
(assert= "before" (nth log 0))
(assert= "body" (nth log 1))
(assert= "after" (nth log 2))))
(deftest
"after fires on call/cc escape"
(let
((log (list)))
(call/cc
(fn
(k)
(dynamic-wind
(fn () (append! log "before"))
(fn () (append! log "body") (k nil))
(fn () (append! log "after")))))
(assert= 3 (len log))
(assert= "before" (nth log 0))
(assert= "body" (nth log 1))
(assert= "after" (nth log 2))))
(deftest
"nested dynamic-wind after-thunks fire LIFO on normal return"
(let
((log (list)))
(dynamic-wind
(fn () (append! log "outer-before"))
(fn
()
(dynamic-wind
(fn () (append! log "inner-before"))
(fn () (append! log "inner-body"))
(fn () (append! log "inner-after"))))
(fn () (append! log "outer-after")))
(assert= 5 (len log))
(assert= "outer-before" (nth log 0))
(assert= "inner-before" (nth log 1))
(assert= "inner-body" (nth log 2))
(assert= "inner-after" (nth log 3))
(assert= "outer-after" (nth log 4))))
(deftest
"nested dynamic-wind after-thunks fire LIFO on raise"
(let
((log (list)))
(guard
(e (true nil))
(dynamic-wind
(fn () (append! log "outer-before"))
(fn
()
(dynamic-wind
(fn () (append! log "inner-before"))
(fn () (append! log "inner-body") (error "boom"))
(fn () (append! log "inner-after"))))
(fn () (append! log "outer-after"))))
(assert= 5 (len log))
(assert= "outer-before" (nth log 0))
(assert= "inner-before" (nth log 1))
(assert= "inner-body" (nth log 2))
(assert= "inner-after" (nth log 3))
(assert= "outer-after" (nth log 4))))
(deftest
"before and after are called"
(let
((count 0))
(dynamic-wind
(fn () (set! count (+ count 1)))
(fn () nil)
(fn () (set! count (+ count 10))))
(assert= 11 count)))
(deftest
"dynamic-wind return value is body result"
(let
((result (dynamic-wind (fn () nil) (fn () 42) (fn () nil))))
(assert= 42 result)))
(deftest
"after fires before guard handler"
(let
((log (list)))
(guard
(e (true (append! log "guard-handler")))
(dynamic-wind
(fn () nil)
(fn () (error "boom"))
(fn () (append! log "after"))))
(assert= 2 (len log))
(assert= "after" (nth log 0))
(assert= "guard-handler" (nth log 1)))))

View File

@@ -88,27 +88,6 @@
(raise _e)))) (raise _e))))
(handler me-val)))))) (handler me-val))))))
;; Evaluate a hyperscript expression, catch the first error raised, and
;; return its message string. Used by runtimeErrors tests.
;; Returns nil if no error is raised (test would then fail equality).
(define eval-hs-error
(fn (src)
(let ((sx (hs-to-sx (hs-compile src))))
(let ((handler (eval-expr-cek
(list (quote fn) (list (quote me))
(list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))
(guard
(_e
(true
(if
(string? _e)
_e
(if
(and (list? _e) (= (first _e) "hs-return"))
nil
(str _e)))))
(begin (handler nil) nil))))))
;; ── add (19 tests) ── ;; ── add (19 tests) ──
(defsuite "hs-upstream-add" (defsuite "hs-upstream-add"
(deftest "can add a value to a set" (deftest "can add a value to a set"
@@ -2174,75 +2153,41 @@
;; ── core/runtimeErrors (18 tests) ── ;; ── core/runtimeErrors (18 tests) ──
(defsuite "hs-upstream-core/runtimeErrors" (defsuite "hs-upstream-core/runtimeErrors"
(deftest "reports basic function invocation null errors properly" (deftest "reports basic function invocation null errors properly"
(assert= (eval-hs-error "x()") "'x' is null") (error "SKIP (untranslated): reports basic function invocation null errors properly"))
(assert= (eval-hs-error "x.y()") "'x' is null")
(assert= (eval-hs-error "x.y.z()") "'x.y' is null")
)
(deftest "reports basic function invocation null errors properly w/ of" (deftest "reports basic function invocation null errors properly w/ of"
(assert= (eval-hs-error "z() of y of x") "'z' is null") (error "SKIP (untranslated): reports basic function invocation null errors properly w/ of"))
)
(deftest "reports basic function invocation null errors properly w/ possessives" (deftest "reports basic function invocation null errors properly w/ possessives"
(assert= (eval-hs-error "x's y()") "'x' is null") (error "SKIP (untranslated): reports basic function invocation null errors properly w/ possessives"))
(assert= (eval-hs-error "x's y's z()") "'x's y' is null")
)
(deftest "reports null errors on add command properly" (deftest "reports null errors on add command properly"
(assert= (eval-hs-error "add .foo to #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on add command properly"))
(assert= (eval-hs-error "add @foo to #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "add {display:none} to #doesntExist") "'#doesntExist' is null")
)
(deftest "reports null errors on decrement command properly" (deftest "reports null errors on decrement command properly"
(assert= (eval-hs-error "decrement #doesntExist's innerHTML") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on decrement command properly"))
)
(deftest "reports null errors on default command properly" (deftest "reports null errors on default command properly"
(assert= (eval-hs-error "default #doesntExist's innerHTML to 'foo'") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on default command properly"))
)
(deftest "reports null errors on hide command properly" (deftest "reports null errors on hide command properly"
(assert= (eval-hs-error "hide #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on hide command properly"))
)
(deftest "reports null errors on increment command properly" (deftest "reports null errors on increment command properly"
(assert= (eval-hs-error "increment #doesntExist's innerHTML") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on increment command properly"))
)
(deftest "reports null errors on measure command properly" (deftest "reports null errors on measure command properly"
(assert= (eval-hs-error "measure #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on measure command properly"))
)
(deftest "reports null errors on put command properly" (deftest "reports null errors on put command properly"
(assert= (eval-hs-error "put 'foo' into #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on put command properly"))
(assert= (eval-hs-error "put 'foo' into #doesntExist's innerHTML") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' into #doesntExist.innerHTML") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' before #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' after #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' at the start of #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' at the end of #doesntExist") "'#doesntExist' is null")
)
(deftest "reports null errors on remove command properly" (deftest "reports null errors on remove command properly"
(assert= (eval-hs-error "remove .foo from #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on remove command properly"))
(assert= (eval-hs-error "remove @foo from #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "remove #doesntExist from #doesntExist") "'#doesntExist' is null")
)
(deftest "reports null errors on send command properly" (deftest "reports null errors on send command properly"
(assert= (eval-hs-error "send 'foo' to #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on send command properly"))
)
(deftest "reports null errors on sets properly" (deftest "reports null errors on sets properly"
(assert= (eval-hs-error "set x's y to true") "'x' is null") (error "SKIP (untranslated): reports null errors on sets properly"))
(assert= (eval-hs-error "set x's @y to true") "'x' is null")
)
(deftest "reports null errors on settle command properly" (deftest "reports null errors on settle command properly"
(assert= (eval-hs-error "settle #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on settle command properly"))
)
(deftest "reports null errors on show command properly" (deftest "reports null errors on show command properly"
(assert= (eval-hs-error "show #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on show command properly"))
)
(deftest "reports null errors on toggle command properly" (deftest "reports null errors on toggle command properly"
(assert= (eval-hs-error "toggle .foo on #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on toggle command properly"))
(assert= (eval-hs-error "toggle between .foo and .bar on #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "toggle @foo on #doesntExist") "'#doesntExist' is null")
)
(deftest "reports null errors on transition command properly" (deftest "reports null errors on transition command properly"
(assert= (eval-hs-error "transition #doesntExist's *visibility to 0") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on transition command properly"))
)
(deftest "reports null errors on trigger command properly" (deftest "reports null errors on trigger command properly"
(assert= (eval-hs-error "trigger 'foo' on #doesntExist") "'#doesntExist' is null") (error "SKIP (untranslated): reports null errors on trigger command properly"))
)
) )
;; ── core/scoping (20 tests) ── ;; ── core/scoping (20 tests) ──

View File

@@ -1,221 +0,0 @@
;; ==========================================================================
;; test-numeric-tower.sx — Numeric tower: Integer vs Float distinction
;;
;; Tests for float contagion, integer arithmetic, predicates,
;; coercions, parsing, and rendering.
;;
;; Note: Use fractional floats (1.5, 3.14) or exact->inexact for round floats,
;; since the SX serializer renders Number 1.0 as "1" (int form).
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Integer arithmetic — result stays Integer when all args are Integer
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:int-arithmetic"
(deftest "int + int = int" (assert (integer? (+ 1 2))))
(deftest "int + int value" (assert= (+ 1 2) 3))
(deftest "int - int = int" (assert (integer? (- 10 3))))
(deftest "int - int value" (assert= (- 10 3) 7))
(deftest "int * int = int" (assert (integer? (* 4 5))))
(deftest "int * int value" (assert= (* 4 5) 20))
(deftest "zero identity" (assert= (+ 0 0) 0))
(deftest "negative int" (assert= (- 0 5) -5))
(deftest
"int negation is int"
(assert (integer? (- 0 7))))
(deftest
"large int product"
(assert= (* 100 100) 10000)))
;; --------------------------------------------------------------------------
;; Float contagion — any float arg promotes result to float
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:float-contagion"
(deftest "int + float = float" (assert (float? (+ 1 1.5))))
(deftest "int + float value" (assert= (+ 1 1.5) 2.5))
(deftest "float + int = float" (assert (float? (+ 1.5 2))))
(deftest "float + float = float" (assert (float? (+ 1.5 2.5))))
(deftest "int * float = float" (assert (float? (* 2 1.5))))
(deftest "int * float value" (assert= (* 2 1.5) 3))
(deftest "int - float = float" (assert (float? (- 5 2.5))))
(deftest "float - int = float" (assert (float? (- 5.5 2))))
(deftest
"three args with float"
(assert (float? (+ 1 2 3.5))))
(deftest
"exact->inexact promotes to float"
(assert (float? (exact->inexact 5)))))
;; --------------------------------------------------------------------------
;; Division always returns float
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:division"
(deftest "int / int = float" (assert (float? (/ 6 2))))
(deftest "exact division value" (assert= (/ 6 2) 3))
(deftest "inexact division" (assert= (/ 1 4) 0.25))
(deftest "float / float = float" (assert (float? (/ 3.5 2.5)))))
;; --------------------------------------------------------------------------
;; Type predicates
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:predicates"
(deftest "integer? on int" (assert (integer? 42)))
(deftest "integer? on negative" (assert (integer? -7)))
(deftest "integer? on zero" (assert (integer? 0)))
(deftest
"integer? on float-int"
(assert (integer? (exact->inexact 2))))
(deftest "integer? on fractional float" (assert (not (integer? 1.5))))
(deftest "float? on 1.5" (assert (float? 1.5)))
(deftest
"float? on exact->inexact"
(assert (float? (exact->inexact 2))))
(deftest "float? on int" (assert (not (float? 42))))
(deftest "number? on int" (assert (number? 42)))
(deftest "number? on float" (assert (number? 3.14)))
(deftest "number? on string" (assert (not (number? "42"))))
(deftest "exact? on int" (assert (exact? 1)))
(deftest
"exact? on exact->inexact"
(assert (not (exact? (exact->inexact 1)))))
(deftest "inexact? on 1.5" (assert (inexact? 1.5)))
(deftest "inexact? on int" (assert (not (inexact? 3)))))
;; --------------------------------------------------------------------------
;; Coercions
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:coercions"
(deftest "exact->inexact int" (assert= (exact->inexact 3) 3))
(deftest
"exact->inexact produces float"
(assert (float? (exact->inexact 5))))
(deftest
"exact->inexact float passthrough"
(assert= (exact->inexact 1.5) 1.5))
(deftest "inexact->exact 1.5" (assert= (inexact->exact 1.5) 2))
(deftest
"inexact->exact produces int"
(assert (integer? (inexact->exact (exact->inexact 4)))))
(deftest "inexact->exact 2.7" (assert= (inexact->exact 2.7) 3))
(deftest
"inexact->exact int passthrough"
(assert= (inexact->exact 5) 5)))
;; --------------------------------------------------------------------------
;; floor / ceiling / truncate / round — return Integer for floats
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:rounding"
(deftest "floor 3.7" (assert= (floor 3.7) 3))
(deftest "floor produces int" (assert (integer? (floor 3.7))))
(deftest "floor negative" (assert= (floor -2.3) -3))
(deftest "truncate 3.9" (assert= (truncate 3.9) 3))
(deftest "truncate negative" (assert= (truncate -3.9) -3))
(deftest "truncate produces int" (assert (integer? (truncate 3.9))))
(deftest "round 2.3 down" (assert= (round 2.3) 2))
(deftest "round produces int" (assert (integer? (round 2.3))))
(deftest
"floor of int passthrough"
(assert= (floor 5) 5))
(deftest "floor of int stays int" (assert (integer? (floor 5)))))
;; --------------------------------------------------------------------------
;; parse-number distinguishes int vs float strings
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:parse-number"
(deftest
"parse-number int string"
(assert= (parse-number "42") 42))
(deftest
"parse-number int is integer?"
(assert (integer? (parse-number "42"))))
(deftest "parse-number 3.14" (assert= (parse-number "3.14") 3.14))
(deftest
"parse-number float is float?"
(assert (float? (parse-number "3.14"))))
(deftest
"parse-number 1.5 is float?"
(assert (float? (parse-number "1.5"))))
(deftest
"parse-number negative int"
(assert= (parse-number "-5") -5))
(deftest
"parse-number negative int is integer?"
(assert (integer? (parse-number "-5"))))
(deftest "parse-int returns integer" (assert (integer? (parse-int "7"))))
(deftest "parse-int value" (assert= (parse-int "7") 7)))
;; --------------------------------------------------------------------------
;; Equality across numeric types
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:equality"
(deftest "int = same int" (assert= 5 5))
(deftest
"int = float eq"
(assert (= 1 (exact->inexact 1))))
(deftest
"float = int eq"
(assert (= (exact->inexact 1) 1)))
(deftest "int != different int" (assert (!= 1 2)))
(deftest "int < float" (assert (< 1 1.5)))
(deftest "float > int" (assert (> 2.5 2)))
(deftest "int <= float" (assert (<= 2 2.5)))
(deftest "int >= int" (assert (>= 3 3))))
;; --------------------------------------------------------------------------
;; mod / remainder / modulo with integers
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:modulo"
(deftest
"mod int int = int"
(assert (integer? (mod 10 3))))
(deftest "mod value" (assert= (mod 10 3) 1))
(deftest
"remainder int int = int"
(assert (integer? (remainder 10 3))))
(deftest
"remainder value"
(assert= (remainder 10 3) 1)))
;; --------------------------------------------------------------------------
;; min / max with mixed types
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:min-max"
(deftest "min two ints" (assert= (min 3 7) 3))
(deftest
"min int result type"
(assert (integer? (min 3 7))))
(deftest "max two ints" (assert= (max 3 7) 7))
(deftest "min with float" (assert= (min 3 2.5) 2.5))
(deftest "max with float" (assert= (max 3 3.5) 3.5)))
;; --------------------------------------------------------------------------
;; str rendering of int vs float
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:stringify"
(deftest "str of int" (assert= (str 42) "42"))
(deftest "str of negative int" (assert= (str -5) "-5"))
(deftest "str of 3.14" (assert= (str 3.14) "3.14"))
(deftest "str of 1.5" (assert= (str 1.5) "1.5")))

View File

@@ -1,131 +0,0 @@
(defsuite
"string-buffer"
(deftest
"make-string-buffer creates a string-buffer"
(let ((buf (make-string-buffer))) (assert (string-buffer? buf))))
(deftest
"string-buffer? is false for non-buffers"
(assert= false (string-buffer? "hello"))
(assert= false (string-buffer? 42))
(assert= false (string-buffer? nil))
(assert= false (string-buffer? (list)))
(assert= false (string-buffer? {:key "val"})))
(deftest
"type-of returns string-buffer"
(assert= "string-buffer" (type-of (make-string-buffer))))
(deftest
"empty buffer converts to empty string"
(let
((buf (make-string-buffer)))
(assert= "" (string-buffer->string buf))))
(deftest
"empty buffer has length zero"
(let
((buf (make-string-buffer)))
(assert= 0 (string-buffer-length buf))))
(deftest
"single append accumulates string"
(let
((buf (make-string-buffer)))
(string-buffer-append! buf "hello")
(assert= "hello" (string-buffer->string buf))))
(deftest
"multiple appends join in order"
(let
((buf (make-string-buffer)))
(string-buffer-append! buf "foo")
(string-buffer-append! buf "bar")
(string-buffer-append! buf "baz")
(assert= "foobarbaz" (string-buffer->string buf))))
(deftest
"length tracks total bytes appended"
(let
((buf (make-string-buffer)))
(string-buffer-append! buf "abc")
(string-buffer-append! buf "de")
(assert= 5 (string-buffer-length buf))))
(deftest
"append returns nil"
(let
((buf (make-string-buffer)))
(assert= nil (string-buffer-append! buf "x"))))
(deftest
"appending empty string is harmless"
(let
((buf (make-string-buffer)))
(string-buffer-append! buf "start")
(string-buffer-append! buf "")
(string-buffer-append! buf "end")
(assert= "startend" (string-buffer->string buf))
(assert= 8 (string-buffer-length buf))))
(deftest
"buffer is still usable after string-buffer->string"
(let
((buf (make-string-buffer)))
(string-buffer-append! buf "hello")
(string-buffer->string buf)
(string-buffer-append! buf " world")
(assert= "hello world" (string-buffer->string buf))))
(deftest
"two buffers are independent"
(let
((b1 (make-string-buffer)) (b2 (make-string-buffer)))
(string-buffer-append! b1 "one")
(string-buffer-append! b2 "two")
(string-buffer-append! b1 "ONE")
(assert= "oneONE" (string-buffer->string b1))
(assert= "two" (string-buffer->string b2))))
(deftest
"loop building — linear string concat"
(let
((buf (make-string-buffer)))
(let
loop
((i 0))
(when
(< i 5)
(string-buffer-append! buf (str i))
(loop (+ i 1))))
(assert= "01234" (string-buffer->string buf))
(assert= 5 (string-buffer-length buf))))
(deftest
"building CSV row with separator"
(let
((buf (make-string-buffer)) (items (list "a" "b" "c" "d")))
(let
loop
((remaining items) (is-first true))
(when
(not (empty? remaining))
(when (not is-first) (string-buffer-append! buf ","))
(string-buffer-append! buf (first remaining))
(loop (rest remaining) false)))
(assert= "a,b,c,d" (string-buffer->string buf))))
(deftest
"unicode characters accumulate correctly"
(let
((buf (make-string-buffer)))
(string-buffer-append! buf "こんにちは")
(string-buffer-append! buf " ")
(string-buffer-append! buf "世界")
(assert= "こんにちは 世界" (string-buffer->string buf))))
(deftest
"repeated to-string calls are consistent"
(let
((buf (make-string-buffer)))
(string-buffer-append! buf "test")
(assert= (string-buffer->string buf) (string-buffer->string buf))))
(deftest
"building with join pattern produces correct output"
(let
((buf (make-string-buffer))
(words (list "the" "quick" "brown" "fox")))
(let
loop
((remaining words) (sep ""))
(when
(not (empty? remaining))
(string-buffer-append! buf sep)
(string-buffer-append! buf (first remaining))
(loop (rest remaining) " ")))
(assert= "the quick brown fox" (string-buffer->string buf)))))

View File

@@ -1,207 +0,0 @@
;; test-vectors.sx — Tests for vector primitives
(defsuite
"vectors"
(deftest
"make-vector default fill is nil"
(let
((v (make-vector 3)))
(assert (vector? v))
(assert-equal 3 (vector-length v))
(assert-equal nil (vector-ref v 0))
(assert-equal nil (vector-ref v 1))
(assert-equal nil (vector-ref v 2))))
(deftest
"make-vector with fill value"
(let
((v (make-vector 4 99)))
(assert-equal 4 (vector-length v))
(assert-equal 99 (vector-ref v 0))
(assert-equal 99 (vector-ref v 1))
(assert-equal 99 (vector-ref v 2))
(assert-equal 99 (vector-ref v 3))))
(deftest
"make-vector size zero"
(let ((v (make-vector 0))) (assert-equal 0 (vector-length v))))
(deftest
"make-vector size one"
(let
((v (make-vector 1 "x")))
(assert-equal 1 (vector-length v))
(assert-equal "x" (vector-ref v 0))))
(deftest
"vector constructor no args"
(let ((v (vector))) (assert-equal 0 (vector-length v))))
(deftest
"vector constructor with args"
(let
((v (vector 10 20 30)))
(assert-equal 3 (vector-length v))
(assert-equal 10 (vector-ref v 0))
(assert-equal 20 (vector-ref v 1))
(assert-equal 30 (vector-ref v 2))))
(deftest
"vector constructor strings"
(let
((v (vector "a" "b" "c")))
(assert-equal "a" (vector-ref v 0))
(assert-equal "b" (vector-ref v 1))
(assert-equal "c" (vector-ref v 2))))
(deftest "vector? true for vector" (assert (vector? (make-vector 3))))
(deftest "vector? false for list" (assert (not (vector? (list 1 2 3)))))
(deftest "vector? false for number" (assert (not (vector? 42))))
(deftest "vector? false for nil" (assert (not (vector? nil))))
(deftest "vector? false for string" (assert (not (vector? "hello"))))
(deftest "vector-length zero" (assert-equal 0 (vector-length (vector))))
(deftest
"vector-length three"
(assert-equal 3 (vector-length (vector 1 2 3))))
(deftest
"vector-length after make-vector"
(assert-equal 7 (vector-length (make-vector 7 0))))
(deftest
"vector-ref first element"
(assert-equal 1 (vector-ref (vector 1 2 3) 0)))
(deftest
"vector-ref last element"
(assert-equal 3 (vector-ref (vector 1 2 3) 2)))
(deftest
"vector-ref middle element"
(assert-equal 2 (vector-ref (vector 1 2 3) 1)))
(deftest
"vector-set! mutates in place"
(let
((v (vector 1 2 3)))
(vector-set! v 1 99)
(assert-equal 99 (vector-ref v 1))
(assert-equal 1 (vector-ref v 0))
(assert-equal 3 (vector-ref v 2))))
(deftest
"vector-set! first slot"
(let
((v (make-vector 3 0)))
(vector-set! v 0 42)
(assert-equal 42 (vector-ref v 0))))
(deftest
"vector-set! last slot"
(let
((v (make-vector 3 0)))
(vector-set! v 2 77)
(assert-equal 77 (vector-ref v 2))))
(deftest
"vector-set! returns nil"
(let ((v (make-vector 3 0))) (assert-equal nil (vector-set! v 0 1))))
(deftest
"vector->list empty"
(assert-equal (list) (vector->list (vector))))
(deftest
"vector->list numbers"
(assert-equal (list 1 2 3) (vector->list (vector 1 2 3))))
(deftest
"vector->list strings"
(assert-equal (list "a" "b") (vector->list (vector "a" "b"))))
(deftest
"list->vector empty"
(let ((v (list->vector (list)))) (assert-equal 0 (vector-length v))))
(deftest
"list->vector numbers"
(let
((v (list->vector (list 10 20 30))))
(assert-equal 3 (vector-length v))
(assert-equal 10 (vector-ref v 0))
(assert-equal 20 (vector-ref v 1))
(assert-equal 30 (vector-ref v 2))))
(deftest
"vector-fill! sets all elements"
(let
((v (vector 1 2 3)))
(vector-fill! v 0)
(assert-equal 0 (vector-ref v 0))
(assert-equal 0 (vector-ref v 1))
(assert-equal 0 (vector-ref v 2))))
(deftest
"vector-fill! returns nil"
(assert-equal nil (vector-fill! (make-vector 2 0) 7)))
(deftest
"vector-fill! string fill"
(let
((v (make-vector 3 "")))
(vector-fill! v "x")
(assert-equal "x" (vector-ref v 0))
(assert-equal "x" (vector-ref v 2))))
(deftest
"vector-copy full copy"
(let
((v1 (vector 1 2 3)) (v2 (vector-copy (vector 1 2 3))))
(assert-equal 3 (vector-length v2))
(assert-equal 1 (vector-ref v2 0))
(assert-equal 2 (vector-ref v2 1))
(assert-equal 3 (vector-ref v2 2))))
(deftest
"vector-copy is independent"
(let
((v1 (vector 1 2 3)))
(let
((v2 (vector-copy v1)))
(vector-set! v1 0 99)
(assert-equal 1 (vector-ref v2 0)))))
(deftest
"vector-copy with start"
(let
((v (vector-copy (vector 10 20 30 40) 1)))
(assert-equal 3 (vector-length v))
(assert-equal 20 (vector-ref v 0))
(assert-equal 30 (vector-ref v 1))
(assert-equal 40 (vector-ref v 2))))
(deftest
"vector-copy with start and end"
(let
((v (vector-copy (vector 10 20 30 40) 1 3)))
(assert-equal 2 (vector-length v))
(assert-equal 20 (vector-ref v 0))
(assert-equal 30 (vector-ref v 1))))
(deftest
"vector-copy empty slice"
(let
((v (vector-copy (vector 1 2 3) 1 1)))
(assert-equal 0 (vector-length v))))
(deftest
"vector-ref out of bounds raises"
(let
((ok false))
(guard (exn (else (set! ok true))) (vector-ref (vector 1 2 3) 5))
(assert ok)))
(deftest
"vector-ref negative index raises"
(let
((ok false))
(guard (exn (else (set! ok true))) (vector-ref (vector 1 2 3) -1))
(assert ok)))
(deftest
"vector-set! out of bounds raises"
(let
((ok false))
(guard
(exn (else (set! ok true)))
(vector-set! (vector 1 2 3) 10 99))
(assert ok)))
(deftest
"vector list round-trip"
(let
((lst (list 5 10 15 20)))
(assert-equal lst (vector->list (list->vector lst)))))
(deftest
"vector mutation does not affect copy"
(let
((v1 (vector 1 2 3)))
(let
((v2 (vector-copy v1)))
(vector-set! v2 0 100)
(assert-equal 1 (vector-ref v1 0))
(assert-equal 100 (vector-ref v2 0)))))
(deftest
"vector-length after fill"
(let
((v (make-vector 5 0)))
(vector-fill! v 1)
(assert-equal 5 (vector-length v)))))

View File

@@ -2333,25 +2333,6 @@ def generate_eval_only_test(test, idx):
hs_expr = extract_hs_expr(m.group(2)) hs_expr = extract_hs_expr(m.group(2))
assertions.append(f' (assert-throws (eval-hs "{hs_expr}"))') assertions.append(f' (assert-throws (eval-hs "{hs_expr}"))')
# Pattern 4: eval-hs-error — expect(await error("expr")).toBe("msg")
# These test that running HS raises an error with a specific message string.
for m in re.finditer(
r'(?:const\s+\w+\s*=\s*)?(?:await\s+)?error\((["\x27`])(.+?)\1\)'
r'(?:[^;]|\n)*?(?:expect\([^)]*\)\.toBe\(([^)]+)\)|\.toBe\(([^)]+)\))',
body, re.DOTALL
):
hs_expr = extract_hs_expr(m.group(2))
expected_raw = (m.group(3) or m.group(4) or '').strip()
# Strip only the outermost JS string delimiter (double or single quote)
# without touching inner quotes inside the string value.
if len(expected_raw) >= 2 and expected_raw[0] == expected_raw[-1] and expected_raw[0] in ('"', "'"):
inner = expected_raw[1:-1]
expected_sx = '"' + inner.replace('\\', '\\\\').replace('"', '\\"') + '"'
else:
expected_sx = js_val_to_sx(expected_raw)
hs_escaped = hs_expr.replace('\\', '\\\\').replace('"', '\\"')
assertions.append(f' (assert= (eval-hs-error "{hs_escaped}") {expected_sx})')
if not assertions: if not assertions:
return None # Can't convert this body pattern return None # Can't convert this body pattern
@@ -2711,27 +2692,6 @@ output.append(' (nth _e 1)')
output.append(' (raise _e))))') output.append(' (raise _e))))')
output.append(' (handler me-val))))))') output.append(' (handler me-val))))))')
output.append('') output.append('')
output.append(';; Evaluate a hyperscript expression, catch the first error raised, and')
output.append(';; return its message string. Used by runtimeErrors tests.')
output.append(';; Returns nil if no error is raised (test would then fail equality).')
output.append('(define eval-hs-error')
output.append(' (fn (src)')
output.append(' (let ((sx (hs-to-sx (hs-compile src))))')
output.append(' (let ((handler (eval-expr-cek')
output.append(' (list (quote fn) (list (quote me))')
output.append(' (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))')
output.append(' (guard')
output.append(' (_e')
output.append(' (true')
output.append(' (if')
output.append(' (string? _e)')
output.append(' _e')
output.append(' (if')
output.append(' (and (list? _e) (= (first _e) "hs-return"))')
output.append(' nil')
output.append(' (str _e)))))')
output.append(' (begin (handler nil) nil))))))')
output.append('')
# Group by category # Group by category
categories = OrderedDict() categories = OrderedDict()