diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 031fe56..64eed60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,13 +5,21 @@ # means on a workstation. New gates belong in the justfile, not here; this # file only supplies a machine to run them on. # -# Why the build steps below are not optional: the two component-loading -# suites (host/tests/counter_test.ts, host/tests/fullstack_test.ts) THROW -# when their .wasm is missing rather than skipping it, so dropping a build -# step turns the suite red rather than quietly green. The one silent-skip -# in the suite is host/tests/decoder_test.ts's "golden vectors pending", -# which fires only if vectors/ is empty — it is committed, so the -# cross-language check against the Rust encoder always runs. +# Why the build steps below are not optional: the component-loading suites +# (host/tests/counter_test.ts, host/tests/fullstack_test.ts, +# host/tests/operations_test.ts) THROW when their .wasm is missing rather +# than skipping it, so dropping a build step turns the suite red rather +# than quietly green. +# +# There is no longer a cross-language golden-vector gate to run. It existed +# because the mutation wire format was implemented twice by hand (a Rust +# encoder and a TypeScript decoder) and nothing but those vectors held the +# two in agreement. The format is now an explicit WIT schema +# (`interface mutations`) with bindgen owning both sides, so the vectors, +# their generator and the TypeScript decoder are all deleted. What replaces +# them as the end-to-end net is host/tests/operations_test.ts, which asserts +# literal DOM output from a real mounted component — hence the counter build +# step below being load-bearing rather than a nicety. # # Everything is version-pinned, matching how the rest of the repo pins # (POLYENGINE_REV in the justfile, `=0.7.10` dioxus, `=0.60.0` @@ -89,6 +97,7 @@ jobs: - name: Build counter example component run: just example counter - # cargo test (incl. the golden-vector encoder check) + deno task test - # (incl. the same vectors against the TypeScript decoder). + # cargo test (the interner unit tests; the writer names the generated + # bindings and so is wasm32-only, covered host-side instead) + + # deno task test. - run: just test diff --git a/Cargo.lock b/Cargo.lock index 0ae02a0..41b8c47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1538,7 +1538,6 @@ dependencies = [ "dioxus-core-types", "dioxus-html", "rustc-hash 2.1.3", - "serde_json", "wit-bindgen 0.60.0", ] diff --git a/Cargo.toml b/Cargo.toml index 39bd61b..34e0a17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ dioxus-html = { version = "=0.7.10", default-features = false } rustc-hash = "2" [dev-dependencies] -serde_json = "1" # Native-only: `rsx!` for driving a real VirtualDom through the writer in unit # tests. Dev-dependencies do not enter the wasm32 component build. dioxus = { version = "=0.7.10", default-features = false, features = [ diff --git a/bench/README.md b/bench/README.md index f873b20..e56eab3 100644 --- a/bench/README.md +++ b/bench/README.md @@ -2,9 +2,10 @@ Tracks absolute row-operation throughput on the pinned polyengine (`justfile`'s `POLYENGINE_REV`), Deno + linkedom (host-side DOM), a real -Dioxus component (`examples/bench-rows`), over the stream transport (the +Dioxus component (`examples/bench-rows`), over the mutation stream (the only transport — see "Transport A/B (historical)" below for why the call -transport was retired): +transport was retired, and "Channel A/B (historical)" below for why an +earlier hand-rolled byte protocol was retired too). The operations are js-framework-benchmark-style row operations (create/append/update/swap/ remove/clear), so there's a baseline to track regressions against and to eventually compare with dioxus-web's published characteristics. @@ -128,7 +129,239 @@ run for the same reason. (or `FrameDecoder`) constructor hook, which is a `host/src` change — outside this track's territory. This is a gap for whoever owns `host/src` to close if the byte/batch columns are wanted later. This - bench therefore reports **wall-clock ms only**. + bench therefore reports **wall-clock ms only**. (The Channel A/B below + needed op *counts* and got them by patching each mounted applier's + `OpSink` methods in a throwaway script — enough for a one-off number, + still not a standing column.) + +## Channel A/B (historical): byte protocol vs explicit WIT schema + +**Retired.** `polymorph:dioxus` used to ship two mutation channels on the +same stream transport: a hand-rolled byte encoding on `run` and an explicit +WIT schema (`interface mutations` / `stream`) on `run-typed`. +The A/B below is what closed that question: the owner decided to keep the +typed channel and delete the byte one (recorded in `wit/world.wit` — `run` +now returns `stream` directly, and there is only one export). +This section preserves the measurement and reasoning that closed the +question, for the record; the "byte" column below no longer exists in this +tree. + +The byte format was documented normatively in `wit/world.wit`'s `run` doc +comment: opcodes, operand widths and framing all live in prose, and the +encoder (formerly `src/protocol.rs`) and decoder (formerly +`host/src/decoder.ts`) were two independent hand-written implementations of +it that could only be kept in agreement by golden vectors and review. The +obvious maintainability alternative was to spell the vocabulary as WIT — +records plus one `operation` variant — and ship `stream` +instead of `stream`, letting bindgen own both sides. + +`wit/world.wit`'s `interface mutations` and the (then-separate) `run-typed` +export were that alternative, built so the two could be measured against +each other. Both channels were compiled into the same component (two +exports); `mountApp`'s `channel` option picked one, and both fed the +identical `DomApplier`, so the DOM work was common and the delta was purely +encode/transport/decode. `host/tests/typed_test.ts` (since replaced by an +absolute-assertion test, its differential reference implementation having +been retired) asserted the two produced identical DOM for the same +interaction sequence — without that, the numbers below would have meant +nothing. + +### What the schema costs before you measure anything + +**WIT forbids recursive type definitions.** `register-template`'s node +grammar is a tree, and + +```wit +record template-element { children: list } +variant template-node { element(template-element), ... } +``` + +is rejected outright: "type `template-node` depends on itself". The +typed schema therefore carries the template as an *arena* — a flat +`nodes` list plus `u32` indices in `roots` and `children` — which is a +strictly weaker encoding than the byte format's self-delimiting +recursive grammar: it admits out-of-range and cyclic index graphs that +the byte grammar could not express, so `applyOperations` (formerly +`applyTyped`) needs explicit validation the byte decoder never needed +(`host/src/operations.ts`'s `rehydrateTemplateArena`). That is a real dent +in the maintainability case, independent of speed, and it did not go away +when the byte channel was retired — it is the cost of the schema that +remains. + +The typed channel also gave up `readDirect`: polyengine's zero-copy +direct-read session is `stream` only, so the host read with ordinary +`read()` and paid a copy per batch. It did **not** reintroduce the +host-retention hazard that killed the call transport (see the historical +section below): that rule licenses quiescence on a retained end, a parked +operation, *or* an unfinished pump (.deps/polyengine/contracts/ +embedder-api.md §"Streams and futures", issue #162), and the host holds the +lifted readable end for the instance's lifetime, so the guest scheduler's +persistent park was exactly as legal here as on the byte channel. No +deadlock trap fired in any run. + +Third, `use mutations.{operation}` in the world makes the interface an +import of every component built against it — including components that +only ever called `run`. The host supplies nothing (the interface has no +items), but the byte channel's own component type changed to add a typed +channel it did not use — moot now that there is only one channel. + +### Measured + +Read the *ratio* column, not the absolute values. Op counts are +**instrumented**, not estimated: a counting `OpSink` wrapped around each +mounted applier during a one-off run recorded the `OpSink` calls in each +timed window. Both channels produced identical counts for all seven +operations, which is an independent check on the equivalence test. + +These numbers are against **polyengine 22b5d3d**, which carries the four +optimization PRs that came out of the finding below +([#263](https://github.com/polymorph-components/polyengine/pull/263) +layout-node cache, +[#264](https://github.com/polymorph-components/polyengine/pull/264) +embedder adapter tables, +[#265](https://github.com/polymorph-components/polyengine/pull/265) +flatten-count memoization, +[#270](https://github.com/polymorph-components/polyengine/pull/270) +variant kind/value). The "before" block further down is against +9e17dc9, which does not. The bump is in the justfile's +`POLYENGINE_REV`, and it is the single biggest input to this table — +do not compare rows across it. + +Deltas below are medians of three full runs. + +| op | ops in the timed window | typed − bytes | per op | +| --- | --- | --- | --- | +| create-10k | 90 006 | +70 ms | 0.78 µs | +| append-1k | 9 002 | +7.0 ms | 0.77 µs | +| create-1k | 9 006 | +6.3 ms | 0.70 µs | +| clear | 10 002 | +1.5 ms | 0.15 µs | +| update-every-10th | 101 | — | at the noise floor | +| swap-rows | 4 | — | at the noise floor | +| remove-row | 2 | — | at the noise floor | + +**The typed channel costs ~0.7-0.8 µs per operation** on the ops whose +mix is dominated by multi-field records with strings, and ~0.15 µs/op on +`clear`, which is almost entirely `remove` — a variant arm carrying a +bare `u32`. As before, one per-op figure reproduces the whole table, and +the ratio column is really measuring how op-heavy each operation is. + +The three small ops (2 to 101 operations in the window) are reported for +completeness only. They swing by more than the effect being measured — +`create-1k` read 0.74x in one of the three runs, i.e. the typed channel +apparently *faster*, which is the bytes column bouncing between 9.5 and +24.3 ms across runs, not a real result. Nothing below the op-heavy four +rows should be read at all. + +### Before the runtime fix (polyengine 9e17dc9) + +| op | ops | typed − bytes | per op | ratio then | ratio now | +| --- | --- | --- | --- | --- | --- | +| create-10k | 90 006 | +437 ms | 4.9 µs | 5.80x | ~1.8x | +| create-1k | 9 006 | +49 ms | 5.5 µs | 4.99x | ~1.6x | +| append-1k | 9 002 | +47 ms | 5.2 µs | 1.59x | ~1.1x | +| clear | 10 002 | +41 ms | 4.1 µs | 12.02x | ~1.4x | + +**Four upstream commits took the typed channel's per-operation cost down +by 6-27x.** `clear` moved most because it was the purest measure of the +per-op cost — nearly no DOM work to dilute it, and the cheapest possible +variant arm paying the full type-tree walk anyway. + +Kept as a dated block rather than overwritten: the before/after is what +makes either column mean anything. + +### On the noise floor, and this file's own guardrail + +The "Interpretation guardrails" section below says a >2x run-to-run +delta on any operation is a bug lead, not a result. This table trips it +repeatedly on the small ops and once on `create-1k`'s bytes column +(9.51 / 12.90 / 24.32 ms across three runs, no code change). Discharging +rather than ignoring it: + +- The byte channel's code is unchanged by this spike. `Interner` was + refactored (`intern_raw` extracted) with `intern` kept as a wrapper, + and `mountApp` was restructured around a `channel` option with the + byte branch moved verbatim under an `else`. The golden byte vectors + (`cargo test --test vectors`) still match. +- The box is shared and busy; the swing is present with and without any + change under test. + +So: box noise. It also means **only the op-heavy four rows carry +signal**, and no ratio here should be read past its first digit. + +### Where the remaining cost is + +The finding that produced the upstream fix, and what is left after it. +Reported as +[polyengine#261](https://github.com/polymorph-components/polyengine/issues/261). + +Several lift strategies run against the same guest memory at the same +rendezvous, interleaved in one timed loop so none is measured under a +different JIT state, each asserted element-by-element to produce the +same values as the interpreter and the same sink calls as +`host/src/operations.ts` (then `host/src/typed.ts`): + +| strategy | ns/element, 9e17dc9 | ns/element, 22b5d3d | +| --- | --- | --- | +| the runtime's own `load()` + `toHost` | ~4080 | ~675-963 | +| compiled lift — closure tree per type, identical JS values | ~232 | ~231-267 | +| compiled visitor — operands passed as arguments | ~127 | ~132-181 | +| the byte decoder here, same sink | ~14 | ~22-31 | + +Upstream's caching closed most of the gap. What remains is that caching +a walk is not the same as not walking: a **compiled lift** — walk the +type descriptor once, build a tree of closures, and every field offset, +`camelCase` name and variant case index becomes a constant — is still +~3x faster than the cached interpreter, needs no `eval`, no emitted +modules and no contract change. A **visitor-shaped read**, handing +operands to a per-case callback instead of materialising a +`{kind, value}` wrapper plus a payload record per element, is worth a +further ~1.8x and would need a contract addition. + +Retracting a claim from an earlier revision of this section: it said the +~0.8 µs/op that would remain after fixing the lift was guest-side +lowering and the rendezvous, "not lift". That was a subtraction between +two numbers measured under different conditions. Now that the lift is +fixed, the end-to-end delta (~0.78 µs/op) and the measured lift cost +(~0.7-0.96 µs/element) are the same size — so lift still accounts for +essentially all of it, and the further ~3x and ~1.8x above are still on +the table. Nobody has isolated the lowering side; that measurement has +not been done. + +What does not go away is the layout: the lowered `list` is +24 bytes/element fixed-stride with strings, paths and node lists all out +of line, against a packed variable-length frame decoded with one +`TextDecoder` pass over one contiguous string segment. + +**Disclosure.** The strategy table, the runtime-stage attribution and +the pure-JS floor came from one-off local experiments not in this tree: +a scratch component exercising both encodings against a counting sink, +and a temporary hook in the gitignored `.deps/polyengine` checkout +(reverted; that checkout is pristine). The upstream issue carries the +detail. They are not re-derivable by running anything committed here; +the two-column table and the instrumented op counts are. + +### Read + +At ~1.1x on `append-1k` and ~1.8x on `create-10k`, the typed channel is +no longer obviously unaffordable — which is a different answer than this +section gave a day ago, and the thing that changed was the runtime, not +this repo. The cost was never the component model or the schema; it was +one interpreter's uncached lift, and four upstream commits removed most +of it. + +If the typed channel is wanted, the case for it is now mostly about the +schema, not the speed. That case is weaker than it looks in one specific +place: WIT forbids recursive types, so `register-template`'s tree +becomes an index arena, which admits malformed index graphs the byte +grammar cannot express and needs validation in `applyTyped` that the +byte decoder never needed. The schema does not retire the +"two implementations must agree" problem for the one op where that +problem is hardest — it relocates it. + +Everywhere else, it does retire it, and 1.1-1.8x on a channel whose cost +is already a small fraction of the DOM work is a real option rather than +a non-starter. There is also another ~3x of headroom upstream if +anyone wants it (above), which would take `create-10k` under ~1.3x. ## Transport A/B (historical) @@ -268,21 +501,21 @@ see the numbers-are-box-relative guardrail below.) -# bench-rows results — 2026-08-31 +# bench-rows results — 2026-09-04 - Deno: 2.9.5 (aarch64-unknown-linux-gnu) -- git rev: ccc3c50 +- git rev: 813b31c-dirty - Box note: numbers are box-relative — compare columns within this run, not across machines. See bench/README.md. | op | ms (median of 5) | | --- | --- | -| create-1k | 7.10 | -| create-10k | 79.41 | -| append-1k | 79.05 | -| update-every-10th | 5.37 | -| swap-rows | 4.99 | -| remove-row | 2.09 | -| clear | 3.07 | +| create-1k | 22.15 | +| create-10k | 192.22 | +| append-1k | 92.77 | +| update-every-10th | 5.39 | +| swap-rows | 3.47 | +| remove-row | 4.04 | +| clear | 7.44 | diff --git a/bench/bench.ts b/bench/bench.ts index a25dd30..e562ee6 100644 --- a/bench/bench.ts +++ b/bench/bench.ts @@ -5,25 +5,19 @@ // A worker may report a null median (host-runtime trap exhausted its // retries — see bench/ops.ts's runOp doc); that renders as "N/A" in the // table rather than aborting the whole run or fabricating a number. -// -// Methodology and interpretation guardrails: bench/README.md. This bench -// tracks stream-transport throughput only — the call transport was -// retired after the historical A/B recorded in bench/README.md's -// "Transport A/B (historical)" section. -import { ops, TRANSPORTS } from "./ops.ts"; -import type { TransportName } from "./ops.ts"; +import { ops } from "./ops.ts"; interface OpResult { op: string; medianMs: number | null; - error?: string; + error: string | undefined; } -async function runWorker(opName: string, transport: TransportName): Promise<{ medianMs: number | null; error?: string }> { +async function runWorker(opName: string): Promise<{ medianMs: number | null; error?: string }> { const workerPath = new URL("./bench_worker.ts", import.meta.url).pathname; const cmd = new Deno.Command(Deno.execPath(), { - args: ["run", "--allow-read=.", "--allow-env", "--allow-run", workerPath, opName, transport], + args: ["run", "--allow-read=.", "--allow-env", "--allow-run", workerPath, opName], cwd: new URL("..", import.meta.url).pathname, stdout: "piped", stderr: "piped", @@ -32,22 +26,21 @@ async function runWorker(opName: string, transport: TransportName): Promise<{ me const stdoutText = new TextDecoder().decode(stdout); if (code !== 0) { const stderrText = new TextDecoder().decode(stderr); - throw new Error(`bench_worker failed for op=${opName} transport=${transport} (exit ${code}):\n${stderrText}`); + throw new Error(`bench_worker failed for op=${opName} (exit ${code}):\n${stderrText}`); } // The worker's only stdout line is its JSON result; be tolerant of any // stray output by taking the last non-empty line. const lines = stdoutText.trim().split("\n").filter((l) => l.length > 0); const last = lines[lines.length - 1]; - const parsed = JSON.parse(last) as { op: string; transport: string; medianMs: number | null; error?: string }; + const parsed = JSON.parse(last) as { op: string; medianMs: number | null; error?: string }; return { medianMs: parsed.medianMs, error: parsed.error }; } async function benchAll(): Promise { const results: OpResult[] = []; - const transport = TRANSPORTS[0]; for (const op of ops) { - const { medianMs, error } = await runWorker(op.name, transport); - results.push({ op: op.name, medianMs, error }); + const r = await runWorker(op.name); + results.push({ op: op.name, medianMs: r.medianMs, error: r.error }); } return results; } @@ -67,8 +60,12 @@ function renderTable(results: OpResult[]): string { } async function gitRev(): Promise { + // `git describe --always --dirty` rather than `rev-parse --short HEAD`: + // this tree may be uncommitted, so a bare HEAD rev could claim a stale + // commit — the `--dirty` suffix makes that visible instead of silently + // misleading. try { - const cmd = new Deno.Command("git", { args: ["rev-parse", "--short", "HEAD"], stdout: "piped" }); + const cmd = new Deno.Command("git", { args: ["describe", "--always", "--dirty"], stdout: "piped" }); const { stdout } = await cmd.output(); return new TextDecoder().decode(stdout).trim() || "unknown"; } catch { diff --git a/bench/bench_worker.ts b/bench/bench_worker.ts index b783d49..8227999 100644 --- a/bench/bench_worker.ts +++ b/bench/bench_worker.ts @@ -8,32 +8,27 @@ // whole bench run; bench.ts renders that op as "N/A" and the failure is // reported plainly, not hidden. // -// Usage: deno run --allow-read=. --allow-env --allow-run bench/bench_worker.ts +// Usage: deno run --allow-read=. --allow-env --allow-run bench/bench_worker.ts import { defaultTranslator } from "@deltic/translator"; import { loadComponentBytes, ops, runOp } from "./ops.ts"; -import type { TransportName } from "./ops.ts"; async function main() { - const [opName, transportArg] = Deno.args; + const [opName] = Deno.args; const op = ops.find((o) => o.name === opName); if (!op) { throw new Error(`unknown op "${opName}"; known ops: ${ops.map((o) => o.name).join(", ")}`); } - if (transportArg !== "stream") { - throw new Error(`transport must be "stream", got "${transportArg}"`); - } - const transport: TransportName = transportArg; const translator = await defaultTranslator(); - const bytes = await loadComponentBytes(transport); + const bytes = await loadComponentBytes(); try { - const medianMs = await runOp(transport, bytes, translator, op); - console.log(JSON.stringify({ op: opName, transport, medianMs })); + const medianMs = await runOp(bytes, translator, op); + console.log(JSON.stringify({ op: opName, medianMs })); } catch (e) { const message = e instanceof Error ? e.message : String(e); - console.error(`bench_worker: op=${opName} transport=${transport} failed after retries: ${message}`); - console.log(JSON.stringify({ op: opName, transport, medianMs: null, error: message })); + console.error(`bench_worker: op=${opName} failed after retries: ${message}`); + console.log(JSON.stringify({ op: opName, medianMs: null, error: message })); } } diff --git a/bench/ops.ts b/bench/ops.ts index b7e126d..8ad19bc 100644 --- a/bench/ops.ts +++ b/bench/ops.ts @@ -28,9 +28,7 @@ import type { Mounted } from "../host/src/host.ts"; import type { UntranslatedArtifacts } from "@deltic/runtime/embedder"; /** Taken from the embedder's own type rather than restated, so this stays - * pinned to what `mountApp`'s `source` actually accepts. (The `unknown` it - * replaces did not hide the stale call shape below — `deno check` reports it - * fine; nothing was running `deno check` over bench/. See deno.json.) */ + * pinned to what `mountApp`'s `source` actually accepts. */ type Translator = UntranslatedArtifacts["translator"]; export const RUNS = 5; @@ -38,15 +36,12 @@ export const RUNS = 5; // (dispatch: "an unexplainable number is a bug lead, not a result"). export const SANITY_FLOOR_MS = 0.5; -export type TransportName = "stream"; -export const TRANSPORTS: TransportName[] = ["stream"]; - -export function componentPath(t: TransportName): string { - return new URL(`./build/bench-rows-${t}.component.wasm`, import.meta.url).pathname; +export function componentPath(): string { + return new URL(`./build/bench-rows.component.wasm`, import.meta.url).pathname; } -export async function loadComponentBytes(t: TransportName): Promise { - const path = componentPath(t); +export async function loadComponentBytes(): Promise { + const path = componentPath(); try { return await Deno.readFile(path); } catch (e) { @@ -147,11 +142,12 @@ function posDataId(root: Element, pos: number): string | undefined { * after a small number of click/render round trips in one mount — this * bench was the first thing to drive repeated interactions through that * transport. Root-caused and fixed at the driver level, NOT here or in - * polyengine (`src/driver.rs`'s `run()`; amendment A15 host retention — - * see its module doc for the full explanation: the stream transport's - * parked `readDirect` session is host-retained state that makes a - * quiescent park legal, the call transport retains nothing host-side - * once flush returns, so the same park was correctly indistinguishable + * polyengine (`src/driver.rs`'s `run()`; see its module doc for the full + * explanation, citing .deps/polyengine/contracts/embedder-api.md + * §"Streams and futures", issue #162 host retention: the stream + * transport's host-retained mutation-stream read end is what makes a + * quiescent park legal, the call transport retained nothing host-side + * once flush returned, so the same park was correctly indistinguishable * from deadlock and traps). This function stays written with minimal * microtask depth between dispatch and poll (tail-returning `waitFor`'s * promise rather than `await`ing it in an `async function`) since that @@ -204,7 +200,6 @@ export function median(xs: number[]): number { } async function freshMount( - t: TransportName, componentBytes: Uint8Array, translator: Translator, ): Promise<{ root: Element; mounted: Mounted; errors: unknown[] }> { @@ -215,9 +210,9 @@ async function freshMount( root, onError: (err) => errors.push(err), }); - await waitFor(() => root.querySelector("#row-count") !== null, `${t}: initial mount`); + await waitFor(() => root.querySelector("#row-count") !== null, "initial mount"); if (errors.length > 0) { - throw new Error(`${t}: onError fired during mount: ${Deno.inspect(errors)}`); + throw new Error(`onError fired during mount: ${Deno.inspect(errors)}`); } return { root, mounted, errors }; } @@ -369,24 +364,23 @@ export const ops: OpDef[] = [ /** * Run one op's warmup + N=5 timed reps against a *fresh* mount, and return - * the median. Each op/transport pair is run in its own Deno process (see - * bench_worker.ts / bench.ts) — cheap process isolation with no known - * cost now, kept as a structural safety margin. + * the median. Each op is run in its own Deno process (see bench_worker.ts / + * bench.ts) — cheap process isolation with no known cost now, kept as a + * structural safety margin. * * Retries the whole attempt (fresh mount) up to `MAX_ATTEMPTS_PER_OP` * times as a defensive net, not because a specific failure mode is * expected: an earlier revision of this bench was the first thing to - * drive repeated interactions through the CALL transport and hit a real - * `run`-task deadlock trap doing so — root-caused and fixed at the - * driver level (`src/driver.rs`'s `run()`, amendment A15 host retention; - * see its module doc for the full explanation) rather than here. Kept as - * a low-cost fallback in case of unrelated future flakiness, not because - * this bug is expected to recur. + * drive repeated interactions through the CALL transport (since retired — + * bench/README.md's "Transport A/B (historical)") and hit a real + * `run`-task deadlock trap doing so — root-caused and fixed at the driver + * level (`src/driver.rs`'s `run()`; see its module doc for the full + * explanation) rather than here. Kept as a low-cost fallback in case of + * unrelated future flakiness, not because this bug is expected to recur. */ const MAX_ATTEMPTS_PER_OP = 3; export async function runOp( - t: TransportName, componentBytes: Uint8Array, translator: Translator, op: OpDef, @@ -394,7 +388,7 @@ export async function runOp( let lastErr: unknown; for (let attempt = 1; attempt <= MAX_ATTEMPTS_PER_OP; attempt++) { try { - return await runOpOnce(t, componentBytes, translator, op); + return await runOpOnce(componentBytes, translator, op); } catch (e) { lastErr = e; // A "FATAL bench bug" is a real defect in this harness (bad @@ -402,24 +396,23 @@ export async function runOp( // so surface it immediately rather than masking it behind retries. if (e instanceof Error && e.message.startsWith("FATAL bench bug")) throw e; console.error( - `bench/ops.ts: "${op.name}" (${t}) attempt ${attempt}/${MAX_ATTEMPTS_PER_OP} failed ` + + `bench/ops.ts: "${op.name}" attempt ${attempt}/${MAX_ATTEMPTS_PER_OP} failed ` + `(${e instanceof Error ? e.message : String(e)}); retrying with a fresh mount if attempts remain.`, ); } } throw new Error( - `"${op.name}" (${t}) failed all ${MAX_ATTEMPTS_PER_OP} attempts. Last error: ` + + `"${op.name}" failed all ${MAX_ATTEMPTS_PER_OP} attempts. Last error: ` + `${lastErr instanceof Error ? lastErr.message : String(lastErr)}`, ); } async function runOpOnce( - t: TransportName, componentBytes: Uint8Array, translator: Translator, op: OpDef, ): Promise { - const { root, mounted } = await freshMount(t, componentBytes, translator); + const { root, mounted } = await freshMount(componentBytes, translator); // Warmup: exercises JIT/allocator warm paths and any one-time template // registration (dioxus registers a template on first encounter — see // src/writer.rs's module doc — so the very first create/update op pays a @@ -440,7 +433,7 @@ async function runOpOnce( const m = median(timings); if (op.rowsInvolved >= 1000 && m < SANITY_FLOOR_MS) { throw new Error( - `FATAL bench bug: "${op.name}" (${t} transport) touches ${op.rowsInvolved} rows but median is ` + + `FATAL bench bug: "${op.name}" touches ${op.rowsInvolved} rows but median is ` + `${m.toFixed(3)}ms, under the ${SANITY_FLOOR_MS}ms sanity floor for that row count. This is not a ` + `credible number for real DOM work at that scale — investigate before reporting (per-rep timings: ` + `${timings.map((x) => x.toFixed(3)).join(", ")}ms).`, diff --git a/bench/results-2026-09-03-aarch64-unknown-linux-gnu.md b/bench/results-2026-09-03-aarch64-unknown-linux-gnu.md new file mode 100644 index 0000000..f10d83e --- /dev/null +++ b/bench/results-2026-09-03-aarch64-unknown-linux-gnu.md @@ -0,0 +1,15 @@ +# bench-rows results — 2026-09-03 + +- Deno: 2.9.5 (aarch64-unknown-linux-gnu) +- git rev: 9dc9810-dirty +- Box note: numbers are box-relative — compare columns within this run, not across machines. See bench/README.md. + +| op | bytes (ms, median of 5) | typed (ms, median of 5) | typed / bytes | +| --- | --- | --- | --- | +| create-1k | 14.53 | 72.53 | 4.99x | +| create-10k | 91.03 | 528.18 | 5.80x | +| append-1k | 83.45 | 133.05 | 1.59x | +| update-every-10th | 4.73 | 5.81 | 1.23x | +| swap-rows | 2.92 | 5.34 | 1.83x | +| remove-row | 4.83 | 5.01 | 1.04x | +| clear | 3.69 | 44.31 | 12.02x | diff --git a/bench/results-2026-09-04-aarch64-unknown-linux-gnu.md b/bench/results-2026-09-04-aarch64-unknown-linux-gnu.md new file mode 100644 index 0000000..5a1869c --- /dev/null +++ b/bench/results-2026-09-04-aarch64-unknown-linux-gnu.md @@ -0,0 +1,15 @@ +# bench-rows results — 2026-09-04 + +- Deno: 2.9.5 (aarch64-unknown-linux-gnu) +- git rev: 813b31c-dirty +- Box note: numbers are box-relative — compare columns within this run, not across machines. See bench/README.md. + +| op | ms (median of 5) | +| --- | --- | +| create-1k | 22.15 | +| create-10k | 192.22 | +| append-1k | 92.77 | +| update-every-10th | 5.39 | +| swap-rows | 3.47 | +| remove-row | 4.04 | +| clear | 7.44 | diff --git a/bench/run.sh b/bench/run.sh index 22e7afe..d6531ce 100755 --- a/bench/run.sh +++ b/bench/run.sh @@ -13,8 +13,8 @@ mkdir -p bench/build echo "== building bench-rows ==" cargo build -p bench-rows-example --target wasm32-wasip2 --release cp target/wasm32-wasip2/release/bench_rows_example.wasm \ - bench/build/bench-rows-stream.component.wasm -wasm-tools validate --features component-model,cm-async bench/build/bench-rows-stream.component.wasm + bench/build/bench-rows.component.wasm +wasm-tools validate --features component-model,cm-async bench/build/bench-rows.component.wasm echo "== running bench.ts ==" deno run --allow-read=. --allow-write=bench --allow-env --allow-run bench/bench.ts "$@" diff --git a/deno.lock b/deno.lock index ec3ffd1..975e502 100644 --- a/deno.lock +++ b/deno.lock @@ -132,11 +132,11 @@ "npm:linkedom@0.18.12" ], "links": { - "jsr:@polyengine/ct-runner@0.5.2": {}, - "jsr:@polyengine/protocol@0.2.3": {}, - "jsr:@polyengine/runtime@0.5.2": {}, - "jsr:@polyengine/translator@0.5.2": {}, - "jsr:@polyengine/wasi@0.5.2": {} + "jsr:@polyengine/ct-runner@0.6.3": {}, + "jsr:@polyengine/protocol@0.3.1": {}, + "jsr:@polyengine/runtime@0.6.3": {}, + "jsr:@polyengine/translator@0.6.3": {}, + "jsr:@polyengine/wasi@0.6.3": {} } } } diff --git a/e2e/tests/counter.spec.ts b/e2e/tests/counter.spec.ts index d4ec300..cb46f2d 100644 --- a/e2e/tests/counter.spec.ts +++ b/e2e/tests/counter.spec.ts @@ -122,16 +122,7 @@ test("counter example: real click/type/submit through Chromium", async ({ page } const marker = await page.evaluate(() => (globalThis as unknown as { __preNavMarker?: string }).__preNavMarker); expect(marker, "a real navigation/reload would have wiped this in-page marker").toBe("still-here"); - // 6) STREAM transport smoke assert: every delivered byte has been - // decoded into whole frames (host/src/host.ts's frameDecoder — "lets a - // test confirm the zero-copy direct-read path actually engaged"). - const pending = await page.evaluate(() => - (globalThis as unknown as { __mountedHandle: { frameDecoder: { pending(): number } } }).__mountedHandle - .frameDecoder.pending() - ); - expect(pending, "frameDecoder must have no partial frame staged after settling").toBe(0); - - // 7) Zero collected page errors / console errors throughout. + // 6) Zero collected page errors / console errors throughout. const collectedErrors = await page.evaluate(() => (globalThis as unknown as { __e2eErrors: unknown[] }).__e2eErrors ); diff --git a/fixtures/surface-probe/src/lib.rs b/fixtures/surface-probe/src/lib.rs index 86cf1b3..fdd7071 100644 --- a/fixtures/surface-probe/src/lib.rs +++ b/fixtures/surface-probe/src/lib.rs @@ -1,7 +1,8 @@ -//! Host-transport test fixture: no-dioxus guest exercising the stream -//! mutation transport and the round-trip event path. +//! Host test fixture: no-dioxus guest exercising the mutation channel and +//! the round-trip event path. //! -//! Governing doc: wit/world.wit (normative wire format + world `app`). +//! Governing doc: wit/world.wit (the `mutations` interface — the normative +//! operation schema — and the world `app`). //! Op-sequence/behavior spec: the polyengine-dioxus host-runtime dispatch //! (fixtures/surface-probe territory) — see its "Behavior" section for the //! exact template/listener layout this file builds; cited inline as @@ -32,7 +33,7 @@ //! //! Reused across BOTH roles where the text is identical (e.g. the string //! "input" serves as both the second template's tag name and the third -//! listener's event name — same interned slot, no wire-format rule forbids +//! listener's event name — same interned slot, nothing in the schema forbids //! it): 0=`section` 1=`class` 2=`click` 3=`keydown` 4=`input` 5=`title` //! 6=`disabled` 7=`touchstart`. `handle-event`'s `name: u16` therefore //! arrives as one of {2, 3, 4, 7} for our own listeners; @@ -73,168 +74,12 @@ wit_bindgen::generate!({ use core::cell::RefCell; use polymorph::dioxus::events::{DomEvent as HostDomEvent, Modifiers}; - -// -- wire encoding ------------------------------------------------------------ -// -// Mirrors host/src/decoder.ts's Cursor, in reverse (encode instead of -// decode). The byte layout is normative in wit/world.wit's "# Opcodes" / -// "# Primitive operand encodings" doc comments (reproduced above the -// `generate!` call via the WIT source itself); cited inline as -// `wit:`. -mod wire { - const NONE_STRREF: u16 = 0xffff; - - pub struct Encoder { - pub ops: Vec, - pub strings: String, - } - - impl Encoder { - pub fn new() -> Self { - Self { ops: Vec::new(), strings: String::new() } - } - - fn u8(&mut self, v: u8) { - self.ops.push(v); - } - fn u16(&mut self, v: u16) { - self.ops.extend_from_slice(&v.to_le_bytes()); - } - fn u32(&mut self, v: u32) { - self.ops.extend_from_slice(&v.to_le_bytes()); - } - - /// strref: u16, 0xffff = none (wit "Primitive operand encodings"). - fn strref(&mut self, v: Option) { - self.u16(v.unwrap_or(NONE_STRREF)); - } - - /// path: u8 length, then that many u8 child indices. - fn path(&mut self, p: &[u8]) { - self.u8(p.len() as u8); - self.ops.extend_from_slice(p); - } - - /// dynstr: u16 UTF-16 code-unit length, then content appended to the - /// string segment (wit "Primitive operand encodings" — the u32 - /// extended-length form is never needed by this fixture's short - /// ASCII strings). - fn dynstr(&mut self, s: &str) { - let len16: usize = s.chars().map(char::len_utf16).sum(); - self.u16(len16 as u16); - self.strings.push_str(s); - } - - // -- ops (wit:0x01.."0x11) -------------------------------------------- - - pub fn cache_string(&mut self, id: u16, s: &str) { - self.u8(0x01); - self.u16(id); - self.dynstr(s); - } - - /// Begin `register-template`; caller writes `nroots` node trees via - /// `element_start`/`attr`/`children_count`/`text_node`/`dynamic_node` - /// in wire order (depth-first, matching decodeTemplateNode). - pub fn register_template_header(&mut self, tmpl: u16, nroots: u16) { - self.u8(0x02); - self.u16(tmpl); - self.u16(nroots); - } - pub fn tmpl_element_header(&mut self, tag: u16, ns: Option, nattrs: u16) { - self.u8(0x00); // node kind: element - self.u16(tag); - self.strref(ns); - self.u16(nattrs); - } - pub fn tmpl_attr(&mut self, name: u16, ns: Option, value: &str) { - self.u16(name); - self.strref(ns); - self.dynstr(value); - } - pub fn tmpl_children_header(&mut self, nchildren: u16) { - self.u16(nchildren); - } - pub fn tmpl_text(&mut self, value: &str) { - self.u8(0x01); - self.dynstr(value); - } - pub fn tmpl_dynamic(&mut self) { - self.u8(0x02); - } - - pub fn append_children(&mut self, id: u32, m: u32) { - self.u8(0x03); - self.u32(id); - self.u32(m); - } - pub fn assign_id(&mut self, path: &[u8], id: u32) { - self.u8(0x04); - self.path(path); - self.u32(id); - } - pub fn create_text_node(&mut self, id: u32, text: &str) { - self.u8(0x06); - self.u32(id); - self.dynstr(text); - } - pub fn load_template(&mut self, tmpl: u16, root: u16, id: u32) { - self.u8(0x07); - self.u16(tmpl); - self.u16(root); - self.u32(id); - } - pub fn replace_placeholder(&mut self, path: &[u8], m: u32) { - self.u8(0x09); - self.path(path); - self.u32(m); - } - pub fn set_attribute_text(&mut self, id: u32, name: u16, ns: Option, value: &str) { - self.u8(0x0c); - self.u32(id); - self.u16(name); - self.strref(ns); - self.u8(0x00); // attrval kind: text - self.dynstr(value); - } - pub fn set_attribute_bool(&mut self, id: u32, name: u16, ns: Option, value: bool) { - self.u8(0x0c); - self.u32(id); - self.u16(name); - self.strref(ns); - self.u8(0x03); // attrval kind: bool - self.u8(value as u8); - } - pub fn set_text(&mut self, id: u32, text: &str) { - self.u8(0x0d); - self.u32(id); - self.dynstr(text); - } - pub fn new_event_listener(&mut self, id: u32, name: u16, bubbles: bool) { - self.u8(0x0e); - self.u32(id); - self.u16(name); - self.u8(bubbles as u8); // flags bit0 = bubbles - } - - pub fn finish(self) -> (Vec, String) { - (self.ops, self.strings) - } - } - - /// Stream-transport framing (wit/world.wit "# Framing"): - /// `frame-len:u32 strings-len:u32 strings ops`. - pub fn frame(ops: &[u8], strings: &str) -> Vec { - let strings_bytes = strings.as_bytes(); - let frame_len = 4 + strings_bytes.len() as u32 + ops.len() as u32; - let mut buf = Vec::with_capacity(4 + frame_len as usize); - buf.extend_from_slice(&frame_len.to_le_bytes()); - buf.extend_from_slice(&(strings_bytes.len() as u32).to_le_bytes()); - buf.extend_from_slice(strings_bytes); - buf.extend_from_slice(ops); - buf - } -} +// `Operation` itself is already in scope: the world `use`s it, so +// `generate!` re-exports it at the crate root. +use polymorph::dioxus::mutations::{ + AssignId, AttrValue, CacheString, CreateTextNode, EventListener, LoadTemplate, PathOp, + RegisterTemplate, SetAttribute, SetText, StackOp, TemplateAttr, TemplateElement, TemplateNode, +}; // -- interned string ids (see module doc) ------------------------------------ @@ -259,62 +104,92 @@ fn event_name_for_id(id: u16) -> &'static str { /// Builds the initial batch: see the module doc's "DOM built" section for /// the exact op sequence and its rationale. -fn build_initial_batch() -> (Vec, String) { - let mut e = wire::Encoder::new(); - - e.cache_string(STR_SECTION, "section"); - e.cache_string(STR_CLASS, "class"); - e.cache_string(STR_CLICK, "click"); - e.cache_string(STR_KEYDOWN, "keydown"); - e.cache_string(STR_INPUT, "input"); - e.cache_string(STR_TITLE, "title"); - e.cache_string(STR_DISABLED, "disabled"); - e.cache_string(STR_TOUCHSTART, "touchstart"); - - // register-template(tmpl=0):
hdr{dyn}{dyn}
- e.register_template_header(0, 1); - e.tmpl_element_header(STR_SECTION, None, 1); - e.tmpl_attr(STR_CLASS, None, "probe"); - e.tmpl_children_header(3); - e.tmpl_text("hdr"); - e.tmpl_dynamic(); // idx1 — replaced below with the "ready" text node - e.tmpl_dynamic(); // idx2 — left as a bare placeholder, assign-id'd - - // register-template(tmpl=1): - e.register_template_header(1, 1); - e.tmpl_element_header(STR_INPUT, None, 0); - e.tmpl_children_header(0); - - // load-template(tmpl=0) -> id=1 (section); stack: [root, section] - e.load_template(0, 0, 1); - // assign-id exercised on the OTHER dynamic slot (idx2), independently - // of the replace-placeholder below (dispatch: "assign-id ... exercise - // ... independently"). - e.assign_id(&[2], 4); - // create-text-node(id=2, "ready"); stack: [root, section, text(2)] - e.create_text_node(2, "ready"); - // replace-placeholder(path=[1], m=1): pop text(2), replace idx1 slot. - // stack: [root, section] - e.replace_placeholder(&[1], 1); - // set-attribute TEXT on section. - e.set_attribute_text(1, STR_TITLE, None, "probe-section"); - // three bubbling listeners on section. - e.new_event_listener(1, STR_CLICK, true); - e.new_event_listener(1, STR_KEYDOWN, true); - e.new_event_listener(1, STR_TOUCHSTART, true); - // append-children(root, m=1): pop section, attach under root. - e.append_children(0, 1); - - // load-template(tmpl=1) -> id=5 (input); stack: [root, input] - e.load_template(1, 0, 5); - // set-attribute BOOL (false -> removed by the applier's boolean- - // attribute table; exercises the bool attrval path regardless). - e.set_attribute_bool(5, STR_DISABLED, None, false); - e.new_event_listener(5, STR_INPUT, true); - // append-children(root, m=1): pop input, attach under root. - e.append_children(0, 1); +fn build_initial_batch() -> Vec { + fn cache(id: u16, s: &str) -> Operation { + Operation::CacheString(CacheString { id, str: s.to_string() }) + } - e.finish() + vec![ + cache(STR_SECTION, "section"), + cache(STR_CLASS, "class"), + cache(STR_CLICK, "click"), + cache(STR_KEYDOWN, "keydown"), + cache(STR_INPUT, "input"), + cache(STR_TITLE, "title"), + cache(STR_DISABLED, "disabled"), + cache(STR_TOUCHSTART, "touchstart"), + // register-template(tmpl=0):
hdr{dyn}{dyn}
+ // `nodes` is the arena, in pre-order; `roots` and `children` index + // into it (wit: `register-template` is not a tree — recursive WIT + // types are rejected). + Operation::RegisterTemplate(RegisterTemplate { + id: 0, + nodes: vec![ + TemplateNode::Element(TemplateElement { + tag: STR_SECTION, + ns: None, + attrs: vec![TemplateAttr { + name: STR_CLASS, + ns: None, + value: "probe".to_string(), + }], + children: vec![1, 2, 3], + }), + TemplateNode::Text("hdr".to_string()), + TemplateNode::Dynamic, // child idx1 — replaced below with "ready" + TemplateNode::Dynamic, // child idx2 — left bare, assign-id'd + ], + roots: vec![0], + }), + // register-template(tmpl=1): + Operation::RegisterTemplate(RegisterTemplate { + id: 1, + nodes: vec![TemplateNode::Element(TemplateElement { + tag: STR_INPUT, + ns: None, + attrs: Vec::new(), + children: Vec::new(), + })], + roots: vec![0], + }), + // load-template(tmpl=0) -> id=1 (section); stack: [root, section] + Operation::LoadTemplate(LoadTemplate { id: 1, tmpl: 0, root: 0 }), + // assign-id exercised on the OTHER dynamic slot (idx2), independently + // of the replace-placeholder below (dispatch: "assign-id ... exercise + // ... independently"). + Operation::AssignId(AssignId { path: vec![2], id: 4 }), + // create-text-node(id=2, "ready"); stack: [root, section, text(2)] + Operation::CreateTextNode(CreateTextNode { id: 2, text: "ready".to_string() }), + // replace-placeholder(path=[1], m=1): pop text(2), replace idx1 slot. + // stack: [root, section] + Operation::ReplacePlaceholder(PathOp { path: vec![1], m: 1 }), + // set-attribute TEXT on section. + Operation::SetAttribute(SetAttribute { + id: 1, + name: STR_TITLE, + ns: None, + value: AttrValue::Text("probe-section".to_string()), + }), + // three bubbling listeners on section. + Operation::NewEventListener(EventListener { id: 1, name: STR_CLICK, bubbles: true }), + Operation::NewEventListener(EventListener { id: 1, name: STR_KEYDOWN, bubbles: true }), + Operation::NewEventListener(EventListener { id: 1, name: STR_TOUCHSTART, bubbles: true }), + // append-children(root, m=1): pop section, attach under root. + Operation::AppendChildren(StackOp { id: 0, m: 1 }), + // load-template(tmpl=1) -> id=5 (input); stack: [root, input] + Operation::LoadTemplate(LoadTemplate { id: 5, tmpl: 1, root: 0 }), + // set-attribute BOOL (false -> removed by the applier's boolean- + // attribute table; exercises the bool attr-value path regardless). + Operation::SetAttribute(SetAttribute { + id: 5, + name: STR_DISABLED, + ns: None, + value: AttrValue::Boolean(false), + }), + Operation::NewEventListener(EventListener { id: 5, name: STR_INPUT, bubbles: true }), + // append-children(root, m=1): pop input, attach under root. + Operation::AppendChildren(StackOp { id: 0, m: 1 }), + ] } /// One-line deterministic summary written into the "ready" text node (id 2) @@ -377,49 +252,44 @@ fn summarize(name: u16, payload: &Payload) -> (String, bool) { } } -fn build_event_batch(name: u16, payload: &Payload) -> (Vec, String, bool) { +fn build_event_batch(name: u16, payload: &Payload) -> (Vec, bool) { let (summary, prevent) = summarize(name, payload); - let mut e = wire::Encoder::new(); - e.set_text(2, &summary); - let (ops, strings) = e.finish(); - (ops, strings, prevent) + (vec![Operation::SetText(SetText { id: 2, text: summary })], prevent) } -// The stream transport's writer half. `run` creates it and parks it here; +// The mutation channel's writer half. `run` creates it and parks it here; // the spawned initial-batch task and every later `handle-event` write take // it out and put it back (dispatch: "stash the writer half ... -// in a thread_local RefCell; writes complete inline while the host session -// is parked, so no cross-task interleaving. Do not hold RefCell borrows +// in a thread_local RefCell; writes complete inline while the host is +// reading, so no cross-task interleaving. Do not hold RefCell borrows // across awaits." — every use below `take()`s the writer out of the cell // before awaiting, and puts it back after, so no borrow spans an `.await`). // Keeping it here is also what holds the stream OPEN: dropping this writer // is what would signal end-of-stream to the host, so it is never dropped. thread_local! { - static WRITER: RefCell>> = const { RefCell::new(None) }; + static WRITER: RefCell>> = const { RefCell::new(None) }; } struct Component; impl Guest for Component { - async fn run() -> wit_bindgen::rt::async_support::StreamReader { + async fn run() -> wit_bindgen::rt::async_support::StreamReader { // Create the channel and hand the read end back as `run`'s return - // value (wit: `export run: async func() -> stream`). Nothing is - // written from this body: a write here would park waiting for a - // reader the host cannot have until this promise settles. - let (writer, reader) = wit_stream::new::(); + // value (wit: `export run: async func() -> stream`). + // Nothing is written from this body: a write here would park waiting + // for a reader the host cannot have until this promise settles. + let (writer, reader) = wit_stream::new::(); WRITER.with(|cell| *cell.borrow_mut() = Some(writer)); // The initial batch goes out from a spawned task, after the return. // Rendezvous semantics make the ordering safe: the write parks until - // the host's `readDirect` session is parked, so it cannot be lost by - // racing ahead of the host. + // the host reads, so it cannot be lost by racing ahead of the host. wit_bindgen::rt::async_support::spawn_local(async move { - let (ops, strings) = build_initial_batch(); - let frame = wire::frame(&ops, &strings); + let batch = build_initial_batch(); let mut writer = WRITER .with(|cell| cell.borrow_mut().take()) .expect("initial batch: writer taken before the first write"); - writer.write_all(frame).await; + writer.write_all(batch).await; WRITER.with(|cell| *cell.borrow_mut() = Some(writer)); }); // This task ends here; the spawned task ends once the initial batch @@ -433,13 +303,12 @@ impl Guest for Component { } async fn handle_event(_target: u32, name: u16, payload: Payload, ev: &HostDomEvent) { - let (ops, strings, prevent) = build_event_batch(name, &payload); + let (batch, prevent) = build_event_batch(name, &payload); - let frame = wire::frame(&ops, &strings); let mut writer = WRITER .with(|cell| cell.borrow_mut().take()) .expect("handle-event dispatched before run() opened the stream"); - writer.write_all(frame).await; + writer.write_all(batch).await; WRITER.with(|cell| *cell.borrow_mut() = Some(writer)); if prevent { diff --git a/harness/entry.ts b/harness/entry.ts index f311179..b567200 100644 --- a/harness/entry.ts +++ b/harness/entry.ts @@ -132,9 +132,8 @@ async function main(): Promise { // mountApp resolving is necessary but the initial mutation batch may // still be async-in-flight — see host/tests/counter_test.ts's own // waitFor(() => root.querySelector("#count") !== null, ...)). - // - window.__mountedHandle: the raw `Mounted` object (frameDecoder, - // dispatcher, dispose) for smoke assertions (STREAM transport - // engagement via frameDecoder.pending()). + // - window.__mountedHandle: the raw `Mounted` object (applier, + // dispatcher, dispose) for smoke assertions. // - window.__e2eErrors: collected page/onError errors (asserted empty). (globalThis as unknown as { __mountedHandle: typeof mounted }).__mountedHandle = mounted; diff --git a/host/src/applier.ts b/host/src/applier.ts index 9e1aa44..27e04f4 100644 --- a/host/src/applier.ts +++ b/host/src/applier.ts @@ -1,4 +1,5 @@ -// DOM applier for the polymorph:dioxus mutation wire format. +// DOM applier for the polymorph:dioxus mutation schema (wit/world.wit's +// `interface mutations`, applied via operations.ts's `applyOperations`). // // DOM semantics (stack machine, node table, template cloning, path walking, // setAttributeInner rules) are ported from dioxus-web's own interpreter @@ -6,11 +7,55 @@ // https://github.com/DioxusLabs/dioxus, `packages/web/src/js/core.ts` and // `packages/interpreter/src/set_attribute.ts`, vendored here for reference // as /tmp/opencode/dioxus-ref/{core,set_attribute}.ts at authoring time). -// The BYTE FORMAT is ours (see decoder.ts / wit/world.wit); only the DOM -// application rules are ported. Cited inline as `ref:core.ts:` / -// `ref:set_attribute.ts:`. - -import type { OpSink, StrRef, TemplateNodeDesc } from "./decoder.ts"; +// The MUTATION SCHEMA is ours (wit/world.wit's `interface mutations`); only +// the DOM application rules are ported. Cited inline as `ref:core.ts:` +// / `ref:set_attribute.ts:`. + +/** An interned string id (wit/world.wit `mutations.str-ref`, a u16), + * defined by a prior `cache-string` operation. */ +export type StrRef = number; + +/** The recursive tree `operations.ts`'s `rehydrateTemplateArena` builds out + * of a `register-template` operation's flat arena, for `OpSink. + * registerTemplate` to consume. */ +export type TemplateNodeDesc = + | { + kind: "element"; + tag: StrRef; + ns: StrRef | null; + attrs: { name: StrRef; ns: StrRef | null; value: string }[]; + children: TemplateNodeDesc[]; + } + | { kind: "text"; value: string } + | { kind: "dynamic" }; + +/** The sink `operations.ts`'s `applyOperations` drives — one method per + * `mutations.operation` arm (wit/world.wit), `register-template`'s arena + * already rehydrated into a `TemplateNodeDesc` tree. `DomApplier` is the + * only implementor. */ +export interface OpSink { + cacheString(id: number, s: string): void; + registerTemplate(tmpl: number, roots: TemplateNodeDesc[]): void; + appendChildren(id: number, m: number): void; + assignId(path: Uint8Array, id: number): void; + createPlaceholder(id: number): void; + createTextNode(id: number, text: string): void; + loadTemplate(tmpl: number, root: number, id: number): void; + replaceWith(id: number, m: number): void; + replacePlaceholder(path: Uint8Array, m: number): void; + insertAfter(id: number, m: number): void; + insertBefore(id: number, m: number): void; + setAttributeText(id: number, name: StrRef, ns: StrRef | null, value: string): void; + setAttributeFloat(id: number, name: StrRef, ns: StrRef | null, value: number): void; + setAttributeInt(id: number, name: StrRef, ns: StrRef | null, value: bigint): void; + setAttributeBool(id: number, name: StrRef, ns: StrRef | null, value: boolean): void; + setAttributeNone(id: number, name: StrRef, ns: StrRef | null): void; + setText(id: number, text: string): void; + newEventListener(id: number, name: StrRef, bubbles: boolean): void; + removeEventListener(id: number, name: StrRef, bubbles: boolean): void; + remove(id: number): void; + pushRoot(id: number): void; +} export interface ListenerDelegate { add( diff --git a/host/src/decoder.ts b/host/src/decoder.ts deleted file mode 100644 index 0590919..0000000 --- a/host/src/decoder.ts +++ /dev/null @@ -1,444 +0,0 @@ -// Decoder for the polymorph:dioxus mutation wire format. -// -// The wire format is normative in wit/world.wit (the `run` export's doc -// comments on the opcode table, framing, and primitive operand encodings). -// This file implements the "op segment" decoder (decodeBatch) plus the -// stream-transport framing layer (FrameDecoder). See wit/world.wit for the -// authoritative byte layout; cited inline as `wit:
`. - -export type StrRef = number; // u16 interned id - -export type TemplateNodeDesc = - | { - kind: "element"; - tag: StrRef; - ns: StrRef | null; - attrs: { name: StrRef; ns: StrRef | null; value: string }[]; - children: TemplateNodeDesc[]; - } - | { kind: "text"; value: string } - | { kind: "dynamic" }; - -export interface OpSink { - cacheString(id: number, s: string): void; - registerTemplate(tmpl: number, roots: TemplateNodeDesc[]): void; - appendChildren(id: number, m: number): void; - assignId(path: Uint8Array, id: number): void; - createPlaceholder(id: number): void; - createTextNode(id: number, text: string): void; - loadTemplate(tmpl: number, root: number, id: number): void; - replaceWith(id: number, m: number): void; - replacePlaceholder(path: Uint8Array, m: number): void; - insertAfter(id: number, m: number): void; - insertBefore(id: number, m: number): void; - setAttributeText(id: number, name: StrRef, ns: StrRef | null, value: string): void; - setAttributeFloat(id: number, name: StrRef, ns: StrRef | null, value: number): void; - setAttributeInt(id: number, name: StrRef, ns: StrRef | null, value: bigint): void; - setAttributeBool(id: number, name: StrRef, ns: StrRef | null, value: boolean): void; - setAttributeNone(id: number, name: StrRef, ns: StrRef | null): void; - setText(id: number, text: string): void; - newEventListener(id: number, name: StrRef, bubbles: boolean): void; - removeEventListener(id: number, name: StrRef, bubbles: boolean): void; - remove(id: number): void; - pushRoot(id: number): void; -} - -// Opcodes — wit/world.wit "# Opcodes" table. -const OP_CACHE_STRING = 0x01; -const OP_REGISTER_TEMPLATE = 0x02; -const OP_APPEND_CHILDREN = 0x03; -const OP_ASSIGN_ID = 0x04; -const OP_CREATE_PLACEHOLDER = 0x05; -const OP_CREATE_TEXT_NODE = 0x06; -const OP_LOAD_TEMPLATE = 0x07; -const OP_REPLACE_WITH = 0x08; -const OP_REPLACE_PLACEHOLDER = 0x09; -const OP_INSERT_AFTER = 0x0a; -const OP_INSERT_BEFORE = 0x0b; -const OP_SET_ATTRIBUTE = 0x0c; -const OP_SET_TEXT = 0x0d; -const OP_NEW_EVENT_LISTENER = 0x0e; -const OP_REMOVE_EVENT_LISTENER = 0x0f; -const OP_REMOVE = 0x10; -const OP_PUSH_ROOT = 0x11; - -// attrval kinds — wit/world.wit set-attribute op. -const ATTR_TEXT = 0x00; -const ATTR_FLOAT = 0x01; -const ATTR_INT = 0x02; -const ATTR_BOOL = 0x03; -const ATTR_NONE = 0x04; - -// register-template node kinds — wit/world.wit register-template op. -const NODE_ELEMENT = 0x00; -const NODE_TEXT = 0x01; -const NODE_DYNAMIC = 0x02; - -const NONE_STRREF = 0xffff; - -/** - * Cursor over one batch's op segment + string segment. Strings are sliced - * sequentially by UTF-16 code-unit length off of `strings` (never a - * per-string TextDecoder call — wit "string segment" doc says the whole - * segment is decoded in one pass by the caller/FrameDecoder). - */ -class Cursor { - view: DataView; - bytes: Uint8Array; - off = 0; - strings: string; - strOff = 0; - - constructor(ops: Uint8Array, strings: string) { - this.bytes = ops; - this.view = new DataView(ops.buffer, ops.byteOffset, ops.byteLength); - this.strings = strings; - } - - u8(): number { - const v = this.view.getUint8(this.off); - this.off += 1; - return v; - } - u16(): number { - const v = this.view.getUint16(this.off, true); - this.off += 2; - return v; - } - u32(): number { - const v = this.view.getUint32(this.off, true); - this.off += 4; - return v; - } - s64(): bigint { - const v = this.view.getBigInt64(this.off, true); - this.off += 8; - return v; - } - f64(): number { - const v = this.view.getFloat64(this.off, true); - this.off += 8; - return v; - } - - /** strref: u16, 0xffff = none (wit "Primitive operand encodings"). */ - strref(): StrRef | null { - const v = this.u16(); - return v === NONE_STRREF ? null : v; - } - - /** path: u8 length, then that many u8 child indices. */ - path(): Uint8Array { - const len = this.u8(); - const p = this.bytes.subarray(this.off, this.off + len); - this.off += len; - return p; - } - - /** - * dynstr: u16 UTF-16 code-unit length, then (iff 0xffff) u32 actual - * length. Content is the next `length` code units of the decoded string - * segment, consumed sequentially (wit "Primitive operand encodings"). - */ - dynstr(): string { - let len = this.u16(); - if (len === 0xffff) { - len = this.u32(); - } - const s = this.strings.substring(this.strOff, this.strOff + len); - this.strOff += len; - return s; - } -} - -function decodeTemplateNode(c: Cursor): TemplateNodeDesc { - const kind = c.u8(); - switch (kind) { - case NODE_ELEMENT: { - const tag = c.u16(); - const ns = c.strref(); - const nattrs = c.u16(); - const attrs: { name: StrRef; ns: StrRef | null; value: string }[] = []; - for (let i = 0; i < nattrs; i++) { - const name = c.u16(); - const attrNs = c.strref(); - const value = c.dynstr(); - attrs.push({ name, ns: attrNs, value }); - } - const nchildren = c.u16(); - const children: TemplateNodeDesc[] = []; - for (let i = 0; i < nchildren; i++) { - children.push(decodeTemplateNode(c)); - } - return { kind: "element", tag, ns, attrs, children }; - } - case NODE_TEXT: { - const value = c.dynstr(); - return { kind: "text", value }; - } - case NODE_DYNAMIC: - return { kind: "dynamic" }; - default: - throw new Error(`decodeBatch: unknown template node kind ${kind}`); - } -} - -/** Decode ONE batch: `ops` op-segment bytes + already-decoded string segment. */ -export function decodeBatch(ops: Uint8Array, strings: string, sink: OpSink): void { - const c = new Cursor(ops, strings); - const len = ops.byteLength; - while (c.off < len) { - const opcode = c.u8(); - switch (opcode) { - case OP_CACHE_STRING: { - const id = c.u16(); - const s = c.dynstr(); - sink.cacheString(id, s); - break; - } - case OP_REGISTER_TEMPLATE: { - const tmpl = c.u16(); - const nroots = c.u16(); - const roots: TemplateNodeDesc[] = []; - for (let i = 0; i < nroots; i++) { - roots.push(decodeTemplateNode(c)); - } - sink.registerTemplate(tmpl, roots); - break; - } - case OP_APPEND_CHILDREN: { - const id = c.u32(); - const m = c.u32(); - sink.appendChildren(id, m); - break; - } - case OP_ASSIGN_ID: { - const path = c.path(); - const id = c.u32(); - sink.assignId(path, id); - break; - } - case OP_CREATE_PLACEHOLDER: { - const id = c.u32(); - sink.createPlaceholder(id); - break; - } - case OP_CREATE_TEXT_NODE: { - const id = c.u32(); - const text = c.dynstr(); - sink.createTextNode(id, text); - break; - } - case OP_LOAD_TEMPLATE: { - const tmpl = c.u16(); - const root = c.u16(); - const id = c.u32(); - sink.loadTemplate(tmpl, root, id); - break; - } - case OP_REPLACE_WITH: { - const id = c.u32(); - const m = c.u32(); - sink.replaceWith(id, m); - break; - } - case OP_REPLACE_PLACEHOLDER: { - const path = c.path(); - const m = c.u32(); - sink.replacePlaceholder(path, m); - break; - } - case OP_INSERT_AFTER: { - const id = c.u32(); - const m = c.u32(); - sink.insertAfter(id, m); - break; - } - case OP_INSERT_BEFORE: { - const id = c.u32(); - const m = c.u32(); - sink.insertBefore(id, m); - break; - } - case OP_SET_ATTRIBUTE: { - const id = c.u32(); - const name = c.u16(); - const ns = c.strref(); - const attrKind = c.u8(); - switch (attrKind) { - case ATTR_TEXT: - sink.setAttributeText(id, name, ns, c.dynstr()); - break; - case ATTR_FLOAT: - sink.setAttributeFloat(id, name, ns, c.f64()); - break; - case ATTR_INT: - sink.setAttributeInt(id, name, ns, c.s64()); - break; - case ATTR_BOOL: - sink.setAttributeBool(id, name, ns, c.u8() !== 0); - break; - case ATTR_NONE: - sink.setAttributeNone(id, name, ns); - break; - default: - throw new Error(`decodeBatch: unknown attrval kind ${attrKind}`); - } - break; - } - case OP_SET_TEXT: { - const id = c.u32(); - const text = c.dynstr(); - sink.setText(id, text); - break; - } - case OP_NEW_EVENT_LISTENER: { - const id = c.u32(); - const name = c.u16(); - // flags:u8, bit0 = bubbles (dioxus-html event_bubbles verdict, - // guest-computed); bits 1..7 reserved. - const bubbles = (c.u8() & 1) !== 0; - sink.newEventListener(id, name, bubbles); - break; - } - case OP_REMOVE_EVENT_LISTENER: { - const id = c.u32(); - const name = c.u16(); - const bubbles = (c.u8() & 1) !== 0; - sink.removeEventListener(id, name, bubbles); - break; - } - case OP_REMOVE: { - const id = c.u32(); - sink.remove(id); - break; - } - case OP_PUSH_ROOT: { - const id = c.u32(); - sink.pushRoot(id); - break; - } - default: - throw new Error(`decodeBatch: unknown opcode 0x${opcode.toString(16)}`); - } - } -} - -const FRAME_DECODER = new TextDecoder("utf-8", { ignoreBOM: true }); - -/** - * Stream-transport framing layer — wit/world.wit "# Framing (stream - * transport only)": - * - * frame := frame-len:u32 strings-len:u32 strings:u8{strings-len} ops:u8{rest} - * frame-len = byte length of everything after the frame-len field - * = 4 + strings-len + len(ops) - */ -export class FrameDecoder { - #sink: OpSink; - #staged: Uint8Array | null = null; - - constructor(sink: OpSink) { - this.#sink = sink; - } - - pending(): number { - return this.#staged ? this.#staged.byteLength : 0; - } - - /** - * Consume as many whole frames as available from `bytes` (optionally - * prefixed by previously staged bytes). Returns the number of bytes of - * `bytes` itself that are accounted for and need NOT be passed to - * stashRest: - * - * - If no frame could be completed even with `bytes` appended to the - * staged carry (invariant: staged never holds a complete frame, so - * this only happens when zero frames complete), this method already - * copies staged+bytes into its own internal state and returns - * `bytes.byteLength` (i.e. "fully absorbed" — satisfies the - * direct-read "never acknowledge zero bytes" requirement without the - * caller having to do anything). - * - Otherwise (progress was made: at least one frame completed), any - * undecoded tail is purely a suffix of `bytes` (the staged carry, if - * any, is fully consumed as a prefix). This method does NOT copy that - * tail itself; it returns the count of `bytes` consumed by whole - * frames, and the caller must call `stashRest(bytes, thatCount)` to - * preserve the remainder before the view becomes invalid. - */ - feed(bytes: Uint8Array): number { - let buf: Uint8Array; - let stagedLen = 0; - if (this.#staged) { - stagedLen = this.#staged.byteLength; - buf = new Uint8Array(stagedLen + bytes.byteLength); - buf.set(this.#staged, 0); - buf.set(bytes, stagedLen); - this.#staged = null; - } else { - buf = bytes; - } - - let off = 0; - // `buf` never changes inside the loop (only `off` advances), so the view - // is hoisted rather than reconstructed per frame. - const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); - while (true) { - if (buf.byteLength - off < 8) break; // need frame-len + strings-len - const frameLen = view.getUint32(off, true); - const total = 4 + frameLen; // frame-len field itself + its payload - if (buf.byteLength - off < total) break; // partial frame - - const stringsLen = view.getUint32(off + 4, true); - const stringsStart = off + 8; - const stringsEnd = stringsStart + stringsLen; - const opsEnd = off + total; - - const stringsBytes = buf.subarray(stringsStart, stringsEnd); - const strings = FRAME_DECODER.decode(stringsBytes); - const ops = buf.subarray(stringsEnd, opsEnd); - decodeBatch(ops, strings, this.#sink); - - off += total; - } - - if (off === 0 && stagedLen > 0) { - // No progress at all: retain the full merged carry ourselves (buf is - // already a fresh copy when staged existed) and report `bytes` as - // fully absorbed so the caller need not (but safely may) stashRest. - this.#staged = buf; - return bytes.byteLength; - } - if (off === 0) { - // No staged carry, no progress: same as above but nothing to merge. - // Do NOT self-copy here — let the caller stashRest the tail (keeps - // the "caller stashes `bytes`' own tail" invariant uniform). - return 0; - } - - // Progress was made past any staged carry (invariant: stagedLen never - // holds a complete frame, so off > 0 implies off > stagedLen here). - const consumedFromBytes = off - stagedLen; - this.#staged = null; - return consumedFromBytes; - } - - /** - * Copy `bytes.subarray(offset)` into internal staging, prepended to the - * next feed(). Used by the caller for whatever tail feed() did not - * report as consumed (direct-read callback contract: must not - * acknowledge zero bytes while a partial frame remains parked). - */ - stashRest(bytes: Uint8Array, offset: number): void { - const tail = bytes.subarray(offset); - if (tail.byteLength === 0) return; - if (this.#staged) { - const merged = new Uint8Array(this.#staged.byteLength + tail.byteLength); - merged.set(this.#staged, 0); - merged.set(tail, this.#staged.byteLength); - this.#staged = merged; - } else { - this.#staged = new Uint8Array(tail); // copy, per contract - } - } -} diff --git a/host/src/dispatch.ts b/host/src/dispatch.ts index 567f8b1..e1e8e82 100644 --- a/host/src/dispatch.ts +++ b/host/src/dispatch.ts @@ -15,16 +15,16 @@ // // 2. A scheduler-driven mutation flush. When the guest re-renders off its // own timer/async work — no `handle-event` in flight at all — the -// mutation frames arrive through the stream's direct-read `consume` -// callback, and DOM application happens synchronously INSIDE that -// callback, i.e. inside the guest's stream-write rendezvous. A live -// guest activation is on the stack: the instance's reentrance bracket -// is turn-scoped, taken around every thread resumption and released -// only when the thread parks (.deps/polyengine/runtime/src/task/ -// thread.ts `resumeWith`, and exec/boundary.ts's matching bracket). So -// a native event fired by the mutation itself would enter the guest -// from inside a live turn — the same trap, with nothing "in flight" -// by the window-1 bookkeeping. +// mutation batch arrives through the mutation stream's read loop +// (host.ts's `await ops.read()` resumption), and DOM application +// happens synchronously INSIDE that resumption, i.e. inside the +// guest's stream-write rendezvous. A live guest activation is on the +// stack: the instance's reentrance bracket is turn-scoped, taken +// around every thread resumption and released only when the thread +// parks (.deps/polyengine/runtime/src/task/thread.ts `resumeWith`, and +// exec/boundary.ts's matching bracket). So a native event fired by the +// mutation itself would enter the guest from inside a live turn — the +// same trap, with nothing "in flight" by the window-1 bookkeeping. // // 3. A host IMPORT invoked by the guest. `dom.set-focus` (wit/world.wit, // `interface dom`) runs host code while the guest that called it is @@ -36,15 +36,12 @@ // brackets its body with `beginApply`/`endApply`, so dispatches raised // by the focus change queue and drain once the guest's turn unwinds. // -// Window 2 is also forbidden outright by contract, independently of the -// trap: .deps/polyengine/contracts/embedder-api.md amendment A21 ("Streams -// and futures", direct-read scoping) — "Inside the callback, calls that can -// run guest code or operate this stream are forbidden (reentrancy)". A21 -// gives a second, memory-safety reason: the `DirectSource` view aliases -// guest linear memory, and views are re-derived only per `remaining()` call -// ("a `memory.grow` **between** rendezvous never yields a stale view" — -// within one rendezvous there is no re-derivation). Guest code run -// mid-callback could `memory.grow` and detach the buffer under the decoder. +// Window 2 also risks a memory-safety hazard if guest code were run inside +// it: `applyOperations` reads lifted values already copied out of guest +// linear memory by the embedder's lift step (contracts/embedder-api.md +// "Value mapping"), so unlike a raw-bytes direct-read view there is no +// aliasing hazard here — but running guest code mid-application would still +// reenter the instance, which is what the gate exists to prevent. // // Why deferring to a microtask is sound: microtasks run only when the JS // stack is empty, and a guest turn holds the stack until it parks or @@ -65,7 +62,7 @@ /** * Serializes guest entries so that at most one is live and none is attempted * inside a forbidden window (see the module comment: in-flight call, - * mutation application inside the direct-read callback, or a host import + * mutation application inside the read loop's resumption, or a host import * running inside the calling guest's turn). * * Pure logic — no DOM, no runtime imports — so the ordering rules can be @@ -77,7 +74,7 @@ export class DispatchGate { /** A guest `handle-event` call is in flight (its promise is unsettled). */ #busy = false; /** Inside a host-code window that runs within a live guest turn: the - * direct-read `consume` callback (mutation application), or the body of + * mutation read loop's chunk-application window (host.ts's `beginApply`/`endApply` around applyOperations), or the body of * a host import the guest called. */ #applying = false; #disposed = false; @@ -97,7 +94,7 @@ export class DispatchGate { /** Leave the mutation-application window and drain what it collected. * * The drain is deferred with `queueMicrotask` rather than run inline: - * `endApply` is called from the `finally` inside the direct-read callback, + * `endApply` is called from the `finally` inside the read loop's chunk-application window, * which is still on the guest's rendezvous stack — the instance is not * enterable until that turn unwinds. An empty JS stack is the observable * proxy for "no live guest turn" (module comment). The same holds for a diff --git a/host/src/events.ts b/host/src/events.ts index 52187d0..79ff1ec 100644 --- a/host/src/events.ts +++ b/host/src/events.ts @@ -583,7 +583,7 @@ function isObserverName(name: string): name is ObserverName { interface Registration { bubbles: boolean; /** The interned string id this listener was registered with - * (decoder.ts's StrRef) — wit/world.wit: "Event names cross back on + * (applier.ts's StrRef) — wit/world.wit: "Event names cross back on * handle-event as the same interned u16", so dispatch hands this same * id back rather than re-deriving it from the applier's string table * (applier.ts is consumed unchanged; this keeps the mapping local). */ @@ -661,8 +661,9 @@ export class EventDispatcher implements ListenerDelegate { // `mounted` likewise finds nothing and is harmless. // // Timing: `add()` is called during batch application, i.e. inside the - // DispatchGate's apply window (host.ts's direct-read `consume` brackets - // it with beginApply/endApply). The gate therefore QUEUES this + // DispatchGate's apply window (host.ts's mutation read loop brackets + // each chunk's `applyOperations` call with beginApply/endApply). The + // gate therefore QUEUES this // dispatch and drains it in a microtask after `endApply` — which is // exactly the contract's requirement that `mounted` fire after the // batch that created the element has been fully applied, with the node diff --git a/host/src/host.ts b/host/src/host.ts index e1fec97..54ef348 100644 --- a/host/src/host.ts +++ b/host/src/host.ts @@ -1,21 +1,22 @@ // Host runtime wiring for polymorph:dioxus — instantiation, the stream -// mutation transport, and DOM event dispatch back into the guest. +// mutation channel, and DOM event dispatch back into the guest. // // Governing docs: wit/world.wit (world `app`, interface `events`), and // .deps/polyengine/contracts/embedder-api.md ("Module wiring and -// instantiation", "Resources", "Streams and futures" amendment A21, "Value -// mapping"). Cited inline as `contract:
`. +// instantiation", "Resources", "Streams and futures", "Value mapping"). +// Cited inline as `contract:
`. import { instantiate } from "@deltic/runtime/embedder"; import { wasi } from "@polyengine/wasi"; import type { InstantiateSource } from "@deltic/runtime/embedder"; -import type { DirectSource, Stream } from "@deltic/protocol"; +import type { Stream } from "@deltic/protocol"; import { DomApplier } from "./applier.ts"; import { DispatchGate } from "./dispatch.ts"; -import { FrameDecoder } from "./decoder.ts"; import { EventDispatcher, serializePayload } from "./events.ts"; import type { NativeEventLike } from "./events.ts"; +import { applyOperations } from "./operations.ts"; +import type { Operation } from "./operations.ts"; export interface MountOptions { /** Component artifacts in either form `instantiate` accepts: a @@ -28,10 +29,10 @@ export interface MountOptions { source: InstantiateSource; root: Element; /** Asynchronous failure after a successful mount: the mutation stream's - * parked direct-read session rejecting (guest trap — `PeerTrappedError` — - * or teardown), or a `handle-event` call rejecting. A failure during mount - * itself is NOT routed here: `await exports.run()` rejects and `mountApp` - * throws it to the caller. */ + * read loop rejecting (guest trap — `PeerTrappedError` — or teardown), or + * a `handle-event` call rejecting. A failure during mount itself is NOT + * routed here: `await exports.run()` rejects and `mountApp` throws it to + * the caller. */ onError?: (err: unknown) => void; } @@ -46,17 +47,13 @@ export interface Mounted { /** Exposed for tests: the event dispatcher wired as the applier's * ListenerDelegate. */ dispatcher: EventDispatcher; - /** Exposed for tests: the stream-transport frame decoder. Lets a - * test confirm the zero-copy direct-read path actually engaged — - * `pending()` returns 0 once every delivered byte has been decoded into - * whole frames, with no heavier instrumentation needed. */ - frameDecoder: FrameDecoder; /** Dispatch a native-event-like value at `targetEl` for `name`, exactly * as a real DOM listener would (used by fullstack tests and by real * event listeners alike). */ dispatch(targetEl: Element | null, name: string, ev: NativeEventLike): void; } + /** * A host-implemented resource class for `events.dom-event` * (contracts/embedder-api.md "Resources": "the host provides a plain class @@ -303,9 +300,9 @@ export function createDomImports(applier: DomApplier, gate: DispatchGate) { * `ListenerDelegate`), instantiates the component with the `events`/`dom` * imports wired per contracts/embedder-api.md "Module wiring and * instantiation" (imports keyed by the verbatim interface id), awaits - * `run()` for the mutation stream's read end, and parks a direct-read - * session on it for the life of the instance. Returns a handle for - * dispatching DOM events and (best-effort) tearing down. + * `run()` for the mutation stream's read end, and starts a read loop over + * it for the life of the instance. Returns a handle for dispatching DOM + * events and (best-effort) tearing down. * * Because the read end now comes back as `run`'s return value rather than * through a host import, a mount-time guest trap rejects THIS await and is @@ -318,7 +315,6 @@ export async function mountApp(opts: MountOptions): Promise { dispatchEvent(elementId, nameId, name, ev); }); const applier = new DomApplier(opts.root, dispatcher); - const frameDecoder = new FrameDecoder(applier); let disposed = false; const onError = opts.onError ?? (() => {}); @@ -351,16 +347,15 @@ export async function mountApp(opts: MountOptions): Promise { // // (2) While mutations are being APPLIED, with no `handle-event` in flight // at all. A scheduler-driven flush (guest timer/async re-render) - // delivers frames through the mutation stream's direct-read `consume` - // callback below, and DOM application runs synchronously inside it — - // i.e. inside the guest's stream-write rendezvous, with a live guest - // turn on the stack (the reentrance bracket is turn-scoped: taken - // around every thread resumption, released when the thread parks — + // delivers a batch through the mutation read loop below, and DOM + // application (`applyOperations`) runs synchronously inside that + // loop's `await ... read()` resumption — i.e. inside the guest's + // stream-write rendezvous, with a live guest turn on the stack (the + // reentrance bracket is turn-scoped: taken around every thread + // resumption, released when the thread parks — // .deps/polyengine/runtime/src/task/thread.ts). A native event fired // by the mutation itself would enter the guest straight out of the - // rendezvous. That is forbidden outright by embedder-api.md amendment - // A21 ("Inside the callback, calls that can run guest code or operate - // this stream are forbidden (reentrancy)") as well as trapping. + // rendezvous — forbidden outright, trap or not. // // Fix: serialize entries into the guest through `DispatchGate` (see // ./dispatch.ts for the full rationale, including why a microtask- @@ -389,78 +384,81 @@ export async function mountApp(opts: MountOptions): Promise { ...wasi(), // Keyed by the verbatim interface id (contract:"Module wiring and // instantiation"), so the version tracks the WIT package version — - // now 0.4.0. `events`' sole host-implemented item is the `dom-event` + // now 0.5.0. `events`' sole host-implemented item is the `dom-event` // resource, named by its bindgen-emitted UpperCamel name // (contract:"Resources"); `dom`'s items are functions, named by their // bindgen-emitted lowerCamel names. - "polymorph:dioxus/events@0.4.0": { DomEvent }, - "polymorph:dioxus/dom@0.4.0": createDomImports(applier, gate), + "polymorph:dioxus/events@0.5.0": { DomEvent }, + "polymorph:dioxus/dom@0.5.0": createDomImports(applier, gate), }; const instance = await instantiate(opts.source, imports); handleEventExport = instance.exports.handleEvent as (...a: unknown[]) => unknown; - // `run` starts the app and returns the mutation channel's read end; its + // `run` starts the app and returns the mutation stream's read end; the // promise settles as soon as the guest hands the reader back (the app's // scheduler keeps running as a spawned guest task). A trap before the // return rejects here and propagates out of `mountApp`. - const ops = await (instance.exports.run as () => Promise>)(); - - // Park a direct-read session for the instance's lifetime. We always consume - // the FULL view per rendezvous — whole frames decoded+applied, any partial - // tail staged in the FrameDecoder — so `readDirect`'s "never acknowledge - // zero bytes" hazard (embedder-api.md amendment A21) never arises: - // `markRead` always receives `view.length`, never 0. - const consume = (src: DirectSource): "more" | "done" => { - const view = src.remaining(); - // The callback runs DOM application only (decodeBatch via the - // FrameDecoder) — no direct guest call. But DOM application can fire - // synchronous NATIVE events (detaching a focused element fires - // `focusout`), whose listeners would dispatch into the guest from - // inside this rendezvous. The gate enforces the A21 rule ("calls that - // can run guest code ... are forbidden" inside a direct-read callback) - // transitively: dispatches raised in this window are queued and drained - // by a microtask, once the rendezvous' guest turn has unwound. - gate.beginApply(); - try { - const n = frameDecoder.feed(view); - if (n < view.length) { - frameDecoder.stashRest(view, n); + const ops = await (instance.exports.run as () => Promise>)(); + + // The guest scheduler's persistent park between renders needs SOME + // host-side reason the store's deadlock verdict stays suppressed. + // wit/world.wit's `run` doc, citing .deps/polyengine/contracts/ + // embedder-api.md §"Streams and futures", issue #162 + // ("Deadlock-verdict suppression tracks host retention"): suppression + // holds "while the host retains a way to act on a stream/future — a + // retained end, a parked host operation, or an unfinished producer pump". + // That rule is disjunctive, and what applies here is the first disjunct, + // not the second: `mountApp` holds the lifted readable end (`ops`) for + // the instance's whole lifetime — never lowered back into the guest — + // which is retention-by-itself, independent of whether a `read()` happens + // to be in flight at any given instant. (Runtime-side, this is + // `HostActivity` arming on the retained end and disarming only on a + // lower-back-to-guest — .deps/polyengine/runtime/src/exec/ + // host_streams.ts.) So there being a brief gap with no read outstanding + // (the await resumption between one `read()`'s chunk landing and + // `applyOperations` finishing, before the next `read()` is issued) is not + // itself a hazard: retention already covers it. + // + // The next read is still issued immediately after applying a chunk, with + // no unnecessary work in between — good practice for latency, not a + // correctness requirement. + // + // MAX_READ must be large enough that a whole batch (up to ~40k operations + // for the 10k-row bench case) arrives in one chunk. + const MAX_READ = 1 << 22; + (async () => { + while (!disposed) { + const chunk = await ops.read(MAX_READ); + if (chunk.length === 0) break; // end of stream + gate.beginApply(); + try { + applyOperations(chunk, applier); + } finally { + gate.endApply(); } - } finally { - gate.endApply(); } - src.markRead(view.length); - return "more"; - }; - // The session only settles on stream end/drop/fault, which for a healthy - // long-lived app never happens in normal operation. Route a rejection (peer - // trap, teardown) to onError rather than letting it become unhandled. - ops.readDirect(consume).catch((err: unknown) => { + })().catch((err: unknown) => { if (!disposed) onError(err); }); const mounted: Mounted = { applier, dispatcher, - frameDecoder, dispatch(targetEl, name, ev) { dispatcher.dispatchTo(targetEl, name, ev); }, dispose() { // Per-STREAM disposal is the documented release path: `Stream` // exposes `drop()` (.deps/polyengine/protocol/src/handles.ts:68-82, - // "`[Symbol.dispose]` alias"), and embedder-api.md amendment A21 - // makes reader-drop the designed teardown handshake — "reader/writer - // drop resolves the session with its total ... a resolution the - // producer's own `done` did not cause is the reader-gone signal". - // So dropping the read end RESOLVES the parked direct-read session - // (a resolution, not a rejection — `onError` stays silent, and the - // `!disposed` guard on the catch is belt-and-braces), and the guest - // observes reader-gone on its next write (its driver detects leftover - // bytes from `write_all`, sets its `dead` flag, and discards further - // batches with bounded memory — src/driver.rs), so it goes dark. + // "`[Symbol.dispose]` alias"), and dropping the read end resolves the + // read loop's next `read()` with `done` (a resolution, not a + // rejection — `onError` stays silent, and the `!disposed` guard on + // the catch is belt-and-braces), and the guest observes reader-gone + // on its next write (its driver detects leftover bytes from + // `write_all`, sets its `dead` flag, and discards further batches + // with bounded memory — src/driver.rs), so it goes dark. // // Instance-level disposal still does not exist in the embedder API // (`EmbedderInstance` is `{ exports, handle, imports }`), so the diff --git a/host/src/mod.ts b/host/src/mod.ts index 6a29cea..fc86df0 100644 --- a/host/src/mod.ts +++ b/host/src/mod.ts @@ -1,10 +1,10 @@ // Barrel module for the polymorph:dioxus host runtime. -export { decodeBatch, FrameDecoder } from "./decoder.ts"; -export type { OpSink, StrRef, TemplateNodeDesc } from "./decoder.ts"; - export { DomApplier } from "./applier.ts"; -export type { ListenerDelegate } from "./applier.ts"; +export type { ListenerDelegate, OpSink, StrRef, TemplateNodeDesc } from "./applier.ts"; + +export { applyOperations } from "./operations.ts"; +export type { Operation } from "./operations.ts"; export { EventDispatcher, serializePayload } from "./events.ts"; export type { DispatchSink, NativeEventLike } from "./events.ts"; diff --git a/host/src/operations.ts b/host/src/operations.ts new file mode 100644 index 0000000..181aeae --- /dev/null +++ b/host/src/operations.ts @@ -0,0 +1,263 @@ +// Applies the polymorph:dioxus mutation schema (`interface mutations` / +// `stream`, wit/world.wit — the normative, and only, encoding of +// a mutation) to a DOM applier. +// +// Walks already-lifted `operation` values (as polyengine's embedder lifts +// them per .deps/polyengine/contracts/embedder-api.md "Value mapping") and +// drives an `OpSink`. + +import type { OpSink, StrRef, TemplateNodeDesc } from "./applier.ts"; + +// -- lifted-value shapes ---------------------------------------------------- +// +// contract:"Value mapping" — variant -> { kind, value? }, record -> plain +// object with camelCased fields, option as a record field -> absent when +// none / bare T when some, list -> Uint8Array, list -> number[], +// s64 -> bigint, f64/u32/u16 -> number, bool -> boolean, string -> string. + +interface TemplateAttrLifted { + name: StrRef; + ns?: StrRef; + value: string; +} + +interface TemplateElementLifted { + tag: StrRef; + ns?: StrRef; + attrs: TemplateAttrLifted[]; + children: number[]; +} + +type TemplateNodeLifted = + | { kind: "element"; value: TemplateElementLifted } + | { kind: "text"; value: string } + | { kind: "dynamic" }; + +interface RegisterTemplateLifted { + id: number; + nodes: TemplateNodeLifted[]; + roots: number[]; +} + +interface StackOpLifted { + id: number; + m: number; +} +interface PathOpLifted { + path: Uint8Array; + m: number; +} +interface AssignIdLifted { + path: Uint8Array; + id: number; +} +interface CreateTextNodeLifted { + id: number; + text: string; +} +interface LoadTemplateLifted { + id: number; + tmpl: number; + root: number; +} +interface SetTextLifted { + id: number; + text: string; +} + +type AttrValueLifted = + | { kind: "text"; value: string } + | { kind: "float"; value: number } + | { kind: "int"; value: bigint } + | { kind: "boolean"; value: boolean } + | { kind: "none" }; + +interface SetAttributeLifted { + id: number; + name: StrRef; + ns?: StrRef; + value: AttrValueLifted; +} + +interface EventListenerLifted { + id: number; + name: StrRef; + bubbles: boolean; +} + +interface CacheStringLifted { + id: number; + str: string; +} + +/** One lifted `operation` variant value. */ +export type Operation = + | { kind: "cache-string"; value: CacheStringLifted } + | { kind: "register-template"; value: RegisterTemplateLifted } + | { kind: "append-children"; value: StackOpLifted } + | { kind: "assign-id"; value: AssignIdLifted } + | { kind: "create-placeholder"; value: number } + | { kind: "create-text-node"; value: CreateTextNodeLifted } + | { kind: "load-template"; value: LoadTemplateLifted } + | { kind: "replace-with"; value: StackOpLifted } + | { kind: "replace-placeholder"; value: PathOpLifted } + | { kind: "insert-after"; value: StackOpLifted } + | { kind: "insert-before"; value: StackOpLifted } + | { kind: "set-attribute"; value: SetAttributeLifted } + | { kind: "set-text"; value: SetTextLifted } + | { kind: "new-event-listener"; value: EventListenerLifted } + | { kind: "remove-event-listener"; value: EventListenerLifted } + | { kind: "remove"; value: number } + | { kind: "push-root"; value: number }; + +/** + * Rehydrate a `register-template` arena (`nodes` flat pre-order list, + * `roots`/`template-element.children` are `u32` indices into it — WIT + * forbids the natural recursive shape, see wit/world.wit's `mutations` + * interface doc) into the recursive `TemplateNodeDesc` tree `OpSink. + * registerTemplate` wants. + * + * The arena admits malformed index graphs a natural recursive shape could + * not express. Two are rejected outright, with a thrown Error rather than a + * hang or a silently + * wrong tree: an out-of-range index, and a cycle (a node that (transitively) + * indexes itself as a child — `state[idx] === 1` below, "on the current + * build path"). A THIRD shape is accepted rather than rejected: a DAG, where + * one node index is reachable as a child from two different parents. That + * is not a cycle (the build terminates), and the `built` memo below makes + * it safe — `build(idx)` is memoized once `state[idx] === 2`, so a + * DAG-shared node is built once and the resulting `TemplateNodeDesc` object + * is aliased into both parents' `children` arrays, rather than double-built + * or refused. `DomApplier.registerTemplate` only reads the desc tree to + * construct fresh DOM nodes per visit (never mutates or identity-compares + * the desc objects), so an aliased subtree is indistinguishable from two + * independently-built ones to every consumer. + */ +function rehydrateTemplateArena(nodes: TemplateNodeLifted[], roots: number[]): TemplateNodeDesc[] { + // 0 = unvisited, 1 = on the current path (cycle detection), 2 = done. + const state = new Uint8Array(nodes.length); + const built: (TemplateNodeDesc | undefined)[] = new Array(nodes.length); + + function build(idx: number): TemplateNodeDesc { + if (idx < 0 || idx >= nodes.length) { + throw new Error(`applyOperations: register-template arena index ${idx} out of range (${nodes.length} nodes)`); + } + if (state[idx] === 1) { + throw new Error(`applyOperations: register-template arena has a cycle at index ${idx}`); + } + if (state[idx] === 2) { + return built[idx]!; + } + state[idx] = 1; + const n = nodes[idx]; + let desc: TemplateNodeDesc; + switch (n.kind) { + case "element": { + const el = n.value; + desc = { + kind: "element", + tag: el.tag, + ns: el.ns ?? null, + attrs: el.attrs.map((a) => ({ name: a.name, ns: a.ns ?? null, value: a.value })), + children: el.children.map(build), + }; + break; + } + case "text": + desc = { kind: "text", value: n.value }; + break; + case "dynamic": + desc = { kind: "dynamic" }; + break; + } + state[idx] = 2; + built[idx] = desc; + return desc; + } + + return roots.map(build); +} + +/** Apply a batch of lifted `operation` values to `sink`. */ +export function applyOperations(ops: Operation[], sink: OpSink): void { + for (const op of ops) { + switch (op.kind) { + case "cache-string": + sink.cacheString(op.value.id, op.value.str); + break; + case "register-template": { + const { id, nodes, roots } = op.value; + sink.registerTemplate(id, rehydrateTemplateArena(nodes, roots)); + break; + } + case "append-children": + sink.appendChildren(op.value.id, op.value.m); + break; + case "assign-id": + sink.assignId(op.value.path, op.value.id); + break; + case "create-placeholder": + sink.createPlaceholder(op.value); + break; + case "create-text-node": + sink.createTextNode(op.value.id, op.value.text); + break; + case "load-template": + sink.loadTemplate(op.value.tmpl, op.value.root, op.value.id); + break; + case "replace-with": + sink.replaceWith(op.value.id, op.value.m); + break; + case "replace-placeholder": + sink.replacePlaceholder(op.value.path, op.value.m); + break; + case "insert-after": + sink.insertAfter(op.value.id, op.value.m); + break; + case "insert-before": + sink.insertBefore(op.value.id, op.value.m); + break; + case "set-attribute": { + const { id, name, ns, value } = op.value; + const nsResolved = ns ?? null; + switch (value.kind) { + case "text": + sink.setAttributeText(id, name, nsResolved, value.value); + break; + case "float": + sink.setAttributeFloat(id, name, nsResolved, value.value); + break; + case "int": + sink.setAttributeInt(id, name, nsResolved, value.value); + break; + case "boolean": + sink.setAttributeBool(id, name, nsResolved, value.value); + break; + case "none": + sink.setAttributeNone(id, name, nsResolved); + break; + } + break; + } + case "set-text": + sink.setText(op.value.id, op.value.text); + break; + case "new-event-listener": + sink.newEventListener(op.value.id, op.value.name, op.value.bubbles); + break; + case "remove-event-listener": + sink.removeEventListener(op.value.id, op.value.name, op.value.bubbles); + break; + case "remove": + sink.remove(op.value); + break; + case "push-root": + sink.pushRoot(op.value); + break; + default: { + const _exhaustive: never = op; + throw new Error(`applyOperations: unknown operation "${(_exhaustive as { kind: string }).kind}"`); + } + } + } +} diff --git a/host/tests/applier_test.ts b/host/tests/applier_test.ts index 16249c1..21e1c1a 100644 --- a/host/tests/applier_test.ts +++ b/host/tests/applier_test.ts @@ -1,7 +1,7 @@ import { assertEquals } from "jsr:@std/assert@1"; import { parseHTML } from "linkedom"; import { DomApplier, type ListenerDelegate } from "../src/applier.ts"; -import type { TemplateNodeDesc } from "../src/decoder.ts"; +import type { TemplateNodeDesc } from "../src/applier.ts"; function makeRoot() { const { document } = parseHTML("
"); diff --git a/host/tests/counter_test.ts b/host/tests/counter_test.ts index 40e777b..6f8b6df 100644 --- a/host/tests/counter_test.ts +++ b/host/tests/counter_test.ts @@ -1,10 +1,9 @@ // Full-stack host-runtime test for the REAL Dioxus counter example -// (examples/counter/src/lib.rs, launch! uses the stream transport, the only -// transport). This +// (examples/counter/src/lib.rs, launch! uses the mutation stream). This // exercises the whole pipeline: driver.rs (run/handle-event tasks), // writer.rs (mutation batching), events.rs (payload conversion), and the -// host-side applier/dispatcher/decoder — none of which had ever executed -// end to end before this test existed. +// host-side applier/dispatcher/operations applier — none of which had ever +// executed end to end before this test existed. // // Requires `just example counter` to have built // examples/build/counter.component.wasm first. @@ -176,9 +175,9 @@ Deno.test("counter example: mount, click, type, list, form submit", async () => // 7) After dispose the runtime is detached: a further dispatch changes // nothing and surfaces no error. Dropping the mutation stream's read end - // RESOLVES the parked direct-read session (embedder-api.md A21's - // reader-drop rule) rather than rejecting it, so `onError` stays silent; - // the guest sees reader-gone on its next write and goes dark. + // resolves the read loop's next `read()` with `done` rather than + // rejecting it, so `onError` stays silent; the guest sees reader-gone on + // its next write and goes dark. const before = root.innerHTML; const postDispose = click(inc); mounted.dispatch(inc, "click", postDispose); diff --git a/host/tests/decoder_test.ts b/host/tests/decoder_test.ts deleted file mode 100644 index 22b8d42..0000000 --- a/host/tests/decoder_test.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { assertEquals } from "jsr:@std/assert@1"; -import { decodeBatch, FrameDecoder, type OpSink } from "../src/decoder.ts"; - -// Recording sink: turns OpSink calls into plain JSON-shaped op records -// matching the golden-vector expected.json format (see dispatch doc). -function recordingSink(ops: unknown[]): OpSink { - return { - cacheString(id, s) { - ops.push({ op: "cache-string", id, s }); - }, - registerTemplate(tmpl, roots) { - ops.push({ op: "register-template", tmpl, roots }); - }, - appendChildren(id, m) { - ops.push({ op: "append-children", id, m }); - }, - assignId(path, id) { - ops.push({ op: "assign-id", path: Array.from(path), id }); - }, - createPlaceholder(id) { - ops.push({ op: "create-placeholder", id }); - }, - createTextNode(id, text) { - ops.push({ op: "create-text-node", id, text }); - }, - loadTemplate(tmpl, root, id) { - ops.push({ op: "load-template", tmpl, root, id }); - }, - replaceWith(id, m) { - ops.push({ op: "replace-with", id, m }); - }, - replacePlaceholder(path, m) { - ops.push({ op: "replace-placeholder", path: Array.from(path), m }); - }, - insertAfter(id, m) { - ops.push({ op: "insert-after", id, m }); - }, - insertBefore(id, m) { - ops.push({ op: "insert-before", id, m }); - }, - setAttributeText(id, name, ns, value) { - ops.push({ op: "set-attribute", id, name, ns, value: { kind: "text", s: value } }); - }, - setAttributeFloat(id, name, ns, value) { - ops.push({ op: "set-attribute", id, name, ns, value: { kind: "float", f: value } }); - }, - setAttributeInt(id, name, ns, value) { - ops.push({ op: "set-attribute", id, name, ns, value: { kind: "int", i: value.toString() } }); - }, - setAttributeBool(id, name, ns, value) { - ops.push({ op: "set-attribute", id, name, ns, value: { kind: "bool", b: value } }); - }, - setAttributeNone(id, name, ns) { - ops.push({ op: "set-attribute", id, name, ns, value: { kind: "none" } }); - }, - setText(id, text) { - ops.push({ op: "set-text", id, text }); - }, - newEventListener(id, name, bubbles) { - ops.push({ op: "new-event-listener", id, name, bubbles }); - }, - removeEventListener(id, name, bubbles) { - ops.push({ op: "remove-event-listener", id, name, bubbles }); - }, - remove(id) { - ops.push({ op: "remove", id }); - }, - pushRoot(id) { - ops.push({ op: "push-root", id }); - }, - }; -} - -// -- byte-encoding helpers for hand-built test frames ----------------------- - -class Writer { - chunks: number[] = []; - u8(v: number) { - this.chunks.push(v & 0xff); - } - u16(v: number) { - this.u8(v & 0xff); - this.u8((v >>> 8) & 0xff); - } - u32(v: number) { - this.u16(v & 0xffff); - this.u16((v >>> 16) & 0xffff); - } - s64(v: bigint) { - const buf = new ArrayBuffer(8); - new DataView(buf).setBigInt64(0, v, true); - this.chunks.push(...new Uint8Array(buf)); - } - f64(v: number) { - const buf = new ArrayBuffer(8); - new DataView(buf).setFloat64(0, v, true); - this.chunks.push(...new Uint8Array(buf)); - } - strref(v: number | null) { - this.u16(v === null ? 0xffff : v); - } - path(p: number[]) { - this.u8(p.length); - for (const x of p) this.u8(x); - } - bytes(): Uint8Array { - return new Uint8Array(this.chunks); - } -} - -// dynstr writer: appends UTF-8 bytes to the shared string-segment builder -// and writes the UTF-16 code-unit length (+ 0xffff escape + u32) into the op -// writer, per wit's dynstr encoding. -class StringSeg { - parts: string[] = []; - push(w: Writer, s: string) { - const len = s.length; // UTF-16 code units - if (len < 0xffff) { - w.u16(len); - } else { - w.u16(0xffff); - w.u32(len); - } - this.parts.push(s); - } - segment(): string { - return this.parts.join(""); - } -} - -function buildFrame(opsBytes: Uint8Array, strings: string): Uint8Array { - const stringBytes = new TextEncoder().encode(strings); - const frameLen = 4 + stringBytes.byteLength + opsBytes.byteLength; - const out = new Uint8Array(4 + frameLen); - const dv = new DataView(out.buffer); - dv.setUint32(0, frameLen, true); - dv.setUint32(4, stringBytes.byteLength, true); - out.set(stringBytes, 8); - out.set(opsBytes, 8 + stringBytes.byteLength); - return out; -} - -/** Hand-encode: cache-string(0,"x") ; create-text-node(1,"hi") ; - * set-attribute(1, name=0, ns=none, float=1.5) ; push-root(1). */ -function handEncodedOpsAndStrings(): { ops: Uint8Array; strings: string } { - const w = new Writer(); - const ss = new StringSeg(); - - w.u8(0x01); // cache-string - w.u16(0); - ss.push(w, "x"); - - w.u8(0x06); // create-text-node - w.u32(1); - ss.push(w, "hi"); - - w.u8(0x0c); // set-attribute - w.u32(1); - w.u16(0); - w.strref(null); - w.u8(0x01); // float - w.f64(1.5); - - w.u8(0x11); // push-root - w.u32(1); - - return { ops: w.bytes(), strings: ss.segment() }; -} - -Deno.test("decodeBatch: hand-encoded multi-op batch", () => { - const { ops, strings } = handEncodedOpsAndStrings(); - const recorded: unknown[] = []; - decodeBatch(ops, strings, recordingSink(recorded)); - assertEquals(recorded, [ - { op: "cache-string", id: 0, s: "x" }, - { op: "create-text-node", id: 1, text: "hi" }, - { op: "set-attribute", id: 1, name: 0, ns: null, value: { kind: "float", f: 1.5 } }, - { op: "push-root", id: 1 }, - ]); -}); - -Deno.test("FrameDecoder: split at every byte boundary matches unsplit decode", () => { - const { ops: ops1, strings: strings1 } = handEncodedOpsAndStrings(); - const frame1 = buildFrame(ops1, strings1); - - const w2 = new Writer(); - const ss2 = new StringSeg(); - w2.u8(0x01); - w2.u16(1); - ss2.push(w2, "y"); - w2.u8(0x10); // remove - w2.u32(1); - const frame2 = buildFrame(w2.bytes(), ss2.segment()); - - const combined = new Uint8Array(frame1.byteLength + frame2.byteLength); - combined.set(frame1, 0); - combined.set(frame2, frame1.byteLength); - - // unsplit baseline - const baseline: unknown[] = []; - { - const dec = new FrameDecoder(recordingSink(baseline)); - const consumed = dec.feed(combined); - assertEquals(consumed, combined.byteLength); - assertEquals(dec.pending(), 0); - } - - // split at every byte boundary - for (let cut = 0; cut <= combined.byteLength; cut++) { - const part1 = combined.subarray(0, cut); - const part2 = combined.subarray(cut); - const recorded: unknown[] = []; - const dec = new FrameDecoder(recordingSink(recorded)); - - const c1 = dec.feed(part1); - if (c1 < part1.byteLength) { - dec.stashRest(part1, c1); - } - const c2 = dec.feed(part2); - if (c2 < part2.byteLength) { - dec.stashRest(part2, c2); - } - - assertEquals(recorded, baseline, `mismatch at cut=${cut}`); - assertEquals(dec.pending(), 0, `pending leftover at cut=${cut}`); - } -}); - -Deno.test("dynstr: 0xffff length escape, empty string, surrogate pair, leading BOM", () => { - const w = new Writer(); - const ss = new StringSeg(); - - // Long ASCII string forcing the 0xffff escape (>= 0xffff UTF-16 units). - const long = "a".repeat(0xffff + 5); - w.u8(0x06); - w.u32(100); - ss.push(w, long); - - // Empty string. - w.u8(0x06); - w.u32(101); - ss.push(w, ""); - - // Surrogate pair (U+1F600 GRINNING FACE = 2 UTF-16 code units). - const emoji = "\u{1F600}"; - w.u8(0x06); - w.u32(102); - ss.push(w, emoji); - - // Leading U+FEFF must survive (ignoreBOM in FrameDecoder's TextDecoder). - const bomStr = "\uFEFFhello"; - w.u8(0x06); - w.u32(103); - ss.push(w, bomStr); - - const frame = buildFrame(w.bytes(), ss.segment()); - const recorded: unknown[] = []; - const dec = new FrameDecoder(recordingSink(recorded)); - const consumed = dec.feed(frame); - assertEquals(consumed, frame.byteLength); - - assertEquals(recorded, [ - { op: "create-text-node", id: 100, text: long }, - { op: "create-text-node", id: 101, text: "" }, - { op: "create-text-node", id: 102, text: emoji }, - { op: "create-text-node", id: 103, text: bomStr }, - ]); -}); - -// -- golden vectors ---------------------------------------------------------- - -const VECTORS_DIR = new URL("../../vectors/", import.meta.url); - -interface ExpectedVector { - frames: Record[][]; -} - -async function listVectorNames(): Promise { - const names: string[] = []; - try { - for await (const entry of Deno.readDir(VECTORS_DIR)) { - if (entry.isFile && entry.name.endsWith(".bin")) { - names.push(entry.name.slice(0, -".bin".length)); - } - } - } catch (e) { - if (!(e instanceof Deno.errors.NotFound)) throw e; - } - return names.sort(); -} - -function normalizeAttrValue(v: unknown): unknown { - const val = v as { kind: string; i?: unknown }; - if (val.kind === "int") { - return { kind: "int", i: BigInt(val.i as string | number).toString() }; - } - return val; -} - -function normalizeOp(op: Record): Record { - if (op.op === "set-attribute" && op.value) { - return { ...op, value: normalizeAttrValue(op.value) }; - } - return op; -} - -const vectorNames = await listVectorNames(); - -if (vectorNames.length === 0) { - Deno.test("golden vectors pending", { ignore: true }, () => {}); -} else { - for (const name of vectorNames) { - Deno.test(`golden vector: ${name}`, async () => { - const bin = await Deno.readFile(new URL(`${name}.bin`, VECTORS_DIR)); - const expectedText = await Deno.readTextFile(new URL(`${name}.expected.json`, VECTORS_DIR)); - const expected: ExpectedVector = JSON.parse(expectedText); - - const framesRecorded: unknown[][] = []; - let off = 0; - while (off < bin.byteLength) { - // Each expected "frame" entry is one wire frame's ops; parse the - // frame-len header ourselves so a single feed() call (which - // consumes as many WHOLE frames as are available) doesn't merge - // multiple wire frames into one recorded frame. - const dv = new DataView(bin.buffer, bin.byteOffset + off, bin.byteLength - off); - const frameLen = dv.getUint32(0, true); - const total = 4 + frameLen; - const frameBytes = bin.subarray(off, off + total); - - const recorded: unknown[] = []; - const frameSink = recordingSink(recorded); - const frameDec = new FrameDecoder(frameSink); - const consumed = frameDec.feed(frameBytes); - if (consumed !== frameBytes.byteLength) { - throw new Error( - `golden vector ${name}: expected to consume exactly one frame (${frameBytes.byteLength}B) at offset ${off}, consumed ${consumed}`, - ); - } - framesRecorded.push(recorded); - off += total; - } - - const actualFrames = framesRecorded.map((ops) => - ops.map((o) => normalizeOp(o as Record)) - ); - const expectedFrames = expected.frames.map((ops) => - ops.map((o) => normalizeOp(o)) - ); - assertEquals(actualFrames, expectedFrames); - }); - } -} diff --git a/host/tests/fullstack_test.ts b/host/tests/fullstack_test.ts index 69e7efb..27dd948 100644 --- a/host/tests/fullstack_test.ts +++ b/host/tests/fullstack_test.ts @@ -2,7 +2,6 @@ // real translator+runtime instantiation path, and exercises the round trip // described in fixtures/surface-probe/src/lib.rs's module doc (op sequence + // event summary format — the authority for the exact assertions below). -// // Requires `just fixtures` to have built // fixtures/build/surface-probe.component.wasm first. @@ -75,10 +74,6 @@ Deno.test("fullstack (stream transport): mount, event round trip, ordering", asy assertEquals(root.innerHTML, EXPECTED_INITIAL_HTML); assertEquals(errors, []); - // 5) The direct-read path decoded the whole delivered view into complete - // frames — nothing staged. - assertEquals(mounted.frameDecoder.pending(), 0); - const section = root.firstElementChild!; assertEquals(section.tagName, "SECTION"); const input = root.lastElementChild!; @@ -101,7 +96,6 @@ Deno.test("fullstack (stream transport): mount, event round trip, ordering", asy assertEquals(root.innerHTML.includes("click:mouse:7,42"), true); assertEquals(prevented, 1, "prevent-default called (buttons === 7)"); assertEquals(stopped, 1, "stop-propagation called (buttons === 7)"); - assertEquals(mounted.frameDecoder.pending(), 0); // 4a) Dispatch input with a value: assert echo. const inputEvent = { type: "input", value: "hi there" }; @@ -159,7 +153,6 @@ Deno.test("fullstack (stream transport): mount, event round trip, ordering", asy mounted.dispatch(section, "touchstart", touchStartEvent); await waitFor(() => root.innerHTML.includes(EXPECTED_TOUCH_SUMMARY), "touchstart summary"); assertEquals(root.innerHTML.includes(EXPECTED_TOUCH_SUMMARY), true); - assertEquals(mounted.frameDecoder.pending(), 0); assertEquals(errors, [], "no onError callback ever fired"); diff --git a/host/tests/operations_test.ts b/host/tests/operations_test.ts new file mode 100644 index 0000000..354861d --- /dev/null +++ b/host/tests/operations_test.ts @@ -0,0 +1,442 @@ +// Mounts the real counter example (examples/counter/src/lib.rs) and asserts +// the resulting `innerHTML` against expected literals at each step of a +// fixed interaction sequence, then unit-tests `applyOperations`'s riskier +// corners directly against a recording sink. +// +// AUTHORITY FOR THE EXPECTED LITERALS BELOW: captured from THIS repo's own +// mountApp output before the byte mutation channel (`run`/decodeBatch) was +// deleted, at a point where host/tests/typed_test.ts's differential test +// ("typed channel matches byte channel") was green — i.e. the byte and +// typed channels were proven to produce byte-for-byte identical DOM for +// this exact sequence. That equivalence makes the captured output a +// trustworthy oracle: this test pins today's (post-deletion) output to +// what was, at capture time, independently cross-checked against the other +// implementation. There is no live second implementation to diff against +// any more (deleting one side of a differential test removes its ability +// to prove anything), so an absolute assertion is what replaces it. + +import { assertEquals, assertNotEquals } from "jsr:@std/assert@1"; +import { parseHTML } from "linkedom"; +import { defaultTranslator } from "@deltic/translator"; +import { mountApp } from "../src/host.ts"; +import { applyOperations } from "../src/operations.ts"; +import type { OpSink, TemplateNodeDesc } from "../src/applier.ts"; + +const COMPONENT_PATH = "../../examples/build/counter.component.wasm"; + +async function waitFor(cond: () => boolean, what: string, maxIters = 2000): Promise { + for (let i = 0; i < maxIters; i++) { + if (cond()) return; + await new Promise((r) => setTimeout(r, 0)); + } + throw new Error(`waitFor timed out: ${what}`); +} + +function makeRoot() { + const { document } = parseHTML("
"); + return document.getElementById("root")!; +} + +async function loadComponentBytes(): Promise { + const url = new URL(COMPONENT_PATH, import.meta.url); + try { + return await Deno.readFile(url); + } catch (e) { + if (e instanceof Deno.errors.NotFound) { + throw new Error( + `component not found at ${url}. Run \`just example counter\` first ` + + `(builds examples/build/counter.component.wasm).`, + ); + } + throw e; + } +} + +function byId(root: Element, id: string): Element { + const el = root.querySelector(`#${id}`); + if (!el) throw new Error(`no element with id=${id} in ${root.innerHTML}`); + return el; +} + +interface TrackedEvent { + type: string; + clientX: number; + clientY: number; + button: number; + buttons: number; + preventDefault(): void; + stopPropagation(): void; +} + +function click(): TrackedEvent { + return { + type: "click", + clientX: 0, + clientY: 0, + button: 0, + buttons: 0, + preventDefault: () => {}, + stopPropagation: () => {}, + }; +} + +const EXPECTED_INITIAL = + '
' + + '0' + + '

count is 0

' + + '

' + + '' + + '
  • alpha
  • beta
' + + '
' + + '

submitted 0 time(s)

'; + +const EXPECTED_AFTER_INC1 = EXPECTED_INITIAL + .replace('0', '1') + .replace('class="even" id="parity">count is 0<', 'class="odd" id="parity">count is 1<'); + +const EXPECTED_AFTER_INC2 = EXPECTED_INITIAL + .replace('0', '2') + .replace('class="even" id="parity">count is 0<', 'class="even" id="parity">count is 2<'); + +const EXPECTED_AFTER_DEC = EXPECTED_AFTER_INC1; // 2 -> 1: back to the same rendered string as after the first inc + +const EXPECTED_AFTER_INC3 = EXPECTED_AFTER_INC2; // 1 -> 2: same rendered string as after the second inc + +const EXPECTED_AFTER_INPUT = EXPECTED_AFTER_INC3 + .replace('', '') + .replace('

', '

hello

'); + +const EXPECTED_AFTER_LIST = EXPECTED_AFTER_INPUT + .replace('
  • alpha
  • beta
', '
  • alpha
  • beta
  • item-0
'); + +const EXPECTED_AFTER_SUBMIT = EXPECTED_AFTER_LIST + .replace('

submitted 0 time(s)

', '

submitted 1 time(s)

'); + +Deno.test("mountApp + applyOperations: counter example, full interaction sequence, absolute DOM assertions", async () => { + const root = makeRoot(); + const componentBytes = await loadComponentBytes(); + const translator = await defaultTranslator(); + const errors: unknown[] = []; + const mounted = await mountApp({ + source: { componentBytes, translator }, + root, + onError: (err) => errors.push(err), + }); + await waitFor(() => root.querySelector("#count") !== null, "initial mount"); + assertEquals(root.innerHTML, EXPECTED_INITIAL, "initial mount"); + assertEquals(errors, []); + + // +/- buttons: set-text + set-attribute (class toggling even/odd). + const steps: [string, string][] = [ + ["inc", EXPECTED_AFTER_INC1], + ["inc", EXPECTED_AFTER_INC2], + ["dec", EXPECTED_AFTER_DEC], + ["inc", EXPECTED_AFTER_INC3], + ]; + let expectedCount = 0; + for (const [step, expected] of steps) { + expectedCount += step === "inc" ? 1 : -1; + const want = String(expectedCount); + mounted.dispatch(byId(root, step), "click", click()); + await waitFor(() => byId(root, "count").textContent === want, `click ${step}`); + assertEquals(root.innerHTML, expected, `after click ${step}`); + } + + // Typed text input: attribute of "text" kind (value) plus dynamic text. + mounted.dispatch(byId(root, "draft"), "input", { type: "input", value: "hello" }); + await waitFor(() => byId(root, "echo").textContent === "hello", "input"); + assertEquals(root.innerHTML, EXPECTED_AFTER_INPUT, "after typed input"); + + // List add/add/remove: keyed diff exercises load-template (nested + // template children: li > text) / assign-id / replace-placeholder / + // remove. + for (const step of ["add", "add", "remove"]) { + mounted.dispatch(byId(root, step), "click", click()); + } + await waitFor(() => root.querySelectorAll("#items li").length === 3, "list settle"); + assertEquals(root.innerHTML, EXPECTED_AFTER_LIST, "after list add/add/remove"); + + // Form submit: onsubmit calls prevent_default(); assert the DOM + // (submitted counter text) matches. + mounted.dispatch(byId(root, "form"), "submit", { ...click(), type: "submit" }); + await waitFor(() => byId(root, "submitted").textContent === "submitted 1 time(s)", "submit"); + assertEquals(root.innerHTML, EXPECTED_AFTER_SUBMIT, "after form submit"); + + assertEquals(errors, [], "no onError on the mutation channel"); + + mounted.dispose(); +}); + +// -- applyOperations risk areas: unit tests directly against a recording -- +// OpSink ------------------------------------------------------------------- +// +// The counter example above never happens to emit a template with more +// than one level of nesting under `register-template` per templates +// batch, nor a non-text `attr-value` case (dioxus's own attribute encoding +// only uses `text` for string-interpolated attrs, which is everything +// counter has) — so those two risks (the arena rehydration walk; the four +// non-text attr-value cases) are exercised here directly against a +// recording OpSink, independent of any guest. + +function recordingSink(ops: unknown[]): OpSink { + return { + cacheString(id, s) { + ops.push({ op: "cache-string", id, s }); + }, + registerTemplate(tmpl, roots) { + ops.push({ op: "register-template", tmpl, roots }); + }, + appendChildren(id, m) { + ops.push({ op: "append-children", id, m }); + }, + assignId(path, id) { + ops.push({ op: "assign-id", path: Array.from(path), id }); + }, + createPlaceholder(id) { + ops.push({ op: "create-placeholder", id }); + }, + createTextNode(id, text) { + ops.push({ op: "create-text-node", id, text }); + }, + loadTemplate(tmpl, root, id) { + ops.push({ op: "load-template", tmpl, root, id }); + }, + replaceWith(id, m) { + ops.push({ op: "replace-with", id, m }); + }, + replacePlaceholder(path, m) { + ops.push({ op: "replace-placeholder", path: Array.from(path), m }); + }, + insertAfter(id, m) { + ops.push({ op: "insert-after", id, m }); + }, + insertBefore(id, m) { + ops.push({ op: "insert-before", id, m }); + }, + setAttributeText(id, name, ns, value) { + ops.push({ op: "set-attribute-text", id, name, ns, value }); + }, + setAttributeFloat(id, name, ns, value) { + ops.push({ op: "set-attribute-float", id, name, ns, value }); + }, + setAttributeInt(id, name, ns, value) { + ops.push({ op: "set-attribute-int", id, name, ns, value }); + }, + setAttributeBool(id, name, ns, value) { + ops.push({ op: "set-attribute-bool", id, name, ns, value }); + }, + setAttributeNone(id, name, ns) { + ops.push({ op: "set-attribute-none", id, name, ns }); + }, + setText(id, text) { + ops.push({ op: "set-text", id, text }); + }, + newEventListener(id, name, bubbles) { + ops.push({ op: "new-event-listener", id, name, bubbles }); + }, + removeEventListener(id, name, bubbles) { + ops.push({ op: "remove-event-listener", id, name, bubbles }); + }, + remove(id) { + ops.push({ op: "remove", id }); + }, + pushRoot(id) { + ops.push({ op: "push-root", id }); + }, + }; +} + +Deno.test("applyOperations: register-template arena rehydrates nested children into a tree", () => { + const ops: unknown[] = []; + const sink = recordingSink(ops); + + // Arena for:
text0text1
+ // nodes[0] = div element, children = [1, 2, 3] + // nodes[1] = text "text0" + // nodes[2] = span element, children = [4] + // nodes[3] = dynamic + // nodes[4] = text "text1" + const nodes = [ + { kind: "element", value: { tag: 0, attrs: [], children: [1, 2, 3] } }, + { kind: "text", value: "text0" }, + { kind: "element", value: { tag: 1, attrs: [], children: [4] } }, + { kind: "dynamic" }, + { kind: "text", value: "text1" }, + ]; + applyOperations([{ kind: "register-template", value: { id: 7, nodes, roots: [0] } }] as never, sink); + + assertEquals(ops.length, 1); + const roots = (ops[0] as { roots: TemplateNodeDesc[] }).roots; + assertEquals(roots, [ + { + kind: "element", + tag: 0, + ns: null, + attrs: [], + children: [ + { kind: "text", value: "text0" }, + { + kind: "element", + tag: 1, + ns: null, + attrs: [], + children: [{ kind: "text", value: "text1" }], + }, + { kind: "dynamic" }, + ], + }, + ]); +}); + +Deno.test("applyOperations: register-template rejects an out-of-range arena index", () => { + const nodes = [{ kind: "element", value: { tag: 0, attrs: [], children: [99] } }]; + let threw = false; + try { + applyOperations( + [{ kind: "register-template", value: { id: 0, nodes, roots: [0] } }] as never, + recordingSink([]), + ); + } catch (e) { + threw = true; + assertEquals(e instanceof Error, true); + } + assertEquals(threw, true, "expected a thrown Error, not a silently wrong tree"); +}); + +Deno.test("applyOperations: register-template rejects a cyclic arena", () => { + // nodes[0].children includes 0 itself. + const nodes = [{ kind: "element", value: { tag: 0, attrs: [], children: [0] } }]; + let threw = false; + try { + applyOperations( + [{ kind: "register-template", value: { id: 0, nodes, roots: [0] } }] as never, + recordingSink([]), + ); + } catch (e) { + threw = true; + assertEquals(e instanceof Error, true); + } + assertEquals(threw, true, "expected a thrown Error, not a hang"); +}); + +Deno.test("applyOperations: set-attribute's non-text attr-value cases survive to the sink", () => { + const ops: unknown[] = []; + const sink = recordingSink(ops); + + applyOperations( + [ + { kind: "set-attribute", value: { id: 1, name: 2, value: { kind: "float", value: 1.5 } } }, + { kind: "set-attribute", value: { id: 1, name: 3, value: { kind: "int", value: 42n } } }, + { kind: "set-attribute", value: { id: 1, name: 4, value: { kind: "boolean", value: true } } }, + { kind: "set-attribute", value: { id: 1, name: 5, value: { kind: "none" } } }, + ] as never, + sink, + ); + + assertEquals(ops, [ + { op: "set-attribute-float", id: 1, name: 2, ns: null, value: 1.5 }, + { op: "set-attribute-int", id: 1, name: 3, ns: null, value: 42n }, + { op: "set-attribute-bool", id: 1, name: 4, ns: null, value: true }, + { op: "set-attribute-none", id: 1, name: 5, ns: null }, + ]); + // int arrives as bigint, matching OpSink.setAttributeInt's declared type. + assertEquals(typeof (ops[1] as { value: unknown }).value, "bigint"); +}); + +Deno.test("applyOperations: option ns absent lifts to null, not {kind:'none'}", () => { + const ops: unknown[] = []; + const sink = recordingSink(ops); + applyOperations( + [{ kind: "set-attribute", value: { id: 1, name: 2, value: { kind: "text", value: "v" } } }] as never, + sink, + ); + assertEquals((ops[0] as { ns: unknown }).ns, null); + assertNotEquals(ops[0], { op: "set-attribute-text", id: 1, name: 2, ns: { kind: "none" }, value: "v" }); +}); + +// -- F12 coverage gaps ------------------------------------------------------ +// +// The counter example's full-stack test above never exercises these shapes +// (its listeners are all bubbling, its templates are single-root with no +// namespaced attrs, and its list never empties to a placeholder) — cheap +// unit cases against the recording sink instead of new integration mounts. + +Deno.test("applyOperations: new-event-listener/remove-event-listener carry bubbles verbatim, both values", () => { + // This matters more than most: an inverted `bubbles` bit would NOT show + // up in an innerHTML diff at all (it only changes the host's listener + // delegation strategy — root-delegated vs per-element — not the + // markup), so this has to be asserted directly against the sink. + const ops: unknown[] = []; + const sink = recordingSink(ops); + applyOperations( + [ + { kind: "new-event-listener", value: { id: 1, name: 10, bubbles: true } }, + { kind: "new-event-listener", value: { id: 2, name: 11, bubbles: false } }, + { kind: "remove-event-listener", value: { id: 1, name: 10, bubbles: true } }, + { kind: "remove-event-listener", value: { id: 2, name: 11, bubbles: false } }, + ] as never, + sink, + ); + assertEquals(ops, [ + { op: "new-event-listener", id: 1, name: 10, bubbles: true }, + { op: "new-event-listener", id: 2, name: 11, bubbles: false }, + { op: "remove-event-listener", id: 1, name: 10, bubbles: true }, + { op: "remove-event-listener", id: 2, name: 11, bubbles: false }, + ]); +}); + +Deno.test("applyOperations: option ns PRESENT lifts to the bare id, on set-attribute and template attrs", () => { + const ops: unknown[] = []; + const sink = recordingSink(ops); + + // set-attribute path. + applyOperations( + [{ kind: "set-attribute", value: { id: 1, name: 2, ns: 9, value: { kind: "text", value: "v" } } }] as never, + sink, + ); + assertEquals(ops, [{ op: "set-attribute-text", id: 1, name: 2, ns: 9, value: "v" }]); + + // template-attr path (attrs: [] in every other arena test means this is + // otherwise uncovered): a static template attribute carrying a namespace. + ops.length = 0; + const nodes = [ + { kind: "element", value: { tag: 0, ns: 9, attrs: [{ name: 3, ns: 9, value: "v" }], children: [] } }, + ]; + applyOperations([{ kind: "register-template", value: { id: 0, nodes, roots: [0] } }] as never, sink); + const roots = (ops[0] as { roots: TemplateNodeDesc[] }).roots; + assertEquals(roots, [{ kind: "element", tag: 0, ns: 9, attrs: [{ name: 3, ns: 9, value: "v" }], children: [] }]); +}); + +Deno.test("applyOperations: register-template with multiple roots indexes each root correctly", () => { + // Arena for two roots: root0 = text "a", root1 = element with a child. + // nodes[0] = text "a" (root 0) + // nodes[1] = element, children = [2] (root 1) + // nodes[2] = text "b" + const ops: unknown[] = []; + const sink = recordingSink(ops); + const nodes = [ + { kind: "text", value: "a" }, + { kind: "element", value: { tag: 0, attrs: [], children: [2] } }, + { kind: "text", value: "b" }, + ]; + applyOperations([{ kind: "register-template", value: { id: 0, nodes, roots: [0, 1] } }] as never, sink); + + const roots = (ops[0] as { roots: TemplateNodeDesc[] }).roots; + assertEquals(roots, [ + { kind: "text", value: "a" }, + { + kind: "element", + tag: 0, + ns: null, + attrs: [], + children: [{ kind: "text", value: "b" }], + }, + ]); +}); + +Deno.test("applyOperations: create-placeholder reaches the sink with the bare element id", () => { + const ops: unknown[] = []; + const sink = recordingSink(ops); + applyOperations([{ kind: "create-placeholder", value: 4 }] as never, sink); + assertEquals(ops, [{ op: "create-placeholder", id: 4 }]); +}); diff --git a/justfile b/justfile index aa5aa86..d954db5 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,12 @@ # A21, so we build against a pinned upstream rev instead. POLYENGINE_REPO := "https://github.com/polymorph-components/polyengine.git" -POLYENGINE_REV := "9e17dc97dd3e" +# Advanced 9e17dc97dd3e -> 22b5d3d for polyengine#261's optimization PRs +# (#263 layout-node cache, #264 embedder adapter tables, #265 flatten-count +# memoization, #270 variant kind/value): the typed mutation channel's cost is +# almost entirely that lift path, so the Channel A/B in bench/README.md is +# only meaningful against a runtime that has them. +POLYENGINE_REV := "22b5d3d" TAILWIND_VERSION := "v4.3.3" default: check test @@ -77,12 +82,6 @@ example name: examples/build/{{name}}.component.wasm \ -o examples/build/{{name}}.plan.json -# Regenerate golden vectors (runs the Rust generator, then verifies the TS -# decoder agrees). -vectors: - cargo test --test vectors -- --ignored generate - deno task test - # Real-browser (Chromium via Playwright) E2E lane for the counter example. # First run: `cd e2e && npm install && npx playwright install chromium --with-deps`. # GitHub-Pages-ready static site for the TodoMVC example, assembled flat diff --git a/src/driver.rs b/src/driver.rs index aa7cb43..88e0bb5 100644 --- a/src/driver.rs +++ b/src/driver.rs @@ -23,8 +23,8 @@ //! //! # Why `run` returns immediately and spawns the scheduler //! -//! `run` is `async func() -> stream`: the host awaits its promise to -//! obtain the read end, then parks a `readDirect` session on it. Under the +//! `run` is `async func() -> stream`: the host awaits its promise +//! to obtain the read end, then reads batches from it. Under the //! component-model async ABI, an async export's Rust body *returning* is //! task.return followed by task exit — so the scheduler cannot live in //! `run`'s own body. Instead `run` creates the stream, stashes the writer @@ -34,25 +34,40 @@ //! //! Ordering is safe by rendezvous: the spawned task's first `write` parks //! until the host actually reads, so nothing is lost if it runs before the -//! host has its session parked. The converse would deadlock, which is why -//! `run`'s own body must not write anything before returning the reader — -//! that write would park while the host is still awaiting `run`'s promise -//! for the reader it needs in order to read. +//! host is reading. The converse would deadlock, which is why `run`'s own +//! body must not write anything before returning the reader — that write +//! would park while the host is still awaiting `run`'s promise for the +//! reader it needs in order to read. //! //! All driver state (`RENDERER`/`RUNTIME`/`INTERNER`/`VDOM`) is installed //! before `run` returns, so a `handle-event` arriving immediately after the //! return finds consistent state. It cannot find a listener yet (no batch //! has been applied), but it will not observe an uninitialized cell. //! +//! # Why the scheduler may park forever +//! +//! The scheduler task's wait between renders is a plain Rust future woken +//! cross-task, with no WIT waitable pending — a state the runtime would +//! otherwise be entitled to call a deadlock. It is legal because the host +//! retains the readable end of the mutation stream for the instance's +//! lifetime, and polyengine's retention rule (#162, +//! `.deps/polyengine/contracts/embedder-api.md` §"Streams and futures") +//! makes "a retained end, a parked host operation, or an unfinished producer +//! pump" each sufficient on its own: a stalled guest is then reported as the +//! documented embedder-may-act hang, never a deadlock trap. The runtime's +//! `HostActivity` (`.deps/polyengine/runtime/src/exec/host_streams.ts`) arms +//! on that retained end and disarms only when the end is lowered back into a +//! guest, which never happens here. +//! //! # How failure surfaces //! //! (Historical: an earlier revision passed the read end to a host import and //! `run`'s promise never settled, so a mount-time failure had nowhere to go //! but that held-open promise.) Now `run`'s promise settles as soon as the //! reader is handed back, and a trap in the spawned scheduler task surfaces -//! as a rejection of the host's parked direct-read session -//! (`PeerTrappedError`) — the channel the host actually watches. A trap -//! during `run`'s own body rejects the host's `await exports.run()`. +//! as a rejection of the host's pending read (`PeerTrappedError`) — the +//! channel the host actually watches. A trap during `run`'s own body rejects +//! the host's `await exports.run()`. use std::cell::RefCell; use std::future::Future; @@ -66,15 +81,16 @@ use dioxus_html::PlatformEventData; use wit_bindgen::rt::async_support::{spawn_local, StreamReader, StreamWriter}; use crate::bindings::polymorph::dioxus::events::Payload; +use crate::bindings::polymorph::dioxus::mutations::Operation; use crate::bindings::{wit_stream, DomEvent}; use crate::events::{WitEventConverter, WitEventData}; -use crate::protocol::Interner; +use crate::interner::Interner; use crate::writer::MutationWriter; /// The read end of the mutation channel: what `run` hands back to the host. /// Named here so [`crate::launch!`] can spell the export's return type /// without the app crate naming wit-bindgen's runtime module. -pub type MutationStream = StreamReader; +pub type MutationStream = StreamReader; /// Everything the flush path needs, shared by the `run` loop and by /// `handle-event`. @@ -83,15 +99,15 @@ struct Renderer { /// Taken for the duration of a write so a second flusher can detect an /// in-flight write instead of panicking on a re-entrant `RefCell` /// borrow. - stream: Option>, - /// Frame bytes staged while another task owns `stream`. The in-flight - /// flusher drains this before giving the writer back, which keeps frames - /// in batch order. - pending: Vec, - /// Reused frame scratch buffer (`Batch::take_frame` appends). - scratch: Vec, - /// Set once the host has dropped the read end of the stream transport - /// (see the reader-gone branch in [`flush`]). Once dead, `flush` drops + stream: Option>, + /// Operations staged while another task owns `stream`, drained by the + /// in-flight flusher before it hands the writer back — keeping batches + /// in order. (Batch boundaries are not preserved across staging; the + /// host applies operations in order and does not depend on where a + /// write ends.) + pending: Vec, + /// Set once the host has dropped the read end of the stream (see the + /// reader-gone branch in [`flush`]). Once dead, `flush` drops /// staged/incoming batches instead of accumulating them in `pending`: /// this is only reachable at host teardown, when there is no longer a /// reader to receive anything, so bounded memory beats a slow leak from @@ -110,19 +126,18 @@ thread_local! { static INTERNER: RefCell>>> = const { RefCell::new(None) }; } -/// Stage the current batch (if non-empty) and push it to the host. +/// Push the current batch (if non-empty) to the host: one batch is one +/// stream write of the whole `Vec`. /// -/// Appends one frame and writes it. `StreamWriter::write_all` returns the -/// values it could *not* write, which happens only once the read end is -/// gone; there is no short-write case to retry because `write_all` already -/// loops. When the host's direct-read session is parked the whole write -/// completes inline, so this await normally does not yield. +/// `StreamWriter::write_all` returns the values it could *not* write, which +/// happens only once the read end is gone; there is no short-write case to +/// retry because `write_all` already loops. async fn flush() { enum Action { Nothing, - Stream(StreamWriter, Vec), - /// Another task owns the stream writer; our frame was staged and will - /// be drained by that task in order. + Stream(StreamWriter, Vec), + /// Another task owns the stream writer; our operations were staged + /// and will be drained by that task in order. Staged, } @@ -132,18 +147,19 @@ async fn flush() { return Action::Nothing; } if r.dead { - // The reader is gone; discard this batch instead of staging it - // into `pending` (which would otherwise grow unboundedly across - // every future flush once the host has torn down its read end). + // The reader is gone; discard rather than growing `pending` + // unboundedly across every future flush. r.writer.batch.clear(); return Action::Nothing; } - r.scratch.clear(); - r.writer.batch.take_frame(&mut r.scratch); + // `mem::take` rather than `clear`: the operations are moved into the + // write, so the batch leaves with them. Its capacity comes back + // through `write_all`'s return value and is recycled below. + let batch = std::mem::take(&mut r.writer.batch); match r.stream.take() { - Some(w) => Action::Stream(w, std::mem::take(&mut r.scratch)), + Some(w) => Action::Stream(w, batch), None => { - r.pending.extend_from_slice(&r.scratch); + r.pending.extend(batch); Action::Staged } } @@ -151,20 +167,20 @@ async fn flush() { match action { Action::Nothing | Action::Staged => {} - Action::Stream(mut w, mut bytes) => { + Action::Stream(mut w, mut ops) => { loop { // `write_all` loops internally over partial writes and gives // back whatever it could not deliver; a non-empty remainder // means the read end is gone, not a short write to retry. - let leftover = w.write_all(bytes).await; + let leftover = w.write_all(ops).await; if !leftover.is_empty() { // The host dropped the read end: the mutation channel is // gone and there is nothing useful left to do with this - // writer. - // Mark the renderer dead and drop whatever was staged so - // far — only reachable at host teardown, and bounded - // memory beats a slow leak from `pending` growing on - // every subsequent flush with no reader left to drain it. + // writer. Mark the renderer dead and drop whatever was + // staged so far — only reachable at host teardown, and + // bounded memory beats a slow leak from `pending` + // growing on every subsequent flush with no reader left + // to drain it. RENDERER.with_borrow_mut(|r| { let r = r.as_mut().unwrap(); r.dead = true; @@ -172,22 +188,32 @@ async fn flush() { }); return; } - // Anything another task staged while we were awaiting goes out - // now, before the writer becomes available again — otherwise - // frames would leave the guest out of batch order. + // Anything another task staged while we were awaiting goes + // out now, before the writer becomes available again — + // otherwise operations would leave the guest out of order. let staged = RENDERER.with_borrow_mut(|r| { let r = r.as_mut().unwrap(); - // `leftover` is the drained buffer; hand its capacity back - // to the scratch slot so steady-state flushing does not - // reallocate. - r.scratch = leftover; + // `leftover` is the drained batch: an empty `Vec` that + // kept its capacity. Hand that capacity back to the + // writer so steady-state flushing does not regrow the + // batch from zero (a batch is tens of thousands of + // operations at bench sizes, so dropping the capacity + // would cost a dozen reallocations per batch). + // + // Only when the writer has not already started filling + // the next batch: another task may have rendered into it + // while we were awaiting, and that batch must not be + // clobbered. + if r.writer.batch.is_empty() { + r.writer.batch = leftover; + } std::mem::take(&mut r.pending) }); if staged.is_empty() { RENDERER.with_borrow_mut(|r| r.as_mut().unwrap().stream = Some(w)); return; } - bytes = staged; + ops = staged; } } } @@ -206,6 +232,18 @@ fn wait_for_work() -> impl Future { }) } +/// Run one render step with both thread-locals borrowed, and nothing awaited +/// in between. +fn render(step: impl FnOnce(&mut VirtualDom, &mut MutationWriter)) { + VDOM.with_borrow_mut(|dom| { + RENDERER.with_borrow_mut(|r| { + let dom = dom.as_mut().expect("driver: vdom not initialized"); + let r = r.as_mut().expect("driver: renderer not initialized"); + step(dom, &mut r.writer); + }) + }) +} + /// Implementation of the world's `run` export. /// /// Installs the event converter, builds the VirtualDom, creates the mutation @@ -232,7 +270,6 @@ pub async fn run(root: fn() -> Element) -> MutationStream { writer: MutationWriter::new(interner), stream: Some(writer), pending: Vec::new(), - scratch: Vec::new(), dead: false, })); @@ -240,11 +277,8 @@ pub async fn run(root: fn() -> Element) -> MutationStream { render(|dom, w| dom.rebuild(w)); flush().await; - // The persistent scheduler loop's park (`wait_for_work`: a plain Rust - // future woken cross-task, no WIT waitable) is legal because the host - // retains a parked direct-read session on our ops stream — amendment - // A15 host retention, so a quiescent instance is the documented - // embedder-may-act state. + // The scheduler loop's persistent park is legal because the host + // retains the readable end of this stream — see the module doc. loop { wait_for_work().await; render(|dom, w| dom.render_immediate(w)); @@ -255,18 +289,6 @@ pub async fn run(root: fn() -> Element) -> MutationStream { reader } -/// Run one render step with both thread-locals borrowed, and nothing awaited -/// in between. -fn render(step: impl FnOnce(&mut VirtualDom, &mut MutationWriter)) { - VDOM.with_borrow_mut(|dom| { - RENDERER.with_borrow_mut(|r| { - let dom = dom.as_mut().expect("driver: vdom not initialized"); - let r = r.as_mut().expect("driver: renderer not initialized"); - step(dom, &mut r.writer); - }) - }) -} - /// Implementation of the world's `handle-event` export. /// /// Dispatch is synchronous (Dioxus's synthetic bubbling included). Afterwards diff --git a/src/interner.rs b/src/interner.rs new file mode 100644 index 0000000..a800239 --- /dev/null +++ b/src/interner.rs @@ -0,0 +1,93 @@ +//! [`Interner`]: the `&'static str` name table shared by the mutation +//! writer and the event-dispatch path. +//! +//! Names (tags, attribute names, event names, namespaces) cross the boundary +//! once as a `cache-string` operation and are referenced by `u16` id +//! thereafter; `handle-event` names arrive back as those same ids, so the +//! reverse lookup lives here too. See the `mutations` interface in +//! `wit/world.wit` for the protocol's side of this. + +use rustc_hash::FxHashMap; + +/// Interns `&'static str` names (tags, attribute/event names, namespaces) by +/// pointer identity, assigning each a `u16` id on first sight of a given +/// pointer+length. +/// +/// Pointer-identity interning (rather than content hashing) is the same +/// keying [`crate::writer::MutationWriter`] uses for template identity, and +/// carries the same tradeoff: two distinct statics with equal contents may +/// get two ids. That's harmless (a few extra `cache-string` operations / +/// interned slots at worst), and avoids hashing string contents on every +/// intern call. +pub struct Interner { + /// Keyed by `(ptr as usize, len)` — the fat-pointer components of the + /// `&'static str`, which uniquely identify a given static allocation. + ids: FxHashMap<(usize, usize), u16>, + /// Reverse map for event dispatch (`resolve`): id -> the original + /// `&'static str`. + names: Vec<&'static str>, +} + +impl Interner { + /// Create an empty interner. + pub fn new() -> Self { + Interner { ids: FxHashMap::default(), names: Vec::new() } + } + + /// Return the interned id for `s`, plus whether *this* call is the one + /// that defined it (i.e. whether the caller owes the wire a + /// `cache-string` operation for it). + /// + /// Panics if more than `u16::MAX + 1` distinct strings are interned: + /// `str-ref` is a `u16` and every value is a legal id (optionality is + /// carried by `option` in the schema, so no id is reserved). + pub fn intern(&mut self, s: &'static str) -> (u16, bool) { + let key = (s.as_ptr() as usize, s.len()); + if let Some(&id) = self.ids.get(&key) { + return (id, false); + } + let next = self.names.len(); + assert!( + next <= u16::MAX as usize, + "interner: interned more than {} strings; str-ref id space (u16) \ + exhausted", + u16::MAX as usize + 1 + ); + let id = next as u16; + self.ids.insert(key, id); + self.names.push(s); + (id, true) + } + + /// Reverse lookup for event dispatch: interned id -> the original + /// `&'static str`, or `None` if `id` was never interned. + pub fn resolve(&self, id: u16) -> Option<&'static str> { + self.names.get(id as usize).copied() + } +} + +impl Default for Interner { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Interning is idempotent per pointer identity, and the reverse lookup + /// `handle-event` depends on round-trips. + #[test] + fn intern_is_idempotent_and_resolves() { + let mut i = Interner::new(); + static DIV: &str = "div"; + let (a, a_new) = i.intern(DIV); + let (b, b_new) = i.intern(DIV); + assert_eq!(a, b); + assert!(a_new, "first sight defines the slot"); + assert!(!b_new, "second sight must not redefine it"); + assert_eq!(i.resolve(a), Some("div")); + assert_eq!(i.resolve(a + 1), None); + } +} diff --git a/src/lib.rs b/src/lib.rs index bf46452..86dd759 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,18 @@ //! Dioxus renderer for components running on polyengine. //! -//! - [`protocol`]: the batch encoder (op segment + string segment, interned -//! and dynamic strings) matching the wire format documented in -//! `wit/world.wit`. -//! - [`writer`]: the `dioxus_core::WriteMutations` sink that fills a batch. -//! - `bindings` / `driver` / `events` (wasm32 only): the generated WIT -//! bindings, the `run`/`handle-event` implementation, and the -//! `HtmlEventConverter` over the WIT payload types. These are gated on -//! `target_arch = "wasm32"` so `cargo test` can exercise the encoder and -//! writer natively. +//! - [`interner`]: the `&'static str` name table behind the protocol's +//! `cache-string` / `str-ref` interning. +//! - `bindings` / `driver` / `events` / `writer` (wasm32 only): the generated +//! WIT bindings, the `run`/`handle-event` implementation, the +//! `HtmlEventConverter` over the WIT payload types, and the +//! `dioxus_core::WriteMutations` sink that fills a batch of +//! `mutations::operation` values. These are gated on +//! `target_arch = "wasm32"` because they name the generated bindings; +//! [`interner`] does not, so `cargo test` can exercise it natively. //! //! An application crate wires itself up with [`launch!`]. -pub mod protocol; -pub mod writer; +pub mod interner; #[cfg(target_arch = "wasm32")] pub mod bindings; @@ -21,3 +20,5 @@ pub mod bindings; pub mod driver; #[cfg(target_arch = "wasm32")] pub mod events; +#[cfg(target_arch = "wasm32")] +pub mod writer; diff --git a/src/protocol.rs b/src/protocol.rs deleted file mode 100644 index 75806bc..0000000 --- a/src/protocol.rs +++ /dev/null @@ -1,525 +0,0 @@ -//! Batch encoder for the wire format documented in `wit/world.wit`. -//! -//! A [`Batch`] accumulates one op segment and one string segment. The two -//! segments are decoded together host-side: op operands reference dynamic -//! string bytes by UTF-16 code-unit length, consumed sequentially from the -//! string segment (itself one contiguous UTF-8 blob, decoded in a single -//! `TextDecoder` pass). See `wit/world.wit`'s `run` export doc -//! comment for the normative format; this module must match it exactly. - -use rustc_hash::FxHashMap; - -/// `strref` sentinel for "no namespace" (wit/world.wit: "0xffff = none where -/// the operand is optional"). -const STRREF_NONE: u16 = 0xffff; - -/// `dynstr` UTF-16-length escape: a length field of `0xffff` is followed by -/// a u32 actual length (wit/world.wit `dynstr` primitive encoding). -const DYNSTR_ESCAPE: u16 = 0xffff; - -/// Opcodes, `wit/world.wit` "# Opcodes" section. Numbering is normative. -mod op { - pub const CACHE_STRING: u8 = 0x01; - pub const REGISTER_TEMPLATE: u8 = 0x02; - pub const APPEND_CHILDREN: u8 = 0x03; - pub const ASSIGN_ID: u8 = 0x04; - pub const CREATE_PLACEHOLDER: u8 = 0x05; - pub const CREATE_TEXT_NODE: u8 = 0x06; - pub const LOAD_TEMPLATE: u8 = 0x07; - pub const REPLACE_WITH: u8 = 0x08; - pub const REPLACE_PLACEHOLDER: u8 = 0x09; - pub const INSERT_AFTER: u8 = 0x0a; - pub const INSERT_BEFORE: u8 = 0x0b; - pub const SET_ATTRIBUTE: u8 = 0x0c; - pub const SET_TEXT: u8 = 0x0d; - pub const NEW_EVENT_LISTENER: u8 = 0x0e; - pub const REMOVE_EVENT_LISTENER: u8 = 0x0f; - pub const REMOVE: u8 = 0x10; - pub const PUSH_ROOT: u8 = 0x11; -} - -/// `register-template` node kinds, `wit/world.wit` `node := kind:u8 ...`. -mod node_kind { - pub const ELEMENT: u8 = 0x00; - pub const TEXT: u8 = 0x01; - pub const DYNAMIC: u8 = 0x02; -} - -/// `set-attribute`'s `attrval := kind:u8 ...` tags. -mod attrval_kind { - pub const TEXT: u8 = 0x00; - pub const FLOAT: u8 = 0x01; - pub const INT: u8 = 0x02; - pub const BOOL: u8 = 0x03; - pub const NONE: u8 = 0x04; -} - -/// Compute the `dynstr` UTF-16 code-unit length of `s`. -/// -/// Fast path: an all-ASCII string's UTF-16 length equals its UTF-8 byte -/// length. Otherwise sum `char::len_utf16` (wit/world.wit dynstr doc: -/// "Rust: sum of char::len_utf16, with an all-ASCII fast path where it -/// equals the byte length"). -fn utf16_len(s: &str) -> u32 { - if s.is_ascii() { - s.len() as u32 - } else { - s.chars().map(char::len_utf16).sum::() as u32 - } -} - -/// Convert an `Option` strref into its wire encoding (`0xffff` = none). -fn strref(ns: Option) -> u16 { - ns.unwrap_or(STRREF_NONE) -} - -/// One batch of protocol ops: an op segment and the string segment its -/// `dynstr` operands reference. See the module doc and `wit/world.wit` for -/// the wire format. -/// -/// A fresh `Batch` is emptied by `take_frame`, so a single -/// instance can be reused across renders without reallocating buffers each -/// time (only `Vec::clear`, which retains capacity). -pub struct Batch { - ops: Vec, - strings: String, -} - -impl Batch { - /// Create an empty batch. - pub fn new() -> Self { - Batch { ops: Vec::new(), strings: String::new() } - } - - /// True if no ops have been recorded (and hence no strings either). - pub fn is_empty(&self) -> bool { - self.ops.is_empty() - } - - /// Append a `dynstr` operand: UTF-16 length (with the `0xffff` escape - /// for lengths that collide with the sentinel or exceed u16), followed - /// by the string's UTF-8 bytes appended to the string segment. - fn push_dynstr(&mut self, s: &str) { - let len16 = utf16_len(s); - if len16 >= DYNSTR_ESCAPE as u32 { - self.ops.extend_from_slice(&DYNSTR_ESCAPE.to_le_bytes()); - self.ops.extend_from_slice(&len16.to_le_bytes()); - } else { - self.ops.extend_from_slice(&(len16 as u16).to_le_bytes()); - } - self.strings.push_str(s); - } - - fn push_strref(&mut self, id: Option) { - self.ops.extend_from_slice(&strref(id).to_le_bytes()); - } - - fn push_path(&mut self, path: &[u8]) { - assert!( - path.len() <= 255, - "protocol: path length {} exceeds u8 max (255)", - path.len() - ); - self.ops.push(path.len() as u8); - self.ops.extend_from_slice(path); - } - - /// `0x01 cache-string id:u16 s:dynstr` — define (or overwrite) interned - /// slot `id`. Normally driven by [`Interner`], not called directly. - pub fn cache_string(&mut self, id: u16, s: &str) { - self.ops.push(op::CACHE_STRING); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.push_dynstr(s); - } - - /// `0x02 register-template` header: `tmpl:u16 nroots:u16`. The caller - /// must follow with exactly `nroots` calls to the node-emitting methods - /// (`template_element_open` + attrs + children, `template_text`, or - /// `template_dynamic`), matching the recursive `node` grammar in - /// `wit/world.wit`. - pub fn register_template(&mut self, tmpl: u16, nroots: u16) { - self.ops.push(op::REGISTER_TEMPLATE); - self.ops.extend_from_slice(&tmpl.to_le_bytes()); - self.ops.extend_from_slice(&nroots.to_le_bytes()); - } - - /// Emit an element `node` header: `kind=0x00 tag:strref ns:strref - /// nattrs:u16`. The caller must follow with exactly `nattrs` calls to - /// `template_attr`, then one call to `template_element_children`, then - /// that many child nodes. - pub fn template_element_open(&mut self, tag: u16, ns: Option, nattrs: u16) { - self.ops.push(node_kind::ELEMENT); - self.ops.extend_from_slice(&tag.to_le_bytes()); - self.push_strref(ns); - self.ops.extend_from_slice(&nattrs.to_le_bytes()); - } - - /// Emit one static `attr := name:strref ns:strref value:dynstr` inside - /// an open element (must follow `template_element_open`, before - /// `template_element_children`). - pub fn template_attr(&mut self, name: u16, ns: Option, value: &str) { - self.ops.extend_from_slice(&name.to_le_bytes()); - self.push_strref(ns); - self.push_dynstr(value); - } - - /// Declare the child count for the currently-open element: - /// `nchildren:u16`. Must follow all of that element's `template_attr` - /// calls; the caller then emits exactly `nchildren` nodes. - pub fn template_element_children(&mut self, nchildren: u16) { - self.ops.extend_from_slice(&nchildren.to_le_bytes()); - } - - /// Emit a text `node`: `kind=0x01 value:dynstr`. - pub fn template_text(&mut self, value: &str) { - self.ops.push(node_kind::TEXT); - self.push_dynstr(value); - } - - /// Emit a dynamic-placeholder `node`: `kind=0x02` (no operands). - pub fn template_dynamic(&mut self) { - self.ops.push(node_kind::DYNAMIC); - } - - /// `0x03 append-children id m`. - pub fn append_children(&mut self, id: u32, m: u32) { - self.ops.push(op::APPEND_CHILDREN); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.ops.extend_from_slice(&m.to_le_bytes()); - } - - /// `0x04 assign-id path id`. Panics if `path.len() > 255` (path's `u8` - /// length prefix cannot represent more). - pub fn assign_id(&mut self, path: &[u8], id: u32) { - self.ops.push(op::ASSIGN_ID); - self.push_path(path); - self.ops.extend_from_slice(&id.to_le_bytes()); - } - - /// `0x05 create-placeholder id`. - pub fn create_placeholder(&mut self, id: u32) { - self.ops.push(op::CREATE_PLACEHOLDER); - self.ops.extend_from_slice(&id.to_le_bytes()); - } - - /// `0x06 create-text-node id text:dynstr`. - pub fn create_text_node(&mut self, id: u32, text: &str) { - self.ops.push(op::CREATE_TEXT_NODE); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.push_dynstr(text); - } - - /// `0x07 load-template tmpl root-index:u16 id`. - pub fn load_template(&mut self, tmpl: u16, root: u16, id: u32) { - self.ops.push(op::LOAD_TEMPLATE); - self.ops.extend_from_slice(&tmpl.to_le_bytes()); - self.ops.extend_from_slice(&root.to_le_bytes()); - self.ops.extend_from_slice(&id.to_le_bytes()); - } - - /// `0x08 replace-with id m`. - pub fn replace_with(&mut self, id: u32, m: u32) { - self.ops.push(op::REPLACE_WITH); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.ops.extend_from_slice(&m.to_le_bytes()); - } - - /// `0x09 replace-placeholder path m`. Panics if `path.len() > 255`. - pub fn replace_placeholder(&mut self, path: &[u8], m: u32) { - self.ops.push(op::REPLACE_PLACEHOLDER); - self.push_path(path); - self.ops.extend_from_slice(&m.to_le_bytes()); - } - - /// `0x0a insert-after id m`. - pub fn insert_after(&mut self, id: u32, m: u32) { - self.ops.push(op::INSERT_AFTER); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.ops.extend_from_slice(&m.to_le_bytes()); - } - - /// `0x0b insert-before id m`. - pub fn insert_before(&mut self, id: u32, m: u32) { - self.ops.push(op::INSERT_BEFORE); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.ops.extend_from_slice(&m.to_le_bytes()); - } - - fn set_attribute_header(&mut self, id: u32, name: u16, ns: Option) { - self.ops.push(op::SET_ATTRIBUTE); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.ops.extend_from_slice(&name.to_le_bytes()); - self.push_strref(ns); - } - - /// `0x0c set-attribute` with `attrval` kind `0x00 text s:dynstr`. - pub fn set_attribute_text(&mut self, id: u32, name: u16, ns: Option, value: &str) { - self.set_attribute_header(id, name, ns); - self.ops.push(attrval_kind::TEXT); - self.push_dynstr(value); - } - - /// `0x0c set-attribute` with `attrval` kind `0x01 float f64`. - pub fn set_attribute_float(&mut self, id: u32, name: u16, ns: Option, value: f64) { - self.set_attribute_header(id, name, ns); - self.ops.push(attrval_kind::FLOAT); - self.ops.extend_from_slice(&value.to_le_bytes()); - } - - /// `0x0c set-attribute` with `attrval` kind `0x02 int s64`. - pub fn set_attribute_int(&mut self, id: u32, name: u16, ns: Option, value: i64) { - self.set_attribute_header(id, name, ns); - self.ops.push(attrval_kind::INT); - self.ops.extend_from_slice(&value.to_le_bytes()); - } - - /// `0x0c set-attribute` with `attrval` kind `0x03 bool u8`. - pub fn set_attribute_bool(&mut self, id: u32, name: u16, ns: Option, value: bool) { - self.set_attribute_header(id, name, ns); - self.ops.push(attrval_kind::BOOL); - self.ops.push(value as u8); - } - - /// `0x0c set-attribute` with `attrval` kind `0x04 none` (remove the - /// attribute; no value operand). - pub fn set_attribute_none(&mut self, id: u32, name: u16, ns: Option) { - self.set_attribute_header(id, name, ns); - self.ops.push(attrval_kind::NONE); - } - - /// `0x0d set-text id text:dynstr`. - pub fn set_text(&mut self, id: u32, text: &str) { - self.ops.push(op::SET_TEXT); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.push_dynstr(text); - } - - /// `0x0e new-event-listener id name:strref flags:u8` (name is never - /// `none` here — the `strref` sentinel is namespace-only per - /// wit/world.wit). `bubbles` is flags bit0: dioxus-html's event_bubbles - /// verdict, looked up by the caller; the host's delegation strategy - /// (root-delegated vs per-element) hangs off it. - pub fn new_event_listener(&mut self, id: u32, name: u16, bubbles: bool) { - self.ops.push(op::NEW_EVENT_LISTENER); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.ops.extend_from_slice(&name.to_le_bytes()); - self.ops.push(bubbles as u8); - } - - /// `0x0f remove-event-listener id name:strref flags:u8` (same flags as - /// new-event-listener; the host needs the bubbles bit to locate the - /// registration it is removing). - pub fn remove_event_listener(&mut self, id: u32, name: u16, bubbles: bool) { - self.ops.push(op::REMOVE_EVENT_LISTENER); - self.ops.extend_from_slice(&id.to_le_bytes()); - self.ops.extend_from_slice(&name.to_le_bytes()); - self.ops.push(bubbles as u8); - } - - /// `0x10 remove id`. - pub fn remove(&mut self, id: u32) { - self.ops.push(op::REMOVE); - self.ops.extend_from_slice(&id.to_le_bytes()); - } - - /// `0x11 push-root id`. - pub fn push_root(&mut self, id: u32) { - self.ops.push(op::PUSH_ROOT); - self.ops.extend_from_slice(&id.to_le_bytes()); - } - - /// Discard the accumulated batch without encoding anything, retaining - /// buffer capacity. The teardown path's cheap alternative to assembling - /// a frame nobody will read. - /// - /// NOTE: interned-string state lives in [`Interner`], not here, and an - /// interner emits each `cache-string` op exactly once — so ops discarded - /// by this method may include cache-string definitions that will never - /// be re-sent. That is fine on a dead channel (nothing will read future - /// batches either); it is NOT a way to "skip" a batch on a live one. - pub fn clear(&mut self) { - self.ops.clear(); - self.strings.clear(); - } - - /// Append one frame to `out` and clear `self`. - /// - /// Frame layout (`wit/world.wit` "# Framing"): - /// `frame-len:u32 strings-len:u32 strings:u8{strings-len} ops:u8{rest}`, - /// where `frame-len` counts everything *after* the frame-len field - /// itself (`4 + strings-len + len(ops)`). - pub fn take_frame(&mut self, out: &mut Vec) { - let strings_len = self.strings.len() as u32; - let frame_len = 4u32 + strings_len + self.ops.len() as u32; - out.extend_from_slice(&frame_len.to_le_bytes()); - out.extend_from_slice(&strings_len.to_le_bytes()); - out.extend_from_slice(self.strings.as_bytes()); - out.extend_from_slice(&self.ops); - self.ops.clear(); - self.strings.clear(); - } -} - -impl Default for Batch { - fn default() -> Self { - Self::new() - } -} - -/// Interns `&'static str` names (tags, attribute/event names, namespaces) by -/// pointer identity, emitting a `cache-string` op into the batch the first -/// time a given pointer+length is seen. -/// -/// Pointer-identity interning (rather than content hashing) is the fast -/// path documented for `register-template` guest identity ("templates are -/// 'static with unique identity, keyed guest-side by pointer" — -/// wit/world.wit `register-template` doc) and is reused here for all -/// `&'static str` names: two distinct statics with equal contents may get -/// two ids. That's harmless (a few extra cache-string ops / interned slots -/// at worst), and avoids hashing string contents on every intern call. -pub struct Interner { - /// Keyed by `(ptr as usize, len)` — the fat-pointer components of the - /// `&'static str`, which uniquely identify a given static allocation. - ids: FxHashMap<(usize, usize), u16>, - /// Reverse map for event dispatch (`resolve`): id -> the original - /// `&'static str`. - names: Vec<&'static str>, -} - -impl Interner { - /// Create an empty interner. - pub fn new() -> Self { - Interner { ids: FxHashMap::default(), names: Vec::new() } - } - - /// Return the interned id for `s`, emitting `cache-string` into `batch` - /// on first sight of this pointer identity. - /// - /// Panics if more than `u16::MAX - 1` distinct strings are interned - /// (id `0xffff` is reserved as the `strref` "none" sentinel, so it must - /// never be assigned). - pub fn intern(&mut self, batch: &mut Batch, s: &'static str) -> u16 { - let key = (s.as_ptr() as usize, s.len()); - if let Some(&id) = self.ids.get(&key) { - return id; - } - let next = self.names.len(); - assert!( - next < STRREF_NONE as usize, - "protocol: interned more than {} strings; id space exhausted \ - (0xffff is reserved as the strref \"none\" sentinel)", - STRREF_NONE - ); - let id = next as u16; - self.ids.insert(key, id); - self.names.push(s); - batch.cache_string(id, s); - id - } - - /// Reverse lookup for event dispatch: interned id -> the original - /// `&'static str`, or `None` if `id` was never interned. - pub fn resolve(&self, id: u16) -> Option<&'static str> { - self.names.get(id as usize).copied() - } -} - -impl Default for Interner { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Parse a `take_frame`-produced buffer back into `(ops, strings)`, for - /// tests that want to inspect the segments directly (the frame is the - /// only segment-extraction API since the call transport's `take_segments` - /// was removed). - fn unframe(frame: &[u8]) -> (Vec, String) { - let strings_len = u32::from_le_bytes(frame[4..8].try_into().unwrap()) as usize; - let strings = String::from_utf8(frame[8..8 + strings_len].to_vec()).unwrap(); - let ops = frame[8 + strings_len..].to_vec(); - (ops, strings) - } - - /// UTF-16 code-unit counting per the `dynstr` doc: ASCII fast path, - /// multi-byte BMP chars (1 code unit each), and surrogate pairs (2 code - /// units for one `char` outside the BMP). - #[test] - fn utf16_len_cases() { - assert_eq!(utf16_len(""), 0); - assert_eq!(utf16_len("hello"), 5); - // "wörld": 'ö' is one BMP char, one UTF-16 code unit, but 2 UTF-8 - // bytes -- exercises the non-ASCII path diverging from byte length. - assert_eq!(utf16_len("wörld"), 5); - // CJK: each char is one BMP code unit (3 UTF-8 bytes each). - assert_eq!(utf16_len("你好世界"), 4); - // Emoji outside the BMP: each is a surrogate pair (2 UTF-16 units, - // 4 UTF-8 bytes). "👍🏽" is thumbs-up + skin-tone modifier, both - // outside the BMP: 2 chars * 2 units = 4. - assert_eq!(utf16_len("👍🏽"), 4); - assert_eq!(utf16_len("👍🏽 emoji"), 4 + 6); - } - - #[test] - fn dynstr_escape_roundtrip_length() { - // A string whose UTF-16 length exceeds u16::MAX-ish must use the - // 0xffff escape followed by a u32 actual length. - let long = "ab£".repeat(30000); // 'a','b' (1 unit each) + '£' (1 unit) = 3 units/rep - let expected_len16 = utf16_len(&long); - assert!(expected_len16 as u32 >= DYNSTR_ESCAPE as u32); - - let mut b = Batch::new(); - b.create_text_node(0, &long); - let mut out = Vec::new(); - b.take_frame(&mut out); - let (ops, strings) = unframe(&out); - // op byte, id:u32, then dynstr: 0xffff u16 + u32 actual len - assert_eq!(ops[0], op::CREATE_TEXT_NODE); - let len16_field = u16::from_le_bytes([ops[5], ops[6]]); - assert_eq!(len16_field, DYNSTR_ESCAPE); - let actual_len = u32::from_le_bytes([ops[7], ops[8], ops[9], ops[10]]); - assert_eq!(actual_len, expected_len16); - assert_eq!(strings, long); - } - - #[test] - fn cache_string_hits_reuse_id() { - let mut batch = Batch::new(); - let mut interner = Interner::new(); - static DIV: &str = "div"; - let a = interner.intern(&mut batch, DIV); - let b = interner.intern(&mut batch, DIV); - assert_eq!(a, b); - assert_eq!(interner.resolve(a), Some("div")); - // Only one cache-string op should have been emitted. - let mut out = Vec::new(); - batch.take_frame(&mut out); - let (ops, _) = unframe(&out); - assert_eq!(ops[0], op::CACHE_STRING); - assert_eq!(ops.iter().filter(|&&b| b == op::CACHE_STRING).count(), 1); - } - - #[test] - #[should_panic(expected = "path length")] - fn assign_id_rejects_long_path() { - let mut b = Batch::new(); - b.assign_id(&[0u8; 256], 0); - } - - #[test] - fn frame_header_matches_layout() { - let mut b = Batch::new(); - b.push_root(42); - let mut out = Vec::new(); - b.take_frame(&mut out); - assert!(b.is_empty()); - let frame_len = u32::from_le_bytes(out[0..4].try_into().unwrap()); - let strings_len = u32::from_le_bytes(out[4..8].try_into().unwrap()); - assert_eq!(strings_len, 0); - assert_eq!(frame_len as usize, out.len() - 4); - } -} diff --git a/src/writer.rs b/src/writer.rs index 111b11f..e031da9 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -1,21 +1,30 @@ -//! [`MutationWriter`]: a `dioxus_core::WriteMutations` sink that encodes -//! straight into a [`protocol::Batch`] — no intermediate `Vec`. +//! [`MutationWriter`]: the mutation channel's `dioxus_core::WriteMutations` +//! sink, pushing `mutations::Operation` values into the batch the driver +//! flushes. //! //! The mapping mirrors dioxus's own `WriteMutations`→channel renderer //! (dioxus-interpreter-js `write_native_mutations.rs`): templates are -//! registered on first encounter and thereafter referenced by a guest-assigned -//! `u16`, and listener ops carry `event_bubbles(name)` because the host -//! delegates bubbling events at the mount root. +//! registered on first encounter and thereafter referenced by a +//! guest-assigned `u16`, and listener ops carry `event_bubbles(name)` +//! because the host delegates bubbling events at the mount root. The schema +//! it writes against is the `mutations` interface in `wit/world.wit`. (This +//! module names the generated bindings, so it is `wasm32`-only and +//! `cargo test` never builds it.) //! //! # Interning invariant //! -//! [`Interner::intern`] *emits* a `cache-string` op the first time it sees a -//! name. That op must never land in the middle of another op's operands, so -//! every string a composite op needs is interned **before** the op's first -//! byte is written. This matters most for `register-template`, whose node -//! grammar is recursive and self-delimiting: `intern_template_node` walks the -//! tree interning tags/namespaces/attribute names first, then -//! `emit_template_node` writes the grammar with no further interning. +//! The host resolves a `str-ref` against `cache-string` definitions it has +//! already seen, so every `CacheString` must precede the operation +//! referencing its id in the batch. Hence the two-pass template walk +//! (`intern_template_node` then `flatten_template_node`) and the +//! intern-then-build order in `set_attribute` and the listener ops. +//! +//! # Why template registration flattens +//! +//! WIT forbids recursive type definitions, so `register-template` carries an +//! arena (`nodes`, plus `u32` indices in `roots` and each element's +//! `children`) rather than a tree. See the `mutations` interface doc in +//! `wit/world.wit`. use std::cell::RefCell; use std::rc::Rc; @@ -23,16 +32,22 @@ use std::rc::Rc; use dioxus_core::{AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, WriteMutations}; use rustc_hash::FxHashMap; -use crate::protocol::{Batch, Interner}; +use crate::bindings::polymorph::dioxus::mutations as m; +use crate::interner::Interner; -/// Encodes dioxus mutations into a [`Batch`]. +/// Encodes dioxus mutations as `mutations::Operation` values. /// /// The interner is shared (`Rc>`) with the event-dispatch path, /// which needs the reverse `u16 -> &'static str` lookup to turn a /// `handle-event` name id back into a dioxus event name. pub struct MutationWriter { - /// The batch being filled. Drained by the driver's flush. - pub batch: Batch, + /// The batch being filled. The driver drains it with `std::mem::take` + /// once per flush and writes the whole `Vec` in one `write_all`. The + /// operations are moved into the write, but the `Vec` itself comes back + /// from `write_all` emptied with its capacity intact, and the driver + /// hands that capacity back here — so a steady-state batch reuses one + /// allocation. See `driver::flush`. + pub batch: Vec, interner: Rc>, /// Guest-assigned template ids, keyed by the pointer identity of /// `template`'s `roots`/`node_paths`/`attr_paths` slices — mirroring @@ -41,35 +56,35 @@ pub struct MutationWriter { /// than only when the build merges identical statics. This makes /// `template_id` O(1) in all build modes; in unmerged-statics builds /// (e.g. debug/dev), two structurally identical templates from distinct - /// `rsx!` sites now register twice instead of once — a harmless - /// duplicate registration (a few extra wire bytes), the same tradeoff - /// `Interner`'s pointer-identity doc in protocol.rs already documents - /// for strings. + /// `rsx!` sites register twice instead of once — a harmless duplicate + /// registration, the same tradeoff [`Interner`]'s pointer-identity doc + /// records for strings. templates: FxHashMap<(usize, usize, usize), u16>, } impl MutationWriter { /// Create a writer sharing `interner` with the event-dispatch path. pub fn new(interner: Rc>) -> Self { - MutationWriter { batch: Batch::new(), interner, templates: FxHashMap::default() } - } - - /// The shared interner handle. - pub fn interner(&self) -> &Rc> { - &self.interner + MutationWriter { batch: Vec::new(), interner, templates: FxHashMap::default() } } + /// Intern `s`, pushing `Operation::CacheString` on first sight of this + /// pointer identity. fn intern(&mut self, s: &'static str) -> u16 { - self.interner.borrow_mut().intern(&mut self.batch, s) + let (id, is_new) = self.interner.borrow_mut().intern(s); + if is_new { + self.batch.push(m::Operation::CacheString(m::CacheString { id, str: s.to_string() })); + } + id } fn intern_opt(&mut self, s: Option<&'static str>) -> Option { s.map(|s| self.intern(s)) } - /// Pass 1 of template registration: intern every `&'static str` the node - /// grammar will reference, so no `cache-string` op interleaves with the - /// grammar emitted by [`Self::emit_template_node`]. + /// Pass 1 of template registration: intern every `&'static str` the + /// template references, so all their `CacheString` operations precede + /// the `RegisterTemplate` that references their ids. fn intern_template_node(&mut self, node: &'static TemplateNode) { if let TemplateNode::Element { tag, namespace, attrs, children } = node { self.intern(tag); @@ -88,44 +103,72 @@ impl MutationWriter { } } - /// Pass 2: emit the `node` grammar from `wit/world.wit`'s - /// `register-template`. Must run after [`Self::intern_template_node`]. - fn emit_template_node(&mut self, node: &'static TemplateNode) { + /// Pass 2: append `node` and its subtree to `nodes` in pre-order, + /// returning `node`'s own index. + /// + /// The node is reserved in `nodes` *before* its children are walked (a + /// `Dynamic` placeholder stands in), so that the parent's index is fixed + /// while the children — which occupy later slots — are appended. The + /// reserved slot is then overwritten with the real element carrying the + /// child indices just collected. Must run after + /// [`Self::intern_template_node`]. + fn flatten_template_node( + &mut self, + node: &'static TemplateNode, + nodes: &mut Vec, + ) -> u32 { match node { TemplateNode::Element { tag, namespace, attrs, children } => { let tag_id = self.intern(tag); let ns_id = self.intern_opt(*namespace); - let static_count = attrs - .iter() - .filter(|attr| matches!(attr, TemplateAttribute::Static { .. })) - .count() as u16; - self.batch.template_element_open(tag_id, ns_id, static_count); // Dynamic template attributes are realized later through // `set_attribute`; only static ones are part of the template. + let mut wit_attrs = Vec::new(); for attr in *attrs { if let TemplateAttribute::Static { name, value, namespace } = attr { let name_id = self.intern(name); let ns_id = self.intern_opt(*namespace); - self.batch.template_attr(name_id, ns_id, value); + wit_attrs.push(m::TemplateAttr { + name: name_id, + ns: ns_id, + value: value.to_string(), + }); } } - self.batch.template_element_children(children.len() as u16); - for child in *children { - self.emit_template_node(child); - } + let index = nodes.len() as u32; + nodes.push(m::TemplateNode::Dynamic); // reserved; overwritten below + let child_indices = children + .iter() + .map(|child| self.flatten_template_node(child, nodes)) + .collect(); + nodes[index as usize] = m::TemplateNode::Element(m::TemplateElement { + tag: tag_id, + ns: ns_id, + attrs: wit_attrs, + children: child_indices, + }); + index + } + TemplateNode::Text { text } => { + let index = nodes.len() as u32; + nodes.push(m::TemplateNode::Text(text.to_string())); + index } - TemplateNode::Text { text } => self.batch.template_text(text), // A runtime-supplied node slot; the host materializes a // placeholder that later ops (assign-id / replace-placeholder) // address by path. - TemplateNode::Dynamic { .. } => self.batch.template_dynamic(), + TemplateNode::Dynamic { .. } => { + let index = nodes.len() as u32; + nodes.push(m::TemplateNode::Dynamic); + index + } } } /// Return the id for `template`, registering it on first encounter. /// /// Panics if more than `u16::MAX` distinct templates are registered - /// (mirrors [`Interner::intern`]'s id-space guard in protocol.rs). + /// (mirrors [`Interner::intern`]'s id-space guard). fn template_id(&mut self, template: Template) -> u16 { let key = ( template.roots.as_ptr() as usize, @@ -145,54 +188,67 @@ impl MutationWriter { let id = next as u16; self.templates.insert(key, id); - + // Pass 1 first, so every CacheString precedes the RegisterTemplate + // referencing its id (see the module doc). for root in template.roots.iter() { self.intern_template_node(root); } - self.batch.register_template(id, template.roots.len() as u16); - for root in template.roots.iter() { - self.emit_template_node(root); - } + let mut nodes = Vec::new(); + let roots = template + .roots + .iter() + .map(|root| self.flatten_template_node(root, &mut nodes)) + .collect(); + self.batch.push(m::Operation::RegisterTemplate(m::RegisterTemplate { id, nodes, roots })); id } } impl WriteMutations for MutationWriter { fn append_children(&mut self, id: ElementId, m: usize) { - self.batch.append_children(id.0 as u32, m as u32); + self.batch.push(m::Operation::AppendChildren(m::StackOp { id: id.0 as u32, m: m as u32 })); } fn assign_node_id(&mut self, path: &'static [u8], id: ElementId) { - self.batch.assign_id(path, id.0 as u32); + self.batch + .push(m::Operation::AssignId(m::AssignId { path: path.to_vec(), id: id.0 as u32 })); } fn create_placeholder(&mut self, id: ElementId) { - self.batch.create_placeholder(id.0 as u32); + self.batch.push(m::Operation::CreatePlaceholder(id.0 as u32)); } fn create_text_node(&mut self, value: &str, id: ElementId) { - self.batch.create_text_node(id.0 as u32, value); + self.batch.push(m::Operation::CreateTextNode(m::CreateTextNode { + id: id.0 as u32, + text: value.to_string(), + })); } fn load_template(&mut self, template: Template, index: usize, id: ElementId) { let tmpl = self.template_id(template); - self.batch.load_template(tmpl, index as u16, id.0 as u32); + self.batch.push(m::Operation::LoadTemplate(m::LoadTemplate { + id: id.0 as u32, + tmpl, + root: index as u16, + })); } fn replace_node_with(&mut self, id: ElementId, m: usize) { - self.batch.replace_with(id.0 as u32, m as u32); + self.batch.push(m::Operation::ReplaceWith(m::StackOp { id: id.0 as u32, m: m as u32 })); } fn replace_placeholder_with_nodes(&mut self, path: &'static [u8], m: usize) { - self.batch.replace_placeholder(path, m as u32); + self.batch + .push(m::Operation::ReplacePlaceholder(m::PathOp { path: path.to_vec(), m: m as u32 })); } fn insert_nodes_after(&mut self, id: ElementId, m: usize) { - self.batch.insert_after(id.0 as u32, m as u32); + self.batch.push(m::Operation::InsertAfter(m::StackOp { id: id.0 as u32, m: m as u32 })); } fn insert_nodes_before(&mut self, id: ElementId, m: usize) { - self.batch.insert_before(id.0 as u32, m as u32); + self.batch.push(m::Operation::InsertBefore(m::StackOp { id: id.0 as u32, m: m as u32 })); } fn set_attribute( @@ -202,235 +258,59 @@ impl WriteMutations for MutationWriter { value: &AttributeValue, id: ElementId, ) { - // Intern first: `cache-string` must not split the set-attribute op. + // Intern first: the CacheStrings must precede the SetAttribute that + // names their ids. let name_id = self.intern(name); let ns_id = self.intern_opt(ns); - let id = id.0 as u32; - match value { - AttributeValue::Text(s) => self.batch.set_attribute_text(id, name_id, ns_id, s), - AttributeValue::Float(f) => self.batch.set_attribute_float(id, name_id, ns_id, *f), - AttributeValue::Int(n) => self.batch.set_attribute_int(id, name_id, ns_id, *n), - AttributeValue::Bool(b) => self.batch.set_attribute_bool(id, name_id, ns_id, *b), - AttributeValue::None => self.batch.set_attribute_none(id, name_id, ns_id), + let value = match value { + AttributeValue::Text(s) => m::AttrValue::Text(s.clone()), + AttributeValue::Float(f) => m::AttrValue::Float(*f), + AttributeValue::Int(n) => m::AttrValue::Int(*n), + AttributeValue::Bool(b) => m::AttrValue::Boolean(*b), + AttributeValue::None => m::AttrValue::None, // Listener: reaches the renderer through `create_event_listener` // instead (dioxus never asks a renderer to serialize a callback). - // Any: a renderer-opaque payload for custom (non-HTML) renderers; - // there is nothing to put on the wire. dioxus's own channel - // renderer treats both as unreachable/ignored. - AttributeValue::Listener(_) | AttributeValue::Any(_) => {} - } + // Any: a renderer-opaque payload for custom (non-HTML) + // renderers; there is nothing to put on the wire. dioxus's own + // channel renderer treats both as unreachable/ignored. + AttributeValue::Listener(_) | AttributeValue::Any(_) => return, + }; + self.batch.push(m::Operation::SetAttribute(m::SetAttribute { + id: id.0 as u32, + name: name_id, + ns: ns_id, + value, + })); } fn set_node_text(&mut self, value: &str, id: ElementId) { - self.batch.set_text(id.0 as u32, value); + self.batch + .push(m::Operation::SetText(m::SetText { id: id.0 as u32, text: value.to_string() })); } fn create_event_listener(&mut self, name: &'static str, id: ElementId) { let name_id = self.intern(name); - self.batch.new_event_listener(id.0 as u32, name_id, dioxus_core_types::event_bubbles(name)); + self.batch.push(m::Operation::NewEventListener(m::EventListener { + id: id.0 as u32, + name: name_id, + bubbles: dioxus_core_types::event_bubbles(name), + })); } fn remove_event_listener(&mut self, name: &'static str, id: ElementId) { let name_id = self.intern(name); - self.batch.remove_event_listener(id.0 as u32, name_id, dioxus_core_types::event_bubbles(name)); + self.batch.push(m::Operation::RemoveEventListener(m::EventListener { + id: id.0 as u32, + name: name_id, + bubbles: dioxus_core_types::event_bubbles(name), + })); } fn remove_node(&mut self, id: ElementId) { - self.batch.remove(id.0 as u32); + self.batch.push(m::Operation::Remove(id.0 as u32)); } fn push_root(&mut self, id: ElementId) { - self.batch.push_root(id.0 as u32); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use dioxus::prelude::*; - - fn writer() -> MutationWriter { - MutationWriter::new(Rc::new(RefCell::new(Interner::new()))) - } - - /// Extract `(ops, strings)` segments from a `Batch` via `take_frame`, - /// parsing off the frame header (the only segment-extraction API since - /// the call transport's `take_segments` was removed — see - /// `protocol.rs`'s own `unframe` test helper, duplicated here for - /// `writer.rs`'s separate test module). - fn take_segments(batch: &mut Batch) -> (Vec, String) { - let mut out = Vec::new(); - batch.take_frame(&mut out); - let strings_len = u32::from_le_bytes(out[4..8].try_into().unwrap()) as usize; - let strings = String::from_utf8(out[8..8 + strings_len].to_vec()).unwrap(); - let ops = out[8 + strings_len..].to_vec(); - (ops, strings) - } - - /// Rebuild a real VirtualDom through the writer and compare the produced - /// segments against a hand-driven `Batch`. Any drift in op order, operand - /// widths, or intern-before-emit ordering shows up as a byte mismatch. - #[test] - fn rebuild_matches_hand_driven_batch() { - fn app() -> Element { - rsx! { - div { class: "root", - button { onclick: move |_| {}, "hi" } - } - } - } - - let mut w = writer(); - let mut dom = VirtualDom::new(app); - dom.rebuild(&mut w); - let (ops, strings) = take_segments(&mut w.batch); - - // Hand-driven expectation. Template 0 has one root: - // div(class="root")[ button[ text "hi" ] ] - // and the onclick listener is a *dynamic* attribute, so it is not part - // of the static template; it arrives as new-event-listener after the - // load. Intern ids are assigned in first-touch order during the - // interning pass over the template: div, class, button. - let mut b = Batch::new(); - let mut i = Interner::new(); - let div = i.intern(&mut b, "div"); - let class = i.intern(&mut b, "class"); - let button = i.intern(&mut b, "button"); - b.register_template(0, 1); - b.template_element_open(div, None, 1); - b.template_attr(class, None, "root"); - b.template_element_children(1); - b.template_element_open(button, None, 0); - b.template_element_children(1); - b.template_text("hi"); - b.load_template(0, 0, 1); - b.assign_id(&[0], 2); - let click = i.intern(&mut b, "click"); - b.new_event_listener(2, click, true); - b.append_children(0, 1); - let (exp_ops, exp_strings) = take_segments(&mut b); - - assert_eq!(strings, exp_strings, "string segment"); - assert_eq!(ops, exp_ops, "op segment"); - } - - /// A template is registered exactly once no matter how many times it is - /// loaded: two list items sharing one `rsx!` site produce one - /// register-template and two load-template ops. - #[test] - fn template_registered_once_per_identity() { - fn app() -> Element { - rsx! { - for n in 0..3 { - span { key: "{n}", "{n}" } - } - } - } - - let mut w = writer(); - let mut dom = VirtualDom::new(app); - dom.rebuild(&mut w); - let (ops, strings) = take_segments(&mut w.batch); - - // One `rsx!` site => one `Template` identity, registered once and - // loaded three times. Asserting on bytes (rather than counting - // opcode-valued bytes, which also occur inside operands) keeps this - // honest: the expected batch below contains exactly one - // register-template. - assert_eq!(w.templates.len(), 1); - - let mut b = Batch::new(); - let mut i = Interner::new(); - let span = i.intern(&mut b, "span"); - b.register_template(0, 1); - b.template_element_open(span, None, 0); - b.template_element_children(1); - b.template_dynamic(); - for (root_id, text_id) in [(1u32, 2u32), (3, 4), (5, 6)] { - b.load_template(0, 0, root_id); - b.create_text_node(text_id, &((root_id - 1) / 2).to_string()); - b.replace_placeholder(&[0], 1); - } - b.append_children(0, 3); - let (exp_ops, exp_strings) = take_segments(&mut b); - assert_eq!(strings, exp_strings, "string segment"); - assert_eq!(ops, exp_ops, "op segment"); - - // Re-rendering the same tree must not re-register anything. - let before = w.templates.len(); - dom.render_immediate(&mut w); - assert_eq!(w.templates.len(), before); - } - - /// Wire flags bit0 is dioxus's own `event_bubbles` verdict. `click` - /// bubbles; `focus` does not — the host keys its delegation strategy off - /// this bit, so getting it backwards silently breaks event delivery. - #[test] - fn listener_bubbles_bit_follows_dioxus_table() { - assert!(dioxus_core_types::event_bubbles("click")); - assert!(!dioxus_core_types::event_bubbles("focus")); - - let mut w = writer(); - w.create_event_listener("click", ElementId(1)); - w.create_event_listener("focus", ElementId(2)); - w.remove_event_listener("focus", ElementId(2)); - let (ops, strings) = take_segments(&mut w.batch); - - let mut b = Batch::new(); - let mut i = Interner::new(); - let click = i.intern(&mut b, "click"); - b.new_event_listener(1, click, true); - let focus = i.intern(&mut b, "focus"); - b.new_event_listener(2, focus, false); - b.remove_event_listener(2, focus, false); - let (exp_ops, exp_strings) = take_segments(&mut b); - - assert_eq!(ops, exp_ops); - assert_eq!(strings, exp_strings); - } - - /// `AttributeValue::None` is a distinct wire kind (unconditional removal), - /// not an empty text value, and the float/int/bool kinds keep their - /// numeric encodings rather than being stringified. - #[test] - fn attribute_value_kinds_map_one_to_one() { - let mut w = writer(); - w.set_attribute("width", None, &AttributeValue::Float(1.5), ElementId(1)); - w.set_attribute("tabindex", None, &AttributeValue::Int(-3), ElementId(1)); - w.set_attribute("hidden", None, &AttributeValue::Bool(false), ElementId(1)); - w.set_attribute("title", None, &AttributeValue::None, ElementId(1)); - let (ops, strings) = take_segments(&mut w.batch); - - let mut b = Batch::new(); - let mut i = Interner::new(); - let width = i.intern(&mut b, "width"); - b.set_attribute_float(1, width, None, 1.5); - let tabindex = i.intern(&mut b, "tabindex"); - b.set_attribute_int(1, tabindex, None, -3); - let hidden = i.intern(&mut b, "hidden"); - b.set_attribute_bool(1, hidden, None, false); - let title = i.intern(&mut b, "title"); - b.set_attribute_none(1, title, None); - let (exp_ops, exp_strings) = take_segments(&mut b); - - assert_eq!(ops, exp_ops); - assert_eq!(strings, exp_strings); - } - - /// `AttributeValue::Any` is renderer-opaque (it exists for non-HTML - /// renderers) and produces no op at all. The `Listener` arm is covered by - /// `rebuild_matches_hand_driven_batch`: the `onclick` there reaches the - /// writer as a listener attribute and contributes a new-event-listener op - /// rather than a set-attribute one. - #[test] - fn any_attribute_value_is_skipped() { - let mut w = writer(); - w.set_attribute("data-x", None, &AttributeValue::any_value(7u32), ElementId(1)); - let (ops, _) = take_segments(&mut w.batch); - // Only the `cache-string` for "data-x" — no set-attribute op. - const CACHE_STRING: u8 = 0x01; - assert_eq!(ops[0], CACHE_STRING); - const SET_ATTRIBUTE: u8 = 0x0c; - assert!(!ops.contains(&SET_ATTRIBUTE)); + self.batch.push(m::Operation::PushRoot(id.0 as u32)); } } diff --git a/tests/vectors.rs b/tests/vectors.rs deleted file mode 100644 index 1a3ad88..0000000 --- a/tests/vectors.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Golden vector generator/verifier for the wire format in `src/protocol.rs` -//! / `wit/world.wit`. These vectors are shared with the TypeScript decoder -//! (host side), so the `.expected.json` shape is contractual — see the -//! dispatch/README for the exact schema. -//! -//! Each vector's bytes and JSON are produced by ONE function that drives -//! both `Batch`/`Interner` (for bytes) and a parallel `serde_json::Value` -//! builder (for the expected JSON), op-by-op, so the two representations -//! cannot drift apart from each other by construction. - -use polyengine_dioxus::protocol::{Batch, Interner}; -use serde_json::{json, Value}; -use std::fs; -use std::path::Path; - -/// Records one vector: drives `Batch`/`Interner` for the byte encoding and -/// builds the matching JSON op list in lockstep. Frame boundaries are -/// explicit (`take_frame`) so the `frames` vector can split one op stream -/// across two physical frames. -struct Rec { - batch: Batch, - interner: Interner, - /// Ops recorded since the last `take_frame` (or start). - current_ops: Vec, - /// One entry per completed frame (each a JSON array of ops). - frames_json: Vec, - /// Framed bytes emitted so far (stream-transport framing). - bytes: Vec, - /// Pointer-identity strings already interned, purely so this recorder - /// knows when to emit a `cache-string` JSON op (mirrors, but does not - /// read, `Interner`'s private dedup — `Interner::intern` is the actual - /// source of truth for the bytes). - seen: std::collections::HashMap<(usize, usize), u16>, -} - -impl Rec { - fn new() -> Self { - Rec { - batch: Batch::new(), - interner: Interner::new(), - current_ops: Vec::new(), - frames_json: Vec::new(), - bytes: Vec::new(), - seen: std::collections::HashMap::new(), - } - } - - fn push_op(&mut self, v: Value) { - self.current_ops.push(v); - } - - /// Intern a `&'static str`, recording a `cache-string` JSON op the - /// first time this pointer identity is seen (matching `Interner`'s - /// documented dedup behavior). - fn intern(&mut self, s: &'static str) -> u16 { - let key = (s.as_ptr() as usize, s.len()); - let was_new = !self.seen.contains_key(&key); - let id = self.interner.intern(&mut self.batch, s); - if was_new { - self.seen.insert(key, id); - self.push_op(json!({"op": "cache-string", "id": id, "s": s})); - } - id - } - - fn ns_json(ns: Option) -> Value { - match ns { - Some(n) => json!(n), - None => Value::Null, - } - } - - fn append_children(&mut self, id: u32, m: u32) { - self.batch.append_children(id, m); - self.push_op(json!({"op": "append-children", "id": id, "m": m})); - } - - fn assign_id(&mut self, path: &[u8], id: u32) { - self.batch.assign_id(path, id); - self.push_op(json!({"op": "assign-id", "path": path, "id": id})); - } - - fn create_placeholder(&mut self, id: u32) { - self.batch.create_placeholder(id); - self.push_op(json!({"op": "create-placeholder", "id": id})); - } - - fn create_text_node(&mut self, id: u32, text: &str) { - self.batch.create_text_node(id, text); - self.push_op(json!({"op": "create-text-node", "id": id, "text": text})); - } - - fn load_template(&mut self, tmpl: u16, root: u16, id: u32) { - self.batch.load_template(tmpl, root, id); - self.push_op(json!({"op": "load-template", "tmpl": tmpl, "root": root, "id": id})); - } - - fn replace_with(&mut self, id: u32, m: u32) { - self.batch.replace_with(id, m); - self.push_op(json!({"op": "replace-with", "id": id, "m": m})); - } - - fn replace_placeholder(&mut self, path: &[u8], m: u32) { - self.batch.replace_placeholder(path, m); - self.push_op(json!({"op": "replace-placeholder", "path": path, "m": m})); - } - - fn insert_after(&mut self, id: u32, m: u32) { - self.batch.insert_after(id, m); - self.push_op(json!({"op": "insert-after", "id": id, "m": m})); - } - - fn insert_before(&mut self, id: u32, m: u32) { - self.batch.insert_before(id, m); - self.push_op(json!({"op": "insert-before", "id": id, "m": m})); - } - - fn set_attribute_text(&mut self, id: u32, name: u16, ns: Option, value: &str) { - self.batch.set_attribute_text(id, name, ns, value); - self.push_op(json!({ - "op": "set-attribute", "id": id, "name": name, "ns": Self::ns_json(ns), - "value": {"kind": "text", "s": value} - })); - } - - fn set_attribute_float(&mut self, id: u32, name: u16, ns: Option, value: f64) { - self.batch.set_attribute_float(id, name, ns, value); - self.push_op(json!({ - "op": "set-attribute", "id": id, "name": name, "ns": Self::ns_json(ns), - "value": {"kind": "float", "f": value} - })); - } - - fn set_attribute_int(&mut self, id: u32, name: u16, ns: Option, value: i64) { - self.batch.set_attribute_int(id, name, ns, value); - self.push_op(json!({ - "op": "set-attribute", "id": id, "name": name, "ns": Self::ns_json(ns), - "value": {"kind": "int", "i": value.to_string()} - })); - } - - fn set_attribute_bool(&mut self, id: u32, name: u16, ns: Option, value: bool) { - self.batch.set_attribute_bool(id, name, ns, value); - self.push_op(json!({ - "op": "set-attribute", "id": id, "name": name, "ns": Self::ns_json(ns), - "value": {"kind": "bool", "b": value} - })); - } - - fn set_attribute_none(&mut self, id: u32, name: u16, ns: Option) { - self.batch.set_attribute_none(id, name, ns); - self.push_op(json!({ - "op": "set-attribute", "id": id, "name": name, "ns": Self::ns_json(ns), - "value": {"kind": "none"} - })); - } - - fn set_text(&mut self, id: u32, text: &str) { - self.batch.set_text(id, text); - self.push_op(json!({"op": "set-text", "id": id, "text": text})); - } - - fn new_event_listener(&mut self, id: u32, name: u16, bubbles: bool) { - self.batch.new_event_listener(id, name, bubbles); - self.push_op( - json!({"op": "new-event-listener", "id": id, "name": name, "bubbles": bubbles}), - ); - } - - fn remove_event_listener(&mut self, id: u32, name: u16, bubbles: bool) { - self.batch.remove_event_listener(id, name, bubbles); - self.push_op( - json!({"op": "remove-event-listener", "id": id, "name": name, "bubbles": bubbles}), - ); - } - - fn remove(&mut self, id: u32) { - self.batch.remove(id); - self.push_op(json!({"op": "remove", "id": id})); - } - - fn push_root(&mut self, id: u32) { - self.batch.push_root(id); - self.push_op(json!({"op": "push-root", "id": id})); - } - - /// Register a template with the given roots, recursing through - /// `TNode` to drive both the `Batch` node-emitting calls and the - /// matching JSON `tnode` values. - fn register_template(&mut self, tmpl: u16, roots: &[TNode]) { - self.batch.register_template(tmpl, roots.len() as u16); - let root_json: Vec = roots.iter().map(|n| self.emit_node(n)).collect(); - self.push_op(json!({"op": "register-template", "tmpl": tmpl, "roots": root_json})); - } - - fn emit_node(&mut self, node: &TNode) -> Value { - match node { - TNode::Element { tag, ns, attrs, children } => { - self.batch.template_element_open(*tag, *ns, attrs.len() as u16); - let attrs_json: Vec = attrs - .iter() - .map(|(name, ns, value)| { - self.batch.template_attr(*name, *ns, value); - json!({"name": name, "ns": Self::ns_json(*ns), "value": value}) - }) - .collect(); - self.batch.template_element_children(children.len() as u16); - let children_json: Vec = children.iter().map(|c| self.emit_node(c)).collect(); - json!({ - "kind": "element", "tag": tag, "ns": Self::ns_json(*ns), - "attrs": attrs_json, "children": children_json - }) - } - TNode::Text(value) => { - self.batch.template_text(value); - json!({"kind": "text", "value": value}) - } - TNode::Dynamic => { - self.batch.template_dynamic(); - json!({"kind": "dynamic"}) - } - } - } - - /// Close out the current frame: flush ops recorded so far into one - /// physical frame (bytes + JSON). - fn take_frame(&mut self) { - self.batch.take_frame(&mut self.bytes); - let ops = std::mem::take(&mut self.current_ops); - self.frames_json.push(Value::Array(ops)); - } - - /// Finish the vector: if anything is pending, close it as a final - /// frame, then return `(bytes, expected-json)`. - fn finish(mut self) -> (Vec, Value) { - if !self.current_ops.is_empty() || !self.batch.is_empty() { - self.take_frame(); - } - (self.bytes, json!({"frames": self.frames_json})) - } -} - -/// A template node spec used only by this test to drive `Batch`'s -/// node-emitting calls and the matching expected-JSON `tnode` value in -/// lockstep (mirrors the `node` grammar in `wit/world.wit`). -enum TNode { - Element { tag: u16, ns: Option, attrs: Vec<(u16, Option, String)>, children: Vec }, - Text(String), - Dynamic, -} - -// Interned name statics. Pointer identity is what `Interner` dedups on, so -// reusing the same static (e.g. `DIV` used twice) is what exercises the -// "cache hit" path; `DIV2` is a distinct static with equal contents to -// confirm the documented "two ids for equal-content statics is harmless" -// behavior is at least exercised. -static DIV: &str = "div"; -static SPAN: &str = "span"; -static CLASS: &str = "class"; -static STYLE: &str = "style"; -static SVG_NS: &str = "http://www.w3.org/2000/svg"; -static CLICK: &str = "click"; - -/// Every opcode at least once; interning with a cache hit (`DIV` used -/// twice); every `attrval` kind; a `none`-namespace and a `some`-namespace -/// `set-attribute`. -fn build_basic(rec: &mut Rec) { - let div = rec.intern(DIV); - let _div_again = rec.intern(DIV); // cache hit: no second cache-string op - let span = rec.intern(SPAN); - let class = rec.intern(CLASS); - let style = rec.intern(STYLE); - let svg_ns = rec.intern(SVG_NS); - let click = rec.intern(CLICK); - - rec.register_template(0, &[TNode::Dynamic]); - rec.load_template(0, 0, 1); - rec.create_placeholder(2); - rec.create_text_node(3, "hello"); - rec.append_children(1, 2); - rec.assign_id(&[0, 1], 5); - rec.replace_with(9, 1); - rec.replace_placeholder(&[0], 2); - rec.insert_after(3, 1); - rec.insert_before(3, 1); - - // Every attrval kind; one none-namespace, one some-namespace. - rec.set_attribute_text(2, class, None, "x"); - rec.set_attribute_float(2, style, Some(svg_ns), 1.5); - rec.set_attribute_int(2, div, None, -3); - rec.set_attribute_bool(2, span, None, true); - rec.set_attribute_none(2, class, None); - - rec.set_text(2, "t"); - rec.new_event_listener(2, click, true); - rec.remove_event_listener(2, click, false); - rec.remove(2); - rec.push_root(1); -} - -/// Dynamic strings covering: empty, ASCII, accented BMP, CJK, surrogate -/// pairs, and a >65600-UTF-16-unit string (exercises the `dynstr` 0xffff -/// escape). -fn build_unicode(rec: &mut Rec) { - // "ab£".repeat(N) has UTF-16 length 3*N (a,b are 1 unit each, £ is 1 - // BMP unit). Need >65600, so N must be > 21867; use 22000 for margin. - let long = "ab£".repeat(22000); - debug_assert!(long.chars().map(char::len_utf16).sum::() > 65600); - - rec.create_text_node(0, ""); - rec.create_text_node(1, "hello"); - rec.create_text_node(2, "héllo wörld"); - rec.create_text_node(3, "你好世界"); - rec.create_text_node(4, "👍🏽 emoji"); - rec.create_text_node(5, &long); -} - -/// `register-template` with two roots: (1) a 3-level element tree mixing a -/// namespaced static attr, a non-namespaced static attr, a text child, and -/// a dynamic placeholder; (2) a bare dynamic root. Followed by -/// load-template + assign-id + replace-placeholder uses. -fn build_template(rec: &mut Rec) { - let div = rec.intern(DIV); - let span = rec.intern(SPAN); - let class = rec.intern(CLASS); - let style = rec.intern(STYLE); - let svg_ns = rec.intern(SVG_NS); - - let roots = vec![ - TNode::Element { - tag: div, - ns: None, - attrs: vec![(class, None, "container".to_string())], - children: vec![ - TNode::Element { - tag: span, - ns: Some(svg_ns), - attrs: vec![(style, Some(svg_ns), "color:red".to_string())], - children: vec![TNode::Text("hello".to_string()), TNode::Dynamic], - }, - TNode::Text("world".to_string()), - ], - }, - TNode::Dynamic, - ]; - rec.register_template(0, &roots); - - rec.load_template(0, 0, 10); - rec.assign_id(&[0, 1], 11); - rec.replace_placeholder(&[0], 1); -} - -fn vector_dir() -> &'static Path { - Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/vectors")) -} - -fn write_vector(name: &str, bytes: &[u8], json: &Value) { - let dir = vector_dir(); - fs::create_dir_all(dir).expect("create vectors dir"); - fs::write(dir.join(format!("{name}.bin")), bytes).expect("write .bin"); - let pretty = serde_json::to_string_pretty(json).expect("serialize json"); - fs::write(dir.join(format!("{name}.expected.json")), pretty + "\n").expect("write .json"); -} - -fn read_vector(name: &str) -> (Vec, Value) { - let dir = vector_dir(); - let bin_path = dir.join(format!("{name}.bin")); - let json_path = dir.join(format!("{name}.expected.json")); - let bytes = fs::read(&bin_path).unwrap_or_else(|_| { - panic!( - "missing {} — run `cargo test --test vectors -- --ignored generate` to (re)generate vectors", - bin_path.display() - ) - }); - let text = fs::read_to_string(&json_path).unwrap_or_else(|_| { - panic!( - "missing {} — run `cargo test --test vectors -- --ignored generate` to (re)generate vectors", - json_path.display() - ) - }); - let json: Value = serde_json::from_str(&text).expect("parse expected.json"); - (bytes, json) -} - -/// Build all four vectors: `(name, bytes, json)`. -fn build_all() -> Vec<(&'static str, Vec, Value)> { - let mut basic_rec = Rec::new(); - build_basic(&mut basic_rec); - let (basic_bytes, basic_json) = basic_rec.finish(); - - let mut unicode_rec = Rec::new(); - build_unicode(&mut unicode_rec); - let (unicode_bytes, unicode_json) = unicode_rec.finish(); - - let mut template_rec = Rec::new(); - build_template(&mut template_rec); - let (template_bytes, template_json) = template_rec.finish(); - - // `frames`: the ops of `basic` split across two frames in one .bin — - // rerun the same driver but call `take_frame` at the halfway point. - let mut frames_rec = Rec::new(); - build_basic_split(&mut frames_rec); - let (frames_bytes, frames_json) = frames_rec.finish(); - - vec![ - ("basic", basic_bytes, basic_json), - ("unicode", unicode_bytes, unicode_json), - ("template", template_bytes, template_json), - ("frames", frames_bytes, frames_json), - ] -} - -/// Same op sequence as `build_basic`, but split into two `take_frame` calls -/// partway through (after `replace_placeholder`) so the `frames` vector -/// exercises multi-frame decoding. -fn build_basic_split(rec: &mut Rec) { - let div = rec.intern(DIV); - let _div_again = rec.intern(DIV); - let span = rec.intern(SPAN); - let class = rec.intern(CLASS); - let style = rec.intern(STYLE); - let svg_ns = rec.intern(SVG_NS); - let click = rec.intern(CLICK); - - rec.register_template(0, &[TNode::Dynamic]); - rec.load_template(0, 0, 1); - rec.create_placeholder(2); - rec.create_text_node(3, "hello"); - rec.append_children(1, 2); - rec.assign_id(&[0, 1], 5); - rec.replace_with(9, 1); - rec.replace_placeholder(&[0], 2); - - rec.take_frame(); // frame boundary mid-stream - - rec.insert_after(3, 1); - rec.insert_before(3, 1); - rec.set_attribute_text(2, class, None, "x"); - rec.set_attribute_float(2, style, Some(svg_ns), 1.5); - rec.set_attribute_int(2, div, None, -3); - rec.set_attribute_bool(2, span, None, true); - rec.set_attribute_none(2, class, None); - rec.set_text(2, "t"); - rec.new_event_listener(2, click, true); - rec.remove_event_listener(2, click, false); - rec.remove(2); - rec.push_root(1); -} - -#[test] -#[ignore = "regenerates committed vector fixtures; run explicitly"] -fn generate() { - for (name, bytes, json) in build_all() { - write_vector(name, &bytes, &json); - } -} - -#[test] -fn golden_matches_committed() { - for (name, bytes, json) in build_all() { - let (committed_bytes, committed_json) = read_vector(name); - assert_eq!( - bytes, committed_bytes, - "vector `{name}`: regenerated bytes differ from vectors/{name}.bin \ - (run `cargo test --test vectors -- --ignored generate` if this is intentional)" - ); - assert_eq!( - json, committed_json, - "vector `{name}`: regenerated JSON differs from vectors/{name}.expected.json" - ); - } -} diff --git a/vectors/basic.bin b/vectors/basic.bin deleted file mode 100644 index d2e6ba2..0000000 Binary files a/vectors/basic.bin and /dev/null differ diff --git a/vectors/basic.expected.json b/vectors/basic.expected.json deleted file mode 100644 index 47c502a..0000000 --- a/vectors/basic.expected.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "frames": [ - [ - { - "id": 0, - "op": "cache-string", - "s": "div" - }, - { - "id": 1, - "op": "cache-string", - "s": "span" - }, - { - "id": 2, - "op": "cache-string", - "s": "class" - }, - { - "id": 3, - "op": "cache-string", - "s": "style" - }, - { - "id": 4, - "op": "cache-string", - "s": "http://www.w3.org/2000/svg" - }, - { - "id": 5, - "op": "cache-string", - "s": "click" - }, - { - "op": "register-template", - "roots": [ - { - "kind": "dynamic" - } - ], - "tmpl": 0 - }, - { - "id": 1, - "op": "load-template", - "root": 0, - "tmpl": 0 - }, - { - "id": 2, - "op": "create-placeholder" - }, - { - "id": 3, - "op": "create-text-node", - "text": "hello" - }, - { - "id": 1, - "m": 2, - "op": "append-children" - }, - { - "id": 5, - "op": "assign-id", - "path": [ - 0, - 1 - ] - }, - { - "id": 9, - "m": 1, - "op": "replace-with" - }, - { - "m": 2, - "op": "replace-placeholder", - "path": [ - 0 - ] - }, - { - "id": 3, - "m": 1, - "op": "insert-after" - }, - { - "id": 3, - "m": 1, - "op": "insert-before" - }, - { - "id": 2, - "name": 2, - "ns": null, - "op": "set-attribute", - "value": { - "kind": "text", - "s": "x" - } - }, - { - "id": 2, - "name": 3, - "ns": 4, - "op": "set-attribute", - "value": { - "f": 1.5, - "kind": "float" - } - }, - { - "id": 2, - "name": 0, - "ns": null, - "op": "set-attribute", - "value": { - "i": "-3", - "kind": "int" - } - }, - { - "id": 2, - "name": 1, - "ns": null, - "op": "set-attribute", - "value": { - "b": true, - "kind": "bool" - } - }, - { - "id": 2, - "name": 2, - "ns": null, - "op": "set-attribute", - "value": { - "kind": "none" - } - }, - { - "id": 2, - "op": "set-text", - "text": "t" - }, - { - "bubbles": true, - "id": 2, - "name": 5, - "op": "new-event-listener" - }, - { - "bubbles": false, - "id": 2, - "name": 5, - "op": "remove-event-listener" - }, - { - "id": 2, - "op": "remove" - }, - { - "id": 1, - "op": "push-root" - } - ] - ] -} diff --git a/vectors/frames.bin b/vectors/frames.bin deleted file mode 100644 index f4b48d8..0000000 Binary files a/vectors/frames.bin and /dev/null differ diff --git a/vectors/frames.expected.json b/vectors/frames.expected.json deleted file mode 100644 index 29a185b..0000000 --- a/vectors/frames.expected.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "frames": [ - [ - { - "id": 0, - "op": "cache-string", - "s": "div" - }, - { - "id": 1, - "op": "cache-string", - "s": "span" - }, - { - "id": 2, - "op": "cache-string", - "s": "class" - }, - { - "id": 3, - "op": "cache-string", - "s": "style" - }, - { - "id": 4, - "op": "cache-string", - "s": "http://www.w3.org/2000/svg" - }, - { - "id": 5, - "op": "cache-string", - "s": "click" - }, - { - "op": "register-template", - "roots": [ - { - "kind": "dynamic" - } - ], - "tmpl": 0 - }, - { - "id": 1, - "op": "load-template", - "root": 0, - "tmpl": 0 - }, - { - "id": 2, - "op": "create-placeholder" - }, - { - "id": 3, - "op": "create-text-node", - "text": "hello" - }, - { - "id": 1, - "m": 2, - "op": "append-children" - }, - { - "id": 5, - "op": "assign-id", - "path": [ - 0, - 1 - ] - }, - { - "id": 9, - "m": 1, - "op": "replace-with" - }, - { - "m": 2, - "op": "replace-placeholder", - "path": [ - 0 - ] - } - ], - [ - { - "id": 3, - "m": 1, - "op": "insert-after" - }, - { - "id": 3, - "m": 1, - "op": "insert-before" - }, - { - "id": 2, - "name": 2, - "ns": null, - "op": "set-attribute", - "value": { - "kind": "text", - "s": "x" - } - }, - { - "id": 2, - "name": 3, - "ns": 4, - "op": "set-attribute", - "value": { - "f": 1.5, - "kind": "float" - } - }, - { - "id": 2, - "name": 0, - "ns": null, - "op": "set-attribute", - "value": { - "i": "-3", - "kind": "int" - } - }, - { - "id": 2, - "name": 1, - "ns": null, - "op": "set-attribute", - "value": { - "b": true, - "kind": "bool" - } - }, - { - "id": 2, - "name": 2, - "ns": null, - "op": "set-attribute", - "value": { - "kind": "none" - } - }, - { - "id": 2, - "op": "set-text", - "text": "t" - }, - { - "bubbles": true, - "id": 2, - "name": 5, - "op": "new-event-listener" - }, - { - "bubbles": false, - "id": 2, - "name": 5, - "op": "remove-event-listener" - }, - { - "id": 2, - "op": "remove" - }, - { - "id": 1, - "op": "push-root" - } - ] - ] -} diff --git a/vectors/template.bin b/vectors/template.bin deleted file mode 100644 index ac91c3f..0000000 Binary files a/vectors/template.bin and /dev/null differ diff --git a/vectors/template.expected.json b/vectors/template.expected.json deleted file mode 100644 index b4db85e..0000000 --- a/vectors/template.expected.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "frames": [ - [ - { - "id": 0, - "op": "cache-string", - "s": "div" - }, - { - "id": 1, - "op": "cache-string", - "s": "span" - }, - { - "id": 2, - "op": "cache-string", - "s": "class" - }, - { - "id": 3, - "op": "cache-string", - "s": "style" - }, - { - "id": 4, - "op": "cache-string", - "s": "http://www.w3.org/2000/svg" - }, - { - "op": "register-template", - "roots": [ - { - "attrs": [ - { - "name": 2, - "ns": null, - "value": "container" - } - ], - "children": [ - { - "attrs": [ - { - "name": 3, - "ns": 4, - "value": "color:red" - } - ], - "children": [ - { - "kind": "text", - "value": "hello" - }, - { - "kind": "dynamic" - } - ], - "kind": "element", - "ns": 4, - "tag": 1 - }, - { - "kind": "text", - "value": "world" - } - ], - "kind": "element", - "ns": null, - "tag": 0 - }, - { - "kind": "dynamic" - } - ], - "tmpl": 0 - }, - { - "id": 10, - "op": "load-template", - "root": 0, - "tmpl": 0 - }, - { - "id": 11, - "op": "assign-id", - "path": [ - 0, - 1 - ] - }, - { - "m": 1, - "op": "replace-placeholder", - "path": [ - 0 - ] - } - ] - ] -} diff --git a/vectors/unicode.bin b/vectors/unicode.bin deleted file mode 100644 index 80bb777..0000000 Binary files a/vectors/unicode.bin and /dev/null differ diff --git a/vectors/unicode.expected.json b/vectors/unicode.expected.json deleted file mode 100644 index 037e506..0000000 --- a/vectors/unicode.expected.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "frames": [ - [ - { - "id": 0, - "op": "create-text-node", - "text": "" - }, - { - "id": 1, - "op": "create-text-node", - "text": "hello" - }, - { - "id": 2, - "op": "create-text-node", - "text": "héllo wörld" - }, - { - "id": 3, - "op": "create-text-node", - "text": "你好世界" - }, - { - "id": 4, - "op": "create-text-node", - "text": "👍🏽 emoji" - }, - { - "id": 5, - "op": "create-text-node", - "text": "ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£ab£" - } - ] - ] -} diff --git a/wit/world.wit b/wit/world.wit index 3712ada..04297c7 100644 --- a/wit/world.wit +++ b/wit/world.wit @@ -1,19 +1,18 @@ /// polymorph:dioxus — a batched DOM-mutation surface for Dioxus apps running /// as components on polyengine. /// -/// Design: the guest queues DOM mutations into a compact byte format -/// (sledgehammer-bindgen-shaped) and delivers whole batches across the -/// boundary; the host decodes with a tight switch loop and applies them to a -/// live DOM subtree. Strings cross the boundary two ways: `&'static` names -/// (tags, attribute names, event names) are interned once and referenced by -/// u16 id thereafter; dynamic text is concatenated per batch and decoded with -/// a single TextDecoder pass. +/// Design: the guest queues DOM mutations as `mutations.operation` values and +/// delivers whole batches across the boundary in one stream write; the host +/// applies them to a live DOM subtree. Strings cross the boundary two ways: +/// `&'static` names (tags, attribute names, event names) are interned once +/// and referenced by u16 id thereafter (`cache-string`); dynamic text rides +/// in the operations themselves. /// -/// The wire format below is normative and versions with this WIT package: -/// any change to opcodes, operand layouts, or framing bumps the package -/// version (instantiation is digest-checked, so a mismatch fails loudly and -/// early). -package polymorph:dioxus@0.4.0; +/// The `mutations` interface is the normative schema and versions with this +/// WIT package: any change to the operation vocabulary or its record shapes +/// bumps the package version (instantiation is digest-checked, so a mismatch +/// fails loudly and early). +package polymorph:dioxus@0.5.0; /// Typed event payloads, host-serialized at dispatch. One snapshot per /// event; fields not applicable to an event family are simply absent by @@ -296,6 +295,145 @@ interface events { } } +/// The mutation protocol: the op vocabulary the guest renders through, +/// spelled as WIT records and one `operation` variant, delivered as +/// `stream` by the world's `run` export. This interface is +/// normative — there is no other encoding of a mutation. +/// +/// The vocabulary mirrors dioxus-core 0.7's `WriteMutations` trait plus two +/// protocol ops (`cache-string`, `register-template`). Element ids are +/// Dioxus ElementIds (slab indices, dense; id 0 is the mount root — the +/// host-designated element the app renders into), `m` is the "top m nodes of +/// the stack" count, `path` is a list of child indices, and stack semantics +/// are Dioxus's mutation-stack semantics, identical to its own web +/// interpreter. `bubbles` on the listener ops is dioxus-html's +/// `event_bubbles` verdict, looked up guest-side: the host delegates +/// bubbling events at the mount root and attaches non-bubbling listeners per +/// element, so it needs the same bit on removal to find the registration. +/// +/// Interning: a `str-ref` is an id defined by a prior `cache-string` +/// operation, in this batch or any earlier one, and the guest emits each +/// definition exactly once per instance. Event names cross back on +/// `handle-event` as the same interned u16, so steady-state event dispatch +/// transfers no string data at all. +/// +/// **`register-template` carries an arena, not a tree.** The natural shape +/// is recursive — `record template-element { children: list }` +/// — and WIT forbids it: recursive type definitions are rejected outright +/// (`wasm-tools`: "type `template-node` depends on itself"). So `nodes` is a +/// flat pre-order list, `roots` and every element's `children` are `u32` +/// indices into it, and the host rebuilds the tree by indexing. The arena +/// therefore admits malformed index graphs — out-of-range or cyclic indices — +/// that a recursive shape could not express, and the host must validate them +/// rather than trust them. + +interface mutations { + /// An interned string id, defined by a prior `cache-string` operation. + type str-ref = u16; + /// A Dioxus ElementId (slab index; dense). 0 is the mount root. + type element-id = u32; + + /// Define (or overwrite) interned slot `id`. Ids are guest-managed and + /// assigned monotonically from 0. + record cache-string { id: str-ref, str: string } + + /// One *static* template attribute. Dynamic template attributes are not + /// part of the template; they arrive later as `set-attribute`. + record template-attr { name: str-ref, ns: option, value: string } + + /// `children` are indices into the enclosing `register-template`'s + /// `nodes` arena — see the interface doc for why this is not a tree. + record template-element { + tag: str-ref, + ns: option, + attrs: list, + children: list, + } + + variant template-node { + element(template-element), + text(string), + /// A runtime-supplied node slot; the host materializes a placeholder + /// that later ops (`assign-id` / `replace-placeholder`) address by path. + dynamic, + } + + /// Register a static template, which the host materializes once + /// (detached) so `load-template` can clone root subtrees from it. Each + /// distinct template is sent once, before its first use. + /// + /// `nodes` is the arena; `roots` indexes into it. + record register-template { + id: u16, + nodes: list, + roots: list, + } + + /// An element id plus a stack count ("top m nodes"). + record stack-op { id: element-id, m: u32 } + /// A template path plus a stack count. + record path-op { path: list, m: u32 } + record assign-id { path: list, id: element-id } + record create-text-node { id: element-id, text: string } + record load-template { id: element-id, tmpl: u16, root: u16 } + record set-text { id: element-id, text: string } + + /// An attribute's new value. `none` removes the attribute + /// unconditionally — it is not an empty text value. + variant attr-value { + text(string), + float(f64), + int(s64), + boolean(bool), + none, + } + + /// Set (or, with `value: none`, remove) one attribute on a live element. + /// + /// The host applies Dioxus's attribute semantics: value/checked/selected + /// as properties, the style namespace via the style object, + /// dangerous_inner_html, and boolean-attribute removal on false. + /// + /// `none` routes via removeAttribute/removeAttributeNS/ + /// style.removeProperty as the name and namespace dictate; the + /// boolean-attribute truthiness table governs only text/bool VALUES. + /// Property-backed fields get the same reset the truthy-value path uses + /// instead of a bare removeAttribute, since removeAttribute alone does not + /// clear a live element's property: value -> "" (+ removeAttribute), + /// checked -> false, selected -> false, dangerous_inner_html -> innerHTML + /// = "". (Ported from dioxus v0.7.10's `remove_attribute` sledgehammer op + /// in packages/interpreter/src/unified_bindings.rs.) + record set-attribute { + id: element-id, + name: str-ref, + ns: option, + value: attr-value, + } + + record event-listener { id: element-id, name: str-ref, bubbles: bool } + + /// One mutation. Arms whose only operand is an element id carry it bare. + variant operation { + cache-string(cache-string), + register-template(register-template), + append-children(stack-op), + assign-id(assign-id), + create-placeholder(element-id), + create-text-node(create-text-node), + load-template(load-template), + replace-with(stack-op), + replace-placeholder(path-op), + insert-after(stack-op), + insert-before(stack-op), + set-attribute(set-attribute), + set-text(set-text), + new-event-listener(event-listener), + remove-event-listener(event-listener), + remove(element-id), + push-root(element-id), + } +} + /// Operations on live elements the guest cannot express declaratively. /// /// Everything else in this world flows one way: the guest describes DOM @@ -379,151 +517,48 @@ interface dom { /// dispatches DOM events via `handle-event`. world app { use events.{payload, dom-event}; + use mutations.{operation}; import events; import dom; /// Start the app and return the mutation channel: the host receives the - /// read end, parks a direct-read session on it for the life of the - /// instance (polyengine amendment A21 `readDirect` — the decode callback - /// runs synchronously inside the guest's `stream.write` rendezvous over a - /// view aliasing guest linear memory; zero copies), and applies decoded - /// batches to the mount root. The app's scheduler keeps running after the - /// return as a spawned task: initial mount (rebuild → one batch), then - /// re-renders forever. That task's persistent park between renders (a - /// plain Rust future woken cross-task, no WIT waitable) is legal because - /// the host's parked direct-read session is amendment-A15 host retention, - /// making a quiescent instance the documented embedder-may-act state — and - /// it also means app failure surfaces on the channel the host actually - /// watches: a trap rejects the parked session (`PeerTrappedError`). + /// read end, reads batches of `operation`s from it for the life of the + /// instance, and applies them to the mount root. The app's scheduler keeps + /// running after the return as a spawned task: initial mount (rebuild → + /// one batch), then re-renders forever. One batch is one stream write of + /// the whole `list`. /// /// Returning the stream (rather than passing it to a host import) makes /// the channel's lifecycle structural: it exists iff the app started, - /// exactly once, tied to this instance. - /// - /// # Batch layout - /// - /// A batch has two segments: - /// - /// - **string segment**: UTF-8 bytes, the concatenation of every dynamic - /// string operand (`dynstr` below) in op order. Decoded host-side in one - /// pass (`TextDecoder("utf-8", { ignoreBOM: true })` — BOM stripping must - /// be disabled or a leading U+FEFF in the first string would misalign - /// slicing), then sliced per-operand by UTF-16 code-unit lengths carried - /// in the op segment. - /// - **op segment**: opcodes and operands, little-endian, unaligned. - /// - /// # Framing - /// - /// ```text - /// frame := frame-len:u32 strings-len:u32 strings:u8{strings-len} ops:u8{rest} - /// frame-len = byte length of everything after the frame-len field - /// = 4 + strings-len + len(ops) - /// ``` - /// - /// Frames arrive back-to-back; a delivery may contain several frames or a - /// partial one (stream writers chunk internally). The decoder consumes - /// only whole frames from a direct-read view and leaves a partial tail - /// unread (redelivered at the next rendezvous); if it cannot consume at - /// least one byte it stages the partial tail instead (a direct-read - /// callback must not acknowledge zero bytes and keep the session parked). - /// - /// # Primitive operand encodings - /// - /// ```text - /// u8/u16/u32/s64/f64 : little-endian, unaligned - /// id : u32 — Dioxus ElementId (slab index; dense). id 0 is the mount - /// root: the host-designated element the app renders into. - /// m : u32 — "top m nodes of the stack" count - /// tmpl : u16 — guest-assigned template id - /// path : u8 length, then that many u8 child indices - /// strref : u16 — interned string id, defined by a prior cache-string op - /// (in this or any earlier batch). The value 0xffff is a - /// "none" sentinel admitted ONLY by namespace operands - /// (every `ns:strref` below, including template element and - /// template attribute namespaces); tag/name operands are - /// mandatory ids and never carry the sentinel. - /// dynstr : u16 UTF-16 code-unit length, then (iff 0xffff) u32 actual - /// length. Content is the next `length` code units of the decoded - /// string segment, consumed sequentially. The encoder appends the - /// string's UTF-8 bytes to the string segment and writes its - /// UTF-16 code-unit count here (Rust: sum of char::len_utf16, - /// with an all-ASCII fast path where it equals the byte length). - /// ``` - /// - /// # Opcodes - /// - /// The vocabulary mirrors dioxus-core 0.7's `WriteMutations` trait plus - /// two protocol ops (cache-string, register-template). Stack semantics - /// ("push", "top m nodes") are Dioxus's mutation-stack semantics, - /// identical to its own web interpreter. - /// - /// ```text - /// 0x01 cache-string id:u16 s:dynstr - /// Define (or overwrite) interned slot `id`. Slots are guest-managed; - /// ids are assigned monotonically from 0 in practice. - /// 0x02 register-template tmpl:u16 nroots:u16 root:node{nroots} - /// node := kind:u8 ... - /// 0x00 element tag:strref ns:strref nattrs:u16 - /// attr{nattrs} := name:strref ns:strref value:dynstr - /// nchildren:u16 node{nchildren} - /// 0x01 text value:dynstr - /// 0x02 dynamic (placeholder for a runtime-supplied node) - /// Registers a static template; the host materializes it once - /// (detached) and later load-template clones root subtrees from it. - /// Guest sends each distinct template once, before its first use - /// (templates are 'static with unique identity, keyed guest-side by - /// pointer). - /// 0x03 append-children id m - /// 0x04 assign-id path id - /// 0x05 create-placeholder id - /// 0x06 create-text-node id text:dynstr - /// 0x07 load-template tmpl root-index:u16 id - /// 0x08 replace-with id m - /// 0x09 replace-placeholder path m - /// 0x0a insert-after id m - /// 0x0b insert-before id m - /// 0x0c set-attribute id name:strref ns:strref value:attrval - /// attrval := kind:u8 ... - /// 0x00 text s:dynstr - /// 0x01 float f64 - /// 0x02 int s64 - /// 0x03 bool u8 - /// 0x04 none (remove the attribute unconditionally — routed via - /// removeAttribute/removeAttributeNS/style.removeProperty - /// as the name/namespace dictates; the boolean-attribute - /// truthiness table governs only text/bool VALUES. - /// Property-backed fields get the same reset the - /// truthy-value path uses instead of a bare - /// removeAttribute, since removeAttribute alone does not - /// clear a live element's property: value -> "" (+ - /// removeAttribute), checked -> false, selected -> - /// false, dangerous_inner_html -> innerHTML = "". - /// Ported from dioxus v0.7.10's `remove_attribute` - /// sledgehammer op in - /// packages/interpreter/src/unified_bindings.rs.) - /// The host applies Dioxus's attribute semantics (value/checked/ - /// selected as properties, style namespace via style object, - /// dangerous_inner_html, boolean attribute removal on false). - /// 0x0d set-text id text:dynstr - /// 0x0e new-event-listener id name:strref flags:u8 - /// 0x0f remove-event-listener id name:strref flags:u8 - /// flags bit0 = the event bubbles (dioxus-html's event_bubbles table, - /// looked up guest-side): the host delegates bubbling events at the - /// mount root and attaches non-bubbling listeners per element, so it - /// needs the same bit on removal to find the registration. Bits 1..7 - /// reserved, must be 0. - /// 0x10 remove id - /// 0x11 push-root id - /// ``` + /// exactly once, tied to this instance. It also means nothing may be + /// written before the reader is returned — such a write would park while + /// the host is still awaiting `run`'s promise for the reader it needs in + /// order to read — and that the scheduler cannot live in this export's own + /// body, since an async export's body returning is task.return followed by + /// task exit. /// - /// Event names cross back on `handle-event` as the same interned u16, so - /// steady-state event dispatch transfers no string data at all. + /// The scheduler task's persistent park between renders (a plain Rust + /// future woken cross-task, no WIT waitable) is legal because the host + /// retains the lifted readable end of this stream for the instance's + /// lifetime. Under polyengine's retention rule — issue #162, + /// `.deps/polyengine/contracts/embedder-api.md` §"Streams and futures", + /// "while the host retains a way to act on a stream/future — a retained + /// end, a parked host operation, or an unfinished producer pump — a + /// stalled guest is reported as the documented embedder-may-act hang, + /// never a deadlock trap" — a retained end alone suffices; the host need + /// not also have a read parked. That is exactly what the runtime + /// implements: `HostActivity` + /// (`.deps/polyengine/runtime/src/exec/host_streams.ts`) arms on the + /// retained end and disarms only when that end is lowered back into a + /// guest. So a quiescent instance is the documented embedder-may-act + /// state, and app failure still surfaces on the channel the host watches: + /// a trap rejects the host's pending read (`PeerTrappedError`). /// /// (Historical: earlier revisions passed this stream to a host import /// `surface.open`, alongside a since-retired synchronous call transport — /// see bench/README.md for the retirement record.) - export run: async func() -> stream; + export run: async func() -> stream; /// Dispatch one DOM event to the listener registered on `target` (the /// ElementId carried by new-event-listener) for the interned event name