@polyengine/runtime 0.1.0-pre.g633468a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +23 -0
- package/esm/cabi/async_values.js +162 -0
- package/esm/cabi/bulk_lists.js +198 -0
- package/esm/cabi/context.js +42 -0
- package/esm/cabi/flatten.js +145 -0
- package/esm/cabi/float.js +67 -0
- package/esm/cabi/handles.js +253 -0
- package/esm/cabi/layout.js +149 -0
- package/esm/cabi/lift.js +196 -0
- package/esm/cabi/load.js +146 -0
- package/esm/cabi/lower.js +141 -0
- package/esm/cabi/memory.js +182 -0
- package/esm/cabi/mod.js +22 -0
- package/esm/cabi/store.js +186 -0
- package/esm/cabi/strings.js +336 -0
- package/esm/cabi/trap.js +38 -0
- package/esm/cabi/types.js +264 -0
- package/esm/cabi/values.js +64 -0
- package/esm/cache/core.js +156 -0
- package/esm/cache/dir.js +170 -0
- package/esm/cache/mod.js +4 -0
- package/esm/cache/web.js +136 -0
- package/esm/digest/digest.js +332 -0
- package/esm/digest/mod.js +3 -0
- package/esm/digest/verify.js +129 -0
- package/esm/embedder/casing.js +56 -0
- package/esm/embedder/copy.js +42 -0
- package/esm/embedder/errors.js +26 -0
- package/esm/embedder/imports.js +63 -0
- package/esm/embedder/instantiate.js +978 -0
- package/esm/embedder/mod.js +40 -0
- package/esm/embedder/resources.js +406 -0
- package/esm/embedder/streams.js +770 -0
- package/esm/embedder/values.js +450 -0
- package/esm/embedder/version.js +273 -0
- package/esm/exec/boundary.js +1902 -0
- package/esm/exec/executor.js +1004 -0
- package/esm/exec/host_streams.js +818 -0
- package/esm/exec/mod.js +4 -0
- package/esm/intrinsics/async_builtins.js +510 -0
- package/esm/intrinsics/context.js +90 -0
- package/esm/intrinsics/errors.js +13 -0
- package/esm/intrinsics/fact_calls.js +865 -0
- package/esm/intrinsics/mod.js +564 -0
- package/esm/intrinsics/stream_builtins.js +578 -0
- package/esm/intrinsics/transcode.js +443 -0
- package/esm/jspi/bridge.js +579 -0
- package/esm/jspi/mechanics.js +89 -0
- package/esm/jspi/mod.js +5 -0
- package/esm/jspi/suspending.js +15 -0
- package/esm/jspi/types.js +29 -0
- package/esm/package.json +3 -0
- package/esm/plan/format.js +5 -0
- package/esm/plan/loader.js +657 -0
- package/esm/plan/mod.js +3 -0
- package/esm/shim/mod.js +2 -0
- package/esm/shim/translator.js +94 -0
- package/esm/task/mod.js +483 -0
- package/esm/task/scheduler.js +1028 -0
- package/esm/task/streams.js +786 -0
- package/esm/task/subtask.js +135 -0
- package/esm/task/thread.js +255 -0
- package/esm/task/waitable.js +144 -0
- package/package.json +91 -0
- package/types/cabi/async_values.d.ts +35 -0
- package/types/cabi/bulk_lists.d.ts +18 -0
- package/types/cabi/context.d.ts +59 -0
- package/types/cabi/flatten.d.ts +14 -0
- package/types/cabi/float.d.ts +14 -0
- package/types/cabi/handles.d.ts +70 -0
- package/types/cabi/layout.d.ts +13 -0
- package/types/cabi/lift.d.ts +25 -0
- package/types/cabi/load.d.ts +14 -0
- package/types/cabi/lower.d.ts +10 -0
- package/types/cabi/memory.d.ts +58 -0
- package/types/cabi/mod.d.ts +15 -0
- package/types/cabi/store.d.ts +12 -0
- package/types/cabi/strings.d.ts +23 -0
- package/types/cabi/trap.d.ts +11 -0
- package/types/cabi/types.d.ts +206 -0
- package/types/cabi/values.d.ts +5 -0
- package/types/cache/core.d.ts +97 -0
- package/types/cache/dir.d.ts +6 -0
- package/types/cache/mod.d.ts +3 -0
- package/types/cache/web.d.ts +10 -0
- package/types/digest/digest.d.ts +17 -0
- package/types/digest/mod.d.ts +2 -0
- package/types/digest/verify.d.ts +48 -0
- package/types/embedder/casing.d.ts +40 -0
- package/types/embedder/copy.d.ts +24 -0
- package/types/embedder/errors.d.ts +11 -0
- package/types/embedder/imports.d.ts +47 -0
- package/types/embedder/instantiate.d.ts +88 -0
- package/types/embedder/mod.d.ts +11 -0
- package/types/embedder/resources.d.ts +158 -0
- package/types/embedder/streams.d.ts +202 -0
- package/types/embedder/values.d.ts +70 -0
- package/types/embedder/version.d.ts +85 -0
- package/types/exec/boundary.d.ts +360 -0
- package/types/exec/executor.d.ts +125 -0
- package/types/exec/host_streams.d.ts +165 -0
- package/types/exec/mod.d.ts +3 -0
- package/types/intrinsics/async_builtins.d.ts +69 -0
- package/types/intrinsics/context.d.ts +28 -0
- package/types/intrinsics/errors.d.ts +5 -0
- package/types/intrinsics/fact_calls.d.ts +120 -0
- package/types/intrinsics/mod.d.ts +187 -0
- package/types/intrinsics/stream_builtins.d.ts +113 -0
- package/types/intrinsics/transcode.d.ts +21 -0
- package/types/jspi/bridge.d.ts +227 -0
- package/types/jspi/mechanics.d.ts +50 -0
- package/types/jspi/mod.d.ts +3 -0
- package/types/jspi/suspending.d.ts +1 -0
- package/types/jspi/types.d.ts +26 -0
- package/types/plan/format.d.ts +369 -0
- package/types/plan/loader.d.ts +113 -0
- package/types/plan/mod.d.ts +2 -0
- package/types/shim/mod.d.ts +1 -0
- package/types/shim/translator.d.ts +55 -0
- package/types/task/mod.d.ts +257 -0
- package/types/task/scheduler.d.ts +421 -0
- package/types/task/streams.d.ts +370 -0
- package/types/task/subtask.d.ts +96 -0
- package/types/task/thread.d.ts +73 -0
- package/types/task/waitable.d.ts +67 -0
package/esm/exec/mod.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
// The 0.3 async canonical built-ins, as host trampolines
|
|
2
|
+
// (contracts/intrinsics.md §B "M2"): task.{return,cancel},
|
|
3
|
+
// backpressure.{set,inc,dec}, waitable-set.{new,wait,poll,drop},
|
|
4
|
+
// waitable.join, subtask.{drop,cancel} and thread.yield.
|
|
5
|
+
//
|
|
6
|
+
// Every one is a direct port of the correspondingly named `canon_*` function
|
|
7
|
+
// in definitions.py (cited per function), with one systematic substitution.
|
|
8
|
+
//
|
|
9
|
+
// ## `current_instance()` vs the trampoline's declared instance
|
|
10
|
+
//
|
|
11
|
+
// definitions.py reads `current_instance()` (line 312), defined as
|
|
12
|
+
// `current_task().inst` — it can, because in the reference a canonical
|
|
13
|
+
// built-in is only ever reached from inside a task. That is not true of a
|
|
14
|
+
// real component: wasmtime lets a core module's **start function** call
|
|
15
|
+
// instance-scoped built-ins (`waitable-set.new`, `backpressure.inc`, ...)
|
|
16
|
+
// during instantiation, before any task exists. The official suite exercises
|
|
17
|
+
// exactly this (e.g. `test/async/dont-block-start.wast`).
|
|
18
|
+
//
|
|
19
|
+
// wasmtime resolves it by naming the owning component instance *statically*
|
|
20
|
+
// in every trampoline declaration (`Trampoline::WaitableSetNew { instance }`
|
|
21
|
+
// and friends — the `instance` field the plan carries). So the built-ins
|
|
22
|
+
// below take their instance from the declaration, which is well-defined at
|
|
23
|
+
// instantiation time and identical to `current_instance()` whenever a task is
|
|
24
|
+
// running. Built-ins that genuinely need the *task* or *thread*
|
|
25
|
+
// (`task.return`, `task.cancel`, `subtask.cancel`, `thread.yield`) still read
|
|
26
|
+
// the current-thread stack: they are meaningless outside a task, and the
|
|
27
|
+
// reference's `trap_if`s are what report that.
|
|
28
|
+
//
|
|
29
|
+
// ## Blocking built-ins in a stackless world
|
|
30
|
+
//
|
|
31
|
+
// `waitable-set.wait`, `thread.yield` and the synchronous `subtask.cancel`
|
|
32
|
+
// all *block the calling wasm frame* in the reference. A callback-ABI guest
|
|
33
|
+
// is stackless: there is no suspendable wasm stack to park, so blocking here
|
|
34
|
+
// genuinely requires JSPI (docs/architecture.md §6, JSPI role 2) and these built-ins say
|
|
35
|
+
// so at the precise point, loudly, instead of faking a wait.
|
|
36
|
+
//
|
|
37
|
+
// They are not, however, unconditionally unavailable. Where the reference can
|
|
38
|
+
// complete *without* suspending — `waitable-set.wait` on a set that already
|
|
39
|
+
// has a pending event, `waitable-set.poll` always, `subtask.cancel` on a
|
|
40
|
+
// subtask that resolved eagerly — this module returns the answer directly.
|
|
41
|
+
// That is not a shortcut: definitions.py's `Thread.wait_until` (line 396) may
|
|
42
|
+
// legitimately return without blocking when `ready_func()` already holds, so
|
|
43
|
+
// taking that branch is a conforming schedule.
|
|
44
|
+
import { blockCurrentActivation } from "../jspi/mod.js";
|
|
45
|
+
import { assert_, trap, trapIf } from "../cabi/trap.js";
|
|
46
|
+
import { CoreValueIter, LiftLowerContext, liftFlatValues, MAX_FLAT_PARAMS, store as storeValue, } from "../cabi/mod.js";
|
|
47
|
+
import { valTypesEqual } from "../cabi/types.js";
|
|
48
|
+
import { currentTask, currentThread, EventCode, liftOptionsEqual, needsJspi, Subtask, Waitable, WaitableSet, } from "../task/mod.js";
|
|
49
|
+
import { cabiOptions, normalizeCoreValues } from "../exec/boundary.js";
|
|
50
|
+
import { traceCopy } from "./stream_builtins.js";
|
|
51
|
+
/**
|
|
52
|
+
* `BLOCKED` sentinel of `canon_subtask_cancel` (definitions.py line 2467).
|
|
53
|
+
*/
|
|
54
|
+
export const BLOCKED = 0xffff_ffff;
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// task.return / task.cancel
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/** definitions.py `canon_task_return` (line 2384). */
|
|
59
|
+
export function createTaskReturn(decl, ctx) {
|
|
60
|
+
const opts = ctx.options(decl.options);
|
|
61
|
+
// plan v3: `resultType` is the interned `plan.types` entry; `results` is the
|
|
62
|
+
// raw wasmtime `TypeTupleIndex` (the FACT `task_return_type` key, consumed
|
|
63
|
+
// by the loader's dictionary). `null` is wire-legal for a task with no
|
|
64
|
+
// declared result type; today's producer always emits the empty tuple
|
|
65
|
+
// instead, so this degenerates to `[]` either way.
|
|
66
|
+
const resultTypes = decl.resultType === null
|
|
67
|
+
? []
|
|
68
|
+
: ctx.resultTypes(decl.resultType);
|
|
69
|
+
return (...flatArgs) => {
|
|
70
|
+
const task = currentTask();
|
|
71
|
+
trapIf(!task.inst.mayLeave, "task.return: cannot leave component instance (may_leave violation)");
|
|
72
|
+
trapIf(!task.opts.async_, "task.return from a non-async task");
|
|
73
|
+
// `trap_if(result_type != task.ft.result)` (definitions.py:2388): the
|
|
74
|
+
// trampoline's interned result tuple must be the lifted function's result
|
|
75
|
+
// type. Compared structurally — the plan's type table interns by
|
|
76
|
+
// structure, so identity comparison would reject valid components.
|
|
77
|
+
//
|
|
78
|
+
// Plan v3 enables this for FACT cross-component tasks too: the callee
|
|
79
|
+
// task's declared result type is now resolvable from the raw
|
|
80
|
+
// `TypeTupleIndex` `prepare-call` carried (contracts/plan-format.md v3
|
|
81
|
+
// amendment 3, wired in fact_calls.ts). It remains skipped for the one
|
|
82
|
+
// case v3 does not answer — a callee the plan maps no `task.return`
|
|
83
|
+
// tuple for, where `ft.results` is a placeholder rather than a
|
|
84
|
+
// declaration (`factResultTypesKnown === false`); comparing against a
|
|
85
|
+
// placeholder would be a false rejection, not a check.
|
|
86
|
+
trapIf((!task.factPassthrough || task.factResultTypesKnown) &&
|
|
87
|
+
!valTypesEqual(resultTypes, task.ft.results), "task.return with a result type that is not the task's result type");
|
|
88
|
+
// `trap_if(not LiftOptions.equal(opts, task.opts))` (definitions.py:2389).
|
|
89
|
+
// The MEMORY half stays skipped for FACT tasks, and plan v3 does NOT
|
|
90
|
+
// change that: the relaxation was never about the type mapping. The
|
|
91
|
+
// task's memory is reconstructed from `prepare-call`'s `memory` field,
|
|
92
|
+
// which is the *adapter's* view of the lift options
|
|
93
|
+
// (`adapter.lift.options...memory`) and is `None` for callees whose own
|
|
94
|
+
// `task.return` options do name a memory — the 17-param async-lifted
|
|
95
|
+
// callees of `test/async/cross-abi-calls.wast` are exactly that shape.
|
|
96
|
+
// The information simply is not in the plan, at v3 as at v2; restoring
|
|
97
|
+
// the check needs `prepare-call`'s indices related to the callee's
|
|
98
|
+
// canonical options, which remains open contract friction.
|
|
99
|
+
//
|
|
100
|
+
// definitions.py `LiftOptions.equal` (line 643) compares string encoding
|
|
101
|
+
// *and* memory identity. Both halves are checked for a host-boundary task.
|
|
102
|
+
//
|
|
103
|
+
// For a FACT task the memory half is skipped, and the reason is specific
|
|
104
|
+
// rather than "we can't be bothered": the task's memory is reconstructed
|
|
105
|
+
// from `prepare-call`'s `memory` field, which carries the *adapter's* view
|
|
106
|
+
// of the lift options (`adapter.lift.options...memory`) and is `None`
|
|
107
|
+
// for callees whose own `task.return` options do name a memory —
|
|
108
|
+
// `test/async/cross-abi-calls.wast`'s 17-param async-lifted callees are
|
|
109
|
+
// exactly that shape. wasmtime tolerates the mismatch because its check is
|
|
110
|
+
// *one-sided*: `concurrent.rs:3344-3358` treats "the `task.return` site
|
|
111
|
+
// specifies no memory" as valid and only compares when it does, against a
|
|
112
|
+
// lift memory it holds first-hand. We hold ours second-hand, so applying
|
|
113
|
+
// either form of the memory comparison produces a false rejection.
|
|
114
|
+
//
|
|
115
|
+
// The string-encoding half IS checked on both paths: `prepare-call` passes
|
|
116
|
+
// the encoding directly, so that reconstruction is exact.
|
|
117
|
+
trapIf(!liftOptionsEqual({ stringEncoding: opts.stringEncoding, memory: opts.memory }, task.factPassthrough
|
|
118
|
+
? { stringEncoding: task.opts.stringEncoding, memory: opts.memory }
|
|
119
|
+
: task.opts), "task.return with canonical options differing from the task's");
|
|
120
|
+
// Type-aware per-lane normalization: `normalizeFlat`'s blanket `>>> 0`
|
|
121
|
+
// silently truncated float lanes (a `task.return` of f64 -1.1 arrived at
|
|
122
|
+
// `[async-return]` as 4294967295). `normalizeCoreValues` consults the
|
|
123
|
+
// declared lane types, so only i32 lanes are coerced.
|
|
124
|
+
const flat = normalizeCoreValues(flatArgs, opts.coreType.params, "task.return arguments");
|
|
125
|
+
if (task.factPassthrough) {
|
|
126
|
+
// FACT cross-component call: the caller's `[async-return]` adapter
|
|
127
|
+
// function does the lift-and-lower itself, in wasm, so the host hands it
|
|
128
|
+
// the callee's flat results untouched. See `Task.factPassthrough`.
|
|
129
|
+
task.return_(flat);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const cx = new LiftLowerContext(cabiOptions(opts), task.inst, task);
|
|
133
|
+
const vi = new CoreValueIter(flat);
|
|
134
|
+
const result = liftFlatValues(cx, MAX_FLAT_PARAMS, vi, task.ft.results);
|
|
135
|
+
task.return_(result);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** definitions.py `canon_task_cancel` (line 2397). */
|
|
139
|
+
export function createTaskCancel() {
|
|
140
|
+
return () => {
|
|
141
|
+
const task = currentTask();
|
|
142
|
+
trapIf(!task.inst.mayLeave, "task.cancel: cannot leave component instance (may_leave violation)");
|
|
143
|
+
trapIf(!task.opts.async_, "task.cancel from a non-async task");
|
|
144
|
+
task.cancel();
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// backpressure
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// `canon_backpressure_set` is not ported: wasmtime 47 emits no
|
|
151
|
+
// `BackpressureSet` trampoline (`component/info.rs` has only
|
|
152
|
+
// `BackpressureInc`/`BackpressureDec`), and the reference's own copy was
|
|
153
|
+
// unreachable dead code until upstream removed it (CM PR #690; see
|
|
154
|
+
// upstream-component-model-repo-findings.md CM-2, RESOLVED). The counter
|
|
155
|
+
// below is the live interface.
|
|
156
|
+
/** definitions.py `canon_backpressure_inc` (line 2368). */
|
|
157
|
+
export function createBackpressureInc(inst) {
|
|
158
|
+
return () => {
|
|
159
|
+
assert_(inst.backpressure >= 0 && inst.backpressure < 2 ** 16, "backpressure counter out of range");
|
|
160
|
+
inst.backpressure += 1;
|
|
161
|
+
trapIf(inst.backpressure === 2 ** 16, "backpressure counter overflow");
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** definitions.py `canon_backpressure_dec` (line 2375). */
|
|
165
|
+
export function createBackpressureDec(inst) {
|
|
166
|
+
return () => {
|
|
167
|
+
assert_(inst.backpressure >= 0 && inst.backpressure < 2 ** 16, "backpressure counter out of range");
|
|
168
|
+
inst.backpressure -= 1;
|
|
169
|
+
trapIf(inst.backpressure < 0, "backpressure counter underflow");
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// waitable sets
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/** definitions.py `canon_waitable_set_new` (line 2406). */
|
|
176
|
+
export function createWaitableSetNew(inst) {
|
|
177
|
+
return () => {
|
|
178
|
+
trapIf(!inst.mayLeave, "waitable-set.new: cannot leave component instance");
|
|
179
|
+
return inst.handles.add(new WaitableSet());
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* definitions.py `canon_waitable_set_wait` (line 2414).
|
|
184
|
+
*
|
|
185
|
+
* The reference blocks the calling thread until the set has an event. From a
|
|
186
|
+
* stackless (callback-ABI) guest there is no wasm stack to suspend, so this
|
|
187
|
+
* only succeeds when an event is *already* pending — which is the reference's
|
|
188
|
+
* own non-blocking branch of `Thread.wait_until`. Otherwise: `needsJspi`.
|
|
189
|
+
*
|
|
190
|
+
* A guest using the callback ABI is expected to return the `WAIT` callback
|
|
191
|
+
* code rather than call this built-in; hitting the JSPI path here means the
|
|
192
|
+
* component uses the stackful async ABI.
|
|
193
|
+
*/
|
|
194
|
+
export function createWaitableSetWait(decl, ctx, inst, mode = "plain") {
|
|
195
|
+
const opts = ctx.options(decl.options);
|
|
196
|
+
// `cancellable` is a *canonical option*, not a trampoline field: wasmtime's
|
|
197
|
+
// `Trampoline::WaitableSetWait` carries only `{instance, options}`
|
|
198
|
+
// (wasmtime-environ 47.0.3 `component/info.rs:815`), while
|
|
199
|
+
// `CanonicalOptions.cancellable` (info.rs:540) is what the guest declared.
|
|
200
|
+
// It reaches definitions.py as `canon_waitable_set_wait`'s first parameter
|
|
201
|
+
// (line 2414).
|
|
202
|
+
const cancellable = opts.cancellable;
|
|
203
|
+
return (si, ptr) => {
|
|
204
|
+
trapIf(!inst.mayLeave, "waitable-set.wait: cannot leave component instance");
|
|
205
|
+
const wset = requireWaitableSet(inst, si ?? 0, "waitable-set.wait");
|
|
206
|
+
const task = currentTask();
|
|
207
|
+
let event;
|
|
208
|
+
if (task.deliverPendingCancel(cancellable)) {
|
|
209
|
+
event = [EventCode.TASK_CANCELLED, 0, 0];
|
|
210
|
+
}
|
|
211
|
+
else if (wset.hasPendingEvent()) {
|
|
212
|
+
// Non-blocking branch: definitions.py `Thread.wait_until` may return
|
|
213
|
+
// immediately when the condition already holds.
|
|
214
|
+
//
|
|
215
|
+
// This deliberately skips `wait_for_event_and`, and with it the
|
|
216
|
+
// `num_waiting += 1 / -= 1` bracket around the block
|
|
217
|
+
// (`WaitableSet.wait_for_event_and`, line 829). That is unobservable:
|
|
218
|
+
// `num_waiting` is read only by `WaitableSet.drop`
|
|
219
|
+
// (`trap_if(self.num_waiting > 0)`, line 852), and since we never yield
|
|
220
|
+
// between the increment and the decrement here, no other code could run
|
|
221
|
+
// to observe a non-zero value. Incrementing and immediately decrementing
|
|
222
|
+
// would be pure ceremony.
|
|
223
|
+
traceCopy(`waitable-set.wait si=${si} FAST (pending event)`);
|
|
224
|
+
event = wset.getPendingEvent();
|
|
225
|
+
}
|
|
226
|
+
else if (mode === "jspi") {
|
|
227
|
+
traceCopy(`waitable-set.wait si=${si} BLOCKS`);
|
|
228
|
+
// SITE 2 (lit). definitions.py `WaitableSet.wait_for_event_and`
|
|
229
|
+
// (line 829): block until the set has an event, then take it.
|
|
230
|
+
//
|
|
231
|
+
// The `num_waiting` bracket is real now. Skipping it was justified only
|
|
232
|
+
// while this path could not actually yield; a genuine block CAN be
|
|
233
|
+
// observed, because `WaitableSet.drop` traps on `num_waiting > 0`
|
|
234
|
+
// (line 852). Incremented before blocking and decremented in
|
|
235
|
+
// `onSettled`, which runs exactly once on EVERY terminal transition —
|
|
236
|
+
// normal resume, cancelled resume, produce-throw, and `abandon`
|
|
237
|
+
// (#106: decrementing in `produce` missed the abandon leg, leaving
|
|
238
|
+
// `numWaiting` elevated forever and a later `waitable-set.drop`
|
|
239
|
+
// trapping spuriously). The decrement is not idempotent, so it lives
|
|
240
|
+
// ONLY here, not in `produce` as well; nothing can observe the still-
|
|
241
|
+
// elevated count between `produce` and the hook — both run
|
|
242
|
+
// synchronously inside the settle, before any other code.
|
|
243
|
+
wset.numWaiting += 1;
|
|
244
|
+
return blockCurrentActivation({
|
|
245
|
+
store: inst.store,
|
|
246
|
+
task,
|
|
247
|
+
readyFunc: () => wset.hasPendingEvent(),
|
|
248
|
+
cancellable,
|
|
249
|
+
produce: (cancelled) => {
|
|
250
|
+
const ev = cancelled
|
|
251
|
+
? [EventCode.TASK_CANCELLED, 0, 0]
|
|
252
|
+
: wset.getPendingEvent();
|
|
253
|
+
return unpackEvent(opts, inst, ptr ?? 0, ev);
|
|
254
|
+
},
|
|
255
|
+
onSettled: () => {
|
|
256
|
+
wset.numWaiting -= 1;
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
needsJspi("waitable-set.wait with no pending event (the calling wasm frame " +
|
|
262
|
+
"must block; a callback-ABI guest should return the WAIT code " +
|
|
263
|
+
"instead)");
|
|
264
|
+
}
|
|
265
|
+
return unpackEvent(opts, inst, ptr ?? 0, event);
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/** definitions.py `canon_waitable_set_poll` (line 2431). */
|
|
269
|
+
export function createWaitableSetPoll(decl, ctx, inst) {
|
|
270
|
+
const opts = ctx.options(decl.options);
|
|
271
|
+
/** See `createWaitableSetWait`: `cancellable` is an option, not a decl field. */
|
|
272
|
+
const cancellable = opts.cancellable;
|
|
273
|
+
return (si, ptr) => {
|
|
274
|
+
trapIf(!inst.mayLeave, "waitable-set.poll: cannot leave component instance");
|
|
275
|
+
const wset = requireWaitableSet(inst, si ?? 0, "waitable-set.poll");
|
|
276
|
+
const event = wset.poll(currentTask(), cancellable);
|
|
277
|
+
return unpackEvent(opts, inst, ptr ?? 0, event);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** definitions.py `canon_waitable_set_drop` (line 2441). */
|
|
281
|
+
export function createWaitableSetDrop(inst) {
|
|
282
|
+
return (i) => {
|
|
283
|
+
trapIf(!inst.mayLeave, "waitable-set.drop: cannot leave component instance");
|
|
284
|
+
const wset = inst.handles.remove(i ?? 0);
|
|
285
|
+
trapIf(!(wset instanceof WaitableSet), "waitable-set.drop: handle is not a waitable set");
|
|
286
|
+
wset.drop();
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** definitions.py `canon_waitable_join` (line 2451). */
|
|
290
|
+
export function createWaitableJoin(inst) {
|
|
291
|
+
return (wi, si) => {
|
|
292
|
+
trapIf(!inst.mayLeave, "waitable.join: cannot leave component instance");
|
|
293
|
+
const w = inst.handles.get(wi ?? 0);
|
|
294
|
+
trapIf(!(w instanceof Waitable), "waitable.join: handle is not a waitable");
|
|
295
|
+
trapIf(w.hasSyncWaiter, "waitable.join on a waitable with a synchronous waiter");
|
|
296
|
+
if ((si ?? 0) === 0) {
|
|
297
|
+
w.join(null);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const wset = requireWaitableSet(inst, si, "waitable.join");
|
|
301
|
+
w.join(wset);
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// subtasks
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
/** definitions.py `canon_subtask_drop` (line 2494). */
|
|
308
|
+
export function createSubtaskDrop(inst) {
|
|
309
|
+
return (i) => {
|
|
310
|
+
trapIf(!inst.mayLeave, "subtask.drop: cannot leave component instance");
|
|
311
|
+
const s = inst.handles.remove(i ?? 0);
|
|
312
|
+
trapIf(!(s instanceof Subtask), "subtask.drop: handle is not a subtask");
|
|
313
|
+
s.drop();
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* definitions.py `canon_subtask_cancel` (line 2469).
|
|
318
|
+
*
|
|
319
|
+
* The synchronous form blocks (`subtask.wait_for_pending_event()`) when the
|
|
320
|
+
* callee does not resolve promptly; from a stackless guest that is JSPI
|
|
321
|
+
* territory. The async form returns `BLOCKED` instead of blocking, and is
|
|
322
|
+
* fully supported.
|
|
323
|
+
*/
|
|
324
|
+
/**
|
|
325
|
+
* The tail shared by `subtask.cancel`'s blocking and non-blocking exits:
|
|
326
|
+
* take the delivered SUBTASK event, check it is the one we expect, and report
|
|
327
|
+
* the resolved state. Factored out so the blocking form can run it at RESUME
|
|
328
|
+
* time inside `produce`.
|
|
329
|
+
*/
|
|
330
|
+
function finishSubtaskCancel(i, st) {
|
|
331
|
+
return () => {
|
|
332
|
+
const [code, index, payload] = st.getPendingEvent();
|
|
333
|
+
assert_(code === EventCode.SUBTASK && index === (i ?? 0) && payload === st.state, "unexpected event delivered by subtask.cancel");
|
|
334
|
+
assert_(st.resolveDelivered(), "subtask.cancel did not deliver the resolution");
|
|
335
|
+
return st.state;
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
export function createSubtaskCancel(decl, inst, mode = "plain") {
|
|
339
|
+
const async_ = decl.async === true;
|
|
340
|
+
return (i) => {
|
|
341
|
+
// The handle table is the **declared** instance's, not
|
|
342
|
+
// `current_thread().task.inst`. definitions.py `canon_subtask_cancel`
|
|
343
|
+
// (line 2469) uses the latter because the reference has no fused
|
|
344
|
+
// adapters, so the running task and the subtask's owner always coincide.
|
|
345
|
+
// With FACT they do not: `async-start-call` adds the subtask to
|
|
346
|
+
// `prepare-call`'s `caller_instance`, which for a nested component is a
|
|
347
|
+
// *different* instance from the one whose task is running — observed as
|
|
348
|
+
// caller=2 vs task.inst=3 in `big-interleaving-test.wast:1584`, where the
|
|
349
|
+
// lookup then failed with "table index out of range". wasmtime names the
|
|
350
|
+
// owner on the trampoline for exactly this reason
|
|
351
|
+
// (`Trampoline::SubtaskCancel { instance, .. }`), which is the same
|
|
352
|
+
// correction already applied to every other instance-scoped built-in —
|
|
353
|
+
// see this module's header.
|
|
354
|
+
trapIf(!inst.mayLeave, "subtask.cancel: cannot leave component instance");
|
|
355
|
+
const subtask = inst.handles.get(i ?? 0);
|
|
356
|
+
trapIf(!(subtask instanceof Subtask), "subtask.cancel: handle is not a subtask");
|
|
357
|
+
const st = subtask;
|
|
358
|
+
const finish = finishSubtaskCancel(i, st);
|
|
359
|
+
trapIf(st.resolveDelivered(), "subtask.cancel on a subtask whose resolution was already delivered");
|
|
360
|
+
trapIf(st.cancellationRequested, "subtask.cancel on a subtask that was already cancelled");
|
|
361
|
+
trapIf(st.inWaitableSet() && !async_, "synchronous subtask.cancel on a subtask that is in a waitable set");
|
|
362
|
+
if (st.resolved()) {
|
|
363
|
+
assert_(st.hasPendingEvent(), "resolved subtask without a pending event at cancellation");
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
st.cancellationRequested = true;
|
|
367
|
+
assert_(st.onCancel !== null, "subtask.cancel on a subtask with no cancellation handler");
|
|
368
|
+
st.onCancel(inst);
|
|
369
|
+
if (!st.resolved()) {
|
|
370
|
+
if (!async_) {
|
|
371
|
+
if (mode === "jspi") {
|
|
372
|
+
// SITE 5 (lit): a sync `subtask.cancel` blocks until the callee
|
|
373
|
+
// actually resolves (definitions.py `canon_subtask_cancel`), then
|
|
374
|
+
// reports the resolved state through the same tail as the
|
|
375
|
+
// non-blocking path. Mirrors SITE 4 (stream_builtins.ts:305-323)
|
|
376
|
+
// and `Waitable.waitForPendingEvent` (definitions.py:786-790,
|
|
377
|
+
// reached from canon_subtask_cancel's `subtask.wait_for_pending_event()`
|
|
378
|
+
// call, :2484): `hasSyncWaiter` must
|
|
379
|
+
// be set for the duration so a concurrent `waitable.join` on
|
|
380
|
+
// this subtask traps (async_builtins.ts:362-365) instead of
|
|
381
|
+
// racing the SUBTASK event away from this resume (#87).
|
|
382
|
+
st.hasSyncWaiter = true;
|
|
383
|
+
return blockCurrentActivation({
|
|
384
|
+
store: inst.store,
|
|
385
|
+
task: currentTask(),
|
|
386
|
+
readyFunc: () => st.hasPendingEvent(),
|
|
387
|
+
cancellable: false,
|
|
388
|
+
produce: () => {
|
|
389
|
+
st.hasSyncWaiter = false;
|
|
390
|
+
return finish();
|
|
391
|
+
},
|
|
392
|
+
// #106: `abandon` never runs `produce`; without the backstop
|
|
393
|
+
// the flag stayed set forever and a later `waitable.join` on
|
|
394
|
+
// this subtask trapped spuriously. Idempotent, so the success
|
|
395
|
+
// path's clear-inside-`produce` ordering is untouched.
|
|
396
|
+
onSettled: () => {
|
|
397
|
+
st.hasSyncWaiter = false;
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
needsJspi("synchronous subtask.cancel whose callee did not resolve " +
|
|
402
|
+
"immediately (the calling wasm frame must block)");
|
|
403
|
+
}
|
|
404
|
+
// The ASYNC form answers "did the cancellation resolve the callee
|
|
405
|
+
// promptly?" — BLOCKED only when it genuinely did not
|
|
406
|
+
// (definitions.py line 2486). Under jspi "promptly" is invisible at
|
|
407
|
+
// this instant: `request_cancellation` delivered TASK_CANCELLED by
|
|
408
|
+
// settling the callee's suspension, and the engine runs the resumed
|
|
409
|
+
// activation (whose `task.cancel` resolves this subtask) on a LATER
|
|
410
|
+
// microtask (pin (j)). Deciding now reports BLOCKED for a callee the
|
|
411
|
+
// reference resolves synchronously (cancellable.wast tests 1-2). So:
|
|
412
|
+
// wait until the callee is DETERMINATE — resolved, finished, or
|
|
413
|
+
// re-parked on a scheduler condition — exactly `async-start-call`'s
|
|
414
|
+
// rule (fact_calls.ts). A callee with a pending (undeliverable)
|
|
415
|
+
// cancel sits parked non-cancellably, which is determinate, so the
|
|
416
|
+
// genuine BLOCKED answer is still immediate. Host-import subtasks
|
|
417
|
+
// carry no callee task: their onCancel is a no-op and their state
|
|
418
|
+
// cannot be mid-hop, so the pre-jspi immediate answer stands.
|
|
419
|
+
//
|
|
420
|
+
// NAMED DIVERGENCE (docs/architecture.md §6, #92): this park makes
|
|
421
|
+
// the async built-in non-atomic — other ready threads may run while
|
|
422
|
+
// it waits, a reordering within the reference's Store.tick freedom
|
|
423
|
+
// taken one built-in early.
|
|
424
|
+
if (mode === "jspi" && st.calleeTask !== null) {
|
|
425
|
+
const t = st.calleeTask;
|
|
426
|
+
const store = inst.store;
|
|
427
|
+
const determinate = () => st.resolved() ||
|
|
428
|
+
t.threads.every((th) => th.done()) ||
|
|
429
|
+
store.waiting.some((w) => w.task === st.calleeTask);
|
|
430
|
+
if (!determinate()) {
|
|
431
|
+
return blockCurrentActivation({
|
|
432
|
+
store: inst.store,
|
|
433
|
+
task: currentTask(),
|
|
434
|
+
readyFunc: determinate,
|
|
435
|
+
cancellable: false,
|
|
436
|
+
produce: () => (st.resolved() ? finish() : BLOCKED),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (!st.resolved())
|
|
440
|
+
return BLOCKED;
|
|
441
|
+
return finish();
|
|
442
|
+
}
|
|
443
|
+
return BLOCKED;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return finish();
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// thread.yield
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
/**
|
|
453
|
+
* definitions.py `canon_thread_yield` (line 2728).
|
|
454
|
+
*
|
|
455
|
+
* Yielding blocks the calling wasm frame until the scheduler comes back to
|
|
456
|
+
* it. A callback-ABI guest expresses the same intent by returning the `YIELD`
|
|
457
|
+
* callback code, which this runtime implements fully (exec/boundary.ts); the
|
|
458
|
+
* *built-in* form needs a suspendable stack.
|
|
459
|
+
*/
|
|
460
|
+
export function createThreadYield(decl, mode = "plain") {
|
|
461
|
+
const cancellable = decl.cancellable === true;
|
|
462
|
+
return () => {
|
|
463
|
+
const thread = currentThread();
|
|
464
|
+
trapIf(!thread.task.inst.mayLeave, "thread.yield: cannot leave component instance");
|
|
465
|
+
// A pending cancellation is deliverable without suspending at all
|
|
466
|
+
// (definitions.py `Thread.yield_` -> `wait_until` -> `deliver_pending_cancel`).
|
|
467
|
+
if (thread.task.deliverPendingCancel(cancellable))
|
|
468
|
+
return 1;
|
|
469
|
+
if (mode === "jspi") {
|
|
470
|
+
// SITE 3 (lit). definitions.py `Thread.yield_` is
|
|
471
|
+
// `wait_until(lambda: True, cancellable)`: immediately ready, but it
|
|
472
|
+
// goes through the scheduler, so other threads get a turn first. A
|
|
473
|
+
// suspension point with an always-true `readyFunc` is exactly that --
|
|
474
|
+
// `Store.tick` will resume it, after whatever else is already ready.
|
|
475
|
+
return blockCurrentActivation({
|
|
476
|
+
store: thread.task.inst.store,
|
|
477
|
+
task: thread.task,
|
|
478
|
+
readyFunc: () => true,
|
|
479
|
+
cancellable,
|
|
480
|
+
produce: (cancelled) => (cancelled ? 1 : 0),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
needsJspi("thread.yield (the calling wasm frame must block; a callback-ABI " +
|
|
484
|
+
"guest should return the YIELD code instead)");
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
// helpers
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
function requireWaitableSet(inst, si, what) {
|
|
491
|
+
const wset = inst.handles.get(si);
|
|
492
|
+
trapIf(!(wset instanceof WaitableSet), `${what}: handle ${si} is not a waitable set`);
|
|
493
|
+
return wset;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* definitions.py `unpack_event` (line 2422): store the two payload words at
|
|
497
|
+
* `ptr` and return the event code.
|
|
498
|
+
*/
|
|
499
|
+
function unpackEvent(opts, inst, ptr, e) {
|
|
500
|
+
const [event, p1, p2] = e;
|
|
501
|
+
const cx = new LiftLowerContext(cabiOptions(opts), inst, null);
|
|
502
|
+
storeValue(cx, p1, { kind: "u32" }, ptr);
|
|
503
|
+
storeValue(cx, p2, { kind: "u32" }, ptr + 4);
|
|
504
|
+
return event;
|
|
505
|
+
}
|
|
506
|
+
// Structural `ValType` equality (the C2-D bugfix) moved to cabi/types.ts
|
|
507
|
+
// (`valTypesEqual`) when the #18 tls smoke found its stream-element sibling;
|
|
508
|
+
// the contract note lives there now.
|
|
509
|
+
/** Unused-import guard: `trap` is re-exported for symmetry with cabi. */
|
|
510
|
+
void trap;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// `CoreDef::UnsafeIntrinsic` (plan v1 / contracts/plan-format.md v0.3):
|
|
2
|
+
// wasmtime compile-time builtins that a component's core modules import
|
|
3
|
+
// directly, bypassing the trampoline table.
|
|
4
|
+
//
|
|
5
|
+
// Of wasmtime-environ 47.0.3's 21 unsafe intrinsics
|
|
6
|
+
// (`component/intrinsic.rs`, `for_each_unsafe_intrinsic!`) only four have
|
|
7
|
+
// Component Model meaning: `context-{get,set}-i32-{0,1}`, the canonical
|
|
8
|
+
// `context.get` / `context.set` built-ins. The other seventeen
|
|
9
|
+
// (`*-native-load` / `*-native-store` / `store-data-address`) are raw host
|
|
10
|
+
// memory access for wasmtime's own internals; they have no portable meaning
|
|
11
|
+
// in a JS host and are refused at instantiate time.
|
|
12
|
+
import { assert_, trapIf } from "../cabi/trap.js";
|
|
13
|
+
import { currentThread } from "../task/mod.js";
|
|
14
|
+
import { ambientDebug, dbgId } from "../task/scheduler.js";
|
|
15
|
+
import { UnsupportedFeatureError } from "./errors.js";
|
|
16
|
+
/**
|
|
17
|
+
* Number of `i32` context slots per thread. definitions.py `Thread.storage`
|
|
18
|
+
* is initialised `[0,0]` (line 347) and `canon_context_{get,set}` assert
|
|
19
|
+
* `i < len(thread.storage)` — so exactly two, matching the intrinsic names
|
|
20
|
+
* `context-*-i32-0` and `context-*-i32-1`.
|
|
21
|
+
*/
|
|
22
|
+
export const NUM_CONTEXT_SLOTS = 2;
|
|
23
|
+
/**
|
|
24
|
+
* definitions.py `canon_context_get` (line 2348).
|
|
25
|
+
*
|
|
26
|
+
* The storage is **per thread**, not per task: two threads of one task have
|
|
27
|
+
* independent context. wit-bindgen 0.60 keeps its async-executor task pointer
|
|
28
|
+
* in slot 0, which is why this intrinsic is the entry blocker for async
|
|
29
|
+
* guests.
|
|
30
|
+
*/
|
|
31
|
+
export function canonContextGet(i) {
|
|
32
|
+
const thread = currentThread();
|
|
33
|
+
assert_(i < NUM_CONTEXT_SLOTS, `context.get slot ${i} out of range`);
|
|
34
|
+
const result = thread.storage[i];
|
|
35
|
+
assert_(result < 2 ** 32, "context.get value out of i32 range");
|
|
36
|
+
if (CTX_TRACE)
|
|
37
|
+
trace(`get[${i}] -> ${result}`, thread);
|
|
38
|
+
return result >>> 0;
|
|
39
|
+
}
|
|
40
|
+
// Standing probe (CE_CTX_TRACE=1): per-call context-slot traffic with the
|
|
41
|
+
// full ambient state — the instrument that isolated issue #24. Cheap and
|
|
42
|
+
// env-gated; keep.
|
|
43
|
+
const CTX_TRACE = (() => {
|
|
44
|
+
try {
|
|
45
|
+
return Deno.env.get("CE_CTX_TRACE") === "1";
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
export function ctxThreadId(t) {
|
|
52
|
+
return dbgId(t);
|
|
53
|
+
}
|
|
54
|
+
function trace(msg, thread) {
|
|
55
|
+
const a = ambientDebug();
|
|
56
|
+
console.error(`[ctx] ${ctxThreadId(thread)} ${msg} storage=${JSON.stringify(thread.storage)} | stack=[${a.stack.map(ctxThreadId).join(",")}] claims=[${a.claims.map(ctxThreadId).join(",")}] resuming=${a.resuming === null ? "-" : ctxThreadId(a.resuming)}`);
|
|
57
|
+
}
|
|
58
|
+
/** definitions.py `canon_context_set` (line 2358). */
|
|
59
|
+
export function canonContextSet(i, v) {
|
|
60
|
+
const thread = currentThread();
|
|
61
|
+
assert_(i < NUM_CONTEXT_SLOTS, `context.set slot ${i} out of range`);
|
|
62
|
+
if (CTX_TRACE)
|
|
63
|
+
trace(`set[${i}] = ${v >>> 0}`, thread);
|
|
64
|
+
thread.storage[i] = v >>> 0;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Materialize one `unsafe-intrinsic` CoreDef as a core function.
|
|
68
|
+
*
|
|
69
|
+
* Called during initializer resolution — i.e. at instantiate time — so an
|
|
70
|
+
* unimplementable symbol fails instantiation rather than the first call
|
|
71
|
+
* (contracts/plan-format.md "Executor obligations").
|
|
72
|
+
*/
|
|
73
|
+
export function createUnsafeIntrinsic(symbol) {
|
|
74
|
+
const match = /^context-(get|set)-i32-(\d+)$/.exec(symbol);
|
|
75
|
+
if (match === null) {
|
|
76
|
+
throw new UnsupportedFeatureError("M2", `component imports the unsafe intrinsic '${symbol}', which has no ` +
|
|
77
|
+
`portable meaning in a JS host (only context.{get,set} do)`);
|
|
78
|
+
}
|
|
79
|
+
const slot = Number(match[2]);
|
|
80
|
+
// A slot outside the canonical range would be a wasmtime/plan inconsistency
|
|
81
|
+
// rather than a missing capability, but refusing it here is still the right
|
|
82
|
+
// shape: instantiate-time, never call-time.
|
|
83
|
+
trapIf(slot >= NUM_CONTEXT_SLOTS, `unsafe intrinsic '${symbol}' addresses context slot ${slot}, but a ` +
|
|
84
|
+
`thread has ${NUM_CONTEXT_SLOTS}`);
|
|
85
|
+
if (match[1] === "get")
|
|
86
|
+
return () => canonContextGet(slot);
|
|
87
|
+
return (v) => {
|
|
88
|
+
canonContextSet(slot, (v ?? 0) >>> 0);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Shared failure type for trampolines/intrinsics scheduled after the current
|
|
2
|
+
// milestone. Split out of ./mod.ts so sibling intrinsic modules can raise it
|
|
3
|
+
// without importing the (much larger) trampoline dispatcher.
|
|
4
|
+
/** Instantiate-time failure for functionality scheduled after M0. */
|
|
5
|
+
export class UnsupportedFeatureError extends Error {
|
|
6
|
+
milestone;
|
|
7
|
+
constructor(milestone, what) {
|
|
8
|
+
super(`${what} — scheduled for ${milestone}, not implemented in the current ` +
|
|
9
|
+
`executor (contracts/intrinsics.md §B)`);
|
|
10
|
+
this.milestone = milestone;
|
|
11
|
+
this.name = "UnsupportedFeatureError";
|
|
12
|
+
}
|
|
13
|
+
}
|