Repository navigation
[Repo] affinescript-dom and affinescript-pixijs as production-ready migration prerequisite #56
Description
Activity
Scoping comment — survey + delivery plan
Surveyed the binding surface and the surrounding language/codebase before writing code. Posting findings so the next session (or anyone picking this up) can resume cold.
Inventory: 31 typed-binding functions
Derived from idaptik's
migration/main/pilot (Boot/Main/StartupError/MultiplayerHandlers) + the 542-file pre-migration ReScript surface (%rawblocks +@val/externaldeclarations).Console (4) — pilot-used:
log(msg: String)— 22 call sites across piloterror(label: String, err: Exn)— legacy only (Main.res error handler)- (plus warn/info convenience pair)
Core DOM (4) — pilot-used:
replaceBody(v: VNode)div(attrs, children: [VNode]) -> VNodeh1(attrs, content: String) -> VNodep(attrs, content: String) -> VNode
Window/Browser (11) — legacy only, future migration slices:
addEventListener/removeEventListener(8+ screen files)matchMedia(DomA11y.res)open(url, target)(MainHubScreen.openExternal)innerWidth/innerHeight(Resize.res)devicePixelRatio(GetResolution.res)setTimeout/setInterval/clearInterval(PhoenixSocket, UPSDevice, ServerDevice)
Document (7) — legacy only:
createElement,body.{appendChild,removeChild,firstChild,innerHTML},getElementById
Utility browser APIs (5) — legacy only:
Date.now(),Number.{isFinite,isNaN},btoa,fetch(the last already covered bystdlib/Http.affine)
Architectural finding: the pilot uses aspirational syntax
The pilot writes
import Dom / {IO};andConsole.log(...). The current language has neither:- Real module-import syntax is
use Dom::{...};(perlib/module_loader.ml) Consoledoes not exist;printlnis the builtin that lowers toconsole.login JS/Deno backends (lib/codegen_deno.ml:136,lib/js_codegen.ml:62)- Effect
IOis a v1 effect (lib/effect.ml:135), so/ IOand/ { IO }both work on extern signatures - Record-literal attrs
{style: bg}parse as#{style: bg}(the#prefix is required; seetests/codegen/test_record_simple.affine); empty{}doesn't have a typed record-literal form
The pilot files were drafted as targets — translating idaptik's
.resto what the migration assistant (#57) should eventually emit. They are not currently buildable. That's fine; this issue is what makes them buildable.Architectural finding: where the binding lives
The existing
affinescript-dom/src/dom.affineis the #183 virtual-DOM reconciler — a different concern. Its API (h(tag, attrs, kids),mount,reconcile) is a higher-level layer; its file-level comment notes runtime is gated on #255. It does not declaremodule Dom;, souse Dom::{...}won't find it.The canonical pattern for typed-binding packages in this repo is
stdlib/*.affine:Http.affine,Vscode.affine,Sqlite.affine,Crypto.affine,Grammy.affineall live there withmodule Foo;headers,pub extern typeopaque handles,pub extern fnprimitives, andpub fnergonomic wrappers.Recommendation: ship
stdlib/Dom.affine+stdlib/Console.affinefollowing the Vscode.affine layout, not extendaffinescript-dom/src/dom.affine. The reconciler can later be layered on top of the canonical Dom module (or remain the higher-level package surface that depends on it). This also avoids forking the VNode type.Delivery plan — 4 PRs
Sequential; each lands a coherent slice.
PR 1 (this issue, idaptik-pilot surface) ≈ 200 LOC
stdlib/Dom.affine:module Dom;,VNodeenum, opaqueElement/Nodeextern types, ~12 externs (createElement, createTextNode, querySelector, appendChild, removeChild, replaceChildren, setAttribute, removeAttribute, setText, replaceBody, addEventListener — placeholder, str_eq), and pub wrapperstext/div/h1/h2/h3/p/span/a/button/img/ul/li/render/mount/replaceBody. Attrs as[(String, String)](Http-headers shape); record-literal ergonomic deferred to a follow-up once#{...}lands as widely-supported attrs.stdlib/Console.affine:module Console;,pub fn log/warn/error/infodelegating to the existingprintln/eprintlnbuiltins (which already lower toconsole.{log,error}on JS/Deno backends).tests/codegen/dom_pilot_surface.affine: exercises every newpub fnandpub extern fnso CI'stools/run_codegen_wasm_tests.shblocks regressions.
PR 2 (browser-runtime wiring)
- Backend lowerings for the Dom externs in
lib/codegen_deno.mlandlib/js_codegen.ml(mirror the__as_httpFetchpattern). - Adds a Deno harness so PR 1's compile-pass test also runs end-to-end against a jsdom-style shim.
PR 3 (Pixi binding)
stdlib/Pixi.affinefollowing the same shape, scoped to the surface idaptik's Pixi usage actually touches. Inventory pending — first migration slice that depends on Pixi will reveal it.
PR 4 (legacy/event/timer surface)
- Add the 11 Window + 7 Document + 5 utility entries above as additional
pub extern fndeclarations + wrappers instdlib/Dom.affine(or a siblingstdlib/Window.affineif size warrants splitting). Driven by demand: each later idaptik migration slice that hits a missing binding adds it here.
What's deferred and why
- Vite plugin wiring (
affinescript-vite): the binding doesn't need the plugin to work for the compile-pass test, and the plugin's compile loop already handles arbitrary.affineimports. Ifuse Dom::{...}resolution in Vite-bundled apps needs special handling, that's aaffinescript-vitePR not a binding PR. affinescript-dom/src/dom.affinereconciler: separate concern (INT-08: DOM reconciler in affinescript-dom #183/wasm codegen:for-in/whileloop bodies never execute in compiled programs (pre-existing, untested) #255). Leaving alone; can be layered on top ofstdlib/Dom.affineonce that exists.- Record-literal attr ergonomic (
#{style: ...}): deferred to follow-up; tuple-list attrs is what compiles today and is symmetric withHttp.affine's header convention. affinescript-pixijs: PR 3, separate slice. The pilot doesn't use Pixi.
Notes for whoever picks this up
- Compiler is built at
_build/default/bin/main.exe;dune buildrebuilds;tools/run_codegen_wasm_tests.shis the codegen-test driver. - v1 effects:
IO, Async, Partial, Throws, Mut(lib/effect.ml:135). Reserved:Random, Time, Net. - Module resolution order: current_dir →
$AFFINESCRIPT_STDLIB(default./stdlib) → search_paths. Putting bindings instdlib/Just Works foruse Foo::{...}. - Record literal syntax:
#{field: value}(the#is mandatory). - Effect-row return syntax:
-> T / IO(single) or-> T / { Net, Async }(multi).
- added 7 commits that reference this issue
on May 26, 2026 Audit (2026-06-11)
Surface-mapped the production state of
affinescript-dom,stdlib/Pixi*.affine, andidaptik's actual binding usage to scope what "production-ready" looks like and what blocks it today.State of in-repo binding packages
Package Location Surface (decls) Build state Runtime state affinescript-domaffinescript-dom/src/dom.affine11 FFI + vDOM reconciler ( h/text/mount/render/reconcile)✅ compile-clean 🔴 blocked by #255 (wasm-codegen for-in/whiledefect)Pixi stdlib/Pixi.affine36 externs covering Application / Container / Sprite / Graphics / Text / Texture / Ticker ✅ ✅ exercised by tests/codegen-deno/pixi_smoke.affinePixiSound stdlib/PixiSound.affine7 externs ✅ ✅ tests/codegen-deno/pixisound_smoke.affinePixiUI stdlib/PixiUI.affine11 externs ✅ ✅ tests/codegen-deno/pixiui_smoke.affineaffinescript-pixijsrepodoes not exist — — — "affinescript-pixijs as a separate repo" never landed; the bindings live inside
stdlib/as three flat namespaces. That's a delivery shape, not a missing repo — calling themPixi::*fromuse stdlib::Pixiworks the same asuse affinescript-pixijs.Idaptik's actual demand vs. current supply
DOM (
migration/main/StartupError.affineis the only consumer today; src/Main.res is the future migration target)Demanded surface (high-level convenience constructors):
Dom.replaceBody(node) Dom.div(attrs, children) Dom.h1(attrs, text) Dom.p(attrs, text)Supplied surface (low-level vDOM):
text(content), h(tag, attrs, children), mount(selector, vnode)Gap: convenience constructors. ~5-10 LoC each, trivial wrappers over
h. Pure ergonomic addition; no host-side work.Real blocker: wasm runtime defect #255 — until that ships, even a perfectly populated DOM binding compiles but does not execute. Closing #56 ahead of #255 buys nothing.
PixiJS (src/bindings/Pixi.res / PixiSound.res / PixiUI.res — these are the bindings idaptik already had under ReScript; the migration must preserve them)
Source binding Modules Approx ReScript decls Affinescript covers Pixi.resPoint, Circle, Rectangle, ObservablePoint, Texture, Container, NineSliceSprite, BlurFilter, Ticker, FederatedPointerEvent, BigPool, Assets, Extensions, ExtensionType, ResizePlugin, Renderer, CanvasStyle, Application 313 decls Application / Container / Sprite / Graphics / Text / Texture / Ticker (~36 fns) PixiSound.ressound (module-default) + Sound.t 12 decls 7 externs (matches the global-sound surface) PixiUI.resFancyButton, Slider, ProgressBar, List, Input 59 decls (with rich constructor options) 11 externs Significant gaps to close before idaptik app-layer can compile:
- Point / Circle / Rectangle (hit areas)
- ObservablePoint (anchors/pivots)
- NineSliceSprite (9-slice texture)
- Filters: BlurFilter (+ general filter trait)
- FederatedPointerEvent (touch/mouse event payload)
- Assets (asset loader —
Assets.load,Assets.cache) - BigPool (object pooling)
- Container: filters / mask / hitArea / cacheAsBitmap / culling
- Sprite: tint, blendMode, anchor.x/y, beyond
from
What "production-ready" should mean for closing #56
- DOM: ship the 5-10 convenience constructors (
Dom.div/Dom.h1/Dom.p/Dom.replaceBody/Dom.text+Dom.button/Dom.inputfor forms). Document theh(\"div\", attrs, children)->Dom.div(attrs, children)mapping. Then unblock wasm codegen:for-in/whileloop bodies never execute in compiled programs (pre-existing, untested) #255 — without that the binding remains compile-clean / runtime-dead. - PixiJS: add the gap categories above. Roughly +30-40 new externs to
stdlib/Pixi.affine, +5-10 toPixiSound.affine, +20-30 toPixiUI.affine. Categorised work items below. - affinescript-vite wiring: the
.affine-> wasm pipeline already exists for the dom package (affinescript-dom/package.jsondeclaresaffinescript-viteas devDep). Same shape can ship for a pixi package; no new tooling needed. - Tests: the
tests/codegen-deno/pixi*_smoke.affinefixtures + harnesses already exist and pass — the model is in place. Each new extern wants a 2-line addition.
Recommended scope-down — split #56 into 3 actionable sub-issues
56-A: DOM convenience layer (small) — add
Dom.div / Dom.h1 / Dom.p / Dom.replaceBody / Dom.text / Dom.button / Dom.inputas thin wrappers overh. ~50 LoC, one PR. Gated on #255 for runtime acceptance, but the static layer can land independently.56-B: PixiJS Tier-1 gap-fill — Point / Circle / Rectangle / ObservablePoint / FederatedPointerEvent / BlurFilter / NineSliceSprite / Assets. These are the categories idaptik's
Pixi.resintroduces that aren't instdlib/Pixi.affine. Estimate: ~50-80 new externs, 2-3 PRs.56-C: PixiUI / PixiSound gap-fill — round out FancyButton / Slider / ProgressBar / List / Input constructor options + the small PixiSound deltas. ~30 new externs, one PR each.
This is also the natural home for #450 Tier-1 items 1-3 (PixiJS core / PixiJS sound / PixiJS UI) — those map directly onto 56-B / 56-C. Item 7 (DOM) maps onto 56-A. So #56 closure ⟹ four #450 Tier-1 items close in one swing.
Decisions for the owner
- Promote
stdlib/Pixi*.affineinto a separateaffinescript-pixijsworkspace package (parallel toaffinescript-dom), or keep them in stdlib? Cost of split is small; benefit is independent versioning. - Accept the 56-A / 56-B / 56-C scope-down, or keep [Repo] affinescript-dom and affinescript-pixijs as production-ready migration prerequisite #56 as the umbrella with sub-issues opened directly?
I'm not opening sub-issues unilaterally pending those calls. When you OK the structure I can file 56-A / 56-B / 56-C in one batch and start landing PRs against them.
(No code change in this comment — pure audit + scope proposal.)
Filed during the language-priority queue work; pairs with PR #551 (#470 + #478 codegen polish).
- addedenhancementNew capability or improvement to existing behaviourNew capability or improvement to existing behaviourmigrationPorting between languages or toolchains (e.g. -> AffineScript)Porting between languages or toolchains (e.g. -> AffineScript)scope:repoConfined to this repositoryConfined to this repository
on Aug 27, 2026 56-A + Pixi gap-fill on the branch:
stdlib/Dom.affine,stdlib/Console.affine,affinescript-domconvenience constructors (div/h1/p/… +replaceBody); Pixi Point/Rectangle/Circle/tint/blend/hitArea/mask/BlurFilter/NineSlice/Assets/event coords; PixiUI ProgressBar/List/Input; PixiSoundisPlaying/duration.Full idaptik 215-file surface + record-literal attrs remain this issue's remainder. Index #175 closed independently.
Landed on
wasi-485-closeout(main...feat/wasi-485-closeout). Index #175 closed.PR opened: #757 (
feat/wasi-485-closeout→main).56-A + Pixi gap-fill landed in #757:
stdlib/Dom.affine,stdlib/Console.affine, affinescript-dom convenience constructors, Pixi geometry/filters/nine-slice/assets/event coords, PixiUI ProgressBar/List/Input, PixiSound isPlaying/duration.Leaving this issue open for the remaining idaptik surface / record-literal attrs.
PR 4 (Window/Document/utility) + Sonar ci.yml pin: #758
Leaving this issue open for record-literal attrs (
#{style: ...}) and the rest of idaptik's Pixi.res decls.- added a commit that references this issue
on Sep 21, 2026
Proposal
The
affinescript-domandaffinescript-pixijsbinding packages need to be production-ready, not work-in-progress, before idaptik (and presumably other ecosystem repos) can start app-layer .res→.affine migration. Without typed bindings every migration accumulates new%rawdebt — exactly the pattern AffineScript was supposed to eliminate.Source observation
idaptik's Main.res has
%rawblocks for log/console (Console.log binding), error page rendering (Dom.replaceBody / Dom.div / Dom.h1 / Dom.p), exception introspection. The pilot'smigration/main/StartupError.affineshows the typed-DOM target. The pilot only works if those bindings exist.What "production-ready" means here
Dom.foo : ... / {IO})Companion
Pairs with the "%raw decision tree" Human Guide issue — that decision tree's recommended option ("use the typed binding") is only sound if the binding exists.
Filed from idaptik Wave 3 pilot. See LESSONS.md and PILOT.md. Idaptik commit hyperpolymath/idaptik@aef38db.