rooklang 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/dist/ast/declarations.d.ts +19 -3
- package/dist/ast/expressions.d.ts +4 -8
- package/dist/ast/statements.d.ts +3 -1
- package/dist/checker/assignments.d.ts +2 -0
- package/dist/checker/assignments.js +9 -3
- package/dist/checker/base.d.ts +8 -2
- package/dist/checker/base.js +3 -3
- package/dist/checker/builtins.js +7 -3
- package/dist/checker/channels.js +1 -2
- package/dist/checker/classes.js +7 -1
- package/dist/checker/closing.js +1 -1
- package/dist/checker/completion.d.ts +1 -1
- package/dist/checker/completion.js +2 -1
- package/dist/checker/component-bindings.d.ts +7 -0
- package/dist/checker/component-bindings.js +45 -0
- package/dist/checker/component-values.js +10 -7
- package/dist/checker/components.d.ts +2 -2
- package/dist/checker/components.js +8 -3
- package/dist/checker/context.d.ts +28 -5
- package/dist/checker/context.js +23 -0
- package/dist/checker/control-flow.d.ts +6 -5
- package/dist/checker/control-flow.js +48 -40
- package/dist/checker/declarations.d.ts +2 -2
- package/dist/checker/declarations.js +3 -2
- package/dist/checker/decorator-exports.js +4 -0
- package/dist/checker/decorator-includes.d.ts +9 -0
- package/dist/checker/decorator-includes.js +71 -0
- package/dist/checker/decorator-members.d.ts +8 -10
- package/dist/checker/decorator-members.js +28 -31
- package/dist/checker/decorator-wrappers.d.ts +1 -1
- package/dist/checker/decorator-wrappers.js +3 -2
- package/dist/checker/decorators.d.ts +14 -5
- package/dist/checker/decorators.js +52 -25
- package/dist/checker/expressions.d.ts +1 -1
- package/dist/checker/expressions.js +7 -6
- package/dist/checker/flow.d.ts +25 -3
- package/dist/checker/flow.js +48 -8
- package/dist/checker/functions.d.ts +2 -2
- package/dist/checker/functions.js +9 -5
- package/dist/checker/helpers.d.ts +4 -1
- package/dist/checker/helpers.js +7 -2
- package/dist/checker/imports.js +5 -2
- package/dist/checker/loops.d.ts +2 -0
- package/dist/checker/loops.js +27 -2
- package/dist/checker/markup.js +14 -2
- package/dist/checker/members.js +3 -1
- package/dist/checker/shared-state.d.ts +11 -0
- package/dist/checker/shared-state.js +37 -0
- package/dist/checker/statements.d.ts +5 -2
- package/dist/checker/statements.js +51 -22
- package/dist/checker/tagged-unions.d.ts +37 -0
- package/dist/checker/tagged-unions.js +132 -0
- package/dist/checker/untyped-globals.d.ts +1 -0
- package/dist/checker/untyped-globals.js +3 -1
- package/dist/codegen/attributes.d.ts +4 -0
- package/dist/codegen/attributes.js +24 -2
- package/dist/codegen/component-helpers.d.ts +6 -1
- package/dist/codegen/component-helpers.js +6 -1
- package/dist/codegen/component-props.d.ts +11 -2
- package/dist/codegen/component-props.js +50 -5
- package/dist/codegen/components.d.ts +6 -5
- package/dist/codegen/components.js +44 -21
- package/dist/codegen/control-flow.d.ts +2 -0
- package/dist/codegen/control-flow.js +23 -9
- package/dist/codegen/creation.js +5 -2
- package/dist/codegen/early-returns.d.ts +4 -3
- package/dist/codegen/early-returns.js +17 -5
- package/dist/codegen/emitter.d.ts +3 -0
- package/dist/codegen/emitter.js +18 -4
- package/dist/codegen/expressions.js +0 -2
- package/dist/codegen/functions.d.ts +11 -1
- package/dist/codegen/functions.js +31 -3
- package/dist/codegen/live.d.ts +60 -0
- package/dist/codegen/live.js +29 -0
- package/dist/codegen/mutations.d.ts +7 -0
- package/dist/codegen/mutations.js +43 -2
- package/dist/codegen/options.d.ts +2 -0
- package/dist/codegen/reactive.d.ts +4 -59
- package/dist/codegen/reactive.js +34 -72
- package/dist/codegen/runtime.js +6 -1
- package/dist/codegen/shared.d.ts +22 -0
- package/dist/codegen/shared.js +93 -0
- package/dist/codegen/simple-statements.d.ts +1 -1
- package/dist/codegen/statements.d.ts +5 -0
- package/dist/codegen/statements.js +35 -0
- package/dist/codegen/updates.d.ts +11 -3
- package/dist/codegen/updates.js +52 -4
- package/dist/codegen/web-components.d.ts +2 -2
- package/dist/codegen/web-components.js +2 -2
- package/dist/compile.js +6 -2
- package/dist/decorator-copy.d.ts +1 -1
- package/dist/decorator-copy.js +1 -0
- package/dist/decorator-names.d.ts +14 -0
- package/dist/decorator-names.js +56 -0
- package/dist/decorators.d.ts +2 -3
- package/dist/decorators.js +36 -14
- package/dist/inline-packages.d.ts +2 -1
- package/dist/inline-packages.js +9 -4
- package/dist/names.d.ts +4 -0
- package/dist/names.js +29 -0
- package/dist/parser/base.d.ts +1 -0
- package/dist/parser/base.js +1 -1
- package/dist/parser/control-flow.d.ts +5 -0
- package/dist/parser/control-flow.js +28 -2
- package/dist/parser/declarations.d.ts +5 -15
- package/dist/parser/declarations.js +15 -119
- package/dist/parser/decorators.d.ts +6 -2
- package/dist/parser/decorators.js +43 -19
- package/dist/parser/params.d.ts +21 -0
- package/dist/parser/params.js +143 -0
- package/dist/parser/simple-statements.d.ts +1 -0
- package/dist/parser/simple-statements.js +22 -6
- package/dist/parser/statements.d.ts +5 -2
- package/dist/parser/statements.js +18 -11
- package/dist/runtime/sources.js +3 -2
- package/dist/walk.js +3 -3
- package/package.json +2 -2
- package/src/runtime/decode.js +41 -10
- package/src/runtime/index.js +1 -0
- package/src/runtime/markup.js +27 -17
- package/src/runtime/shared.js +17 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DeclarationParser } from "./declarations.js";
|
|
2
2
|
import { describe, FUNCTION_BODY } from "./syntax.js";
|
|
3
3
|
// Decorators: their declarations `dec name(...) { ... }`, the decorators written before `comp`
|
|
4
|
-
// (`@html-tag` and `@visible("300px")`), and the access to their members,
|
|
5
|
-
//
|
|
4
|
+
// (`@html-tag` and `@visible("300px")`), and the access to their members, `@visible.shown` and
|
|
5
|
+
// `@visible.show()`.
|
|
6
6
|
export class DecoratorParser extends DeclarationParser {
|
|
7
7
|
/** `dec visible(margin string = "0px") { ... }`: `dec` is a keyword only here. */
|
|
8
8
|
isDecoratorDeclaration() {
|
|
@@ -17,6 +17,7 @@ export class DecoratorParser extends DeclarationParser {
|
|
|
17
17
|
const name = this.parseIdentifier('decorator name');
|
|
18
18
|
const params = this.parseParams(false, true);
|
|
19
19
|
const needs = this.parseNeeds();
|
|
20
|
+
const includes = this.parseIncludes();
|
|
20
21
|
const body = this.withContext({ ...FUNCTION_BODY, inComponent: true }, () => this.parseDecoratorBody());
|
|
21
22
|
return {
|
|
22
23
|
kind: 'DecoratorDeclaration',
|
|
@@ -24,6 +25,7 @@ export class DecoratorParser extends DeclarationParser {
|
|
|
24
25
|
name,
|
|
25
26
|
params,
|
|
26
27
|
needs,
|
|
28
|
+
includes,
|
|
27
29
|
body,
|
|
28
30
|
...this.span(start),
|
|
29
31
|
};
|
|
@@ -41,6 +43,34 @@ export class DecoratorParser extends DeclarationParser {
|
|
|
41
43
|
needs.push(this.parseIdentifier('decorator name'));
|
|
42
44
|
return needs;
|
|
43
45
|
}
|
|
46
|
+
/** `includes visible(margin), cart` after `needs`: `includes` is a keyword only here. */
|
|
47
|
+
parseIncludes() {
|
|
48
|
+
if (!this.checkWord('includes'))
|
|
49
|
+
return [];
|
|
50
|
+
if (this.peek(1).kind !== 'Identifier') {
|
|
51
|
+
this.fail('"includes" is followed by the decorators this one applies, e.g. includes cart');
|
|
52
|
+
}
|
|
53
|
+
this.next();
|
|
54
|
+
const includes = [this.parseIncluded()];
|
|
55
|
+
while (this.accept(','))
|
|
56
|
+
includes.push(this.parseIncluded());
|
|
57
|
+
return includes;
|
|
58
|
+
}
|
|
59
|
+
/** `visible(margin)` or `cart`: like a decorator before `comp`, without the `@`. */
|
|
60
|
+
parseIncluded() {
|
|
61
|
+
const name = this.parseIdentifier('decorator name');
|
|
62
|
+
const args = this.check('(') ? this.parseArguments() : [];
|
|
63
|
+
const spread = args.find((arg) => arg.kind === 'SpreadElement');
|
|
64
|
+
if (spread) {
|
|
65
|
+
this.error('arguments of a decorator cannot be spread', spread.start, spread.end);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
kind: 'DecoratorUse',
|
|
69
|
+
name,
|
|
70
|
+
arguments: args.filter((arg) => arg.kind !== 'SpreadElement'),
|
|
71
|
+
...this.span(name.start),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
44
74
|
/** Like the body of a component, but `state`, `const` and `func` may be `public`. */
|
|
45
75
|
parseDecoratorBody() {
|
|
46
76
|
const start = this.expect('{').start;
|
|
@@ -71,8 +101,11 @@ export class DecoratorParser extends DeclarationParser {
|
|
|
71
101
|
declaration.isPublic = true;
|
|
72
102
|
return declaration;
|
|
73
103
|
}
|
|
74
|
-
if (!this.check('const') &&
|
|
75
|
-
this.
|
|
104
|
+
if (!this.check('const') &&
|
|
105
|
+
!this.check('let') &&
|
|
106
|
+
!this.isStateDeclaration() &&
|
|
107
|
+
!this.isRefDeclaration()) {
|
|
108
|
+
this.fail(`only state, derived, const, ref and func can be public, not ${describe(this.peek())}`);
|
|
76
109
|
}
|
|
77
110
|
const declaration = this.parseVariableDeclaration(modifier.start, false);
|
|
78
111
|
this.endStatement();
|
|
@@ -137,18 +170,7 @@ export class DecoratorParser extends DeclarationParser {
|
|
|
137
170
|
/** `@visible("300px")` or `@visible`: the arguments are positional, as in a call. */
|
|
138
171
|
parseDecoratorUse() {
|
|
139
172
|
const at = this.expect('@');
|
|
140
|
-
|
|
141
|
-
const args = this.check('(') ? this.parseArguments() : [];
|
|
142
|
-
const spread = args.find((arg) => arg.kind === 'SpreadElement');
|
|
143
|
-
if (spread) {
|
|
144
|
-
this.error('arguments of a decorator cannot be spread', spread.start, spread.end);
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
kind: 'DecoratorUse',
|
|
148
|
-
name,
|
|
149
|
-
arguments: args.filter((arg) => arg.kind !== 'SpreadElement'),
|
|
150
|
-
...this.span(at.start),
|
|
151
|
-
};
|
|
173
|
+
return { ...this.parseIncluded(), ...this.span(at.start) };
|
|
152
174
|
}
|
|
153
175
|
/** `@visible.show`: a member of a decorator; a statement may start with it. */
|
|
154
176
|
isDecoratorMember() {
|
|
@@ -162,7 +184,7 @@ export class DecoratorParser extends DeclarationParser {
|
|
|
162
184
|
const member = this.parsePropertyName('member name');
|
|
163
185
|
return { kind: 'DecoratorMember', decorator, member, ...this.span(at.start) };
|
|
164
186
|
}
|
|
165
|
-
/** `get(@visible.shown)
|
|
187
|
+
/** `get(@visible.shown)`, the old way to read a member: reported, and read as the member. */
|
|
166
188
|
isDecoratorGet() {
|
|
167
189
|
return this.checkWord('get') && this.peek(1).kind === '(' && this.peek(2).kind === '@';
|
|
168
190
|
}
|
|
@@ -170,7 +192,9 @@ export class DecoratorParser extends DeclarationParser {
|
|
|
170
192
|
const start = this.next().start;
|
|
171
193
|
this.expect('(');
|
|
172
194
|
const target = this.parseDecoratorMember();
|
|
173
|
-
|
|
174
|
-
|
|
195
|
+
const { decorator, member } = target;
|
|
196
|
+
this.expect(')');
|
|
197
|
+
this.error(`read a member of a decorator without get: @${decorator.name}.${member.name}`, start, this.span(start).end);
|
|
198
|
+
return target;
|
|
175
199
|
}
|
|
176
200
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as ast from '../ast.ts';
|
|
2
|
+
import { JumpParser } from './jumps.ts';
|
|
3
|
+
export declare abstract class ParamParser extends JumpParser {
|
|
4
|
+
/**
|
|
5
|
+
* `(a, b number, c string)`. Names without a type take the type of the next typed name, as in
|
|
6
|
+
* Go; only arrow functions may leave types out entirely. The parameters of a constructor can be
|
|
7
|
+
* properties: `(private name string)`.
|
|
8
|
+
*/
|
|
9
|
+
protected parseParams(typesOptional: boolean, allowDefaults?: boolean, allowRest?: boolean, allowProperties?: boolean): ast.Parameter[];
|
|
10
|
+
/** `private`, `readonly` or both before a parameter that is a property of its class. */
|
|
11
|
+
private parsePropertyModifiers;
|
|
12
|
+
/**
|
|
13
|
+
* `bind` before a property of a component, `bind active string`. Followed by a name and then
|
|
14
|
+
* not by `,` or `)`, so a parameter named bind, `(bind Mode)`, stays one.
|
|
15
|
+
*/
|
|
16
|
+
private parseBindModifier;
|
|
17
|
+
/** `...attrs`: the attributes that a component passes on to one of its elements. */
|
|
18
|
+
private parseRestParam;
|
|
19
|
+
/** Result types after a parameter list: nothing, `T`, or `(T1, T2)`. */
|
|
20
|
+
protected parseResults(): ast.TypeNode[];
|
|
21
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { JumpParser } from "./jumps.js";
|
|
2
|
+
import { VISIBILITIES } from "./syntax.js";
|
|
3
|
+
// The parameters of functions, constructors and components, with their modifiers, and the results
|
|
4
|
+
// of functions.
|
|
5
|
+
export class ParamParser extends JumpParser {
|
|
6
|
+
/**
|
|
7
|
+
* `(a, b number, c string)`. Names without a type take the type of the next typed name, as in
|
|
8
|
+
* Go; only arrow functions may leave types out entirely. The parameters of a constructor can be
|
|
9
|
+
* properties: `(private name string)`.
|
|
10
|
+
*/
|
|
11
|
+
parseParams(typesOptional, allowDefaults = false, allowRest = false, allowProperties = false) {
|
|
12
|
+
this.expect('(');
|
|
13
|
+
const params = [];
|
|
14
|
+
let untyped = 0;
|
|
15
|
+
while (!this.check(')')) {
|
|
16
|
+
if (this.check('...')) {
|
|
17
|
+
params.push(this.parseRestParam(allowRest));
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
const paramStart = this.peek().start;
|
|
21
|
+
const property = this.parsePropertyModifiers(allowProperties);
|
|
22
|
+
const bind = this.parseBindModifier(allowRest);
|
|
23
|
+
const name = this.parseIdentifier('parameter name');
|
|
24
|
+
const type = this.canStartType() ? this.parseType() : null;
|
|
25
|
+
const defaultValue = allowDefaults && this.accept('=') ? this.parseExpression() : null;
|
|
26
|
+
if (bind && name.name === 'children') {
|
|
27
|
+
this.error('children cannot be bound: it is the content between the tags', paramStart, name.end);
|
|
28
|
+
}
|
|
29
|
+
params.push({
|
|
30
|
+
kind: 'Parameter',
|
|
31
|
+
name,
|
|
32
|
+
type,
|
|
33
|
+
defaultValue,
|
|
34
|
+
...(property ? { property } : {}),
|
|
35
|
+
...(bind ? { bind: true } : {}),
|
|
36
|
+
...this.span(paramStart),
|
|
37
|
+
});
|
|
38
|
+
if (type === null) {
|
|
39
|
+
untyped++;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const param of params.slice(params.length - 1 - untyped))
|
|
43
|
+
param.type = type;
|
|
44
|
+
untyped = 0;
|
|
45
|
+
}
|
|
46
|
+
if (!this.accept(','))
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
this.expect(')');
|
|
50
|
+
if (untyped > 0 && !typesOptional) {
|
|
51
|
+
const param = params[params.length - untyped];
|
|
52
|
+
this.error(`missing type for parameter "${param.name.name}"`, param.start, param.end);
|
|
53
|
+
}
|
|
54
|
+
return params;
|
|
55
|
+
}
|
|
56
|
+
/** `private`, `readonly` or both before a parameter that is a property of its class. */
|
|
57
|
+
parsePropertyModifiers(allowed) {
|
|
58
|
+
let visibility = null;
|
|
59
|
+
let readonly = false;
|
|
60
|
+
const start = this.peek().start;
|
|
61
|
+
// Elsewhere `readonly number` is a parameter named readonly, and `private x number` an error.
|
|
62
|
+
while (this.check('Identifier') &&
|
|
63
|
+
(VISIBILITIES.has(this.peek().text) || this.peek().text === 'readonly') &&
|
|
64
|
+
this.peek(1).kind === 'Identifier' &&
|
|
65
|
+
(allowed || this.peek(2).kind === 'Identifier')) {
|
|
66
|
+
const modifier = this.next();
|
|
67
|
+
if (modifier.text === 'readonly') {
|
|
68
|
+
if (readonly)
|
|
69
|
+
this.error('duplicate "readonly" modifier', modifier.start, modifier.end);
|
|
70
|
+
readonly = true;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
if (visibility)
|
|
74
|
+
this.error('duplicate visibility modifier', modifier.start, modifier.end);
|
|
75
|
+
visibility = modifier.text;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (visibility === null && !readonly)
|
|
79
|
+
return null;
|
|
80
|
+
if (!allowed) {
|
|
81
|
+
this.error('only the parameters of a constructor can be properties, e.g. constructor(private name string)', start, this.span(start).end);
|
|
82
|
+
}
|
|
83
|
+
return { visibility: visibility ?? 'public', ...(readonly ? { readonly: true } : {}) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `bind` before a property of a component, `bind active string`. Followed by a name and then
|
|
87
|
+
* not by `,` or `)`, so a parameter named bind, `(bind Mode)`, stays one.
|
|
88
|
+
*/
|
|
89
|
+
parseBindModifier(allowed) {
|
|
90
|
+
const after = this.peek(2).kind;
|
|
91
|
+
if (!this.checkWord('bind') ||
|
|
92
|
+
this.peek(1).kind !== 'Identifier' ||
|
|
93
|
+
after === ',' ||
|
|
94
|
+
after === ')') {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const modifier = this.next();
|
|
98
|
+
if (!allowed) {
|
|
99
|
+
this.error('only the properties of components can be bound, e.g. comp Tabs(bind active string)', modifier.start, modifier.end);
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/** `...attrs`: the attributes that a component passes on to one of its elements. */
|
|
104
|
+
parseRestParam(allowed) {
|
|
105
|
+
const start = this.next().start;
|
|
106
|
+
const name = this.parseIdentifier('parameter name');
|
|
107
|
+
if (!allowed) {
|
|
108
|
+
this.error(`...${name.name} is only for components: it takes the attributes of an element`, start, name.end);
|
|
109
|
+
}
|
|
110
|
+
if (this.canStartType() || this.check('=')) {
|
|
111
|
+
const at = this.peek().start;
|
|
112
|
+
// After the error, the type and the default are read, so they are not reported again.
|
|
113
|
+
if (this.canStartType())
|
|
114
|
+
this.parseType();
|
|
115
|
+
if (this.accept('='))
|
|
116
|
+
this.parseExpression();
|
|
117
|
+
this.error(`...${name.name} has no type and no default: it gets the type of the element it is spread on`, at, this.span(at).end);
|
|
118
|
+
}
|
|
119
|
+
// After the error, the rest of the list is skipped up to the `)`.
|
|
120
|
+
if (this.check(',')) {
|
|
121
|
+
this.error(`...${name.name} is the last property of a component`, start, name.end);
|
|
122
|
+
while (!this.check(')') && !this.check('EOF'))
|
|
123
|
+
this.next();
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
kind: 'Parameter',
|
|
127
|
+
name,
|
|
128
|
+
type: null,
|
|
129
|
+
defaultValue: null,
|
|
130
|
+
rest: true,
|
|
131
|
+
...this.span(start),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Result types after a parameter list: nothing, `T`, or `(T1, T2)`. */
|
|
135
|
+
parseResults() {
|
|
136
|
+
if (this.accept('('))
|
|
137
|
+
return this.parseList(')', () => this.parseType());
|
|
138
|
+
// `{` after the parameters starts the body, so an object result type needs parentheses.
|
|
139
|
+
if (this.canStartType() && !this.check('{'))
|
|
140
|
+
return [this.parseType()];
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -18,5 +18,6 @@ export declare abstract class SimpleStatementParser extends ParserBase {
|
|
|
18
18
|
protected parseExpressionList(): ast.Expression[];
|
|
19
19
|
/** `ref input` is one name, and the markup gives its element. */
|
|
20
20
|
private checkRef;
|
|
21
|
+
private parseDerivedBlock;
|
|
21
22
|
protected parseVariableDeclaration(start: number, exported: boolean): ast.VariableDeclaration;
|
|
22
23
|
}
|
|
@@ -67,7 +67,6 @@ export class SimpleStatementParser extends ParserBase {
|
|
|
67
67
|
checkAssignable(target) {
|
|
68
68
|
// Members of decorators are read-only outside them: the checker explains it.
|
|
69
69
|
const assignable = target.kind === 'Identifier' ||
|
|
70
|
-
target.kind === 'DecoratorGet' ||
|
|
71
70
|
target.kind === 'DecoratorMember' ||
|
|
72
71
|
((target.kind === 'MemberExpression' || target.kind === 'IndexExpression') &&
|
|
73
72
|
!target.optional);
|
|
@@ -125,6 +124,17 @@ export class SimpleStatementParser extends ParserBase {
|
|
|
125
124
|
this.error(`ref declares one name, e.g. ref ${name}`, start, end);
|
|
126
125
|
}
|
|
127
126
|
}
|
|
127
|
+
parseDerivedBlock() {
|
|
128
|
+
const body = this.parseFunctionBody();
|
|
129
|
+
return {
|
|
130
|
+
kind: 'FuncExpression',
|
|
131
|
+
params: [],
|
|
132
|
+
results: [],
|
|
133
|
+
body,
|
|
134
|
+
start: body.start,
|
|
135
|
+
end: body.end,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
128
138
|
parseVariableDeclaration(start, exported) {
|
|
129
139
|
const token = this.next();
|
|
130
140
|
const keyword = token.kind === 'const'
|
|
@@ -135,16 +145,21 @@ export class SimpleStatementParser extends ParserBase {
|
|
|
135
145
|
const names = [this.parseIdentifier('variable name')];
|
|
136
146
|
while (this.accept(','))
|
|
137
147
|
names.push(this.parseIdentifier('variable name'));
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
// `derived summary {` starts the block that computes it, as `{` after the parameters of a
|
|
149
|
+
// function starts its body: an object type goes in parentheses.
|
|
150
|
+
const block = () => keyword === 'derived' && this.check('{');
|
|
151
|
+
const type = !block() && this.canStartType() ? this.parseType() : null;
|
|
152
|
+
const body = block() ? this.parseDerivedBlock() : null;
|
|
153
|
+
const values = body === null && this.accept('=') ? this.parseExpressionList() : [];
|
|
140
154
|
const { end } = this.span(start);
|
|
141
155
|
if (keyword === 'ref') {
|
|
142
156
|
this.checkRef(names, values.length > 0, start, end);
|
|
143
157
|
}
|
|
144
|
-
else if (keyword === 'derived' &&
|
|
145
|
-
|
|
158
|
+
else if (keyword === 'derived' &&
|
|
159
|
+
(names.length !== 1 || values.length + Number(!!body) !== 1)) {
|
|
160
|
+
this.error('derived is one name and the expression it is computed from, e.g. derived double = count * 2, or a block that returns it: derived total number { ... return sum }', start, end);
|
|
146
161
|
}
|
|
147
|
-
else if (values.length === 0 && type === null) {
|
|
162
|
+
else if (values.length === 0 && type === null && body === null) {
|
|
148
163
|
this.error(`"${names[0].name}" needs a type or a value`, start, end);
|
|
149
164
|
}
|
|
150
165
|
else if (values.length === 0 && keyword === 'const') {
|
|
@@ -159,6 +174,7 @@ export class SimpleStatementParser extends ParserBase {
|
|
|
159
174
|
names,
|
|
160
175
|
type,
|
|
161
176
|
values,
|
|
177
|
+
body,
|
|
162
178
|
start,
|
|
163
179
|
end,
|
|
164
180
|
};
|
|
@@ -21,9 +21,12 @@ export declare abstract class StatementParser extends SimpleStatementParser {
|
|
|
21
21
|
protected parseStyle(): ast.StyleStatement;
|
|
22
22
|
/** `effect {`: like `state`, a keyword only at the start of a statement in a component. */
|
|
23
23
|
protected isEffectDeclaration(): boolean;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* `mount {`: like `state`, a keyword only at the start of a statement in a component. The old
|
|
26
|
+
* form `mount() {` is recognized too, so that parseMount can say how to write it now.
|
|
27
|
+
*/
|
|
25
28
|
protected isMountDeclaration(): boolean;
|
|
26
|
-
/** `mount
|
|
29
|
+
/** `mount { ... }`; its body is a function that runs later, so `return` is its own. */
|
|
27
30
|
protected parseMount(): ast.MountStatement;
|
|
28
31
|
/** `effect { ... }`: its body runs again later, so `return` and `defer` are its own. */
|
|
29
32
|
protected parseEffect(): ast.EffectStatement;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SimpleStatementParser } from "./simple-statements.js";
|
|
2
2
|
import { FOREIGN_KEYWORDS, FUNCTION_BODY, describe } from "./syntax.js";
|
|
3
|
-
// Statements: blocks, `mount
|
|
3
|
+
// Statements: blocks, `mount`, and which statement a token starts.
|
|
4
4
|
export class StatementParser extends SimpleStatementParser {
|
|
5
5
|
parseStatements(isEnd) {
|
|
6
6
|
const body = [];
|
|
@@ -95,14 +95,16 @@ export class StatementParser extends SimpleStatementParser {
|
|
|
95
95
|
const misplaced = this.isStateDeclaration() || this.isRefDeclaration()
|
|
96
96
|
? this.peek().text
|
|
97
97
|
: this.isMountDeclaration()
|
|
98
|
-
? 'mount
|
|
98
|
+
? 'mount'
|
|
99
99
|
: this.isEffectDeclaration()
|
|
100
100
|
? 'effect'
|
|
101
101
|
: null;
|
|
102
102
|
if (misplaced !== null) {
|
|
103
103
|
this.fail(this.context.inComponent
|
|
104
104
|
? `${misplaced} is declared at the top level of a component, not inside blocks or functions`
|
|
105
|
-
:
|
|
105
|
+
: misplaced === 'state'
|
|
106
|
+
? 'state can only be declared inside a component or at the top level of a module'
|
|
107
|
+
: `${misplaced} can only be declared inside a component`);
|
|
106
108
|
}
|
|
107
109
|
const statement = this.parseSimpleStatement();
|
|
108
110
|
if (statement.kind === 'ExpressionStatement') {
|
|
@@ -133,7 +135,7 @@ export class StatementParser extends SimpleStatementParser {
|
|
|
133
135
|
return this.parseFuncDeclaration(start, exported);
|
|
134
136
|
case 'async':
|
|
135
137
|
if (this.peek(1).kind === 'comp') {
|
|
136
|
-
this.fail('a component cannot be async: load the data in mount
|
|
138
|
+
this.fail('a component cannot be async: load the data in mount or in an async func');
|
|
137
139
|
}
|
|
138
140
|
if (this.peek(1).kind !== 'func' || this.peek(2).kind !== 'Identifier')
|
|
139
141
|
return undefined;
|
|
@@ -223,18 +225,23 @@ export class StatementParser extends SimpleStatementParser {
|
|
|
223
225
|
isEffectDeclaration() {
|
|
224
226
|
return this.checkWord('effect') && this.peek(1).kind === '{';
|
|
225
227
|
}
|
|
226
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* `mount {`: like `state`, a keyword only at the start of a statement in a component. The old
|
|
230
|
+
* form `mount() {` is recognized too, so that parseMount can say how to write it now.
|
|
231
|
+
*/
|
|
227
232
|
isMountDeclaration() {
|
|
228
233
|
return (this.checkWord('mount') &&
|
|
229
|
-
this.peek(1).kind === '
|
|
230
|
-
|
|
231
|
-
this.peek(3).kind === '{');
|
|
234
|
+
(this.peek(1).kind === '{' ||
|
|
235
|
+
(this.peek(1).kind === '(' && this.peek(2).kind === ')' && this.peek(3).kind === '{')));
|
|
232
236
|
}
|
|
233
|
-
/** `mount
|
|
237
|
+
/** `mount { ... }`; its body is a function that runs later, so `return` is its own. */
|
|
234
238
|
parseMount() {
|
|
235
239
|
const { start } = this.next();
|
|
236
|
-
this.
|
|
237
|
-
|
|
240
|
+
if (this.check('(')) {
|
|
241
|
+
const open = this.next();
|
|
242
|
+
const close = this.next();
|
|
243
|
+
this.error('mount has no parentheses, like effect: write mount { ... }', open.start, close.end);
|
|
244
|
+
}
|
|
238
245
|
const body = this.withContext({ ...FUNCTION_BODY, inComponent: true }, () => this.parseBlock());
|
|
239
246
|
return { kind: 'MountStatement', body, ...this.span(start) };
|
|
240
247
|
}
|
package/dist/runtime/sources.js
CHANGED
|
@@ -6,11 +6,12 @@ export const RUNTIME_FILES = {
|
|
|
6
6
|
'channel.js': "// `Channel`: what Rook code and the code compiled from it do with channels, by name. The functions\n// themselves are in chan.js, select.js, signals.js, streams.js and timers.js.\n\nimport { $$chan, $$close, $$range, $$receive, $$send } from './chan.js';\nimport { $$select, $$try } from './select.js';\nimport { $$abortSignal, $$done, $$signal } from './signals.js';\nimport { $$fromStream, $$stream } from './streams.js';\nimport { $$after, $$tick } from './timers.js';\n\nexport const $$Channel = Object.freeze({\n // The zero value of channel types: closed, and the same in every thread (threads.js).\n EMPTY: Object.freeze({ $$chan: 'empty', size: 0, zero: null }),\n make: $$chan,\n send: $$send,\n receive: $$receive,\n close: $$close,\n range: $$range,\n select: $$select,\n // [] for a send and [value, ok] for a receive that goes on at once, else null.\n try: (channel, send, value) => $$try(channel.$$chan, send, value),\n fromStream: $$fromStream,\n stream: $$stream,\n after: $$after,\n tick: $$tick,\n signal: $$signal,\n done: $$done,\n abortSignal: $$abortSignal,\n});\n",
|
|
7
7
|
'classes.js': "// Data classes: `data class Point` registers its class with an id of its own, the name and a hash\n// of its code, the same in a module and in its worker module, which has a copy of the class. A copy\n// of a value that goes to another thread loses the class of its instances (structuredClone); the\n// thread that sends lists them with their ids (threads.js), and the one that gets the copy gives\n// them their class again.\n\nfunction registry() {\n return (globalThis[Symbol.for('rook.classes')] ??= { byId: new Map(), ids: new WeakMap() });\n}\n\nexport function $$dataClass(cls, id) {\n const { byId, ids } = registry();\n\n byId.set(id, cls.prototype);\n ids.set(cls.prototype, id);\n}\n\n/** The id of the data class of an instance, or undefined. */\nexport function $$classOf(value) {\n return registry().ids.get(Object.getPrototypeOf(value));\n}\n\n/** Gives the copies of instances their classes back: `[object, id]` pairs of a copied message. */\nexport function $$reclass(instances) {\n const { byId } = registry();\n\n for (const [object, id] of instances) {\n const prototype = byId.get(id);\n\n if (prototype) Object.setPrototypeOf(object, prototype);\n }\n}\n",
|
|
8
8
|
'core.js': '// What the code of any module may need: `for` over a value of unknown type, `defer`, and wrapped\n// errors.\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\n// `error("load user", err)`: an error whose message ends with that of its cause, as `%w` of Go\n// gives "load user: not found", and whose `cause` is the error itself, for code that looks at it.\nexport function $$wrap(message, cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n\n return new Error(`${message}: ${reason}`, { cause });\n}\n',
|
|
9
|
-
'decode.js': "// `decode<T>(x)`: JSON text, or a value from JS, checked against the type T, which the compiler\n// writes as data: \"number\", \"string\", \"bool\", \"any\", { literal: [values] }, { nullable: T },\n// { array: T }, { record: T }, { union: [types] } and $$type(name, fields) for an object. A value\n// that fits is returned as it is, with the fields that T does not have; a nullable field may be\n// missing. The error says where the value does not fit: `decode User: tags[1]: expected string,\n// got 42`.\n\n/** An object type. Its fields come from a function, so that types can refer to each other. */\nexport function $$type(name, fields) {\n let resolved = null;\n\n return {\n name,\n get fields() {\n return (resolved ??= fields());\n },\n };\n}\n\n/** `decode<T>(text)` of a string: the JSON text is parsed, then checked. */\nexport function $$decodeJson(text, type) {\n let value;\n\n try {\n value = JSON.parse(text);\n } catch (error) {\n return [null, new Error(`decode ${describe(type)}: invalid JSON: ${error.message}`)];\n }\n\n return $$decode(value, type);\n}\n\n/**\n * `decode<T>(value)` of a value from JS. Without the types of the checker (`rook build --no-check`)\n * the type is null: then a string is parsed as JSON text, and nothing is checked.\n */\nexport function $$decode(value, type) {\n if (type === null) return typeof value === 'string' ? $$decodeJson(value, 'any') : [value, null];\n\n const problem = check(value, type, []);\n\n if (problem === null) return [value, null];\n\n const where = problem.path.length > 0 ? `${pathText(problem.path)}: ` : '';\n\n return [null, new Error(`decode ${describe(type)}: ${where}${problem.message}`)];\n}\n\nconst PRIMITIVES = { number: 'number', string: 'string', bool: 'boolean' };\n\n
|
|
9
|
+
'decode.js': "// `decode<T>(x)`: JSON text, or a value from JS, checked against the type T, which the compiler\n// writes as data: \"number\", \"string\", \"bool\", \"any\", { literal: [values] }, { nullable: T },\n// { array: T }, { record: T }, { union: [types] } and $$type(name, fields) for an object. A value\n// that fits is returned as it is, with the fields that T does not have; a nullable field may be\n// missing. The error says where the value does not fit: `decode User: tags[1]: expected string,\n// got 42`.\n\n/** An object type. Its fields come from a function, so that types can refer to each other. */\nexport function $$type(name, fields) {\n let resolved = null;\n\n return {\n name,\n get fields() {\n return (resolved ??= fields());\n },\n };\n}\n\n/** `decode<T>(text)` of a string: the JSON text is parsed, then checked. */\nexport function $$decodeJson(text, type) {\n let value;\n\n try {\n value = JSON.parse(text);\n } catch (error) {\n return [null, new Error(`decode ${describe(type)}: invalid JSON: ${error.message}`)];\n }\n\n return $$decode(value, type);\n}\n\n/**\n * `decode<T>(value)` of a value from JS. Without the types of the checker (`rook build --no-check`)\n * the type is null: then a string is parsed as JSON text, and nothing is checked.\n */\nexport function $$decode(value, type) {\n if (type === null) return typeof value === 'string' ? $$decodeJson(value, 'any') : [value, null];\n\n const problem = check(value, type, []);\n\n if (problem === null) return [value, null];\n\n const where = problem.path.length > 0 ? `${pathText(problem.path)}: ` : '';\n\n return [null, new Error(`decode ${describe(type)}: ${where}${problem.message}`)];\n}\n\nconst PRIMITIVES = { number: 'number', string: 'string', bool: 'boolean' };\n\n/**\n * Where and how the value does not fit the type, or null if it does. For a member of a union,\n * `tagsFirst` checks its fields of literal types first: a member whose tag does not fit reports\n * that, not another field.\n */\nfunction check(value, type, path, tagsFirst = false) {\n if (typeof type === 'string') {\n return type === 'any' || typeof value === PRIMITIVES[type] ? null : mismatch(value, type, path);\n }\n\n if ('literal' in type) {\n // Remembered for unions: a tag that fits no member lists the tags of all of them.\n return type.literal.includes(value)\n ? null\n : { ...mismatch(value, type, path), literal: type.literal, value };\n }\n\n if ('nullable' in type) {\n if (value == null) return null;\n\n const problem = check(value, type.nullable, path);\n\n // The value itself is of another type: the message names ?T, not T.\n return problem !== null && problem.path.length === path.length\n ? mismatch(value, type, path)\n : problem;\n }\n\n if ('array' in type) {\n if (!Array.isArray(value)) return mismatch(value, type, path);\n\n for (let i = 0; i < value.length; i++) {\n const problem = check(value[i], type.array, [...path, i]);\n\n if (problem !== null) return problem;\n }\n\n return null;\n }\n\n if ('union' in type) return checkUnion(value, type, path);\n if (!isObject(value)) return mismatch(value, type, path);\n\n if ('record' in type) {\n for (const key of Object.keys(value)) {\n const problem = check(value[key], type.record, [...path, key]);\n\n if (problem !== null) return problem;\n }\n\n return null;\n }\n\n const fields = Object.entries(type.fields);\n\n if (tagsFirst) fields.sort(([, a], [, b]) => Number(isLiteral(b)) - Number(isLiteral(a)));\n\n for (const [name, field] of fields) {\n // Only own fields: a field named `toString` is not the method of every object.\n if (!Object.hasOwn(value, name)) {\n if (isOptional(field)) continue;\n\n return { path: [...path, name], message: 'missing' };\n }\n\n const problem = check(value[name], field, [...path, name]);\n\n if (problem !== null) return problem;\n }\n\n return null;\n}\n\n/**\n * The value fits one of the types. If none fits, the type whose check went deepest into the value\n * is most likely the one meant, and of those the one whose tag fits: `{ kind: \"circle\", r: \"2\" }`\n * of `Circle | Square` is a Circle, so its problem is the one reported. When no tag fits, the\n * message lists them all: `kind: expected \"circle\" | \"square\", got \"oval\"`.\n */\nfunction checkUnion(value, type, path) {\n const problems = [];\n\n for (const member of type.union) {\n const problem = check(value, member, path, true);\n\n if (problem === null) return null;\n problems.push(problem);\n }\n\n const depth = Math.max(...problems.map((problem) => problem.path.length));\n\n if (depth === path.length) return mismatch(value, type, path);\n\n const deepest = problems.filter((problem) => problem.path.length === depth);\n const fitting = deepest.find((problem) => !problem.literal);\n\n if (fitting) return fitting;\n\n const [first] = deepest;\n const same = deepest.filter((problem) => pathText(problem.path) === pathText(first.path));\n const literal = [...new Set(same.flatMap((problem) => problem.literal))];\n\n return { ...mismatch(first.value, { literal }, first.path), literal, value: first.value };\n}\n\nfunction isObject(value) {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isLiteral(type) {\n return typeof type === 'object' && 'literal' in type;\n}\n\nfunction isOptional(type) {\n return type === 'any' || (typeof type === 'object' && 'nullable' in type);\n}\n\nfunction mismatch(value, type, path) {\n return { path, message: `expected ${describe(type)}, got ${shown(value)}` };\n}\n\n/** The type as Rook writes it: `[]string`, `?User`, `\"admin\" | \"user\"`. */\nfunction describe(type) {\n if (typeof type === 'string') return type;\n if ('literal' in type) return type.literal.map((value) => JSON.stringify(value)).join(' | ');\n if ('nullable' in type) return `?${grouped(type.nullable)}`;\n if ('array' in type) return `[]${grouped(type.array)}`;\n if ('record' in type) return `{ [key string]: ${describe(type.record)} }`;\n if ('union' in type) return type.union.map(describe).join(' | ');\n\n return type.name;\n}\n\nfunction grouped(type) {\n const several = typeof type === 'object' && ('union' in type || type.literal?.length > 1);\n\n return several ? `(${describe(type)})` : describe(type);\n}\n\n/** The value in a message: a primitive itself, a long string cut, and the kind of anything else. */\nfunction shown(value) {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'an array';\n\n switch (typeof value) {\n case 'string':\n return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value);\n\n case 'number':\n case 'boolean':\n return String(value);\n\n case 'object':\n return 'an object';\n\n default:\n return typeof value;\n }\n}\n\n/** `tags[1]`, `boss.name`, `[\"first name\"]`. */\nfunction pathText(path) {\n return path\n .map((key, i) => {\n if (typeof key === 'number') return `[${key}]`;\n if (/^[A-Za-z_$][\\w$]*$/.test(key)) return i === 0 ? key : `.${key}`;\n\n return `[${JSON.stringify(key)}]`;\n })\n .join('');\n}\n",
|
|
10
10
|
'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 $$reclassed,\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 $$reclassed(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 $$reclassed(data);\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",
|
|
11
|
-
'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",
|
|
11
|
+
'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: what the `mount` and `effect`\n// of its components deferred, 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. What its `defer`s put into `cleanups` runs when the owner is removed;\n// they are added to the owner once it mounts, so the cleanups of the decorators, which mount after\n// the component, run before those of the component.\nexport const $$waiting = new Set();\nexport let $$observer = null;\n\nexport function $$mount(node, mount, cleanups) {\n const entry = { node, mount, cleanups, 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 entry.mount();\n if (entry.cleanups) entry.owner?.cleanups.push(() => $$runDeferred(entry.cleanups.splice(0)));\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. With `key`, of `with key item.id`, they are\n// found by the key instead, so an item replaced by another with the same key keeps its block,\n// which gets the new item.\nexport function $$list(items, create, key = (item) => item) {\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.key);\n\n if (same) same.push(block);\n else old.set(block.key, [block]);\n }\n\n const next = [];\n let index = 0;\n\n for (const item of items()) {\n const id = key(item, index);\n let block = old.get(id)?.shift();\n\n if (block) {\n block.update?.(index, item);\n } else {\n const [[content, blockUpdate], remove] = $$owned(() => create(item, index));\n\n block = { key: id, 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// The class of `class={[styles.tab, active ? styles.active : null]}`: the classes that are not\n// null or empty, separated by spaces.\nexport function $$classes(list) {\n return list.filter((each) => each).join(' ');\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",
|
|
12
12
|
'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',
|
|
13
13
|
'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, $$isEmpty, $$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, with the zero value that Channel.EMPTY gives, if it is\n// not null; from another thread, { channel, send, value }. One that can go on at once wins, a\n// random one as in Go; else `default`, -1, if there is one; else the first that can go on later.\n// A case with a null channel never goes on, and one with Channel.EMPTY always can.\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 zero: each.zero ?? null,\n }));\n const live = all.filter(({ id }) => id !== null);\n const owners = new Set(live.filter(({ id }) => !$$isEmpty(id)).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) && live.every(({ id }) => !$$isEmpty(id))) {\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, zero } = all[i];\n const owner = $$ownerOf(id);\n const ready = $$isEmpty(id)\n ? send\n ? []\n : [zero, false]\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",
|
|
14
|
+
'shared.js': "// State of a module, `state items []Item = []` at its top level: the set of the updates of the\n// components that read it, which every change of it runs.\n\nimport { $$onRemove } from './markup.js';\n\n// A component adds its update while it is on the page: `{if}`, `{switch}` or `{for}` that removes\n// it takes the update away.\nexport function $$watch(listeners, update) {\n listeners.add(update);\n $$onRemove(() => listeners.delete(update));\n}\n\n// After a change of the state: the updates run on a copy, since one may remove a component, or\n// add one, that listens too.\nexport function $$changed(listeners) {\n for (const update of [...listeners]) update();\n}\n",
|
|
14
15
|
'signals.js': "// Signals of the process and AbortSignal as channels: `signal(...names)`, `done(abortSignal)` and\n// `abortSignal(ch)`, as signal.Notify and the Done() of a context are in Go.\n\nimport { $$chan, $$closeHere, $$receive } from './chan.js';\nimport { $$try } from './select.js';\nimport { $$registry } from './threads.js';\n\n// `signal(\"SIGINT\", \"SIGTERM\")`: a channel that gets the name of each signal that comes. While it\n// listens, the signals do not end the process, and it keeps the program running, as a tick does;\n// `close(ch)` stops it, and the signals do what they did before. A signal that finds the last one\n// not received yet is dropped. Only the main thread of Node gets the signals of the process.\nexport function $$signal(...names) {\n // Node's own, which a browser does not have.\n const { process } = globalThis;\n\n if (process === undefined) throw new Error('signal() needs Node: a browser has no signals');\n\n const channel = $$chan(1, '');\n const listener = (name) => $$try(channel.$$chan, true, name);\n // A listener of a signal does not keep the program running, and a timer that never fires does.\n const timer = setInterval(() => {}, 2 ** 31 - 1);\n\n for (const name of names) process.on(name, listener);\n $$registry().states.get(channel.$$chan).onClose = () => {\n clearInterval(timer);\n for (const name of names) process.off(name, listener);\n };\n\n return channel;\n}\n\n// `done(signal)`: a channel that gets the reason of an AbortSignal when it aborts, and is closed\n// then, so that `case <-done(request.signal):` goes on once the request is cancelled.\nexport function $$done(signal) {\n const channel = $$chan(1, null);\n const abort = () => {\n $$try(channel.$$chan, true, signal.reason);\n $$closeHere(channel.$$chan);\n };\n\n if (signal.aborted) abort();\n else signal.addEventListener('abort', abort, { once: true });\n\n return channel;\n}\n\n// `abortSignal(ch)`: an AbortSignal that aborts when the channel gets a value, with the value as\n// its reason, or is closed, for `fetch(url, { signal: abortSignal(stop) })`. It receives that one\n// value, as any receiver of the channel would.\nexport function $$abortSignal(channel) {\n const controller = new AbortController();\n\n $$receive(channel, true).then(\n ([value, ok]) => controller.abort(ok ? value : undefined),\n (error) => controller.abort(error),\n );\n\n return controller.signal;\n}\n",
|
|
15
16
|
'streams.js': "// Streams and channels: `chan(stream, size)` and `stream(ch)`.\n\nimport { $$chan, $$closeHere, $$receive, $$send } from './chan.js';\nimport { $$try } from './select.js';\nimport { $$registry } from './threads.js';\n\n// `chan(stream, size)`: a channel of the chunks of a stream and one of its error. The next chunk is\n// read only when the last one is in the channel, so a full channel stops the stream; closing\n// the channel cancels it.\nexport function $$fromStream(stream, size, zero) {\n const channel = $$chan(size, zero);\n const failed = $$chan(1, null);\n const id = channel.$$chan;\n\n (async () => {\n const reader = stream?.getReader();\n let failure = null;\n\n while (reader) {\n let chunk;\n\n try {\n chunk = await reader.read();\n } catch (error) {\n failure = error;\n break;\n }\n\n if (chunk.done) break;\n try {\n await $$send(channel, chunk.value);\n } catch {\n reader.cancel().catch(() => {});\n break;\n }\n }\n\n // The pump holds both channels, so they are not forgotten; the one of the error has room.\n if (!$$registry().states.get(id).closed) $$closeHere(id);\n $$try(failed.$$chan, true, failure);\n $$closeHere(failed.$$chan);\n })();\n\n return [channel, failed];\n}\n\n// `stream(ch)`: a stream that reads one value of the channel for each read, so the room of the\n// channel is all it buffers, and ends when the channel is closed.\nexport function $$stream(channel) {\n return new ReadableStream(\n {\n async pull(controller) {\n const [value, ok] = await $$receive(channel, true);\n\n if (ok) controller.enqueue(value);\n else controller.close();\n },\n },\n { highWaterMark: 0 },\n );\n}\n",
|
|
16
17
|
'threads.js': "// What threads share: a channel is data, `{ $$chan: id, size, zero }`, copied to other threads like\n// any value, and what it holds lives in the thread that made it, whose key starts its id. Each\n// thread has a registry of its channels and of its ports to the other threads, shared by its\n// modules; the operations of a thread on the channels of another go straight to that thread\n// through a MessagePort, which the main thread sets up on the first need (go.js).\n\nimport { $$classOf, $$reclass } from './classes.js';\nimport { $$Queue } from './queue.js';\n\n// The state of this thread that its modules share: the channels it made, its ports to the other\n// threads by their keys, and the requests that wait for an answer.\nexport function $$registry() {\n return (globalThis[Symbol.for('rook.threads')] ??= {\n key: Math.random().toString(36).slice(2),\n channels: 0,\n states: new Map(),\n held: new FinalizationRegistry((id) => $$hold(id, -1)),\n ports: new Map(),\n pins: new Map(),\n idle: null,\n waiting: new Map(),\n gone: new Set(),\n main: null,\n requests: new Map(),\n asks: 0,\n selects: new Map(),\n moved: new WeakSet(),\n cancels: new WeakMap(),\n handle: null,\n });\n}\n\n// Channel.EMPTY, the zero value of channel types, has this id in every thread: it is closed, and\n// no thread keeps it (channel.js).\nexport function $$isEmpty(id) {\n return id === 'empty';\n}\n\n// The key of the thread that made a channel, which keeps what it holds.\nexport function $$ownerOf(id) {\n return id.slice(0, id.lastIndexOf(':'));\n}\n\n// Sends a message to a thread. The channels in it are counted first, as the thread gets copies,\n// and the buffers of transfer() move there. A message waits for the port of a thread that has just\n// started.\nexport function $$post(key, message) {\n const registry = $$registry();\n\n if (registry.gone.has(key)) return;\n\n const { channels, moved, instances } = $$walk(message);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\n // The copies of the instances of data classes are listed in the copy, by reference.\n if (instances.length > 0) message = { ...message, $$instances: instances };\n\n const port = registry.ports.get(key);\n\n if (port) return port.postMessage(message, moved);\n if (!registry.waiting.has(key)) registry.waiting.set(key, []);\n registry.waiting.get(key).push([message, moved]);\n}\n\n// A request to a thread, answered by a message with its number.\nexport function $$ask(key, message) {\n const registry = $$registry();\n\n if (registry.gone.has(key)) return Promise.reject($$stopped());\n $$pin(key, 1);\n\n return new Promise((resolve, reject) => {\n const ask = registry.asks++;\n\n registry.requests.set(ask, { resolve, reject, key });\n $$post(key, { ...message, ask });\n });\n}\n\n// A thread that this one waits for keeps the program running: in Node, its worker or port is\n// referenced while some request or select waits for it.\nexport function $$pin(key, count) {\n const registry = $$registry();\n const pins = (registry.pins.get(key) ?? 0) + count;\n\n registry.pins.set(key, pins);\n\n const port = registry.ports.get(key);\n\n if (pins > 0) port?.ref?.();\n else if (registry.idle) registry.idle(key);\n else port?.unref?.();\n}\n\nexport function $$stopped() {\n return new Error('the goroutine that made the channel has stopped');\n}\n\n// A port to a thread: messages that waited for it go now.\nexport function $$connect(key, port, listen) {\n const registry = $$registry();\n\n registry.ports.set(key, port);\n if (registry.pins.get(key) > 0) port.ref?.();\n if (listen) {\n const receive = (data) => $$incoming(key, data);\n\n if (port.on) {\n port.on('message', receive);\n if (!(registry.pins.get(key) > 0)) port.unref?.();\n } else {\n port.onmessage = (event) => receive(event.data);\n }\n }\n\n for (const [message, moved] of registry.waiting.get(key) ?? []) port.postMessage(message, moved);\n registry.waiting.delete(key);\n}\n\n// A thread has stopped: what waits for it fails, and what it waited for is dropped.\nexport function $$gone(key) {\n const registry = $$registry();\n\n registry.gone.add(key);\n registry.ports.get(key)?.close?.();\n registry.ports.delete(key);\n registry.waiting.delete(key);\n for (const [ask, request] of registry.requests) {\n if (request.key !== key) continue;\n registry.requests.delete(ask);\n request.reject($$stopped());\n }\n\n for (const [id, state] of registry.states) {\n const alive = (entry) => (entry.select?.key ?? entry.from) !== key;\n\n state.receivers.$$keep(alive);\n state.senders.$$keep(alive);\n registry.handle?.pump(id);\n }\n}\n\n// A message from another thread: an answer to a request, or an operation on a channel of this one.\nexport function $$incoming(key, data) {\n const registry = $$registry();\n\n $$adopt(data);\n $$reclassed(data);\n if ('answer' in data) {\n const request = registry.requests.get(data.answer);\n\n registry.requests.delete(data.answer);\n if (request) $$pin(request.key, -1);\n if ('error' in data) request?.reject(data.error);\n else request?.resolve(data.value);\n } else if (data.op === 'hold') {\n $$hold(data.channel, data.count);\n } else if ('op' in data) {\n registry.handle.message(key, data);\n }\n}\n\n// Each value of a channel, in any thread, holds it: the thread that made it forgets it when none\n// is left. A thread counts the channels of a value before a copy of it goes to another thread, and\n// a copy lets go of them when it is collected.\nexport function $$hold(id, count) {\n const registry = $$registry();\n const owner = $$ownerOf(id);\n\n if (owner !== registry.key) return $$post(owner, { op: 'hold', channel: id, count });\n\n const state = registry.states.get(id);\n\n if (!state) return;\n state.holders += count;\n if (state.holders <= 0) registry.states.delete(id);\n}\n\n// A message with copies of the instances of data classes gives them their classes back.\nexport function $$reclassed(data) {\n if (data.$$instances) $$reclass(data.$$instances);\n}\n\nexport function $$adopt(value) {\n const { held } = $$registry();\n\n for (const channel of $$walk(value).channels) held.register(channel, channel.$$chan);\n}\n\n// A copy in this thread. One that `own` the runtime, such as the arguments of `go` that wait for\n// a worker, moves its buffers on.\nexport function $$copy(value, own = false) {\n const { channels, moved, instances } = $$walk(value);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\n\n const { copy, $$instances } = structuredClone(\n { copy: value, $$instances: instances },\n {\n transfer: moved,\n },\n );\n\n $$reclass($$instances);\n\n const { held } = $$registry();\n\n for (const channel of $$walk(copy, own).channels) held.register(channel, channel.$$chan);\n\n return copy;\n}\n\n// `transfer(x)`: the buffer of x moves to the thread that a value with it goes to, instead of a\n// copy; here it is left empty.\nexport function $$transfer(value) {\n $$registry().moved.add(ArrayBuffer.isView(value) ? value.buffer : value);\n\n return value;\n}\n\n// The channels in a value, each of which becomes a copy in the thread that gets the value, and the\n// buffers that move there. A copy of a queue is its fields: here it gets its class again.\nexport function $$walk(\n value,\n own = false,\n found = { channels: new Set(), moved: [], instances: [] },\n seen = new Set(),\n) {\n if (typeof value !== 'object' || value === null || seen.has(value)) return found;\n seen.add(value);\n\n const buffer =\n value instanceof ArrayBuffer ? value : ArrayBuffer.isView(value) ? value.buffer : null;\n\n // A SharedArrayBuffer is shared with the other thread, not moved.\n if (buffer && !(buffer instanceof ArrayBuffer)) return found;\n if (buffer) {\n const { moved } = $$registry();\n\n if (own) moved.add(buffer);\n if (moved.has(buffer) && !found.moved.includes(buffer)) found.moved.push(buffer);\n\n return found;\n }\n\n if (Array.isArray(value.$$queue)) Object.setPrototypeOf(value, $$Queue.prototype);\n\n const id = $$classOf(value);\n\n if (id !== undefined) found.instances.push([value, id]);\n if (typeof value.$$chan === 'string') {\n if (!$$isEmpty(value.$$chan)) found.channels.add(value);\n\n return found;\n }\n\n const items =\n value instanceof Map\n ? [...value.keys(), ...value.values()]\n : value instanceof Set || Array.isArray(value)\n ? value\n : Object.values(value);\n\n for (const item of items) $$walk(item, own, found, seen);\n\n return found;\n}\n",
|