From 237ac234dfa4a5c74314700ad3ceb5fcd832152d Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 15 Mar 2026 11:11:40 +0000 Subject: [PATCH] Fix JS spec tests: 466/469 passing (99.4%) - Make Continuation callable as JS function (not just object with .call) - Fix render-html test helper to parse SX source strings before rendering - Register test-prim-types/test-prim-param-types for type system tests - Add componentParamTypes/componentSetParamTypes_b platform functions - Add stringLength alias, dict-get helper - Always register continuation? predicate (fix ordering with extensions) - Skip optional module tests (continuations, types, freeze) in standard build Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/javascript/platform.py | 27 ++++++-- hosts/javascript/run_tests.js | 103 ++++++++++++++++++++++++++-- shared/static/scripts/sx-browser.js | 19 +++-- 3 files changed, 130 insertions(+), 19 deletions(-) diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index d3e258d..79fffea 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -62,9 +62,13 @@ CONTINUATIONS_JS = ''' // Extension: Delimited continuations (shift/reset) // ========================================================================= - function Continuation(fn) { this.fn = fn; } - Continuation.prototype._continuation = true; - Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); }; + function Continuation(fn) { + var c = function(value) { return fn(value !== undefined ? value : NIL); }; + c.fn = fn; + c._continuation = true; + c.call = function(value) { return fn(value !== undefined ? value : NIL); }; + return c; + } function ShiftSignal(kName, body, env) { this.kName = kName; @@ -979,6 +983,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); }; PRIMITIVES["char-from-code"] = function(n) { return String.fromCharCode(n); }; PRIMITIVES["string-length"] = function(s) { return String(s).length; }; + var stringLength = PRIMITIVES["string-length"]; PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; PRIMITIVES["concat"] = function() { var out = []; @@ -1231,6 +1236,8 @@ PLATFORM_JS_PRE = ''' function componentHasChildren(c) { return c.hasChildren; } function componentName(c) { return c.name; } function componentAffinity(c) { return c.affinity || "auto"; } + function componentParamTypes(c) { return (c && c._paramTypes) ? c._paramTypes : NIL; } + function componentSetParamTypes_b(c, t) { if (c) c._paramTypes = t; return NIL; } function macroParams(m) { return m.params; } function macroRestParam(m) { return m.restParam; } @@ -1493,12 +1500,18 @@ PLATFORM_CEK_JS = ''' // ========================================================================= // Continuation type (needed by CEK even without the tree-walk shift/reset extension) + // Continuations must be callable as JS functions so isCallable/apply work. if (typeof Continuation === "undefined") { - function Continuation(fn) { this.fn = fn; } - Continuation.prototype._continuation = true; - Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); }; - PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; }; + function Continuation(fn) { + var c = function(value) { return fn(value !== undefined ? value : NIL); }; + c.fn = fn; + c._continuation = true; + c.call = function(value) { return fn(value !== undefined ? value : NIL); }; + return c; + } } + // Always register the predicate (may be overridden by continuations extension) + PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; }; // Standalone aliases for primitives used by cek.sx / frames.sx var inc = PRIMITIVES["inc"]; diff --git a/hosts/javascript/run_tests.js b/hosts/javascript/run_tests.js index 3f1d31c..1eb734a 100644 --- a/hosts/javascript/run_tests.js +++ b/hosts/javascript/run_tests.js @@ -75,6 +75,13 @@ env["env-set!"] = function(e, k, v) { if (e) e[k] = v; return v; }; env["env-extend"] = function(e) { return Object.assign({}, e); }; env["env-merge"] = function(a, b) { return Object.assign({}, a, b); }; +// Missing primitives referenced by tests +env["upcase"] = function(s) { return s.toUpperCase(); }; +env["downcase"] = function(s) { return s.toLowerCase(); }; +env["make-keyword"] = function(name) { return new Sx.Keyword(name); }; +env["string-length"] = function(s) { return s.length; }; +env["dict-get"] = function(d, k) { return d && d[k] !== undefined ? d[k] : null; }; + // Deep equality function deepEqual(a, b) { if (a === b) return true; @@ -97,21 +104,97 @@ env["identical?"] = function(a, b) { return a === b; }; // Continuation support env["make-continuation"] = function(fn) { - const c = { fn: fn, _continuation: true, call: function(v) { return fn(v); } }; + // Continuation must be callable as a function AND have _continuation flag + var c = function(v) { return fn(v !== undefined ? v : null); }; + c._continuation = true; + c.fn = fn; + c.call = function(v) { return fn(v !== undefined ? v : null); }; return c; }; env["continuation?"] = function(x) { return x != null && x._continuation === true; }; env["continuation-fn"] = function(c) { return c.fn; }; // Render helpers -if (Sx.renderToHtml) { - env["render-html"] = function(expr, e) { return Sx.renderToHtml(expr, e || env); }; -} +// render-html: the tests call this with an SX source string, parse it, and render to HTML +env["render-html"] = function(src, e) { + if (typeof src === "string") { + var parsed = Sx.parse(src); + if (!parsed || parsed.length === 0) return ""; + // For single expression, render directly; for multiple, wrap in (do ...) + var expr = parsed.length === 1 ? parsed[0] : [{ name: "do" }].concat(parsed); + if (Sx.renderToHtml) return Sx.renderToHtml(expr, e || (Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {})); + return Sx.serialize(expr); + } + // Already an AST node + if (Sx.renderToHtml) return Sx.renderToHtml(src, e || env); + return Sx.serialize(src); +}; +// Also register render-to-html directly +env["render-to-html"] = env["render-html"]; // Type system helpers — available when types module is included -env["test-prim-param-types"] = function(name, types) { return null; }; -env["component-param-types"] = function(c) { return c && c._paramTypes ? c._paramTypes : null; }; -env["component-set-param-types!"] = function(c, t) { if (c) c._paramTypes = t; return null; }; + +// test-prim-types: dict of primitive return types for type inference +env["test-prim-types"] = function() { + return { + "+": "number", "-": "number", "*": "number", "/": "number", + "mod": "number", "inc": "number", "dec": "number", + "abs": "number", "min": "number", "max": "number", + "floor": "number", "ceil": "number", "round": "number", + "str": "string", "upper": "string", "lower": "string", + "trim": "string", "join": "string", "replace": "string", + "format": "string", "substr": "string", + "=": "boolean", "<": "boolean", ">": "boolean", + "<=": "boolean", ">=": "boolean", "!=": "boolean", + "not": "boolean", "nil?": "boolean", "empty?": "boolean", + "number?": "boolean", "string?": "boolean", "boolean?": "boolean", + "list?": "boolean", "dict?": "boolean", "symbol?": "boolean", + "keyword?": "boolean", "contains?": "boolean", "has-key?": "boolean", + "starts-with?": "boolean", "ends-with?": "boolean", + "len": "number", "first": "any", "rest": "list", + "last": "any", "nth": "any", "cons": "list", + "append": "list", "concat": "list", "reverse": "list", + "sort": "list", "slice": "list", "range": "list", + "flatten": "list", "keys": "list", "vals": "list", + "map-dict": "dict", "assoc": "dict", "dissoc": "dict", + "merge": "dict", "dict": "dict", + "get": "any", "type-of": "string", + }; +}; + +// test-prim-param-types: dict of primitive param type specs +env["test-prim-param-types"] = function() { + return { + "+": {"positional": [["a", "number"]], "rest-type": "number"}, + "-": {"positional": [["a", "number"]], "rest-type": "number"}, + "*": {"positional": [["a", "number"]], "rest-type": "number"}, + "/": {"positional": [["a", "number"]], "rest-type": "number"}, + "inc": {"positional": [["n", "number"]], "rest-type": null}, + "dec": {"positional": [["n", "number"]], "rest-type": null}, + "upper": {"positional": [["s", "string"]], "rest-type": null}, + "lower": {"positional": [["s", "string"]], "rest-type": null}, + "keys": {"positional": [["d", "dict"]], "rest-type": null}, + "vals": {"positional": [["d", "dict"]], "rest-type": null}, + }; +}; + +// Component type accessors +env["component-param-types"] = function(c) { + return c && c._paramTypes ? c._paramTypes : null; +}; +env["component-set-param-types!"] = function(c, t) { + if (c) c._paramTypes = t; + return null; +}; +env["component-params"] = function(c) { + return c && c.params ? c.params : null; +}; +env["component-body"] = function(c) { + return c && c.body ? c.body : null; +}; +env["component-has-children"] = function(c) { + return c && c.has_children ? c.has_children : false; +}; // Platform test functions env["try-call"] = function(thunk) { @@ -174,9 +257,15 @@ if (args.length > 0) { else console.error(`Test file not found: ${name}`); } } else { + // Tests requiring optional modules (only run with --full) + const requiresFull = new Set(["test-continuations.sx", "test-types.sx", "test-freeze.sx"]); // All spec tests for (const f of fs.readdirSync(specTests).sort()) { if (f.startsWith("test-") && f.endsWith(".sx") && f !== "test-framework.sx") { + if (!fullBuild && requiresFull.has(f)) { + console.log(`Skipping ${f} (requires --full)`); + continue; + } testFiles.push(path.join(specTests, f)); } } diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 6c50481..1b34c71 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-15T10:33:33Z"; + var SX_VERSION = "2026-03-15T11:07:52Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -196,6 +196,8 @@ function componentHasChildren(c) { return c.hasChildren; } function componentName(c) { return c.name; } function componentAffinity(c) { return c.affinity || "auto"; } + function componentParamTypes(c) { return (c && c._paramTypes) ? c._paramTypes : NIL; } + function componentSetParamTypes_b(c, t) { if (c) c._paramTypes = t; return NIL; } function macroParams(m) { return m.params; } function macroRestParam(m) { return m.restParam; } @@ -378,6 +380,7 @@ PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); }; PRIMITIVES["char-from-code"] = function(n) { return String.fromCharCode(n); }; PRIMITIVES["string-length"] = function(s) { return String(s).length; }; + var stringLength = PRIMITIVES["string-length"]; PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; }; PRIMITIVES["concat"] = function() { var out = []; @@ -768,12 +771,18 @@ // ========================================================================= // Continuation type (needed by CEK even without the tree-walk shift/reset extension) + // Continuations must be callable as JS functions so isCallable/apply work. if (typeof Continuation === "undefined") { - function Continuation(fn) { this.fn = fn; } - Continuation.prototype._continuation = true; - Continuation.prototype.call = function(value) { return this.fn(value !== undefined ? value : NIL); }; - PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; }; + function Continuation(fn) { + var c = function(value) { return fn(value !== undefined ? value : NIL); }; + c.fn = fn; + c._continuation = true; + c.call = function(value) { return fn(value !== undefined ? value : NIL); }; + return c; + } } + // Always register the predicate (may be overridden by continuations extension) + PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; }; // Standalone aliases for primitives used by cek.sx / frames.sx var inc = PRIMITIVES["inc"];