From b1ce1927022d374198441afafeb27199fc8a052a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 24 Sep 2026 17:14:54 +0100 Subject: Establish native micro runtime checkpoint --- prototypes/workbench/CONTRACT.md | 575 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 prototypes/workbench/CONTRACT.md (limited to 'prototypes/workbench/CONTRACT.md') diff --git a/prototypes/workbench/CONTRACT.md b/prototypes/workbench/CONTRACT.md new file mode 100644 index 0000000..498129e --- /dev/null +++ b/prototypes/workbench/CONTRACT.md @@ -0,0 +1,575 @@ +# Workbench experiment contract + +An author starts with empty source. There is no built-in player, physics, jump, +score, or timer behavior. All behavior is declared by the author. This is a +bounded scalar interaction language experiment, not a general language claim. + +The implementation now lives in explicit `Engine` instances in `engine.zig`. +`core.zig` retains the browser ABI and tests; `native.zig` exposes caller-owned +instances through the selected C module. Native prepared definitions, host input +ownership and conditional snapshots are documented in [native/README.md](native/README.md) +and [024](../../docs/research/024-native-app-instances.md). Existing scalar limits +remain. This does not make the selected scalar VM mandatory for every Pure domain. + +## Source language + +```text +state count = 0 +input tap = Space +box tile(80, 150, 60, 60, "#356548") +circle dot(200, 180, 20, "#c78638") +text label(80, 100, count, "Count: ") +on tap.down { + count = count + 1 +} +on frame { + if count > 20 { + count = 0 + } +} +``` + +Declarations and statements end at a newline or semicolon; `#` comments outside +strings. ASCII identifiers, finite numeric literals, state initializers must be +numeric constants (optional unary sign). Input key codes accepted: +Space, Enter, ArrowLeft/Right/Up/Down, KeyA..KeyZ, Digit0..Digit9. Input names +must be unique, key codes unique, names cannot shadow `frame`, `dt`, `min`, `max`, +`clamp`, `event_x`, `event_y`. State and input names use separate namespaces, entity IDs unique. + +Expressions: parentheses, state references, dt (seconds on frame, zero on input), +unary - and !, + - * /, < <= > >= == !=, && ||, min(a,b), max(a,b), clamp(x,lo,hi). +Logical operators evaluate both sides. Division by zero/non-finite results fail +the whole event without committing state changes. No loops, recursion or eval. +An if without else executes its condition once; assignment statements are ordered. +Multiple handlers for the same event execute in source order within one atomic event. +`dt` is allowed in handlers, not view bindings. A frame always uses 1/60 seconds. + +box: x,y,width,height expressions + literal hex color; circle: x,y,radius + color; +text: x,y,value expression + optional literal prefix. Values are world units in a +640x360 view. No implicit clipping/collision/physics. Render expressions are pure. +Unknown identifiers and invalid syntax include 1-based line+column diagnostics. + +Initial scene is empty. Optional examples are explicit insertions, never auto-run. +Successful apply preserves scalar states by exact name; new names use initializers, +removed names disappear. Apply/reset are separate. Failed parse/load changes nothing. +Apply is refused while any input is held; release first. This avoids inventing +release events across changed mappings. Focus loss sends .up to ordinary buttons +and .cancel to pointer inputs. + +## Compiler interface (web/compiler.mjs, browser and Node) + +`compile(source)` returns: +``` +{ + states:[{name,initial,line}], inputs:[{name,key,line} | {name,target,line} | {name,type,routes,line}], + instructions:[{op,arg,value,line}], handlers:[{event,start,length,line}], + views:[{id,type,line,color?,prefix?,bindings:{x:{start,length},...}}], + pointerTargets:[{input,base,count}], metrics:{...} +} +``` +Bindings: box x/y/width/height; circle x/y/radius; text x/y/value. +Compiler throws an Error with .line and .column. Empty source is a valid empty program. +Handlers and binding expressions all occupy instructions (maximum 512 total), but +only handlers execute on events; bindings are evaluated on demand by VM eval_at. +Limits: 32 states, 8 inputs, 24 views, 64 handlers, 512 instructions, stack 64, +source 32768 UTF-8 bytes. State IDs follow declaration order. Input event IDs: +frame=0; input i down=1+2*i, up=2+2*i, move=17+i, cancel=25+i. +Move/cancel handlers require a pointer input in source. States may be referenced before declaration; +symbols must be resolved after gathering top-level declarations. `metrics` can +record source bytes/lines and instruction/handler/view/state counts. + +## VM opcodes + +| op | operation | +|---:|---| +|0| push constant `value` | +|1| push state `arg` | +|2| push dt | +|3..6| add, subtract, multiply, divide | +|7| negate | +|8..13| less, less-equal, greater, greater-equal, equal, not-equal | +|14..16| AND, OR, NOT (nonzero true) | +|17..19| min, max, clamp (push x,lo,hi; invalid lo>hi fails) | +|20| store state `arg` (pop) | +|21| jump-if-zero to absolute instruction index `arg` (pop), forward only | +|22,23| push current pointer event x,y; fail without point context | +|24,25| load/store field at handler parameter_base + arg; handler-bound only | + +Rules start with an empty stack and must end with empty stack. Bindings must finish +with exactly one value and cannot STORE, branch, or read dt/event coordinates. Branch targets may be handler end. + +## Zig exported ABI + +`reset_all()` clears programs and state. `begin_program(state_count,input_count)` +starts staging, returns0 success; `put_state(id,initial,old_id)` old_id=-1 means init; +`put_instruction(op,arg,value)` append; `put_handler(event,start,length)` append; +`commit_program()` all0 success otherwise error code, atomic; candidate is separate +from the active program, but shares its workspace with optional evaluation records. +Preserved IDs read active values only at successful commit. Validate limits, +indices, finite values and forward branch ranges; runtime stack errors are checked. +`reset_state()` restores active initializers, resets tick/event diagnostics. +`dispatch(event,dt)` accepts0..16 and finite dt; event0 dt must equal1/60, input dt0; +returns0 success, other code failure with no state mutation. Tick increments for +successful frame; one event runs all matching handlers in source order. +`dispatch_point(event,x,y)` accepts existing input down/up IDs plus move/cancel IDs +within input_count, never frame0. Coordinates must be finite; dt is implicitly0. +The core only knows input count, not host device kinds. The compiler and host enforce +pointer ownership and device mapping. Calling legacy dispatch on a coordinate-reading +handler fails with bad_event9. Point operands are ephemeral execution context, not +extra persistent state slots. Both dispatch paths use the same atomic commit/history. +The host requires this new export; older Wasm must be rebuilt. +`eval_at(start,length)` returns f64 or NaN on invalid/pure-expression failure; +must not mutate state or event diagnostics. `get_state(id)` returns f64 orNaN. +`info(id)` u32: 0 tick,1 last_error,2 error_pc (0xffffffff none),3 last_executed_ops, +4 revision,5 state_count,6 instruction_count,7 handler_count,8 input_count, +9 successful_event_count. `layout(id)` u32: 0 Engine reserved @sizeOf, +1 Program reserved @sizeOf,2 Instruction @sizeOf,3 Handler @sizeOf, +4 active used IR bytes (instructions+handlers+pointer targets),5 active state-value bytes, +6 staging reserved bytes (subset of Engine),7 evaluation stack capacity bytes,8 pointer target entry bytes,9 active pointer target count. +`error_message` belongs to host map; core error codes documented in core.zig. +No allocation or imported runtime in Wasm. Fixed64KiB linear memory target with +16KiB stack; increase only if actual linker evidence requires and document it. + +## Host interface (web/runtime.mjs) + +`await createRuntime(wasmBytes?, evaluationOptions?)` fetches core.wasm in browser if omitted. +Returns object with apply(source), reset(), frame(count=1), down(name,source), +up(name,source), point(name,phase,x,y,source,target?), releaseAll(), snapshot(), exportProject(), currentProgram. +Multiple sources per input share one logical held state; down emits only first +source, up only last. Source strings identify keyboard vs onscreen pointer. +`apply` returns compiled program or throws with diagnostic. It must fully validate +in host and stage+commit in core. It preserves by name. `reset` keeps active program. +`snapshot` returns {states:{name:value}, views:[{id,type,x,y,width?,height?,radius?, +value?,prefix?,color?}], tick,revision,lastError,errorLine,lastOps,memory:{...},held:[]} +`currentProgram` is the latest successfully applied compiled program or null. +The program and all nested metadata are frozen so readers cannot corrupt state +name mappings or binding ranges. `lastFailure` records the last failed event; +its `lastError`/line remain visible through successful release or focus cleanup +until successful apply or explicit reset. `lastEvent` still describes the latest +event. A view evaluation failure uses host error100 and does not roll back events. +UI draft and applied code are distinct. Browser/CLI share compiler/runtime/wasm; +instances are separate, not a live collaborative session. No arbitrary JS evaluation. + +## Bounded display evaluation (2026-09-22) + +The existing event and float semantics above are unchanged. The forgetful C +prototype's contract is adapted into `evaluation.zig`; its uint64 DAG is not +used as a substitute for this VM's float instructions. + +Each snapshot starts one display budget, shared by all bindings. Default options +are `{cacheEntries:0, historyEntries:8, workLimit:2048}`. A unit is a binding +request, a candidate-cache probe, a change-record probe, or an executed VM +instruction. State extraction, host object creation, event dispatch and DOM work +are outside this budget. It is not a time limit or a bound on whole-app work. +For the current compiler limits, 96 bindings * 10 units plus 512 instructions +is at most 1,472 units; the default admits even an uncached valid scene with +cache lookup/validation enabled. Malformed bindings may fail sooner. + +`configureEvaluation({cacheEntries?,historyEntries?,workLimit?})` accepts integers +0..32, 0..8, and 0..4294967295 respectively. Missing fields retain prior settings. +Valid configuration clears reuse records; invalid configuration changes nothing. +`forgetEvaluation('values'|'history'|'all')` drops optional records without +changing authored state, event diagnostics or program identity. +`evaluationStats()` returns the options plus work, instructions, cacheHits for +the last snapshot, and reservedBytes for the complete fixed evaluation structure. +Statistics are separate from snapshots so repeated observations remain equivalent. + +Memo entries contain the value, instruction range, input dependency bit mask +and validated revision. Direct mapping uses one candidate probe, with collisions +causing recomputation. Only successful computations are cached. Constant results +need no input-change history. All other reuse requires a complete retained +history since validation and no intersection with changed states. Dependencies +are collected from actual VM LOADs, not trusted compiler-supplied annotations. +History loss causes recomputation. All history and memo storage is fixed size; +setting capacities to zero does not return reserved linear memory. + +Only bitwise-different committed states advance change history (including signed +zero). Failed events and net-zero updates do not. Successful apply and reset +discard all reuse records. Parse/host validation failure before staging preserves +records. Beginning core staging discards them, including if that load is later +rejected or aborted. Epoch wrap also clears records. No inputs or effectful event +handlers are replayed to rebuild a binding. + +Budget exhaustion returns an empty views array for the entire snapshot, with +displayError `{code:101,line,message}`. Successfully committed event state remains. +There is no automatic retry or silent return of an old scene. Raising the budget +allows a fresh attempt. Arithmetic/size errors retain the earlier per-view error100 +behavior. `displayError` is null on display success; a latched execution error +still takes precedence in lastError/errorMessage, while displayError independently +reports display failure. Partial successful binding memos may survive a failed +snapshot but no partial scene is published. + +New core exports: `configure_evaluation(cache,history)` returns 0 or capacity1; +`forget_evaluation(flags)` bits 1=values, 2=history; `begin_evaluation(limit)`; +`eval_bounded(start,length)` returns f64 or NaN and consumes that shared budget. +`evaluation_info(id)` returns 0 status (10=work limit), 1 work, 2 instructions, +3 cache hits, 4 cache capacity, 5 history capacity, 6 reserved bytes. These +diagnostics do not replace event info. The unbounded pure `eval_at` remains for +reference checks and is not the host snapshot path. + +## Shared load/reuse workspace (2026-09-23) + +The pending definition/restore values and evaluation Tables occupy one untagged union. The active +Program, current state and evaluation control fields remain separate. All core +calls are single-threaded and synchronous; no callback can reenter a phase change. +No pointer to the reused tables is retained between calls. + +`begin_program` switches the workspace to staging and drops optional records. +Until commit/abort, old events and bindings still run against active. Binding +evaluation bypasses reuse and recomputes within its budget; cache configuration, +forget and reset must not write the staging payload. Old-id state preservation +reads the current values at successful commit, including events/reset during load. + +Successful commit publishes the new Program and then initializes empty Tables in +the shared workspace. `abort_program()` discards a pending definition and restores +empty Tables without changing active program/state/event diagnostics. Inactive +abort is a no-op. A failed commit leaves staging open for explicit abort or the +next begin. An invalid begin closes any previous pending load. Host apply aborts +after a core loading failure. Lost reuse may make a previously warm tiny budget +insufficient; neither a preserved cache nor completion at that budget is promised. + +`evaluation_info` additionally reports 7 dedicated control bytes, 8 shared Tables +bytes, 9 reuse available (false during load), and 10 union safety-tag overhead. +The host exposes these as dedicatedBytes, sharedBytes, reuseAvailable and +workspaceTagBytes. reservedBytes is control + Tables capacity, not incremental +Engine storage. The Tables capacity overlaps stagingReservedBytes and must not +be added to it. Before document restoration was added, the Wasm control was 48 B, shared Tables +1,152 B, union overhead 8 B. That Engine was 19,128 B, only 56 B larger than the +19,072 B pre-evaluation core; linear memory remains 65,536 B. No OS pages are +returned and this is not a measurement of browser/process memory. + +## Direct output publication (2026-09-23) + +`present(target)` evaluates the same bindings, in the same order and shared +budget as `snapshot()`, then calls a synchronous trusted output adapter: + +- `begin(program)`: the frozen current definition, or null at startup. +- `state(index, value)`: each current authored state, without a host state map. +- `view(index, valid)`: every declared view's publication status. +- `value(viewIndex, bindingName, value)`: each binding of a valid view only. +- `end(visibleCount)`: number of valid views. + +It returns the snapshot's diagnostics, held inputs, event information and memory +report, without `states` or `views`. Evaluation finishes before the first adapter +callback. On budget error101 every view is invalid and no binding values are +published; committed state values are still published. Error100 invalidates only +the affected views, as before. No stale value is silently substituted. + +Runtime mutations, `snapshot()` and nested `present()` are rejected during +callbacks. Read-only program/export/statistics access remains available. Adapters +must be synchronous and must not throw. If one does throw, the exception +propagates and the runtime unlocks in a finally block; arbitrary external adapter +effects cannot be rolled back. A later call republishes every value. This is not +a transaction across arbitrary output devices or an untrusted plugin boundary. + +Evaluation uses a private reusable Float64Array(96) and Uint8Array(24), allocated +on first snapshot/presentation. Their payload totals 792 host bytes, outside the +64 KiB Wasm memory. Typed-array object overhead, binding-entry arrays built at +apply, and DOM destination references are additional host memory. The numeric +buffer is a publication barrier, not another authoritative state or public +mutable scene. `snapshot()` remains an independent object representation for +CLI/export/debugging, built only when explicitly requested. + +The browser calls `present()` for normal refreshes. Its output adapter binds DOM +destinations once per successful definition change and compares values with the +existing DOM before writing. Input refreshes reuse scene, state and cost elements. +Invalid views remain allocated but are hidden using SVG `display="none"`; recovery +reveals them. A definition change can replace all scene/state destinations. This +does not skip unchanged binding evaluation, eliminate all host allocations, or +provide measured process-memory/paint-latency savings. + +## Authored direct manipulation (2026-09-23) + +`input drag = pointer(tile)` binds a logical input to a declared view, including a +forward declaration. One input per target; it shares the existing 8-input limit +with keyboard inputs. `on drag.down/move/up/cancel` handlers may read `event_x` +and `event_y` in the same 640x360 world coordinates as output. They are unavailable +in keyboard/frame handlers and display bindings; copy to authored state to retain +them. There is no built-in drag constraint, snap, grab offset, or gesture rollback. +The direct.pure example authors these using ordinary assignments and expressions. + +Host `point` requires a pointer input, valid phase, finite coordinates and nonempty +source string. Each logical input has one owning source; different targets may +have independent active contacts. Same-owner repeated down is a no-op; another +owner's down throws. Move/up/cancel without that owner are no-ops. Down establishes +ownership before dispatch; up/cancel release it before dispatch, even if authored +arithmetic fails. An event failure rolls back that event's state writes, not the +whole gesture. `releaseAll` cancels owned pointers at their last delivered position, +then releases ordinary buttons. `reset` directly clears ownership and resets state. +Last point events include x,y. A failed cancel can leave authored state unchanged, +but cannot strand host ownership; the failure remains visible until reset/apply. + +Browser hit testing selects the declared target on primary-button pointerdown. +The stable SVG root captures that pointer, including movement/release outside the +scene. Client coordinates are transformed through the inverse SVG screen matrix. +Pointerup exposes its own final coordinates to authored handlers. The direct.pure +example updates position only on move and ends dragging on up, preserving the +last displayed position even if a host repeats stale coordinates on release. Termination is observed in the window capture phase, including releases outside +the SVG. A primary mouseup can finish an owned mouse gesture if pointerup was +not delivered; it cannot finish a touch gesture. Duplicate termination is ignored. +Only capture loss of the SVG root ends its owner, not a bubbled descendant event. +Pointercancel, lostpointercapture and +focus loss use the last delivered position and dispatch cancel, never fake up. +Missing/non-invertible coordinate transforms cancel an active gesture. A second +contact on the same target is ignored; contacts on different targets are independent. +Touch scenes disable native panning on the SVG. Mouse/touch share this adapter; +pen-specific pressure, tilt and button meanings are not implemented or validated. + +Every delivered pointermove executes synchronously; this layer adds no event queue, +coalescing, frame gating or newest-only drop policy. This is not a guarantee that +browsers expose every hardware sample. Keyboard commands and pointer coordinates +enter the same authored state/commit/output path. CLI `point NAME PHASE X Y` uses +that host API and core with source `terminal`; it does not replay browser hit testing. + + +## Placement documents (2026-09-23) + +Code-only version1 JSON and .pure files keep their earlier behavior. Version2 +stores a restorable authored document, not a suspended execution image: + +```json +{"format":"pure-workbench","version":2,"source":"state x=80","draft":"state x=80", + "states":[{"name":"x","value":350}]} +``` + +`source` is the applied definition; optional `draft` is separate unapplied editor +text (default source). Each is at most 32768 UTF-8 bytes. Draft syntax may be +unfinished. State records must contain exactly one finite value for every declared +state name, with no extra, missing or duplicate names. Record order is irrelevant. +JSON numbers preserve finite f64 values, except negative zero which is encoded as +the exact string `"-0"`. No other numeric string is accepted. Input latches, running +clock, diagnostics, caches, DOM, compiled IR and external resources are not saved. +The serialized input file limit is 1449984 bytes, including escaped source/draft +and up to 32 generated item.field names (which can repeat a long schema name). + +`exportDocument(draft?)` returns an independent version2 object. It refuses an +unapplied runtime, held input or latched execution failure. It reads core state, +without constructing an output snapshot. `restoreDocument(document)` validates +and compiles before staging, refuses held input, then commits the definition and +all saved values together. It returns `{source,draft}`. Failed validation leaves +active program/values/diagnostics intact. A failed core load is aborted; optional +reuse records may be lost as with ordinary apply. Successful restore discards old +reuse, clears diagnostics, resets tick/event count and starts with no held inputs. +The original source initializers remain intact: explicit reset restores those, +not saved values. Existing `apply` continues to preserve current values by name. +Both new document APIs reject reentry during output publication. + +Core exports `put_restored_value(id,value)` and `commit_restored_program()` extend +the existing begin/put/commit loader. Every declared state needs exactly one finite +restored value; duplicate/out-of-range/nonfinite input poisons staging. Restored +commit checks completeness and program validity before touching active state. +Ordinary commit rejects staged restored values. Pending values live alongside the +pending Program in the same union as optional evaluation Tables; active Program +layout does not grow. `layout(6)` now includes Pending, rather than Program only. +Before shared rule bindings, the Wasm Pending was 9656 B (264 B added for 32 values, mask and alignment), +Engine was 19392 B; linear memory remains fixed at 65536 B. These are build-layout +facts, not a browser RAM or performance improvement claim. `evaluation_info(10)` +continues to report only union tag overhead, excluding restoration payload. + +Browser **배치 저장** stores applied source, all current values and editor draft. +It requires a stopped clock. **파일 열기** restores version2 after replacement +confirmation when existing work is present; it starts stopped. Version1/plain +source still only replaces the editor draft. Async reading checks draft generation, +program identity and successful event count so intervening edits/inputs cannot be +overwritten by a stale read. Invalid files do not alter the working document. +**.pure 저장 / 코드 JSON 저장** remain draft-only exports. + +CLI `save-project PATH` writes version2 with exclusive creation; `restore PATH` +restores it. Loading a .json path (including at startup) distinguishes versions. +A browser draft is retained through CLI save-project, but never applied implicitly. +Existing `save PATH` remains applied-source-only. No filesystem overwrite is added. + + +## Shared rules and named records (2026-09-23) + +```text +record Position { x=0; y=0 } +item green = Position(x=80,y=120) +item blue = Position(x=280,y=160) +rule move(t: Position) { t.x=event_x; t.y=event_y } +input drag_green = pointer(green_box) +input drag_blue = pointer(blue_box) +box green_box(green.x,green.y,56,44,"#c9f08b") +box blue_box(blue.x,blue.y,56,44,"#74c4d4") +on drag_green.move use move(green) +on drag_blue.move use move(blue) +``` + +A record defines 1..8 named finite numeric initializers. An item has its own values; +optional literal field overrides replace defaults. Its fields occupy contiguous +slots in the existing scalar state array, with public names `item.field`. They +count toward the same 32-state limit. Schemas and items may be forward-referenced. +Unknown/duplicate fields, types or items fail compilation. Scalar and item names +cannot collide; type and rule names have separate namespaces. There are no runtime +record objects, nested records, record copying, dynamic item creation or deletion. + +A rule takes exactly one named record parameter with an exact schema name match. +Its body uses the existing expressions, ordered assignments and conditional blocks. +Parameter fields read/write the connected item's current slots; globals and other +qualified state names remain accessible. The parameter shadows the same qualified +prefix only inside its body. No rule calls from expressions or rule bodies, recursion, +return values, stack frames, or implicit event loop are introduced. Frame handlers +may use rules; coordinate-reading rules may only bind to pointer events. +Unused rules are name/type checked without emitting a body into the VM program. + +`on input.edge use rule(item)` is one event handler. Each used rule's instructions +are emitted once at its first use. Every use adds handler metadata referencing that +same exact instruction range, with parameterBase/parameterCount. Bindings run in +source order, including repeated uses of one item. All matching bindings and +ordinary handlers participate in the same atomic event: a failure in any rolls +back every state write in that event. Error PCs refer to the shared source body. + +Core `bind_handler(handler_index,base,count)` sets a previously unbound staged +handler's contiguous state range. Counts are 1..8; the entire range must fit current +staged state_count. A duplicate binding poisons staging. All owners of shared code +must have the same nonzero field count and exactly the same range. Partial overlap +remains invalid. Parameter opcodes must belong to a bound handler and use an index +below every owner's field count. Pure display evaluation rejects parameter ops; +view bindings use ordinary resolved state loads and their normal cache dependencies. + +Handler metadata grows from 12 to 16 B (base/count plus alignment). There is no +per-field indirection list or per-event parameter-value copy. At the shared-rules step, Wasm Program +was 9648 B, Pending 9912 B, Engine 19904 B: 512 B more fixed reservation across active +and pending handler arrays. The linear-memory limit stays 64 KiB. The required +bind_handler export makes the host reject older incompatible Wasm. + +Apply preserves each qualified field by name. Reordering schema fields or item +instances rebuilds physical bindings without changing their named values. Removing +an item removes its fields; adding it again initializes new fields. An active +input still blocks definition changes. Version2 documents save qualified field +names, not physical offsets; restoration recompiles all bindings and validates all +values before commit. This is static named-field identity, not a general lifetime +handle for dynamically deleted/reused objects. Example `id` and `selected` are +ordinary author-defined numbers; uniqueness/selection policy is not enforced by +an implicit object system. + + +## Target-routed pointer groups (2026-09-23) + +```text +input drag = pointer { green_box: green; blue_box: blue } +on drag.move use move(drag.target) +``` + +A group resolves named views to named items of exactly one record type. Forward +references work. Every view may be connected only once across single-target inputs +and groups. Every item may occur in only one group route, even across groups; +this prevents two group captures from writing aliased field ranges. Explicit +ordinary handlers and statically bound rules may still write other states as authored. +Groups must be nonempty. There are at most 24 routes in the program, independently +of the existing eight logical inputs. State/view/instruction limits are unchanged. +`input.target` is a contextual rule argument for the current group's event only, +not a scalar expression or persistent object handle. Wrong groups/types are rejected. + +A group handler is emitted once per authored `on`, independent of target count. +It can share its exact body with statically bound handlers. One route consists of +three bytes: logical input index, field base, field count. The compiler's routes +also retain source view/item names for the host. The host builds one bounded view +lookup Map per successful load; target-name lookup does not scan all group targets +on each event. JS Map/metadata allocations are outside the core memory accounting. + +`put_pointer_target(input,base,count)` appends a staged route, with count 1..8 and +an in-bounds, nonoverlapping state range. Same-input routes must have equal counts. +`bind_target_handler(handler,count)` binds a previously unbound staged handler to +the event target, encoded as parameter_base=255. Commit requires a non-frame event +with matching routes/counts. Existing `bind_handler` accepts only concrete bases. +`dispatch_target(event,route,x,y)` validates the active route's membership in the +event's logical input, then supplies its base to the matching handlers. Missing, +foreign, and out-of-range targets fail before any authored writes. Ordinary dispatch +and dispatch_point reject inputs that require a route. All matching static, routed, +and inline handlers still run in source order within one atomic event. +The VM indexes the route directly; it still scans the bounded handler list to match +event IDs. No claim of constant-time complete dispatch is made. + +Host `point(name,phase,x,y,source,target)` requires a group view name in the final +argument. Legacy single-target calls keep the old form and reject an extra target. +Capture/held ownership is `(input,view)`, so two views in a group can be manipulated +concurrently; a second source cannot steal the same view. Continuations from a +non-owner are ignored. Browser capture records retain the initial view across +movement, release outside the view, and cancellation. `releaseAll` cancels every +owned view once. `held` uses `input:view` for groups and the old input names for +legacy inputs; lastEvent/lastFailure add `target` for group events. + +Route indices live only in the active definition. Apply/restore remain blocked +while inputs are held. Reset clears owners; load rebuilds routes by source names. +Documents still store source and named scalar fields, never physical route indices. +Deletion/recreation at runtime and generation-checked dynamic identity are not added. +CLI uses `point-target NAME VIEW PHASE X Y`; existing `point` is unchanged. + +Each Program adds 80 B (24 three-byte routes, mask/count and alignment). Current +Program=9728 B, Pending=9992 B, Engine=20064 B, an additional 160 B of fixed engine +reservation over the shared-rules step. Handler remains 16 B, Instruction 16 B. +The active IR byte count now includes used routes. Linear memory remains 65536 B. +The host requires put_pointer_target, bind_target_handler, and dispatch_target. + +## Numeric host inputs and native document layout (2026-09-23) + +```text +state ratio = 50 +input split = value +on split.change { ratio = clamp(event_value,20,80) } +``` + +`value` is a numeric input kind, not a state initializer. Its only edge is +`.change`, event ID `33 + input_index` (33..40). `event_value` is reserved and +available only in these handlers; view expressions and pointer/keyboard/frame +bindings cannot read it. Shared rules may use it only when bound to a value input. +A rule requiring both pointer coordinates and a scalar value has no compatible +binding. Values are ephemeral event operands, not hidden state slots. + +Opcode26 pushes the event's numeric value. Core `dispatch_value(event,value)` +requires the scalar event range and a finite value. Ordinary dispatch/dispatch_point +reject scalar event IDs; scalar dispatch rejects other event IDs. Pure evaluation +rejects opcode26. The same ordered, atomic state commit and display-cache invalidation +apply. A later handler failure rolls back all earlier writes in this event. +No new persistent core tables are added: Engine remains 20064 B; linear memory is +65536 B. The existing event context's x payload holds the scalar under an explicit +scalar tag, separate from pointer context. Host requires dispatch_value export. + +Host `change(name,value,source='external')` validates the input kind and value, +returns the dispatch status, and records `.change` and `value` in the last event. +It does not acquire a held-input owner. Repeated equal requests still dispatch; +no authored side effects are dropped. Reentrant output callbacks cannot invoke it. +`down/up/point` reject numeric inputs. Source forms can submit finite numbers through +native number controls. CLI command is `change NAME VALUE`. + +`document.html` is a concrete host, not a generic UI declaration language. It uses +CSS Grid/media queries and native textarea editing, scrolling and text rendering. +The authored policy contains one scalar ratio and no output views. With no declared +views, display evaluation resets its budget then returns without allocating the +96-number/24-flag display publication buffers. No browser layout tree, text buffer, +scroll offset or viewport dimensions are copied into VM state. + +The document host projects ratio to CSS and accessible controls. Values outside +20..80 are rejected at publication, leaving the last valid visual ratio in place; +this host-output error is separate from VM arithmetic rollback. A later valid value +or corrected rule can restore valid output. Policy apply during its own splitter +capture is refused. The generic numeric port itself has no capture semantics. + +The splitter owns one pointer and an original ratio for cancel. Mouse, touch and +keyboard feed the same policy. Moves are coalesced to the latest coordinate once +per animation frame; release flushes a pending move and does not replace it with a +possibly stale release coordinate. Cancel/Escape/blur/capture loss restore the +original ratio. Crossing into narrow stacked layout cancels. Geometry is read at +gesture start and invalidated by resize observation, window resize and scroll; +observer callbacks do not write layout. No geometry reads occur in inactive resize, +scroll or text-edit handlers. A single geometry cache remains in the host between +gestures and is remeasured on every new gesture; it is not a layout-tree mirror. + +The textarea is the editable document authority. The reading DOM contains a second +text projection. There is no additional persistent JS string model, AST or document +copy in Wasm. This does NOT mean a zero-copy editor: native undo/layout storage, +DOM text and transient reads/decoding/encoding are outside core memory accounting. +Text edits schedule one reading publication per frame, deferred through composition. +Publication currently replaces the entire reading text; no incremental text format +or delta editor is claimed. Resize/split/scroll do not trigger text publication. + +Open/save use plain UTF-8 .txt, up to 2 MiB per imported/exported file. Invalid UTF-8 +and oversize files are refused without changing the current document. File reads +use generation/request guards against stale replacement. Native textarea normalizes +line endings; saved text uses LF and no BOM, not byte-identical preservation of +all original files. Files contain text only, not panel ratios or Pure rules. An +unavailable Wasm disables split policy controls while text editing/preview/save stay +available. Native undo is retained for ordinary editing; file/sample replacement +starts a new document value. No complete accessibility certification or physical +mobile/IME test is claimed by the browser automation. -- cgit v1.2.3