@yabgu_gm/tensionscript 0.0.0-stage → 0.11.0
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 +201 -0
- package/NOTICE +97 -0
- package/README.md +156 -2
- package/bin/tensionscript.js +35 -0
- package/dist/asc.d.ts +4 -0
- package/dist/asc.generated.d.ts +12007 -0
- package/dist/asc.js +23474 -0
- package/dist/asc.js.map +7 -0
- package/dist/assemblyscript.d.ts +4 -0
- package/dist/assemblyscript.generated.d.ts +13221 -0
- package/dist/assemblyscript.js +337 -0
- package/dist/assemblyscript.js.map +7 -0
- package/dist/importmap.json +9 -0
- package/dist/transform.cjs +1 -0
- package/dist/transform.d.ts +1 -0
- package/dist/transform.js +1 -0
- package/dist/web.js +22 -0
- package/lib/binaryen.d.ts +2 -0
- package/lib/binaryen.js +2 -0
- package/package.json +116 -4
- package/std/README.md +6 -0
- package/std/assembly/array.ts +526 -0
- package/std/assembly/arraybuffer.ts +77 -0
- package/std/assembly/atomics.ts +127 -0
- package/std/assembly/bindings/asyncify.ts +16 -0
- package/std/assembly/bindings/dom.ts +291 -0
- package/std/assembly/bindings/node.ts +6 -0
- package/std/assembly/builtins.ts +2642 -0
- package/std/assembly/closures.ts +22 -0
- package/std/assembly/compat.ts +2 -0
- package/std/assembly/console.ts +42 -0
- package/std/assembly/crypto.ts +9 -0
- package/std/assembly/dataview.ts +181 -0
- package/std/assembly/date.ts +373 -0
- package/std/assembly/diagnostics.ts +11 -0
- package/std/assembly/error.ts +44 -0
- package/std/assembly/function.ts +38 -0
- package/std/assembly/index.d.ts +2721 -0
- package/std/assembly/iterator.ts +35 -0
- package/std/assembly/map.ts +260 -0
- package/std/assembly/math.ts +3289 -0
- package/std/assembly/memory.ts +123 -0
- package/std/assembly/number.ts +388 -0
- package/std/assembly/object.ts +36 -0
- package/std/assembly/performance.ts +9 -0
- package/std/assembly/polyfills.ts +27 -0
- package/std/assembly/process.ts +50 -0
- package/std/assembly/reference.ts +48 -0
- package/std/assembly/regexp.ts +12 -0
- package/std/assembly/rt/README.md +81 -0
- package/std/assembly/rt/common.ts +81 -0
- package/std/assembly/rt/index-incremental.ts +2 -0
- package/std/assembly/rt/index-minimal.ts +2 -0
- package/std/assembly/rt/index-stub.ts +1 -0
- package/std/assembly/rt/index.d.ts +37 -0
- package/std/assembly/rt/itcms.ts +419 -0
- package/std/assembly/rt/rtrace.ts +15 -0
- package/std/assembly/rt/stub.ts +133 -0
- package/std/assembly/rt/tcms.ts +254 -0
- package/std/assembly/rt/tlsf.ts +592 -0
- package/std/assembly/rt.ts +87 -0
- package/std/assembly/set.ts +220 -0
- package/std/assembly/shared/feature.ts +65 -0
- package/std/assembly/shared/runtime.ts +11 -0
- package/std/assembly/shared/target.ts +11 -0
- package/std/assembly/shared/tsconfig.json +11 -0
- package/std/assembly/shared/typeinfo.ts +72 -0
- package/std/assembly/staticarray.ts +401 -0
- package/std/assembly/string.ts +847 -0
- package/std/assembly/symbol.ts +114 -0
- package/std/assembly/table.ts +16 -0
- package/std/assembly/task-rt.ts +609 -0
- package/std/assembly/task.ts +90 -0
- package/std/assembly/tsconfig.json +6 -0
- package/std/assembly/typedarray.ts +1945 -0
- package/std/assembly/uri.ts +17 -0
- package/std/assembly/util/bytes.ts +135 -0
- package/std/assembly/util/casemap.ts +497 -0
- package/std/assembly/util/error.ts +54 -0
- package/std/assembly/util/hash.ts +117 -0
- package/std/assembly/util/math.ts +1922 -0
- package/std/assembly/util/memory.ts +290 -0
- package/std/assembly/util/number.ts +873 -0
- package/std/assembly/util/sort.ts +313 -0
- package/std/assembly/util/string.ts +1202 -0
- package/std/assembly/util/uri.ts +275 -0
- package/std/assembly/vector.ts +4 -0
- package/std/assembly.json +16 -0
- package/std/portable/index.d.ts +462 -0
- package/std/portable/index.js +416 -0
- package/std/portable.json +11 -0
- package/std/types/assembly/index.d.ts +1 -0
- package/std/types/assembly/package.json +3 -0
- package/std/types/portable/index.d.ts +1 -0
- package/std/types/portable/package.json +3 -0
- package/tsconfig-base.json +13 -0
- package/util/README.md +23 -0
- package/util/browser/fs.js +1 -0
- package/util/browser/module.js +5 -0
- package/util/browser/path.js +520 -0
- package/util/browser/process.js +59 -0
- package/util/browser/url.js +23 -0
- package/util/cpu.d.ts +9 -0
- package/util/cpu.js +42 -0
- package/util/find.d.ts +6 -0
- package/util/find.js +20 -0
- package/util/node.d.ts +21 -0
- package/util/node.js +34 -0
- package/util/options.d.ts +70 -0
- package/util/options.js +262 -0
- package/util/terminal.d.ts +52 -0
- package/util/terminal.js +35 -0
- package/util/text.d.ts +26 -0
- package/util/text.js +114 -0
- package/util/tsconfig.json +9 -0
- package/util/web.d.ts +11 -0
- package/util/web.js +33 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/// <reference path="./rt/index.d.ts" />
|
|
2
|
+
|
|
3
|
+
import { TaskRecord } from "./task";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The guest async runtime's seam — and, since Phase 4, the machine behind it.
|
|
7
|
+
*
|
|
8
|
+
* `docs/async-await-plan-v2.md` §4.7 is the specification, §3.2 the record
|
|
9
|
+
* layout (`std/assembly/task.ts`), §6 the scheduler, §6.4 the dropped-completion
|
|
10
|
+
* rule. The split it pins: **std carries the machine**, the framework connects
|
|
11
|
+
* it to the host's completion stream. The framework's halves are the record
|
|
12
|
+
* subclass (when a capability wants one), the class→family map for its
|
|
13
|
+
* capabilities, and the two call sites — `onBatch`/`onEvent` decoding a
|
|
14
|
+
* delivered record into `__taskDeliver`, and the frame loop calling
|
|
15
|
+
* `__taskTick` (§6.1-§6.3).
|
|
16
|
+
*
|
|
17
|
+
* Why a *top-level* std file (`~lib/task-rt.ts`) and not a subdirectory module:
|
|
18
|
+
* top-level std files are parsed by every build (`cli/index.js:471-479`), and
|
|
19
|
+
* their exports resolve as bare identifiers from any source through the
|
|
20
|
+
* program's global element index (`resolver.ts:1301` — the same fallback that
|
|
21
|
+
* makes `Task` itself resolvable, `program.ts:559-567`). A subdirectory module
|
|
22
|
+
* is parsed only on demand, and the lowering transform cannot create that
|
|
23
|
+
* demand: an import it injects into an already-parsed source is not queued, and
|
|
24
|
+
* the CLI's second `parseBacklog` drains only what the parser queued
|
|
25
|
+
* (`cli/index.js:703-706`). Measured: an injected import naming an unparsed
|
|
26
|
+
* `~lib/` module does not resolve.
|
|
27
|
+
*
|
|
28
|
+
* ## What is implemented, and what Phase 4a does not yet do
|
|
29
|
+
*
|
|
30
|
+
* Implemented (4a): registration, the settled check, parking (which no longer
|
|
31
|
+
* traps), delivery's payload copy, the ready queue and `__taskTick`, the two
|
|
32
|
+
* startup hooks, and `__taskHasRunnable`.
|
|
33
|
+
*
|
|
34
|
+
* Not yet: **§6.4's dropped-completion rule** (4e), and **the status a failure
|
|
35
|
+
* class derives** (§3.4) — both need the framework's class bindings, which 4b
|
|
36
|
+
* adds. A delivered record's `status` is therefore 0 even for a failure class
|
|
37
|
+
* until then; nothing in 4a delivers, so no program can observe it.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The base class of a task's environment — the object that holds a captured
|
|
42
|
+
* binding's single storage (§4.4: "a captured binding's storage *is* the
|
|
43
|
+
* environment").
|
|
44
|
+
*
|
|
45
|
+
* It declares no fields: the fields belong to whatever the closures transform
|
|
46
|
+
* decided the environment looks like (`src/closure/lower.ts`), and std must not
|
|
47
|
+
* pre-empt that. What std fixes is the *type*, so the state struct's `$env`
|
|
48
|
+
* field has a nameable type to point at, and so the shape does not change when
|
|
49
|
+
* closures land. Being a class-typed field, it is traced by the generated
|
|
50
|
+
* visitor, which is what lets the collector follow state → environment for a
|
|
51
|
+
* parked task (§4.4).
|
|
52
|
+
*/
|
|
53
|
+
export class TaskEnv {}
|
|
54
|
+
|
|
55
|
+
/** The base class of every lowered state machine (§4.7.3). */
|
|
56
|
+
export class TaskState {
|
|
57
|
+
/**
|
|
58
|
+
* The task's own record. Assigned by `__taskRegister`; the
|
|
59
|
+
* definite-assignment assertion is what keeps it non-nullable without a dummy
|
|
60
|
+
* initializer (`common.ts:42-43`).
|
|
61
|
+
*/
|
|
62
|
+
record!: TaskRecord;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The function that resumes this task — `f$resume`, assigned by the entry
|
|
66
|
+
* immediately before the synchronous prefix runs (Phase 4 ruling, plan
|
|
67
|
+
* §4.7.3).
|
|
68
|
+
*
|
|
69
|
+
* Without it a parked task has no way back into its own body: the entry calls
|
|
70
|
+
* `f$resume` directly and returns, and nothing else holds a reference to it.
|
|
71
|
+
* The type is the *base* shape `(state: TaskState) => void` because the field
|
|
72
|
+
* is declared once for every state struct; the entry assigns
|
|
73
|
+
* `f$resume` through a `changetype`, which is the same one cast §8.3 already
|
|
74
|
+
* uses for a non-nullable read of a carrier field. A managed function value
|
|
75
|
+
* (every `f$resume` is a top-level synthesized function, so the value is a
|
|
76
|
+
* static `Function<T>` and allocates nothing), traced by the generated
|
|
77
|
+
* visitor like every other reference in §4.4's chain.
|
|
78
|
+
*/
|
|
79
|
+
resume!: (state: TaskState) => void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The task-table slot this suspension holds, or `-1` when it holds none
|
|
83
|
+
* (0.6.0's push path; `-1` for a task that never parked, or one already
|
|
84
|
+
* resumed).
|
|
85
|
+
*
|
|
86
|
+
* A plain `i32` on the state rather than a `Map<TaskState, i32>`: the state
|
|
87
|
+
* already *is* the key, the value is one word, and a map would pull the whole
|
|
88
|
+
* `Map` machinery into every guest that reaches `__taskTick` — measured at
|
|
89
|
+
* +522 bytes of code in a guest that never parks. The field costs four bytes
|
|
90
|
+
* per state struct instead.
|
|
91
|
+
*/
|
|
92
|
+
slot: i32 = -1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- the machine's state ----------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Creates records: the default allocates the base class, and
|
|
99
|
+
* `__taskBindRecordFactory` replaces it with a capability's subclass (§3.2).
|
|
100
|
+
*/
|
|
101
|
+
let recordFactory: (() => TaskRecord) | null = null;
|
|
102
|
+
|
|
103
|
+
/** `class_ → family` (§3.1). Unbound classes are their own family. */
|
|
104
|
+
let families: Map<i32, i32> | null = null;
|
|
105
|
+
|
|
106
|
+
/** `record → the states parked on it`. The rendezvous for a task handle: an
|
|
107
|
+
* `async function`'s own record is awaited through this map alone, and `wake`
|
|
108
|
+
* is what resumes it when that task completes (0.7.0). */
|
|
109
|
+
let waiters: Map<TaskRecord, Array<TaskState>> | null = null;
|
|
110
|
+
|
|
111
|
+
/** States a completion has released, drained at the next host entry
|
|
112
|
+
* (`drainReady`). The guest-side half of the wake, for waiters on a guest
|
|
113
|
+
* task's own record — the host has no key for those. */
|
|
114
|
+
let ready: Array<TaskState> | null = null;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* §3.2's `payload` offset — the record's integer and `bool` result slot.
|
|
118
|
+
*
|
|
119
|
+
* **Derived from the declaration**, not hand-copied: `TaskRecord`
|
|
120
|
+
* (`./task.ts:41-55`) is the layout, and `offsetof` reports it as a compile-time
|
|
121
|
+
* constant (measured: 8, and 32 for the record's size). A field that moved
|
|
122
|
+
* would move this with it; a literal would not.
|
|
123
|
+
*
|
|
124
|
+
* A constant rather than `record.payload = …` at the use sites because the
|
|
125
|
+
* 64-bit path below is generic: `R` is one of `i64`/`u64`/`f64`, which lands
|
|
126
|
+
* across `f0`/`f1` — two declared `f32` fields — so that store is necessarily a
|
|
127
|
+
* raw 8-byte write at `f0`'s offset. `payload` keeps the same spelling as its
|
|
128
|
+
* sibling for symmetry, not because it needs the offset form.
|
|
129
|
+
*/
|
|
130
|
+
const RECORD_PAYLOAD: usize = offsetof<TaskRecord>("payload");
|
|
131
|
+
/** §3.2's `f0` offset — `f32` results, and the low half of the 64-bit ones. */
|
|
132
|
+
const RECORD_F0: usize = offsetof<TaskRecord>("f0");
|
|
133
|
+
|
|
134
|
+
// ── the push path (the only path, 0.7.0) ────────────────────────────────────
|
|
135
|
+
//
|
|
136
|
+
// `docs/scheduler-contract.md` §2.1-§2.2 is the specification. What stood beside
|
|
137
|
+
// it — `__taskDeliver`, `__taskTick`, `__taskReportDrops`, the early-completion
|
|
138
|
+
// buffer and the `waitingByKey` index they fed — is gone: the host publishes,
|
|
139
|
+
// matches and wakes, and it attributes drops itself (`0.7.0c`). What remains
|
|
140
|
+
// guest-side is the one rendezvous the host cannot own: two guest tasks, one
|
|
141
|
+
// awaiting the other.
|
|
142
|
+
|
|
143
|
+
/** `scheduler::park(task_id, class, key)` — one call per suspension (§2.1).
|
|
144
|
+
* `void`: a refusal comes back as a completion, never as a return value. */
|
|
145
|
+
@external("scheduler", "park")
|
|
146
|
+
declare function hostPark(taskId: i64, class_: i32, key: i32): void;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One entry of the guest's task table.
|
|
150
|
+
*
|
|
151
|
+
* **One managed field, and that is load-bearing.** `state` is a class-typed
|
|
152
|
+
* reference, so the compiler-generated visitor traces it, exactly as it traces
|
|
153
|
+
* `Task<T>._record` (`task.ts:30-45`). An opaque `usize` would not be traced,
|
|
154
|
+
* and a parked task's state struct — with its environment and its lifted
|
|
155
|
+
* locals — could be collected while the task was still parked. The record and
|
|
156
|
+
* the resume function are read *through* this reference (`state.record`,
|
|
157
|
+
* `state.resume`), so the slot needs no fields of its own.
|
|
158
|
+
*/
|
|
159
|
+
class TaskSlot {
|
|
160
|
+
state: TaskState | null = null;
|
|
161
|
+
/**
|
|
162
|
+
* The record this task parked on (0.7.0c). `__taskFail` needs it: a refused
|
|
163
|
+
* park or a lost completion resumes the task with a *status*, and the status
|
|
164
|
+
* lives on the awaited record — the one the transform's continuation checks
|
|
165
|
+
* and `rt.classify` reads — not on the task's own result record.
|
|
166
|
+
*/
|
|
167
|
+
record: TaskRecord | null = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The table: `task_id` is a slot index (§2.1). Allocated at bind time. */
|
|
171
|
+
let slots: Array<TaskSlot> | null = null;
|
|
172
|
+
/** Free slot indices, as a stack. Empty means the table is exhausted. */
|
|
173
|
+
let freeSlots: Array<i32> | null = null;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Binds the task table: the guest allocates it once at startup and the host
|
|
177
|
+
* never allocates on the guest's behalf (§2.1's "the executor maintains a task
|
|
178
|
+
* table keyed by task_id" — in this implementation the executor's table is the
|
|
179
|
+
* guest's, because the continuations live in guest memory).
|
|
180
|
+
*
|
|
181
|
+
* `capacity` is the number of concurrently parked tasks the guest will allow;
|
|
182
|
+
* a park past it is the **scheduler refusal**, `-ENOSPC` (§5.1). Idempotent: a
|
|
183
|
+
* second bind replaces the table, which discards whatever was parked — so the
|
|
184
|
+
* framework calls it once, at startup.
|
|
185
|
+
*/
|
|
186
|
+
export function __taskBindTable(capacity: i32): void {
|
|
187
|
+
if (capacity <= 0) return;
|
|
188
|
+
const table = new Array<TaskSlot>(capacity);
|
|
189
|
+
const free = new Array<i32>(capacity);
|
|
190
|
+
for (let i = 0; i < capacity; ++i) {
|
|
191
|
+
table[i] = new TaskSlot();
|
|
192
|
+
// Descending, so the free stack hands out index 0 first.
|
|
193
|
+
free[i] = capacity - 1 - i;
|
|
194
|
+
}
|
|
195
|
+
slots = table;
|
|
196
|
+
freeSlots = free;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The push half of a park: allocate a slot, record the state, tell the host.
|
|
201
|
+
*
|
|
202
|
+
* Called from `__taskPark` **after** the pull-path registration, so a guest
|
|
203
|
+
* that binds no table (or a host with no `scheduler::park`) behaves exactly as
|
|
204
|
+
* it did before this release — the function returns early and the pull path is
|
|
205
|
+
* all there is.
|
|
206
|
+
*/
|
|
207
|
+
function parkState(state: TaskState, awaited: TaskRecord): void {
|
|
208
|
+
const table = slots;
|
|
209
|
+
if (table == null) return; // no table bound: pull path only
|
|
210
|
+
// A record with no producer stamp has no rendezvous to post: a guest task's
|
|
211
|
+
// own record is resolved by the handle, not by a key.
|
|
212
|
+
if (awaited.class_ == 0) return;
|
|
213
|
+
|
|
214
|
+
// **The slot is already this task's** (0.7.0d-pre). `__taskRegister` claimed it
|
|
215
|
+
// when the task was created and `__taskComplete*` frees it; a park only records
|
|
216
|
+
// the rendezvous. That is what makes -ENOSPC a creation-time answer: by the
|
|
217
|
+
// time a task can park, its slot exists, so the refusal has an id to fail and
|
|
218
|
+
// no task can strand with nowhere to be woken.
|
|
219
|
+
const id = state.slot;
|
|
220
|
+
if (id < 0) return; // registration refused: not runnable
|
|
221
|
+
const slot = table[id];
|
|
222
|
+
slot.state = state;
|
|
223
|
+
slot.record = awaited;
|
|
224
|
+
hostPark(<i64>id, awaited.class_, awaited.key);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* `__taskFail(idsPtr, statusesPtr, count)` — the host names tasks to resume
|
|
229
|
+
* with a status and no payload (§4.4's lost completion, §5.1's refused park).
|
|
230
|
+
*
|
|
231
|
+
* `idsPtr` points at `count` packed `u64` task ids; `statusesPtr` at `count`
|
|
232
|
+
* packed `i32` statuses (a negative errno each). The status goes on the record
|
|
233
|
+
* the task *awaited*: that is the one the transform's settled check reads and
|
|
234
|
+
* the one `rt.classify` reports, so a lost stream signal returns `-EIO` rather
|
|
235
|
+
* than the `0` a bare wake would look like.
|
|
236
|
+
*
|
|
237
|
+
* This is the mechanism `scheduler::park`'s refusal already described in
|
|
238
|
+
* `scheduler.rs` — "the next `wake(ids)` carries the id" — split out so the
|
|
239
|
+
* host has one call for "resume with a status" and another for "resume with a
|
|
240
|
+
* completion", rather than a wake that sometimes means both.
|
|
241
|
+
*
|
|
242
|
+
* An id whose slot is empty is skipped, exactly as `__taskWake` skips one: a
|
|
243
|
+
* stale id must never resume anything twice.
|
|
244
|
+
*/
|
|
245
|
+
export function __taskFail(idsPtr: usize, statusesPtr: usize, count: i32): void {
|
|
246
|
+
const table = slots;
|
|
247
|
+
if (table == null || count <= 0) return;
|
|
248
|
+
for (let i = 0; i < count; ++i) {
|
|
249
|
+
const id = <i32>load<u64>(idsPtr + <usize>i * 8);
|
|
250
|
+
if (id < 0 || id >= table.length) continue;
|
|
251
|
+
const slot = table[id];
|
|
252
|
+
const state = slot.state;
|
|
253
|
+
if (state == null) continue; // already resumed: a stale id
|
|
254
|
+
const record = slot.record;
|
|
255
|
+
// Clear the park, keep the slot: the task is still live and will free its
|
|
256
|
+
// slot when its body returns (0.7.0d-pre). Freeing here would let a second
|
|
257
|
+
// task claim the id while this one is still running, and the host's next
|
|
258
|
+
// wake for that id would resume the wrong task.
|
|
259
|
+
slot.state = null;
|
|
260
|
+
slot.record = null;
|
|
261
|
+
if (record != null) {
|
|
262
|
+
// §3.4: a lost completion is a status, not an exception. The payload
|
|
263
|
+
// stays 0 — nothing was delivered — and the guest reads the capability's
|
|
264
|
+
// table to learn what really happened.
|
|
265
|
+
record.status = <i32>load<i32>(statusesPtr + <usize>i * 4);
|
|
266
|
+
record.done = 1;
|
|
267
|
+
}
|
|
268
|
+
state.resume(state);
|
|
269
|
+
}
|
|
270
|
+
drainReady();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* `__taskResume(idsPtr, classesPtr, payloadsPtr, count)` — resume tasks with the
|
|
275
|
+
* completion that woke them (0.7.0d-pre).
|
|
276
|
+
*
|
|
277
|
+
* `__taskWake` names ids and nothing else, which was enough while the guest's
|
|
278
|
+
* own `__taskDeliver` was still filling records from the ring. Under the push
|
|
279
|
+
* path the host is the only thing that knows what arrived, so the record has to
|
|
280
|
+
* come with the wake: `classesPtr` points at `count` packed `i32` class ids and
|
|
281
|
+
* `payloadsPtr` at `count` packed `i32` payload words, both written by the host.
|
|
282
|
+
*
|
|
283
|
+
* Three parallel arrays rather than one packed record, so a chunk costs no more
|
|
284
|
+
* scratch than the ids already do.
|
|
285
|
+
*
|
|
286
|
+
* An id whose slot is empty is skipped: a stale id must never resume a task
|
|
287
|
+
* twice (§1.2).
|
|
288
|
+
*/
|
|
289
|
+
export function __taskResume(
|
|
290
|
+
idsPtr: usize,
|
|
291
|
+
classesPtr: usize,
|
|
292
|
+
payloadsPtr: usize,
|
|
293
|
+
count: i32
|
|
294
|
+
): void {
|
|
295
|
+
const table = slots;
|
|
296
|
+
if (table == null || count <= 0) return;
|
|
297
|
+
for (let i = 0; i < count; ++i) {
|
|
298
|
+
const id = <i32>load<u64>(idsPtr + <usize>i * 8);
|
|
299
|
+
if (id < 0 || id >= table.length) continue;
|
|
300
|
+
const slot = table[id];
|
|
301
|
+
const state = slot.state;
|
|
302
|
+
if (state == null) continue; // already resumed: a stale id
|
|
303
|
+
const record = slot.record;
|
|
304
|
+
// Clear the park, keep the slot (0.7.0d-pre).
|
|
305
|
+
slot.state = null;
|
|
306
|
+
slot.record = null;
|
|
307
|
+
if (record != null) {
|
|
308
|
+
record.class_ = <i32>load<i32>(classesPtr + <usize>i * 4);
|
|
309
|
+
record.payload = <i32>load<i32>(payloadsPtr + <usize>i * 4);
|
|
310
|
+
record.status = 0;
|
|
311
|
+
record.done = 1;
|
|
312
|
+
}
|
|
313
|
+
state.resume(state);
|
|
314
|
+
}
|
|
315
|
+
drainReady();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Release a slot: the task it held has resumed, and the id may be reused. */
|
|
319
|
+
function releaseSlot(id: i32): void {
|
|
320
|
+
const table = slots;
|
|
321
|
+
const free = freeSlots;
|
|
322
|
+
if (table == null || free == null) return;
|
|
323
|
+
if (id < 0 || id >= table.length) return;
|
|
324
|
+
table[id].state = null;
|
|
325
|
+
table[id].record = null;
|
|
326
|
+
free.push(id);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Release whatever slot `state` holds, if it holds one. */
|
|
330
|
+
function releaseSlotOf(state: TaskState): void {
|
|
331
|
+
const id = state.slot;
|
|
332
|
+
if (id < 0) return;
|
|
333
|
+
state.slot = -1;
|
|
334
|
+
releaseSlot(id);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* `__taskWake(idsPtr, count)` — the host names the tasks to resume (§2.2).
|
|
339
|
+
*
|
|
340
|
+
* `idsPtr` points at `count` packed little-endian `u64` task ids in guest
|
|
341
|
+
* memory; the host wrote them before the call. Each id is resolved through the
|
|
342
|
+
* table and its continuation invoked **exactly once** (§1.2, §4.1), then the
|
|
343
|
+
* slot is freed so the id can be reused.
|
|
344
|
+
*
|
|
345
|
+
* An id whose slot is empty is skipped, not an error: a stale id can only
|
|
346
|
+
* arrive from a host index that outlived its park (see `scheduler.rs`'s note on
|
|
347
|
+
* overwrite), and waking nothing is the correct behaviour — resuming twice
|
|
348
|
+
* never is.
|
|
349
|
+
*/
|
|
350
|
+
export function __taskWake(idsPtr: usize, count: i32): void {
|
|
351
|
+
const table = slots;
|
|
352
|
+
if (table == null || count <= 0) return;
|
|
353
|
+
for (let i = 0; i < count; ++i) {
|
|
354
|
+
const id = <i32>load<u64>(idsPtr + <usize>i * 8);
|
|
355
|
+
if (id < 0 || id >= table.length) continue;
|
|
356
|
+
const slot = table[id];
|
|
357
|
+
const state = slot.state;
|
|
358
|
+
if (state == null) continue; // already resumed: a stale id
|
|
359
|
+
// Clear the park, keep the slot (0.7.0d-pre): the task is live until its
|
|
360
|
+
// body returns, and `__taskComplete*` frees the id then.
|
|
361
|
+
slot.state = null;
|
|
362
|
+
slot.record = null;
|
|
363
|
+
state.resume(state);
|
|
364
|
+
}
|
|
365
|
+
drainReady();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** `(family << 32) | (a & 0xffffffff)`, the correlation key (§3.1). */
|
|
369
|
+
function keyOf(family: i32, a: i32): i64 {
|
|
370
|
+
return (<i64>family << 32) | <i64>(<u32>a);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The family a class belongs to; an unbound class is its own family (§3.1). */
|
|
374
|
+
function familyOf(class_: i32): i32 {
|
|
375
|
+
let map = families;
|
|
376
|
+
if (map != null && map.has(class_)) {
|
|
377
|
+
return map.get(class_);
|
|
378
|
+
}
|
|
379
|
+
return class_;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Creates the task record for `state`, roots it, and answers it. */
|
|
383
|
+
export function __taskRegister(state: TaskState): TaskRecord {
|
|
384
|
+
let make = recordFactory;
|
|
385
|
+
let record = make != null ? make() : new TaskRecord();
|
|
386
|
+
state.record = record;
|
|
387
|
+
|
|
388
|
+
// **The slot is claimed here, at creation** (0.7.0d-pre): the table's capacity
|
|
389
|
+
// is a bound on *live* tasks, not on parked ones, and the id is the task's
|
|
390
|
+
// handle from creation to completion. §5.1's refusal therefore has a moment
|
|
391
|
+
// where it can be answered: a task that cannot get an id is settled with
|
|
392
|
+
// -ENOSPC before its body runs, and its caller's `await` falls through on the
|
|
393
|
+
// settled record instead of suspending forever.
|
|
394
|
+
const table = slots;
|
|
395
|
+
const free = freeSlots;
|
|
396
|
+
if (table == null || free == null) return record; // no table bound: pull path
|
|
397
|
+
if (free.length == 0) {
|
|
398
|
+
record.status = -28; // -ENOSPC
|
|
399
|
+
record.done = 1;
|
|
400
|
+
state.slot = -1;
|
|
401
|
+
return record;
|
|
402
|
+
}
|
|
403
|
+
const id = free.pop();
|
|
404
|
+
table[id].state = state;
|
|
405
|
+
table[id].record = null;
|
|
406
|
+
state.slot = id;
|
|
407
|
+
return record;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Whether this task got a slot — the entry's guard (0.7.0d-pre). A task whose
|
|
411
|
+
* registration was refused must not run its body: its record is already settled
|
|
412
|
+
* with the refusal, and the caller's await has already been answered. */
|
|
413
|
+
export function __taskRegistered(state: TaskState): bool {
|
|
414
|
+
// "May run", not "has a slot" (0.7.0d-pre). A guest that binds no table is on
|
|
415
|
+
// the pull path: `__taskRegister` is a no-op there and every task runs. Only a
|
|
416
|
+
// *refused* registration — a table is bound and no slot was free — is false.
|
|
417
|
+
return state.slot >= 0 || slots == null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Frees a task's slot when its body returns. Idempotent. */
|
|
421
|
+
function finishTask(state: TaskState): void {
|
|
422
|
+
const id = state.slot;
|
|
423
|
+
if (id < 0) return;
|
|
424
|
+
state.slot = -1;
|
|
425
|
+
releaseSlot(id);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Whether `record`'s task is already settled — §1.2 rule 4's check, so a
|
|
430
|
+
* settled `await` returns immediately and does not suspend.
|
|
431
|
+
*
|
|
432
|
+
* `done`, not `status`: §3.2 defines `done` as the settled word (0 = parked,
|
|
433
|
+
* 1 = settled) and `status` as the outcome (0 = success, otherwise the errno).
|
|
434
|
+
* Reading `status` would make every *successful* completion look unsettled.
|
|
435
|
+
*/
|
|
436
|
+
export function __taskSettled(record: TaskRecord): bool {
|
|
437
|
+
return record.done != 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Parks `state` on `awaited` until the runtime delivers a completion for it
|
|
442
|
+
* (§3.1, §6.5). Never traps: a task that parks and is never delivered stays
|
|
443
|
+
* parked, which is exactly what the machine must do when no producer exists
|
|
444
|
+
* yet.
|
|
445
|
+
*
|
|
446
|
+
* Called at every suspension, *after* `state.resumePoint` has been set, so a
|
|
447
|
+
* delivery that arrives before the park would still find a valid resume point.
|
|
448
|
+
*/
|
|
449
|
+
export function __taskPark(state: TaskState, awaited: TaskRecord): void {
|
|
450
|
+
// The push half is the whole park (0.7.0). `parkState` records the state on
|
|
451
|
+
// the slot the task has held since creation and tells the host, which owns the
|
|
452
|
+
// rendezvous. A record with no producer stamp (`class_ == 0`) is not the host's
|
|
453
|
+
// business — its rendezvous is the handle — so `parkState` returns early and
|
|
454
|
+
// `wake` resumes it when the owning task completes.
|
|
455
|
+
//
|
|
456
|
+
// What the retirement removed: the guest-side early-completion buffer and the
|
|
457
|
+
// `waitingByKey` registration that fed `__taskDeliver`/`__taskTick`. Early
|
|
458
|
+
// completions are the host's now (`scheduler::RecentCompletions`), delivered
|
|
459
|
+
// through `__taskResume`; the pull delivery they existed for is gone.
|
|
460
|
+
let byRecord = waiters;
|
|
461
|
+
if (byRecord == null) {
|
|
462
|
+
byRecord = new Map();
|
|
463
|
+
waiters = byRecord;
|
|
464
|
+
}
|
|
465
|
+
let list: Array<TaskState> | null = null;
|
|
466
|
+
if (byRecord.has(awaited)) list = byRecord.get(awaited);
|
|
467
|
+
if (list == null) {
|
|
468
|
+
list = new Array<TaskState>();
|
|
469
|
+
byRecord.set(awaited, list);
|
|
470
|
+
}
|
|
471
|
+
list.push(state);
|
|
472
|
+
parkState(state, awaited);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Move the states parked on `record` to the ready list.
|
|
477
|
+
*
|
|
478
|
+
* This is the one rendezvous the host cannot own: `record` is a **guest task's
|
|
479
|
+
* own record** (class 0), so there is no producer key to match and the host was
|
|
480
|
+
* never told. The list is cleared so a second completion for the same record (a
|
|
481
|
+
* re-used slot) does not wake the same task twice.
|
|
482
|
+
*
|
|
483
|
+
* Since 0.7.0 there is no `slot >= 0` filter here. That guard read "the host
|
|
484
|
+
* owns this one", which was true when a slot was allocated at park; pre-park
|
|
485
|
+
* registration gives every task a slot at creation, so the filter dropped every
|
|
486
|
+
* waiter and a task awaiting a *suspending guest task* was resumed by nobody —
|
|
487
|
+
* measured as `guest-chain` hanging.
|
|
488
|
+
*/
|
|
489
|
+
function wake(record: TaskRecord): void {
|
|
490
|
+
let byRecord = waiters;
|
|
491
|
+
if (byRecord == null) return;
|
|
492
|
+
let list: Array<TaskState> | null = null;
|
|
493
|
+
if (byRecord.has(record)) list = byRecord.get(record);
|
|
494
|
+
if (list != null && list.length != 0) {
|
|
495
|
+
let queue = ready;
|
|
496
|
+
if (queue == null) {
|
|
497
|
+
queue = new Array<TaskState>();
|
|
498
|
+
ready = queue;
|
|
499
|
+
}
|
|
500
|
+
for (let i = 0, k = list.length; i < k; ++i) queue.push(list[i]);
|
|
501
|
+
list.length = 0;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Resume the states queued by `wake` — at the executor's entry, never from
|
|
507
|
+
* inside a completing task's own body.
|
|
508
|
+
*
|
|
509
|
+
* Under the pull path `__taskTick` drained this from the guest's frame loop; a
|
|
510
|
+
* host-driven guest never ticks, so the drain moved to the host's own entry
|
|
511
|
+
* points — `__taskWake`, `__taskResume` and `__taskFail` all end here. That keeps
|
|
512
|
+
* every resume at the top of a host call, which is the property the list existed
|
|
513
|
+
* for.
|
|
514
|
+
*
|
|
515
|
+
* Loops until empty: resuming one state can complete another task, whose own
|
|
516
|
+
* waiters are then queued. Each entry is resumed once and the list is emptied
|
|
517
|
+
* before the first resume, so this terminates.
|
|
518
|
+
*/
|
|
519
|
+
function drainReady(): void {
|
|
520
|
+
let guard: i32 = 0;
|
|
521
|
+
let pending = ready;
|
|
522
|
+
while (pending != null && pending.length != 0 && guard < 1024) {
|
|
523
|
+
let queue = pending;
|
|
524
|
+
ready = null;
|
|
525
|
+
for (let i = 0, k = queue.length; i < k; ++i) {
|
|
526
|
+
let state = queue[i];
|
|
527
|
+
state.resume(state);
|
|
528
|
+
}
|
|
529
|
+
pending = ready;
|
|
530
|
+
guard += 1;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* The zero of `T`, for the state struct's lifted-local initializers.
|
|
536
|
+
*
|
|
537
|
+
* `changetype<T>(0)` is the only spelling the lowering can emit without knowing
|
|
538
|
+
* whether `T` is a float, an integer or a reference — and it does not compile
|
|
539
|
+
* for a float (measured: AS202, an i32 literal changed to f64). The branches
|
|
540
|
+
* here are compile-time constants per instantiation, so exactly one line of
|
|
541
|
+
* each is emitted.
|
|
542
|
+
*/
|
|
543
|
+
export function __asyncZero<T>(): T {
|
|
544
|
+
if (isFloat<T>()) return <T>0.0;
|
|
545
|
+
if (isInteger<T>()) return <T>0;
|
|
546
|
+
return changetype<T>(0);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Completes a task from inside the guest: `done`, `status = 0`, and the states
|
|
551
|
+
* parked on its record are queued for resume (§4.7.3, Phase 4b).
|
|
552
|
+
*
|
|
553
|
+
* The value half is {@link __taskCompleteResult}; this is what a `void` body's
|
|
554
|
+
* terminal calls. Split because `T` has to be named at the call site.
|
|
555
|
+
*/
|
|
556
|
+
export function __taskComplete(state: TaskState): void {
|
|
557
|
+
let record = state.record;
|
|
558
|
+
record.status = 0;
|
|
559
|
+
record.done = 1;
|
|
560
|
+
finishTask(state);
|
|
561
|
+
wake(record);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Completes a task with a value: stores it into the record at §3.2's offset for
|
|
566
|
+
* `R`, then {@link __taskComplete}.
|
|
567
|
+
*
|
|
568
|
+
* The offset mapping is codegen's read, mirrored exactly (`compiler.ts:8025-8027`):
|
|
569
|
+
* `f32` and the 64-bit kinds live at `f0` (16), everything narrower at `payload`
|
|
570
|
+
* (8). It is decided by `sizeof`/`isFloat` rather than by a name here, so a
|
|
571
|
+
* type alias resolves the same way it does at the read — the reason this is
|
|
572
|
+
* generic rather than a store the transform emits itself.
|
|
573
|
+
*/
|
|
574
|
+
export function __taskCompleteResult<R>(state: TaskState, value: R): void {
|
|
575
|
+
let record = state.record;
|
|
576
|
+
let base = changetype<usize>(record);
|
|
577
|
+
if (sizeof<R>() == 4 && isFloat<R>()) {
|
|
578
|
+
store<f32>(base + RECORD_F0, <f32>value);
|
|
579
|
+
} else if (sizeof<R>() > 4) {
|
|
580
|
+
store<R>(base + RECORD_F0, value);
|
|
581
|
+
} else {
|
|
582
|
+
store<R>(base + RECORD_PAYLOAD, value);
|
|
583
|
+
}
|
|
584
|
+
__taskComplete(state);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Whether any task is ready to resume — the frame loop's `wait(0)` test (§6.3). */
|
|
588
|
+
export function __taskHasRunnable(): bool {
|
|
589
|
+
let queue = ready;
|
|
590
|
+
return queue != null && queue.length != 0;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The framework's startup hook: the record subclass `__taskRegister` allocates. */
|
|
594
|
+
export function __taskBindRecordFactory(factory: () => TaskRecord): void {
|
|
595
|
+
recordFactory = factory;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* The framework's startup hook: which completion classes resolve one await
|
|
600
|
+
* (§3.1). `JOB_DONE`/`JOB_FAILED` are one family; an unbound class is its own.
|
|
601
|
+
*/
|
|
602
|
+
export function __taskBindFamily(class_: i32, family: i32): void {
|
|
603
|
+
let map = families;
|
|
604
|
+
if (map == null) {
|
|
605
|
+
map = new Map();
|
|
606
|
+
families = map;
|
|
607
|
+
}
|
|
608
|
+
map.set(class_, family);
|
|
609
|
+
}
|