Skip to content

[Repo] affinescript-dom and affinescript-pixijs as production-ready migration prerequisite #56

Description

@hyperpolymath

Proposal

The affinescript-dom and affinescript-pixijs binding 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 %raw debt — exactly the pattern AffineScript was supposed to eliminate.

Source observation

idaptik's Main.res has %raw blocks for log/console (Console.log binding), error page rendering (Dom.replaceBody / Dom.div / Dom.h1 / Dom.p), exception introspection. The pilot's migration/main/StartupError.affine shows the typed-DOM target. The pilot only works if those bindings exist.

What "production-ready" means here

  • Covers the DOM/Pixi surface idaptik actually uses (~25 functions, can produce a checklist)
  • Has the affinescript-vite plugin wired to load them
  • Has tests that survive an idaptik build pass
  • Documented effects (Dom.foo : ... / {IO})
  • Stable API — won't churn as the language evolves

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.

Activity

  1. hyperpolymath commented on May 21, 2026

    @hyperpolymath
    OwnerAuthor

    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 (%raw blocks + @val/external declarations).

    Console (4) — pilot-used:

    • log(msg: String) — 22 call sites across pilot
    • error(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]) -> VNode
    • h1(attrs, content: String) -> VNode
    • p(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 by stdlib/Http.affine)

    Architectural finding: the pilot uses aspirational syntax

    The pilot writes import Dom / {IO}; and Console.log(...). The current language has neither:

    • Real module-import syntax is use Dom::{...}; (per lib/module_loader.ml)
    • Console does not exist; println is the builtin that lowers to console.log in JS/Deno backends (lib/codegen_deno.ml:136, lib/js_codegen.ml:62)
    • Effect IO is a v1 effect (lib/effect.ml:135), so / IO and / { IO } both work on extern signatures
    • Record-literal attrs {style: bg} parse as #{style: bg} (the # prefix is required; see tests/codegen/test_record_simple.affine); empty {} doesn't have a typed record-literal form

    The pilot files were drafted as targets — translating idaptik's .res to 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.affine is 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 declare module Dom;, so use 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.affine all live there with module Foo; headers, pub extern type opaque handles, pub extern fn primitives, and pub fn ergonomic wrappers.

    Recommendation: ship stdlib/Dom.affine + stdlib/Console.affine following the Vscode.affine layout, not extend affinescript-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;, VNode enum, opaque Element/Node extern types, ~12 externs (createElement, createTextNode, querySelector, appendChild, removeChild, replaceChildren, setAttribute, removeAttribute, setText, replaceBody, addEventListener — placeholder, str_eq), and pub wrappers text/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/info delegating to the existing println/eprintln builtins (which already lower to console.{log,error} on JS/Deno backends).
    • tests/codegen/dom_pilot_surface.affine: exercises every new pub fn and pub extern fn so CI's tools/run_codegen_wasm_tests.sh blocks regressions.

    PR 2 (browser-runtime wiring)

    • Backend lowerings for the Dom externs in lib/codegen_deno.ml and lib/js_codegen.ml (mirror the __as_httpFetch pattern).
    • 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.affine following 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 fn declarations + wrappers in stdlib/Dom.affine (or a sibling stdlib/Window.affine if 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 .affine imports. If use Dom::{...} resolution in Vite-bundled apps needs special handling, that's a affinescript-vite PR not a binding PR.
    • affinescript-dom/src/dom.affine reconciler: separate concern (INT-08: DOM reconciler in affinescript-dom #183/wasm codegen: for-in/while loop bodies never execute in compiled programs (pre-existing, untested) #255). Leaving alone; can be layered on top of stdlib/Dom.affine once that exists.
    • Record-literal attr ergonomic (#{style: ...}): deferred to follow-up; tuple-list attrs is what compiles today and is symmetric with Http.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 build rebuilds; tools/run_codegen_wasm_tests.sh is 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 in stdlib/ Just Works for use Foo::{...}.
    • Record literal syntax: #{field: value} (the # is mandatory).
    • Effect-row return syntax: -> T / IO (single) or -> T / { Net, Async } (multi).
  2. hyperpolymath commented on Jun 11, 2026

    @hyperpolymath
    OwnerAuthor

    Audit (2026-06-11)

    Surface-mapped the production state of affinescript-dom, stdlib/Pixi*.affine, and idaptik'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-dom affinescript-dom/src/dom.affine 11 FFI + vDOM reconciler (h/text/mount/render/reconcile) ✅ compile-clean 🔴 blocked by #255 (wasm-codegen for-in/while defect)
    Pixi stdlib/Pixi.affine 36 externs covering Application / Container / Sprite / Graphics / Text / Texture / Ticker ✅ ✅ exercised by tests/codegen-deno/pixi_smoke.affine
    PixiSound stdlib/PixiSound.affine 7 externs ✅ ✅ tests/codegen-deno/pixisound_smoke.affine
    PixiUI stdlib/PixiUI.affine 11 externs ✅ ✅ tests/codegen-deno/pixiui_smoke.affine
    affinescript-pixijs repo does 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 them Pixi::* from use stdlib::Pixi works the same as use affinescript-pixijs.

    Idaptik's actual demand vs. current supply

    DOM (migration/main/StartupError.affine is 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.res Point, 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.res sound (module-default) + Sound.t 12 decls 7 externs (matches the global-sound surface)
    PixiUI.res FancyButton, 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

    1. DOM: ship the 5-10 convenience constructors (Dom.div/Dom.h1/Dom.p/Dom.replaceBody/Dom.text + Dom.button/Dom.input for forms). Document the h(\"div\", attrs, children) -> Dom.div(attrs, children) mapping. Then unblock wasm codegen: for-in/while loop bodies never execute in compiled programs (pre-existing, untested) #255 — without that the binding remains compile-clean / runtime-dead.
    2. PixiJS: add the gap categories above. Roughly +30-40 new externs to stdlib/Pixi.affine, +5-10 to PixiSound.affine, +20-30 to PixiUI.affine. Categorised work items below.
    3. affinescript-vite wiring: the .affine -> wasm pipeline already exists for the dom package (affinescript-dom/package.json declares affinescript-vite as devDep). Same shape can ship for a pixi package; no new tooling needed.
    4. Tests: the tests/codegen-deno/pixi*_smoke.affine fixtures + 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.input as thin wrappers over h. ~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.res introduces that aren't in stdlib/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

    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).

  3. added
    enhancementNew capability or improvement to existing behaviour
    migrationPorting between languages or toolchains (e.g. -> AffineScript)
    scope:repoConfined to this repository
    on Aug 27, 2026
  4. hyperpolymath commented on Sep 21, 2026

    @hyperpolymath
    OwnerAuthor

    56-A + Pixi gap-fill on the branch: stdlib/Dom.affine, stdlib/Console.affine, affinescript-dom convenience constructors (div/h1/p/… + replaceBody); Pixi Point/Rectangle/Circle/tint/blend/hitArea/mask/BlurFilter/NineSlice/Assets/event coords; PixiUI ProgressBar/List/Input; PixiSound isPlaying/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.

  5. added a commit that references this issue on Sep 21, 2026
  6. hyperpolymath commented on Sep 21, 2026

    @hyperpolymath
    OwnerAuthor

    PR opened: #757 (feat/wasi-485-closeout → main).

  7. hyperpolymath commented on Sep 21, 2026

    @hyperpolymath
    OwnerAuthor

    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.

  8. hyperpolymath commented on Sep 21, 2026

    @hyperpolymath
    OwnerAuthor

    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.

  9. added a commit that references this issue on Sep 21, 2026
  10. hyperpolymath commented on Sep 21, 2026

    @hyperpolymath
    OwnerAuthor

    Remainder for the original #56 bar is in #759 (Bun-ESM only, aspirational StartupError fixtures, affinescript-vite scaffold, Canvas-style effect erasure).

    Expanded Pixi.res (~313 decls beyond the operational stdlib/Pixi.affine surface) is now #760 — not a close-blocker for this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvement to existing behaviourmigrationPorting between languages or toolchains (e.g. -> AffineScript)scope:repoConfined to this repository

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions