rooklang 0.3.1 → 0.3.2

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.
Files changed (93) hide show
  1. package/README.md +17 -4
  2. package/README.ru.md +17 -4
  3. package/dist/ast/expressions.d.ts +16 -1
  4. package/dist/ast/markup.d.ts +6 -1
  5. package/dist/browser.d.ts +5 -0
  6. package/dist/browser.js +1 -0
  7. package/dist/checker/base.js +4 -4
  8. package/dist/checker/builtins.js +4 -35
  9. package/dist/checker/bytes.d.ts +2 -2
  10. package/dist/checker/bytes.js +2 -2
  11. package/dist/checker/channels.d.ts +3 -0
  12. package/dist/checker/channels.js +39 -1
  13. package/dist/checker/expressions.d.ts +2 -0
  14. package/dist/checker/expressions.js +4 -0
  15. package/dist/checker/helpers.js +2 -0
  16. package/dist/checker/queues.d.ts +6 -0
  17. package/dist/checker/queues.js +24 -0
  18. package/dist/checker/resolution.js +6 -2
  19. package/dist/checker/transfer.js +4 -1
  20. package/dist/checker/types/printing.js +7 -0
  21. package/dist/checker/types/queue.d.ts +6 -0
  22. package/dist/checker/types/queue.js +42 -0
  23. package/dist/checker/types/relations.js +12 -1
  24. package/dist/checker/types.d.ts +1 -0
  25. package/dist/checker/types.js +1 -0
  26. package/dist/checker/untyped-globals.d.ts +1 -0
  27. package/dist/checker/untyped-globals.js +47 -0
  28. package/dist/codegen/attributes.js +0 -1
  29. package/dist/codegen/calls.d.ts +9 -0
  30. package/dist/codegen/calls.js +41 -1
  31. package/dist/codegen/component-values.js +0 -1
  32. package/dist/codegen/components.js +0 -1
  33. package/dist/codegen/control-flow.js +0 -3
  34. package/dist/codegen/control-statements.js +0 -4
  35. package/dist/codegen/creation.d.ts +6 -8
  36. package/dist/codegen/creation.js +1 -10
  37. package/dist/codegen/early-returns.js +0 -1
  38. package/dist/codegen/elements.js +0 -3
  39. package/dist/codegen/emitter.d.ts +3 -5
  40. package/dist/codegen/emitter.js +0 -2
  41. package/dist/codegen/expressions.d.ts +4 -1
  42. package/dist/codegen/expressions.js +9 -13
  43. package/dist/codegen/functions.js +0 -2
  44. package/dist/codegen/js.d.ts +0 -7
  45. package/dist/codegen/js.js +33 -59
  46. package/dist/codegen/parallel.js +2 -4
  47. package/dist/codegen/refs.js +0 -1
  48. package/dist/codegen/runtime.d.ts +15 -0
  49. package/dist/codegen/runtime.js +99 -0
  50. package/dist/codegen/simple-statements.js +3 -1
  51. package/dist/codegen/styles.d.ts +1 -1
  52. package/dist/codegen/styles.js +2 -2
  53. package/dist/codegen/web-components.js +0 -1
  54. package/dist/compile.d.ts +7 -0
  55. package/dist/compile.js +1 -0
  56. package/dist/completion.js +2 -1
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +1 -0
  59. package/dist/loader.js +8 -0
  60. package/dist/parser/base.d.ts +3 -1
  61. package/dist/parser/expressions.js +9 -2
  62. package/dist/parser/literals.d.ts +7 -0
  63. package/dist/parser/literals.js +45 -1
  64. package/dist/parser/simple-statements.js +3 -0
  65. package/dist/parser/types.d.ts +1 -1
  66. package/dist/parser/types.js +10 -3
  67. package/dist/runtime/sources.d.ts +2 -0
  68. package/dist/runtime/sources.js +14 -0
  69. package/dist/ts-types.js +9 -1
  70. package/dist/vite.js +7 -1
  71. package/dist/walk-types.js +1 -0
  72. package/dist/walk.js +9 -0
  73. package/package.json +6 -4
  74. package/src/runtime/chan.js +277 -0
  75. package/src/runtime/core.js +23 -0
  76. package/src/runtime/go.js +334 -0
  77. package/src/runtime/index.js +13 -0
  78. package/src/runtime/markup.js +336 -0
  79. package/src/runtime/queue.js +118 -0
  80. package/src/runtime/select.js +170 -0
  81. package/src/runtime/streams.js +61 -0
  82. package/src/runtime/threads.js +240 -0
  83. package/src/runtime/timers.js +27 -0
  84. package/dist/codegen/chan-helpers.d.ts +0 -2
  85. package/dist/codegen/chan-helpers.js +0 -253
  86. package/dist/codegen/go-helpers.d.ts +0 -9
  87. package/dist/codegen/go-helpers.js +0 -307
  88. package/dist/codegen/helpers.d.ts +0 -10
  89. package/dist/codegen/helpers.js +0 -356
  90. package/dist/codegen/select-helpers.d.ts +0 -2
  91. package/dist/codegen/select-helpers.js +0 -135
  92. package/dist/codegen/thread-helpers.d.ts +0 -2
  93. package/dist/codegen/thread-helpers.js +0 -198
@@ -0,0 +1,47 @@
1
+ // JS globals that the checker knows by name only, without types: everything about them is `any`.
2
+ // With TypeScript's lib files (in Node) the library types them; the browser compiler of the
3
+ // playground has only these names.
4
+ export const UNTYPED_GLOBALS = [
5
+ 'Object',
6
+ 'Array',
7
+ 'Date',
8
+ 'RegExp',
9
+ 'Map',
10
+ 'Set',
11
+ 'WeakMap',
12
+ 'WeakSet',
13
+ 'Promise',
14
+ 'Symbol',
15
+ 'Reflect',
16
+ 'Proxy',
17
+ 'Intl',
18
+ 'globalThis',
19
+ 'process',
20
+ 'Buffer',
21
+ 'URL',
22
+ 'URLSearchParams',
23
+ 'TextEncoder',
24
+ 'TextDecoder',
25
+ // Streams, and what makes them: chan(stream) and stream(ch) work with them.
26
+ 'ReadableStream',
27
+ 'WritableStream',
28
+ 'TransformStream',
29
+ 'TextEncoderStream',
30
+ 'TextDecoderStream',
31
+ 'Blob',
32
+ 'Response',
33
+ 'Request',
34
+ 'Headers',
35
+ 'AbortController',
36
+ 'fetch',
37
+ 'structuredClone',
38
+ 'queueMicrotask',
39
+ 'window',
40
+ 'document',
41
+ 'navigator',
42
+ 'location',
43
+ 'localStorage',
44
+ 'sessionStorage',
45
+ 'crypto',
46
+ 'performance',
47
+ ];
@@ -88,7 +88,6 @@ export class AttributeEmitter extends UpdateEmitter {
88
88
  this.setLive(live, value, (text) => `${element}.setAttribute(${quoted}, ${text});`);
89
89
  }
90
90
  else if (live && live.dependencies(value).size > 0) {
91
- this.helpers.add('attribute');
92
91
  this.setLive(live, value, (text) => `$$attribute(${element}, ${quoted}, ${text});`);
93
92
  }
94
93
  else {
@@ -19,4 +19,13 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
19
19
  */
20
20
  protected make(node: ast.MakeExpression): string;
21
21
  protected conversion(node: ast.ConversionExpression): string;
22
+ /** `chan<number>(10)`: the channel keeps the zero value that a closed one gives. */
23
+ protected chan(node: ast.ChanExpression): string;
24
+ /** `queue<number>(items)`: the queue copies the values, as `[...items]` would. */
25
+ protected queue(node: ast.QueueExpression): string;
26
+ /**
27
+ * `chan(stream, size)`: the channel keeps the zero value of its values, as `chan<T>(size)` does,
28
+ * from the type of the stream that the checker found; without it, null.
29
+ */
30
+ protected streamChan(node: ast.StreamChanExpression): string;
22
31
  }
@@ -97,10 +97,50 @@ export class CallEmitter extends ExpressionEmitter {
97
97
  const bytes = size === 1
98
98
  ? this.expression(node.length, ARROW)
99
99
  : `${size} * ${this.expression(node.length, BINARY['*'] + 1)}`;
100
- this.helpers.add('shared');
101
100
  return `new ${typed}($$shared(${bytes}))`;
102
101
  }
103
102
  conversion(node) {
104
103
  return `new TextEncoder().encode(${this.expression(node.argument, 0)})`;
105
104
  }
105
+ /** `chan<number>(10)`: the channel keeps the zero value that a closed one gives. */
106
+ chan(node) {
107
+ const size = node.size ? this.expression(node.size, ARROW) : '0';
108
+ return `$$chan(${size}, ${this.zeroValue(node.element) ?? 'null'})`;
109
+ }
110
+ /** `queue<number>(items)`: the queue copies the values, as `[...items]` would. */
111
+ queue(node) {
112
+ return `new $$Queue(${node.items ? this.expression(node.items, ARROW) : ''})`;
113
+ }
114
+ /**
115
+ * `chan(stream, size)`: the channel keeps the zero value of its values, as `chan<T>(size)` does,
116
+ * from the type of the stream that the checker found; without it, null.
117
+ */
118
+ streamChan(node) {
119
+ const stream = this.expression(node.stream, ARROW);
120
+ const size = node.size ? this.expression(node.size, ARROW) : '0';
121
+ const type = this.options.types?.get(node);
122
+ const channel = type?.kind === 'tuple' ? type.types[0] : undefined;
123
+ const element = channel?.kind === 'object' ? channel.instanceOf?.args[0] : undefined;
124
+ return `$$fromStream(${stream}, ${size}, ${element ? zeroOf(element) : 'null'})`;
125
+ }
126
+ }
127
+ /** The zero value of a type, as zeroValue gives for a type as written. */
128
+ function zeroOf(type) {
129
+ const typed = typedArrayOf(type);
130
+ if (typed !== null)
131
+ return `new ${typed}()`;
132
+ switch (type.kind) {
133
+ case 'number':
134
+ return '0';
135
+ case 'bigint':
136
+ return '0n';
137
+ case 'string':
138
+ return '""';
139
+ case 'bool':
140
+ return 'false';
141
+ case 'array':
142
+ return '[]';
143
+ default:
144
+ return 'null';
145
+ }
106
146
  }
@@ -52,7 +52,6 @@ export class ComponentValueEmitter extends ComponentPropsEmitter {
52
52
  finally {
53
53
  this.fixedComponents.delete(node);
54
54
  }
55
- this.helpers.add('dynamic');
56
55
  this.line(`const [${name}, ${update}] = $$dynamic(${this.liveFunction(value)}, ${create});`);
57
56
  live.depend(live.dependencies(node), `${update}();`);
58
57
  return name;
@@ -78,7 +78,6 @@ export class ComponentEmitter extends StyleEmitter {
78
78
  * it changes updates the markup.
79
79
  */
80
80
  mountStatement(node) {
81
- this.helpers.add('mount');
82
81
  this.inHandler++;
83
82
  try {
84
83
  const [, body] = this.withRendering(0, () => this.func([], node.body));
@@ -26,19 +26,16 @@ export class ControlFlowEmitter extends ElementEmitter {
26
26
  const [choose, blocks] = statement.kind === 'IfStatement'
27
27
  ? this.ifBranches(statement, live)
28
28
  : this.switchBranches(statement, live);
29
- this.helpers.add('branches');
30
29
  call = `$$branches(${choose}, [${blocks.join(', ')}])`;
31
30
  break;
32
31
  }
33
32
  case 'ForInStatement': {
34
- this.helpers.add('list');
35
33
  const items = this.liveFunction(statement.iterable);
36
34
  call = `$$list(${items}, ${this.blockCreator(statement.body.body, live, statement)})`;
37
35
  break;
38
36
  }
39
37
  case 'ForStatement':
40
38
  // Without items to find the blocks by, the loop runs again and creates new content.
41
- this.helpers.add('content');
42
39
  call = `$$content(${this.loopContent(statement)})`;
43
40
  break;
44
41
  }
@@ -53,7 +53,6 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
53
53
  case 'ExpressionStatement':
54
54
  return this.expression(node.expression, 0);
55
55
  case 'SendStatement':
56
- this.helpers.add('chan');
57
56
  return `await $$send(${this.expression(node.channel, ARROW)}, ${this.expression(node.value, ARROW)})`;
58
57
  }
59
58
  }
@@ -71,7 +70,6 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
71
70
  let head;
72
71
  // A channel gives its values as they come, until it is closed.
73
72
  if (iterable && isChan(iterable)) {
74
- this.helpers.add('chan');
75
73
  this.line(`for await (const ${value} of $$range(${this.expression(node.iterable, ARROW)})) ${this.loopBody(node.body)}`);
76
74
  return;
77
75
  }
@@ -92,7 +90,6 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
92
90
  if (iterable && isMap(iterable))
93
91
  values += '.values()';
94
92
  else if (untyped) {
95
- this.helpers.add('values');
96
93
  values = `$$values(${values})`;
97
94
  }
98
95
  head = `const ${value} of ${values}`;
@@ -140,7 +137,6 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
140
137
  * switch runs the case; a `break` inside it leaves the select, as in Go. `default` is -1.
141
138
  */
142
139
  selectStatement(node) {
143
- this.helpers.add('chan');
144
140
  const operations = node.cases.flatMap(({ operation }) => {
145
141
  if (operation === null)
146
142
  return [];
@@ -2,11 +2,11 @@ import type * as ast from '../ast.ts';
2
2
  import { AttributeEmitter } from './attributes.ts';
3
3
  import type { Live } from './reactive.ts';
4
4
  /**
5
- * The creation of elements. In a module that creates many of them (see ELEMENT_HELPER_LIMIT in
6
- * js.ts) an element is created by a helper together with the attributes it has from the start:
7
- * `$$element("a", { href: "/", className: "link" }, { "aria-label": label })` takes the place of
8
- * a `createElement` and a statement for each attribute. The first object holds properties, the
9
- * second attributes set with setAttribute; an SVG element has only attributes.
5
+ * The creation of elements. An element is created by a function of the runtime together with the
6
+ * attributes it has from the start: `$$element("a", { href: "/", className: "link" },
7
+ * { "aria-label": label })` takes the place of a `createElement` and a statement for each
8
+ * attribute. The first object holds properties, the second attributes set with setAttribute; an
9
+ * SVG element has only attributes.
10
10
  */
11
11
  /** Attributes given to the helper that creates an element, which need no statements of their own. */
12
12
  export interface Creation {
@@ -15,10 +15,8 @@ export interface Creation {
15
15
  scoped: boolean;
16
16
  }
17
17
  export declare abstract class CreationEmitter extends AttributeEmitter {
18
- /** The elements the code creates, counted where it creates them, not as often as it runs. */
19
- createdElements: number;
20
18
  /**
21
- * Writes `declaration = <a new element>`. With the helper it is given the attributes before the
19
+ * Writes `declaration = <a new element>`. It is given the attributes before the
22
20
  * first spread, which may override them, except those that need more than a value: handlers,
23
21
  * `bind:`, styles, properties of web components and attributes that may be null. `scopeClass`
24
22
  * is the class of the style for an element without a class attribute.
@@ -2,21 +2,14 @@ import { domProperty } from "../checker/dom.js";
2
2
  import { AttributeEmitter } from "./attributes.js";
3
3
  import { ARROW } from "./syntax.js";
4
4
  export class CreationEmitter extends AttributeEmitter {
5
- /** The elements the code creates, counted where it creates them, not as often as it runs. */
6
- createdElements = 0;
7
5
  /**
8
- * Writes `declaration = <a new element>`. With the helper it is given the attributes before the
6
+ * Writes `declaration = <a new element>`. It is given the attributes before the
9
7
  * first spread, which may override them, except those that need more than a value: handlers,
10
8
  * `bind:`, styles, properties of web components and attributes that may be null. `scopeClass`
11
9
  * is the class of the style for an element without a class attribute.
12
10
  */
13
11
  createWith(declaration, element, tag, svg, attributes, live, scope, scopeClass) {
14
12
  const covered = new Set();
15
- this.createdElements++;
16
- if (!this.options.elementHelper) {
17
- this.line(`${declaration} = ${this.createElement(tag, svg)};`);
18
- return { covered, scoped: false };
19
- }
20
13
  const entries = [];
21
14
  for (const attribute of attributes) {
22
15
  if (attribute.kind === 'JsxSpreadAttribute')
@@ -48,13 +41,11 @@ export class CreationEmitter extends AttributeEmitter {
48
41
  const attributesObject = object(true);
49
42
  const args = [JSON.stringify(tag)];
50
43
  if (svg) {
51
- this.helpers.add('svgElement');
52
44
  if (attributesObject)
53
45
  args.push(attributesObject);
54
46
  this.line(`${declaration} = $$svgElement(${args.join(', ')});`);
55
47
  }
56
48
  else {
57
- this.helpers.add('element');
58
49
  if (properties || attributesObject)
59
50
  args.push(properties ?? '{}');
60
51
  if (attributesObject)
@@ -94,7 +94,6 @@ export class EarlyReturnEmitter extends ComponentValueEmitter {
94
94
  this.line(`${effect.name}();`);
95
95
  if (effect.cleanups === null)
96
96
  continue;
97
- this.helpers.add('owner');
98
97
  this.line(`$$onRemove(() => $$runDeferred(${effect.cleanups}.splice(0)));`);
99
98
  }
100
99
  }
@@ -93,7 +93,6 @@ export class ElementEmitter extends CreationEmitter {
93
93
  this.line(`${target} = document.createDocumentFragment();`);
94
94
  }
95
95
  else if (preset) {
96
- this.createdElements++;
97
96
  this.line(`${target} = ${preset.name} ?? ${this.createElement(tag, svg)};`);
98
97
  }
99
98
  else {
@@ -217,7 +216,6 @@ export class ElementEmitter extends CreationEmitter {
217
216
  }
218
217
  case 'unknown':
219
218
  flush();
220
- this.helpers.add('append');
221
219
  this.line(`$$append(${element}, ${this.expression(expression, ARROW)});`);
222
220
  break;
223
221
  }
@@ -258,7 +256,6 @@ export class ElementEmitter extends CreationEmitter {
258
256
  }
259
257
  const name = `$$content${id}`;
260
258
  const update = `$$updateContent${id}`;
261
- this.helpers.add('content');
262
259
  this.line(`const [${name}, ${update}] = $$content(${this.liveFunction(expression)});`);
263
260
  live.depend(sources, `${update}();`);
264
261
  return name;
@@ -6,7 +6,7 @@ import { type ImportedComponent, type LocalizedPackage } from './imported.ts';
6
6
  import type { Root } from '../refs.ts';
7
7
  import type { StyleModule } from '../style-modules.ts';
8
8
  import type { BindingKind } from './analysis.ts';
9
- import type { Helper } from './helpers.ts';
9
+ import type { RuntimeMode } from './runtime.ts';
10
10
  export interface JsOptions {
11
11
  /** The Rook source, used to keep the blank lines between statements. */
12
12
  source: string;
@@ -22,14 +22,14 @@ export interface JsOptions {
22
22
  functions?: ReadonlySet<string> | undefined;
23
23
  /** The CSS modules the module imports, by their paths as written. */
24
24
  styleModules?: ReadonlyMap<string, StyleModule> | undefined;
25
- /** Create elements with their attributes by a helper, $$element; see creation.ts. */
26
- elementHelper?: boolean | undefined;
27
25
  /** For `go`: where the worker module is (`./app.worker.js`), or in it, the functions it runs. */
28
26
  goroutines?: {
29
27
  workerUrl: string;
30
28
  } | {
31
29
  targets: readonly string[];
32
30
  } | undefined;
31
+ /** How the module gets the runtime it uses: imported, by default, or in it (runtime.ts). */
32
+ runtime?: RuntimeMode | undefined;
33
33
  }
34
34
  /** A decorator of another module, with the types of the expressions of that module. */
35
35
  export interface ImportedDecorator extends DecoratorSource {
@@ -67,8 +67,6 @@ export declare abstract class Emitter {
67
67
  private level;
68
68
  private readonly scopes;
69
69
  protected fn: FunctionContext;
70
- /** Helpers that the module must define, such as $$runDeferred. */
71
- protected readonly helpers: Set<Helper>;
72
70
  /**
73
71
  * Whether code for the current expression may be written before the statement that contains it.
74
72
  * Elements use this to be created in statements of their own; it is off where an expression is
@@ -27,8 +27,6 @@ export class Emitter {
27
27
  temps: 0,
28
28
  breakTargets: [],
29
29
  };
30
- /** Helpers that the module must define, such as $$runDeferred. */
31
- helpers = new Set();
32
30
  /**
33
31
  * Whether code for the current expression may be written before the statement that contains it.
34
32
  * Elements use this to be created in statements of their own; it is off where an expression is
@@ -18,11 +18,14 @@ export declare abstract class ExpressionEmitter extends Emitter {
18
18
  protected abstract arrayLiteral(node: ast.ArrayLiteral): string;
19
19
  protected abstract conversion(node: ast.ConversionExpression): string;
20
20
  protected abstract make(node: ast.MakeExpression): string;
21
+ protected abstract chan(node: ast.ChanExpression): string;
22
+ protected abstract streamChan(node: ast.StreamChanExpression): string;
23
+ protected abstract queue(node: ast.QueueExpression): string;
21
24
  /** `<-ch` that gives `[value, ok]`, in `v, ok = <-ch`. */
22
25
  protected readonly commaOk: WeakSet<ast.Expression>;
23
26
  /** `items[i]` in a part of `go for`, which is `items[$$i]` there (parallel.ts). */
24
27
  protected readonly partElements: Set<ast.Expression>;
25
- /** In the worker module, `go` runs in the same worker (go-helpers.ts). */
28
+ /** In the worker module, `go` runs in the same worker (runtime.ts). */
26
29
  protected get inWorker(): boolean;
27
30
  /** `v, ok = <-ch`: two variables from a single receive. */
28
31
  protected markCommaOk(count: number, values: readonly ast.Expression[]): void;
@@ -3,7 +3,7 @@ import { goCaptures, goParams, goTarget } from "../goroutines.js";
3
3
  import { isIterator, usesThis } from "../iterators.js";
4
4
  import { containsElement, startsWithObjectLiteral } from "./analysis.js";
5
5
  import { Emitter } from "./emitter.js";
6
- import { PREDECLARED } from "./helpers.js";
6
+ import { PREDECLARED } from "./runtime.js";
7
7
  import { ARROW, BINARY, CONDITIONAL, POSTFIX, PRIMARY, UNARY } from "./syntax.js";
8
8
  /** Expressions, with the parentheses that JS operator precedence requires. */
9
9
  export class ExpressionEmitter extends Emitter {
@@ -11,7 +11,7 @@ export class ExpressionEmitter extends Emitter {
11
11
  commaOk = new WeakSet();
12
12
  /** `items[i]` in a part of `go for`, which is `items[$$i]` there (parallel.ts). */
13
13
  partElements = new Set();
14
- /** In the worker module, `go` runs in the same worker (go-helpers.ts). */
14
+ /** In the worker module, `go` runs in the same worker (runtime.ts). */
15
15
  get inWorker() {
16
16
  return this.options.goroutines !== undefined && 'targets' in this.options.goroutines;
17
17
  }
@@ -59,9 +59,7 @@ export class ExpressionEmitter extends Emitter {
59
59
  return [this.functionNames.get(component) ?? `$$${component.name.name}`, PRIMARY];
60
60
  }
61
61
  // `close(ch)` and the like are predeclared; JS has a `close` of its own, the window's.
62
- const helper = Object.hasOwn(PREDECLARED, node.name) ? PREDECLARED[node.name] : undefined;
63
- if (helper && this.lookup(node.name) === undefined) {
64
- this.helpers.add(helper);
62
+ if (PREDECLARED.has(node.name) && this.lookup(node.name) === undefined) {
65
63
  return [`$$${node.name}`, PRIMARY];
66
64
  }
67
65
  // The predeclared `error` used as a value is JS's `Error`, which also works without `new`.
@@ -108,12 +106,12 @@ export class ExpressionEmitter extends Emitter {
108
106
  }
109
107
  case 'ArrowFunction':
110
108
  return [this.arrowFunction(node), ARROW];
111
- // `chan<number>(10)`: the channel keeps the zero value that a closed one gives.
112
- case 'ChanExpression': {
113
- const size = node.size ? this.expression(node.size, ARROW) : '0';
114
- this.helpers.add('chan');
115
- return [`$$chan(${size}, ${this.zeroValue(node.element) ?? 'null'})`, POSTFIX];
116
- }
109
+ case 'ChanExpression':
110
+ return [this.chan(node), POSTFIX];
111
+ case 'StreamChanExpression':
112
+ return [this.streamChan(node), POSTFIX];
113
+ case 'QueueExpression':
114
+ return [this.queue(node), POSTFIX];
117
115
  // The worker module has the function under the same name, a literal under the name given to
118
116
  // it, after the local names it uses; the arguments are copied there. In the worker module a
119
117
  // literal runs in the same worker, with copies of those names as its first parameters.
@@ -129,7 +127,6 @@ export class ExpressionEmitter extends Emitter {
129
127
  const fn = literal && this.inWorker
130
128
  ? this.expression({ ...literal, params: [...goParams(literal, captured), ...literal.params] }, ARROW)
131
129
  : JSON.stringify(goTarget(this.program, node));
132
- this.helpers.add(this.inWorker ? 'goLocal' : 'go');
133
130
  return [`$$go(${fn}, [${args.filter((arg) => arg !== '').join(', ')}])`, POSTFIX];
134
131
  }
135
132
  case 'UnaryExpression':
@@ -225,7 +222,6 @@ export class ExpressionEmitter extends Emitter {
225
222
  }
226
223
  unary(node) {
227
224
  if (node.operator === '<-') {
228
- this.helpers.add('chan');
229
225
  const channel = this.expression(node.argument, ARROW);
230
226
  return `await $$receive(${channel}${this.commaOk.has(node) ? ', true' : ''})`;
231
227
  }
@@ -22,7 +22,6 @@ export class FunctionEmitter extends WebComponentEmitter {
22
22
  this.statementsWithDefers(body.body);
23
23
  }
24
24
  else if (mode === 'stack') {
25
- this.helpers.add('runDeferred');
26
25
  this.line('const $$defer = [];');
27
26
  const statements = this.block(() => this.statements(body.body));
28
27
  const finalizer = this.block(() => this.line('$$runDeferred($$defer);'));
@@ -60,7 +59,6 @@ export class FunctionEmitter extends WebComponentEmitter {
60
59
  effectBody(body, cleanups) {
61
60
  return this.withFunction('effect', [], () => {
62
61
  this.fn.deferList = cleanups;
63
- this.helpers.add('runDeferred');
64
62
  return this.block(() => this.withScope(declarationsOf(body.body), () => {
65
63
  this.line(`$$runDeferred(${cleanups}.splice(0));`);
66
64
  this.statements(body.body);
@@ -3,18 +3,11 @@ import type { JsOptions } from './emitter.ts';
3
3
  export type { JsOptions } from './emitter.ts';
4
4
  /** A component whose inlined copies add more lines than this to its module becomes a function. */
5
5
  export declare const INLINE_LIMIT = 100;
6
- /**
7
- * A module whose code creates more elements than this creates them with $$element, which takes
8
- * their attributes: its definition takes less than the statements it saves.
9
- */
10
- export declare const ELEMENT_HELPER_LIMIT = 20;
11
6
  /**
12
7
  * Generates a readable ES module from a program that has parsed without errors. A component is
13
8
  * inlined where it is used, unless that adds more than INLINE_LIMIT lines compared to a function
14
9
  * and its calls. The module is generated again with each such candidate as a function, and the one
15
10
  * that saves the most goes first: that changes the sizes of the components inside it. A component
16
11
  * of another module is a candidate too; its function is in that module, and counts as one copy.
17
- * Then, if the code creates more than ELEMENT_HELPER_LIMIT elements, it is generated again with
18
- * the helper that creates them.
19
12
  */
20
13
  export declare function generateJs(program: ast.Program, options: JsOptions): string;
@@ -1,31 +1,16 @@
1
1
  import { declarationsOf } from "./analysis.js";
2
2
  import { FunctionEmitter } from "./functions.js";
3
- import { startWorker } from "./go-helpers.js";
4
- import { HELPER_NEEDS, HELPERS } from "./helpers.js";
3
+ import { moduleGo, runtimeCode, runtimeNames } from "./runtime.js";
5
4
  /** A component whose inlined copies add more lines than this to its module becomes a function. */
6
5
  export const INLINE_LIMIT = 100;
7
- /**
8
- * A module whose code creates more elements than this creates them with $$element, which takes
9
- * their attributes: its definition takes less than the statements it saves.
10
- */
11
- export const ELEMENT_HELPER_LIMIT = 20;
12
6
  /**
13
7
  * Generates a readable ES module from a program that has parsed without errors. A component is
14
8
  * inlined where it is used, unless that adds more than INLINE_LIMIT lines compared to a function
15
9
  * and its calls. The module is generated again with each such candidate as a function, and the one
16
10
  * that saves the most goes first: that changes the sizes of the components inside it. A component
17
11
  * of another module is a candidate too; its function is in that module, and counts as one copy.
18
- * Then, if the code creates more than ELEMENT_HELPER_LIMIT elements, it is generated again with
19
- * the helper that creates them.
20
12
  */
21
13
  export function generateJs(program, options) {
22
- const { code, functions, elements } = chooseFunctions(program, options);
23
- if (elements <= ELEMENT_HELPER_LIMIT)
24
- return code;
25
- return new JsGenerator(program, { ...options, functions, elementHelper: true }).generate();
26
- }
27
- /** The code with the components that become functions; see generateJs. */
28
- function chooseFunctions(program, options) {
29
14
  const functions = new Set();
30
15
  const generate = (names) => {
31
16
  const generator = new JsGenerator(program, { ...options, functions: names });
@@ -47,14 +32,13 @@ function chooseFunctions(program, options) {
47
32
  if (saved > INLINE_LIMIT && (!best || saved > best.saved))
48
33
  best = { ...trial, name, saved };
49
34
  }
50
- if (!best) {
51
- return { code: current.code, functions, elements: current.generator.createdElements };
52
- }
35
+ if (!best)
36
+ return current.code;
53
37
  functions.add(best.name);
54
38
  current = best;
55
39
  }
56
40
  }
57
- /** The whole module: its statements, plus the helpers that its code needs. */
41
+ /** The whole module: its statements, plus the runtime that its code needs. */
58
42
  class JsGenerator extends FunctionEmitter {
59
43
  generate() {
60
44
  this.withScope(declarationsOf(this.program.body), () => {
@@ -63,51 +47,41 @@ class JsGenerator extends FunctionEmitter {
63
47
  this.hiddenExports();
64
48
  });
65
49
  const { goroutines } = this.options;
50
+ const inWorker = goroutines !== undefined && 'targets' in goroutines;
66
51
  // The worker module runs the functions that `go` calls (see goroutines.ts).
67
- if (goroutines && 'targets' in goroutines) {
52
+ if (inWorker) {
68
53
  const names = goroutines.targets.map((name) => this.name(name));
69
- this.helpers.add('serve');
70
54
  this.lines.push('', `const $$goroutines = { ${names.join(', ')} };`, '$$serve($$goroutines);');
71
55
  }
56
+ let imports = 0;
57
+ while (this.lines[imports]?.startsWith('import '))
58
+ imports++;
59
+ const rest = this.lines.slice(imports);
72
60
  // The CSS of the components goes before the code, so that the page has it before the markup.
73
61
  const styles = this.styleRegistrations();
74
- let lines = this.lines;
75
- if (styles.length > 0)
76
- this.helpers.add('style');
77
- if (this.helpers.size > 0) {
78
- const needed = new Set();
79
- const add = (helper) => {
80
- if (needed.has(helper))
81
- return;
82
- needed.add(helper);
83
- for (const other of HELPER_NEEDS[helper] ?? [])
84
- add(other);
85
- };
86
- for (const helper of this.helpers)
87
- add(helper);
88
- const workerUrl = goroutines && 'workerUrl' in goroutines ? goroutines.workerUrl : '';
89
- const helpers = [...needed]
90
- .sort()
91
- .flatMap((helper, i) => [
92
- ...(i > 0 ? [''] : []),
93
- ...HELPERS[helper],
94
- ...(helper === 'go' ? ['', ...startWorker(workerUrl)] : []),
95
- ]);
96
- let imports = 0;
97
- while (lines[imports]?.startsWith('import '))
98
- imports++;
99
- const rest = lines.slice(imports);
100
- while (rest[0] === '')
101
- rest.shift();
102
- lines = [
103
- ...lines.slice(0, imports),
104
- ...(imports > 0 ? [''] : []),
105
- ...helpers,
106
- ...(styles.length > 0 ? ['', ...styles] : []),
107
- ...(rest.length > 0 ? [''] : []),
108
- ...rest,
109
- ];
62
+ // What is the module's own: its `go`, and the CSS of its components for their shadow roots.
63
+ const own = [
64
+ ...(/(?<![.\w$])\$\$go(?![\w$])/.test(rest.join('\n'))
65
+ ? moduleGo(inWorker ? null : (goroutines?.workerUrl ?? ''))
66
+ : []),
67
+ ...(styles.length > 0 ? ['const $$styles = [];'] : []),
68
+ ];
69
+ const mode = this.options.runtime ?? 'import';
70
+ const runtime = runtimeCode(runtimeNames([...own, ...styles, ...rest].join('\n')), mode);
71
+ if (runtime.length === 0 && own.length === 0 && styles.length === 0) {
72
+ return this.lines.length === 0 ? '' : `${this.lines.join('\n')}\n`;
110
73
  }
111
- return lines.length === 0 ? '' : `${lines.join('\n')}\n`;
74
+ while (rest[0] === '')
75
+ rest.shift();
76
+ // The import of the runtime goes with the other imports; the runtime itself, after them.
77
+ const head = this.lines.slice(0, imports);
78
+ const sections = [
79
+ mode === 'import' ? [...head, ...runtime] : head,
80
+ mode === 'inline' ? runtime : [],
81
+ own,
82
+ styles,
83
+ rest,
84
+ ].filter((section) => section.length > 0);
85
+ return `${sections.flatMap((section, i) => [...(i > 0 ? [''] : []), ...section]).join('\n')}\n`;
112
86
  }
113
87
  }
@@ -4,7 +4,7 @@ import { declaredIn } from "../names.js";
4
4
  import { ControlStatementEmitter } from "./control-statements.js";
5
5
  import { ARROW } from "./syntax.js";
6
6
  // `go for i, v in items { ... }`: `await $$goFor("$$goroutine0", items, ...)` splits the array into
7
- // parts and runs each as a goroutine (go-helpers.ts). In the worker module, the function of a part
7
+ // parts and runs each as a goroutine (src/runtime/go.js). In the worker module, the function of a part
8
8
  // loops over it: `i` is the index in the whole array, from `$$from`, the index of the part's first
9
9
  // element, and `items[i]` is `items[$$i]`, the index in the part. The part goes back if the body
10
10
  // writes into it; a typed array is moved back, not copied.
@@ -24,9 +24,7 @@ export class ParallelLoopEmitter extends ControlStatementEmitter {
24
24
  this.expression(node.iterable, ARROW),
25
25
  ...goCaptures(this.program, node).map((name) => this.name(name)),
26
26
  ];
27
- this.helpers.add('goFor');
28
- this.helpers.add(this.inWorker ? 'goLocal' : 'go');
29
- this.line(`await $$goFor(${args.join(', ')});`);
27
+ this.line(`await $$goFor($$go, ${args.join(', ')});`);
30
28
  }
31
29
  /** The loop over a part, in the function of the worker module that runs it. */
32
30
  partLoop(node, loop) {
@@ -65,7 +65,6 @@ export class RefEmitter extends EarlyReturnEmitter {
65
65
  const scopeClass = scope && !hasClass(element) ? scope : null;
66
66
  let creation = { covered: new Set(), scoped: false };
67
67
  if (preset) {
68
- this.createdElements++;
69
68
  this.line(`const ${name} = ${preset.name} ?? ${this.createElement(tag, svg)};`);
70
69
  }
71
70
  else {
@@ -0,0 +1,15 @@
1
+ /** Where compiled code imports the runtime from. */
2
+ export declare const RUNTIME_MODULE = "rooklang/runtime";
3
+ /** How a module gets the runtime: by an import, or with the code of the runtime in it. */
4
+ export type RuntimeMode = 'import' | 'inline';
5
+ /** Predeclared functions of goroutines and channels: `close(ch)` is `$$close(ch)`. */
6
+ export declare const PREDECLARED: ReadonlySet<string>;
7
+ /** The names of the runtime that code uses, in order. */
8
+ export declare function runtimeNames(code: string): string[];
9
+ /** What gives a module these names of the runtime: an import, or the files that have them. */
10
+ export declare function runtimeCode(names: readonly string[], mode: RuntimeMode): string[];
11
+ /**
12
+ * The `go` of a module: in a module, a call in a worker that starts with its worker module; in the
13
+ * worker module, a call in the same worker, of a function of `$$goroutines`.
14
+ */
15
+ export declare function moduleGo(workerUrl: string | null): string[];