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
@@ -77,6 +77,7 @@ export class AssignmentChecker extends InitializerChecker {
77
77
  checkTarget(target) {
78
78
  if (this.checkDecoratorWrite(target) || this.checkSharedWrite(target))
79
79
  return UNKNOWN;
80
+ this.checkPropWrite(target);
80
81
  switch (target.kind) {
81
82
  case 'Identifier': {
82
83
  if (target.name === '_')
@@ -192,7 +193,7 @@ export class AssignmentChecker extends InitializerChecker {
192
193
  }
193
194
  const expected = fn.results;
194
195
  if (values.length === 0) {
195
- if (expected.length > 0) {
196
+ if (expected.length > 0 && !fn.mayEnd) {
196
197
  this.error(`missing return values: expected ${countValues(expected.length)}`, node);
197
198
  }
198
199
  return;
@@ -6,6 +6,8 @@ export declare abstract class CheckerBase {
6
6
  protected abstract checkClassBodies(node: ast.ClassDeclaration, info: ClassInfo): void;
7
7
  protected abstract ensureClassResolved(info: ClassInfo): void;
8
8
  protected abstract resolveClass(node: ast.ClassDeclaration, info: ClassInfo): void;
9
+ protected abstract typeEarly(node: ast.VariableDeclaration): void;
10
+ protected abstract resolveResults(results: readonly ast.TypeNode[]): Type[];
9
11
  protected abstract inferredType(type: Type, node: ast.Expression): Type;
10
12
  protected abstract resolveAlias(entry: AliasEntry): Type;
11
13
  protected abstract resolveType(node: ast.TypeNode): Type;
@@ -52,6 +54,7 @@ export declare abstract class CheckerBase {
52
54
  protected abstract wrappedType(info: ComponentInfo, own: Type, seen: Set<ComponentInfo>): Type;
53
55
  protected abstract checkDecoratorMember(node: ast.DecoratorMember): Type;
54
56
  protected abstract checkDecoratorWrite(target: ast.Expression): boolean;
57
+ protected abstract checkPropWrite(target: ast.Expression): void;
55
58
  protected abstract checkComponentUse(node: ast.ElementExpression, tag: ast.Identifier): Type;
56
59
  protected abstract checkRefDeclaration(node: ast.VariableDeclaration): void;
57
60
  protected abstract checkStyle(node: ast.StyleStatement): void;
@@ -7,7 +7,7 @@ import { explainMismatch, isAssignable, LazyMap, memberNames, nonNull, property,
7
7
  // shared state → declarations → class members → classes → resolution → initializers → assignments → statements → control flow →
8
8
  // tagged unions → functions → expressions → array literals → operators → markup → forwarding → component bindings → components → component values →
9
9
  // refs → styles → decorator wrappers → decorator exports → decorators → decorator includes → decorator members →
10
- // members → calls → goroutines → channels → signals → queues → topics → decode → try → bytes → loops → go for →
10
+ // members → calls → tags → goroutines → channels → signals → queues → topics → decode → try → bytes → loops → go for →
11
11
  // iterators → closing → completion → Checker (checker.ts). Statements, expressions and markup
12
12
  // check each other recursively, so methods of later layers that earlier ones call are declared
13
13
  // here as abstract. This layer has the state of a check, diagnostics, scopes and names, and the DOM types.
@@ -7,7 +7,7 @@ import { CHANNEL_OBJECT, TOPIC_OBJECT } from "./channel-objects.js";
7
7
  import { PROMISE } from "./promise.js";
8
8
  import { BUFFERS } from "./transfer.js";
9
9
  import { UNTYPED_CLASSES, UNTYPED_GLOBALS } from "./untyped-globals.js";
10
- import { ANY, arrayOf, BIGINT, BOOL, ERROR, ERROR_CLASS, func, method, NUMBER, nullable, property, STRING, topicOf, union, } from "./types.js";
10
+ import { ANY, arrayOf, BIGINT, BOOL, ERROR, ERROR_CLASS, func, method, NEVER, nullable, NUMBER, property, STRING, topicOf, union, VOID, } from "./types.js";
11
11
  // Types of the JS standard library that Rook programs use most. Anything not listed here
12
12
  // can still be reached through the untyped globals below.
13
13
  function object(name, members, call) {
@@ -280,6 +280,10 @@ export const GLOBAL_TYPES = new Map([
280
280
  ['string', STRING],
281
281
  ['bool', BOOL],
282
282
  ['any', ANY],
283
+ // A function whose result is `never` does not end: it throws or exits, as `process.exit`.
284
+ ['never', NEVER],
285
+ // The value of `Promise<void>`; as the result of a function, `void` is no result.
286
+ ['void', VOID],
283
287
  ['error', ERROR],
284
288
  ['Error', ERROR_CLASS.instance],
285
289
  ...['Date', 'RegExp', 'Map', 'Set', 'URL', ...UNTYPED_CLASSES, ...BUFFERS].map((name) => [name, ANY]),
@@ -21,3 +21,5 @@ export declare abstract class CallChecker extends MemberChecker {
21
21
  protected checkArguments(signature: FunctionType, args: readonly (ast.Expression | ast.SpreadElement)[], node: ast.NodeBase, inferred?: Map<TypeParam, Type>, expected?: Type | null): Type[];
22
22
  protected checkArgumentList(signature: FunctionType, args: readonly (ast.Expression | ast.SpreadElement)[], inferred: Map<TypeParam, Type>, needsContext: (arg: ast.Expression | ast.SpreadElement) => boolean, beforeContext?: () => void): void;
23
23
  }
24
+ /** The inferred type parameters, and the default, or unknown, of each of the others. */
25
+ export declare function withDefaults(params: readonly TypeParam[], inferred: ReadonlyMap<TypeParam, Type>): Map<TypeParam, Type>;
@@ -136,7 +136,10 @@ export class CallChecker extends MemberChecker {
136
136
  }
137
137
  // Arrow functions without parameter types are checked last: their parameter types may depend
138
138
  // on type parameters inferred from the other arguments (e.g. `reduce(f, initial)`).
139
- const needsContext = (arg) => arg.kind === 'ArrowFunction' && arg.params.some((param) => param.type === null);
139
+ // So are object literals with such functions in their fields: `serve({ fetch: req => ... })`.
140
+ const needsContext = (arg) => (arg.kind === 'ArrowFunction' && arg.params.some((param) => param.type === null)) ||
141
+ (arg.kind === 'ObjectLiteral' &&
142
+ arg.properties.some((property) => property.kind === 'Property' && needsContext(property.value)));
140
143
  const inferring = this.inferring;
141
144
  this.inferring = new Set(signature.typeParams);
142
145
  const errors = this.diagnostics.length;
@@ -200,8 +203,11 @@ export class CallChecker extends MemberChecker {
200
203
  return;
201
204
  }
202
205
  // An expected type with parameters that are not inferred yet would reject the argument:
203
- // `[1, 2]` for `[]T` is checked on its own, and T is inferred from it.
204
- const expected = substitute(param, inferred);
206
+ // `[1, 2]` for `[]T` is checked on its own, and T is inferred from it. An object literal
207
+ // checked last takes what is still open as its default, or unknown, as in TypeScript.
208
+ const expected = contextPass && arg.kind === 'ObjectLiteral'
209
+ ? substitute(param, withDefaults(signature.typeParams, inferred))
210
+ : substitute(param, inferred);
205
211
  const type = this.checkValue(arg, arg.kind === 'ArrowFunction' || !containsTypeParam(expected, this.inferring)
206
212
  ? expected
207
213
  : null);
@@ -211,3 +217,12 @@ export class CallChecker extends MemberChecker {
211
217
  }
212
218
  }
213
219
  }
220
+ /** The inferred type parameters, and the default, or unknown, of each of the others. */
221
+ export function withDefaults(params, inferred) {
222
+ const bindings = new Map(inferred);
223
+ for (const param of params) {
224
+ if (!bindings.has(param))
225
+ bindings.set(param, param.default ?? UNKNOWN);
226
+ }
227
+ return bindings;
228
+ }
@@ -253,6 +253,14 @@ export class ClassChecker extends ClassMemberChecker {
253
253
  break;
254
254
  }
255
255
  }
256
+ // `export { Rows } from "./rows.rk"`: the names of another module, as it exports them.
257
+ for (const kind of ['values', 'types', 'decorators', 'components']) {
258
+ for (const [name, value] of this.reexported[kind] ?? []) {
259
+ exports[kind].set(name, value);
260
+ }
261
+ }
262
+ for (const name of this.reexported.shared ?? [])
263
+ exports.shared.add(name);
256
264
  return { exports, declarations };
257
265
  }
258
266
  }
@@ -4,4 +4,20 @@ import { ForwardingChecker } from './forwarding.ts';
4
4
  export declare abstract class ComponentBindingChecker extends ForwardingChecker {
5
5
  /** `bind:active={tab}` on a component; adds the name of the property to `given`. */
6
6
  protected checkPropBinding(attribute: ast.JsxAttribute, info: ComponentInfo, component: string, given: Set<string>): void;
7
+ /**
8
+ * The variable that `bind:` writes into. The item of `for row in rows` is one too: the component
9
+ * changes it in place, and a new value replaces it in `rows`, which must be a variable or a field.
10
+ */
11
+ private bindTarget;
12
+ /**
13
+ * `row.name = "x"` or `bind:value={row.name}` where `row` is a property that is not bound: the
14
+ * object is the parent's, which would change without its markup knowing.
15
+ */
16
+ protected checkPropWrite(target: ast.Expression): void;
17
+ /**
18
+ * `items.push(x)` or `seen.add(id)` where the property is not bound. Other methods are not
19
+ * reported: a method of a class or a function in a field may change nothing.
20
+ */
21
+ protected checkPropMutation(node: ast.CallExpression): void;
22
+ private propChanged;
7
23
  }
@@ -1,7 +1,8 @@
1
1
  import { attrsParam } from "../attrs.js";
2
+ import { ARRAY_MUTATORS } from "../methods.js";
2
3
  import { ForwardingChecker } from "./forwarding.js";
3
4
  import { isUntyped } from "./helpers.js";
4
- import { isAssignable, typeToString } from "./types.js";
5
+ import { isAssignable, nonNull, typeToString, UNKNOWN } from "./types.js";
5
6
  // Bound properties: `comp Tabs(bind active string)` may change `active`, and
6
7
  // `<Tabs bind:active={tab} />` writes those changes into the parent's `tab`, as `bind:value` of an
7
8
  // input does. The value goes both ways, so the variable and the property have the same type.
@@ -33,7 +34,7 @@ export class ComponentBindingChecker extends ForwardingChecker {
33
34
  this.error(`"bind:${name}" needs a variable or a field to write to`, value);
34
35
  return;
35
36
  }
36
- const type = this.checkTarget(value);
37
+ const type = this.bindTarget(value, name);
37
38
  this.checkedTypes.set(value, type);
38
39
  if (!prop?.bind || isUntyped(type) || isUntyped(prop.type))
39
40
  return;
@@ -42,4 +43,66 @@ export class ComponentBindingChecker extends ForwardingChecker {
42
43
  this.error(`cannot bind ${typeToString(type)} to "${name}" of <${component}>, which is ${typeToString(prop.type)}`, value);
43
44
  }
44
45
  }
46
+ /**
47
+ * The variable that `bind:` writes into. The item of `for row in rows` is one too: the component
48
+ * changes it in place, and a new value replaces it in `rows`, which must be a variable or a field.
49
+ */
50
+ bindTarget(value, name) {
51
+ const binding = value.kind === 'Identifier' ? this.lookupValue(value.name) : undefined;
52
+ if (binding?.kind !== 'loop' || value.kind !== 'Identifier')
53
+ return this.checkTarget(value);
54
+ const list = binding.iterable;
55
+ if (list?.kind !== 'Identifier' && list?.kind !== 'MemberExpression') {
56
+ this.error(`"bind:${name}" of "${value.name}" needs a loop over a list in a variable or a field, e.g. for ${value.name} in items`, value);
57
+ return this.checkValue(value);
58
+ }
59
+ // Written as `rows[i] = value`, so `rows` must be one that can be changed here.
60
+ const at = { start: value.start, end: value.end };
61
+ this.checkTarget({
62
+ kind: 'IndexExpression',
63
+ object: list,
64
+ index: { kind: 'NumberLiteral', value: 0, raw: '0', ...at },
65
+ optional: false,
66
+ start: value.start,
67
+ end: value.end,
68
+ });
69
+ return this.checkValue(value);
70
+ }
71
+ /**
72
+ * `row.name = "x"` or `bind:value={row.name}` where `row` is a property that is not bound: the
73
+ * object is the parent's, which would change without its markup knowing.
74
+ */
75
+ checkPropWrite(target) {
76
+ if (target.kind === 'MemberExpression' || target.kind === 'IndexExpression') {
77
+ this.propChanged(target.object, target);
78
+ }
79
+ }
80
+ /**
81
+ * `items.push(x)` or `seen.add(id)` where the property is not bound. Other methods are not
82
+ * reported: a method of a class or a function in a field may change nothing.
83
+ */
84
+ checkPropMutation(node) {
85
+ const { callee } = node;
86
+ if (callee.kind !== 'MemberExpression')
87
+ return;
88
+ const object = nonNull(this.typeOfChecked(callee.object) ?? UNKNOWN);
89
+ const name = callee.property.name;
90
+ const collection = object.kind === 'object' ? (object.instanceOf?.template.name ?? object.name) : null;
91
+ const changes = object.kind === 'array'
92
+ ? ARRAY_MUTATORS.has(name)
93
+ : (collection === 'Map' || collection === 'Set') && COLLECTION_MUTATORS.has(name);
94
+ if (changes)
95
+ this.propChanged(callee.object, node);
96
+ }
97
+ propChanged(object, node) {
98
+ let root = object;
99
+ while (root.kind === 'MemberExpression' || root.kind === 'IndexExpression')
100
+ root = root.object;
101
+ if (root.kind !== 'Identifier' || !this.component || this.decorator)
102
+ return;
103
+ if (this.lookupValue(root.name)?.kind !== 'prop')
104
+ return;
105
+ this.error(`"${root.name}" is a property: changing it in place would not update the parent; declare it bound, e.g. comp ${this.component.node.name.name}(bind ${root.name} ...), and pass bind:${root.name}={...}`, node);
106
+ }
45
107
  }
108
+ const COLLECTION_MUTATORS = new Set(['set', 'add', 'delete', 'clear']);
@@ -110,6 +110,13 @@ export interface Binding {
110
110
  */
111
111
  guards?: Binding[];
112
112
  guardedBy?: Binding;
113
+ /**
114
+ * A constant of the module with a written type or a literal value: its type, known before its
115
+ * declaration is checked. The fields of classes, which are resolved first, may use it.
116
+ */
117
+ early?: Type;
118
+ /** The list that `for item in items` goes over, which `bind:item={item}` writes into. */
119
+ iterable?: ast.Expression;
113
120
  }
114
121
  export interface ComponentInfo {
115
122
  node: ast.ComponentDeclaration;
@@ -212,6 +219,11 @@ export interface FunctionContext {
212
219
  isComponent?: boolean;
213
220
  /** An async function, which may wait: `await`, `<-ch`, `for v in ch`. */
214
221
  isAsync?: boolean;
222
+ /**
223
+ * The function may end without a value, as in TypeScript: its result is `any`, or it is a
224
+ * callback whose result in a `.d.ts` is `void | T`, `onopen = () => { ... }`.
225
+ */
226
+ mayEnd?: boolean;
215
227
  }
216
228
  export interface ClassContext {
217
229
  info: ClassInfo;
@@ -21,6 +21,8 @@ export declare abstract class DeclarationChecker extends SharedStateChecker {
21
21
  /** The type of a function; an async one returns a Promise of its results, and one whose
22
22
  * parameter is `yield` is an iterator (iterators.ts). */
23
23
  protected signature(params: readonly ast.Parameter[], results: readonly ast.TypeNode[], async?: boolean): FunctionType;
24
+ /** `...parts []any`: what each of the arguments it takes is, `any`. */
25
+ private restElement;
24
26
  /** `Promise<...>` of the results of an async function, with the global `Promise` type. */
25
27
  protected promiseOf(results: readonly Type[]): Type;
26
28
  }
@@ -5,7 +5,7 @@ import { recursiveComponents } from "../recursion.js";
5
5
  import { Scope } from "./context.js";
6
6
  import { SharedStateChecker } from "./shared-state.js";
7
7
  import { awaited, isPromise, promiseOf } from "./promise.js";
8
- import { createClass, func, typeToString, UNKNOWN, } from "./types.js";
8
+ import { ANY, createClass, func, typeToString, UNKNOWN, } from "./types.js";
9
9
  // Declarations of a list of statements: imports, types, functions, components, decorators and
10
10
  // classes, declared before the statements are checked, so that they can refer to each other.
11
11
  export class DeclarationChecker extends SharedStateChecker {
@@ -32,6 +32,9 @@ export class DeclarationChecker extends SharedStateChecker {
32
32
  case 'ImportDeclaration':
33
33
  this.declareImport(statement);
34
34
  break;
35
+ case 'ExportFromDeclaration':
36
+ this.declareExportFrom(statement);
37
+ break;
35
38
  case 'ClassDeclaration': {
36
39
  const info = createClass(statement.name.name);
37
40
  info.data = statement.data;
@@ -130,6 +133,12 @@ export class DeclarationChecker extends SharedStateChecker {
130
133
  if (entry?.kind === 'alias')
131
134
  this.resolveAlias(entry);
132
135
  }
136
+ if (topLevel) {
137
+ for (const statement of statements) {
138
+ if (statement.kind === 'VariableDeclaration')
139
+ this.typeEarly(statement);
140
+ }
141
+ }
133
142
  for (const [node, binding] of functions) {
134
143
  const typeParams = this.createTypeParams(node.typeParams);
135
144
  binding.type = this.withTypeParams(typeParams, node.typeParams, () => ({
@@ -242,7 +251,7 @@ export class DeclarationChecker extends SharedStateChecker {
242
251
  /** The type of a function; an async one returns a Promise of its results, and one whose
243
252
  * parameter is `yield` is an iterator (iterators.ts). */
244
253
  signature(params, results, async = false) {
245
- const types = results.map((result) => this.resolveType(result));
254
+ const types = this.resolveResults(results);
246
255
  if (async) {
247
256
  types.forEach((type, i) => {
248
257
  if (!isPromise(type))
@@ -251,7 +260,33 @@ export class DeclarationChecker extends SharedStateChecker {
251
260
  types[i] = awaited(type);
252
261
  });
253
262
  }
254
- return func(params.map((param) => (param.type ? this.resolveType(param.type) : UNKNOWN)), async ? [this.promiseOf(types)] : types, { iterator: isIterator(params) });
263
+ const last = params.at(-1);
264
+ const rest = last?.rest ? this.restElement(last) : undefined;
265
+ const plain = rest === undefined ? params : params.slice(0, -1);
266
+ const firstDefault = plain.findIndex((param) => param.defaultValue !== null);
267
+ const required = firstDefault === -1 ? plain.length : firstDefault;
268
+ for (const param of plain.slice(required)) {
269
+ if (param.defaultValue === null) {
270
+ this.error(`"${param.name.name}" comes after a parameter with a default value, so it needs one too, e.g. ${param.name.name} = ...`, param);
271
+ }
272
+ }
273
+ return func(
274
+ // `times = 1` has the type of its value.
275
+ plain.map((param) => param.type
276
+ ? this.resolveType(param.type)
277
+ : param.defaultValue
278
+ ? this.inferredType(this.checkValue(param.defaultValue), param.defaultValue)
279
+ : UNKNOWN), async ? [this.promiseOf(types)] : types, { iterator: isIterator(params), required, ...(rest ? { rest } : {}) });
280
+ }
281
+ /** `...parts []any`: what each of the arguments it takes is, `any`. */
282
+ restElement(param) {
283
+ const type = param.type ? this.resolveType(param.type) : ANY;
284
+ if (type.kind === 'array')
285
+ return type.element;
286
+ if (type.kind !== 'any' && type.kind !== 'unknown') {
287
+ this.error(`...${param.name.name} takes the arguments in an array: write its type as one, e.g. ...${param.name.name} []${typeToString(type)}`, param.type ?? param);
288
+ }
289
+ return ANY;
255
290
  }
256
291
  /** `Promise<...>` of the results of an async function, with the global `Promise` type. */
257
292
  promiseOf(results) {
@@ -16,6 +16,7 @@ export declare abstract class ExpressionChecker extends FunctionChecker {
16
16
  /** `[1, 2]`; implemented by ArrayLiteralChecker. */
17
17
  protected abstract checkArrayLiteral(node: ast.ArrayLiteral, expected: Type | null): Type;
18
18
  protected abstract checkTryExpression(node: ast.TryExpression): Type;
19
+ protected abstract checkTaggedTemplate(node: ast.TaggedTemplateExpression): Type;
19
20
  /** `[]byte("text")`; implemented by BytesChecker. */
20
21
  protected abstract checkConversion(node: ast.ConversionExpression): Type;
21
22
  protected abstract checkMake(node: ast.MakeExpression): Type;
@@ -89,6 +89,11 @@ export class ExpressionChecker extends FunctionChecker {
89
89
  return this.checkNew(node, expected);
90
90
  case 'ElementExpression':
91
91
  return this.checkElement(node);
92
+ // `user!`: taken not to be null, as in TypeScript.
93
+ case 'NonNullExpression':
94
+ return nonNull(this.checkExpression(node.expression, expected));
95
+ case 'TaggedTemplateExpression':
96
+ return this.checkTaggedTemplate(node);
92
97
  case 'DecoratorMember':
93
98
  return this.checkDecoratorMember(node);
94
99
  case 'MemberExpression':
@@ -125,6 +130,9 @@ export class ExpressionChecker extends FunctionChecker {
125
130
  this.error(initializerError, node);
126
131
  return UNKNOWN;
127
132
  }
133
+ // The field of a class is set when an instance is made, after the constants of the module.
134
+ if (binding.type === null && binding.early && this.cls)
135
+ return binding.early;
128
136
  if (binding.type === null) {
129
137
  this.error(`"${node.name}" is used before its declaration`, node);
130
138
  return UNKNOWN;
@@ -154,11 +162,15 @@ export class ExpressionChecker extends FunctionChecker {
154
162
  }
155
163
  checkObjectLiteral(node, expected) {
156
164
  let target = expected ? nonNull(expected) : null;
165
+ // The objects of a union that no tag chooses from: fields take what they share from them.
166
+ let union = null;
157
167
  // `?(ScrollIntoViewOptions | bool)`: an object literal can only be the object type in it, or
158
168
  // the one that its tag chooses, `{ kind: "pickup" }` of `Pickup | Courier`.
159
169
  if (target?.kind === 'union') {
160
170
  const objects = target.types.filter((type) => type.kind === 'object' || type.kind === 'class');
161
171
  target = objects.length === 1 ? objects[0] : this.taggedMember(node, objects, target);
172
+ if (target === null)
173
+ union = objects;
162
174
  }
163
175
  const index = target?.kind === 'object' ? target.index : undefined;
164
176
  const targetMembers = target?.kind === 'object'
@@ -202,7 +214,11 @@ export class ExpressionChecker extends FunctionChecker {
202
214
  if (targetMembers && !expectedMember && target) {
203
215
  this.error(`${typeToString(target)} has no field "${name}"`, property.key);
204
216
  }
205
- let type = this.checkValue(property.value, expectedMember?.type ?? null);
217
+ const context = expectedMember?.type ?? (union && this.commonField(union, name));
218
+ // In JS `this` of a method is the object, not the class around it: it is not used.
219
+ let type = property.method
220
+ ? this.withClass(null, () => this.checkValue(property.value, context))
221
+ : this.checkValue(property.value, context);
206
222
  if (expectedMember) {
207
223
  // Reported at the field; the literal then counts as having the expected field type.
208
224
  this.expectAssignable(type, expectedMember.type, property.value, ` for field "${name}"`);
@@ -34,7 +34,7 @@ export declare function withoutAssigned(flow: Flow, names: ReadonlySet<string>,
34
34
  export declare function withNarrowing(flow: Flow, narrowing: Narrowing): Flow;
35
35
  /** Narrowing that holds after both branches: the places narrowed the same way in both. */
36
36
  export declare function mergeFlows(a: Flow, b: Flow): Flow;
37
- /** Go's terminating statements: execution never continues after them. */
37
+ export declare function markNeverEnding(node: ast.Statement): void;
38
38
  export declare function isTerminating(node: ast.Statement): boolean;
39
39
  /** Whether execution never continues after the statement, counting `break` and `continue`. */
40
40
  export declare function leaves(node: ast.Statement): boolean;
@@ -124,11 +124,21 @@ export function mergeFlows(a, b) {
124
124
  return result;
125
125
  }
126
126
  /** Go's terminating statements: execution never continues after them. */
127
+ /**
128
+ * Statements that call a function whose result is `never`, `process.exit(1)`: the checker marks
129
+ * them, and they end a block as `throw` does.
130
+ */
131
+ const neverEnding = new WeakSet();
132
+ export function markNeverEnding(node) {
133
+ neverEnding.add(node);
134
+ }
127
135
  export function isTerminating(node) {
128
136
  switch (node.kind) {
129
137
  case 'ReturnStatement':
130
138
  case 'ThrowStatement':
131
139
  return true;
140
+ case 'ExpressionStatement':
141
+ return neverEnding.has(node);
132
142
  case 'BlockStatement': {
133
143
  const last = node.body.at(-1);
134
144
  return last !== undefined && isTerminating(last);
@@ -13,6 +13,8 @@ export declare abstract class FunctionChecker extends TaggedUnionChecker {
13
13
  closure?: boolean;
14
14
  paramKind?: BindingKind;
15
15
  async?: boolean;
16
+ /** A callback whose result in a `.d.ts` is `void | T`: it may end without a value. */
17
+ mayEnd?: boolean;
16
18
  inspect?: (scope: Scope) => void;
17
19
  }): Type[];
18
20
  /** The body of `x => x * 2`. */
@@ -3,7 +3,7 @@ import { isTerminating } from "./flow.js";
3
3
  import { isUntyped, callSignature } from "./helpers.js";
4
4
  import { awaitedResults, isPromise } from "./promise.js";
5
5
  import { TaggedUnionChecker } from "./tagged-unions.js";
6
- import { commonType, containsTypeParam, func, nonNull, UNKNOWN, } from "./types.js";
6
+ import { ANY, arrayOf, commonType, containsTypeParam, func, isNullable, nonNull, UNKNOWN, } from "./types.js";
7
7
  // Bodies of functions, arrow functions and func literals, and the results they return.
8
8
  export class FunctionChecker extends TaggedUnionChecker {
9
9
  /**
@@ -25,18 +25,28 @@ export class FunctionChecker extends TaggedUnionChecker {
25
25
  isConstructor: options.isConstructor ?? false,
26
26
  isComponent: options.isComponent ?? false,
27
27
  isAsync: async,
28
+ mayEnd: options.mayEnd === true || (declared?.length === 1 && declared[0].kind === 'any'),
28
29
  };
29
30
  try {
30
31
  // A generic function: its type parameters are types in its body.
31
32
  for (const typeParam of type.typeParams)
32
33
  this.scope.types.set(typeParam.name, typeParam);
33
- params.forEach((param, i) =>
34
- // A bound property of a component, `bind active string`, changes as its state does.
35
- this.declareValue(param.name, param.bind ? 'state' : (options.paramKind ?? 'param'), type.params[i] ?? UNKNOWN));
34
+ params.forEach((param, i) => {
35
+ // `...parts []any` is the array of the arguments; `times = 1` sees the parameters before it.
36
+ const paramType = param.rest && type.rest ? arrayOf(type.rest) : (type.params[i] ?? UNKNOWN);
37
+ if (param.defaultValue && !options.isComponent) {
38
+ this.expectAssignable(this.checkValue(param.defaultValue, paramType), paramType, param.defaultValue);
39
+ }
40
+ // A bound property of a component, `bind active string`, changes as its state does.
41
+ this.declareValue(param.name, param.bind ? 'state' : (options.paramKind ?? 'param'), paramType);
42
+ });
36
43
  if (body.kind === 'BlockStatement') {
37
44
  this.checkStatementList(body.body, false);
38
45
  options.inspect?.(this.scope);
39
- if (declared && declared.length > 0 && !isTerminating(body)) {
46
+ if (declared?.length === 1 && declared[0].kind === 'never' && !isTerminating(body)) {
47
+ this.error('a function whose result is never must not reach its end: throw, or call one that never returns, e.g. process.exit(1)', { start: body.end - 1, end: body.end });
48
+ }
49
+ else if (declared && declared.length > 0 && !this.fn.mayEnd && !isTerminating(body)) {
40
50
  this.error('missing return at the end of the function', {
41
51
  start: body.end - 1,
42
52
  end: body.end,
@@ -101,7 +111,9 @@ export class FunctionChecker extends TaggedUnionChecker {
101
111
  const context = expected ? callSignature(nonNull(expected)) : null;
102
112
  // Callbacks passed to untyped JS functions get untyped parameters.
103
113
  const untypedContext = expected !== null && isUntyped(nonNull(expected));
104
- const params = node.params.map((param, i) => {
114
+ const last = node.params.at(-1);
115
+ const plain = last?.rest ? node.params.slice(0, -1) : node.params;
116
+ const params = plain.map((param, i) => {
105
117
  if (param.type)
106
118
  return this.resolveType(param.type);
107
119
  const fromContext = context ? (context.params[i] ?? context.rest) : null;
@@ -109,9 +121,19 @@ export class FunctionChecker extends TaggedUnionChecker {
109
121
  return fromContext;
110
122
  if (untypedContext)
111
123
  return nonNull(expected);
124
+ if (param.defaultValue)
125
+ return this.inferredType(this.checkValue(param.defaultValue), param.defaultValue);
112
126
  this.error(`cannot infer the type of parameter "${param.name.name}": add a type`, param);
113
127
  return UNKNOWN;
114
128
  });
129
+ // `(...parts) => ...` takes the rest of the context, or anything.
130
+ const restType = last?.rest
131
+ ? last.type
132
+ ? this.resolveType(last.type)
133
+ : arrayOf(context?.rest ?? ANY)
134
+ : null;
135
+ const rest = restType?.kind === 'array' ? restType.element : restType ? ANY : null;
136
+ const firstDefault = plain.findIndex((param) => param.defaultValue !== null);
115
137
  // Known result types from the context are checked; otherwise they are inferred. Those of an
116
138
  // async function must be a Promise.
117
139
  const contextResults = context &&
@@ -120,7 +142,10 @@ export class FunctionChecker extends TaggedUnionChecker {
120
142
  (!node.async || (context.results.length === 1 && isPromise(context.results[0])))
121
143
  ? context.results
122
144
  : null;
123
- const type = func(params, []);
145
+ const type = func(params, [], {
146
+ required: firstDefault === -1 ? params.length : firstDefault,
147
+ ...(rest ? { rest } : {}),
148
+ });
124
149
  const checkBody = () => {
125
150
  const inferring = this.inferring;
126
151
  this.inferring = new Set();
@@ -128,6 +153,7 @@ export class FunctionChecker extends TaggedUnionChecker {
128
153
  return this.checkFunction(node.params, type, contextResults, node.body, {
129
154
  closure: true,
130
155
  async: node.async,
156
+ mayEnd: contextResults?.length === 1 && isNullable(contextResults[0]),
131
157
  });
132
158
  }
133
159
  finally {
@@ -1,7 +1,7 @@
1
1
  import type * as ast from '../ast.ts';
2
- import { CallChecker } from './calls.ts';
2
+ import { TagChecker } from './tags.ts';
3
3
  import { type Type } from './types.ts';
4
- export declare abstract class GoroutineChecker extends CallChecker {
4
+ export declare abstract class GoroutineChecker extends TagChecker {
5
5
  /** The functions already checked for what their code reaches, so each is reported once. */
6
6
  private readonly reached;
7
7
  protected checkGo(node: ast.GoExpression): Type;
@@ -1,7 +1,7 @@
1
1
  import { reach, recordCaptures } from "../goroutines.js";
2
2
  import { assignedNames, freeNames } from "../names.js";
3
3
  import { forEachChild } from "../walk.js";
4
- import { CallChecker } from "./calls.js";
4
+ import { TagChecker } from "./tags.js";
5
5
  import { awaited } from "./promise.js";
6
6
  import { notCopied } from "./transfer.js";
7
7
  import { UNKNOWN } from "./types.js";
@@ -10,7 +10,7 @@ import { UNKNOWN } from "./types.js";
10
10
  // arguments and gives back a copy of the result, so both must be data that can be copied; and the
11
11
  // code of the function must not need what a worker does not have: the variables of the module and
12
12
  // a document.
13
- export class GoroutineChecker extends CallChecker {
13
+ export class GoroutineChecker extends TagChecker {
14
14
  /** The functions already checked for what their code reaches, so each is reported once. */
15
15
  reached = new Set();
16
16
  checkGo(node) {
@@ -1,6 +1,16 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { CheckerBase } from './base.ts';
3
+ import type { ModuleExports } from './context.ts';
3
4
  export declare abstract class ImportChecker extends CheckerBase {
5
+ /** `export { Rows } from "./rows.rk"`: what this module exports of another, by name here. */
6
+ protected readonly reexported: ModuleExports;
7
+ /** The exports of the module that an import or an `export ... from` names. */
8
+ private exportsOf;
9
+ /**
10
+ * `export { Row, Rows } from "./rows.rk"`: the names go into the exports of this module, with
11
+ * what a component or a decorator brings. Without types of the module, nothing is known of them.
12
+ */
13
+ protected declareExportFrom(node: ast.ExportFromDeclaration): void;
4
14
  protected declareImport(node: ast.ImportDeclaration): void;
5
15
  /** `import { visible } from "./visible.rk"`: decorators have a namespace of their own. */
6
16
  private importDecorator;