Fix lambda multi-body, reactive island demos, and add React is Hypermedia essay
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Lambda multi-body fix: sf-lambda used (nth args 1), dropping all but the first body expression. Fixed to collect all body expressions and wrap in (begin ...). This was foundational — every multi-expression lambda in every island silently dropped expressions after the first. Reactive islands: fix dom-parent marker timing (first effect run before marker is in DOM), fix :key eager evaluation, fix error boundary scope isolation, fix resource/suspense reactive cond tracking, fix inc not available as JS var. New essay: "React is Hypermedia" — argues that reactive islands are hypermedia controls whose behavior is specified in SX, not a departure from hypermedia. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -169,34 +169,42 @@
|
||||
(< (inc (get state "i")) (len args)))
|
||||
;; Keyword arg → attribute
|
||||
(let ((attr-name (keyword-name arg))
|
||||
(attr-val (trampoline
|
||||
(eval-expr
|
||||
(nth args (inc (get state "i")))
|
||||
env))))
|
||||
(attr-expr (nth args (inc (get state "i")))))
|
||||
(cond
|
||||
;; nil or false → skip
|
||||
(or (nil? attr-val) (= attr-val false))
|
||||
nil
|
||||
;; Event handler: on-click, on-submit, on-input, etc.
|
||||
;; Value must be callable (lambda/function)
|
||||
(and (starts-with? attr-name "on-")
|
||||
(callable? attr-val))
|
||||
(dom-listen el (slice attr-name 3) attr-val)
|
||||
;; Event handler: evaluate eagerly, bind listener
|
||||
(starts-with? attr-name "on-")
|
||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||
(when (callable? attr-val)
|
||||
(dom-listen el (slice attr-name 3) attr-val)))
|
||||
;; Two-way input binding: :bind signal
|
||||
(and (= attr-name "bind") (signal? attr-val))
|
||||
(bind-input el attr-val)
|
||||
(= attr-name "bind")
|
||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||
(when (signal? attr-val) (bind-input el attr-val)))
|
||||
;; ref: set ref.current to this element
|
||||
(= attr-name "ref")
|
||||
(dict-set! attr-val "current" el)
|
||||
;; Boolean attr
|
||||
(contains? BOOLEAN_ATTRS attr-name)
|
||||
(when attr-val (dom-set-attr el attr-name ""))
|
||||
;; true → empty attr
|
||||
(= attr-val true)
|
||||
(dom-set-attr el attr-name "")
|
||||
;; Normal attr
|
||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||
(dict-set! attr-val "current" el))
|
||||
;; key: reconciliation hint, evaluate eagerly (not reactive)
|
||||
(= attr-name "key")
|
||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||
(dom-set-attr el "key" (str attr-val)))
|
||||
;; Inside island scope: reactive attribute binding.
|
||||
;; The effect tracks signal deps automatically — if none
|
||||
;; are deref'd, it fires once and never again (safe).
|
||||
*island-scope*
|
||||
(reactive-attr el attr-name
|
||||
(fn () (trampoline (eval-expr attr-expr env))))
|
||||
;; Static attribute (outside islands)
|
||||
:else
|
||||
(dom-set-attr el attr-name (str attr-val)))
|
||||
(let ((attr-val (trampoline (eval-expr attr-expr env))))
|
||||
(cond
|
||||
(or (nil? attr-val) (= attr-val false)) nil
|
||||
(contains? BOOLEAN_ATTRS attr-name)
|
||||
(when attr-val (dom-set-attr el attr-name ""))
|
||||
(= attr-val true)
|
||||
(dom-set-attr el attr-name "")
|
||||
:else
|
||||
(dom-set-attr el attr-name (str attr-val)))))
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
|
||||
;; Positional arg → child
|
||||
@@ -319,32 +327,131 @@
|
||||
(define dispatch-render-form
|
||||
(fn (name expr env ns)
|
||||
(cond
|
||||
;; if
|
||||
;; if — reactive inside islands (re-renders when signal deps change)
|
||||
(= name "if")
|
||||
(let ((cond-val (trampoline (eval-expr (nth expr 1) env))))
|
||||
(if cond-val
|
||||
(render-to-dom (nth expr 2) env ns)
|
||||
(if (> (len expr) 3)
|
||||
(render-to-dom (nth expr 3) env ns)
|
||||
(create-fragment))))
|
||||
(if *island-scope*
|
||||
(let ((marker (create-comment "r-if"))
|
||||
(current-nodes (list))
|
||||
(initial-result nil))
|
||||
;; Effect runs synchronously on first call, tracking signal deps.
|
||||
;; On first run, store result in initial-result (marker has no parent yet).
|
||||
;; On subsequent runs, swap DOM nodes after marker.
|
||||
(effect (fn ()
|
||||
(let ((result (let ((cond-val (trampoline (eval-expr (nth expr 1) env))))
|
||||
(if cond-val
|
||||
(render-to-dom (nth expr 2) env ns)
|
||||
(if (> (len expr) 3)
|
||||
(render-to-dom (nth expr 3) env ns)
|
||||
(create-fragment))))))
|
||||
(if (dom-parent marker)
|
||||
;; Marker is in DOM — swap nodes
|
||||
(do
|
||||
(for-each (fn (n) (dom-remove n)) current-nodes)
|
||||
(set! current-nodes
|
||||
(if (dom-is-fragment? result)
|
||||
(dom-child-nodes result)
|
||||
(list result)))
|
||||
(dom-insert-after marker result))
|
||||
;; Marker not yet in DOM (first run) — just save result
|
||||
(set! initial-result result)))))
|
||||
;; Return fragment: marker + initial render result
|
||||
(let ((frag (create-fragment)))
|
||||
(dom-append frag marker)
|
||||
(when initial-result
|
||||
(set! current-nodes
|
||||
(if (dom-is-fragment? initial-result)
|
||||
(dom-child-nodes initial-result)
|
||||
(list initial-result)))
|
||||
(dom-append frag initial-result))
|
||||
frag))
|
||||
;; Static if
|
||||
(let ((cond-val (trampoline (eval-expr (nth expr 1) env))))
|
||||
(if cond-val
|
||||
(render-to-dom (nth expr 2) env ns)
|
||||
(if (> (len expr) 3)
|
||||
(render-to-dom (nth expr 3) env ns)
|
||||
(create-fragment)))))
|
||||
|
||||
;; when
|
||||
;; when — reactive inside islands
|
||||
(= name "when")
|
||||
(if (not (trampoline (eval-expr (nth expr 1) env)))
|
||||
(create-fragment)
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 2 (len expr)))
|
||||
frag))
|
||||
(if *island-scope*
|
||||
(let ((marker (create-comment "r-when"))
|
||||
(current-nodes (list))
|
||||
(initial-result nil))
|
||||
(effect (fn ()
|
||||
(if (dom-parent marker)
|
||||
;; In DOM — swap nodes
|
||||
(do
|
||||
(for-each (fn (n) (dom-remove n)) current-nodes)
|
||||
(set! current-nodes (list))
|
||||
(when (trampoline (eval-expr (nth expr 1) env))
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 2 (len expr)))
|
||||
(set! current-nodes (dom-child-nodes frag))
|
||||
(dom-insert-after marker frag))))
|
||||
;; First run — save result for fragment
|
||||
(when (trampoline (eval-expr (nth expr 1) env))
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 2 (len expr)))
|
||||
(set! current-nodes (dom-child-nodes frag))
|
||||
(set! initial-result frag))))))
|
||||
(let ((frag (create-fragment)))
|
||||
(dom-append frag marker)
|
||||
(when initial-result (dom-append frag initial-result))
|
||||
frag))
|
||||
;; Static when
|
||||
(if (not (trampoline (eval-expr (nth expr 1) env)))
|
||||
(create-fragment)
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 2 (len expr)))
|
||||
frag)))
|
||||
|
||||
;; cond
|
||||
;; cond — reactive inside islands
|
||||
(= name "cond")
|
||||
(let ((branch (eval-cond (rest expr) env)))
|
||||
(if branch
|
||||
(render-to-dom branch env ns)
|
||||
(create-fragment)))
|
||||
(if *island-scope*
|
||||
(let ((marker (create-comment "r-cond"))
|
||||
(current-nodes (list))
|
||||
(initial-result nil))
|
||||
(effect (fn ()
|
||||
(let ((branch (eval-cond (rest expr) env)))
|
||||
(if (dom-parent marker)
|
||||
;; In DOM — swap nodes
|
||||
(do
|
||||
(for-each (fn (n) (dom-remove n)) current-nodes)
|
||||
(set! current-nodes (list))
|
||||
(when branch
|
||||
(let ((result (render-to-dom branch env ns)))
|
||||
(set! current-nodes
|
||||
(if (dom-is-fragment? result)
|
||||
(dom-child-nodes result)
|
||||
(list result)))
|
||||
(dom-insert-after marker result))))
|
||||
;; First run — save result for fragment
|
||||
(when branch
|
||||
(let ((result (render-to-dom branch env ns)))
|
||||
(set! current-nodes
|
||||
(if (dom-is-fragment? result)
|
||||
(dom-child-nodes result)
|
||||
(list result)))
|
||||
(set! initial-result result)))))))
|
||||
(let ((frag (create-fragment)))
|
||||
(dom-append frag marker)
|
||||
(when initial-result (dom-append frag initial-result))
|
||||
frag))
|
||||
;; Static cond
|
||||
(let ((branch (eval-cond (rest expr) env)))
|
||||
(if branch
|
||||
(render-to-dom branch env ns)
|
||||
(create-fragment))))
|
||||
|
||||
;; case
|
||||
(= name "case")
|
||||
@@ -429,11 +536,11 @@
|
||||
|
||||
;; portal — render children into a remote target element
|
||||
(= name "portal")
|
||||
(render-dom-portal args env ns)
|
||||
(render-dom-portal (rest expr) env ns)
|
||||
|
||||
;; error-boundary — catch errors, render fallback
|
||||
(= name "error-boundary")
|
||||
(render-dom-error-boundary args env ns)
|
||||
(render-dom-error-boundary (rest expr) env ns)
|
||||
|
||||
;; for-each (render variant)
|
||||
(= name "for-each")
|
||||
@@ -620,9 +727,9 @@
|
||||
(key-order (list)))
|
||||
(dom-append container marker)
|
||||
(effect (fn ()
|
||||
(let ((parent (dom-parent marker))
|
||||
(items (deref items-sig)))
|
||||
(when parent
|
||||
(let ((items (deref items-sig)))
|
||||
(if (dom-parent marker)
|
||||
;; Marker in DOM: reconcile
|
||||
(let ((new-map (dict))
|
||||
(new-keys (list))
|
||||
(has-keys false))
|
||||
@@ -674,7 +781,17 @@
|
||||
|
||||
;; Update state for next render
|
||||
(set! key-map new-map)
|
||||
(set! key-order new-keys))))))
|
||||
(set! key-order new-keys))
|
||||
|
||||
;; First run (marker not in DOM yet): render initial items into container
|
||||
(for-each-indexed
|
||||
(fn (idx item)
|
||||
(let ((rendered (render-list-item map-fn item env ns))
|
||||
(key (extract-key rendered idx)))
|
||||
(dict-set! key-map key rendered)
|
||||
(append! key-order key)
|
||||
(dom-append container rendered)))
|
||||
items)))))
|
||||
container)))
|
||||
|
||||
|
||||
@@ -726,12 +843,10 @@
|
||||
(define render-dom-portal
|
||||
(fn (args env ns)
|
||||
(let ((selector (trampoline (eval-expr (first args) env)))
|
||||
(target (dom-query selector)))
|
||||
(target (or (dom-query selector)
|
||||
(dom-ensure-element selector))))
|
||||
(if (not target)
|
||||
;; Target not found — render nothing, log warning
|
||||
(do
|
||||
(log-warn (str "Portal target not found: " selector))
|
||||
(create-comment (str "portal: " selector " (not found)")))
|
||||
(create-comment (str "portal: " selector " (not found)"))
|
||||
(let ((marker (create-comment (str "portal: " selector)))
|
||||
(frag (create-fragment)))
|
||||
;; Render children into the fragment
|
||||
@@ -770,58 +885,49 @@
|
||||
(let ((fallback-expr (first args))
|
||||
(body-exprs (rest args))
|
||||
(container (dom-create-element "div" nil))
|
||||
(boundary-disposers (list)))
|
||||
;; retry-version: bump this signal to force re-render after fallback
|
||||
(retry-version (signal 0)))
|
||||
(dom-set-attr container "data-sx-boundary" "true")
|
||||
|
||||
;; Render body with its own island scope for disposal
|
||||
(let ((render-body
|
||||
(fn ()
|
||||
;; Dispose old boundary content
|
||||
(for-each (fn (d) (d)) boundary-disposers)
|
||||
(set! boundary-disposers (list))
|
||||
;; The entire body is rendered inside ONE effect + try-catch.
|
||||
;; Body renders WITHOUT *island-scope* so that if/when/cond use static
|
||||
;; paths — their signal reads become direct deref calls tracked by THIS
|
||||
;; effect. Errors from signal changes throw synchronously within try-catch.
|
||||
;; The error boundary's own effect handles all reactivity for its subtree.
|
||||
(effect (fn ()
|
||||
;; Touch retry-version so the effect re-runs when retry is called
|
||||
(deref retry-version)
|
||||
|
||||
;; Clear container
|
||||
(dom-set-prop container "innerHTML" "")
|
||||
;; Clear container
|
||||
(dom-set-prop container "innerHTML" "")
|
||||
|
||||
;; Try to render body
|
||||
(try-catch
|
||||
(fn ()
|
||||
;; Render body children, tracking disposers
|
||||
(with-island-scope
|
||||
(fn (disposable)
|
||||
(append! boundary-disposers disposable)
|
||||
(register-in-scope disposable))
|
||||
(fn ()
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (child)
|
||||
(dom-append frag (render-to-dom child env ns)))
|
||||
body-exprs)
|
||||
(dom-append container frag)))))
|
||||
(fn (err)
|
||||
;; Dispose any partially-created effects
|
||||
(for-each (fn (d) (d)) boundary-disposers)
|
||||
(set! boundary-disposers (list))
|
||||
;; Save and clear island scope BEFORE try-catch so it can be
|
||||
;; restored in both success and error paths.
|
||||
(let ((saved-scope *island-scope*))
|
||||
(set! *island-scope* nil)
|
||||
(try-catch
|
||||
(fn ()
|
||||
;; Body renders statically — signal reads tracked by THIS effect,
|
||||
;; throws propagate to our try-catch.
|
||||
(let ((frag (create-fragment)))
|
||||
(for-each
|
||||
(fn (child)
|
||||
(dom-append frag (render-to-dom child env ns)))
|
||||
body-exprs)
|
||||
(dom-append container frag))
|
||||
(set! *island-scope* saved-scope))
|
||||
(fn (err)
|
||||
;; Restore scope first, then render fallback
|
||||
(set! *island-scope* saved-scope)
|
||||
(let ((fallback-fn (trampoline (eval-expr fallback-expr env)))
|
||||
(retry-fn (fn () (swap! retry-version (fn (n) (+ n 1))))))
|
||||
(let ((fallback-dom
|
||||
(if (lambda? fallback-fn)
|
||||
(render-lambda-dom fallback-fn (list err retry-fn) env ns)
|
||||
(render-to-dom (apply fallback-fn (list err retry-fn)) env ns))))
|
||||
(dom-append container fallback-dom))))))))
|
||||
|
||||
;; Render fallback with error + retry
|
||||
(let ((fallback-fn (trampoline (eval-expr fallback-expr env)))
|
||||
(retry-fn (fn () (render-body))))
|
||||
(let ((fallback-dom
|
||||
(if (lambda? fallback-fn)
|
||||
(render-lambda-dom fallback-fn (list err retry-fn) env ns)
|
||||
(render-to-dom (apply fallback-fn (list err retry-fn)) env ns))))
|
||||
(dom-append container fallback-dom))))))))
|
||||
|
||||
;; Initial render
|
||||
(render-body)
|
||||
|
||||
;; Register boundary disposers with parent island scope
|
||||
(register-in-scope
|
||||
(fn ()
|
||||
(for-each (fn (d) (d)) boundary-disposers)
|
||||
(set! boundary-disposers (list))))
|
||||
|
||||
container))))
|
||||
container)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -102,9 +102,11 @@ class JSEmitter:
|
||||
# Map SX names to JS names
|
||||
return self._mangle(name)
|
||||
|
||||
def _mangle(self, name: str) -> str:
|
||||
"""Convert SX identifier to valid JS identifier."""
|
||||
RENAMES = {
|
||||
# Explicit SX→JS name mappings. Auto-mangle (kebab→camelCase, ?→_p, !→_b)
|
||||
# is only a fallback. Every platform symbol used in spec .sx files MUST have
|
||||
# an entry here — relying on auto-mangle is fragile and has caused runtime
|
||||
# errors (e.g. has-key? → hasKey_p instead of dictHas).
|
||||
RENAMES = {
|
||||
"nil": "NIL",
|
||||
"true": "true",
|
||||
"false": "false",
|
||||
@@ -328,6 +330,7 @@ class JSEmitter:
|
||||
"dom-listen": "domListen",
|
||||
"event-detail": "eventDetail",
|
||||
"dom-query": "domQuery",
|
||||
"dom-ensure-element": "domEnsureElement",
|
||||
"dom-query-all": "domQueryAll",
|
||||
"dom-tag-name": "domTagName",
|
||||
"create-comment": "createComment",
|
||||
@@ -338,6 +341,7 @@ class JSEmitter:
|
||||
"dom-get-data": "domGetData",
|
||||
"json-parse": "jsonParse",
|
||||
"dict-has?": "dictHas",
|
||||
"has-key?": "dictHas",
|
||||
"dict-delete!": "dictDelete",
|
||||
"process-bindings": "processBindings",
|
||||
"eval-cond": "evalCond",
|
||||
@@ -413,6 +417,7 @@ class JSEmitter:
|
||||
"promise-resolve": "promiseResolve",
|
||||
"promise-then": "promiseThen",
|
||||
"promise-catch": "promiseCatch",
|
||||
"promise-delayed": "promiseDelayed",
|
||||
"abort-previous": "abortPrevious",
|
||||
"track-controller": "trackController",
|
||||
"new-abort-controller": "newAbortController",
|
||||
@@ -590,9 +595,12 @@ class JSEmitter:
|
||||
"match-route": "matchRoute",
|
||||
"find-matching-route": "findMatchingRoute",
|
||||
"for-each-indexed": "forEachIndexed",
|
||||
}
|
||||
if name in RENAMES:
|
||||
return RENAMES[name]
|
||||
}
|
||||
|
||||
def _mangle(self, name: str) -> str:
|
||||
"""Convert SX identifier to valid JS identifier."""
|
||||
if name in self.RENAMES:
|
||||
return self.RENAMES[name]
|
||||
# General mangling: replace - with camelCase, ? with _p, ! with _b
|
||||
result = name
|
||||
if result.endswith("?"):
|
||||
@@ -2196,6 +2204,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
|
||||
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
|
||||
PRIMITIVES["zero?"] = function(n) { return n === 0; };
|
||||
PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; };
|
||||
''',
|
||||
|
||||
"core.strings": '''
|
||||
@@ -2219,6 +2228,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
|
||||
PRIMITIVES["substring"] = function(s, a, b) { return String(s).substring(a, b); };
|
||||
PRIMITIVES["string-length"] = function(s) { return String(s).length; };
|
||||
PRIMITIVES["string-contains?"] = function(s, sub) { return String(s).indexOf(String(sub)) !== -1; };
|
||||
PRIMITIVES["concat"] = function() {
|
||||
var out = [];
|
||||
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]);
|
||||
@@ -2254,6 +2264,12 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
PRIMITIVES["zip-pairs"] = function(c) {
|
||||
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
|
||||
};
|
||||
PRIMITIVES["reverse"] = function(c) { return Array.isArray(c) ? c.slice().reverse() : String(c).split("").reverse().join(""); };
|
||||
PRIMITIVES["flatten"] = function(c) {
|
||||
var out = [];
|
||||
function walk(a) { for (var i = 0; i < a.length; i++) Array.isArray(a[i]) ? walk(a[i]) : out.push(a[i]); }
|
||||
walk(c || []); return out;
|
||||
};
|
||||
''',
|
||||
|
||||
"core.dict": '''
|
||||
@@ -2276,6 +2292,7 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
return out;
|
||||
};
|
||||
PRIMITIVES["dict-set!"] = function(d, k, v) { d[k] = v; return v; };
|
||||
PRIMITIVES["has-key?"] = function(d, k) { return d !== null && d !== undefined && k in d; };
|
||||
PRIMITIVES["into"] = function(target, coll) {
|
||||
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
|
||||
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
|
||||
@@ -2906,6 +2923,20 @@ PLATFORM_DOM_JS = """
|
||||
return _hasDom ? document.querySelector(sel) : null;
|
||||
}
|
||||
|
||||
function domEnsureElement(sel) {
|
||||
if (!_hasDom) return null;
|
||||
var el = document.querySelector(sel);
|
||||
if (el) return el;
|
||||
// Parse #id selector → create div with that id, append to body
|
||||
if (sel.charAt(0) === '#') {
|
||||
el = document.createElement('div');
|
||||
el.id = sel.slice(1);
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function domQueryAll(root, sel) {
|
||||
if (!root || !root.querySelectorAll) return [];
|
||||
return Array.prototype.slice.call(root.querySelectorAll(sel));
|
||||
@@ -3039,6 +3070,12 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
|
||||
function promiseCatch(p, fn) { return p && p.catch ? p.catch(fn) : p; }
|
||||
|
||||
function promiseDelayed(ms, value) {
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(function() { resolve(value); }, ms);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Abort controllers ---
|
||||
|
||||
var _controllers = typeof WeakMap !== "undefined" ? new WeakMap() : null;
|
||||
@@ -3075,8 +3112,9 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
function clearTimeout_(id) { clearTimeout(id); }
|
||||
function clearInterval_(id) { clearInterval(id); }
|
||||
function requestAnimationFrame_(fn) {
|
||||
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(fn);
|
||||
else setTimeout(fn, 16);
|
||||
var cb = _wrapSxFn(fn);
|
||||
if (typeof requestAnimationFrame !== "undefined") requestAnimationFrame(cb);
|
||||
else setTimeout(cb, 16);
|
||||
}
|
||||
|
||||
// --- Fetch ---
|
||||
@@ -3475,14 +3513,19 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
function stopPropagation_(e) { if (e && e.stopPropagation) e.stopPropagation(); }
|
||||
function domFocus(el) { if (el && el.focus) el.focus(); }
|
||||
function tryCatch(tryFn, catchFn) {
|
||||
try { return tryFn(); } catch (e) { return catchFn(e); }
|
||||
var t = _wrapSxFn(tryFn);
|
||||
var c = catchFn && catchFn._lambda
|
||||
? function(e) { return trampoline(callLambda(catchFn, [e], lambdaClosure(catchFn))); }
|
||||
: catchFn;
|
||||
try { return t(); } catch (e) { return c(e); }
|
||||
}
|
||||
function errorMessage(e) {
|
||||
return e && e.message ? e.message : String(e);
|
||||
}
|
||||
function scheduleIdle(fn) {
|
||||
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(fn);
|
||||
else setTimeout(fn, 0);
|
||||
var cb = _wrapSxFn(fn);
|
||||
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(cb);
|
||||
else setTimeout(cb, 0);
|
||||
}
|
||||
function elementValue(el) { return el && el.value !== undefined ? el.value : NIL; }
|
||||
|
||||
@@ -4028,11 +4071,24 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False):
|
||||
PRIMITIVES["dom-listen"] = domListen;
|
||||
PRIMITIVES["dom-dispatch"] = domDispatch;
|
||||
PRIMITIVES["event-detail"] = eventDetail;
|
||||
PRIMITIVES["resource"] = resource;
|
||||
PRIMITIVES["promise-delayed"] = promiseDelayed;
|
||||
PRIMITIVES["promise-then"] = promiseThen;
|
||||
PRIMITIVES["def-store"] = defStore;
|
||||
PRIMITIVES["use-store"] = useStore;
|
||||
PRIMITIVES["emit-event"] = emitEvent;
|
||||
PRIMITIVES["on-event"] = onEvent;
|
||||
PRIMITIVES["bridge-event"] = bridgeEvent;''')
|
||||
PRIMITIVES["bridge-event"] = bridgeEvent;
|
||||
// DOM primitives for island code
|
||||
PRIMITIVES["dom-focus"] = domFocus;
|
||||
PRIMITIVES["dom-tag-name"] = domTagName;
|
||||
PRIMITIVES["dom-get-prop"] = domGetProp;
|
||||
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
||||
PRIMITIVES["error-message"] = errorMessage;
|
||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||
PRIMITIVES["invoke"] = invoke;
|
||||
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
||||
PRIMITIVES["filter"] = filter;''')
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
||||
@@ -476,7 +476,10 @@
|
||||
(define sf-lambda
|
||||
(fn (args env)
|
||||
(let ((params-expr (first args))
|
||||
(body (nth args 1))
|
||||
(body-exprs (rest args))
|
||||
(body (if (= (len body-exprs) 1)
|
||||
(first body-exprs)
|
||||
(cons (make-symbol "begin") body-exprs)))
|
||||
(param-names (map (fn (p)
|
||||
(if (= (type-of p) "symbol")
|
||||
(symbol-name p)
|
||||
|
||||
@@ -337,7 +337,32 @@
|
||||
(deftest "higher-order returns lambda"
|
||||
(let ((make-adder (fn (n) (fn (x) (+ n x)))))
|
||||
(let ((add5 (make-adder 5)))
|
||||
(assert-equal 8 (add5 3))))))
|
||||
(assert-equal 8 (add5 3)))))
|
||||
|
||||
(deftest "multi-body lambda returns last value"
|
||||
;; All body expressions must execute. Return value is the last.
|
||||
;; Catches: sf-lambda using nth(args,1) instead of rest(args).
|
||||
(let ((f (fn (x) (+ x 1) (+ x 2) (+ x 3))))
|
||||
(assert-equal 13 (f 10))))
|
||||
|
||||
(deftest "multi-body lambda side effects via dict mutation"
|
||||
;; Verify all body expressions run by mutating a shared dict.
|
||||
(let ((state (dict "a" 0 "b" 0)))
|
||||
(let ((f (fn ()
|
||||
(dict-set! state "a" 1)
|
||||
(dict-set! state "b" 2)
|
||||
"done")))
|
||||
(assert-equal "done" (f))
|
||||
(assert-equal 1 (get state "a"))
|
||||
(assert-equal 2 (get state "b")))))
|
||||
|
||||
(deftest "multi-body lambda two expressions"
|
||||
;; Simplest case: two body expressions, return value is second.
|
||||
(assert-equal 20
|
||||
((fn (x) (+ x 1) (* x 2)) 10))
|
||||
;; And with zero-arg lambda
|
||||
(assert-equal 42
|
||||
((fn () (+ 1 2) 42)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user