Add boundary.sx declaring all 34 I/O primitives, 32 page helpers, and 9 allowed boundary types. Runtime validation in boundary.py checks every registration against the spec — undeclared primitives/helpers crash at startup with SX_BOUNDARY_STRICT=1 (now set in both dev and prod). Key changes: - Move 5 I/O-in-disguise primitives (app-url, asset-url, config, jinja-global, relations-from) from primitives.py to primitives_io.py - Remove duplicate url-for/route-prefix from primitives.py (already in IO) - Fix parse-datetime to return ISO string instead of raw datetime - Add datetime→isoformat conversion in _convert_result at the edge - Wrap page helper return values with boundary type validation - Replace all SxExpr(f"...") patterns with sx_call() or _sx_fragment() - Add assert declaration to primitives.sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.0 KiB
SX Boundary Enforcement
Principle
SX is an uninterrupted island of pure evaluation. Host code (Python, JavaScript, Rust, etc.) interacts with it only through declared entry points. The specification enforces this — violations are errors, not style suggestions.
The Three Tiers
Tier 1: Pure Primitives
Declared in primitives.sx. Stateless, synchronous, no side effects. Available in every SX environment on every target.
Examples: +, str, map, get, concat, merge
Tier 2: I/O Primitives
Declared in boundary.sx. Async, side-effectful, require host context (request, config, services). Server-side only.
Examples: frag, query, current-user, csrf-token, request-arg
Tier 3: Page Helpers
Declared in boundary.sx with a :service scope. Registered per-app, return data that .sx components render. Server-side only.
Examples: highlight (sx), editor-data (blog), all-markets-data (market)
Boundary Types
Only these types may cross the host-SX boundary:
| Type | Python | JavaScript | Rust (future) |
|---|---|---|---|
| number | int, float |
number |
f64 |
| string | str |
string |
String |
| boolean | bool |
boolean |
bool |
| nil | NIL sentinel |
NIL sentinel |
SxValue::Nil |
| keyword | str (colon-prefixed) |
string |
String |
| list | list |
Array |
Vec<SxValue> |
| dict | dict |
Object / Map |
HashMap<String, SxValue> |
| sx-source | SxExpr wrapper |
string |
String |
| style-value | StyleValue |
StyleValue |
StyleValue |
NOT allowed: ORM models, datetime objects, request objects, raw callables, framework types. Convert at the edge before crossing.
Enforcement Mechanism
The bootstrappers (bootstrap_js.py, bootstrap_py.py, future bootstrap_rs.py, etc.) read boundary.sx and emit target-native validation:
-
Typed targets (Rust, Haskell, TypeScript): Boundary types become an enum/ADT/discriminated union. Registration functions have type signatures that reject non-SX values at compile time. You literally cannot register a primitive that returns a
datetime— it won't typecheck. -
Python + mypy: Boundary types become a
Protocol/Uniontype.validate_boundary_value()checks at runtime; mypy catches most violations statically. -
JavaScript: Runtime validation only.
registerPrimitive()checks the name against the declared set. Boundary type checking is runtime.
The Contract
-
Spec-first. Every primitive, I/O function, and page helper must be declared in
primitives.sxorboundary.sxbefore it can be registered. Undeclared registration = error. -
SX types only. Values crossing the boundary must be SX-typed. Host-native types (datetime, ORM models, request objects) must be converted to dicts/strings at the edge.
-
Data in, markup out. Python returns data (dicts, lists, strings).
.sxfiles compose markup. No SX source construction in Python — no f-strings, no string concatenation, noSxExpr(f"..."). -
Closed island. SX code can only call symbols in its env + declared primitives. There is no FFI, no
eval-python, no escape hatch from inside SX. -
Fail fast. Violations are runtime errors (startup crash), not warnings. For typed targets, they're compile errors.
Adding a New Primitive
- Add declaration to
primitives.sx(pure) orboundary.sx(I/O / page helper) - Implement in the target language's primitive file
- The bootstrapper-emitted validator will accept it on next rebuild/restart
- If you skip step 1, the app crashes on startup telling you exactly what's missing
File Map
shared/sx/ref/
primitives.sx — Pure primitive declarations
boundary.sx — I/O primitive + page helper + boundary type declarations
bootstrap_js.py — JS bootstrapper (reads both, emits validation)
bootstrap_py.py — Python bootstrapper (reads both, emits validation)
eval.sx — Evaluator spec (symbol resolution, env model)
parser.sx — Parser spec
render.sx — Renderer spec (shared registries)