rooklang 0.3.2 → 0.3.4

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 (178) hide show
  1. package/README.md +32 -7
  2. package/README.ru.md +32 -7
  3. package/dist/ast/declarations.d.ts +20 -1
  4. package/dist/ast/expressions.d.ts +31 -1
  5. package/dist/ast/markup.d.ts +6 -1
  6. package/dist/ast/statements.d.ts +25 -1
  7. package/dist/auto-import.d.ts +11 -0
  8. package/dist/auto-import.js +81 -0
  9. package/dist/breaks.d.ts +6 -0
  10. package/dist/breaks.js +36 -0
  11. package/dist/build.d.ts +7 -0
  12. package/dist/build.js +23 -2
  13. package/dist/checker/array-literals.d.ts +6 -0
  14. package/dist/checker/array-literals.js +59 -0
  15. package/dist/checker/assignments.d.ts +7 -0
  16. package/dist/checker/assignments.js +48 -8
  17. package/dist/checker/base.d.ts +2 -1
  18. package/dist/checker/base.js +6 -12
  19. package/dist/checker/builtins.js +15 -5
  20. package/dist/checker/bytes.d.ts +2 -2
  21. package/dist/checker/bytes.js +2 -2
  22. package/dist/checker/calls.js +3 -0
  23. package/dist/checker/channel-objects.d.ts +3 -0
  24. package/dist/checker/channel-objects.js +40 -0
  25. package/dist/checker/channels.d.ts +1 -0
  26. package/dist/checker/channels.js +16 -2
  27. package/dist/checker/checker.d.ts +1 -1
  28. package/dist/checker/checker.js +19 -0
  29. package/dist/checker/class-members.d.ts +22 -0
  30. package/dist/checker/class-members.js +105 -0
  31. package/dist/checker/classes.d.ts +5 -4
  32. package/dist/checker/classes.js +41 -12
  33. package/dist/checker/closing.d.ts +21 -0
  34. package/dist/checker/closing.js +164 -0
  35. package/dist/checker/completion.d.ts +2 -2
  36. package/dist/checker/completion.js +2 -2
  37. package/dist/checker/context.d.ts +19 -0
  38. package/dist/checker/control-flow.d.ts +6 -0
  39. package/dist/checker/control-flow.js +34 -0
  40. package/dist/checker/declarations.js +1 -0
  41. package/dist/checker/decode.d.ts +8 -0
  42. package/dist/checker/decode.js +32 -0
  43. package/dist/checker/error-results.d.ts +6 -0
  44. package/dist/checker/error-results.js +31 -0
  45. package/dist/checker/expressions.d.ts +5 -1
  46. package/dist/checker/expressions.js +13 -34
  47. package/dist/checker/flow.js +8 -4
  48. package/dist/checker/helpers.d.ts +1 -1
  49. package/dist/checker/helpers.js +9 -2
  50. package/dist/checker/iterables.d.ts +3 -1
  51. package/dist/checker/iterables.js +13 -0
  52. package/dist/checker/iterators.d.ts +4 -1
  53. package/dist/checker/iterators.js +12 -4
  54. package/dist/checker/json.d.ts +5 -0
  55. package/dist/checker/json.js +92 -0
  56. package/dist/checker/loops.js +23 -3
  57. package/dist/checker/members.js +7 -0
  58. package/dist/checker/operators.d.ts +2 -2
  59. package/dist/checker/operators.js +7 -5
  60. package/dist/checker/queues.d.ts +2 -2
  61. package/dist/checker/queues.js +2 -2
  62. package/dist/checker/resolution.js +5 -3
  63. package/dist/checker/signals.d.ts +8 -0
  64. package/dist/checker/signals.js +48 -0
  65. package/dist/checker/statements.d.ts +5 -0
  66. package/dist/checker/statements.js +19 -0
  67. package/dist/checker/topics.d.ts +6 -0
  68. package/dist/checker/topics.js +17 -0
  69. package/dist/checker/transfer.d.ts +3 -1
  70. package/dist/checker/transfer.js +25 -5
  71. package/dist/checker/try.d.ts +6 -0
  72. package/dist/checker/try.js +19 -0
  73. package/dist/checker/types/constructors.d.ts +1 -1
  74. package/dist/checker/types/constructors.js +2 -2
  75. package/dist/checker/types/generics.js +1 -1
  76. package/dist/checker/types/iterables.d.ts +13 -0
  77. package/dist/checker/types/iterables.js +52 -0
  78. package/dist/checker/types/model.d.ts +25 -0
  79. package/dist/checker/types/params.js +22 -3
  80. package/dist/checker/types/printing.js +4 -3
  81. package/dist/checker/types/relations.js +19 -25
  82. package/dist/checker/types/topic.d.ts +6 -0
  83. package/dist/checker/types/topic.js +23 -0
  84. package/dist/checker/types.d.ts +2 -0
  85. package/dist/checker/types.js +2 -0
  86. package/dist/cli.js +18 -36
  87. package/dist/codegen/calls.d.ts +23 -0
  88. package/dist/codegen/calls.js +88 -7
  89. package/dist/codegen/control-statements.d.ts +19 -3
  90. package/dist/codegen/control-statements.js +105 -20
  91. package/dist/codegen/decode.d.ts +14 -0
  92. package/dist/codegen/decode.js +93 -0
  93. package/dist/codegen/emitter.d.ts +10 -41
  94. package/dist/codegen/emitter.js +9 -4
  95. package/dist/codegen/expressions.d.ts +9 -0
  96. package/dist/codegen/expressions.js +15 -24
  97. package/dist/codegen/js.d.ts +2 -2
  98. package/dist/codegen/js.js +5 -2
  99. package/dist/codegen/options.d.ts +40 -0
  100. package/dist/codegen/options.js +1 -0
  101. package/dist/codegen/runtime.d.ts +7 -5
  102. package/dist/codegen/runtime.js +28 -20
  103. package/dist/codegen/simple-statements.d.ts +10 -4
  104. package/dist/codegen/simple-statements.js +40 -15
  105. package/dist/codegen/statements.js +64 -4
  106. package/dist/codegen/try.d.ts +5 -0
  107. package/dist/codegen/try.js +40 -0
  108. package/dist/compile.d.ts +9 -0
  109. package/dist/compile.js +19 -6
  110. package/dist/completion.d.ts +4 -2
  111. package/dist/completion.js +10 -5
  112. package/dist/declarations.js +32 -6
  113. package/dist/decorator-copy.js +5 -2
  114. package/dist/goroutines.d.ts +1 -1
  115. package/dist/goroutines.js +5 -4
  116. package/dist/index.d.ts +6 -2
  117. package/dist/index.js +14 -5
  118. package/dist/lexer/base.d.ts +2 -0
  119. package/dist/lexer/base.js +6 -1
  120. package/dist/lexer/lexer.js +3 -0
  121. package/dist/lexer/literals.d.ts +5 -0
  122. package/dist/lexer/literals.js +51 -1
  123. package/dist/lexer/markup.js +3 -4
  124. package/dist/lexer/tables.js +2 -0
  125. package/dist/lexer/token.d.ts +4 -3
  126. package/dist/lexer/token.js +0 -2
  127. package/dist/loader.js +40 -13
  128. package/dist/parser/base.d.ts +4 -0
  129. package/dist/parser/base.js +2 -1
  130. package/dist/parser/classes.d.ts +2 -0
  131. package/dist/parser/classes.js +88 -23
  132. package/dist/parser/control-flow.d.ts +3 -1
  133. package/dist/parser/control-flow.js +21 -24
  134. package/dist/parser/declarations.d.ts +7 -4
  135. package/dist/parser/declarations.js +45 -6
  136. package/dist/parser/expressions.js +27 -3
  137. package/dist/parser/jumps.d.ts +11 -0
  138. package/dist/parser/jumps.js +77 -0
  139. package/dist/parser/literals.d.ts +9 -0
  140. package/dist/parser/literals.js +45 -6
  141. package/dist/parser/markup.js +1 -1
  142. package/dist/parser/simple-statements.d.ts +1 -1
  143. package/dist/parser/simple-statements.js +16 -9
  144. package/dist/parser/statements.d.ts +0 -3
  145. package/dist/parser/statements.js +22 -25
  146. package/dist/parser/syntax.d.ts +11 -1
  147. package/dist/parser/syntax.js +16 -2
  148. package/dist/parser/types.js +8 -3
  149. package/dist/project-modules.d.ts +12 -0
  150. package/dist/project-modules.js +106 -0
  151. package/dist/run-file.d.ts +6 -0
  152. package/dist/run-file.js +111 -0
  153. package/dist/runtime/sources.js +11 -5
  154. package/dist/runtime-module.d.ts +1 -0
  155. package/dist/runtime-module.js +3 -0
  156. package/dist/source-map.d.ts +17 -0
  157. package/dist/source-map.js +75 -0
  158. package/dist/ts-types.d.ts +4 -0
  159. package/dist/ts-types.js +20 -3
  160. package/dist/typescript/convert-objects.d.ts +2 -1
  161. package/dist/typescript/convert-objects.js +18 -9
  162. package/dist/vite.js +13 -5
  163. package/dist/walk-types.js +1 -0
  164. package/dist/walk.d.ts +0 -2
  165. package/dist/walk.js +11 -28
  166. package/package.json +1 -1
  167. package/src/runtime/chan.js +15 -5
  168. package/src/runtime/channel.js +28 -0
  169. package/src/runtime/classes.js +32 -0
  170. package/src/runtime/core.js +10 -1
  171. package/src/runtime/decode.js +190 -0
  172. package/src/runtime/go.js +14 -5
  173. package/src/runtime/index.js +6 -0
  174. package/src/runtime/select.js +14 -9
  175. package/src/runtime/signals.js +59 -0
  176. package/src/runtime/threads.js +32 -5
  177. package/src/runtime/topic.js +118 -0
  178. package/src/runtime/try.js +45 -0
@@ -1,4 +1,6 @@
1
- import { typedArrayOf } from "../checker/types.js";
1
+ import { elementOf, isChan } from "../checker/channel.js";
2
+ import { isNullable, isQueue, isTopic, nonNull, topicElement, typedArrayOf, } from "../checker/types.js";
3
+ import { goCaptures, goParams, goTarget } from "../goroutines.js";
2
4
  import { isYieldCall } from "../iterators.js";
3
5
  import { TYPED_ARRAYS, TYPED_ELEMENTS } from "../typed-arrays.js";
4
6
  import { hasOptionalLink } from "./analysis.js";
@@ -8,21 +10,50 @@ import { ARROW, BINARY, POSTFIX } from "./syntax.js";
8
10
  // checker found one expected becomes `new Uint8Array([...])`, and the conversions of Go between
9
11
  // text and its UTF-8 bytes use TextEncoder and TextDecoder, `[]byte(s)` and `string(data)`. In an
10
12
  // iterator, a generator, `yield(v)` yields: its value is `true`, since a generator that the loop
11
- // over it leaves is closed at that `yield` and goes no further, so `false` is never seen.
13
+ // over it leaves is closed at that `yield` and goes no further, so `false` is never seen. Making
14
+ // channels, queues and topics is here too, with what a topic needs where it is received or closed.
12
15
  export class CallEmitter extends ExpressionEmitter {
13
16
  call(node) {
14
17
  // After `?.` the arguments may not be evaluated at all.
15
18
  const conditional = node.optional || hasOptionalLink(node.callee);
16
19
  const args = this.withHoisting(this.hoist && !conditional, () => this.elements(node.arguments));
17
- if (this.isBuiltinError(node.callee) && !node.optional)
18
- return `new Error(${args})`;
20
+ // `error("load user", err)` wraps an error, with its message after this one (runtime/core.js).
21
+ if (this.isBuiltinError(node.callee) && !node.optional) {
22
+ return node.arguments.length > 1 ? `$$wrap(${args})` : `new Error(${args})`;
23
+ }
19
24
  if (this.isStringConversion(node.callee))
20
25
  return `new TextDecoder().decode(${args})`;
21
26
  const yielded = this.yielded(node);
22
27
  if (yielded !== null)
23
28
  return `(${yielded}, true)`;
29
+ // `close(t)` of a topic: it stops listening and sending.
30
+ const [argument] = node.arguments;
31
+ if (node.callee.kind === 'Identifier' &&
32
+ node.callee.name === 'close' &&
33
+ this.lookup('close') === undefined &&
34
+ argument?.kind !== 'SpreadElement' &&
35
+ argument !== undefined &&
36
+ this.isTopicValue(argument)) {
37
+ return `$$Topic.close(${args})`;
38
+ }
24
39
  return `${this.expression(node.callee, POSTFIX)}${node.optional ? '?.' : ''}(${args})`;
25
40
  }
41
+ /**
42
+ * `go f(x)`: the worker module has the function under the same name, a literal under the name
43
+ * given to it, after the local names it uses; the arguments are copied there. In the worker
44
+ * module a literal runs in the same worker, with copies of those names as its first parameters.
45
+ */
46
+ go(node) {
47
+ const { callee } = node.call;
48
+ const literal = callee.kind === 'FuncExpression' ? callee : null;
49
+ const captured = literal ? goCaptures(this.program, node) : [];
50
+ const args = [...captured.map((name) => this.name(name)), this.elements(node.call.arguments)];
51
+ // Without type checking, a callee that is neither fails when the call runs.
52
+ const fn = literal && this.inWorker
53
+ ? this.expression({ ...literal, params: [...goParams(literal, captured), ...literal.params] }, ARROW)
54
+ : JSON.stringify(goTarget(this.program, node));
55
+ return `$$go(${fn}, [${args.filter((arg) => arg !== '').join(', ')}])`;
56
+ }
26
57
  /** `yield v` for `yield(v)` in an iterator, `yield [k, v]` for `yield(k, v)`; else `null`. */
27
58
  yielded(node) {
28
59
  if (!this.fn.iterator || !isYieldCall(node) || this.lookup('yield') !== 'param')
@@ -37,7 +68,8 @@ export class CallEmitter extends ExpressionEmitter {
37
68
  yieldStatement(node) {
38
69
  const [only] = node.consequent.body;
39
70
  const { condition } = node;
40
- if (node.alternate ||
71
+ if (node.init ||
72
+ node.alternate ||
41
73
  node.consequent.body.length !== 1 ||
42
74
  only?.kind !== 'ReturnStatement' ||
43
75
  only.values.length > 0 ||
@@ -105,12 +137,55 @@ export class CallEmitter extends ExpressionEmitter {
105
137
  /** `chan<number>(10)`: the channel keeps the zero value that a closed one gives. */
106
138
  chan(node) {
107
139
  const size = node.size ? this.expression(node.size, ARROW) : '0';
108
- return `$$chan(${size}, ${this.zeroValue(node.element) ?? 'null'})`;
140
+ return `$$Channel.make(${size}, ${this.zeroValue(node.element) ?? 'null'})`;
109
141
  }
110
142
  /** `queue<number>(items)`: the queue copies the values, as `[...items]` would. */
111
143
  queue(node) {
112
144
  return `new $$Queue(${node.items ? this.expression(node.items, ARROW) : ''})`;
113
145
  }
146
+ /** Whether the checker found a topic here, or a `?topic T` in a case of `select`. */
147
+ isTopicValue(node) {
148
+ const type = this.options.types?.get(node);
149
+ return type !== undefined && isTopic(isNullable(type) ? nonNull(type) : type);
150
+ }
151
+ /**
152
+ * The channel that `<-x`, `for v in x` and a case of `select` receive from: a topic listens from
153
+ * the first of them on, into a channel of its own (src/runtime/topic.js).
154
+ */
155
+ receivedFrom(node) {
156
+ const channel = this.expression(node, ARROW);
157
+ return this.isTopicValue(node) ? `$$Topic.listen(${channel})` : channel;
158
+ }
159
+ /**
160
+ * The zero value that `<-x` gives if `x` is `Channel.EMPTY` or `Topic.EMPTY`, which are the same
161
+ * for all types; any other channel keeps its own. `null` when it is null.
162
+ */
163
+ receivedZero(node) {
164
+ const type = this.options.types?.get(node);
165
+ const channel = type && (isNullable(type) ? nonNull(type) : type);
166
+ if (channel && isChan(channel))
167
+ return zeroOf(elementOf(channel));
168
+ if (channel && isTopic(channel))
169
+ return zeroOf(topicElement(channel));
170
+ return 'null';
171
+ }
172
+ receive(node, withOk) {
173
+ const zero = this.receivedZero(node);
174
+ const args = zero !== 'null' ? `, ${withOk}, ${zero}` : withOk ? ', true' : '';
175
+ return `$$Channel.receive(${this.receivedFrom(node)}${args})`;
176
+ }
177
+ /** `ch <- v` waits until the channel takes the value; a topic sends and goes on. */
178
+ send(node) {
179
+ const channel = this.expression(node.channel, ARROW);
180
+ const value = this.expression(node.value, ARROW);
181
+ return this.isTopicValue(node.channel)
182
+ ? `$$Topic.publish(${channel}, ${value})`
183
+ : `await $$Channel.send(${channel}, ${value})`;
184
+ }
185
+ /** `topic<T>(name)`: a topic keeps the zero value that a closed one gives, as a channel does. */
186
+ topic(node) {
187
+ return `$$Topic.make(${this.expression(node.name, ARROW)}, ${this.zeroValue(node.element) ?? 'null'})`;
188
+ }
114
189
  /**
115
190
  * `chan(stream, size)`: the channel keeps the zero value of its values, as `chan<T>(size)` does,
116
191
  * from the type of the stream that the checker found; without it, null.
@@ -121,7 +196,7 @@ export class CallEmitter extends ExpressionEmitter {
121
196
  const type = this.options.types?.get(node);
122
197
  const channel = type?.kind === 'tuple' ? type.types[0] : undefined;
123
198
  const element = channel?.kind === 'object' ? channel.instanceOf?.args[0] : undefined;
124
- return `$$fromStream(${stream}, ${size}, ${element ? zeroOf(element) : 'null'})`;
199
+ return `$$Channel.fromStream(${stream}, ${size}, ${element ? zeroOf(element) : 'null'})`;
125
200
  }
126
201
  }
127
202
  /** The zero value of a type, as zeroValue gives for a type as written. */
@@ -140,6 +215,12 @@ function zeroOf(type) {
140
215
  return 'false';
141
216
  case 'array':
142
217
  return '[]';
218
+ case 'object':
219
+ if (isQueue(type))
220
+ return 'new $$Queue()';
221
+ if (isChan(type))
222
+ return '$$Channel.EMPTY';
223
+ return isTopic(type) ? '$$Topic.EMPTY' : 'null';
143
224
  default:
144
225
  return 'null';
145
226
  }
@@ -1,8 +1,14 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { SimpleStatementEmitter } from './simple-statements.ts';
3
- /** if, for, switch and try. */
4
3
  export declare abstract class ControlStatementEmitter extends SimpleStatementEmitter {
5
4
  protected ifStatement(node: ast.IfStatement): string;
5
+ /**
6
+ * `if const v = f(); v > 0 { }` → `{ const v = f(); if (v > 0) { } }`: a block keeps the
7
+ * variables of the initializer to the statement, which `emit` writes.
8
+ */
9
+ private withInit;
10
+ /** `outer: for (...)`: the label goes on the loop itself, after what is hoisted before it. */
11
+ private labeled;
6
12
  protected loopBody(body: ast.BlockStatement): string;
7
13
  protected forStatement(node: ast.ForStatement): void;
8
14
  /** A statement inside a `for` header, without the `;`. */
@@ -12,8 +18,18 @@ export declare abstract class ControlStatementEmitter extends SimpleStatementEmi
12
18
  * `for i, x in xs` iterates `xs.entries()`, which gives the keys and values of a Map too.
13
19
  */
14
20
  protected forInStatement(node: ast.ForInStatement): void;
21
+ /** `[i, v]` of a loop over pairs; `_` leaves its place empty. */
22
+ private pair;
23
+ /**
24
+ * `for i, v in items` over an array or a typed array: a loop over the indexes, which runs faster
25
+ * than one over the pairs of `entries()`. The variables of the loop cannot be assigned, so `i` is
26
+ * the counter itself. The array is read once, as `entries()` reads it, into `$$items` unless it is
27
+ * a name that cannot change.
28
+ */
29
+ private indexLoop;
30
+ protected switchStatement(node: ast.SwitchStatement): void;
15
31
  /** Each `case` ends with an implicit `break`; a case with declarations gets its own block. */
16
- protected switchStatement(node: ast.SwitchStatement, discriminant: ast.Expression): void;
32
+ private valueSwitch;
17
33
  /**
18
34
  * `select`: $$select gives the index of the case that went on and what it received, and a
19
35
  * switch runs the case; a `break` inside it leaves the select, as in Go. `default` is -1.
@@ -23,6 +39,6 @@ export declare abstract class ControlStatementEmitter extends SimpleStatementEmi
23
39
  * `switch { case cond: ... }` becomes an if/else chain. A `break` inside it must leave the
24
40
  * switch, not an enclosing loop, so then the chain gets a label to break to.
25
41
  */
26
- protected taglessSwitch(node: ast.SwitchStatement): void;
42
+ private taglessSwitch;
27
43
  protected tryStatement(node: ast.TryStatement): string;
28
44
  }
@@ -1,12 +1,19 @@
1
1
  import { isChan } from "../checker/channel.js";
2
- import { isMap } from "../checker/iterables.js";
3
- import { containsBreak } from "../walk.js";
2
+ import { asyncIterated, isMap } from "../checker/iterables.js";
3
+ import { isTopic, typedArrayOf } from "../checker/types.js";
4
+ import { declaredIn } from "../names.js";
5
+ import { containsBreak } from "../breaks.js";
4
6
  import { declarationsOf, endsWithJump } from "./analysis.js";
5
7
  import { SimpleStatementEmitter } from "./simple-statements.js";
6
8
  import { ARROW, BINARY, POSTFIX } from "./syntax.js";
7
9
  /** if, for, switch and try. */
10
+ /** Names that keep their value, so a loop over the indexes may read the array through them. */
11
+ const FIXED = new Set(['const', 'import', 'loop']);
8
12
  export class ControlStatementEmitter extends SimpleStatementEmitter {
9
13
  ifStatement(node) {
14
+ const { init } = node;
15
+ if (init)
16
+ return this.withInit(init, () => this.line(this.ifStatement({ ...node, init: null })));
10
17
  let text = `if (${this.expression(node.condition, 0)}) ${this.blockStatement(node.consequent)}`;
11
18
  if (node.alternate?.kind === 'IfStatement') {
12
19
  // The condition of `else if` is evaluated only when the first one is false.
@@ -18,6 +25,20 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
18
25
  }
19
26
  return text;
20
27
  }
28
+ /**
29
+ * `if const v = f(); v > 0 { }` → `{ const v = f(); if (v > 0) { } }`: a block keeps the
30
+ * variables of the initializer to the statement, which `emit` writes.
31
+ */
32
+ withInit(init, emit) {
33
+ return this.block(() => this.withScope(declarationsOf([init]), () => {
34
+ this.statement(init);
35
+ this.withHoisting(true, emit);
36
+ }));
37
+ }
38
+ /** `outer: for (...)`: the label goes on the loop itself, after what is hoisted before it. */
39
+ labeled(node, text) {
40
+ return node.label ? `${this.label(node.label)}: ${text}` : text;
41
+ }
21
42
  loopBody(body) {
22
43
  return this.withBreakTarget(null, () => this.blockStatement(body));
23
44
  }
@@ -33,10 +54,10 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
33
54
  node.update ? this.simpleStatement(node.update) : '',
34
55
  ]);
35
56
  if (node.init === null && node.update === null) {
36
- this.line(`while (${condition || 'true'}) ${this.loopBody(node.body)}`);
57
+ this.line(this.labeled(node, `while (${condition || 'true'}) ${this.loopBody(node.body)}`));
37
58
  return;
38
59
  }
39
- this.line(`for (${init}; ${condition}; ${update}) ${this.loopBody(node.body)}`);
60
+ this.line(this.labeled(node, `for (${init}; ${condition}; ${update}) ${this.loopBody(node.body)}`));
40
61
  });
41
62
  }
42
63
  /** A statement inside a `for` header, without the `;`. */
@@ -53,7 +74,7 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
53
74
  case 'ExpressionStatement':
54
75
  return this.expression(node.expression, 0);
55
76
  case 'SendStatement':
56
- return `await $$send(${this.expression(node.channel, ARROW)}, ${this.expression(node.value, ARROW)})`;
77
+ return this.send(node);
57
78
  }
58
79
  }
59
80
  /**
@@ -68,9 +89,14 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
68
89
  const value = this.name(node.value.name);
69
90
  const iterable = this.options.types?.get(node.iterable);
70
91
  let head;
71
- // A channel gives its values as they come, until it is closed.
72
- if (iterable && isChan(iterable)) {
73
- this.line(`for await (const ${value} of $$range(${this.expression(node.iterable, ARROW)})) ${this.loopBody(node.body)}`);
92
+ // An async iterable, such as a stream of Node, gives its values as their Promises do.
93
+ if (iterable && asyncIterated(iterable) !== null) {
94
+ this.line(this.labeled(node, `for await (const ${value} of ${this.expression(node.iterable, POSTFIX)}) ${this.loopBody(node.body)}`));
95
+ return;
96
+ }
97
+ // A channel gives its values as they come, until it is closed, and so does a topic.
98
+ if (iterable && (isChan(iterable) || isTopic(iterable))) {
99
+ this.line(this.labeled(node, `for await (const ${value} of $$Channel.range(${this.receivedFrom(node.iterable)})) ${this.loopBody(node.body)}`));
74
100
  return;
75
101
  }
76
102
  // An iterator is a generator function: called, it gives a lazy JS iterator, of pairs if its
@@ -94,16 +120,70 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
94
120
  }
95
121
  head = `const ${value} of ${values}`;
96
122
  }
123
+ else if (iterable &&
124
+ (iterable.kind === 'array' || iterable.kind === 'string' || typedArrayOf(iterable) !== null)) {
125
+ // Without the index, the loop goes over the values.
126
+ if (node.key.name === '_' && node.value.name !== '_') {
127
+ head = `const ${value} of ${this.expression(node.iterable, POSTFIX)}`;
128
+ }
129
+ else if (iterable.kind === 'string') {
130
+ // A string has no entries(): its characters, as `for...of` gives them, are numbered.
131
+ const pair = this.pair(node.key, node.value);
132
+ head = `const ${pair} of Array.from(${this.expression(node.iterable, ARROW)}).entries()`;
133
+ }
134
+ else {
135
+ this.indexLoop(node, node.key);
136
+ return;
137
+ }
138
+ }
97
139
  else {
98
- const key = node.key.name === '_' ? '' : this.name(node.key.name);
99
- const pair = node.value.name === '_' ? `[${key}]` : `[${key}, ${value}]`;
100
- head = `const ${pair} of ${this.expression(node.iterable, POSTFIX)}.entries()`;
140
+ head = `const ${this.pair(node.key, node.value)} of ${this.expression(node.iterable, POSTFIX)}.entries()`;
101
141
  }
102
- this.line(`for (${head}) ${this.loopBody(node.body)}`);
142
+ this.line(this.labeled(node, `for (${head}) ${this.loopBody(node.body)}`));
103
143
  });
104
144
  }
145
+ /** `[i, v]` of a loop over pairs; `_` leaves its place empty. */
146
+ pair(key, value) {
147
+ const name = key.name === '_' ? '' : this.name(key.name);
148
+ return value.name === '_' ? `[${name}]` : `[${name}, ${this.name(value.name)}]`;
149
+ }
150
+ /**
151
+ * `for i, v in items` over an array or a typed array: a loop over the indexes, which runs faster
152
+ * than one over the pairs of `entries()`. The variables of the loop cannot be assigned, so `i` is
153
+ * the counter itself. The array is read once, as `entries()` reads it, into `$$items` unless it is
154
+ * a name that cannot change.
155
+ */
156
+ indexLoop(node, keyNode) {
157
+ const key = keyNode.name === '_' ? '$$i' : this.name(keyNode.name);
158
+ const value = node.value.name === '_' ? null : this.name(node.value.name);
159
+ const { iterable } = node;
160
+ const fixed = iterable.kind === 'Identifier' && FIXED.has(this.lookup(iterable.name) ?? 'let');
161
+ const items = fixed ? this.expression(iterable, POSTFIX) : '$$items';
162
+ const init = fixed ? '' : `, $$items = ${this.expression(iterable, ARROW)}`;
163
+ // A declaration of the same name in the body would clash with the element's in one block.
164
+ const hidden = declaredIn(node.body.body).some((name) => name === keyNode.name || name === node.value.name);
165
+ const body = this.withBreakTarget(null, () => this.block(() => {
166
+ if (value !== null)
167
+ this.line(`const ${value} = ${items}[${key}];`);
168
+ if (hidden)
169
+ this.line(this.blockStatement(node.body));
170
+ else
171
+ this.blockStatements(node.body.body);
172
+ }));
173
+ this.line(this.labeled(node, `for (let ${key} = 0${init}; ${key} < ${items}.length; ${key}++) ${body}`));
174
+ }
175
+ switchStatement(node) {
176
+ const { init, discriminant } = node;
177
+ if (init) {
178
+ this.line(this.withInit(init, () => this.switchStatement({ ...node, init: null })));
179
+ }
180
+ else if (discriminant === null)
181
+ this.taglessSwitch(node);
182
+ else
183
+ this.valueSwitch(node, discriminant);
184
+ }
105
185
  /** Each `case` ends with an implicit `break`; a case with declarations gets its own block. */
106
- switchStatement(node, discriminant) {
186
+ valueSwitch(node, discriminant) {
107
187
  const head = `switch (${this.expression(discriminant, 0)})`;
108
188
  const body = this.withBreakTarget(null, () => this.block(() => {
109
189
  node.cases.forEach((switchCase, i) => {
@@ -130,7 +210,7 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
130
210
  }
131
211
  });
132
212
  }));
133
- this.line(`${head} ${body}`);
213
+ this.line(this.labeled(node, `${head} ${body}`));
134
214
  }
135
215
  /**
136
216
  * `select`: $$select gives the index of the case that went on and what it received, and a
@@ -141,7 +221,10 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
141
221
  if (operation === null)
142
222
  return [];
143
223
  if (operation.kind === 'UnaryExpression') {
144
- return [`{ channel: ${this.expression(operation.argument, ARROW)} }`];
224
+ const zero = this.receivedZero(operation.argument);
225
+ return [
226
+ `{ channel: ${this.receivedFrom(operation.argument)}${zero !== 'null' ? `, zero: ${zero}` : ''} }`,
227
+ ];
145
228
  }
146
229
  const channel = this.expression(operation.channel, ARROW);
147
230
  return [`{ channel: ${channel}, value: ${this.expression(operation.value, ARROW)} }`];
@@ -149,7 +232,7 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
149
232
  const hasDefault = node.cases.some((selectCase) => selectCase.operation === null);
150
233
  let index = 0;
151
234
  const select = this.block(() => {
152
- this.line(`const [$$case, $$received, $$ok] = await $$select([${operations.join(', ')}], ${hasDefault});`);
235
+ this.line(`const [$$case, $$received, $$ok] = await $$Channel.select([${operations.join(', ')}], ${hasDefault});`);
153
236
  const body = this.withBreakTarget(null, () => this.block(() => {
154
237
  for (const { names, operation, body: statements } of node.cases) {
155
238
  const label = operation === null ? 'default:' : `case ${index++}:`;
@@ -175,7 +258,7 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
175
258
  }));
176
259
  this.line(`switch ($$case) ${body}`);
177
260
  });
178
- this.line(select);
261
+ this.line(this.labeled(node, select));
179
262
  }
180
263
  /**
181
264
  * `switch { case cond: ... }` becomes an if/else chain. A `break` inside it must leave the
@@ -184,9 +267,11 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
184
267
  taglessSwitch(node) {
185
268
  const cases = node.cases.filter((switchCase) => switchCase.tests.length > 0);
186
269
  const fallback = node.cases.find((switchCase) => switchCase.tests.length === 0);
187
- const label = node.cases.some((switchCase) => containsBreak(switchCase.body))
188
- ? `$$switch${++this.labels}`
189
- : null;
270
+ const label = node.label
271
+ ? this.label(node.label)
272
+ : node.cases.some((switchCase) => containsBreak(switchCase.body))
273
+ ? `$$switch${++this.labels}`
274
+ : null;
190
275
  const text = this.withBreakTarget(label, () => {
191
276
  const caseBlock = (body) => this.block(() => this.blockStatements(body));
192
277
  const branches = cases.map((switchCase) => {
@@ -0,0 +1,14 @@
1
+ import type * as ast from '../ast.ts';
2
+ import { CallEmitter } from './calls.ts';
3
+ export declare abstract class DecodeEmitter extends CallEmitter {
4
+ /** The constants of the object types that decode checks against, by type, in order. */
5
+ private readonly decodeTypes;
6
+ /** `$$decodeJson(text, type)` for a string, `$$decode(value, type)` for a value from JS. */
7
+ protected decode(node: ast.DecodeExpression): string;
8
+ /** The constants of the object types, for the start of the module. */
9
+ protected decodeTypeLines(): string[];
10
+ /** A type as runtime/decode.js has it: `"string"`, `{ array: "number" }`, `$$typeUser`. */
11
+ private shape;
12
+ /** The constant of an object type, made the first time it is used. */
13
+ private objectType;
14
+ }
@@ -0,0 +1,93 @@
1
+ import { isText } from "../checker/json.js";
2
+ import { memberNames, typeToString } from "../checker/types.js";
3
+ import { CallEmitter } from "./calls.js";
4
+ import { ARROW } from "./syntax.js";
5
+ // `decode<User>(body)`: the type, which the checker found, becomes data that the runtime checks the
6
+ // value against (runtime/decode.js). An object type is a constant of the module, `$$typeUser`,
7
+ // written before its code (js.ts), so that types that refer to themselves or each other can do so,
8
+ // and the value of each call is only its name: `$$decodeJson(body, $$typeUser)`.
9
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
10
+ export class DecodeEmitter extends CallEmitter {
11
+ /** The constants of the object types that decode checks against, by type, in order. */
12
+ decodeTypes = new Map();
13
+ /** `$$decodeJson(text, type)` for a string, `$$decode(value, type)` for a value from JS. */
14
+ decode(node) {
15
+ const argument = this.expression(node.argument, ARROW);
16
+ const result = this.options.types?.get(node);
17
+ const type = result?.kind === 'tuple' ? result.types[0] : undefined;
18
+ // Without the types of the checker nothing is checked (runtime/decode.js).
19
+ if (type === undefined)
20
+ return `$$decode(${argument}, null)`;
21
+ const given = this.options.types?.get(node.argument);
22
+ const decode = given !== undefined && isText(given) ? '$$decodeJson' : '$$decode';
23
+ return `${decode}(${argument}, ${this.shape(type)})`;
24
+ }
25
+ /** The constants of the object types, for the start of the module. */
26
+ decodeTypeLines() {
27
+ return [...this.decodeTypes.values()].flatMap(({ lines }) => lines);
28
+ }
29
+ /** A type as runtime/decode.js has it: `"string"`, `{ array: "number" }`, `$$typeUser`. */
30
+ shape(type) {
31
+ switch (type.kind) {
32
+ case 'number':
33
+ case 'string':
34
+ case 'bool':
35
+ case 'any':
36
+ return `"${type.kind}"`;
37
+ case 'null':
38
+ return '{ literal: [null] }';
39
+ case 'literal':
40
+ return `{ literal: [${JSON.stringify(type.value)}] }`;
41
+ case 'nullable':
42
+ return `{ nullable: ${this.shape(type.type)} }`;
43
+ case 'array':
44
+ return `{ array: ${this.shape(type.element)} }`;
45
+ case 'union': {
46
+ // The literals of a union are one list, as `"admin" | "user"` reads.
47
+ const literals = type.types.flatMap((each) => each.kind === 'literal' ? [each.value] : []);
48
+ const others = type.types.filter((each) => each.kind !== 'literal');
49
+ const shapes = [
50
+ ...(literals.length > 0 ? [`{ literal: [${literals.map(json).join(', ')}] }`] : []),
51
+ ...others.map((each) => this.shape(each)),
52
+ ];
53
+ return shapes.length === 1 ? shapes[0] : `{ union: [${shapes.join(', ')}] }`;
54
+ }
55
+ case 'object':
56
+ return type.index && [...memberNames(type.members)].length === 0
57
+ ? `{ record: ${this.shape(type.index)} }`
58
+ : this.objectType(type);
59
+ // The checker lets no other type through.
60
+ default:
61
+ return '"any"';
62
+ }
63
+ }
64
+ /** The constant of an object type, made the first time it is used. */
65
+ objectType(type) {
66
+ const known = this.decodeTypes.get(type);
67
+ if (known)
68
+ return known.name;
69
+ // `$$type` itself is the function of the runtime that makes them.
70
+ const written = type.instanceOf?.template.name ?? type.name ?? 'Object';
71
+ const base = `$$type${written.replace(/[^\w$]/g, '')}`;
72
+ const taken = new Set([...this.decodeTypes.values()].map(({ name }) => name));
73
+ let name = base;
74
+ for (let i = 2; taken.has(name); i++)
75
+ name = `${base}${i}`;
76
+ const entry = { name, lines: [] };
77
+ // Registered before its fields, which may refer to it.
78
+ this.decodeTypes.set(type, entry);
79
+ const fields = [...memberNames(type.members)].flatMap((field) => {
80
+ const member = type.members.get(field);
81
+ const key = IDENTIFIER.test(field) ? field : json(field);
82
+ return member ? [` ${key}: ${this.shape(member.type)},`] : [];
83
+ });
84
+ entry.lines =
85
+ fields.length === 0
86
+ ? [`const ${name} = $$type(${json(typeToString(type))}, () => ({}));`]
87
+ : [`const ${name} = $$type(${json(typeToString(type))}, () => ({`, ...fields, '}));'];
88
+ return name;
89
+ }
90
+ }
91
+ function json(value) {
92
+ return JSON.stringify(value);
93
+ }
@@ -1,42 +1,8 @@
1
1
  import type * as ast from '../ast.ts';
2
- import type { Type } from '../checker/types.ts';
3
- import { type DecoratorSource } from '../decorators.ts';
4
- import type { InlinePackage } from '../inline-packages.ts';
5
- import { type ImportedComponent, type LocalizedPackage } from './imported.ts';
2
+ import { type LocalizedPackage } from './imported.ts';
6
3
  import type { Root } from '../refs.ts';
7
- import type { StyleModule } from '../style-modules.ts';
8
4
  import type { BindingKind } from './analysis.ts';
9
- import type { RuntimeMode } from './runtime.ts';
10
- export interface JsOptions {
11
- /** The Rook source, used to keep the blank lines between statements. */
12
- source: string;
13
- /** Replace `.rk` with `.js` in relative import paths, for output written next to the sources. */
14
- rewriteImports: boolean;
15
- /** Types from the checker, when it ran; without them the generated code is more general. */
16
- types?: WeakMap<ast.Expression, Type> | undefined;
17
- /** Decorators imported from other modules, by name: their code is inlined here. */
18
- decorators?: ReadonlyMap<string, ImportedDecorator> | undefined;
19
- /** Components imported from other modules, by name: called, or inlined when they are small. */
20
- components?: ReadonlyMap<string, ImportedComponent> | undefined;
21
- /** Components of the module that become functions, since inlining them adds too many lines. */
22
- functions?: ReadonlySet<string> | undefined;
23
- /** The CSS modules the module imports, by their paths as written. */
24
- styleModules?: ReadonlyMap<string, StyleModule> | undefined;
25
- /** For `go`: where the worker module is (`./app.worker.js`), or in it, the functions it runs. */
26
- goroutines?: {
27
- workerUrl: string;
28
- } | {
29
- targets: readonly string[];
30
- } | undefined;
31
- /** How the module gets the runtime it uses: imported, by default, or in it (runtime.ts). */
32
- runtime?: RuntimeMode | undefined;
33
- }
34
- /** A decorator of another module, with the types of the expressions of that module. */
35
- export interface ImportedDecorator extends DecoratorSource {
36
- types: WeakMap<ast.Expression, Type> | null;
37
- /** The components of that module its code uses, which go with it into the components here. */
38
- inline: InlinePackage | null;
39
- }
5
+ import type { JsOptions } from './options.ts';
40
6
  /** How a function implements its `defer` statements. */
41
7
  export type DeferMode = 'none' | 'try' | 'stack' | 'effect';
42
8
  interface FunctionContext {
@@ -54,10 +20,11 @@ interface FunctionContext {
54
20
  * The base of the JS generator: the output buffer, scopes of declared names and the state of the
55
21
  * current function. The generator is split into layers, each adding to the previous one:
56
22
  *
57
- * Emitter → ExpressionEmitter → CallEmitter → SimpleStatementEmitter → ControlStatementEmitter
58
- * → ParallelLoopEmitter → StatementEmitter → UpdateEmitter → AttributeEmitter
59
- * → CreationEmitter → ElementEmitter → ControlFlowEmitter → ComponentPropsEmitter
60
- * → ComponentValueEmitter → EarlyReturnEmitter → RefEmitter
23
+ * Emitter → ExpressionEmitter → CallEmitter → DecodeEmitter → TryEmitter
24
+ * → SimpleStatementEmitter → ControlStatementEmitter → ParallelLoopEmitter
25
+ * → StatementEmitter → UpdateEmitter → AttributeEmitter → CreationEmitter → ElementEmitter
26
+ * → ControlFlowEmitter → ComponentPropsEmitter → ComponentValueEmitter
27
+ * → EarlyReturnEmitter → RefEmitter
61
28
  * → StyleEmitter → ComponentEmitter → WebComponentEmitter → FunctionEmitter → JsGenerator
62
29
  */
63
30
  export declare abstract class Emitter {
@@ -100,7 +67,7 @@ export declare abstract class Emitter {
100
67
  /** The decorators that components of the module apply, by name. */
101
68
  protected readonly appliedDecorators: Set<string>;
102
69
  /** Copies of the code of other modules: their positions are in the sources of those. */
103
- private readonly foreign;
70
+ protected readonly foreign: WeakSet<ast.Node>;
104
71
  /** Components of other modules inlined here, by name, with what their packages bring. */
105
72
  protected readonly inlinedImports: Map<string, LocalizedPackage>;
106
73
  /** The components that decorators of other modules bring, by the names of the decorators. */
@@ -123,6 +90,8 @@ export declare abstract class Emitter {
123
90
  protected lookup(name: string): BindingKind | undefined;
124
91
  /** The JS name for a Rook name. */
125
92
  protected name(name: string): string;
93
+ /** Labels are names of their own in JS, which cannot be reserved words either. */
94
+ protected label(name: ast.Identifier): string;
126
95
  protected params(params: ast.Parameter[]): string;
127
96
  protected withFunction<T>(mode: DeferMode, params: ast.Parameter[], emit: () => T): T;
128
97
  protected withHoisting<T>(hoist: boolean, emit: () => T): T;
@@ -9,10 +9,11 @@ import { JS_RESERVED, JS_UNDECLARABLE } from "./syntax.js";
9
9
  * The base of the JS generator: the output buffer, scopes of declared names and the state of the
10
10
  * current function. The generator is split into layers, each adding to the previous one:
11
11
  *
12
- * Emitter → ExpressionEmitter → CallEmitter → SimpleStatementEmitter → ControlStatementEmitter
13
- * → ParallelLoopEmitter → StatementEmitter → UpdateEmitter → AttributeEmitter
14
- * → CreationEmitter → ElementEmitter → ControlFlowEmitter → ComponentPropsEmitter
15
- * → ComponentValueEmitter → EarlyReturnEmitter → RefEmitter
12
+ * Emitter → ExpressionEmitter → CallEmitter → DecodeEmitter → TryEmitter
13
+ * → SimpleStatementEmitter → ControlStatementEmitter → ParallelLoopEmitter
14
+ * → StatementEmitter → UpdateEmitter → AttributeEmitter → CreationEmitter → ElementEmitter
15
+ * → ControlFlowEmitter → ComponentPropsEmitter → ComponentValueEmitter
16
+ * → EarlyReturnEmitter → RefEmitter
16
17
  * → StyleEmitter → ComponentEmitter → WebComponentEmitter → FunctionEmitter → JsGenerator
17
18
  */
18
19
  export class Emitter {
@@ -250,6 +251,10 @@ export class Emitter {
250
251
  const rename = JS_RESERVED.has(name) || (JS_UNDECLARABLE.has(name) && this.lookup(name) !== undefined);
251
252
  return rename ? `${name}$` : name;
252
253
  }
254
+ /** Labels are names of their own in JS, which cannot be reserved words either. */
255
+ label(name) {
256
+ return JS_RESERVED.has(name.name) ? `${name.name}$` : name.name;
257
+ }
253
258
  params(params) {
254
259
  // Several `_` parameters would be duplicate names in JS.
255
260
  const blanks = params.filter((param) => param.name.name === '_').length;
@@ -21,6 +21,15 @@ export declare abstract class ExpressionEmitter extends Emitter {
21
21
  protected abstract chan(node: ast.ChanExpression): string;
22
22
  protected abstract streamChan(node: ast.StreamChanExpression): string;
23
23
  protected abstract queue(node: ast.QueueExpression): string;
24
+ protected abstract topic(node: ast.TopicExpression): string;
25
+ /** `go f(x)`; implemented by CallEmitter. */
26
+ protected abstract go(node: ast.GoExpression): string;
27
+ /** `decode<T>(x)`; implemented by DecodeEmitter. */
28
+ protected abstract decode(node: ast.DecodeExpression): string;
29
+ /** `try x`, with the precedence of its code; implemented by TryEmitter. */
30
+ protected abstract tryValue(node: ast.TryExpression): [string, number];
31
+ /** `<-x` without its `await`; implemented by CallEmitter. */
32
+ protected abstract receive(node: ast.Expression, withOk: boolean): string;
24
33
  /** `<-ch` that gives `[value, ok]`, in `v, ok = <-ch`. */
25
34
  protected readonly commaOk: WeakSet<ast.Expression>;
26
35
  /** `items[i]` in a part of `go for`, which is `items[$$i]` there (parallel.ts). */