@kumikijs/compiler 0.1.0 → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,34 +1,34 @@
1
- # @kumikijs/compiler
2
-
3
- Kumiki compiler — lexer, parser, typechecker, and code generator. Part of [Kumiki](https://github.com/kage1020/Kumiki), an AI-first web framework language.
4
-
5
- ## Install
6
-
7
- ```sh
8
- npm i @kumikijs/compiler
9
- ```
10
-
11
- ## Usage
12
-
13
- ```ts
14
- import { check, compile, lex, parse } from "@kumikijs/compiler";
15
-
16
- // Type-check a .kumiki source (returns diagnostics)
17
- const diagnostics = check(source);
18
-
19
- // Compile a source into a runnable HTML app
20
- const result = compile(source);
21
- ```
22
-
23
- To inline the runtime bundle from disk in a Node environment, use the `./node` subpath:
24
-
25
- ```ts
26
- import { compile } from "@kumikijs/compiler";
27
- import { nodeRuntimeBundleReader } from "@kumikijs/compiler/node";
28
-
29
- const result = compile(source, { bundle: true, readRuntimeBundle: nodeRuntimeBundleReader });
30
- ```
31
-
32
- ## License
33
-
34
- Apache-2.0
1
+ # @kumikijs/compiler
2
+
3
+ Kumiki compiler — lexer, parser, typechecker, and code generator. Part of [Kumiki](https://github.com/kage1020/Kumiki), an AI-first web framework language.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm i @kumikijs/compiler
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { check, compile, lex, parse } from "@kumikijs/compiler";
15
+
16
+ // Type-check a .kumiki source (returns diagnostics)
17
+ const diagnostics = check(source);
18
+
19
+ // Compile a source into a runnable HTML app
20
+ const result = compile(source);
21
+ ```
22
+
23
+ To inline the runtime bundle from disk in a Node environment, use the `./node` subpath:
24
+
25
+ ```ts
26
+ import { compile } from "@kumikijs/compiler";
27
+ import { nodeRuntimeBundleReader } from "@kumikijs/compiler/node";
28
+
29
+ const result = compile(source, { bundle: true, readRuntimeBundle: nodeRuntimeBundleReader });
30
+ ```
31
+
32
+ ## License
33
+
34
+ Apache-2.0
@@ -0,0 +1,75 @@
1
+ //#region src/capabilities.ts
2
+ /** Capabilities that may appear in `app.caps` without any manifest. */
3
+ const STANDARD_CAPABILITIES = new Set([
4
+ "http.get",
5
+ "http.post",
6
+ "http.put",
7
+ "http.patch",
8
+ "http.delete",
9
+ "storage.read",
10
+ "storage.write",
11
+ "session.read",
12
+ "session.write",
13
+ "indexed.read",
14
+ "indexed.write",
15
+ "nav.push",
16
+ "nav.replace",
17
+ "nav.back",
18
+ "clipboard.read",
19
+ "clipboard.write",
20
+ "notification.show",
21
+ "analytics.send",
22
+ "log.write",
23
+ "crypto.random",
24
+ "crypto.hash",
25
+ "media.camera",
26
+ "media.microphone",
27
+ "geo.read",
28
+ "socket.connect",
29
+ "socket.send"
30
+ ]);
31
+ /** A capability name must look like `group.action` (lowercase, dot-separated). */
32
+ const CAP_NAME = /^[a-z][a-z0-9]*\.[a-z][a-z0-9-]*$/;
33
+ /**
34
+ * Validate a parsed `kumiki.caps.json` value. Accepts either bare strings or
35
+ * `{ name, description? }` objects in the `capabilities` array. Pure — the
36
+ * caller does the file read + JSON parse and reports the location.
37
+ */
38
+ function parseCapabilityManifest(raw) {
39
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {
40
+ ok: false,
41
+ error: "manifest must be a JSON object"
42
+ };
43
+ const caps = raw.capabilities;
44
+ if (!Array.isArray(caps)) return {
45
+ ok: false,
46
+ error: "\"capabilities\" must be an array"
47
+ };
48
+ const names = [];
49
+ for (let i = 0; i < caps.length; i++) {
50
+ const entry = caps[i];
51
+ const name = typeof entry === "string" ? entry : pickName(entry);
52
+ if (typeof name !== "string" || name.length === 0) return {
53
+ ok: false,
54
+ error: `capabilities[${i}] must be a string or an object with a non-empty "name"`
55
+ };
56
+ if (!CAP_NAME.test(name)) return {
57
+ ok: false,
58
+ error: `capability "${name}" must look like "group.action" (lowercase, dot-separated)`
59
+ };
60
+ if (STANDARD_CAPABILITIES.has(name)) return {
61
+ ok: false,
62
+ error: `capability "${name}" is already a standard capability — remove it from the manifest`
63
+ };
64
+ names.push(name);
65
+ }
66
+ return {
67
+ ok: true,
68
+ manifest: { capabilities: names }
69
+ };
70
+ }
71
+ function pickName(entry) {
72
+ if (typeof entry === "object" && entry !== null) return entry.name;
73
+ }
74
+ //#endregion
75
+ export { parseCapabilityManifest as n, STANDARD_CAPABILITIES as t };
package/dist/index.d.ts CHANGED
@@ -31,7 +31,16 @@ type Program = {
31
31
  kind: "Program";
32
32
  defs: Def[];
33
33
  };
34
- type Def = TypeDef | SlotDef | ReducerDef | TileDef | FnDef | EffectDef | AppDef | ThemeDef;
34
+ type Def = TypeDef | SlotDef | ReducerDef | TileDef | FnDef | EffectDef | AppDef | ThemeDef | MotionDef | TestDef;
35
+ type TestDef = {
36
+ kind: "TestDef";
37
+ name: string; /** `reducer-test` targets a reducer name; `tile-test` targets a tile name. */
38
+ testKind: "reducer-test" | "tile-test";
39
+ target: string; /** The `given = { ... }` record literal (interpreted, not codegen'd as-is). */
40
+ given: Expr; /** `expect = { slots, effects }` / `{ panic }` (record) for reducer-test; a tile expression for tile-test. */
41
+ expect: Expr | TileExpr;
42
+ pos: Pos;
43
+ };
35
44
  type ThemeValue = string | number | {
36
45
  [k: string]: ThemeValue;
37
46
  };
@@ -43,6 +52,14 @@ type ThemeDef = {
43
52
  };
44
53
  pos: Pos;
45
54
  };
55
+ type MotionDef = {
56
+ kind: "MotionDef";
57
+ name: string;
58
+ body: {
59
+ [k: string]: ThemeValue;
60
+ };
61
+ pos: Pos;
62
+ };
46
63
  type TypeDef = {
47
64
  kind: "TypeDef";
48
65
  name: string;
@@ -168,6 +185,7 @@ type EventPattern = {
168
185
  } | {
169
186
  kind: "TimerEvent";
170
187
  intervalMs: number;
188
+ name?: string;
171
189
  pos: Pos;
172
190
  } | {
173
191
  kind: "LifecycleEvent";
@@ -190,6 +208,10 @@ type Statement = {
190
208
  effect: string;
191
209
  args: Expr[];
192
210
  pos: Pos;
211
+ } | {
212
+ kind: "StopTimer";
213
+ name: string;
214
+ pos: Pos;
193
215
  } | {
194
216
  kind: "ForStmt";
195
217
  bind: string;
@@ -415,9 +437,30 @@ type TileProp = {
415
437
  value: Expr;
416
438
  };
417
439
  //#endregion
440
+ //#region src/capabilities.d.ts
441
+ /** Capabilities that may appear in `app.caps` without any manifest. */
442
+ declare const STANDARD_CAPABILITIES: ReadonlySet<string>;
443
+ type CapabilityManifest = {
444
+ capabilities: string[];
445
+ };
446
+ type ManifestResult = {
447
+ ok: true;
448
+ manifest: CapabilityManifest;
449
+ } | {
450
+ ok: false;
451
+ error: string;
452
+ };
453
+ /**
454
+ * Validate a parsed `kumiki.caps.json` value. Accepts either bare strings or
455
+ * `{ name, description? }` objects in the `capabilities` array. Pure — the
456
+ * caller does the file read + JSON parse and reports the location.
457
+ */
458
+ declare function parseCapabilityManifest(raw: unknown): ManifestResult;
459
+ //#endregion
418
460
  //#region src/codegen.d.ts
419
461
  type CodegenOptions = {
420
- runtimeSpecifier: string;
462
+ runtimeSpecifier: string; /** Emit the in-language `test` definitions (`__kumikiTests`). Off for production builds. */
463
+ includeTests?: boolean;
421
464
  };
422
465
  declare function codegen(program: Program, opts: CodegenOptions): string;
423
466
  declare const RUNTIME_HELPERS = "\nfunction _setPath(obj, path, value) {\n if (path.length === 0) return value;\n const [head, ...rest] = path;\n const cur = obj ?? {};\n return { ...cur, [head]: _setPath(cur[head], rest, value) };\n}\nfunction _children(...xs) {\n const out = [];\n for (const x of xs) {\n if (x === null || x === undefined) continue;\n if (Array.isArray(x)) {\n for (const y of x) if (y !== null && y !== undefined) out.push(y);\n } else {\n out.push(x);\n }\n }\n return out;\n}\nfunction _attachProps(node, props) {\n if (!node || !props) return node;\n return { ...node, props: { ...(node.props || {}), ...props } };\n}\n";
@@ -429,9 +472,15 @@ type KumikiError = {
429
472
  message: string;
430
473
  pos: Pos;
431
474
  };
432
- /** Returns errors with a11y warnings filtered out (unless strict). */
475
+ /**
476
+ * Returns errors with a11y warnings filtered out (unless strict).
477
+ * `capabilities` lists project-registered capabilities (from a
478
+ * `kumiki.caps.json` manifest) that are accepted in `app.caps` in addition to
479
+ * the standard set.
480
+ */
433
481
  declare function check(program: Program, opts?: {
434
482
  strictA11y?: boolean;
483
+ capabilities?: string[];
435
484
  }): KumikiError[];
436
485
  //#endregion
437
486
  //#region src/compile.d.ts
@@ -453,7 +502,8 @@ type ExtendedCodegenOptions = CodegenOptions & {
453
502
  * Node-only imports, so it can run unchanged in the browser. Node callers can
454
503
  * use `nodeRuntimeBundleReader` from `@kumikijs/compiler/node`.
455
504
  */
456
- readRuntimeBundle?: () => string;
505
+ readRuntimeBundle?: () => string; /** Project-registered capabilities (from `kumiki.caps.json`) accepted in `app.caps`. */
506
+ capabilities?: string[];
457
507
  };
458
508
  /** Inline a runtime bundle into generated module code, stripping the bridging import/export lines. */
459
509
  declare function inlineRuntime(generatedJs: string, runtimeBundleJs: string): string;
@@ -469,4 +519,4 @@ declare class ParseError extends Error {
469
519
  }
470
520
  declare function parse(tokens: Token[]): Program;
471
521
  //#endregion
472
- export { type AppDef, type BinOp, type CodegenOptions, type CompileFail, type CompileOk, type CompileResult, type Def, type EffectDef, type EventPattern, type Expr, type ExtendedCodegenOptions, type FnDef, type KumikiError, type Lvalue, type MatchArm, ParseError, type Pattern, type PolicyExpr, type Pos, type Program, RUNTIME_HELPERS, type ReducerDef, type Refinement, type RetryExpr, type SlotDef, type Statement, type ThemeDef, type ThemeValue, type TileArg, type TileDef, type TileExpr, type TileMatchArm, type TileProp, type Token, type TypeDef, type TypeExpr, type UiEventKind, check, codegen, compile, inlineRuntime, lex, parse };
522
+ export { type AppDef, type BinOp, type CapabilityManifest, type CodegenOptions, type CompileFail, type CompileOk, type CompileResult, type Def, type EffectDef, type EventPattern, type Expr, type ExtendedCodegenOptions, type FnDef, type KumikiError, type Lvalue, type ManifestResult, type MatchArm, type MotionDef, ParseError, type Pattern, type PolicyExpr, type Pos, type Program, RUNTIME_HELPERS, type ReducerDef, type Refinement, type RetryExpr, STANDARD_CAPABILITIES, type SlotDef, type Statement, type TestDef, type ThemeDef, type ThemeValue, type TileArg, type TileDef, type TileExpr, type TileMatchArm, type TileProp, type Token, type TypeDef, type TypeExpr, type UiEventKind, check, codegen, compile, inlineRuntime, lex, parse, parseCapabilityManifest };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { n as parseCapabilityManifest, t as STANDARD_CAPABILITIES } from "./capabilities-Bdub5oAe.js";
1
2
  //#region src/codegen.ts
2
3
  function codegen(program, opts) {
3
4
  const types = new Map(program.defs.filter((d) => d.kind === "TypeDef").map((d) => [d.name, d]));
@@ -8,6 +9,8 @@ function codegen(program, opts) {
8
9
  const tiles = program.defs.filter((d) => d.kind === "TileDef");
9
10
  const apps = program.defs.filter((d) => d.kind === "AppDef");
10
11
  const themes = program.defs.filter((d) => d.kind === "ThemeDef");
12
+ const motions = program.defs.filter((d) => d.kind === "MotionDef");
13
+ const tests = program.defs.filter((d) => d.kind === "TestDef");
11
14
  const app = apps[0];
12
15
  if (!app) throw new Error("No app definition found");
13
16
  const ctx = {
@@ -59,6 +62,10 @@ function codegen(program, opts) {
59
62
  lines.push("};");
60
63
  const themeRef = app.theme ? JSON.stringify(app.theme) : "null";
61
64
  lines.push("");
65
+ lines.push("const _motions = {");
66
+ for (const m of motions) lines.push(` ${JSON.stringify(m.name)}: ${JSON.stringify(m.body)},`);
67
+ lines.push("};");
68
+ lines.push("");
62
69
  lines.push("const App = {");
63
70
  lines.push(" slots: _slots,");
64
71
  lines.push(` caps: ${JSON.stringify(app.caps)},`);
@@ -69,12 +76,110 @@ function codegen(program, opts) {
69
76
  lines.push(" live: _live,");
70
77
  lines.push(" themes: _themes,");
71
78
  lines.push(` themeName: ${themeRef},`);
79
+ lines.push(" motions: _motions,");
72
80
  lines.push("};");
73
81
  lines.push("");
74
82
  lines.push("globalThis.__kumikiApp = App;");
83
+ if (opts.includeTests && tests.length > 0) {
84
+ lines.push("");
85
+ lines.push("const _tilesById = {");
86
+ for (const tile of tiles) lines.push(` ${JSON.stringify(tile.name)}: (${jsName("$1")}) => ${genTile(tile, ctx)},`);
87
+ lines.push("};");
88
+ lines.push("void _tilesById;");
89
+ lines.push("const __kumikiTests = [");
90
+ for (const t of tests) lines.push(genTest(t, ctx));
91
+ lines.push("];");
92
+ lines.push("globalThis.__kumikiTests = __kumikiTests;");
93
+ }
94
+ lines.push("");
75
95
  lines.push(`mount(App, document.getElementById("root"));`);
76
96
  return lines.join("\n");
77
97
  }
98
+ function recordField(e, name) {
99
+ if (e.kind !== "RecordLit") return void 0;
100
+ return e.fields.find((f) => f.name === name)?.value;
101
+ }
102
+ function genTest(t, gen) {
103
+ const ctx = makeEvalCtx(gen, /* @__PURE__ */ new Set());
104
+ const nameJs = JSON.stringify(t.name);
105
+ if (t.testKind === "reducer-test") {
106
+ const slots = recordField(t.given, "slots");
107
+ const event = recordField(t.given, "event");
108
+ const slotsJs = slots ? jsOfExpr(slots, ctx) : "({})";
109
+ const elJs = eventPayloadJs(event, ctx);
110
+ const panic = recordField(t.expect, "panic");
111
+ let expectJs;
112
+ if (panic) expectJs = `{ kind: "panic", message: ${jsOfExpr(panic, ctx)} }`;
113
+ else {
114
+ const xs = recordField(t.expect, "slots");
115
+ const xe = recordField(t.expect, "effects");
116
+ expectJs = `{ kind: "state", slots: ${xs ? jsOfExpr(xs, ctx) : "({})"}, effects: ${xe ? effectListJs(xe, ctx) : "[]"} }`;
117
+ }
118
+ return ` {
119
+ name: ${nameJs},
120
+ kind: "reducer-test",
121
+ run: () => {
122
+ _s.resetLive(_live, _slots, ${slotsJs});
123
+ const _el = ${elJs};
124
+ const _r = _reducers.find((r) => r.name === ${JSON.stringify(t.target)});
125
+ if (!_r) throw new Error("reducer ${t.target} not found");
126
+ let _res = null, _panic = null;
127
+ try { _res = _r.apply(_live, { $el: _el, $event: _el }); }
128
+ catch (e) { _panic = (e && e.message) ? e.message : String(e); }
129
+ return _s.runReducerTest({ name: ${nameJs}, givenSlots: { ..._live }, result: _res, panic: _panic, expect: ${expectJs} });
130
+ },
131
+ },`;
132
+ }
133
+ const slots = recordField(t.given, "slots");
134
+ const slotsJs = slots ? jsOfExpr(slots, ctx) : "({})";
135
+ const inField = recordField(t.given, "in");
136
+ const inJs = inField ? jsOfExpr(inField, ctx) : "undefined";
137
+ const expectedJs = tileExprJs(t.expect, gen, ctx);
138
+ return ` {
139
+ name: ${nameJs},
140
+ kind: "tile-test",
141
+ run: () => {
142
+ _s.resetLive(_live, _slots, ${slotsJs});
143
+ const _actual = _tilesById[${JSON.stringify(t.target)}](${inJs});
144
+ const _expected = ${expectedJs};
145
+ return _s.runTileTest({ name: ${nameJs}, actual: _actual, expected: _expected });
146
+ },
147
+ },`;
148
+ }
149
+ /**
150
+ * Compile a `[eff(a), ...]` expect.effects list into `[{effect, args, argsSpecified}]`.
151
+ * A bare name (`persist`) matches by name only (`argsSpecified: false`); a call
152
+ * (`persist(x)`, even `persist()`) pins the exact arguments (`argsSpecified: true`).
153
+ */
154
+ function effectListJs(e, ctx) {
155
+ if (e.kind !== "ListLit") return "[]";
156
+ return `[${e.items.map((it) => {
157
+ if (it.kind === "Call") {
158
+ const args = it.args.map((a) => jsOfExpr(a, ctx)).join(", ");
159
+ return `{ effect: ${JSON.stringify(it.callee)}, args: [${args}], argsSpecified: true }`;
160
+ }
161
+ if (it.kind === "Ref") return `{ effect: ${JSON.stringify(it.name)}, args: [], argsSpecified: false }`;
162
+ return `{ effect: "?", args: [], argsSpecified: false }`;
163
+ }).join(", ")}]`;
164
+ }
165
+ /**
166
+ * The reducer payload (`$el` / `$event`) for a reducer-test's `given.event`.
167
+ * Uses `el` when present (spec §8.5), otherwise the event's other fields
168
+ * (everything except `type` / `target`) so flat `{type, target, value}` forms
169
+ * still reach the reducer.
170
+ */
171
+ function eventPayloadJs(event, ctx) {
172
+ if (event?.kind !== "RecordLit") return "({})";
173
+ const el = event.fields.find((f) => f.name === "el");
174
+ if (el) return jsOfExpr(el.value, ctx);
175
+ const rest = event.fields.filter((f) => f.name !== "type" && f.name !== "target");
176
+ if (rest.length === 0) return "({})";
177
+ return jsOfExpr({
178
+ kind: "RecordLit",
179
+ fields: rest,
180
+ pos: event.pos
181
+ }, ctx);
182
+ }
78
183
  function makeEvalCtx(gen, locals, reducerScope = false) {
79
184
  return {
80
185
  gen,
@@ -145,11 +250,14 @@ function genReducer(r, gen) {
145
250
  eventJs = `{ kind: "ui", ev: ${JSON.stringify(r.on.ev)} }`;
146
251
  selectorJs = `{ tile: ${JSON.stringify(r.on.selector.tile)}${r.on.selector.id ? `, id: ${JSON.stringify(r.on.selector.id)}` : ""} }`;
147
252
  } else if (r.on.kind === "EffectEvent") eventJs = `{ kind: "effect", effect: ${JSON.stringify(r.on.effect)}, outcome: ${JSON.stringify(r.on.outcome)} }`;
148
- else if (r.on.kind === "TimerEvent") eventJs = `{ kind: "timer", intervalMs: ${r.on.intervalMs} }`;
149
- else eventJs = `{ kind: "lifecycle", name: ${JSON.stringify(r.on.name)} }`;
253
+ else if (r.on.kind === "TimerEvent") {
254
+ const nameJs = r.on.name !== void 0 ? `, name: ${JSON.stringify(r.on.name)}` : "";
255
+ eventJs = `{ kind: "timer", intervalMs: ${r.on.intervalMs}${nameJs} }`;
256
+ } else eventJs = `{ kind: "lifecycle", name: ${JSON.stringify(r.on.name)} }`;
150
257
  const stmtLines = [];
151
258
  stmtLines.push(`const _next = {};`);
152
259
  stmtLines.push(`const _emits = [];`);
260
+ stmtLines.push(`const _stops = [];`);
153
261
  if (r.on.kind === "EffectEvent") for (let i = 0; i < r.on.binds.length; i++) {
154
262
  const name = r.on.binds[i];
155
263
  if (name === "_") continue;
@@ -159,7 +267,7 @@ function genReducer(r, gen) {
159
267
  stmtLines.push(`const ${jsName("$event")} = _payload.$event || _payload || {};`);
160
268
  stmtLines.push(`const ${jsName("$route")} = _payload.$route || {};`);
161
269
  for (const st of r.do) stmtLines.push(genStatement(st, ctx));
162
- stmtLines.push(`return { slots: _next, emits: _emits };`);
270
+ stmtLines.push(`return { slots: _next, emits: _emits, stopTimers: _stops };`);
163
271
  return ` {
164
272
  name: ${JSON.stringify(r.name)},
165
273
  selector: ${selectorJs},
@@ -206,6 +314,7 @@ function genStatement(s, ctx) {
206
314
  const args = s.args.map((a) => jsOfExpr(a, ctx)).join(", ");
207
315
  return `_emits.push({ effect: ${JSON.stringify(s.effect)}, args: [${args}] });`;
208
316
  }
317
+ if (s.kind === "StopTimer") return `_stops.push(${JSON.stringify(s.name)});`;
209
318
  return genSlotAssign(s.lvalue, s.rhs, ctx);
210
319
  }
211
320
  function genSlotAssign(lv, rhs, ctx) {
@@ -283,6 +392,18 @@ function jsOfExpr(e, ctx) {
283
392
  if (e.field === "unique") return `[...new Set((${baseJs}) ?? [])]`;
284
393
  if (e.field === "reverse") return `[...((${baseJs}) ?? [])].reverse()`;
285
394
  if (e.field === "sort") return `[...((${baseJs}) ?? [])].sort()`;
395
+ if (e.field === "head") return `_s.listHead(${baseJs})`;
396
+ if (e.field === "tail") return `_s.listTail(${baseJs})`;
397
+ if (e.field === "last") return `_s.listLast(${baseJs})`;
398
+ if (e.field === "to-list") return `_s.toList(${baseJs})`;
399
+ if (e.field === "get-err") return `_s.getErr(${baseJs})`;
400
+ if (e.field === "to-option") return `_s.toOption(${baseJs})`;
401
+ if (e.field === "parse-int") return `_s.parseIntOpt(${baseJs})`;
402
+ if (e.field === "parse-float") return `_s.parseFloatOpt(${baseJs})`;
403
+ if (e.field === "abs") return `Math.abs(${baseJs})`;
404
+ if (e.field === "neg") return `(-(${baseJs}))`;
405
+ if (e.field === "to-float") return `(${baseJs})`;
406
+ if (e.field === "to-int") return `Math.trunc(${baseJs})`;
286
407
  return `(${baseJs})[${JSON.stringify(e.field)}]`;
287
408
  }
288
409
  case "Index": return `(${jsOfExpr(e.base, ctx)})[${jsOfExpr(e.index, ctx)}]`;
@@ -385,7 +506,19 @@ const KNOWN_METHODS = new Set([
385
506
  "replace",
386
507
  "min",
387
508
  "max",
388
- "clamp"
509
+ "clamp",
510
+ "head",
511
+ "tail",
512
+ "last",
513
+ "to-list",
514
+ "get-err",
515
+ "to-option",
516
+ "parse-int",
517
+ "parse-float",
518
+ "abs",
519
+ "neg",
520
+ "to-float",
521
+ "to-int"
389
522
  ]);
390
523
  function methodCallJs(recv, method, args, ctx) {
391
524
  const inner = makeEvalCtx(ctx.gen, ctx.localBinds);
@@ -449,6 +582,18 @@ function methodCallJs(recv, method, args, ctx) {
449
582
  case "min": return `Math.min((${recvJs}), (${argRaw(args[0])}))`;
450
583
  case "max": return `Math.max((${recvJs}), (${argRaw(args[0])}))`;
451
584
  case "clamp": return `Math.min(Math.max((${recvJs}), (${argRaw(args[0])})), (${argRaw(args[1])}))`;
585
+ case "head": return `_s.listHead(${recvJs})`;
586
+ case "tail": return `_s.listTail(${recvJs})`;
587
+ case "last": return `_s.listLast(${recvJs})`;
588
+ case "to-list": return `_s.toList(${recvJs})`;
589
+ case "get-err": return `_s.getErr(${recvJs})`;
590
+ case "to-option": return `_s.toOption(${recvJs})`;
591
+ case "parse-int": return `_s.parseIntOpt(${recvJs})`;
592
+ case "parse-float": return `_s.parseFloatOpt(${recvJs})`;
593
+ case "abs": return `Math.abs(${recvJs})`;
594
+ case "neg": return `(-(${recvJs}))`;
595
+ case "to-float": return `(${recvJs})`;
596
+ case "to-int": return `Math.trunc(${recvJs})`;
452
597
  default: return `(${recvJs}).${jsName(method)}(${args.map(argRaw).join(", ")})`;
453
598
  }
454
599
  }
@@ -529,6 +674,7 @@ const BUILTIN_TILES$2 = new Set([
529
674
  "panel",
530
675
  "grid",
531
676
  "stack",
677
+ "overlay",
532
678
  "region",
533
679
  "scroll",
534
680
  "divider",
@@ -621,6 +767,7 @@ function tileCallJs(t, gen, ctx, enclosingTile) {
621
767
  case "box":
622
768
  case "grid":
623
769
  case "stack":
770
+ case "overlay":
624
771
  case "region":
625
772
  case "scroll":
626
773
  case "divider":
@@ -873,6 +1020,7 @@ const KEYWORDS = new Set([
873
1020
  "tile",
874
1021
  "fn",
875
1022
  "app",
1023
+ "test",
876
1024
  "nominal",
877
1025
  "where",
878
1026
  "when",
@@ -1172,6 +1320,7 @@ const BUILTIN_TILES$1 = new Set([
1172
1320
  "row",
1173
1321
  "column",
1174
1322
  "stack",
1323
+ "overlay",
1175
1324
  "grid",
1176
1325
  "box",
1177
1326
  "card",
@@ -1272,6 +1421,10 @@ var Parser = class {
1272
1421
  defs.push(this.parseThemeDef());
1273
1422
  continue;
1274
1423
  }
1424
+ if (this.matchT("ident", "motion")) {
1425
+ defs.push(this.parseMotionDef());
1426
+ continue;
1427
+ }
1275
1428
  defs.push(this.parseDef());
1276
1429
  }
1277
1430
  return {
@@ -1290,6 +1443,17 @@ var Parser = class {
1290
1443
  pos: start.pos
1291
1444
  };
1292
1445
  }
1446
+ parseMotionDef() {
1447
+ const start = this.eat("ident", "motion");
1448
+ const name = this.eat("ident").value;
1449
+ this.eat("op", "=");
1450
+ return {
1451
+ kind: "MotionDef",
1452
+ name,
1453
+ body: this.parseThemeRecord(),
1454
+ pos: start.pos
1455
+ };
1456
+ }
1293
1457
  parseThemeRecord() {
1294
1458
  this.eat("op", "{");
1295
1459
  const out = {};
@@ -1331,6 +1495,7 @@ var Parser = class {
1331
1495
  case "fn": return this.parseFn();
1332
1496
  case "effect": return this.parseEffect();
1333
1497
  case "app": return this.parseApp();
1498
+ case "test": return this.parseTest();
1334
1499
  default: throw new ParseError(`Unsupported definition keyword "${t.value}"`, t.pos);
1335
1500
  }
1336
1501
  }
@@ -1582,10 +1747,23 @@ var Parser = class {
1582
1747
  this.next();
1583
1748
  this.eat("op", "(");
1584
1749
  const intervalMs = this.parseDuration();
1750
+ let name;
1751
+ if (this.matchOp(",")) {
1752
+ this.next();
1753
+ const kw = this.eat("ident");
1754
+ if (kw.value !== "name") throw new ParseError(`Expected "name=" in timer(...)`, kw.pos);
1755
+ this.eat("op", "=");
1756
+ name = this.eat("ident").value;
1757
+ }
1585
1758
  this.eat("op", ")");
1586
- return {
1759
+ return name === void 0 ? {
1760
+ kind: "TimerEvent",
1761
+ intervalMs,
1762
+ pos: t.pos
1763
+ } : {
1587
1764
  kind: "TimerEvent",
1588
1765
  intervalMs,
1766
+ name,
1589
1767
  pos: t.pos
1590
1768
  };
1591
1769
  }
@@ -1772,6 +1950,18 @@ var Parser = class {
1772
1950
  pos: start.pos
1773
1951
  };
1774
1952
  }
1953
+ const cur = this.peek();
1954
+ if (cur.kind === "ident" && cur.value === "stop-timer") {
1955
+ this.next();
1956
+ this.eat("op", "(");
1957
+ const name = this.eat("ident").value;
1958
+ this.eat("op", ")");
1959
+ return {
1960
+ kind: "StopTimer",
1961
+ name,
1962
+ pos: cur.pos
1963
+ };
1964
+ }
1775
1965
  const lvalue = this.parseLvalue();
1776
1966
  this.eat("op", ":=");
1777
1967
  return {
@@ -2279,25 +2469,30 @@ var Parser = class {
2279
2469
  };
2280
2470
  }
2281
2471
  let isRecord = false;
2282
- if (this.peek().kind === "ident") {
2472
+ const k0 = this.peek();
2473
+ if (k0.kind === "ident" || k0.kind === "kw") {
2283
2474
  const peek1 = this.peek(1);
2284
2475
  if (peek1.kind === "op" && (peek1.value === "=" || peek1.value === ":" || peek1.value === "," || peek1.value === "}")) isRecord = true;
2285
2476
  }
2286
2477
  if (isRecord) {
2287
2478
  const fields = [];
2288
2479
  while (true) {
2289
- const nameTok = this.eat("ident");
2480
+ const keyTok = this.peek();
2481
+ if (keyTok.kind !== "ident" && keyTok.kind !== "kw") throw new ParseError("Expected a record field name", keyTok.pos);
2482
+ const fieldName = keyTok.value;
2483
+ const fieldPos = keyTok.pos;
2484
+ this.next();
2290
2485
  let value;
2291
2486
  if (this.matchOp("=") || this.matchOp(":")) {
2292
2487
  this.next();
2293
2488
  value = this.parseExpr();
2294
2489
  } else value = {
2295
2490
  kind: "Ref",
2296
- name: nameTok.value,
2297
- pos: nameTok.pos
2491
+ name: fieldName,
2492
+ pos: fieldPos
2298
2493
  };
2299
2494
  fields.push({
2300
- name: nameTok.value,
2495
+ name: fieldName,
2301
2496
  value
2302
2497
  });
2303
2498
  if (!this.matchOp(",")) break;
@@ -2764,6 +2959,31 @@ var Parser = class {
2764
2959
  if (t.kind === "kw") return true;
2765
2960
  return false;
2766
2961
  }
2962
+ parseTest() {
2963
+ const start = this.eat("kw", "test");
2964
+ const name = this.eat("ident").value;
2965
+ this.eat("op", "=");
2966
+ const kindTok = this.eat("ident");
2967
+ if (kindTok.value !== "reducer-test" && kindTok.value !== "tile-test") throw new ParseError(`Unknown test kind "${kindTok.value}" (expected reducer-test or tile-test)`, kindTok.pos);
2968
+ const target = this.eat("ident").value;
2969
+ const givenKw = this.eat("ident");
2970
+ if (givenKw.value !== "given") throw new ParseError(`Expected "given" in test "${name}"`, givenKw.pos);
2971
+ this.eat("op", "=");
2972
+ const given = this.parseExpr();
2973
+ const expectKw = this.eat("ident");
2974
+ if (expectKw.value !== "expect") throw new ParseError(`Expected "expect" in test "${name}"`, expectKw.pos);
2975
+ this.eat("op", "=");
2976
+ const expect = kindTok.value === "tile-test" ? this.parseTileExpr() : this.parseExpr();
2977
+ return {
2978
+ kind: "TestDef",
2979
+ name,
2980
+ testKind: kindTok.value,
2981
+ target,
2982
+ given,
2983
+ expect,
2984
+ pos: start.pos
2985
+ };
2986
+ }
2767
2987
  parseQualifiedList() {
2768
2988
  this.eat("op", "[");
2769
2989
  const out = [];
@@ -2838,6 +3058,7 @@ const BUILTIN_TILES = new Set([
2838
3058
  "row",
2839
3059
  "column",
2840
3060
  "stack",
3061
+ "overlay",
2841
3062
  "grid",
2842
3063
  "box",
2843
3064
  "card",
@@ -2886,13 +3107,18 @@ const A11Y_CODES = new Set([
2886
3107
  "E0702",
2887
3108
  "E0703"
2888
3109
  ]);
2889
- /** Returns errors with a11y warnings filtered out (unless strict). */
3110
+ /**
3111
+ * Returns errors with a11y warnings filtered out (unless strict).
3112
+ * `capabilities` lists project-registered capabilities (from a
3113
+ * `kumiki.caps.json` manifest) that are accepted in `app.caps` in addition to
3114
+ * the standard set.
3115
+ */
2890
3116
  function check(program, opts) {
2891
- const errors = checkAll(program);
3117
+ const errors = checkAll(program, new Set(opts?.capabilities ?? []));
2892
3118
  if (opts?.strictA11y) return errors;
2893
3119
  return errors.filter((e) => !A11Y_CODES.has(e.code));
2894
3120
  }
2895
- function checkAll(program) {
3121
+ function checkAll(program, registeredCaps) {
2896
3122
  const errors = [];
2897
3123
  const sym = {
2898
3124
  types: /* @__PURE__ */ new Map(),
@@ -2900,7 +3126,9 @@ function checkAll(program) {
2900
3126
  reducers: /* @__PURE__ */ new Map(),
2901
3127
  tiles: /* @__PURE__ */ new Map(),
2902
3128
  fns: /* @__PURE__ */ new Map(),
2903
- effects: /* @__PURE__ */ new Map()
3129
+ effects: /* @__PURE__ */ new Map(),
3130
+ timerNames: /* @__PURE__ */ new Set(),
3131
+ motions: /* @__PURE__ */ new Set()
2904
3132
  };
2905
3133
  for (const def of program.defs) switch (def.kind) {
2906
3134
  case "TypeDef":
@@ -2911,6 +3139,13 @@ function checkAll(program) {
2911
3139
  break;
2912
3140
  case "ReducerDef":
2913
3141
  sym.reducers.set(def.name, def);
3142
+ if (def.on.kind === "TimerEvent" && def.on.name !== void 0) if (sym.timerNames.has(def.on.name)) errors.push({
3143
+ code: "E0002",
3144
+ kind: "duplicate-timer-name",
3145
+ message: `Timer name "${def.on.name}" is declared more than once`,
3146
+ pos: def.on.pos
3147
+ });
3148
+ else sym.timerNames.add(def.on.name);
2914
3149
  break;
2915
3150
  case "TileDef":
2916
3151
  sym.tiles.set(def.name, def);
@@ -2921,6 +3156,9 @@ function checkAll(program) {
2921
3156
  case "EffectDef":
2922
3157
  sym.effects.set(def.name, def);
2923
3158
  break;
3159
+ case "MotionDef":
3160
+ sym.motions.add(def.name);
3161
+ break;
2924
3162
  case "AppDef":
2925
3163
  sym.app = def;
2926
3164
  break;
@@ -2931,10 +3169,137 @@ function checkAll(program) {
2931
3169
  if (def.kind === "ReducerDef") checkReducer(def, sym, errors);
2932
3170
  if (def.kind === "FnDef") checkFn(def, sym, errors);
2933
3171
  if (def.kind === "EffectDef") checkEffect(def, sym, errors);
2934
- if (def.kind === "AppDef") checkApp(def, sym, errors);
3172
+ if (def.kind === "AppDef") checkApp(def, sym, errors, registeredCaps);
3173
+ if (def.kind === "MotionDef") checkMotion(def, errors);
3174
+ if (def.kind === "TestDef") checkTest(def, sym, errors);
2935
3175
  }
2936
3176
  return errors;
2937
3177
  }
3178
+ const MOTION_KEYFRAME_PROPS = new Set([
3179
+ "opacity",
3180
+ "translate-x",
3181
+ "translate-y",
3182
+ "scale",
3183
+ "rotate"
3184
+ ]);
3185
+ const MOTION_EASINGS = new Set([
3186
+ "linear",
3187
+ "ease",
3188
+ "ease-in",
3189
+ "ease-out",
3190
+ "ease-in-out"
3191
+ ]);
3192
+ const MOTION_DURATION_TOKENS = new Set([
3193
+ "fast",
3194
+ "normal",
3195
+ "slow"
3196
+ ]);
3197
+ const MOTION_DIRECTIONS = new Set([
3198
+ "normal",
3199
+ "reverse",
3200
+ "alternate",
3201
+ "alternate-reverse"
3202
+ ]);
3203
+ const MOTION_TIMING_KEYS = new Set([
3204
+ "duration",
3205
+ "easing",
3206
+ "iteration",
3207
+ "direction"
3208
+ ]);
3209
+ /** `duration` (ms) and `iteration` are spec'd as positive integers (no 0 / negative / float). */
3210
+ const isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
3211
+ /**
3212
+ * Validate a `motion` definition against the closed grammar (ADR-001). Purity
3213
+ * (no slots/effects) is already guaranteed by the parser — the body is a literal
3214
+ * record — so this only enforces the closed property + timing vocabularies.
3215
+ */
3216
+ function checkMotion(def, errors) {
3217
+ const body = def.body;
3218
+ const keyframes = body.keyframes;
3219
+ if (typeof keyframes !== "object" || Array.isArray(keyframes)) {
3220
+ errors.push({
3221
+ code: "E0403",
3222
+ kind: "motion-malformed",
3223
+ message: `motion "${def.name}" must declare a "keyframes" record`,
3224
+ pos: def.pos
3225
+ });
3226
+ return;
3227
+ }
3228
+ const stops = keyframes;
3229
+ for (const required of ["from", "to"]) {
3230
+ const stop = stops[required];
3231
+ if (typeof stop !== "object" || Array.isArray(stop)) {
3232
+ errors.push({
3233
+ code: "E0403",
3234
+ kind: "motion-malformed",
3235
+ message: `motion "${def.name}" keyframes must include a "${required}" record`,
3236
+ pos: def.pos
3237
+ });
3238
+ return;
3239
+ }
3240
+ }
3241
+ for (const stopName of Object.keys(stops)) {
3242
+ if (stopName !== "from" && stopName !== "to") {
3243
+ errors.push({
3244
+ code: "E0403",
3245
+ kind: "motion-malformed",
3246
+ message: `motion "${def.name}" keyframes support only "from" / "to" (got "${stopName}")`,
3247
+ pos: def.pos
3248
+ });
3249
+ continue;
3250
+ }
3251
+ const stop = stops[stopName];
3252
+ for (const [prop, val] of Object.entries(stop)) if (!MOTION_KEYFRAME_PROPS.has(prop)) errors.push({
3253
+ code: "E0401",
3254
+ kind: "motion-unknown-property",
3255
+ message: `motion "${def.name}": unknown keyframe property "${prop}" (allowed: ${[...MOTION_KEYFRAME_PROPS].join(", ")})`,
3256
+ pos: def.pos
3257
+ });
3258
+ else if (typeof val !== "number") errors.push({
3259
+ code: "E0401",
3260
+ kind: "motion-unknown-property",
3261
+ message: `motion "${def.name}": keyframe property "${prop}" must be a number`,
3262
+ pos: def.pos
3263
+ });
3264
+ }
3265
+ for (const key of Object.keys(body)) {
3266
+ if (key === "keyframes") continue;
3267
+ if (!MOTION_TIMING_KEYS.has(key)) errors.push({
3268
+ code: "E0402",
3269
+ kind: "motion-invalid-timing",
3270
+ message: `motion "${def.name}": unknown field "${key}" (allowed: keyframes, ${[...MOTION_TIMING_KEYS].join(", ")})`,
3271
+ pos: def.pos
3272
+ });
3273
+ }
3274
+ const dur = body.duration;
3275
+ if (dur !== void 0 && !(isPositiveInt(dur) || MOTION_DURATION_TOKENS.has(String(dur)))) errors.push({
3276
+ code: "E0402",
3277
+ kind: "motion-invalid-timing",
3278
+ message: `motion "${def.name}": duration must be a positive Int (ms) or one of fast/normal/slow`,
3279
+ pos: def.pos
3280
+ });
3281
+ const eas = body.easing;
3282
+ if (eas !== void 0 && !MOTION_EASINGS.has(String(eas))) errors.push({
3283
+ code: "E0402",
3284
+ kind: "motion-invalid-timing",
3285
+ message: `motion "${def.name}": easing must be one of ${[...MOTION_EASINGS].join(", ")}`,
3286
+ pos: def.pos
3287
+ });
3288
+ const iter = body.iteration;
3289
+ if (iter !== void 0 && !(isPositiveInt(iter) || iter === "infinite")) errors.push({
3290
+ code: "E0402",
3291
+ kind: "motion-invalid-timing",
3292
+ message: `motion "${def.name}": iteration must be a positive Int or "infinite"`,
3293
+ pos: def.pos
3294
+ });
3295
+ const dir = body.direction;
3296
+ if (dir !== void 0 && !MOTION_DIRECTIONS.has(String(dir))) errors.push({
3297
+ code: "E0402",
3298
+ kind: "motion-invalid-timing",
3299
+ message: `motion "${def.name}": direction must be one of ${[...MOTION_DIRECTIONS].join(", ")}`,
3300
+ pos: def.pos
3301
+ });
3302
+ }
2938
3303
  function checkSlot(slot, sym, errors) {
2939
3304
  resolveType(slot.type, sym, errors);
2940
3305
  checkExpr(slot.init, sym, errors, {
@@ -3072,6 +3437,13 @@ function checkTileCall(t, sym, errors, ctx) {
3072
3437
  message: `Reference to undefined reducer "${ref.name}"`,
3073
3438
  pos: ref.pos
3074
3439
  });
3440
+ } else if (prop.name === "motion" && prop.value.kind === "Str") {
3441
+ if (!sym.motions.has(prop.value.value)) errors.push({
3442
+ code: "E0107",
3443
+ kind: "undef-motion",
3444
+ message: `Reference to undefined motion "${prop.value.value}"`,
3445
+ pos: prop.value.pos
3446
+ });
3075
3447
  } else checkExpr(prop.value, sym, errors, ctx);
3076
3448
  }
3077
3449
  function checkReducer(r, sym, errors) {
@@ -3158,6 +3530,15 @@ function checkStmt(s, sym, errors, ctx, writtenRoots) {
3158
3530
  for (const a of s.args) checkExpr(a, sym, errors, ctx);
3159
3531
  return;
3160
3532
  }
3533
+ if (s.kind === "StopTimer") {
3534
+ if (!sym.timerNames.has(s.name)) errors.push({
3535
+ code: "E0106",
3536
+ kind: "undef-timer",
3537
+ message: `stop-timer refers to undefined timer name "${s.name}"`,
3538
+ pos: s.pos
3539
+ });
3540
+ return;
3541
+ }
3161
3542
  const root = lvalueRoot(s.lvalue);
3162
3543
  if (!sym.slots.has(root)) errors.push({
3163
3544
  code: "E0103",
@@ -3325,7 +3706,34 @@ function checkEffect(eff, sym, errors) {
3325
3706
  localBinds: new Set(["$1"])
3326
3707
  });
3327
3708
  }
3328
- function checkApp(app, sym, errors) {
3709
+ function checkTest(t, sym, errors) {
3710
+ if (t.testKind === "reducer-test") {
3711
+ if (!sym.reducers.has(t.target)) errors.push({
3712
+ code: "E0102",
3713
+ kind: "undef-reducer",
3714
+ message: `Reference to undefined reducer "${t.target}"`,
3715
+ pos: t.pos
3716
+ });
3717
+ return;
3718
+ }
3719
+ if (!BUILTIN_TILES.has(t.target) && !sym.tiles.has(t.target)) errors.push({
3720
+ code: "E0105",
3721
+ kind: "undef-tile",
3722
+ message: `Reference to undefined tile "${t.target}"`,
3723
+ pos: t.pos
3724
+ });
3725
+ checkTileExpr(t.expect, sym, errors, {
3726
+ kind: "tile",
3727
+ localBinds: /* @__PURE__ */ new Set()
3728
+ });
3729
+ }
3730
+ function checkApp(app, sym, errors, registeredCaps) {
3731
+ for (const cap of app.caps) if (!STANDARD_CAPABILITIES.has(cap) && !registeredCaps.has(cap)) errors.push({
3732
+ code: "E0302",
3733
+ kind: "unknown-capability",
3734
+ message: `Unknown capability "${cap}" in app.caps — use a standard capability or register it in kumiki.caps.json`,
3735
+ pos: app.pos
3736
+ });
3329
3737
  let saw404 = false;
3330
3738
  for (const r of app.routes) {
3331
3739
  if (r.tile.startsWith(">>")) continue;
@@ -3380,7 +3788,7 @@ function inlineRuntime(generatedJs, runtimeBundleJs) {
3380
3788
  }
3381
3789
  function compile(source, opts) {
3382
3790
  const program = parse(lex(source));
3383
- const errors = check(program);
3791
+ const errors = check(program, { capabilities: opts.capabilities ?? [] });
3384
3792
  if (errors.length > 0) return {
3385
3793
  kind: "fail",
3386
3794
  errors
@@ -3397,4 +3805,4 @@ function compile(source, opts) {
3397
3805
  };
3398
3806
  }
3399
3807
  //#endregion
3400
- export { ParseError, RUNTIME_HELPERS, check, codegen, compile, inlineRuntime, lex, parse };
3808
+ export { ParseError, RUNTIME_HELPERS, STANDARD_CAPABILITIES, check, codegen, compile, inlineRuntime, lex, parse, parseCapabilityManifest };
package/dist/node.d.ts CHANGED
@@ -4,5 +4,15 @@
4
4
  * `compile(source, { bundle: true, readRuntimeBundle: nodeRuntimeBundleReader })`.
5
5
  */
6
6
  declare function nodeRuntimeBundleReader(): string;
7
+ /** Thrown when a `kumiki.caps.json` exists but is malformed. */
8
+ declare class CapabilityManifestError extends Error {}
9
+ /**
10
+ * Resolve project-registered capabilities from a `kumiki.caps.json` in the same
11
+ * directory as the given `.kumiki` file. Returns `[]` when no manifest exists;
12
+ * throws `CapabilityManifestError` (with the path) when one exists but is
13
+ * invalid. Pass the result as `compile(src, { capabilities })` /
14
+ * `check(program, { capabilities })`.
15
+ */
16
+ declare function resolveCapabilities(kumikiFilePath: string): string[];
7
17
  //#endregion
8
- export { nodeRuntimeBundleReader };
18
+ export { CapabilityManifestError, nodeRuntimeBundleReader, resolveCapabilities };
package/dist/node.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { n as parseCapabilityManifest } from "./capabilities-Bdub5oAe.js";
1
2
  import { createRequire } from "node:module";
2
- import { readFileSync } from "node:fs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
3
5
  //#region src/node.ts
4
6
  /**
5
7
  * Reads the prebuilt @kumikijs/runtime bundle from disk. Pass as
@@ -8,5 +10,27 @@ import { readFileSync } from "node:fs";
8
10
  function nodeRuntimeBundleReader() {
9
11
  return readFileSync(createRequire(import.meta.url).resolve("@kumikijs/runtime/bundle"), "utf8");
10
12
  }
13
+ /** Thrown when a `kumiki.caps.json` exists but is malformed. */
14
+ var CapabilityManifestError = class extends Error {};
15
+ /**
16
+ * Resolve project-registered capabilities from a `kumiki.caps.json` in the same
17
+ * directory as the given `.kumiki` file. Returns `[]` when no manifest exists;
18
+ * throws `CapabilityManifestError` (with the path) when one exists but is
19
+ * invalid. Pass the result as `compile(src, { capabilities })` /
20
+ * `check(program, { capabilities })`.
21
+ */
22
+ function resolveCapabilities(kumikiFilePath) {
23
+ const manifestPath = join(dirname(kumikiFilePath), "kumiki.caps.json");
24
+ if (!existsSync(manifestPath)) return [];
25
+ let raw;
26
+ try {
27
+ raw = JSON.parse(readFileSync(manifestPath, "utf8"));
28
+ } catch (e) {
29
+ throw new CapabilityManifestError(`${manifestPath}: invalid JSON — ${e.message}`);
30
+ }
31
+ const result = parseCapabilityManifest(raw);
32
+ if (!result.ok) throw new CapabilityManifestError(`${manifestPath}: ${result.error}`);
33
+ return result.manifest.capabilities;
34
+ }
11
35
  //#endregion
12
- export { nodeRuntimeBundleReader };
36
+ export { CapabilityManifestError, nodeRuntimeBundleReader, resolveCapabilities };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kumikijs/compiler",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Kumiki compiler — lexer, parser, typechecker, and code generator.",
6
6
  "license": "Apache-2.0",
@@ -26,11 +26,11 @@
26
26
  "exports": {
27
27
  ".": {
28
28
  "types": "./dist/index.d.ts",
29
- "default": "./src/index.ts"
29
+ "import": "./dist/index.js"
30
30
  },
31
31
  "./node": {
32
32
  "types": "./dist/node.d.ts",
33
- "default": "./src/node.ts"
33
+ "import": "./dist/node.js"
34
34
  }
35
35
  },
36
36
  "files": [
@@ -38,30 +38,20 @@
38
38
  ],
39
39
  "publishConfig": {
40
40
  "access": "public",
41
- "provenance": true,
42
- "exports": {
43
- ".": {
44
- "types": "./dist/index.d.ts",
45
- "import": "./dist/index.js"
46
- },
47
- "./node": {
48
- "types": "./dist/node.d.ts",
49
- "import": "./dist/node.js"
50
- }
51
- }
41
+ "provenance": true
42
+ },
43
+ "dependencies": {
44
+ "@kumikijs/runtime": "0.2.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^25.9.1",
48
+ "typescript": "^6.0.3",
49
+ "vitest": "^4.1.7"
52
50
  },
53
51
  "scripts": {
54
52
  "build": "tsdown",
55
53
  "typecheck": "tsc -p tsconfig.json --noEmit",
56
54
  "test": "vitest run",
57
55
  "lint": "biome check src test"
58
- },
59
- "dependencies": {
60
- "@kumikijs/runtime": "workspace:*"
61
- },
62
- "devDependencies": {
63
- "@types/node": "catalog:",
64
- "typescript": "catalog:",
65
- "vitest": "catalog:"
66
56
  }
67
- }
57
+ }