rooklang 0.3.4 → 0.4.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 (123) hide show
  1. package/README.md +2 -0
  2. package/README.ru.md +2 -0
  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/checker/assignments.d.ts +2 -0
  7. package/dist/checker/assignments.js +9 -3
  8. package/dist/checker/base.d.ts +8 -2
  9. package/dist/checker/base.js +3 -3
  10. package/dist/checker/builtins.js +7 -3
  11. package/dist/checker/channels.js +1 -2
  12. package/dist/checker/classes.js +7 -1
  13. package/dist/checker/closing.js +1 -1
  14. package/dist/checker/completion.d.ts +1 -1
  15. package/dist/checker/completion.js +2 -1
  16. package/dist/checker/component-bindings.d.ts +7 -0
  17. package/dist/checker/component-bindings.js +45 -0
  18. package/dist/checker/component-values.js +10 -7
  19. package/dist/checker/components.d.ts +2 -2
  20. package/dist/checker/components.js +8 -3
  21. package/dist/checker/context.d.ts +28 -5
  22. package/dist/checker/context.js +23 -0
  23. package/dist/checker/control-flow.d.ts +6 -5
  24. package/dist/checker/control-flow.js +48 -40
  25. package/dist/checker/declarations.d.ts +2 -2
  26. package/dist/checker/declarations.js +3 -2
  27. package/dist/checker/decorator-exports.js +4 -0
  28. package/dist/checker/decorator-includes.d.ts +9 -0
  29. package/dist/checker/decorator-includes.js +71 -0
  30. package/dist/checker/decorator-members.d.ts +8 -10
  31. package/dist/checker/decorator-members.js +28 -31
  32. package/dist/checker/decorator-wrappers.d.ts +1 -1
  33. package/dist/checker/decorator-wrappers.js +3 -2
  34. package/dist/checker/decorators.d.ts +14 -5
  35. package/dist/checker/decorators.js +52 -25
  36. package/dist/checker/expressions.d.ts +1 -1
  37. package/dist/checker/expressions.js +7 -6
  38. package/dist/checker/flow.d.ts +25 -3
  39. package/dist/checker/flow.js +48 -8
  40. package/dist/checker/functions.d.ts +2 -2
  41. package/dist/checker/functions.js +9 -5
  42. package/dist/checker/helpers.d.ts +4 -1
  43. package/dist/checker/helpers.js +7 -2
  44. package/dist/checker/imports.js +5 -2
  45. package/dist/checker/loops.d.ts +2 -0
  46. package/dist/checker/loops.js +27 -2
  47. package/dist/checker/markup.js +14 -2
  48. package/dist/checker/members.js +3 -1
  49. package/dist/checker/shared-state.d.ts +11 -0
  50. package/dist/checker/shared-state.js +37 -0
  51. package/dist/checker/statements.d.ts +5 -2
  52. package/dist/checker/statements.js +51 -22
  53. package/dist/checker/tagged-unions.d.ts +37 -0
  54. package/dist/checker/tagged-unions.js +132 -0
  55. package/dist/checker/untyped-globals.d.ts +1 -0
  56. package/dist/checker/untyped-globals.js +3 -1
  57. package/dist/codegen/attributes.d.ts +4 -0
  58. package/dist/codegen/attributes.js +24 -2
  59. package/dist/codegen/component-helpers.d.ts +6 -1
  60. package/dist/codegen/component-helpers.js +6 -1
  61. package/dist/codegen/component-props.d.ts +11 -2
  62. package/dist/codegen/component-props.js +50 -5
  63. package/dist/codegen/components.d.ts +6 -5
  64. package/dist/codegen/components.js +44 -21
  65. package/dist/codegen/control-flow.d.ts +2 -0
  66. package/dist/codegen/control-flow.js +23 -9
  67. package/dist/codegen/creation.js +5 -2
  68. package/dist/codegen/early-returns.d.ts +4 -3
  69. package/dist/codegen/early-returns.js +17 -5
  70. package/dist/codegen/emitter.d.ts +3 -0
  71. package/dist/codegen/emitter.js +18 -4
  72. package/dist/codegen/expressions.js +0 -2
  73. package/dist/codegen/functions.d.ts +11 -1
  74. package/dist/codegen/functions.js +31 -3
  75. package/dist/codegen/live.d.ts +60 -0
  76. package/dist/codegen/live.js +29 -0
  77. package/dist/codegen/mutations.d.ts +7 -0
  78. package/dist/codegen/mutations.js +43 -2
  79. package/dist/codegen/options.d.ts +2 -0
  80. package/dist/codegen/reactive.d.ts +4 -59
  81. package/dist/codegen/reactive.js +34 -72
  82. package/dist/codegen/runtime.js +6 -1
  83. package/dist/codegen/shared.d.ts +22 -0
  84. package/dist/codegen/shared.js +93 -0
  85. package/dist/codegen/simple-statements.d.ts +1 -1
  86. package/dist/codegen/statements.d.ts +5 -0
  87. package/dist/codegen/statements.js +35 -0
  88. package/dist/codegen/updates.d.ts +11 -3
  89. package/dist/codegen/updates.js +52 -4
  90. package/dist/codegen/web-components.d.ts +2 -2
  91. package/dist/codegen/web-components.js +2 -2
  92. package/dist/compile.js +6 -2
  93. package/dist/decorator-copy.d.ts +1 -1
  94. package/dist/decorator-copy.js +1 -0
  95. package/dist/decorator-names.d.ts +14 -0
  96. package/dist/decorator-names.js +56 -0
  97. package/dist/decorators.d.ts +2 -3
  98. package/dist/decorators.js +36 -14
  99. package/dist/inline-packages.d.ts +2 -1
  100. package/dist/inline-packages.js +9 -4
  101. package/dist/names.d.ts +4 -0
  102. package/dist/names.js +29 -0
  103. package/dist/parser/base.d.ts +1 -0
  104. package/dist/parser/base.js +1 -1
  105. package/dist/parser/control-flow.d.ts +5 -0
  106. package/dist/parser/control-flow.js +28 -2
  107. package/dist/parser/declarations.d.ts +5 -15
  108. package/dist/parser/declarations.js +15 -119
  109. package/dist/parser/decorators.d.ts +6 -2
  110. package/dist/parser/decorators.js +43 -19
  111. package/dist/parser/params.d.ts +21 -0
  112. package/dist/parser/params.js +143 -0
  113. package/dist/parser/simple-statements.d.ts +1 -0
  114. package/dist/parser/simple-statements.js +22 -6
  115. package/dist/parser/statements.d.ts +5 -2
  116. package/dist/parser/statements.js +18 -11
  117. package/dist/runtime/sources.js +3 -2
  118. package/dist/walk.js +3 -3
  119. package/package.json +2 -2
  120. package/src/runtime/decode.js +41 -10
  121. package/src/runtime/index.js +1 -0
  122. package/src/runtime/markup.js +27 -17
  123. package/src/runtime/shared.js +17 -0
package/README.md CHANGED
@@ -58,6 +58,8 @@ close(results)
58
58
  The guide with examples and a playground: <https://qwern-9876r.github.io/RookLang/>.
59
59
  Specification: [docs/spec.md](docs/spec.md), design of components:
60
60
  [docs/components.md](docs/components.md), an example: [examples/hello.rk](examples/hello.rk).
61
+ Where Rook stands and the road to 1.0: [docs/roadmap.md](docs/roadmap.md); what changed in each
62
+ version: [CHANGELOG.md](CHANGELOG.md).
61
63
 
62
64
  ## Installation
63
65
 
package/README.ru.md CHANGED
@@ -58,6 +58,8 @@ close(results)
58
58
  Руководство с примерами и песочницей: <https://qwern-9876r.github.io/RookLang/ru/>.
59
59
  Спецификация: [docs/spec.ru.md](docs/spec.ru.md), концепция компонентов:
60
60
  [docs/components.ru.md](docs/components.ru.md), пример: [examples/hello.rk](examples/hello.rk).
61
+ Где Rook сейчас и путь к 1.0: [docs/roadmap.ru.md](docs/roadmap.ru.md); что изменилось в каждой
62
+ версии: [CHANGELOG.ru.md](CHANGELOG.ru.md).
61
63
 
62
64
  ## Установка
63
65
 
@@ -1,4 +1,4 @@
1
- import type { Expression, Identifier, StringLiteral } from './expressions.ts';
1
+ import type { Expression, FuncExpression, Identifier, StringLiteral } from './expressions.ts';
2
2
  import type { TypeMember, TypeNode, TypeReference } from './markup.ts';
3
3
  import type { BlockStatement, Statement } from './statements.ts';
4
4
  export interface NodeBase {
@@ -57,6 +57,11 @@ export interface Parameter extends NodeBase {
57
57
  * properties, which `{...attrs}` puts on one of its elements.
58
58
  */
59
59
  rest?: true;
60
+ /**
61
+ * `bind active string`: a property of a component that it may change. `bind:active={tab}`
62
+ * writes the changes into the parent's `tab`; without it the property is the component's state.
63
+ */
64
+ bind?: true;
60
65
  /**
61
66
  * `constructor(private name string)`: the parameter is a property too, which the constructor
62
67
  * assigns, as in TypeScript.
@@ -97,6 +102,11 @@ export interface DecoratorDeclaration extends NodeBase {
97
102
  params: Parameter[];
98
103
  /** `needs cart, auth`: the decorators whose public members it uses; they are applied above it. */
99
104
  needs: Identifier[];
105
+ /**
106
+ * `includes visible(margin), cart`: decorators applied with it, above its own body, whose
107
+ * arguments see its parameters. Their public members are its own for the component, `@shop.count`.
108
+ */
109
+ includes: DecoratorUse[];
100
110
  body: BlockStatement;
101
111
  }
102
112
  /** `@visible("300px")` or `@visible` before `comp`; the arguments are positional. */
@@ -118,14 +128,20 @@ export interface VariableDeclaration extends NodeBase {
118
128
  exported: boolean;
119
129
  keyword: 'let' | 'const' | 'state' | 'derived' | 'ref';
120
130
  /**
121
- * `public state`, `public derived` or `public const` in a decorator: the component reads it with
122
- * `get(@name.x)`.
131
+ * `public state`, `public derived` or `public const` in a decorator: the component reads it as
132
+ * `@name.x`.
123
133
  */
124
134
  isPublic: boolean;
125
135
  /** `_` is an ordinary identifier here; the checker treats it as "skip this value". */
126
136
  names: Identifier[];
127
137
  type: TypeNode | null;
128
138
  values: Expression[];
139
+ /**
140
+ * `derived summary string { ...; return text }`: the block that computes a derived value, a
141
+ * function without parameters, so that what looks for the `return`s, `defer`s and `break`s of a
142
+ * component stops at it, as at other functions. Its result type is `type`; `values` is empty.
143
+ */
144
+ body: FuncExpression | null;
129
145
  }
130
146
  /** `class Admin extends User implements Shape { ... }` */
131
147
  export interface ClassDeclaration extends NodeBase {
@@ -1,7 +1,7 @@
1
1
  import type { NodeBase, Parameter } from './declarations.ts';
2
2
  import type { ElementExpression, TypeNode } from './markup.ts';
3
3
  import type { BlockStatement } from './statements.ts';
4
- export type Expression = Identifier | NumberLiteral | BigIntLiteral | StringLiteral | RegExpLiteral | TemplateLiteral | BooleanLiteral | NullLiteral | ThisExpression | SuperExpression | ArrayLiteral | ObjectLiteral | FuncExpression | ArrowFunction | UnaryExpression | BinaryExpression | ConditionalExpression | CallExpression | NewExpression | MemberExpression | IndexExpression | ElementExpression | DecoratorMember | DecoratorGet | AsExpression | GoExpression | ChanExpression | StreamChanExpression | QueueExpression | TopicExpression | DecodeExpression | TryExpression | ConversionExpression | MakeExpression;
4
+ export type Expression = Identifier | NumberLiteral | BigIntLiteral | StringLiteral | RegExpLiteral | TemplateLiteral | BooleanLiteral | NullLiteral | ThisExpression | SuperExpression | ArrayLiteral | ObjectLiteral | FuncExpression | ArrowFunction | UnaryExpression | BinaryExpression | ConditionalExpression | CallExpression | NewExpression | MemberExpression | IndexExpression | ElementExpression | DecoratorMember | AsExpression | GoExpression | ChanExpression | StreamChanExpression | QueueExpression | TopicExpression | DecodeExpression | TryExpression | ConversionExpression | MakeExpression;
5
5
  /**
6
6
  * `[]byte("rook")`: the UTF-8 bytes of a string. `string(data)`, the way back, is a call, since
7
7
  * `string` may be the name of a value.
@@ -231,16 +231,12 @@ export interface IndexExpression extends NodeBase {
231
231
  optional: boolean;
232
232
  }
233
233
  /**
234
- * `@visible.shown`: a public member of a decorator applied to the component. The names belong to
235
- * the decorator, not to any scope, so walkers do not visit them as identifiers.
234
+ * `@visible.shown` or `@visible.show()`: a public member of a decorator applied to the component,
235
+ * read or called. The names belong to the decorator, not to any scope, so walkers do not visit them
236
+ * as identifiers.
236
237
  */
237
238
  export interface DecoratorMember extends NodeBase {
238
239
  kind: 'DecoratorMember';
239
240
  decorator: Identifier;
240
241
  member: Identifier;
241
242
  }
242
- /** `get(@visible.shown)`: reads public state or a constant of a decorator. */
243
- export interface DecoratorGet extends NodeBase {
244
- kind: 'DecoratorGet';
245
- target: DecoratorMember;
246
- }
@@ -86,6 +86,8 @@ export interface ForInStatement extends NodeBase {
86
86
  key: Identifier | null;
87
87
  value: Identifier;
88
88
  iterable: Expression;
89
+ /** `with key todo.id` of a `{for}` in markup: what its blocks are found again by. */
90
+ withKey?: Expression;
89
91
  body: BlockStatement;
90
92
  go?: boolean;
91
93
  }
@@ -156,7 +158,7 @@ export interface DeferStatement extends NodeBase {
156
158
  body: CallExpression | BlockStatement;
157
159
  }
158
160
  /**
159
- * `mount() { ... }` at the top level of a component: runs once the component's markup is in the
161
+ * `mount { ... }` at the top level of a component: runs once the component's markup is in the
160
162
  * document. It may return a function, which runs when `{if}`, `{switch}` or `{for}` removes the
161
163
  * markup.
162
164
  */
@@ -2,6 +2,8 @@ import type * as ast from '../ast.ts';
2
2
  import { InitializerChecker } from './initializers.ts';
3
3
  import { type Type } from './types.ts';
4
4
  export declare abstract class AssignmentChecker extends InitializerChecker {
5
+ /** The name of the `derived` whose block is being checked: it computes, so it changes no state. */
6
+ protected derivedBlock: string | null;
5
7
  /**
6
8
  * `a, b = f()` or `return f()`: the call must return exactly `count` values. Returns their
7
9
  * types.
@@ -1,3 +1,4 @@
1
+ import { placeOf } from "./context.js";
1
2
  import { guardedByError, unpackedResults } from "./error-results.js";
2
3
  import { isUntyped, countValues, callName, isCall } from "./helpers.js";
3
4
  import { InitializerChecker } from "./initializers.js";
@@ -5,6 +6,8 @@ import { BOOL, ANY, isAssignable, isNullable, nonNull, nullable, typeToString, U
5
6
  // Where values go: assignments to variables, fields and elements, and `return`. Both unpack a call
6
7
  // that returns several values.
7
8
  export class AssignmentChecker extends InitializerChecker {
9
+ /** The name of the `derived` whose block is being checked: it computes, so it changes no state. */
10
+ derivedBlock = null;
8
11
  /**
9
12
  * `a, b = f()` or `return f()`: the call must return exactly `count` values. Returns their
10
13
  * types.
@@ -66,13 +69,13 @@ export class AssignmentChecker extends InitializerChecker {
66
69
  const binding = this.narrowableBinding(target);
67
70
  const declared = binding?.type;
68
71
  if (binding && declared && isNullable(declared) && !isNullable(valueTypes[i] ?? UNKNOWN)) {
69
- this.flow.set(binding, nonNull(declared));
72
+ this.flow.set(placeOf(binding), nonNull(declared));
70
73
  }
71
74
  });
72
75
  }
73
76
  /** The type a value assigned to `target` must have. */
74
77
  checkTarget(target) {
75
- if (this.checkDecoratorWrite(target))
78
+ if (this.checkDecoratorWrite(target) || this.checkSharedWrite(target))
76
79
  return UNKNOWN;
77
80
  switch (target.kind) {
78
81
  case 'Identifier': {
@@ -92,13 +95,16 @@ export class AssignmentChecker extends InitializerChecker {
92
95
  else if (binding.kind === 'ref') {
93
96
  this.error(`cannot assign to "${target.name}": a ref always points to its element`, target);
94
97
  }
98
+ else if (binding.kind === 'state' && this.derivedBlock !== null) {
99
+ this.error(`derived "${this.derivedBlock}" cannot change the state "${target.name}": change it in an event handler or an effect`, target);
100
+ }
95
101
  else if (binding.kind === 'loop') {
96
102
  this.error(`cannot assign to loop variable "${target.name}"`, target);
97
103
  }
98
104
  else if (binding.kind === 'prop') {
99
105
  this.error(this.decorator
100
106
  ? `cannot assign to "${target.name}": parameters of a decorator are read-only`
101
- : `cannot assign to "${target.name}": component properties are read-only; to change the parent's state, pass a function`, target);
107
+ : `cannot assign to "${target.name}": component properties are read-only; to change it, declare it with bind, e.g. comp ${this.component?.node.name.name ?? 'Tabs'}(bind ${target.name} ...)`, target);
102
108
  }
103
109
  else if (binding.kind !== 'let' &&
104
110
  binding.kind !== 'state' &&
@@ -1,6 +1,6 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import type { Diagnostic } from '../diagnostics.ts';
3
- import { type CheckOptions, type BindingKind, type Binding, type ComponentInfo, type DecoratorInfo, type AliasEntry, Scope, type FunctionContext, type ClassContext, type Flow } from './context.ts';
3
+ import { type CheckOptions, type BindingKind, type Binding, type ComponentInfo, type DecoratorInfo, type AliasEntry, Scope, type FunctionContext, type ClassContext, type Flow, type Place } from './context.ts';
4
4
  import { type ClassInfo, type FunctionType, type Member, type Type, type TypeParam } from './types.ts';
5
5
  export declare abstract class CheckerBase {
6
6
  protected abstract checkClassBodies(node: ast.ClassDeclaration, info: ClassInfo): void;
@@ -18,6 +18,13 @@ export declare abstract class CheckerBase {
18
18
  protected abstract checkTry(node: ast.TryStatement): void;
19
19
  protected abstract forget(target: ast.Expression): void;
20
20
  protected abstract narrowableBinding(node: ast.Expression): Binding | undefined;
21
+ /** A variable or a path of data fields that conditions narrow, with its type at this point. */
22
+ protected abstract narrowable(node: ast.Expression): {
23
+ place: Place;
24
+ type: Type;
25
+ } | undefined;
26
+ /** Whether an assignment to `field` of `place` may change the member of a union it is. */
27
+ protected abstract isTagOf(place: Place, field: string): boolean;
21
28
  /** The error for a value that may be null where it must not be. */
22
29
  protected abstract nullError(node: ast.Expression): void;
23
30
  protected abstract checkFunction(params: readonly ast.Parameter[], type: FunctionType, results: Type[] | null, body: ast.BlockStatement | ast.Expression, options?: {
@@ -44,7 +51,6 @@ export declare abstract class CheckerBase {
44
51
  protected abstract orderByNeeds(decorators: readonly DecoratorInfo[]): DecoratorInfo[];
45
52
  protected abstract wrappedType(info: ComponentInfo, own: Type, seen: Set<ComponentInfo>): Type;
46
53
  protected abstract checkDecoratorMember(node: ast.DecoratorMember): Type;
47
- protected abstract checkDecoratorGet(node: ast.DecoratorGet): Type;
48
54
  protected abstract checkDecoratorWrite(target: ast.Expression): boolean;
49
55
  protected abstract checkComponentUse(node: ast.ElementExpression, tag: ast.Identifier): Type;
50
56
  protected abstract checkRefDeclaration(node: ast.VariableDeclaration): void;
@@ -4,9 +4,9 @@ import { Scope, globalScope, } from "./context.js";
4
4
  import { DOCUMENT_FRAGMENT, eventType, HTML_ELEMENT, NODE } from "./dom.js";
5
5
  import { explainMismatch, isAssignable, LazyMap, memberNames, nonNull, property, typeToString, UNKNOWN, } from "./types.js";
6
6
  // The type checker is a chain of layers, one per area, like the code generator: base → imports →
7
- // declarations → class members → classes → resolution → initializers → assignments → statements → control flow →
8
- // functions → expressions → array literals → operators → markup → forwarding → components → component values →
9
- // refs → styles → decorator wrappers → decorator exports → decorators → decorator members →
7
+ // shared state → declarations → class members → classes → resolution → initializers → assignments → statements → control flow →
8
+ // tagged unions → functions → expressions → array literals → operators → markup → forwarding → component bindings → components → component values →
9
+ // refs → styles → decorator wrappers → decorator exports → decorators → decorator includes → decorator members →
10
10
  // members → calls → goroutines → channels → signals → queues → topics → decode → try → bytes → loops → go for →
11
11
  // iterators → closing → completion → Checker (checker.ts). Statements, expressions and markup
12
12
  // check each other recursively, so methods of later layers that earlier ones call are declared
@@ -6,7 +6,7 @@ import { chanOf } from "./channel.js";
6
6
  import { CHANNEL_OBJECT, TOPIC_OBJECT } from "./channel-objects.js";
7
7
  import { PROMISE } from "./promise.js";
8
8
  import { BUFFERS } from "./transfer.js";
9
- import { UNTYPED_GLOBALS } from "./untyped-globals.js";
9
+ import { UNTYPED_CLASSES, UNTYPED_GLOBALS } from "./untyped-globals.js";
10
10
  import { ANY, arrayOf, BIGINT, BOOL, ERROR, ERROR_CLASS, func, method, NUMBER, nullable, property, STRING, topicOf, union, } from "./types.js";
11
11
  // Types of the JS standard library that Rook programs use most. Anything not listed here
12
12
  // can still be reached through the untyped globals below.
@@ -249,7 +249,10 @@ export const GLOBAL_VALUES = new Map([
249
249
  ['cancel', func([ANY], [])],
250
250
  ['Channel', CHANNEL_OBJECT],
251
251
  ['Topic', TOPIC_OBJECT],
252
- ...[...UNTYPED_GLOBALS, ...BUFFERS].map((name) => [name, ANY]),
252
+ ...[...UNTYPED_GLOBALS, ...UNTYPED_CLASSES, ...BUFFERS].map((name) => [
253
+ name,
254
+ ANY,
255
+ ]),
253
256
  ]);
254
257
  /** Built-in types that a library from TypeScript's lib files replaces: untyped ones and the DOM. */
255
258
  export const LIBRARY_TYPES = new Set([
@@ -260,6 +263,7 @@ export const LIBRARY_TYPES = new Set([
260
263
  'Promise',
261
264
  'URL',
262
265
  'Event',
266
+ ...UNTYPED_CLASSES,
263
267
  ...BUFFERS,
264
268
  ...[...DOM_TYPES.keys()].filter((name) => name !== 'Content'),
265
269
  ...SVG_TYPES.keys(),
@@ -278,7 +282,7 @@ export const GLOBAL_TYPES = new Map([
278
282
  ['any', ANY],
279
283
  ['error', ERROR],
280
284
  ['Error', ERROR_CLASS.instance],
281
- ...['Date', 'RegExp', 'Map', 'Set', 'URL', ...BUFFERS].map((name) => [name, ANY]),
285
+ ...['Date', 'RegExp', 'Map', 'Set', 'URL', ...UNTYPED_CLASSES, ...BUFFERS].map((name) => [name, ANY]),
282
286
  ['Promise', PROMISE],
283
287
  ...DOM_TYPES,
284
288
  ...SVG_TYPES,
@@ -1,4 +1,3 @@
1
- import { assignedNames } from "../names.js";
2
1
  import { cannotWait } from "../parser/syntax.js";
3
2
  import { chanDirection, chanOf, elementOf, isChan } from "./channel.js";
4
3
  import { GoroutineChecker } from "./goroutines.js";
@@ -115,7 +114,7 @@ export class ChannelChecker extends GoroutineChecker {
115
114
  : ANY;
116
115
  }
117
116
  checkSelect(node) {
118
- this.dropNarrowing(assignedNames(node));
117
+ this.dropNarrowing(node);
119
118
  const entry = this.flow;
120
119
  for (const { names, operation, body } of node.cases) {
121
120
  this.flow = new Map(entry);
@@ -195,6 +195,7 @@ export class ClassChecker extends ClassMemberChecker {
195
195
  types: new Map(),
196
196
  decorators: new Map(),
197
197
  components: new Map(),
198
+ shared: new Set(),
198
199
  };
199
200
  const declarations = { values: new Map(), types: new Map() };
200
201
  const valueOf = (name) => this.moduleScope.values.get(name)?.type ?? UNKNOWN;
@@ -217,6 +218,8 @@ export class ClassChecker extends ClassMemberChecker {
217
218
  case 'VariableDeclaration':
218
219
  for (const name of statement.names) {
219
220
  add(statement.exported, 'values', name.name, valueOf(name.name));
221
+ if (statement.exported && statement.keyword === 'state')
222
+ exports.shared.add(name.name);
220
223
  }
221
224
  break;
222
225
  case 'ClassDeclaration':
@@ -230,7 +233,10 @@ export class ClassChecker extends ClassMemberChecker {
230
233
  case 'ComponentDeclaration': {
231
234
  const info = this.moduleScope.values.get(statement.name.name)?.component;
232
235
  if (statement.exported && info?.node === statement) {
233
- info.inline = this.modulePackages().components.get(info.node.name.name) ?? null;
236
+ const found = this.modulePackages().components.get(info.node.name.name) ?? null;
237
+ // Code that reads state another module shares listens to it here, so it stays here.
238
+ const shared = found?.moduleNames.some((name) => this.moduleScope.values.get(name)?.shared !== undefined);
239
+ info.inline = shared ? null : found;
234
240
  info.expressionTypes = this.checkedTypes;
235
241
  exports.components.set(statement.name.name, info);
236
242
  }
@@ -4,7 +4,7 @@ import { IteratorChecker } from "./iterators.js";
4
4
  const OPERATIONS = new Set(['send', 'receive', 'range', 'stream', 'publish', 'listen', 'try']);
5
5
  const MESSAGES = {
6
6
  chan: (name) => `"${name}" is never closed: close it when nothing more is sent, e.g. close(${name})`,
7
- topic: (name) => `"${name}" is never closed, and a topic listens until it is closed: close it when it is no longer needed, e.g. close(${name}), in a component in the function that mount() returns`,
7
+ topic: (name) => `"${name}" is never closed, and a topic listens until it is closed: close it when it is no longer needed, e.g. close(${name}); a component closes it with defer close(${name}) in mount`,
8
8
  tick: (name) => `"${name}" is never closed, and a tick runs until it is closed: e.g. close(${name})`,
9
9
  signal: (name) => `"${name}" is never closed, and it keeps the program running until it is: close it once the signal has come, e.g. close(${name})`,
10
10
  };
@@ -1,6 +1,6 @@
1
1
  import type * as ast from '../ast.ts';
2
2
  import { ClosingChecker } from './closing.ts';
3
- import type { CompletionResult } from './context.ts';
3
+ import { type CompletionResult } from './context.ts';
4
4
  import { type Type } from './types.ts';
5
5
  export declare abstract class CompletionChecker extends ClosingChecker {
6
6
  protected completion: CompletionResult | null;
@@ -1,5 +1,6 @@
1
1
  import { ClosingChecker } from "./closing.js";
2
2
  import { ARRAY_MEMBERS, ATTRIBUTE_NAMES, BIGINT_MEMBERS, BOOL_MEMBERS, EVENT_ATTRIBUTES, HTML_TAGS, NUMBER_MEMBERS, STRING_MEMBERS, } from "./completion-names.js";
3
+ import { placeOf, } from "./context.js";
3
4
  import { isSubclass, literalBase, memberNames, memberTypeOf, nonNull, typeToString, union, UNKNOWN, } from "./types.js";
4
5
  // Completion in editors (src/completion.ts): the editor writes the name `options.completion` where
5
6
  // the cursor is, and this layer lists what can be written there when the check reaches that name:
@@ -79,7 +80,7 @@ export class CompletionChecker extends ClosingChecker {
79
80
  if (component) {
80
81
  return { name, kind: 'component', detail: typeToString(this.componentValue(component)) };
81
82
  }
82
- const type = binding.type && (this.flow.get(binding) ?? binding.type);
83
+ const type = binding.type && (this.flow.get(placeOf(binding)) ?? binding.type);
83
84
  const detail = type ? { detail: typeToString(type) } : {};
84
85
  if (kind === 'function' || type?.kind === 'function')
85
86
  return { name, kind: 'function', ...detail };
@@ -0,0 +1,7 @@
1
+ import type * as ast from '../ast.ts';
2
+ import type { ComponentInfo } from './context.ts';
3
+ import { ForwardingChecker } from './forwarding.ts';
4
+ export declare abstract class ComponentBindingChecker extends ForwardingChecker {
5
+ /** `bind:active={tab}` on a component; adds the name of the property to `given`. */
6
+ protected checkPropBinding(attribute: ast.JsxAttribute, info: ComponentInfo, component: string, given: Set<string>): void;
7
+ }
@@ -0,0 +1,45 @@
1
+ import { attrsParam } from "../attrs.js";
2
+ import { ForwardingChecker } from "./forwarding.js";
3
+ import { isUntyped } from "./helpers.js";
4
+ import { isAssignable, typeToString } from "./types.js";
5
+ // Bound properties: `comp Tabs(bind active string)` may change `active`, and
6
+ // `<Tabs bind:active={tab} />` writes those changes into the parent's `tab`, as `bind:value` of an
7
+ // input does. The value goes both ways, so the variable and the property have the same type.
8
+ export class ComponentBindingChecker extends ForwardingChecker {
9
+ /** `bind:active={tab}` on a component; adds the name of the property to `given`. */
10
+ checkPropBinding(attribute, info, component, given) {
11
+ const name = attribute.name.name.slice('bind:'.length);
12
+ const prop = info.props.get(name);
13
+ const { value } = attribute;
14
+ if (given.has(name))
15
+ this.error(`duplicate property "${name}"`, attribute.name);
16
+ given.add(name);
17
+ if (!prop) {
18
+ this.error(attrsParam(info.node)
19
+ ? `${attribute.name.name} does not go through the attributes of <${component}>`
20
+ : `<${component}> has no property "${name}" to bind`, attribute.name);
21
+ }
22
+ else if (!prop.bind) {
23
+ this.error(`"${name}" of <${component}> is not bound: the component declares it with bind to change it, e.g. comp ${component}(bind ${name} ...)`, attribute.name);
24
+ }
25
+ if (value === null || value.kind === 'StringLiteral' || value.kind === 'EventHandler') {
26
+ this.error(`"bind:${name}" needs a variable in braces, e.g. bind:${name}={${name}}`, attribute);
27
+ return;
28
+ }
29
+ const bindable = (value.kind === 'Identifier' && value.name !== '_') ||
30
+ ((value.kind === 'MemberExpression' || value.kind === 'IndexExpression') && !value.optional);
31
+ if (!bindable) {
32
+ this.checkValue(value);
33
+ this.error(`"bind:${name}" needs a variable or a field to write to`, value);
34
+ return;
35
+ }
36
+ const type = this.checkTarget(value);
37
+ this.checkedTypes.set(value, type);
38
+ if (!prop?.bind || isUntyped(type) || isUntyped(prop.type))
39
+ return;
40
+ // The parent gives its value, and the component writes its own back.
41
+ if (!isAssignable(type, prop.type) || !isAssignable(prop.type, type)) {
42
+ this.error(`cannot bind ${typeToString(type)} to "${name}" of <${component}>, which is ${typeToString(prop.type)}`, value);
43
+ }
44
+ }
45
+ }
@@ -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
  }