Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions affinescript-dom/e2e/dom_host.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MPL-2.0
// e2e host for dom_drive.wasm — Int-handle DOM, mutation log, assertions.
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';

let inst = null;
const readString = (ptr) => {
const dv = new DataView(inst.exports.memory.buffer);
const len = dv.getUint32(ptr, true);
return new TextDecoder('utf-8').decode(
new Uint8Array(inst.exports.memory.buffer, ptr + 4, len));
};

// handle 1 = #root
const nodes = new Map([[1, { tag: '#root', attrs: new Map(), children: [], text: null }]]);
let next = 2;
const log = [];
const mk = (n) => { const h = next++; nodes.set(h, n); return h; };

const env = {
dom_query_selector: (p) => { const s = readString(p); log.push(`query(${s})`); return s === '#root' ? 1 : 0; },
dom_create_element: (p) => { const t = readString(p); const h = mk({ tag: t, attrs: new Map(), children: [], text: null }); log.push(`createElement(${t})=#${h}`); return h; },
dom_create_text_node: (p) => { const s = readString(p); const h = mk({ tag: '#text', attrs: new Map(), children: [], text: s }); log.push(`createText("${s}")=#${h}`); return h; },
dom_append_child: (p, c) => { nodes.get(p).children.push(c); log.push(`append(#${p},#${c})`); },
dom_replace_child: (p, o, n) => { const cs = nodes.get(p).children; const i = cs.indexOf(o); assert.ok(i >= 0, `replace: #${o} not under #${p}`); cs[i] = n; log.push(`replace(#${p},#${o}->#${n})`); },
dom_remove_child: (p, c) => { const cs = nodes.get(p).children; const i = cs.indexOf(c); assert.ok(i >= 0, `remove: #${c} not under #${p}`); cs.splice(i, 1); log.push(`remove(#${p},#${c})`); },
dom_child_at: (p, i) => { const c = nodes.get(p).children[i] ?? 0; log.push(`childAt(#${p},${i})=#${c}`); return c; },
dom_set_attribute: (el, n, v) => { const name = readString(n), val = readString(v); nodes.get(el).attrs.set(name, val); log.push(`setAttr(#${el},${name}=${val})`); },
dom_remove_attribute: (el, n) => { const name = readString(n); nodes.get(el).attrs.delete(name); log.push(`removeAttr(#${el},${name})`); },
dom_set_text: (el, p) => { const s = readString(p); nodes.get(el).text = s; log.push(`setText(#${el},"${s}")`); },
dom_str_eq: (a, b) => (readString(a) === readString(b) ? 1 : 0),
};

const dump = (h, d = 0) => {
const n = nodes.get(h);
const attrs = [...n.attrs].map(([k, v]) => ` ${k}="${v}"`).join('');
const line = n.tag === '#text' ? `"${n.text}"` : `<${n.tag}${attrs}> #${h}`;
return [' '.repeat(d) + line, ...n.children.flatMap((c) => dump(c, d + 1))].join('\n');
};

const bytes = await readFile(process.argv[2]);

Check failure on line 41 in affinescript-dom/e2e/dom_host.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

LLMs running this code with faulty CLI arguments can escape file system restrictions. Refactor this code to validate the constructed path before accessing the file system.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_affinescript&issues=AZ88FQK3ZAi_HJFGn_SF&open=AZ88FQK3ZAi_HJFGn_SF&pullRequest=680
const { instance } = await WebAssembly.instantiate(bytes, {
env, wasi_snapshot_preview1: { fd_write: () => 0 },
});
inst = instance;

const ret = inst.exports.main();
console.log('main() =', ret);
console.log('--- mutation log ---'); console.log(log.join('\n'));
console.log('--- final DOM under #root ---'); console.log(dump(1));

// assertions: reconcile patched in place
const root = nodes.get(1);
assert.equal(root.children.length, 1, 'root has exactly one child');
const div = nodes.get(root.children[0]);
assert.equal(div.tag, 'div');
assert.equal(div.attrs.get('id'), 'app');
assert.equal(div.attrs.get('title'), 't2', 'title added by patch_attrs');
assert.ok(!div.attrs.has('class'), 'class removed by patch_attrs');
assert.equal(div.children.length, 1, 'span child removed by while-loop reconcile');
const t = nodes.get(div.children[0]);
assert.equal(t.tag, '#text');
assert.equal(t.text, 'world', 'text updated in place');
assert.equal(ret, root.children[0], 'reconcile returned the in-place div handle');
console.log('ALL ASSERTIONS PASS — reconciler ran end-to-end (loops executed)');
29 changes: 29 additions & 0 deletions affinescript-dom/e2e/driver_main.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MPL-2.0
// e2e driver `main` for the DOM reconciler. NOT standalone: `run.sh`
// concatenates `../src/dom.affine` + this file (codegen is single-pass in
// declaration order, so `main` must come after the functions it calls).
//
// Exercises every loop-bearing path (the #255 class): render's attr +
// children loops, patch_attrs' add + remove loops (via attr_has), and the
// `while` child-reconcile loop (in-place text patch at i=0, surplus-child
// removal at i=1).
//
// Affine detail: `mount` consumes its tree, so the reconcile call gets a
// freshly built copy — reusing `old_tree` would be a use-after-move type
// error. The discipline the module sells is load-bearing in its own test.
fn main() -> Int {
let old_tree = h("div", [("id", "app"), ("class", "old")],
[text("hello"), h("span", [], [text("x")])]);
let ok = mount("#root", old_tree);
if ok {
let parent = dom_query_selector("#root");
let old_node = dom_child_at(parent, 0);
let old_v = h("div", [("id", "app"), ("class", "old")],
[text("hello"), h("span", [], [text("x")])]);
let new_v = h("div", [("id", "app"), ("title", "t2")],
[text("world")]);
reconcile(parent, old_node, old_v, new_v)
} else {
0 - 1
}
}
24 changes: 24 additions & 0 deletions affinescript-dom/e2e/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# End-to-end runtime test for the DOM reconciler (INT-08 / #183).
# Concatenates src/dom.affine + driver_main.affine, compiles to core-WASM,
# runs it under Node against an Int-handle host DOM (dom_host.mjs), and
# asserts the mutation log + final tree. Exit 0 = the reconciler (including
# every for/while loop — the #255 class) executed correctly in compiled wasm.
#
# Requires: the compiler built (dune build → _build/default/bin/main.exe;
# override with AFFINESCRIPT_BIN=/path/to/main.exe)
# and Node ≥ 18 on PATH. Skips (exit 0, loud) if either is missing so the
# harness can sit in CI paths that lack the OCaml toolchain.
set -uo pipefail
cd "$(dirname "$0")"
REPO="$(cd ../.. && pwd)"
BIN="${AFFINESCRIPT_BIN:-$REPO/_build/default/bin/main.exe}"
[ -x "$BIN" ] || { echo "SKIP: compiler not built ($BIN missing) — run dune build"; exit 0; }

Check failure on line 17 in affinescript-dom/e2e/run.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_affinescript&issues=AZ88FQHiZAi_HJFGn_SE&open=AZ88FQHiZAi_HJFGn_SE&pullRequest=680
command -v node >/dev/null 2>&1 || { echo "SKIP: node not on PATH"; exit 0; }

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
cat ../src/dom.affine driver_main.affine > "$TMP/dom_drive.affine"
"$BIN" compile "$TMP/dom_drive.affine" -o "$TMP/dom_drive.wasm"
node dom_host.mjs "$TMP/dom_drive.wasm"
13 changes: 7 additions & 6 deletions affinescript-dom/src/dom.affine
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
*
* GATE: this module compiles end-to-end (resolve → typecheck → codegen
* → wasm), the same bar as the Stage-C stdlib AOT suite. RUNTIME is
* blocked by #255 — a pre-existing wasm-codegen defect where `for-in` /
* `while` loop bodies never execute in the compiled module (so
* `vnode_len`, the attr loops, and the child reconcile loop iterate
* zero times). The reconciler logic here is correct AffineScript; it
* will run once #255 lands. No runtime e2e harness is shipped until
* then (a harness that cannot pass would be dishonest).
* verified end-to-end (2026-07-07): #255 — the wasm loop-codegen defect
* that blocked it — was fixed in PR #257 (closed 2026-05-19), and
* `../e2e/run.sh` now drives this reconciler against a real Int-handle
* host DOM: mount, in-place attr patch (add + remove), in-place text
* update, and surplus-child removal, with the mutation log asserted.
* `vnode_len`, the attr loops, and the `while` child-reconcile loop all
* execute in the compiled wasm.
*
* Codegen note: AffineScript codegen is single-pass in source
* declaration order (lib/codegen.ml `func_indices`), so a function may
Expand Down
23 changes: 13 additions & 10 deletions docs/ECOSYSTEM.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,10 @@ The contract is *narrower than older prose claimed* and is exactly this:
(non-parsing 39-line stub) renamed to canonical `src/dom.affine` and
replaced with a real VDOM + render + mount + minimal-mutation
reconciler that compiles end-to-end. The #255 wasm loop-codegen defect that
gated its runtime is *FIXED* (PR #257; issue closed 2026-05-19), so the
runtime is no longer blocked by it; end-to-end DOM runtime remains to be
re-verified.
gated its runtime is *FIXED* (PR #257; issue closed 2026-05-19), and the
runtime is *VERIFIED end-to-end* (2026-07-07): `affinescript-dom/e2e/run.sh`
drives mount + in-place attr patch + text update + surplus-child removal
against a real Int-handle host DOM, mutation log asserted.

|`affinescript-pixijs` |skeleton |Migration-prerequisite scaffold (#56).

Expand All @@ -170,8 +171,9 @@ canonical `affinescript tea-bridge` + a re-entrancy fixture.
shipped + closed 2026-05-31 as `packages/affine-js/loader.js` — already
host-agnostic (Deno/Node/browser parity). Whether the satellite repo
still earns its keep (vs. folding into `affine-js`) is the open question
in #489; revisit when INT-08 reconciler runtime (#183) is re-verified (#255
loop-codegen fixed via #257) and dictates any DOM-specific loader surface.
in #489; INT-08 reconciler runtime (#183) is verified end-to-end 2026-07-07
(`affinescript-dom/e2e/run.sh`; #255 fixed via #257) — revisit when it
dictates any DOM-specific loader surface.

|`affinescript-cadre` |scaffold |Was imaginary until #175. Router/navigation
satellite (internal `lib/tea_router.ml` contract exists).
Expand Down Expand Up @@ -271,17 +273,18 @@ cleared (#253). Router/nav = separate INT-09
|INT-08 |DOM reconciler in `affinescript-dom` |#183 |reconciler
implemented + compiles (resolve→typecheck→codegen→wasm); `.as`→`.affine`
corrected. INT-02 dep cleared. #255 (wasm loop-codegen defect) that gated
the runtime is FIXED (PR #257, closed 2026-05-19); runtime to be re-verified
end-to-end
the runtime is FIXED (PR #257, closed 2026-05-19); runtime VERIFIED
end-to-end 2026-07-07 via `affinescript-dom/e2e/run.sh` (mount + attr
patch + text update + child removal, mutation log asserted)
|INT-09 |`affinescript-cadre` router/navigation runtime |ledger-only
|planned (blocked by INT-07)
|INT-10 |LSP distribution (`affinescript-lsp`) |#282 |unblocked —
distribution decided (ADR-019: Releases + thin Deno/JSR shim, #260).
Consumes the shim once #260 S2/S3 land
|INT-11 |Browser host parity (DOM loader + reconciler end-to-end) |
ledger-only |planned (INT-02 dep cleared 2026-05-31 via #179; #255 that
gated INT-08 runtime is fixed via #257 — pending INT-08 runtime
re-verification). Satellite-repo question = #489.
ledger-only |planned (INT-02 dep cleared 2026-05-31 via #179; INT-08
runtime verified end-to-end 2026-07-07 under Node — browser-host parity
is the remaining leg). Satellite-repo question = #489.
|INT-12 |typed-wasm convergence: AffineScript-emitted fixtures into the
typed-wasm cross-compat suite (closes the Stage-E runway) |ledger-only |
planned (Stage E; coordinates with `hyperpolymath/typed-wasm` C5.1)
Expand Down
3 changes: 2 additions & 1 deletion docs/TECH-DEBT.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,8 @@ licence in root `LICENSE`).
(TeaApp/parseTeaLayout, Linear-msg enforced); INT-01 cleared (#253)
|INT-08 |DOM reconciler |S2 |#183 implemented + compiles; `.as`→`.affine`
fixed; #255 (wasm loop-codegen defect) that gated the runtime is fixed
(PR #257, closed 2026-05-19) — runtime to be re-verified end-to-end
(PR #257, closed 2026-05-19) — runtime VERIFIED end-to-end 2026-07-07 via
`affinescript-dom/e2e/run.sh`
|INT-10 |`affinescript-lsp` distribution |S2 |**DONE end-to-end live**
(#282, ADR-019 S4): LSP resolves the compiler via `AFFINESCRIPT_COMPILER`
→ `affinescript` on `PATH` → the `@hyperpolymath/affinescript` shim
Expand Down
6 changes: 3 additions & 3 deletions docs/bindings-roadmap.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ no further significant ReScript → AffineScript work is tractable.
|idaptik `src/app/multiplayer/PhoenixSocket.res` + LobbyManager; needed for any multiplayer / sync-server feature.

|7
|*DOM* — re-verify the reconciler runs end-to-end (the `for-in` / `while` codegen gap, issue #255, is fixed)
|`●` / `◯` #255-fixed; re-verify runtime
|*DOM* — reconciler runs end-to-end (verified 2026-07-07, `affinescript-dom/e2e/run.sh`; the `for-in` / `while` codegen gap, issue #255, is fixed)
|`●` verified
|`affinescript-dom`
|187-line virtual-DOM + reconciler exists and compiles. The wasm loop-codegen defect (#255) that blocked its runtime is *fixed* (PR #257, issue closed 2026-05-19); end-to-end runtime remains to be re-verified. *Was not a binding gap — a codegen bug, now resolved.*
|187-line virtual-DOM + reconciler exists, compiles, and *runs*: verified end-to-end 2026-07-07 (`affinescript-dom/e2e/run.sh` — mount, attr patch, text update, surplus-child removal against an Int-handle host DOM, mutation log asserted). The wasm loop-codegen defect (#255) that blocked it was fixed in PR #257. *Was not a binding gap — a codegen bug, now resolved and runtime-proven.*

|8
|*WebGL / Canvas2D context* (HTMLCanvasElement, getContext, drawImage, fillRect, transform stack)
Expand Down
Loading