Refactor SX primitives: modular, isomorphic, general-purpose

Spec modularization:
- Add (define-module :name) markers to primitives.sx creating 11 modules
  (7 core, 4 stdlib). Bootstrappers can now selectively include modules.
- Add parse_primitives_by_module() to boundary_parser.py.
- Remove split-ids primitive; inline at 4 call sites in blog/market queries.

Python file split:
- primitives.py: slimmed to registry + core primitives only (~350 lines)
- primitives_stdlib.py: NEW — stdlib primitives (format, text, style, debug)
- primitives_ctx.py: NEW — extracted 12 page context builders from IO
- primitives_io.py: add register_io_handler decorator, auto-derive
  IO_PRIMITIVES from registry, move sync IO bridges here

JS parity fixes:
- = uses === (strict equality), != uses !==
- round supports optional ndigits parameter
- concat uses nil-check not falsy-check (preserves 0, "", false)
- escape adds single quote entity (') matching Python/markupsafe
- assert added (was missing from JS entirely)

Bootstrapper modularization:
- PRIMITIVES_JS_MODULES / PRIMITIVES_PY_MODULES dicts keyed by module
- --modules CLI flag for selective inclusion (core.* always included)
- Regenerated sx-ref.js and sx_ref.py with all fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 01:45:29 +00:00
parent cfde5bc491
commit f77d7350dd
13 changed files with 3545 additions and 1303 deletions

View File

@@ -996,13 +996,19 @@ ADAPTER_DEPS = {
}
def compile_ref_to_js(adapters: list[str] | None = None) -> str:
def compile_ref_to_js(
adapters: list[str] | None = None,
modules: list[str] | None = None,
) -> str:
"""Read reference .sx files and emit JavaScript.
Args:
adapters: List of adapter names to include.
Valid names: html, sx, dom, engine.
None = include all adapters.
modules: List of primitive module names to include.
core.* are always included. stdlib.* are opt-in.
None = include all modules (backward compatible).
"""
ref_dir = os.path.dirname(os.path.abspath(__file__))
emitter = JSEmitter()
@@ -1060,9 +1066,25 @@ def compile_ref_to_js(adapters: list[str] | None = None) -> str:
has_parser = "parser" in adapter_set
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
# Determine which primitive modules to include
prim_modules = None # None = all
if modules is not None:
prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")]
for m in modules:
if m not in prim_modules:
if m not in PRIMITIVES_JS_MODULES:
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}")
prim_modules.append(m)
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_JS)
parts.append(PLATFORM_JS_PRE)
parts.append('\n // =========================================================================')
parts.append(' // Primitives')
parts.append(' // =========================================================================\n')
parts.append(' var PRIMITIVES = {};')
parts.append(_assemble_primitives_js(prim_modules))
parts.append(PLATFORM_JS_POST)
# Parser platform must come before compiled parser.sx
if has_parser:
@@ -1181,7 +1203,235 @@ PREAMBLE = '''\
return arguments.length ? arguments[arguments.length - 1] : false;
}'''
PLATFORM_JS = '''
# ---------------------------------------------------------------------------
# Primitive modules — JS implementations keyed by spec module name.
# core.* modules are always included; stdlib.* are opt-in.
# ---------------------------------------------------------------------------
PRIMITIVES_JS_MODULES: dict[str, str] = {
"core.arithmetic": '''
// core.arithmetic
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
PRIMITIVES["/"] = function(a, b) { return a / b; };
PRIMITIVES["mod"] = function(a, b) { return a % b; };
PRIMITIVES["inc"] = function(n) { return n + 1; };
PRIMITIVES["dec"] = function(n) { return n - 1; };
PRIMITIVES["abs"] = Math.abs;
PRIMITIVES["floor"] = Math.floor;
PRIMITIVES["ceil"] = Math.ceil;
PRIMITIVES["round"] = function(x, n) {
if (n === undefined || n === 0) return Math.round(x);
var f = Math.pow(10, n); return Math.round(x * f) / f;
};
PRIMITIVES["min"] = Math.min;
PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
''',
"core.comparison": '''
// core.comparison
PRIMITIVES["="] = function(a, b) { return a === b; };
PRIMITIVES["!="] = function(a, b) { return a !== b; };
PRIMITIVES["<"] = function(a, b) { return a < b; };
PRIMITIVES[">"] = function(a, b) { return a > b; };
PRIMITIVES["<="] = function(a, b) { return a <= b; };
PRIMITIVES[">="] = function(a, b) { return a >= b; };
''',
"core.logic": '''
// core.logic
PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
''',
"core.predicates": '''
// core.predicates
PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function(c, k) {
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
if (Array.isArray(c)) return c.indexOf(k) !== -1;
return k in c;
};
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
PRIMITIVES["zero?"] = function(n) { return n === 0; };
''',
"core.strings": '''
// core.strings
PRIMITIVES["str"] = function() {
var p = [];
for (var i = 0; i < arguments.length; i++) {
var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
}
return p.join("");
};
PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
PRIMITIVES["concat"] = function() {
var out = [];
for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]);
return out;
};
''',
"core.collections": '''
// core.collections
PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
PRIMITIVES["dict"] = function() {
var d = {};
for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
return d;
};
PRIMITIVES["range"] = function(a, b, step) {
var r = []; step = step || 1;
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
return r;
};
PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
PRIMITIVES["chunk-every"] = function(c, n) {
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
};
PRIMITIVES["zip-pairs"] = function(c) {
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
};
''',
"core.dict": '''
// core.dict
PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
PRIMITIVES["merge"] = function() {
var out = {};
for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
return out;
};
PRIMITIVES["assoc"] = function(d) {
var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
return out;
};
PRIMITIVES["dissoc"] = function(d) {
var out = {}; for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
return out;
};
PRIMITIVES["into"] = function(target, coll) {
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
return r;
};
''',
"stdlib.format": '''
// stdlib.format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
PRIMITIVES["format-date"] = function(s, fmt) {
if (!s) return "";
try {
var d = new Date(s);
if (isNaN(d.getTime())) return String(s);
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
} catch (e) { return String(s); }
};
PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
''',
"stdlib.text": '''
// stdlib.text
PRIMITIVES["pluralize"] = function(n, s, p) {
if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
return n == 1 ? "" : "s";
};
PRIMITIVES["escape"] = function(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;");
};
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
''',
"stdlib.style": '''
// stdlib.style
PRIMITIVES["css"] = function() {
var atoms = [];
for (var i = 0; i < arguments.length; i++) {
var a = arguments[i];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
return new StyleValue("sx-merged", allDecls, [], [], []);
};
''',
"stdlib.debug": '''
// stdlib.debug
PRIMITIVES["assert"] = function(cond, msg) {
if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed"));
return true;
};
''',
}
# Modules to include by default (all)
_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys())
# Selected primitive modules for current compilation (None = all)
def _assemble_primitives_js(modules: list[str] | None = None) -> str:
"""Assemble JS primitive code from selected modules.
If modules is None, all modules are included.
Core modules are always included regardless of the list.
"""
if modules is None:
modules = _ALL_JS_MODULES
parts = []
for mod in modules:
if mod in PRIMITIVES_JS_MODULES:
parts.append(PRIMITIVES_JS_MODULES[mod])
return "\n".join(parts)
PLATFORM_JS_PRE = '''
// =========================================================================
// Platform interface — JS implementation
// =========================================================================
@@ -1305,180 +1555,9 @@ PLATFORM_JS = '''
function error(msg) { throw new Error(msg); }
function inspect(x) { return JSON.stringify(x); }
// =========================================================================
// Primitives
// =========================================================================
var PRIMITIVES = {};
// Arithmetic
PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; };
PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; };
PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; };
PRIMITIVES["/"] = function(a, b) { return a / b; };
PRIMITIVES["mod"] = function(a, b) { return a % b; };
PRIMITIVES["inc"] = function(n) { return n + 1; };
PRIMITIVES["dec"] = function(n) { return n - 1; };
PRIMITIVES["abs"] = Math.abs;
PRIMITIVES["floor"] = Math.floor;
PRIMITIVES["ceil"] = Math.ceil;
PRIMITIVES["round"] = Math.round;
PRIMITIVES["min"] = Math.min;
PRIMITIVES["max"] = Math.max;
PRIMITIVES["sqrt"] = Math.sqrt;
PRIMITIVES["pow"] = Math.pow;
PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); };
// Comparison
PRIMITIVES["="] = function(a, b) { return a == b; };
PRIMITIVES["!="] = function(a, b) { return a != b; };
PRIMITIVES["<"] = function(a, b) { return a < b; };
PRIMITIVES[">"] = function(a, b) { return a > b; };
PRIMITIVES["<="] = function(a, b) { return a <= b; };
PRIMITIVES[">="] = function(a, b) { return a >= b; };
// Logic
PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); };
// String
PRIMITIVES["str"] = function() {
var p = [];
for (var i = 0; i < arguments.length; i++) {
var v = arguments[i]; if (isNil(v)) continue; p.push(String(v));
}
return p.join("");
};
PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); };
PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); };
PRIMITIVES["trim"] = function(s) { return String(s).trim(); };
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); };
PRIMITIVES["concat"] = function() {
var out = [];
for (var i = 0; i < arguments.length; i++) if (arguments[i]) out = out.concat(arguments[i]);
return out;
};
PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); };
// Predicates
PRIMITIVES["nil?"] = isNil;
PRIMITIVES["number?"] = function(x) { return typeof x === "number"; };
PRIMITIVES["string?"] = function(x) { return typeof x === "string"; };
PRIMITIVES["list?"] = Array.isArray;
PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; };
PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); };
PRIMITIVES["contains?"] = function(c, k) {
if (typeof c === "string") return c.indexOf(String(k)) !== -1;
if (Array.isArray(c)) return c.indexOf(k) !== -1;
return k in c;
};
PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; };
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
PRIMITIVES["zero?"] = function(n) { return n === 0; };
// Collections
PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); };
PRIMITIVES["dict"] = function() {
var d = {};
for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1];
return d;
};
PRIMITIVES["range"] = function(a, b, step) {
var r = []; step = step || 1;
for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i);
return r;
};
PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); };
PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; };
PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; };
PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; };
PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; };
PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; };
PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); };
PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); };
PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); };
PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; };
PRIMITIVES["merge"] = function() {
var out = {};
for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; }
return out;
};
PRIMITIVES["assoc"] = function(d) {
var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1];
return out;
};
PRIMITIVES["dissoc"] = function(d) {
var out = {}; for (var k in d) out[k] = d[k];
for (var i = 1; i < arguments.length; i++) delete out[arguments[i]];
return out;
};
PRIMITIVES["chunk-every"] = function(c, n) {
var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r;
};
PRIMITIVES["zip-pairs"] = function(c) {
var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r;
};
PRIMITIVES["into"] = function(target, coll) {
if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll);
var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; }
return r;
};
// Format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; };
PRIMITIVES["pluralize"] = function(n, s, p) {
if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s");
return n == 1 ? "" : "s";
};
PRIMITIVES["escape"] = function(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
};
PRIMITIVES["format-date"] = function(s, fmt) {
if (!s) return "";
try {
var d = new Date(s);
if (isNaN(d.getTime())) return String(s);
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
} catch (e) { return String(s); }
};
PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; };
PRIMITIVES["split-ids"] = function(s) {
if (!s) return [];
return String(s).split(",").map(function(x) { return x.trim(); }).filter(function(x) { return x; });
};
PRIMITIVES["css"] = function() {
// Stub — CSSX requires style dictionary which is browser-only
var atoms = [];
for (var i = 0; i < arguments.length; i++) {
var a = arguments[i];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
return new StyleValue("sx-merged", allDecls, [], [], []);
};
'''
PLATFORM_JS_POST = '''
function isPrimitive(name) { return name in PRIMITIVES; }
function getPrimitive(name) { return PRIMITIVES[name]; }
@@ -2825,18 +2904,22 @@ if __name__ == "__main__":
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript")
p.add_argument("--adapters", "-a",
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
p.add_argument("--modules", "-m",
help="Comma-separated primitive modules (core.* always included). Default: all")
p.add_argument("--output", "-o",
help="Output file (default: stdout)")
args = p.parse_args()
adapters = args.adapters.split(",") if args.adapters else None
js = compile_ref_to_js(adapters)
modules = args.modules.split(",") if args.modules else None
js = compile_ref_to_js(adapters, modules)
if args.output:
with open(args.output, "w") as f:
f.write(js)
included = ", ".join(adapters) if adapters else "all"
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included})",
mods = ", ".join(modules) if modules else "all"
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods})",
file=sys.stderr)
else:
print(js)

View File

@@ -801,14 +801,30 @@ ADAPTER_FILES = {
}
def compile_ref_to_py(adapters: list[str] | None = None) -> str:
def compile_ref_to_py(
adapters: list[str] | None = None,
modules: list[str] | None = None,
) -> str:
"""Read reference .sx files and emit Python.
Args:
adapters: List of adapter names to include.
Valid names: html, sx.
None = include all server-side adapters.
modules: List of primitive module names to include.
core.* are always included. stdlib.* are opt-in.
None = include all modules (backward compatible).
"""
# Determine which primitive modules to include
prim_modules = None # None = all
if modules is not None:
prim_modules = [m for m in _ALL_PY_MODULES if m.startswith("core.")]
for m in modules:
if m not in prim_modules:
if m not in PRIMITIVES_PY_MODULES:
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_PY_MODULES)}")
prim_modules.append(m)
ref_dir = os.path.dirname(os.path.abspath(__file__))
emitter = PyEmitter()
@@ -849,7 +865,9 @@ def compile_ref_to_py(adapters: list[str] | None = None) -> str:
parts = []
parts.append(PREAMBLE)
parts.append(PLATFORM_PY)
parts.append(PRIMITIVES_PY)
parts.append(PRIMITIVES_PY_PRE)
parts.append(_assemble_primitives_py(prim_modules))
parts.append(PRIMITIVES_PY_POST)
for label, defines in all_sections:
parts.append(f"\n# === Transpiled from {label} ===\n")
@@ -1506,29 +1524,14 @@ def aser_special(name, expr, env):
return trampoline(result)
'''
PRIMITIVES_PY = '''
# =========================================================================
# Primitives
# =========================================================================
# ---------------------------------------------------------------------------
# Primitive modules — Python implementations keyed by spec module name.
# core.* modules are always included; stdlib.* are opt-in.
# ---------------------------------------------------------------------------
# Save builtins before shadowing
_b_len = len
_b_map = map
_b_filter = filter
_b_range = range
_b_list = list
_b_dict = dict
_b_max = max
_b_min = min
_b_round = round
_b_abs = abs
_b_sum = sum
_b_zip = zip
_b_int = int
PRIMITIVES = {}
# Arithmetic
PRIMITIVES_PY_MODULES: dict[str, str] = {
"core.arithmetic": '''
# core.arithmetic
PRIMITIVES["+"] = lambda *args: _b_sum(args)
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
@@ -1551,37 +1554,25 @@ def _sx_mul(*args):
for a in args:
r *= a
return r
''',
# Comparison
"core.comparison": '''
# core.comparison
PRIMITIVES["="] = lambda a, b: a == b
PRIMITIVES["!="] = lambda a, b: a != b
PRIMITIVES["<"] = lambda a, b: a < b
PRIMITIVES[">"] = lambda a, b: a > b
PRIMITIVES["<="] = lambda a, b: a <= b
PRIMITIVES[">="] = lambda a, b: a >= b
''',
# Logic
"core.logic": '''
# core.logic
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
''',
# String
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
# Predicates
"core.predicates": '''
# core.predicates
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
@@ -1598,8 +1589,25 @@ PRIMITIVES["contains?"] = lambda c, k: (
PRIMITIVES["odd?"] = lambda n: n % 2 != 0
PRIMITIVES["even?"] = lambda n: n % 2 == 0
PRIMITIVES["zero?"] = lambda n: n == 0
''',
# Collections
"core.strings": '''
# core.strings
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
''',
"core.collections": '''
# core.collections
PRIMITIVES["list"] = lambda *args: _b_list(args)
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
@@ -1611,22 +1619,20 @@ PRIMITIVES["rest"] = lambda c: c[1:] if c else []
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
''',
"core.dict": '''
# core.dict
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
# Format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
def _sx_merge_dicts(*args):
out = {}
for d in args:
@@ -1639,13 +1645,80 @@ def _sx_assoc(d, *kvs):
for i in _b_range(0, _b_len(kvs) - 1, 2):
out[kvs[i]] = kvs[i + 1]
return out
''',
"stdlib.format": '''
# stdlib.format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
def _sx_parse_int(v, default=0):
try:
return _b_int(v)
except (ValueError, TypeError):
return default
''',
"stdlib.text": '''
# stdlib.text
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
''',
"stdlib.style": '''
# stdlib.style — stubs (CSSX needs full runtime)
''',
"stdlib.debug": '''
# stdlib.debug
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
''',
}
_ALL_PY_MODULES = list(PRIMITIVES_PY_MODULES.keys())
def _assemble_primitives_py(modules: list[str] | None = None) -> str:
"""Assemble Python primitive code from selected modules."""
if modules is None:
modules = _ALL_PY_MODULES
parts = []
for mod in modules:
if mod in PRIMITIVES_PY_MODULES:
parts.append(PRIMITIVES_PY_MODULES[mod])
return "\n".join(parts)
PRIMITIVES_PY_PRE = '''
# =========================================================================
# Primitives
# =========================================================================
# Save builtins before shadowing
_b_len = len
_b_map = map
_b_filter = filter
_b_range = range
_b_list = list
_b_dict = dict
_b_max = max
_b_min = min
_b_round = round
_b_abs = abs
_b_sum = sum
_b_zip = zip
_b_int = int
PRIMITIVES = {}
'''
PRIMITIVES_PY_POST = '''
def is_primitive(name):
if name in PRIMITIVES:
return True
@@ -1811,9 +1884,15 @@ def main():
default=None,
help="Comma-separated adapter names (html,sx). Default: all server-side.",
)
parser.add_argument(
"--modules",
default=None,
help="Comma-separated primitive modules (core.* always included). Default: all.",
)
args = parser.parse_args()
adapters = args.adapters.split(",") if args.adapters else None
print(compile_ref_to_py(adapters))
modules = args.modules.split(",") if args.modules else None
print(compile_ref_to_py(adapters, modules))
if __name__ == "__main__":

View File

@@ -42,17 +42,44 @@ def _extract_keyword_arg(expr: list, key: str) -> Any:
def parse_primitives_sx() -> frozenset[str]:
"""Parse primitives.sx and return frozenset of declared pure primitive names."""
by_module = parse_primitives_by_module()
all_names: set[str] = set()
for names in by_module.values():
all_names.update(names)
return frozenset(all_names)
def parse_primitives_by_module() -> dict[str, frozenset[str]]:
"""Parse primitives.sx and return primitives grouped by module.
Returns:
Dict mapping module name (e.g. "core.arithmetic") to frozenset of
primitive names declared under that module.
"""
source = _read_file("primitives.sx")
exprs = parse_all(source)
names: set[str] = set()
modules: dict[str, set[str]] = {}
current_module = "_unscoped"
for expr in exprs:
if (isinstance(expr, list) and len(expr) >= 2
and isinstance(expr[0], Symbol)
and expr[0].name == "define-primitive"):
if not isinstance(expr, list) or len(expr) < 2:
continue
if not isinstance(expr[0], Symbol):
continue
if expr[0].name == "define-module":
mod_name = expr[1]
if isinstance(mod_name, Keyword):
current_module = mod_name.name
elif isinstance(mod_name, str):
current_module = mod_name
elif expr[0].name == "define-primitive":
name = expr[1]
if isinstance(name, str):
names.add(name)
return frozenset(names)
modules.setdefault(current_module, set()).add(name)
return {mod: frozenset(names) for mod, names in modules.items()}
def parse_boundary_sx() -> tuple[frozenset[str], dict[str, frozenset[str]]]:

View File

@@ -18,13 +18,19 @@
;; The :body is optional — when provided, it gives a reference
;; implementation in SX that bootstrap compilers MAY use for testing
;; or as a fallback. Most targets will implement natively for performance.
;;
;; Modules: (define-module :name) scopes subsequent define-primitive
;; entries until the next define-module. Bootstrappers use this to
;; selectively include primitive groups.
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Arithmetic
;; Core — Arithmetic
;; --------------------------------------------------------------------------
(define-module :core.arithmetic)
(define-primitive "+"
:params (&rest args)
:returns "number"
@@ -115,9 +121,11 @@
;; --------------------------------------------------------------------------
;; Comparison
;; Core — Comparison
;; --------------------------------------------------------------------------
(define-module :core.comparison)
(define-primitive "="
:params (a b)
:returns "boolean"
@@ -151,9 +159,11 @@
;; --------------------------------------------------------------------------
;; Predicates
;; Core — Predicates
;; --------------------------------------------------------------------------
(define-module :core.predicates)
(define-primitive "odd?"
:params (n)
:returns "boolean"
@@ -209,9 +219,11 @@
;; --------------------------------------------------------------------------
;; Logic
;; Core — Logic
;; --------------------------------------------------------------------------
(define-module :core.logic)
(define-primitive "not"
:params (x)
:returns "boolean"
@@ -219,9 +231,11 @@
;; --------------------------------------------------------------------------
;; Strings
;; Core — Strings
;; --------------------------------------------------------------------------
(define-module :core.strings)
(define-primitive "str"
:params (&rest args)
:returns "string"
@@ -279,9 +293,11 @@
;; --------------------------------------------------------------------------
;; Collections — construction
;; Core — Collections
;; --------------------------------------------------------------------------
(define-module :core.collections)
(define-primitive "list"
:params (&rest args)
:returns "list"
@@ -297,11 +313,6 @@
:returns "list"
:doc "Integer range [start, end) with optional step.")
;; --------------------------------------------------------------------------
;; Collections — access
;; --------------------------------------------------------------------------
(define-primitive "get"
:params (coll key &rest default)
:returns "any"
@@ -354,9 +365,11 @@
;; --------------------------------------------------------------------------
;; Collectionsdict operations
;; CoreDict operations
;; --------------------------------------------------------------------------
(define-module :core.dict)
(define-primitive "keys"
:params (d)
:returns "list"
@@ -389,9 +402,11 @@
;; --------------------------------------------------------------------------
;; Format helpers
;; Stdlib — Format
;; --------------------------------------------------------------------------
(define-module :stdlib.format)
(define-primitive "format-date"
:params (date-str fmt)
:returns "string"
@@ -407,11 +422,18 @@
:returns "number"
:doc "Parse string to integer with optional default on failure.")
(define-primitive "parse-datetime"
:params (s)
:returns "string"
:doc "Parse datetime string — identity passthrough (returns string or nil).")
;; --------------------------------------------------------------------------
;; Text helpers
;; Stdlib — Text
;; --------------------------------------------------------------------------
(define-module :stdlib.text)
(define-primitive "pluralize"
:params (count &rest forms)
:returns "string"
@@ -429,33 +451,10 @@
;; --------------------------------------------------------------------------
;; Date & parsing helpers
;; Stdlib — Style
;; --------------------------------------------------------------------------
(define-primitive "parse-datetime"
:params (s)
:returns "string"
:doc "Parse datetime string — identity passthrough (returns string or nil).")
(define-primitive "split-ids"
:params (s)
:returns "list"
:doc "Split comma-separated ID string into list of trimmed non-empty strings.")
;; --------------------------------------------------------------------------
;; Assertions
;; --------------------------------------------------------------------------
(define-primitive "assert"
:params (condition &rest message)
:returns "boolean"
:doc "Assert condition is truthy; raise error with message if not.")
;; --------------------------------------------------------------------------
;; CSSX — style system primitives
;; --------------------------------------------------------------------------
(define-module :stdlib.style)
(define-primitive "css"
:params (&rest atoms)
@@ -467,3 +466,15 @@
:params (&rest styles)
:returns "style-value"
:doc "Merge multiple StyleValues into one combined StyleValue.")
;; --------------------------------------------------------------------------
;; Stdlib — Debug
;; --------------------------------------------------------------------------
(define-module :stdlib.debug)
(define-primitive "assert"
:params (condition &rest message)
:returns "boolean"
:doc "Assert condition is truthy; raise error with message if not.")

View File

@@ -656,7 +656,8 @@ _b_int = int
PRIMITIVES = {}
# Arithmetic
# core.arithmetic
PRIMITIVES["+"] = lambda *args: _b_sum(args)
PRIMITIVES["-"] = lambda a, b=None: -a if b is None else a - b
PRIMITIVES["*"] = lambda *args: _sx_mul(*args)
@@ -680,7 +681,8 @@ def _sx_mul(*args):
r *= a
return r
# Comparison
# core.comparison
PRIMITIVES["="] = lambda a, b: a == b
PRIMITIVES["!="] = lambda a, b: a != b
PRIMITIVES["<"] = lambda a, b: a < b
@@ -688,28 +690,12 @@ PRIMITIVES[">"] = lambda a, b: a > b
PRIMITIVES["<="] = lambda a, b: a <= b
PRIMITIVES[">="] = lambda a, b: a >= b
# Logic
# core.logic
PRIMITIVES["not"] = lambda x: not sx_truthy(x)
# String
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
# Predicates
# core.predicates
PRIMITIVES["nil?"] = lambda x: x is None or x is NIL
PRIMITIVES["number?"] = lambda x: isinstance(x, (int, float)) and not isinstance(x, bool)
PRIMITIVES["string?"] = lambda x: isinstance(x, str)
@@ -727,7 +713,22 @@ PRIMITIVES["odd?"] = lambda n: n % 2 != 0
PRIMITIVES["even?"] = lambda n: n % 2 == 0
PRIMITIVES["zero?"] = lambda n: n == 0
# Collections
# core.strings
PRIMITIVES["str"] = sx_str
PRIMITIVES["upper"] = lambda s: str(s).upper()
PRIMITIVES["lower"] = lambda s: str(s).lower()
PRIMITIVES["trim"] = lambda s: str(s).strip()
PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep)
PRIMITIVES["join"] = lambda sep, coll: sep.join(coll)
PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new)
PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p)
PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p)
PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:]
PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), [])
# core.collections
PRIMITIVES["list"] = lambda *args: _b_list(args)
PRIMITIVES["dict"] = lambda *args: {args[i]: args[i+1] for i in _b_range(0, _b_len(args)-1, 2)}
PRIMITIVES["range"] = lambda a, b, step=1: _b_list(_b_range(_b_int(a), _b_int(b), _b_int(step)))
@@ -739,22 +740,19 @@ PRIMITIVES["rest"] = lambda c: c[1:] if c else []
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
# core.dict
PRIMITIVES["keys"] = lambda d: _b_list((d or {}).keys())
PRIMITIVES["vals"] = lambda d: _b_list((d or {}).values())
PRIMITIVES["merge"] = lambda *args: _sx_merge_dicts(*args)
PRIMITIVES["assoc"] = lambda d, *kvs: _sx_assoc(d, *kvs)
PRIMITIVES["dissoc"] = lambda d, *ks: {k: v for k, v in d.items() if k not in ks}
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
PRIMITIVES["into"] = lambda target, coll: (_b_list(coll) if isinstance(target, _b_list) else {p[0]: p[1] for p in coll if isinstance(p, _b_list) and _b_len(p) >= 2})
PRIMITIVES["zip"] = lambda *colls: [_b_list(t) for t in _b_zip(*colls)]
# Format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
def _sx_merge_dicts(*args):
out = {}
for d in args:
@@ -768,12 +766,36 @@ def _sx_assoc(d, *kvs):
out[kvs[i]] = kvs[i + 1]
return out
# stdlib.format
PRIMITIVES["format-decimal"] = lambda v, p=2: f"{float(v):.{p}f}"
PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
def _sx_parse_int(v, default=0):
try:
return _b_int(v)
except (ValueError, TypeError):
return default
# stdlib.text
PRIMITIVES["pluralize"] = lambda n, s="", p="s": s if n == 1 else p
PRIMITIVES["escape"] = escape_html
PRIMITIVES["strip-tags"] = lambda s: _strip_tags(str(s))
import re as _re
def _strip_tags(s):
return _re.sub(r"<[^>]+>", "", s)
# stdlib.style — stubs (CSSX needs full runtime)
# stdlib.debug
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
def is_primitive(name):
if name in PRIMITIVES:
return True
@@ -1161,4 +1183,4 @@ def render(expr, env=None):
def make_env(**kwargs):
"""Create an environment dict with initial bindings."""
return dict(kwargs)
return dict(kwargs)