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,6 +1,5 @@
1
1
  import { ComponentChecker } from "./components.js";
2
2
  import { isUntyped } from "./helpers.js";
3
- const upperFirst = (name) => name.charAt(0).toUpperCase() + name.slice(1);
4
3
  import { isNullable, typeToString, UNKNOWN, } from "./types.js";
5
4
  // Components as values: `content: Intro` has the type `comp()`, and a tag takes the component
6
5
  // from a value, `<section.content />`, or `<Page />` when `Page` is a variable. Such a tag passes
@@ -20,12 +19,8 @@ export class ComponentValueChecker extends ComponentChecker {
20
19
  const name = tag.name;
21
20
  if (isUntyped(type) || type.kind !== 'component') {
22
21
  if (isNullable(type)) {
23
- // Only variables are narrowed, so a path goes into one first.
24
- const variable = /^[A-Z]/.test(name) ? name : upperFirst(name.split('.').at(-1));
25
- const show = `{if ${variable} != null { <${variable} /> }}`;
26
- this.error(name === variable
27
- ? `<${name}> may be null: show it with {if}, e.g. ${show}`
28
- : `<${name}> may be null: put it into a variable and show it with {if}, e.g. const ${variable} = ${name}, then ${show}`, tag);
22
+ // A path of fields is narrowed like a variable: `{if page.other != null { <page.other /> }}`.
23
+ this.error(`<${name}> may be null: show it with {if}, e.g. {if ${name} != null { <${name} /> }}`, tag);
29
24
  }
30
25
  else if (!isUntyped(type)) {
31
26
  this.error(`<${name}> is ${typeToString(type)}, not a component: give it a type like comp(title string)`, tag);
@@ -58,6 +53,14 @@ export class ComponentValueChecker extends ComponentChecker {
58
53
  this.error(`ref cannot point to <${name}>: its component is a value, so its element is not known in advance`, attribute);
59
54
  continue;
60
55
  }
56
+ // The type of the value does not say which properties are bound.
57
+ if (attributeName.startsWith('bind:')) {
58
+ const { value } = attribute;
59
+ if (value && value.kind !== 'EventHandler')
60
+ this.checkValue(value);
61
+ this.error(`<${name}> takes its component from a value, so it cannot bind its properties; pass a function, e.g. onChange={...}`, attribute.name);
62
+ continue;
63
+ }
61
64
  if (given.has(attributeName)) {
62
65
  this.error(`duplicate property "${attributeName}"`, attribute.name);
63
66
  }
@@ -1,8 +1,8 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import type { ComponentInfo } from './context.ts';
3
- import { ForwardingChecker } from './forwarding.ts';
3
+ import { ComponentBindingChecker } from './component-bindings.ts';
4
4
  import { type Type } from './types.ts';
5
- export declare abstract class ComponentChecker extends ForwardingChecker {
5
+ export declare abstract class ComponentChecker extends ComponentBindingChecker {
6
6
  protected resolveProps(info: ComponentInfo): void;
7
7
  /** The body runs once where the component is used and must end with `return <markup>`. */
8
8
  protected checkComponentBody(info: ComponentInfo): void;
@@ -3,10 +3,11 @@ import { freeNames } from "../names.js";
3
3
  import { endlessRecursion } from "../recursion.js";
4
4
  import { CONTENT } from "./dom.js";
5
5
  import { lookupIn, ownStatements, isUntyped, callSignature } from "./helpers.js";
6
- import { ATTRIBUTES, ForwardingChecker } from "./forwarding.js";
6
+ import { ComponentBindingChecker } from "./component-bindings.js";
7
+ import { ATTRIBUTES } from "./forwarding.js";
7
8
  import { BOOL, func, hasZeroValue, isAssignable, isNullable, nonNull, spreadFields, typesEqual, typeToString, UNKNOWN, } from "./types.js";
8
9
  // Components: their bodies and uses, properties and spread, names they use from the module, recursion.
9
- export class ComponentChecker extends ForwardingChecker {
10
+ export class ComponentChecker extends ComponentBindingChecker {
10
11
  resolveProps(info) {
11
12
  for (const param of info.node.params) {
12
13
  // `...attrs` takes the attributes that are not properties.
@@ -20,7 +21,7 @@ export class ComponentChecker extends ForwardingChecker {
20
21
  if (info.props.has(name))
21
22
  this.error(`duplicate property "${name}"`, param.name);
22
23
  const optional = param.defaultValue !== null || isNullable(type) || name === 'children';
23
- info.props.set(name, { type, optional });
24
+ info.props.set(name, { type, optional, ...(param.bind ? { bind: true } : {}) });
24
25
  }
25
26
  }
26
27
  /** The body runs once where the component is used and must end with `return <markup>`. */
@@ -96,6 +97,10 @@ export class ComponentChecker extends ForwardingChecker {
96
97
  this.checkRefAttribute(attribute, node);
97
98
  continue;
98
99
  }
100
+ if (name.startsWith('bind:')) {
101
+ this.checkPropBinding(attribute, info, tag.name, given);
102
+ continue;
103
+ }
99
104
  if (given.has(name))
100
105
  this.error(`duplicate property "${name}"`, attribute.name);
101
106
  given.add(name);
@@ -11,6 +11,8 @@ export interface ModuleExports {
11
11
  decorators?: Map<string, DecoratorInfo>;
12
12
  /** `export comp`: components are used as tags, not as values. */
13
13
  components?: Map<string, ComponentInfo>;
14
+ /** `export state`: other modules read it, and change it only with the functions of its module. */
15
+ shared?: Set<string>;
14
16
  }
15
17
  /** What `importModule` found: the module's exports, an error for the import, or nothing (untyped). */
16
18
  export type ImportResult = {
@@ -100,6 +102,8 @@ export interface Binding {
100
102
  type: Type | null;
101
103
  /** For components, which exist only at compile time and are used as tags. */
102
104
  component?: ComponentInfo;
105
+ /** State of another module, `state items` there, which is read-only here: its module's path. */
106
+ shared?: string;
103
107
  /**
104
108
  * `const user, err = loadUser(id)` of `(User, error)`: the error has the values that are not null
105
109
  * once it is null, and each of them has the error (error-results.ts).
@@ -113,6 +117,7 @@ export interface ComponentInfo {
113
117
  props: Map<string, {
114
118
  type: Type;
115
119
  optional: boolean;
120
+ bind?: true;
116
121
  }>;
117
122
  /** Names from the module that the component's code uses, computed when first needed. */
118
123
  freeNames: Set<string> | null;
@@ -160,6 +165,8 @@ export interface DecoratorInfo {
160
165
  } | null;
161
166
  /** The decorators after `needs` that exist; they are applied above it. */
162
167
  needed: DecoratorInfo[];
168
+ /** The decorators after `includes` that exist: applied with it, above its own body. */
169
+ included: DecoratorInfo[];
163
170
  /**
164
171
  * For an exported decorator: the names of its module that its code uses, which other modules get
165
172
  * through hidden exports, and the types of its expressions, for the code generator there.
@@ -170,9 +177,15 @@ export interface DecoratorInfo {
170
177
  inline: InlinePackage | null;
171
178
  }
172
179
  export interface PublicMember {
173
- kind: 'state' | 'derived' | 'const' | 'func';
180
+ kind: 'state' | 'derived' | 'const' | 'ref' | 'func';
174
181
  type: Type;
175
182
  }
183
+ /**
184
+ * The decorators applied with those they include, which come before them: `@shop` of
185
+ * `dec shop() includes visible, cart` gives visible, cart, shop. A cycle of includes is reported
186
+ * where the decorators are declared; here it stops.
187
+ */
188
+ export declare function withIncluded(decorators: readonly DecoratorInfo[], seen?: ReadonlySet<DecoratorInfo>): DecoratorInfo[];
176
189
  /** A `type` alias, resolved when first used. */
177
190
  export interface AliasEntry {
178
191
  kind: 'alias';
@@ -205,11 +218,21 @@ export interface ClassContext {
205
218
  isStatic: boolean;
206
219
  }
207
220
  /**
208
- * Types of variables narrowed by the code before the current point, e.g. `?User` → `User`
209
- * after `if user == null { return }`.
221
+ * A variable, or a path of data fields from it that is narrowed like one: `order.delivery` is the
222
+ * binding of `order` with the path `"delivery"`. The path of the variable itself is empty.
223
+ */
224
+ export interface Place {
225
+ binding: Binding;
226
+ path: string;
227
+ }
228
+ /** The same object for the same variable and path, so that it is a key of a `Flow`. */
229
+ export declare function placeOf(binding: Binding, path?: string): Place;
230
+ /**
231
+ * Types of variables and paths narrowed by the code before the current point, e.g. `?User` →
232
+ * `User` after `if user == null { return }`.
210
233
  */
211
- export type Flow = Map<Binding, Type>;
212
- export type Narrowing = [Binding, Type][];
234
+ export type Flow = Map<Place, Type>;
235
+ export type Narrowing = [Place, Type][];
213
236
  export declare const NARROWABLE: ReadonlySet<BindingKind>;
214
237
  /** Built-in globals. With a library, the untyped ones and the DOM types come from it instead. */
215
238
  export declare function globalScope(library: Library | undefined): Scope;
@@ -1,5 +1,13 @@
1
1
  import { GLOBAL_TYPES, GLOBAL_VALUES, LIBRARY_TYPES } from "./builtins.js";
2
2
  import {} from "./types.js";
3
+ /**
4
+ * The decorators applied with those they include, which come before them: `@shop` of
5
+ * `dec shop() includes visible, cart` gives visible, cart, shop. A cycle of includes is reported
6
+ * where the decorators are declared; here it stops.
7
+ */
8
+ export function withIncluded(decorators, seen = new Set()) {
9
+ return decorators.flatMap((each) => seen.has(each) ? [] : [...withIncluded(each.included, new Set([...seen, each])), each]);
10
+ }
3
11
  export class Scope {
4
12
  parent;
5
13
  values = new Map();
@@ -8,6 +16,21 @@ export class Scope {
8
16
  this.parent = parent;
9
17
  }
10
18
  }
19
+ const places = new WeakMap();
20
+ /** The same object for the same variable and path, so that it is a key of a `Flow`. */
21
+ export function placeOf(binding, path = '') {
22
+ let byPath = places.get(binding);
23
+ if (!byPath) {
24
+ byPath = new Map();
25
+ places.set(binding, byPath);
26
+ }
27
+ let place = byPath.get(path);
28
+ if (!place) {
29
+ place = { binding, path };
30
+ byPath.set(path, place);
31
+ }
32
+ return place;
33
+ }
11
34
  export const NARROWABLE = new Set([
12
35
  'let',
13
36
  'const',
@@ -1,5 +1,5 @@
1
1
  import type * as ast from '../ast.ts';
2
- import { type Binding, type Flow, type Narrowing } from './context.ts';
2
+ import { type Binding, type Narrowing } from './context.ts';
3
3
  import { StatementChecker } from './statements.ts';
4
4
  export declare abstract class ControlFlowChecker extends StatementChecker {
5
5
  protected checkIf(node: ast.IfStatement): void;
@@ -15,16 +15,17 @@ export declare abstract class ControlFlowChecker extends StatementChecker {
15
15
  * `switch filter { case "all": ... default: ... }`: in a case the discriminant has the literals
16
16
  * of its tests, in `default` the literals of no case.
17
17
  */
18
- protected narrowCase(node: ast.SwitchStatement, switchCase: ast.SwitchCase, entry: Flow): Narrowing;
18
+ protected narrowCase(node: ast.SwitchStatement, switchCase: ast.SwitchCase): Narrowing;
19
19
  protected checkTry(node: ast.TryStatement): void;
20
20
  protected checkCondition(node: ast.Expression): void;
21
- /** Variables whose type is narrowed when `node` evaluates to `assumeTrue`. */
21
+ /** Variables and paths whose type is narrowed when `node` evaluates to `assumeTrue`. */
22
22
  protected narrow(node: ast.Expression, assumeTrue: boolean): Narrowing;
23
23
  /** `x instanceof Date`: an instance of the class when true, the rest of a union when false. */
24
24
  protected narrowInstanceof(left: ast.Expression, right: ast.Expression, assumeTrue: boolean): Narrowing;
25
25
  protected nullError(node: ast.Expression): void;
26
26
  protected narrowableBinding(node: ast.Expression): Binding | undefined;
27
- /** An assignment to a variable ends its narrowing. */
27
+ /** An assignment to a variable ends its narrowing; one to a path, that of the paths it changes. */
28
28
  protected forget(target: ast.Expression): void;
29
- protected dropNarrowing(names: ReadonlySet<string>): void;
29
+ /** Ends the narrowing of what `node` assigns: before a loop, or a statement with several ways. */
30
+ protected dropNarrowing(node: ast.Node): void;
30
31
  }
@@ -1,10 +1,11 @@
1
- import { assignedNames } from "../names.js";
2
- import { NARROWABLE } from "./context.js";
3
- import { narrowByTypeof, literalValue, withoutLiteral, withNarrowing, mergeFlows, leaves, } from "./flow.js";
1
+ import { assignedNames, assignedPaths, pathOf } from "../names.js";
2
+ import { NARROWABLE, placeOf } from "./context.js";
3
+ import { narrowByTypeof, literalValue, withoutLiteral, withNarrowing, mergeFlows, leaves, forgetWritten, withoutAssigned, } from "./flow.js";
4
4
  import { instanceTypeOf } from "./helpers.js";
5
5
  import { StatementChecker } from "./statements.js";
6
6
  import { ANY, BOOL, isAssignable, isComparable, isNullable, isSubclass, literal, nonNull, NULL, typeToString, union, unionMembers, widenLiterals, } from "./types.js";
7
- // if, for, switch and try, and how conditions narrow the types of variables in the branches.
7
+ // if, for, switch and try, and how conditions narrow the types of variables and paths in the
8
+ // branches.
8
9
  export class ControlFlowChecker extends StatementChecker {
9
10
  checkIf(node) {
10
11
  if (this.checkWithInit(node.init, () => this.checkIf({ ...node, init: null })))
@@ -39,8 +40,8 @@ export class ControlFlowChecker extends StatementChecker {
39
40
  this.withScope(() => {
40
41
  if (node.init)
41
42
  this.checkSimpleStatement(node.init);
42
- // Variables assigned in the loop may change between iterations.
43
- this.dropNarrowing(assignedNames(node));
43
+ // Variables and paths assigned in the loop may change between iterations.
44
+ this.dropNarrowing(node);
44
45
  const entry = this.flow;
45
46
  if (node.condition)
46
47
  this.checkCondition(node.condition);
@@ -71,7 +72,7 @@ export class ControlFlowChecker extends StatementChecker {
71
72
  if (this.checkWithInit(node.init, () => this.checkSwitch({ ...node, init: null })))
72
73
  return;
73
74
  const discriminant = node.discriminant ? this.checkValue(node.discriminant) : null;
74
- this.dropNarrowing(assignedNames(node));
75
+ this.dropNarrowing(node);
75
76
  const entry = this.flow;
76
77
  for (const switchCase of node.cases) {
77
78
  this.flow = entry;
@@ -89,7 +90,7 @@ export class ControlFlowChecker extends StatementChecker {
89
90
  ? switchCase.tests.length === 1
90
91
  ? this.narrow(switchCase.tests[0], true)
91
92
  : []
92
- : this.narrowCase(node, switchCase, entry);
93
+ : this.narrowCase(node, switchCase);
93
94
  this.flow = withNarrowing(entry, narrowing);
94
95
  this.checkBlock(switchCase.body);
95
96
  }
@@ -99,18 +100,18 @@ export class ControlFlowChecker extends StatementChecker {
99
100
  * `switch filter { case "all": ... default: ... }`: in a case the discriminant has the literals
100
101
  * of its tests, in `default` the literals of no case.
101
102
  */
102
- narrowCase(node, switchCase, entry) {
103
- const binding = node.discriminant ? this.narrowableBinding(node.discriminant) : undefined;
104
- if (!binding?.type)
103
+ narrowCase(node, switchCase) {
104
+ const target = node.discriminant ? this.narrowable(node.discriminant) : undefined;
105
+ if (!target)
105
106
  return [];
106
- const current = entry.get(binding) ?? binding.type;
107
+ const { place, type: current } = target;
107
108
  if (!unionMembers(nonNull(current)).some((member) => member.kind === 'literal'))
108
109
  return [];
109
110
  if (switchCase.tests.length > 0) {
110
111
  const values = switchCase.tests.map(literalValue);
111
112
  if (values.some((value) => value === undefined))
112
113
  return [];
113
- return [[binding, union(values.map((value) => literal(value)))]];
114
+ return [[place, union(values.map((value) => literal(value)))]];
114
115
  }
115
116
  let rest = current;
116
117
  for (const other of node.cases) {
@@ -120,10 +121,10 @@ export class ControlFlowChecker extends StatementChecker {
120
121
  rest = withoutLiteral(rest, value);
121
122
  }
122
123
  }
123
- return [[binding, rest]];
124
+ return [[place, rest]];
124
125
  }
125
126
  checkTry(node) {
126
- this.dropNarrowing(assignedNames(node));
127
+ this.dropNarrowing(node);
127
128
  const entry = this.flow;
128
129
  this.flow = new Map(entry);
129
130
  this.checkBlock(node.block.body);
@@ -157,7 +158,7 @@ export class ControlFlowChecker extends StatementChecker {
157
158
  : '';
158
159
  this.error(`condition must be bool, not ${typeToString(type)}${hint}`, node);
159
160
  }
160
- /** Variables whose type is narrowed when `node` evaluates to `assumeTrue`. */
161
+ /** Variables and paths whose type is narrowed when `node` evaluates to `assumeTrue`. */
161
162
  narrow(node, assumeTrue) {
162
163
  if (node.kind === 'UnaryExpression' && node.operator === '!') {
163
164
  return this.narrow(node.argument, !assumeTrue);
@@ -183,17 +184,21 @@ export class ControlFlowChecker extends StatementChecker {
183
184
  // `x != null` when true, `x == null` when false: `x` is not null.
184
185
  if ((operator === '!=' && assumeTrue) || (operator === '==' && !assumeTrue)) {
185
186
  const target = right.kind === 'NullLiteral' ? left : left.kind === 'NullLiteral' ? right : null;
186
- const binding = target ? this.narrowableBinding(target) : undefined;
187
- if (binding?.type)
188
- return [[binding, nonNull(this.flow.get(binding) ?? binding.type)]];
187
+ const found = target ? this.narrowable(target) : undefined;
188
+ if (found)
189
+ return [[found.place, nonNull(found.type)]];
189
190
  }
190
191
  // `err == null` when true, `err != null` when false: the values returned with `err` are not
191
192
  // null (error-results.ts).
192
193
  if ((operator === '==' && assumeTrue) || (operator === '!=' && !assumeTrue)) {
193
194
  const target = right.kind === 'NullLiteral' ? left : left.kind === 'NullLiteral' ? right : null;
194
195
  const guards = target ? this.narrowableBinding(target)?.guards : undefined;
195
- if (guards)
196
- return guards.map((value) => [value, nonNull(this.flow.get(value) ?? value.type)]);
196
+ if (guards) {
197
+ return guards.map((value) => {
198
+ const place = placeOf(value);
199
+ return [place, nonNull(this.flow.get(place) ?? value.type)];
200
+ });
201
+ }
197
202
  }
198
203
  if (operator === 'instanceof')
199
204
  return this.narrowInstanceof(left, right, assumeTrue);
@@ -208,41 +213,41 @@ export class ControlFlowChecker extends StatementChecker {
208
213
  ? [right, left]
209
214
  : [null, null];
210
215
  if (check && tag?.kind === 'StringLiteral') {
211
- const binding = this.narrowableBinding(check.argument);
212
- if (!binding?.type)
216
+ const found = this.narrowable(check.argument);
217
+ if (!found)
213
218
  return [];
214
- const type = narrowByTypeof(this.flow.get(binding) ?? binding.type, tag.value, equal);
215
- return type ? [[binding, type]] : [];
219
+ const type = narrowByTypeof(found.type, tag.value, equal);
220
+ return type ? [[found.place, type]] : [];
216
221
  }
217
222
  // `filter == "all"`: narrows a type with literals in it.
218
223
  const value = literalValue(right) ?? literalValue(left);
219
224
  const target = literalValue(right) !== undefined ? left : right;
220
- const binding = value === undefined ? undefined : this.narrowableBinding(target);
221
- if (value === undefined || !binding?.type)
225
+ const found = value === undefined ? undefined : this.narrowable(target);
226
+ if (value === undefined || !found)
222
227
  return [];
223
- const current = this.flow.get(binding) ?? binding.type;
228
+ const { place, type: current } = found;
224
229
  if (!unionMembers(nonNull(current)).some((member) => member.kind === 'literal'))
225
230
  return [];
226
- return [[binding, equal ? literal(value) : withoutLiteral(current, value)]];
231
+ return [[place, equal ? literal(value) : withoutLiteral(current, value)]];
227
232
  }
228
233
  /** `x instanceof Date`: an instance of the class when true, the rest of a union when false. */
229
234
  narrowInstanceof(left, right, assumeTrue) {
230
- const binding = this.narrowableBinding(left);
235
+ const found = this.narrowable(left);
231
236
  const classType = this.typeOfChecked(right);
232
237
  const instance = instanceTypeOf(classType);
233
- if (!binding?.type || !instance)
238
+ if (!found || !instance)
234
239
  return [];
235
- const current = this.flow.get(binding) ?? binding.type;
240
+ const { place, type: current } = found;
236
241
  const members = unionMembers(nonNull(current));
237
242
  if (assumeTrue) {
238
243
  const kept = members.filter((member) => isAssignable(member, instance));
239
- return [[binding, kept.length > 0 ? union(kept) : instance]];
244
+ return [[place, kept.length > 0 ? union(kept) : instance]];
240
245
  }
241
246
  // A class keeps what is not its subclass; a DOM interface what is not assignable to it.
242
247
  const kept = members.filter((member) => classType.kind === 'classValue'
243
248
  ? !(member.kind === 'class' && isSubclass(member.info, classType.info))
244
249
  : !isAssignable(member, instance));
245
- return [[binding, union(isNullable(current) ? [...kept, NULL] : kept)]];
250
+ return [[place, union(isNullable(current) ? [...kept, NULL] : kept)]];
246
251
  }
247
252
  nullError(node) {
248
253
  const name = node.kind === 'Identifier' ? node.name : null;
@@ -259,13 +264,16 @@ export class ControlFlowChecker extends StatementChecker {
259
264
  const binding = this.lookupValue(node.name);
260
265
  return binding && NARROWABLE.has(binding.kind) ? binding : undefined;
261
266
  }
262
- /** An assignment to a variable ends its narrowing. */
267
+ /** An assignment to a variable ends its narrowing; one to a path, that of the paths it changes. */
263
268
  forget(target) {
264
- const binding = this.narrowableBinding(target);
265
- if (binding)
266
- this.flow.delete(binding);
269
+ const path = pathOf(target);
270
+ const binding = path ? this.lookupValue(path[0]) : undefined;
271
+ if (path && binding && NARROWABLE.has(binding.kind)) {
272
+ forgetWritten(this.flow, binding, path.slice(1).join('.'), (place, field) => this.isTagOf(place, field));
273
+ }
267
274
  }
268
- dropNarrowing(names) {
269
- this.flow = new Map([...this.flow].filter(([binding]) => !names.has(binding.name)));
275
+ /** Ends the narrowing of what `node` assigns: before a loop, or a statement with several ways. */
276
+ dropNarrowing(node) {
277
+ this.flow = withoutAssigned(this.flow, assignedNames(node), assignedPaths(node), (place, field) => this.isTagOf(place, field));
270
278
  }
271
279
  }
@@ -1,7 +1,7 @@
1
1
  import type * as ast from '../ast.ts';
2
- import { ImportChecker } from './imports.ts';
2
+ import { SharedStateChecker } from './shared-state.ts';
3
3
  import { type FunctionType, type ObjectType, type Type, type TypeParam } from './types.ts';
4
- export declare abstract class DeclarationChecker extends ImportChecker {
4
+ export declare abstract class DeclarationChecker extends SharedStateChecker {
5
5
  /**
6
6
  * Declares the names of a statement list first (so functions, classes and types can be used
7
7
  * before their declaration), then checks the statements in order. Function and method bodies are
@@ -3,12 +3,12 @@ import { isIterator } from "../iterators.js";
3
3
  import { collectValueNames } from "../names.js";
4
4
  import { recursiveComponents } from "../recursion.js";
5
5
  import { Scope } from "./context.js";
6
- import { ImportChecker } from "./imports.js";
6
+ import { SharedStateChecker } from "./shared-state.js";
7
7
  import { awaited, isPromise, promiseOf } from "./promise.js";
8
8
  import { createClass, func, typeToString, UNKNOWN, } from "./types.js";
9
9
  // Declarations of a list of statements: imports, types, functions, components, decorators and
10
10
  // classes, declared before the statements are checked, so that they can refer to each other.
11
- export class DeclarationChecker extends ImportChecker {
11
+ export class DeclarationChecker extends SharedStateChecker {
12
12
  /**
13
13
  * Declares the names of a statement list first (so functions, classes and types can be used
14
14
  * before their declaration), then checks the statements in order. Function and method bodies are
@@ -95,6 +95,7 @@ export class DeclarationChecker extends ImportChecker {
95
95
  freeNames: null,
96
96
  wrapper: null,
97
97
  needed: [],
98
+ included: [],
98
99
  moduleNames: [],
99
100
  expressionTypes: null,
100
101
  inline: null,
@@ -20,6 +20,10 @@ export class DecoratorExportChecker extends DecoratorWrapperChecker {
20
20
  // Other modules can read the variables of this one, but not assign them.
21
21
  const assigned = assignedNames(node.body);
22
22
  for (const used of info.moduleNames) {
23
+ if (this.moduleScope.values.get(used)?.kind === 'state') {
24
+ this.error(`@${name} is exported, so its code cannot read "${used}", state of its module, yet: read it in a component of the module and pass it to @${name} as an argument`, node.name);
25
+ continue;
26
+ }
23
27
  if (!assigned.has(used))
24
28
  continue;
25
29
  this.error(`@${name} is exported, so it cannot change "${used}" of its module: other modules can only read it; change it in a function of the module`, node.name);
@@ -0,0 +1,9 @@
1
+ import type { DecoratorInfo } from './context.ts';
2
+ import { DecoratorChecker } from './decorators.ts';
3
+ export declare abstract class DecoratorIncludeChecker extends DecoratorChecker {
4
+ protected resolveDecoratorParams(info: DecoratorInfo): void;
5
+ /** The decorators it includes are checked before it: see orderByNeeds. */
6
+ protected checkDecoratorBody(info: DecoratorInfo): void;
7
+ /** The public members of the decorators it includes become its own; each name comes once. */
8
+ private addIncludedMembers;
9
+ }
@@ -0,0 +1,71 @@
1
+ import { DecoratorChecker } from "./decorators.js";
2
+ import { UNKNOWN } from "./types.js";
3
+ // Decorators that include others, `dec shop(margin string) includes visible(margin), cart { ... }`:
4
+ // those are applied with it, above its own body, their arguments see its parameters, and their
5
+ // public members are its own for the component, `@shop.count`.
6
+ export class DecoratorIncludeChecker extends DecoratorChecker {
7
+ resolveDecoratorParams(info) {
8
+ super.resolveDecoratorParams(info);
9
+ const { node } = info;
10
+ const current = node.name.name;
11
+ const included = new Set();
12
+ for (const use of node.includes) {
13
+ const name = use.name.name;
14
+ const other = this.decorators.get(name);
15
+ if (name === current)
16
+ this.error(`@${name} cannot include itself`, use.name);
17
+ else if (included.has(name))
18
+ this.error(`@${name} is included twice: name it once`, use.name);
19
+ else if (node.needs.some((need) => need.name === name)) {
20
+ this.error(`@${current} both needs and includes @${name}: includes is enough`, use.name);
21
+ }
22
+ else if (!other)
23
+ this.error(`unknown decorator @${name}`, use.name);
24
+ else
25
+ info.included.push(other);
26
+ included.add(name);
27
+ }
28
+ }
29
+ /** The decorators it includes are checked before it: see orderByNeeds. */
30
+ checkDecoratorBody(info) {
31
+ super.checkDecoratorBody(info);
32
+ const { node } = info;
33
+ const params = node.params.map((param, i) => [
34
+ param.name,
35
+ info.params[i] ?? UNKNOWN,
36
+ ]);
37
+ for (const use of node.includes) {
38
+ const included = this.decorators.get(use.name.name);
39
+ if (included)
40
+ this.checkDecoratorArguments(use, included, params);
41
+ }
42
+ this.addIncludedMembers(info);
43
+ // A module that applies it would need the code of those it includes too.
44
+ if (node.exported && node.includes.length > 0) {
45
+ const names = node.includes.map((use) => `@${use.name.name}`).join(' ');
46
+ this.error(`@${node.name.name} is exported, so it cannot include decorators yet: apply them before it, e.g. ${names} @${node.name.name} comp ...`, node.includes[0]);
47
+ }
48
+ }
49
+ /** The public members of the decorators it includes become its own; each name comes once. */
50
+ addIncludedMembers(info) {
51
+ const current = info.node.name.name;
52
+ const from = new Map();
53
+ for (const included of info.included) {
54
+ const name = included.node.name.name;
55
+ const use = info.node.includes.find((each) => each.name.name === name);
56
+ for (const [member, found] of included.members) {
57
+ const other = from.get(member);
58
+ if (other !== undefined) {
59
+ this.error(`@${current} gets "${member}" from both @${other} and @${name}: rename one of them`, use);
60
+ }
61
+ else if (info.members.has(member)) {
62
+ this.error(`@${current} declares "${member}", which it also gets from @${name}: rename one of them`, use);
63
+ }
64
+ else {
65
+ info.members.set(member, found);
66
+ from.set(member, name);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
@@ -1,26 +1,24 @@
1
1
  import type * as ast from '../ast.ts';
2
- import type { DecoratorInfo, PublicMember } from './context.ts';
3
- import { DecoratorChecker } from './decorators.ts';
2
+ import { type DecoratorInfo, type PublicMember } from './context.ts';
3
+ import { DecoratorIncludeChecker } from './decorator-includes.ts';
4
4
  import { type Type } from './types.ts';
5
- export declare abstract class DecoratorMemberChecker extends DecoratorChecker {
6
- /** `@visible.show`: a public function; state and constants are read with `get`. */
5
+ export declare abstract class DecoratorMemberChecker extends DecoratorIncludeChecker {
6
+ /** `@visible.shown` or `@visible.show`: public state, a constant or a function. */
7
7
  protected checkDecoratorMember(node: ast.DecoratorMember): Type;
8
- /** `get(@visible.shown)`: public state or a constant. */
9
- protected checkDecoratorGet(node: ast.DecoratorGet): Type;
10
8
  /**
11
9
  * A public member of a decorator applied to the component being checked, or of one that the
12
10
  * decorator being checked needs.
13
11
  */
14
12
  protected findDecoratorMember(node: ast.DecoratorMember): PublicMember | null;
15
- /** `get(@cart.count)` in `dec cartBadge() needs cart`. */
13
+ /** `@cart.count` in `dec cartBadge() needs cart`. */
16
14
  protected neededMember(node: ast.DecoratorMember, decorator: ast.DecoratorDeclaration): PublicMember | null;
17
15
  protected publicMember(node: ast.DecoratorMember, info: DecoratorInfo): PublicMember | null;
18
16
  /**
19
- * Members of a decorator are read-only outside it: `get(@cart.count) = 0`, `get(@cart.items)[0]
20
- * = x`. Returns whether `target` is one of them.
17
+ * Members of a decorator are read-only outside it: `@cart.count = 0`, `@cart.items[0] = x`.
18
+ * Returns whether `target` is one of them.
21
19
  */
22
20
  protected checkDecoratorWrite(target: ast.Expression): boolean;
23
- /** `get(@cart.items).push(x)`: a method that changes the array. */
21
+ /** `@cart.items.push(x)`: a method that changes the array. */
24
22
  protected checkDecoratorMutation(node: ast.CallExpression): void;
25
23
  protected readOnlyError(member: ast.DecoratorMember, node: ast.NodeBase): void;
26
24
  /** `card.count` without `@`: `card` is not a value, but a decorator may have that name. */