rooklang 0.3.2 → 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.
@@ -11,4 +11,9 @@ export declare abstract class AssignmentChecker extends InitializerChecker {
11
11
  /** The type a value assigned to `target` must have. */
12
12
  protected checkTarget(target: ast.Expression): Type;
13
13
  protected checkReturn(node: ast.ReturnStatement): void;
14
+ /**
15
+ * `return null, err` in a function of `(User, error)`: null takes the place of a value only
16
+ * together with an error that is not null. Returns whether the null is checked here.
17
+ */
18
+ private nullWithError;
14
19
  }
@@ -1,6 +1,7 @@
1
+ import { guardedByError, unpackedResults } from "./error-results.js";
1
2
  import { isUntyped, countValues, callName, isCall } from "./helpers.js";
2
3
  import { InitializerChecker } from "./initializers.js";
3
- import { BOOL, ANY, isAssignable, isNullable, nonNull, typeToString, UNKNOWN, } from "./types.js";
4
+ import { BOOL, ANY, isAssignable, isNullable, nonNull, nullable, typeToString, UNKNOWN, } from "./types.js";
4
5
  // Where values go: assignments to variables, fields and elements, and `return`. Both unpack a call
5
6
  // that returns several values.
6
7
  export class AssignmentChecker extends InitializerChecker {
@@ -26,12 +27,15 @@ export class AssignmentChecker extends InitializerChecker {
26
27
  this.error(`${prefix} ${callName(value)} returns ${countValues(returned)}`, value);
27
28
  return Array(count).fill(UNKNOWN);
28
29
  }
30
+ // `return f()` passes the results on as they are; variables get the values before an error
31
+ // as nullable (error-results.ts).
32
+ const types = context === 'return' ? type.types : unpackedResults(type.types);
29
33
  if (declared) {
30
- for (const element of type.types)
34
+ for (const element of types)
31
35
  this.expectAssignable(element, declared, value);
32
- return type.types.map(() => declared);
36
+ return types.map(() => declared);
33
37
  }
34
- return type.types;
38
+ return types;
35
39
  }
36
40
  checkAssignment(node) {
37
41
  const { targets, values, operator } = node;
@@ -187,11 +191,33 @@ export class AssignmentChecker extends InitializerChecker {
187
191
  if (values.length !== expected.length) {
188
192
  this.error(`wrong number of return values: expected ${expected.length}, got ${values.length}`, node);
189
193
  }
190
- values.forEach((value, i) => {
194
+ const types = values.map((value, i) => this.checkValue(value, expected[i] ?? null));
195
+ types.forEach((type, i) => {
191
196
  const target = expected[i];
192
- const type = this.checkValue(value, target ?? null);
193
- if (target)
194
- this.expectAssignable(type, target, value, ' in return');
197
+ if (!target)
198
+ return;
199
+ if (type.kind === 'null' && this.nullWithError(expected, types, values, i))
200
+ return;
201
+ this.expectAssignable(type, target, values[i], ' in return');
195
202
  });
196
203
  }
204
+ /**
205
+ * `return null, err` in a function of `(User, error)`: null takes the place of a value only
206
+ * together with an error that is not null. Returns whether the null is checked here.
207
+ */
208
+ nullWithError(expected, types, values, i) {
209
+ const last = expected.length - 1;
210
+ if (!guardedByError(expected).includes(i) || types.length !== expected.length)
211
+ return false;
212
+ const error = types[last];
213
+ const target = typeToString(expected[i]);
214
+ if (error.kind === 'null') {
215
+ this.error(`cannot return null with no error: make the result ${typeToString(nullable(expected[i]))} if the value may be null without an error`, values[i]);
216
+ }
217
+ else if (isNullable(error)) {
218
+ const name = values[last].kind === 'Identifier' ? values[last].name : 'err';
219
+ this.error(`null in place of ${target} needs an error that is not null: check it first, e.g. if ${name} != null { return null, ${name} }`, values[i]);
220
+ }
221
+ return true;
222
+ }
197
223
  }
@@ -18,6 +18,8 @@ export declare abstract class CheckerBase {
18
18
  protected abstract checkTry(node: ast.TryStatement): void;
19
19
  protected abstract forget(target: ast.Expression): void;
20
20
  protected abstract narrowableBinding(node: ast.Expression): Binding | undefined;
21
+ /** The error for a value that may be null where it must not be. */
22
+ protected abstract nullError(node: ast.Expression): void;
21
23
  protected abstract checkFunction(params: readonly ast.Parameter[], type: FunctionType, results: Type[] | null, body: ast.BlockStatement | ast.Expression, options?: {
22
24
  isConstructor?: boolean;
23
25
  isComponent?: boolean;
@@ -94,7 +96,6 @@ export declare abstract class CheckerBase {
94
96
  constructor(program: ast.Program, options: CheckOptions);
95
97
  protected error(message: string, node: ast.NodeBase): void;
96
98
  protected expectAssignable(source: Type, target: Type, node: ast.NodeBase, context?: string): void;
97
- protected nullError(node: ast.Expression): void;
98
99
  protected withScope<T>(check: () => T): T;
99
100
  protected withClass<T>(context: ClassContext | null, check: () => T): T;
100
101
  protected lookupValue(name: string): Binding | undefined;
@@ -66,12 +66,6 @@ export class CheckerBase {
66
66
  }
67
67
  this.error(message, node);
68
68
  }
69
- nullError(node) {
70
- const name = node.kind === 'Identifier' ? node.name : null;
71
- this.error(name !== null
72
- ? `"${name}" may be null: check it with "if ${name} != null" or use "?."`
73
- : 'this value may be null: check it for null or use "?."', node);
74
- }
75
69
  withScope(check) {
76
70
  const saved = this.scope;
77
71
  this.scope = new Scope(saved);
@@ -87,6 +87,12 @@ export interface Binding {
87
87
  type: Type | null;
88
88
  /** For components, which exist only at compile time and are used as tags. */
89
89
  component?: ComponentInfo;
90
+ /**
91
+ * `const user, err = loadUser(id)` of `(User, error)`: the error has the values that are not null
92
+ * once it is null, and each of them has the error (error-results.ts).
93
+ */
94
+ guards?: Binding[];
95
+ guardedBy?: Binding;
90
96
  }
91
97
  export interface ComponentInfo {
92
98
  node: ast.ComponentDeclaration;
@@ -17,6 +17,7 @@ export declare abstract class ControlFlowChecker extends StatementChecker {
17
17
  protected narrow(node: ast.Expression, assumeTrue: boolean): Narrowing;
18
18
  /** `x instanceof Date`: an instance of the class when true, the rest of a union when false. */
19
19
  protected narrowInstanceof(left: ast.Expression, right: ast.Expression, assumeTrue: boolean): Narrowing;
20
+ protected nullError(node: ast.Expression): void;
20
21
  protected narrowableBinding(node: ast.Expression): Binding | undefined;
21
22
  /** An assignment to a variable ends its narrowing. */
22
23
  protected forget(target: ast.Expression): void;
@@ -170,6 +170,14 @@ export class ControlFlowChecker extends StatementChecker {
170
170
  if (binding?.type)
171
171
  return [[binding, nonNull(this.flow.get(binding) ?? binding.type)]];
172
172
  }
173
+ // `err == null` when true, `err != null` when false: the values returned with `err` are not
174
+ // null (error-results.ts).
175
+ if ((operator === '==' && assumeTrue) || (operator === '!=' && !assumeTrue)) {
176
+ const target = right.kind === 'NullLiteral' ? left : left.kind === 'NullLiteral' ? right : null;
177
+ const guards = target ? this.narrowableBinding(target)?.guards : undefined;
178
+ if (guards)
179
+ return guards.map((value) => [value, nonNull(this.flow.get(value) ?? value.type)]);
180
+ }
173
181
  if (operator === 'instanceof')
174
182
  return this.narrowInstanceof(left, right, assumeTrue);
175
183
  if (operator !== '==' && operator !== '!=')
@@ -219,6 +227,15 @@ export class ControlFlowChecker extends StatementChecker {
219
227
  : !isAssignable(member, instance));
220
228
  return [[binding, union(isNullable(current) ? [...kept, NULL] : kept)]];
221
229
  }
230
+ nullError(node) {
231
+ const name = node.kind === 'Identifier' ? node.name : null;
232
+ const error = name !== null ? this.lookupValue(name)?.guardedBy?.name : undefined;
233
+ this.error(error !== undefined
234
+ ? `"${name}" may be null until "${error}" is checked: e.g. if ${error} != null { return }`
235
+ : name !== null
236
+ ? `"${name}" may be null: check it with "if ${name} != null" or use "?."`
237
+ : 'this value may be null: check it for null or use "?."', node);
238
+ }
222
239
  narrowableBinding(node) {
223
240
  if (node.kind !== 'Identifier')
224
241
  return undefined;
@@ -0,0 +1,6 @@
1
+ import { type Type } from './types.ts';
2
+ export declare function isErrorType(type: Type): boolean;
3
+ /** Positions of the results that may be null only together with an error: all before it. */
4
+ export declare function guardedByError(results: readonly Type[]): number[];
5
+ /** The types of the unpacked results: those before the error may be null. */
6
+ export declare function unpackedResults(results: readonly Type[]): Type[];
@@ -0,0 +1,31 @@
1
+ import { ERROR, isNullable, nullable, typesEqual } from "./types.js";
2
+ // Values returned with an error: in `(User, error)` the function may return `null, err` instead of
3
+ // a User, as long as the error is not null. So where the results are unpacked, the values before
4
+ // the error may be null, and they are not null once the error is found to be null. A result that
5
+ // is nullable itself, as in `(?User, error)`, may be null without an error too.
6
+ export function isErrorType(type) {
7
+ return typesEqual(type, ERROR);
8
+ }
9
+ /** Positions of the results that may be null only together with an error: all before it. */
10
+ export function guardedByError(results) {
11
+ if (results.length < 2 || !isErrorType(results.at(-1)))
12
+ return [];
13
+ return results.flatMap((type, i) => (i < results.length - 1 && canBeGuarded(type) ? [i] : []));
14
+ }
15
+ /** The types of the unpacked results: those before the error may be null. */
16
+ export function unpackedResults(results) {
17
+ const guarded = guardedByError(results);
18
+ return results.map((type, i) => (guarded.includes(i) ? nullable(type) : type));
19
+ }
20
+ function canBeGuarded(type) {
21
+ switch (type.kind) {
22
+ case 'any':
23
+ case 'unknown':
24
+ case 'void':
25
+ case 'never':
26
+ case 'null':
27
+ return false;
28
+ default:
29
+ return !isNullable(type);
30
+ }
31
+ }
@@ -23,6 +23,11 @@ export declare abstract class StatementChecker extends AssignmentChecker {
23
23
  private checkDelete;
24
24
  protected checkBlock(statements: readonly ast.Statement[]): void;
25
25
  protected checkVariableDeclaration(node: ast.VariableDeclaration): void;
26
+ /**
27
+ * `const user, err = loadUser(id)` of `(User, error)`: `user` is not null once `err` is found to
28
+ * be null. Variables of `let` may be assigned apart, so only constants are linked.
29
+ */
30
+ private linkError;
26
31
  /** The types of the variables of a declaration: the written one, or those of their values. */
27
32
  private initializerTypes;
28
33
  }
@@ -1,3 +1,4 @@
1
+ import { guardedByError } from "./error-results.js";
1
2
  import { AssignmentChecker } from "./assignments.js";
2
3
  import { isUntyped, isNumeric, ownStatements } from "./helpers.js";
3
4
  import { func, hasZeroValue, isNullable, nonNull, nullable, typeToString, UNKNOWN, } from "./types.js";
@@ -182,8 +183,26 @@ export class StatementChecker extends AssignmentChecker {
182
183
  this.flow.set(binding, nonNull(type));
183
184
  }
184
185
  });
186
+ if (node.keyword === 'const' && !declared)
187
+ this.linkError(node, bindings);
185
188
  });
186
189
  }
190
+ /**
191
+ * `const user, err = loadUser(id)` of `(User, error)`: `user` is not null once `err` is found to
192
+ * be null. Variables of `let` may be assigned apart, so only constants are linked.
193
+ */
194
+ linkError(node, bindings) {
195
+ const results = node.values.length === 1 ? this.typeOfChecked(node.values[0]) : null;
196
+ const error = bindings.at(-1);
197
+ if (results?.kind !== 'tuple' || results.types.length !== bindings.length || !error)
198
+ return;
199
+ const guarded = guardedByError(results.types).flatMap((i) => bindings[i] ?? []);
200
+ if (guarded.length === 0)
201
+ return;
202
+ error.guards = guarded;
203
+ for (const binding of guarded)
204
+ binding.guardedBy = error;
205
+ }
187
206
  /** The types of the variables of a declaration: the written one, or those of their values. */
188
207
  initializerTypes(node, declared) {
189
208
  const { names, values } = node;
@@ -1,6 +1,5 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { SimpleStatementEmitter } from './simple-statements.ts';
3
- /** if, for, switch and try. */
4
3
  export declare abstract class ControlStatementEmitter extends SimpleStatementEmitter {
5
4
  protected ifStatement(node: ast.IfStatement): string;
6
5
  protected loopBody(body: ast.BlockStatement): string;
@@ -12,6 +11,15 @@ export declare abstract class ControlStatementEmitter extends SimpleStatementEmi
12
11
  * `for i, x in xs` iterates `xs.entries()`, which gives the keys and values of a Map too.
13
12
  */
14
13
  protected forInStatement(node: ast.ForInStatement): void;
14
+ /** `[i, v]` of a loop over pairs; `_` leaves its place empty. */
15
+ private pair;
16
+ /**
17
+ * `for i, v in items` over an array or a typed array: a loop over the indexes, which runs faster
18
+ * than one over the pairs of `entries()`. The variables of the loop cannot be assigned, so `i` is
19
+ * the counter itself. The array is read once, as `entries()` reads it, into `$$items` unless it is
20
+ * a name that cannot change.
21
+ */
22
+ private indexLoop;
15
23
  /** Each `case` ends with an implicit `break`; a case with declarations gets its own block. */
16
24
  protected switchStatement(node: ast.SwitchStatement, discriminant: ast.Expression): void;
17
25
  /**
@@ -1,10 +1,14 @@
1
1
  import { isChan } from "../checker/channel.js";
2
2
  import { isMap } from "../checker/iterables.js";
3
+ import { typedArrayOf } from "../checker/types.js";
4
+ import { declaredIn } from "../names.js";
3
5
  import { containsBreak } from "../walk.js";
4
6
  import { declarationsOf, endsWithJump } from "./analysis.js";
5
7
  import { SimpleStatementEmitter } from "./simple-statements.js";
6
8
  import { ARROW, BINARY, POSTFIX } from "./syntax.js";
7
9
  /** if, for, switch and try. */
10
+ /** Names that keep their value, so a loop over the indexes may read the array through them. */
11
+ const FIXED = new Set(['const', 'import', 'loop']);
8
12
  export class ControlStatementEmitter extends SimpleStatementEmitter {
9
13
  ifStatement(node) {
10
14
  let text = `if (${this.expression(node.condition, 0)}) ${this.blockStatement(node.consequent)}`;
@@ -94,14 +98,58 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
94
98
  }
95
99
  head = `const ${value} of ${values}`;
96
100
  }
101
+ else if (iterable &&
102
+ (iterable.kind === 'array' || iterable.kind === 'string' || typedArrayOf(iterable) !== null)) {
103
+ // Without the index, the loop goes over the values.
104
+ if (node.key.name === '_' && node.value.name !== '_') {
105
+ head = `const ${value} of ${this.expression(node.iterable, POSTFIX)}`;
106
+ }
107
+ else if (iterable.kind === 'string') {
108
+ // A string has no entries(): its characters, as `for...of` gives them, are numbered.
109
+ const pair = this.pair(node.key, node.value);
110
+ head = `const ${pair} of Array.from(${this.expression(node.iterable, ARROW)}).entries()`;
111
+ }
112
+ else {
113
+ this.indexLoop(node, node.key);
114
+ return;
115
+ }
116
+ }
97
117
  else {
98
- const key = node.key.name === '_' ? '' : this.name(node.key.name);
99
- const pair = node.value.name === '_' ? `[${key}]` : `[${key}, ${value}]`;
100
- head = `const ${pair} of ${this.expression(node.iterable, POSTFIX)}.entries()`;
118
+ head = `const ${this.pair(node.key, node.value)} of ${this.expression(node.iterable, POSTFIX)}.entries()`;
101
119
  }
102
120
  this.line(`for (${head}) ${this.loopBody(node.body)}`);
103
121
  });
104
122
  }
123
+ /** `[i, v]` of a loop over pairs; `_` leaves its place empty. */
124
+ pair(key, value) {
125
+ const name = key.name === '_' ? '' : this.name(key.name);
126
+ return value.name === '_' ? `[${name}]` : `[${name}, ${this.name(value.name)}]`;
127
+ }
128
+ /**
129
+ * `for i, v in items` over an array or a typed array: a loop over the indexes, which runs faster
130
+ * than one over the pairs of `entries()`. The variables of the loop cannot be assigned, so `i` is
131
+ * the counter itself. The array is read once, as `entries()` reads it, into `$$items` unless it is
132
+ * a name that cannot change.
133
+ */
134
+ indexLoop(node, keyNode) {
135
+ const key = keyNode.name === '_' ? '$$i' : this.name(keyNode.name);
136
+ const value = node.value.name === '_' ? null : this.name(node.value.name);
137
+ const { iterable } = node;
138
+ const fixed = iterable.kind === 'Identifier' && FIXED.has(this.lookup(iterable.name) ?? 'let');
139
+ const items = fixed ? this.expression(iterable, POSTFIX) : '$$items';
140
+ const init = fixed ? '' : `, $$items = ${this.expression(iterable, ARROW)}`;
141
+ // A declaration of the same name in the body would clash with the element's in one block.
142
+ const hidden = declaredIn(node.body.body).some((name) => name === keyNode.name || name === node.value.name);
143
+ const body = this.withBreakTarget(null, () => this.block(() => {
144
+ if (value !== null)
145
+ this.line(`const ${value} = ${items}[${key}];`);
146
+ if (hidden)
147
+ this.line(this.blockStatement(node.body));
148
+ else
149
+ this.blockStatements(node.body.body);
150
+ }));
151
+ this.line(`for (let ${key} = 0${init}; ${key} < ${items}.length; ${key}++) ${body}`);
152
+ }
105
153
  /** Each `case` ends with an implicit `break`; a case with declarations gets its own block. */
106
154
  switchStatement(node, discriminant) {
107
155
  const head = `switch (${this.expression(discriminant, 0)})`;
@@ -1,5 +1,3 @@
1
- /** Where compiled code imports the runtime from. */
2
- export declare const RUNTIME_MODULE = "rooklang/runtime";
3
1
  /** How a module gets the runtime: by an import, or with the code of the runtime in it. */
4
2
  export type RuntimeMode = 'import' | 'inline';
5
3
  /** Predeclared functions of goroutines and channels: `close(ch)` is `$$close(ch)`. */
@@ -1,11 +1,5 @@
1
+ import { RUNTIME_MODULE } from "../runtime-module.js";
1
2
  import { RUNTIME_FILES } from "../runtime/sources.js";
2
- // The runtime of generated code, src/runtime. A module imports the names of it that its code uses
3
- // from "rooklang/runtime"; with `runtime: 'inline'` the files that have them are put into the
4
- // module instead, for code that runs without imports, such as that of the playground. A module
5
- // keeps what is its own: the start of its workers, whose URL a bundler must see in it, the `go`
6
- // that uses it, and the CSS of its components.
7
- /** Where compiled code imports the runtime from. */
8
- export const RUNTIME_MODULE = 'rooklang/runtime';
9
3
  /** Predeclared functions of goroutines and channels: `close(ch)` is `$$close(ch)`. */
10
4
  export const PREDECLARED = new Set([
11
5
  'after',
package/dist/loader.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { registerHooks } from 'node:module';
4
- import { join } from 'node:path';
3
+ import { createRequire, registerHooks } from 'node:module';
4
+ import { extname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { isMainThread } from 'node:worker_threads';
7
- import { displayPath } from "./build.js";
8
- import { RUNTIME_MODULE } from "./codegen/runtime.js";
9
- import { compileFile } from "./compile-file.js";
7
+ import { RUNTIME_MODULE } from "./runtime-module.js";
10
8
  // `rook run` imports this module first (`node --import`): Node then imports `.rk` files,
11
9
  // compiled when they load, and `app.rk?goroutines` is the worker module of `go` in app.rk.
12
10
  // The worker threads of `go` import it too, since they get the `--import` of the process. The main
13
11
  // thread compiles a module before any worker needs it, so it leaves the code of each module and
14
12
  // of its worker module in ROOK_RUN_CACHE, a directory of the run, and the workers take it from
15
- // there: compiling again, with the types, would take each of them as long. ROOK_NO_CHECK turns
16
- // type checking off, and ROOK_MARKUP, 1 or 0, markup and components on or off for every file.
13
+ // there: compiling again, with the types, would take each of them as long. So a worker does not
14
+ // even load the compiler, which takes ~80 ms, unless a module is missing from the cache; the hooks
15
+ // are synchronous, so it is loaded with require(). ROOK_NO_CHECK turns type checking off, and
16
+ // ROOK_MARKUP, 1 or 0, markup and components on or off for every file.
17
17
  const typeCheck = process.env['ROOK_NO_CHECK'] === undefined;
18
18
  const markupSetting = process.env['ROOK_MARKUP'];
19
19
  const markup = markupSetting === undefined ? undefined : markupSetting === '1';
@@ -24,6 +24,9 @@ function cached(url) {
24
24
  }
25
25
  // The compiled code imports the runtime of this compiler, wherever the program is.
26
26
  const runtime = import.meta.resolve(RUNTIME_MODULE);
27
+ const require = createRequire(import.meta.url);
28
+ // The files next to this one: .ts in src, .js in the dist of the package.
29
+ const extension = extname(fileURLToPath(import.meta.url));
27
30
  registerHooks({
28
31
  resolve(specifier, context, nextResolve) {
29
32
  if (specifier === RUNTIME_MODULE)
@@ -39,6 +42,8 @@ registerHooks({
39
42
  return { format: 'module', source: readFileSync(file, 'utf8'), shortCircuit: true };
40
43
  }
41
44
  const moduleUrl = new URL(pathname, url).href;
45
+ const { displayPath } = require(`./build${extension}`);
46
+ const { compileFile } = require(`./compile-file${extension}`);
42
47
  const path = displayPath(fileURLToPath(moduleUrl));
43
48
  const result = compileFile(path, {
44
49
  rewriteImports: false,
@@ -4,7 +4,7 @@
4
4
  export const RUNTIME_FILES = {
5
5
  'chan.js': "// Channels: sending, receiving and closing, here or in the thread that made the channel\n// (threads.js), and the queues of a channel. Receiving from a closed channel gives\n// the zero value of its type, as in Go.\n\nimport { $$Queue } from './queue.js';\nimport { $$commit, $$lock, $$select, $$try, $$unlock, $$unwait } from './select.js';\nimport { $$adopt, $$ask, $$ownerOf, $$post, $$registry, $$stopped } from './threads.js';\n\n$$registry().handle ??= { message: $$message, pump: $$pump };\n\nexport function $$chan(size, zero) {\n const registry = $$registry();\n const id = `${registry.key}:${registry.channels++}`;\n const channel = { $$chan: id, size, zero };\n\n registry.states.set(id, {\n size,\n zero,\n buffer: new $$Queue(),\n receivers: new $$Queue(),\n senders: new $$Queue(),\n closed: false,\n holders: 1,\n busy: false,\n });\n $$adopt(channel);\n\n return channel;\n}\n\n// A local operation waits in the queue of its channel; one of another thread asks the thread\n// that made the channel.\nexport function $$send(channel, value) {\n const id = channel.$$chan;\n const owner = $$ownerOf(id);\n\n if (owner !== $$registry().key) return $$ask(owner, { op: 'send', channel: id, value });\n\n return new Promise((resolve, reject) =>\n $$wait(id, 'senders', { value, done: resolve, fail: reject }),\n );\n}\n\nexport function $$receive(channel, withOk = false) {\n const id = channel.$$chan;\n const owner = $$ownerOf(id);\n const received =\n owner !== $$registry().key\n ? $$ask(owner, { op: 'receive', channel: id })\n : new Promise((resolve, reject) => $$wait(id, 'receivers', { done: resolve, fail: reject }));\n\n return withOk ? received : received.then(([value]) => value);\n}\n\n// Closing a channel of another thread fails this one if it was closed already, as a panic would.\nexport function $$close(channel) {\n const id = channel.$$chan;\n const owner = $$ownerOf(id);\n\n if (owner === $$registry().key) return $$closeHere(id);\n $$ask(owner, { op: 'close', channel: id }).catch((error) => {\n setTimeout(() => {\n throw error;\n });\n });\n}\n\nexport function $$closeHere(id) {\n const state = $$registry().states.get(id);\n\n if (state.closed) throw new Error('close of a closed channel');\n state.closed = true;\n $$pump(id);\n}\n\n// What a closed channel gives: a value of its own for each receiver, if it is an array or a queue.\nexport function $$zero({ zero }) {\n return Array.isArray(zero) ? [] : zero instanceof $$Queue ? new $$Queue() : zero;\n}\n\nexport async function* $$range(channel) {\n while (true) {\n const [value, ok] = await $$receive(channel, true);\n\n if (!ok) return;\n yield value;\n }\n}\n\nexport function $$wait(id, queue, entry) {\n const state = $$registry().states.get(id);\n\n if (!state) return entry.fail?.($$stopped());\n state[queue].push(entry);\n $$pump(id);\n}\n\n// The operations of other threads on the channels of this one.\nexport function $$message(from, data) {\n const answer = (value) => $$post(from, { answer: data.ask, value });\n const fail = (error) => $$post(from, { answer: data.ask, error });\n const select = { key: from, sid: data.sid };\n\n switch (data.op) {\n case 'send':\n return $$wait(data.channel, 'senders', { from, value: data.value, done: answer, fail });\n\n case 'receive':\n return $$wait(data.channel, 'receivers', { from, done: answer, fail });\n\n case 'close':\n try {\n $$closeHere(data.channel);\n\n return answer();\n } catch (error) {\n return fail(error);\n }\n\n case 'try':\n try {\n return answer($$try(data.channel, data.send, data.value));\n } catch (error) {\n return fail(error);\n }\n\n case 'select':\n return $$select(data.cases, data.hasDefault).then(answer, fail);\n\n case 'wait':\n return $$wait(data.channel, data.send ? 'senders' : 'receivers', {\n select,\n case: data.case,\n value: data.value,\n });\n\n case 'cancel':\n return $$unwait(select, data.channels);\n\n case 'lock':\n return answer($$lock(data.sid));\n\n case 'unlock':\n return $$unlock(data.sid);\n\n case 'commit':\n return $$commit(data.sid, data.case, data.result, data.error);\n }\n}\n\n// Matches what waits in a channel: a receiver with a value of the buffer, a sender or the zero value\n// after close; a sender with room in the buffer. A case of a select goes on only if its select\n// has not gone on with another case, which the thread of the select tells.\nexport function $$pump(id) {\n const state = $$registry().states.get(id);\n\n while (state && !state.busy) {\n const receiver = state.receivers.at(0);\n const sender = state.senders.at(0);\n\n if (receiver && (state.buffer.length > 0 || state.closed)) {\n if (!$$pair(id, state, receiver, null)) return;\n } else if (receiver && sender) {\n if (!$$pair(id, state, receiver, sender)) return;\n } else if (sender && (state.closed || state.buffer.length < state.size)) {\n if (!$$pair(id, state, null, sender)) return;\n } else {\n return;\n }\n }\n}\n\n// Whether the pump can go on at once: a pair whose selects are all in this thread is decided here.\nexport function $$pair(id, state, receiver, sender) {\n const entries = [receiver, sender].filter((entry) => entry !== null);\n const key = $$registry().key;\n\n if (entries.some((entry) => entry.select && entry.select.key !== key)) {\n $$pairLater(id, state, receiver, sender);\n\n return false;\n }\n\n const locks = entries.map((entry) => (entry.select ? $$lock(entry.select.sid) : 'locked'));\n\n if (locks.every((lock) => lock === 'locked')) {\n $$complete(state, receiver, sender);\n\n return true;\n }\n\n entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));\n\n return $$settle(id, state, entries, locks);\n}\n\n// A pair with a select of another thread: the channel waits for the answers of that thread.\nexport async function $$pairLater(id, state, receiver, sender) {\n state.busy = true;\n\n const entries = [receiver, sender].filter((entry) => entry !== null);\n const locks = [];\n\n for (const { select } of entries) {\n const lock = !select\n ? 'locked'\n : select.key === $$registry().key\n ? $$lock(select.sid)\n : await $$ask(select.key, { op: 'lock', sid: select.sid }).catch(() => 'done');\n\n locks.push(lock);\n if (lock !== 'locked') break;\n }\n\n state.busy = false;\n if (locks.length === entries.length && locks.every((lock) => lock === 'locked')) {\n $$complete(state, receiver, sender);\n } else {\n entries.forEach((entry, i) => locks[i] === 'locked' && $$unlockAt(entry.select));\n if (!$$settle(id, state, entries, locks)) return;\n }\n\n $$pump(id);\n}\n\n// After a pair that could not go on: a select that has gone on leaves the queue; one that another\n// channel is deciding is tried again a little later.\nexport function $$settle(id, state, entries, locks) {\n entries.forEach((entry, i) => {\n if (locks[i] === 'done') $$remove(state, entry);\n });\n if (locks.includes('done')) return true;\n setTimeout(() => $$pump(id), 1 + Math.random() * 4);\n\n return false;\n}\n\nexport function $$remove(state, entry) {\n state.receivers.$$delete(entry);\n state.senders.$$delete(entry);\n}\n\n// Takes the pair off the queues and passes the value.\nexport function $$complete(state, receiver, sender) {\n if (receiver) state.receivers.$$delete(receiver);\n if (sender) state.senders.$$delete(sender);\n if (receiver) {\n const result =\n state.buffer.length > 0\n ? [state.buffer.shift(), true]\n : sender\n ? [sender.value, true]\n : [$$zero(state), false];\n\n $$finish(receiver, result);\n if (sender) $$finish(sender);\n } else if (state.closed) {\n $$finish(sender, undefined, new Error('send on a closed channel'));\n } else {\n state.buffer.push(sender.value);\n $$finish(sender);\n }\n}\n\nexport function $$finish(entry, result, error) {\n const { select } = entry;\n\n if (!select) return error ? entry.fail(error) : entry.done(result);\n if (select.key === $$registry().key) return $$commit(select.sid, entry.case, result, error);\n $$post(select.key, { op: 'commit', sid: select.sid, case: entry.case, result, error });\n}\n\nexport function $$unlockAt(select) {\n if (!select) return;\n if (select.key === $$registry().key) $$unlock(select.sid);\n else $$post(select.key, { op: 'unlock', sid: select.sid });\n}\n",
6
6
  'core.js': "// What the code of any module may need: `for` over a value of unknown type, and `defer`.\n\n// `for v in x` over an untyped value: a Map gives its values, as a typed one does.\nexport function $$values(iterable) {\n if (typeof iterable === 'function') return iterable();\n\n return iterable instanceof Map ? iterable.values() : iterable;\n}\n\n// Runs deferred calls in reverse order; every call runs even if an earlier one throws.\nexport function $$runDeferred(deferred) {\n let failure;\n\n for (let i = deferred.length - 1; i >= 0; i--) {\n try {\n deferred[i]();\n } catch (error) {\n failure = { error };\n }\n }\n\n if (failure) throw failure.error;\n}\n",
7
- 'go.js': "// `go`: the pool of workers of the program, and the side of the worker. They work\n// with Web Workers and with the worker threads of Node, whose API differs: `on(\"message\")` instead\n// of `onmessage`, and `unref()`, so that idle workers do not keep the program running.\n\nimport {\n $$adopt,\n $$connect,\n $$copy,\n $$gone,\n $$incoming,\n $$post,\n $$registry,\n $$transfer,\n} from './threads.js';\n\n// `go` in a module: a call in a worker of the pool. `start` starts a worker of the module, with its\n// worker module: it is $$startWorker of the module, whose URL a bundler needs to see there.\nexport function $$goWorker(start, name, args) {\n return (globalThis[Symbol.for('rook.goroutines')] ??= $$pool()).call(\n start,\n name,\n $$copy(args, true),\n );\n}\n\n// The workers of the program, shared by its modules: up to one per processor. Each runs the worker\n// module of one module, `start`. Calls wait in one queue and go to a worker of their module that is\n// free: one whose thread is not running code, though its earlier calls may wait in `await`. A\n// worker that is still busy keeps its calls for later; an idle worker of another module makes room\n// for a new one, unless it keeps channels.\nexport function $$pool() {\n const size = globalThis.navigator?.hardwareConcurrency ?? 4;\n const registry = $$registry();\n const workers = [];\n const queue = [];\n const calls = new Map();\n let ids = 0;\n // A worker without calls lets the program end, unless something here waits for it.\n const unref = (worker) => {\n if (worker.calls === 0 && !(registry.pins.get(worker.key) > 0)) worker.thread.unref?.();\n };\n\n registry.idle = (key) => {\n const worker = workers.find((each) => each.key === key);\n\n if (worker) unref(worker);\n };\n\n const send = (worker, call) => {\n worker.busy = true;\n worker.calls++;\n worker.thread.ref?.();\n call.worker = worker;\n calls.set(call.id, call);\n $$post(worker.key, { call: call.id, name: call.name, args: call.args });\n call.args = null;\n };\n const dispatch = () => {\n const starting = new Map();\n\n for (const worker of workers) {\n if (!worker.key) starting.set(worker.start, (starting.get(worker.start) ?? 0) + 1);\n }\n\n for (let i = 0; i < queue.length; i++) {\n const call = queue[i];\n let free = null;\n\n for (const worker of workers) {\n if (worker.start !== call.start || !worker.key || worker.busy) continue;\n if (!free || worker.calls < free.calls) free = worker;\n }\n\n // A free worker without calls; else a new worker; else one whose calls wait in await.\n if (free?.calls === 0 || (free && workers.length >= size)) {\n queue.splice(i--, 1);\n send(free, call);\n } else if (starting.get(call.start) > 0) {\n starting.set(call.start, starting.get(call.start) - 1);\n } else if (workers.length < size || retire(call.start)) {\n add(call.start);\n } else {\n // It waits for a busy worker of its module, which may have let go of the program when\n // its last call returned: the program runs on until that worker is free and takes it.\n for (const worker of workers)\n if (worker.start === call.start && worker.key) worker.thread.ref?.();\n }\n }\n };\n const retire = (start) => {\n const idle = workers.find(\n (worker) =>\n worker.key && worker.start !== start && !worker.busy && worker.calls === 0 && !worker.owns,\n );\n\n if (!idle) return false;\n idle.thread.terminate();\n remove(idle, null);\n\n return true;\n };\n // A worker that stopped: its calls fail, and the threads that talked to it forget it.\n const remove = (worker, error) => {\n workers.splice(workers.indexOf(worker), 1);\n for (const [id, call] of calls) {\n if (call.worker !== worker) continue;\n calls.delete(id);\n call.reject(error);\n }\n\n if (worker.key) {\n $$gone(worker.key);\n for (const other of workers) {\n if (other.key) other.thread.postMessage({ gone: worker.key });\n }\n }\n };\n const fail = (worker, error) => {\n remove(worker, error);\n dispatch();\n };\n const receive = (worker, data) => {\n if ('op' in data || 'answer' in data) return $$incoming(worker.key, data);\n if ('hello' in data) {\n worker.key = data.hello;\n $$connect(worker.key, worker.thread, false);\n worker.thread.postMessage({ init: registry.key });\n // Workers talk to each other through ports of their own, also while this thread is busy.\n for (const other of workers) {\n if (other === worker || !other.key) continue;\n\n const { port1, port2 } = new MessageChannel();\n\n worker.thread.postMessage({ port: other.key, through: port1 }, [port1]);\n other.thread.postMessage({ port: worker.key, through: port2 }, [port2]);\n }\n\n worker.busy = false;\n unref(worker);\n } else if ('free' in data) {\n worker.busy = false;\n worker.owns = data.owns;\n } else if ('returned' in data) {\n $$adopt(data);\n\n const call = calls.get(data.returned);\n\n calls.delete(data.returned);\n worker.owns = data.owns;\n worker.calls--;\n unref(worker);\n if ('error' in data) call?.reject(data.error);\n else call?.resolve(data.value);\n }\n\n dispatch();\n // A worker that a waiting call held on to, now free and without a call, lets go again.\n if ('free' in data) unref(worker);\n };\n const add = (start) => {\n const worker = { start, key: null, thread: null, busy: true, calls: 0, owns: false };\n\n workers.push(worker);\n start().then(\n (thread) => {\n worker.thread = thread;\n if (thread.on) {\n thread.on('message', (data) => receive(worker, data));\n thread.on('error', (error) => fail(worker, error));\n } else {\n thread.onmessage = (event) => receive(worker, event.data);\n thread.onerror = (event) => fail(worker, new Error(event.message ?? 'a worker failed'));\n }\n },\n (error) => {\n remove(worker, error);\n for (const call of queue.filter((each) => each.start === start)) {\n queue.splice(queue.indexOf(call), 1);\n call.reject(error);\n }\n },\n );\n };\n const tasks = new WeakMap();\n\n return {\n call: (start, name, args) => {\n const call = { id: ids++, start, name, args, resolve: null, reject: null, worker: null };\n const task = new Promise((resolve, reject) => Object.assign(call, { resolve, reject }));\n\n tasks.set(task, call);\n queue.push(call);\n dispatch();\n\n return task;\n },\n // A call that waits leaves the queue; a worker cannot stop a call, so the worker stops, and\n // the other calls in it fail too.\n cancel: (task) => {\n const call = tasks.get(task);\n\n tasks.delete(task);\n if (!call || (!queue.includes(call) && !calls.has(call.id))) return;\n task.catch(() => {});\n if (queue.includes(call)) {\n queue.splice(queue.indexOf(call), 1);\n\n return call.reject(new Error('goroutine cancelled'));\n }\n\n calls.delete(call.id);\n call.reject(new Error('goroutine cancelled'));\n call.worker.thread.terminate();\n remove(\n call.worker,\n new Error('the worker of the goroutine stopped: another goroutine in it was cancelled'),\n );\n dispatch();\n },\n };\n}\n\n// `go` inside a goroutine: the function runs in the same worker, with copies of the arguments. A\n// name is one of the `functions` of the worker module, a function literal is itself.\nexport function $$goHere(functions, fn, args) {\n const copies = $$copy(args);\n const run = typeof fn === 'function' ? fn : functions[fn];\n let cancel;\n const cancelled = new Promise((_, reject) => (cancel = reject));\n const task = Promise.race([cancelled, Promise.resolve().then(() => cancel && run(...copies))]);\n\n $$registry().cancels.set(task, () => {\n task.catch(() => {});\n cancel(new Error('goroutine cancelled'));\n cancel = null;\n });\n\n return task;\n}\n\n// `go for`: the array is split into parts, one per processor, each part runs as a goroutine of `go`\n// of the module, and\n// the loop waits for all of them. A typed array on a SharedArrayBuffer is shared with the workers,\n// which write into it; other parts are copies, moved to the workers and back into the array.\nexport async function $$goFor(go, name, items, ...captured) {\n const count = Math.min(globalThis.navigator?.hardwareConcurrency ?? 4, items.length);\n const shared =\n typeof SharedArrayBuffer === 'function' && items.buffer instanceof SharedArrayBuffer;\n const parts = [];\n\n for (let k = 0; k < count; k++) {\n const from = Math.floor((items.length * k) / count);\n const to = Math.floor((items.length * (k + 1)) / count);\n const part = shared ? items.subarray(from, to) : items.slice(from, to);\n\n if (ArrayBuffer.isView(part) && !shared) $$transfer(part);\n parts.push(\n go(name, [part, from, ...captured]).then((written) => {\n if (written === undefined || shared) return;\n if (ArrayBuffer.isView(items)) items.set(written, from);\n else for (let i = 0; i < written.length; i++) items[from + i] = written[i];\n }),\n );\n }\n\n await Promise.all(parts);\n}\n\n// `make([]T, n, shared)`: the memory of a typed array that the workers of `go for` write into,\n// without copies. A page has SharedArrayBuffer only with cross-origin isolation.\nexport function $$shared(bytes) {\n if (typeof SharedArrayBuffer !== 'function') {\n throw new Error(\n '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',\n );\n }\n\n return new SharedArrayBuffer(bytes);\n}\n\n// The worker: runs the calls that the pool sends and sends back their results or errors. Once the\n// thread is free again, it says so, and the pool may send another call.\nexport async function $$serve(functions) {\n const threads = 'node:worker_threads';\n const port =\n typeof postMessage === 'function'\n ? globalThis\n : (await import(/* @vite-ignore */ threads)).parentPort;\n const registry = $$registry();\n\n // The port to the main thread keeps the worker running.\n registry.idle = (key) => key !== registry.main && registry.ports.get(key)?.unref?.();\n port.postMessage({ hello: registry.key });\n\n const free = () => port.postMessage({ free: true, owns: registry.states.size > 0 });\n const receive = async (data) => {\n if ('op' in data || 'answer' in data) return $$incoming(registry.main, data);\n if ('init' in data) {\n registry.main = data.init;\n\n return $$connect(data.init, port, false);\n }\n\n if ('port' in data) return $$connect(data.port, data.through, true);\n if ('gone' in data) return $$gone(data.gone);\n setTimeout(free);\n $$adopt(data.args);\n\n const { main } = registry;\n\n try {\n const value = await functions[data.name](...data.args);\n\n $$post(main, { returned: data.call, value, owns: registry.states.size > 0 });\n } catch (error) {\n try {\n $$post(main, { returned: data.call, error, owns: registry.states.size > 0 });\n } catch {\n $$post(main, { returned: data.call, error: new Error(String(error)), owns: true });\n }\n }\n };\n\n if (port.on) port.on('message', receive);\n else port.onmessage = (event) => receive(event.data);\n}\n\n// `cancel(task)`: stops a goroutine of `go`, whose Promise fails with \"goroutine cancelled\". A call\n// in a worker stops with its worker; inside a goroutine, `go` runs in the same worker, so its\n// Promise fails but code that has started goes on.\nexport function $$cancel(task) {\n globalThis[Symbol.for('rook.goroutines')]?.cancel(task);\n $$registry().cancels.get(task)?.();\n}\n",
7
+ 'go.js': "// `go`: the pool of workers of the program, and the side of the worker. They work\n// with Web Workers and with the worker threads of Node, whose API differs: `on(\"message\")` instead\n// of `onmessage`, and `unref()`, so that idle workers do not keep the program running.\n\nimport {\n $$adopt,\n $$connect,\n $$copy,\n $$gone,\n $$incoming,\n $$post,\n $$registry,\n $$transfer,\n} from './threads.js';\n\n// `go` in a module: a call in a worker of the pool. `start` starts a worker of the module, with its\n// worker module: it is $$startWorker of the module, whose URL a bundler needs to see there.\nexport function $$goWorker(start, name, args) {\n return (globalThis[Symbol.for('rook.goroutines')] ??= $$pool()).call(\n start,\n name,\n $$copy(args, true),\n );\n}\n\n// The workers of the program, shared by its modules: up to one per processor. Each runs the worker\n// module of one module, `start`. Calls wait in one queue and go to a worker of their module that is\n// free: one whose thread is not running code, though its earlier calls may wait in `await`. A\n// worker that is still busy keeps its calls for later; an idle worker of another module makes room\n// for a new one, unless it keeps channels.\nexport function $$pool() {\n const size = globalThis.navigator?.hardwareConcurrency ?? 4;\n const registry = $$registry();\n const workers = [];\n const queue = [];\n const calls = new Map();\n let ids = 0;\n // A worker without calls lets the program end, unless something here waits for it.\n const unref = (worker) => {\n if (worker.calls === 0 && !(registry.pins.get(worker.key) > 0)) worker.thread.unref?.();\n };\n\n registry.idle = (key) => {\n const worker = workers.find((each) => each.key === key);\n\n if (worker) unref(worker);\n };\n\n const send = (worker, call) => {\n worker.busy = true;\n worker.calls++;\n worker.thread.ref?.();\n call.worker = worker;\n calls.set(call.id, call);\n $$post(worker.key, { call: call.id, name: call.name, args: call.args });\n call.args = null;\n };\n const dispatch = () => {\n const starting = new Map();\n\n for (const worker of workers) {\n if (!worker.key) starting.set(worker.start, (starting.get(worker.start) ?? 0) + 1);\n }\n\n for (let i = 0; i < queue.length; i++) {\n const call = queue[i];\n let free = null;\n\n for (const worker of workers) {\n if (worker.start !== call.start || !worker.key || worker.busy) continue;\n if (!free || worker.calls < free.calls) free = worker;\n }\n\n // A free worker without calls; else a new worker; else one whose calls wait in await.\n if (free?.calls === 0 || (free && workers.length >= size)) {\n queue.splice(i--, 1);\n send(free, call);\n } else if (starting.get(call.start) > 0) {\n starting.set(call.start, starting.get(call.start) - 1);\n } else if (workers.length < size || retire(call.start)) {\n add(call.start);\n } else {\n // It waits for a busy worker of its module, which may have let go of the program when\n // its last call returned: the program runs on until that worker is free and takes it.\n for (const worker of workers)\n if (worker.start === call.start && worker.key) worker.thread.ref?.();\n }\n }\n };\n const retire = (start) => {\n const idle = workers.find(\n (worker) =>\n worker.key && worker.start !== start && !worker.busy && worker.calls === 0 && !worker.owns,\n );\n\n if (!idle) return false;\n idle.thread.terminate();\n remove(idle, null);\n\n return true;\n };\n // A worker that stopped: its calls fail, and the threads that talked to it forget it.\n const remove = (worker, error) => {\n workers.splice(workers.indexOf(worker), 1);\n for (const [id, call] of calls) {\n if (call.worker !== worker) continue;\n calls.delete(id);\n call.reject(error);\n }\n\n if (worker.key) {\n $$gone(worker.key);\n for (const other of workers) {\n if (other.key) other.thread.postMessage({ gone: worker.key });\n }\n }\n };\n const fail = (worker, error) => {\n remove(worker, error);\n dispatch();\n };\n const receive = (worker, data) => {\n if ('op' in data || 'answer' in data) return $$incoming(worker.key, data);\n if ('hello' in data) {\n worker.key = data.hello;\n $$connect(worker.key, worker.thread, false);\n worker.thread.postMessage({ init: registry.key });\n // Workers talk to each other through ports of their own, also while this thread is busy.\n for (const other of workers) {\n if (other === worker || !other.key) continue;\n\n const { port1, port2 } = new MessageChannel();\n\n worker.thread.postMessage({ port: other.key, through: port1 }, [port1]);\n other.thread.postMessage({ port: worker.key, through: port2 }, [port2]);\n }\n\n worker.busy = false;\n unref(worker);\n } else if ('free' in data) {\n worker.busy = false;\n worker.owns = data.owns;\n } else if ('returned' in data) {\n $$adopt(data);\n\n const call = calls.get(data.returned);\n\n calls.delete(data.returned);\n worker.owns = data.owns;\n worker.calls--;\n unref(worker);\n if ('error' in data) call?.reject(data.error);\n else call?.resolve(data.value);\n }\n\n dispatch();\n // A worker that a waiting call held on to, now free and without a call, lets go again.\n if ('free' in data) unref(worker);\n };\n const add = (start) => {\n const worker = { start, key: null, thread: null, busy: true, calls: 0, owns: false };\n\n workers.push(worker);\n start().then(\n (thread) => {\n worker.thread = thread;\n if (thread.on) {\n thread.on('message', (data) => receive(worker, data));\n thread.on('error', (error) => fail(worker, error));\n } else {\n thread.onmessage = (event) => receive(worker, event.data);\n thread.onerror = (event) => fail(worker, new Error(event.message ?? 'a worker failed'));\n }\n },\n (error) => {\n remove(worker, error);\n for (const call of queue.filter((each) => each.start === start)) {\n queue.splice(queue.indexOf(call), 1);\n call.reject(error);\n }\n },\n );\n };\n const tasks = new WeakMap();\n\n return {\n call: (start, name, args) => {\n const call = { id: ids++, start, name, args, resolve: null, reject: null, worker: null };\n const task = new Promise((resolve, reject) => Object.assign(call, { resolve, reject }));\n\n tasks.set(task, call);\n queue.push(call);\n dispatch();\n\n return task;\n },\n // A call that waits leaves the queue; a worker cannot stop a call, so the worker stops, and\n // the other calls in it fail too.\n cancel: (task) => {\n const call = tasks.get(task);\n\n tasks.delete(task);\n if (!call || (!queue.includes(call) && !calls.has(call.id))) return;\n task.catch(() => {});\n if (queue.includes(call)) {\n queue.splice(queue.indexOf(call), 1);\n\n return call.reject(new Error('goroutine cancelled'));\n }\n\n calls.delete(call.id);\n call.reject(new Error('goroutine cancelled'));\n call.worker.thread.terminate();\n remove(\n call.worker,\n new Error('the worker of the goroutine stopped: another goroutine in it was cancelled'),\n );\n dispatch();\n },\n };\n}\n\n// `go` inside a goroutine: the function runs in the same worker, with copies of the arguments. A\n// name is one of the `functions` of the worker module, a function literal is itself.\nexport function $$goHere(functions, fn, args) {\n const copies = $$copy(args);\n const run = typeof fn === 'function' ? fn : functions[fn];\n let cancel;\n const cancelled = new Promise((_, reject) => (cancel = reject));\n const task = Promise.race([cancelled, Promise.resolve().then(() => cancel && run(...copies))]);\n\n $$registry().cancels.set(task, () => {\n task.catch(() => {});\n cancel(new Error('goroutine cancelled'));\n cancel = null;\n });\n\n return task;\n}\n\n// `go for`: the array is split into parts, each part runs as a goroutine of `go` of the module, and\n// the loop waits for all of them. The pool gives a part to the first worker that is free, so with\n// several parts per processor the workers end at about the same time, also when some elements take\n// longer than others or some processors are slower. The names that the body uses are copied with\n// each part: if one of them is an object, which may be large, there is one part per processor. A\n// typed array on a SharedArrayBuffer is shared with the workers, which write into it; other parts\n// are copies, moved to the workers and back into the array.\nexport async function $$goFor(go, name, items, ...captured) {\n const partsPerProcessor = 4;\n const processors = globalThis.navigator?.hardwareConcurrency ?? 4;\n const small = captured.every((value) => typeof value !== 'object' || value === null);\n const count = Math.min(small ? processors * partsPerProcessor : processors, items.length);\n const shared =\n typeof SharedArrayBuffer === 'function' && items.buffer instanceof SharedArrayBuffer;\n const parts = [];\n\n for (let k = 0; k < count; k++) {\n const from = Math.floor((items.length * k) / count);\n const to = Math.floor((items.length * (k + 1)) / count);\n const part = shared ? items.subarray(from, to) : items.slice(from, to);\n\n if (ArrayBuffer.isView(part) && !shared) $$transfer(part);\n parts.push(\n go(name, [part, from, ...captured]).then((written) => {\n if (written === undefined || shared) return;\n if (ArrayBuffer.isView(items)) items.set(written, from);\n else for (let i = 0; i < written.length; i++) items[from + i] = written[i];\n }),\n );\n }\n\n await Promise.all(parts);\n}\n\n// `make([]T, n, shared)`: the memory of a typed array that the workers of `go for` write into,\n// without copies. A page has SharedArrayBuffer only with cross-origin isolation.\nexport function $$shared(bytes) {\n if (typeof SharedArrayBuffer !== 'function') {\n throw new Error(\n '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',\n );\n }\n\n return new SharedArrayBuffer(bytes);\n}\n\n// The worker: runs the calls that the pool sends and sends back their results or errors. Once the\n// thread is free again, it says so, and the pool may send another call.\nexport async function $$serve(functions) {\n const threads = 'node:worker_threads';\n const port =\n typeof postMessage === 'function'\n ? globalThis\n : (await import(/* @vite-ignore */ threads)).parentPort;\n const registry = $$registry();\n\n // The port to the main thread keeps the worker running.\n registry.idle = (key) => key !== registry.main && registry.ports.get(key)?.unref?.();\n port.postMessage({ hello: registry.key });\n\n const free = () => port.postMessage({ free: true, owns: registry.states.size > 0 });\n const receive = async (data) => {\n if ('op' in data || 'answer' in data) return $$incoming(registry.main, data);\n if ('init' in data) {\n registry.main = data.init;\n\n return $$connect(data.init, port, false);\n }\n\n if ('port' in data) return $$connect(data.port, data.through, true);\n if ('gone' in data) return $$gone(data.gone);\n setTimeout(free);\n $$adopt(data.args);\n\n const { main } = registry;\n\n try {\n const value = await functions[data.name](...data.args);\n\n $$post(main, { returned: data.call, value, owns: registry.states.size > 0 });\n } catch (error) {\n try {\n $$post(main, { returned: data.call, error, owns: registry.states.size > 0 });\n } catch {\n $$post(main, { returned: data.call, error: new Error(String(error)), owns: true });\n }\n }\n };\n\n if (port.on) port.on('message', receive);\n else port.onmessage = (event) => receive(event.data);\n}\n\n// `cancel(task)`: stops a goroutine of `go`, whose Promise fails with \"goroutine cancelled\". A call\n// in a worker stops with its worker; inside a goroutine, `go` runs in the same worker, so its\n// Promise fails but code that has started goes on.\nexport function $$cancel(task) {\n globalThis[Symbol.for('rook.goroutines')]?.cancel(task);\n $$registry().cancels.get(task)?.();\n}\n",
8
8
  'markup.js': "// Markup: the owners of the blocks that `{if}`, `{switch}` and `{for}` remove, the\n// `mount()` of components, the blocks themselves, elements and the CSS of components.\n\nimport { $$runDeferred } from './core.js';\n\n// Owners of markup that `{if}`, `{switch}` and `{for}` may remove: the functions returned by the\n// `mount()` of its components, and the removal of the blocks inside it, run when it is removed.\n// The current owner is shared by the modules of a page, since the blocks of one module may hold\n// the components of another.\nexport const $$owner = Symbol.for('rook.owner');\n\nexport function $$owned(create) {\n const owner = { removed: false, cleanups: [] };\n const parent = globalThis[$$owner];\n\n globalThis[$$owner] = owner;\n\n let result;\n\n try {\n result = create();\n } finally {\n globalThis[$$owner] = parent;\n }\n\n const remove = () => {\n if (owner.removed) return;\n owner.removed = true;\n $$runDeferred(owner.cleanups);\n };\n\n return [result, remove];\n}\n\nexport function $$onRemove(cleanup) {\n globalThis[$$owner]?.cleanups.push(cleanup);\n}\n\n// `mount() { ... }` of a component: runs when its markup is in the document. That is checked after\n// the code that creates the component; markup that is not in the document yet is waited for with a\n// MutationObserver, which watches only while some markup waits. Markup that its owner removes\n// before that never mounts. The function `mount` returns runs when the owner is removed.\nexport const $$waiting = new Set();\nexport let $$observer = null;\n\nexport function $$mount(node, mount) {\n const entry = { node, mount, owner: globalThis[$$owner] };\n\n entry.owner?.cleanups.push(() => $$waiting.delete(entry));\n queueMicrotask(() => $$tryMount(entry));\n}\n\nexport function $$tryMount(entry) {\n if (entry.owner?.removed) return;\n\n const node = entry.node();\n\n if (node != null && !node.isConnected) {\n $$waiting.add(entry);\n if (!$$observer) {\n $$observer = new MutationObserver($$mountWaiting);\n $$observer.observe(node.ownerDocument ?? document, { childList: true, subtree: true });\n }\n\n return;\n }\n\n const cleanup = entry.mount();\n\n if (typeof cleanup === 'function') entry.owner?.cleanups.push(cleanup);\n}\n\nexport function $$mountWaiting() {\n for (const entry of $$waiting) {\n if (!entry.node().isConnected) continue;\n $$waiting.delete(entry);\n $$tryMount(entry);\n }\n\n if ($$waiting.size === 0) {\n $$observer.disconnect();\n $$observer = null;\n }\n}\n\n// Appends element content whose type is not known: null adds nothing, arrays add each item.\nexport function $$append(parent, value) {\n if (value == null || value === false) return;\n if (Array.isArray(value)) {\n for (const item of value) $$append(parent, item);\n } else {\n parent.append(value);\n }\n}\n\n// Nodes of markup that change, `{list}` or `{ok ? <b /> : null}`: they are kept before an end\n// marker. Nodes that stay are moved rather than created again. Returns the markers with the first\n// nodes, to be inserted, and the update.\nexport function $$content(value) {\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(end);\n\n let nodes = [];\n let remove;\n const update = () => {\n const next = [];\n const add = (item) => {\n if (item == null || item === false) return;\n if (Array.isArray(item)) {\n for (const each of item) add(each);\n } else if (item.nodeType === 11) {\n next.push(...item.childNodes);\n } else {\n next.push(typeof item === 'object' ? item : document.createTextNode(String(item)));\n }\n };\n const [created, removeCreated] = $$owned(value);\n\n add(created);\n remove?.();\n remove = removeCreated;\n\n const kept = new Set(next);\n\n for (const node of nodes) if (!kept.has(node)) node.remove();\n end.before(...next);\n nodes = next;\n };\n\n update();\n $$onRemove(() => remove?.());\n\n return [fragment, update];\n}\n\n// `{if ...}` and `{switch ...}` in markup: shows the block that `choose` picks (-1 for none) between\n// two markers. The block is created again only when the choice changes; otherwise it is updated.\nexport function $$branches(choose, blocks) {\n const start = document.createTextNode('');\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(start, end);\n\n let current = null;\n let update;\n let remove;\n const render = () => {\n const next = choose();\n\n if (next === current) {\n update?.();\n\n return;\n }\n\n current = next;\n remove?.();\n while (start.nextSibling !== end) start.nextSibling.remove();\n update = undefined;\n remove = undefined;\n if (next >= 0) {\n const [[content, blockUpdate], removeBlock] = $$owned(blocks[next]);\n\n end.before(content);\n update = blockUpdate;\n remove = removeBlock;\n }\n };\n\n render();\n $$onRemove(() => remove?.());\n\n return [fragment, render];\n}\n\n// `<Page />` whose component is a value that changes: when it does, the block that `create` makes\n// with it is created again between two markers; otherwise the block is updated.\nexport function $$dynamic(component, create) {\n const start = document.createTextNode('');\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(start, end);\n\n let current;\n let update;\n let remove;\n const render = () => {\n const next = component();\n\n if (remove && next === current) {\n update?.();\n\n return;\n }\n\n current = next;\n remove?.();\n while (start.nextSibling !== end) start.nextSibling.remove();\n\n const [[content, blockUpdate], removeBlock] = $$owned(() => create(next));\n\n end.before(content);\n update = blockUpdate;\n remove = removeBlock;\n };\n\n render();\n $$onRemove(() => remove?.());\n\n return [fragment, render];\n}\n\n// `{for item in items { ... }}` in markup: a block for each item, between its own markers. On\n// updates the blocks are found again by the item itself: blocks of items that stay are updated and\n// moved into place, others are created or removed.\nexport function $$list(items, create) {\n const end = document.createTextNode('');\n const fragment = document.createDocumentFragment();\n\n fragment.append(end);\n\n let blocks = [];\n const nodesOf = (block) => {\n const nodes = [];\n\n for (let node = block.start; node !== block.end; node = node.nextSibling) nodes.push(node);\n nodes.push(block.end);\n\n return nodes;\n };\n const update = () => {\n const old = new Map();\n\n for (const block of blocks) {\n const same = old.get(block.item);\n\n if (same) same.push(block);\n else old.set(block.item, [block]);\n }\n\n const next = [];\n let index = 0;\n\n for (const item of items()) {\n let block = old.get(item)?.shift();\n\n if (block) {\n block.update?.(index);\n } else {\n const [[content, blockUpdate], remove] = $$owned(() => create(item, index));\n\n block = { item, start: document.createTextNode(''), end: document.createTextNode('') };\n block.update = blockUpdate;\n block.remove = remove;\n block.content = document.createDocumentFragment();\n block.content.append(block.start, content, block.end);\n }\n\n next.push(block);\n index++;\n }\n\n for (const same of old.values()) {\n for (const block of same) {\n block.remove();\n for (const node of nodesOf(block)) node.remove();\n }\n }\n\n let before = end;\n\n for (let i = next.length - 1; i >= 0; i--) {\n const block = next[i];\n\n if (block.content) {\n before.before(block.content);\n block.content = null;\n } else if (block.end.nextSibling !== before) {\n before.before(...nodesOf(block));\n }\n\n before = block.start;\n }\n\n blocks = next;\n };\n\n update();\n $$onRemove(() => {\n for (const block of blocks) block.remove();\n });\n\n return [fragment, update];\n}\n\n// Creates an element with its properties and the attributes that are set with setAttribute.\nexport function $$element(tag, properties, attributes) {\n const element = Object.assign(document.createElement(tag), properties);\n\n for (const name in attributes) element.setAttribute(name, attributes[name]);\n\n return element;\n}\n\n// Creates an SVG element with its attributes.\nexport function $$svgElement(tag, attributes) {\n const element = document.createElementNS('http://www.w3.org/2000/svg', tag);\n\n for (const name in attributes) element.setAttribute(name, attributes[name]);\n\n return element;\n}\n\n// Sets an attribute whose value may be null: then the attribute is removed.\nexport function $$attribute(element, name, value) {\n if (value == null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n}\n\n// Adds the CSS of a component to the page once: modules that inline the same component add it with\n// the same id. `styles` keeps the CSS of the module for the shadow roots of its web components,\n// which the styles of the page do not reach.\nexport function $$style(styles, id, css) {\n styles.push(css);\n if (document.getElementById(id)) return;\n\n const style = document.createElement('style');\n\n style.id = id;\n style.textContent = css;\n document.head.append(style);\n}\n",
9
9
  'queue.js': '// The class of `queue T`. Its values are a ring in an array: `shift()` and `unshift()` move the\n// start instead of the other values, as they do in an array. A full ring is copied into one twice\n// its size, and one that is mostly empty into one half its size, so each operation costs a few\n// steps on average. The fields are data, so a copy for another thread keeps the queue, and the\n// thread that gets it gives it back its class (threads.js).\n\nexport class $$Queue {\n constructor(items = []) {\n this.$$queue = [...items];\n this.$$start = 0;\n this.$$length = this.$$queue.length;\n }\n\n get length() {\n return this.$$length;\n }\n\n push(...items) {\n for (const item of items) {\n if (this.$$length === this.$$queue.length) this.$$resize();\n this.$$queue[(this.$$start + this.$$length++) % this.$$queue.length] = item;\n }\n\n return this.$$length;\n }\n\n unshift(...items) {\n for (let i = items.length - 1; i >= 0; i--) {\n if (this.$$length === this.$$queue.length) this.$$resize();\n this.$$start = (this.$$start || this.$$queue.length) - 1;\n this.$$queue[this.$$start] = items[i];\n this.$$length++;\n }\n\n return this.$$length;\n }\n\n // An empty queue gives null; a value that is taken out is not held any more.\n shift() {\n if (this.$$length === 0) return null;\n\n const item = this.$$queue[this.$$start];\n\n this.$$queue[this.$$start] = undefined;\n this.$$start = (this.$$start + 1) % this.$$queue.length;\n this.$$length--;\n this.$$shrink();\n\n return item;\n }\n\n pop() {\n if (this.$$length === 0) return null;\n\n const at = (this.$$start + --this.$$length) % this.$$queue.length;\n const item = this.$$queue[at];\n\n this.$$queue[at] = undefined;\n this.$$shrink();\n\n return item;\n }\n\n // A position from the start, or from the end if it is negative, as with an array.\n at(index) {\n const i = Math.trunc(index) || 0;\n const at = i < 0 ? i + this.$$length : i;\n\n if (at < 0 || at >= this.$$length) return null;\n\n return this.$$queue[(this.$$start + at) % this.$$queue.length];\n }\n\n clear() {\n this.$$queue = [];\n this.$$start = 0;\n this.$$length = 0;\n }\n\n *[Symbol.iterator]() {\n for (let i = 0; i < this.$$length; i++) {\n yield this.$$queue[(this.$$start + i) % this.$$queue.length];\n }\n }\n\n // Removes a value, for the channels: at the start it costs a step, elsewhere the whole queue.\n $$delete(item) {\n if (this.$$length > 0 && this.$$queue[this.$$start] === item) return this.shift();\n\n const items = [...this];\n const at = items.indexOf(item);\n\n if (at >= 0) this.$$keep((_, i) => i !== at);\n }\n\n // Keeps the values that pass the test, in their order.\n $$keep(test) {\n const items = [...this].filter(test);\n\n this.$$queue = items;\n this.$$start = 0;\n this.$$length = items.length;\n this.$$shrink();\n }\n\n // The values from the start of a new ring with room for as many again, and at least 8.\n $$resize() {\n const items = [...this];\n\n items.length = Math.max(8, this.$$length * 2);\n this.$$queue = items;\n this.$$start = 0;\n }\n\n $$shrink() {\n if (this.$$queue.length > 8 && this.$$length * 4 <= this.$$queue.length) this.$$resize();\n }\n}\n',
10
10
  'select.js': "// `select`. Its cases may wait in channels of several threads; a channel that can go on with a case\n// asks the thread of the select to lock it first, so that only one case goes on, as Go locks the\n// channels of a select.\n\nimport { $$finish, $$pump, $$zero } from './chan.js';\nimport { $$ask, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';\n\n// `select`: the index of the case that went on, and what it received. A case is { channel } to\n// receive or { channel, value } to send; from another thread, { channel, send, value }. One that\n// can go on at once wins, a random one as in Go; else `default`, -1, if there is one; else the\n// first that can go on later. A case with a null channel never goes on.\nexport async function $$select(cases, hasDefault) {\n const registry = $$registry();\n const all = cases.map((each, i) => ({\n i,\n id: typeof each.channel === 'string' ? each.channel : (each.channel?.$$chan ?? null),\n send: each.send ?? 'value' in each,\n value: each.value,\n }));\n const live = all.filter(({ id }) => id !== null);\n const owners = new Set(live.map(({ id }) => $$ownerOf(id)));\n\n // The channels of one other thread: the select runs there.\n if (owners.size === 1 && !owners.has(registry.key)) {\n const [owner] = owners;\n\n return $$ask(owner, {\n op: 'select',\n cases: all.map(({ id: channel, send, value }) => ({ channel, send, value })),\n hasDefault,\n });\n }\n\n const order = live.map(({ i }) => i).sort(() => Math.random() - 0.5);\n\n for (const i of order) {\n const { id, send, value } = all[i];\n const owner = $$ownerOf(id);\n const ready =\n owner === registry.key\n ? $$try(id, send, value)\n : await $$ask(owner, { op: 'try', channel: id, send, value });\n\n if (ready) return [i, ...ready];\n }\n\n if (hasDefault) return [-1];\n\n return new Promise((resolve, reject) => {\n const sid = registry.asks++;\n const channels = new Map();\n\n registry.selects.set(sid, { locked: false, resolve, reject, channels });\n live.forEach(({ i, id, send, value }) => {\n const owner = $$ownerOf(id);\n\n if (!channels.has(owner)) channels.set(owner, []);\n channels.get(owner).push(id);\n if (owner === registry.key) {\n const entry = { select: { key: owner, sid }, case: i, value };\n\n registry.states.get(id)[send ? 'senders' : 'receivers'].push(entry);\n } else {\n $$post(owner, { op: 'wait', sid, case: i, channel: id, send, value });\n }\n });\n for (const owner of channels.keys()) if (owner !== registry.key) $$pin(owner, 1);\n for (const id of channels.get(registry.key) ?? []) $$pump(id);\n });\n}\n\n// What a case of a select gets at once, if it can go on: [] for a send, [value, ok] for a receive.\n// A receiver or sender that a select of another thread waits with is left to $$pump.\nexport function $$try(id, send, value) {\n const state = $$registry().states.get(id);\n\n if (!state) throw $$stopped();\n if (state.busy) return null;\n if (send && state.closed) throw new Error('send on a closed channel');\n if (!send && state.buffer.length > 0) {\n const received = state.buffer.shift();\n\n $$pump(id);\n\n return [received, true];\n }\n\n const queue = send ? state.receivers : state.senders;\n\n while (queue.length > 0) {\n const entry = queue.at(0);\n\n if (entry.select && entry.select.key !== $$registry().key) break;\n\n const lock = entry.select ? $$lock(entry.select.sid) : 'locked';\n\n if (lock === 'busy') break;\n queue.shift();\n if (lock === 'done') continue;\n if (!send) {\n $$finish(entry);\n\n return [entry.value, true];\n }\n\n $$finish(entry, [value, true]);\n\n return [];\n }\n\n if (send && state.buffer.length < state.size) {\n state.buffer.push(value);\n $$pump(id);\n\n return [];\n }\n\n return !send && state.closed ? [$$zero(state), false] : null;\n}\n\n// The thread of a select decides which of its cases goes on: a channel locks the select, then\n// commits a case or unlocks it.\nexport function $$lock(sid) {\n const select = $$registry().selects.get(sid);\n\n if (!select) return 'done';\n if (select.locked) return 'busy';\n select.locked = true;\n\n return 'locked';\n}\n\nexport function $$unlock(sid) {\n const select = $$registry().selects.get(sid);\n\n if (select) select.locked = false;\n}\n\nexport function $$commit(sid, i, result, error) {\n const registry = $$registry();\n const select = registry.selects.get(sid);\n\n if (!select) return;\n registry.selects.delete(sid);\n // The other cases leave the queues of their channels.\n for (const [owner, channels] of select.channels) {\n if (owner === registry.key) {\n $$unwait({ key: owner, sid }, channels);\n } else {\n $$post(owner, { op: 'cancel', sid, channels });\n $$pin(owner, -1);\n }\n }\n\n if (error) select.reject(error);\n else select.resolve([i, ...(result ?? [])]);\n}\n\nexport function $$unwait(select, channels) {\n const { states } = $$registry();\n const other = (entry) => entry.select?.key !== select.key || entry.select.sid !== select.sid;\n\n for (const id of channels) {\n const state = states.get(id);\n\n if (!state) continue;\n state.receivers.$$keep(other);\n state.senders.$$keep(other);\n }\n}\n",
@@ -0,0 +1 @@
1
+ export declare const RUNTIME_MODULE = "rooklang/runtime";
@@ -0,0 +1,3 @@
1
+ // Where compiled code imports the runtime from, in a module of its own: the loader of `rook run`
2
+ // needs the name in every worker, without the code generator and the text of the runtime.
3
+ export const RUNTIME_MODULE = 'rooklang/runtime';
@@ -1,6 +1,10 @@
1
1
  import { type FunctionType, type Type } from './checker/types.ts';
2
2
  /** A nullable property may be left out of an object: `name?: string | null`. */
3
3
  export declare function property(name: string, type: Type): string;
4
+ /**
5
+ * `(User, error)` → `[User, null] | [null, Error]`: the value is null only together with an error
6
+ * (error-results.ts), and TypeScript narrows such a tuple when it is unpacked into constants.
7
+ */
4
8
  export declare function resultType(results: readonly Type[]): string;
5
9
  /**
6
10
  * A type in TypeScript syntax. A union keeps the name of its alias, except in the alias itself
package/dist/ts-types.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CONTENT } from "./checker/dom.js";
2
+ import { guardedByError } from "./checker/error-results.js";
2
3
  import { ERROR_CLASS, isQueue, memberNames, queueElement, } from "./checker/types.js";
3
4
  // Types of the checker in TypeScript syntax, for declaration files (declarations.ts).
4
5
  /** A nullable property may be left out of an object: `name?: string | null`. */
@@ -6,12 +7,24 @@ export function property(name, type) {
6
7
  const optional = type.kind === 'nullable' || type.kind === 'null' ? '?' : '';
7
8
  return `${name}${optional}: ${tsType(type)};`;
8
9
  }
10
+ /**
11
+ * `(User, error)` → `[User, null] | [null, Error]`: the value is null only together with an error
12
+ * (error-results.ts), and TypeScript narrows such a tuple when it is unpacked into constants.
13
+ */
9
14
  export function resultType(results) {
10
15
  if (results.length === 0)
11
16
  return 'void';
12
17
  if (results.length === 1)
13
18
  return tsType(results[0]);
14
- return `[${results.map((result) => tsType(result)).join(', ')}]`;
19
+ const guarded = guardedByError(results);
20
+ const tuple = (types) => `[${types.join(', ')}]`;
21
+ const types = results.map((result) => tsType(result));
22
+ if (guarded.length === 0)
23
+ return tuple(types);
24
+ const last = results.length - 1;
25
+ const ok = types.map((type, i) => (i === last ? 'null' : type));
26
+ const failed = types.map((type, i) => i === last ? 'Error' : guarded.includes(i) ? 'null' : type);
27
+ return `${tuple(ok)} | ${tuple(failed)}`;
15
28
  }
16
29
  /**
17
30
  * A type in TypeScript syntax. A union keeps the name of its alias, except in the alias itself
@@ -40,8 +53,9 @@ export function tsType(type, expand = false) {
40
53
  return type.types.map(grouped).join(' | ');
41
54
  case 'array':
42
55
  return `${element(type.element)}[]`;
56
+ // The results of an async function, in its Promise.
43
57
  case 'tuple':
44
- return `[${type.types.map((each) => tsType(each)).join(', ')}]`;
58
+ return resultType(type.types);
45
59
  case 'function':
46
60
  return functionType(type);
47
61
  // The function of the component: it returns the node and a function that sets the properties.
package/dist/vite.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { RUNTIME_MODULE } from "./codegen/runtime.js";
4
+ import { RUNTIME_MODULE } from "./runtime-module.js";
5
5
  import { formatDiagnostic } from "./diagnostics.js";
6
6
  import { compile } from "./index.js";
7
7
  import { SourceFile } from "./source.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rooklang",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "A language with JavaScript semantics, Go-style syntax, goroutines on real threads and static types that compiles to readable JavaScript",
5
5
  "keywords": [
6
6
  "rooklang",
package/src/runtime/go.js CHANGED
@@ -238,12 +238,18 @@ export function $$goHere(functions, fn, args) {
238
238
  return task;
239
239
  }
240
240
 
241
- // `go for`: the array is split into parts, one per processor, each part runs as a goroutine of `go`
242
- // of the module, and
243
- // the loop waits for all of them. A typed array on a SharedArrayBuffer is shared with the workers,
244
- // which write into it; other parts are copies, moved to the workers and back into the array.
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.
245
248
  export async function $$goFor(go, name, items, ...captured) {
246
- const count = Math.min(globalThis.navigator?.hardwareConcurrency ?? 4, items.length);
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);
247
253
  const shared =
248
254
  typeof SharedArrayBuffer === 'function' && items.buffer instanceof SharedArrayBuffer;
249
255
  const parts = [];