Commit572695a9Recorded30 May 2026Repositoryblackice

Fix rendering: import (sigil struct), symbol tags, use (sigil web client)

Message

Three bugs found by running the built wasm in the browser:

- level/game used define-struct but imported only (sigil core); it compiled clean but failed at wasm load with "unbound variable 'define-struct'". Added (sigil struct) imports. - the SXML el/el helpers built element tags as strings, which the SXML layer renders as text nodes (the page showed literal "div" text). Tags must be symbols; el/el now string->symbol the tag. - switched the renderer from the low-level (sigil wasm dom) mount to (sigil web client) web-client-update, which diffs the new view against the last and patches only changed nodes (keyed reconciliation) instead of a full per-move re-mount.

Validated in-browser: menu + levels 1-2 render correctly, keyboard input drives moves, block-push onto port works, win banner shows, and progress persists to local-storage across reload. Bridge report updated (F3 now "diff exists but only in sigil-web-client", plus F9 string-tag footgun and F10 missing-macro-import compiles-clean-fails-at-load).

Changed
 docs/wasm-bridge-report.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++-------------
 package.sgl                |  3 ++-
 src/blackice/game.sgl      |  1 +
 src/blackice/level.sgl     |  1 +
 src/blackice/render.sgl    | 26 ++++++++++++++++----------
 5 files changed, 68 insertions(+), 24 deletions(-)
Diff
docs/wasm-bridge-report.mdmodified
@@ -66,19 +66,26 @@ auto-stub any import the bridges don't provide (see `stubMissingImports`). That
66
keeps app authors free of phantom dependencies and is a good behavior to fold
67
into the emitted loader regardless.
68
69
### F3 — No render diff / reconciliation primitive
70
71
The only mount primitive is `wasm-dom-mount-sxml`, which does
72
`replace-children` + full rebuild. There is no keyed diff/patch. The task framed
73
rendering as "diffed per move", but the bridge only supports full subtree
74
teardown. For blackICE's small grid this is fine (a full re-mount per move is
75
cheap), but it is O(nodes) per render and will not scale to large or
76
animation-heavy UIs, and it discards focus/scroll/transition state.
77
78
**Suggestion.** Add a `wasm-dom-reconcile parent old-sxml new-sxml` (or a
79
retained-mode handle that diffs against its last tree) so renderers can patch
80
instead of replace. Even a coarse keyed-children diff would remove the main
81
scaling cliff. Document the current full-replace cost explicitly in the meantime.
+69
### F3 — Render diff exists, but only in `(sigil web client)`, not the low-level bridge — RESOLVED, with a discoverability gap
+70
+71
The low-level `(sigil wasm dom sxml)` only offers `wasm-dom-mount-sxml`
+72
(`replace-children` + full rebuild). The keyed diff/patch the design wanted lives
+73
one layer up in **`(sigil web client)`**: `web-client-update selector view`
+74
reconciles the new SXML against the last rendered tree and patches only changed
+75
nodes. blackICE initially rendered via the low-level bridge directly and got both
+76
a correctness bug (see F9) and full re-mounts; switching to `web-client-update`
+77
fixed both.
+78
+79
So this is not a missing primitive — it is a **discoverability/layering** issue:
+80
nothing about the bridge package surface points an app author at
+81
`(sigil web client)` as *the* renderer. A first-time author reads
+82
`(sigil wasm dom)` (the documented bridge) and builds on `wasm-dom-mount-sxml`,
+83
missing the diffing layer entirely.
+84
+85
**Suggestion.** Make `(sigil web client)` the documented, default browser-render
+86
API (and have the generated app template import it), with the raw
+87
`(sigil wasm dom)` clearly marked as the low-level escape hatch. Folded into the
+88
naming question in F6.
89
90
### F4 — JS→Sigil calls are stringly-typed `eval`
91
@@ -160,6 +167,34 @@ extend that to `.sgb` outputs). Document the `rm -rf build` step meanwhile.
167
168
---
169
+170
### F9 — SXML string-tag footgun renders silently wrong
+171
+172
The SXML layer (`(sigil wasm dom sxml)` and `(sigil web client)`) decides a list
+173
is an element only when `(symbol? (car list))`. If the tag is a **string**
+174
(`("div" (@ ...) "hi")`) there is no error: the list is treated as a content
+175
sequence and every item, including the literal string `"div"`, is rendered as a
+176
text node. blackICE built tags as strings and the page showed `divdivblackICE…`
+177
with no diagnostic. This is easy to hit because everything else (classes,
+178
attribute names, text) is happily a string.
+179
+180
**Suggestion.** Either accept string tags (string→symbol at the boundary) or warn
+181
when a list's head is a string in element position. Silent mis-rendering with no
+182
console output is the worst failure mode for a first-time author.
+183
+184
### F10 — Missing macro import compiles clean, fails only at wasm load
+185
+186
`define-struct` comes from `(sigil struct)`. blackICE's `level`/`game` used it but
+187
imported only `(sigil core)`. The build **compiled and linked with no error**;
+188
the failure surfaced only at runtime in the browser as
+189
`Error loading entrypoint: unbound variable 'define-struct'`. A missing
+190
macro-providing import should be a compile-time error, not a load-time one — the
+191
compiler clearly had enough information (the macro was unbound during expansion)
+192
to reject it.
+193
+194
**Suggestion.** Make unresolved macro/binding references in a compiled module a
+195
build error (or at least a warning), so the failure is caught at
+196
`sigil build` rather than after instantiation in the browser.
+197
198
## The zero-JS-wiring standard browser-app pattern (promotion spec)
199
200
Goal (set by David): **an app author writes zero direct JS wiring.** blackICE
package.sglmodified
@@ -44,7 +44,8 @@
44
(from-path dir: "../sigil" package: "sigil-stdlib")
45
(from-path dir: "../sigil" package: "sigil-wasm-runtime")
46
(from-path dir: "../sigil" package: "sigil-wasm-dom")
47
(from-path dir: "../sigil" package: "sigil-browser"))
+47
(from-path dir: "../sigil" package: "sigil-browser")
+48
(from-path dir: "../sigil-web" package: "sigil-web"))
49
50
tasks: (list
51
(task
src/blackice/game.sglmodified
@@ -14,6 +14,7 @@
14
15
(define-library (blackice game)
16
(import (sigil core)
+17
(sigil struct)
18
(sigil math)
19
(blackice grid)
20
(blackice level))
src/blackice/level.sglmodified
@@ -26,6 +26,7 @@
26
27
(define-library (blackice level)
28
(import (sigil core)
+29
(sigil struct)
30
(sigil math)
31
(sigil string)
32
(blackice grid))
src/blackice/render.sglmodified
@@ -1,11 +1,15 @@
1
;;; (blackice render) - Game/menu state -> SXML, mounted into the DOM.
2
;;;
3
;;; This is the ONLY module in the rules+render stack that references the DOM
4
;;; bridge. It builds an SXML tree from a `game` (or the level-select menu) and
5
;;; mounts it under #app via sigil-wasm-dom, re-rendering the whole subtree once
+3
;;; This is the ONLY module in the rules+render stack that references the DOM.
+4
;;; It builds an SXML tree from a `game` (or the level-select menu) and hands it
+5
;;; to (sigil web client), whose web-client-update diffs the new tree against the
+6
;;; last one and patches only the changed DOM nodes (keyed reconciliation) once
7
;;; per move. The tree is plain data, so swapping in a canvas/WebGL renderer
8
;;; means replacing this file alone. Classes drive the neon-on-black styling in
9
;;; styles.css; glyphs are ASCII for encoding safety.
+10
;;;
+11
;;; Element tags must be SYMBOLS: the SXML layer only treats a list as an element
+12
;;; when its head is a symbol, so the `el`/`el*` helpers string->symbol the tag.
13
14
(define-library (blackice render)
15
(import (sigil core)
@@ -13,19 +17,20 @@
17
(blackice grid)
18
(blackice level)
19
(blackice game)
16
(sigil wasm dom)
17
(sigil wasm dom sxml))
+20
(sigil web client))
21
22
(export render-game render-menu render-to-app)
23
24
(begin
25
;; --- SXML construction helpers -----------------------------------------
26
+27
;; The SXML renderer only treats a list as an element when its head is a
+28
;; symbol, so element tags must be symbols (not strings).
29
(define (el tag cls . children)
25
(cons tag (cons (list '@ (list 'class cls)) children)))
+30
(cons (string->symbol tag) (cons (list '@ (list 'class cls)) children)))
31
32
(define (el* tag attrs . children)
28
(cons tag (cons (cons '@ attrs) children)))
+33
(cons (string->symbol tag) (cons (cons '@ attrs) children)))
34
35
(define (txt s) s)
36
@@ -209,7 +214,8 @@
214
215
;; --- mount --------------------------------------------------------------
216
+217
;; web-client-update diffs the new view against the previously rendered
+218
;; tree and patches the DOM (keyed child reconciliation), so each move
+219
;; touches only the cells that changed rather than rebuilding the subtree.
220
(define (render-to-app sxml)
213
(let ((app (wasm-dom-query-selector "#app")))
214
(when (> app 0)
215
(wasm-dom-mount-sxml app sxml))))))
+221
(web-client-update "#app" sxml))))