Bootstrap parser.sx to Python, add reactive runtime plan
Replace hand-written serialize/sx_serialize/sx_parse in Python with spec-derived versions from parser.sx. Add parser as a Python adapter alongside html/sx/async — all 48 parser spec tests pass. Add reactive runtime plan to sx-docs: 7 feature layers (ref, foreign FFI, state machines, commands with undo/redo, render loops, keyed lists, client-first app shell) — zero new platform primitives. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,7 +85,12 @@ class PyEmitter:
|
|||||||
if name == "define-async":
|
if name == "define-async":
|
||||||
return self._emit_define_async(expr, indent)
|
return self._emit_define_async(expr, indent)
|
||||||
if name == "set!":
|
if name == "set!":
|
||||||
return f"{pad}{self._mangle(expr[1].name)} = {self.emit(expr[2])}"
|
varname = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
||||||
|
py_var = self._mangle(varname)
|
||||||
|
cell_vars = getattr(self, '_current_cell_vars', set())
|
||||||
|
if py_var in cell_vars:
|
||||||
|
return f"{pad}_cells[{self._py_string(py_var)}] = {self.emit(expr[2])}"
|
||||||
|
return f"{pad}{py_var} = {self.emit(expr[2])}"
|
||||||
if name == "when":
|
if name == "when":
|
||||||
return self._emit_when_stmt(expr, indent)
|
return self._emit_when_stmt(expr, indent)
|
||||||
if name == "do" or name == "begin":
|
if name == "do" or name == "begin":
|
||||||
@@ -747,12 +752,12 @@ class PyEmitter:
|
|||||||
nested_set_vars = self._find_nested_set_vars(body)
|
nested_set_vars = self._find_nested_set_vars(body)
|
||||||
def_kw = "async def" if is_async else "def"
|
def_kw = "async def" if is_async else "def"
|
||||||
lines = [f"{pad}{def_kw} {py_name}({params_str}):"]
|
lines = [f"{pad}{def_kw} {py_name}({params_str}):"]
|
||||||
if nested_set_vars:
|
|
||||||
lines.append(f"{pad} _cells = {{}}")
|
|
||||||
# Emit body with cell var tracking (and async context if needed)
|
# Emit body with cell var tracking (and async context if needed)
|
||||||
old_cells = getattr(self, '_current_cell_vars', set())
|
old_cells = getattr(self, '_current_cell_vars', set())
|
||||||
|
if nested_set_vars and not old_cells:
|
||||||
|
lines.append(f"{pad} _cells = {{}}")
|
||||||
old_async = self._in_async
|
old_async = self._in_async
|
||||||
self._current_cell_vars = nested_set_vars
|
self._current_cell_vars = old_cells | nested_set_vars
|
||||||
if is_async:
|
if is_async:
|
||||||
self._in_async = True
|
self._in_async = True
|
||||||
self._emit_body_stmts(body, lines, indent + 1)
|
self._emit_body_stmts(body, lines, indent + 1)
|
||||||
|
|||||||
@@ -2179,18 +2179,18 @@ def sx_parse(source):
|
|||||||
_cells['pos'] = 0
|
_cells['pos'] = 0
|
||||||
len_src = len(source)
|
len_src = len(source)
|
||||||
def skip_comment():
|
def skip_comment():
|
||||||
if sx_truthy(((pos < len_src) if not sx_truthy((pos < len_src)) else (not sx_truthy((nth(source, pos) == '\n'))))):
|
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (not sx_truthy((nth(source, _cells['pos']) == '\n'))))):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return skip_comment()
|
return skip_comment()
|
||||||
return NIL
|
return NIL
|
||||||
def skip_ws():
|
def skip_ws():
|
||||||
if sx_truthy((pos < len_src)):
|
if sx_truthy((_cells['pos'] < len_src)):
|
||||||
ch = nth(source, pos)
|
ch = nth(source, _cells['pos'])
|
||||||
if sx_truthy(((ch == ' ') if sx_truthy((ch == ' ')) else ((ch == '\t') if sx_truthy((ch == '\t')) else ((ch == '\n') if sx_truthy((ch == '\n')) else (ch == '\r'))))):
|
if sx_truthy(((ch == ' ') if sx_truthy((ch == ' ')) else ((ch == '\t') if sx_truthy((ch == '\t')) else ((ch == '\n') if sx_truthy((ch == '\n')) else (ch == '\r'))))):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return skip_ws()
|
return skip_ws()
|
||||||
elif sx_truthy((ch == ';')):
|
elif sx_truthy((ch == ';')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
skip_comment()
|
skip_comment()
|
||||||
return skip_ws()
|
return skip_ws()
|
||||||
else:
|
else:
|
||||||
@@ -2203,35 +2203,35 @@ def sx_parse(source):
|
|||||||
_cells['pos'] = (_cells['pos'] + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
_cells['buf'] = ''
|
_cells['buf'] = ''
|
||||||
def read_str_loop():
|
def read_str_loop():
|
||||||
if sx_truthy((pos >= len_src)):
|
if sx_truthy((_cells['pos'] >= len_src)):
|
||||||
return error('Unterminated string')
|
return error('Unterminated string')
|
||||||
else:
|
else:
|
||||||
ch = nth(source, pos)
|
ch = nth(source, _cells['pos'])
|
||||||
if sx_truthy((ch == '"')):
|
if sx_truthy((ch == '"')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return NIL
|
return NIL
|
||||||
elif sx_truthy((ch == '\\')):
|
elif sx_truthy((ch == '\\')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
esc = nth(source, pos)
|
esc = nth(source, _cells['pos'])
|
||||||
if sx_truthy((esc == 'u')):
|
if sx_truthy((esc == 'u')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
d0 = hex_digit_value(nth(source, pos))
|
d0 = hex_digit_value(nth(source, _cells['pos']))
|
||||||
_ = _sx_cell_set(_cells, 'pos', (pos + 1))
|
_ = _sx_cell_set(_cells, 'pos', (_cells['pos'] + 1))
|
||||||
d1 = hex_digit_value(nth(source, pos))
|
d1 = hex_digit_value(nth(source, _cells['pos']))
|
||||||
_ = _sx_cell_set(_cells, 'pos', (pos + 1))
|
_ = _sx_cell_set(_cells, 'pos', (_cells['pos'] + 1))
|
||||||
d2 = hex_digit_value(nth(source, pos))
|
d2 = hex_digit_value(nth(source, _cells['pos']))
|
||||||
_ = _sx_cell_set(_cells, 'pos', (pos + 1))
|
_ = _sx_cell_set(_cells, 'pos', (_cells['pos'] + 1))
|
||||||
d3 = hex_digit_value(nth(source, pos))
|
d3 = hex_digit_value(nth(source, _cells['pos']))
|
||||||
_ = _sx_cell_set(_cells, 'pos', (pos + 1))
|
_ = _sx_cell_set(_cells, 'pos', (_cells['pos'] + 1))
|
||||||
buf = sx_str(buf, char_from_code(((d0 * 4096) + (d1 * 256))))
|
_cells['buf'] = sx_str(_cells['buf'], char_from_code(((d0 * 4096) + (d1 * 256))))
|
||||||
return read_str_loop()
|
return read_str_loop()
|
||||||
else:
|
else:
|
||||||
buf = sx_str(buf, ('\n' if sx_truthy((esc == 'n')) else ('\t' if sx_truthy((esc == 't')) else ('\r' if sx_truthy((esc == 'r')) else esc))))
|
_cells['buf'] = sx_str(_cells['buf'], ('\n' if sx_truthy((esc == 'n')) else ('\t' if sx_truthy((esc == 't')) else ('\r' if sx_truthy((esc == 'r')) else esc))))
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_str_loop()
|
return read_str_loop()
|
||||||
else:
|
else:
|
||||||
buf = sx_str(buf, ch)
|
_cells['buf'] = sx_str(_cells['buf'], ch)
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_str_loop()
|
return read_str_loop()
|
||||||
read_str_loop()
|
read_str_loop()
|
||||||
return _cells['buf']
|
return _cells['buf']
|
||||||
@@ -2239,14 +2239,14 @@ def sx_parse(source):
|
|||||||
_cells = {}
|
_cells = {}
|
||||||
start = _cells['pos']
|
start = _cells['pos']
|
||||||
def read_ident_loop():
|
def read_ident_loop():
|
||||||
if sx_truthy(((pos < len_src) if not sx_truthy((pos < len_src)) else ident_char_p(nth(source, pos)))):
|
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else ident_char_p(nth(source, _cells['pos'])))):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_ident_loop()
|
return read_ident_loop()
|
||||||
return NIL
|
return NIL
|
||||||
read_ident_loop()
|
read_ident_loop()
|
||||||
return slice(source, start, _cells['pos'])
|
return slice(source, start, _cells['pos'])
|
||||||
def read_keyword():
|
def read_keyword():
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return make_keyword(read_ident())
|
return make_keyword(read_ident())
|
||||||
def read_number():
|
def read_number():
|
||||||
_cells = {}
|
_cells = {}
|
||||||
@@ -2254,8 +2254,8 @@ def sx_parse(source):
|
|||||||
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '-'))):
|
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '-'))):
|
||||||
_cells['pos'] = (_cells['pos'] + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
def read_digits():
|
def read_digits():
|
||||||
if sx_truthy(((pos < len_src) if not sx_truthy((pos < len_src)) else (lambda c: ((c >= '0') if not sx_truthy((c >= '0')) else (c <= '9')))(nth(source, pos)))):
|
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (lambda c: ((c >= '0') if not sx_truthy((c >= '0')) else (c <= '9')))(nth(source, _cells['pos'])))):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_digits()
|
return read_digits()
|
||||||
return NIL
|
return NIL
|
||||||
read_digits()
|
read_digits()
|
||||||
@@ -2283,11 +2283,11 @@ def sx_parse(source):
|
|||||||
items = []
|
items = []
|
||||||
def read_list_loop():
|
def read_list_loop():
|
||||||
skip_ws()
|
skip_ws()
|
||||||
if sx_truthy((pos >= len_src)):
|
if sx_truthy((_cells['pos'] >= len_src)):
|
||||||
return error('Unterminated list')
|
return error('Unterminated list')
|
||||||
else:
|
else:
|
||||||
if sx_truthy((nth(source, pos) == close_ch)):
|
if sx_truthy((nth(source, _cells['pos']) == close_ch)):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return NIL
|
return NIL
|
||||||
else:
|
else:
|
||||||
items.append(read_expr())
|
items.append(read_expr())
|
||||||
@@ -2299,11 +2299,11 @@ def sx_parse(source):
|
|||||||
result = {}
|
result = {}
|
||||||
def read_map_loop():
|
def read_map_loop():
|
||||||
skip_ws()
|
skip_ws()
|
||||||
if sx_truthy((pos >= len_src)):
|
if sx_truthy((_cells['pos'] >= len_src)):
|
||||||
return error('Unterminated map')
|
return error('Unterminated map')
|
||||||
else:
|
else:
|
||||||
if sx_truthy((nth(source, pos) == '}')):
|
if sx_truthy((nth(source, _cells['pos']) == '}')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return NIL
|
return NIL
|
||||||
else:
|
else:
|
||||||
key_expr = read_expr()
|
key_expr = read_expr()
|
||||||
@@ -2317,63 +2317,63 @@ def sx_parse(source):
|
|||||||
_cells = {}
|
_cells = {}
|
||||||
_cells['buf'] = ''
|
_cells['buf'] = ''
|
||||||
def raw_loop():
|
def raw_loop():
|
||||||
if sx_truthy((pos >= len_src)):
|
if sx_truthy((_cells['pos'] >= len_src)):
|
||||||
return error('Unterminated raw string')
|
return error('Unterminated raw string')
|
||||||
else:
|
else:
|
||||||
ch = nth(source, pos)
|
ch = nth(source, _cells['pos'])
|
||||||
if sx_truthy((ch == '|')):
|
if sx_truthy((ch == '|')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return NIL
|
return NIL
|
||||||
else:
|
else:
|
||||||
buf = sx_str(buf, ch)
|
_cells['buf'] = sx_str(_cells['buf'], ch)
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return raw_loop()
|
return raw_loop()
|
||||||
raw_loop()
|
raw_loop()
|
||||||
return _cells['buf']
|
return _cells['buf']
|
||||||
def read_expr():
|
def read_expr():
|
||||||
skip_ws()
|
skip_ws()
|
||||||
if sx_truthy((pos >= len_src)):
|
if sx_truthy((_cells['pos'] >= len_src)):
|
||||||
return error('Unexpected end of input')
|
return error('Unexpected end of input')
|
||||||
else:
|
else:
|
||||||
ch = nth(source, pos)
|
ch = nth(source, _cells['pos'])
|
||||||
if sx_truthy((ch == '(')):
|
if sx_truthy((ch == '(')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_list(')')
|
return read_list(')')
|
||||||
elif sx_truthy((ch == '[')):
|
elif sx_truthy((ch == '[')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_list(']')
|
return read_list(']')
|
||||||
elif sx_truthy((ch == '{')):
|
elif sx_truthy((ch == '{')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_map()
|
return read_map()
|
||||||
elif sx_truthy((ch == '"')):
|
elif sx_truthy((ch == '"')):
|
||||||
return read_string()
|
return read_string()
|
||||||
elif sx_truthy((ch == ':')):
|
elif sx_truthy((ch == ':')):
|
||||||
return read_keyword()
|
return read_keyword()
|
||||||
elif sx_truthy((ch == '`')):
|
elif sx_truthy((ch == '`')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return [make_symbol('quasiquote'), read_expr()]
|
return [make_symbol('quasiquote'), read_expr()]
|
||||||
elif sx_truthy((ch == ',')):
|
elif sx_truthy((ch == ',')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
if sx_truthy(((pos < len_src) if not sx_truthy((pos < len_src)) else (nth(source, pos) == '@'))):
|
if sx_truthy(((_cells['pos'] < len_src) if not sx_truthy((_cells['pos'] < len_src)) else (nth(source, _cells['pos']) == '@'))):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return [make_symbol('splice-unquote'), read_expr()]
|
return [make_symbol('splice-unquote'), read_expr()]
|
||||||
else:
|
else:
|
||||||
return [make_symbol('unquote'), read_expr()]
|
return [make_symbol('unquote'), read_expr()]
|
||||||
elif sx_truthy((ch == '#')):
|
elif sx_truthy((ch == '#')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
if sx_truthy((pos >= len_src)):
|
if sx_truthy((_cells['pos'] >= len_src)):
|
||||||
return error('Unexpected end of input after #')
|
return error('Unexpected end of input after #')
|
||||||
else:
|
else:
|
||||||
dispatch_ch = nth(source, pos)
|
dispatch_ch = nth(source, _cells['pos'])
|
||||||
if sx_truthy((dispatch_ch == ';')):
|
if sx_truthy((dispatch_ch == ';')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
read_expr()
|
read_expr()
|
||||||
return read_expr()
|
return read_expr()
|
||||||
elif sx_truthy((dispatch_ch == '|')):
|
elif sx_truthy((dispatch_ch == '|')):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return read_raw_string()
|
return read_raw_string()
|
||||||
elif sx_truthy((dispatch_ch == "'")):
|
elif sx_truthy((dispatch_ch == "'")):
|
||||||
pos = (pos + 1)
|
_cells['pos'] = (_cells['pos'] + 1)
|
||||||
return [make_symbol('quote'), read_expr()]
|
return [make_symbol('quote'), read_expr()]
|
||||||
elif sx_truthy(ident_start_p(dispatch_ch)):
|
elif sx_truthy(ident_start_p(dispatch_ch)):
|
||||||
macro_name = read_ident()
|
macro_name = read_ident()
|
||||||
@@ -2384,10 +2384,10 @@ def sx_parse(source):
|
|||||||
return error(sx_str('Unknown reader macro: #', macro_name))
|
return error(sx_str('Unknown reader macro: #', macro_name))
|
||||||
else:
|
else:
|
||||||
return error(sx_str('Unknown reader macro: #', dispatch_ch))
|
return error(sx_str('Unknown reader macro: #', dispatch_ch))
|
||||||
elif sx_truthy((((ch >= '0') if not sx_truthy((ch >= '0')) else (ch <= '9')) if sx_truthy(((ch >= '0') if not sx_truthy((ch >= '0')) else (ch <= '9'))) else ((ch == '-') if not sx_truthy((ch == '-')) else (((pos + 1) < len_src) if not sx_truthy(((pos + 1) < len_src)) else (lambda next_ch: ((next_ch >= '0') if not sx_truthy((next_ch >= '0')) else (next_ch <= '9')))(nth(source, (pos + 1))))))):
|
elif sx_truthy((((ch >= '0') if not sx_truthy((ch >= '0')) else (ch <= '9')) if sx_truthy(((ch >= '0') if not sx_truthy((ch >= '0')) else (ch <= '9'))) else ((ch == '-') if not sx_truthy((ch == '-')) else (((_cells['pos'] + 1) < len_src) if not sx_truthy(((_cells['pos'] + 1) < len_src)) else (lambda next_ch: ((next_ch >= '0') if not sx_truthy((next_ch >= '0')) else (next_ch <= '9')))(nth(source, (_cells['pos'] + 1))))))):
|
||||||
return read_number()
|
return read_number()
|
||||||
elif sx_truthy(((ch == '.') if not sx_truthy((ch == '.')) else (((pos + 2) < len_src) if not sx_truthy(((pos + 2) < len_src)) else ((nth(source, (pos + 1)) == '.') if not sx_truthy((nth(source, (pos + 1)) == '.')) else (nth(source, (pos + 2)) == '.'))))):
|
elif sx_truthy(((ch == '.') if not sx_truthy((ch == '.')) else (((_cells['pos'] + 2) < len_src) if not sx_truthy(((_cells['pos'] + 2) < len_src)) else ((nth(source, (_cells['pos'] + 1)) == '.') if not sx_truthy((nth(source, (_cells['pos'] + 1)) == '.')) else (nth(source, (_cells['pos'] + 2)) == '.'))))):
|
||||||
pos = (pos + 3)
|
_cells['pos'] = (_cells['pos'] + 3)
|
||||||
return make_symbol('...')
|
return make_symbol('...')
|
||||||
elif sx_truthy(ident_start_p(ch)):
|
elif sx_truthy(ident_start_p(ch)):
|
||||||
return read_symbol()
|
return read_symbol()
|
||||||
@@ -2396,7 +2396,7 @@ def sx_parse(source):
|
|||||||
exprs = []
|
exprs = []
|
||||||
def parse_loop():
|
def parse_loop():
|
||||||
skip_ws()
|
skip_ws()
|
||||||
if sx_truthy((pos < len_src)):
|
if sx_truthy((_cells['pos'] < len_src)):
|
||||||
exprs.append(read_expr())
|
exprs.append(read_expr())
|
||||||
return parse_loop()
|
return parse_loop()
|
||||||
return NIL
|
return NIL
|
||||||
|
|||||||
@@ -234,7 +234,9 @@
|
|||||||
(dict :label "Foundations" :href "/sx/(etc.(plan.foundations))"
|
(dict :label "Foundations" :href "/sx/(etc.(plan.foundations))"
|
||||||
:summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).")
|
:summary "The computational floor — from scoped effects through algebraic effects and delimited continuations to the CEK machine. Why three registers are irreducible, and the three-axis model (depth, topology, linearity).")
|
||||||
(dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))"
|
(dict :label "Deref as Shift" :href "/sx/(etc.(plan.cek-reactive))"
|
||||||
:summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.")))
|
:summary "Phase B: replace explicit effect wrapping with implicit continuation capture. Deref inside reactive-reset performs shift, capturing the rest of the expression as the subscriber.")
|
||||||
|
(dict :label "Reactive Runtime" :href "/sx/(etc.(plan.reactive-runtime))"
|
||||||
|
:summary "Seven feature layers — ref, foreign FFI, state machines, commands with undo/redo, render loops, keyed lists, client-first app shell. Zero new platform primitives.")))
|
||||||
|
|
||||||
(define reactive-islands-nav-items (list
|
(define reactive-islands-nav-items (list
|
||||||
(dict :label "Overview" :href "/sx/(geography.(reactive))"
|
(dict :label "Overview" :href "/sx/(geography.(reactive))"
|
||||||
|
|||||||
209
sx/sx/plans/reactive-runtime.sx
Normal file
209
sx/sx/plans/reactive-runtime.sx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
;; Reactive Application Runtime — 7 Feature Layers
|
||||||
|
;; Zero new platform primitives. All macros composing existing signals + DOM ops.
|
||||||
|
|
||||||
|
(defcomp ~plans/reactive-runtime/plan-reactive-runtime-content ()
|
||||||
|
(~docs/page :title "Reactive Application Runtime"
|
||||||
|
|
||||||
|
(p :class "text-stone-500 text-sm italic mb-8"
|
||||||
|
"Seven feature layers that take SX from reactive values to a full reactive application runtime. "
|
||||||
|
"Every layer is a macro expanding to existing primitives — zero new CEK frames, "
|
||||||
|
"zero new platform primitives. Proves the existing platform interface is sufficient "
|
||||||
|
"for any application pattern.")
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; Motivation
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Motivation" :id "motivation"
|
||||||
|
|
||||||
|
(p "SX has signals, computed, effects, batch, islands, lakes, stores, and bridge events — "
|
||||||
|
"sufficient for reactive documents (forms, toggles, counters, live search). "
|
||||||
|
"But complex client-heavy apps (drawing tools, editors, games) need structure on top:")
|
||||||
|
|
||||||
|
(table :class "w-full mb-6 text-sm"
|
||||||
|
(thead
|
||||||
|
(tr (th :class "text-left p-2" "Have") (th :class "text-left p-2" "Missing")))
|
||||||
|
(tbody
|
||||||
|
(tr (td :class "p-2" "Signal holds a value") (td :class "p-2" "Ref holds a value " (em "without") " reactivity"))
|
||||||
|
(tr (td :class "p-2" "Effect runs on change") (td :class "p-2" "Loop runs " (em "continuously")))
|
||||||
|
(tr (td :class "p-2" "Store shares state") (td :class "p-2" "Machine manages " (em "modal") " state"))
|
||||||
|
(tr (td :class "p-2" "reset!/swap! update") (td :class "p-2" "Commands update " (em "with history")))
|
||||||
|
(tr (td :class "p-2" "DOM rendering") (td :class "p-2" "Foreign calls " (em "any host API")))
|
||||||
|
(tr (td :class "p-2" "Server-first hydration") (td :class "p-2" "Client-first " (em "app shell"))))))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; Implementation Order
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Implementation Order" :id "order"
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"L0 Ref -> standalone, trivial (~95 LOC)\nL1 Foreign FFI -> standalone, macro over dom-call-method (~140 LOC)\nL5 Keyed Lists -> enhances existing reactive-list (~155 LOC)\nL2 State Machine -> uses signals (~200 LOC)\nL4 Render Loop -> uses refs (L0), existing RAF (~140 LOC)\nL3 Commands -> extends def-store, uses signals (~320 LOC)\nL6 App Shell -> orchestrates all above (~330 LOC)\n Total: ~1380 LOC"
|
||||||
|
"text"))
|
||||||
|
|
||||||
|
(p "Order rationale: L0 and L1 are independent foundations. L5 enhances existing code. "
|
||||||
|
"L2 and L4 depend on L0. L3 builds on signals. L6 ties everything together."))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; L0: Ref
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Layer 0: Ref (Mutable Box Without Reactivity)" :id "l0-ref"
|
||||||
|
|
||||||
|
(p "A " (code "ref") " is like a signal but with NO subscriber tracking, NO notifications. "
|
||||||
|
"Just a mutable cell for DOM handles, canvas contexts, timer IDs.")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(ref initial-value) ;; create ref, auto-registers dispose in island scope\n(ref-deref r) ;; read (no tracking)\n(ref-set! r v) ;; write (no notification)\n(ref? x) ;; predicate"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p "Plain dicts with " (code "__ref") " marker (mirrors " (code "__signal") " pattern). "
|
||||||
|
"On island disposal, auto-nil'd via " (code "register-in-scope") ". ~35 lines of spec.")
|
||||||
|
|
||||||
|
(~docs/subsection :title "Files"
|
||||||
|
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||||
|
(li (strong "NEW") " " (code "shared/sx/ref/refs.sx") " — ref, ref-deref, ref-set!, ref?")
|
||||||
|
(li (strong "NEW") " " (code "shared/sx/ref/test-refs.sx") " — tests: create, read, set, predicate, scope disposal")
|
||||||
|
(li "Add " (code "\"refs\"") " to " (code "SPEC_MODULES") " in both platform files"))))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; L1: Foreign
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Layer 1: Foreign (Host API Interop)" :id "l1-foreign"
|
||||||
|
|
||||||
|
(p "Clean boundary for calling host APIs (Canvas, WebGL, WebAudio) from SX code. "
|
||||||
|
"Uses " (strong "existing") " platform primitives — no new ones needed:")
|
||||||
|
|
||||||
|
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||||
|
(li (code "dom-call-method(obj, \"methodName\", args...)") " — method calls")
|
||||||
|
(li (code "dom-get-prop(obj, \"propName\")") " — property getter")
|
||||||
|
(li (code "dom-set-prop(obj, \"propName\", value)") " — property setter"))
|
||||||
|
|
||||||
|
(p (code "def-foreign") " is a macro that generates calls to these existing primitives.")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(def-foreign canvas-2d\n (fill-rect x y w h) ;; method call\n (:fill-style color)) ;; property setter (keyword = property)\n\n;; Get host object via existing primitive\n(let ((ctx (dom-call-method canvas \"getContext\" \"2d\")))\n (canvas-2d.fill-rect ctx 0 0 100 100)\n (canvas-2d.fill-style! ctx \"red\"))"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(~docs/subsection :title "Macro Expansion"
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(def-foreign canvas-2d\n (fill-rect x y w h)\n (:fill-style color))\n;; expands to:\n(do\n (define canvas-2d.fill-rect\n (fn (ctx x y w h) (dom-call-method ctx \"fillRect\" x y w h)))\n (define canvas-2d.fill-style!\n (fn (ctx color) (dom-set-prop ctx \"fillStyle\" color))))"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p "Dot notation works because " (code "ident-char?") " includes " (code ".") ". "
|
||||||
|
"The macro converts SX naming (" (code "fill-rect") ") to host naming (" (code "fillRect") ") via camelCase transform.")))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; L5: Keyed Lists
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Layer 5: Keyed List Reconciliation" :id "l5-keyed"
|
||||||
|
|
||||||
|
(p "Enhance existing " (code "reactive-list") " (adapter-dom.sx:1000) with explicit " (code ":key") " parameter. "
|
||||||
|
"Current code already has keyed reconciliation via DOM " (code "key") " attributes — this adds "
|
||||||
|
"an explicit key extraction callback and stable identity tracking.")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(map (fn (el) (~shape-handle el)) (deref items) :key (fn (el) (get el :id)))"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p "Changes to " (code "render-dom-list") " detection (line 563-575) and " (code "reactive-list") " implementation. "
|
||||||
|
"No new platform primitives — existing DOM ops suffice."))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; L2: State Machines
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Layer 2: State Machines (defmachine)" :id "l2-machine"
|
||||||
|
|
||||||
|
(p "Modal state management for complex UI modes. Machine state IS a signal — "
|
||||||
|
"composes naturally with computed/effects.")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(defmachine drawing-tool\n :initial :idle\n :states {\n :idle {:on {:pointer-down (fn (ev) {:state :drawing\n :actions [(start-shape! ev)]})}}\n :drawing {:on {:pointer-move (fn (ev) {:state :drawing\n :actions [(update-shape! ev)]})\n :pointer-up (fn (ev) {:state :idle\n :actions [(finish-shape! ev)]})}}})\n\n(machine-send! drawing-tool :pointer-down event)\n(machine-matches? drawing-tool :drawing) ;; reactive via deref\n(machine-state drawing-tool) ;; returns the state signal"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p (code "defmachine") " is a macro expanding to a " (code "let") " with a signal for current state, "
|
||||||
|
"a transitions dict, and a " (code "send!") " function. Built on signals + dicts."))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; L4: Render Loop
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Layer 4: Continuous Rendering Loop (defloop)" :id "l4-loop"
|
||||||
|
|
||||||
|
(p (code "requestAnimationFrame") " integration for canvas/animation apps, "
|
||||||
|
"with island lifecycle cleanup.")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(defloop render-loop (fn (timestamp dt)\n (let ((ctx (ref-deref ctx-ref)))\n (clear-canvas! ctx)\n (draw-scene! ctx (deref elements)))))\n\n(loop-start! render-loop)\n(loop-stop! render-loop)\n(loop-running? render-loop) ;; reactive signal"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p "Uses the " (strong "running-ref pattern") " to avoid needing " (code "cancelAnimationFrame") ":")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(let ((running (ref true))\n (last-ts (ref 0)))\n (define tick (fn (ts)\n (when (ref-deref running)\n (let ((dt (- ts (ref-deref last-ts))))\n (ref-set! last-ts ts)\n (user-fn ts dt))\n (request-animation-frame tick))))\n (request-animation-frame tick)\n ;; stop: (ref-set! running false) -- loop dies on next frame"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p "Uses existing " (code "request-animation-frame") " (already wired in JS). "
|
||||||
|
"Auto-stops on island disposal via " (code "register-in-scope") ". "
|
||||||
|
"Depends on L0 (refs)."))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; L3: Commands
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Layer 3: Commands with History (Undo/Redo)" :id "l3-commands"
|
||||||
|
|
||||||
|
(p "Command pattern built into signal stores. Commands are s-expressions "
|
||||||
|
"in a history stack — trivially serializable.")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(def-command-store canvas-state\n :initial {:elements '() :selection nil}\n :commands {\n :add-element (fn (state el)\n (assoc state :elements\n (append (get state :elements) (list el))))\n :move-element (fn (state id dx dy) ...)\n }\n :max-history 100)\n\n(dispatch! canvas-state :add-element rect-1)\n(undo! canvas-state)\n(redo! canvas-state)\n(can-undo? canvas-state) ;; reactive\n(can-redo? canvas-state) ;; reactive\n(group-start! canvas-state \"drag\") ;; transaction grouping\n(group-end! canvas-state)"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p "Macro wraps signal with undo-stack and redo-stack (both signals of lists). "
|
||||||
|
(code "group-start!") "/" (code "group-end!") " collapses multiple dispatches "
|
||||||
|
"into one undo entry — essential for drag operations."))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; L6: App Shell
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Layer 6: Client-First App Shell (defapp)" :id "l6-app"
|
||||||
|
|
||||||
|
(p "Skip SSR entirely for canvas-heavy apps. Server returns minimal HTML shell, "
|
||||||
|
"all rendering client-side.")
|
||||||
|
|
||||||
|
(~docs/code :code (highlight
|
||||||
|
"(defapp excalidraw\n :render :client\n :entry ~drawing-app\n :stores (canvas-state tool-state)\n :routes {\"/\" ~drawing-app\n \"/gallery\" ~gallery-view}\n :head [(link :rel \"stylesheet\" :href \"/static/app.css\")])"
|
||||||
|
"lisp"))
|
||||||
|
|
||||||
|
(p "Macro generates:")
|
||||||
|
(ul :class "list-disc pl-6 mb-4 space-y-1"
|
||||||
|
(li (strong "Server:") " minimal HTML shell (doctype + " (code "<div id=\"sx-app-root\">") " + SX loader)")
|
||||||
|
(li (strong "Client:") " " (code "sx-app-boot") " function using existing " (code "sx-mount") " for initial render")))
|
||||||
|
|
||||||
|
;; =====================================================================
|
||||||
|
;; Zero Platform Primitives
|
||||||
|
;; =====================================================================
|
||||||
|
|
||||||
|
(~docs/section :title "Zero New Platform Primitives" :id "zero-primitives"
|
||||||
|
|
||||||
|
(p "All 7 layers are pure " (code ".sx") " macros composing existing primitives:")
|
||||||
|
|
||||||
|
(table :class "w-full mb-6 text-sm"
|
||||||
|
(thead
|
||||||
|
(tr (th :class "text-left p-2" "Existing Primitive") (th :class "text-left p-2" "Used By")))
|
||||||
|
(tbody
|
||||||
|
(tr (td :class "p-2" (code "signal, deref, reset!, swap!, computed, effect")) (td :class "p-2" "L2, L3, L4"))
|
||||||
|
(tr (td :class "p-2" (code "dom-call-method, dom-get-prop, dom-set-prop")) (td :class "p-2" "L1 (Foreign FFI)"))
|
||||||
|
(tr (td :class "p-2" (code "request-animation-frame")) (td :class "p-2" "L4 (Render Loop)"))
|
||||||
|
(tr (td :class "p-2" (code "register-in-scope, scope-push!, scope-pop!")) (td :class "p-2" "L0, L4"))
|
||||||
|
(tr (td :class "p-2" (code "sx-mount, sx-hydrate-islands")) (td :class "p-2" "L6 (App Shell)"))
|
||||||
|
(tr (td :class "p-2" (code "DOM ops (dom-insert-after, dom-remove, ...)")) (td :class "p-2" "L5 (Keyed Lists)"))))
|
||||||
|
|
||||||
|
(p :class "text-stone-600 italic"
|
||||||
|
"This validates SX's architecture: the existing platform interface is complete "
|
||||||
|
"enough for any application pattern."))))
|
||||||
Reference in New Issue
Block a user