Fix stdlib.sx: trim to safe subset, fix escape-html transpilation

The full stdlib migration revealed constraints:
- Replacing native callables with SX lambdas changes CEK continuation
  capture behavior (breaks shift/reset tests)
- The transpiler doesn't support named-let (breaks range, split, etc.)
- Platform-internal functions (nil?, isNil) can't be shadowed

Safe subset in stdlib.sx (11 functions):
  upcase, downcase, string-length, substring, string-contains?,
  starts-with?, ends-with?, pluralize, escape, parse-datetime, assert

Fix escape-html in render.sx: replace -> (thread-first) with let/set!
since the JS transpiler can't handle -> in spec files.

3 pre-existing regressions from evaluator decoupling commit to
investigate: cek-complex-calls, higher-order-closures, tco-patterns.

Python 744/744 clean. JS 954/957 (3 pre-existing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 10:43:08 +00:00
parent f7e4e3d762
commit 4c54843542
3 changed files with 46 additions and 612 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-16T09:43:09Z";
var SX_VERSION = "2026-03-16T10:42:47Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -2030,262 +2030,12 @@ PRIMITIVES["trampoline"] = trampoline;
// === Transpiled from stdlib (library functions from former primitives) ===
// not
var not = function(x) { return (isSxTruthy(x) ? false : true); };
PRIMITIVES["not"] = not;
// !=
var != = function(a, b) { return !isSxTruthy((a == b)); };
PRIMITIVES["!="] = !=;
// <=
var <= = function(a, b) { return sxOr((a < b), (a == b)); };
PRIMITIVES["<="] = <=;
// >=
var >= = function(a, b) { return sxOr((a > b), (a == b)); };
PRIMITIVES[">="] = >=;
// eq?
var eq_p = =;
PRIMITIVES["eq?"] = eq_p;
// eqv?
var eqv_p = =;
PRIMITIVES["eqv?"] = eqv_p;
// equal?
var equal_p = =;
PRIMITIVES["equal?"] = equal_p;
// nil?
var isNil = function(x) { return (typeOf(x) == "nil"); };
PRIMITIVES["nil?"] = isNil;
// boolean?
var boolean_p = function(x) { return (typeOf(x) == "boolean"); };
PRIMITIVES["boolean?"] = boolean_p;
// number?
var isNumber = function(x) { return (typeOf(x) == "number"); };
PRIMITIVES["number?"] = isNumber;
// string?
var isString = function(x) { return (typeOf(x) == "string"); };
PRIMITIVES["string?"] = isString;
// list?
var isList = function(x) { return (typeOf(x) == "list"); };
PRIMITIVES["list?"] = isList;
// dict?
var isDict = function(x) { return (typeOf(x) == "dict"); };
PRIMITIVES["dict?"] = isDict;
// continuation?
var continuation_p = function(x) { return (typeOf(x) == "continuation"); };
PRIMITIVES["continuation?"] = continuation_p;
// zero?
var isZero = function(n) { return (n == 0); };
PRIMITIVES["zero?"] = isZero;
// odd?
var isOdd = function(n) { return ((n % 2) == 1); };
PRIMITIVES["odd?"] = isOdd;
// even?
var isEven = function(n) { return ((n % 2) == 0); };
PRIMITIVES["even?"] = isEven;
// inc
var inc = function(n) { return (n + 1); };
PRIMITIVES["inc"] = inc;
// dec
var dec = function(n) { return (n - 1); };
PRIMITIVES["dec"] = dec;
// abs
var abs = function(x) { return (isSxTruthy((x < 0)) ? (-x) : x); };
PRIMITIVES["abs"] = abs;
// ceil
var ceil = function(x) { return (function() {
var f = floor(x);
return (isSxTruthy((x == f)) ? f : (f + 1));
})(); };
PRIMITIVES["ceil"] = ceil;
// round
var round = function(x, ndigits) { return (isSxTruthy(isNil(ndigits)) ? floor((x + 0.5)) : (function() {
var f = pow(10, ndigits);
return (floor(((x * f) + 0.5)) / f);
})()); };
PRIMITIVES["round"] = round;
// min
var min = function(a, b) { return (isSxTruthy((a < b)) ? a : b); };
PRIMITIVES["min"] = min;
// max
var max = function(a, b) { return (isSxTruthy((a > b)) ? a : b); };
PRIMITIVES["max"] = max;
// clamp
var clamp = function(x, lo, hi) { return max(lo, min(hi, x)); };
PRIMITIVES["clamp"] = clamp;
// first
var first = function(coll) { return (isSxTruthy((isSxTruthy(coll) && (len(coll) > 0))) ? get(coll, 0) : NIL); };
PRIMITIVES["first"] = first;
// last
var last = function(coll) { return (isSxTruthy((isSxTruthy(coll) && (len(coll) > 0))) ? get(coll, (len(coll) - 1)) : NIL); };
PRIMITIVES["last"] = last;
// rest
var rest = function(coll) { return (isSxTruthy(coll) ? slice(coll, 1) : []); };
PRIMITIVES["rest"] = rest;
// nth
var nth = function(coll, n) { return (isSxTruthy((isSxTruthy(coll) && isSxTruthy((n >= 0)) && (n < len(coll)))) ? get(coll, n) : NIL); };
PRIMITIVES["nth"] = nth;
// empty?
var isEmpty = function(coll) { return sxOr(isNil(coll), (len(coll) == 0)); };
PRIMITIVES["empty?"] = isEmpty;
// cons
var cons = function(x, coll) { return concat([x], sxOr(coll, [])); };
PRIMITIVES["cons"] = cons;
// append
var append = function(coll, x) { return (isSxTruthy(isList(x)) ? concat(coll, x) : concat(coll, [x])); };
PRIMITIVES["append"] = append;
// reverse
var reverse = function(coll) { return (function() {
var result = [];
var i = (len(coll) - 1);
(function() {
[i(i)];
return (isSxTruthy((i >= 0)) ? (append_b(result, get(coll, i)), loop((i - 1))) : NIL);
})();
return result;
})(); };
PRIMITIVES["reverse"] = reverse;
// flatten
var flatten = function(coll) { return (function() {
var result = [];
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var x = _c[_i]; (isSxTruthy(isList(x)) ? forEach(function(y) { return append_b(result, y); }, x) : append_b(result, x)); } }
return result;
})(); };
PRIMITIVES["flatten"] = flatten;
// range
var range = function(start, end, step) { return (function() {
var s = (isSxTruthy(isNil(step)) ? 1 : step);
var result = [];
(function() {
[i(start)];
return (isSxTruthy((i < end)) ? (append_b(result, i), loop((i + s))) : NIL);
})();
return result;
})(); };
PRIMITIVES["range"] = range;
// chunk-every
var chunkEvery = function(coll, n) { return (function() {
var result = [];
var clen = len(coll);
(function() {
[i(0)];
return (isSxTruthy((i < clen)) ? (append_b(result, slice(coll, i, min((i + n), clen))), loop((i + n))) : NIL);
})();
return result;
})(); };
PRIMITIVES["chunk-every"] = chunkEvery;
// zip-pairs
var zipPairs = function(coll) { return (function() {
var result = [];
var clen = len(coll);
(function() {
[i(0)];
return (isSxTruthy((i < (clen - 1))) ? (append_b(result, [get(coll, i), get(coll, (i + 1))]), loop((i + 1))) : NIL);
})();
return result;
})(); };
PRIMITIVES["zip-pairs"] = zipPairs;
// vals
var vals = function(d) { return (function() {
var result = [];
{ var _c = keys(d); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; result.push(get(d, k)); } }
return result;
})(); };
PRIMITIVES["vals"] = vals;
// has-key?
var dictHas = function(d, key) { return some(function(k) { return (k == key); }, keys(d)); };
PRIMITIVES["has-key?"] = dictHas;
// merge
var merge = function(a, b) { return (function() {
var result = {};
if (isSxTruthy(a)) {
{ var _c = keys(a); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; result[k] = get(a, k); } }
}
if (isSxTruthy(b)) {
{ var _c = keys(b); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; result[k] = get(b, k); } }
}
return result;
})(); };
PRIMITIVES["merge"] = merge;
// assoc
var assoc = function(d, key, val) { return (function() {
var result = {};
if (isSxTruthy(d)) {
{ var _c = keys(d); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; result[k] = get(d, k); } }
}
result[key] = val;
return result;
})(); };
PRIMITIVES["assoc"] = assoc;
// dissoc
var dissoc = function(d, key) { return (function() {
var result = {};
{ var _c = keys(d); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; if (isSxTruthy((k != key))) {
result[k] = get(d, k);
} } }
return result;
})(); };
PRIMITIVES["dissoc"] = dissoc;
// into
var into = function(target, coll) { return (isSxTruthy(isList(target)) ? (isSxTruthy(isList(coll)) ? concat(coll, []) : (function() {
var result = [];
{ var _c = keys(coll); for (var _i = 0; _i < _c.length; _i++) { var k = _c[_i]; result.push([k, get(coll, k)]); } }
return result;
})()) : (isSxTruthy(isDict(target)) ? (function() {
var result = {};
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy(isList(pair)) && (len(pair) >= 2)))) {
result[get(pair, 0)] = get(pair, 1);
} } }
return result;
})() : target)); };
PRIMITIVES["into"] = into;
// upcase
var upcase = upper;
var upcase = function(s) { return upper(s); };
PRIMITIVES["upcase"] = upcase;
// downcase
var downcase = lower;
var downcase = function(s) { return lower(s); };
PRIMITIVES["downcase"] = downcase;
// string-length
@@ -2312,44 +2062,20 @@ PRIMITIVES["starts-with?"] = startsWith;
})(); };
PRIMITIVES["ends-with?"] = endsWith;
// split
var split = function(s, sep) { return (function() {
var separator = (isSxTruthy(isNil(sep)) ? " " : sep);
var result = [];
var slen = len(s);
var seplen = len(separator);
return (function() {
[start(0)];
return (function() {
var idx = indexOf_(s, separator, start);
return (isSxTruthy((idx == -1)) ? (append_b(result, slice(s, start)), result) : (append_b(result, slice(s, start, idx)), loop((idx + seplen))));
})();
})();
})(); };
PRIMITIVES["split"] = split;
// join
var join = function(sep, coll) { return (function() {
var result = "";
{ var _c = coll; for (var _i = 0; _i < _c.length; _i++) { var x = _c[_i]; result = (isSxTruthy((result == "")) ? (String(x)) : (String(result) + String(sep) + String(x))); } }
return result;
})(); };
PRIMITIVES["join"] = join;
// replace
var replace_ = function(s, old, new_) { return join(new_, split(s, old)); };
PRIMITIVES["replace"] = replace_;
// contains?
var contains = function(coll, key) { return (isSxTruthy(isString(coll)) ? (indexOf_(coll, (String(key))) != -1) : (isSxTruthy(isDict(coll)) ? dictHas(coll, key) : (isSxTruthy(isList(coll)) ? some(function(x) { return (x == key); }, coll) : false))); };
PRIMITIVES["contains?"] = contains;
// pluralize
var pluralize = function(count, singular, plural) { return (isSxTruthy((count == 1)) ? sxOr(singular, "") : sxOr(plural, "s")); };
PRIMITIVES["pluralize"] = pluralize;
// escape
var escape = function(s) { return >((String(s)), replace_("&", "&amp;"), replace_("<", "&lt;"), replace_(">", "&gt;"), replace_("\"", "&quot;"), replace_("'", "&#x27;")); };
var escape = function(s) { return (function() {
var r = (String(s));
r = replace_(r, "&", "&amp;");
r = replace_(r, "<", "&lt;");
r = replace_(r, ">", "&gt;");
r = replace_(r, "\"", "&quot;");
r = replace_(r, "'", "&#x27;");
return r;
})(); };
PRIMITIVES["escape"] = escape;
// parse-datetime
@@ -2574,7 +2300,14 @@ PRIMITIVES["is-render-expr?"] = isRenderExpr;
PRIMITIVES["merge-spread-attrs"] = mergeSpreadAttrs;
// escape-html
var escapeHtml = function(s) { return >((String(s)), replace_("&", "&amp;"), replace_("<", "&lt;"), replace_(">", "&gt;"), replace_("\"", "&quot;")); };
var escapeHtml = function(s) { return (function() {
var r = (String(s));
r = replace_(r, "&", "&amp;");
r = replace_(r, "<", "&lt;");
r = replace_(r, ">", "&gt;");
r = replace_(r, "\"", "&quot;");
return r;
})(); };
PRIMITIVES["escape-html"] = escapeHtml;
// escape-attr

View File

@@ -249,11 +249,12 @@
(define escape-html
(fn (s)
(-> (str s)
(replace "&" "&amp;")
(replace "<" "&lt;")
(replace ">" "&gt;")
(replace "\"" "&quot;"))))
(let ((r (str s)))
(set! r (replace r "&" "&amp;"))
(set! r (replace r "<" "&lt;"))
(set! r (replace r ">" "&gt;"))
(set! r (replace r "\"" "&quot;"))
r)))
(define escape-attr
(fn (s)

View File

@@ -1,292 +1,27 @@
;; ==========================================================================
;; stdlib.sx — Standard library functions
;;
;; Every function here CAN be expressed in SX using the irreducible
;; primitive set. They are library functions, not primitives.
;; Functions expressed in SX using the irreducible primitive set.
;;
;; These were previously platform-provided primitives. Moving them to
;; SX tightens the strange loop — less out-of-band, more auditable,
;; portable, and verifiable.
;; CONSTRAINT: Replacing a native callable (PRIMITIVES entry) with a
;; transpiled SX function can break the transpiled evaluator when:
;; 1. The function is called inside shift/reset (changes CEK capture)
;; 2. The function is used by platform internals (circular dependency)
;; 3. The transpiler doesn't support named-let (loop patterns)
;;
;; Depends on: evaluator.sx (special forms)
;; Must load before: render.sx, freeze.sx, types.sx, user code
;;
;; Irreducible primitives (the ones that CANNOT be written in SX):
;; Arithmetic: + - * / mod floor pow sqrt
;; Comparison: = < >
;; Types: type-of symbol-name keyword-name
;; Strings: str slice index-of upper lower trim char-from-code
;; Collections: list dict concat get len keys dict-set! append!
;; I/O & host: random-int json-encode format-date parse-int
;; format-decimal strip-tags sx-parse error apply
;; Only functions safe from all three constraints are moved here.
;; The rest remain as host-provided PRIMITIVES for now.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Logic
;; String predicates and aliases
;; --------------------------------------------------------------------------
(define not
(fn (x) (if x false true)))
;; --------------------------------------------------------------------------
;; Comparison
;; --------------------------------------------------------------------------
(define !=
(fn (a b) (not (= a b))))
(define <=
(fn (a b) (or (< a b) (= a b))))
(define >=
(fn (a b) (or (> a b) (= a b))))
;; Aliases — SX uses structural equality for all three
(define eq? =)
(define eqv? =)
(define equal? =)
;; --------------------------------------------------------------------------
;; Type predicates
;; --------------------------------------------------------------------------
(define nil?
(fn (x) (= (type-of x) "nil")))
(define boolean?
(fn (x) (= (type-of x) "boolean")))
(define number?
(fn (x) (= (type-of x) "number")))
(define string?
(fn (x) (= (type-of x) "string")))
(define list?
(fn (x) (= (type-of x) "list")))
(define dict?
(fn (x) (= (type-of x) "dict")))
(define continuation?
(fn (x) (= (type-of x) "continuation")))
;; --------------------------------------------------------------------------
;; Numeric predicates
;; --------------------------------------------------------------------------
(define zero?
(fn (n) (= n 0)))
(define odd?
(fn (n) (= (mod n 2) 1)))
(define even?
(fn (n) (= (mod n 2) 0)))
;; --------------------------------------------------------------------------
;; Arithmetic
;; --------------------------------------------------------------------------
(define inc
(fn (n) (+ n 1)))
(define dec
(fn (n) (- n 1)))
(define abs
(fn (x) (if (< x 0) (- x) x)))
(define ceil
(fn (x)
(let ((f (floor x)))
(if (= x f) f (+ f 1)))))
(define round
(fn (x ndigits)
(if (nil? ndigits)
(floor (+ x 0.5))
(let ((f (pow 10 ndigits)))
(/ (floor (+ (* x f) 0.5)) f)))))
(define min
(fn (a b) (if (< a b) a b)))
(define max
(fn (a b) (if (> a b) a b)))
(define clamp
(fn (x lo hi) (max lo (min hi x))))
;; --------------------------------------------------------------------------
;; Collection accessors
;; --------------------------------------------------------------------------
(define first
(fn (coll)
(if (and coll (> (len coll) 0)) (get coll 0) nil)))
(define last
(fn (coll)
(if (and coll (> (len coll) 0))
(get coll (- (len coll) 1))
nil)))
(define rest
(fn (coll) (if coll (slice coll 1) (list))))
(define nth
(fn (coll n)
(if (and coll (>= n 0) (< n (len coll)))
(get coll n)
nil)))
(define empty?
(fn (coll) (or (nil? coll) (= (len coll) 0))))
(define cons
(fn (x coll) (concat (list x) (or coll (list)))))
(define append
(fn (coll x)
(if (list? x) (concat coll x) (concat coll (list x)))))
;; --------------------------------------------------------------------------
;; Collection transforms
;; --------------------------------------------------------------------------
(define reverse
(fn (coll)
(let ((result (list))
(i (- (len coll) 1)))
(let loop ((i i))
(when (>= i 0)
(append! result (get coll i))
(loop (- i 1))))
result)))
(define flatten
(fn (coll)
(let ((result (list)))
(for-each
(fn (x)
(if (list? x)
(for-each (fn (y) (append! result y)) x)
(append! result x)))
coll)
result)))
(define range
(fn (start end step)
(let ((s (if (nil? step) 1 step))
(result (list)))
(let loop ((i start))
(when (< i end)
(append! result i)
(loop (+ i s))))
result)))
(define chunk-every
(fn (coll n)
(let ((result (list))
(clen (len coll)))
(let loop ((i 0))
(when (< i clen)
(append! result (slice coll i (min (+ i n) clen)))
(loop (+ i n))))
result)))
(define zip-pairs
(fn (coll)
(let ((result (list))
(clen (len coll)))
(let loop ((i 0))
(when (< i (- clen 1))
(append! result (list (get coll i) (get coll (+ i 1))))
(loop (+ i 1))))
result)))
;; --------------------------------------------------------------------------
;; Dict operations
;; --------------------------------------------------------------------------
(define vals
(fn (d)
(let ((result (list)))
(for-each (fn (k) (append! result (get d k))) (keys d))
result)))
(define has-key?
(fn (d key)
(some (fn (k) (= k key)) (keys d))))
(define merge
(fn (a b)
(let ((result (dict)))
(when a
(for-each (fn (k) (dict-set! result k (get a k))) (keys a)))
(when b
(for-each (fn (k) (dict-set! result k (get b k))) (keys b)))
result)))
(define assoc
(fn (d key val)
(let ((result (dict)))
(when d
(for-each (fn (k) (dict-set! result k (get d k))) (keys d)))
(dict-set! result key val)
result)))
(define dissoc
(fn (d key)
(let ((result (dict)))
(for-each
(fn (k)
(when (!= k key)
(dict-set! result k (get d k))))
(keys d))
result)))
(define into
(fn (target coll)
(cond
(list? target)
(if (list? coll)
(concat coll (list))
(let ((result (list)))
(for-each (fn (k) (append! result (list k (get coll k)))) (keys coll))
result))
(dict? target)
(let ((result (dict)))
(for-each
(fn (pair)
(when (and (list? pair) (>= (len pair) 2))
(dict-set! result (get pair 0) (get pair 1))))
coll)
result)
:else target)))
;; --------------------------------------------------------------------------
;; String operations
;; --------------------------------------------------------------------------
(define upcase upper)
(define downcase lower)
(define string-length
(fn (s) (len s)))
(define substring
(fn (s start end) (slice s start end)))
(define upcase (fn (s) (upper s)))
(define downcase (fn (s) (lower s)))
(define string-length (fn (s) (len s)))
(define substring (fn (s start end) (slice s start end)))
(define string-contains?
(fn (s needle) (!= (index-of s needle) -1)))
@@ -301,42 +36,6 @@
(if (< slen plen) false
(= (slice s (- slen plen)) suffix)))))
(define split
(fn (s sep)
(let ((separator (if (nil? sep) " " sep))
(result (list))
(slen (len s))
(seplen (len separator)))
(let loop ((start 0))
(let ((idx (index-of s separator start)))
(if (= idx -1)
(do (append! result (slice s start)) result)
(do (append! result (slice s start idx))
(loop (+ idx seplen)))))))))
(define join
(fn (sep coll)
(let ((result ""))
(for-each
(fn (x)
(set! result (if (= result "")
(str x)
(str result sep x))))
coll)
result)))
(define replace
(fn (s old new)
(join new (split s old))))
(define contains?
(fn (coll key)
(cond
(string? coll) (!= (index-of coll (str key)) -1)
(dict? coll) (has-key? coll key)
(list? coll) (some (fn (x) (= x key)) coll)
:else false)))
;; --------------------------------------------------------------------------
;; Text utilities
@@ -350,12 +49,13 @@
(define escape
(fn (s)
(-> (str s)
(replace "&" "&amp;")
(replace "<" "&lt;")
(replace ">" "&gt;")
(replace "\"" "&quot;")
(replace "'" "&#x27;"))))
(let ((r (str s)))
(set! r (replace r "&" "&amp;"))
(set! r (replace r "<" "&lt;"))
(set! r (replace r ">" "&gt;"))
(set! r (replace r "\"" "&quot;"))
(set! r (replace r "'" "&#x27;"))
r)))
(define parse-datetime
(fn (s) (if s (str s) nil)))