rooklang 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/README.ru.md +17 -4
- package/dist/ast/expressions.d.ts +16 -1
- package/dist/ast/markup.d.ts +6 -1
- package/dist/browser.d.ts +5 -0
- package/dist/browser.js +1 -0
- package/dist/checker/assignments.d.ts +5 -0
- package/dist/checker/assignments.js +34 -8
- package/dist/checker/base.d.ts +2 -1
- package/dist/checker/base.js +4 -10
- package/dist/checker/builtins.js +4 -35
- package/dist/checker/bytes.d.ts +2 -2
- package/dist/checker/bytes.js +2 -2
- package/dist/checker/channels.d.ts +3 -0
- package/dist/checker/channels.js +39 -1
- package/dist/checker/context.d.ts +6 -0
- package/dist/checker/control-flow.d.ts +1 -0
- package/dist/checker/control-flow.js +17 -0
- package/dist/checker/error-results.d.ts +6 -0
- package/dist/checker/error-results.js +31 -0
- package/dist/checker/expressions.d.ts +2 -0
- package/dist/checker/expressions.js +4 -0
- package/dist/checker/helpers.js +2 -0
- package/dist/checker/queues.d.ts +6 -0
- package/dist/checker/queues.js +24 -0
- package/dist/checker/resolution.js +6 -2
- package/dist/checker/statements.d.ts +5 -0
- package/dist/checker/statements.js +19 -0
- package/dist/checker/transfer.js +4 -1
- package/dist/checker/types/printing.js +7 -0
- package/dist/checker/types/queue.d.ts +6 -0
- package/dist/checker/types/queue.js +42 -0
- package/dist/checker/types/relations.js +12 -1
- package/dist/checker/types.d.ts +1 -0
- package/dist/checker/types.js +1 -0
- package/dist/checker/untyped-globals.d.ts +1 -0
- package/dist/checker/untyped-globals.js +47 -0
- package/dist/codegen/attributes.js +0 -1
- package/dist/codegen/calls.d.ts +9 -0
- package/dist/codegen/calls.js +41 -1
- package/dist/codegen/component-values.js +0 -1
- package/dist/codegen/components.js +0 -1
- package/dist/codegen/control-flow.js +0 -3
- package/dist/codegen/control-statements.d.ts +9 -1
- package/dist/codegen/control-statements.js +51 -7
- package/dist/codegen/creation.d.ts +6 -8
- package/dist/codegen/creation.js +1 -10
- package/dist/codegen/early-returns.js +0 -1
- package/dist/codegen/elements.js +0 -3
- package/dist/codegen/emitter.d.ts +3 -5
- package/dist/codegen/emitter.js +0 -2
- package/dist/codegen/expressions.d.ts +4 -1
- package/dist/codegen/expressions.js +9 -13
- package/dist/codegen/functions.js +0 -2
- package/dist/codegen/js.d.ts +0 -7
- package/dist/codegen/js.js +33 -59
- package/dist/codegen/parallel.js +2 -4
- package/dist/codegen/refs.js +0 -1
- package/dist/codegen/runtime.d.ts +13 -0
- package/dist/codegen/runtime.js +93 -0
- package/dist/codegen/simple-statements.js +3 -1
- package/dist/codegen/styles.d.ts +1 -1
- package/dist/codegen/styles.js +2 -2
- package/dist/codegen/web-components.js +0 -1
- package/dist/compile.d.ts +7 -0
- package/dist/compile.js +1 -0
- package/dist/completion.js +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/loader.js +19 -6
- package/dist/parser/base.d.ts +3 -1
- package/dist/parser/expressions.js +9 -2
- package/dist/parser/literals.d.ts +7 -0
- package/dist/parser/literals.js +45 -1
- package/dist/parser/simple-statements.js +3 -0
- package/dist/parser/types.d.ts +1 -1
- package/dist/parser/types.js +10 -3
- package/dist/runtime/sources.d.ts +2 -0
- package/dist/runtime/sources.js +14 -0
- package/dist/runtime-module.d.ts +1 -0
- package/dist/runtime-module.js +3 -0
- package/dist/ts-types.d.ts +4 -0
- package/dist/ts-types.js +25 -3
- package/dist/vite.js +7 -1
- package/dist/walk-types.js +1 -0
- package/dist/walk.js +9 -0
- package/package.json +6 -4
- package/src/runtime/chan.js +277 -0
- package/src/runtime/core.js +23 -0
- package/src/runtime/go.js +340 -0
- package/src/runtime/index.js +13 -0
- package/src/runtime/markup.js +336 -0
- package/src/runtime/queue.js +118 -0
- package/src/runtime/select.js +170 -0
- package/src/runtime/streams.js +61 -0
- package/src/runtime/threads.js +240 -0
- package/src/runtime/timers.js +27 -0
- package/dist/codegen/chan-helpers.d.ts +0 -2
- package/dist/codegen/chan-helpers.js +0 -253
- package/dist/codegen/go-helpers.d.ts +0 -9
- package/dist/codegen/go-helpers.js +0 -307
- package/dist/codegen/helpers.d.ts +0 -10
- package/dist/codegen/helpers.js +0 -356
- package/dist/codegen/select-helpers.d.ts +0 -2
- package/dist/codegen/select-helpers.js +0 -135
- package/dist/codegen/thread-helpers.d.ts +0 -2
- package/dist/codegen/thread-helpers.js +0 -198
|
@@ -176,6 +176,11 @@ export class ExpressionParser extends TypeParser {
|
|
|
176
176
|
return this.parseDecoratorGet();
|
|
177
177
|
if (this.isMake())
|
|
178
178
|
return this.parseMake();
|
|
179
|
+
if (this.checkWord('queue') && this.peek(1).kind === '<') {
|
|
180
|
+
const queue = this.parseQueue();
|
|
181
|
+
if (queue)
|
|
182
|
+
return queue;
|
|
183
|
+
}
|
|
179
184
|
this.next();
|
|
180
185
|
return { kind: 'Identifier', name: token.text, start, end };
|
|
181
186
|
case 'Number':
|
|
@@ -218,10 +223,12 @@ export class ExpressionParser extends TypeParser {
|
|
|
218
223
|
return this.parseObjectLiteral();
|
|
219
224
|
case 'func':
|
|
220
225
|
return this.parseFuncExpression();
|
|
221
|
-
// `chan<number>(10)`: a new channel.
|
|
226
|
+
// `chan<number>(10)`: a new channel; `chan(res.body, 4)`: one that receives a stream.
|
|
222
227
|
case 'chan': {
|
|
223
228
|
this.next();
|
|
224
|
-
this.
|
|
229
|
+
if (this.check('('))
|
|
230
|
+
return this.parseStreamChan(start);
|
|
231
|
+
this.expect('<', '"<" or "(": a channel is made as chan<T>(size), e.g. chan<number>(10), or from a stream as chan(stream, size)');
|
|
225
232
|
const element = this.parseType();
|
|
226
233
|
this.expectTypeClose('">" after the type of the values');
|
|
227
234
|
this.expect('(');
|
|
@@ -14,6 +14,13 @@ export declare abstract class LiteralParser extends ExpressionParser {
|
|
|
14
14
|
protected parseArrayLiteral(): ast.ArrayLiteral;
|
|
15
15
|
/** `make([]float64, n)` or `make([]float64, n, shared)`. */
|
|
16
16
|
protected parseMake(): ast.MakeExpression;
|
|
17
|
+
/**
|
|
18
|
+
* `queue<number>()` or `queue<number>(items)`. A variable may be named `queue`, so `queue < n`
|
|
19
|
+
* that has no type and `(` after it is a comparison: then this gives null and reads nothing.
|
|
20
|
+
*/
|
|
21
|
+
protected parseQueue(): ast.QueueExpression | null;
|
|
22
|
+
/** `chan(stream)` or `chan(stream, size)`, after `chan`. */
|
|
23
|
+
protected parseStreamChan(start: number): ast.StreamChanExpression;
|
|
17
24
|
/** `[]byte("text")`: a type, then its value in parentheses. */
|
|
18
25
|
protected parseConversion(): ast.ConversionExpression;
|
|
19
26
|
protected parseObjectLiteral(): ast.ObjectLiteral;
|
package/dist/parser/literals.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isKeyword } from "../lexer/token.js";
|
|
2
2
|
import { ExpressionParser } from "./expressions.js";
|
|
3
|
-
import { FUNCTION_BODY, describe, templateElement } from "./syntax.js";
|
|
3
|
+
import { FUNCTION_BODY, ParseError, describe, templateElement } from "./syntax.js";
|
|
4
4
|
// Functions, `new`, literals of arrays, objects and templates, names.
|
|
5
5
|
export class LiteralParser extends ExpressionParser {
|
|
6
6
|
/** Whether the `(` at the current position opens arrow function parameters: `(...) =>`. */
|
|
@@ -128,6 +128,50 @@ export class LiteralParser extends ExpressionParser {
|
|
|
128
128
|
this.expect(')');
|
|
129
129
|
return { kind: 'MakeExpression', type, length, shared, ...this.span(start) };
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* `queue<number>()` or `queue<number>(items)`. A variable may be named `queue`, so `queue < n`
|
|
133
|
+
* that has no type and `(` after it is a comparison: then this gives null and reads nothing.
|
|
134
|
+
*/
|
|
135
|
+
parseQueue() {
|
|
136
|
+
const { pos } = this;
|
|
137
|
+
// Closing the type may split a `>>` token, and errors are reported as the type is read.
|
|
138
|
+
const tokens = this.tokens.slice(pos);
|
|
139
|
+
const reported = this.diagnostics.length;
|
|
140
|
+
const start = this.next().start;
|
|
141
|
+
let element;
|
|
142
|
+
try {
|
|
143
|
+
this.expect('<');
|
|
144
|
+
element = this.parseType();
|
|
145
|
+
this.expectTypeClose('">" after the type of the values');
|
|
146
|
+
if (!this.check('('))
|
|
147
|
+
throw new ParseError('not a queue');
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (!(error instanceof ParseError))
|
|
151
|
+
throw error;
|
|
152
|
+
tokens.forEach((token, i) => (this.tokens[pos + i] = token));
|
|
153
|
+
this.diagnostics.length = reported;
|
|
154
|
+
this.pos = pos;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
this.next();
|
|
158
|
+
const items = this.check(')') ? null : this.nested(() => this.parseExpression());
|
|
159
|
+
this.accept(',');
|
|
160
|
+
this.expect(')', '")": a queue takes its first values as one array, e.g. queue<number>([1, 2])');
|
|
161
|
+
return { kind: 'QueueExpression', element, items, ...this.span(start) };
|
|
162
|
+
}
|
|
163
|
+
/** `chan(stream)` or `chan(stream, size)`, after `chan`. */
|
|
164
|
+
parseStreamChan(start) {
|
|
165
|
+
this.expect('(');
|
|
166
|
+
const [stream, size] = this.nested(() => {
|
|
167
|
+
const stream = this.parseExpression();
|
|
168
|
+
const size = this.accept(',') && !this.check(')') ? this.parseExpression() : null;
|
|
169
|
+
this.accept(',');
|
|
170
|
+
return [stream, size];
|
|
171
|
+
});
|
|
172
|
+
this.expect(')');
|
|
173
|
+
return { kind: 'StreamChanExpression', stream, size, ...this.span(start) };
|
|
174
|
+
}
|
|
131
175
|
/** `[]byte("text")`: a type, then its value in parentheses. */
|
|
132
176
|
parseConversion() {
|
|
133
177
|
const { start } = this.peek();
|
|
@@ -87,6 +87,9 @@ export class SimpleStatementParser extends ParserBase {
|
|
|
87
87
|
const call = awaited?.kind === 'GoExpression' ? awaited.call : awaited;
|
|
88
88
|
if (values.length === 1 && call?.kind === 'CallExpression')
|
|
89
89
|
return;
|
|
90
|
+
// `chunks, failed = chan(res.body)`: the channel of a stream and that of its error.
|
|
91
|
+
if (values.length === 1 && value?.kind === 'StreamChanExpression')
|
|
92
|
+
return;
|
|
90
93
|
this.error(`assignment mismatch: ${plural(targets, 'variable')} but ${plural(values.length, 'value')}`, start, this.span(start).end);
|
|
91
94
|
}
|
|
92
95
|
/** `delete scores[name]`: removes a field, so the target is one. */
|
package/dist/parser/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ClassParser } from './classes.ts';
|
|
|
3
3
|
export declare abstract class TypeParser extends ClassParser {
|
|
4
4
|
/** `<number, string>` after the name of a generic type. */
|
|
5
5
|
protected parseTypeArgs(): ast.TypeNode[];
|
|
6
|
-
protected canStartType(): boolean;
|
|
6
|
+
protected canStartType(offset?: number): boolean;
|
|
7
7
|
/** A type, possibly a union: `string | number`. */
|
|
8
8
|
protected parseType(): ast.TypeNode;
|
|
9
9
|
protected parsePrimaryType(): ast.TypeNode;
|
package/dist/parser/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ClassParser } from "./classes.js";
|
|
2
2
|
import { describe } from "./syntax.js";
|
|
3
|
-
// Types: `?T`, `[]T`, unions, literal types, function, component and object types, type arguments.
|
|
3
|
+
// Types: `?T`, `[]T`, `queue T`, unions, literal types, function, component and object types, type arguments.
|
|
4
4
|
/** `iterator: true` for the parameters of an iterator: one, named `yield`. */
|
|
5
5
|
function iterator(names) {
|
|
6
6
|
return names.length === 1 && names[0] === 'yield' ? { iterator: true } : {};
|
|
@@ -16,8 +16,8 @@ export class TypeParser extends ClassParser {
|
|
|
16
16
|
this.expectTypeClose('">" after the type arguments');
|
|
17
17
|
return args;
|
|
18
18
|
}
|
|
19
|
-
canStartType() {
|
|
20
|
-
switch (this.peek().kind) {
|
|
19
|
+
canStartType(offset = 0) {
|
|
20
|
+
switch (this.peek(offset).kind) {
|
|
21
21
|
case 'Identifier':
|
|
22
22
|
case '[':
|
|
23
23
|
case '?':
|
|
@@ -106,7 +106,14 @@ export class TypeParser extends ClassParser {
|
|
|
106
106
|
const members = this.parseTypeMembers();
|
|
107
107
|
return { kind: 'ObjectType', members, ...this.span(start) };
|
|
108
108
|
}
|
|
109
|
+
// `queue number`: `queue` is a type of its own only before the type of its values, so that it
|
|
110
|
+
// stays a name elsewhere.
|
|
109
111
|
default: {
|
|
112
|
+
if (this.checkWord('queue') && this.canStartType(1)) {
|
|
113
|
+
this.next();
|
|
114
|
+
const element = this.parsePrimaryType();
|
|
115
|
+
return { kind: 'QueueType', element, ...this.span(start) };
|
|
116
|
+
}
|
|
110
117
|
const name = this.parseIdentifier('type');
|
|
111
118
|
const typeArgs = this.parseTypeArgs();
|
|
112
119
|
return { kind: 'TypeReference', name, typeArgs, ...this.span(start) };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Written by scripts/runtime-sources.ts from the files of src/runtime: change them, not this,
|
|
2
|
+
// and `npm run format` writes it again.
|
|
3
|
+
/** The code of the runtime by file, for modules that have it in them (codegen/runtime.ts). */
|
|
4
|
+
export const RUNTIME_FILES = {
|
|
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
|
+
'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, 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
|
+
'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
|
+
'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
|
+
'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",
|
|
11
|
+
'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",
|
|
12
|
+
'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 { $$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// 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 } = $$walk(message);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\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 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\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 } = $$walk(value);\n\n for (const channel of channels) $$hold(channel.$$chan, 1);\n\n const copy = structuredClone(value, { transfer: moved });\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: [] },\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 if (typeof value.$$chan === 'string') {\n 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",
|
|
13
|
+
'timers.js': "// Channels of time, as in Go: `after(ms)` and `tick(ms)`.\n\nimport { $$chan, $$send } from './chan.js';\nimport { $$try } from './select.js';\nimport { $$registry } from './threads.js';\n\n// `after(ms)`: a channel that gets the time once, after `ms`. `tick(ms)`: one that gets it every\n// `ms` until it is closed; a tick that finds the last one still there is dropped, as in Go.\nexport function $$after(ms) {\n const channel = $$chan(1, 0);\n\n setTimeout(() => $$send(channel, Date.now()), ms);\n\n return channel;\n}\n\nexport function $$tick(ms) {\n const channel = $$chan(1, 0);\n const timer = setInterval(() => {\n const state = $$registry().states.get(channel.$$chan);\n\n if (!state || state.closed) clearInterval(timer);\n else $$try(channel.$$chan, true, Date.now());\n }, ms);\n\n return channel;\n}\n",
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const RUNTIME_MODULE = "rooklang/runtime";
|
package/dist/ts-types.d.ts
CHANGED
|
@@ -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,17 +1,30 @@
|
|
|
1
1
|
import { CONTENT } from "./checker/dom.js";
|
|
2
|
-
import {
|
|
2
|
+
import { guardedByError } from "./checker/error-results.js";
|
|
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`. */
|
|
5
6
|
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
|
-
|
|
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
|
|
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.
|
|
@@ -94,6 +108,14 @@ function objectType(type) {
|
|
|
94
108
|
if (type.name === 'chan' || type.name === 'chan<-' || type.name === '<-chan') {
|
|
95
109
|
return `{ readonly $$chan: string }`;
|
|
96
110
|
}
|
|
111
|
+
// A queue is an instance of $$Queue of the runtime (src/runtime/queue.js): TypeScript code uses its
|
|
112
|
+
// members.
|
|
113
|
+
if (isQueue(type)) {
|
|
114
|
+
const value = queueElement(type);
|
|
115
|
+
const items = `...items: ${element(value)}[]`;
|
|
116
|
+
const end = value.kind === 'nullable' ? tsType(value) : `${grouped(value)} | null`;
|
|
117
|
+
return `{ readonly length: number; push(${items}): number; unshift(${items}): number; shift(): ${end}; pop(): ${end}; at(index: number): ${end}; clear(): void; [Symbol.iterator](): Iterator<${tsType(value)}> }`;
|
|
118
|
+
}
|
|
97
119
|
if (type.name !== null) {
|
|
98
120
|
const args = type.instanceOf?.args ?? type.typeParams;
|
|
99
121
|
return args ? `${type.name}<${args.map((arg) => tsType(arg)).join(', ')}>` : type.name;
|
package/dist/vite.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { RUNTIME_MODULE } from "./runtime-module.js";
|
|
3
5
|
import { formatDiagnostic } from "./diagnostics.js";
|
|
4
6
|
import { compile } from "./index.js";
|
|
5
7
|
import { SourceFile } from "./source.js";
|
|
@@ -7,7 +9,8 @@ import { SourceFile } from "./source.js";
|
|
|
7
9
|
// checking, so `import { App } from "./app.rk"` works in a Vite project. Type errors stop the
|
|
8
10
|
// build and show up in the browser overlay of the dev server. `app.rk.goroutines.js` is the
|
|
9
11
|
// worker module of `go` in app.rk, which Vite bundles as a worker: it is not a file, and its
|
|
10
|
-
// path has no query, since Vite drops the query of a worker's URL.
|
|
12
|
+
// path has no query, since Vite drops the query of a worker's URL. The compiled code imports the
|
|
13
|
+
// runtime, "rooklang/runtime", of the plugin's own package.
|
|
11
14
|
const GOROUTINES = '.goroutines.js';
|
|
12
15
|
export function rook(options = {}) {
|
|
13
16
|
const typeCheck = options.typeCheck ?? true;
|
|
@@ -26,6 +29,9 @@ export function rook(options = {}) {
|
|
|
26
29
|
root = config.root;
|
|
27
30
|
},
|
|
28
31
|
resolveId(source, importer) {
|
|
32
|
+
// The runtime of the compiled code is the one of this compiler, whichever the project has.
|
|
33
|
+
if (source === RUNTIME_MODULE)
|
|
34
|
+
return fileURLToPath(import.meta.resolve(RUNTIME_MODULE));
|
|
29
35
|
const [path = source] = source.split('?');
|
|
30
36
|
if (!path.endsWith(`.rk${GOROUTINES}`))
|
|
31
37
|
return null;
|
package/dist/walk-types.js
CHANGED
package/dist/walk.js
CHANGED
|
@@ -103,6 +103,14 @@ export function forEachChild(node, visit) {
|
|
|
103
103
|
visit(node.element);
|
|
104
104
|
optional(node.size);
|
|
105
105
|
break;
|
|
106
|
+
case 'StreamChanExpression':
|
|
107
|
+
visit(node.stream);
|
|
108
|
+
optional(node.size);
|
|
109
|
+
break;
|
|
110
|
+
case 'QueueExpression':
|
|
111
|
+
visit(node.element);
|
|
112
|
+
optional(node.items);
|
|
113
|
+
break;
|
|
106
114
|
case 'MakeExpression':
|
|
107
115
|
visit(node.type);
|
|
108
116
|
visit(node.length);
|
|
@@ -256,6 +264,7 @@ export function forEachChild(node, visit) {
|
|
|
256
264
|
case 'TypeParameter':
|
|
257
265
|
case 'ArrayType':
|
|
258
266
|
case 'ChanType':
|
|
267
|
+
case 'QueueType':
|
|
259
268
|
case 'NullableType':
|
|
260
269
|
case 'FuncType':
|
|
261
270
|
case 'ComponentType':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rooklang",
|
|
3
|
-
"version": "0.3.
|
|
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",
|
|
@@ -47,10 +47,12 @@
|
|
|
47
47
|
"types": "./dist/browser.d.ts",
|
|
48
48
|
"default": "./dist/browser.js"
|
|
49
49
|
},
|
|
50
|
+
"./runtime": "./src/runtime/index.js",
|
|
50
51
|
"./package.json": "./package.json"
|
|
51
52
|
},
|
|
52
53
|
"files": [
|
|
53
|
-
"dist"
|
|
54
|
+
"dist",
|
|
55
|
+
"src/runtime/*.js"
|
|
54
56
|
],
|
|
55
57
|
"scripts": {
|
|
56
58
|
"rook": "node src/cli.ts",
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"site": "npm run site:build && node src/cli.ts run site/server.rk",
|
|
60
62
|
"site:images": "node scripts/og-image.ts",
|
|
61
63
|
"vscode:build": "node scripts/build-vscode.ts",
|
|
62
|
-
"vscode:package": "npm run vscode:build && cd editors/vscode && npx --yes @vscode/vsce@4 package --no-dependencies --
|
|
64
|
+
"vscode:package": "npm run vscode:build && cd editors/vscode && npx --yes @vscode/vsce@4 package --no-dependencies --baseContentUrl https://github.com/QWERN-9876r/RookLang/blob/HEAD/editors/vscode --baseImagesUrl https://github.com/QWERN-9876r/RookLang/raw/HEAD/editors/vscode --out rooklang.vsix",
|
|
63
65
|
"vscode:install": "npm run vscode:package && code --install-extension editors/vscode/rooklang.vsix --force",
|
|
64
66
|
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
|
|
65
67
|
"prepack": "npm run build",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
"test": "vitest run",
|
|
69
71
|
"test:watch": "vitest",
|
|
70
72
|
"lint": "eslint .",
|
|
71
|
-
"format": "prettier --write . && eslint --fix --fix-type layout .",
|
|
73
|
+
"format": "prettier --write . && eslint --fix --fix-type layout . && node scripts/runtime-sources.ts",
|
|
72
74
|
"format:check": "prettier --check .",
|
|
73
75
|
"check": "npm run typecheck && npm run lint && npm run format:check && npm test",
|
|
74
76
|
"prepare": "husky"
|