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.
Files changed (107) 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/assignments.d.ts +5 -0
  8. package/dist/checker/assignments.js +34 -8
  9. package/dist/checker/base.d.ts +2 -1
  10. package/dist/checker/base.js +4 -10
  11. package/dist/checker/builtins.js +4 -35
  12. package/dist/checker/bytes.d.ts +2 -2
  13. package/dist/checker/bytes.js +2 -2
  14. package/dist/checker/channels.d.ts +3 -0
  15. package/dist/checker/channels.js +39 -1
  16. package/dist/checker/context.d.ts +6 -0
  17. package/dist/checker/control-flow.d.ts +1 -0
  18. package/dist/checker/control-flow.js +17 -0
  19. package/dist/checker/error-results.d.ts +6 -0
  20. package/dist/checker/error-results.js +31 -0
  21. package/dist/checker/expressions.d.ts +2 -0
  22. package/dist/checker/expressions.js +4 -0
  23. package/dist/checker/helpers.js +2 -0
  24. package/dist/checker/queues.d.ts +6 -0
  25. package/dist/checker/queues.js +24 -0
  26. package/dist/checker/resolution.js +6 -2
  27. package/dist/checker/statements.d.ts +5 -0
  28. package/dist/checker/statements.js +19 -0
  29. package/dist/checker/transfer.js +4 -1
  30. package/dist/checker/types/printing.js +7 -0
  31. package/dist/checker/types/queue.d.ts +6 -0
  32. package/dist/checker/types/queue.js +42 -0
  33. package/dist/checker/types/relations.js +12 -1
  34. package/dist/checker/types.d.ts +1 -0
  35. package/dist/checker/types.js +1 -0
  36. package/dist/checker/untyped-globals.d.ts +1 -0
  37. package/dist/checker/untyped-globals.js +47 -0
  38. package/dist/codegen/attributes.js +0 -1
  39. package/dist/codegen/calls.d.ts +9 -0
  40. package/dist/codegen/calls.js +41 -1
  41. package/dist/codegen/component-values.js +0 -1
  42. package/dist/codegen/components.js +0 -1
  43. package/dist/codegen/control-flow.js +0 -3
  44. package/dist/codegen/control-statements.d.ts +9 -1
  45. package/dist/codegen/control-statements.js +51 -7
  46. package/dist/codegen/creation.d.ts +6 -8
  47. package/dist/codegen/creation.js +1 -10
  48. package/dist/codegen/early-returns.js +0 -1
  49. package/dist/codegen/elements.js +0 -3
  50. package/dist/codegen/emitter.d.ts +3 -5
  51. package/dist/codegen/emitter.js +0 -2
  52. package/dist/codegen/expressions.d.ts +4 -1
  53. package/dist/codegen/expressions.js +9 -13
  54. package/dist/codegen/functions.js +0 -2
  55. package/dist/codegen/js.d.ts +0 -7
  56. package/dist/codegen/js.js +33 -59
  57. package/dist/codegen/parallel.js +2 -4
  58. package/dist/codegen/refs.js +0 -1
  59. package/dist/codegen/runtime.d.ts +13 -0
  60. package/dist/codegen/runtime.js +93 -0
  61. package/dist/codegen/simple-statements.js +3 -1
  62. package/dist/codegen/styles.d.ts +1 -1
  63. package/dist/codegen/styles.js +2 -2
  64. package/dist/codegen/web-components.js +0 -1
  65. package/dist/compile.d.ts +7 -0
  66. package/dist/compile.js +1 -0
  67. package/dist/completion.js +2 -1
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.js +1 -0
  70. package/dist/loader.js +19 -6
  71. package/dist/parser/base.d.ts +3 -1
  72. package/dist/parser/expressions.js +9 -2
  73. package/dist/parser/literals.d.ts +7 -0
  74. package/dist/parser/literals.js +45 -1
  75. package/dist/parser/simple-statements.js +3 -0
  76. package/dist/parser/types.d.ts +1 -1
  77. package/dist/parser/types.js +10 -3
  78. package/dist/runtime/sources.d.ts +2 -0
  79. package/dist/runtime/sources.js +14 -0
  80. package/dist/runtime-module.d.ts +1 -0
  81. package/dist/runtime-module.js +3 -0
  82. package/dist/ts-types.d.ts +4 -0
  83. package/dist/ts-types.js +25 -3
  84. package/dist/vite.js +7 -1
  85. package/dist/walk-types.js +1 -0
  86. package/dist/walk.js +9 -0
  87. package/package.json +6 -4
  88. package/src/runtime/chan.js +277 -0
  89. package/src/runtime/core.js +23 -0
  90. package/src/runtime/go.js +340 -0
  91. package/src/runtime/index.js +13 -0
  92. package/src/runtime/markup.js +336 -0
  93. package/src/runtime/queue.js +118 -0
  94. package/src/runtime/select.js +170 -0
  95. package/src/runtime/streams.js +61 -0
  96. package/src/runtime/threads.js +240 -0
  97. package/src/runtime/timers.js +27 -0
  98. package/dist/codegen/chan-helpers.d.ts +0 -2
  99. package/dist/codegen/chan-helpers.js +0 -253
  100. package/dist/codegen/go-helpers.d.ts +0 -9
  101. package/dist/codegen/go-helpers.js +0 -307
  102. package/dist/codegen/helpers.d.ts +0 -10
  103. package/dist/codegen/helpers.js +0 -356
  104. package/dist/codegen/select-helpers.d.ts +0 -2
  105. package/dist/codegen/select-helpers.js +0 -135
  106. package/dist/codegen/thread-helpers.d.ts +0 -2
  107. package/dist/codegen/thread-helpers.js +0 -198
@@ -0,0 +1,24 @@
1
+ import { ChannelChecker } from "./channels.js";
2
+ import { isUntyped } from "./helpers.js";
3
+ import { arrayOf, queueOf, typeToString } from "./types.js";
4
+ // Queues: `queue<number>()` makes an empty one, `queue<number>(items)` one with the values of an
5
+ // array or of another iterable. The members of `queue T` are in types/queue.ts.
6
+ export class QueueChecker extends ChannelChecker {
7
+ checkQueue(node) {
8
+ const element = this.resolveType(node.element);
9
+ if (node.items) {
10
+ // An array literal gets the type of the values, so that `[]` and `[1, null]` fit.
11
+ const items = this.checkValue(node.items, arrayOf(element));
12
+ if (items.kind === 'object' && items.iterates) {
13
+ this.expectAssignable(items.iterates, element, node.items);
14
+ }
15
+ else if (items.kind === 'array' || isUntyped(items)) {
16
+ this.expectAssignable(items, arrayOf(element), node.items);
17
+ }
18
+ else {
19
+ this.error(`queue<T>(items) takes its first values as an array or another iterable, not ${typeToString(items)}, e.g. queue<number>([1, 2, 3])`, node.items);
20
+ }
21
+ }
22
+ return queueOf(element);
23
+ }
24
+ }
@@ -2,7 +2,7 @@ import { holdsBigInts, TYPED_ELEMENTS } from "../typed-arrays.js";
2
2
  import { chanOf } from "./channel.js";
3
3
  import { ClassChecker } from "./classes.js";
4
4
  import { CONTENT } from "./dom.js";
5
- import { ANY, arrayOf, BIGINT, bindParams, builtInTypedArray, classInstance, func, instantiate, isAssignable, isNullable, literal, nullable, NUMBER, substitute, typeToString, union, UNKNOWN, } from "./types.js";
5
+ import { ANY, arrayOf, BIGINT, bindParams, builtInTypedArray, classInstance, func, instantiate, isAssignable, isNullable, literal, nullable, NUMBER, queueOf, substitute, typeToString, union, UNKNOWN, } from "./types.js";
6
6
  // Type annotations as types: names, aliases, generics with their arguments; and the type a variable gets from its value.
7
7
  export class TypeResolver extends ClassChecker {
8
8
  resolveType(node) {
@@ -15,6 +15,8 @@ export class TypeResolver extends ClassChecker {
15
15
  }
16
16
  case 'ChanType':
17
17
  return chanOf(this.resolveType(node.element), node.direction);
18
+ case 'QueueType':
19
+ return queueOf(this.resolveType(node.element));
18
20
  case 'NullableType':
19
21
  return nullable(this.resolveType(node.type));
20
22
  case 'FuncType':
@@ -79,7 +81,9 @@ export class TypeResolver extends ClassChecker {
79
81
  if (entry === undefined) {
80
82
  this.error(this.lookupValue(name.name)
81
83
  ? `"${name.name}" is a value, not a type`
82
- : `unknown type "${name.name}"`, name);
84
+ : name.name === 'queue'
85
+ ? 'a queue type is written with the type of its values after it, e.g. "queue number"'
86
+ : `unknown type "${name.name}"`, name);
83
87
  return UNKNOWN;
84
88
  }
85
89
  const args = typeArgs.map((arg) => this.resolveType(arg));
@@ -23,6 +23,11 @@ export declare abstract class StatementChecker extends AssignmentChecker {
23
23
  private checkDelete;
24
24
  protected checkBlock(statements: readonly ast.Statement[]): void;
25
25
  protected checkVariableDeclaration(node: ast.VariableDeclaration): void;
26
+ /**
27
+ * `const user, err = loadUser(id)` of `(User, error)`: `user` is not null once `err` is found to
28
+ * be null. Variables of `let` may be assigned apart, so only constants are linked.
29
+ */
30
+ private linkError;
26
31
  /** The types of the variables of a declaration: the written one, or those of their values. */
27
32
  private initializerTypes;
28
33
  }
@@ -1,3 +1,4 @@
1
+ import { guardedByError } from "./error-results.js";
1
2
  import { AssignmentChecker } from "./assignments.js";
2
3
  import { isUntyped, isNumeric, ownStatements } from "./helpers.js";
3
4
  import { func, hasZeroValue, isNullable, nonNull, nullable, typeToString, UNKNOWN, } from "./types.js";
@@ -182,8 +183,26 @@ export class StatementChecker extends AssignmentChecker {
182
183
  this.flow.set(binding, nonNull(type));
183
184
  }
184
185
  });
186
+ if (node.keyword === 'const' && !declared)
187
+ this.linkError(node, bindings);
185
188
  });
186
189
  }
190
+ /**
191
+ * `const user, err = loadUser(id)` of `(User, error)`: `user` is not null once `err` is found to
192
+ * be null. Variables of `let` may be assigned apart, so only constants are linked.
193
+ */
194
+ linkError(node, bindings) {
195
+ const results = node.values.length === 1 ? this.typeOfChecked(node.values[0]) : null;
196
+ const error = bindings.at(-1);
197
+ if (results?.kind !== 'tuple' || results.types.length !== bindings.length || !error)
198
+ return;
199
+ const guarded = guardedByError(results.types).flatMap((i) => bindings[i] ?? []);
200
+ if (guarded.length === 0)
201
+ return;
202
+ error.guards = guarded;
203
+ for (const binding of guarded)
204
+ binding.guardedBy = error;
205
+ }
187
206
  /** The types of the variables of a declaration: the written one, or those of their values. */
188
207
  initializerTypes(node, declared) {
189
208
  const { names, values } = node;
@@ -1,4 +1,4 @@
1
- import { memberNames, typeToString } from "./types.js";
1
+ import { isQueue, memberNames, queueElement, typeToString } from "./types.js";
2
2
  // What can go to a worker and back: a worker gets a copy of each value (the structured clone of
3
3
  // postMessage), and a copy has the data of a value but not its methods.
4
4
  /** Buffers and their views: copied whole, or moved by `transfer(x)` (channels.ts). */
@@ -66,6 +66,9 @@ export function notCopied(type, seen = new Set()) {
66
66
  ? null
67
67
  : `a copy of ${typeToString(type)} would lose its class and methods`;
68
68
  case 'object': {
69
+ // A queue goes as its values, and the other thread makes a queue of them (src/runtime/threads.js).
70
+ if (isQueue(type))
71
+ return inner([queueElement(type)]);
69
72
  const name = type.instanceOf?.template.name ?? type.name;
70
73
  if (name !== null && COPIED.has(name))
71
74
  return inner(type.instanceOf?.args ?? []);
@@ -1,6 +1,7 @@
1
1
  import { TYPED_ARRAYS, typedArrayName } from "../../typed-arrays.js";
2
2
  import { ERROR_CLASS } from "./classes.js";
3
3
  import { nullable } from "./constructors.js";
4
+ import { isQueue, queueElement } from "./queue.js";
4
5
  // Types as text, for messages.
5
6
  /** How deep object types without a name are printed with their members. */
6
7
  const MAX_OBJECT_DEPTH = 3;
@@ -58,6 +59,12 @@ export function typeToString(type) {
58
59
  (type.name === 'chan' || type.name === 'chan<-' || type.name === '<-chan')) {
59
60
  return `${type.name} ${typeToString(type.instanceOf.args[0])}`;
60
61
  }
62
+ // `queue (string | number)`: the parentheses that the type is written with.
63
+ if (isQueue(type)) {
64
+ const element = queueElement(type);
65
+ const text = typeToString(element);
66
+ return `queue ${element.kind === 'union' && !element.name ? `(${text})` : text}`;
67
+ }
61
68
  if (type.instanceOf && type.name !== null) {
62
69
  return `${type.name}<${type.instanceOf.args.map(typeToString).join(', ')}>`;
63
70
  }
@@ -0,0 +1,6 @@
1
+ import { type ObjectType, type Type } from './model.ts';
2
+ export declare function queueOf(element: Type): ObjectType;
3
+ /** Whether a type is `queue T`. */
4
+ export declare function isQueue(type: Type): boolean;
5
+ /** The type of the values of a queue type. */
6
+ export declare function queueElement(queue: Type): Type;
@@ -0,0 +1,42 @@
1
+ import { func, method, nullable, property } from "./constructors.js";
2
+ import { instantiate } from "./generics.js";
3
+ import { NUMBER } from "./model.js";
4
+ // `queue T`, made by `queue<T>()`: values that go in and out at both ends without moving the
5
+ // others, as an array moves them on `shift()`. A queue type is an instance of a generic object
6
+ // with the members of an array that a queue keeps, and only a queue is assignable to it
7
+ // (relations.ts). The template is made on first use, since the modules of types import each other.
8
+ let template = null;
9
+ function queueTemplate() {
10
+ if (template)
11
+ return template;
12
+ const T = { kind: 'param', name: 'T' };
13
+ template = {
14
+ kind: 'object',
15
+ name: 'queue',
16
+ members: new Map([
17
+ ['length', property(NUMBER)],
18
+ ['push', method(func([], [NUMBER], { rest: T }))],
19
+ ['unshift', method(func([], [NUMBER], { rest: T }))],
20
+ // An empty queue gives null, as `pop()` of an empty array gives undefined.
21
+ ['shift', method(func([], [nullable(T)]))],
22
+ ['pop', method(func([], [nullable(T)]))],
23
+ ['at', method(func([NUMBER], [nullable(T)]))],
24
+ ['clear', method(func([], []))],
25
+ ]),
26
+ call: null,
27
+ iterates: T,
28
+ typeParams: [T],
29
+ };
30
+ return template;
31
+ }
32
+ export function queueOf(element) {
33
+ return instantiate(queueTemplate(), [element]);
34
+ }
35
+ /** Whether a type is `queue T`. */
36
+ export function isQueue(type) {
37
+ return type.kind === 'object' && template !== null && type.instanceOf?.template === template;
38
+ }
39
+ /** The type of the values of a queue type. */
40
+ export function queueElement(queue) {
41
+ return queue.instanceOf.args[0];
42
+ }
@@ -3,6 +3,7 @@ import { memberNames } from "./lazy.js";
3
3
  import { isNullable, literalBase, nonNull, union } from "./constructors.js";
4
4
  import { UNKNOWN, VOID, } from "./model.js";
5
5
  import { typedArrayOf, typeToString } from "./printing.js";
6
+ import { isQueue, queueElement } from "./queue.js";
6
7
  // Relations between types: assignability, comparison, equality, common types.
7
8
  /** Whether a value of type `source` can be used where `target` is expected. */
8
9
  export function isAssignable(source, target) {
@@ -75,6 +76,10 @@ export function assignable(source, target, assuming) {
75
76
  return missingMember(source, members, nested) === null;
76
77
  }
77
78
  case 'object': {
79
+ // Only a queue is a queue, as only an array is an array, and its values are checked as theirs.
80
+ if (isQueue(target)) {
81
+ return isQueue(source) && assignable(queueElement(source), queueElement(target), nested);
82
+ }
78
83
  // A channel both takes and gives values, so `chan number` is not a `chan ?number`, as in Go;
79
84
  // `chan any` is any channel, for `close(ch)`. A `chan T` is also a channel of one
80
85
  // direction, `chan<- T` or `<-chan T`, but not the other way round.
@@ -201,10 +206,13 @@ function missingMember(source, members, assuming) {
201
206
  }
202
207
  /** Why `source` is not assignable to an object or class `target`, e.g. `missing field "y"`. */
203
208
  export function explainMismatch(source, target) {
204
- // `Stack<number>` and `Stack<string>`: the type arguments already say what differs.
209
+ // `Stack<number>` and `Stack<string>`: the type arguments already say what differs; only a queue
210
+ // is a queue.
205
211
  if (source.kind === 'class' && target.kind === 'class' && source.info === target.info) {
206
212
  return null;
207
213
  }
214
+ if (isQueue(target))
215
+ return null;
208
216
  const members = target.kind === 'object'
209
217
  ? target.members
210
218
  : target.kind === 'class'
@@ -288,6 +296,9 @@ export function hasZeroValue(type) {
288
296
  case 'any':
289
297
  case 'unknown':
290
298
  return true;
299
+ // An empty queue, as `[]` is the zero value of an array.
300
+ case 'object':
301
+ return isQueue(type);
291
302
  default:
292
303
  return false;
293
304
  }
@@ -7,3 +7,4 @@ export * from './types/relations.ts';
7
7
  export * from './types/params.ts';
8
8
  export * from './types/printing.ts';
9
9
  export * from './types/bytes.ts';
10
+ export * from './types/queue.ts';
@@ -8,3 +8,4 @@ export * from "./types/relations.js";
8
8
  export * from "./types/params.js";
9
9
  export * from "./types/printing.js";
10
10
  export * from "./types/bytes.js";
11
+ export * from "./types/queue.js";
@@ -0,0 +1 @@
1
+ export declare const UNTYPED_GLOBALS: readonly string[];
@@ -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
  }
@@ -1,6 +1,5 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { SimpleStatementEmitter } from './simple-statements.ts';
3
- /** if, for, switch and try. */
4
3
  export declare abstract class ControlStatementEmitter extends SimpleStatementEmitter {
5
4
  protected ifStatement(node: ast.IfStatement): string;
6
5
  protected loopBody(body: ast.BlockStatement): string;
@@ -12,6 +11,15 @@ export declare abstract class ControlStatementEmitter extends SimpleStatementEmi
12
11
  * `for i, x in xs` iterates `xs.entries()`, which gives the keys and values of a Map too.
13
12
  */
14
13
  protected forInStatement(node: ast.ForInStatement): void;
14
+ /** `[i, v]` of a loop over pairs; `_` leaves its place empty. */
15
+ private pair;
16
+ /**
17
+ * `for i, v in items` over an array or a typed array: a loop over the indexes, which runs faster
18
+ * than one over the pairs of `entries()`. The variables of the loop cannot be assigned, so `i` is
19
+ * the counter itself. The array is read once, as `entries()` reads it, into `$$items` unless it is
20
+ * a name that cannot change.
21
+ */
22
+ private indexLoop;
15
23
  /** Each `case` ends with an implicit `break`; a case with declarations gets its own block. */
16
24
  protected switchStatement(node: ast.SwitchStatement, discriminant: ast.Expression): void;
17
25
  /**
@@ -1,10 +1,14 @@
1
1
  import { isChan } from "../checker/channel.js";
2
2
  import { isMap } from "../checker/iterables.js";
3
+ import { typedArrayOf } from "../checker/types.js";
4
+ import { declaredIn } from "../names.js";
3
5
  import { containsBreak } from "../walk.js";
4
6
  import { declarationsOf, endsWithJump } from "./analysis.js";
5
7
  import { SimpleStatementEmitter } from "./simple-statements.js";
6
8
  import { ARROW, BINARY, POSTFIX } from "./syntax.js";
7
9
  /** if, for, switch and try. */
10
+ /** Names that keep their value, so a loop over the indexes may read the array through them. */
11
+ const FIXED = new Set(['const', 'import', 'loop']);
8
12
  export class ControlStatementEmitter extends SimpleStatementEmitter {
9
13
  ifStatement(node) {
10
14
  let text = `if (${this.expression(node.condition, 0)}) ${this.blockStatement(node.consequent)}`;
@@ -53,7 +57,6 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
53
57
  case 'ExpressionStatement':
54
58
  return this.expression(node.expression, 0);
55
59
  case 'SendStatement':
56
- this.helpers.add('chan');
57
60
  return `await $$send(${this.expression(node.channel, ARROW)}, ${this.expression(node.value, ARROW)})`;
58
61
  }
59
62
  }
@@ -71,7 +74,6 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
71
74
  let head;
72
75
  // A channel gives its values as they come, until it is closed.
73
76
  if (iterable && isChan(iterable)) {
74
- this.helpers.add('chan');
75
77
  this.line(`for await (const ${value} of $$range(${this.expression(node.iterable, ARROW)})) ${this.loopBody(node.body)}`);
76
78
  return;
77
79
  }
@@ -92,19 +94,62 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
92
94
  if (iterable && isMap(iterable))
93
95
  values += '.values()';
94
96
  else if (untyped) {
95
- this.helpers.add('values');
96
97
  values = `$$values(${values})`;
97
98
  }
98
99
  head = `const ${value} of ${values}`;
99
100
  }
101
+ else if (iterable &&
102
+ (iterable.kind === 'array' || iterable.kind === 'string' || typedArrayOf(iterable) !== null)) {
103
+ // Without the index, the loop goes over the values.
104
+ if (node.key.name === '_' && node.value.name !== '_') {
105
+ head = `const ${value} of ${this.expression(node.iterable, POSTFIX)}`;
106
+ }
107
+ else if (iterable.kind === 'string') {
108
+ // A string has no entries(): its characters, as `for...of` gives them, are numbered.
109
+ const pair = this.pair(node.key, node.value);
110
+ head = `const ${pair} of Array.from(${this.expression(node.iterable, ARROW)}).entries()`;
111
+ }
112
+ else {
113
+ this.indexLoop(node, node.key);
114
+ return;
115
+ }
116
+ }
100
117
  else {
101
- const key = node.key.name === '_' ? '' : this.name(node.key.name);
102
- const pair = node.value.name === '_' ? `[${key}]` : `[${key}, ${value}]`;
103
- head = `const ${pair} of ${this.expression(node.iterable, POSTFIX)}.entries()`;
118
+ head = `const ${this.pair(node.key, node.value)} of ${this.expression(node.iterable, POSTFIX)}.entries()`;
104
119
  }
105
120
  this.line(`for (${head}) ${this.loopBody(node.body)}`);
106
121
  });
107
122
  }
123
+ /** `[i, v]` of a loop over pairs; `_` leaves its place empty. */
124
+ pair(key, value) {
125
+ const name = key.name === '_' ? '' : this.name(key.name);
126
+ return value.name === '_' ? `[${name}]` : `[${name}, ${this.name(value.name)}]`;
127
+ }
128
+ /**
129
+ * `for i, v in items` over an array or a typed array: a loop over the indexes, which runs faster
130
+ * than one over the pairs of `entries()`. The variables of the loop cannot be assigned, so `i` is
131
+ * the counter itself. The array is read once, as `entries()` reads it, into `$$items` unless it is
132
+ * a name that cannot change.
133
+ */
134
+ indexLoop(node, keyNode) {
135
+ const key = keyNode.name === '_' ? '$$i' : this.name(keyNode.name);
136
+ const value = node.value.name === '_' ? null : this.name(node.value.name);
137
+ const { iterable } = node;
138
+ const fixed = iterable.kind === 'Identifier' && FIXED.has(this.lookup(iterable.name) ?? 'let');
139
+ const items = fixed ? this.expression(iterable, POSTFIX) : '$$items';
140
+ const init = fixed ? '' : `, $$items = ${this.expression(iterable, ARROW)}`;
141
+ // A declaration of the same name in the body would clash with the element's in one block.
142
+ const hidden = declaredIn(node.body.body).some((name) => name === keyNode.name || name === node.value.name);
143
+ const body = this.withBreakTarget(null, () => this.block(() => {
144
+ if (value !== null)
145
+ this.line(`const ${value} = ${items}[${key}];`);
146
+ if (hidden)
147
+ this.line(this.blockStatement(node.body));
148
+ else
149
+ this.blockStatements(node.body.body);
150
+ }));
151
+ this.line(`for (let ${key} = 0${init}; ${key} < ${items}.length; ${key}++) ${body}`);
152
+ }
108
153
  /** Each `case` ends with an implicit `break`; a case with declarations gets its own block. */
109
154
  switchStatement(node, discriminant) {
110
155
  const head = `switch (${this.expression(discriminant, 0)})`;
@@ -140,7 +185,6 @@ export class ControlStatementEmitter extends SimpleStatementEmitter {
140
185
  * switch runs the case; a `break` inside it leaves the select, as in Go. `default` is -1.
141
186
  */
142
187
  selectStatement(node) {
143
- this.helpers.add('chan');
144
188
  const operations = node.cases.flatMap(({ operation }) => {
145
189
  if (operation === null)
146
190
  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;