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
@@ -3,15 +3,82 @@ import { ANY, LazyMap, memberNames, UNKNOWN } from "./types.js";
3
3
  // Imports: the names of other modules, typed by their exports (`importModule` for `.rk` modules,
4
4
  // `importDeclarations` for the rest), or untyped when nothing is known about them.
5
5
  export class ImportChecker extends CheckerBase {
6
- declareImport(node) {
7
- const specifier = node.source.value;
8
- const isRook = specifier.endsWith('.rk');
9
- const result = isRook
6
+ /** `export { Rows } from "./rows.rk"`: what this module exports of another, by name here. */
7
+ reexported = {
8
+ values: new Map(),
9
+ types: new Map(),
10
+ decorators: new Map(),
11
+ components: new Map(),
12
+ shared: new Set(),
13
+ };
14
+ /** The exports of the module that an import or an `export ... from` names. */
15
+ exportsOf(source) {
16
+ const specifier = source.value;
17
+ const result = specifier.endsWith('.rk')
10
18
  ? this.options.importModule?.(specifier)
11
19
  : this.options.importDeclarations?.(specifier);
12
20
  if (result && 'error' in result)
13
- this.error(result.error, node.source);
14
- const exports = result && 'exports' in result ? result.exports : null;
21
+ this.error(result.error, source);
22
+ return result && 'exports' in result ? result.exports : null;
23
+ }
24
+ /**
25
+ * `export { Row, Rows } from "./rows.rk"`: the names go into the exports of this module, with
26
+ * what a component or a decorator brings. Without types of the module, nothing is known of them.
27
+ */
28
+ declareExportFrom(node) {
29
+ const exports = this.exportsOf(node.source);
30
+ if (!exports)
31
+ return;
32
+ const specifiers = node.specifiers ??
33
+ [
34
+ ...new Set([
35
+ ...exports.values.keys(),
36
+ ...exports.types.keys(),
37
+ ...(exports.decorators?.keys() ?? []),
38
+ ...(exports.components?.keys() ?? []),
39
+ ]),
40
+ ].map((name) => {
41
+ const id = { kind: 'Identifier', name, start: node.start, end: node.start };
42
+ return {
43
+ kind: 'ImportSpecifier',
44
+ imported: id,
45
+ local: id,
46
+ start: node.start,
47
+ end: node.start,
48
+ };
49
+ });
50
+ const into = this.reexported;
51
+ for (const { imported, local } of specifiers) {
52
+ const name = imported.name;
53
+ const value = exports.values.get(name);
54
+ const type = exports.types.get(name);
55
+ const decorator = exports.decorators?.get(name);
56
+ const component = exports.components?.get(name);
57
+ if ([value, type, decorator, component].every((each) => each === undefined)) {
58
+ this.error(`"${name}" is not exported by "${node.source.value}"`, imported);
59
+ continue;
60
+ }
61
+ // Their code and the hidden names it uses keep the name they have in their module.
62
+ if ((decorator || component) && local.name !== name) {
63
+ this.error(`renaming ${component ? 'components' : 'decorators'} is not supported yet: export { ${name} }`, local);
64
+ continue;
65
+ }
66
+ if (value !== undefined)
67
+ into.values.set(local.name, value);
68
+ if (type !== undefined)
69
+ into.types.set(local.name, type);
70
+ if (decorator)
71
+ into.decorators.set(name, decorator);
72
+ if (component)
73
+ into.components.set(name, component);
74
+ if (exports.shared?.has(name))
75
+ into.shared.add(local.name);
76
+ }
77
+ }
78
+ declareImport(node) {
79
+ const specifier = node.source.value;
80
+ const isRook = specifier.endsWith('.rk');
81
+ const exports = this.exportsOf(node.source);
15
82
  if (node.defaultImport && exports && !isRook) {
16
83
  // `import fs from "node:fs"`: the default export of a module with types.
17
84
  const value = exports.values.get('default');
@@ -1,6 +1,7 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import type { Binding } from './context.ts';
3
3
  import { TypeResolver } from './resolution.ts';
4
+ import { type Type } from './types.ts';
4
5
  export declare abstract class InitializerChecker extends TypeResolver {
5
6
  /** Initializers being checked, the outermost first. */
6
7
  private readonly initializers;
@@ -16,7 +17,18 @@ export declare abstract class InitializerChecker extends TypeResolver {
16
17
  protected deferBody(body: ast.Node, check: () => void): boolean;
17
18
  /** The error for `binding` used in its own initializer, or `null` if this use is fine. */
18
19
  protected initializerError(binding: Binding): string | null;
20
+ /**
21
+ * `const { name, size: bytes, ...rest } = file`: the types of the fields, and of the rest, an
22
+ * object with the other fields. A method would lose its object, so it is not taken.
23
+ */
24
+ protected patternTypes(node: ast.VariableDeclaration): Type[];
19
25
  private lookupFrom;
20
26
  private saveState;
21
27
  private restoreState;
28
+ /**
29
+ * `const LIMIT = 5` or `const NAMES []string = ...` of the module: its type, before the
30
+ * statements are checked, for the fields of classes (Binding.early). Only a written type or a
31
+ * literal value, which reads no other names; what is reported here is reported again later.
32
+ */
33
+ protected typeEarly(node: ast.VariableDeclaration): void;
22
34
  }
@@ -1,5 +1,7 @@
1
1
  import { mentionsName } from "../names.js";
2
2
  import { TypeResolver } from "./resolution.js";
3
+ import { isUntyped } from "./helpers.js";
4
+ import { ANY, instanceMembers, isNullable, typeToString, UNKNOWN, } from "./types.js";
3
5
  export class InitializerChecker extends TypeResolver {
4
6
  /** Initializers being checked, the outermost first. */
5
7
  initializers = [];
@@ -71,6 +73,41 @@ export class InitializerChecker extends TypeResolver {
71
73
  ? `"${name}" is used in its own initializer: give it a type, e.g. ${declaration.keyword} ${name} func() = ...`
72
74
  : `"${name}" is used in its own initializer: write its type after the name`;
73
75
  }
76
+ /**
77
+ * `const { name, size: bytes, ...rest } = file`: the types of the fields, and of the rest, an
78
+ * object with the other fields. A method would lose its object, so it is not taken.
79
+ */
80
+ patternTypes(node) {
81
+ const { keys, rest } = node.pattern;
82
+ const value = node.values[0];
83
+ const object = this.checkValue(value);
84
+ if (isUntyped(object))
85
+ return node.names.map(() => ANY);
86
+ if (isNullable(object)) {
87
+ this.error(`cannot take the fields of ${typeToString(object)}: it may be null, check it first`, value);
88
+ return node.names.map(() => UNKNOWN);
89
+ }
90
+ const types = keys.map((key) => {
91
+ const name = key.kind === 'Identifier' ? key.name : key.value;
92
+ const member = this.findMember(object, name, key);
93
+ if (member === 'any')
94
+ return ANY;
95
+ if (member?.method) {
96
+ this.error(`"${name}" is a method: call it on the object, e.g. ${value.kind === 'Identifier' ? value.name : 'object'}.${name}()`, key);
97
+ }
98
+ return member?.type ?? UNKNOWN;
99
+ });
100
+ if (!rest)
101
+ return types;
102
+ const taken = new Set(keys.map((key) => (key.kind === 'Identifier' ? key.name : key.value)));
103
+ const all = object.kind === 'class'
104
+ ? instanceMembers(object.info)
105
+ : object.kind === 'object'
106
+ ? object.members
107
+ : new Map();
108
+ const members = new Map([...all].filter(([name, member]) => !taken.has(name) && !member.method && member.visibility === 'public'));
109
+ return [...types, { kind: 'object', name: null, members, call: null }];
110
+ }
74
111
  lookupFrom(scope, name) {
75
112
  for (; scope; scope = scope.parent) {
76
113
  const binding = scope.values.get(name);
@@ -101,4 +138,47 @@ export class InitializerChecker extends TypeResolver {
101
138
  decorator: this.decorator,
102
139
  } = state);
103
140
  }
141
+ /**
142
+ * `const LIMIT = 5` or `const NAMES []string = ...` of the module: its type, before the
143
+ * statements are checked, for the fields of classes (Binding.early). Only a written type or a
144
+ * literal value, which reads no other names; what is reported here is reported again later.
145
+ */
146
+ typeEarly(node) {
147
+ const [name] = node.names;
148
+ const [value] = node.values;
149
+ const binding = name && this.scope.values.get(name.name);
150
+ if (node.keyword !== 'const' || node.names.length !== 1 || !binding)
151
+ return;
152
+ if (!node.type && (node.values.length !== 1 || !value || !isLiteral(value)))
153
+ return;
154
+ const count = this.diagnostics.length;
155
+ try {
156
+ binding.early = node.type
157
+ ? this.resolveType(node.type)
158
+ : this.inferredType(this.checkValue(value), value);
159
+ }
160
+ finally {
161
+ this.diagnostics.length = count;
162
+ }
163
+ }
164
+ }
165
+ /** A value made of literals only: `5`, `"a"`, `-1`, `[1, 2]`, `{ retries: 3 }`. */
166
+ function isLiteral(node) {
167
+ switch (node.kind) {
168
+ case 'NumberLiteral':
169
+ case 'StringLiteral':
170
+ case 'BooleanLiteral':
171
+ return true;
172
+ case 'TemplateLiteral':
173
+ return node.expressions.length === 0;
174
+ case 'UnaryExpression':
175
+ return node.operator === '-' && node.argument.kind === 'NumberLiteral';
176
+ case 'ArrayLiteral':
177
+ return (node.elements.length > 0 &&
178
+ node.elements.every((element) => element.kind !== 'SpreadElement' && isLiteral(element)));
179
+ case 'ObjectLiteral':
180
+ return node.properties.every((property) => property.kind === 'Property' && isLiteral(property.value));
181
+ default:
182
+ return false;
183
+ }
104
184
  }
@@ -76,7 +76,9 @@ export class LoopChecker extends BytesChecker {
76
76
  this.withScope(() => {
77
77
  if (node.key)
78
78
  this.declareValue(node.key, 'loop', key);
79
- this.declareValue(node.value, 'loop', value);
79
+ const item = this.declareValue(node.value, 'loop', value);
80
+ if (iterable.kind === 'array')
81
+ item.iterable = node.iterable;
80
82
  if (node.withKey)
81
83
  this.checkListKey(node.withKey);
82
84
  this.dropNarrowing(node.body);
@@ -17,6 +17,7 @@ export class MemberChecker extends DecoratorMemberChecker {
17
17
  let [callee, shortCircuits] = this.chainPart(node.callee);
18
18
  this.checkDecoratorMutation(node);
19
19
  this.checkSharedMutation(node);
20
+ this.checkPropMutation(node);
20
21
  if (isNullable(callee)) {
21
22
  if (node.optional)
22
23
  shortCircuits = true;
@@ -15,6 +15,8 @@ export declare abstract class TypeResolver extends ClassChecker {
15
15
  protected typedArray(name: string): Type;
16
16
  /** `[]byte`, a Uint8Array. */
17
17
  protected bytes(): Type;
18
+ /** The results of a function: `void` alone, as in `close() void`, is no result. */
19
+ protected resolveResults(results: readonly ast.TypeNode[]): Type[];
18
20
  protected resolveTypeName(name: ast.Identifier, typeArgs?: readonly ast.TypeNode[]): Type;
19
21
  protected resolveAlias(entry: AliasEntry): Type;
20
22
  protected resolveAliasType(entry: AliasEntry): Type;
@@ -21,8 +21,14 @@ export class TypeResolver extends ClassChecker {
21
21
  return topicOf(this.resolveType(node.element));
22
22
  case 'NullableType':
23
23
  return nullable(this.resolveType(node.type));
24
- case 'FuncType':
25
- return func(node.params.map((param) => this.resolveType(param)), node.results.map((result) => this.resolveType(result)), { iterator: node.iterator === true });
24
+ case 'FuncType': {
25
+ // `func(string, ...[]any)`: the rest is an array of what each argument is.
26
+ const rest = node.rest ? this.resolveType(node.rest) : null;
27
+ return func(node.params.map((param) => this.resolveType(param)), this.resolveResults(node.results), {
28
+ iterator: node.iterator === true,
29
+ ...(rest ? { rest: rest.kind === 'array' ? rest.element : ANY } : {}),
30
+ });
31
+ }
26
32
  case 'ComponentType':
27
33
  return this.resolveComponentType(node);
28
34
  case 'ObjectType': {
@@ -78,6 +84,11 @@ export class TypeResolver extends ClassChecker {
78
84
  bytes() {
79
85
  return this.typedArray('Uint8Array');
80
86
  }
87
+ /** The results of a function: `void` alone, as in `close() void`, is no result. */
88
+ resolveResults(results) {
89
+ const types = results.map((result) => this.resolveType(result));
90
+ return types.length === 1 && types[0].kind === 'void' ? [] : types;
91
+ }
81
92
  resolveTypeName(name, typeArgs = []) {
82
93
  const entry = this.lookupType(name.name);
83
94
  if (entry === undefined) {
@@ -24,6 +24,11 @@ export declare abstract class StatementChecker extends AssignmentChecker {
24
24
  private checkDelete;
25
25
  protected checkBlock(statements: readonly ast.Statement[]): void;
26
26
  protected checkVariableDeclaration(node: ast.VariableDeclaration): void;
27
+ /**
28
+ * `export const Rows = Inner` of a component, without a type: its .d.ts would lose the
29
+ * properties, and other modules import a component by its name, with `export { ... } from`.
30
+ */
31
+ private componentInConstant;
27
32
  /**
28
33
  * `const user, err = loadUser(id)` of `(User, error)`: `user` is not null once `err` is found to
29
34
  * be null. Variables of `let` may be assigned apart, so only constants are linked.
@@ -1,7 +1,7 @@
1
1
  import { guardedByError } from "./error-results.js";
2
2
  import { AssignmentChecker } from "./assignments.js";
3
3
  import { placeOf } from "./context.js";
4
- import { isTerminating } from "./flow.js";
4
+ import { isTerminating, markNeverEnding } from "./flow.js";
5
5
  import { isUntyped, isNumeric, ownStatements } from "./helpers.js";
6
6
  import { func, hasZeroValue, isNullable, nonNull, nullable, typeToString, UNKNOWN, } from "./types.js";
7
7
  // Statements: declarations of variables, blocks, `mount`, `effect`. Assignments and `return`
@@ -16,7 +16,8 @@ export class StatementChecker extends AssignmentChecker {
16
16
  this.checkBlock(node.body);
17
17
  break;
18
18
  case 'ExpressionStatement':
19
- this.checkExpression(node.expression, null);
19
+ if (this.checkExpression(node.expression, null).kind === 'never')
20
+ markNeverEnding(node);
20
21
  break;
21
22
  case 'JsxElementStatement':
22
23
  this.checkExpression(node.element, null);
@@ -75,6 +76,7 @@ export class StatementChecker extends AssignmentChecker {
75
76
  this.checkEffect(node);
76
77
  break;
77
78
  case 'ImportDeclaration':
79
+ case 'ExportFromDeclaration':
78
80
  case 'FuncDeclaration':
79
81
  case 'ComponentDeclaration':
80
82
  case 'DecoratorDeclaration':
@@ -181,8 +183,26 @@ export class StatementChecker extends AssignmentChecker {
181
183
  });
182
184
  if (node.keyword === 'const' && !declared)
183
185
  this.linkError(node, bindings);
186
+ if (node.exported && !declared)
187
+ this.componentInConstant(node);
184
188
  });
185
189
  }
190
+ /**
191
+ * `export const Rows = Inner` of a component, without a type: its .d.ts would lose the
192
+ * properties, and other modules import a component by its name, with `export { ... } from`.
193
+ */
194
+ componentInConstant(node) {
195
+ const [value] = node.values;
196
+ const local = value?.kind === 'Identifier' ? value.name : null;
197
+ if (local === null || !this.lookupValue(local)?.component)
198
+ return;
199
+ const imported = this.program.body.flatMap((statement) => statement.kind === 'ImportDeclaration'
200
+ ? statement.namedImports
201
+ .filter((specifier) => specifier.local.name === local)
202
+ .map((specifier) => `export { ${specifier.imported.name} } from ${statement.source.raw}`)
203
+ : []);
204
+ this.error(`a component is exported by its name, not in a constant without a type: ${imported.length > 0 ? `write ${imported[0]}` : 'write export comp where it is declared'}, or give the constant a type, e.g. export const ${node.names[0].name} comp(title string) = ${local}`, node);
205
+ }
186
206
  /**
187
207
  * `const user, err = loadUser(id)` of `(User, error)`: `user` is not null once `err` is found to
188
208
  * be null. Variables of `let` may be assigned apart, so only constants are linked.
@@ -203,7 +223,10 @@ export class StatementChecker extends AssignmentChecker {
203
223
  initializerTypes(node, declared) {
204
224
  const { names, values } = node;
205
225
  let types;
206
- if (node.body) {
226
+ if (node.pattern) {
227
+ types = this.patternTypes(node);
228
+ }
229
+ else if (node.body) {
207
230
  types = [this.derivedBlockType(names[0].name, node.body.body, declared)];
208
231
  }
209
232
  else if (values.length === 0) {
@@ -27,6 +27,12 @@ export declare abstract class TaggedUnionChecker extends ControlFlowChecker {
27
27
  /** `switch d.kind`: in a case the members with its tags, in `default` those of no case. */
28
28
  protected narrowCase(node: ast.SwitchStatement, switchCase: ast.SwitchCase): Narrowing;
29
29
  private narrowTag;
30
+ /**
31
+ * The type of a field in the objects of a union that no tag chooses from, when it is the same in
32
+ * each that has it: the literal takes it from the context, as `fetch: req => ...` of the options
33
+ * of `Bun.serve`. Functions only need the same parameters, which is what their literals take.
34
+ */
35
+ protected commonField(objects: readonly Type[], name: string): Type | null;
30
36
  /**
31
37
  * `{ kind: "pickup", shop: "Main st." }` where one of several objects is expected: the member
32
38
  * that a tag of the literal chooses, or `null` when no tag chooses one. A tag of no member is
@@ -1,7 +1,8 @@
1
1
  import { placeOf } from "./context.js";
2
2
  import { ControlFlowChecker } from "./control-flow.js";
3
3
  import { literalValue, narrowByTag } from "./flow.js";
4
- import { findClassMember, memberTypeOf, nonNull, typeToString, unionMembers, UNKNOWN, } from "./types.js";
4
+ import { callSignature } from "./helpers.js";
5
+ import { findClassMember, memberTypeOf, nonNull, typesEqual, typeToString, unionMembers, UNKNOWN, } from "./types.js";
5
6
  export class TaggedUnionChecker extends ControlFlowChecker {
6
7
  /** The paths among the checked member expressions, with the types of their fields. */
7
8
  paths = new WeakMap();
@@ -103,6 +104,35 @@ export class TaggedUnionChecker extends ControlFlowChecker {
103
104
  const tags = owner && this.tagValues(unionMembers(nonNull(owner.type)), target.property.name);
104
105
  return owner && tags ? [[owner.place, narrowByTag(owner.type, tags, values, equal)]] : [];
105
106
  }
107
+ /**
108
+ * The type of a field in the objects of a union that no tag chooses from, when it is the same in
109
+ * each that has it: the literal takes it from the context, as `fetch: req => ...` of the options
110
+ * of `Bun.serve`. Functions only need the same parameters, which is what their literals take.
111
+ */
112
+ commonField(objects, name) {
113
+ const types = objects.flatMap((object) => {
114
+ const member = object.kind === 'object'
115
+ ? object.members.get(name)
116
+ : object.kind === 'class'
117
+ ? findClassMember(object.info, name, false)
118
+ : undefined;
119
+ return member ? [member.type] : [];
120
+ });
121
+ const [first] = types;
122
+ if (!first)
123
+ return null;
124
+ if (types.every((type) => typesEqual(type, first)))
125
+ return first;
126
+ const signature = callSignature(nonNull(first));
127
+ const sameParams = types.every((type) => {
128
+ const other = callSignature(nonNull(type));
129
+ return (signature &&
130
+ other &&
131
+ other.params.length === signature.params.length &&
132
+ other.params.every((param, i) => typesEqual(param, signature.params[i])));
133
+ });
134
+ return sameParams ? first : null;
135
+ }
106
136
  /**
107
137
  * `{ kind: "pickup", shop: "Main st." }` where one of several objects is expected: the member
108
138
  * that a tag of the literal chooses, or `null` when no tag chooses one. A tag of no member is
@@ -0,0 +1,6 @@
1
+ import type * as ast from '../ast.ts';
2
+ import { CallChecker } from './calls.ts';
3
+ import { type Type } from './types.ts';
4
+ export declare abstract class TagChecker extends CallChecker {
5
+ protected checkTaggedTemplate(node: ast.TaggedTemplateExpression): Type;
6
+ }
@@ -0,0 +1,44 @@
1
+ import { CallChecker, withDefaults } from "./calls.js";
2
+ import { callSignature, isUntyped, signaturesOf } from "./helpers.js";
3
+ import { ANY, containsTypeParam, inferTypeParams, nonNull, substitute, typeToString, UNKNOWN, VOID, } from "./types.js";
4
+ // Tagged templates, sql`select * from users where id = ${id}`: a call of the tag with the strings
5
+ // of the template, a TemplateStringsArray, and its values, which are checked as the arguments that
6
+ // follow the strings.
7
+ export class TagChecker extends CallChecker {
8
+ checkTaggedTemplate(node) {
9
+ const tag = this.checkValue(node.tag);
10
+ const values = node.quasi.expressions;
11
+ const signature = isUntyped(tag) ? null : callSignature(nonNull(tag));
12
+ if (!signature) {
13
+ if (!isUntyped(tag)) {
14
+ this.error(`${typeToString(tag)} cannot be a tag: a tag is a function, e.g. func sql(parts TemplateStringsArray, ...values []any)`, node.tag);
15
+ }
16
+ for (const value of values)
17
+ this.checkValue(value);
18
+ return isUntyped(tag) ? ANY : UNKNOWN;
19
+ }
20
+ // Of overloads, the one that takes the strings of a template, as `sql` of postgres has.
21
+ const chosen = signaturesOf(signature).find(takesTemplate) ?? signature;
22
+ const inferred = new Map();
23
+ values.forEach((value, i) => {
24
+ const param = chosen.params[i + 1] ?? chosen.rest;
25
+ if (!param) {
26
+ this.error(`too many values for the tag: it takes ${chosen.params.length - 1} after the strings`, value);
27
+ this.checkValue(value);
28
+ return;
29
+ }
30
+ const type = this.checkValue(value, containsTypeParam(param) ? null : param);
31
+ inferTypeParams(param, type, inferred);
32
+ this.expectAssignable(type, substitute(param, inferred), value, ` in value ${i + 1}`);
33
+ });
34
+ const [result] = chosen.results;
35
+ return result ? substitute(result, withDefaults(chosen.typeParams, inferred)) : VOID;
36
+ }
37
+ }
38
+ /** Whether the first parameter takes the strings of a template: TemplateStringsArray or []string. */
39
+ function takesTemplate(signature) {
40
+ const [first] = signature.params;
41
+ return (first !== undefined &&
42
+ ((first.kind === 'object' && first.name === 'TemplateStringsArray') ||
43
+ (first.kind === 'array' && first.element.kind === 'string')));
44
+ }
@@ -94,10 +94,15 @@ export function isNullable(type) {
94
94
  return type.kind === 'nullable' || type.kind === 'null';
95
95
  }
96
96
  export function func(params, results, options = {}) {
97
+ let required = options.required ?? params.length;
98
+ // A parameter that takes `void` may be left out at the end, as in TypeScript: the `resolve()`
99
+ // of a `Promise<void>`.
100
+ while (required > 0 && takesVoid(params[required - 1]))
101
+ required--;
97
102
  return {
98
103
  kind: 'function',
99
104
  params,
100
- required: options.required ?? params.length,
105
+ required,
101
106
  rest: options.rest ?? null,
102
107
  results,
103
108
  typeParams: options.typeParams ?? [],
@@ -127,3 +132,7 @@ export function createClass(name) {
127
132
  info.value = { kind: 'classValue', info };
128
133
  return info;
129
134
  }
135
+ /** `void`, or a union with it: `void | PromiseLike<void>`. */
136
+ function takesVoid(type) {
137
+ return type.kind === 'void' || (type.kind === 'union' && type.types.some(takesVoid));
138
+ }
@@ -2,8 +2,8 @@ import { instanceMembers } from "./classes.js";
2
2
  import { arrayOf, func, nonNull, nullable, union } from "./constructors.js";
3
3
  import { instantiate } from "./generics.js";
4
4
  import { iteratedBy } from "./iterables.js";
5
- import { memberNames } from "./lazy.js";
6
- import { VOID, } from "./model.js";
5
+ import { LazyMap, memberNames } from "./lazy.js";
6
+ import { VOID } from "./model.js";
7
7
  import { typesEqual } from "./relations.js";
8
8
  // Type parameters: substituting type arguments and inferring them from the arguments of a call.
9
9
  /** Whether a type mentions type parameters: any of them, or only those in `only`. */
@@ -61,7 +61,8 @@ export function substitute(type, bindings, fallback) {
61
61
  // A generic method of a generic interface keeps its own parameters: `map<U>` of `Box<T>`.
62
62
  // A default that uses the interface's parameters needs a parameter of its own:
63
63
  // `then<TResult1 = T>` of `Promise<Response>` has the default Response.
64
- const own = new Map(bindings);
64
+ // The same bindings, when no parameter needs a copy, so that substituteObject knows a cycle.
65
+ let own = bindings;
65
66
  const typeParams = type.typeParams
66
67
  .filter((param) => !bindings.has(param))
67
68
  .map((param) => {
@@ -69,6 +70,8 @@ export function substitute(type, bindings, fallback) {
69
70
  return param;
70
71
  }
71
72
  const fresh = freshParam(param, substitute(param.default, bindings));
73
+ if (own === bindings)
74
+ own = new Map(bindings);
72
75
  own.set(param, fresh);
73
76
  return fresh;
74
77
  });
@@ -122,11 +125,34 @@ function substituteObject(type, bindings, fallback) {
122
125
  // A named interface without parameters cannot use them; an object type literal might.
123
126
  if (type.name !== null || !containsTypeParam(type))
124
127
  return type;
125
- const members = new Map();
126
- for (const [name, member] of type.members)
127
- members.set(name, { ...member, type: sub(member.type) });
128
- return { ...type, members, call: type.call && sub(type.call) };
128
+ // Type literals of .d.ts files may refer to themselves, as the chains of Vitest do
129
+ // (`describe.concurrent.skip`): members are substituted when they are used, as in instantiate,
130
+ // and the same bindings give the same copy, so a member that comes back gets it.
131
+ let copies = fallback === undefined ? substituted.get(bindings) : undefined;
132
+ if (fallback === undefined && !copies) {
133
+ copies = new Map();
134
+ substituted.set(bindings, copies);
135
+ }
136
+ const copy = copies?.get(type);
137
+ if (copy)
138
+ return copy;
139
+ let call;
140
+ const result = {
141
+ ...type,
142
+ members: new LazyMap(() => memberNames(type.members), (name) => {
143
+ const member = type.members.get(name);
144
+ return member && { ...member, type: sub(member.type) };
145
+ }),
146
+ };
147
+ Object.defineProperty(result, 'call', {
148
+ enumerable: true,
149
+ get: () => (call === undefined ? (call = type.call && sub(type.call)) : call),
150
+ });
151
+ copies?.set(type, result);
152
+ return result;
129
153
  }
154
+ /** The copies of object type literals, by the bindings they were made with. */
155
+ const substituted = new WeakMap();
130
156
  const MAX_STRUCTURAL_DEPTH = 3;
131
157
  /** The names of the templates of channel types (checker/channel.ts). */
132
158
  const CHANNELS = new Set(['chan', 'chan<-', '<-chan']);
@@ -8,6 +8,8 @@ import { isTopic, topicElement } from "./topic.js";
8
8
  const MAX_OBJECT_DEPTH = 3;
9
9
  /** How many members of an object type without a name are printed: `globalThis` has hundreds. */
10
10
  const MAX_OBJECT_MEMBERS = 8;
11
+ /** How long an object type inside another one may be before it is printed as `{ ... }`. */
12
+ const MAX_NESTED_LENGTH = 80;
11
13
  let objectDepth = 0;
12
14
  /** `[]byte`: a Uint8Array, of the lib files or the built-in one. */
13
15
  export function isBytes(type) {
@@ -85,7 +87,9 @@ export function typeToString(type) {
85
87
  members.unshift(`[number]: ${typeToString(type.numberIndex)}`);
86
88
  if (type.index)
87
89
  members.unshift(`[string]: ${typeToString(type.index)}`);
88
- return members.length === 0 ? '{}' : `{ ${members.join('; ')} }`;
90
+ const text = members.length === 0 ? '{}' : `{ ${members.join('; ')} }`;
91
+ // A long type inside another one, as the chains of Vitest, would fill the screen.
92
+ return objectDepth > 1 && text.length > MAX_NESTED_LENGTH ? '{ ... }' : text;
89
93
  }
90
94
  finally {
91
95
  objectDepth--;
@@ -187,7 +187,8 @@ function missingMember(source, members, assuming) {
187
187
  for (const [name, member] of members) {
188
188
  const found = own.get(name);
189
189
  if (!found) {
190
- if (!member.method && isNullable(member.type))
190
+ // `unix?: never` of a `.d.ts` is a field that must be left out, as in the options of Bun.serve.
191
+ if (!member.method && (isNullable(member.type) || member.type.kind === 'void'))
191
192
  continue;
192
193
  return { name, found: null, expected: member.type };
193
194
  }
@@ -7,6 +7,8 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
7
7
  * given to it, after the local names it uses; the arguments are copied there. In the worker
8
8
  * module a literal runs in the same worker, with copies of those names as its first parameters.
9
9
  */
10
+ protected objectLiteral(node: ast.ObjectLiteral): string;
11
+ protected taggedTemplate(node: ast.TaggedTemplateExpression): string;
10
12
  protected go(node: ast.GoExpression): string;
11
13
  /** `yield v` for `yield(v)` in an iterator, `yield [k, v]` for `yield(k, v)`; else `null`. */
12
14
  private yielded;
@@ -5,7 +5,7 @@ import { isYieldCall } from "../iterators.js";
5
5
  import { TYPED_ARRAYS, TYPED_ELEMENTS } from "../typed-arrays.js";
6
6
  import { hasOptionalLink } from "./analysis.js";
7
7
  import { ExpressionEmitter } from "./expressions.js";
8
- import { ARROW, BINARY, POSTFIX } from "./syntax.js";
8
+ import { ARROW, BINARY, POSTFIX, PRIMARY } from "./syntax.js";
9
9
  // Calls, `new`, array literals and conversions. A `[]byte` is a Uint8Array: a literal where the
10
10
  // checker found one expected becomes `new Uint8Array([...])`, and the conversions of Go between
11
11
  // text and its UTF-8 bytes use TextEncoder and TextDecoder, `[]byte(s)` and `string(data)`. In an
@@ -43,6 +43,28 @@ export class CallEmitter extends ExpressionEmitter {
43
43
  * given to it, after the local names it uses; the arguments are copied there. In the worker
44
44
  * module a literal runs in the same worker, with copies of those names as its first parameters.
45
45
  */
46
+ objectLiteral(node) {
47
+ if (node.properties.length === 0)
48
+ return '{}';
49
+ const properties = node.properties.map((property) => {
50
+ if (property.kind === 'SpreadElement') {
51
+ return `...${this.expression(property.argument, ARROW)}`;
52
+ }
53
+ const key = property.key.kind === 'StringLiteral' ? property.key.raw : property.key.name;
54
+ // `fetch(req) { ... }`: a method of JS, whose `this` is the object.
55
+ if (property.method && property.value.kind === 'FuncExpression') {
56
+ const [params, body] = this.func(property.value.params, property.value.body);
57
+ return `${property.value.async ? 'async ' : ''}${key}(${params}) ${body}`;
58
+ }
59
+ const value = this.expression(property.value, ARROW);
60
+ // A renamed shorthand (`{ delete }` → `delete$`) must keep its key.
61
+ return property.shorthand && value === key ? key : `${key}: ${value}`;
62
+ });
63
+ return `{ ${properties.join(', ')} }`;
64
+ }
65
+ taggedTemplate(node) {
66
+ return `${this.expression(node.tag, POSTFIX)}${this.expression(node.quasi, PRIMARY)}`;
67
+ }
46
68
  go(node) {
47
69
  const { callee } = node.call;
48
70
  const literal = callee.kind === 'FuncExpression' ? callee : null;