rooklang 0.3.1 → 0.3.3
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/README.md +17 -4
- package/README.ru.md +17 -4
- package/dist/ast/expressions.d.ts +16 -1
- package/dist/ast/markup.d.ts +6 -1
- package/dist/browser.d.ts +5 -0
- package/dist/browser.js +1 -0
- package/dist/checker/assignments.d.ts +5 -0
- package/dist/checker/assignments.js +34 -8
- package/dist/checker/base.d.ts +2 -1
- package/dist/checker/base.js +4 -10
- package/dist/checker/builtins.js +4 -35
- package/dist/checker/bytes.d.ts +2 -2
- package/dist/checker/bytes.js +2 -2
- package/dist/checker/channels.d.ts +3 -0
- package/dist/checker/channels.js +39 -1
- package/dist/checker/context.d.ts +6 -0
- package/dist/checker/control-flow.d.ts +1 -0
- package/dist/checker/control-flow.js +17 -0
- package/dist/checker/error-results.d.ts +6 -0
- package/dist/checker/error-results.js +31 -0
- package/dist/checker/expressions.d.ts +2 -0
- package/dist/checker/expressions.js +4 -0
- package/dist/checker/helpers.js +2 -0
- package/dist/checker/queues.d.ts +6 -0
- package/dist/checker/queues.js +24 -0
- package/dist/checker/resolution.js +6 -2
- package/dist/checker/statements.d.ts +5 -0
- package/dist/checker/statements.js +19 -0
- package/dist/checker/transfer.js +4 -1
- package/dist/checker/types/printing.js +7 -0
- package/dist/checker/types/queue.d.ts +6 -0
- package/dist/checker/types/queue.js +42 -0
- package/dist/checker/types/relations.js +12 -1
- package/dist/checker/types.d.ts +1 -0
- package/dist/checker/types.js +1 -0
- package/dist/checker/untyped-globals.d.ts +1 -0
- package/dist/checker/untyped-globals.js +47 -0
- package/dist/codegen/attributes.js +0 -1
- package/dist/codegen/calls.d.ts +9 -0
- package/dist/codegen/calls.js +41 -1
- package/dist/codegen/component-values.js +0 -1
- package/dist/codegen/components.js +0 -1
- package/dist/codegen/control-flow.js +0 -3
- package/dist/codegen/control-statements.d.ts +9 -1
- package/dist/codegen/control-statements.js +51 -7
- package/dist/codegen/creation.d.ts +6 -8
- package/dist/codegen/creation.js +1 -10
- package/dist/codegen/early-returns.js +0 -1
- package/dist/codegen/elements.js +0 -3
- package/dist/codegen/emitter.d.ts +3 -5
- package/dist/codegen/emitter.js +0 -2
- package/dist/codegen/expressions.d.ts +4 -1
- package/dist/codegen/expressions.js +9 -13
- package/dist/codegen/functions.js +0 -2
- package/dist/codegen/js.d.ts +0 -7
- package/dist/codegen/js.js +33 -59
- package/dist/codegen/parallel.js +2 -4
- package/dist/codegen/refs.js +0 -1
- package/dist/codegen/runtime.d.ts +13 -0
- package/dist/codegen/runtime.js +93 -0
- package/dist/codegen/simple-statements.js +3 -1
- package/dist/codegen/styles.d.ts +1 -1
- package/dist/codegen/styles.js +2 -2
- package/dist/codegen/web-components.js +0 -1
- package/dist/compile.d.ts +7 -0
- package/dist/compile.js +1 -0
- package/dist/completion.js +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/loader.js +19 -6
- package/dist/parser/base.d.ts +3 -1
- package/dist/parser/expressions.js +9 -2
- package/dist/parser/literals.d.ts +7 -0
- package/dist/parser/literals.js +45 -1
- package/dist/parser/simple-statements.js +3 -0
- package/dist/parser/types.d.ts +1 -1
- package/dist/parser/types.js +10 -3
- package/dist/runtime/sources.d.ts +2 -0
- package/dist/runtime/sources.js +14 -0
- package/dist/runtime-module.d.ts +1 -0
- package/dist/runtime-module.js +3 -0
- package/dist/ts-types.d.ts +4 -0
- package/dist/ts-types.js +25 -3
- package/dist/vite.js +7 -1
- package/dist/walk-types.js +1 -0
- package/dist/walk.js +9 -0
- package/package.json +6 -4
- package/src/runtime/chan.js +277 -0
- package/src/runtime/core.js +23 -0
- package/src/runtime/go.js +340 -0
- package/src/runtime/index.js +13 -0
- package/src/runtime/markup.js +336 -0
- package/src/runtime/queue.js +118 -0
- package/src/runtime/select.js +170 -0
- package/src/runtime/streams.js +61 -0
- package/src/runtime/threads.js +240 -0
- package/src/runtime/timers.js +27 -0
- package/dist/codegen/chan-helpers.d.ts +0 -2
- package/dist/codegen/chan-helpers.js +0 -253
- package/dist/codegen/go-helpers.d.ts +0 -9
- package/dist/codegen/go-helpers.js +0 -307
- package/dist/codegen/helpers.d.ts +0 -10
- package/dist/codegen/helpers.js +0 -356
- package/dist/codegen/select-helpers.d.ts +0 -2
- package/dist/codegen/select-helpers.js +0 -135
- package/dist/codegen/thread-helpers.d.ts +0 -2
- package/dist/codegen/thread-helpers.js +0 -198
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Channels: sending, receiving and closing, here or in the thread that made the channel
|
|
2
|
+
// (threads.js), and the queues of a channel. Receiving from a closed channel gives
|
|
3
|
+
// the zero value of its type, as in Go.
|
|
4
|
+
|
|
5
|
+
import { $$Queue } from './queue.js';
|
|
6
|
+
import { $$commit, $$lock, $$select, $$try, $$unlock, $$unwait } from './select.js';
|
|
7
|
+
import { $$adopt, $$ask, $$ownerOf, $$post, $$registry, $$stopped } from './threads.js';
|
|
8
|
+
|
|
9
|
+
$$registry().handle ??= { message: $$message, pump: $$pump };
|
|
10
|
+
|
|
11
|
+
export function $$chan(size, zero) {
|
|
12
|
+
const registry = $$registry();
|
|
13
|
+
const id = `${registry.key}:${registry.channels++}`;
|
|
14
|
+
const channel = { $$chan: id, size, zero };
|
|
15
|
+
|
|
16
|
+
registry.states.set(id, {
|
|
17
|
+
size,
|
|
18
|
+
zero,
|
|
19
|
+
buffer: new $$Queue(),
|
|
20
|
+
receivers: new $$Queue(),
|
|
21
|
+
senders: new $$Queue(),
|
|
22
|
+
closed: false,
|
|
23
|
+
holders: 1,
|
|
24
|
+
busy: false,
|
|
25
|
+
});
|
|
26
|
+
$$adopt(channel);
|
|
27
|
+
|
|
28
|
+
return channel;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A local operation waits in the queue of its channel; one of another thread asks the thread
|
|
32
|
+
// that made the channel.
|
|
33
|
+
export function $$send(channel, value) {
|
|
34
|
+
const id = channel.$$chan;
|
|
35
|
+
const owner = $$ownerOf(id);
|
|
36
|
+
|
|
37
|
+
if (owner !== $$registry().key) return $$ask(owner, { op: 'send', channel: id, value });
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) =>
|
|
40
|
+
$$wait(id, 'senders', { value, done: resolve, fail: reject }),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function $$receive(channel, withOk = false) {
|
|
45
|
+
const id = channel.$$chan;
|
|
46
|
+
const owner = $$ownerOf(id);
|
|
47
|
+
const received =
|
|
48
|
+
owner !== $$registry().key
|
|
49
|
+
? $$ask(owner, { op: 'receive', channel: id })
|
|
50
|
+
: new Promise((resolve, reject) => $$wait(id, 'receivers', { done: resolve, fail: reject }));
|
|
51
|
+
|
|
52
|
+
return withOk ? received : received.then(([value]) => value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Closing a channel of another thread fails this one if it was closed already, as a panic would.
|
|
56
|
+
export function $$close(channel) {
|
|
57
|
+
const id = channel.$$chan;
|
|
58
|
+
const owner = $$ownerOf(id);
|
|
59
|
+
|
|
60
|
+
if (owner === $$registry().key) return $$closeHere(id);
|
|
61
|
+
$$ask(owner, { op: 'close', channel: id }).catch((error) => {
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function $$closeHere(id) {
|
|
69
|
+
const state = $$registry().states.get(id);
|
|
70
|
+
|
|
71
|
+
if (state.closed) throw new Error('close of a closed channel');
|
|
72
|
+
state.closed = true;
|
|
73
|
+
$$pump(id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// What a closed channel gives: a value of its own for each receiver, if it is an array or a queue.
|
|
77
|
+
export function $$zero({ zero }) {
|
|
78
|
+
return Array.isArray(zero) ? [] : zero instanceof $$Queue ? new $$Queue() : zero;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function* $$range(channel) {
|
|
82
|
+
while (true) {
|
|
83
|
+
const [value, ok] = await $$receive(channel, true);
|
|
84
|
+
|
|
85
|
+
if (!ok) return;
|
|
86
|
+
yield value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function $$wait(id, queue, entry) {
|
|
91
|
+
const state = $$registry().states.get(id);
|
|
92
|
+
|
|
93
|
+
if (!state) return entry.fail?.($$stopped());
|
|
94
|
+
state[queue].push(entry);
|
|
95
|
+
$$pump(id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The operations of other threads on the channels of this one.
|
|
99
|
+
export function $$message(from, data) {
|
|
100
|
+
const answer = (value) => $$post(from, { answer: data.ask, value });
|
|
101
|
+
const fail = (error) => $$post(from, { answer: data.ask, error });
|
|
102
|
+
const select = { key: from, sid: data.sid };
|
|
103
|
+
|
|
104
|
+
switch (data.op) {
|
|
105
|
+
case 'send':
|
|
106
|
+
return $$wait(data.channel, 'senders', { from, value: data.value, done: answer, fail });
|
|
107
|
+
|
|
108
|
+
case 'receive':
|
|
109
|
+
return $$wait(data.channel, 'receivers', { from, done: answer, fail });
|
|
110
|
+
|
|
111
|
+
case 'close':
|
|
112
|
+
try {
|
|
113
|
+
$$closeHere(data.channel);
|
|
114
|
+
|
|
115
|
+
return answer();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return fail(error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'try':
|
|
121
|
+
try {
|
|
122
|
+
return answer($$try(data.channel, data.send, data.value));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return fail(error);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case 'select':
|
|
128
|
+
return $$select(data.cases, data.hasDefault).then(answer, fail);
|
|
129
|
+
|
|
130
|
+
case 'wait':
|
|
131
|
+
return $$wait(data.channel, data.send ? 'senders' : 'receivers', {
|
|
132
|
+
select,
|
|
133
|
+
case: data.case,
|
|
134
|
+
value: data.value,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
case 'cancel':
|
|
138
|
+
return $$unwait(select, data.channels);
|
|
139
|
+
|
|
140
|
+
case 'lock':
|
|
141
|
+
return answer($$lock(data.sid));
|
|
142
|
+
|
|
143
|
+
case 'unlock':
|
|
144
|
+
return $$unlock(data.sid);
|
|
145
|
+
|
|
146
|
+
case 'commit':
|
|
147
|
+
return $$commit(data.sid, data.case, data.result, data.error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Matches what waits in a channel: a receiver with a value of the buffer, a sender or the zero value
|
|
152
|
+
// after close; a sender with room in the buffer. A case of a select goes on only if its select
|
|
153
|
+
// has not gone on with another case, which the thread of the select tells.
|
|
154
|
+
export function $$pump(id) {
|
|
155
|
+
const state = $$registry().states.get(id);
|
|
156
|
+
|
|
157
|
+
while (state && !state.busy) {
|
|
158
|
+
const receiver = state.receivers.at(0);
|
|
159
|
+
const sender = state.senders.at(0);
|
|
160
|
+
|
|
161
|
+
if (receiver && (state.buffer.length > 0 || state.closed)) {
|
|
162
|
+
if (!$$pair(id, state, receiver, null)) return;
|
|
163
|
+
} else if (receiver && sender) {
|
|
164
|
+
if (!$$pair(id, state, receiver, sender)) return;
|
|
165
|
+
} else if (sender && (state.closed || state.buffer.length < state.size)) {
|
|
166
|
+
if (!$$pair(id, state, null, sender)) return;
|
|
167
|
+
} else {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Whether the pump can go on at once: a pair whose selects are all in this thread is decided here.
|
|
174
|
+
export function $$pair(id, state, receiver, sender) {
|
|
175
|
+
const entries = [receiver, sender].filter((entry) => entry !== null);
|
|
176
|
+
const key = $$registry().key;
|
|
177
|
+
|
|
178
|
+
if (entries.some((entry) => entry.select && entry.select.key !== key)) {
|
|
179
|
+
$$pairLater(id, state, receiver, sender);
|
|
180
|
+
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const locks = entries.map((entry) => (entry.select ? $$lock(entry.select.sid) : 'locked'));
|
|
185
|
+
|
|
186
|
+
if (locks.every((lock) => lock === 'locked')) {
|
|
187
|
+
$$complete(state, receiver, sender);
|
|
188
|
+
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));
|
|
193
|
+
|
|
194
|
+
return $$settle(id, state, entries, locks);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A pair with a select of another thread: the channel waits for the answers of that thread.
|
|
198
|
+
export async function $$pairLater(id, state, receiver, sender) {
|
|
199
|
+
state.busy = true;
|
|
200
|
+
|
|
201
|
+
const entries = [receiver, sender].filter((entry) => entry !== null);
|
|
202
|
+
const locks = [];
|
|
203
|
+
|
|
204
|
+
for (const { select } of entries) {
|
|
205
|
+
const lock = !select
|
|
206
|
+
? 'locked'
|
|
207
|
+
: select.key === $$registry().key
|
|
208
|
+
? $$lock(select.sid)
|
|
209
|
+
: await $$ask(select.key, { op: 'lock', sid: select.sid }).catch(() => 'done');
|
|
210
|
+
|
|
211
|
+
locks.push(lock);
|
|
212
|
+
if (lock !== 'locked') break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
state.busy = false;
|
|
216
|
+
if (locks.length === entries.length && locks.every((lock) => lock === 'locked')) {
|
|
217
|
+
$$complete(state, receiver, sender);
|
|
218
|
+
} else {
|
|
219
|
+
entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));
|
|
220
|
+
if (!$$settle(id, state, entries, locks)) return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
$$pump(id);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// After a pair that could not go on: a select that has gone on leaves the queue; one that another
|
|
227
|
+
// channel is deciding is tried again a little later.
|
|
228
|
+
export function $$settle(id, state, entries, locks) {
|
|
229
|
+
entries.forEach((entry, i) => {
|
|
230
|
+
if (locks[i] === 'done') $$remove(state, entry);
|
|
231
|
+
});
|
|
232
|
+
if (locks.includes('done')) return true;
|
|
233
|
+
setTimeout(() => $$pump(id), 1 + Math.random() * 4);
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function $$remove(state, entry) {
|
|
239
|
+
state.receivers.$$delete(entry);
|
|
240
|
+
state.senders.$$delete(entry);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Takes the pair off the queues and passes the value.
|
|
244
|
+
export function $$complete(state, receiver, sender) {
|
|
245
|
+
if (receiver) state.receivers.$$delete(receiver);
|
|
246
|
+
if (sender) state.senders.$$delete(sender);
|
|
247
|
+
if (receiver) {
|
|
248
|
+
const result =
|
|
249
|
+
state.buffer.length > 0
|
|
250
|
+
? [state.buffer.shift(), true]
|
|
251
|
+
: sender
|
|
252
|
+
? [sender.value, true]
|
|
253
|
+
: [$$zero(state), false];
|
|
254
|
+
|
|
255
|
+
$$finish(receiver, result);
|
|
256
|
+
if (sender) $$finish(sender);
|
|
257
|
+
} else if (state.closed) {
|
|
258
|
+
$$finish(sender, undefined, new Error('send on a closed channel'));
|
|
259
|
+
} else {
|
|
260
|
+
state.buffer.push(sender.value);
|
|
261
|
+
$$finish(sender);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function $$finish(entry, result, error) {
|
|
266
|
+
const { select } = entry;
|
|
267
|
+
|
|
268
|
+
if (!select) return error ? entry.fail(error) : entry.done(result);
|
|
269
|
+
if (select.key === $$registry().key) return $$commit(select.sid, entry.case, result, error);
|
|
270
|
+
$$post(select.key, { op: 'commit', sid: select.sid, case: entry.case, result, error });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function $$unlockAt(select) {
|
|
274
|
+
if (!select) return;
|
|
275
|
+
if (select.key === $$registry().key) $$unlock(select.sid);
|
|
276
|
+
else $$post(select.key, { op: 'unlock', sid: select.sid });
|
|
277
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// What the code of any module may need: `for` over a value of unknown type, and `defer`.
|
|
2
|
+
|
|
3
|
+
// `for v in x` over an untyped value: a Map gives its values, as a typed one does.
|
|
4
|
+
export function $$values(iterable) {
|
|
5
|
+
if (typeof iterable === 'function') return iterable();
|
|
6
|
+
|
|
7
|
+
return iterable instanceof Map ? iterable.values() : iterable;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Runs deferred calls in reverse order; every call runs even if an earlier one throws.
|
|
11
|
+
export function $$runDeferred(deferred) {
|
|
12
|
+
let failure;
|
|
13
|
+
|
|
14
|
+
for (let i = deferred.length - 1; i >= 0; i--) {
|
|
15
|
+
try {
|
|
16
|
+
deferred[i]();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
failure = { error };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (failure) throw failure.error;
|
|
23
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// `go`: the pool of workers of the program, and the side of the worker. They work
|
|
2
|
+
// with Web Workers and with the worker threads of Node, whose API differs: `on("message")` instead
|
|
3
|
+
// of `onmessage`, and `unref()`, so that idle workers do not keep the program running.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
$$adopt,
|
|
7
|
+
$$connect,
|
|
8
|
+
$$copy,
|
|
9
|
+
$$gone,
|
|
10
|
+
$$incoming,
|
|
11
|
+
$$post,
|
|
12
|
+
$$registry,
|
|
13
|
+
$$transfer,
|
|
14
|
+
} from './threads.js';
|
|
15
|
+
|
|
16
|
+
// `go` in a module: a call in a worker of the pool. `start` starts a worker of the module, with its
|
|
17
|
+
// worker module: it is $$startWorker of the module, whose URL a bundler needs to see there.
|
|
18
|
+
export function $$goWorker(start, name, args) {
|
|
19
|
+
return (globalThis[Symbol.for('rook.goroutines')] ??= $$pool()).call(
|
|
20
|
+
start,
|
|
21
|
+
name,
|
|
22
|
+
$$copy(args, true),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The workers of the program, shared by its modules: up to one per processor. Each runs the worker
|
|
27
|
+
// module of one module, `start`. Calls wait in one queue and go to a worker of their module that is
|
|
28
|
+
// free: one whose thread is not running code, though its earlier calls may wait in `await`. A
|
|
29
|
+
// worker that is still busy keeps its calls for later; an idle worker of another module makes room
|
|
30
|
+
// for a new one, unless it keeps channels.
|
|
31
|
+
export function $$pool() {
|
|
32
|
+
const size = globalThis.navigator?.hardwareConcurrency ?? 4;
|
|
33
|
+
const registry = $$registry();
|
|
34
|
+
const workers = [];
|
|
35
|
+
const queue = [];
|
|
36
|
+
const calls = new Map();
|
|
37
|
+
let ids = 0;
|
|
38
|
+
// A worker without calls lets the program end, unless something here waits for it.
|
|
39
|
+
const unref = (worker) => {
|
|
40
|
+
if (worker.calls === 0 && !(registry.pins.get(worker.key) > 0)) worker.thread.unref?.();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
registry.idle = (key) => {
|
|
44
|
+
const worker = workers.find((each) => each.key === key);
|
|
45
|
+
|
|
46
|
+
if (worker) unref(worker);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const send = (worker, call) => {
|
|
50
|
+
worker.busy = true;
|
|
51
|
+
worker.calls++;
|
|
52
|
+
worker.thread.ref?.();
|
|
53
|
+
call.worker = worker;
|
|
54
|
+
calls.set(call.id, call);
|
|
55
|
+
$$post(worker.key, { call: call.id, name: call.name, args: call.args });
|
|
56
|
+
call.args = null;
|
|
57
|
+
};
|
|
58
|
+
const dispatch = () => {
|
|
59
|
+
const starting = new Map();
|
|
60
|
+
|
|
61
|
+
for (const worker of workers) {
|
|
62
|
+
if (!worker.key) starting.set(worker.start, (starting.get(worker.start) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < queue.length; i++) {
|
|
66
|
+
const call = queue[i];
|
|
67
|
+
let free = null;
|
|
68
|
+
|
|
69
|
+
for (const worker of workers) {
|
|
70
|
+
if (worker.start !== call.start || !worker.key || worker.busy) continue;
|
|
71
|
+
if (!free || worker.calls < free.calls) free = worker;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A free worker without calls; else a new worker; else one whose calls wait in await.
|
|
75
|
+
if (free?.calls === 0 || (free && workers.length >= size)) {
|
|
76
|
+
queue.splice(i--, 1);
|
|
77
|
+
send(free, call);
|
|
78
|
+
} else if (starting.get(call.start) > 0) {
|
|
79
|
+
starting.set(call.start, starting.get(call.start) - 1);
|
|
80
|
+
} else if (workers.length < size || retire(call.start)) {
|
|
81
|
+
add(call.start);
|
|
82
|
+
} else {
|
|
83
|
+
// It waits for a busy worker of its module, which may have let go of the program when
|
|
84
|
+
// its last call returned: the program runs on until that worker is free and takes it.
|
|
85
|
+
for (const worker of workers)
|
|
86
|
+
if (worker.start === call.start && worker.key) worker.thread.ref?.();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const retire = (start) => {
|
|
91
|
+
const idle = workers.find(
|
|
92
|
+
(worker) =>
|
|
93
|
+
worker.key && worker.start !== start && !worker.busy && worker.calls === 0 && !worker.owns,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!idle) return false;
|
|
97
|
+
idle.thread.terminate();
|
|
98
|
+
remove(idle, null);
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
// A worker that stopped: its calls fail, and the threads that talked to it forget it.
|
|
103
|
+
const remove = (worker, error) => {
|
|
104
|
+
workers.splice(workers.indexOf(worker), 1);
|
|
105
|
+
for (const [id, call] of calls) {
|
|
106
|
+
if (call.worker !== worker) continue;
|
|
107
|
+
calls.delete(id);
|
|
108
|
+
call.reject(error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (worker.key) {
|
|
112
|
+
$$gone(worker.key);
|
|
113
|
+
for (const other of workers) {
|
|
114
|
+
if (other.key) other.thread.postMessage({ gone: worker.key });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const fail = (worker, error) => {
|
|
119
|
+
remove(worker, error);
|
|
120
|
+
dispatch();
|
|
121
|
+
};
|
|
122
|
+
const receive = (worker, data) => {
|
|
123
|
+
if ('op' in data || 'answer' in data) return $$incoming(worker.key, data);
|
|
124
|
+
if ('hello' in data) {
|
|
125
|
+
worker.key = data.hello;
|
|
126
|
+
$$connect(worker.key, worker.thread, false);
|
|
127
|
+
worker.thread.postMessage({ init: registry.key });
|
|
128
|
+
// Workers talk to each other through ports of their own, also while this thread is busy.
|
|
129
|
+
for (const other of workers) {
|
|
130
|
+
if (other === worker || !other.key) continue;
|
|
131
|
+
|
|
132
|
+
const { port1, port2 } = new MessageChannel();
|
|
133
|
+
|
|
134
|
+
worker.thread.postMessage({ port: other.key, through: port1 }, [port1]);
|
|
135
|
+
other.thread.postMessage({ port: worker.key, through: port2 }, [port2]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
worker.busy = false;
|
|
139
|
+
unref(worker);
|
|
140
|
+
} else if ('free' in data) {
|
|
141
|
+
worker.busy = false;
|
|
142
|
+
worker.owns = data.owns;
|
|
143
|
+
} else if ('returned' in data) {
|
|
144
|
+
$$adopt(data);
|
|
145
|
+
|
|
146
|
+
const call = calls.get(data.returned);
|
|
147
|
+
|
|
148
|
+
calls.delete(data.returned);
|
|
149
|
+
worker.owns = data.owns;
|
|
150
|
+
worker.calls--;
|
|
151
|
+
unref(worker);
|
|
152
|
+
if ('error' in data) call?.reject(data.error);
|
|
153
|
+
else call?.resolve(data.value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
dispatch();
|
|
157
|
+
// A worker that a waiting call held on to, now free and without a call, lets go again.
|
|
158
|
+
if ('free' in data) unref(worker);
|
|
159
|
+
};
|
|
160
|
+
const add = (start) => {
|
|
161
|
+
const worker = { start, key: null, thread: null, busy: true, calls: 0, owns: false };
|
|
162
|
+
|
|
163
|
+
workers.push(worker);
|
|
164
|
+
start().then(
|
|
165
|
+
(thread) => {
|
|
166
|
+
worker.thread = thread;
|
|
167
|
+
if (thread.on) {
|
|
168
|
+
thread.on('message', (data) => receive(worker, data));
|
|
169
|
+
thread.on('error', (error) => fail(worker, error));
|
|
170
|
+
} else {
|
|
171
|
+
thread.onmessage = (event) => receive(worker, event.data);
|
|
172
|
+
thread.onerror = (event) => fail(worker, new Error(event.message ?? 'a worker failed'));
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
(error) => {
|
|
176
|
+
remove(worker, error);
|
|
177
|
+
for (const call of queue.filter((each) => each.start === start)) {
|
|
178
|
+
queue.splice(queue.indexOf(call), 1);
|
|
179
|
+
call.reject(error);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
const tasks = new WeakMap();
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
call: (start, name, args) => {
|
|
188
|
+
const call = { id: ids++, start, name, args, resolve: null, reject: null, worker: null };
|
|
189
|
+
const task = new Promise((resolve, reject) => Object.assign(call, { resolve, reject }));
|
|
190
|
+
|
|
191
|
+
tasks.set(task, call);
|
|
192
|
+
queue.push(call);
|
|
193
|
+
dispatch();
|
|
194
|
+
|
|
195
|
+
return task;
|
|
196
|
+
},
|
|
197
|
+
// A call that waits leaves the queue; a worker cannot stop a call, so the worker stops, and
|
|
198
|
+
// the other calls in it fail too.
|
|
199
|
+
cancel: (task) => {
|
|
200
|
+
const call = tasks.get(task);
|
|
201
|
+
|
|
202
|
+
tasks.delete(task);
|
|
203
|
+
if (!call || (!queue.includes(call) && !calls.has(call.id))) return;
|
|
204
|
+
task.catch(() => {});
|
|
205
|
+
if (queue.includes(call)) {
|
|
206
|
+
queue.splice(queue.indexOf(call), 1);
|
|
207
|
+
|
|
208
|
+
return call.reject(new Error('goroutine cancelled'));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
calls.delete(call.id);
|
|
212
|
+
call.reject(new Error('goroutine cancelled'));
|
|
213
|
+
call.worker.thread.terminate();
|
|
214
|
+
remove(
|
|
215
|
+
call.worker,
|
|
216
|
+
new Error('the worker of the goroutine stopped: another goroutine in it was cancelled'),
|
|
217
|
+
);
|
|
218
|
+
dispatch();
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// `go` inside a goroutine: the function runs in the same worker, with copies of the arguments. A
|
|
224
|
+
// name is one of the `functions` of the worker module, a function literal is itself.
|
|
225
|
+
export function $$goHere(functions, fn, args) {
|
|
226
|
+
const copies = $$copy(args);
|
|
227
|
+
const run = typeof fn === 'function' ? fn : functions[fn];
|
|
228
|
+
let cancel;
|
|
229
|
+
const cancelled = new Promise((_, reject) => (cancel = reject));
|
|
230
|
+
const task = Promise.race([cancelled, Promise.resolve().then(() => cancel && run(...copies))]);
|
|
231
|
+
|
|
232
|
+
$$registry().cancels.set(task, () => {
|
|
233
|
+
task.catch(() => {});
|
|
234
|
+
cancel(new Error('goroutine cancelled'));
|
|
235
|
+
cancel = null;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return task;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// `go for`: the array is split into parts, each part runs as a goroutine of `go` of the module, and
|
|
242
|
+
// the loop waits for all of them. The pool gives a part to the first worker that is free, so with
|
|
243
|
+
// several parts per processor the workers end at about the same time, also when some elements take
|
|
244
|
+
// longer than others or some processors are slower. The names that the body uses are copied with
|
|
245
|
+
// each part: if one of them is an object, which may be large, there is one part per processor. A
|
|
246
|
+
// typed array on a SharedArrayBuffer is shared with the workers, which write into it; other parts
|
|
247
|
+
// are copies, moved to the workers and back into the array.
|
|
248
|
+
export async function $$goFor(go, name, items, ...captured) {
|
|
249
|
+
const partsPerProcessor = 4;
|
|
250
|
+
const processors = globalThis.navigator?.hardwareConcurrency ?? 4;
|
|
251
|
+
const small = captured.every((value) => typeof value !== 'object' || value === null);
|
|
252
|
+
const count = Math.min(small ? processors * partsPerProcessor : processors, items.length);
|
|
253
|
+
const shared =
|
|
254
|
+
typeof SharedArrayBuffer === 'function' && items.buffer instanceof SharedArrayBuffer;
|
|
255
|
+
const parts = [];
|
|
256
|
+
|
|
257
|
+
for (let k = 0; k < count; k++) {
|
|
258
|
+
const from = Math.floor((items.length * k) / count);
|
|
259
|
+
const to = Math.floor((items.length * (k + 1)) / count);
|
|
260
|
+
const part = shared ? items.subarray(from, to) : items.slice(from, to);
|
|
261
|
+
|
|
262
|
+
if (ArrayBuffer.isView(part) && !shared) $$transfer(part);
|
|
263
|
+
parts.push(
|
|
264
|
+
go(name, [part, from, ...captured]).then((written) => {
|
|
265
|
+
if (written === undefined || shared) return;
|
|
266
|
+
if (ArrayBuffer.isView(items)) items.set(written, from);
|
|
267
|
+
else for (let i = 0; i < written.length; i++) items[from + i] = written[i];
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await Promise.all(parts);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// `make([]T, n, shared)`: the memory of a typed array that the workers of `go for` write into,
|
|
276
|
+
// without copies. A page has SharedArrayBuffer only with cross-origin isolation.
|
|
277
|
+
export function $$shared(bytes) {
|
|
278
|
+
if (typeof SharedArrayBuffer !== 'function') {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'make(..., shared) needs SharedArrayBuffer, which a page has only with cross-origin isolation: the headers Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return new SharedArrayBuffer(bytes);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// The worker: runs the calls that the pool sends and sends back their results or errors. Once the
|
|
288
|
+
// thread is free again, it says so, and the pool may send another call.
|
|
289
|
+
export async function $$serve(functions) {
|
|
290
|
+
const threads = 'node:worker_threads';
|
|
291
|
+
const port =
|
|
292
|
+
typeof postMessage === 'function'
|
|
293
|
+
? globalThis
|
|
294
|
+
: (await import(/* @vite-ignore */ threads)).parentPort;
|
|
295
|
+
const registry = $$registry();
|
|
296
|
+
|
|
297
|
+
// The port to the main thread keeps the worker running.
|
|
298
|
+
registry.idle = (key) => key !== registry.main && registry.ports.get(key)?.unref?.();
|
|
299
|
+
port.postMessage({ hello: registry.key });
|
|
300
|
+
|
|
301
|
+
const free = () => port.postMessage({ free: true, owns: registry.states.size > 0 });
|
|
302
|
+
const receive = async (data) => {
|
|
303
|
+
if ('op' in data || 'answer' in data) return $$incoming(registry.main, data);
|
|
304
|
+
if ('init' in data) {
|
|
305
|
+
registry.main = data.init;
|
|
306
|
+
|
|
307
|
+
return $$connect(data.init, port, false);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if ('port' in data) return $$connect(data.port, data.through, true);
|
|
311
|
+
if ('gone' in data) return $$gone(data.gone);
|
|
312
|
+
setTimeout(free);
|
|
313
|
+
$$adopt(data.args);
|
|
314
|
+
|
|
315
|
+
const { main } = registry;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const value = await functions[data.name](...data.args);
|
|
319
|
+
|
|
320
|
+
$$post(main, { returned: data.call, value, owns: registry.states.size > 0 });
|
|
321
|
+
} catch (error) {
|
|
322
|
+
try {
|
|
323
|
+
$$post(main, { returned: data.call, error, owns: registry.states.size > 0 });
|
|
324
|
+
} catch {
|
|
325
|
+
$$post(main, { returned: data.call, error: new Error(String(error)), owns: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (port.on) port.on('message', receive);
|
|
331
|
+
else port.onmessage = (event) => receive(event.data);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// `cancel(task)`: stops a goroutine of `go`, whose Promise fails with "goroutine cancelled". A call
|
|
335
|
+
// in a worker stops with its worker; inside a goroutine, `go` runs in the same worker, so its
|
|
336
|
+
// Promise fails but code that has started goes on.
|
|
337
|
+
export function $$cancel(task) {
|
|
338
|
+
globalThis[Symbol.for('rook.goroutines')]?.cancel(task);
|
|
339
|
+
$$registry().cancels.get(task)?.();
|
|
340
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The runtime of Rook: what compiled code imports from "rooklang/runtime", by area. The names start
|
|
2
|
+
// with $$, as all names of generated code do, so they cannot clash with the names of a program.
|
|
3
|
+
// The compiler can also put the parts that a module needs into the module itself (runtime.ts).
|
|
4
|
+
|
|
5
|
+
export * from './chan.js';
|
|
6
|
+
export * from './core.js';
|
|
7
|
+
export * from './go.js';
|
|
8
|
+
export * from './markup.js';
|
|
9
|
+
export * from './queue.js';
|
|
10
|
+
export * from './select.js';
|
|
11
|
+
export * from './streams.js';
|
|
12
|
+
export * from './threads.js';
|
|
13
|
+
export * from './timers.js';
|