rooklang 0.3.4 → 0.4.1

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 (135) hide show
  1. package/README.md +33 -2
  2. package/README.ru.md +33 -2
  3. package/dist/ast/declarations.d.ts +19 -3
  4. package/dist/ast/expressions.d.ts +4 -8
  5. package/dist/ast/statements.d.ts +3 -1
  6. package/dist/bun-register.d.ts +1 -0
  7. package/dist/bun-register.js +9 -0
  8. package/dist/bun.d.ts +34 -0
  9. package/dist/bun.js +99 -0
  10. package/dist/checker/assignments.d.ts +2 -0
  11. package/dist/checker/assignments.js +9 -3
  12. package/dist/checker/base.d.ts +8 -2
  13. package/dist/checker/base.js +3 -3
  14. package/dist/checker/builtins.js +7 -3
  15. package/dist/checker/channels.js +1 -2
  16. package/dist/checker/classes.js +7 -1
  17. package/dist/checker/closing.js +1 -1
  18. package/dist/checker/completion.d.ts +1 -1
  19. package/dist/checker/completion.js +2 -1
  20. package/dist/checker/component-bindings.d.ts +7 -0
  21. package/dist/checker/component-bindings.js +45 -0
  22. package/dist/checker/component-values.js +10 -7
  23. package/dist/checker/components.d.ts +2 -2
  24. package/dist/checker/components.js +8 -3
  25. package/dist/checker/context.d.ts +28 -5
  26. package/dist/checker/context.js +23 -0
  27. package/dist/checker/control-flow.d.ts +6 -5
  28. package/dist/checker/control-flow.js +48 -40
  29. package/dist/checker/declarations.d.ts +2 -2
  30. package/dist/checker/declarations.js +3 -2
  31. package/dist/checker/decorator-exports.js +4 -0
  32. package/dist/checker/decorator-includes.d.ts +9 -0
  33. package/dist/checker/decorator-includes.js +71 -0
  34. package/dist/checker/decorator-members.d.ts +8 -10
  35. package/dist/checker/decorator-members.js +28 -31
  36. package/dist/checker/decorator-wrappers.d.ts +1 -1
  37. package/dist/checker/decorator-wrappers.js +3 -2
  38. package/dist/checker/decorators.d.ts +14 -5
  39. package/dist/checker/decorators.js +52 -25
  40. package/dist/checker/expressions.d.ts +1 -1
  41. package/dist/checker/expressions.js +7 -6
  42. package/dist/checker/flow.d.ts +25 -3
  43. package/dist/checker/flow.js +48 -8
  44. package/dist/checker/functions.d.ts +2 -2
  45. package/dist/checker/functions.js +9 -5
  46. package/dist/checker/helpers.d.ts +4 -1
  47. package/dist/checker/helpers.js +7 -2
  48. package/dist/checker/imports.js +5 -2
  49. package/dist/checker/loops.d.ts +2 -0
  50. package/dist/checker/loops.js +27 -2
  51. package/dist/checker/markup.js +14 -2
  52. package/dist/checker/members.js +3 -1
  53. package/dist/checker/shared-state.d.ts +11 -0
  54. package/dist/checker/shared-state.js +37 -0
  55. package/dist/checker/statements.d.ts +5 -2
  56. package/dist/checker/statements.js +51 -22
  57. package/dist/checker/tagged-unions.d.ts +37 -0
  58. package/dist/checker/tagged-unions.js +132 -0
  59. package/dist/checker/untyped-globals.d.ts +1 -0
  60. package/dist/checker/untyped-globals.js +3 -1
  61. package/dist/codegen/attributes.d.ts +4 -0
  62. package/dist/codegen/attributes.js +24 -2
  63. package/dist/codegen/component-helpers.d.ts +6 -1
  64. package/dist/codegen/component-helpers.js +6 -1
  65. package/dist/codegen/component-props.d.ts +11 -2
  66. package/dist/codegen/component-props.js +50 -5
  67. package/dist/codegen/components.d.ts +6 -5
  68. package/dist/codegen/components.js +44 -21
  69. package/dist/codegen/control-flow.d.ts +2 -0
  70. package/dist/codegen/control-flow.js +23 -9
  71. package/dist/codegen/creation.js +5 -2
  72. package/dist/codegen/early-returns.d.ts +4 -3
  73. package/dist/codegen/early-returns.js +17 -5
  74. package/dist/codegen/emitter.d.ts +3 -0
  75. package/dist/codegen/emitter.js +18 -4
  76. package/dist/codegen/expressions.js +0 -2
  77. package/dist/codegen/functions.d.ts +11 -1
  78. package/dist/codegen/functions.js +31 -3
  79. package/dist/codegen/live.d.ts +60 -0
  80. package/dist/codegen/live.js +29 -0
  81. package/dist/codegen/mutations.d.ts +7 -0
  82. package/dist/codegen/mutations.js +43 -2
  83. package/dist/codegen/options.d.ts +2 -0
  84. package/dist/codegen/reactive.d.ts +4 -59
  85. package/dist/codegen/reactive.js +34 -72
  86. package/dist/codegen/runtime.js +24 -7
  87. package/dist/codegen/shared.d.ts +22 -0
  88. package/dist/codegen/shared.js +93 -0
  89. package/dist/codegen/simple-statements.d.ts +1 -1
  90. package/dist/codegen/statements.d.ts +5 -0
  91. package/dist/codegen/statements.js +35 -0
  92. package/dist/codegen/updates.d.ts +11 -3
  93. package/dist/codegen/updates.js +52 -4
  94. package/dist/codegen/web-components.d.ts +2 -2
  95. package/dist/codegen/web-components.js +2 -2
  96. package/dist/compile-file.d.ts +4 -1
  97. package/dist/compile.d.ts +1 -1
  98. package/dist/compile.js +6 -2
  99. package/dist/decorator-copy.d.ts +1 -1
  100. package/dist/decorator-copy.js +1 -0
  101. package/dist/decorator-names.d.ts +14 -0
  102. package/dist/decorator-names.js +56 -0
  103. package/dist/decorators.d.ts +2 -3
  104. package/dist/decorators.js +36 -14
  105. package/dist/index.d.ts +1 -1
  106. package/dist/inline-packages.d.ts +2 -1
  107. package/dist/inline-packages.js +9 -4
  108. package/dist/loader.js +6 -19
  109. package/dist/names.d.ts +4 -0
  110. package/dist/names.js +29 -0
  111. package/dist/parser/base.d.ts +1 -0
  112. package/dist/parser/base.js +1 -1
  113. package/dist/parser/control-flow.d.ts +5 -0
  114. package/dist/parser/control-flow.js +28 -2
  115. package/dist/parser/declarations.d.ts +5 -15
  116. package/dist/parser/declarations.js +15 -119
  117. package/dist/parser/decorators.d.ts +6 -2
  118. package/dist/parser/decorators.js +43 -19
  119. package/dist/parser/params.d.ts +21 -0
  120. package/dist/parser/params.js +143 -0
  121. package/dist/parser/simple-statements.d.ts +1 -0
  122. package/dist/parser/simple-statements.js +22 -6
  123. package/dist/parser/statements.d.ts +5 -2
  124. package/dist/parser/statements.js +18 -11
  125. package/dist/run-cache.d.ts +7 -0
  126. package/dist/run-cache.js +24 -0
  127. package/dist/run-file.js +6 -1
  128. package/dist/runtime/sources.js +4 -3
  129. package/dist/walk.js +3 -3
  130. package/package.json +10 -2
  131. package/src/runtime/decode.js +41 -10
  132. package/src/runtime/go.js +10 -4
  133. package/src/runtime/index.js +1 -0
  134. package/src/runtime/markup.js +27 -17
  135. package/src/runtime/shared.js +17 -0
@@ -1,4 +1,3 @@
1
- import { assignedNames } from "../names.js";
2
1
  import { BytesChecker } from "./bytes.js";
3
2
  import { chanDirection, elementOf, isChan } from "./channel.js";
4
3
  import { asyncIterated, isMap, loopEntry } from "./iterables.js";
@@ -78,13 +77,22 @@ export class LoopChecker extends BytesChecker {
78
77
  if (node.key)
79
78
  this.declareValue(node.key, 'loop', key);
80
79
  this.declareValue(node.value, 'loop', value);
81
- this.dropNarrowing(assignedNames(node.body));
80
+ if (node.withKey)
81
+ this.checkListKey(node.withKey);
82
+ this.dropNarrowing(node.body);
82
83
  const entry = this.flow;
83
84
  this.flow = new Map(entry);
84
85
  this.checkBlock(node.body.body);
85
86
  this.flow = entry;
86
87
  });
87
88
  }
89
+ /** `with key todo.id`: a string or a number, by which the blocks of the list are found again. */
90
+ checkListKey(key) {
91
+ const type = this.checkValue(key);
92
+ if (!isListKey(type)) {
93
+ this.error(`the key of a list must be a string or a number, not ${typeToString(type)}: e.g. "with key todo.id"`, key);
94
+ }
95
+ }
88
96
  /**
89
97
  * The key and the value of a loop over an iterator: what its `yield` takes. Of two, one variable
90
98
  * gets the second, as with a Map; of one, there is no key.
@@ -100,3 +108,20 @@ export class LoopChecker extends BytesChecker {
100
108
  return [UNKNOWN, taken[0] ?? UNKNOWN];
101
109
  }
102
110
  }
111
+ /** A key of a list: a string, a number, a literal of one or a union of them. */
112
+ function isListKey(type) {
113
+ switch (type.kind) {
114
+ case 'string':
115
+ case 'number':
116
+ case 'bigint':
117
+ case 'any':
118
+ case 'unknown':
119
+ return true;
120
+ case 'literal':
121
+ return typeof type.value !== 'boolean';
122
+ case 'union':
123
+ return type.types.every(isListKey);
124
+ default:
125
+ return false;
126
+ }
127
+ }
@@ -4,8 +4,10 @@ import { domProperty, elementType } from "./dom.js";
4
4
  import { svgElementType } from "./dom-svg.js";
5
5
  import { isContent, isAttributeValue, isUntyped } from "./helpers.js";
6
6
  import { OperatorChecker } from "./operators.js";
7
- import { BOOL, func, STRING, typeToString } from "./types.js";
7
+ import { arrayOf, BOOL, func, nullable, STRING, typeToString } from "./types.js";
8
8
  // Markup: elements, their children and attributes, `bind:` and event handlers.
9
+ /** The type of a list of classes, `class={[styles.tab, active ? styles.active : null]}`. */
10
+ const CLASS_LIST = arrayOf(nullable(STRING));
9
11
  export class MarkupChecker extends OperatorChecker {
10
12
  /** Whether the markup being checked is inside `<svg>`: its elements are SVG then. */
11
13
  inSvg = false;
@@ -98,7 +100,17 @@ export class MarkupChecker extends OperatorChecker {
98
100
  }
99
101
  if (value?.kind === 'EventHandler')
100
102
  return;
101
- const valueType = value === null ? BOOL : value.kind === 'StringLiteral' ? STRING : this.checkValue(value);
103
+ const list = name === 'class' && value?.kind === 'ArrayLiteral' ? CLASS_LIST : null;
104
+ const valueType = value === null
105
+ ? BOOL
106
+ : value.kind === 'StringLiteral'
107
+ ? STRING
108
+ : this.checkValue(value, list);
109
+ // `class={[styles.tab, active ? styles.active : null]}`: the classes that are not null.
110
+ if (name === 'class' && valueType.kind === 'array') {
111
+ this.expectAssignable(valueType, CLASS_LIST, value, ' for attribute "class"');
112
+ return;
113
+ }
102
114
  if (name === 'style') {
103
115
  if (!isUntyped(valueType) && valueType.kind !== 'string' && valueType.kind !== 'object') {
104
116
  this.error(`style must be a string or an object, not ${typeToString(valueType)}`, attribute);
@@ -16,6 +16,7 @@ export class MemberChecker extends DecoratorMemberChecker {
16
16
  return [this.checkSuperCall(node), false];
17
17
  let [callee, shortCircuits] = this.chainPart(node.callee);
18
18
  this.checkDecoratorMutation(node);
19
+ this.checkSharedMutation(node);
19
20
  if (isNullable(callee)) {
20
21
  if (node.optional)
21
22
  shortCircuits = true;
@@ -46,7 +47,8 @@ export class MemberChecker extends DecoratorMemberChecker {
46
47
  else if (member !== 'any' && member?.abstract && node.object.kind === 'SuperExpression') {
47
48
  this.error(`super.${name} is abstract in ${member.owner?.name ?? 'the base class'}: it has no code to run`, node.property);
48
49
  }
49
- return [member === 'any' ? ANY : (member?.type ?? UNKNOWN), shortCircuits];
50
+ const type = member === 'any' ? ANY : (member?.type ?? UNKNOWN);
51
+ return [this.pathType(node, object, type), shortCircuits];
50
52
  }
51
53
  const keyed = this.keyedIndex(object, node.index);
52
54
  if (keyed)
@@ -0,0 +1,11 @@
1
+ import type * as ast from '../ast.ts';
2
+ import { ImportChecker } from './imports.ts';
3
+ export declare abstract class SharedStateChecker extends ImportChecker {
4
+ /** `items = []` or `items[0].done = true` in a module that imported `items`. */
5
+ protected checkSharedWrite(target: ast.Expression): boolean;
6
+ /** `items.push(x)`: a method that changes state that another module shares. */
7
+ protected checkSharedMutation(node: ast.CallExpression): void;
8
+ /** The import that a target writes into, `items` of `items[0].done`, if it is shared state. */
9
+ private importedShared;
10
+ private sharedError;
11
+ }
@@ -0,0 +1,37 @@
1
+ import { mayChange } from "../methods.js";
2
+ import { ImportChecker } from "./imports.js";
3
+ // State shared by components: `state items []Item = []` at the top level of a module. Its module
4
+ // changes it anywhere; other modules import it to read, and change it only by calling a function
5
+ // of its module, so that the module keeps its data consistent, as a decorator keeps its state.
6
+ export class SharedStateChecker extends ImportChecker {
7
+ /** `items = []` or `items[0].done = true` in a module that imported `items`. */
8
+ checkSharedWrite(target) {
9
+ const shared = this.importedShared(target);
10
+ if (shared)
11
+ this.sharedError(shared, target);
12
+ return shared !== null;
13
+ }
14
+ /** `items.push(x)`: a method that changes state that another module shares. */
15
+ checkSharedMutation(node) {
16
+ const { callee } = node;
17
+ if (callee.kind !== 'MemberExpression')
18
+ return;
19
+ const shared = this.importedShared(callee.object);
20
+ if (shared && mayChange(this.typeOfChecked(callee.object), callee.property.name)) {
21
+ this.sharedError(shared, node);
22
+ }
23
+ }
24
+ /** The import that a target writes into, `items` of `items[0].done`, if it is shared state. */
25
+ importedShared(target) {
26
+ let root = target;
27
+ while (root.kind === 'MemberExpression' || root.kind === 'IndexExpression')
28
+ root = root.object;
29
+ if (root.kind !== 'Identifier')
30
+ return null;
31
+ const binding = this.lookupValue(root.name);
32
+ return binding?.shared ? binding : null;
33
+ }
34
+ sharedError(binding, node) {
35
+ this.error(`"${binding.name}" is state of "${binding.shared}", which is read-only here; change it with a function that "${binding.shared}" exports`, node);
36
+ }
37
+ }
@@ -12,10 +12,11 @@ export declare abstract class StatementChecker extends AssignmentChecker {
12
12
  */
13
13
  protected checkEffect(node: ast.EffectStatement): void;
14
14
  /**
15
- * `mount() { ... }`: a function that runs once the markup is in the document, like a handler.
16
- * It may return the function that cleans up, or nothing.
15
+ * `mount { ... }`: a function that runs once the markup is in the document, like a handler. It
16
+ * cleans up with `defer`, as an effect does, so it returns nothing.
17
17
  */
18
18
  protected checkMount(node: ast.MountStatement): void;
19
+ private checkDeferring;
19
20
  /**
20
21
  * `delete scores[name]` removes a key of an object used as a map, `{ [string]: T }`, or a field
21
22
  * that may be missing, of type `?T`; values of type `any` are not checked.
@@ -30,4 +31,6 @@ export declare abstract class StatementChecker extends AssignmentChecker {
30
31
  private linkError;
31
32
  /** The types of the variables of a declaration: the written one, or those of their values. */
32
33
  private initializerTypes;
34
+ /** `derived total number { ... }`: the block runs like the body of a closure and returns the value. */
35
+ private derivedBlockType;
33
36
  }
@@ -1,8 +1,10 @@
1
1
  import { guardedByError } from "./error-results.js";
2
2
  import { AssignmentChecker } from "./assignments.js";
3
+ import { placeOf } from "./context.js";
4
+ import { isTerminating } from "./flow.js";
3
5
  import { isUntyped, isNumeric, ownStatements } from "./helpers.js";
4
6
  import { func, hasZeroValue, isNullable, nonNull, nullable, typeToString, UNKNOWN, } from "./types.js";
5
- // Statements: declarations of variables, blocks, `mount()`, `effect`. Assignments and `return`
7
+ // Statements: declarations of variables, blocks, `mount`, `effect`. Assignments and `return`
6
8
  // are in assignments.ts.
7
9
  export class StatementChecker extends AssignmentChecker {
8
10
  checkStatement(node) {
@@ -92,30 +94,22 @@ export class StatementChecker extends AssignmentChecker {
92
94
  * a handler. It cleans up with `defer`, so it returns nothing.
93
95
  */
94
96
  checkEffect(node) {
95
- for (const statement of ownStatements(node.body)) {
96
- if (statement.kind === 'ReturnStatement' && statement.values.length > 0) {
97
- this.error('an effect returns nothing; clean up with defer, e.g. defer clearInterval(timer)', statement.values[0]);
98
- }
99
- }
100
- this.checkFunction([], func([], []), null, node.body, { closure: true });
97
+ this.checkDeferring(node.body, 'an effect');
101
98
  }
102
99
  /**
103
- * `mount() { ... }`: a function that runs once the markup is in the document, like a handler.
104
- * It may return the function that cleans up, or nothing.
100
+ * `mount { ... }`: a function that runs once the markup is in the document, like a handler. It
101
+ * cleans up with `defer`, as an effect does, so it returns nothing.
105
102
  */
106
103
  checkMount(node) {
107
- const results = this.checkFunction([], func([], []), null, node.body, { closure: true });
108
- if (results.length === 0)
109
- return;
110
- const result = results.length === 1 ? nonNull(results[0]) : null;
111
- const cleans = result !== null &&
112
- (isUntyped(result) ||
113
- result.kind === 'never' ||
114
- (result.kind === 'function' && result.required === 0));
115
- if (!cleans) {
116
- const returned = results.length === 1 ? typeToString(results[0]) : `${results.length} values`;
117
- this.error(`mount() returns the function that cleans up, e.g. return () => clearInterval(timer), not ${returned}`, { start: node.start, end: node.start + 'mount'.length });
104
+ this.checkDeferring(node.body, 'mount');
105
+ }
106
+ checkDeferring(body, what) {
107
+ for (const statement of ownStatements(body)) {
108
+ if (statement.kind === 'ReturnStatement' && statement.values.length > 0) {
109
+ this.error(`${what} returns nothing; clean up with defer, e.g. defer clearInterval(timer)`, statement.values[0]);
110
+ }
118
111
  }
112
+ this.checkFunction([], func([], []), null, body, { closure: true });
119
113
  }
120
114
  /**
121
115
  * `delete scores[name]` removes a key of an object used as a map, `{ [string]: T }`, or a field
@@ -133,6 +127,8 @@ export class StatementChecker extends AssignmentChecker {
133
127
  : null;
134
128
  if (target.kind === 'IndexExpression')
135
129
  this.checkValue(target.index);
130
+ // `delete user.boss` makes the field null again.
131
+ this.forget(target);
136
132
  if (isUntyped(object))
137
133
  return;
138
134
  if (object.kind === 'array') {
@@ -180,7 +176,7 @@ export class StatementChecker extends AssignmentChecker {
180
176
  binding.type = type;
181
177
  const value = values.length === names.length ? values[i] : undefined;
182
178
  if (value && isNullable(type) && !isNullable(this.typeOfChecked(value))) {
183
- this.flow.set(binding, nonNull(type));
179
+ this.flow.set(placeOf(binding), nonNull(type));
184
180
  }
185
181
  });
186
182
  if (node.keyword === 'const' && !declared)
@@ -207,7 +203,10 @@ export class StatementChecker extends AssignmentChecker {
207
203
  initializerTypes(node, declared) {
208
204
  const { names, values } = node;
209
205
  let types;
210
- if (values.length === 0) {
206
+ if (node.body) {
207
+ types = [this.derivedBlockType(names[0].name, node.body.body, declared)];
208
+ }
209
+ else if (values.length === 0) {
211
210
  if (declared && !hasZeroValue(declared) && declared.kind !== 'unknown') {
212
211
  const type = typeToString(declared);
213
212
  this.error(`${type} has no zero value: give "${names[0].name}" a value or make it nullable with ${typeToString(nullable(declared))}`, node.type ?? node);
@@ -228,4 +227,34 @@ export class StatementChecker extends AssignmentChecker {
228
227
  }
229
228
  return types;
230
229
  }
230
+ /** `derived total number { ... }`: the block runs like the body of a closure and returns the value. */
231
+ derivedBlockType(name, body, declared) {
232
+ const ends = isTerminating(body);
233
+ if (!ends) {
234
+ this.error(`derived "${name}" must return its value at the end of the block, e.g. return total`, {
235
+ start: body.end - 1,
236
+ end: body.end,
237
+ });
238
+ }
239
+ const saved = this.derivedBlock;
240
+ const results = declared ? [declared] : null;
241
+ this.derivedBlock = name;
242
+ try {
243
+ const types = this.checkFunction([], func([], results ?? []), results, body, {
244
+ closure: true,
245
+ });
246
+ if (types.length === 1)
247
+ return types[0];
248
+ // A block that does not end with a return is reported above.
249
+ if (ends) {
250
+ this.error(types.length === 0
251
+ ? `derived "${name}" returns no value: return it, e.g. return total`
252
+ : `derived "${name}" is one value, but its block returns ${types.length}`, body);
253
+ }
254
+ return UNKNOWN;
255
+ }
256
+ finally {
257
+ this.derivedBlock = saved;
258
+ }
259
+ }
231
260
  }
@@ -0,0 +1,37 @@
1
+ import type * as ast from '../ast.ts';
2
+ import { type Narrowing, type Place } from './context.ts';
3
+ import { ControlFlowChecker } from './control-flow.ts';
4
+ import { type LiteralType, type Member, type Type } from './types.ts';
5
+ type Value = LiteralType['value'];
6
+ export declare abstract class TaggedUnionChecker extends ControlFlowChecker {
7
+ /** The paths among the checked member expressions, with the types of their fields. */
8
+ private readonly paths;
9
+ /** The types of the fields of the paths, before narrowing: what their tags are found in. */
10
+ private readonly declared;
11
+ protected isTagOf(place: Place, field: string): boolean;
12
+ protected narrowable(node: ast.Expression): {
13
+ place: Place;
14
+ type: Type;
15
+ } | undefined;
16
+ /**
17
+ * The type of `object.field` read by `node`, which is `type` unless the expression is a path
18
+ * narrowed here. Getters and methods are not paths: they may give another value each time.
19
+ */
20
+ protected pathType(node: ast.MemberExpression, object: Type, type: Type): Type;
21
+ /** The field `name` of an object or a class instance, unless it is a method or an accessor. */
22
+ protected dataField(type: Type, name: string): Member | undefined;
23
+ /** The values of the tag `name` in each member of a union; `null` when `name` is not a tag. */
24
+ protected tagValues(members: readonly Type[], name: string): Value[][] | null;
25
+ /** `d.kind == "courier"` narrows `d` too: the members with this tag, or the others. */
26
+ protected narrow(node: ast.Expression, assumeTrue: boolean): Narrowing;
27
+ /** `switch d.kind`: in a case the members with its tags, in `default` those of no case. */
28
+ protected narrowCase(node: ast.SwitchStatement, switchCase: ast.SwitchCase): Narrowing;
29
+ private narrowTag;
30
+ /**
31
+ * `{ kind: "pickup", shop: "Main st." }` where one of several objects is expected: the member
32
+ * that a tag of the literal chooses, or `null` when no tag chooses one. A tag of no member is
33
+ * reported, and the literal then gets `UNKNOWN`, so that it is not reported again.
34
+ */
35
+ protected taggedMember(node: ast.ObjectLiteral, objects: readonly Type[], union: Type): Type | null;
36
+ }
37
+ export {};
@@ -0,0 +1,132 @@
1
+ import { placeOf } from "./context.js";
2
+ import { ControlFlowChecker } from "./control-flow.js";
3
+ import { literalValue, narrowByTag } from "./flow.js";
4
+ import { findClassMember, memberTypeOf, nonNull, typeToString, unionMembers, UNKNOWN, } from "./types.js";
5
+ export class TaggedUnionChecker extends ControlFlowChecker {
6
+ /** The paths among the checked member expressions, with the types of their fields. */
7
+ paths = new WeakMap();
8
+ /** The types of the fields of the paths, before narrowing: what their tags are found in. */
9
+ declared = new WeakMap();
10
+ isTagOf(place, field) {
11
+ const type = place.path === '' ? place.binding.type : this.declared.get(place);
12
+ return type != null && this.tagValues(unionMembers(nonNull(type)), field) !== null;
13
+ }
14
+ narrowable(node) {
15
+ if (node.kind === 'Identifier') {
16
+ const binding = this.narrowableBinding(node);
17
+ if (!binding?.type)
18
+ return undefined;
19
+ const place = placeOf(binding);
20
+ return { place, type: this.flow.get(place) ?? binding.type };
21
+ }
22
+ const path = node.kind === 'MemberExpression' ? this.paths.get(node) : undefined;
23
+ return path && { place: path.place, type: this.flow.get(path.place) ?? path.type };
24
+ }
25
+ /**
26
+ * The type of `object.field` read by `node`, which is `type` unless the expression is a path
27
+ * narrowed here. Getters and methods are not paths: they may give another value each time.
28
+ */
29
+ pathType(node, object, type) {
30
+ const parent = node.optional ? undefined : this.narrowable(node.object)?.place;
31
+ const { name } = node.property;
32
+ if (!parent || !unionMembers(object).every((member) => this.dataField(member, name))) {
33
+ this.paths.delete(node);
34
+ return type;
35
+ }
36
+ const place = placeOf(parent.binding, parent.path === '' ? name : `${parent.path}.${name}`);
37
+ this.paths.set(node, { place, type });
38
+ this.declared.set(place, type);
39
+ return this.flow.get(place) ?? type;
40
+ }
41
+ /** The field `name` of an object or a class instance, unless it is a method or an accessor. */
42
+ dataField(type, name) {
43
+ let member;
44
+ if (type.kind === 'object') {
45
+ member = type.members.get(name);
46
+ }
47
+ else if (type.kind === 'class') {
48
+ this.ensureClassResolved(type.info);
49
+ member = findClassMember(type.info, name, false);
50
+ if (member && type.args)
51
+ member = { ...member, type: memberTypeOf(type, member) };
52
+ }
53
+ return member && !member.method && member.accessor === undefined ? member : undefined;
54
+ }
55
+ /** The values of the tag `name` in each member of a union; `null` when `name` is not a tag. */
56
+ tagValues(members, name) {
57
+ if (members.length < 2)
58
+ return null;
59
+ const tags = [];
60
+ for (const member of members) {
61
+ const field = this.dataField(member, name);
62
+ const types = field ? unionMembers(field.type) : [];
63
+ if (!types.every((type) => type.kind === 'literal'))
64
+ return null;
65
+ if (types.length === 0)
66
+ return null;
67
+ tags.push(types.map((type) => type.value));
68
+ }
69
+ return tags;
70
+ }
71
+ /** `d.kind == "courier"` narrows `d` too: the members with this tag, or the others. */
72
+ narrow(node, assumeTrue) {
73
+ const narrowing = super.narrow(node, assumeTrue);
74
+ if (node.kind !== 'BinaryExpression')
75
+ return narrowing;
76
+ if (node.operator !== '==' && node.operator !== '!=')
77
+ return narrowing;
78
+ const right = literalValue(node.right);
79
+ const value = right ?? literalValue(node.left);
80
+ if (value === undefined)
81
+ return narrowing;
82
+ const target = right !== undefined ? node.left : node.right;
83
+ const equal = (node.operator === '==') === assumeTrue;
84
+ return [...narrowing, ...this.narrowTag(target, [value], equal)];
85
+ }
86
+ /** `switch d.kind`: in a case the members with its tags, in `default` those of no case. */
87
+ narrowCase(node, switchCase) {
88
+ const narrowing = super.narrowCase(node, switchCase);
89
+ if (!node.discriminant)
90
+ return narrowing;
91
+ const isDefault = switchCase.tests.length === 0;
92
+ const tests = isDefault ? node.cases.flatMap((each) => each.tests) : switchCase.tests;
93
+ const values = tests.map(literalValue);
94
+ if (!isDefault && values.includes(undefined))
95
+ return narrowing;
96
+ const known = values.filter((value) => value !== undefined);
97
+ return [...narrowing, ...this.narrowTag(node.discriminant, known, !isDefault)];
98
+ }
99
+ narrowTag(target, values, equal) {
100
+ if (target.kind !== 'MemberExpression' || target.optional)
101
+ return [];
102
+ const owner = this.narrowable(target.object);
103
+ const tags = owner && this.tagValues(unionMembers(nonNull(owner.type)), target.property.name);
104
+ return owner && tags ? [[owner.place, narrowByTag(owner.type, tags, values, equal)]] : [];
105
+ }
106
+ /**
107
+ * `{ kind: "pickup", shop: "Main st." }` where one of several objects is expected: the member
108
+ * that a tag of the literal chooses, or `null` when no tag chooses one. A tag of no member is
109
+ * reported, and the literal then gets `UNKNOWN`, so that it is not reported again.
110
+ */
111
+ taggedMember(node, objects, union) {
112
+ for (const property of node.properties) {
113
+ if (property.kind === 'SpreadElement')
114
+ continue;
115
+ const name = property.key.kind === 'Identifier' ? property.key.name : property.key.value;
116
+ const value = literalValue(property.value);
117
+ const tags = value === undefined ? null : this.tagValues(objects, name);
118
+ if (value === undefined || !tags)
119
+ continue;
120
+ const chosen = objects.filter((_, i) => tags[i].includes(value));
121
+ if (chosen.length === 1)
122
+ return chosen[0];
123
+ if (chosen.length > 1)
124
+ continue;
125
+ const known = [...new Set(tags.flat())].map((each) => JSON.stringify(each));
126
+ const choices = known.length > 1 ? `${known.slice(0, -1).join(', ')} or ${known.at(-1)}` : known[0];
127
+ this.error(`${JSON.stringify(value)} is not a kind of ${typeToString(union)}: write ${choices}`, property.value);
128
+ return UNKNOWN;
129
+ }
130
+ return null;
131
+ }
132
+ }
@@ -1 +1,2 @@
1
1
  export declare const UNTYPED_GLOBALS: readonly string[];
2
+ export declare const UNTYPED_CLASSES: readonly string[];
@@ -32,7 +32,6 @@ export const UNTYPED_GLOBALS = [
32
32
  'Response',
33
33
  'Request',
34
34
  'Headers',
35
- 'AbortController',
36
35
  'fetch',
37
36
  'structuredClone',
38
37
  'queueMicrotask',
@@ -45,3 +44,6 @@ export const UNTYPED_GLOBALS = [
45
44
  'crypto',
46
45
  'performance',
47
46
  ];
47
+ // Classes that are names of types too, `any` in both: `new AbortController()` and
48
+ // `signal AbortSignal`. TypeScript's lib files replace them as they replace the DOM.
49
+ export const UNTYPED_CLASSES = ['AbortController', 'AbortSignal', 'ImageData'];
@@ -20,6 +20,10 @@ export declare abstract class AttributeEmitter extends UpdateEmitter {
20
20
  protected attribute(element: string, tag: string | null, attribute: ast.JsxAttribute | ast.JsxSpreadAttribute, live: Live | null, svg?: boolean, scope?: string | null): void;
21
21
  /** The class of an element with the class of its style: `"card rook-card-1x9k2az"`. */
22
22
  private scopedClass;
23
+ /** Whether `class={...}` is a list of classes, `[styles.tab, active ? styles.active : null]`. */
24
+ protected isClassList(value: ast.Expression): boolean;
25
+ /** The classes of `class={...}` as a string, before the class of the style is added to them. */
26
+ protected classText(value: ast.Expression, text: string): string;
23
27
  /** Sets the class of an element: `className`, or the attribute of an SVG element. */
24
28
  protected scopeClass(element: string, classes: string, svg: boolean): void;
25
29
  /** `<app-card item-count={1} />`: the property of a web component of this module, if it has one. */
@@ -3,6 +3,7 @@ import { attributeName } from "../html-tag.js";
3
3
  import { SVG_NAMESPACE } from "../svg.js";
4
4
  import { eventName, mentions } from "./element-helpers.js";
5
5
  import { ARROW } from "./syntax.js";
6
+ import { rootsOf } from "./shared.js";
6
7
  import { UpdateEmitter } from "./updates.js";
7
8
  // Attributes of elements: properties, setAttribute, styles, `bind:` and event handlers.
8
9
  export class AttributeEmitter extends UpdateEmitter {
@@ -46,6 +47,15 @@ export class AttributeEmitter extends UpdateEmitter {
46
47
  this.scopedClass(element, value, live, svg, scope);
47
48
  return;
48
49
  }
50
+ if (name === 'class' &&
51
+ value !== null &&
52
+ value.kind !== 'EventHandler' &&
53
+ this.isClassList(value)) {
54
+ this.setLive(live, value, (text) => svg
55
+ ? `${element}.setAttribute("class", $$classes(${text}));`
56
+ : `${element}.className = $$classes(${text});`);
57
+ return;
58
+ }
49
59
  const webProperty = tag === null || svg ? null : this.webComponentProperty(tag, name);
50
60
  if (webProperty) {
51
61
  this.webComponentAttribute(element, webProperty, value, live);
@@ -102,15 +112,24 @@ export class AttributeEmitter extends UpdateEmitter {
102
112
  this.scopeClass(element, `${value?.value ?? ''} ${scope}`.trim(), svg);
103
113
  }
104
114
  else if (value.kind !== 'EventHandler') {
105
- const nullable = this.typeOf(value)?.kind !== 'string';
106
115
  this.setLive(live, value, (text) => {
107
- const scoped = `\`\${${nullable ? `${text} ?? ""` : text}} ${scope}\``;
116
+ const scoped = `\`\${${this.classText(value, text)}} ${scope}\``;
108
117
  return svg
109
118
  ? `${element}.setAttribute("class", ${scoped});`
110
119
  : `${element}.className = ${scoped};`;
111
120
  });
112
121
  }
113
122
  }
123
+ /** Whether `class={...}` is a list of classes, `[styles.tab, active ? styles.active : null]`. */
124
+ isClassList(value) {
125
+ return value.kind === 'ArrayLiteral' || this.typeOf(value)?.kind === 'array';
126
+ }
127
+ /** The classes of `class={...}` as a string, before the class of the style is added to them. */
128
+ classText(value, text) {
129
+ if (this.isClassList(value))
130
+ return `$$classes(${text})`;
131
+ return this.typeOf(value)?.kind === 'string' ? text : `${text} ?? ""`;
132
+ }
114
133
  /** Sets the class of an element: `className`, or the attribute of an SVG element. */
115
134
  scopeClass(element, classes, svg) {
116
135
  const text = JSON.stringify(classes);
@@ -178,6 +197,9 @@ export class AttributeEmitter extends UpdateEmitter {
178
197
  try {
179
198
  const body = this.block(() => {
180
199
  this.line(`${target} = ${current};`);
200
+ for (const name of rootsOf(value, this.shared)) {
201
+ this.line(`$$changed(${this.shared.listeners.get(name)});`);
202
+ }
181
203
  for (const source of written) {
182
204
  // The element already shows what was entered: its own update is skipped.
183
205
  const own = live && dependsOn.has(source) ? live.ownIndex(source) : null;
@@ -25,9 +25,14 @@ export interface EarlyReturns {
25
25
  }
26
26
  /** Marks where the setters of an inlined component with early returns go: before any return. */
27
27
  export declare const SETTERS: string;
28
+ /**
29
+ * Marks the `$$watch(...)` of a component that reads state of the module, around the id of the
30
+ * source: it stays only if something depends on the state (resolveMarkers).
31
+ */
32
+ export declare const LISTEN: string;
28
33
  /** `f(a, undefined, undefined)` → `f(a)`: missing arguments are undefined anyway. */
29
34
  export declare function withoutTrailingUndefined(args: string[]): string[];
30
- /** Whether a component has `mount() { ... }`. */
35
+ /** Whether a component has `mount { ... }`. */
31
36
  export declare function hasMount(component: ast.ComponentDeclaration): boolean;
32
37
  /** Whether a component returns before the end of its body. */
33
38
  export declare function hasEarlyReturn(component: ast.ComponentDeclaration): boolean;
@@ -1,6 +1,11 @@
1
1
  import { containsReturn } from "../recursion.js";
2
2
  /** Marks where the setters of an inlined component with early returns go: before any return. */
3
3
  export const SETTERS = String.fromCharCode(0xe002);
4
+ /**
5
+ * Marks the `$$watch(...)` of a component that reads state of the module, around the id of the
6
+ * source: it stays only if something depends on the state (resolveMarkers).
7
+ */
8
+ export const LISTEN = String.fromCharCode(0xe004);
4
9
  /** `f(a, undefined, undefined)` → `f(a)`: missing arguments are undefined anyway. */
5
10
  export function withoutTrailingUndefined(args) {
6
11
  let end = args.length;
@@ -8,7 +13,7 @@ export function withoutTrailingUndefined(args) {
8
13
  end--;
9
14
  return args.slice(0, end);
10
15
  }
11
- /** Whether a component has `mount() { ... }`. */
16
+ /** Whether a component has `mount { ... }`. */
12
17
  export function hasMount(component) {
13
18
  return component.body.body.some((statement) => statement.kind === 'MountStatement');
14
19
  }
@@ -9,7 +9,7 @@ export declare abstract class ComponentPropsEmitter extends ControlFlowEmitter {
9
9
  /** Setters waiting for their place at the top of an inlined component, by its result. */
10
10
  protected readonly pendingSetters: Map<string, string[]>;
11
11
  /**
12
- * In a component with `mount()`: the variable that gets its first node once the markup is
12
+ * In a component with `mount`: the variable that gets its first node once the markup is
13
13
  * created, so that `$$mount` can wait until that node is in the document.
14
14
  */
15
15
  protected mountNode: string | null;
@@ -17,7 +17,14 @@ export declare abstract class ComponentPropsEmitter extends ControlFlowEmitter {
17
17
  * A use of a recursive component: a call of its function. The parent's updates call the
18
18
  * returned function with the new values of the properties that depend on its state.
19
19
  */
20
- protected callComponent(component: ast.ComponentDeclaration, values: ReadonlyMap<string, string>, reactiveProps: ReadonlyMap<string, [string, Set<Source>]>, live: Live | null, root?: string): string;
20
+ protected callComponent(component: ast.ComponentDeclaration, values: ReadonlyMap<string, string>, reactiveProps: ReadonlyMap<string, [string, Set<Source>]>, live: Live | null, root?: string, binders?: ReadonlyMap<string, string>): string;
21
+ /**
22
+ * `bind:active={tab}`: the function that the component calls with the new value of `active`. It
23
+ * is written in the caller, so it writes the caller's `tab` and updates what reads it there.
24
+ */
25
+ protected binder(target: ast.Expression): string;
26
+ /** `$$bindActive?.(active);` in a function component, which gets the binders as arguments. */
27
+ protected bindersOf(component: ast.ComponentDeclaration, reactive: Reactive): string[];
21
28
  protected reactiveOf(component: ast.ComponentDeclaration, props: Iterable<string>): Reactive;
22
29
  /**
23
30
  * `title, kind = "info"`: parameters with their default values, each name with a prefix. A tag
@@ -40,3 +47,5 @@ export declare abstract class ComponentPropsEmitter extends ControlFlowEmitter {
40
47
  */
41
48
  protected spreadProps(argument: ast.Expression, component: ast.ComponentDeclaration, live: Live | null, values: Map<string, string>, reactiveProps: Map<string, [string, Set<Source>]>): void;
42
49
  }
50
+ /** The bound properties of a component, `bind active string`, in their order. */
51
+ export declare function boundParams(component: ast.ComponentDeclaration): ast.Parameter[];