Merge worktree-typed into macros: deftype, defeffect, and effect annotations

This commit is contained in:
2026-03-11 23:24:37 +00:00
23 changed files with 1236 additions and 353 deletions

View File

@@ -14,7 +14,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-03-11T21:11:04Z"; var SX_VERSION = "2026-03-11T23:22:03Z";
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); }
@@ -729,10 +729,10 @@
var args = rest(expr); var args = rest(expr);
return (isSxTruthy(!isSxTruthy(sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list")))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() { return (isSxTruthy(!isSxTruthy(sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list")))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() {
var name = symbolName(head); var name = symbolName(head);
return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "letrec")) ? sfLetrec(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defisland")) ? sfDefisland(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() { return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "letrec")) ? sfLetrec(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defisland")) ? sfDefisland(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "deftype")) ? sfDeftype(args, env) : (isSxTruthy((name == "defeffect")) ? sfDefeffect(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() {
var mac = envGet(env, name); var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env); return makeThunk(expandMacro(mac, args, env), env);
})() : (isSxTruthy((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))))))); })() : (isSxTruthy((isSxTruthy(renderActiveP()) && isRenderExpr(expr))) ? renderExpr(expr, env) : evalCall(head, args, env)))))))))))))))))))))))))))))))))))))))));
})() : evalCall(head, args, env))); })() : evalCall(head, args, env)));
})(); }; })(); };
@@ -888,11 +888,22 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
// sf-define // sf-define
var sfDefine = function(args, env) { return (function() { var sfDefine = function(args, env) { return (function() {
var nameSym = first(args); var nameSym = first(args);
var value = trampoline(evalExpr(nth(args, 1), env)); var hasEffects = (isSxTruthy((len(args) >= 4)) && isSxTruthy((typeOf(nth(args, 1)) == "keyword")) && (keywordName(nth(args, 1)) == "effects"));
var valIdx = (isSxTruthy((isSxTruthy((len(args) >= 4)) && isSxTruthy((typeOf(nth(args, 1)) == "keyword")) && (keywordName(nth(args, 1)) == "effects"))) ? 3 : 1);
var value = trampoline(evalExpr(nth(args, valIdx), env));
if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) { if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) {
value.name = symbolName(nameSym); value.name = symbolName(nameSym);
} }
envSet(env, symbolName(nameSym), value); envSet(env, symbolName(nameSym), value);
if (isSxTruthy(hasEffects)) {
(function() {
var effectsRaw = nth(args, 2);
var effectList = (isSxTruthy((typeOf(effectsRaw) == "list")) ? map(function(e) { return (isSxTruthy((typeOf(e) == "symbol")) ? symbolName(e) : (String(e))); }, effectsRaw) : [(String(effectsRaw))]);
var effectAnns = (isSxTruthy(envHas(env, "*effect-annotations*")) ? envGet(env, "*effect-annotations*") : {});
effectAnns[symbolName(nameSym)] = effectList;
return envSet(env, "*effect-annotations*", effectAnns);
})();
}
return value; return value;
})(); }; })(); };
@@ -909,8 +920,17 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var affinity = defcompKwarg(args, "affinity", "auto"); var affinity = defcompKwarg(args, "affinity", "auto");
return (function() { return (function() {
var comp = makeComponent(compName, params, hasChildren, body, env, affinity); var comp = makeComponent(compName, params, hasChildren, body, env, affinity);
var effects = defcompKwarg(args, "effects", NIL);
if (isSxTruthy((isSxTruthy(!isSxTruthy(isNil(paramTypes))) && !isSxTruthy(isEmpty(keys(paramTypes)))))) { if (isSxTruthy((isSxTruthy(!isSxTruthy(isNil(paramTypes))) && !isSxTruthy(isEmpty(keys(paramTypes)))))) {
componentSetParamTypes_b(comp, paramTypes); componentSetParamTypes_b(comp, paramTypes);
}
if (isSxTruthy(!isSxTruthy(isNil(effects)))) {
(function() {
var effectList = (isSxTruthy((typeOf(effects) == "list")) ? map(function(e) { return (isSxTruthy((typeOf(e) == "symbol")) ? symbolName(e) : (String(e))); }, effects) : [(String(effects))]);
var effectAnns = (isSxTruthy(envHas(env, "*effect-annotations*")) ? envGet(env, "*effect-annotations*") : {});
effectAnns[symbolName(nameSym)] = effectList;
return envSet(env, "*effect-annotations*", effectAnns);
})();
} }
envSet(env, symbolName(nameSym), comp); envSet(env, symbolName(nameSym), comp);
return comp; return comp;
@@ -997,6 +1017,45 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
return value; return value;
})(); }; })(); };
// make-type-def
var makeTypeDef = function(name, params, body) { return {"name": name, "params": params, "body": body}; };
// normalize-type-body
var normalizeTypeBody = function(body) { return (isSxTruthy(isNil(body)) ? "nil" : (isSxTruthy((typeOf(body) == "symbol")) ? symbolName(body) : (isSxTruthy((typeOf(body) == "string")) ? body : (isSxTruthy((typeOf(body) == "keyword")) ? keywordName(body) : (isSxTruthy((typeOf(body) == "dict")) ? mapDict(function(k, v) { return normalizeTypeBody(v); }, body) : (isSxTruthy((typeOf(body) == "list")) ? (isSxTruthy(isEmpty(body)) ? "any" : (function() {
var head = first(body);
return (function() {
var headName = (isSxTruthy((typeOf(head) == "symbol")) ? symbolName(head) : (String(head)));
return (isSxTruthy((headName == "union")) ? cons("or", map(normalizeTypeBody, rest(body))) : cons(headName, map(normalizeTypeBody, rest(body))));
})();
})()) : (String(body)))))))); };
// sf-deftype
var sfDeftype = function(args, env) { return (function() {
var nameOrForm = first(args);
var bodyExpr = nth(args, 1);
var typeName = NIL;
var typeParams = [];
(isSxTruthy((typeOf(nameOrForm) == "symbol")) ? (typeName = symbolName(nameOrForm)) : (isSxTruthy((typeOf(nameOrForm) == "list")) ? ((typeName = symbolName(first(nameOrForm))), (typeParams = map(function(p) { return (isSxTruthy((typeOf(p) == "symbol")) ? symbolName(p) : (String(p))); }, rest(nameOrForm)))) : NIL));
return (function() {
var body = normalizeTypeBody(bodyExpr);
var registry = (isSxTruthy(envHas(env, "*type-registry*")) ? envGet(env, "*type-registry*") : {});
registry[typeName] = makeTypeDef(typeName, typeParams, body);
envSet(env, "*type-registry*", registry);
return NIL;
})();
})(); };
// sf-defeffect
var sfDefeffect = function(args, env) { return (function() {
var effectName = (isSxTruthy((typeOf(first(args)) == "symbol")) ? symbolName(first(args)) : (String(first(args))));
var registry = (isSxTruthy(envHas(env, "*effect-registry*")) ? envGet(env, "*effect-registry*") : []);
if (isSxTruthy(!isSxTruthy(contains(registry, effectName)))) {
registry.push(effectName);
}
envSet(env, "*effect-registry*", registry);
return NIL;
})(); };
// sf-begin // sf-begin
var sfBegin = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 0, (len(args) - 1))), makeThunk(last(args), env))); }; var sfBegin = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 0, (len(args) - 1))), makeThunk(last(args), env))); };
@@ -1156,7 +1215,7 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai
var BOOLEAN_ATTRS = ["async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "inert", "ismap", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected"]; var BOOLEAN_ATTRS = ["async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "inert", "ismap", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected"];
// definition-form? // definition-form?
var isDefinitionForm = function(name) { return sxOr((name == "define"), (name == "defcomp"), (name == "defisland"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler")); }; var isDefinitionForm = function(name) { return sxOr((name == "define"), (name == "defcomp"), (name == "defisland"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "deftype"), (name == "defeffect")); };
// parse-element-args // parse-element-args
var parseElementArgs = function(args, env) { return (function() { var parseElementArgs = function(args, env) { return (function() {
@@ -1366,7 +1425,7 @@ return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m =
var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); return escapeHtml((String(val))); })(); }; var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); return escapeHtml((String(val))); })(); };
// RENDER_HTML_FORMS // RENDER_HTML_FORMS
var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "map", "map-indexed", "filter", "for-each"]; var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defisland", "defmacro", "defstyle", "defhandler", "deftype", "defeffect", "map", "map-indexed", "filter", "for-each"];
// render-html-form? // render-html-form?
var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); }; var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); };
@@ -1574,7 +1633,7 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
})(); }; })(); };
// SPECIAL_FORM_NAMES // SPECIAL_FORM_NAMES
var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland"]; var SPECIAL_FORM_NAMES = ["if", "when", "cond", "case", "and", "or", "let", "let*", "lambda", "fn", "define", "defcomp", "defmacro", "defstyle", "defhandler", "defpage", "defquery", "defaction", "defrelation", "begin", "do", "quote", "quasiquote", "->", "set!", "letrec", "dynamic-wind", "defisland", "deftype", "defeffect"];
// HO_FORM_NAMES // HO_FORM_NAMES
var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"]; var HO_FORM_NAMES = ["map", "map-indexed", "filter", "reduce", "some", "every?", "for-each"];
@@ -1645,7 +1704,7 @@ return result; }, args);
return append_b(results, aser(lambdaBody(f), local)); return append_b(results, aser(lambdaBody(f), local));
})() : invoke(f, item)); } } })() : invoke(f, item)); } }
return (isSxTruthy(isEmpty(results)) ? NIL : results); return (isSxTruthy(isEmpty(results)) ? NIL : results);
})() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"))) ? (trampoline(evalExpr(expr, env)), NIL) : trampoline(evalExpr(expr, env))))))))))))))); })() : (isSxTruthy((name == "defisland")) ? (trampoline(evalExpr(expr, env)), serialize(expr)) : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defhandler"), (name == "defpage"), (name == "defquery"), (name == "defaction"), (name == "defrelation"), (name == "deftype"), (name == "defeffect"))) ? (trampoline(evalExpr(expr, env)), NIL) : trampoline(evalExpr(expr, env)))))))))))))));
})(); }; })(); };
// eval-case-aser // eval-case-aser
@@ -3999,20 +4058,12 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
function domCallMethod() { function domCallMethod() {
var obj = arguments[0], method = arguments[1]; var obj = arguments[0], method = arguments[1];
var args = Array.prototype.slice.call(arguments, 2); var args = Array.prototype.slice.call(arguments, 2);
console.log("[sx] dom-call-method:", obj, method, args);
if (obj && typeof obj[method] === 'function') { if (obj && typeof obj[method] === 'function') {
try { return obj[method].apply(obj, args); } try { return obj[method].apply(obj, args); }
catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; } catch(e) { console.error("[sx] dom-call-method error:", e); return NIL; }
} }
return NIL; console.warn("[sx] dom-call-method: method not found or obj null", obj, method);
}
// Post a message to an iframe's contentWindow without exposing the cross-origin
// Window object to the SX evaluator (which would trigger _thunk access errors).
function domPostMessage(iframe, msg, origin) {
try {
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(msg, origin || '*');
}
} catch(e) { console.error("[sx] domPostMessage error:", e); }
return NIL; return NIL;
} }
@@ -5221,7 +5272,6 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
PRIMITIVES["dom-get-prop"] = domGetProp; PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp; PRIMITIVES["dom-set-prop"] = domSetProp;
PRIMITIVES["dom-call-method"] = domCallMethod; PRIMITIVES["dom-call-method"] = domCallMethod;
PRIMITIVES["dom-post-message"] = domPostMessage;
PRIMITIVES["stop-propagation"] = stopPropagation_; PRIMITIVES["stop-propagation"] = stopPropagation_;
PRIMITIVES["error-message"] = errorMessage; PRIMITIVES["error-message"] = errorMessage;
PRIMITIVES["schedule-idle"] = scheduleIdle; PRIMITIVES["schedule-idle"] = scheduleIdle;

View File

@@ -40,7 +40,7 @@
;; Async HTML renderer ;; Async HTML renderer
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render (define-async async-render :effects [render io]
(fn (expr (env :as dict) ctx) (fn (expr (env :as dict) ctx)
(case (type-of expr) (case (type-of expr)
"nil" "" "nil" ""
@@ -56,7 +56,7 @@
:else (escape-html (str expr))))) :else (escape-html (str expr)))))
(define-async async-render-list (define-async async-render-list :effects [render io]
(fn (expr (env :as dict) ctx) (fn (expr (env :as dict) ctx)
(let ((head (first expr))) (let ((head (first expr)))
(if (not (= (type-of head) "symbol")) (if (not (= (type-of head) "symbol"))
@@ -138,7 +138,7 @@
;; async-render-raw — handle (raw! ...) in async context ;; async-render-raw — handle (raw! ...) in async context
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-raw (define-async async-render-raw :effects [render io]
(fn ((args :as list) (env :as dict) ctx) (fn ((args :as list) (env :as dict) ctx)
(let ((parts (list))) (let ((parts (list)))
(for-each (for-each
@@ -157,7 +157,7 @@
;; async-render-element — render an HTML element with async arg evaluation ;; async-render-element — render an HTML element with async arg evaluation
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-element (define-async async-render-element :effects [render io]
(fn ((tag :as string) (args :as list) (env :as dict) ctx) (fn ((tag :as string) (args :as list) (env :as dict) ctx)
(let ((attrs (dict)) (let ((attrs (dict))
(children (list))) (children (list)))
@@ -185,7 +185,7 @@
;; Uses for-each + mutable state instead of reduce, because the bootstrapper ;; Uses for-each + mutable state instead of reduce, because the bootstrapper
;; compiles inline for-each lambdas as for loops (which can contain await). ;; compiles inline for-each lambdas as for loops (which can contain await).
(define-async async-parse-element-args (define-async async-parse-element-args :effects [render io]
(fn ((args :as list) (attrs :as dict) (children :as list) (env :as dict) ctx) (fn ((args :as list) (attrs :as dict) (children :as list) (env :as dict) ctx)
(let ((skip false) (let ((skip false)
(i 0)) (i 0))
@@ -210,7 +210,7 @@
;; async-render-component — expand and render a component asynchronously ;; async-render-component — expand and render a component asynchronously
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-component (define-async async-render-component :effects [render io]
(fn ((comp :as component) (args :as list) (env :as dict) ctx) (fn ((comp :as component) (args :as list) (env :as dict) ctx)
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -232,7 +232,7 @@
;; async-render-island — SSR render of reactive island with hydration markers ;; async-render-island — SSR render of reactive island with hydration markers
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-island (define-async async-render-island :effects [render io]
(fn ((island :as island) (args :as list) (env :as dict) ctx) (fn ((island :as island) (args :as list) (env :as dict) ctx)
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -261,7 +261,7 @@
;; async-render-lambda — render lambda body in HTML context ;; async-render-lambda — render lambda body in HTML context
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-lambda (define-async async-render-lambda :effects [render io]
(fn ((f :as lambda) (args :as list) (env :as dict) ctx) (fn ((f :as lambda) (args :as list) (env :as dict) ctx)
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
(for-each-indexed (for-each-indexed
@@ -274,7 +274,7 @@
;; async-parse-kw-args — parse keyword args and children with async eval ;; async-parse-kw-args — parse keyword args and children with async eval
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-parse-kw-args (define-async async-parse-kw-args :effects [render io]
(fn ((args :as list) (kwargs :as dict) (children :as list) (env :as dict) ctx) (fn ((args :as list) (kwargs :as dict) (children :as list) (env :as dict) ctx)
(let ((skip false) (let ((skip false)
(i 0)) (i 0))
@@ -300,7 +300,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Bootstrapper emits this as: [await async_render(x, env, ctx) for x in exprs] ;; Bootstrapper emits this as: [await async_render(x, env, ctx) for x in exprs]
(define-async async-map-render (define-async async-map-render :effects [render io]
(fn ((exprs :as list) (env :as dict) ctx) (fn ((exprs :as list) (env :as dict) ctx)
(let ((results (list))) (let ((results (list)))
(for-each (for-each
@@ -316,9 +316,10 @@
(define ASYNC_RENDER_FORMS (define ASYNC_RENDER_FORMS
(list "if" "when" "cond" "case" "let" "let*" "begin" "do" (list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"deftype" "defeffect"
"map" "map-indexed" "filter" "for-each")) "map" "map-indexed" "filter" "for-each"))
(define async-render-form? (define async-render-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
(contains? ASYNC_RENDER_FORMS name))) (contains? ASYNC_RENDER_FORMS name)))
@@ -330,7 +331,7 @@
;; Uses cond-scheme? from eval.sx (the FIXED version with every? check) ;; Uses cond-scheme? from eval.sx (the FIXED version with every? check)
;; and eval-cond from render.sx for correct scheme/clojure classification. ;; and eval-cond from render.sx for correct scheme/clojure classification.
(define-async dispatch-async-render-form (define-async dispatch-async-render-form :effects [render io]
(fn ((name :as string) expr (env :as dict) ctx) (fn ((name :as string) expr (env :as dict) ctx)
(cond (cond
;; if ;; if
@@ -406,7 +407,7 @@
;; async-render-cond-scheme — scheme-style cond for render mode ;; async-render-cond-scheme — scheme-style cond for render mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-cond-scheme (define-async async-render-cond-scheme :effects [render io]
(fn ((clauses :as list) (env :as dict) ctx) (fn ((clauses :as list) (env :as dict) ctx)
(if (empty? clauses) (if (empty? clauses)
"" ""
@@ -428,7 +429,7 @@
;; async-render-cond-clojure — clojure-style cond for render mode ;; async-render-cond-clojure — clojure-style cond for render mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-render-cond-clojure (define-async async-render-cond-clojure :effects [render io]
(fn ((clauses :as list) (env :as dict) ctx) (fn ((clauses :as list) (env :as dict) ctx)
(if (< (len clauses) 2) (if (< (len clauses) 2)
"" ""
@@ -448,7 +449,7 @@
;; async-process-bindings — evaluate let-bindings asynchronously ;; async-process-bindings — evaluate let-bindings asynchronously
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-process-bindings (define-async async-process-bindings :effects [render io]
(fn (bindings (env :as dict) ctx) (fn (bindings (env :as dict) ctx)
;; env-extend (not merge) — Env is not a dict subclass, so merge() ;; env-extend (not merge) — Env is not a dict subclass, so merge()
;; returns an empty dict, losing all parent scope bindings. ;; returns an empty dict, losing all parent scope bindings.
@@ -469,7 +470,7 @@
local))) local)))
(define-async async-process-bindings-flat (define-async async-process-bindings-flat :effects [render io]
(fn ((bindings :as list) (local :as dict) ctx) (fn ((bindings :as list) (local :as dict) ctx)
(let ((skip false) (let ((skip false)
(i 0)) (i 0))
@@ -494,7 +495,7 @@
;; async-map-fn-render — map a lambda/callable over collection for render ;; async-map-fn-render — map a lambda/callable over collection for render
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-map-fn-render (define-async async-map-fn-render :effects [render io]
(fn (f (coll :as list) (env :as dict) ctx) (fn (f (coll :as list) (env :as dict) ctx)
(let ((results (list))) (let ((results (list)))
(for-each (for-each
@@ -511,7 +512,7 @@
;; async-map-indexed-fn-render — map-indexed variant for render ;; async-map-indexed-fn-render — map-indexed variant for render
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-map-indexed-fn-render (define-async async-map-indexed-fn-render :effects [render io]
(fn (f (coll :as list) (env :as dict) ctx) (fn (f (coll :as list) (env :as dict) ctx)
(let ((results (list)) (let ((results (list))
(i 0)) (i 0))
@@ -530,7 +531,7 @@
;; async-invoke — call a native callable, await if coroutine ;; async-invoke — call a native callable, await if coroutine
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-invoke (define-async async-invoke :effects [io]
(fn (f &rest args) (fn (f &rest args)
(let ((r (apply f args))) (let ((r (apply f args)))
(if (async-coroutine? r) (if (async-coroutine? r)
@@ -542,7 +543,7 @@
;; Async SX wire format (aser) ;; Async SX wire format (aser)
;; ========================================================================== ;; ==========================================================================
(define-async async-aser (define-async async-aser :effects [render io]
(fn (expr (env :as dict) ctx) (fn (expr (env :as dict) ctx)
(case (type-of expr) (case (type-of expr)
"number" expr "number" expr
@@ -572,7 +573,7 @@
:else expr))) :else expr)))
(define-async async-aser-dict (define-async async-aser-dict :effects [render io]
(fn ((expr :as dict) (env :as dict) ctx) (fn ((expr :as dict) (env :as dict) ctx)
(let ((result (dict))) (let ((result (dict)))
(for-each (for-each
@@ -586,7 +587,7 @@
;; async-aser-list — dispatch on list head for aser mode ;; async-aser-list — dispatch on list head for aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-list (define-async async-aser-list :effects [render io]
(fn (expr (env :as dict) ctx) (fn (expr (env :as dict) ctx)
(let ((head (first expr)) (let ((head (first expr))
(args (rest expr))) (args (rest expr)))
@@ -665,7 +666,7 @@
;; async-aser-eval-call — evaluate a function call fully in aser mode ;; async-aser-eval-call — evaluate a function call fully in aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-eval-call (define-async async-aser-eval-call :effects [render io]
(fn (head (args :as list) (env :as dict) ctx) (fn (head (args :as list) (env :as dict) ctx)
(let ((f (async-eval head env ctx)) (let ((f (async-eval head env ctx))
(evaled-args (async-eval-args args env ctx))) (evaled-args (async-eval-args args env ctx)))
@@ -693,7 +694,7 @@
;; async-eval-args — evaluate a list of args asynchronously ;; async-eval-args — evaluate a list of args asynchronously
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-eval-args (define-async async-eval-args :effects [io]
(fn ((args :as list) (env :as dict) ctx) (fn ((args :as list) (env :as dict) ctx)
(let ((results (list))) (let ((results (list)))
(for-each (for-each
@@ -706,7 +707,7 @@
;; async-aser-map-list — aser each element of a list ;; async-aser-map-list — aser each element of a list
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-map-list (define-async async-aser-map-list :effects [render io]
(fn ((exprs :as list) (env :as dict) ctx) (fn ((exprs :as list) (env :as dict) ctx)
(let ((results (list))) (let ((results (list)))
(for-each (for-each
@@ -719,7 +720,7 @@
;; async-aser-fragment — serialize (<> child1 child2 ...) in aser mode ;; async-aser-fragment — serialize (<> child1 child2 ...) in aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-fragment (define-async async-aser-fragment :effects [render io]
(fn ((children :as list) (env :as dict) ctx) (fn ((children :as list) (env :as dict) ctx)
(let ((parts (list))) (let ((parts (list)))
(for-each (for-each
@@ -743,7 +744,7 @@
;; async-aser-component — expand component server-side in aser mode ;; async-aser-component — expand component server-side in aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-component (define-async async-aser-component :effects [render io]
(fn ((comp :as component) (args :as list) (env :as dict) ctx) (fn ((comp :as component) (args :as list) (env :as dict) ctx)
(let ((kwargs (dict)) (let ((kwargs (dict))
(children (list))) (children (list)))
@@ -775,7 +776,7 @@
;; async-parse-aser-kw-args — parse keyword args for aser mode ;; async-parse-aser-kw-args — parse keyword args for aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-parse-aser-kw-args (define-async async-parse-aser-kw-args :effects [render io]
(fn ((args :as list) (kwargs :as dict) (children :as list) (env :as dict) ctx) (fn ((args :as list) (kwargs :as dict) (children :as list) (env :as dict) ctx)
(let ((skip false) (let ((skip false)
(i 0)) (i 0))
@@ -800,7 +801,7 @@
;; async-aser-call — serialize an SX call (tag or component) in aser mode ;; async-aser-call — serialize an SX call (tag or component) in aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-call (define-async async-aser-call :effects [render io]
(fn ((name :as string) (args :as list) (env :as dict) ctx) (fn ((name :as string) (args :as list) (env :as dict) ctx)
(let ((token (if (or (= name "svg") (= name "math")) (let ((token (if (or (= name "svg") (= name "math"))
(svg-context-set! true) (svg-context-set! true)
@@ -853,12 +854,13 @@
"let" "let*" "lambda" "fn" "let" "let*" "lambda" "fn"
"define" "defcomp" "defmacro" "defstyle" "define" "defcomp" "defmacro" "defstyle"
"defhandler" "defpage" "defquery" "defaction" "defhandler" "defpage" "defquery" "defaction"
"begin" "do" "quote" "->" "set!" "defisland")) "begin" "do" "quote" "->" "set!" "defisland"
"deftype" "defeffect"))
(define ASYNC_ASER_HO_NAMES (define ASYNC_ASER_HO_NAMES
(list "map" "map-indexed" "filter" "for-each")) (list "map" "map-indexed" "filter" "for-each"))
(define async-aser-form? (define async-aser-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
(or (contains? ASYNC_ASER_FORM_NAMES name) (or (contains? ASYNC_ASER_FORM_NAMES name)
(contains? ASYNC_ASER_HO_NAMES name)))) (contains? ASYNC_ASER_HO_NAMES name))))
@@ -870,7 +872,7 @@
;; ;;
;; Uses cond-scheme? from eval.sx (the FIXED version with every? check). ;; Uses cond-scheme? from eval.sx (the FIXED version with every? check).
(define-async dispatch-async-aser-form (define-async dispatch-async-aser-form :effects [render io]
(fn ((name :as string) expr (env :as dict) ctx) (fn ((name :as string) expr (env :as dict) ctx)
(let ((args (rest expr))) (let ((args (rest expr)))
(cond (cond
@@ -987,7 +989,8 @@
;; Definition forms — evaluate for side effects ;; Definition forms — evaluate for side effects
(or (= name "define") (= name "defcomp") (= name "defmacro") (or (= name "define") (= name "defcomp") (= name "defmacro")
(= name "defstyle") (= name "defhandler") (= name "defpage") (= name "defstyle") (= name "defhandler") (= name "defpage")
(= name "defquery") (= name "defaction")) (= name "defquery") (= name "defaction")
(= name "deftype") (= name "defeffect"))
(do (async-eval expr env ctx) nil) (do (async-eval expr env ctx) nil)
;; Fallback ;; Fallback
@@ -999,7 +1002,7 @@
;; async-aser-cond-scheme — scheme-style cond for aser mode ;; async-aser-cond-scheme — scheme-style cond for aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-cond-scheme (define-async async-aser-cond-scheme :effects [render io]
(fn ((clauses :as list) (env :as dict) ctx) (fn ((clauses :as list) (env :as dict) ctx)
(if (empty? clauses) (if (empty? clauses)
nil nil
@@ -1021,7 +1024,7 @@
;; async-aser-cond-clojure — clojure-style cond for aser mode ;; async-aser-cond-clojure — clojure-style cond for aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-cond-clojure (define-async async-aser-cond-clojure :effects [render io]
(fn ((clauses :as list) (env :as dict) ctx) (fn ((clauses :as list) (env :as dict) ctx)
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil
@@ -1041,7 +1044,7 @@
;; async-aser-case-loop — case dispatch for aser mode ;; async-aser-case-loop — case dispatch for aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-case-loop (define-async async-aser-case-loop :effects [render io]
(fn (match-val (clauses :as list) (env :as dict) ctx) (fn (match-val (clauses :as list) (env :as dict) ctx)
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil
@@ -1061,7 +1064,7 @@
;; async-aser-thread-first — -> form in aser mode ;; async-aser-thread-first — -> form in aser mode
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-thread-first (define-async async-aser-thread-first :effects [render io]
(fn ((args :as list) (env :as dict) ctx) (fn ((args :as list) (env :as dict) ctx)
(let ((result (async-eval (first args) env ctx))) (let ((result (async-eval (first args) env ctx)))
(for-each (for-each
@@ -1081,7 +1084,7 @@
;; async-invoke-or-lambda — invoke a callable or lambda with args ;; async-invoke-or-lambda — invoke a callable or lambda with args
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-invoke-or-lambda (define-async async-invoke-or-lambda :effects [render io]
(fn (f (args :as list) (env :as dict) ctx) (fn (f (args :as list) (env :as dict) ctx)
(cond (cond
(and (callable? f) (not (lambda? f)) (not (component? f))) (and (callable? f) (not (lambda? f)) (not (component? f)))
@@ -1103,7 +1106,7 @@
;; Async aser HO forms (map, map-indexed, for-each) ;; Async aser HO forms (map, map-indexed, for-each)
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define-async async-aser-ho-map (define-async async-aser-ho-map :effects [render io]
(fn ((args :as list) (env :as dict) ctx) (fn ((args :as list) (env :as dict) ctx)
(let ((f (async-eval (first args) env ctx)) (let ((f (async-eval (first args) env ctx))
(coll (async-eval (nth args 1) env ctx)) (coll (async-eval (nth args 1) env ctx))
@@ -1119,7 +1122,7 @@
results))) results)))
(define-async async-aser-ho-map-indexed (define-async async-aser-ho-map-indexed :effects [render io]
(fn ((args :as list) (env :as dict) ctx) (fn ((args :as list) (env :as dict) ctx)
(let ((f (async-eval (first args) env ctx)) (let ((f (async-eval (first args) env ctx))
(coll (async-eval (nth args 1) env ctx)) (coll (async-eval (nth args 1) env ctx))
@@ -1138,7 +1141,7 @@
results))) results)))
(define-async async-aser-ho-for-each (define-async async-aser-ho-for-each :effects [render io]
(fn ((args :as list) (env :as dict) ctx) (fn ((args :as list) (env :as dict) ctx)
(let ((f (async-eval (first args) env ctx)) (let ((f (async-eval (first args) env ctx))
(coll (async-eval (nth args 1) env ctx)) (coll (async-eval (nth args 1) env ctx))
@@ -1169,7 +1172,7 @@
;; (sx-expr? x) — check if SxExpr ;; (sx-expr? x) — check if SxExpr
;; (set-expand-components!) — enable component expansion context var ;; (set-expand-components!) — enable component expansion context var
(define-async async-eval-slot-inner (define-async async-eval-slot-inner :effects [render io]
(fn (expr (env :as dict) ctx) (fn (expr (env :as dict) ctx)
;; NOTE: Uses statement-form let + set! to avoid expression-context ;; NOTE: Uses statement-form let + set! to avoid expression-context
;; let (IIFE lambdas) which can't contain await in Python. ;; let (IIFE lambdas) which can't contain await in Python.
@@ -1195,7 +1198,7 @@
(make-sx-expr (serialize result)))))))) (make-sx-expr (serialize result))))))))
(define-async async-maybe-expand-result (define-async async-maybe-expand-result :effects [render io]
(fn (result (env :as dict) ctx) (fn (result (env :as dict) ctx)
;; If the aser result is a component call string like "(~foo ...)", ;; If the aser result is a component call string like "(~foo ...)",
;; re-parse and expand it. This handles indirect component references ;; re-parse and expand it. This handles indirect component references

View File

@@ -18,7 +18,7 @@
;; render-to-dom — main entry point ;; render-to-dom — main entry point
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-to-dom (define render-to-dom :effects [render]
(fn (expr (env :as dict) (ns :as string)) (fn (expr (env :as dict) (ns :as string))
(set-render-active! true) (set-render-active! true)
(case (type-of expr) (case (type-of expr)
@@ -66,7 +66,7 @@
;; render-dom-list — dispatch on list head ;; render-dom-list — dispatch on list head
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-list (define render-dom-list :effects [render]
(fn (expr (env :as dict) (ns :as string)) (fn (expr (env :as dict) (ns :as string))
(let ((head (first expr))) (let ((head (first expr)))
(cond (cond
@@ -165,7 +165,7 @@
;; render-dom-element — create a DOM element with attrs and children ;; render-dom-element — create a DOM element with attrs and children
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-element (define render-dom-element :effects [render]
(fn ((tag :as string) (args :as list) (env :as dict) (ns :as string)) (fn ((tag :as string) (args :as list) (env :as dict) (ns :as string))
;; Detect namespace from tag ;; Detect namespace from tag
(let ((new-ns (cond (= tag "svg") SVG_NS (let ((new-ns (cond (= tag "svg") SVG_NS
@@ -236,7 +236,7 @@
;; render-dom-component — expand and render a component ;; render-dom-component — expand and render a component
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-component (define render-dom-component :effects [render]
(fn ((comp :as component) (args :as list) (env :as dict) (ns :as string)) (fn ((comp :as component) (args :as list) (env :as dict) (ns :as string))
;; Parse kwargs and children, bind into component env, render body. ;; Parse kwargs and children, bind into component env, render body.
(let ((kwargs (dict)) (let ((kwargs (dict))
@@ -283,7 +283,7 @@
;; render-dom-fragment — render children into a DocumentFragment ;; render-dom-fragment — render children into a DocumentFragment
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-fragment (define render-dom-fragment :effects [render]
(fn ((args :as list) (env :as dict) (ns :as string)) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((frag (create-fragment))) (let ((frag (create-fragment)))
(for-each (for-each
@@ -296,7 +296,7 @@
;; render-dom-raw — insert unescaped content ;; render-dom-raw — insert unescaped content
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-raw (define render-dom-raw :effects [render]
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
(let ((frag (create-fragment))) (let ((frag (create-fragment)))
(for-each (for-each
@@ -317,7 +317,7 @@
;; render-dom-unknown-component — visible warning element ;; render-dom-unknown-component — visible warning element
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-dom-unknown-component (define render-dom-unknown-component :effects [render]
(fn ((name :as string)) (fn ((name :as string))
(error (str "Unknown component: " name)))) (error (str "Unknown component: " name))))
@@ -334,11 +334,11 @@
"map" "map-indexed" "filter" "for-each" "portal" "map" "map-indexed" "filter" "for-each" "portal"
"error-boundary")) "error-boundary"))
(define render-dom-form? (define render-dom-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
(contains? RENDER_DOM_FORMS name))) (contains? RENDER_DOM_FORMS name)))
(define dispatch-render-form (define dispatch-render-form :effects [render]
(fn ((name :as string) expr (env :as dict) (ns :as string)) (fn ((name :as string) expr (env :as dict) (ns :as string))
(cond (cond
;; if — reactive inside islands (re-renders when signal deps change) ;; if — reactive inside islands (re-renders when signal deps change)
@@ -580,7 +580,7 @@
;; render-lambda-dom — render a lambda body in DOM context ;; render-lambda-dom — render a lambda body in DOM context
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-lambda-dom (define render-lambda-dom :effects [render]
(fn ((f :as lambda) (args :as list) (env :as dict) (ns :as string)) (fn ((f :as lambda) (args :as list) (env :as dict) (ns :as string))
;; Bind lambda params and render body as DOM ;; Bind lambda params and render body as DOM
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
@@ -604,7 +604,7 @@
;; - Attribute bindings: (deref sig) in attr → reactive attribute ;; - Attribute bindings: (deref sig) in attr → reactive attribute
;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide ;; - Conditional fragments: (when (deref sig) ...) → reactive show/hide
(define render-dom-island (define render-dom-island :effects [render mutation]
(fn ((island :as island) (args :as list) (env :as dict) (ns :as string)) (fn ((island :as island) (args :as list) (env :as dict) (ns :as string))
;; Parse kwargs and children (same as component) ;; Parse kwargs and children (same as component)
(let ((kwargs (dict)) (let ((kwargs (dict))
@@ -678,7 +678,7 @@
;; ;;
;; Supports :tag keyword to change wrapper element (default "div"). ;; Supports :tag keyword to change wrapper element (default "div").
(define render-dom-lake (define render-dom-lake :effects [render]
(fn ((args :as list) (env :as dict) (ns :as string)) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((lake-id nil) (let ((lake-id nil)
(lake-tag "div") (lake-tag "div")
@@ -722,7 +722,7 @@
;; Renders as <div data-sx-marsh="name">children</div>. ;; Renders as <div data-sx-marsh="name">children</div>.
;; Stores the island env and transform on the element for morph retrieval. ;; Stores the island env and transform on the element for morph retrieval.
(define render-dom-marsh (define render-dom-marsh :effects [render]
(fn ((args :as list) (env :as dict) (ns :as string)) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((marsh-id nil) (let ((marsh-id nil)
(marsh-tag "div") (marsh-tag "div")
@@ -769,7 +769,7 @@
;; reactive-text — create a text node bound to a signal ;; reactive-text — create a text node bound to a signal
;; Used when (deref sig) appears in a text position inside an island. ;; Used when (deref sig) appears in a text position inside an island.
(define reactive-text (define reactive-text :effects [render mutation]
(fn (sig) (fn (sig)
(let ((node (create-text-node (str (deref sig))))) (let ((node (create-text-node (str (deref sig)))))
(effect (fn () (effect (fn ()
@@ -780,7 +780,7 @@
;; Used when an attribute value contains (deref sig) inside an island. ;; Used when an attribute value contains (deref sig) inside an island.
;; Marks the attribute name on the element via data-sx-reactive-attrs so ;; Marks the attribute name on the element via data-sx-reactive-attrs so
;; the morph algorithm knows not to overwrite it with server content. ;; the morph algorithm knows not to overwrite it with server content.
(define reactive-attr (define reactive-attr :effects [render mutation]
(fn (el (attr-name :as string) (compute-fn :as lambda)) (fn (el (attr-name :as string) (compute-fn :as lambda))
;; Mark this attribute as reactively managed ;; Mark this attribute as reactively managed
(let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") "")) (let ((existing (or (dom-get-attr el "data-sx-reactive-attrs") ""))
@@ -801,7 +801,7 @@
;; reactive-fragment — conditionally render a fragment based on a signal ;; reactive-fragment — conditionally render a fragment based on a signal
;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island. ;; Used for (when (deref sig) ...) or (if (deref sig) ...) inside an island.
(define reactive-fragment (define reactive-fragment :effects [render mutation]
(fn ((test-fn :as lambda) (render-fn :as lambda) (env :as dict) (ns :as string)) (fn ((test-fn :as lambda) (render-fn :as lambda) (env :as dict) (ns :as string))
(let ((marker (create-comment "island-fragment")) (let ((marker (create-comment "island-fragment"))
(current-nodes (list))) (current-nodes (list)))
@@ -823,13 +823,13 @@
;; existing DOM nodes are reused across updates. Only additions, removals, ;; existing DOM nodes are reused across updates. Only additions, removals,
;; and reorderings touch the DOM. Without keys, falls back to clear+rerender. ;; and reorderings touch the DOM. Without keys, falls back to clear+rerender.
(define render-list-item (define render-list-item :effects [render]
(fn ((map-fn :as lambda) item (env :as dict) (ns :as string)) (fn ((map-fn :as lambda) item (env :as dict) (ns :as string))
(if (lambda? map-fn) (if (lambda? map-fn)
(render-lambda-dom map-fn (list item) env ns) (render-lambda-dom map-fn (list item) env ns)
(render-to-dom (apply map-fn (list item)) env ns)))) (render-to-dom (apply map-fn (list item)) env ns))))
(define extract-key (define extract-key :effects [render]
(fn (node (index :as number)) (fn (node (index :as number))
;; Extract key from rendered node: :key attr, data-key, or index fallback ;; Extract key from rendered node: :key attr, data-key, or index fallback
(let ((k (dom-get-attr node "key"))) (let ((k (dom-get-attr node "key")))
@@ -838,7 +838,7 @@
(let ((dk (dom-get-data node "key"))) (let ((dk (dom-get-data node "key")))
(if dk (str dk) (str "__idx_" index))))))) (if dk (str dk) (str "__idx_" index)))))))
(define reactive-list (define reactive-list :effects [render mutation]
(fn ((map-fn :as lambda) (items-sig :as signal) (env :as dict) (ns :as string)) (fn ((map-fn :as lambda) (items-sig :as signal) (env :as dict) (ns :as string))
(let ((container (create-fragment)) (let ((container (create-fragment))
(marker (create-comment "island-list")) (marker (create-comment "island-list"))
@@ -924,7 +924,7 @@
;; ;;
;; Handles: input[text/number/email/...], textarea, select, checkbox, radio ;; Handles: input[text/number/email/...], textarea, select, checkbox, radio
(define bind-input (define bind-input :effects [render mutation]
(fn (el (sig :as signal)) (fn (el (sig :as signal))
(let ((input-type (lower (or (dom-get-attr el "type") ""))) (let ((input-type (lower (or (dom-get-attr el "type") "")))
(is-checkbox (or (= input-type "checkbox") (is-checkbox (or (= input-type "checkbox")
@@ -959,7 +959,7 @@
;; position. Registers a disposer to clean up portal content on island ;; position. Registers a disposer to clean up portal content on island
;; teardown. ;; teardown.
(define render-dom-portal (define render-dom-portal :effects [render]
(fn ((args :as list) (env :as dict) (ns :as string)) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((selector (trampoline (eval-expr (first args) env))) (let ((selector (trampoline (eval-expr (first args) env)))
(target (or (dom-query selector) (target (or (dom-query selector)
@@ -999,7 +999,7 @@
;; (fn (err retry) ...) ;; (fn (err retry) ...)
;; Calling (retry) re-renders the body, replacing the fallback. ;; Calling (retry) re-renders the body, replacing the fallback.
(define render-dom-error-boundary (define render-dom-error-boundary :effects [render]
(fn ((args :as list) (env :as dict) (ns :as string)) (fn ((args :as list) (env :as dict) (ns :as string))
(let ((fallback-expr (first args)) (let ((fallback-expr (first args))
(body-exprs (rest args)) (body-exprs (rest args))

View File

@@ -13,7 +13,7 @@
;; ========================================================================== ;; ==========================================================================
(define render-to-html (define render-to-html :effects [render]
(fn (expr (env :as dict)) (fn (expr (env :as dict))
(set-render-active! true) (set-render-active! true)
(case (type-of expr) (case (type-of expr)
@@ -33,7 +33,7 @@
;; Everything else — evaluate first ;; Everything else — evaluate first
:else (render-value-to-html (trampoline (eval-expr expr env)) env)))) :else (render-value-to-html (trampoline (eval-expr expr env)) env))))
(define render-value-to-html (define render-value-to-html :effects [render]
(fn (val (env :as dict)) (fn (val (env :as dict))
(case (type-of val) (case (type-of val)
"nil" "" "nil" ""
@@ -52,9 +52,10 @@
(define RENDER_HTML_FORMS (define RENDER_HTML_FORMS
(list "if" "when" "cond" "case" "let" "let*" "begin" "do" (list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler" "define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
"deftype" "defeffect"
"map" "map-indexed" "filter" "for-each")) "map" "map-indexed" "filter" "for-each"))
(define render-html-form? (define render-html-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
(contains? RENDER_HTML_FORMS name))) (contains? RENDER_HTML_FORMS name)))
@@ -63,7 +64,7 @@
;; render-list-to-html — dispatch on list head ;; render-list-to-html — dispatch on list head
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-list-to-html (define render-list-to-html :effects [render]
(fn ((expr :as list) (env :as dict)) (fn ((expr :as list) (env :as dict))
(if (empty? expr) (if (empty? expr)
"" ""
@@ -134,7 +135,7 @@
;; dispatch-html-form — render-aware special form handling for HTML output ;; dispatch-html-form — render-aware special form handling for HTML output
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define dispatch-html-form (define dispatch-html-form :effects [render]
(fn ((name :as string) (expr :as list) (env :as dict)) (fn ((name :as string) (expr :as list) (env :as dict))
(cond (cond
;; if ;; if
@@ -234,7 +235,7 @@
;; render-lambda-html — render a lambda body in HTML context ;; render-lambda-html — render a lambda body in HTML context
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-lambda-html (define render-lambda-html :effects [render]
(fn ((f :as lambda) (args :as list) (env :as dict)) (fn ((f :as lambda) (args :as list) (env :as dict))
(let ((local (env-merge (lambda-closure f) env))) (let ((local (env-merge (lambda-closure f) env)))
(for-each-indexed (for-each-indexed
@@ -248,7 +249,7 @@
;; render-html-component — expand and render a component ;; render-html-component — expand and render a component
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define render-html-component (define render-html-component :effects [render]
(fn ((comp :as component) (args :as list) (env :as dict)) (fn ((comp :as component) (args :as list) (env :as dict))
;; Expand component and render body through HTML adapter. ;; Expand component and render body through HTML adapter.
;; Component body contains rendering forms (HTML tags) that only the ;; Component body contains rendering forms (HTML tags) that only the
@@ -287,7 +288,7 @@
(render-to-html (component-body comp) local))))) (render-to-html (component-body comp) local)))))
(define render-html-element (define render-html-element :effects [render]
(fn ((tag :as string) (args :as list) (env :as dict)) (fn ((tag :as string) (args :as list) (env :as dict))
(let ((parsed (parse-element-args args env)) (let ((parsed (parse-element-args args env))
(attrs (first parsed)) (attrs (first parsed))
@@ -311,7 +312,7 @@
;; Lakes are server territory inside islands. The morph can update lake ;; Lakes are server territory inside islands. The morph can update lake
;; content while preserving surrounding reactive DOM. ;; content while preserving surrounding reactive DOM.
(define render-html-lake (define render-html-lake :effects [render]
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
(let ((lake-id nil) (let ((lake-id nil)
(lake-tag "div") (lake-tag "div")
@@ -350,7 +351,7 @@
;; re-evaluated in the island's signal scope. Server renders children normally; ;; re-evaluated in the island's signal scope. Server renders children normally;
;; the :transform is a client-only concern. ;; the :transform is a client-only concern.
(define render-html-marsh (define render-html-marsh :effects [render]
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
(let ((marsh-id nil) (let ((marsh-id nil)
(marsh-tag "div") (marsh-tag "div")
@@ -393,7 +394,7 @@
;; (reset! s v) → no-op ;; (reset! s v) → no-op
;; (swap! s f) → no-op ;; (swap! s f) → no-op
(define render-html-island (define render-html-island :effects [render]
(fn ((island :as island) (args :as list) (env :as dict)) (fn ((island :as island) (args :as list) (env :as dict))
;; Parse kwargs and children (same pattern as render-html-component) ;; Parse kwargs and children (same pattern as render-html-component)
(let ((kwargs (dict)) (let ((kwargs (dict))
@@ -451,7 +452,7 @@
;; Uses the SX serializer (not JSON) so the client can parse with sx-parse. ;; Uses the SX serializer (not JSON) so the client can parse with sx-parse.
;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts. ;; Handles all SX types natively: numbers, strings, booleans, nil, lists, dicts.
(define serialize-island-state (define serialize-island-state :effects []
(fn ((kwargs :as dict)) (fn ((kwargs :as dict))
(if (empty-dict? kwargs) (if (empty-dict? kwargs)
nil nil

View File

@@ -11,7 +11,7 @@
;; ========================================================================== ;; ==========================================================================
(define render-to-sx (define render-to-sx :effects [render]
(fn (expr (env :as dict)) (fn (expr (env :as dict))
(let ((result (aser expr env))) (let ((result (aser expr env)))
;; aser-call already returns serialized SX strings; ;; aser-call already returns serialized SX strings;
@@ -20,8 +20,8 @@
result result
(serialize result))))) (serialize result)))))
(define aser (define aser :effects [render]
(fn (expr (env :as dict)) (fn ((expr :as any) (env :as dict))
;; Evaluate for SX wire format — serialize rendering forms, ;; Evaluate for SX wire format — serialize rendering forms,
;; evaluate control flow and function calls. ;; evaluate control flow and function calls.
(set-render-active! true) (set-render-active! true)
@@ -51,7 +51,7 @@
:else expr))) :else expr)))
(define aser-list (define aser-list :effects [render]
(fn ((expr :as list) (env :as dict)) (fn ((expr :as list) (env :as dict))
(let ((head (first expr)) (let ((head (first expr))
(args (rest expr))) (args (rest expr)))
@@ -103,7 +103,7 @@
:else (error (str "Not callable: " (inspect f))))))))))) :else (error (str "Not callable: " (inspect f)))))))))))
(define aser-fragment (define aser-fragment :effects [render]
(fn ((children :as list) (env :as dict)) (fn ((children :as list) (env :as dict))
;; Serialize (<> child1 child2 ...) to sx source string ;; Serialize (<> child1 child2 ...) to sx source string
;; Must flatten list results (e.g. from map/filter) to avoid nested parens ;; Must flatten list results (e.g. from map/filter) to avoid nested parens
@@ -125,7 +125,7 @@
(str "(<> " (join " " parts) ")"))))) (str "(<> " (join " " parts) ")")))))
(define aser-call (define aser-call :effects [render]
(fn ((name :as string) (args :as list) (env :as dict)) (fn ((name :as string) (args :as list) (env :as dict))
;; Serialize (name :key val child ...) — evaluate args but keep as sx ;; Serialize (name :key val child ...) — evaluate args but keep as sx
;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops ;; Uses for-each + mutable state (not reduce) so bootstrapper emits for-loops
@@ -170,17 +170,18 @@
"define" "defcomp" "defmacro" "defstyle" "define" "defcomp" "defmacro" "defstyle"
"defhandler" "defpage" "defquery" "defaction" "defrelation" "defhandler" "defpage" "defquery" "defaction" "defrelation"
"begin" "do" "quote" "quasiquote" "begin" "do" "quote" "quasiquote"
"->" "set!" "letrec" "dynamic-wind" "defisland")) "->" "set!" "letrec" "dynamic-wind" "defisland"
"deftype" "defeffect"))
(define HO_FORM_NAMES (define HO_FORM_NAMES
(list "map" "map-indexed" "filter" "reduce" (list "map" "map-indexed" "filter" "reduce"
"some" "every?" "for-each")) "some" "every?" "for-each"))
(define special-form? (define special-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
(contains? SPECIAL_FORM_NAMES name))) (contains? SPECIAL_FORM_NAMES name)))
(define ho-form? (define ho-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
(contains? HO_FORM_NAMES name))) (contains? HO_FORM_NAMES name)))
@@ -193,7 +194,7 @@
;; through aser (serializing tags/components instead of rendering HTML). ;; through aser (serializing tags/components instead of rendering HTML).
;; Definition forms evaluate for side effects and return nil. ;; Definition forms evaluate for side effects and return nil.
(define aser-special (define aser-special :effects [render]
(fn ((name :as string) (expr :as list) (env :as dict)) (fn ((name :as string) (expr :as list) (env :as dict))
(let ((args (rest expr))) (let ((args (rest expr)))
(cond (cond
@@ -304,7 +305,8 @@
;; Definition forms — evaluate for side effects ;; Definition forms — evaluate for side effects
(or (= name "define") (= name "defcomp") (= name "defmacro") (or (= name "define") (= name "defcomp") (= name "defmacro")
(= name "defstyle") (= name "defhandler") (= name "defpage") (= name "defstyle") (= name "defhandler") (= name "defpage")
(= name "defquery") (= name "defaction") (= name "defrelation")) (= name "defquery") (= name "defaction") (= name "defrelation")
(= name "deftype") (= name "defeffect"))
(do (trampoline (eval-expr expr env)) nil) (do (trampoline (eval-expr expr env)) nil)
;; Everything else — evaluate normally ;; Everything else — evaluate normally
@@ -313,7 +315,7 @@
;; Helper: case dispatch for aser mode ;; Helper: case dispatch for aser mode
(define eval-case-aser (define eval-case-aser :effects [render]
(fn (match-val (clauses :as list) (env :as dict)) (fn (match-val (clauses :as list) (env :as dict))
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil

View File

@@ -26,7 +26,7 @@
(define HEAD_HOIST_SELECTOR (define HEAD_HOIST_SELECTOR
"meta, title, link[rel='canonical'], script[type='application/ld+json']") "meta, title, link[rel='canonical'], script[type='application/ld+json']")
(define hoist-head-elements-full (define hoist-head-elements-full :effects [mutation io]
(fn (root) (fn (root)
(let ((els (dom-query-all root HEAD_HOIST_SELECTOR))) (let ((els (dom-query-all root HEAD_HOIST_SELECTOR)))
(for-each (for-each
@@ -71,7 +71,7 @@
;; Mount — render SX source into a DOM element ;; Mount — render SX source into a DOM element
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sx-mount (define sx-mount :effects [mutation io]
(fn (target (source :as string) (extra-env :as dict)) (fn (target (source :as string) (extra-env :as dict))
;; Render SX source string into target element. ;; Render SX source string into target element.
;; target: Element or CSS selector string ;; target: Element or CSS selector string
@@ -100,7 +100,7 @@
;; Finds the suspense wrapper by data-suspense attribute, renders the ;; Finds the suspense wrapper by data-suspense attribute, renders the
;; new SX content, and replaces the wrapper's children. ;; new SX content, and replaces the wrapper's children.
(define resolve-suspense (define resolve-suspense :effects [mutation io]
(fn ((id :as string) (sx :as string)) (fn ((id :as string) (sx :as string))
;; Process any new <script type="text/sx"> tags that arrived via ;; Process any new <script type="text/sx"> tags that arrived via
;; streaming (e.g. extra component defs) before resolving. ;; streaming (e.g. extra component defs) before resolving.
@@ -127,7 +127,7 @@
;; Hydrate — render all [data-sx] elements ;; Hydrate — render all [data-sx] elements
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sx-hydrate-elements (define sx-hydrate-elements :effects [mutation io]
(fn (root) (fn (root)
;; Find all [data-sx] elements within root and render them. ;; Find all [data-sx] elements within root and render them.
(let ((els (dom-query-all (or root (dom-body)) "[data-sx]"))) (let ((els (dom-query-all (or root (dom-body)) "[data-sx]")))
@@ -143,7 +143,7 @@
;; Update — re-render a [data-sx] element with new env data ;; Update — re-render a [data-sx] element with new env data
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sx-update-element (define sx-update-element :effects [mutation io]
(fn (el new-env) (fn (el new-env)
;; Re-render a [data-sx] element. ;; Re-render a [data-sx] element.
;; Reads source from data-sx attr, base env from data-sx-env attr. ;; Reads source from data-sx attr, base env from data-sx-env attr.
@@ -165,7 +165,7 @@
;; Render component — build synthetic call from kwargs dict ;; Render component — build synthetic call from kwargs dict
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sx-render-component (define sx-render-component :effects [mutation io]
(fn ((name :as string) (kwargs :as dict) (extra-env :as dict)) (fn ((name :as string) (kwargs :as dict) (extra-env :as dict))
;; Render a named component with keyword args. ;; Render a named component with keyword args.
;; name: component name (with or without ~ prefix) ;; name: component name (with or without ~ prefix)
@@ -190,7 +190,7 @@
;; Script processing — <script type="text/sx"> ;; Script processing — <script type="text/sx">
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-sx-scripts (define process-sx-scripts :effects [mutation io]
(fn (root) (fn (root)
;; Process all <script type="text/sx"> tags. ;; Process all <script type="text/sx"> tags.
;; - data-components + data-hash → localStorage cache ;; - data-components + data-hash → localStorage cache
@@ -235,7 +235,7 @@
;; Component script with caching ;; Component script with caching
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-component-script (define process-component-script :effects [mutation io]
(fn (script (text :as string)) (fn (script (text :as string))
;; Handle <script type="text/sx" data-components data-hash="..."> ;; Handle <script type="text/sx" data-components data-hash="...">
(let ((hash (dom-get-attr script "data-hash"))) (let ((hash (dom-get-attr script "data-hash")))
@@ -288,7 +288,7 @@
(define _page-routes (list)) (define _page-routes (list))
(define process-page-scripts (define process-page-scripts :effects [mutation io]
(fn () (fn ()
;; Process <script type="text/sx-pages"> tags. ;; Process <script type="text/sx-pages"> tags.
;; Parses SX page registry and builds route entries with parsed patterns. ;; Parses SX page registry and builds route entries with parsed patterns.
@@ -331,7 +331,7 @@
;; 5. Morph existing DOM to preserve structure, focus, scroll ;; 5. Morph existing DOM to preserve structure, focus, scroll
;; 6. Store disposers on the element for cleanup ;; 6. Store disposers on the element for cleanup
(define sx-hydrate-islands (define sx-hydrate-islands :effects [mutation io]
(fn (root) (fn (root)
(let ((els (dom-query-all (or root (dom-body)) "[data-sx-island]"))) (let ((els (dom-query-all (or root (dom-body)) "[data-sx-island]")))
(for-each (for-each
@@ -341,7 +341,7 @@
(hydrate-island el))) (hydrate-island el)))
els)))) els))))
(define hydrate-island (define hydrate-island :effects [mutation io]
(fn (el) (fn (el)
(let ((name (dom-get-attr el "data-sx-island")) (let ((name (dom-get-attr el "data-sx-island"))
(state-sx (or (dom-get-attr el "data-sx-state") "{}"))) (state-sx (or (dom-get-attr el "data-sx-state") "{}")))
@@ -388,7 +388,7 @@
;; Island disposal — clean up when island removed from DOM ;; Island disposal — clean up when island removed from DOM
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define dispose-island (define dispose-island :effects [mutation io]
(fn (el) (fn (el)
(let ((disposers (dom-get-data el "sx-disposers"))) (let ((disposers (dom-get-data el "sx-disposers")))
(when disposers (when disposers
@@ -398,7 +398,7 @@
disposers) disposers)
(dom-set-data el "sx-disposers" nil))))) (dom-set-data el "sx-disposers" nil)))))
(define dispose-islands-in (define dispose-islands-in :effects [mutation io]
(fn (root) (fn (root)
;; Dispose islands within root, but SKIP hydrated islands — ;; Dispose islands within root, but SKIP hydrated islands —
;; they may be preserved across morphs. Only dispose islands ;; they may be preserved across morphs. Only dispose islands
@@ -419,7 +419,7 @@
;; Full boot sequence ;; Full boot sequence
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define boot-init (define boot-init :effects [mutation io]
(fn () (fn ()
;; Full browser initialization: ;; Full browser initialization:
;; 1. CSS tracking ;; 1. CSS tracking

View File

@@ -664,7 +664,12 @@ class PyEmitter:
def _emit_define(self, expr, indent: int = 0) -> str: def _emit_define(self, expr, indent: int = 0) -> str:
pad = " " * indent pad = " " * indent
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
val_expr = expr[2] # Handle (define name :effects [...] value) — skip :effects annotation
if (len(expr) >= 5 and isinstance(expr[2], Keyword)
and expr[2].name == "effects"):
val_expr = expr[4]
else:
val_expr = expr[2]
# Always emit fn-bodied defines as def statements for flat control flow # Always emit fn-bodied defines as def statements for flat control flow
if (isinstance(val_expr, list) and val_expr and if (isinstance(val_expr, list) and val_expr and
isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")): isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
@@ -675,7 +680,12 @@ class PyEmitter:
def _emit_define_async(self, expr, indent: int = 0) -> str: def _emit_define_async(self, expr, indent: int = 0) -> str:
"""Emit a define-async form as an async def statement.""" """Emit a define-async form as an async def statement."""
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
val_expr = expr[2] # Handle (define-async name :effects [...] value) — skip :effects annotation
if (len(expr) >= 5 and isinstance(expr[2], Keyword)
and expr[2].name == "effects"):
val_expr = expr[4]
else:
val_expr = expr[2]
if (isinstance(val_expr, list) and val_expr and if (isinstance(val_expr, list) and val_expr and
isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")): isinstance(val_expr[0], Symbol) and val_expr[0].name in ("fn", "lambda")):
return self._emit_define_as_def(name, val_expr, indent, is_async=True) return self._emit_define_as_def(name, val_expr, indent, is_async=True)

View File

@@ -12,6 +12,7 @@
;; (define-io-primitive "name" ;; (define-io-primitive "name"
;; :params (param1 param2 &key ...) ;; :params (param1 param2 &key ...)
;; :returns "type" ;; :returns "type"
;; :effects [io]
;; :async true ;; :async true
;; :doc "description" ;; :doc "description"
;; :context :request) ;; :context :request)
@@ -38,6 +39,7 @@
(define-io-primitive "current-user" (define-io-primitive "current-user"
:params () :params ()
:returns "dict?" :returns "dict?"
:effects [io]
:async true :async true
:doc "Current authenticated user dict, or nil." :doc "Current authenticated user dict, or nil."
:context :request) :context :request)
@@ -45,6 +47,7 @@
(define-io-primitive "request-arg" (define-io-primitive "request-arg"
:params (name &rest default) :params (name &rest default)
:returns "any" :returns "any"
:effects [io]
:async true :async true
:doc "Read a query string argument from the current request." :doc "Read a query string argument from the current request."
:context :request) :context :request)
@@ -52,6 +55,7 @@
(define-io-primitive "request-path" (define-io-primitive "request-path"
:params () :params ()
:returns "string" :returns "string"
:effects [io]
:async true :async true
:doc "Current request path." :doc "Current request path."
:context :request) :context :request)
@@ -59,6 +63,7 @@
(define-io-primitive "request-view-args" (define-io-primitive "request-view-args"
:params (key) :params (key)
:returns "any" :returns "any"
:effects [io]
:async true :async true
:doc "Read a URL view argument from the current request." :doc "Read a URL view argument from the current request."
:context :request) :context :request)
@@ -66,6 +71,7 @@
(define-io-primitive "csrf-token" (define-io-primitive "csrf-token"
:params () :params ()
:returns "string" :returns "string"
:effects [io]
:async true :async true
:doc "Current CSRF token string." :doc "Current CSRF token string."
:context :request) :context :request)
@@ -73,6 +79,7 @@
(define-io-primitive "abort" (define-io-primitive "abort"
:params (status &rest message) :params (status &rest message)
:returns "nil" :returns "nil"
:effects [io]
:async true :async true
:doc "Raise HTTP error from SX." :doc "Raise HTTP error from SX."
:context :request) :context :request)
@@ -82,6 +89,7 @@
(define-io-primitive "url-for" (define-io-primitive "url-for"
:params (endpoint &key) :params (endpoint &key)
:returns "string" :returns "string"
:effects [io]
:async true :async true
:doc "Generate URL for a named endpoint." :doc "Generate URL for a named endpoint."
:context :request) :context :request)
@@ -89,6 +97,7 @@
(define-io-primitive "route-prefix" (define-io-primitive "route-prefix"
:params () :params ()
:returns "string" :returns "string"
:effects [io]
:async true :async true
:doc "Service URL prefix for dev/prod routing." :doc "Service URL prefix for dev/prod routing."
:context :request) :context :request)
@@ -98,6 +107,7 @@
(define-io-primitive "app-url" (define-io-primitive "app-url"
:params (service &rest path) :params (service &rest path)
:returns "string" :returns "string"
:effects [io]
:async false :async false
:doc "Full URL for a service: (app-url \"blog\" \"/my-post/\")." :doc "Full URL for a service: (app-url \"blog\" \"/my-post/\")."
:context :config) :context :config)
@@ -105,6 +115,7 @@
(define-io-primitive "asset-url" (define-io-primitive "asset-url"
:params (&rest path) :params (&rest path)
:returns "string" :returns "string"
:effects [io]
:async false :async false
:doc "Versioned static asset URL." :doc "Versioned static asset URL."
:context :config) :context :config)
@@ -112,6 +123,7 @@
(define-io-primitive "config" (define-io-primitive "config"
:params (key) :params (key)
:returns "any" :returns "any"
:effects [io]
:async false :async false
:doc "Read a value from host configuration." :doc "Read a value from host configuration."
:context :config) :context :config)
@@ -138,11 +150,13 @@
(declare-signal-primitive "signal" (declare-signal-primitive "signal"
:params (initial-value) :params (initial-value)
:returns "signal" :returns "signal"
:effects []
:doc "Create a reactive signal container with an initial value.") :doc "Create a reactive signal container with an initial value.")
(declare-signal-primitive "deref" (declare-signal-primitive "deref"
:params (signal) :params (signal)
:returns "any" :returns "any"
:effects []
:doc "Read a signal's current value. In a reactive context (inside an island), :doc "Read a signal's current value. In a reactive context (inside an island),
subscribes the current DOM binding to the signal. Outside reactive subscribes the current DOM binding to the signal. Outside reactive
context, just returns the value.") context, just returns the value.")
@@ -150,23 +164,27 @@
(declare-signal-primitive "reset!" (declare-signal-primitive "reset!"
:params (signal value) :params (signal value)
:returns "nil" :returns "nil"
:effects [mutation]
:doc "Set a signal to a new value. Notifies all subscribers.") :doc "Set a signal to a new value. Notifies all subscribers.")
(declare-signal-primitive "swap!" (declare-signal-primitive "swap!"
:params (signal f &rest args) :params (signal f &rest args)
:returns "nil" :returns "nil"
:effects [mutation]
:doc "Update a signal by applying f to its current value. (swap! s inc) :doc "Update a signal by applying f to its current value. (swap! s inc)
is equivalent to (reset! s (inc (deref s))) but atomic.") is equivalent to (reset! s (inc (deref s))) but atomic.")
(declare-signal-primitive "computed" (declare-signal-primitive "computed"
:params (compute-fn) :params (compute-fn)
:returns "signal" :returns "signal"
:effects []
:doc "Create a derived signal that recomputes when its dependencies change. :doc "Create a derived signal that recomputes when its dependencies change.
Dependencies are discovered automatically by tracking deref calls.") Dependencies are discovered automatically by tracking deref calls.")
(declare-signal-primitive "effect" (declare-signal-primitive "effect"
:params (effect-fn) :params (effect-fn)
:returns "lambda" :returns "lambda"
:effects [mutation]
:doc "Run a side effect that re-runs when its signal dependencies change. :doc "Run a side effect that re-runs when its signal dependencies change.
Returns a dispose function. If the effect function returns a function, Returns a dispose function. If the effect function returns a function,
it is called as cleanup before the next run.") it is called as cleanup before the next run.")
@@ -174,5 +192,6 @@
(declare-signal-primitive "batch" (declare-signal-primitive "batch"
:params (thunk) :params (thunk)
:returns "any" :returns "any"
:effects [mutation]
:doc "Group multiple signal writes. Subscribers are notified once at the end, :doc "Group multiple signal writes. Subscribers are notified once at the end,
after all values have been updated.") after all values have been updated.")

View File

@@ -31,14 +31,14 @@
;; Walks all branches of control flow (if/when/cond/case) to find ;; Walks all branches of control flow (if/when/cond/case) to find
;; every component that *could* be rendered. ;; every component that *could* be rendered.
(define scan-refs (define scan-refs :effects []
(fn (node) (fn (node)
(let ((refs (list))) (let ((refs (list)))
(scan-refs-walk node refs) (scan-refs-walk node refs)
refs))) refs)))
(define scan-refs-walk (define scan-refs-walk :effects []
(fn (node (refs :as list)) (fn (node (refs :as list))
(cond (cond
;; Symbol starting with ~ → component reference ;; Symbol starting with ~ → component reference
@@ -67,7 +67,7 @@
;; Given a component name and an environment, compute all components ;; Given a component name and an environment, compute all components
;; that it can transitively render. Handles cycles via seen-set. ;; that it can transitively render. Handles cycles via seen-set.
(define transitive-deps-walk (define transitive-deps-walk :effects []
(fn ((n :as string) (seen :as list) (env :as dict)) (fn ((n :as string) (seen :as list) (env :as dict))
(when (not (contains? seen n)) (when (not (contains? seen n))
(append! seen n) (append! seen n)
@@ -82,7 +82,7 @@
:else nil))))) :else nil)))))
(define transitive-deps (define transitive-deps :effects []
(fn ((name :as string) (env :as dict)) (fn ((name :as string) (env :as dict))
(let ((seen (list)) (let ((seen (list))
(key (if (starts-with? name "~") name (str "~" name)))) (key (if (starts-with? name "~") name (str "~" name))))
@@ -100,7 +100,7 @@
;; (env-components env) → list of component names in env ;; (env-components env) → list of component names in env
;; (component-set-deps! comp deps) → store deps on component ;; (component-set-deps! comp deps) → store deps on component
(define compute-all-deps (define compute-all-deps :effects [mutation]
(fn ((env :as dict)) (fn ((env :as dict))
(for-each (for-each
(fn ((name :as string)) (fn ((name :as string))
@@ -119,7 +119,7 @@
;; Platform interface: ;; Platform interface:
;; (regex-find-all pattern source) → list of matched group strings ;; (regex-find-all pattern source) → list of matched group strings
(define scan-components-from-source (define scan-components-from-source :effects []
(fn ((source :as string)) (fn ((source :as string))
(let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)" source))) (let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)" source)))
(map (fn ((m :as string)) (str "~" m)) matches)))) (map (fn ((m :as string)) (str "~" m)) matches))))
@@ -131,7 +131,7 @@
;; Scans page source for direct component references, then computes ;; Scans page source for direct component references, then computes
;; the transitive closure. Returns list of ~names. ;; the transitive closure. Returns list of ~names.
(define components-needed (define components-needed :effects []
(fn ((page-source :as string) (env :as dict)) (fn ((page-source :as string) (env :as dict))
(let ((direct (scan-components-from-source page-source)) (let ((direct (scan-components-from-source page-source))
(all-needed (list))) (all-needed (list)))
@@ -165,7 +165,7 @@
;; ;;
;; This replaces the "send everything" approach with per-page bundles. ;; This replaces the "send everything" approach with per-page bundles.
(define page-component-bundle (define page-component-bundle :effects []
(fn ((page-source :as string) (env :as dict)) (fn ((page-source :as string) (env :as dict))
(components-needed page-source env))) (components-needed page-source env)))
@@ -180,7 +180,7 @@
;; (component-css-classes c) → set/list of class strings ;; (component-css-classes c) → set/list of class strings
;; (scan-css-classes source) → set/list of class strings from source ;; (scan-css-classes source) → set/list of class strings from source
(define page-css-classes (define page-css-classes :effects []
(fn ((page-source :as string) (env :as dict)) (fn ((page-source :as string) (env :as dict))
(let ((needed (components-needed page-source env)) (let ((needed (components-needed page-source env))
(classes (list))) (classes (list)))
@@ -218,7 +218,7 @@
;; (component-io-refs c) → cached IO ref list (may be empty) ;; (component-io-refs c) → cached IO ref list (may be empty)
;; (component-set-io-refs! c r) → cache IO refs on component ;; (component-set-io-refs! c r) → cache IO refs on component
(define scan-io-refs-walk (define scan-io-refs-walk :effects []
(fn (node (io-names :as list) (refs :as list)) (fn (node (io-names :as list) (refs :as list))
(cond (cond
;; Symbol → check if name is in the IO set ;; Symbol → check if name is in the IO set
@@ -241,7 +241,7 @@
:else nil))) :else nil)))
(define scan-io-refs (define scan-io-refs :effects []
(fn (node (io-names :as list)) (fn (node (io-names :as list))
(let ((refs (list))) (let ((refs (list)))
(scan-io-refs-walk node io-names refs) (scan-io-refs-walk node io-names refs)
@@ -252,7 +252,7 @@
;; 9. Transitive IO refs — follow component deps and union IO refs ;; 9. Transitive IO refs — follow component deps and union IO refs
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define transitive-io-refs-walk (define transitive-io-refs-walk :effects []
(fn ((n :as string) (seen :as list) (all-refs :as list) (env :as dict) (io-names :as list)) (fn ((n :as string) (seen :as list) (all-refs :as list) (env :as dict) (io-names :as list))
(when (not (contains? seen n)) (when (not (contains? seen n))
(append! seen n) (append! seen n)
@@ -285,7 +285,7 @@
:else nil))))) :else nil)))))
(define transitive-io-refs (define transitive-io-refs :effects []
(fn ((name :as string) (env :as dict) (io-names :as list)) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((all-refs (list)) (let ((all-refs (list))
(seen (list)) (seen (list))
@@ -298,7 +298,7 @@
;; 10. Compute IO refs for all components in an environment ;; 10. Compute IO refs for all components in an environment
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define compute-all-io-refs (define compute-all-io-refs :effects [mutation]
(fn ((env :as dict) (io-names :as list)) (fn ((env :as dict) (io-names :as list))
(for-each (for-each
(fn ((name :as string)) (fn ((name :as string))
@@ -308,7 +308,7 @@
(env-components env)))) (env-components env))))
(define component-io-refs-cached (define component-io-refs-cached :effects []
(fn ((name :as string) (env :as dict) (io-names :as list)) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key))) (let ((val (env-get env key)))
@@ -319,7 +319,7 @@
;; Fallback: not yet cached (shouldn't happen after compute-all-io-refs) ;; Fallback: not yet cached (shouldn't happen after compute-all-io-refs)
(transitive-io-refs name env io-names)))))) (transitive-io-refs name env io-names))))))
(define component-pure? (define component-pure? :effects []
(fn ((name :as string) (env :as dict) (io-names :as list)) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key))) (let ((val (env-get env key)))
@@ -343,7 +343,7 @@
;; ;;
;; Returns: "server" | "client" ;; Returns: "server" | "client"
(define render-target (define render-target :effects []
(fn ((name :as string) (env :as dict) (io-names :as list)) (fn ((name :as string) (env :as dict) (io-names :as list))
(let ((key (if (starts-with? name "~") name (str "~" name)))) (let ((key (if (starts-with? name "~") name (str "~" name))))
(let ((val (env-get env key))) (let ((val (env-get env key)))
@@ -372,7 +372,7 @@
;; The async evaluator and client router both use it to make decisions ;; The async evaluator and client router both use it to make decisions
;; without recomputing at every request. ;; without recomputing at every request.
(define page-render-plan (define page-render-plan :effects []
(fn ((page-source :as string) (env :as dict) (io-names :as list)) (fn ((page-source :as string) (env :as dict) (io-names :as list))
(let ((needed (components-needed page-source env)) (let ((needed (components-needed page-source env))
(comp-targets (dict)) (comp-targets (dict))
@@ -450,7 +450,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Moved from platform to spec: pure logic using type predicates. ;; Moved from platform to spec: pure logic using type predicates.
(define env-components (define env-components :effects []
(fn ((env :as dict)) (fn ((env :as dict))
(filter (filter
(fn ((k :as string)) (fn ((k :as string))

View File

@@ -31,7 +31,7 @@
;; Parses the sx-trigger attribute value into a list of trigger descriptors. ;; Parses the sx-trigger attribute value into a list of trigger descriptors.
;; Each descriptor is a dict with "event" and "modifiers" keys. ;; Each descriptor is a dict with "event" and "modifiers" keys.
(define parse-time (define parse-time :effects []
(fn ((s :as string)) (fn ((s :as string))
;; Parse time string: "2s" → 2000, "500ms" → 500 ;; Parse time string: "2s" → 2000, "500ms" → 500
;; Uses nested if (not cond) because cond misclassifies 2-element ;; Uses nested if (not cond) because cond misclassifies 2-element
@@ -42,7 +42,7 @@
(parse-int s 0)))))) (parse-int s 0))))))
(define parse-trigger-spec (define parse-trigger-spec :effects []
(fn ((spec :as string)) (fn ((spec :as string))
;; Parse "click delay:500ms once,change" → list of trigger descriptors ;; Parse "click delay:500ms once,change" → list of trigger descriptors
(if (nil? spec) (if (nil? spec)
@@ -80,7 +80,7 @@
raw-parts)))))) raw-parts))))))
(define default-trigger (define default-trigger :effects []
(fn ((tag-name :as string)) (fn ((tag-name :as string))
;; Default trigger for element type ;; Default trigger for element type
(cond (cond
@@ -98,7 +98,7 @@
;; Verb extraction ;; Verb extraction
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define get-verb-info (define get-verb-info :effects [io]
(fn (el) (fn (el)
;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil. ;; Check element for sx-get, sx-post, etc. Returns (dict "method" "url") or nil.
(some (some
@@ -114,7 +114,7 @@
;; Request header building ;; Request header building
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define build-request-headers (define build-request-headers :effects [io]
(fn (el (loaded-components :as list) (css-hash :as string)) (fn (el (loaded-components :as list) (css-hash :as string))
;; Build the SX request headers dict ;; Build the SX request headers dict
(let ((headers (dict (let ((headers (dict
@@ -150,7 +150,7 @@
;; Response header processing ;; Response header processing
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-response-headers (define process-response-headers :effects []
(fn ((get-header :as lambda)) (fn ((get-header :as lambda))
;; Extract all SX response header directives into a dict. ;; Extract all SX response header directives into a dict.
;; get-header is (fn (name) → string or nil). ;; get-header is (fn (name) → string or nil).
@@ -174,7 +174,7 @@
;; Swap specification parsing ;; Swap specification parsing
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define parse-swap-spec (define parse-swap-spec :effects []
(fn ((raw-swap :as string) (global-transitions? :as boolean)) (fn ((raw-swap :as string) (global-transitions? :as boolean))
;; Parse "innerHTML transition:true" → dict with style + transition flag ;; Parse "innerHTML transition:true" → dict with style + transition flag
(let ((parts (split (or raw-swap DEFAULT_SWAP) " ")) (let ((parts (split (or raw-swap DEFAULT_SWAP) " "))
@@ -193,7 +193,7 @@
;; Retry logic ;; Retry logic
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define parse-retry-spec (define parse-retry-spec :effects []
(fn ((retry-attr :as string)) (fn ((retry-attr :as string))
;; Parse "exponential:1000:30000" → spec dict or nil ;; Parse "exponential:1000:30000" → spec dict or nil
(if (nil? retry-attr) (if (nil? retry-attr)
@@ -205,7 +205,7 @@
"cap-ms" (parse-int (nth parts 2) 30000)))))) "cap-ms" (parse-int (nth parts 2) 30000))))))
(define next-retry-ms (define next-retry-ms :effects []
(fn ((current-ms :as number) (cap-ms :as number)) (fn ((current-ms :as number) (cap-ms :as number))
;; Exponential backoff: double current, cap at max ;; Exponential backoff: double current, cap at max
(min (* current-ms 2) cap-ms))) (min (* current-ms 2) cap-ms)))
@@ -215,7 +215,7 @@
;; Form parameter filtering ;; Form parameter filtering
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define filter-params (define filter-params :effects []
(fn ((params-spec :as string) (all-params :as list)) (fn ((params-spec :as string) (all-params :as list))
;; Filter form parameters by sx-params spec. ;; Filter form parameters by sx-params spec.
;; all-params is a list of (key value) pairs. ;; all-params is a list of (key value) pairs.
@@ -239,7 +239,7 @@
;; Target resolution ;; Target resolution
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define resolve-target (define resolve-target :effects [io]
(fn (el) (fn (el)
;; Resolve the swap target for an element ;; Resolve the swap target for an element
(let ((sel (dom-get-attr el "sx-target"))) (let ((sel (dom-get-attr el "sx-target")))
@@ -253,7 +253,7 @@
;; Optimistic updates ;; Optimistic updates
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define apply-optimistic (define apply-optimistic :effects [mutation io]
(fn (el) (fn (el)
;; Apply optimistic update preview. Returns state for reverting, or nil. ;; Apply optimistic update preview. Returns state for reverting, or nil.
(let ((directive (dom-get-attr el "sx-optimistic"))) (let ((directive (dom-get-attr el "sx-optimistic")))
@@ -278,7 +278,7 @@
state))))) state)))))
(define revert-optimistic (define revert-optimistic :effects [mutation io]
(fn ((state :as dict)) (fn ((state :as dict))
;; Revert an optimistic update ;; Revert an optimistic update
(when state (when state
@@ -299,7 +299,7 @@
;; Out-of-band swap identification ;; Out-of-band swap identification
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define find-oob-swaps (define find-oob-swaps :effects [mutation io]
(fn (container) (fn (container)
;; Find elements marked for out-of-band swapping. ;; Find elements marked for out-of-band swapping.
;; Returns list of (dict "element" el "swap-type" type "target-id" id). ;; Returns list of (dict "element" el "swap-type" type "target-id" id).
@@ -329,7 +329,7 @@
;; preserving event listeners, focus, scroll position, and form state ;; preserving event listeners, focus, scroll position, and form state
;; on keyed (id) elements. ;; on keyed (id) elements.
(define morph-node (define morph-node :effects [mutation io]
(fn (old-node new-node) (fn (old-node new-node)
;; Morph old-node to match new-node, preserving listeners/state. ;; Morph old-node to match new-node, preserving listeners/state.
(cond (cond
@@ -371,7 +371,7 @@
(morph-children old-node new-node)))))) (morph-children old-node new-node))))))
(define sync-attrs (define sync-attrs :effects [mutation io]
(fn (old-el new-el) (fn (old-el new-el)
;; Sync attributes from new to old, but skip reactively managed attrs. ;; Sync attributes from new to old, but skip reactively managed attrs.
;; data-sx-reactive-attrs="style,class" means those attrs are owned by ;; data-sx-reactive-attrs="style,class" means those attrs are owned by
@@ -398,7 +398,7 @@
(dom-attr-list old-el))))) (dom-attr-list old-el)))))
(define morph-children (define morph-children :effects [mutation io]
(fn (old-parent new-parent) (fn (old-parent new-parent)
;; Reconcile children of old-parent to match new-parent. ;; Reconcile children of old-parent to match new-parent.
;; Keyed elements (with id) are matched and moved in-place. ;; Keyed elements (with id) are matched and moved in-place.
@@ -472,7 +472,7 @@
;; - Lakes = server substance (content, morphed) ;; - Lakes = server substance (content, morphed)
;; - The morph = Aufhebung (cancellation/preservation/elevation of both) ;; - The morph = Aufhebung (cancellation/preservation/elevation of both)
(define morph-island-children (define morph-island-children :effects [mutation io]
(fn (old-island new-island) (fn (old-island new-island)
;; Find all lake and marsh slots in both old and new islands ;; Find all lake and marsh slots in both old and new islands
(let ((old-lakes (dom-query-all old-island "[data-sx-lake]")) (let ((old-lakes (dom-query-all old-island "[data-sx-lake]"))
@@ -522,7 +522,7 @@
;; as SX and rendered in the island's signal context. If the marsh has a ;; as SX and rendered in the island's signal context. If the marsh has a
;; :transform function, it reshapes the content before evaluation. ;; :transform function, it reshapes the content before evaluation.
(define morph-marsh (define morph-marsh :effects [mutation io]
(fn (old-marsh new-marsh island-el) (fn (old-marsh new-marsh island-el)
(let ((transform (dom-get-data old-marsh "sx-marsh-transform")) (let ((transform (dom-get-data old-marsh "sx-marsh-transform"))
(env (dom-get-data old-marsh "sx-marsh-env")) (env (dom-get-data old-marsh "sx-marsh-env"))
@@ -555,7 +555,7 @@
;; ;;
;; Values are JSON-parsed: "7" → 7, "\"hello\"" → "hello", "true" → true. ;; Values are JSON-parsed: "7" → 7, "\"hello\"" → "hello", "true" → true.
(define process-signal-updates (define process-signal-updates :effects [mutation io]
(fn (root) (fn (root)
(let ((signal-els (dom-query-all root "[data-sx-signal]"))) (let ((signal-els (dom-query-all root "[data-sx-signal]")))
(for-each (for-each
@@ -576,7 +576,7 @@
;; Swap dispatch ;; Swap dispatch
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define swap-dom-nodes (define swap-dom-nodes :effects [mutation io]
(fn (target new-nodes (strategy :as string)) (fn (target new-nodes (strategy :as string))
;; Execute a swap strategy on live DOM nodes. ;; Execute a swap strategy on live DOM nodes.
;; new-nodes is typically a DocumentFragment or Element. ;; new-nodes is typically a DocumentFragment or Element.
@@ -630,7 +630,7 @@
(morph-children target wrapper)))))) (morph-children target wrapper))))))
(define insert-remaining-siblings (define insert-remaining-siblings :effects [mutation io]
(fn (parent ref-node sib) (fn (parent ref-node sib)
;; Insert sibling chain after ref-node ;; Insert sibling chain after ref-node
(when sib (when sib
@@ -643,7 +643,7 @@
;; String-based swap (fallback for HTML responses) ;; String-based swap (fallback for HTML responses)
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define swap-html-string (define swap-html-string :effects [mutation io]
(fn (target (html :as string) (strategy :as string)) (fn (target (html :as string) (strategy :as string))
;; Execute a swap strategy using an HTML string (DOMParser pipeline). ;; Execute a swap strategy using an HTML string (DOMParser pipeline).
(case strategy (case strategy
@@ -674,7 +674,7 @@
;; History management ;; History management
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define handle-history (define handle-history :effects [io]
(fn (el (url :as string) (resp-headers :as dict)) (fn (el (url :as string) (resp-headers :as dict))
;; Process history push/replace based on element attrs and response headers ;; Process history push/replace based on element attrs and response headers
(let ((push-url (dom-get-attr el "sx-push-url")) (let ((push-url (dom-get-attr el "sx-push-url"))
@@ -700,7 +700,7 @@
(define PRELOAD_TTL 30000) ;; 30 seconds (define PRELOAD_TTL 30000) ;; 30 seconds
(define preload-cache-get (define preload-cache-get :effects [mutation]
(fn ((cache :as dict) (url :as string)) (fn ((cache :as dict) (url :as string))
;; Get and consume a cached preload response. ;; Get and consume a cached preload response.
;; Returns (dict "text" ... "content-type" ...) or nil. ;; Returns (dict "text" ... "content-type" ...) or nil.
@@ -712,7 +712,7 @@
(do (dict-delete! cache url) entry)))))) (do (dict-delete! cache url) entry))))))
(define preload-cache-set (define preload-cache-set :effects [mutation]
(fn ((cache :as dict) (url :as string) (text :as string) (content-type :as string)) (fn ((cache :as dict) (url :as string) (text :as string) (content-type :as string))
;; Store a preloaded response ;; Store a preloaded response
(dict-set! cache url (dict-set! cache url
@@ -725,7 +725,7 @@
;; Maps trigger event names to binding strategies. ;; Maps trigger event names to binding strategies.
;; This is the logic; actual browser event binding is platform interface. ;; This is the logic; actual browser event binding is platform interface.
(define classify-trigger (define classify-trigger :effects []
(fn ((trigger :as dict)) (fn ((trigger :as dict))
;; Classify a parsed trigger descriptor for binding. ;; Classify a parsed trigger descriptor for binding.
;; Returns one of: "poll", "intersect", "load", "revealed", "event" ;; Returns one of: "poll", "intersect", "load", "revealed", "event"
@@ -742,7 +742,7 @@
;; Boost logic ;; Boost logic
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define should-boost-link? (define should-boost-link? :effects [io]
(fn (link) (fn (link)
;; Whether a link inside an sx-boost container should be boosted ;; Whether a link inside an sx-boost container should be boosted
(let ((href (dom-get-attr link "href"))) (let ((href (dom-get-attr link "href")))
@@ -756,7 +756,7 @@
(not (dom-has-attr? link "sx-disable")))))) (not (dom-has-attr? link "sx-disable"))))))
(define should-boost-form? (define should-boost-form? :effects [io]
(fn (form) (fn (form)
;; Whether a form inside an sx-boost container should be boosted ;; Whether a form inside an sx-boost container should be boosted
(and (not (dom-has-attr? form "sx-get")) (and (not (dom-has-attr? form "sx-get"))
@@ -768,7 +768,7 @@
;; SSE event classification ;; SSE event classification
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define parse-sse-swap (define parse-sse-swap :effects [io]
(fn (el) (fn (el)
;; Parse sx-sse-swap attribute ;; Parse sx-sse-swap attribute
;; Returns event name to listen for (default "message") ;; Returns event name to listen for (default "message")

View File

@@ -55,7 +55,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define trampoline (define trampoline
(fn (val) (fn ((val :as any))
;; Iteratively resolve thunks until we get an actual value. ;; Iteratively resolve thunks until we get an actual value.
;; Each target implements thunk? and thunk-expr/thunk-env. ;; Each target implements thunk? and thunk-expr/thunk-env.
(let ((result val)) (let ((result val))
@@ -151,6 +151,8 @@
(= name "defpage") (sf-defpage args env) (= name "defpage") (sf-defpage args env)
(= name "defquery") (sf-defquery args env) (= name "defquery") (sf-defquery args env)
(= name "defaction") (sf-defaction args env) (= name "defaction") (sf-defaction args env)
(= name "deftype") (sf-deftype args env)
(= name "defeffect") (sf-defeffect args env)
(= name "begin") (sf-begin args env) (= name "begin") (sf-begin args env)
(= name "do") (sf-begin args env) (= name "do") (sf-begin args env)
(= name "quote") (sf-quote args env) (= name "quote") (sf-quote args env)
@@ -506,11 +508,32 @@
(define sf-define (define sf-define
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
;; Detect :effects keyword: (define name :effects [...] value)
(let ((name-sym (first args)) (let ((name-sym (first args))
(value (trampoline (eval-expr (nth args 1) env)))) (has-effects (and (>= (len args) 4)
(= (type-of (nth args 1)) "keyword")
(= (keyword-name (nth args 1)) "effects")))
(val-idx (if (and (>= (len args) 4)
(= (type-of (nth args 1)) "keyword")
(= (keyword-name (nth args 1)) "effects"))
3 1))
(value (trampoline (eval-expr (nth args val-idx) env))))
(when (and (lambda? value) (nil? (lambda-name value))) (when (and (lambda? value) (nil? (lambda-name value)))
(set-lambda-name! value (symbol-name name-sym))) (set-lambda-name! value (symbol-name name-sym)))
(env-set! env (symbol-name name-sym) value) (env-set! env (symbol-name name-sym) value)
;; Store effect annotation if declared
(when has-effects
(let ((effects-raw (nth args 2))
(effect-list (if (= (type-of effects-raw) "list")
(map (fn (e) (if (= (type-of e) "symbol")
(symbol-name e) (str e)))
effects-raw)
(list (str effects-raw))))
(effect-anns (if (env-has? env "*effect-annotations*")
(env-get env "*effect-annotations*")
(dict))))
(dict-set! effect-anns (symbol-name name-sym) effect-list)
(env-set! env "*effect-annotations*" effect-anns)))
value))) value)))
@@ -528,11 +551,24 @@
(has-children (nth parsed 1)) (has-children (nth parsed 1))
(param-types (nth parsed 2)) (param-types (nth parsed 2))
(affinity (defcomp-kwarg args "affinity" "auto"))) (affinity (defcomp-kwarg args "affinity" "auto")))
(let ((comp (make-component comp-name params has-children body env affinity))) (let ((comp (make-component comp-name params has-children body env affinity))
(effects (defcomp-kwarg args "effects" nil)))
;; Store type annotations if any were declared ;; Store type annotations if any were declared
(when (and (not (nil? param-types)) (when (and (not (nil? param-types))
(not (empty? (keys param-types)))) (not (empty? (keys param-types))))
(component-set-param-types! comp param-types)) (component-set-param-types! comp param-types))
;; Store effect annotation if declared
(when (not (nil? effects))
(let ((effect-list (if (= (type-of effects) "list")
(map (fn (e) (if (= (type-of e) "symbol")
(symbol-name e) (str e)))
effects)
(list (str effects))))
(effect-anns (if (env-has? env "*effect-annotations*")
(env-get env "*effect-annotations*")
(dict))))
(dict-set! effect-anns (symbol-name name-sym) effect-list)
(env-set! env "*effect-annotations*" effect-anns)))
(env-set! env (symbol-name name-sym) comp) (env-set! env (symbol-name name-sym) comp)
comp)))) comp))))
@@ -654,6 +690,82 @@
value))) value)))
;; -- deftype helpers (must be in eval.sx, not types.sx, because
;; sf-deftype is always compiled but types.sx is a spec module) --
(define make-type-def
(fn ((name :as string) (params :as list) body)
{:name name :params params :body body}))
(define normalize-type-body
(fn (body)
;; Convert AST type expressions to type representation.
;; Symbols → strings, (union ...) → (or ...), dict keys → strings.
(cond
(nil? body) "nil"
(= (type-of body) "symbol")
(symbol-name body)
(= (type-of body) "string")
body
(= (type-of body) "keyword")
(keyword-name body)
(= (type-of body) "dict")
;; Record type — normalize values
(map-dict (fn (k v) (normalize-type-body v)) body)
(= (type-of body) "list")
(if (empty? body) "any"
(let ((head (first body)))
(let ((head-name (if (= (type-of head) "symbol")
(symbol-name head) (str head))))
;; (union a b) → (or a b)
(if (= head-name "union")
(cons "or" (map normalize-type-body (rest body)))
;; (or a b), (list-of t), (-> ...) etc.
(cons head-name (map normalize-type-body (rest body)))))))
:else (str body))))
(define sf-deftype
(fn ((args :as list) (env :as dict))
;; (deftype name body) or (deftype (name a b ...) body)
(let ((name-or-form (first args))
(body-expr (nth args 1))
(type-name nil)
(type-params (list)))
;; Parse name — symbol or (symbol params...)
(if (= (type-of name-or-form) "symbol")
(set! type-name (symbol-name name-or-form))
(when (= (type-of name-or-form) "list")
(set! type-name (symbol-name (first name-or-form)))
(set! type-params
(map (fn (p) (if (= (type-of p) "symbol")
(symbol-name p) (str p)))
(rest name-or-form)))))
;; Normalize and store in *type-registry*
(let ((body (normalize-type-body body-expr))
(registry (if (env-has? env "*type-registry*")
(env-get env "*type-registry*")
(dict))))
(dict-set! registry type-name
(make-type-def type-name type-params body))
(env-set! env "*type-registry*" registry)
nil))))
(define sf-defeffect
(fn ((args :as list) (env :as dict))
;; (defeffect name) — register an effect name
(let ((effect-name (if (= (type-of (first args)) "symbol")
(symbol-name (first args))
(str (first args))))
(registry (if (env-has? env "*effect-registry*")
(env-get env "*effect-registry*")
(list))))
(when (not (contains? registry effect-name))
(append! registry effect-name))
(env-set! env "*effect-registry*" registry)
nil)))
(define sf-begin (define sf-begin
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
(if (empty? args) (if (empty? args)

View File

@@ -1318,10 +1318,15 @@
(define js-emit-define (define js-emit-define
(fn (expr) (fn (expr)
;; Handle (define name :effects [...] value) — skip :effects annotation
(let ((name (if (= (type-of (nth expr 1)) "symbol") (let ((name (if (= (type-of (nth expr 1)) "symbol")
(symbol-name (nth expr 1)) (symbol-name (nth expr 1))
(str (nth expr 1)))) (str (nth expr 1))))
(val-expr (nth expr 2))) (val-expr (if (and (>= (len expr) 5)
(= (type-of (nth expr 2)) "keyword")
(= (keyword-name (nth expr 2)) "effects"))
(nth expr 4)
(nth expr 2))))
(if (nil? val-expr) (if (nil? val-expr)
(str "var " (js-mangle name) " = NIL;") (str "var " (js-mangle name) " = NIL;")
;; Detect zero-arg self-tail-recursive functions → while loops ;; Detect zero-arg self-tail-recursive functions → while loops

View File

@@ -33,7 +33,7 @@
;; Event dispatch helpers ;; Event dispatch helpers
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define dispatch-trigger-events (define dispatch-trigger-events :effects [mutation io]
(fn (el (header-val :as string)) (fn (el (header-val :as string))
;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers. ;; Dispatch events from SX-Trigger / SX-Trigger-After-Swap headers.
;; Value can be JSON object (name → detail) or comma-separated names. ;; Value can be JSON object (name → detail) or comma-separated names.
@@ -58,7 +58,7 @@
;; CSS tracking ;; CSS tracking
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define init-css-tracking (define init-css-tracking :effects [mutation io]
(fn () (fn ()
;; Read initial CSS hash from meta tag ;; Read initial CSS hash from meta tag
(let ((meta (dom-query "meta[name=\"sx-css-classes\"]"))) (let ((meta (dom-query "meta[name=\"sx-css-classes\"]")))
@@ -72,7 +72,7 @@
;; Request execution ;; Request execution
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define execute-request (define execute-request :effects [mutation io]
(fn (el (verbInfo :as dict) (extraParams :as dict)) (fn (el (verbInfo :as dict) (extraParams :as dict))
;; Gate checks then delegate to do-fetch. ;; Gate checks then delegate to do-fetch.
;; verbInfo: dict with "method" and "url" (or nil to read from element). ;; verbInfo: dict with "method" and "url" (or nil to read from element).
@@ -105,7 +105,7 @@
extraParams)))))))))))) extraParams))))))))))))
(define do-fetch (define do-fetch :effects [mutation io]
(fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict)) (fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Execute the actual fetch. Manages abort, headers, body, loading state. ;; Execute the actual fetch. Manages abort, headers, body, loading state.
(let ((sync (dom-get-attr el "sx-sync"))) (let ((sync (dom-get-attr el "sx-sync")))
@@ -201,7 +201,7 @@
(dict "error" err)))))))))))) (dict "error" err))))))))))))
(define handle-fetch-success (define handle-fetch-success :effects [mutation io]
(fn (el (url :as string) (verb :as string) (extraParams :as dict) get-header (text :as string)) (fn (el (url :as string) (verb :as string) (extraParams :as dict) get-header (text :as string))
;; Route a successful response through the appropriate handler. ;; Route a successful response through the appropriate handler.
(let ((resp-headers (process-response-headers get-header))) (let ((resp-headers (process-response-headers get-header)))
@@ -269,7 +269,7 @@
(dict "target" target-el "swap" swap-style))))))) (dict "target" target-el "swap" swap-style)))))))
(define handle-sx-response (define handle-sx-response :effects [mutation io]
(fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean)) (fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle SX-format response: strip components, extract CSS, render, swap. ;; Handle SX-format response: strip components, extract CSS, render, swap.
(let ((cleaned (strip-component-scripts text))) (let ((cleaned (strip-component-scripts text)))
@@ -300,7 +300,7 @@
(post-swap target))))))))))) (post-swap target)))))))))))
(define handle-html-response (define handle-html-response :effects [mutation io]
(fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean)) (fn (el target (text :as string) (swap-style :as string) (use-transition :as boolean))
;; Handle HTML-format response: parse, OOB, select, swap. ;; Handle HTML-format response: parse, OOB, select, swap.
(let ((doc (dom-parse-html-document text))) (let ((doc (dom-parse-html-document text)))
@@ -337,7 +337,7 @@
;; Retry ;; Retry
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define handle-retry (define handle-retry :effects [mutation io]
(fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict)) (fn (el (verb :as string) (method :as string) (url :as string) (extraParams :as dict))
;; Handle retry on failure if sx-retry is configured ;; Handle retry on failure if sx-retry is configured
(let ((retry-attr (dom-get-attr el "sx-retry")) (let ((retry-attr (dom-get-attr el "sx-retry"))
@@ -357,7 +357,7 @@
;; Trigger binding ;; Trigger binding
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define bind-triggers (define bind-triggers :effects [mutation io]
(fn (el (verbInfo :as dict)) (fn (el (verbInfo :as dict))
;; Bind triggers from sx-trigger attribute (or defaults) ;; Bind triggers from sx-trigger attribute (or defaults)
(let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger")) (let ((triggers (or (parse-trigger-spec (dom-get-attr el "sx-trigger"))
@@ -392,7 +392,7 @@
triggers)))) triggers))))
(define bind-event (define bind-event :effects [mutation io]
(fn (el (event-name :as string) (mods :as dict) (verbInfo :as dict)) (fn (el (event-name :as string) (mods :as dict) (verbInfo :as dict))
;; Bind a standard DOM event trigger. ;; Bind a standard DOM event trigger.
;; Handles delay, once, changed, optimistic, preventDefault. ;; Handles delay, once, changed, optimistic, preventDefault.
@@ -453,7 +453,7 @@
;; Post-swap lifecycle ;; Post-swap lifecycle
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define post-swap (define post-swap :effects [mutation io]
(fn (root) (fn (root)
;; Run lifecycle after swap: activate scripts, process SX, hydrate, process ;; Run lifecycle after swap: activate scripts, process SX, hydrate, process
(activate-scripts root) (activate-scripts root)
@@ -474,7 +474,7 @@
;; ;;
;; Example: (button :sx-get "/search" :sx-on-settle "(reset! (use-store \"count\") 0)") ;; Example: (button :sx-get "/search" :sx-on-settle "(reset! (use-store \"count\") 0)")
(define process-settle-hooks (define process-settle-hooks :effects [mutation io]
(fn (el) (fn (el)
(let ((settle-expr (dom-get-attr el "sx-on-settle"))) (let ((settle-expr (dom-get-attr el "sx-on-settle")))
(when (and settle-expr (not (empty? settle-expr))) (when (and settle-expr (not (empty? settle-expr)))
@@ -484,7 +484,7 @@
exprs)))))) exprs))))))
(define activate-scripts (define activate-scripts :effects [mutation io]
(fn (root) (fn (root)
;; Re-activate scripts in swapped content. ;; Re-activate scripts in swapped content.
;; Scripts inserted via innerHTML are inert — clone to make them execute. ;; Scripts inserted via innerHTML are inert — clone to make them execute.
@@ -505,7 +505,7 @@
;; OOB swap processing ;; OOB swap processing
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-oob-swaps (define process-oob-swaps :effects [mutation io]
(fn (container (swap-fn :as lambda)) (fn (container (swap-fn :as lambda))
;; Find and process out-of-band swaps in container. ;; Find and process out-of-band swaps in container.
;; swap-fn is (fn (target oob-element swap-type) ...). ;; swap-fn is (fn (target oob-element swap-type) ...).
@@ -529,7 +529,7 @@
;; Head element hoisting ;; Head element hoisting
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define hoist-head-elements (define hoist-head-elements :effects [mutation io]
(fn (container) (fn (container)
;; Move style[data-sx-css] and link[rel=stylesheet] to <head> ;; Move style[data-sx-css] and link[rel=stylesheet] to <head>
;; so they take effect globally. ;; so they take effect globally.
@@ -551,7 +551,7 @@
;; Boost processing ;; Boost processing
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-boosted (define process-boosted :effects [mutation io]
(fn (root) (fn (root)
;; Find [sx-boost] containers and boost their descendants ;; Find [sx-boost] containers and boost their descendants
(for-each (for-each
@@ -560,7 +560,7 @@
(dom-query-all (or root (dom-body)) "[sx-boost]")))) (dom-query-all (or root (dom-body)) "[sx-boost]"))))
(define boost-descendants (define boost-descendants :effects [mutation io]
(fn (container) (fn (container)
;; Boost links and forms within a container. ;; Boost links and forms within a container.
;; The sx-boost attribute value is the default target selector ;; The sx-boost attribute value is the default target selector
@@ -609,7 +609,7 @@
(define _page-data-cache (dict)) (define _page-data-cache (dict))
(define _page-data-cache-ttl 30000) ;; 30 seconds in ms (define _page-data-cache-ttl 30000) ;; 30 seconds in ms
(define page-data-cache-key (define page-data-cache-key :effects []
(fn ((page-name :as string) (params :as dict)) (fn ((page-name :as string) (params :as dict))
;; Build a cache key from page name + params. ;; Build a cache key from page name + params.
;; Params are from route matching so order is deterministic. ;; Params are from route matching so order is deterministic.
@@ -623,7 +623,7 @@
(keys params)) (keys params))
(str base ":" (join "&" parts))))))) (str base ":" (join "&" parts)))))))
(define page-data-cache-get (define page-data-cache-get :effects [mutation io]
(fn ((cache-key :as string)) (fn ((cache-key :as string))
;; Return cached data if fresh, else nil. ;; Return cached data if fresh, else nil.
(let ((entry (get _page-data-cache cache-key))) (let ((entry (get _page-data-cache cache-key)))
@@ -635,7 +635,7 @@
nil) nil)
(get entry "data")))))) (get entry "data"))))))
(define page-data-cache-set (define page-data-cache-set :effects [mutation io]
(fn ((cache-key :as string) data) (fn ((cache-key :as string) data)
;; Store data with current timestamp. ;; Store data with current timestamp.
(dict-set! _page-data-cache cache-key (dict-set! _page-data-cache cache-key
@@ -646,7 +646,7 @@
;; Client-side routing — cache management ;; Client-side routing — cache management
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define invalidate-page-cache (define invalidate-page-cache :effects [mutation io]
(fn ((page-name :as string)) (fn ((page-name :as string))
;; Clear cached data for a page. Removes all cache entries whose key ;; Clear cached data for a page. Removes all cache entries whose key
;; matches page-name (exact) or starts with "page-name:" (with params). ;; matches page-name (exact) or starts with "page-name:" (with params).
@@ -659,14 +659,14 @@
(sw-post-message {"type" "invalidate" "page" page-name}) (sw-post-message {"type" "invalidate" "page" page-name})
(log-info (str "sx:cache invalidate " page-name)))) (log-info (str "sx:cache invalidate " page-name))))
(define invalidate-all-page-cache (define invalidate-all-page-cache :effects [mutation io]
(fn () (fn ()
;; Clear all cached page data and notify service worker. ;; Clear all cached page data and notify service worker.
(set! _page-data-cache (dict)) (set! _page-data-cache (dict))
(sw-post-message {"type" "invalidate" "page" "*"}) (sw-post-message {"type" "invalidate" "page" "*"})
(log-info "sx:cache invalidate *"))) (log-info "sx:cache invalidate *")))
(define update-page-cache (define update-page-cache :effects [mutation io]
(fn ((page-name :as string) data) (fn ((page-name :as string) data)
;; Replace cached data for a page with server-provided data. ;; Replace cached data for a page with server-provided data.
;; Uses a bare page-name key (no params) — the server knows the ;; Uses a bare page-name key (no params) — the server knows the
@@ -675,7 +675,7 @@
(page-data-cache-set cache-key data) (page-data-cache-set cache-key data)
(log-info (str "sx:cache update " page-name))))) (log-info (str "sx:cache update " page-name)))))
(define process-cache-directives (define process-cache-directives :effects [mutation io]
(fn (el (resp-headers :as dict) (response-text :as string)) (fn (el (resp-headers :as dict) (response-text :as string))
;; Process cache invalidation and update directives from both ;; Process cache invalidation and update directives from both
;; element attributes and response headers. ;; element attributes and response headers.
@@ -721,7 +721,7 @@
(define _optimistic-snapshots (dict)) (define _optimistic-snapshots (dict))
(define optimistic-cache-update (define optimistic-cache-update :effects [mutation]
(fn ((cache-key :as string) (mutator :as lambda)) (fn ((cache-key :as string) (mutator :as lambda))
;; Apply predicted mutation to cached data. Saves snapshot for rollback. ;; Apply predicted mutation to cached data. Saves snapshot for rollback.
;; Returns predicted data or nil if no cached data exists. ;; Returns predicted data or nil if no cached data exists.
@@ -734,7 +734,7 @@
(page-data-cache-set cache-key predicted) (page-data-cache-set cache-key predicted)
predicted))))) predicted)))))
(define optimistic-cache-revert (define optimistic-cache-revert :effects [mutation]
(fn ((cache-key :as string)) (fn ((cache-key :as string))
;; Revert to pre-mutation snapshot. Returns restored data or nil. ;; Revert to pre-mutation snapshot. Returns restored data or nil.
(let ((snapshot (get _optimistic-snapshots cache-key))) (let ((snapshot (get _optimistic-snapshots cache-key)))
@@ -743,12 +743,12 @@
(dict-delete! _optimistic-snapshots cache-key) (dict-delete! _optimistic-snapshots cache-key)
snapshot)))) snapshot))))
(define optimistic-cache-confirm (define optimistic-cache-confirm :effects [mutation]
(fn ((cache-key :as string)) (fn ((cache-key :as string))
;; Server accepted — discard the rollback snapshot. ;; Server accepted — discard the rollback snapshot.
(dict-delete! _optimistic-snapshots cache-key))) (dict-delete! _optimistic-snapshots cache-key)))
(define submit-mutation (define submit-mutation :effects [mutation io]
(fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda)) (fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda))
;; Optimistic mutation: predict locally, send to server, confirm or revert. ;; Optimistic mutation: predict locally, send to server, confirm or revert.
;; on-complete is called with "confirmed" or "reverted" status. ;; on-complete is called with "confirmed" or "reverted" status.
@@ -787,14 +787,14 @@
(define _is-online true) (define _is-online true)
(define _offline-queue (list)) (define _offline-queue (list))
(define offline-is-online? (define offline-is-online? :effects [io]
(fn () _is-online)) (fn () _is-online))
(define offline-set-online! (define offline-set-online! :effects [mutation]
(fn ((val :as boolean)) (fn ((val :as boolean))
(set! _is-online val))) (set! _is-online val)))
(define offline-queue-mutation (define offline-queue-mutation :effects [mutation io]
(fn ((action-name :as string) payload (page-name :as string) (params :as dict) (mutator-fn :as lambda)) (fn ((action-name :as string) payload (page-name :as string) (params :as dict) (mutator-fn :as lambda))
;; Queue a mutation for later sync. Apply optimistic update locally. ;; Queue a mutation for later sync. Apply optimistic update locally.
(let ((cache-key (page-data-cache-key page-name params)) (let ((cache-key (page-data-cache-key page-name params))
@@ -813,7 +813,7 @@
(log-info (str "sx:offline queued " action-name " (" (len _offline-queue) " pending)")) (log-info (str "sx:offline queued " action-name " (" (len _offline-queue) " pending)"))
entry))) entry)))
(define offline-sync (define offline-sync :effects [mutation io]
(fn () (fn ()
;; Replay all pending mutations. Called on reconnect. ;; Replay all pending mutations. Called on reconnect.
(let ((pending (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue))) (let ((pending (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue)))
@@ -830,11 +830,11 @@
(log-warn (str "sx:offline sync failed " (get entry "action") ": " error))))) (log-warn (str "sx:offline sync failed " (get entry "action") ": " error)))))
pending))))) pending)))))
(define offline-pending-count (define offline-pending-count :effects [io]
(fn () (fn ()
(len (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue)))) (len (filter (fn ((e :as dict)) (= (get e "status") "pending")) _offline-queue))))
(define offline-aware-mutation (define offline-aware-mutation :effects [mutation io]
(fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda)) (fn ((page-name :as string) (params :as dict) (action-name :as string) payload (mutator-fn :as lambda) (on-complete :as lambda))
;; Top-level mutation function. Routes to submit-mutation when online, ;; Top-level mutation function. Routes to submit-mutation when online,
;; offline-queue-mutation when offline. ;; offline-queue-mutation when offline.
@@ -849,7 +849,7 @@
;; Client-side routing ;; Client-side routing
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define current-page-layout (define current-page-layout :effects [io]
(fn () (fn ()
;; Find the layout name of the currently displayed page by matching ;; Find the layout name of the currently displayed page by matching
;; the browser URL against the page route table. ;; the browser URL against the page route table.
@@ -859,7 +859,7 @@
(or (get match "layout") ""))))) (or (get match "layout") "")))))
(define swap-rendered-content (define swap-rendered-content :effects [mutation io]
(fn (target rendered (pathname :as string)) (fn (target rendered (pathname :as string))
;; Swap rendered DOM content into target and run post-processing. ;; Swap rendered DOM content into target and run post-processing.
;; Shared by pure and data page client routes. ;; Shared by pure and data page client routes.
@@ -875,7 +875,7 @@
(log-info (str "sx:route client " pathname))))) (log-info (str "sx:route client " pathname)))))
(define resolve-route-target (define resolve-route-target :effects [io]
(fn ((target-sel :as string)) (fn ((target-sel :as string))
;; Resolve a target selector to a DOM element, or nil. ;; Resolve a target selector to a DOM element, or nil.
(if (and target-sel (not (= target-sel "true"))) (if (and target-sel (not (= target-sel "true")))
@@ -883,7 +883,7 @@
nil))) nil)))
(define deps-satisfied? (define deps-satisfied? :effects [io]
(fn ((match :as dict)) (fn ((match :as dict))
;; Check if all component deps for a page are loaded client-side. ;; Check if all component deps for a page are loaded client-side.
(let ((deps (get match "deps")) (let ((deps (get match "deps"))
@@ -893,7 +893,7 @@
(every? (fn ((dep :as string)) (contains? loaded dep)) deps))))) (every? (fn ((dep :as string)) (contains? loaded dep)) deps)))))
(define try-client-route (define try-client-route :effects [mutation io]
(fn ((pathname :as string) (target-sel :as string)) (fn ((pathname :as string) (target-sel :as string))
;; Try to render a page client-side. Returns true if successful, false otherwise. ;; Try to render a page client-side. Returns true if successful, false otherwise.
;; target-sel is the CSS selector for the swap target (from sx-boost value). ;; target-sel is the CSS selector for the swap target (from sx-boost value).
@@ -1011,7 +1011,7 @@
true)))))))))))))))))) true))))))))))))))))))
(define bind-client-route-link (define bind-client-route-link :effects [mutation io]
(fn (link (href :as string)) (fn (link (href :as string))
;; Bind a boost link with client-side routing. If the route can be ;; Bind a boost link with client-side routing. If the route can be
;; rendered client-side (pure page, no :data), do so. Otherwise ;; rendered client-side (pure page, no :data), do so. Otherwise
@@ -1026,7 +1026,7 @@
;; SSE processing ;; SSE processing
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define process-sse (define process-sse :effects [mutation io]
(fn (root) (fn (root)
;; Find and bind SSE elements ;; Find and bind SSE elements
(for-each (for-each
@@ -1037,7 +1037,7 @@
(dom-query-all (or root (dom-body)) "[sx-sse]")))) (dom-query-all (or root (dom-body)) "[sx-sse]"))))
(define bind-sse (define bind-sse :effects [mutation io]
(fn (el) (fn (el)
;; Connect to SSE endpoint and bind swap handler ;; Connect to SSE endpoint and bind swap handler
(let ((url (dom-get-attr el "sx-sse"))) (let ((url (dom-get-attr el "sx-sse")))
@@ -1049,7 +1049,7 @@
(bind-sse-swap el data)))))))) (bind-sse-swap el data))))))))
(define bind-sse-swap (define bind-sse-swap :effects [mutation io]
(fn (el (data :as string)) (fn (el (data :as string))
;; Handle an SSE event: swap data into element ;; Handle an SSE event: swap data into element
(let ((target (resolve-target el)) (let ((target (resolve-target el))
@@ -1081,7 +1081,7 @@
;; Inline event handlers ;; Inline event handlers
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define bind-inline-handlers (define bind-inline-handlers :effects [mutation io]
(fn (root) (fn (root)
;; Find elements with sx-on:* attributes and bind SX event handlers. ;; Find elements with sx-on:* attributes and bind SX event handlers.
;; Handler bodies are SX expressions evaluated with `event` and `this` ;; Handler bodies are SX expressions evaluated with `event` and `this`
@@ -1115,7 +1115,7 @@
;; Preload ;; Preload
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define bind-preload-for (define bind-preload-for :effects [mutation io]
(fn (el) (fn (el)
;; Bind preload event listeners based on sx-preload attribute ;; Bind preload event listeners based on sx-preload attribute
(let ((preload-attr (dom-get-attr el "sx-preload"))) (let ((preload-attr (dom-get-attr el "sx-preload")))
@@ -1134,7 +1134,7 @@
(loaded-component-names) _css-hash))))))))))) (loaded-component-names) _css-hash)))))))))))
(define do-preload (define do-preload :effects [mutation io]
(fn ((url :as string) (headers :as dict)) (fn ((url :as string) (headers :as dict))
;; Execute a preload fetch into the cache ;; Execute a preload fetch into the cache
(when (nil? (preload-cache-get _preload-cache url)) (when (nil? (preload-cache-get _preload-cache url))
@@ -1148,7 +1148,7 @@
(define VERB_SELECTOR (define VERB_SELECTOR
(str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]")) (str "[sx-get],[sx-post],[sx-put],[sx-delete],[sx-patch]"))
(define process-elements (define process-elements :effects [mutation io]
(fn (root) (fn (root)
;; Find all elements with sx-* verb attributes and process them. ;; Find all elements with sx-* verb attributes and process them.
(let ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR))) (let ((els (dom-query-all (or root (dom-body)) VERB_SELECTOR)))
@@ -1165,7 +1165,7 @@
(process-emit-elements root))) (process-emit-elements root)))
(define process-one (define process-one :effects [mutation io]
(fn (el) (fn (el)
;; Process a single element with an sx-* verb attribute ;; Process a single element with an sx-* verb attribute
(let ((verb-info (get-verb-info el))) (let ((verb-info (get-verb-info el)))
@@ -1193,7 +1193,7 @@
;; On click → dispatches CustomEvent "cart:add" with detail {id:42, name:"Widget"} ;; On click → dispatches CustomEvent "cart:add" with detail {id:42, name:"Widget"}
;; The event bubbles up to the island container where bridge-event catches it. ;; The event bubbles up to the island container where bridge-event catches it.
(define process-emit-elements (define process-emit-elements :effects [mutation io]
(fn (root) (fn (root)
(let ((els (dom-query-all (or root (dom-body)) "[data-sx-emit]"))) (let ((els (dom-query-all (or root (dom-body)) "[data-sx-emit]")))
(for-each (for-each
@@ -1214,7 +1214,7 @@
;; History: popstate handler ;; History: popstate handler
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define handle-popstate (define handle-popstate :effects [mutation io]
(fn ((scrollY :as number)) (fn ((scrollY :as number))
;; Handle browser back/forward navigation. ;; Handle browser back/forward navigation.
;; Derive target from [sx-boost] container or fall back to #main-panel. ;; Derive target from [sx-boost] container or fall back to #main-panel.
@@ -1241,7 +1241,7 @@
;; Initialization ;; Initialization
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define engine-init (define engine-init :effects [mutation io]
(fn () (fn ()
;; Initialize: CSS tracking, scripts, hydrate, process. ;; Initialize: CSS tracking, scripts, hydrate, process.
(do (do

View File

@@ -49,20 +49,20 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Returns a list of top-level AST expressions. ;; Returns a list of top-level AST expressions.
(define sx-parse (define sx-parse :effects []
(fn ((source :as string)) (fn ((source :as string))
(let ((pos 0) (let ((pos 0)
(len-src (len source))) (len-src (len source)))
;; -- Cursor helpers (closure over pos, source, len-src) -- ;; -- Cursor helpers (closure over pos, source, len-src) --
(define skip-comment (define skip-comment :effects []
(fn () (fn ()
(when (and (< pos len-src) (not (= (nth source pos) "\n"))) (when (and (< pos len-src) (not (= (nth source pos) "\n")))
(set! pos (inc pos)) (set! pos (inc pos))
(skip-comment)))) (skip-comment))))
(define skip-ws (define skip-ws :effects []
(fn () (fn ()
(when (< pos len-src) (when (< pos len-src)
(let ((ch (nth source pos))) (let ((ch (nth source pos)))
@@ -80,11 +80,11 @@
;; -- Atom readers -- ;; -- Atom readers --
(define read-string (define read-string :effects []
(fn () (fn ()
(set! pos (inc pos)) ;; skip opening " (set! pos (inc pos)) ;; skip opening "
(let ((buf "")) (let ((buf ""))
(define read-str-loop (define read-str-loop :effects []
(fn () (fn ()
(if (>= pos len-src) (if (>= pos len-src)
(error "Unterminated string") (error "Unterminated string")
@@ -110,10 +110,10 @@
(read-str-loop) (read-str-loop)
buf))) buf)))
(define read-ident (define read-ident :effects []
(fn () (fn ()
(let ((start pos)) (let ((start pos))
(define read-ident-loop (define read-ident-loop :effects []
(fn () (fn ()
(when (and (< pos len-src) (when (and (< pos len-src)
(ident-char? (nth source pos))) (ident-char? (nth source pos)))
@@ -122,19 +122,19 @@
(read-ident-loop) (read-ident-loop)
(slice source start pos)))) (slice source start pos))))
(define read-keyword (define read-keyword :effects []
(fn () (fn ()
(set! pos (inc pos)) ;; skip : (set! pos (inc pos)) ;; skip :
(make-keyword (read-ident)))) (make-keyword (read-ident))))
(define read-number (define read-number :effects []
(fn () (fn ()
(let ((start pos)) (let ((start pos))
;; Optional leading minus ;; Optional leading minus
(when (and (< pos len-src) (= (nth source pos) "-")) (when (and (< pos len-src) (= (nth source pos) "-"))
(set! pos (inc pos))) (set! pos (inc pos)))
;; Integer digits ;; Integer digits
(define read-digits (define read-digits :effects []
(fn () (fn ()
(when (and (< pos len-src) (when (and (< pos len-src)
(let ((c (nth source pos))) (let ((c (nth source pos)))
@@ -158,7 +158,7 @@
(read-digits)) (read-digits))
(parse-number (slice source start pos))))) (parse-number (slice source start pos)))))
(define read-symbol (define read-symbol :effects []
(fn () (fn ()
(let ((name (read-ident))) (let ((name (read-ident)))
(cond (cond
@@ -169,10 +169,10 @@
;; -- Composite readers -- ;; -- Composite readers --
(define read-list (define read-list :effects []
(fn ((close-ch :as string)) (fn ((close-ch :as string))
(let ((items (list))) (let ((items (list)))
(define read-list-loop (define read-list-loop :effects []
(fn () (fn ()
(skip-ws) (skip-ws)
(if (>= pos len-src) (if (>= pos len-src)
@@ -184,10 +184,10 @@
(read-list-loop) (read-list-loop)
items))) items)))
(define read-map (define read-map :effects []
(fn () (fn ()
(let ((result (dict))) (let ((result (dict)))
(define read-map-loop (define read-map-loop :effects []
(fn () (fn ()
(skip-ws) (skip-ws)
(if (>= pos len-src) (if (>= pos len-src)
@@ -206,10 +206,10 @@
;; -- Raw string reader (for #|...|) -- ;; -- Raw string reader (for #|...|) --
(define read-raw-string (define read-raw-string :effects []
(fn () (fn ()
(let ((buf "")) (let ((buf ""))
(define raw-loop (define raw-loop :effects []
(fn () (fn ()
(if (>= pos len-src) (if (>= pos len-src)
(error "Unterminated raw string") (error "Unterminated raw string")
@@ -224,7 +224,7 @@
;; -- Main expression reader -- ;; -- Main expression reader --
(define read-expr (define read-expr :effects []
(fn () (fn ()
(skip-ws) (skip-ws)
(if (>= pos len-src) (if (>= pos len-src)
@@ -322,7 +322,7 @@
;; -- Entry point: parse all top-level expressions -- ;; -- Entry point: parse all top-level expressions --
(let ((exprs (list))) (let ((exprs (list)))
(define parse-loop (define parse-loop :effects []
(fn () (fn ()
(skip-ws) (skip-ws)
(when (< pos len-src) (when (< pos len-src)
@@ -336,7 +336,7 @@
;; Serializer — AST → SX source text ;; Serializer — AST → SX source text
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define sx-serialize (define sx-serialize :effects []
(fn (val) (fn (val)
(case (type-of val) (case (type-of val)
"nil" "nil" "nil" "nil"
@@ -351,7 +351,7 @@
:else (str val)))) :else (str val))))
(define sx-serialize-dict (define sx-serialize-dict :effects []
(fn ((d :as dict)) (fn ((d :as dict))
(str "{" (str "{"
(join " " (join " "

View File

@@ -575,6 +575,11 @@ def strip_prefix(s, prefix):
return s[len(prefix):] if s.startswith(prefix) else s return s[len(prefix):] if s.startswith(prefix) else s
def debug_log(*args):
import sys
print(*args, file=sys.stderr)
def error(msg): def error(msg):
raise EvalError(msg) raise EvalError(msg)

View File

@@ -71,13 +71,14 @@
;; Shared utilities ;; Shared utilities
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define definition-form? (define definition-form? :effects []
(fn ((name :as string)) (fn ((name :as string))
(or (= name "define") (= name "defcomp") (= name "defisland") (or (= name "define") (= name "defcomp") (= name "defisland")
(= name "defmacro") (= name "defstyle") (= name "defhandler")))) (= name "defmacro") (= name "defstyle") (= name "defhandler")
(= name "deftype") (= name "defeffect"))))
(define parse-element-args (define parse-element-args :effects [render]
(fn ((args :as list) (env :as dict)) (fn ((args :as list) (env :as dict))
;; Parse (:key val :key2 val2 child1 child2) into (attrs-dict children-list) ;; Parse (:key val :key2 val2 child1 child2) into (attrs-dict children-list)
(let ((attrs (dict)) (let ((attrs (dict))
@@ -100,7 +101,7 @@
(list attrs children)))) (list attrs children))))
(define render-attrs (define render-attrs :effects []
(fn ((attrs :as dict)) (fn ((attrs :as dict))
;; Render an attrs dict to an HTML attribute string. ;; Render an attrs dict to an HTML attribute string.
;; Used by adapter-html.sx and adapter-sx.sx. ;; Used by adapter-html.sx and adapter-sx.sx.
@@ -132,13 +133,13 @@
;; eval-cond: find matching cond branch, return unevaluated body expr. ;; eval-cond: find matching cond branch, return unevaluated body expr.
;; Handles both scheme-style ((test body) ...) and clojure-style ;; Handles both scheme-style ((test body) ...) and clojure-style
;; (test body test body ...). ;; (test body test body ...).
(define eval-cond (define eval-cond :effects []
(fn ((clauses :as list) (env :as dict)) (fn ((clauses :as list) (env :as dict))
(if (cond-scheme? clauses) (if (cond-scheme? clauses)
(eval-cond-scheme clauses env) (eval-cond-scheme clauses env)
(eval-cond-clojure clauses env)))) (eval-cond-clojure clauses env))))
(define eval-cond-scheme (define eval-cond-scheme :effects []
(fn ((clauses :as list) (env :as dict)) (fn ((clauses :as list) (env :as dict))
(if (empty? clauses) (if (empty? clauses)
nil nil
@@ -155,7 +156,7 @@
body body
(eval-cond-scheme (rest clauses) env))))))) (eval-cond-scheme (rest clauses) env)))))))
(define eval-cond-clojure (define eval-cond-clojure :effects []
(fn ((clauses :as list) (env :as dict)) (fn ((clauses :as list) (env :as dict))
(if (< (len clauses) 2) (if (< (len clauses) 2)
nil nil
@@ -172,7 +173,7 @@
;; process-bindings: evaluate let-binding pairs, return extended env. ;; process-bindings: evaluate let-binding pairs, return extended env.
;; bindings = ((name1 expr1) (name2 expr2) ...) ;; bindings = ((name1 expr1) (name2 expr2) ...)
(define process-bindings (define process-bindings :effects [mutation]
(fn ((bindings :as list) (env :as dict)) (fn ((bindings :as list) (env :as dict))
;; env-extend (not merge) — Env is not a dict subclass, so merge() ;; env-extend (not merge) — Env is not a dict subclass, so merge()
;; returns an empty dict, losing all parent scope bindings. ;; returns an empty dict, losing all parent scope bindings.
@@ -194,7 +195,7 @@
;; Used by eval-list to dispatch rendering forms to the active adapter ;; Used by eval-list to dispatch rendering forms to the active adapter
;; (HTML, SX wire, or DOM) rather than evaluating them as function calls. ;; (HTML, SX wire, or DOM) rather than evaluating them as function calls.
(define is-render-expr? (define is-render-expr? :effects []
(fn (expr) (fn (expr)
(if (or (not (= (type-of expr) "list")) (empty? expr)) (if (or (not (= (type-of expr) "list")) (empty? expr))
false false

View File

@@ -17,7 +17,7 @@
;; "/" → () ;; "/" → ()
;; "/docs/" → ("docs") ;; "/docs/" → ("docs")
(define split-path-segments (define split-path-segments :effects []
(fn ((path :as string)) (fn ((path :as string))
(let ((trimmed (if (starts-with? path "/") (slice path 1) path))) (let ((trimmed (if (starts-with? path "/") (slice path 1) path)))
(let ((trimmed2 (if (and (not (empty? trimmed)) (let ((trimmed2 (if (and (not (empty? trimmed))
@@ -35,7 +35,7 @@
;; "/docs/<slug>" → ({"type" "literal" "value" "docs"} ;; "/docs/<slug>" → ({"type" "literal" "value" "docs"}
;; {"type" "param" "value" "slug"}) ;; {"type" "param" "value" "slug"})
(define make-route-segment (define make-route-segment :effects []
(fn ((seg :as string)) (fn ((seg :as string))
(if (and (starts-with? seg "<") (ends-with? seg ">")) (if (and (starts-with? seg "<") (ends-with? seg ">"))
(let ((param-name (slice seg 1 (- (len seg) 1)))) (let ((param-name (slice seg 1 (- (len seg) 1))))
@@ -48,7 +48,7 @@
(dict-set! d "value" seg) (dict-set! d "value" seg)
d)))) d))))
(define parse-route-pattern (define parse-route-pattern :effects []
(fn ((pattern :as string)) (fn ((pattern :as string))
(let ((segments (split-path-segments pattern))) (let ((segments (split-path-segments pattern)))
(map make-route-segment segments)))) (map make-route-segment segments))))
@@ -59,7 +59,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Returns params dict if match, nil if no match. ;; Returns params dict if match, nil if no match.
(define match-route-segments (define match-route-segments :effects []
(fn ((path-segs :as list) (parsed-segs :as list)) (fn ((path-segs :as list) (parsed-segs :as list))
(if (not (= (len path-segs) (len parsed-segs))) (if (not (= (len path-segs) (len parsed-segs)))
nil nil
@@ -87,7 +87,7 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Returns params dict (may be empty for exact matches) or nil. ;; Returns params dict (may be empty for exact matches) or nil.
(define match-route (define match-route :effects []
(fn ((path :as string) (pattern :as string)) (fn ((path :as string) (pattern :as string))
(let ((path-segs (split-path-segments path)) (let ((path-segs (split-path-segments path))
(parsed-segs (parse-route-pattern pattern))) (parsed-segs (parse-route-pattern pattern)))
@@ -100,7 +100,7 @@
;; Each entry: {"pattern" "/docs/<slug>" "parsed" [...] "name" "docs-page" ...} ;; Each entry: {"pattern" "/docs/<slug>" "parsed" [...] "name" "docs-page" ...}
;; Returns matching entry with "params" added, or nil. ;; Returns matching entry with "params" added, or nil.
(define find-matching-route (define find-matching-route :effects []
(fn ((path :as string) (routes :as list)) (fn ((path :as string) (routes :as list))
(let ((path-segs (split-path-segments path)) (let ((path-segs (split-path-segments path))
(result nil)) (result nil))

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""Run test-types.sx using the bootstrapped evaluator with types module loaded."""
from __future__ import annotations
import os, sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
sys.path.insert(0, _PROJECT)
from shared.sx.parser import parse_all
from shared.sx.ref.sx_ref import eval_expr, trampoline, make_env
from shared.sx.types import NIL, Component
# Build env with primitives
env = make_env()
# Platform test functions
_suite_stack: list[str] = []
_pass_count = 0
_fail_count = 0
def _try_call(thunk):
try:
trampoline(eval_expr([thunk], env)) # call the thunk
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
def _report_pass(name):
global _pass_count
_pass_count += 1
ctx = " > ".join(_suite_stack)
print(f" PASS: {ctx} > {name}")
return NIL
def _report_fail(name, error):
global _fail_count
_fail_count += 1
ctx = " > ".join(_suite_stack)
print(f" FAIL: {ctx} > {name}: {error}")
return NIL
def _push_suite(name):
_suite_stack.append(name)
print(f"{' ' * (len(_suite_stack)-1)}Suite: {name}")
return NIL
def _pop_suite():
if _suite_stack:
_suite_stack.pop()
return NIL
env["try-call"] = _try_call
env["report-pass"] = _report_pass
env["report-fail"] = _report_fail
env["push-suite"] = _push_suite
env["pop-suite"] = _pop_suite
# Test fixtures — provide the functions that tests expect
# test-prim-types: dict of primitive return types for type inference
def _test_prim_types():
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
# Format: {name → {"positional" [["name" "type"] ...] "rest-type" type-or-nil}}
def _test_prim_param_types():
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": NIL},
"dec": {"positional": [["n", "number"]], "rest-type": NIL},
"upper": {"positional": [["s", "string"]], "rest-type": NIL},
"lower": {"positional": [["s", "string"]], "rest-type": NIL},
"keys": {"positional": [["d", "dict"]], "rest-type": NIL},
"vals": {"positional": [["d", "dict"]], "rest-type": NIL},
}
# test-env: returns a fresh env for use in tests (same as the test env)
def _test_env():
return env
# sx-parse: parse an SX string and return list of AST nodes
def _sx_parse(source):
return parse_all(source)
# dict-get: used in some legacy tests
def _dict_get(d, k):
v = d.get(k) if isinstance(d, dict) else NIL
return v if v is not None else NIL
# component-set-param-types! and component-param-types: type annotation accessors
def _component_set_param_types(comp, types_dict):
comp.param_types = types_dict
return NIL
def _component_param_types(comp):
return getattr(comp, 'param_types', NIL)
# Platform functions used by types.sx but not SX primitives
def _component_params(c):
return c.params
def _component_body(c):
return c.body
def _component_has_children(c):
return c.has_children
def _map_dict(fn, d):
from shared.sx.types import Lambda as _Lambda
result = {}
for k, v in d.items():
if isinstance(fn, _Lambda):
# Call SX lambda through the evaluator
result[k] = trampoline(eval_expr([fn, k, v], env))
else:
result[k] = fn(k, v)
return result
env["test-prim-types"] = _test_prim_types
env["test-prim-param-types"] = _test_prim_param_types
env["test-env"] = _test_env
env["sx-parse"] = _sx_parse
env["dict-get"] = _dict_get
env["component-set-param-types!"] = _component_set_param_types
env["component-param-types"] = _component_param_types
env["component-params"] = _component_params
env["component-body"] = _component_body
env["component-has-children"] = _component_has_children
env["map-dict"] = _map_dict
# Load test framework (macros + assertion helpers)
with open(os.path.join(_HERE, "test-framework.sx")) as f:
for expr in parse_all(f.read()):
trampoline(eval_expr(expr, env))
# Load types module
with open(os.path.join(_HERE, "types.sx")) as f:
for expr in parse_all(f.read()):
trampoline(eval_expr(expr, env))
# Run tests
print("=" * 60)
print("Running test-types.sx")
print("=" * 60)
with open(os.path.join(_HERE, "test-types.sx")) as f:
for expr in parse_all(f.read()):
trampoline(eval_expr(expr, env))
print("=" * 60)
print(f"Results: {_pass_count} passed, {_fail_count} failed")
print("=" * 60)
sys.exit(1 if _fail_count > 0 else 0)

View File

@@ -41,8 +41,8 @@
;; 1. signal — create a reactive container ;; 1. signal — create a reactive container
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define signal (define signal :effects []
(fn (initial-value) (fn ((initial-value :as any))
(make-signal initial-value))) (make-signal initial-value)))
@@ -54,8 +54,8 @@
;; signal as a dependency. Outside reactive context, deref just returns ;; signal as a dependency. Outside reactive context, deref just returns
;; the current value — no subscription, no overhead. ;; the current value — no subscription, no overhead.
(define deref (define deref :effects []
(fn (s) (fn ((s :as any))
(if (not (signal? s)) (if (not (signal? s))
s ;; non-signal values pass through s ;; non-signal values pass through
(let ((ctx (get-tracking-context))) (let ((ctx (get-tracking-context)))
@@ -71,7 +71,7 @@
;; 3. reset! — write a new value, notify subscribers ;; 3. reset! — write a new value, notify subscribers
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define reset! (define reset! :effects [mutation]
(fn ((s :as signal) value) (fn ((s :as signal) value)
(when (signal? s) (when (signal? s)
(let ((old (signal-value s))) (let ((old (signal-value s)))
@@ -84,7 +84,7 @@
;; 4. swap! — update signal via function ;; 4. swap! — update signal via function
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define swap! (define swap! :effects [mutation]
(fn ((s :as signal) (f :as lambda) &rest args) (fn ((s :as signal) (f :as lambda) &rest args)
(when (signal? s) (when (signal? s)
(let ((old (signal-value s)) (let ((old (signal-value s))
@@ -102,7 +102,7 @@
;; of its dependencies change. The dependency set is discovered automatically ;; of its dependencies change. The dependency set is discovered automatically
;; by tracking deref calls during evaluation. ;; by tracking deref calls during evaluation.
(define computed (define computed :effects [mutation]
(fn ((compute-fn :as lambda)) (fn ((compute-fn :as lambda))
(let ((s (make-signal nil)) (let ((s (make-signal nil))
(deps (list)) (deps (list))
@@ -145,7 +145,7 @@
;; Like computed, but doesn't produce a signal value. Returns a dispose ;; Like computed, but doesn't produce a signal value. Returns a dispose
;; function that tears down the effect. ;; function that tears down the effect.
(define effect (define effect :effects [mutation]
(fn ((effect-fn :as lambda)) (fn ((effect-fn :as lambda))
(let ((deps (list)) (let ((deps (list))
(disposed false) (disposed false)
@@ -201,7 +201,7 @@
(define *batch-depth* 0) (define *batch-depth* 0)
(define *batch-queue* (list)) (define *batch-queue* (list))
(define batch (define batch :effects [mutation]
(fn ((thunk :as lambda)) (fn ((thunk :as lambda))
(set! *batch-depth* (+ *batch-depth* 1)) (set! *batch-depth* (+ *batch-depth* 1))
(invoke thunk) (invoke thunk)
@@ -231,14 +231,14 @@
;; ;;
;; If inside a batch, queues the signal. Otherwise, notifies immediately. ;; If inside a batch, queues the signal. Otherwise, notifies immediately.
(define notify-subscribers (define notify-subscribers :effects [mutation]
(fn ((s :as signal)) (fn ((s :as signal))
(if (> *batch-depth* 0) (if (> *batch-depth* 0)
(when (not (contains? *batch-queue* s)) (when (not (contains? *batch-queue* s))
(append! *batch-queue* s)) (append! *batch-queue* s))
(flush-subscribers s)))) (flush-subscribers s))))
(define flush-subscribers (define flush-subscribers :effects [mutation]
(fn ((s :as signal)) (fn ((s :as signal))
(for-each (for-each
(fn ((sub :as lambda)) (sub)) (fn ((sub :as lambda)) (sub))
@@ -268,7 +268,7 @@
;; For computed signals, unsubscribe from all dependencies. ;; For computed signals, unsubscribe from all dependencies.
;; For effects, the dispose function is returned by effect itself. ;; For effects, the dispose function is returned by effect itself.
(define dispose-computed (define dispose-computed :effects [mutation]
(fn ((s :as signal)) (fn ((s :as signal))
(when (signal? s) (when (signal? s)
(for-each (for-each
@@ -287,7 +287,7 @@
(define *island-scope* nil) (define *island-scope* nil)
(define with-island-scope (define with-island-scope :effects [mutation]
(fn ((scope-fn :as lambda) (body-fn :as lambda)) (fn ((scope-fn :as lambda) (body-fn :as lambda))
(let ((prev *island-scope*)) (let ((prev *island-scope*))
(set! *island-scope* scope-fn) (set! *island-scope* scope-fn)
@@ -299,7 +299,7 @@
;; The platform's make-signal should call (register-in-scope s) if ;; The platform's make-signal should call (register-in-scope s) if
;; *island-scope* is non-nil. ;; *island-scope* is non-nil.
(define register-in-scope (define register-in-scope :effects [mutation]
(fn ((disposable :as lambda)) (fn ((disposable :as lambda))
(when *island-scope* (when *island-scope*
(*island-scope* disposable)))) (*island-scope* disposable))))
@@ -322,7 +322,7 @@
;; (dom-set-data el key val) → void — store JS value on element ;; (dom-set-data el key val) → void — store JS value on element
;; (dom-get-data el key) → any — retrieve stored value ;; (dom-get-data el key) → any — retrieve stored value
(define with-marsh-scope (define with-marsh-scope :effects [mutation io]
(fn (marsh-el (body-fn :as lambda)) (fn (marsh-el (body-fn :as lambda))
;; Execute body-fn collecting all disposables into a marsh-local list. ;; Execute body-fn collecting all disposables into a marsh-local list.
;; Nested under the current island scope — if the island is disposed, ;; Nested under the current island scope — if the island is disposed,
@@ -335,7 +335,7 @@
;; Store disposers on the marsh element for later cleanup ;; Store disposers on the marsh element for later cleanup
(dom-set-data marsh-el "sx-marsh-disposers" disposers)))) (dom-set-data marsh-el "sx-marsh-disposers" disposers))))
(define dispose-marsh-scope (define dispose-marsh-scope :effects [mutation io]
(fn (marsh-el) (fn (marsh-el)
;; Dispose all effects/computeds registered in this marsh's scope. ;; Dispose all effects/computeds registered in this marsh's scope.
;; Parent island scope and sibling marshes are unaffected. ;; Parent island scope and sibling marshes are unaffected.
@@ -358,7 +358,7 @@
(define *store-registry* (dict)) (define *store-registry* (dict))
(define def-store (define def-store :effects [mutation]
(fn ((name :as string) (init-fn :as lambda)) (fn ((name :as string) (init-fn :as lambda))
(let ((registry *store-registry*)) (let ((registry *store-registry*))
;; Only create the store once — subsequent calls return existing ;; Only create the store once — subsequent calls return existing
@@ -366,14 +366,14 @@
(set! *store-registry* (assoc registry name (invoke init-fn)))) (set! *store-registry* (assoc registry name (invoke init-fn))))
(get *store-registry* name)))) (get *store-registry* name))))
(define use-store (define use-store :effects []
(fn ((name :as string)) (fn ((name :as string))
(if (has-key? *store-registry* name) (if (has-key? *store-registry* name)
(get *store-registry* name) (get *store-registry* name)
(error (str "Store not found: " name (error (str "Store not found: " name
". Call (def-store ...) before (use-store ...)."))))) ". Call (def-store ...) before (use-store ...).")))))
(define clear-stores (define clear-stores :effects [mutation]
(fn () (fn ()
(set! *store-registry* (dict)))) (set! *store-registry* (dict))))
@@ -401,11 +401,11 @@
;; ;;
;; These are platform primitives because they require browser DOM APIs. ;; These are platform primitives because they require browser DOM APIs.
(define emit-event (define emit-event :effects [io]
(fn (el (event-name :as string) detail) (fn (el (event-name :as string) detail)
(dom-dispatch el event-name detail))) (dom-dispatch el event-name detail)))
(define on-event (define on-event :effects [io]
(fn (el (event-name :as string) (handler :as lambda)) (fn (el (event-name :as string) (handler :as lambda))
(dom-listen el event-name handler))) (dom-listen el event-name handler)))
@@ -415,7 +415,7 @@
;; When the effect is disposed (island teardown), the listener is ;; When the effect is disposed (island teardown), the listener is
;; removed automatically via the cleanup return. ;; removed automatically via the cleanup return.
(define bridge-event (define bridge-event :effects [mutation io]
(fn (el (event-name :as string) (target-signal :as signal) transform-fn) (fn (el (event-name :as string) (target-signal :as signal) transform-fn)
(effect (fn () (effect (fn ()
(let ((remove (dom-listen el event-name (let ((remove (dom-listen el event-name
@@ -449,7 +449,7 @@
;; Platform interface required: ;; Platform interface required:
;; (promise-then promise on-resolve on-reject) → void ;; (promise-then promise on-resolve on-reject) → void
(define resource (define resource :effects [mutation io]
(fn ((fetch-fn :as lambda)) (fn ((fetch-fn :as lambda))
(let ((state (signal (dict "loading" true "data" nil "error" nil)))) (let ((state (signal (dict "loading" true "data" nil "error" nil))))
;; Kick off the async operation ;; Kick off the async operation

View File

@@ -209,6 +209,29 @@
:example "(defmacro unless (condition &rest body) :example "(defmacro unless (condition &rest body)
`(when (not ~condition) ~@body))") `(when (not ~condition) ~@body))")
(define-special-form "deftype"
:syntax (deftype name body)
:doc "Define a named type. The name can be a simple symbol for type aliases
and records, or a list (name param ...) for parameterized types.
Body is a type expression: a symbol (alias), (union t1 t2 ...) for
union types, or {:field1 type1 :field2 type2} for record types.
Type definitions are metadata for the type checker with no runtime cost."
:tail-position "none"
:example "(deftype price number)
(deftype card-props {:title string :price number})
(deftype (maybe a) (union a nil))")
(define-special-form "defeffect"
:syntax (defeffect name)
:doc "Declare a named effect. Effects annotate functions and components
to track side effects. A pure function (:effects [pure]) cannot
call IO functions. Unannotated functions are assumed to have all
effects. Effect checking is gradual — annotations opt in."
:tail-position "none"
:example "(defeffect io)
(defeffect async)
(define add :effects [pure] (fn (a b) (+ a b)))")
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Sequencing and threading ;; Sequencing and threading

View File

@@ -534,6 +534,11 @@ def strip_prefix(s, prefix):
return s[len(prefix):] if s.startswith(prefix) else s return s[len(prefix):] if s.startswith(prefix) else s
def debug_log(*args):
import sys
print(*args, file=sys.stderr)
def error(msg): def error(msg):
raise EvalError(msg) raise EvalError(msg)
@@ -1255,6 +1260,10 @@ def eval_list(expr, env):
return sf_defquery(args, env) return sf_defquery(args, env)
elif sx_truthy((name == 'defaction')): elif sx_truthy((name == 'defaction')):
return sf_defaction(args, env) return sf_defaction(args, env)
elif sx_truthy((name == 'deftype')):
return sf_deftype(args, env)
elif sx_truthy((name == 'defeffect')):
return sf_defeffect(args, env)
elif sx_truthy((name == 'begin')): elif sx_truthy((name == 'begin')):
return sf_begin(args, env) return sf_begin(args, env)
elif sx_truthy((name == 'do')): elif sx_truthy((name == 'do')):
@@ -1504,10 +1513,18 @@ def sf_lambda(args, env):
# sf-define # sf-define
def sf_define(args, env): def sf_define(args, env):
name_sym = first(args) name_sym = first(args)
value = trampoline(eval_expr(nth(args, 1), env)) has_effects = ((len(args) >= 4) if not sx_truthy((len(args) >= 4)) else ((type_of(nth(args, 1)) == 'keyword') if not sx_truthy((type_of(nth(args, 1)) == 'keyword')) else (keyword_name(nth(args, 1)) == 'effects')))
val_idx = (3 if sx_truthy(((len(args) >= 4) if not sx_truthy((len(args) >= 4)) else ((type_of(nth(args, 1)) == 'keyword') if not sx_truthy((type_of(nth(args, 1)) == 'keyword')) else (keyword_name(nth(args, 1)) == 'effects')))) else 1)
value = trampoline(eval_expr(nth(args, val_idx), env))
if sx_truthy((is_lambda(value) if not sx_truthy(is_lambda(value)) else is_nil(lambda_name(value)))): if sx_truthy((is_lambda(value) if not sx_truthy(is_lambda(value)) else is_nil(lambda_name(value)))):
value.name = symbol_name(name_sym) value.name = symbol_name(name_sym)
env[symbol_name(name_sym)] = value env[symbol_name(name_sym)] = value
if sx_truthy(has_effects):
effects_raw = nth(args, 2)
effect_list = (map(lambda e: (symbol_name(e) if sx_truthy((type_of(e) == 'symbol')) else sx_str(e)), effects_raw) if sx_truthy((type_of(effects_raw) == 'list')) else [sx_str(effects_raw)])
effect_anns = (env_get(env, '*effect-annotations*') if sx_truthy(env_has(env, '*effect-annotations*')) else {})
effect_anns[symbol_name(name_sym)] = effect_list
env['*effect-annotations*'] = effect_anns
return value return value
# sf-defcomp # sf-defcomp
@@ -1522,8 +1539,14 @@ def sf_defcomp(args, env):
param_types = nth(parsed, 2) param_types = nth(parsed, 2)
affinity = defcomp_kwarg(args, 'affinity', 'auto') affinity = defcomp_kwarg(args, 'affinity', 'auto')
comp = make_component(comp_name, params, has_children, body, env, affinity) comp = make_component(comp_name, params, has_children, body, env, affinity)
effects = defcomp_kwarg(args, 'effects', NIL)
if sx_truthy(((not sx_truthy(is_nil(param_types))) if not sx_truthy((not sx_truthy(is_nil(param_types)))) else (not sx_truthy(empty_p(keys(param_types)))))): if sx_truthy(((not sx_truthy(is_nil(param_types))) if not sx_truthy((not sx_truthy(is_nil(param_types)))) else (not sx_truthy(empty_p(keys(param_types)))))):
component_set_param_types(comp, param_types) component_set_param_types(comp, param_types)
if sx_truthy((not sx_truthy(is_nil(effects)))):
effect_list = (map(lambda e: (symbol_name(e) if sx_truthy((type_of(e) == 'symbol')) else sx_str(e)), effects) if sx_truthy((type_of(effects) == 'list')) else [sx_str(effects)])
effect_anns = (env_get(env, '*effect-annotations*') if sx_truthy(env_has(env, '*effect-annotations*')) else {})
effect_anns[symbol_name(name_sym)] = effect_list
env['*effect-annotations*'] = effect_anns
env[symbol_name(name_sym)] = comp env[symbol_name(name_sym)] = comp
return comp return comp
@@ -1610,6 +1633,62 @@ def sf_defstyle(args, env):
env[symbol_name(name_sym)] = value env[symbol_name(name_sym)] = value
return value return value
# make-type-def
def make_type_def(name, params, body):
return {'name': name, 'params': params, 'body': body}
# normalize-type-body
def normalize_type_body(body):
if sx_truthy(is_nil(body)):
return 'nil'
elif sx_truthy((type_of(body) == 'symbol')):
return symbol_name(body)
elif sx_truthy((type_of(body) == 'string')):
return body
elif sx_truthy((type_of(body) == 'keyword')):
return keyword_name(body)
elif sx_truthy((type_of(body) == 'dict')):
return map_dict(lambda k, v: normalize_type_body(v), body)
elif sx_truthy((type_of(body) == 'list')):
if sx_truthy(empty_p(body)):
return 'any'
else:
head = first(body)
head_name = (symbol_name(head) if sx_truthy((type_of(head) == 'symbol')) else sx_str(head))
if sx_truthy((head_name == 'union')):
return cons('or', map(normalize_type_body, rest(body)))
else:
return cons(head_name, map(normalize_type_body, rest(body)))
else:
return sx_str(body)
# sf-deftype
def sf_deftype(args, env):
name_or_form = first(args)
body_expr = nth(args, 1)
type_name = NIL
type_params = []
if sx_truthy((type_of(name_or_form) == 'symbol')):
type_name = symbol_name(name_or_form)
else:
if sx_truthy((type_of(name_or_form) == 'list')):
type_name = symbol_name(first(name_or_form))
type_params = map(lambda p: (symbol_name(p) if sx_truthy((type_of(p) == 'symbol')) else sx_str(p)), rest(name_or_form))
body = normalize_type_body(body_expr)
registry = (env_get(env, '*type-registry*') if sx_truthy(env_has(env, '*type-registry*')) else {})
registry[type_name] = make_type_def(type_name, type_params, body)
env['*type-registry*'] = registry
return NIL
# sf-defeffect
def sf_defeffect(args, env):
effect_name = (symbol_name(first(args)) if sx_truthy((type_of(first(args)) == 'symbol')) else sx_str(first(args)))
registry = (env_get(env, '*effect-registry*') if sx_truthy(env_has(env, '*effect-registry*')) else [])
if sx_truthy((not sx_truthy(contains_p(registry, effect_name)))):
registry.append(effect_name)
env['*effect-registry*'] = registry
return NIL
# sf-begin # sf-begin
def sf_begin(args, env): def sf_begin(args, env):
if sx_truthy(empty_p(args)): if sx_truthy(empty_p(args)):
@@ -1869,7 +1948,7 @@ BOOLEAN_ATTRS = ['async', 'autofocus', 'autoplay', 'checked', 'controls', 'defau
# definition-form? # definition-form?
def is_definition_form(name): def is_definition_form(name):
return ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defisland') if sx_truthy((name == 'defisland')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else (name == 'defhandler')))))) return ((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defisland') if sx_truthy((name == 'defisland')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect'))))))))
# parse-element-args # parse-element-args
def parse_element_args(args, env): def parse_element_args(args, env):
@@ -1995,7 +2074,7 @@ def render_value_to_html(val, env):
return escape_html(sx_str(val)) return escape_html(sx_str(val))
# RENDER_HTML_FORMS # RENDER_HTML_FORMS
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each'] RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each']
# render-html-form? # render-html-form?
def is_render_html_form(name): def is_render_html_form(name):
@@ -2285,7 +2364,7 @@ def aser_call(name, args, env):
return sx_str('(', join(' ', parts), ')') return sx_str('(', join(' ', parts), ')')
# SPECIAL_FORM_NAMES # SPECIAL_FORM_NAMES
SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland'] SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland', 'deftype', 'defeffect']
# HO_FORM_NAMES # HO_FORM_NAMES
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each'] HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each']
@@ -2379,7 +2458,7 @@ def aser_special(name, expr, env):
elif sx_truthy((name == 'defisland')): elif sx_truthy((name == 'defisland')):
trampoline(eval_expr(expr, env)) trampoline(eval_expr(expr, env))
return serialize(expr) return serialize(expr)
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else (name == 'defrelation')))))))))): elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'defrelation') if sx_truthy((name == 'defrelation')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect')))))))))))):
trampoline(eval_expr(expr, env)) trampoline(eval_expr(expr, env))
return NIL return NIL
else: else:
@@ -3143,7 +3222,7 @@ async def async_map_render(exprs, env, ctx):
return results return results
# ASYNC_RENDER_FORMS # ASYNC_RENDER_FORMS
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'map', 'map-indexed', 'filter', 'for-each'] ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each']
# async-render-form? # async-render-form?
def async_render_form_p(name): def async_render_form_p(name):
@@ -3518,7 +3597,7 @@ async def async_aser_call(name, args, env, ctx):
return make_sx_expr(sx_str('(', join(' ', parts), ')')) return make_sx_expr(sx_str('(', join(' ', parts), ')'))
# ASYNC_ASER_FORM_NAMES # ASYNC_ASER_FORM_NAMES
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland'] ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect']
# ASYNC_ASER_HO_NAMES # ASYNC_ASER_HO_NAMES
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each'] ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
@@ -3609,7 +3688,7 @@ async def dispatch_async_aser_form(name, expr, env, ctx):
elif sx_truthy((name == 'defisland')): elif sx_truthy((name == 'defisland')):
(await async_eval(expr, env, ctx)) (await async_eval(expr, env, ctx))
return serialize(expr) return serialize(expr)
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else (name == 'defaction'))))))))): elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect'))))))))))):
(await async_eval(expr, env, ctx)) (await async_eval(expr, env, ctx))
return NIL return NIL
else: else:
@@ -3858,4 +3937,4 @@ def render(expr, env=None):
def make_env(**kwargs): def make_env(**kwargs):
"""Create an environment with initial bindings.""" """Create an environment with initial bindings."""
return _Env(dict(kwargs)) return _Env(dict(kwargs))

View File

@@ -427,6 +427,173 @@
(body (first (sx-parse "(div (+ name 1))"))) (body (first (sx-parse "(div (+ name 1))")))
(type-env {"name" "string"}) (type-env {"name" "string"})
(diagnostics (list))) (diagnostics (list)))
(check-body-walk body "~bad-math" type-env (test-prim-types) ppt (test-env) diagnostics) (check-body-walk body "~bad-math" type-env (test-prim-types) ppt (test-env) diagnostics nil nil)
(assert-true (> (len diagnostics) 0)) (assert-true (> (len diagnostics) 0))
(assert-equal "error" (get (first diagnostics) "level"))))) (assert-equal "error" (get (first diagnostics) "level")))))
;; --------------------------------------------------------------------------
;; deftype — type aliases
;; --------------------------------------------------------------------------
(defsuite "deftype-alias"
(deftest "simple alias resolves"
(let ((registry {"price" {:name "price" :params () :body "number"}}))
(assert-equal "number" (resolve-type "price" registry))))
(deftest "alias chain resolves"
(let ((registry {"price" {:name "price" :params () :body "number"}
"cost" {:name "cost" :params () :body "price"}}))
(assert-equal "number" (resolve-type "cost" registry))))
(deftest "unknown type passes through"
(let ((registry {"price" {:name "price" :params () :body "number"}}))
(assert-equal "string" (resolve-type "string" registry))))
(deftest "subtype-resolved? works through alias"
(let ((registry {"price" {:name "price" :params () :body "number"}}))
(assert-true (subtype-resolved? "price" "number" registry))
(assert-true (subtype-resolved? "number" "price" registry)))))
;; --------------------------------------------------------------------------
;; deftype — union types
;; --------------------------------------------------------------------------
(defsuite "deftype-union"
(deftest "union resolves"
(let ((registry {"status" {:name "status" :params () :body ("or" "string" "number")}}))
(let ((resolved (resolve-type "status" registry)))
(assert-true (= (type-of resolved) "list"))
(assert-equal "or" (first resolved)))))
(deftest "subtype through named union"
(let ((registry {"status" {:name "status" :params () :body ("or" "string" "number")}}))
(assert-true (subtype-resolved? "string" "status" registry))
(assert-true (subtype-resolved? "number" "status" registry))
(assert-false (subtype-resolved? "boolean" "status" registry)))))
;; --------------------------------------------------------------------------
;; deftype — record types
;; --------------------------------------------------------------------------
(defsuite "deftype-record"
(deftest "record resolves to dict"
(let ((registry {"card-props" {:name "card-props" :params ()
:body {"title" "string" "price" "number"}}}))
(let ((resolved (resolve-type "card-props" registry)))
(assert-equal "dict" (type-of resolved))
(assert-equal "string" (get resolved "title"))
(assert-equal "number" (get resolved "price")))))
(deftest "record structural subtyping"
(let ((registry {"card-props" {:name "card-props" :params ()
:body {"title" "string" "price" "number"}}
"titled" {:name "titled" :params ()
:body {"title" "string"}}}))
;; card-props has title+price, titled has just title
;; card-props <: titled (has all required fields)
(assert-true (subtype-resolved? "card-props" "titled" registry))))
(deftest "get infers field type from record"
(let ((registry {"card-props" {:name "card-props" :params ()
:body {"title" "string" "price" "number"}}})
(type-env {"d" "card-props"})
(expr (first (sx-parse "(get d :title)"))))
(assert-equal "string"
(infer-type expr type-env (test-prim-types) registry)))))
;; --------------------------------------------------------------------------
;; deftype — parameterized types
;; --------------------------------------------------------------------------
(defsuite "deftype-parameterized"
(deftest "maybe instantiation"
(let ((registry {"maybe" {:name "maybe" :params ("a")
:body ("or" "a" "nil")}}))
(let ((resolved (resolve-type (list "maybe" "string") registry)))
(assert-true (= (type-of resolved) "list"))
(assert-equal "or" (first resolved))
(assert-true (contains? resolved "string"))
(assert-true (contains? resolved "nil")))))
(deftest "subtype through parameterized type"
(let ((registry {"maybe" {:name "maybe" :params ("a")
:body ("or" "a" "nil")}}))
(assert-true (subtype-resolved? "string" (list "maybe" "string") registry))
(assert-true (subtype-resolved? "nil" (list "maybe" "string") registry))
(assert-false (subtype-resolved? "number" (list "maybe" "string") registry))))
(deftest "substitute-type-vars works"
(let ((result (substitute-type-vars ("or" "a" "nil") (list "a") (list "number"))))
(assert-equal "or" (first result))
(assert-true (contains? result "number"))
(assert-true (contains? result "nil")))))
;; --------------------------------------------------------------------------
;; defeffect — effect basics
;; --------------------------------------------------------------------------
(defsuite "defeffect-basics"
(deftest "get-effects returns nil for unannotated"
(let ((anns {"fetch" ("io")}))
(assert-true (nil? (get-effects "unknown" anns)))))
(deftest "get-effects returns effects for annotated"
(let ((anns {"fetch" ("io")}))
(assert-equal (list "io") (get-effects "fetch" anns))))
(deftest "nil annotations returns nil"
(assert-true (nil? (get-effects "anything" nil)))))
;; --------------------------------------------------------------------------
;; defeffect — effect checking
;; --------------------------------------------------------------------------
(defsuite "effect-checking"
(deftest "pure cannot call io"
(let ((anns {"~pure-comp" () "fetch" ("io")}))
(let ((diagnostics (check-effect-call "fetch" (list) anns "~pure-comp")))
(assert-true (> (len diagnostics) 0))
(assert-equal "error" (get (first diagnostics) "level")))))
(deftest "io context allows io"
(let ((anns {"~io-comp" ("io") "fetch" ("io")}))
(let ((diagnostics (check-effect-call "fetch" (list "io") anns "~io-comp")))
(assert-equal 0 (len diagnostics)))))
(deftest "unannotated caller allows everything"
(let ((anns {"fetch" ("io")}))
(let ((diagnostics (check-effect-call "fetch" nil anns "~unknown")))
(assert-equal 0 (len diagnostics)))))
(deftest "unannotated callee skips check"
(let ((anns {"~pure-comp" ()}))
(let ((diagnostics (check-effect-call "unknown-fn" (list) anns "~pure-comp")))
(assert-equal 0 (len diagnostics))))))
;; --------------------------------------------------------------------------
;; defeffect — subset checking
;; --------------------------------------------------------------------------
(defsuite "effect-subset"
(deftest "empty is subset of anything"
(assert-true (effects-subset? (list) (list "io")))
(assert-true (effects-subset? (list) (list))))
(deftest "io is subset of io"
(assert-true (effects-subset? (list "io") (list "io" "async"))))
(deftest "io is not subset of pure"
(assert-false (effects-subset? (list "io") (list))))
(deftest "nil callee skips check"
(assert-true (effects-subset? nil (list))))
(deftest "nil caller allows all"
(assert-true (effects-subset? (list "io") nil))))

View File

@@ -224,7 +224,7 @@
;; type-env is a dict mapping variable names → types. ;; type-env is a dict mapping variable names → types.
(define infer-type (define infer-type
(fn (node (type-env :as dict) (prim-types :as dict)) (fn (node (type-env :as dict) (prim-types :as dict) type-registry)
(let ((kind (type-of node))) (let ((kind (type-of node)))
(if (= kind "number") "number" (if (= kind "number") "number"
(if (= kind "string") "string" (if (= kind "string") "string"
@@ -234,24 +234,24 @@
(if (= kind "symbol") (if (= kind "symbol")
(let ((name (symbol-name node))) (let ((name (symbol-name node)))
;; Look up in type env ;; Look up in type env
(if (dict-has? type-env name) (if (has-key? type-env name)
(dict-get type-env name) (get type-env name)
;; Builtins ;; Builtins
(if (= name "true") "boolean" (if (= name "true") "boolean"
(if (= name "false") "boolean" (if (= name "false") "boolean"
(if (= name "nil") "nil" (if (= name "nil") "nil"
;; Check primitive return types ;; Check primitive return types
(if (dict-has? prim-types name) (if (has-key? prim-types name)
(dict-get prim-types name) (get prim-types name)
"any")))))) "any"))))))
(if (= kind "dict") "dict" (if (= kind "dict") "dict"
(if (= kind "list") (if (= kind "list")
(infer-list-type node type-env prim-types) (infer-list-type node type-env prim-types type-registry)
"any"))))))))))) "any")))))))))))
(define infer-list-type (define infer-list-type
(fn (node (type-env :as dict) (prim-types :as dict)) (fn (node (type-env :as dict) (prim-types :as dict) type-registry)
;; Infer type of a list expression (function call, special form, etc.) ;; Infer type of a list expression (function call, special form, etc.)
(if (empty? node) "list" (if (empty? node) "list"
(let ((head (first node)) (let ((head (first node))
@@ -261,32 +261,32 @@
(let ((name (symbol-name head))) (let ((name (symbol-name head)))
;; Special forms ;; Special forms
(if (= name "if") (if (= name "if")
(infer-if-type args type-env prim-types) (infer-if-type args type-env prim-types type-registry)
(if (= name "when") (if (= name "when")
(if (>= (len args) 2) (if (>= (len args) 2)
(type-union (infer-type (last args) type-env prim-types) "nil") (type-union (infer-type (last args) type-env prim-types type-registry) "nil")
"nil") "nil")
(if (or (= name "cond") (= name "case")) (if (or (= name "cond") (= name "case"))
"any" ;; complex — could be refined later "any" ;; complex — could be refined later
(if (= name "let") (if (= name "let")
(infer-let-type args type-env prim-types) (infer-let-type args type-env prim-types type-registry)
(if (or (= name "do") (= name "begin")) (if (or (= name "do") (= name "begin"))
(if (empty? args) "nil" (if (empty? args) "nil"
(infer-type (last args) type-env prim-types)) (infer-type (last args) type-env prim-types type-registry))
(if (or (= name "lambda") (= name "fn")) (if (or (= name "lambda") (= name "fn"))
"lambda" "lambda"
(if (= name "and") (if (= name "and")
(if (empty? args) "boolean" (if (empty? args) "boolean"
(infer-type (last args) type-env prim-types)) (infer-type (last args) type-env prim-types type-registry))
(if (= name "or") (if (= name "or")
(if (empty? args) "boolean" (if (empty? args) "boolean"
;; or returns first truthy — union of all args ;; or returns first truthy — union of all args
(reduce type-union "never" (reduce type-union "never"
(map (fn (a) (infer-type a type-env prim-types)) args))) (map (fn (a) (infer-type a type-env prim-types type-registry)) args)))
(if (= name "map") (if (= name "map")
;; map returns a list ;; map returns a list
(if (>= (len args) 2) (if (>= (len args) 2)
(let ((fn-type (infer-type (first args) type-env prim-types))) (let ((fn-type (infer-type (first args) type-env prim-types type-registry)))
;; If the fn's return type is known, produce (list-of return-type) ;; If the fn's return type is known, produce (list-of return-type)
(if (and (= (type-of fn-type) "list") (if (and (= (type-of fn-type) "list")
(= (first fn-type) "->")) (= (first fn-type) "->"))
@@ -296,7 +296,7 @@
(if (= name "filter") (if (= name "filter")
;; filter preserves element type ;; filter preserves element type
(if (>= (len args) 2) (if (>= (len args) 2)
(infer-type (nth args 1) type-env prim-types) (infer-type (nth args 1) type-env prim-types type-registry)
"list") "list")
(if (= name "reduce") (if (= name "reduce")
;; reduce returns the accumulator type — too complex to infer ;; reduce returns the accumulator type — too complex to infer
@@ -311,26 +311,45 @@
"string" "string"
(if (= name "not") (if (= name "not")
"boolean" "boolean"
(if (= name "get")
;; get — resolve record field type from type registry
(if (and (>= (len args) 2) (not (nil? type-registry)))
(let ((dict-type (infer-type (first args) type-env prim-types type-registry))
(key-arg (nth args 1))
(key-name (cond
(= (type-of key-arg) "keyword") (keyword-name key-arg)
(= (type-of key-arg) "string") key-arg
:else nil)))
(if (and key-name
(= (type-of dict-type) "string")
(has-key? type-registry dict-type))
(let ((resolved (resolve-type dict-type type-registry)))
(if (and (= (type-of resolved) "dict")
(has-key? resolved key-name))
(get resolved key-name)
"any"))
"any"))
"any")
(if (starts-with? name "~") (if (starts-with? name "~")
"element" ;; component call "element" ;; component call
;; Regular function call: look up return type ;; Regular function call: look up return type
(if (dict-has? prim-types name) (if (has-key? prim-types name)
(dict-get prim-types name) (get prim-types name)
"any")))))))))))))))))))))))) "any")))))))))))))))))))))))))
(define infer-if-type (define infer-if-type
(fn ((args :as list) (type-env :as dict) (prim-types :as dict)) (fn ((args :as list) (type-env :as dict) (prim-types :as dict) type-registry)
;; (if test then else?) → union of then and else types ;; (if test then else?) → union of then and else types
(if (< (len args) 2) "nil" (if (< (len args) 2) "nil"
(let ((then-type (infer-type (nth args 1) type-env prim-types))) (let ((then-type (infer-type (nth args 1) type-env prim-types type-registry)))
(if (>= (len args) 3) (if (>= (len args) 3)
(type-union then-type (infer-type (nth args 2) type-env prim-types)) (type-union then-type (infer-type (nth args 2) type-env prim-types type-registry))
(type-union then-type "nil")))))) (type-union then-type "nil"))))))
(define infer-let-type (define infer-let-type
(fn ((args :as list) (type-env :as dict) (prim-types :as dict)) (fn ((args :as list) (type-env :as dict) (prim-types :as dict) type-registry)
;; (let ((x expr) ...) body) → type of body in extended type-env ;; (let ((x expr) ...) body) → type of body in extended type-env
(if (< (len args) 2) "nil" (if (< (len args) 2) "nil"
(let ((bindings (first args)) (let ((bindings (first args))
@@ -343,10 +362,10 @@
(let ((name (if (= (type-of (first binding)) "symbol") (let ((name (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding)) (symbol-name (first binding))
(str (first binding)))) (str (first binding))))
(val-type (infer-type (nth binding 1) extended prim-types))) (val-type (infer-type (nth binding 1) extended prim-types type-registry)))
(dict-set! extended name val-type)))) (dict-set! extended name val-type))))
bindings) bindings)
(infer-type body extended prim-types))))) (infer-type body extended prim-types type-registry)))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -371,14 +390,14 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define check-primitive-call (define check-primitive-call
(fn ((name :as string) (args :as list) (type-env :as dict) (prim-types :as dict) prim-param-types (comp-name :as string)) (fn ((name :as string) (args :as list) (type-env :as dict) (prim-types :as dict) prim-param-types (comp-name :as string) type-registry)
;; Check a primitive call site against declared param types. ;; Check a primitive call site against declared param types.
;; prim-param-types is a dict: {prim-name → {:positional [...] :rest-type type-or-nil}} ;; prim-param-types is a dict: {prim-name → {:positional [...] :rest-type type-or-nil}}
;; Each positional entry is a list (name type-or-nil). ;; Each positional entry is a list (name type-or-nil).
;; Returns list of diagnostics. ;; Returns list of diagnostics.
(let ((diagnostics (list))) (let ((diagnostics (list)))
(when (and (not (nil? prim-param-types)) (when (and (not (nil? prim-param-types))
(dict-has? prim-param-types name)) (has-key? prim-param-types name))
(let ((sig (get prim-param-types name)) (let ((sig (get prim-param-types name))
(positional (get sig "positional")) (positional (get sig "positional"))
(rest-type (get sig "rest-type"))) (rest-type (get sig "rest-type")))
@@ -392,10 +411,10 @@
(arg-expr (nth args idx))) (arg-expr (nth args idx)))
(let ((expected-type (nth param-info 1))) (let ((expected-type (nth param-info 1)))
(when (not (nil? expected-type)) (when (not (nil? expected-type))
(let ((actual (infer-type arg-expr type-env prim-types))) (let ((actual (infer-type arg-expr type-env prim-types type-registry)))
(when (and (not (type-any? expected-type)) (when (and (not (type-any? expected-type))
(not (type-any? actual)) (not (type-any? actual))
(not (subtype? actual expected-type))) (not (subtype-resolved? actual expected-type type-registry)))
(append! diagnostics (append! diagnostics
(make-diagnostic "error" (make-diagnostic "error"
(str "Argument " (+ idx 1) " of `" name (str "Argument " (+ idx 1) " of `" name
@@ -404,10 +423,10 @@
;; Rest param — check against rest-type ;; Rest param — check against rest-type
(when (not (nil? rest-type)) (when (not (nil? rest-type))
(let ((arg-expr (nth args idx)) (let ((arg-expr (nth args idx))
(actual (infer-type arg-expr type-env prim-types))) (actual (infer-type arg-expr type-env prim-types type-registry)))
(when (and (not (type-any? rest-type)) (when (and (not (type-any? rest-type))
(not (type-any? actual)) (not (type-any? actual))
(not (subtype? actual rest-type))) (not (subtype-resolved? actual rest-type type-registry)))
(append! diagnostics (append! diagnostics
(make-diagnostic "error" (make-diagnostic "error"
(str "Argument " (+ idx 1) " of `" name (str "Argument " (+ idx 1) " of `" name
@@ -418,7 +437,7 @@
(define check-component-call (define check-component-call
(fn ((comp-name :as string) (comp :as component) (call-args :as list) (type-env :as dict) (prim-types :as dict)) (fn ((comp-name :as string) (comp :as component) (call-args :as list) (type-env :as dict) (prim-types :as dict) type-registry)
;; Check a component call site against its declared param types. ;; Check a component call site against its declared param types.
;; comp is the component value, call-args is the list of args ;; comp is the component value, call-args is the list of args
;; from the call site (after the component name). ;; from the call site (after the component name).
@@ -440,12 +459,12 @@
(when (< (+ idx 1) (len call-args)) (when (< (+ idx 1) (len call-args))
(let ((val-expr (nth call-args (+ idx 1)))) (let ((val-expr (nth call-args (+ idx 1))))
;; Check type of value against declared param type ;; Check type of value against declared param type
(when (dict-has? param-types key-name) (when (has-key? param-types key-name)
(let ((expected (dict-get param-types key-name)) (let ((expected (get param-types key-name))
(actual (infer-type val-expr type-env prim-types))) (actual (infer-type val-expr type-env prim-types type-registry)))
(when (and (not (type-any? expected)) (when (and (not (type-any? expected))
(not (type-any? actual)) (not (type-any? actual))
(not (subtype? actual expected))) (not (subtype-resolved? actual expected type-registry)))
(append! diagnostics (append! diagnostics
(make-diagnostic "error" (make-diagnostic "error"
(str "Keyword :" key-name " of " comp-name (str "Keyword :" key-name " of " comp-name
@@ -456,9 +475,9 @@
;; Check for missing required params (those with declared types) ;; Check for missing required params (those with declared types)
(for-each (for-each
(fn (param-name) (fn (param-name)
(when (and (dict-has? param-types param-name) (when (and (has-key? param-types param-name)
(not (contains? provided-keys param-name)) (not (contains? provided-keys param-name))
(not (type-nullable? (dict-get param-types param-name)))) (not (type-nullable? (get param-types param-name))))
(append! diagnostics (append! diagnostics
(make-diagnostic "warning" (make-diagnostic "warning"
(str "Required param :" param-name " of " comp-name " not provided") (str "Required param :" param-name " of " comp-name " not provided")
@@ -482,9 +501,11 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define check-body-walk (define check-body-walk
(fn (node (comp-name :as string) (type-env :as dict) (prim-types :as dict) prim-param-types env (diagnostics :as list)) (fn (node (comp-name :as string) (type-env :as dict) (prim-types :as dict) prim-param-types env (diagnostics :as list) type-registry effect-annotations)
;; Recursively walk an AST and collect diagnostics. ;; Recursively walk an AST and collect diagnostics.
;; prim-param-types: dict of {name → {:positional [...] :rest-type t}} or nil ;; prim-param-types: dict of {name → {:positional [...] :rest-type t}} or nil
;; type-registry: dict of {type-name → type-def} or nil
;; effect-annotations: dict of {fn-name → effect-list} or nil
(let ((kind (type-of node))) (let ((kind (type-of node)))
(when (= kind "list") (when (= kind "list")
(when (not (empty? node)) (when (not (empty? node))
@@ -500,16 +521,30 @@
(for-each (for-each
(fn (d) (append! diagnostics d)) (fn (d) (append! diagnostics d))
(check-component-call name comp-val args (check-component-call name comp-val args
type-env prim-types))))) type-env prim-types type-registry))))
;; Effect check for component calls
(when (not (nil? effect-annotations))
(let ((caller-effects (get-effects comp-name effect-annotations)))
(for-each
(fn (d) (append! diagnostics d))
(check-effect-call name caller-effects effect-annotations comp-name)))))
;; Primitive call — check param types ;; Primitive call — check param types
(when (and (not (starts-with? name "~")) (when (and (not (starts-with? name "~"))
(not (nil? prim-param-types)) (not (nil? prim-param-types))
(dict-has? prim-param-types name)) (has-key? prim-param-types name))
(for-each (for-each
(fn (d) (append! diagnostics d)) (fn (d) (append! diagnostics d))
(check-primitive-call name args type-env prim-types (check-primitive-call name args type-env prim-types
prim-param-types comp-name))) prim-param-types comp-name type-registry)))
;; Effect check for function calls
(when (and (not (starts-with? name "~"))
(not (nil? effect-annotations)))
(let ((caller-effects (get-effects comp-name effect-annotations)))
(for-each
(fn (d) (append! diagnostics d))
(check-effect-call name caller-effects effect-annotations comp-name))))
;; Recurse into let with extended type env ;; Recurse into let with extended type env
(when (or (= name "let") (= name "let*")) (when (or (= name "let") (= name "let*"))
@@ -524,12 +559,12 @@
(let ((bname (if (= (type-of (first binding)) "symbol") (let ((bname (if (= (type-of (first binding)) "symbol")
(symbol-name (first binding)) (symbol-name (first binding))
(str (first binding)))) (str (first binding))))
(val-type (infer-type (nth binding 1) extended prim-types))) (val-type (infer-type (nth binding 1) extended prim-types type-registry)))
(dict-set! extended bname val-type)))) (dict-set! extended bname val-type))))
bindings) bindings)
(for-each (for-each
(fn (body) (fn (body)
(check-body-walk body comp-name extended prim-types prim-param-types env diagnostics)) (check-body-walk body comp-name extended prim-types prim-param-types env diagnostics type-registry effect-annotations))
body-exprs)))) body-exprs))))
;; Recurse into define with type binding ;; Recurse into define with type binding
@@ -541,13 +576,13 @@
(def-val (nth args 1))) (def-val (nth args 1)))
(when def-name (when def-name
(dict-set! type-env def-name (dict-set! type-env def-name
(infer-type def-val type-env prim-types))) (infer-type def-val type-env prim-types type-registry)))
(check-body-walk def-val comp-name type-env prim-types prim-param-types env diagnostics)))))) (check-body-walk def-val comp-name type-env prim-types prim-param-types env diagnostics type-registry effect-annotations))))))
;; Recurse into all child expressions ;; Recurse into all child expressions
(for-each (for-each
(fn (child) (fn (child)
(check-body-walk child comp-name type-env prim-types prim-param-types env diagnostics)) (check-body-walk child comp-name type-env prim-types prim-param-types env diagnostics type-registry effect-annotations))
args))))))) args)))))))
@@ -556,9 +591,11 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define check-component (define check-component
(fn ((comp-name :as string) env (prim-types :as dict) prim-param-types) (fn ((comp-name :as string) env (prim-types :as dict) prim-param-types type-registry effect-annotations)
;; Type-check a component's body. Returns list of diagnostics. ;; Type-check a component's body. Returns list of diagnostics.
;; prim-param-types: dict of param type info, or nil to skip primitive checking. ;; prim-param-types: dict of param type info, or nil to skip primitive checking.
;; type-registry: dict of {type-name → type-def} or nil
;; effect-annotations: dict of {fn-name → effect-list} or nil
(let ((comp (env-get env comp-name)) (let ((comp (env-get env comp-name))
(diagnostics (list))) (diagnostics (list)))
(when (= (type-of comp) "component") (when (= (type-of comp) "component")
@@ -572,15 +609,15 @@
(fn (p) (fn (p)
(dict-set! type-env p (dict-set! type-env p
(if (and (not (nil? param-types)) (if (and (not (nil? param-types))
(dict-has? param-types p)) (has-key? param-types p))
(dict-get param-types p) (get param-types p)
"any"))) "any")))
params) params)
;; Add children as (list-of element) if component has children ;; Add children as (list-of element) if component has children
(when (component-has-children comp) (when (component-has-children comp)
(dict-set! type-env "children" (list "list-of" "element"))) (dict-set! type-env "children" (list "list-of" "element")))
(check-body-walk body comp-name type-env prim-types prim-param-types env diagnostics))) (check-body-walk body comp-name type-env prim-types prim-param-types env diagnostics type-registry effect-annotations)))
diagnostics))) diagnostics)))
@@ -589,9 +626,11 @@
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
(define check-all (define check-all
(fn (env (prim-types :as dict) prim-param-types) (fn (env (prim-types :as dict) prim-param-types type-registry effect-annotations)
;; Type-check every component in the environment. ;; Type-check every component in the environment.
;; prim-param-types: dict of param type info, or nil to skip primitive checking. ;; prim-param-types: dict of param type info, or nil to skip primitive checking.
;; type-registry: dict of {type-name → type-def} or nil
;; effect-annotations: dict of {fn-name → effect-list} or nil
;; Returns list of all diagnostics. ;; Returns list of all diagnostics.
(let ((all-diagnostics (list))) (let ((all-diagnostics (list)))
(for-each (for-each
@@ -600,7 +639,7 @@
(when (= (type-of val) "component") (when (= (type-of val) "component")
(for-each (for-each
(fn (d) (append! all-diagnostics d)) (fn (d) (append! all-diagnostics d))
(check-component name env prim-types prim-param-types))))) (check-component name env prim-types prim-param-types type-registry effect-annotations)))))
(keys env)) (keys env))
all-diagnostics))) all-diagnostics)))
@@ -619,21 +658,208 @@
(let ((registry (dict))) (let ((registry (dict)))
(for-each (for-each
(fn (decl) (fn (decl)
(let ((name (dict-get decl "name")) (let ((name (get decl "name"))
(returns (dict-get decl "returns"))) (returns (get decl "returns")))
(when (and (not (nil? name)) (not (nil? returns))) (when (and (not (nil? name)) (not (nil? returns)))
(dict-set! registry name returns)))) (dict-set! registry name returns))))
prim-declarations) prim-declarations)
(for-each (for-each
(fn (decl) (fn (decl)
(let ((name (dict-get decl "name")) (let ((name (get decl "name"))
(returns (dict-get decl "returns"))) (returns (get decl "returns")))
(when (and (not (nil? name)) (not (nil? returns))) (when (and (not (nil? name)) (not (nil? returns)))
(dict-set! registry name returns)))) (dict-set! registry name returns))))
io-declarations) io-declarations)
registry))) registry)))
;; --------------------------------------------------------------------------
;; 13. User-defined types (deftype)
;; --------------------------------------------------------------------------
;; Type definitions are plain dicts: {:name "price" :params [] :body "number"}
;; Stored in env under "*type-registry*" mapping type names to defs.
;; make-type-def and normalize-type-body are defined in eval.sx
;; (always compiled). They're available when types.sx is compiled as a spec module.
;; -- Standard type definitions --
;; These define the record types used throughout the type system itself.
;; Universal: nullable shorthand
(deftype (maybe a) (union a nil))
;; A type definition entry in the registry
(deftype type-def
{:name string :params list :body any})
;; A diagnostic produced by the type checker
(deftype diagnostic
{:level string :message string :component string? :expr any})
;; Primitive parameter type signature
(deftype prim-param-sig
{:positional list :rest-type string?})
;; Effect declarations
(defeffect io)
(defeffect mutation)
(defeffect render)
(define type-def-name
(fn (td) (get td "name")))
(define type-def-params
(fn (td) (get td "params")))
(define type-def-body
(fn (td) (get td "body")))
(define resolve-type
(fn (t registry)
;; Resolve a type through the registry.
;; Returns the resolved type representation.
(if (nil? registry) t
(cond
;; String — might be a named type alias
(= (type-of t) "string")
(if (has-key? registry t)
(let ((td (get registry t)))
(let ((params (type-def-params td))
(body (type-def-body td)))
(if (empty? params)
;; Simple alias — resolve the body recursively
(resolve-type body registry)
;; Parameterized with no args — return as-is
t)))
t)
;; List — might be parameterized type application or compound
(= (type-of t) "list")
(if (empty? t) t
(let ((head (first t)))
(cond
;; (or ...), (list-of ...), (-> ...) — recurse into members
(or (= head "or") (= head "list-of") (= head "->")
(= head "dict-of"))
(cons head (map (fn (m) (resolve-type m registry)) (rest t)))
;; Parameterized type application: ("maybe" "string") etc.
(and (= (type-of head) "string")
(has-key? registry head))
(let ((td (get registry head))
(params (type-def-params td))
(body (type-def-body td))
(args (rest t)))
(if (= (len params) (len args))
(resolve-type
(substitute-type-vars body params args)
registry)
;; Wrong arity — return as-is
t))
:else t)))
;; Dict — record type, resolve field types
(= (type-of t) "dict")
(map-dict (fn (k v) (resolve-type v registry)) t)
;; Anything else — return as-is
:else t))))
(define substitute-type-vars
(fn (body (params :as list) (args :as list))
;; Substitute type variables in body.
;; params is a list of type var names, args is corresponding types.
(let ((subst (dict)))
(for-each
(fn (i)
(dict-set! subst (nth params i) (nth args i)))
(range 0 (len params) 1))
(substitute-in-type body subst))))
(define substitute-in-type
(fn (t (subst :as dict))
;; Recursively substitute type variables.
(cond
(= (type-of t) "string")
(if (has-key? subst t) (get subst t) t)
(= (type-of t) "list")
(map (fn (m) (substitute-in-type m subst)) t)
(= (type-of t) "dict")
(map-dict (fn (k v) (substitute-in-type v subst)) t)
:else t)))
(define subtype-resolved?
(fn (a b registry)
;; Resolve both sides through the registry, then check subtype.
(if (nil? registry)
(subtype? a b)
(let ((ra (resolve-type a registry))
(rb (resolve-type b registry)))
;; Handle record structural subtyping: dict a <: dict b
;; if every field in b exists in a with compatible type
(if (and (= (type-of ra) "dict") (= (type-of rb) "dict"))
(every?
(fn (key)
(and (has-key? ra key)
(subtype-resolved? (get ra key) (get rb key) registry)))
(keys rb))
(subtype? ra rb))))))
;; --------------------------------------------------------------------------
;; 14. Effect checking (defeffect)
;; --------------------------------------------------------------------------
;; Effects are annotations on functions/components describing their
;; side effects. A pure function cannot call IO functions.
(define get-effects
(fn ((name :as string) effect-annotations)
;; Look up declared effects for a function/component.
;; Returns list of effect strings, or nil if unannotated.
(if (nil? effect-annotations) nil
(if (has-key? effect-annotations name)
(get effect-annotations name)
nil))))
(define effects-subset?
(fn (callee-effects caller-effects)
;; Are all callee effects allowed by caller?
;; nil effects = unannotated = assumed to have all effects.
;; Empty list = pure = no effects.
(if (nil? caller-effects) true ;; unannotated caller allows everything
(if (nil? callee-effects) true ;; unannotated callee — skip check
(every?
(fn (e) (contains? caller-effects e))
callee-effects)))))
(define check-effect-call
(fn ((callee-name :as string) caller-effects effect-annotations (comp-name :as string))
;; Check that callee's effects are allowed by caller's effects.
;; Returns list of diagnostics.
(let ((diagnostics (list))
(callee-effects (get-effects callee-name effect-annotations)))
(when (and (not (nil? caller-effects))
(not (nil? callee-effects))
(not (effects-subset? callee-effects caller-effects)))
(append! diagnostics
(make-diagnostic "error"
(str "`" callee-name "` has effects "
(join ", " callee-effects)
" but `" comp-name "` only allows "
(if (empty? caller-effects) "[pure]"
(join ", " caller-effects)))
comp-name nil)))
diagnostics)))
(define build-effect-annotations
(fn ((io-declarations :as list))
;; Assign [io] effect to all IO primitives.
(let ((annotations (dict)))
(for-each
(fn (decl)
(let ((name (get decl "name")))
(when (not (nil? name))
(dict-set! annotations name (list "io")))))
io-declarations)
annotations)))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Platform interface summary ;; Platform interface summary
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------