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
@@ -65,15 +65,38 @@ export class DeclarationParser extends ParamParser {
65
65
  if (!this.context.topLevel) {
66
66
  this.error('"export" is only allowed at the top level of a module', start, this.span(start).end);
67
67
  }
68
+ if (this.check('{') || this.check('*'))
69
+ return this.parseExportFrom(start);
68
70
  return (this.parseDeclaration(start, true) ??
69
71
  this.fail(`expected a declaration after "export", found ${describe(this.peek())}`));
70
72
  }
73
+ /** `export { Row, Rows as Table } from "./rows.rk"` or `export * from "./rows.rk"`. */
74
+ parseExportFrom(start) {
75
+ let specifiers = null;
76
+ if (!this.accept('*')) {
77
+ this.expect('{');
78
+ specifiers = this.parseList('}', () => this.parseImportSpecifier());
79
+ }
80
+ if (!this.checkWord('from')) {
81
+ this.fail('a module exports its own names where they are declared, e.g. export func f(); export { ... } needs from "./module.rk"');
82
+ }
83
+ this.next();
84
+ const source = this.parseStringLiteral('module path');
85
+ const declaration = {
86
+ kind: 'ExportFromDeclaration',
87
+ specifiers,
88
+ source,
89
+ ...this.span(start),
90
+ };
91
+ this.endStatement();
92
+ return declaration;
93
+ }
71
94
  parseFuncDeclaration(start, exported) {
72
95
  const async = this.accept('async');
73
96
  this.expect('func');
74
97
  const name = this.parseIdentifier('function name');
75
98
  const typeParams = this.parseTypeParams();
76
- const params = this.parseParams(false);
99
+ const params = this.parseParams(false, { defaults: true, rest: 'typed' });
77
100
  const results = this.parseResults();
78
101
  const body = this.parseFunctionBody(async);
79
102
  return {
@@ -131,7 +154,7 @@ export class DeclarationParser extends ParamParser {
131
154
  if (!htmlTag && !/^[A-Z]/.test(name.name)) {
132
155
  this.error(`component names start with a capital letter, as in <${name.name.charAt(0).toUpperCase()}${name.name.slice(1)} />`, name.start, name.end);
133
156
  }
134
- const params = this.parseParams(false, true, true);
157
+ const params = this.parseParams(false, { defaults: true, rest: 'attrs' });
135
158
  const body = this.withContext({ ...FUNCTION_BODY, inComponent: true }, () => this.parseComponentBody());
136
159
  return {
137
160
  kind: 'ComponentDeclaration',
@@ -15,7 +15,7 @@ export class DecoratorParser extends DeclarationParser {
15
15
  this.error('decorators can only be declared at the top level of a module', keyword.start, keyword.end);
16
16
  }
17
17
  const name = this.parseIdentifier('decorator name');
18
- const params = this.parseParams(false, true);
18
+ const params = this.parseParams(false, { defaults: true });
19
19
  const needs = this.parseNeeds();
20
20
  const includes = this.parseIncludes();
21
21
  const body = this.withContext({ ...FUNCTION_BODY, inComponent: true }, () => this.parseDecoratorBody());
@@ -35,6 +35,12 @@ export class ExpressionParser extends TypeParser {
35
35
  const precedence = BINARY_PRECEDENCE[operator];
36
36
  if (precedence < minPrecedence)
37
37
  return left;
38
+ // `await load<number>(1)` reads `await load` first.
39
+ const callee = left.kind === 'UnaryExpression' && left.operator === 'await' ? left.argument : left;
40
+ if (operator === '<' &&
41
+ (callee.kind === 'Identifier' || callee.kind === 'MemberExpression')) {
42
+ this.rejectTypeArguments();
43
+ }
38
44
  this.next();
39
45
  // `**` is right-associative, all other binary operators are left-associative.
40
46
  const right = this.parseBinary(operator === '**' ? precedence : precedence + 1);
@@ -1,7 +1,7 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { type StringToken, type TokenKind } from '../lexer/token.ts';
3
- import { ExpressionParser } from './expressions.ts';
4
- export declare abstract class LiteralParser extends ExpressionParser {
3
+ import { ObjectParser } from './objects.ts';
4
+ export declare abstract class LiteralParser extends ObjectParser {
5
5
  /** Whether the `(` at the current position opens arrow function parameters: `(...) =>`. */
6
6
  protected isArrowFunctionAhead(): boolean;
7
7
  protected parseArrowFunction(async?: boolean): ast.ArrowFunction;
@@ -32,8 +32,6 @@ export declare abstract class LiteralParser extends ExpressionParser {
32
32
  protected parseStreamChan(start: number): ast.StreamChanExpression;
33
33
  /** `[]byte("text")`: a type, then its value in parentheses. */
34
34
  protected parseConversion(): ast.ConversionExpression;
35
- protected parseObjectLiteral(): ast.ObjectLiteral;
36
- protected parseProperty(): ast.Property | ast.SpreadElement;
37
35
  protected parseTemplate(first: StringToken): ast.TemplateLiteral;
38
36
  /** Parses `item, item, ...` up to `close`, which is consumed. A trailing comma is allowed. */
39
37
  protected parseList<T>(close: TokenKind, parseItem: () => T): T[];
@@ -1,8 +1,8 @@
1
1
  import { isKeyword } from "../lexer/token.js";
2
- import { ExpressionParser } from "./expressions.js";
2
+ import { ObjectParser } from "./objects.js";
3
3
  import { FUNCTION_BODY, ParseError, describe, templateElement } from "./syntax.js";
4
4
  // Functions, `new`, literals of arrays, objects and templates, names.
5
- export class LiteralParser extends ExpressionParser {
5
+ export class LiteralParser extends ObjectParser {
6
6
  /** Whether the `(` at the current position opens arrow function parameters: `(...) =>`. */
7
7
  isArrowFunctionAhead() {
8
8
  let depth = 0;
@@ -39,7 +39,7 @@ export class LiteralParser extends ExpressionParser {
39
39
  ];
40
40
  }
41
41
  else {
42
- params = this.parseParams(true);
42
+ params = this.parseParams(true, { defaults: true, rest: 'typed' });
43
43
  }
44
44
  this.expect('=>');
45
45
  const body = this.withContext({ ...FUNCTION_BODY, inAsync: async }, () => this.check('{') ? this.parseBlock() : this.parseExpression());
@@ -60,7 +60,7 @@ export class LiteralParser extends ExpressionParser {
60
60
  }
61
61
  parseFuncExpression(async = false) {
62
62
  const start = this.expect('func').start;
63
- const params = this.parseParams(false);
63
+ const params = this.parseParams(false, { defaults: true, rest: 'typed' });
64
64
  const results = this.parseResults();
65
65
  const body = this.parseFunctionBody(async);
66
66
  return {
@@ -86,6 +86,9 @@ export class LiteralParser extends ExpressionParser {
86
86
  ...this.span(callee.start),
87
87
  };
88
88
  }
89
+ // `new Promise<void>(...)`: the type arguments are inferred, as in calls.
90
+ if (this.check('<'))
91
+ this.rejectTypeArguments();
89
92
  const args = this.check('(') ? this.parseArguments() : [];
90
93
  return { kind: 'NewExpression', callee, arguments: args, ...this.span(start) };
91
94
  }
@@ -220,26 +223,6 @@ export class LiteralParser extends ExpressionParser {
220
223
  this.expect(')');
221
224
  return { kind: 'ConversionExpression', type, argument, ...this.span(start) };
222
225
  }
223
- parseObjectLiteral() {
224
- const start = this.expect('{').start;
225
- const properties = this.nested(() => this.parseList('}', () => this.parseProperty()));
226
- return { kind: 'ObjectLiteral', properties, ...this.span(start) };
227
- }
228
- parseProperty() {
229
- const token = this.peek();
230
- if (token.kind === '...')
231
- return this.parseElement();
232
- const key = token.kind === 'String' ? this.parseStringLiteral() : this.parsePropertyName();
233
- if (this.accept(':')) {
234
- const value = this.parseExpression();
235
- return { kind: 'Property', key, value, shorthand: false, ...this.span(token.start) };
236
- }
237
- // Shorthand `{ name }` means `{ name: name }`, so the key must be a plain name.
238
- if (key.kind !== 'Identifier' || token.kind !== 'Identifier') {
239
- this.fail(`expected ":" after property name, found ${describe(this.peek())}`);
240
- }
241
- return { kind: 'Property', key, value: { ...key }, shorthand: true, ...this.span(token.start) };
242
- }
243
226
  parseTemplate(first) {
244
227
  this.next();
245
228
  const quasis = [templateElement(first)];
@@ -248,6 +231,11 @@ export class LiteralParser extends ExpressionParser {
248
231
  while (true) {
249
232
  expressions.push(this.nested(() => this.parseExpression()));
250
233
  const part = this.peek();
234
+ if ((part.kind === '++' || part.kind === '--') &&
235
+ expressions.at(-1)?.kind === 'Identifier') {
236
+ const name = expressions.at(-1).name;
237
+ this.fail(`"${part.kind}" is a statement and cannot be used inside a template: use ${name} here and write ${name}${part.kind} on a line of its own`);
238
+ }
251
239
  if (part.kind !== 'TemplateMiddle' && part.kind !== 'TemplateTail') {
252
240
  this.fail(`expected "}" to close the template substitution, found ${describe(part)}`);
253
241
  }
@@ -0,0 +1,8 @@
1
+ import type * as ast from '../ast.ts';
2
+ import { PostfixParser } from './postfix.ts';
3
+ export declare abstract class ObjectParser extends PostfixParser {
4
+ protected parseObjectLiteral(): ast.ObjectLiteral;
5
+ protected parseProperty(): ast.Property | ast.SpreadElement;
6
+ /** `fetch(req Request) Response { ... }`: typed parameters, as those of a `func` literal. */
7
+ private parseObjectMethod;
8
+ }
@@ -0,0 +1,51 @@
1
+ import { PostfixParser } from "./postfix.js";
2
+ import { describe } from "./syntax.js";
3
+ // Object literals: `{ name: value }`, `{ name }`, `{ ...other }`, and methods written as in a
4
+ // class, `{ fetch(req Request) Response { ... } }`.
5
+ export class ObjectParser extends PostfixParser {
6
+ parseObjectLiteral() {
7
+ const start = this.expect('{').start;
8
+ const properties = this.nested(() => this.parseList('}', () => this.parseProperty()));
9
+ return { kind: 'ObjectLiteral', properties, ...this.span(start) };
10
+ }
11
+ parseProperty() {
12
+ const token = this.peek();
13
+ if (token.kind === '...') {
14
+ this.next();
15
+ const argument = this.parseExpression();
16
+ return { kind: 'SpreadElement', argument, ...this.span(token.start) };
17
+ }
18
+ // `async load() T { ... }`: `async` before the name of a method.
19
+ const async = token.kind === 'async' && this.peek(2).kind === '(';
20
+ if (async)
21
+ this.next();
22
+ const key = this.check('String') ? this.parseStringLiteral() : this.parsePropertyName();
23
+ if (this.check('('))
24
+ return this.parseObjectMethod(key, token.start, async);
25
+ if (this.accept(':')) {
26
+ const value = this.parseExpression();
27
+ return { kind: 'Property', key, value, shorthand: false, ...this.span(token.start) };
28
+ }
29
+ // Shorthand `{ name }` means `{ name: name }`, so the key must be a plain name.
30
+ if (key.kind !== 'Identifier' || token.kind !== 'Identifier') {
31
+ this.fail(`expected ":" after property name, found ${describe(this.peek())}`);
32
+ }
33
+ return { kind: 'Property', key, value: { ...key }, shorthand: true, ...this.span(token.start) };
34
+ }
35
+ /** `fetch(req Request) Response { ... }`: typed parameters, as those of a `func` literal. */
36
+ parseObjectMethod(key, start, async) {
37
+ const funcStart = this.peek().start;
38
+ const params = this.parseParams(false, { defaults: true, rest: 'typed' });
39
+ const results = this.parseResults();
40
+ const body = this.parseFunctionBody(async);
41
+ const value = {
42
+ kind: 'FuncExpression',
43
+ ...(async ? { async } : {}),
44
+ params,
45
+ results,
46
+ body,
47
+ ...this.span(funcStart),
48
+ };
49
+ return { kind: 'Property', key, value, shorthand: false, method: true, ...this.span(start) };
50
+ }
51
+ }
@@ -4,9 +4,14 @@ export declare abstract class ParamParser extends JumpParser {
4
4
  /**
5
5
  * `(a, b number, c string)`. Names without a type take the type of the next typed name, as in
6
6
  * Go; only arrow functions may leave types out entirely. The parameters of a constructor can be
7
- * properties: `(private name string)`.
7
+ * properties: `(private name string)`. Functions take `...parts []any` last, and components
8
+ * `...attrs`, the attributes of an element; both may have default values, `times = 1`.
8
9
  */
9
- protected parseParams(typesOptional: boolean, allowDefaults?: boolean, allowRest?: boolean, allowProperties?: boolean): ast.Parameter[];
10
+ protected parseParams(typesOptional: boolean, options?: {
11
+ defaults?: boolean;
12
+ rest?: 'typed' | 'attrs';
13
+ properties?: boolean;
14
+ }): ast.Parameter[];
10
15
  /** `private`, `readonly` or both before a parameter that is a property of its class. */
11
16
  private parsePropertyModifiers;
12
17
  /**
@@ -14,8 +19,36 @@ export declare abstract class ParamParser extends JumpParser {
14
19
  * not by `,` or `)`, so a parameter named bind, `(bind Mode)`, stays one.
15
20
  */
16
21
  private parseBindModifier;
22
+ /** `...parts []any`: the arguments after the others, in an array, as in JS. */
23
+ private parseTypedRest;
17
24
  /** `...attrs`: the attributes that a component passes on to one of its elements. */
18
25
  private parseRestParam;
19
- /** Result types after a parameter list: nothing, `T`, or `(T1, T2)`. */
20
- protected parseResults(): ast.TypeNode[];
26
+ /**
27
+ * Result types after a parameter list: nothing, `T`, or `(T1, T2)`. A `{` may start the body
28
+ * instead: it starts an object type when the body follows that type,
29
+ * `func names() { [string]: string } { ... }`, or a value does, `f func() { [string]: T } = g`.
30
+ * A method of an interface has no body, so there `{` is always a type.
31
+ */
32
+ protected parseResults(method?: boolean): ast.TypeNode[];
33
+ /** Whether the `{ ... }` here is followed by a body, `{`, or by a value, `=`. */
34
+ private objectTypeBeforeBody;
35
+ /**
36
+ * The type of a parameter of a function type, after its name if it has one, kept in `names`;
37
+ * `...` before the rest parameter is kept there too, `...parts`.
38
+ */
39
+ protected parseParamType(names: string[]): ast.TypeNode;
40
+ /** `func(string, ...[]any)`: the last parameter, taken out of `params`, if it is the rest. */
41
+ protected restOf(params: ast.TypeNode[], names: string[]): ast.TypeNode | null;
42
+ /**
43
+ * `func(a, b number)`: as in Go, if a parameter has a name, all have, so `a` alone is a name that
44
+ * shares the type of the next one. Without names, `func(A, B)` is a list of types.
45
+ */
46
+ protected groupParams(params: ast.TypeNode[], names: string[]): ast.TypeNode[];
47
+ /** `paths []string` in `(paths []string) => Shape`: a name before the type of a parameter. */
48
+ private startsNamedParam;
49
+ /**
50
+ * `load<number>(1)` would be two comparisons, `(load < number) > (1)`: a call takes no type
51
+ * arguments, which are inferred, so the likely mistake is reported as one.
52
+ */
53
+ protected rejectTypeArguments(): void;
21
54
  }
@@ -6,15 +6,20 @@ export class ParamParser extends JumpParser {
6
6
  /**
7
7
  * `(a, b number, c string)`. Names without a type take the type of the next typed name, as in
8
8
  * Go; only arrow functions may leave types out entirely. The parameters of a constructor can be
9
- * properties: `(private name string)`.
9
+ * properties: `(private name string)`. Functions take `...parts []any` last, and components
10
+ * `...attrs`, the attributes of an element; both may have default values, `times = 1`.
10
11
  */
11
- parseParams(typesOptional, allowDefaults = false, allowRest = false, allowProperties = false) {
12
+ parseParams(typesOptional, options = {}) {
13
+ const { defaults: allowDefaults = false, properties: allowProperties = false } = options;
14
+ const allowRest = options.rest === 'attrs';
12
15
  this.expect('(');
13
16
  const params = [];
14
17
  let untyped = 0;
15
18
  while (!this.check(')')) {
16
19
  if (this.check('...')) {
17
- params.push(this.parseRestParam(allowRest));
20
+ params.push(options.rest === 'typed'
21
+ ? this.parseTypedRest(typesOptional)
22
+ : this.parseRestParam(allowRest));
18
23
  break;
19
24
  }
20
25
  const paramStart = this.peek().start;
@@ -35,7 +40,11 @@ export class ParamParser extends JumpParser {
35
40
  ...(bind ? { bind: true } : {}),
36
41
  ...this.span(paramStart),
37
42
  });
38
- if (type === null) {
43
+ // `times = 1` has the type of its value.
44
+ if (type === null && defaultValue !== null) {
45
+ untyped = 0;
46
+ }
47
+ else if (type === null) {
39
48
  untyped++;
40
49
  }
41
50
  else {
@@ -100,12 +109,30 @@ export class ParamParser extends JumpParser {
100
109
  }
101
110
  return true;
102
111
  }
112
+ /** `...parts []any`: the arguments after the others, in an array, as in JS. */
113
+ parseTypedRest(typesOptional) {
114
+ const start = this.next().start;
115
+ const name = this.parseIdentifier('parameter name');
116
+ const type = this.canStartType() ? this.parseType() : null;
117
+ if (type === null && !typesOptional) {
118
+ this.error(`...${name.name} needs the type of its array, e.g. ...${name.name} []any`, start, name.end);
119
+ }
120
+ // After the error, the rest of the list is skipped up to the `)`.
121
+ if (this.check(',')) {
122
+ this.error(`...${name.name} is the last parameter`, start, name.end);
123
+ while (!this.check(')') && !this.check('EOF'))
124
+ this.next();
125
+ }
126
+ if (this.check('='))
127
+ this.error(`...${name.name} has no default value: it is [] without arguments`, start, name.end);
128
+ return { kind: 'Parameter', name, type, defaultValue: null, rest: true, ...this.span(start) };
129
+ }
103
130
  /** `...attrs`: the attributes that a component passes on to one of its elements. */
104
131
  parseRestParam(allowed) {
105
132
  const start = this.next().start;
106
133
  const name = this.parseIdentifier('parameter name');
107
134
  if (!allowed) {
108
- this.error(`...${name.name} is only for components: it takes the attributes of an element`, start, name.end);
135
+ this.error(`...${name.name} needs the type of its array, e.g. ...${name.name} []any`, start, name.end);
109
136
  }
110
137
  if (this.canStartType() || this.check('=')) {
111
138
  const at = this.peek().start;
@@ -131,13 +158,120 @@ export class ParamParser extends JumpParser {
131
158
  ...this.span(start),
132
159
  };
133
160
  }
134
- /** Result types after a parameter list: nothing, `T`, or `(T1, T2)`. */
135
- parseResults() {
161
+ /**
162
+ * Result types after a parameter list: nothing, `T`, or `(T1, T2)`. A `{` may start the body
163
+ * instead: it starts an object type when the body follows that type,
164
+ * `func names() { [string]: string } { ... }`, or a value does, `f func() { [string]: T } = g`.
165
+ * A method of an interface has no body, so there `{` is always a type.
166
+ */
167
+ parseResults(method = false) {
136
168
  if (this.accept('('))
137
169
  return this.parseList(')', () => this.parseType());
138
- // `{` after the parameters starts the body, so an object result type needs parentheses.
139
- if (this.canStartType() && !this.check('{'))
170
+ if (this.check('{') && !method && !this.objectTypeBeforeBody())
171
+ return [];
172
+ if (this.canStartType())
140
173
  return [this.parseType()];
141
174
  return [];
142
175
  }
176
+ /** Whether the `{ ... }` here is followed by a body, `{`, or by a value, `=`. */
177
+ objectTypeBeforeBody() {
178
+ let depth = 0;
179
+ for (let offset = 0;; offset++) {
180
+ const { kind } = this.peek(offset);
181
+ if (kind === 'EOF')
182
+ return false;
183
+ if (kind === '{')
184
+ depth++;
185
+ else if (kind === '}' && --depth === 0) {
186
+ const next = this.peek(offset + 1).kind;
187
+ return next === '{' || next === '=';
188
+ }
189
+ }
190
+ }
191
+ /**
192
+ * The type of a parameter of a function type, after its name if it has one, kept in `names`;
193
+ * `...` before the rest parameter is kept there too, `...parts`.
194
+ */
195
+ parseParamType(names) {
196
+ const rest = this.accept('...') ? '...' : '';
197
+ names.push(rest + (this.check('Identifier') && this.startsNamedParam() ? this.next().text : ''));
198
+ return this.parseType();
199
+ }
200
+ /** `func(string, ...[]any)`: the last parameter, taken out of `params`, if it is the rest. */
201
+ restOf(params, names) {
202
+ const at = names.findIndex((name) => name.startsWith('...'));
203
+ if (at === -1)
204
+ return null;
205
+ if (at !== params.length - 1) {
206
+ this.error('the rest parameter is the last one, e.g. func(string, ...[]any)', params[at].start, params[at].end);
207
+ }
208
+ names.splice(at, 1);
209
+ return params.splice(at, 1)[0];
210
+ }
211
+ /**
212
+ * `func(a, b number)`: as in Go, if a parameter has a name, all have, so `a` alone is a name that
213
+ * shares the type of the next one. Without names, `func(A, B)` is a list of types.
214
+ */
215
+ groupParams(params, names) {
216
+ // `...` alone, an unnamed rest parameter, gives no name.
217
+ const named = (name) => name.replace(/^\.\.\./, '') !== '';
218
+ if (!names.some(named))
219
+ return params;
220
+ return params.map((param, i) => {
221
+ if (names[i] !== '' || param.kind !== 'TypeReference' || param.typeArgs.length > 0) {
222
+ return param;
223
+ }
224
+ const shared = names.findIndex((name, j) => j > i && named(name));
225
+ if (shared === -1) {
226
+ this.error(`parameter "${param.name.name}" has no type: write it after the name, e.g. func(a, b number)`, param.start, param.end);
227
+ return param;
228
+ }
229
+ names[i] = param.name.name;
230
+ return params[shared];
231
+ });
232
+ }
233
+ /** `paths []string` in `(paths []string) => Shape`: a name before the type of a parameter. */
234
+ startsNamedParam() {
235
+ const next = this.peek(1);
236
+ switch (next.kind) {
237
+ case 'Identifier':
238
+ case '?':
239
+ case '{':
240
+ case '(':
241
+ case 'func':
242
+ case 'chan':
243
+ case '<-':
244
+ case 'String':
245
+ case 'Number':
246
+ return true;
247
+ // `x []number` is named.
248
+ case '[':
249
+ return this.peek(2).kind === ']';
250
+ default:
251
+ return false;
252
+ }
253
+ }
254
+ /**
255
+ * `load<number>(1)` would be two comparisons, `(load < number) > (1)`: a call takes no type
256
+ * arguments, which are inferred, so the likely mistake is reported as one.
257
+ */
258
+ rejectTypeArguments() {
259
+ const TYPE_TOKENS = new Set(['Identifier', ',', '[', ']', '?', '.', '|', 'chan']);
260
+ let depth = 0;
261
+ for (let offset = 0;; offset++) {
262
+ const { kind } = this.peek(offset);
263
+ if (kind === '<')
264
+ depth++;
265
+ else if (kind === '>')
266
+ depth--;
267
+ else if (!TYPE_TOKENS.has(kind))
268
+ return;
269
+ if (depth === 0) {
270
+ if (this.peek(offset + 1).kind !== '(')
271
+ return;
272
+ break;
273
+ }
274
+ }
275
+ this.fail('a call takes no type arguments: they are inferred from the arguments and the expected type; give the variable a type instead, e.g. const done Promise<void> = new Promise(...)');
276
+ }
143
277
  }
@@ -0,0 +1,5 @@
1
+ import type * as ast from '../ast.ts';
2
+ import { ExpressionParser } from './expressions.ts';
3
+ export declare abstract class PostfixParser extends ExpressionParser {
4
+ protected parsePostfix(expression: ast.Expression): ast.Expression;
5
+ }
@@ -0,0 +1,36 @@
1
+ import { ExpressionParser } from "./expressions.js";
2
+ // What may follow a value besides members, indexes and calls: `user!`, which takes it not to be
3
+ // null, and a template right after it, sql`select ... ${id}`, which calls it with its parts.
4
+ export class PostfixParser extends ExpressionParser {
5
+ parsePostfix(expression) {
6
+ let result = super.parsePostfix(expression);
7
+ while (true) {
8
+ const token = this.peek();
9
+ // `user!.name`: the `!` right after the value, so that `!` before a value stays a negation.
10
+ if (token.kind === '!' && token.start === result.end) {
11
+ this.next();
12
+ result = {
13
+ kind: 'NonNullExpression',
14
+ expression: result,
15
+ start: result.start,
16
+ end: token.end,
17
+ };
18
+ }
19
+ else if ((token.kind === 'Template' || token.kind === 'TemplateHead') &&
20
+ !token.newlineBefore) {
21
+ const quasi = this.parseTemplate(token);
22
+ result = {
23
+ kind: 'TaggedTemplateExpression',
24
+ tag: result,
25
+ quasi,
26
+ start: result.start,
27
+ end: quasi.end,
28
+ };
29
+ }
30
+ else {
31
+ return result;
32
+ }
33
+ result = super.parsePostfix(result);
34
+ }
35
+ }
36
+ }
@@ -19,5 +19,7 @@ export declare abstract class SimpleStatementParser extends ParserBase {
19
19
  /** `ref input` is one name, and the markup gives its element. */
20
20
  private checkRef;
21
21
  private parseDerivedBlock;
22
+ /** `const { name, size: bytes, ...rest } = file`: fields of an object, one level deep. */
23
+ private parseDestructuring;
22
24
  protected parseVariableDeclaration(start: number, exported: boolean): ast.VariableDeclaration;
23
25
  }
@@ -96,6 +96,10 @@ export class SimpleStatementParser extends ParserBase {
96
96
  // `data, err = try JSON.parse(text)`: the value and the error, as from a call.
97
97
  if (values.length === 1 && value?.kind === 'TryExpression')
98
98
  return;
99
+ if (values.length === 1 && value?.kind === 'ConditionalExpression') {
100
+ this.error('a conditional expression gives one value, not the results of a call: unpack them in the branches of if/else, e.g. if ok { a, err = two() } else { a, err = other() }', start, this.span(start).end);
101
+ return;
102
+ }
99
103
  this.error(`assignment mismatch: ${plural(targets, 'variable')} but ${plural(values.length, 'value')}`, start, this.span(start).end);
100
104
  }
101
105
  /** `delete scores[name]`: removes a field, so the target is one. */
@@ -135,6 +139,49 @@ export class SimpleStatementParser extends ParserBase {
135
139
  end: body.end,
136
140
  };
137
141
  }
142
+ /** `const { name, size: bytes, ...rest } = file`: fields of an object, one level deep. */
143
+ parseDestructuring(start, exported, keyword) {
144
+ const names = [];
145
+ const keys = [];
146
+ let rest = false;
147
+ this.expect('{');
148
+ this.parseList('}', () => {
149
+ if (rest)
150
+ this.fail('...rest is the last name: it gets the other fields');
151
+ if (this.accept('...')) {
152
+ rest = true;
153
+ names.push(this.parseIdentifier('variable name'));
154
+ return;
155
+ }
156
+ const token = this.peek();
157
+ const key = this.check('String') ? this.parseStringLiteral() : this.parsePropertyName();
158
+ if (this.accept(':')) {
159
+ names.push(this.parseIdentifier('variable name'));
160
+ }
161
+ else if (key.kind === 'Identifier' && token.kind === 'Identifier') {
162
+ names.push({ ...key });
163
+ }
164
+ else {
165
+ const shown = token.kind === 'String' ? token.text : `"${token.text}"`;
166
+ this.fail(`${shown} needs a name to take its value, e.g. { ${token.text}: name }`);
167
+ }
168
+ keys.push(key);
169
+ });
170
+ this.expect('=', '"=" and the object: a pattern takes its fields from a value');
171
+ const value = this.parseExpression();
172
+ return {
173
+ kind: 'VariableDeclaration',
174
+ exported,
175
+ keyword,
176
+ isPublic: false,
177
+ names,
178
+ type: null,
179
+ values: [value],
180
+ body: null,
181
+ pattern: { keys, rest },
182
+ ...this.span(start),
183
+ };
184
+ }
138
185
  parseVariableDeclaration(start, exported) {
139
186
  const token = this.next();
140
187
  const keyword = token.kind === 'const'
@@ -142,6 +189,9 @@ export class SimpleStatementParser extends ParserBase {
142
189
  : token.text === 'state' || token.text === 'derived' || token.text === 'ref'
143
190
  ? token.text
144
191
  : 'let';
192
+ if (this.check('{') && (keyword === 'const' || keyword === 'let')) {
193
+ return this.parseDestructuring(start, exported, keyword);
194
+ }
145
195
  const names = [this.parseIdentifier('variable name')];
146
196
  while (this.accept(','))
147
197
  names.push(this.parseIdentifier('variable name'));
@@ -158,14 +158,15 @@ export class StatementParser extends SimpleStatementParser {
158
158
  return this.parseDecorated(start, exported);
159
159
  case 'interface':
160
160
  return this.parseInterface(start, exported);
161
- case 'type': {
162
- const declaration = this.parseTypeAlias(start, exported);
163
- this.endStatement();
164
- return declaration;
165
- }
166
161
  default: {
167
162
  if (this.isDecoratorDeclaration())
168
163
  return this.parseDecoratorDeclaration(start, exported);
164
+ // `type` is a keyword only before the name of a type: `const type = blob.type` is a name.
165
+ if (this.checkWord('type') && this.peek(1).kind === 'Identifier') {
166
+ const declaration = this.parseTypeAlias(start, exported);
167
+ this.endStatement();
168
+ return declaration;
169
+ }
169
170
  // `data` and `abstract` are keywords only before `class`: elsewhere they are names.
170
171
  let words = this.checkWord('abstract') ? 1 : 0;
171
172
  if (this.peek(words).text === 'data')
@@ -178,8 +179,9 @@ export class StatementParser extends SimpleStatementParser {
178
179
  }
179
180
  /** A statement ends with a newline or `;`, or right before the `}` that closes its block. */
180
181
  endStatement() {
181
- if (this.accept(';') || this.check('}') || this.check('EOF'))
182
+ if (this.accept(';') || this.check('}') || this.check('EOF') || this.afterTypeArguments()) {
182
183
  return;
184
+ }
183
185
  const token = this.peek();
184
186
  if (token.kind === '++' || token.kind === '--') {
185
187
  this.fail(`"${token.kind}" is a statement and cannot be used inside an expression`);
@@ -83,7 +83,6 @@ export const STATEMENT_KEYWORDS = new Set([
83
83
  'const',
84
84
  'class',
85
85
  'interface',
86
- 'type',
87
86
  'return',
88
87
  'if',
89
88
  'for',
@@ -13,10 +13,6 @@ export declare abstract class TypeParser extends ClassParser {
13
13
  * names, which only document them.
14
14
  */
15
15
  protected parseParenthesizedType(): ast.TypeNode;
16
- /** The type of a parameter of a function type, after its name if it has one, kept in `names`. */
17
- private parseParamType;
18
- /** `paths []string` in `(paths []string) => Shape`: a name before the type of a parameter. */
19
- private startsNamedParam;
20
16
  /** `{ name string; x, y number; area() number }`, separated by newlines, `;` or `,`. */
21
17
  protected parseTypeMembers(): ast.TypeMember[];
22
18
  protected parseTypeMember(): ast.TypeMember[];