rooklang 0.4.2 → 0.5.0

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 (100) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/CHANGELOG.ru.md +235 -0
  3. package/dist/aliases.d.ts +2 -0
  4. package/dist/aliases.js +83 -0
  5. package/dist/ast/declarations.d.ts +18 -0
  6. package/dist/ast/expressions.d.ts +14 -1
  7. package/dist/ast/markup.d.ts +2 -0
  8. package/dist/ast/statements.d.ts +2 -2
  9. package/dist/checker/assignments.js +2 -1
  10. package/dist/checker/base.d.ts +3 -0
  11. package/dist/checker/base.js +1 -1
  12. package/dist/checker/builtins.js +5 -1
  13. package/dist/checker/calls.d.ts +2 -0
  14. package/dist/checker/calls.js +18 -3
  15. package/dist/checker/classes.js +8 -0
  16. package/dist/checker/component-bindings.d.ts +16 -0
  17. package/dist/checker/component-bindings.js +65 -2
  18. package/dist/checker/context.d.ts +12 -0
  19. package/dist/checker/declarations.d.ts +2 -0
  20. package/dist/checker/declarations.js +38 -3
  21. package/dist/checker/expressions.d.ts +1 -0
  22. package/dist/checker/expressions.js +17 -1
  23. package/dist/checker/flow.d.ts +1 -1
  24. package/dist/checker/flow.js +10 -0
  25. package/dist/checker/functions.d.ts +2 -0
  26. package/dist/checker/functions.js +33 -7
  27. package/dist/checker/goroutines.d.ts +2 -2
  28. package/dist/checker/goroutines.js +2 -2
  29. package/dist/checker/imports.d.ts +10 -0
  30. package/dist/checker/imports.js +73 -6
  31. package/dist/checker/initializers.d.ts +12 -0
  32. package/dist/checker/initializers.js +80 -0
  33. package/dist/checker/loops.js +3 -1
  34. package/dist/checker/members.js +1 -0
  35. package/dist/checker/resolution.d.ts +2 -0
  36. package/dist/checker/resolution.js +13 -2
  37. package/dist/checker/statements.d.ts +5 -0
  38. package/dist/checker/statements.js +26 -3
  39. package/dist/checker/tagged-unions.d.ts +6 -0
  40. package/dist/checker/tagged-unions.js +31 -1
  41. package/dist/checker/tags.d.ts +6 -0
  42. package/dist/checker/tags.js +44 -0
  43. package/dist/checker/types/constructors.js +10 -1
  44. package/dist/checker/types/params.js +33 -7
  45. package/dist/checker/types/printing.js +5 -1
  46. package/dist/checker/types/relations.js +2 -1
  47. package/dist/codegen/calls.d.ts +2 -0
  48. package/dist/codegen/calls.js +23 -1
  49. package/dist/codegen/component-props.js +8 -1
  50. package/dist/codegen/control-flow.d.ts +3 -0
  51. package/dist/codegen/control-flow.js +17 -0
  52. package/dist/codegen/expressions.d.ts +4 -1
  53. package/dist/codegen/expressions.js +4 -15
  54. package/dist/codegen/functions.d.ts +2 -0
  55. package/dist/codegen/functions.js +13 -0
  56. package/dist/codegen/mutations.d.ts +6 -0
  57. package/dist/codegen/mutations.js +36 -0
  58. package/dist/codegen/options.d.ts +2 -0
  59. package/dist/codegen/reactive.js +9 -5
  60. package/dist/codegen/simple-statements.js +3 -1
  61. package/dist/codegen/statements.d.ts +4 -0
  62. package/dist/codegen/statements.js +37 -1
  63. package/dist/compile.js +2 -0
  64. package/dist/completion.js +2 -1
  65. package/dist/declarations.js +18 -0
  66. package/dist/lexer/base.d.ts +3 -2
  67. package/dist/lexer/base.js +14 -5
  68. package/dist/lexer/lexer.js +3 -0
  69. package/dist/lexer/token.d.ts +6 -1
  70. package/dist/lexer/token.js +0 -1
  71. package/dist/modules.js +4 -3
  72. package/dist/parser/base.d.ts +5 -0
  73. package/dist/parser/base.js +9 -1
  74. package/dist/parser/classes.js +4 -3
  75. package/dist/parser/declarations.d.ts +2 -0
  76. package/dist/parser/declarations.js +25 -2
  77. package/dist/parser/decorators.js +1 -1
  78. package/dist/parser/expressions.js +6 -0
  79. package/dist/parser/literals.d.ts +2 -4
  80. package/dist/parser/literals.js +12 -24
  81. package/dist/parser/objects.d.ts +8 -0
  82. package/dist/parser/objects.js +51 -0
  83. package/dist/parser/params.d.ts +37 -4
  84. package/dist/parser/params.js +143 -9
  85. package/dist/parser/postfix.d.ts +5 -0
  86. package/dist/parser/postfix.js +36 -0
  87. package/dist/parser/simple-statements.d.ts +2 -0
  88. package/dist/parser/simple-statements.js +50 -0
  89. package/dist/parser/statements.js +8 -6
  90. package/dist/parser/syntax.js +0 -1
  91. package/dist/parser/types.d.ts +0 -4
  92. package/dist/parser/types.js +17 -32
  93. package/dist/project-modules.js +21 -0
  94. package/dist/reexports.d.ts +4 -0
  95. package/dist/reexports.js +56 -0
  96. package/dist/typescript/importer.d.ts +4 -0
  97. package/dist/typescript/importer.js +27 -3
  98. package/dist/typescript/library.js +5 -2
  99. package/dist/walk.js +12 -0
  100. package/package.json +4 -2
@@ -58,10 +58,17 @@ export class ComponentPropsEmitter extends ControlFlowEmitter {
58
58
  */
59
59
  binder(target) {
60
60
  const name = `$$bind${this.nextId()}`;
61
+ const list = target.kind === 'Identifier' ? this.loopLists.get(target.name) : undefined;
61
62
  const text = this.liveExpression(target);
62
63
  // `todos` of `bind:todos={todos}`, or the state that `bind:open={menu.open}` writes into.
63
- const written = [...(this.reactive?.rootSources(target) ?? [])].filter((source) => source.kind === 'state');
64
+ const written = [...(this.reactive?.rootSources(list ?? target) ?? [])].filter((source) => source.kind === 'state');
64
65
  const body = this.block(() => {
66
+ // The item of `{for row in rows}`: a new one replaces it in the list, wherever it is now.
67
+ if (list) {
68
+ const items = this.liveExpression(list, POSTFIX);
69
+ this.line(`const $$at = ${items}.indexOf(${text});`);
70
+ this.line(`if ($$at !== -1) ${items}[$$at] = $$value;`);
71
+ }
65
72
  this.line(`${text} = $$value;`);
66
73
  for (const name of rootsOf(target, this.shared)) {
67
74
  this.line(`$$changed(${this.shared.listeners.get(name)});`);
@@ -8,6 +8,8 @@ import { type Live, type Source } from './reactive.ts';
8
8
  * returns its update.
9
9
  */
10
10
  export declare abstract class ControlFlowEmitter extends ElementEmitter {
11
+ /** The lists of the `{for}` loops around the code, by the names of their items. */
12
+ protected readonly loopLists: Map<string, ast.Expression>;
11
13
  /** Writes the code that creates the content and returns what to append; see the class. */
12
14
  protected liveControl(statement: ControlStatement, live: Live, sources: ReadonlySet<Source>): string;
13
15
  /** The choice of an if/else chain, `() => a ? 0 : b ? 1 : -1`, and its blocks. */
@@ -20,6 +22,7 @@ export declare abstract class ControlFlowEmitter extends ElementEmitter {
20
22
  * state go into its update. The block of `<Page />` gets the component, `($$component3) => ...`.
21
23
  */
22
24
  protected blockCreator(statements: readonly ast.Statement[], parent: Live, loop?: ast.ForInStatement, component?: ast.Identifier): string;
25
+ private blockFunction;
23
26
  /** The key of a list, `with key todo.id`: `(todo) => todo.id`, which its blocks are found by. */
24
27
  private listKey;
25
28
  /** A classic `for` loop in live markup: a function that runs it and returns the content. */
@@ -9,6 +9,8 @@ import { CONDITIONAL, indentMore, upperFirst } from "./syntax.js";
9
9
  * returns its update.
10
10
  */
11
11
  export class ControlFlowEmitter extends ElementEmitter {
12
+ /** The lists of the `{for}` loops around the code, by the names of their items. */
13
+ loopLists = new Map();
12
14
  /** Writes the code that creates the content and returns what to append; see the class. */
13
15
  liveControl(statement, live, sources) {
14
16
  const id = this.nextId();
@@ -88,6 +90,21 @@ export class ControlFlowEmitter extends ElementEmitter {
88
90
  blockCreator(statements, parent, loop, component) {
89
91
  const block = new Block(parent);
90
92
  const params = loop ? loopParameters(loop) : component ? [parameter(component)] : [];
93
+ const saved = loop && this.loopLists.get(loop.value.name);
94
+ // `bind:row={row}` in the block writes into the list.
95
+ if (loop)
96
+ this.loopLists.set(loop.value.name, loop.iterable);
97
+ try {
98
+ return this.blockFunction(block, statements, params, loop);
99
+ }
100
+ finally {
101
+ if (loop && saved)
102
+ this.loopLists.set(loop.value.name, saved);
103
+ else if (loop)
104
+ this.loopLists.delete(loop.value.name);
105
+ }
106
+ }
107
+ blockFunction(block, statements, params, loop) {
91
108
  return this.withFunction('none', params, () => this.withRendering(this.rendering + 1, () => {
92
109
  const paramList = this.params(params);
93
110
  const fragment = `$$block${this.nextId()}`;
@@ -22,6 +22,10 @@ export declare abstract class ExpressionEmitter extends Emitter {
22
22
  protected abstract streamChan(node: ast.StreamChanExpression): string;
23
23
  protected abstract queue(node: ast.QueueExpression): string;
24
24
  protected abstract topic(node: ast.TopicExpression): string;
25
+ /** `{ a: 1, ...b, f() { ... } }`; implemented by CallEmitter. */
26
+ protected abstract objectLiteral(node: ast.ObjectLiteral): string;
27
+ /** sql`select ... ${id}`; implemented by CallEmitter. */
28
+ protected abstract taggedTemplate(node: ast.TaggedTemplateExpression): string;
25
29
  /** `go f(x)`; implemented by CallEmitter. */
26
30
  protected abstract go(node: ast.GoExpression): string;
27
31
  /** `decode<T>(x)`; implemented by DecodeEmitter. */
@@ -54,7 +58,6 @@ export declare abstract class ExpressionEmitter extends Emitter {
54
58
  private expressionWithPrecedence;
55
59
  protected elements(elements: readonly (ast.Expression | ast.SpreadElement)[]): string;
56
60
  private template;
57
- private objectLiteral;
58
61
  private arrowFunction;
59
62
  private unary;
60
63
  private binary;
@@ -82,9 +82,12 @@ export class ExpressionEmitter extends Emitter {
82
82
  return [decoratedName(node.decorator.name, node.member.name), PRIMARY];
83
83
  case 'ThisExpression':
84
84
  return ['this', PRIMARY];
85
- // `as` is only for the checker.
85
+ // `as` and `user!` are only for the checker.
86
86
  case 'AsExpression':
87
+ case 'NonNullExpression':
87
88
  return this.expressionWithPrecedence(node.expression);
89
+ case 'TaggedTemplateExpression':
90
+ return [this.taggedTemplate(node), POSTFIX];
88
91
  case 'SuperExpression':
89
92
  return ['super', PRIMARY];
90
93
  case 'ArrayLiteral': {
@@ -171,20 +174,6 @@ export class ExpressionEmitter extends Emitter {
171
174
  });
172
175
  return `${text}\``;
173
176
  }
174
- objectLiteral(node) {
175
- if (node.properties.length === 0)
176
- return '{}';
177
- const properties = node.properties.map((property) => {
178
- if (property.kind === 'SpreadElement') {
179
- return `...${this.expression(property.argument, ARROW)}`;
180
- }
181
- const key = property.key.kind === 'StringLiteral' ? property.key.raw : property.key.name;
182
- const value = this.expression(property.value, ARROW);
183
- // A renamed shorthand (`{ delete }` → `delete$`) must keep its key.
184
- return property.shorthand && value === key ? key : `${key}: ${value}`;
185
- });
186
- return `{ ${properties.join(', ')} }`;
187
- }
188
177
  arrowFunction(node) {
189
178
  const { body } = node;
190
179
  const async = node.async ? 'async ' : '';
@@ -2,6 +2,8 @@ import type * as ast from '../ast.ts';
2
2
  import { WebComponentEmitter } from './web-components.ts';
3
3
  /** Function bodies and `defer`. */
4
4
  export declare abstract class FunctionEmitter extends WebComponentEmitter {
5
+ /** `a, times = 1, ...parts`: default values and the rest parameter are those of JS. */
6
+ protected params(params: ast.Parameter[]): string;
5
7
  /** Parameters and body of a function, method or func literal. */
6
8
  protected func(params: ast.Parameter[], body: ast.BlockStatement): [string, string];
7
9
  /**
@@ -4,6 +4,19 @@ import { WebComponentEmitter } from "./web-components.js";
4
4
  import { ARROW, POSTFIX } from "./syntax.js";
5
5
  /** Function bodies and `defer`. */
6
6
  export class FunctionEmitter extends WebComponentEmitter {
7
+ /** `a, times = 1, ...parts`: default values and the rest parameter are those of JS. */
8
+ params(params) {
9
+ const names = super.params(params).split(', ');
10
+ return params
11
+ .map((param, i) => {
12
+ if (param.rest)
13
+ return `...${names[i]}`;
14
+ return param.defaultValue
15
+ ? `${names[i]} = ${this.expression(param.defaultValue, ARROW)}`
16
+ : names[i];
17
+ })
18
+ .join(', ');
19
+ }
7
20
  /** Parameters and body of a function, method or func literal. */
8
21
  func(params, body) {
9
22
  const defers = findDefers(body);
@@ -4,6 +4,12 @@ import type { Type } from '../checker/types.ts';
4
4
  export declare function rootName(node: ast.Expression): string | null;
5
5
  /** Names whose values a node reads, not counting code that runs on events. */
6
6
  export declare function readNames(node: ast.Node, names: Set<string>): void;
7
+ /**
8
+ * Names of the functions that code may run while it runs: those it calls and those it gives to a
9
+ * call, `items.map(format)`. A function it only gives as a value, `act={copy}` or
10
+ * `act={() => show("x")}`, runs later, when whoever gets it calls it.
11
+ */
12
+ export declare function calledNames(node: ast.Node, names: Set<string>): void;
7
13
  /**
8
14
  * Names an effect reads while it runs: not in the functions it creates, which run later, nor in
9
15
  * its `defer`, nor the variables it writes, or `count++` would run it again.
@@ -57,6 +57,42 @@ export function readNames(node, names) {
57
57
  forEachChild(node, (child) => readNames(child, names));
58
58
  }
59
59
  }
60
+ /**
61
+ * Names of the functions that code may run while it runs: those it calls and those it gives to a
62
+ * call, `items.map(format)`. A function it only gives as a value, `act={copy}` or
63
+ * `act={() => show("x")}`, runs later, when whoever gets it calls it.
64
+ */
65
+ export function calledNames(node, names) {
66
+ switch (node.kind) {
67
+ case 'CallExpression':
68
+ if (node.callee.kind === 'Identifier')
69
+ names.add(node.callee.name);
70
+ else
71
+ calledNames(node.callee, names);
72
+ for (const arg of node.arguments) {
73
+ if (arg.kind === 'Identifier')
74
+ names.add(arg.name);
75
+ else if (arg.kind === 'ArrowFunction' || arg.kind === 'FuncExpression') {
76
+ forEachChild(arg, (child) => calledNames(child, names));
77
+ }
78
+ else
79
+ calledNames(arg, names);
80
+ }
81
+ return;
82
+ case 'ArrowFunction':
83
+ case 'FuncExpression':
84
+ case 'EventHandler':
85
+ case 'MountStatement':
86
+ case 'EffectStatement':
87
+ return;
88
+ case 'JsxAttribute':
89
+ if (node.value !== null && !/^on[A-Z]/.test(node.name.name))
90
+ calledNames(node.value, names);
91
+ return;
92
+ default:
93
+ forEachChild(node, (child) => calledNames(child, names));
94
+ }
95
+ }
60
96
  /**
61
97
  * Names an effect reads while it runs: not in the functions it creates, which run later, nor in
62
98
  * its `defer`, nor the variables it writes, or `count++` would run it again.
@@ -33,6 +33,8 @@ export interface JsOptions {
33
33
  dataClasses?: ReadonlySet<string> | undefined;
34
34
  /** State of other modules that this one imports, `local → name there`: it only reads it. */
35
35
  shared?: ReadonlyMap<string, string> | undefined;
36
+ /** What each `export { ... } from` passes on in JS, with hidden names (reexports.ts). */
37
+ reexports?: ReadonlyMap<ast.ExportFromDeclaration, readonly string[]> | undefined;
36
38
  }
37
39
  /** A decorator of another module, with the types of the expressions of that module. */
38
40
  export interface ImportedDecorator extends DecoratorSource {
@@ -2,7 +2,7 @@ import { ownStatements } from "../checker/helpers.js";
2
2
  import { wrappedMarkup } from "../decorators.js";
3
3
  import { containsReturn, returnedMarkup } from "../recursion.js";
4
4
  import { forEachChild } from "../walk.js";
5
- import { readNames, rootName, writeTargets } from "./mutations.js";
5
+ import { calledNames, readNames, rootName, writeTargets } from "./mutations.js";
6
6
  // Reactivity of components: which markup depends on which state, and which code changes it.
7
7
  export { Block, updatesOf } from "./live.js";
8
8
  const EMPTY = new Set();
@@ -69,19 +69,23 @@ export class Reactive {
69
69
  ...wrappedMarkup(component),
70
70
  ];
71
71
  const seen = new Set();
72
- const visit = (node) => {
72
+ // A function the markup only gives away, `act={copy}`, runs later and keeps its updates.
73
+ const visit = (node, body = false) => {
73
74
  const names = new Set();
74
- readNames(node, names);
75
+ if (body)
76
+ forEachChild(node, (child) => calledNames(child, names));
77
+ else
78
+ calledNames(node, names);
75
79
  for (const name of names) {
76
80
  const fn = this.functions.get(name);
77
81
  if (!fn || seen.has(name))
78
82
  continue;
79
83
  seen.add(name);
80
84
  this.renderFunctions.add(fn);
81
- visit(fn);
85
+ visit(fn, true);
82
86
  }
83
87
  };
84
- markup.forEach(visit);
88
+ markup.forEach((node) => visit(node));
85
89
  }
86
90
  /** Whether a statement of the body declares a function that the live markup calls. */
87
91
  declaresRenderFunction(node) {
@@ -170,8 +170,10 @@ export class SimpleStatementEmitter extends TryEmitter {
170
170
  case 'EffectStatement':
171
171
  this.effectStatement(node);
172
172
  break;
173
+ // Components write their styles themselves (ComponentEmitter), and StatementEmitter the
174
+ // exports of other modules.
173
175
  case 'StyleStatement':
174
- // Components write their styles themselves (ComponentEmitter).
176
+ case 'ExportFromDeclaration':
175
177
  break;
176
178
  }
177
179
  }
@@ -20,6 +20,10 @@ export declare abstract class StatementEmitter extends ParallelLoopEmitter {
20
20
  * decorators uses, and that of exported components when other modules inline them.
21
21
  */
22
22
  protected hiddenExports(): void;
23
+ /** `const { name, size: bytes, ...rest } = file`, as in JS. */
24
+ protected variableDeclaration(node: ast.VariableDeclaration): string;
25
+ /** `export { Rows } from "./rows.js"` with the hidden names that go with them (reexports.ts). */
26
+ private exportFrom;
23
27
  protected importPath(source: ast.StringLiteral): string;
24
28
  protected classDeclaration(node: ast.ClassDeclaration): void;
25
29
  /** Visibility modifiers are checked by the compiler and do not exist in the output. */
@@ -17,6 +17,10 @@ export class StatementEmitter extends ParallelLoopEmitter {
17
17
  sharedReads = null;
18
18
  /** State of the module: the updates of the components that read it are kept beside it. */
19
19
  emitStatement(node) {
20
+ if (node.kind === 'ExportFromDeclaration') {
21
+ this.exportFrom(node);
22
+ return;
23
+ }
20
24
  super.emitStatement(node);
21
25
  if (node.kind !== 'VariableDeclaration' || node.keyword !== 'state')
22
26
  return;
@@ -124,9 +128,34 @@ export class StatementEmitter extends ParallelLoopEmitter {
124
128
  this.blankLine();
125
129
  this.line(`export { ${names.join(', ')} };`);
126
130
  }
131
+ /** `const { name, size: bytes, ...rest } = file`, as in JS. */
132
+ variableDeclaration(node) {
133
+ if (!node.pattern)
134
+ return super.variableDeclaration(node);
135
+ const { keys, rest } = node.pattern;
136
+ const fields = keys.map((key, i) => {
137
+ const name = this.name(node.names[i].name);
138
+ const field = key.kind === 'StringLiteral' ? key.raw : key.name;
139
+ return field === name ? name : `${field}: ${name}`;
140
+ });
141
+ if (rest)
142
+ fields.push(`...${this.name(node.names.at(-1).name)}`);
143
+ return `${node.keyword} { ${fields.join(', ')} } = ${this.expression(node.values[0], ARROW)}`;
144
+ }
145
+ /** `export { Rows } from "./rows.js"` with the hidden names that go with them (reexports.ts). */
146
+ exportFrom(node) {
147
+ const source = this.importPath(node.source);
148
+ const names = node.specifiers ? (this.options.reexports?.get(node) ?? []) : null;
149
+ if (names === null)
150
+ this.line(`export * from ${source};`);
151
+ else if (names.length > 0)
152
+ this.line(`export { ${names.join(', ')} } from ${source};`);
153
+ }
127
154
  importPath(source) {
128
155
  const path = source.value;
129
- if (this.options.rewriteImports && /^\.{1,2}\//.test(path) && path.endsWith('.rk')) {
156
+ // `./button.rk` and the aliases `#ui/button.rk` and `@/ui/button.rk` (aliases.ts): the built
157
+ // module is next to its source.
158
+ if (this.options.rewriteImports && !isBare(path) && path.endsWith('.rk')) {
130
159
  return JSON.stringify(`${path.slice(0, -'.rk'.length)}.js`);
131
160
  }
132
161
  return source.raw;
@@ -235,3 +264,10 @@ function hash(text) {
235
264
  }
236
265
  return value.toString(16).padStart(8, '0');
237
266
  }
267
+ /** `pkg/file.rk` of another package, which is not built with this one. */
268
+ function isBare(path) {
269
+ return (!/^\.{1,2}\//.test(path) &&
270
+ !path.startsWith('#') &&
271
+ !path.startsWith('@/') &&
272
+ !path.startsWith('~/'));
273
+ }
package/dist/compile.js CHANGED
@@ -6,6 +6,7 @@ import { goroutines, goTarget, workerProgram } from "./goroutines.js";
6
6
  import { withoutMarks } from "./source-map.js";
7
7
  import { parse } from "./parser/parser.js";
8
8
  import { isStyleModule, styleModule, unusedClasses } from "./style-modules.js";
9
+ import { reexportedNames } from "./reexports.js";
9
10
  /**
10
11
  * Where the worker module of a module is: a `.worker.js` next to its `.js`, or, where `.rk`
11
12
  * files are imported as they are (`rook run`), the module itself with `?goroutines`. Vite drops
@@ -48,6 +49,7 @@ export function compileModule(source, options = {}) {
48
49
  runtime: options.runtime,
49
50
  dataClasses,
50
51
  ...importedCode(program, options.importModule),
52
+ reexports: reexportedNames(program, options.importModule),
51
53
  };
52
54
  const sourceName = options.sourceName ?? 'main.rk';
53
55
  const main = withoutMarks(generateJs(program, {
@@ -16,7 +16,8 @@ const STRINGS = new Set([
16
16
  'CssText',
17
17
  'RegExp',
18
18
  ]);
19
- const VALUE_KEYWORDS = KEYWORDS;
19
+ // `type` is a keyword only at the start of a statement, where it is offered too.
20
+ const VALUE_KEYWORDS = [...KEYWORDS, 'type'];
20
21
  // `queue` is a type of its own only in a type: elsewhere it is a name.
21
22
  const TYPE_KEYWORDS = ['func', 'comp', 'chan', 'queue', 'topic'];
22
23
  /** Keywords only at the start of a statement at the top level of a component or a decorator. */
@@ -29,6 +29,10 @@ class DeclarationPrinter {
29
29
  case 'ImportDeclaration':
30
30
  this.importDeclaration(statement);
31
31
  break;
32
+ case 'ExportFromDeclaration':
33
+ exports = true;
34
+ this.exportFrom(statement);
35
+ break;
32
36
  case 'FuncDeclaration':
33
37
  if (statement.exported)
34
38
  this.functionDeclaration(statement);
@@ -73,6 +77,20 @@ class DeclarationPrinter {
73
77
  const source = node.source.value.replace(/\.rk$/, '.js');
74
78
  this.lines.push(`import ${parts.join(', ')} from ${JSON.stringify(source)};`);
75
79
  }
80
+ /** `export { Row } from "./rows.js"`: the values and types it passes on; components have none. */
81
+ exportFrom(node) {
82
+ const source = JSON.stringify(node.source.value.replace(/\.rk$/, '.js'));
83
+ if (!node.specifiers) {
84
+ this.lines.push(`export * from ${source};`);
85
+ return;
86
+ }
87
+ const { exports } = this.checked;
88
+ const names = node.specifiers
89
+ .filter(({ local }) => exports.values.has(local.name) || exports.types.has(local.name))
90
+ .map(({ imported, local }) => imported.name === local.name ? local.name : `${imported.name} as ${local.name}`);
91
+ if (names.length > 0)
92
+ this.lines.push(`export { ${names.join(', ')} } from ${source};`);
93
+ }
76
94
  functionDeclaration(node) {
77
95
  const type = this.checked.declarations.values.get(node.name.name);
78
96
  if (type?.kind !== 'function')
@@ -14,8 +14,9 @@ export declare abstract class LexerBase {
14
14
  protected lastEndsValue(): boolean;
15
15
  protected lastEndsStatement(): boolean;
16
16
  /**
17
- * Unlike Go, a line starting with `.` / `?.` continues a method chain, and a line starting with
18
- * a closing bracket ends a list, so no semicolon is needed before it.
17
+ * Unlike Go, a line starting with `.` / `?.` continues a method chain, a line starting with `?`
18
+ * or `:` continues a conditional expression, and a line starting with a closing bracket ends a
19
+ * list, so no semicolon is needed before it.
19
20
  */
20
21
  protected nextLineContinues(): boolean;
21
22
  protected insertSemicolon(): void;
@@ -55,21 +55,30 @@ export class LexerBase {
55
55
  }
56
56
  lastEndsStatement() {
57
57
  const last = this.tokens.at(-1);
58
- return last !== undefined && ENDS_STATEMENT.has(last.kind);
58
+ if (last === undefined)
59
+ return false;
60
+ // `env.PORT!` at the end of a line: the `!` right after a value, which takes it not to be null.
61
+ if (last.kind === '!') {
62
+ const before = this.tokens.at(-2);
63
+ return before !== undefined && before.end === last.start && ENDS_VALUE.has(before.kind);
64
+ }
65
+ return ENDS_STATEMENT.has(last.kind);
59
66
  }
60
67
  /**
61
- * Unlike Go, a line starting with `.` / `?.` continues a method chain, and a line starting with
62
- * a closing bracket ends a list, so no semicolon is needed before it.
68
+ * Unlike Go, a line starting with `.` / `?.` continues a method chain, a line starting with `?`
69
+ * or `:` continues a conditional expression, and a line starting with a closing bracket ends a
70
+ * list, so no semicolon is needed before it.
63
71
  */
64
72
  nextLineContinues() {
65
73
  const t = this.text;
66
74
  const c = t[this.pos];
67
75
  const next = t[this.pos + 1];
68
- if (c === ')' || c === ']' || c === '}')
76
+ if (c === ')' || c === ']' || c === '}' || c === ':')
69
77
  return true;
70
78
  if (c === '.')
71
79
  return next !== '.' && !isDigit(next);
72
- return c === '?' && next === '.' && !isDigit(t[this.pos + 2]);
80
+ // `? "yes"` and `?.name`, but not `??`, which stays at the end of the line as other operators.
81
+ return c === '?' && next !== '?';
73
82
  }
74
83
  insertSemicolon() {
75
84
  const end = this.tokens.at(-1)?.end ?? 0;
@@ -30,7 +30,10 @@ class Lexer extends StyleLexer {
30
30
  }
31
31
  if (this.pos >= this.text.length)
32
32
  break;
33
+ const count = this.tokens.length;
33
34
  this.scanToken();
35
+ if (sawNewline && this.tokens.length > count)
36
+ this.tokens[count].newlineBefore = true;
34
37
  }
35
38
  if (this.lastEndsStatement())
36
39
  this.insertSemicolon();
@@ -1,4 +1,4 @@
1
- export declare const KEYWORDS: readonly ["func", "comp", "go", "chan", "return", "let", "const", "type", "interface", "class", "extends", "new", "this", "super", "if", "else", "for", "in", "break", "continue", "switch", "case", "default", "defer", "try", "catch", "finally", "throw", "import", "export", "null", "true", "false", "typeof", "instanceof", "async", "await"];
1
+ export declare const KEYWORDS: readonly ["func", "comp", "go", "chan", "return", "let", "const", "interface", "class", "extends", "new", "this", "super", "if", "else", "for", "in", "break", "continue", "switch", "case", "default", "defer", "try", "catch", "finally", "throw", "import", "export", "null", "true", "false", "typeof", "instanceof", "async", "await"];
2
2
  export declare const PUNCTUATORS: readonly ["{", "}", "(", ")", "[", "]", ";", ",", ":", ".", "...", "?", "?.", "??", "=>", "=", "+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>=", "&&=", "||=", "??=", "==", "!=", "<", "<-", ">", "<=", ">=", "+", "-", "*", "/", "%", "**", "++", "--", "&", "|", "^", "~", "<<", ">>", ">>>", "&&", "||", "!", "@"];
3
3
  export type Keyword = (typeof KEYWORDS)[number];
4
4
  export type Punctuator = (typeof PUNCTUATORS)[number];
@@ -24,6 +24,11 @@ interface TokenBase {
24
24
  text: string;
25
25
  start: number;
26
26
  end: number;
27
+ /**
28
+ * A line break comes before the token. No semicolon is inserted after `>`, which may be an
29
+ * operator; the parser ends a declaration after `Map<string, number>` by it.
30
+ */
31
+ newlineBefore?: boolean;
27
32
  }
28
33
  export interface NumberToken extends TokenBase {
29
34
  kind: 'Number';
@@ -6,7 +6,6 @@ export const KEYWORDS = [
6
6
  'return',
7
7
  'let',
8
8
  'const',
9
- 'type',
10
9
  'interface',
11
10
  'class',
12
11
  'extends',
package/dist/modules.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
2
+ import { resolveRookModule } from "./aliases.js";
3
3
  import { check } from "./checker/checker.js";
4
4
  import { markupFor } from "./config.js";
5
5
  import { parse } from "./parser/parser.js";
@@ -9,9 +9,10 @@ const cache = new Map();
9
9
  const loading = new Set();
10
10
  /** Finds the types exported by a `.rk` module imported from the file `fromFile`. */
11
11
  export function loadModuleExports(fromFile, specifier, markup) {
12
- if (!specifier.startsWith('./') && !specifier.startsWith('../'))
12
+ // A relative path, or an alias of package.json or tsconfig.json (aliases.ts).
13
+ const path = specifier.endsWith('.rk') ? resolveRookModule(fromFile, specifier) : null;
14
+ if (path === null)
13
15
  return undefined;
14
- const path = resolve(dirname(fromFile), specifier);
15
16
  let version;
16
17
  try {
17
18
  const stats = statSync(path);
@@ -66,6 +66,11 @@ export declare abstract class ParserBase {
66
66
  protected requireMarkup(what: string): void;
67
67
  protected peek(offset?: number): Token;
68
68
  protected check(kind: TokenKind): boolean;
69
+ /**
70
+ * The line ended with the `>` of type arguments, `votes Map<string, number>`: the lexer inserts
71
+ * no semicolon after `>`, which may be an operator, so the line break ends the declaration here.
72
+ */
73
+ protected afterTypeArguments(): boolean;
69
74
  /** Whether the current token is the word `word`, e.g. the contextual keyword `from`. */
70
75
  protected checkWord(word: string): boolean;
71
76
  protected next(): Token;
@@ -1,6 +1,7 @@
1
1
  import { STATEMENT_KEYWORDS, BRACKETS, ParseError, describe } from "./syntax.js";
2
2
  // The parser is a chain of layers, one per area: base → simple statements → statements → control
3
- // flow → jumps → params → declarations → decorators → classes → types → expressions → literals → markup → Parser
3
+ // flow → jumps → params → declarations → decorators → classes → types → expressions → postfix →
4
+ // objects → literals → markup → Parser
4
5
  // (parser.ts). Statements, expressions and markup contain each other, so methods of later layers
5
6
  // that earlier ones call are declared here as abstract.
6
7
  //
@@ -43,6 +44,13 @@ export class ParserBase {
43
44
  check(kind) {
44
45
  return this.peek().kind === kind;
45
46
  }
47
+ /**
48
+ * The line ended with the `>` of type arguments, `votes Map<string, number>`: the lexer inserts
49
+ * no semicolon after `>`, which may be an operator, so the line break ends the declaration here.
50
+ */
51
+ afterTypeArguments() {
52
+ return this.pos > 0 && this.peek(-1).kind === '>' && this.peek().newlineBefore === true;
53
+ }
46
54
  /** Whether the current token is the word `word`, e.g. the contextual keyword `from`. */
47
55
  checkWord(word) {
48
56
  const token = this.peek();
@@ -83,7 +83,7 @@ export class ClassParser extends DecoratorParser {
83
83
  if (modifier)
84
84
  this.error(`a constructor cannot be ${word}`, modifier.start, modifier.end);
85
85
  }
86
- const params = this.parseParams(false, false, false, true);
86
+ const params = this.parseParams(false, { defaults: true, rest: 'typed', properties: true });
87
87
  const resultsStart = this.peek().start;
88
88
  if (this.parseResults().length > 0) {
89
89
  this.error('constructors cannot have result types', resultsStart, this.span(start).end);
@@ -161,7 +161,7 @@ export class ClassParser extends DecoratorParser {
161
161
  if (accessor && asyncToken) {
162
162
  this.error(`a ${accessor}ter cannot be async`, asyncToken.start, asyncToken.end);
163
163
  }
164
- const params = this.parseParams(false);
164
+ const params = this.parseParams(false, { defaults: true, rest: 'typed' });
165
165
  const results = this.parseResults();
166
166
  const header = this.span(start);
167
167
  if (accessor === 'get' && (params.length > 0 || results.length !== 1)) {
@@ -209,7 +209,8 @@ export class ClassParser extends DecoratorParser {
209
209
  };
210
210
  }
211
211
  parseTypeAlias(start, exported) {
212
- this.expect('type');
212
+ // The word `type`, which the caller has seen.
213
+ this.next();
213
214
  const name = this.parseIdentifier('type name');
214
215
  const typeParams = this.parseTypeParams();
215
216
  const type = this.parseType();
@@ -6,6 +6,8 @@ export declare abstract class DeclarationParser extends ParamParser {
6
6
  /** `state items []Item = []` at the top level of a module: state its components share. */
7
7
  protected parseDeclaration(start: number, exported: boolean): ast.Statement | undefined;
8
8
  protected parseExport(): ast.Statement;
9
+ /** `export { Row, Rows as Table } from "./rows.rk"` or `export * from "./rows.rk"`. */
10
+ private parseExportFrom;
9
11
  protected parseFuncDeclaration(start: number, exported: boolean): ast.FuncDeclaration;
10
12
  /**
11
13
  * `<T any, U Shape>` after a name; `<K, V any>` gives both the constraint, as in Go.