Fix rendering: import (sigil struct), symbol tags, use (sigil web client)
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).
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(-)docs/wasm-bridge-report.mdmodified
keeps app authors free of phantom dependencies and is a good behavior to foldinto the emitted loader regardless.### F3 — No render diff / reconciliation primitiveThe only mount primitive is `wasm-dom-mount-sxml`, which does`replace-children` + full rebuild. There is no keyed diff/patch. The task framedrendering as "diffed per move", but the bridge only supports full subtreeteardown. For blackICE's small grid this is fine (a full re-mount per move ischeap), but it is O(nodes) per render and will not scale to large oranimation-heavy UIs, and it discards focus/scroll/transition state.**Suggestion.** Add a `wasm-dom-reconcile parent old-sxml new-sxml` (or aretained-mode handle that diffs against its last tree) so renderers can patchinstead of replace. Even a coarse keyed-children diff would remove the mainscaling cliff. Document the current full-replace cost explicitly in the meantime.### F3 — Render diff exists, but only in `(sigil web client)`, not the low-level bridge — RESOLVED, with a discoverability gapThe low-level `(sigil wasm dom sxml)` only offers `wasm-dom-mount-sxml`(`replace-children` + full rebuild). The keyed diff/patch the design wanted livesone layer up in **`(sigil web client)`**: `web-client-update selector view`reconciles the new SXML against the last rendered tree and patches only changednodes. blackICE initially rendered via the low-level bridge directly and got botha correctness bug (see F9) and full re-mounts; switching to `web-client-update`fixed both.So this is not a missing primitive — it is a **discoverability/layering** issue:nothing about the bridge package surface points an app author at`(sigil web client)` as *the* renderer. A first-time author reads`(sigil wasm dom)` (the documented bridge) and builds on `wasm-dom-mount-sxml`,missing the diffing layer entirely.**Suggestion.** Make `(sigil web client)` the documented, default browser-renderAPI (and have the generated app template import it), with the raw`(sigil wasm dom)` clearly marked as the low-level escape hatch. Folded into thenaming question in F6.### F4 — JS→Sigil calls are stringly-typed `eval`---### F9 — SXML string-tag footgun renders silently wrongThe SXML layer (`(sigil wasm dom sxml)` and `(sigil web client)`) decides a listis an element only when `(symbol? (car list))`. If the tag is a **string**(`("div" (@ ...) "hi")`) there is no error: the list is treated as a contentsequence and every item, including the literal string `"div"`, is rendered as atext node. blackICE built tags as strings and the page showed `divdivblackICE…`with no diagnostic. This is easy to hit because everything else (classes,attribute names, text) is happily a string.**Suggestion.** Either accept string tags (string→symbol at the boundary) or warnwhen a list's head is a string in element position. Silent mis-rendering with noconsole output is the worst failure mode for a first-time author.### F10 — Missing macro import compiles clean, fails only at wasm load`define-struct` comes from `(sigil struct)`. blackICE's `level`/`game` used it butimported only `(sigil core)`. The build **compiled and linked with no error**;the failure surfaced only at runtime in the browser as`Error loading entrypoint: unbound variable 'define-struct'`. A missingmacro-providing import should be a compile-time error, not a load-time one — thecompiler clearly had enough information (the macro was unbound during expansion)to reject it.**Suggestion.** Make unresolved macro/binding references in a compiled module abuild error (or at least a warning), so the failure is caught at`sigil build` rather than after instantiation in the browser.## The zero-JS-wiring standard browser-app pattern (promotion spec)Goal (set by David): **an app author writes zero direct JS wiring.** blackICEpackage.sglmodified
(from-path dir: "../sigil" package: "sigil-stdlib") (from-path dir: "../sigil" package: "sigil-wasm-runtime") (from-path dir: "../sigil" package: "sigil-wasm-dom") (from-path dir: "../sigil" package: "sigil-browser")) (from-path dir: "../sigil" package: "sigil-browser") (from-path dir: "../sigil-web" package: "sigil-web")) tasks: (list (tasksrc/blackice/game.sglmodified
(define-library (blackice game) (import (sigil core) (sigil struct) (sigil math) (blackice grid) (blackice level))src/blackice/level.sglmodified
(define-library (blackice level) (import (sigil core) (sigil struct) (sigil math) (sigil string) (blackice grid))src/blackice/render.sglmodified
;;; (blackice render) - Game/menu state -> SXML, mounted into the DOM.;;;;;; This is the ONLY module in the rules+render stack that references the DOM;;; bridge. It builds an SXML tree from a `game` (or the level-select menu) and;;; mounts it under #app via sigil-wasm-dom, re-rendering the whole subtree once;;; This is the ONLY module in the rules+render stack that references the DOM.;;; It builds an SXML tree from a `game` (or the level-select menu) and hands it;;; to (sigil web client), whose web-client-update diffs the new tree against the;;; last one and patches only the changed DOM nodes (keyed reconciliation) once;;; per move. The tree is plain data, so swapping in a canvas/WebGL renderer;;; means replacing this file alone. Classes drive the neon-on-black styling in;;; styles.css; glyphs are ASCII for encoding safety.;;;;;; Element tags must be SYMBOLS: the SXML layer only treats a list as an element;;; when its head is a symbol, so the `el`/`el*` helpers string->symbol the tag.(define-library (blackice render) (import (sigil core) (blackice grid) (blackice level) (blackice game) (sigil wasm dom) (sigil wasm dom sxml)) (sigil web client)) (export render-game render-menu render-to-app) (begin ;; --- SXML construction helpers ----------------------------------------- ;; The SXML renderer only treats a list as an element when its head is a ;; symbol, so element tags must be symbols (not strings). (define (el tag cls . children) (cons tag (cons (list '@ (list 'class cls)) children))) (cons (string->symbol tag) (cons (list '@ (list 'class cls)) children))) (define (el* tag attrs . children) (cons tag (cons (cons '@ attrs) children))) (cons (string->symbol tag) (cons (cons '@ attrs) children))) (define (txt s) s) ;; --- mount -------------------------------------------------------------- ;; web-client-update diffs the new view against the previously rendered ;; tree and patches the DOM (keyed child reconciliation), so each move ;; touches only the cells that changed rather than rebuilding the subtree. (define (render-to-app sxml) (let ((app (wasm-dom-query-selector "#app"))) (when (> app 0) (wasm-dom-mount-sxml app sxml)))))) (web-client-update "#app" sxml))))