rooklang 0.3.3 → 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 (170) 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 +2 -0
  16. package/dist/checker/assignments.js +14 -0
  17. package/dist/checker/base.js +6 -6
  18. package/dist/checker/builtins.js +15 -5
  19. package/dist/checker/bytes.d.ts +2 -2
  20. package/dist/checker/bytes.js +2 -2
  21. package/dist/checker/calls.js +3 -0
  22. package/dist/checker/channel-objects.d.ts +3 -0
  23. package/dist/checker/channel-objects.js +40 -0
  24. package/dist/checker/channels.d.ts +1 -0
  25. package/dist/checker/channels.js +16 -2
  26. package/dist/checker/checker.d.ts +1 -1
  27. package/dist/checker/checker.js +19 -0
  28. package/dist/checker/class-members.d.ts +22 -0
  29. package/dist/checker/class-members.js +105 -0
  30. package/dist/checker/classes.d.ts +5 -4
  31. package/dist/checker/classes.js +41 -12
  32. package/dist/checker/closing.d.ts +21 -0
  33. package/dist/checker/closing.js +164 -0
  34. package/dist/checker/completion.d.ts +2 -2
  35. package/dist/checker/completion.js +2 -2
  36. package/dist/checker/context.d.ts +13 -0
  37. package/dist/checker/control-flow.d.ts +5 -0
  38. package/dist/checker/control-flow.js +17 -0
  39. package/dist/checker/declarations.js +1 -0
  40. package/dist/checker/decode.d.ts +8 -0
  41. package/dist/checker/decode.js +32 -0
  42. package/dist/checker/expressions.d.ts +5 -1
  43. package/dist/checker/expressions.js +13 -34
  44. package/dist/checker/flow.js +8 -4
  45. package/dist/checker/helpers.d.ts +1 -1
  46. package/dist/checker/helpers.js +9 -2
  47. package/dist/checker/iterables.d.ts +3 -1
  48. package/dist/checker/iterables.js +13 -0
  49. package/dist/checker/iterators.d.ts +4 -1
  50. package/dist/checker/iterators.js +12 -4
  51. package/dist/checker/json.d.ts +5 -0
  52. package/dist/checker/json.js +92 -0
  53. package/dist/checker/loops.js +23 -3
  54. package/dist/checker/members.js +7 -0
  55. package/dist/checker/operators.d.ts +2 -2
  56. package/dist/checker/operators.js +7 -5
  57. package/dist/checker/queues.d.ts +2 -2
  58. package/dist/checker/queues.js +2 -2
  59. package/dist/checker/resolution.js +5 -3
  60. package/dist/checker/signals.d.ts +8 -0
  61. package/dist/checker/signals.js +48 -0
  62. package/dist/checker/topics.d.ts +6 -0
  63. package/dist/checker/topics.js +17 -0
  64. package/dist/checker/transfer.d.ts +3 -1
  65. package/dist/checker/transfer.js +25 -5
  66. package/dist/checker/try.d.ts +6 -0
  67. package/dist/checker/try.js +19 -0
  68. package/dist/checker/types/constructors.d.ts +1 -1
  69. package/dist/checker/types/constructors.js +2 -2
  70. package/dist/checker/types/generics.js +1 -1
  71. package/dist/checker/types/iterables.d.ts +13 -0
  72. package/dist/checker/types/iterables.js +52 -0
  73. package/dist/checker/types/model.d.ts +25 -0
  74. package/dist/checker/types/params.js +22 -3
  75. package/dist/checker/types/printing.js +4 -3
  76. package/dist/checker/types/relations.js +19 -25
  77. package/dist/checker/types/topic.d.ts +6 -0
  78. package/dist/checker/types/topic.js +23 -0
  79. package/dist/checker/types.d.ts +2 -0
  80. package/dist/checker/types.js +2 -0
  81. package/dist/cli.js +18 -36
  82. package/dist/codegen/calls.d.ts +23 -0
  83. package/dist/codegen/calls.js +88 -7
  84. package/dist/codegen/control-statements.d.ts +10 -2
  85. package/dist/codegen/control-statements.js +56 -19
  86. package/dist/codegen/decode.d.ts +14 -0
  87. package/dist/codegen/decode.js +93 -0
  88. package/dist/codegen/emitter.d.ts +10 -41
  89. package/dist/codegen/emitter.js +9 -4
  90. package/dist/codegen/expressions.d.ts +9 -0
  91. package/dist/codegen/expressions.js +15 -24
  92. package/dist/codegen/js.d.ts +2 -2
  93. package/dist/codegen/js.js +5 -2
  94. package/dist/codegen/options.d.ts +40 -0
  95. package/dist/codegen/options.js +1 -0
  96. package/dist/codegen/runtime.d.ts +7 -3
  97. package/dist/codegen/runtime.js +27 -13
  98. package/dist/codegen/simple-statements.d.ts +10 -4
  99. package/dist/codegen/simple-statements.js +40 -15
  100. package/dist/codegen/statements.js +64 -4
  101. package/dist/codegen/try.d.ts +5 -0
  102. package/dist/codegen/try.js +40 -0
  103. package/dist/compile.d.ts +9 -0
  104. package/dist/compile.js +19 -6
  105. package/dist/completion.d.ts +4 -2
  106. package/dist/completion.js +10 -5
  107. package/dist/declarations.js +32 -6
  108. package/dist/decorator-copy.js +5 -2
  109. package/dist/goroutines.d.ts +1 -1
  110. package/dist/goroutines.js +5 -4
  111. package/dist/index.d.ts +6 -2
  112. package/dist/index.js +14 -5
  113. package/dist/lexer/base.d.ts +2 -0
  114. package/dist/lexer/base.js +6 -1
  115. package/dist/lexer/lexer.js +3 -0
  116. package/dist/lexer/literals.d.ts +5 -0
  117. package/dist/lexer/literals.js +51 -1
  118. package/dist/lexer/markup.js +3 -4
  119. package/dist/lexer/tables.js +2 -0
  120. package/dist/lexer/token.d.ts +4 -3
  121. package/dist/lexer/token.js +0 -2
  122. package/dist/loader.js +30 -8
  123. package/dist/parser/base.d.ts +4 -0
  124. package/dist/parser/base.js +2 -1
  125. package/dist/parser/classes.d.ts +2 -0
  126. package/dist/parser/classes.js +88 -23
  127. package/dist/parser/control-flow.d.ts +3 -1
  128. package/dist/parser/control-flow.js +21 -24
  129. package/dist/parser/declarations.d.ts +7 -4
  130. package/dist/parser/declarations.js +45 -6
  131. package/dist/parser/expressions.js +27 -3
  132. package/dist/parser/jumps.d.ts +11 -0
  133. package/dist/parser/jumps.js +77 -0
  134. package/dist/parser/literals.d.ts +9 -0
  135. package/dist/parser/literals.js +45 -6
  136. package/dist/parser/markup.js +1 -1
  137. package/dist/parser/simple-statements.d.ts +1 -1
  138. package/dist/parser/simple-statements.js +16 -9
  139. package/dist/parser/statements.d.ts +0 -3
  140. package/dist/parser/statements.js +22 -25
  141. package/dist/parser/syntax.d.ts +11 -1
  142. package/dist/parser/syntax.js +16 -2
  143. package/dist/parser/types.js +8 -3
  144. package/dist/project-modules.d.ts +12 -0
  145. package/dist/project-modules.js +106 -0
  146. package/dist/run-file.d.ts +6 -0
  147. package/dist/run-file.js +111 -0
  148. package/dist/runtime/sources.js +11 -5
  149. package/dist/source-map.d.ts +17 -0
  150. package/dist/source-map.js +75 -0
  151. package/dist/ts-types.js +4 -1
  152. package/dist/typescript/convert-objects.d.ts +2 -1
  153. package/dist/typescript/convert-objects.js +18 -9
  154. package/dist/vite.js +12 -4
  155. package/dist/walk-types.js +1 -0
  156. package/dist/walk.d.ts +0 -2
  157. package/dist/walk.js +11 -28
  158. package/package.json +1 -1
  159. package/src/runtime/chan.js +15 -5
  160. package/src/runtime/channel.js +28 -0
  161. package/src/runtime/classes.js +32 -0
  162. package/src/runtime/core.js +10 -1
  163. package/src/runtime/decode.js +190 -0
  164. package/src/runtime/go.js +3 -0
  165. package/src/runtime/index.js +6 -0
  166. package/src/runtime/select.js +14 -9
  167. package/src/runtime/signals.js +59 -0
  168. package/src/runtime/threads.js +32 -5
  169. package/src/runtime/topic.js +118 -0
  170. package/src/runtime/try.js +45 -0
@@ -9,10 +9,13 @@ export class ObjectConverter extends ConverterBase {
9
9
  const [element] = checker.getTypeArguments(type);
10
10
  return arrayOf(element ? this.convert(element) : ANY);
11
11
  }
12
- // A tuple is an array of any of its elements: `[string, number]` is `[](string | number)`.
12
+ // A tuple is an array of any of its elements: `[string, number]` is `[](string | number)`,
13
+ // which keeps them by position too, to infer the type parameters of `[K, V]`.
13
14
  if (checker.isTupleType(type)) {
14
- const elements = checker.getTypeArguments(type);
15
- return arrayOf(elements.length > 0 ? union(elements.map((each) => this.convert(each))) : ANY);
15
+ const elements = checker
16
+ .getTypeArguments(type)
17
+ .map((each) => this.convert(each));
18
+ return elements.length > 0 ? arrayOf(union(elements), elements) : arrayOf(ANY);
16
19
  }
17
20
  if (type.objectFlags & ObjectFlags.Reference) {
18
21
  const reference = type;
@@ -116,23 +119,26 @@ export class ObjectConverter extends ConverterBase {
116
119
  object.index = this.convert(index);
117
120
  if (numberIndex && numberIndex !== index)
118
121
  object.numberIndex = this.convert(numberIndex);
119
- this.iterates(object, type);
122
+ this.iterates(object, type, 'iterates');
123
+ this.iterates(object, type, 'asyncIterates');
120
124
  return object;
121
125
  }
122
126
  /**
123
127
  * What `for v in x` gives: the `value` of what `next()` of `[Symbol.iterator]()` returns while
124
- * it is not `done`. Found when it is first needed, as most types are never looped over.
128
+ * it is not `done`, or of what its Promise gives for `[Symbol.asyncIterator]()`. Found when it is
129
+ * first needed, as most types are never looped over.
125
130
  */
126
- iterates(object, type) {
131
+ iterates(object, type, key) {
127
132
  const { checker } = this;
133
+ const prefix = key === 'iterates' ? '__@iterator@' : '__@asyncIterator@';
128
134
  const iterator = type
129
135
  .getProperties()
130
- .find((property) => String(property.escapedName).startsWith('__@iterator@'));
136
+ .find((property) => String(property.escapedName).startsWith(prefix));
131
137
  if (!iterator)
132
138
  return;
133
139
  let found;
134
140
  let done = false;
135
- Object.defineProperty(object, 'iterates', {
141
+ Object.defineProperty(object, key, {
136
142
  enumerable: true,
137
143
  get: () => {
138
144
  if (done)
@@ -143,7 +149,8 @@ export class ObjectConverter extends ConverterBase {
143
149
  .getCallSignatures()[0]
144
150
  ?.getReturnType()
145
151
  .getProperty('next');
146
- const result = next && checker.getTypeOfSymbol(next).getCallSignatures()[0]?.getReturnType();
152
+ const returned = next && checker.getTypeOfSymbol(next).getCallSignatures()[0]?.getReturnType();
153
+ const result = returned && key === 'asyncIterates' ? checker.getAwaitedType(returned) : returned;
147
154
  const results = result?.isUnion() ? result.types : result ? [result] : [];
148
155
  const yielded = results.find((each) => {
149
156
  const flag = each.getProperty('done');
@@ -240,6 +247,8 @@ export class ObjectConverter extends ConverterBase {
240
247
  if (base)
241
248
  info.superClass = this.classInfo(base.symbol);
242
249
  info.ctor = this.signatures(checker.getSignaturesOfType(statics, SignatureKind.Construct));
250
+ // A stream of Node is a class, gone over with `for await` in JS.
251
+ this.iterates(info, instance, 'asyncIterates');
243
252
  return info;
244
253
  }
245
254
  }
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
2
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { RUNTIME_MODULE } from "./runtime-module.js";
5
5
  import { formatDiagnostic } from "./diagnostics.js";
@@ -12,6 +12,8 @@ import { SourceFile } from "./source.js";
12
12
  // path has no query, since Vite drops the query of a worker's URL. The compiled code imports the
13
13
  // runtime, "rooklang/runtime", of the plugin's own package.
14
14
  const GOROUTINES = '.goroutines.js';
15
+ /** The loader of `.rk` files of `rook run`, next to this file: .ts in src, .js in the package. */
16
+ const LOADER = new URL(`./loader${extname(fileURLToPath(import.meta.url))}`, import.meta.url).href;
15
17
  export function rook(options = {}) {
16
18
  const typeCheck = options.typeCheck ?? true;
17
19
  let root = process.cwd();
@@ -46,7 +48,7 @@ export function rook(options = {}) {
46
48
  return null;
47
49
  return readFileSync(path.slice(0, -GOROUTINES.length), 'utf8');
48
50
  },
49
- transform(source, id) {
51
+ transform(source, id, transformOptions) {
50
52
  const [name = id, query] = id.split('?');
51
53
  const worker = name.endsWith(`.rk${GOROUTINES}`);
52
54
  const path = worker ? name.slice(0, -GOROUTINES.length) : name;
@@ -59,7 +61,11 @@ export function rook(options = {}) {
59
61
  rewriteImports: false,
60
62
  typeCheck,
61
63
  markup: options.markup,
62
- workerUrl: `./${basename(path)}${GOROUTINES}`,
64
+ // In Node, as under Vitest, a worker thread loads the module itself, `app.rk?goroutines`,
65
+ // with the loader of `rook run`, which it imports first: it has no Vite.
66
+ ...(transformOptions?.ssr === true
67
+ ? { workerUrl: `./${basename(path)}?goroutines`, nodeLoader: LOADER }
68
+ : { workerUrl: `./${basename(path)}${GOROUTINES}` }),
63
69
  });
64
70
  const file = new SourceFile(relative(root, path), source);
65
71
  // Vite counts columns from 0.
@@ -86,7 +92,9 @@ export function rook(options = {}) {
86
92
  for (const dependency of result.dependencies) {
87
93
  this.addWatchFile(resolve(dirname(path), dependency));
88
94
  }
89
- return { code: worker ? (result.worker ?? '') : result.code, map: null };
95
+ return worker
96
+ ? { code: result.worker ?? '', map: result.workerMap ?? null }
97
+ : { code: result.code, map: result.map ?? null };
90
98
  },
91
99
  };
92
100
  }
@@ -19,6 +19,7 @@ export function forEachTypeChild(node, visit) {
19
19
  case 'ArrayType':
20
20
  case 'ChanType':
21
21
  case 'QueueType':
22
+ case 'TopicType':
22
23
  visit(node.element);
23
24
  break;
24
25
  case 'NullableType':
package/dist/walk.d.ts CHANGED
@@ -1,5 +1,3 @@
1
1
  import type * as ast from './ast.ts';
2
2
  /** Calls `visit` for each direct child of `node`, in source order. */
3
3
  export declare function forEachChild(node: ast.Node, visit: (child: ast.Node) => void): void;
4
- /** Whether a `break` in these statements leaves the enclosing loop or switch. */
5
- export declare function containsBreak(statements: readonly ast.Statement[]): boolean;
package/dist/walk.js CHANGED
@@ -30,7 +30,7 @@ export function forEachChild(node, visit) {
30
30
  visit(node.name);
31
31
  each(node.params);
32
32
  each(node.results);
33
- visit(node.body);
33
+ optional(node.body);
34
34
  break;
35
35
  case 'Parameter':
36
36
  visit(node.name);
@@ -111,11 +111,16 @@ export function forEachChild(node, visit) {
111
111
  visit(node.element);
112
112
  optional(node.items);
113
113
  break;
114
+ case 'TopicExpression':
115
+ visit(node.element);
116
+ visit(node.name);
117
+ break;
114
118
  case 'MakeExpression':
115
119
  visit(node.type);
116
120
  visit(node.length);
117
121
  break;
118
122
  case 'ConversionExpression':
123
+ case 'DecodeExpression':
119
124
  visit(node.type);
120
125
  visit(node.argument);
121
126
  break;
@@ -123,6 +128,7 @@ export function forEachChild(node, visit) {
123
128
  each(node.values);
124
129
  break;
125
130
  case 'IfStatement':
131
+ optional(node.init);
126
132
  visit(node.condition);
127
133
  visit(node.consequent);
128
134
  optional(node.alternate);
@@ -140,6 +146,7 @@ export function forEachChild(node, visit) {
140
146
  visit(node.body);
141
147
  break;
142
148
  case 'SwitchStatement':
149
+ optional(node.init);
143
150
  optional(node.discriminant);
144
151
  each(node.cases);
145
152
  break;
@@ -204,6 +211,7 @@ export function forEachChild(node, visit) {
204
211
  visit(node.body);
205
212
  break;
206
213
  case 'UnaryExpression':
214
+ case 'TryExpression':
207
215
  visit(node.argument);
208
216
  break;
209
217
  case 'GoExpression':
@@ -265,6 +273,7 @@ export function forEachChild(node, visit) {
265
273
  case 'ArrayType':
266
274
  case 'ChanType':
267
275
  case 'QueueType':
276
+ case 'TopicType':
268
277
  case 'NullableType':
269
278
  case 'FuncType':
270
279
  case 'ComponentType':
@@ -287,6 +296,7 @@ export function forEachChild(node, visit) {
287
296
  case 'NumberLiteral':
288
297
  case 'BigIntLiteral':
289
298
  case 'StringLiteral':
299
+ case 'RegExpLiteral':
290
300
  case 'TemplateElement':
291
301
  case 'BooleanLiteral':
292
302
  case 'NullLiteral':
@@ -295,30 +305,3 @@ export function forEachChild(node, visit) {
295
305
  break;
296
306
  }
297
307
  }
298
- /** Whether a `break` in these statements leaves the enclosing loop or switch. */
299
- export function containsBreak(statements) {
300
- let found = false;
301
- const visit = (node) => {
302
- switch (node.kind) {
303
- case 'BreakStatement':
304
- found = true;
305
- return;
306
- // A `break` inside these belongs to them.
307
- case 'ForStatement':
308
- case 'ForInStatement':
309
- case 'SwitchStatement':
310
- case 'SelectStatement':
311
- case 'FuncDeclaration':
312
- case 'FuncExpression':
313
- case 'ArrowFunction':
314
- case 'ClassDeclaration':
315
- case 'MountStatement':
316
- case 'EffectStatement':
317
- return;
318
- default:
319
- forEachChild(node, visit);
320
- }
321
- };
322
- statements.forEach(visit);
323
- return found;
324
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rooklang",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "A language with JavaScript semantics, Go-style syntax, goroutines on real threads and static types that compiles to readable JavaScript",
5
5
  "keywords": [
6
6
  "rooklang",
@@ -1,10 +1,11 @@
1
1
  // Channels: sending, receiving and closing, here or in the thread that made the channel
2
2
  // (threads.js), and the queues of a channel. Receiving from a closed channel gives
3
- // the zero value of its type, as in Go.
3
+ // the zero value of its type, as in Go. Channel.EMPTY is closed and belongs to no thread: a send to
4
+ // it goes nowhere, and receiving gives the zero value that the code that receives passes.
4
5
 
5
6
  import { $$Queue } from './queue.js';
6
7
  import { $$commit, $$lock, $$select, $$try, $$unlock, $$unwait } from './select.js';
7
- import { $$adopt, $$ask, $$ownerOf, $$post, $$registry, $$stopped } from './threads.js';
8
+ import { $$adopt, $$ask, $$isEmpty, $$ownerOf, $$post, $$registry, $$stopped } from './threads.js';
8
9
 
9
10
  $$registry().handle ??= { message: $$message, pump: $$pump };
10
11
 
@@ -32,6 +33,9 @@ export function $$chan(size, zero) {
32
33
  // that made the channel.
33
34
  export function $$send(channel, value) {
34
35
  const id = channel.$$chan;
36
+
37
+ if ($$isEmpty(id)) return Promise.resolve();
38
+
35
39
  const owner = $$ownerOf(id);
36
40
 
37
41
  if (owner !== $$registry().key) return $$ask(owner, { op: 'send', channel: id, value });
@@ -41,11 +45,12 @@ export function $$send(channel, value) {
41
45
  );
42
46
  }
43
47
 
44
- export function $$receive(channel, withOk = false) {
48
+ export function $$receive(channel, withOk = false, zero = null) {
45
49
  const id = channel.$$chan;
46
50
  const owner = $$ownerOf(id);
47
- const received =
48
- owner !== $$registry().key
51
+ const received = $$isEmpty(id)
52
+ ? Promise.resolve([zero, false])
53
+ : owner !== $$registry().key
49
54
  ? $$ask(owner, { op: 'receive', channel: id })
50
55
  : new Promise((resolve, reject) => $$wait(id, 'receivers', { done: resolve, fail: reject }));
51
56
 
@@ -55,6 +60,9 @@ export function $$receive(channel, withOk = false) {
55
60
  // Closing a channel of another thread fails this one if it was closed already, as a panic would.
56
61
  export function $$close(channel) {
57
62
  const id = channel.$$chan;
63
+
64
+ if ($$isEmpty(id)) return;
65
+
58
66
  const owner = $$ownerOf(id);
59
67
 
60
68
  if (owner === $$registry().key) return $$closeHere(id);
@@ -70,6 +78,8 @@ export function $$closeHere(id) {
70
78
 
71
79
  if (state.closed) throw new Error('close of a closed channel');
72
80
  state.closed = true;
81
+ // What made the channel stops with it, as signal() does (signals.js).
82
+ state.onClose?.();
73
83
  $$pump(id);
74
84
  }
75
85
 
@@ -0,0 +1,28 @@
1
+ // `Channel`: what Rook code and the code compiled from it do with channels, by name. The functions
2
+ // themselves are in chan.js, select.js, signals.js, streams.js and timers.js.
3
+
4
+ import { $$chan, $$close, $$range, $$receive, $$send } from './chan.js';
5
+ import { $$select, $$try } from './select.js';
6
+ import { $$abortSignal, $$done, $$signal } from './signals.js';
7
+ import { $$fromStream, $$stream } from './streams.js';
8
+ import { $$after, $$tick } from './timers.js';
9
+
10
+ export const $$Channel = Object.freeze({
11
+ // The zero value of channel types: closed, and the same in every thread (threads.js).
12
+ EMPTY: Object.freeze({ $$chan: 'empty', size: 0, zero: null }),
13
+ make: $$chan,
14
+ send: $$send,
15
+ receive: $$receive,
16
+ close: $$close,
17
+ range: $$range,
18
+ select: $$select,
19
+ // [] for a send and [value, ok] for a receive that goes on at once, else null.
20
+ try: (channel, send, value) => $$try(channel.$$chan, send, value),
21
+ fromStream: $$fromStream,
22
+ stream: $$stream,
23
+ after: $$after,
24
+ tick: $$tick,
25
+ signal: $$signal,
26
+ done: $$done,
27
+ abortSignal: $$abortSignal,
28
+ });
@@ -0,0 +1,32 @@
1
+ // Data classes: `data class Point` registers its class with an id of its own, the name and a hash
2
+ // of its code, the same in a module and in its worker module, which has a copy of the class. A copy
3
+ // of a value that goes to another thread loses the class of its instances (structuredClone); the
4
+ // thread that sends lists them with their ids (threads.js), and the one that gets the copy gives
5
+ // them their class again.
6
+
7
+ function registry() {
8
+ return (globalThis[Symbol.for('rook.classes')] ??= { byId: new Map(), ids: new WeakMap() });
9
+ }
10
+
11
+ export function $$dataClass(cls, id) {
12
+ const { byId, ids } = registry();
13
+
14
+ byId.set(id, cls.prototype);
15
+ ids.set(cls.prototype, id);
16
+ }
17
+
18
+ /** The id of the data class of an instance, or undefined. */
19
+ export function $$classOf(value) {
20
+ return registry().ids.get(Object.getPrototypeOf(value));
21
+ }
22
+
23
+ /** Gives the copies of instances their classes back: `[object, id]` pairs of a copied message. */
24
+ export function $$reclass(instances) {
25
+ const { byId } = registry();
26
+
27
+ for (const [object, id] of instances) {
28
+ const prototype = byId.get(id);
29
+
30
+ if (prototype) Object.setPrototypeOf(object, prototype);
31
+ }
32
+ }
@@ -1,4 +1,5 @@
1
- // What the code of any module may need: `for` over a value of unknown type, and `defer`.
1
+ // What the code of any module may need: `for` over a value of unknown type, `defer`, and wrapped
2
+ // errors.
2
3
 
3
4
  // `for v in x` over an untyped value: a Map gives its values, as a typed one does.
4
5
  export function $$values(iterable) {
@@ -21,3 +22,11 @@ export function $$runDeferred(deferred) {
21
22
 
22
23
  if (failure) throw failure.error;
23
24
  }
25
+
26
+ // `error("load user", err)`: an error whose message ends with that of its cause, as `%w` of Go
27
+ // gives "load user: not found", and whose `cause` is the error itself, for code that looks at it.
28
+ export function $$wrap(message, cause) {
29
+ const reason = cause instanceof Error ? cause.message : String(cause);
30
+
31
+ return new Error(`${message}: ${reason}`, { cause });
32
+ }
@@ -0,0 +1,190 @@
1
+ // `decode<T>(x)`: JSON text, or a value from JS, checked against the type T, which the compiler
2
+ // writes as data: "number", "string", "bool", "any", { literal: [values] }, { nullable: T },
3
+ // { array: T }, { record: T }, { union: [types] } and $$type(name, fields) for an object. A value
4
+ // that fits is returned as it is, with the fields that T does not have; a nullable field may be
5
+ // missing. The error says where the value does not fit: `decode User: tags[1]: expected string,
6
+ // got 42`.
7
+
8
+ /** An object type. Its fields come from a function, so that types can refer to each other. */
9
+ export function $$type(name, fields) {
10
+ let resolved = null;
11
+
12
+ return {
13
+ name,
14
+ get fields() {
15
+ return (resolved ??= fields());
16
+ },
17
+ };
18
+ }
19
+
20
+ /** `decode<T>(text)` of a string: the JSON text is parsed, then checked. */
21
+ export function $$decodeJson(text, type) {
22
+ let value;
23
+
24
+ try {
25
+ value = JSON.parse(text);
26
+ } catch (error) {
27
+ return [null, new Error(`decode ${describe(type)}: invalid JSON: ${error.message}`)];
28
+ }
29
+
30
+ return $$decode(value, type);
31
+ }
32
+
33
+ /**
34
+ * `decode<T>(value)` of a value from JS. Without the types of the checker (`rook build --no-check`)
35
+ * the type is null: then a string is parsed as JSON text, and nothing is checked.
36
+ */
37
+ export function $$decode(value, type) {
38
+ if (type === null) return typeof value === 'string' ? $$decodeJson(value, 'any') : [value, null];
39
+
40
+ const problem = check(value, type, []);
41
+
42
+ if (problem === null) return [value, null];
43
+
44
+ const where = problem.path.length > 0 ? `${pathText(problem.path)}: ` : '';
45
+
46
+ return [null, new Error(`decode ${describe(type)}: ${where}${problem.message}`)];
47
+ }
48
+
49
+ const PRIMITIVES = { number: 'number', string: 'string', bool: 'boolean' };
50
+
51
+ /** Where and how the value does not fit the type, or null if it does. */
52
+ function check(value, type, path) {
53
+ if (typeof type === 'string') {
54
+ return type === 'any' || typeof value === PRIMITIVES[type] ? null : mismatch(value, type, path);
55
+ }
56
+
57
+ if ('literal' in type) return type.literal.includes(value) ? null : mismatch(value, type, path);
58
+
59
+ if ('nullable' in type) {
60
+ if (value == null) return null;
61
+
62
+ const problem = check(value, type.nullable, path);
63
+
64
+ // The value itself is of another type: the message names ?T, not T.
65
+ return problem !== null && problem.path.length === path.length
66
+ ? mismatch(value, type, path)
67
+ : problem;
68
+ }
69
+
70
+ if ('array' in type) {
71
+ if (!Array.isArray(value)) return mismatch(value, type, path);
72
+
73
+ for (let i = 0; i < value.length; i++) {
74
+ const problem = check(value[i], type.array, [...path, i]);
75
+
76
+ if (problem !== null) return problem;
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ if ('union' in type) return checkUnion(value, type, path);
83
+ if (!isObject(value)) return mismatch(value, type, path);
84
+
85
+ if ('record' in type) {
86
+ for (const key of Object.keys(value)) {
87
+ const problem = check(value[key], type.record, [...path, key]);
88
+
89
+ if (problem !== null) return problem;
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ for (const [name, field] of Object.entries(type.fields)) {
96
+ // Only own fields: a field named `toString` is not the method of every object.
97
+ if (!Object.hasOwn(value, name)) {
98
+ if (isOptional(field)) continue;
99
+
100
+ return { path: [...path, name], message: 'missing' };
101
+ }
102
+
103
+ const problem = check(value[name], field, [...path, name]);
104
+
105
+ if (problem !== null) return problem;
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * The value fits one of the types. If none fits, the type whose check went deepest into the value
113
+ * is most likely the one meant, `{ kind: "circle", r: "2" }` of `Circle | Square`, so its problem
114
+ * is the one reported.
115
+ */
116
+ function checkUnion(value, type, path) {
117
+ let deepest = null;
118
+
119
+ for (const member of type.union) {
120
+ const problem = check(value, member, path);
121
+
122
+ if (problem === null) return null;
123
+ if (problem.path.length > (deepest?.path.length ?? path.length)) deepest = problem;
124
+ }
125
+
126
+ return deepest ?? mismatch(value, type, path);
127
+ }
128
+
129
+ function isObject(value) {
130
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
131
+ }
132
+
133
+ function isOptional(type) {
134
+ return type === 'any' || (typeof type === 'object' && 'nullable' in type);
135
+ }
136
+
137
+ function mismatch(value, type, path) {
138
+ return { path, message: `expected ${describe(type)}, got ${shown(value)}` };
139
+ }
140
+
141
+ /** The type as Rook writes it: `[]string`, `?User`, `"admin" | "user"`. */
142
+ function describe(type) {
143
+ if (typeof type === 'string') return type;
144
+ if ('literal' in type) return type.literal.map((value) => JSON.stringify(value)).join(' | ');
145
+ if ('nullable' in type) return `?${grouped(type.nullable)}`;
146
+ if ('array' in type) return `[]${grouped(type.array)}`;
147
+ if ('record' in type) return `{ [key string]: ${describe(type.record)} }`;
148
+ if ('union' in type) return type.union.map(describe).join(' | ');
149
+
150
+ return type.name;
151
+ }
152
+
153
+ function grouped(type) {
154
+ const several = typeof type === 'object' && ('union' in type || type.literal?.length > 1);
155
+
156
+ return several ? `(${describe(type)})` : describe(type);
157
+ }
158
+
159
+ /** The value in a message: a primitive itself, a long string cut, and the kind of anything else. */
160
+ function shown(value) {
161
+ if (value === null) return 'null';
162
+ if (Array.isArray(value)) return 'an array';
163
+
164
+ switch (typeof value) {
165
+ case 'string':
166
+ return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value);
167
+
168
+ case 'number':
169
+ case 'boolean':
170
+ return String(value);
171
+
172
+ case 'object':
173
+ return 'an object';
174
+
175
+ default:
176
+ return typeof value;
177
+ }
178
+ }
179
+
180
+ /** `tags[1]`, `boss.name`, `["first name"]`. */
181
+ function pathText(path) {
182
+ return path
183
+ .map((key, i) => {
184
+ if (typeof key === 'number') return `[${key}]`;
185
+ if (/^[A-Za-z_$][\w$]*$/.test(key)) return i === 0 ? key : `.${key}`;
186
+
187
+ return `[${JSON.stringify(key)}]`;
188
+ })
189
+ .join('');
190
+ }
package/src/runtime/go.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  $$gone,
10
10
  $$incoming,
11
11
  $$post,
12
+ $$reclassed,
12
13
  $$registry,
13
14
  $$transfer,
14
15
  } from './threads.js';
@@ -142,6 +143,7 @@ export function $$pool() {
142
143
  worker.owns = data.owns;
143
144
  } else if ('returned' in data) {
144
145
  $$adopt(data);
146
+ $$reclassed(data);
145
147
 
146
148
  const call = calls.get(data.returned);
147
149
 
@@ -311,6 +313,7 @@ export async function $$serve(functions) {
311
313
  if ('gone' in data) return $$gone(data.gone);
312
314
  setTimeout(free);
313
315
  $$adopt(data.args);
316
+ $$reclassed(data);
314
317
 
315
318
  const { main } = registry;
316
319
 
@@ -3,11 +3,17 @@
3
3
  // The compiler can also put the parts that a module needs into the module itself (runtime.ts).
4
4
 
5
5
  export * from './chan.js';
6
+ export * from './channel.js';
7
+ export * from './classes.js';
6
8
  export * from './core.js';
9
+ export * from './decode.js';
7
10
  export * from './go.js';
8
11
  export * from './markup.js';
9
12
  export * from './queue.js';
10
13
  export * from './select.js';
14
+ export * from './signals.js';
11
15
  export * from './streams.js';
12
16
  export * from './threads.js';
13
17
  export * from './timers.js';
18
+ export * from './topic.js';
19
+ export * from './try.js';
@@ -3,12 +3,13 @@
3
3
  // channels of a select.
4
4
 
5
5
  import { $$finish, $$pump, $$zero } from './chan.js';
6
- import { $$ask, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';
6
+ import { $$ask, $$isEmpty, $$ownerOf, $$pin, $$post, $$registry, $$stopped } from './threads.js';
7
7
 
8
8
  // `select`: the index of the case that went on, and what it received. A case is { channel } to
9
- // receive or { channel, value } to send; from another thread, { channel, send, value }. One that
10
- // can go on at once wins, a random one as in Go; else `default`, -1, if there is one; else the
11
- // first that can go on later. A case with a null channel never goes on.
9
+ // receive or { channel, value } to send, with the zero value that Channel.EMPTY gives, if it is
10
+ // not null; from another thread, { channel, send, value }. One that can go on at once wins, a
11
+ // random one as in Go; else `default`, -1, if there is one; else the first that can go on later.
12
+ // A case with a null channel never goes on, and one with Channel.EMPTY always can.
12
13
  export async function $$select(cases, hasDefault) {
13
14
  const registry = $$registry();
14
15
  const all = cases.map((each, i) => ({
@@ -16,12 +17,13 @@ export async function $$select(cases, hasDefault) {
16
17
  id: typeof each.channel === 'string' ? each.channel : (each.channel?.$$chan ?? null),
17
18
  send: each.send ?? 'value' in each,
18
19
  value: each.value,
20
+ zero: each.zero ?? null,
19
21
  }));
20
22
  const live = all.filter(({ id }) => id !== null);
21
- const owners = new Set(live.map(({ id }) => $$ownerOf(id)));
23
+ const owners = new Set(live.filter(({ id }) => !$$isEmpty(id)).map(({ id }) => $$ownerOf(id)));
22
24
 
23
25
  // The channels of one other thread: the select runs there.
24
- if (owners.size === 1 && !owners.has(registry.key)) {
26
+ if (owners.size === 1 && !owners.has(registry.key) && live.every(({ id }) => !$$isEmpty(id))) {
25
27
  const [owner] = owners;
26
28
 
27
29
  return $$ask(owner, {
@@ -34,10 +36,13 @@ export async function $$select(cases, hasDefault) {
34
36
  const order = live.map(({ i }) => i).sort(() => Math.random() - 0.5);
35
37
 
36
38
  for (const i of order) {
37
- const { id, send, value } = all[i];
39
+ const { id, send, value, zero } = all[i];
38
40
  const owner = $$ownerOf(id);
39
- const ready =
40
- owner === registry.key
41
+ const ready = $$isEmpty(id)
42
+ ? send
43
+ ? []
44
+ : [zero, false]
45
+ : owner === registry.key
41
46
  ? $$try(id, send, value)
42
47
  : await $$ask(owner, { op: 'try', channel: id, send, value });
43
48