From 6772f1141ffaa54ed9e50a0bc90f8ae23a388ee1 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 7 Mar 2026 00:21:17 +0000 Subject: [PATCH] Register append! and dict-set! as proper primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously these mutating operations were internal helpers in the JS bootstrapper but not declared in primitives.sx or registered in the Python evaluator. Now properly specced and available in both hosts. Removes mock injections from cache tests — they use real primitives. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 6 ++++-- shared/sx/primitives.py | 12 ++++++++++++ shared/sx/ref/bootstrap_js.py | 4 +++- shared/sx/ref/primitives.sx | 10 ++++++++++ shared/sx/tests/test_page_data.py | 10 ---------- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index dbc2a17..398f705 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-07T00:04:07Z"; + var SX_VERSION = "2026-03-07T00:20:00Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -177,7 +177,8 @@ function envExtend(env) { return merge(env); } function envMerge(base, overlay) { return merge(base, overlay); } - function dictSet(d, k, v) { d[k] = v; } + function dictSet(d, k, v) { d[k] = v; return v; } + PRIMITIVES["dict-set!"] = dictSet; function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; } // Render-expression detection — lets the evaluator delegate to the active adapter. @@ -452,6 +453,7 @@ var range = PRIMITIVES["range"]; function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; } function append_b(arr, x) { arr.push(x); return arr; } + PRIMITIVES["append!"] = append_b; var apply = function(f, args) { return f.apply(null, args); }; // Additional primitive aliases used by adapter/engine transpiled code diff --git a/shared/sx/primitives.py b/shared/sx/primitives.py index cfc6d3d..cbd356d 100644 --- a/shared/sx/primitives.py +++ b/shared/sx/primitives.py @@ -386,6 +386,11 @@ def prim_cons(x: Any, coll: Any) -> list: def prim_append(coll: Any, x: Any) -> list: return list(coll) + [x] if coll else [x] +@register_primitive("append!") +def prim_append_mut(coll: Any, x: Any) -> list: + coll.append(x) + return coll + @register_primitive("chunk-every") def prim_chunk_every(coll: Any, n: Any) -> list: n = int(n) @@ -439,6 +444,13 @@ def prim_dissoc(d: Any, *keys_to_remove: Any) -> dict: result.pop(key, None) return result +@register_primitive("dict-set!") +def prim_dict_set_mut(d: Any, key: Any, val: Any) -> Any: + if isinstance(key, Keyword): + key = key.name + d[key] = val + return val + @register_primitive("into") def prim_into(target: Any, coll: Any) -> Any: if isinstance(target, list): diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index e1704ea..739b9a3 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -1711,7 +1711,8 @@ PLATFORM_JS_PRE = ''' function envExtend(env) { return merge(env); } function envMerge(base, overlay) { return merge(base, overlay); } - function dictSet(d, k, v) { d[k] = v; } + function dictSet(d, k, v) { d[k] = v; return v; } + PRIMITIVES["dict-set!"] = dictSet; function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; } // Render-expression detection — lets the evaluator delegate to the active adapter. @@ -1791,6 +1792,7 @@ PLATFORM_JS_POST = ''' var range = PRIMITIVES["range"]; function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; } function append_b(arr, x) { arr.push(x); return arr; } + PRIMITIVES["append!"] = append_b; var apply = function(f, args) { return f.apply(null, args); }; // Additional primitive aliases used by adapter/engine transpiled code diff --git a/shared/sx/ref/primitives.sx b/shared/sx/ref/primitives.sx index 91e2904..6035a66 100644 --- a/shared/sx/ref/primitives.sx +++ b/shared/sx/ref/primitives.sx @@ -384,6 +384,11 @@ :returns "list" :doc "Append x to end of coll (returns new list).") +(define-primitive "append!" + :params (coll x) + :returns "list" + :doc "Mutate coll by appending x in-place. Returns coll.") + (define-primitive "chunk-every" :params (coll n) :returns "list" @@ -426,6 +431,11 @@ :returns "dict" :doc "Return new dict with keys removed.") +(define-primitive "dict-set!" + :params (d key val) + :returns "any" + :doc "Mutate dict d by setting key to val in-place. Returns val.") + (define-primitive "into" :params (target coll) :returns "any" diff --git a/shared/sx/tests/test_page_data.py b/shared/sx/tests/test_page_data.py index d67d0ad..7cd2280 100644 --- a/shared/sx/tests/test_page_data.py +++ b/shared/sx/tests/test_page_data.py @@ -305,16 +305,6 @@ class TestDataCache: self._time = current_time_ms env["now-ms"] = lambda: self._time - # Mutating primitives needed by cache (available in JS, not bare Python) - def _dict_set(d, k, v): - d[k] = v - return v - def _append_b(lst, item): - lst.append(item) - return lst - env["dict-set!"] = _dict_set - env["append!"] = _append_b - # Define the cache functions from orchestration.sx cache_src = """ (define _page-data-cache (dict))