@kumikijs/compiler 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,3400 @@
1
+ //#region src/codegen.ts
2
+ function codegen(program, opts) {
3
+ const types = new Map(program.defs.filter((d) => d.kind === "TypeDef").map((d) => [d.name, d]));
4
+ const slots = program.defs.filter((d) => d.kind === "SlotDef");
5
+ const effects = program.defs.filter((d) => d.kind === "EffectDef");
6
+ const reducers = program.defs.filter((d) => d.kind === "ReducerDef");
7
+ const fns = program.defs.filter((d) => d.kind === "FnDef");
8
+ const tiles = program.defs.filter((d) => d.kind === "TileDef");
9
+ const apps = program.defs.filter((d) => d.kind === "AppDef");
10
+ const themes = program.defs.filter((d) => d.kind === "ThemeDef");
11
+ const app = apps[0];
12
+ if (!app) throw new Error("No app definition found");
13
+ const ctx = {
14
+ slots,
15
+ fns,
16
+ tiles,
17
+ reducers,
18
+ effects,
19
+ types
20
+ };
21
+ const lines = [];
22
+ lines.push(`import { mount, _stdlib, builtinEffects } from "${opts.runtimeSpecifier}";`);
23
+ lines.push("");
24
+ lines.push("const _s = _stdlib;");
25
+ lines.push("");
26
+ for (const fn of fns) lines.push(genFn(fn, ctx));
27
+ lines.push("const _effects = {");
28
+ for (const eff of effects) lines.push(` ${JSON.stringify(eff.name)}: ${genEffect(eff, ctx)},`);
29
+ lines.push("};");
30
+ lines.push("");
31
+ lines.push("const _slots = {");
32
+ for (const s of slots) {
33
+ const refine = refinementJs(s.type, ctx);
34
+ const init = jsOfExpr(s.init, makeEvalCtx(ctx, /* @__PURE__ */ new Set()));
35
+ lines.push(` ${JSON.stringify(s.name)}: { value: ${init}${refine ? `, refine: ${refine}` : ""} },`);
36
+ }
37
+ lines.push("};");
38
+ lines.push("");
39
+ lines.push("const _live = {};");
40
+ lines.push("for (const [k, v] of Object.entries(_slots)) _live[k] = v.value;");
41
+ lines.push("");
42
+ lines.push("const _reducers = [");
43
+ for (const r of reducers) lines.push(genReducer(r, ctx));
44
+ lines.push("];");
45
+ lines.push("");
46
+ lines.push("const _routes = [");
47
+ for (const r of app.routes) if (r.tile.startsWith(">>")) {
48
+ const target = r.tile.slice(2);
49
+ lines.push(` { pattern: ${JSON.stringify(r.path)}, redirectTo: ${JSON.stringify(target)} },`);
50
+ } else {
51
+ const tile = tiles.find((t) => t.name === r.tile);
52
+ if (!tile) throw new Error(`Route ${r.path} targets undefined tile "${r.tile}"`);
53
+ lines.push(` { pattern: ${JSON.stringify(r.path)}, tile: () => ${genTile(tile, ctx)} },`);
54
+ }
55
+ lines.push("];");
56
+ lines.push("");
57
+ lines.push("const _themes = {");
58
+ for (const t of themes) lines.push(` ${JSON.stringify(t.name)}: ${JSON.stringify(t.body)},`);
59
+ lines.push("};");
60
+ const themeRef = app.theme ? JSON.stringify(app.theme) : "null";
61
+ lines.push("");
62
+ lines.push("const App = {");
63
+ lines.push(" slots: _slots,");
64
+ lines.push(` caps: ${JSON.stringify(app.caps)},`);
65
+ lines.push(" reducers: _reducers,");
66
+ lines.push(" effects: _effects,");
67
+ lines.push(` init: [${app.init.map((e) => emitFromInitExpr(e)).join(", ")}],`);
68
+ lines.push(" routes: _routes,");
69
+ lines.push(" live: _live,");
70
+ lines.push(" themes: _themes,");
71
+ lines.push(` themeName: ${themeRef},`);
72
+ lines.push("};");
73
+ lines.push("");
74
+ lines.push("globalThis.__kumikiApp = App;");
75
+ lines.push(`mount(App, document.getElementById("root"));`);
76
+ return lines.join("\n");
77
+ }
78
+ function makeEvalCtx(gen, locals, reducerScope = false) {
79
+ return {
80
+ gen,
81
+ localBinds: new Set(locals),
82
+ reducerScope
83
+ };
84
+ }
85
+ function genFn(fn, gen) {
86
+ const params = fn.params.map((p) => p.name).join(", ");
87
+ const ctx = makeEvalCtx(gen, new Set([
88
+ ...fn.params.map((p) => p.name),
89
+ "$1",
90
+ "$2"
91
+ ]));
92
+ return `function ${jsName(fn.name)}(${params}) { return ${jsOfExpr(fn.body, ctx)}; }`;
93
+ }
94
+ function genEffect(eff, gen) {
95
+ let invokeBody;
96
+ if (eff.cap === "storage.read") if (eff.mapRequest) {
97
+ const mapJs = jsOfExpr(eff.mapRequest, makeEvalCtx(gen, new Set(["$1"])));
98
+ invokeBody = `async (${jsName("$1")}, _caps) => { const req = ${mapJs}; return builtinEffects.storageRead({ key: req.key }); }`;
99
+ } else invokeBody = `async (input) => builtinEffects.storageRead(input)`;
100
+ else if (eff.cap === "storage.write") if (eff.mapRequest) {
101
+ const mapJs = jsOfExpr(eff.mapRequest, makeEvalCtx(gen, new Set(["$1"])));
102
+ invokeBody = `async (${jsName("$1")}, _caps) => { const req = ${mapJs}; return builtinEffects.storageWrite({ key: req.key, value: req.value }); }`;
103
+ } else invokeBody = `async (input) => builtinEffects.storageWrite(input)`;
104
+ else if (eff.cap.startsWith("http.")) {
105
+ const method = eff.cap.slice(5).toUpperCase();
106
+ if (eff.mapRequest) {
107
+ const mapJs = jsOfExpr(eff.mapRequest, makeEvalCtx(gen, new Set(["$1"])));
108
+ invokeBody = `async (${jsName("$1")}, _caps) => { const req = ${mapJs}; return builtinEffects.httpFetch(${JSON.stringify(method)}, req, ""); }`;
109
+ } else invokeBody = `async (input) => builtinEffects.httpFetch(${JSON.stringify(method)}, input, "")`;
110
+ } else invokeBody = `async () => ({ kind: "err", value: { message: "Capability ${eff.cap} not implemented" } })`;
111
+ return `{
112
+ name: ${JSON.stringify(eff.name)},
113
+ cap: ${JSON.stringify(eff.cap)},
114
+ policy: ${policyJs(eff.policy)},
115
+ invoke: ${invokeBody},
116
+ }`;
117
+ }
118
+ function policyJs(p) {
119
+ if (!p) return "undefined";
120
+ switch (p.kind) {
121
+ case "PolLatest": return `{ kind: "latest" }`;
122
+ case "PolLatestKey": return `{ kind: "latest-per-key", keyOf: ((${jsName("$1")}) => String(${jsOfExpr(p.key, {
123
+ gen: {},
124
+ localBinds: new Set(["$1"])
125
+ })})) }`;
126
+ case "PolQueue": return `{ kind: "queue" }`;
127
+ case "PolDebounce": return `{ kind: "debounce", ms: ${p.ms} }`;
128
+ case "PolThrottle": return `{ kind: "throttle", ms: ${p.ms} }`;
129
+ case "PolOnce": return `{ kind: "once" }`;
130
+ }
131
+ }
132
+ function genReducer(r, gen) {
133
+ const locals = new Set([
134
+ "$el",
135
+ "$event",
136
+ "$route"
137
+ ]);
138
+ if (r.on.kind === "EffectEvent") {
139
+ for (const b of r.on.binds) if (b !== "_") locals.add(b);
140
+ }
141
+ const ctx = makeEvalCtx(gen, locals, true);
142
+ let eventJs;
143
+ let selectorJs = "undefined";
144
+ if (r.on.kind === "UiEvent") {
145
+ eventJs = `{ kind: "ui", ev: ${JSON.stringify(r.on.ev)} }`;
146
+ selectorJs = `{ tile: ${JSON.stringify(r.on.selector.tile)}${r.on.selector.id ? `, id: ${JSON.stringify(r.on.selector.id)}` : ""} }`;
147
+ } 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)} }`;
150
+ const stmtLines = [];
151
+ stmtLines.push(`const _next = {};`);
152
+ stmtLines.push(`const _emits = [];`);
153
+ if (r.on.kind === "EffectEvent") for (let i = 0; i < r.on.binds.length; i++) {
154
+ const name = r.on.binds[i];
155
+ if (name === "_") continue;
156
+ stmtLines.push(`const ${jsName(name)} = _payload[${JSON.stringify(`$${i + 1}`)}];`);
157
+ }
158
+ stmtLines.push(`const ${jsName("$el")} = _payload.$el || {};`);
159
+ stmtLines.push(`const ${jsName("$event")} = _payload.$event || _payload || {};`);
160
+ stmtLines.push(`const ${jsName("$route")} = _payload.$route || {};`);
161
+ for (const st of r.do) stmtLines.push(genStatement(st, ctx));
162
+ stmtLines.push(`return { slots: _next, emits: _emits };`);
163
+ return ` {
164
+ name: ${JSON.stringify(r.name)},
165
+ selector: ${selectorJs},
166
+ event: ${eventJs},
167
+ apply: (_slotsLive, _payload) => {
168
+ ${stmtLines.join("\n ")}
169
+ },
170
+ },`;
171
+ }
172
+ function genStatement(s, ctx) {
173
+ if (s.kind === "ForStmt") {
174
+ const iter = jsOfExpr(s.iter, ctx);
175
+ const inner = makeEvalCtx(ctx.gen, ctx.localBinds, ctx.reducerScope);
176
+ inner.localBinds.add(s.bind);
177
+ const body = s.body.map((b) => genStatement(b, inner)).join("\n ");
178
+ return `for (const ${jsName(s.bind)} of ((${iter}) || [])) {\n ${body}\n}`;
179
+ }
180
+ if (s.kind === "IfStmt") return `if (${jsOfExpr(s.cond, ctx)}) {\n ${s.consequent.map((b) => genStatement(b, ctx)).join("\n ")}\n} else {\n ${s.alternate.map((b) => genStatement(b, ctx)).join("\n ")}\n}`;
181
+ if (s.kind === "MatchStmt") return `{ const _v = ${jsOfExpr(s.scrutinee, ctx)};\n ${s.arms.map((arm) => {
182
+ if (arm.pattern.kind === "PVariant") {
183
+ const inner = makeEvalCtx(ctx.gen, ctx.localBinds, ctx.reducerScope);
184
+ for (const b of arm.pattern.binds) if (b !== "_") inner.localBinds.add(b);
185
+ const binds = arm.pattern.binds.map((b, i) => b !== "_" ? `const ${jsName(b)} = _v[${JSON.stringify(`_${i}`)}];` : "").join(" ");
186
+ const body = arm.body.map((b) => genStatement(b, inner)).join("\n ");
187
+ return `if (_s.variantIs(_v, ${JSON.stringify(arm.pattern.name)})) { ${binds}\n ${body}\n}`;
188
+ }
189
+ if (arm.pattern.kind === "PBind") {
190
+ const inner = makeEvalCtx(ctx.gen, ctx.localBinds, ctx.reducerScope);
191
+ inner.localBinds.add(arm.pattern.name);
192
+ const body = arm.body.map((b) => genStatement(b, inner)).join("\n ");
193
+ return `if (true) { const ${jsName(arm.pattern.name)} = _v;\n ${body}\n}`;
194
+ }
195
+ if (arm.pattern.kind === "PWildcard") return `if (true) {\n ${arm.body.map((b) => genStatement(b, ctx)).join("\n ")}\n}`;
196
+ const body = arm.body.map((b) => genStatement(b, ctx)).join("\n ");
197
+ return `if (_v === ${JSON.stringify(arm.pattern.value)}) {\n ${body}\n}`;
198
+ }).join(" else ")}\n}`;
199
+ if (s.kind === "NoopStmt") return `/* no-op */`;
200
+ if (s.kind === "LetStmt") {
201
+ const rhs = jsOfExpr(s.rhs, ctx);
202
+ ctx.localBinds.add(s.name);
203
+ return `const ${jsName(s.name)} = ${rhs};`;
204
+ }
205
+ if (s.kind === "Emit") {
206
+ const args = s.args.map((a) => jsOfExpr(a, ctx)).join(", ");
207
+ return `_emits.push({ effect: ${JSON.stringify(s.effect)}, args: [${args}] });`;
208
+ }
209
+ return genSlotAssign(s.lvalue, s.rhs, ctx);
210
+ }
211
+ function genSlotAssign(lv, rhs, ctx) {
212
+ const rhsJs = jsOfExpr(rhs, ctx);
213
+ if (lv.kind === "LSlot") return `_next[${JSON.stringify(lv.name)}] = ${rhsJs};`;
214
+ const root = lvalueRootName(lv);
215
+ const path = [];
216
+ let cur = lv;
217
+ while (cur.kind !== "LSlot") {
218
+ if (cur.kind === "LField") path.unshift({
219
+ kind: "field",
220
+ name: cur.field
221
+ });
222
+ else path.unshift({
223
+ kind: "index",
224
+ expr: cur.index
225
+ });
226
+ cur = cur.base;
227
+ }
228
+ const rootKey = JSON.stringify(root);
229
+ const baseJs = ctx.reducerScope ? `(((_next[${rootKey}] !== undefined) ? _next[${rootKey}] : _live[${rootKey}]) ?? {})` : `(_live[${rootKey}] ?? {})`;
230
+ let pathExpr = "";
231
+ for (const seg of path) if (seg.kind === "field") pathExpr += `, ${JSON.stringify(seg.name)}`;
232
+ else pathExpr += `, ${jsOfExpr(seg.expr, ctx)}`;
233
+ return `_next[${JSON.stringify(root)}] = _setPath(${baseJs}, [${pathExpr.replace(/^, /, "")}], ${rhsJs});`;
234
+ }
235
+ function lvalueRootName(lv) {
236
+ while (lv.kind !== "LSlot") lv = lv.base;
237
+ return lv.name;
238
+ }
239
+ function jsOfExpr(e, ctx) {
240
+ switch (e.kind) {
241
+ case "Num": return String(e.value);
242
+ case "Str": return JSON.stringify(e.value);
243
+ case "Bool": return e.value ? "true" : "false";
244
+ case "Unit": return "null";
245
+ case "Ref":
246
+ if (ctx.localBinds.has(e.name)) return jsName(e.name);
247
+ if (e.name === "now") return `_s.now()`;
248
+ if (e.name === "route") return ctx.reducerScope ? `((_next["route"] !== undefined) ? _next["route"] : _live["route"])` : `_live["route"]`;
249
+ if (ctx.gen.slots?.some((s) => s.name === e.name)) {
250
+ const key = JSON.stringify(e.name);
251
+ return ctx.reducerScope ? `((_next[${key}] !== undefined) ? _next[${key}] : _live[${key}])` : `_live[${key}]`;
252
+ }
253
+ return jsName(e.name);
254
+ case "BinOp": {
255
+ const l = jsOfExpr(e.lhs, ctx);
256
+ const r = jsOfExpr(e.rhs, ctx);
257
+ if (e.op === "+") return `_s.add(${l}, ${r})`;
258
+ if (e.op === "&") return `(${l} && ${r})`;
259
+ if (e.op === "|") return `(${l} || ${r})`;
260
+ if (e.op === "==") return `_s.eq(${l}, ${r})`;
261
+ if (e.op === "!=") return `(!_s.eq(${l}, ${r}))`;
262
+ return `(${l} ${e.op} ${r})`;
263
+ }
264
+ case "UnaryOp": return `(${e.op === "!" ? "!" : "-"}${jsOfExpr(e.rhs, ctx)})`;
265
+ case "FieldAccess": {
266
+ const baseJs = jsOfExpr(e.base, ctx);
267
+ if (e.field === "get") return `_s.unwrap(${baseJs})`;
268
+ if (e.field === "is-some") return `(_s.variantIs(${baseJs}, "Some"))`;
269
+ if (e.field === "is-none") return `(_s.variantIs(${baseJs}, "None"))`;
270
+ if (e.field === "is-ok") return `(_s.variantIs(${baseJs}, "Ok"))`;
271
+ if (e.field === "is-err") return `(_s.variantIs(${baseJs}, "Err"))`;
272
+ if (e.field === "keys") return `_s.mapKeys(${baseJs})`;
273
+ if (e.field === "values") return `_s.mapValues(${baseJs})`;
274
+ if (e.field === "entries") return `_s.mapEntries(${baseJs})`;
275
+ if (e.field === "size") return `_s.mapSize(${baseJs})`;
276
+ if (e.field === "to-ms" || e.field === "ms") return `(${baseJs})`;
277
+ if (e.field === "show") return `_s.show(${baseJs})`;
278
+ if (e.field === "length") return `((${baseJs}) ?? "").length`;
279
+ if (e.field === "is-empty") return `(((${baseJs}) ?? []).length === 0 || ((${baseJs}) ?? "") === "")`;
280
+ if (e.field === "lower") return `(String((${baseJs}) ?? "")).toLowerCase()`;
281
+ if (e.field === "upper") return `(String((${baseJs}) ?? "")).toUpperCase()`;
282
+ if (e.field === "trim") return `(String((${baseJs}) ?? "")).trim()`;
283
+ if (e.field === "unique") return `[...new Set((${baseJs}) ?? [])]`;
284
+ if (e.field === "reverse") return `[...((${baseJs}) ?? [])].reverse()`;
285
+ if (e.field === "sort") return `[...((${baseJs}) ?? [])].sort()`;
286
+ return `(${baseJs})[${JSON.stringify(e.field)}]`;
287
+ }
288
+ case "Index": return `(${jsOfExpr(e.base, ctx)})[${jsOfExpr(e.index, ctx)}]`;
289
+ case "Call": {
290
+ const cn = e.callee;
291
+ if (cn === "now") return `_s.now()`;
292
+ if (/^[A-Z][A-Za-z0-9_]*\.fresh$/.test(cn)) return `_s.freshId()`;
293
+ if (/^[A-Z][A-Za-z0-9_]*\.parse$/.test(cn)) {
294
+ const a = e.args[0] ? jsOfExpr(e.args[0], ctx) : "\"\"";
295
+ const qualifier = cn.split(".")[0];
296
+ if (qualifier === "Int") return `((_v) => { const _n = Number(_v); return (String(_v).trim() !== "" && Number.isFinite(_n)) ? _s.Some(Math.trunc(_n)) : _s.None; })(${a})`;
297
+ if (qualifier === "Float") return `((_v) => { const _n = Number(_v); return (String(_v).trim() !== "" && Number.isFinite(_n)) ? _s.Some(_n) : _s.None; })(${a})`;
298
+ return `((_v) => (typeof _v === "string" && _v.length > 0) ? _s.Some(_v) : _s.None)(${a})`;
299
+ }
300
+ if (/^[A-Z][A-Za-z0-9_]*\.show$/.test(cn)) return `_s.show(${e.args[0] ? jsOfExpr(e.args[0], ctx) : "\"\""})`;
301
+ if (cn === "Duration.ms") return `(${e.args[0] ? jsOfExpr(e.args[0], ctx) : "0"})`;
302
+ if (cn === "Duration.s") return `((${e.args[0] ? jsOfExpr(e.args[0], ctx) : "0"}) * 1000)`;
303
+ if (cn === "Duration.m" || cn === "Duration.min") return `((${e.args[0] ? jsOfExpr(e.args[0], ctx) : "0"}) * 60000)`;
304
+ if (cn === "Duration.h") return `((${e.args[0] ? jsOfExpr(e.args[0], ctx) : "0"}) * 3600000)`;
305
+ if (cn === "Duration.d" || cn === "Duration.days") return `((${e.args[0] ? jsOfExpr(e.args[0], ctx) : "0"}) * 86400000)`;
306
+ if (cn === "Decoder.Json") return `"json"`;
307
+ if (cn === "Decoder.Text") return `"text"`;
308
+ if (cn === "Decoder.Bytes") return `"bytes"`;
309
+ if (cn === "Decoder.None") return `"none"`;
310
+ if (cn === "fmt") {
311
+ const args = e.args.map((a) => jsOfExpr(a, ctx));
312
+ return `_s.fmt ? _s.fmt(${args.join(", ")}) : ${args[0] ?? "\"\""}`;
313
+ }
314
+ const args = e.args.map((a) => jsOfExpr(a, ctx)).join(", ");
315
+ return `${jsName(cn)}(${args})`;
316
+ }
317
+ case "MethodCall": return methodCallJs(e.receiver, e.method, e.args, ctx);
318
+ case "RecordLit": return `{ ${e.fields.map((f) => `${JSON.stringify(f.name)}: ${jsOfExpr(f.value, ctx)}`).join(", ")} }`;
319
+ case "ListLit": return `[${e.items.map((it) => jsOfExpr(it, ctx)).join(", ")}]`;
320
+ case "MapLit": return `{ ${e.entries.map((en) => `[${jsOfExpr(en.key, ctx)}]: ${jsOfExpr(en.value, ctx)}`).join(", ")} }`;
321
+ case "MatchExpr": return matchExprJs(e, ctx);
322
+ case "IfExpr": return `((${jsOfExpr(e.cond, ctx)}) ? (${jsOfExpr(e.consequent, ctx)}) : (${jsOfExpr(e.alternate, ctx)}))`;
323
+ case "LetIn": {
324
+ const inner = makeEvalCtx(ctx.gen, ctx.localBinds);
325
+ inner.localBinds.add(e.name);
326
+ return `(() => { const ${jsName(e.name)} = ${jsOfExpr(e.value, ctx)}; return ${jsOfExpr(e.body, inner)}; })()`;
327
+ }
328
+ case "Variant": return variantJs(e.name, e.payload, ctx);
329
+ }
330
+ }
331
+ /**
332
+ * Methods the code generator actually implements (the `methodCallJs` switch
333
+ * cases below). This is the single source of truth for what `obj.method(...)`
334
+ * calls are runnable; the typechecker uses it to flag unimplemented methods
335
+ * (E0801) at `check` time instead of letting them throw or misbehave at runtime.
336
+ * Keep this in exact sync with the `switch (method)` cases.
337
+ */
338
+ const KNOWN_METHODS = new Set([
339
+ "filter",
340
+ "map",
341
+ "flat-map",
342
+ "size",
343
+ "keys",
344
+ "has",
345
+ "toggle",
346
+ "get",
347
+ "get-or",
348
+ "remove",
349
+ "insert",
350
+ "sort-by",
351
+ "fold",
352
+ "show",
353
+ "is-some",
354
+ "is-none",
355
+ "is-empty",
356
+ "to-ms",
357
+ "copy",
358
+ "find",
359
+ "push",
360
+ "unique",
361
+ "reverse",
362
+ "join",
363
+ "split",
364
+ "contains",
365
+ "starts-with",
366
+ "ends-with",
367
+ "length",
368
+ "slice",
369
+ "trim",
370
+ "format",
371
+ "plus",
372
+ "minus",
373
+ "diff",
374
+ "concat",
375
+ "prepend",
376
+ "chunk",
377
+ "zip",
378
+ "merge",
379
+ "update",
380
+ "add",
381
+ "union",
382
+ "intersect",
383
+ "or",
384
+ "map-err",
385
+ "replace",
386
+ "min",
387
+ "max",
388
+ "clamp"
389
+ ]);
390
+ function methodCallJs(recv, method, args, ctx) {
391
+ const inner = makeEvalCtx(ctx.gen, ctx.localBinds);
392
+ inner.localBinds.add("$1");
393
+ inner.localBinds.add("$2");
394
+ const recvJs = jsOfExpr(recv, ctx);
395
+ const argFnList = (a) => `((__x, __y) => { const _isPair = (Array.isArray(__x) && __x.length === 2); const ${jsName("$1")} = _isPair ? __x[0] : __x; const ${jsName("$2")} = _isPair ? __x[1] : (__y !== undefined ? __y : __x); return ${jsOfExpr(a, inner)}; })`;
396
+ const argRaw = (a) => jsOfExpr(a, ctx);
397
+ switch (method) {
398
+ case "filter": return `_s.filter(${recvJs}, ${argFnList(args[0])})`;
399
+ case "map": return `_s.mapOver(${recvJs}, ${argFnList(args[0])})`;
400
+ case "flat-map": return `_s.flatMapOption(${recvJs}, ((${jsName("$1")}) => ${jsOfExpr(args[0], inner)}))`;
401
+ case "size": return `_s.mapSize(${recvJs})`;
402
+ case "keys": return `_s.mapKeys(${recvJs})`;
403
+ case "has": return `_s.setHas(${recvJs}, ${argRaw(args[0])})`;
404
+ case "toggle": return `_s.setToggle(${recvJs}, ${argRaw(args[0])})`;
405
+ case "get": return `((_v) => _v === undefined ? _s.None : _s.Some(_v))(_s.mapGet(${recvJs}, ${argRaw(args[0])}))`;
406
+ case "get-or":
407
+ if (args.length === 1) return `_s.getOr(${recvJs}, ${argRaw(args[0])})`;
408
+ return `_s.mapGetOr(${recvJs}, ${argRaw(args[0])}, ${argRaw(args[1])})`;
409
+ case "remove": return `_s.mapRemove(${recvJs}, ${argRaw(args[0])})`;
410
+ case "insert": return `_s.mapInsert(${recvJs}, ${argRaw(args[0])}, ${argRaw(args[1])})`;
411
+ case "sort-by": return `_s.listSortBy(${recvJs}, ${argFnList(args[0])})`;
412
+ case "fold": return `_s.listFold(${recvJs}, ${argRaw(args[0])}, (${jsName("$1")}, ${jsName("$2")}) => ${jsOfExpr(args[1], inner)})`;
413
+ case "show": return `_s.show(${recvJs})`;
414
+ case "is-some": return `_s.variantIs(${recvJs}, "Some")`;
415
+ case "is-none": return `_s.variantIs(${recvJs}, "None")`;
416
+ case "is-empty": return `(_s.mapSize(${recvJs}) === 0)`;
417
+ case "to-ms": return `(${recvJs})`;
418
+ case "copy":
419
+ if (args[0] && args[0].kind === "RecordLit") return `_s.recordCopy(${recvJs}, ${jsOfExpr(args[0], ctx)})`;
420
+ return `_s.recordCopy(${recvJs}, {})`;
421
+ case "find": return `((${recvJs}) || []).find(${argFnList(args[0])})`;
422
+ case "push": return `[...(${recvJs} ?? []), ${argRaw(args[0])}]`;
423
+ case "unique": return `[...new Set((${recvJs} ?? []))]`;
424
+ case "reverse": return `[...(${recvJs} ?? [])].reverse()`;
425
+ case "join": return `((${recvJs}) ?? []).join(${argRaw(args[0])})`;
426
+ case "split": return `((${recvJs}) ?? "").split(${argRaw(args[0])})`;
427
+ case "contains": return `(typeof (${recvJs}) === "string" ? ((${recvJs}) ?? "").includes(${argRaw(args[0])}) : ((${recvJs}) ?? []).includes(${argRaw(args[0])}))`;
428
+ case "starts-with": return `((${recvJs}) ?? "").startsWith(${argRaw(args[0])})`;
429
+ case "ends-with": return `((${recvJs}) ?? "").endsWith(${argRaw(args[0])})`;
430
+ case "length": return `((${recvJs}) || "").length`;
431
+ case "slice": return `((${recvJs}) || "").slice(${args.map(argRaw).join(", ")})`;
432
+ case "trim": return `((${recvJs}) || "").trim()`;
433
+ case "format": return `(new Date(${recvJs})).toISOString().slice(0, 10)`;
434
+ case "plus": return `((${recvJs}) + (${argRaw(args[0])}))`;
435
+ case "minus": return `((${recvJs}) - (${argRaw(args[0])}))`;
436
+ case "diff": return `_s.diff(${recvJs}, ${argRaw(args[0])})`;
437
+ case "concat": return `[...((${recvJs}) ?? []), ...((${argRaw(args[0])}) ?? [])]`;
438
+ case "prepend": return `[${argRaw(args[0])}, ...((${recvJs}) ?? [])]`;
439
+ case "chunk": return `_s.listChunk(${recvJs}, ${argRaw(args[0])})`;
440
+ case "zip": return `_s.listZip(${recvJs}, ${argRaw(args[0])})`;
441
+ case "merge": return `({ ...((${recvJs}) ?? {}), ...((${argRaw(args[0])}) ?? {}) })`;
442
+ case "update": return `_s.mapUpdate(${recvJs}, ${argRaw(args[0])}, ((${jsName("$1")}) => (${jsOfExpr(args[1], inner)})))`;
443
+ case "add": return `_s.setAdd(${recvJs}, ${argRaw(args[0])})`;
444
+ case "union": return `_s.setUnion(${recvJs}, ${argRaw(args[0])})`;
445
+ case "intersect": return `_s.setIntersect(${recvJs}, ${argRaw(args[0])})`;
446
+ case "or": return `_s.or(${recvJs}, ${argRaw(args[0])})`;
447
+ case "map-err": return `_s.mapErr(${recvJs}, ((${jsName("$1")}) => (${jsOfExpr(args[0], inner)})))`;
448
+ case "replace": return `String((${recvJs}) ?? "").replaceAll(${argRaw(args[0])}, ${argRaw(args[1])})`;
449
+ case "min": return `Math.min((${recvJs}), (${argRaw(args[0])}))`;
450
+ case "max": return `Math.max((${recvJs}), (${argRaw(args[0])}))`;
451
+ case "clamp": return `Math.min(Math.max((${recvJs}), (${argRaw(args[0])})), (${argRaw(args[1])}))`;
452
+ default: return `(${recvJs}).${jsName(method)}(${args.map(argRaw).join(", ")})`;
453
+ }
454
+ }
455
+ function variantJs(name, payload, ctx) {
456
+ if (payload.length === 0) {
457
+ if (name === "None") return `_s.None`;
458
+ return `({ _tag: ${JSON.stringify(name)} })`;
459
+ }
460
+ if (name === "Some") return `_s.Some(${jsOfExpr(payload[0], ctx)})`;
461
+ if (name === "Ok") return `_s.Ok(${jsOfExpr(payload[0], ctx)})`;
462
+ if (name === "Err") return `_s.Err(${jsOfExpr(payload[0], ctx)})`;
463
+ return `_s.variant(${JSON.stringify(name)}, ${payload.map((p) => jsOfExpr(p, ctx)).join(", ")})`;
464
+ }
465
+ function matchExprJs(e, ctx) {
466
+ const sc = jsOfExpr(e.scrutinee, ctx);
467
+ return `((_v) => { ${e.arms.map((arm) => matchArmJs(arm.pattern, arm.body, ctx, "_v")).join(" else ")} else { return undefined; } })(${sc})`;
468
+ }
469
+ function matchArmJs(p, body, ctx, scVar) {
470
+ if (p.kind === "PWildcard") return `if (true) { return ${jsOfExpr(body, ctx)}; }`;
471
+ if (p.kind === "PBind") {
472
+ const inner = makeEvalCtx(ctx.gen, ctx.localBinds);
473
+ inner.localBinds.add(p.name);
474
+ return `if (true) { const ${jsName(p.name)} = ${scVar}; return ${jsOfExpr(body, inner)}; }`;
475
+ }
476
+ if (p.kind === "PLiteral") return `if (${scVar} === ${JSON.stringify(p.value)}) { return ${jsOfExpr(body, ctx)}; }`;
477
+ const tag = p.name;
478
+ const inner = makeEvalCtx(ctx.gen, ctx.localBinds);
479
+ const bindAssigns = [];
480
+ for (let i = 0; i < p.binds.length; i++) {
481
+ const name = p.binds[i];
482
+ if (name === "_") continue;
483
+ inner.localBinds.add(name);
484
+ bindAssigns.push(`const ${jsName(name)} = (${scVar})[${JSON.stringify(`_${i}`)}];`);
485
+ }
486
+ return `if (_s.variantIs(${scVar}, ${JSON.stringify(tag)})) { ${bindAssigns.join(" ")} return ${jsOfExpr(body, inner)}; }`;
487
+ }
488
+ function genTile(tile, gen) {
489
+ const ctx = makeEvalCtx(gen, new Set(tile.in ? ["$1"] : []));
490
+ return tileExprJs(tile.body, gen, ctx, tile.name);
491
+ }
492
+ function tileExprJs(t, gen, ctx, enclosingTile) {
493
+ switch (t.kind) {
494
+ case "TileFor": {
495
+ const iter = jsOfExpr(t.iter, ctx);
496
+ const inner = makeEvalCtx(gen, ctx.localBinds);
497
+ inner.localBinds.add(t.bind);
498
+ return `((${iter}) || []).map((${jsName(t.bind)}) => (${tileExprJs(t.body, gen, inner, enclosingTile)}))`;
499
+ }
500
+ case "TileWhen": return `((${jsOfExpr(t.cond, ctx)}) ? (${tileExprJs(t.body, gen, ctx, enclosingTile)}) : null)`;
501
+ case "TileIf": return `((${jsOfExpr(t.cond, ctx)}) ? (${tileExprJs(t.consequent, gen, ctx, enclosingTile)}) : (${tileExprJs(t.alternate, gen, ctx, enclosingTile)}))`;
502
+ case "TileMatch": {
503
+ const sc = jsOfExpr(t.scrutinee, ctx);
504
+ return `((_v) => { ${t.arms.map((arm) => {
505
+ if (arm.pattern.kind === "PVariant") {
506
+ const inner = makeEvalCtx(gen, ctx.localBinds);
507
+ for (const b of arm.pattern.binds) if (b !== "_") inner.localBinds.add(b);
508
+ const binds = arm.pattern.binds.map((b, i) => b !== "_" ? `const ${jsName(b)} = _v[${JSON.stringify(`_${i}`)}];` : "").join(" ");
509
+ return `if (_s.variantIs(_v, ${JSON.stringify(arm.pattern.name)})) { ${binds} return ${tileExprJs(arm.body, gen, inner, enclosingTile)}; }`;
510
+ }
511
+ if (arm.pattern.kind === "PBind") {
512
+ const inner = makeEvalCtx(gen, ctx.localBinds);
513
+ inner.localBinds.add(arm.pattern.name);
514
+ return `if (true) { const ${jsName(arm.pattern.name)} = _v; return ${tileExprJs(arm.body, gen, inner, enclosingTile)}; }`;
515
+ }
516
+ if (arm.pattern.kind === "PWildcard") return `if (true) { return ${tileExprJs(arm.body, gen, ctx, enclosingTile)}; }`;
517
+ return `if (_v === ${JSON.stringify(arm.pattern.value)}) { return ${tileExprJs(arm.body, gen, ctx, enclosingTile)}; }`;
518
+ }).join(" else ")} else { return { kind: "text", text: "" }; } })(${sc})`;
519
+ }
520
+ case "TileCall": return tileCallJs(t, gen, ctx, enclosingTile);
521
+ }
522
+ }
523
+ const BUILTIN_TILES$2 = new Set([
524
+ "page",
525
+ "row",
526
+ "column",
527
+ "card",
528
+ "box",
529
+ "panel",
530
+ "grid",
531
+ "stack",
532
+ "region",
533
+ "scroll",
534
+ "divider",
535
+ "fieldset",
536
+ "heading",
537
+ "text",
538
+ "button",
539
+ "form",
540
+ "input",
541
+ "textarea",
542
+ "label",
543
+ "check",
544
+ "spinner",
545
+ "select",
546
+ "radio",
547
+ "slider",
548
+ "switch",
549
+ "link",
550
+ "markdown",
551
+ "skeleton",
552
+ "image",
553
+ "icon"
554
+ ]);
555
+ /**
556
+ * For `bind=draft` or `bind=draft.title.deeper`, extract the root slot name,
557
+ * the static path (string field names), and a JS expression to read the value.
558
+ * Only static field-access paths are supported (no Index, no dynamic lookups).
559
+ * Returns null if no `bind=` arg exists or the path isn't statically resolvable.
560
+ */
561
+ function extractBindPath(args) {
562
+ const bindArg = args.find((a) => a.name === "bind");
563
+ if (!bindArg) return null;
564
+ let cur = bindArg.value;
565
+ const reverseSegments = [];
566
+ while (cur.kind === "FieldAccess") {
567
+ reverseSegments.push(cur.field);
568
+ cur = cur.base;
569
+ }
570
+ if (cur.kind !== "Ref") return null;
571
+ const root = cur.name;
572
+ const path = reverseSegments.reverse();
573
+ let readRaw = `_live[${JSON.stringify(root)}]`;
574
+ for (const seg of path) readRaw = `((${readRaw}) ?? {})[${JSON.stringify(seg)}]`;
575
+ return {
576
+ root,
577
+ path,
578
+ readJs: readRaw,
579
+ readJsRaw: readRaw
580
+ };
581
+ }
582
+ function tileCallJs(t, gen, ctx, enclosingTile) {
583
+ const name = t.name;
584
+ if (!BUILTIN_TILES$2.has(name)) {
585
+ const def = gen.tiles.find((x) => x.name === name);
586
+ if (!def) throw new Error(`Tile "${name}" not found`);
587
+ const inner = makeEvalCtx(gen, ctx.localBinds);
588
+ const arg1 = t.args[0];
589
+ const TILE_KINDS = new Set([
590
+ "TileCall",
591
+ "TileFor",
592
+ "TileWhen",
593
+ "TileIf",
594
+ "TileMatch"
595
+ ]);
596
+ const wrapBoundary = (body) => {
597
+ if (!def.errorBoundary) return body;
598
+ const fb = gen.tiles.find((x) => x.name === def.errorBoundary);
599
+ if (!fb) return body;
600
+ const fbCtx = makeEvalCtx(gen, new Set(["$1"]));
601
+ const fbBody = tileExprJs(fb.body, gen, fbCtx, fb.name);
602
+ return `((() => { try { return ${body}; } catch (_err) { const ${jsName("$1")} = { message: String(_err && _err.message || _err), location: ${JSON.stringify(def.name)} }; return ${fbBody}; } })())`;
603
+ };
604
+ if (arg1) {
605
+ const v = arg1.value;
606
+ if (TILE_KINDS.has(v.kind ?? "")) return wrapBoundary(tileExprJs(v, gen, inner, def.name));
607
+ const oneJs = jsOfExpr(v, ctx);
608
+ const propsJs = propsFor(t, ctx);
609
+ const bodyJs = tileExprJs(def.body, gen, addBind(inner, "$1"), def.name);
610
+ return wrapBoundary(`((_arg, _propsOuter) => { const ${jsName("$1")} = _arg; return _attachProps(${bodyJs}, _propsOuter); })(${oneJs}, ${propsJs})`);
611
+ }
612
+ const propsJs = propsFor(t, ctx);
613
+ return wrapBoundary(`(_attachProps(${tileExprJs(def.body, gen, inner, def.name)}, ${propsJs}))`);
614
+ }
615
+ const propsObj = propsFor(t, ctx, enclosingTile);
616
+ switch (name) {
617
+ case "page":
618
+ case "row":
619
+ case "column":
620
+ case "card":
621
+ case "box":
622
+ case "grid":
623
+ case "stack":
624
+ case "region":
625
+ case "scroll":
626
+ case "divider":
627
+ case "fieldset":
628
+ case "panel": {
629
+ const children = collectChildren(t.args, gen, ctx, enclosingTile);
630
+ return `({ kind: ${JSON.stringify(name)}, children: [${children}], props: ${propsObj} })`;
631
+ }
632
+ case "heading": return `({ kind: "heading", text: _s.show(${t.args[0] ? jsOfExpr(asExpr(t.args[0].value), ctx) : "\"\""}), props: ${propsObj} })`;
633
+ case "text": return `({ kind: "text", text: _s.show(${t.args[0] ? jsOfExpr(asExpr(t.args[0].value), ctx) : "\"\""}), props: ${propsObj} })`;
634
+ case "button": {
635
+ const textArg = t.args.find((a) => a.name === "text");
636
+ return `({ kind: "button", text: _s.show(${textArg ? jsOfExpr(asExpr(textArg.value), ctx) : "\"\""}), props: ${propsObj} })`;
637
+ }
638
+ case "input": {
639
+ const fields = [`kind: "input"`];
640
+ const bindInfo = extractBindPath(t.args);
641
+ for (const arg of t.args) {
642
+ if (!arg.name || arg.name === "bind") continue;
643
+ const valJs = jsOfExpr(asExpr(arg.value), ctx);
644
+ if (arg.name === "value") fields.push(`value: _s.show(${valJs})`);
645
+ else if (arg.name === "placeholder") fields.push(`placeholder: ${valJs}`);
646
+ else if (arg.name === "type") fields.push(`type: ${valJs}`);
647
+ else if (arg.name === "id") fields.push(`id: ${valJs}`);
648
+ else if (arg.name === "auto-focus") fields.push(`autoFocus: ${valJs}`);
649
+ else if (arg.name === "required") fields.push(`required: ${valJs}`);
650
+ }
651
+ if (bindInfo) {
652
+ fields.push(`bind: ${JSON.stringify(bindInfo.root)}`);
653
+ if (bindInfo.path.length > 0) fields.push(`bindPath: ${JSON.stringify(bindInfo.path)}`);
654
+ fields.push(`value: _s.show(${bindInfo.readJs})`);
655
+ }
656
+ fields.push(`props: ${propsObj}`);
657
+ return `({ ${fields.join(", ")} })`;
658
+ }
659
+ case "textarea": {
660
+ const fields = [`kind: "textarea"`];
661
+ const bindInfo = extractBindPath(t.args);
662
+ for (const arg of t.args) {
663
+ if (!arg.name || arg.name === "bind") continue;
664
+ const valJs = jsOfExpr(asExpr(arg.value), ctx);
665
+ if (arg.name === "value") fields.push(`value: _s.show(${valJs})`);
666
+ else if (arg.name === "placeholder") fields.push(`placeholder: ${valJs}`);
667
+ else if (arg.name === "id") fields.push(`id: ${valJs}`);
668
+ else if (arg.name === "rows") fields.push(`rows: ${valJs}`);
669
+ }
670
+ if (bindInfo) {
671
+ fields.push(`bind: ${JSON.stringify(bindInfo.root)}`);
672
+ if (bindInfo.path.length > 0) fields.push(`bindPath: ${JSON.stringify(bindInfo.path)}`);
673
+ fields.push(`value: _s.show(${bindInfo.readJs})`);
674
+ }
675
+ fields.push(`props: ${propsObj}`);
676
+ return `({ ${fields.join(", ")} })`;
677
+ }
678
+ case "check": {
679
+ const valArg = t.args.find((a) => a.name === "value");
680
+ return `({ kind: "check", checked: !!(${valArg ? jsOfExpr(asExpr(valArg.value), ctx) : "false"}), props: ${propsObj} })`;
681
+ }
682
+ case "select": {
683
+ const fields = [`kind: "select"`];
684
+ const bindInfo = extractBindPath(t.args);
685
+ if (bindInfo) {
686
+ fields.push(`bind: ${JSON.stringify(bindInfo.root)}`);
687
+ if (bindInfo.path.length > 0) fields.push(`bindPath: ${JSON.stringify(bindInfo.path)}`);
688
+ fields.push(`value: ${bindInfo.readJsRaw}`);
689
+ } else {
690
+ const valArg = t.args.find((a) => a.name === "value");
691
+ if (valArg) fields.push(`value: ${jsOfExpr(asExpr(valArg.value), ctx)}`);
692
+ }
693
+ const optionsArg = t.args.find((a) => a.name === "options");
694
+ if (optionsArg) fields.push(`options: ${jsOfExpr(asExpr(optionsArg.value), ctx)}`);
695
+ else fields.push(`options: []`);
696
+ const placeholderArg = t.args.find((a) => a.name === "placeholder");
697
+ if (placeholderArg) fields.push(`placeholder: ${jsOfExpr(asExpr(placeholderArg.value), ctx)}`);
698
+ fields.push(`props: ${propsObj}`);
699
+ return `({ ${fields.join(", ")} })`;
700
+ }
701
+ case "radio": {
702
+ const fields = [`kind: "radio"`];
703
+ for (const arg of t.args) {
704
+ if (!arg.name) continue;
705
+ const valJs = jsOfExpr(asExpr(arg.value), ctx);
706
+ if (arg.name === "group") fields.push(`group: ${valJs}`);
707
+ else if (arg.name === "value") fields.push(`value: ${valJs}`);
708
+ else if (arg.name === "selected") fields.push(`selected: !!(${valJs})`);
709
+ }
710
+ fields.push(`props: ${propsObj}`);
711
+ return `({ ${fields.join(", ")} })`;
712
+ }
713
+ case "spinner": return `({ kind: "spinner", props: ${propsObj} })`;
714
+ case "form": return `({ kind: "form", children: [${collectChildren(t.args, gen, ctx, enclosingTile)}], props: ${propsObj} })`;
715
+ case "label": {
716
+ const text = t.args.find((a) => a.name === "text");
717
+ return `({ kind: "label", text: _s.show(${text ? jsOfExpr(asExpr(text.value), ctx) : "\"\""}), props: ${propsObj} })`;
718
+ }
719
+ case "link": {
720
+ const toArg = t.args.find((a) => a.name === "to");
721
+ const to = toArg ? jsOfExpr(asExpr(toArg.value), ctx) : "\"\"";
722
+ const textProp = t.props.find((p) => p.name === "text");
723
+ return `({ kind: "link", text: _s.show(${textProp ? jsOfExpr(textProp.value, ctx) : "\"\""}), to: _s.show(${to}), props: ${propsObj} })`;
724
+ }
725
+ case "markdown": return `({ kind: "markdown", text: _s.show(${t.args[0] ? jsOfExpr(asExpr(t.args[0].value), ctx) : "\"\""}), props: ${propsObj} })`;
726
+ case "skeleton": return `({ kind: "skeleton", props: ${propsObj} })`;
727
+ case "image": {
728
+ const src = t.args.find((a) => a.name === "src");
729
+ return `({ kind: "image", src: _s.show(${src ? jsOfExpr(asExpr(src.value), ctx) : "\"\""}), props: ${propsObj} })`;
730
+ }
731
+ case "icon": {
732
+ const name = t.args.find((a) => a.name === "name");
733
+ return `({ kind: "icon", name: _s.show(${name ? jsOfExpr(asExpr(name.value), ctx) : "\"\""}), props: ${propsObj} })`;
734
+ }
735
+ }
736
+ throw new Error(`Unsupported builtin tile "${name}"`);
737
+ }
738
+ function asExpr(v) {
739
+ return v;
740
+ }
741
+ function collectChildren(args, gen, ctx, enclosingTile) {
742
+ const parts = [];
743
+ for (const a of args) {
744
+ if (a.name) continue;
745
+ const v = a.value;
746
+ if (v.kind === "TileCall" || v.kind === "TileFor" || v.kind === "TileWhen" || v.kind === "TileIf" || v.kind === "TileMatch") parts.push(tileExprJs(v, gen, ctx, enclosingTile));
747
+ else if (v.kind === "Ref") {
748
+ const refName = v.name;
749
+ const def = gen.tiles.find((x) => x.name === refName);
750
+ if (def) parts.push(tileExprJs(def.body, gen, ctx, def.name));
751
+ else parts.push("null");
752
+ }
753
+ }
754
+ return `..._children(${parts.join(", ")})`;
755
+ }
756
+ function propsFor(t, ctx, enclosingTile) {
757
+ const entries = [];
758
+ for (const a of t.args) {
759
+ if (!a.name) continue;
760
+ if (a.name === "onClick" || a.name === "onSubmit" || a.name === "onChange" || a.name === "onInput") {
761
+ if (a.value.kind === "Ref") {
762
+ const reducerName = a.value.name;
763
+ entries.push(`${a.name}: (el) => globalThis.__kumikiApp._dispatch(${JSON.stringify(reducerName)}, el)`);
764
+ }
765
+ }
766
+ }
767
+ for (const p of t.props) {
768
+ if (p.name === "onClick" || p.name === "onSubmit" || p.name === "onChange" || p.name === "onInput") {
769
+ if (p.value.kind === "Ref") {
770
+ const reducerName = p.value.name;
771
+ entries.push(`${p.name}: (el) => globalThis.__kumikiApp._dispatch(${JSON.stringify(reducerName)}, el)`);
772
+ }
773
+ continue;
774
+ }
775
+ entries.push(`${jsName(p.name)}: ${jsOfExpr(p.value, ctx)}`);
776
+ }
777
+ if (t.name === "button" && enclosingTile) {
778
+ const r = ctx.gen.reducers.find((rr) => rr.on.kind === "UiEvent" && rr.on.ev === "click" && rr.on.selector.tile === enclosingTile);
779
+ if (r && !entries.some((e) => e.startsWith("onClick"))) entries.push(`onClick: (el) => globalThis.__kumikiApp._dispatch(${JSON.stringify(r.name)}, el)`);
780
+ }
781
+ if (t.name === "check" && enclosingTile) {
782
+ const r = ctx.gen.reducers.find((rr) => rr.on.kind === "UiEvent" && rr.on.ev === "click" && rr.on.selector.tile === enclosingTile);
783
+ if (r && !entries.some((e) => e.startsWith("onClick"))) entries.push(`onClick: (el) => globalThis.__kumikiApp._dispatch(${JSON.stringify(r.name)}, el)`);
784
+ }
785
+ if (t.name === "form" && enclosingTile) {
786
+ const r = ctx.gen.reducers.find((rr) => rr.on.kind === "UiEvent" && rr.on.ev === "submit" && rr.on.selector.tile === enclosingTile);
787
+ if (r) entries.push(`onSubmit: (el) => globalThis.__kumikiApp._dispatch(${JSON.stringify(r.name)}, el)`);
788
+ }
789
+ if ((t.name === "select" || t.name === "input" || t.name === "textarea") && enclosingTile) {
790
+ const r = ctx.gen.reducers.find((rr) => rr.on.kind === "UiEvent" && rr.on.ev === "change" && rr.on.selector.tile === enclosingTile);
791
+ if (r && !entries.some((e) => e.startsWith("onChange"))) entries.push(`onChange: (el) => globalThis.__kumikiApp._dispatch(${JSON.stringify(r.name)}, el)`);
792
+ }
793
+ if ((t.name === "input" || t.name === "textarea") && enclosingTile) {
794
+ const r = ctx.gen.reducers.find((rr) => rr.on.kind === "UiEvent" && rr.on.ev === "input" && rr.on.selector.tile === enclosingTile);
795
+ if (r && !entries.some((e) => e.startsWith("onInput"))) entries.push(`onInput: (el) => globalThis.__kumikiApp._dispatch(${JSON.stringify(r.name)}, el)`);
796
+ }
797
+ const elProps = [];
798
+ for (const p of t.props) {
799
+ if (p.name === "onClick" || p.name === "onSubmit" || p.name === "onChange" || p.name === "onInput") continue;
800
+ elProps.push(`${jsName(p.name)}: ${jsOfExpr(p.value, ctx)}`);
801
+ }
802
+ if (elProps.length > 0) entries.push(`el: { ${elProps.join(", ")} }`);
803
+ return `{ ${entries.join(", ")} }`;
804
+ }
805
+ function addBind(ctx, name) {
806
+ const out = makeEvalCtx(ctx.gen, ctx.localBinds);
807
+ out.localBinds.add(name);
808
+ return out;
809
+ }
810
+ function jsName(name) {
811
+ return name.replace(/^\$/, "_d_").replace(/-/g, "_").replace(/\./g, "_");
812
+ }
813
+ function refinementJs(t, gen) {
814
+ let target = t;
815
+ if (t.kind === "TypeRef") {
816
+ const def = gen.types.get(t.name);
817
+ if (!def) return void 0;
818
+ target = def.body;
819
+ }
820
+ if (target.kind === "TypeNominal" || target.kind === "TypeRefinement") {
821
+ const r = target.refinement;
822
+ if (r) return refinementToJs(r);
823
+ }
824
+ }
825
+ function refinementToJs(r) {
826
+ switch (r.pred) {
827
+ case "between": return `(v) => typeof v === "number" && v >= ${r.args[0]} && v <= ${r.args[1]}`;
828
+ case "nonempty": return `(v) => typeof v === "string" && v.length > 0`;
829
+ case "len-lt": return `(v) => typeof v === "string" && v.length < ${r.args[0]}`;
830
+ case "len-gt": return `(v) => typeof v === "string" && v.length > ${r.args[0]}`;
831
+ case "len-eq": return `(v) => typeof v === "string" && v.length === ${r.args[0]}`;
832
+ default: return `(_v) => true`;
833
+ }
834
+ }
835
+ function emitFromInitExpr(e) {
836
+ if (e.kind === "Call") return `{ effect: ${JSON.stringify(e.callee)}, args: [${e.args.map((a) => jsOfExpr(a, {
837
+ gen: {},
838
+ localBinds: /* @__PURE__ */ new Set()
839
+ })).join(", ")}] }`;
840
+ return "null";
841
+ }
842
+ const RUNTIME_HELPERS = `
843
+ function _setPath(obj, path, value) {
844
+ if (path.length === 0) return value;
845
+ const [head, ...rest] = path;
846
+ const cur = obj ?? {};
847
+ return { ...cur, [head]: _setPath(cur[head], rest, value) };
848
+ }
849
+ function _children(...xs) {
850
+ const out = [];
851
+ for (const x of xs) {
852
+ if (x === null || x === undefined) continue;
853
+ if (Array.isArray(x)) {
854
+ for (const y of x) if (y !== null && y !== undefined) out.push(y);
855
+ } else {
856
+ out.push(x);
857
+ }
858
+ }
859
+ return out;
860
+ }
861
+ function _attachProps(node, props) {
862
+ if (!node || !props) return node;
863
+ return { ...node, props: { ...(node.props || {}), ...props } };
864
+ }
865
+ `;
866
+ //#endregion
867
+ //#region src/lexer.ts
868
+ const KEYWORDS = new Set([
869
+ "type",
870
+ "slot",
871
+ "effect",
872
+ "reducer",
873
+ "tile",
874
+ "fn",
875
+ "app",
876
+ "nominal",
877
+ "where",
878
+ "when",
879
+ "for",
880
+ "in",
881
+ "let",
882
+ "if",
883
+ "then",
884
+ "else",
885
+ "match",
886
+ "with",
887
+ "on",
888
+ "do",
889
+ "emit",
890
+ "cap",
891
+ "out",
892
+ "policy",
893
+ "retry",
894
+ "true",
895
+ "false",
896
+ "fresh",
897
+ "self",
898
+ "now",
899
+ "null"
900
+ ]);
901
+ const MULTI_CHAR_OPS = [
902
+ "->>",
903
+ ":=",
904
+ "==",
905
+ "!=",
906
+ "<=",
907
+ ">=",
908
+ "->",
909
+ "||",
910
+ "&&"
911
+ ];
912
+ const SINGLE_CHAR_OPS = new Set([
913
+ "+",
914
+ "-",
915
+ "*",
916
+ "/",
917
+ "%",
918
+ "<",
919
+ ">",
920
+ "=",
921
+ "|",
922
+ "&",
923
+ "!",
924
+ "(",
925
+ ")",
926
+ "{",
927
+ "}",
928
+ "[",
929
+ "]",
930
+ ",",
931
+ ";",
932
+ ":",
933
+ ".",
934
+ "#"
935
+ ]);
936
+ const MAX_IDENT_LEN = 32;
937
+ var LexError = class extends Error {
938
+ pos;
939
+ constructor(message, pos) {
940
+ super(`Lex error at ${pos.line}:${pos.col}: ${message}`);
941
+ this.pos = pos;
942
+ }
943
+ };
944
+ function lex(source) {
945
+ const tokens = [];
946
+ let i = 0;
947
+ let line = 1;
948
+ let col = 1;
949
+ const advance = (n = 1) => {
950
+ for (let k = 0; k < n; k++) {
951
+ if (source[i] === "\n") {
952
+ line++;
953
+ col = 1;
954
+ } else col++;
955
+ i++;
956
+ }
957
+ };
958
+ const pos = () => ({
959
+ line,
960
+ col
961
+ });
962
+ while (i < source.length) {
963
+ const c = source[i];
964
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
965
+ advance();
966
+ continue;
967
+ }
968
+ if (c === "#") {
969
+ const prev = i > 0 ? source[i - 1] : void 0;
970
+ if (prev !== void 0 && (isIdentCont(prev) || prev === ")" || prev === "]" || prev === "}")) {
971
+ tokens.push({
972
+ kind: "op",
973
+ value: "#",
974
+ pos: pos()
975
+ });
976
+ advance();
977
+ continue;
978
+ }
979
+ while (i < source.length && source[i] !== "\n") advance();
980
+ continue;
981
+ }
982
+ const startPos = pos();
983
+ if (c === "\"") {
984
+ let value = "";
985
+ advance();
986
+ while (i < source.length && source[i] !== "\"") {
987
+ const ch = source[i];
988
+ if (ch === "\\") {
989
+ advance();
990
+ const esc = source[i];
991
+ if (esc === void 0) throw new LexError("Unterminated string", startPos);
992
+ if (esc === "n") value += "\n";
993
+ else if (esc === "t") value += " ";
994
+ else if (esc === "r") value += "\r";
995
+ else if (esc === "\"") value += "\"";
996
+ else if (esc === "\\") value += "\\";
997
+ else throw new LexError(`Unknown escape \\${esc}`, pos());
998
+ advance();
999
+ } else {
1000
+ value += ch;
1001
+ advance();
1002
+ }
1003
+ }
1004
+ if (source[i] !== "\"") throw new LexError("Unterminated string", startPos);
1005
+ advance();
1006
+ tokens.push({
1007
+ kind: "str",
1008
+ value,
1009
+ pos: startPos
1010
+ });
1011
+ continue;
1012
+ }
1013
+ if (isDigit(c)) {
1014
+ let raw = "";
1015
+ while (i < source.length && isDigit(source[i])) {
1016
+ raw += source[i];
1017
+ advance();
1018
+ }
1019
+ if (source[i] === "." && isDigit(source[i + 1])) {
1020
+ raw += ".";
1021
+ advance();
1022
+ while (i < source.length && isDigit(source[i])) {
1023
+ raw += source[i];
1024
+ advance();
1025
+ }
1026
+ }
1027
+ tokens.push({
1028
+ kind: "num",
1029
+ value: Number(raw),
1030
+ pos: startPos
1031
+ });
1032
+ continue;
1033
+ }
1034
+ if (c === "$") {
1035
+ let raw = "$";
1036
+ advance();
1037
+ while (i < source.length && isIdentCont(source[i])) {
1038
+ raw += source[i];
1039
+ advance();
1040
+ }
1041
+ if (raw.length === 1) throw new LexError(`Bare "$" is not a token`, startPos);
1042
+ tokens.push({
1043
+ kind: "ident",
1044
+ value: raw,
1045
+ pos: startPos
1046
+ });
1047
+ continue;
1048
+ }
1049
+ if (isIdentStart(c)) {
1050
+ let raw = "";
1051
+ while (i < source.length && isIdentCont(source[i])) {
1052
+ raw += source[i];
1053
+ advance();
1054
+ }
1055
+ if (raw.length > MAX_IDENT_LEN) throw new LexError(`Identifier too long (max ${MAX_IDENT_LEN}): "${raw}"`, startPos);
1056
+ if (KEYWORDS.has(raw)) tokens.push({
1057
+ kind: "kw",
1058
+ value: raw,
1059
+ pos: startPos
1060
+ });
1061
+ else tokens.push({
1062
+ kind: "ident",
1063
+ value: raw,
1064
+ pos: startPos
1065
+ });
1066
+ continue;
1067
+ }
1068
+ let matched;
1069
+ for (const op of MULTI_CHAR_OPS) if (source.startsWith(op, i)) {
1070
+ matched = op;
1071
+ break;
1072
+ }
1073
+ if (matched !== void 0) {
1074
+ tokens.push({
1075
+ kind: "op",
1076
+ value: matched,
1077
+ pos: startPos
1078
+ });
1079
+ advance(matched.length);
1080
+ continue;
1081
+ }
1082
+ if (SINGLE_CHAR_OPS.has(c)) {
1083
+ tokens.push({
1084
+ kind: "op",
1085
+ value: c,
1086
+ pos: startPos
1087
+ });
1088
+ advance();
1089
+ continue;
1090
+ }
1091
+ throw new LexError(`Unexpected character "${c}"`, startPos);
1092
+ }
1093
+ tokens.push({
1094
+ kind: "eof",
1095
+ pos: pos()
1096
+ });
1097
+ return tokens;
1098
+ }
1099
+ function isDigit(c) {
1100
+ return c >= "0" && c <= "9";
1101
+ }
1102
+ function isIdentStart(c) {
1103
+ return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
1104
+ }
1105
+ function isIdentCont(c) {
1106
+ return isIdentStart(c) || isDigit(c) || c === "_" || c === "-";
1107
+ }
1108
+ //#endregion
1109
+ //#region src/parser.ts
1110
+ var ParseError = class extends Error {
1111
+ pos;
1112
+ constructor(message, pos) {
1113
+ super(`Parse error at ${pos.line}:${pos.col}: ${message}`);
1114
+ this.pos = pos;
1115
+ }
1116
+ };
1117
+ const PRIM_TYPES = new Set([
1118
+ "Int",
1119
+ "Text",
1120
+ "Bool",
1121
+ "Unit",
1122
+ "Float",
1123
+ "Time",
1124
+ "Bytes"
1125
+ ]);
1126
+ const VALUE_ARG_BUILTINS = new Set([
1127
+ "text",
1128
+ "heading",
1129
+ "markdown",
1130
+ "label",
1131
+ "link",
1132
+ "image",
1133
+ "icon"
1134
+ ]);
1135
+ const VALUE_NAMED_ARGS = new Set([
1136
+ "text",
1137
+ "value",
1138
+ "placeholder",
1139
+ "to",
1140
+ "src",
1141
+ "id",
1142
+ "key",
1143
+ "name",
1144
+ "label",
1145
+ "title",
1146
+ "type",
1147
+ "color",
1148
+ "bg",
1149
+ "size",
1150
+ "weight",
1151
+ "variant",
1152
+ "pad",
1153
+ "gap",
1154
+ "align",
1155
+ "justify",
1156
+ "wrap",
1157
+ "w",
1158
+ "h",
1159
+ "min-w",
1160
+ "min-h",
1161
+ "max-w",
1162
+ "max-h",
1163
+ "radius",
1164
+ "shadow",
1165
+ "rows",
1166
+ "cols",
1167
+ "aspect"
1168
+ ]);
1169
+ const BUILTIN_TILES$1 = new Set([
1170
+ "page",
1171
+ "region",
1172
+ "row",
1173
+ "column",
1174
+ "stack",
1175
+ "grid",
1176
+ "box",
1177
+ "card",
1178
+ "panel",
1179
+ "divider",
1180
+ "scroll",
1181
+ "text",
1182
+ "heading",
1183
+ "link",
1184
+ "code",
1185
+ "markdown",
1186
+ "image",
1187
+ "icon",
1188
+ "video",
1189
+ "button",
1190
+ "input",
1191
+ "textarea",
1192
+ "check",
1193
+ "radio",
1194
+ "select",
1195
+ "slider",
1196
+ "switch",
1197
+ "form",
1198
+ "label",
1199
+ "fieldset",
1200
+ "error",
1201
+ "list",
1202
+ "list-item",
1203
+ "table",
1204
+ "table-head",
1205
+ "table-body",
1206
+ "table-row",
1207
+ "table-cell",
1208
+ "modal",
1209
+ "drawer",
1210
+ "tooltip",
1211
+ "popover",
1212
+ "toast",
1213
+ "spinner",
1214
+ "progress",
1215
+ "skeleton",
1216
+ "route-outlet"
1217
+ ]);
1218
+ const REFINE_PREDS = new Set([
1219
+ "between",
1220
+ "nonempty",
1221
+ "len-eq",
1222
+ "len-lt",
1223
+ "len-gt",
1224
+ "positive",
1225
+ "negative",
1226
+ "email",
1227
+ "url",
1228
+ "uuid",
1229
+ "regex",
1230
+ "one-of"
1231
+ ]);
1232
+ var Parser = class {
1233
+ tokens;
1234
+ i = 0;
1235
+ constructor(tokens) {
1236
+ this.tokens = tokens;
1237
+ }
1238
+ peek(offset = 0) {
1239
+ return this.tokens[this.i + offset] ?? this.tokens[this.tokens.length - 1];
1240
+ }
1241
+ next() {
1242
+ return this.tokens[this.i++] ?? this.tokens[this.tokens.length - 1];
1243
+ }
1244
+ eat(kind, value) {
1245
+ const t = this.peek();
1246
+ if (t.kind !== kind || value !== void 0 && "value" in t && t.value !== value) {
1247
+ const got = t.kind === "eof" ? "eof" : `${t.kind}(${t.value})`;
1248
+ throw new ParseError(`Expected ${value !== void 0 ? `${kind}(${value})` : kind}, got ${got}`, t.pos);
1249
+ }
1250
+ this.next();
1251
+ return t;
1252
+ }
1253
+ matchTAt(offset, kind, value) {
1254
+ const t = this.peek(offset);
1255
+ if (t.kind !== kind) return false;
1256
+ if (value !== void 0 && "value" in t && t.value !== value) return false;
1257
+ return true;
1258
+ }
1259
+ matchT(kind, value) {
1260
+ return this.matchTAt(0, kind, value);
1261
+ }
1262
+ matchOp(value) {
1263
+ return this.matchT("op", value);
1264
+ }
1265
+ matchKw(value) {
1266
+ return this.matchT("kw", value);
1267
+ }
1268
+ parseProgram() {
1269
+ const defs = [];
1270
+ while (!this.matchT("eof")) {
1271
+ if (this.matchT("ident", "theme")) {
1272
+ defs.push(this.parseThemeDef());
1273
+ continue;
1274
+ }
1275
+ defs.push(this.parseDef());
1276
+ }
1277
+ return {
1278
+ kind: "Program",
1279
+ defs
1280
+ };
1281
+ }
1282
+ parseThemeDef() {
1283
+ const start = this.eat("ident", "theme");
1284
+ const name = this.eat("ident").value;
1285
+ this.eat("op", "=");
1286
+ return {
1287
+ kind: "ThemeDef",
1288
+ name,
1289
+ body: this.parseThemeRecord(),
1290
+ pos: start.pos
1291
+ };
1292
+ }
1293
+ parseThemeRecord() {
1294
+ this.eat("op", "{");
1295
+ const out = {};
1296
+ if (!this.matchOp("}")) {
1297
+ this.parseThemeEntry(out);
1298
+ while (this.matchOp(",")) {
1299
+ this.next();
1300
+ if (this.matchOp("}")) break;
1301
+ this.parseThemeEntry(out);
1302
+ }
1303
+ }
1304
+ this.eat("op", "}");
1305
+ return out;
1306
+ }
1307
+ parseThemeEntry(out) {
1308
+ const keyTok = this.peek();
1309
+ if (keyTok.kind !== "ident" && keyTok.kind !== "kw" && keyTok.kind !== "str") throw new ParseError(`Expected theme key`, keyTok.pos);
1310
+ this.next();
1311
+ const key = keyTok.value;
1312
+ this.eat("op", ":");
1313
+ const v = this.peek();
1314
+ if (v.kind === "op" && v.value === "{") out[key] = this.parseThemeRecord();
1315
+ else if (v.kind === "str") {
1316
+ this.next();
1317
+ out[key] = v.value;
1318
+ } else if (v.kind === "num") {
1319
+ this.next();
1320
+ out[key] = v.value;
1321
+ } else throw new ParseError(`Theme values must be string, number, or nested record`, v.pos);
1322
+ }
1323
+ parseDef() {
1324
+ const t = this.peek();
1325
+ if (t.kind !== "kw") throw new ParseError("Expected a definition keyword", t.pos);
1326
+ switch (t.value) {
1327
+ case "type": return this.parseType();
1328
+ case "slot": return this.parseSlot();
1329
+ case "reducer": return this.parseReducer();
1330
+ case "tile": return this.parseTile();
1331
+ case "fn": return this.parseFn();
1332
+ case "effect": return this.parseEffect();
1333
+ case "app": return this.parseApp();
1334
+ default: throw new ParseError(`Unsupported definition keyword "${t.value}"`, t.pos);
1335
+ }
1336
+ }
1337
+ parseType() {
1338
+ const start = this.eat("kw", "type");
1339
+ const name = this.eat("ident").value;
1340
+ const params = [];
1341
+ if (this.matchOp("(")) {
1342
+ this.next();
1343
+ if (!this.matchOp(")")) {
1344
+ params.push(this.eat("ident").value);
1345
+ while (this.matchOp(",")) {
1346
+ this.next();
1347
+ params.push(this.eat("ident").value);
1348
+ }
1349
+ }
1350
+ this.eat("op", ")");
1351
+ }
1352
+ this.eat("op", "=");
1353
+ return {
1354
+ kind: "TypeDef",
1355
+ name,
1356
+ params,
1357
+ body: this.parseTypeExpr(),
1358
+ pos: start.pos
1359
+ };
1360
+ }
1361
+ parseTypeExpr() {
1362
+ const first = this.parseTypeUnionAtom();
1363
+ if (this.matchOp("|")) {
1364
+ const variants = [this.typeAsVariant(first)];
1365
+ while (this.matchOp("|")) {
1366
+ this.next();
1367
+ variants.push(this.typeAsVariant(this.parseTypeUnionAtom()));
1368
+ }
1369
+ return {
1370
+ kind: "TypeUnion",
1371
+ variants,
1372
+ pos: first.pos
1373
+ };
1374
+ }
1375
+ if (this.matchKw("where")) {
1376
+ this.next();
1377
+ return {
1378
+ kind: "TypeRefinement",
1379
+ inner: first,
1380
+ refinement: this.parseRefinement(),
1381
+ pos: first.pos
1382
+ };
1383
+ }
1384
+ return first;
1385
+ }
1386
+ typeAsVariant(t) {
1387
+ if (t.kind === "TypeRef") return {
1388
+ name: t.name,
1389
+ payloads: []
1390
+ };
1391
+ if (t.kind === "TypeApp") return {
1392
+ name: t.name,
1393
+ payloads: t.args
1394
+ };
1395
+ throw new ParseError(`Unsupported variant form`, t.pos);
1396
+ }
1397
+ parseTypeUnionAtom() {
1398
+ if (this.matchKw("nominal")) {
1399
+ const start = this.next();
1400
+ const inner = this.parseTypeAtom();
1401
+ let refinement;
1402
+ if (this.matchKw("where")) {
1403
+ this.next();
1404
+ refinement = this.parseRefinement();
1405
+ }
1406
+ const node = {
1407
+ kind: "TypeNominal",
1408
+ inner,
1409
+ pos: start.pos
1410
+ };
1411
+ if (refinement) node.refinement = refinement;
1412
+ return node;
1413
+ }
1414
+ const atom = this.parseTypeAtom();
1415
+ if (this.matchKw("where")) {
1416
+ this.next();
1417
+ return {
1418
+ kind: "TypeRefinement",
1419
+ inner: atom,
1420
+ refinement: this.parseRefinement(),
1421
+ pos: atom.pos
1422
+ };
1423
+ }
1424
+ return atom;
1425
+ }
1426
+ parseTypeAtom() {
1427
+ if (this.matchOp("{")) {
1428
+ const start = this.next();
1429
+ const fields = [];
1430
+ if (!this.matchOp("}")) {
1431
+ fields.push(this.parseTypeField());
1432
+ while (this.matchOp(",")) {
1433
+ this.next();
1434
+ fields.push(this.parseTypeField());
1435
+ }
1436
+ }
1437
+ this.eat("op", "}");
1438
+ return {
1439
+ kind: "TypeRecord",
1440
+ fields,
1441
+ pos: start.pos
1442
+ };
1443
+ }
1444
+ const t = this.eat("ident");
1445
+ const name = t.value;
1446
+ if (this.matchOp("(")) {
1447
+ this.next();
1448
+ const args = [];
1449
+ if (!this.matchOp(")")) {
1450
+ args.push(this.parseTypeExpr());
1451
+ while (this.matchOp(",")) {
1452
+ this.next();
1453
+ args.push(this.parseTypeExpr());
1454
+ }
1455
+ }
1456
+ this.eat("op", ")");
1457
+ return {
1458
+ kind: "TypeApp",
1459
+ name,
1460
+ args,
1461
+ pos: t.pos
1462
+ };
1463
+ }
1464
+ if (PRIM_TYPES.has(name)) return {
1465
+ kind: "TypePrim",
1466
+ name,
1467
+ pos: t.pos
1468
+ };
1469
+ return {
1470
+ kind: "TypeRef",
1471
+ name,
1472
+ pos: t.pos
1473
+ };
1474
+ }
1475
+ parseTypeField() {
1476
+ const name = this.eat("ident").value;
1477
+ this.eat("op", ":");
1478
+ return {
1479
+ name,
1480
+ type: this.parseTypeExpr()
1481
+ };
1482
+ }
1483
+ parseRefinement() {
1484
+ const t = this.eat("ident");
1485
+ const name = t.value;
1486
+ if (!REFINE_PREDS.has(name)) throw new ParseError(`Unknown refinement predicate "${name}"`, t.pos);
1487
+ const args = [];
1488
+ if (this.matchOp("(")) {
1489
+ this.next();
1490
+ if (!this.matchOp(")")) {
1491
+ args.push(this.parseRefinementArg());
1492
+ while (this.matchOp(",")) {
1493
+ this.next();
1494
+ args.push(this.parseRefinementArg());
1495
+ }
1496
+ }
1497
+ this.eat("op", ")");
1498
+ }
1499
+ return {
1500
+ kind: "Refinement",
1501
+ pred: name,
1502
+ args,
1503
+ pos: t.pos
1504
+ };
1505
+ }
1506
+ parseRefinementArg() {
1507
+ const t = this.peek();
1508
+ if (t.kind === "num") {
1509
+ this.next();
1510
+ return t.value;
1511
+ }
1512
+ if (t.kind === "str") {
1513
+ this.next();
1514
+ return t.value;
1515
+ }
1516
+ throw new ParseError("Refinement argument must be a literal", t.pos);
1517
+ }
1518
+ parseSlot() {
1519
+ const start = this.eat("kw", "slot");
1520
+ const name = this.eat("ident").value;
1521
+ this.eat("op", ":");
1522
+ const type = this.parseTypeExpr();
1523
+ let modifier;
1524
+ if (this.matchT("ident", "transient")) {
1525
+ this.next();
1526
+ modifier = "transient";
1527
+ } else if (this.matchT("ident", "volatile")) {
1528
+ this.next();
1529
+ modifier = "volatile";
1530
+ }
1531
+ this.eat("op", "=");
1532
+ const def = {
1533
+ kind: "SlotDef",
1534
+ name,
1535
+ type,
1536
+ init: this.parseExpr(),
1537
+ pos: start.pos
1538
+ };
1539
+ if (modifier) def.modifier = modifier;
1540
+ return def;
1541
+ }
1542
+ parseReducer() {
1543
+ const start = this.eat("kw", "reducer");
1544
+ const name = this.eat("ident").value;
1545
+ this.eat("kw", "on");
1546
+ this.eat("op", "=");
1547
+ const on = this.parseEventPattern();
1548
+ this.eat("kw", "do");
1549
+ this.eat("op", "=");
1550
+ const stmts = [this.parseStatement()];
1551
+ while (this.matchOp(";") || this.statementLookahead()) {
1552
+ if (this.matchOp(";")) this.next();
1553
+ stmts.push(this.parseStatement());
1554
+ }
1555
+ return {
1556
+ kind: "ReducerDef",
1557
+ name,
1558
+ on,
1559
+ do: stmts,
1560
+ pos: start.pos
1561
+ };
1562
+ }
1563
+ statementLookahead() {
1564
+ if (this.matchKw("let") || this.matchKw("emit") || this.matchKw("for") || this.matchKw("if") || this.matchKw("match")) return true;
1565
+ if (this.peek().kind === "ident") {
1566
+ const j = this.i + 1;
1567
+ while (j < this.tokens.length) {
1568
+ const t = this.tokens[j];
1569
+ if (t.kind === "op" && (t.value === ":=" || t.value === "[" || t.value === ".")) return true;
1570
+ if (t.kind === "op" && (t.value === ":=" || t.value === "(")) return true;
1571
+ if (t.kind === "kw") return false;
1572
+ if (t.kind === "ident") return false;
1573
+ if (t.kind === "eof") return false;
1574
+ return false;
1575
+ }
1576
+ }
1577
+ return false;
1578
+ }
1579
+ parseEventPattern() {
1580
+ const t = this.peek();
1581
+ if (t.kind === "ident" && t.value === "timer") {
1582
+ this.next();
1583
+ this.eat("op", "(");
1584
+ const intervalMs = this.parseDuration();
1585
+ this.eat("op", ")");
1586
+ return {
1587
+ kind: "TimerEvent",
1588
+ intervalMs,
1589
+ pos: t.pos
1590
+ };
1591
+ }
1592
+ if (t.kind === "ident" || t.kind === "kw" && (t.value === "app" || t.value === "tile")) {
1593
+ const name = t.value;
1594
+ this.next();
1595
+ this.eat("op", ".");
1596
+ const sub = this.eat("ident").value;
1597
+ if (name === "ui") {
1598
+ if (sub !== "click" && sub !== "submit" && sub !== "change" && sub !== "input" && sub !== "focus" && sub !== "blur") throw new ParseError(`Unknown ui event "${sub}"`, t.pos);
1599
+ this.eat("op", "(");
1600
+ const tile = this.eat("ident").value;
1601
+ let id;
1602
+ if (this.matchOp("#")) {
1603
+ this.next();
1604
+ id = this.eat("ident").value;
1605
+ }
1606
+ this.eat("op", ")");
1607
+ const sel = { tile };
1608
+ if (id) sel.id = id;
1609
+ return {
1610
+ kind: "UiEvent",
1611
+ ev: sub,
1612
+ selector: sel,
1613
+ pos: t.pos
1614
+ };
1615
+ }
1616
+ if (name === "app") return {
1617
+ kind: "LifecycleEvent",
1618
+ name: `app.${sub}`,
1619
+ pos: t.pos
1620
+ };
1621
+ if (name === "tile") {
1622
+ if (this.matchOp("(")) {
1623
+ this.next();
1624
+ this.eat("ident");
1625
+ this.eat("op", ")");
1626
+ }
1627
+ return {
1628
+ kind: "LifecycleEvent",
1629
+ name: `tile.${sub}`,
1630
+ pos: t.pos
1631
+ };
1632
+ }
1633
+ if (name === "route") {
1634
+ if (this.matchOp("(")) {
1635
+ this.next();
1636
+ this.eat("str");
1637
+ this.eat("op", ")");
1638
+ }
1639
+ return {
1640
+ kind: "LifecycleEvent",
1641
+ name: `route.${sub}`,
1642
+ pos: t.pos
1643
+ };
1644
+ }
1645
+ if (sub === "ok" || sub === "err") {
1646
+ this.eat("op", "(");
1647
+ const binds = [];
1648
+ if (!this.matchOp(")")) {
1649
+ binds.push(this.readBind());
1650
+ while (this.matchOp(",")) {
1651
+ this.next();
1652
+ binds.push(this.readBind());
1653
+ }
1654
+ }
1655
+ this.eat("op", ")");
1656
+ return {
1657
+ kind: "EffectEvent",
1658
+ effect: name,
1659
+ outcome: sub,
1660
+ binds,
1661
+ pos: t.pos
1662
+ };
1663
+ }
1664
+ throw new ParseError(`Unsupported event pattern "${name}.${sub}"`, t.pos);
1665
+ }
1666
+ throw new ParseError("Expected event pattern", t.pos);
1667
+ }
1668
+ readBind() {
1669
+ if (this.matchOp("_")) {
1670
+ this.next();
1671
+ return "_";
1672
+ }
1673
+ const t = this.peek();
1674
+ if (t.kind === "op" && t.value === "$") {}
1675
+ if (this.matchT("ident", "_")) {
1676
+ this.next();
1677
+ return "_";
1678
+ }
1679
+ return this.eat("ident").value;
1680
+ }
1681
+ parseStatement() {
1682
+ if (this.matchKw("for")) {
1683
+ const start = this.next();
1684
+ const bindTok = this.eat("ident");
1685
+ this.eat("kw", "in");
1686
+ const iter = this.parseExpr();
1687
+ const body = this.parseStatementBody();
1688
+ return {
1689
+ kind: "ForStmt",
1690
+ bind: bindTok.value,
1691
+ iter,
1692
+ body,
1693
+ pos: start.pos
1694
+ };
1695
+ }
1696
+ if (this.matchKw("if")) {
1697
+ const start = this.next();
1698
+ const cond = this.parseExpr();
1699
+ this.eat("kw", "then");
1700
+ const thenBody = this.parseStatementBody();
1701
+ let elseBody = [];
1702
+ if (this.matchKw("else")) {
1703
+ this.next();
1704
+ elseBody = this.parseStatementBody();
1705
+ }
1706
+ return {
1707
+ kind: "IfStmt",
1708
+ cond,
1709
+ consequent: thenBody,
1710
+ alternate: elseBody,
1711
+ pos: start.pos
1712
+ };
1713
+ }
1714
+ if (this.matchKw("match")) {
1715
+ const start = this.next();
1716
+ const scrutinee = this.parseExpr();
1717
+ this.eat("kw", "with");
1718
+ const arms = [];
1719
+ while (this.matchOp("|")) {
1720
+ this.next();
1721
+ const pattern = this.parsePattern();
1722
+ this.eat("op", "->");
1723
+ const body = this.parseStatementBody();
1724
+ arms.push({
1725
+ pattern,
1726
+ body
1727
+ });
1728
+ }
1729
+ return {
1730
+ kind: "MatchStmt",
1731
+ scrutinee,
1732
+ arms,
1733
+ pos: start.pos
1734
+ };
1735
+ }
1736
+ if (this.matchOp("(") && this.matchTAt(1, "op", ")")) {
1737
+ const tok = this.next();
1738
+ this.eat("op", ")");
1739
+ return {
1740
+ kind: "NoopStmt",
1741
+ pos: tok.pos
1742
+ };
1743
+ }
1744
+ if (this.matchKw("let")) {
1745
+ const start = this.next();
1746
+ const name = this.eat("ident").value;
1747
+ this.eat("op", "=");
1748
+ return {
1749
+ kind: "LetStmt",
1750
+ name,
1751
+ rhs: this.parseExpr(),
1752
+ pos: start.pos
1753
+ };
1754
+ }
1755
+ if (this.matchKw("emit")) {
1756
+ const start = this.next();
1757
+ const effect = this.eat("ident").value;
1758
+ this.eat("op", "(");
1759
+ const args = [];
1760
+ if (!this.matchOp(")")) {
1761
+ args.push(this.parseExpr());
1762
+ while (this.matchOp(",")) {
1763
+ this.next();
1764
+ args.push(this.parseExpr());
1765
+ }
1766
+ }
1767
+ this.eat("op", ")");
1768
+ return {
1769
+ kind: "Emit",
1770
+ effect,
1771
+ args,
1772
+ pos: start.pos
1773
+ };
1774
+ }
1775
+ const lvalue = this.parseLvalue();
1776
+ this.eat("op", ":=");
1777
+ return {
1778
+ kind: "SlotAssign",
1779
+ lvalue,
1780
+ rhs: this.parseExpr(),
1781
+ pos: lvalue.pos
1782
+ };
1783
+ }
1784
+ /**
1785
+ * Body of a control-flow statement (for / if-stmt / match-stmt arm).
1786
+ * Three accepted forms:
1787
+ * - `{ stmt (; stmt)* }` explicit brace block
1788
+ * - `stmt; stmt; ...` semicolon-separated (until next branch keyword)
1789
+ * - `stmt\n stmt\n ...` newline-separated (until next branch keyword)
1790
+ * Stops at `else`, `}`, `|` (match arm), or EOF — those belong to the enclosing form.
1791
+ */
1792
+ parseStatementBody() {
1793
+ if (this.matchOp("{")) {
1794
+ this.next();
1795
+ const out = [];
1796
+ if (!this.matchOp("}")) {
1797
+ out.push(this.parseStatement());
1798
+ while (this.matchOp(";") || this.statementLookahead() && !this.matchOp("}")) {
1799
+ if (this.matchOp(";")) this.next();
1800
+ if (this.matchOp("}")) break;
1801
+ out.push(this.parseStatement());
1802
+ }
1803
+ }
1804
+ this.eat("op", "}");
1805
+ return out;
1806
+ }
1807
+ const out = [this.parseStatement()];
1808
+ while (this.matchOp(";") || this.statementLookahead()) {
1809
+ if (this.matchOp(";")) this.next();
1810
+ if (this.matchKw("else") || this.matchOp("}") || this.matchOp("|")) break;
1811
+ out.push(this.parseStatement());
1812
+ }
1813
+ return out;
1814
+ }
1815
+ parseLvalue() {
1816
+ const tok = this.eat("ident");
1817
+ let lv = {
1818
+ kind: "LSlot",
1819
+ name: tok.value,
1820
+ pos: tok.pos
1821
+ };
1822
+ while (true) if (this.matchOp(".")) {
1823
+ this.next();
1824
+ const f = this.eat("ident");
1825
+ lv = {
1826
+ kind: "LField",
1827
+ base: lv,
1828
+ field: f.value,
1829
+ pos: f.pos
1830
+ };
1831
+ } else if (this.matchOp("[")) {
1832
+ const t = this.next();
1833
+ const idx = this.parseExpr();
1834
+ this.eat("op", "]");
1835
+ lv = {
1836
+ kind: "LIndex",
1837
+ base: lv,
1838
+ index: idx,
1839
+ pos: t.pos
1840
+ };
1841
+ } else break;
1842
+ return lv;
1843
+ }
1844
+ parseExpr() {
1845
+ return this.parseLogicOr();
1846
+ }
1847
+ parseLogicOr() {
1848
+ let lhs = this.parseLogicAnd();
1849
+ while (this.matchOp("||") || this.matchOp("|") && !this.looksLikeMatchArm()) {
1850
+ const op = "|";
1851
+ this.next();
1852
+ const rhs = this.parseLogicAnd();
1853
+ lhs = {
1854
+ kind: "BinOp",
1855
+ op,
1856
+ lhs,
1857
+ rhs,
1858
+ pos: lhs.pos
1859
+ };
1860
+ }
1861
+ return lhs;
1862
+ }
1863
+ /** Heuristic: after `|`, does it look like the start of a match arm? */
1864
+ looksLikeMatchArm() {
1865
+ const next = this.peek(1);
1866
+ if (next.kind === "ident" && next.value === "_") return true;
1867
+ if (next.kind === "ident" && next.value[0] && next.value[0] >= "A" && next.value[0] <= "Z") {
1868
+ const after = this.peek(2);
1869
+ if (after.kind === "op" && (after.value === "->" || after.value === "(")) return true;
1870
+ }
1871
+ return false;
1872
+ }
1873
+ parseLogicAnd() {
1874
+ let lhs = this.parseCmp();
1875
+ while (this.matchOp("&&") || this.matchOp("&")) {
1876
+ this.next();
1877
+ const rhs = this.parseCmp();
1878
+ lhs = {
1879
+ kind: "BinOp",
1880
+ op: "&",
1881
+ lhs,
1882
+ rhs,
1883
+ pos: lhs.pos
1884
+ };
1885
+ }
1886
+ return lhs;
1887
+ }
1888
+ parseCmp() {
1889
+ let lhs = this.parseAdd();
1890
+ while (this.matchAnyOp([
1891
+ "==",
1892
+ "!=",
1893
+ "<",
1894
+ ">",
1895
+ "<=",
1896
+ ">="
1897
+ ])) {
1898
+ const op = this.eat("op").value;
1899
+ const rhs = this.parseAdd();
1900
+ lhs = {
1901
+ kind: "BinOp",
1902
+ op,
1903
+ lhs,
1904
+ rhs,
1905
+ pos: lhs.pos
1906
+ };
1907
+ }
1908
+ return lhs;
1909
+ }
1910
+ parseAdd() {
1911
+ let lhs = this.parseMul();
1912
+ while (this.matchAnyOp(["+", "-"])) {
1913
+ const op = this.eat("op").value;
1914
+ const rhs = this.parseMul();
1915
+ lhs = {
1916
+ kind: "BinOp",
1917
+ op,
1918
+ lhs,
1919
+ rhs,
1920
+ pos: lhs.pos
1921
+ };
1922
+ }
1923
+ return lhs;
1924
+ }
1925
+ parseMul() {
1926
+ let lhs = this.parseUnary();
1927
+ while (this.matchAnyOp([
1928
+ "*",
1929
+ "/",
1930
+ "%"
1931
+ ])) {
1932
+ const op = this.eat("op").value;
1933
+ const rhs = this.parseUnary();
1934
+ lhs = {
1935
+ kind: "BinOp",
1936
+ op,
1937
+ lhs,
1938
+ rhs,
1939
+ pos: lhs.pos
1940
+ };
1941
+ }
1942
+ return lhs;
1943
+ }
1944
+ parseUnary() {
1945
+ if (this.matchOp("-")) {
1946
+ const tok = this.next();
1947
+ return {
1948
+ kind: "UnaryOp",
1949
+ op: "-",
1950
+ rhs: this.parseUnary(),
1951
+ pos: tok.pos
1952
+ };
1953
+ }
1954
+ if (this.matchOp("!")) {
1955
+ const tok = this.next();
1956
+ return {
1957
+ kind: "UnaryOp",
1958
+ op: "!",
1959
+ rhs: this.parseUnary(),
1960
+ pos: tok.pos
1961
+ };
1962
+ }
1963
+ if (this.matchT("ident", "not")) {
1964
+ const tok = this.next();
1965
+ return {
1966
+ kind: "UnaryOp",
1967
+ op: "!",
1968
+ rhs: this.parseUnary(),
1969
+ pos: tok.pos
1970
+ };
1971
+ }
1972
+ return this.parsePostfix();
1973
+ }
1974
+ parsePostfix() {
1975
+ let e = this.parsePrimary();
1976
+ while (true) if (this.matchOp(".")) {
1977
+ this.next();
1978
+ const fldTok = this.peek();
1979
+ if (fldTok.kind !== "ident" && fldTok.kind !== "kw") throw new ParseError(`Expected field or method name`, fldTok.pos);
1980
+ this.next();
1981
+ const fld = fldTok.value;
1982
+ if (this.matchOp("(")) {
1983
+ this.next();
1984
+ const args = [];
1985
+ if (fld === "copy" && this.matchT("ident") && this.matchTAt(1, "op", "=") && !this.matchTAt(2, "op", "=")) {
1986
+ const fields = [];
1987
+ const firstPos = this.peek().pos;
1988
+ const readKwarg = () => {
1989
+ const nameTok = this.eat("ident");
1990
+ this.eat("op", "=");
1991
+ const value = this.parseExpr();
1992
+ fields.push({
1993
+ kind: "RecordField",
1994
+ name: nameTok.value,
1995
+ value,
1996
+ pos: nameTok.pos
1997
+ });
1998
+ };
1999
+ readKwarg();
2000
+ while (this.matchOp(",")) {
2001
+ this.next();
2002
+ readKwarg();
2003
+ }
2004
+ args.push({
2005
+ kind: "RecordLit",
2006
+ fields,
2007
+ pos: firstPos
2008
+ });
2009
+ } else if (!this.matchOp(")")) {
2010
+ args.push(this.parseExpr());
2011
+ while (this.matchOp(",")) {
2012
+ this.next();
2013
+ args.push(this.parseExpr());
2014
+ }
2015
+ }
2016
+ this.eat("op", ")");
2017
+ e = {
2018
+ kind: "MethodCall",
2019
+ receiver: e,
2020
+ method: fld,
2021
+ args,
2022
+ pos: e.pos
2023
+ };
2024
+ } else e = {
2025
+ kind: "FieldAccess",
2026
+ base: e,
2027
+ field: fld,
2028
+ pos: e.pos
2029
+ };
2030
+ } else if (this.matchOp("[")) {
2031
+ this.next();
2032
+ const idx = this.parseExpr();
2033
+ this.eat("op", "]");
2034
+ e = {
2035
+ kind: "Index",
2036
+ base: e,
2037
+ index: idx,
2038
+ pos: e.pos
2039
+ };
2040
+ } else break;
2041
+ return e;
2042
+ }
2043
+ parsePrimary() {
2044
+ const t = this.peek();
2045
+ if (t.kind === "num") {
2046
+ this.next();
2047
+ return {
2048
+ kind: "Num",
2049
+ value: t.value,
2050
+ pos: t.pos
2051
+ };
2052
+ }
2053
+ if (t.kind === "str") {
2054
+ this.next();
2055
+ return {
2056
+ kind: "Str",
2057
+ value: t.value,
2058
+ pos: t.pos
2059
+ };
2060
+ }
2061
+ if (t.kind === "kw" && (t.value === "true" || t.value === "false")) {
2062
+ this.next();
2063
+ return {
2064
+ kind: "Bool",
2065
+ value: t.value === "true",
2066
+ pos: t.pos
2067
+ };
2068
+ }
2069
+ if (t.kind === "kw" && t.value === "now") {
2070
+ this.next();
2071
+ return {
2072
+ kind: "Call",
2073
+ callee: "now",
2074
+ args: [],
2075
+ pos: t.pos
2076
+ };
2077
+ }
2078
+ if (t.kind === "kw" && t.value === "if") return this.parseIfExpr();
2079
+ if (t.kind === "kw" && t.value === "let") return this.parseLetIn();
2080
+ if (t.kind === "kw" && t.value === "match") return this.parseMatchExpr();
2081
+ if (t.kind === "op" && t.value === "(") {
2082
+ this.next();
2083
+ const inner = this.parseExpr();
2084
+ this.eat("op", ")");
2085
+ return inner;
2086
+ }
2087
+ if (t.kind === "op" && t.value === "{") return this.parseRecordOrMapLit();
2088
+ if (t.kind === "op" && t.value === "[") return this.parseListLit();
2089
+ if (t.kind === "ident") {
2090
+ this.next();
2091
+ const name = t.value;
2092
+ if (!!name[0] && name[0] >= "A" && name[0] <= "Z" && this.matchOp(".") && (this.matchTAt(1, "ident") || this.matchTAt(1, "kw")) && this.matchTAt(2, "op", "(")) {
2093
+ this.next();
2094
+ const subTok = this.next();
2095
+ const sub = "value" in subTok ? String(subTok.value) : "";
2096
+ this.eat("op", "(");
2097
+ const args = [];
2098
+ if (!this.matchOp(")")) {
2099
+ args.push(this.parseExpr());
2100
+ while (this.matchOp(",")) {
2101
+ this.next();
2102
+ args.push(this.parseExpr());
2103
+ }
2104
+ }
2105
+ this.eat("op", ")");
2106
+ return {
2107
+ kind: "Call",
2108
+ callee: `${name}.${sub}`,
2109
+ args,
2110
+ pos: t.pos
2111
+ };
2112
+ }
2113
+ if (this.matchOp("(")) {
2114
+ this.next();
2115
+ const args = [];
2116
+ if (!this.matchOp(")")) {
2117
+ args.push(this.parseExpr());
2118
+ while (this.matchOp(",")) {
2119
+ this.next();
2120
+ args.push(this.parseExpr());
2121
+ }
2122
+ }
2123
+ this.eat("op", ")");
2124
+ if (name[0] && name[0] >= "A" && name[0] <= "Z") return {
2125
+ kind: "Variant",
2126
+ name,
2127
+ payload: args,
2128
+ pos: t.pos
2129
+ };
2130
+ return {
2131
+ kind: "Call",
2132
+ callee: name,
2133
+ args,
2134
+ pos: t.pos
2135
+ };
2136
+ }
2137
+ if (name[0] && name[0] >= "A" && name[0] <= "Z") return {
2138
+ kind: "Variant",
2139
+ name,
2140
+ payload: [],
2141
+ pos: t.pos
2142
+ };
2143
+ return {
2144
+ kind: "Ref",
2145
+ name,
2146
+ pos: t.pos
2147
+ };
2148
+ }
2149
+ if (t.kind === "op" && (t.value === "$1" || t.value === "$2")) {}
2150
+ throw new ParseError(`Unexpected token in expression`, t.pos);
2151
+ }
2152
+ parseIfExpr() {
2153
+ const start = this.eat("kw", "if");
2154
+ const cond = this.parseExpr();
2155
+ this.eat("kw", "then");
2156
+ const thenE = this.parseExpr();
2157
+ this.eat("kw", "else");
2158
+ return {
2159
+ kind: "IfExpr",
2160
+ cond,
2161
+ consequent: thenE,
2162
+ alternate: this.parseExpr(),
2163
+ pos: start.pos
2164
+ };
2165
+ }
2166
+ parseLetIn() {
2167
+ const start = this.eat("kw", "let");
2168
+ const name = this.eat("ident").value;
2169
+ this.eat("op", "=");
2170
+ const value = this.parseExpr();
2171
+ this.eat("kw", "in");
2172
+ return {
2173
+ kind: "LetIn",
2174
+ name,
2175
+ value,
2176
+ body: this.parseExpr(),
2177
+ pos: start.pos
2178
+ };
2179
+ }
2180
+ parseMatchExpr() {
2181
+ const start = this.eat("kw", "match");
2182
+ const scrutinee = this.parseExpr();
2183
+ this.eat("kw", "with");
2184
+ const arms = [];
2185
+ while (this.matchOp("|")) {
2186
+ this.next();
2187
+ const pattern = this.parsePattern();
2188
+ this.eat("op", "->");
2189
+ const body = this.parseExpr();
2190
+ arms.push({
2191
+ pattern,
2192
+ body
2193
+ });
2194
+ }
2195
+ if (arms.length === 0) throw new ParseError("match requires at least one arm", start.pos);
2196
+ return {
2197
+ kind: "MatchExpr",
2198
+ scrutinee,
2199
+ arms,
2200
+ pos: start.pos
2201
+ };
2202
+ }
2203
+ parsePattern() {
2204
+ const t = this.peek();
2205
+ if (t.kind === "ident" && t.value === "_") {
2206
+ this.next();
2207
+ return {
2208
+ kind: "PWildcard",
2209
+ pos: t.pos
2210
+ };
2211
+ }
2212
+ if (t.kind === "ident") {
2213
+ const name = t.value;
2214
+ this.next();
2215
+ if (this.matchOp("(")) {
2216
+ this.next();
2217
+ const binds = [];
2218
+ if (!this.matchOp(")")) {
2219
+ binds.push(this.parsePatternBind());
2220
+ while (this.matchOp(",")) {
2221
+ this.next();
2222
+ binds.push(this.parsePatternBind());
2223
+ }
2224
+ }
2225
+ this.eat("op", ")");
2226
+ return {
2227
+ kind: "PVariant",
2228
+ name,
2229
+ binds,
2230
+ pos: t.pos
2231
+ };
2232
+ }
2233
+ if (name[0] && name[0] >= "A" && name[0] <= "Z") return {
2234
+ kind: "PVariant",
2235
+ name,
2236
+ binds: [],
2237
+ pos: t.pos
2238
+ };
2239
+ return {
2240
+ kind: "PBind",
2241
+ name,
2242
+ pos: t.pos
2243
+ };
2244
+ }
2245
+ if (t.kind === "num") {
2246
+ this.next();
2247
+ return {
2248
+ kind: "PLiteral",
2249
+ value: t.value,
2250
+ pos: t.pos
2251
+ };
2252
+ }
2253
+ if (t.kind === "str") {
2254
+ this.next();
2255
+ return {
2256
+ kind: "PLiteral",
2257
+ value: t.value,
2258
+ pos: t.pos
2259
+ };
2260
+ }
2261
+ throw new ParseError("Expected pattern", t.pos);
2262
+ }
2263
+ parsePatternBind() {
2264
+ const t = this.peek();
2265
+ if (t.kind === "ident") {
2266
+ this.next();
2267
+ return t.value;
2268
+ }
2269
+ throw new ParseError("Expected pattern bind", t.pos);
2270
+ }
2271
+ parseRecordOrMapLit() {
2272
+ const start = this.eat("op", "{");
2273
+ if (this.matchOp("}")) {
2274
+ this.next();
2275
+ return {
2276
+ kind: "MapLit",
2277
+ entries: [],
2278
+ pos: start.pos
2279
+ };
2280
+ }
2281
+ let isRecord = false;
2282
+ if (this.peek().kind === "ident") {
2283
+ const peek1 = this.peek(1);
2284
+ if (peek1.kind === "op" && (peek1.value === "=" || peek1.value === ":" || peek1.value === "," || peek1.value === "}")) isRecord = true;
2285
+ }
2286
+ if (isRecord) {
2287
+ const fields = [];
2288
+ while (true) {
2289
+ const nameTok = this.eat("ident");
2290
+ let value;
2291
+ if (this.matchOp("=") || this.matchOp(":")) {
2292
+ this.next();
2293
+ value = this.parseExpr();
2294
+ } else value = {
2295
+ kind: "Ref",
2296
+ name: nameTok.value,
2297
+ pos: nameTok.pos
2298
+ };
2299
+ fields.push({
2300
+ name: nameTok.value,
2301
+ value
2302
+ });
2303
+ if (!this.matchOp(",")) break;
2304
+ this.next();
2305
+ }
2306
+ this.eat("op", "}");
2307
+ return {
2308
+ kind: "RecordLit",
2309
+ fields,
2310
+ pos: start.pos
2311
+ };
2312
+ }
2313
+ const entries = [];
2314
+ entries.push(this.parseMapEntry());
2315
+ while (this.matchOp(",")) {
2316
+ this.next();
2317
+ entries.push(this.parseMapEntry());
2318
+ }
2319
+ this.eat("op", "}");
2320
+ return {
2321
+ kind: "MapLit",
2322
+ entries,
2323
+ pos: start.pos
2324
+ };
2325
+ }
2326
+ parseMapEntry() {
2327
+ const key = this.parseExpr();
2328
+ this.eat("op", ":");
2329
+ return {
2330
+ key,
2331
+ value: this.parseExpr()
2332
+ };
2333
+ }
2334
+ parseListLit() {
2335
+ const start = this.eat("op", "[");
2336
+ const items = [];
2337
+ if (!this.matchOp("]")) {
2338
+ items.push(this.parseExpr());
2339
+ while (this.matchOp(",")) {
2340
+ this.next();
2341
+ items.push(this.parseExpr());
2342
+ }
2343
+ }
2344
+ this.eat("op", "]");
2345
+ return {
2346
+ kind: "ListLit",
2347
+ items,
2348
+ pos: start.pos
2349
+ };
2350
+ }
2351
+ matchAnyOp(ops) {
2352
+ const t = this.peek();
2353
+ return t.kind === "op" && ops.includes(t.value);
2354
+ }
2355
+ parseTile() {
2356
+ const start = this.eat("kw", "tile");
2357
+ const name = this.eat("ident").value;
2358
+ let inType;
2359
+ let errorBoundary;
2360
+ while (!this.matchOp("=")) {
2361
+ if (this.matchKw("in")) {
2362
+ this.next();
2363
+ this.eat("op", "=");
2364
+ inType = this.parseTypeExpr();
2365
+ continue;
2366
+ }
2367
+ if (this.matchT("ident", "error-boundary")) {
2368
+ this.next();
2369
+ this.eat("op", "=");
2370
+ errorBoundary = this.eat("ident").value;
2371
+ continue;
2372
+ }
2373
+ if (this.matchT("ident", "scroll-restoration")) {
2374
+ this.next();
2375
+ this.eat("op", "=");
2376
+ const t = this.peek();
2377
+ if (t.kind === "kw" && (t.value === "true" || t.value === "false")) this.next();
2378
+ else this.parseExpr();
2379
+ continue;
2380
+ }
2381
+ if (this.matchT("ident", "sub-routes")) {
2382
+ this.next();
2383
+ this.eat("op", "=");
2384
+ this.parseRouteMap();
2385
+ continue;
2386
+ }
2387
+ throw new ParseError(`Unexpected token in tile definition`, this.peek().pos);
2388
+ }
2389
+ this.eat("op", "=");
2390
+ const def = {
2391
+ kind: "TileDef",
2392
+ name,
2393
+ body: this.parseTileExpr(),
2394
+ pos: start.pos
2395
+ };
2396
+ if (inType) def.in = inType;
2397
+ if (errorBoundary) def.errorBoundary = errorBoundary;
2398
+ return def;
2399
+ }
2400
+ parseTileExpr() {
2401
+ if (this.matchKw("for")) {
2402
+ const start = this.next();
2403
+ const bindTok = this.eat("ident");
2404
+ this.eat("kw", "in");
2405
+ const iter = this.parseExpr();
2406
+ const body = this.parseTileExpr();
2407
+ return {
2408
+ kind: "TileFor",
2409
+ bind: bindTok.value,
2410
+ iter,
2411
+ body,
2412
+ pos: start.pos
2413
+ };
2414
+ }
2415
+ if (this.matchKw("when")) {
2416
+ const start = this.next();
2417
+ this.eat("op", "(");
2418
+ const cond = this.parseExpr();
2419
+ this.eat("op", ",");
2420
+ const body = this.parseTileExpr();
2421
+ this.eat("op", ")");
2422
+ return {
2423
+ kind: "TileWhen",
2424
+ cond,
2425
+ body,
2426
+ pos: start.pos
2427
+ };
2428
+ }
2429
+ if (this.matchKw("if")) {
2430
+ const start = this.next();
2431
+ const cond = this.parseExpr();
2432
+ this.eat("kw", "then");
2433
+ const thenT = this.parseTileExpr();
2434
+ this.eat("kw", "else");
2435
+ return {
2436
+ kind: "TileIf",
2437
+ cond,
2438
+ consequent: thenT,
2439
+ alternate: this.parseTileExpr(),
2440
+ pos: start.pos
2441
+ };
2442
+ }
2443
+ if (this.matchKw("match")) {
2444
+ const start = this.next();
2445
+ const scrut = this.parseExpr();
2446
+ this.eat("kw", "with");
2447
+ const arms = [];
2448
+ while (this.matchOp("|")) {
2449
+ this.next();
2450
+ const pattern = this.parsePattern();
2451
+ this.eat("op", "->");
2452
+ const body = this.parseTileExpr();
2453
+ arms.push({
2454
+ pattern,
2455
+ body
2456
+ });
2457
+ }
2458
+ return {
2459
+ kind: "TileMatch",
2460
+ scrutinee: scrut,
2461
+ arms,
2462
+ pos: start.pos
2463
+ };
2464
+ }
2465
+ return this.parseTileCall();
2466
+ }
2467
+ parseTileCall() {
2468
+ const nameTok = this.eat("ident");
2469
+ const name = nameTok.value;
2470
+ const isBuiltin = BUILTIN_TILES$1.has(name);
2471
+ const takesValueArg = VALUE_ARG_BUILTINS.has(name);
2472
+ const args = [];
2473
+ if (this.matchOp("(")) {
2474
+ this.next();
2475
+ if (!this.matchOp(")")) {
2476
+ args.push(this.parseTileArg(isBuiltin, takesValueArg));
2477
+ while (this.matchOp(",")) {
2478
+ this.next();
2479
+ args.push(this.parseTileArg(isBuiltin, takesValueArg));
2480
+ }
2481
+ }
2482
+ this.eat("op", ")");
2483
+ }
2484
+ const props = [];
2485
+ if (this.matchOp("{")) {
2486
+ this.next();
2487
+ if (!this.matchOp("}")) {
2488
+ props.push(this.parseTileProp());
2489
+ while (this.matchOp(",")) {
2490
+ this.next();
2491
+ props.push(this.parseTileProp());
2492
+ }
2493
+ }
2494
+ this.eat("op", "}");
2495
+ }
2496
+ return {
2497
+ kind: "TileCall",
2498
+ name,
2499
+ args,
2500
+ props,
2501
+ pos: nameTok.pos
2502
+ };
2503
+ }
2504
+ parseTileArg(parentIsBuiltin, parentTakesValueArg = false) {
2505
+ const first = this.peek();
2506
+ if ((first.kind === "ident" || first.kind === "kw") && this.matchTAt(1, "op", "=")) {
2507
+ const name = first.value;
2508
+ this.next();
2509
+ this.eat("op", "=");
2510
+ const argTakesValue = parentTakesValueArg || VALUE_NAMED_ARGS.has(name);
2511
+ return {
2512
+ kind: "TileArg",
2513
+ name,
2514
+ value: this.parseArgValue(parentIsBuiltin, argTakesValue)
2515
+ };
2516
+ }
2517
+ return {
2518
+ kind: "TileArg",
2519
+ value: this.parseArgValue(parentIsBuiltin, parentTakesValueArg)
2520
+ };
2521
+ }
2522
+ parseArgValue(parentIsBuiltin = true, parentTakesValueArg = false) {
2523
+ if (this.matchKw("for") || this.matchKw("when")) return this.parseTileExpr();
2524
+ if (this.matchKw("match")) {
2525
+ if (parentTakesValueArg) return this.parseExpr();
2526
+ return this.parseTileExpr();
2527
+ }
2528
+ if (this.matchKw("if")) {
2529
+ if (parentTakesValueArg) return this.parseExpr();
2530
+ return this.parseTileExpr();
2531
+ }
2532
+ const tok0 = this.peek();
2533
+ if (tok0.kind === "ident") {
2534
+ const name = tok0.value;
2535
+ const p1 = this.peek(1);
2536
+ const looksLikeTileCall = p1.kind === "op" && (p1.value === "(" || p1.value === "{");
2537
+ const isBuiltin = BUILTIN_TILES$1.has(name);
2538
+ const isCapital = !!name[0] && name[0] >= "A" && name[0] <= "Z";
2539
+ if (isBuiltin && looksLikeTileCall) return this.parseTileCall();
2540
+ if (!parentIsBuiltin) return this.parseExpr();
2541
+ if (isCapital && looksLikeTileCall) return this.parseTileCall();
2542
+ if (isCapital && !looksLikeTileCall) return this.parseTileCall();
2543
+ }
2544
+ return this.parseExpr();
2545
+ }
2546
+ parseTileProp() {
2547
+ const nameTok = this.peek();
2548
+ if (nameTok.kind !== "ident" && nameTok.kind !== "kw") throw new ParseError("Expected prop name", nameTok.pos);
2549
+ this.next();
2550
+ this.eat("op", ":");
2551
+ const value = this.parseExpr();
2552
+ return {
2553
+ kind: "TileProp",
2554
+ name: nameTok.value,
2555
+ value
2556
+ };
2557
+ }
2558
+ parseFn() {
2559
+ const start = this.eat("kw", "fn");
2560
+ const name = this.eat("ident").value;
2561
+ this.eat("op", "(");
2562
+ const params = [];
2563
+ if (!this.matchOp(")")) {
2564
+ params.push(this.parseFnParam());
2565
+ while (this.matchOp(",")) {
2566
+ this.next();
2567
+ params.push(this.parseFnParam());
2568
+ }
2569
+ }
2570
+ this.eat("op", ")");
2571
+ let ret;
2572
+ if (this.matchOp("->")) {
2573
+ this.next();
2574
+ ret = this.parseTypeExpr();
2575
+ }
2576
+ this.eat("op", "=");
2577
+ const def = {
2578
+ kind: "FnDef",
2579
+ name,
2580
+ params,
2581
+ body: this.parseExpr(),
2582
+ pos: start.pos
2583
+ };
2584
+ if (ret) def.ret = ret;
2585
+ return def;
2586
+ }
2587
+ parseFnParam() {
2588
+ const name = this.eat("ident").value;
2589
+ this.eat("op", ":");
2590
+ return {
2591
+ name,
2592
+ type: this.parseTypeExpr()
2593
+ };
2594
+ }
2595
+ parseEffect() {
2596
+ const start = this.eat("kw", "effect");
2597
+ const name = this.eat("ident").value;
2598
+ let cap;
2599
+ let inType;
2600
+ let outType;
2601
+ let policy;
2602
+ let retry;
2603
+ let mapRequest;
2604
+ while (this.isEffectField()) {
2605
+ const key = this.peek();
2606
+ if (key.kind === "kw" && key.value === "cap") {
2607
+ this.next();
2608
+ this.eat("op", "=");
2609
+ cap = this.readQualifiedName();
2610
+ } else if (key.kind === "kw" && key.value === "in") {
2611
+ this.next();
2612
+ this.eat("op", "=");
2613
+ inType = this.parseTypeExpr();
2614
+ } else if (key.kind === "kw" && key.value === "out") {
2615
+ this.next();
2616
+ this.eat("op", "=");
2617
+ outType = this.parseTypeExpr();
2618
+ } else if (key.kind === "kw" && key.value === "policy") {
2619
+ this.next();
2620
+ this.eat("op", "=");
2621
+ policy = this.parsePolicy();
2622
+ } else if (key.kind === "kw" && key.value === "retry") {
2623
+ this.next();
2624
+ this.eat("op", "=");
2625
+ retry = this.parseRetry();
2626
+ } else if (key.kind === "ident" && key.value === "map-request") {
2627
+ this.next();
2628
+ this.eat("op", "=");
2629
+ mapRequest = this.parseExpr();
2630
+ } else break;
2631
+ }
2632
+ if (!cap || !inType || !outType) throw new ParseError(`effect requires cap, in, out`, start.pos);
2633
+ const def = {
2634
+ kind: "EffectDef",
2635
+ name,
2636
+ cap,
2637
+ inType,
2638
+ outType,
2639
+ pos: start.pos
2640
+ };
2641
+ if (policy) def.policy = policy;
2642
+ if (retry) def.retry = retry;
2643
+ if (mapRequest) def.mapRequest = mapRequest;
2644
+ return def;
2645
+ }
2646
+ isEffectField() {
2647
+ const t = this.peek();
2648
+ if (t.kind === "kw" && [
2649
+ "cap",
2650
+ "in",
2651
+ "out",
2652
+ "policy",
2653
+ "retry"
2654
+ ].includes(t.value)) return true;
2655
+ if (t.kind === "ident" && t.value === "map-request") return true;
2656
+ return false;
2657
+ }
2658
+ parsePolicy() {
2659
+ const t = this.eat("ident");
2660
+ if (t.value === "latest") return { kind: "PolLatest" };
2661
+ if (t.value === "queue") return { kind: "PolQueue" };
2662
+ if (t.value === "once") return { kind: "PolOnce" };
2663
+ if (t.value === "latest-per-key") {
2664
+ this.eat("op", "(");
2665
+ const key = this.parseExpr();
2666
+ this.eat("op", ")");
2667
+ return {
2668
+ kind: "PolLatestKey",
2669
+ key
2670
+ };
2671
+ }
2672
+ if (t.value === "debounce") {
2673
+ this.eat("op", "(");
2674
+ const ms = this.parseDuration();
2675
+ this.eat("op", ")");
2676
+ return {
2677
+ kind: "PolDebounce",
2678
+ ms
2679
+ };
2680
+ }
2681
+ if (t.value === "throttle") {
2682
+ this.eat("op", "(");
2683
+ const ms = this.parseDuration();
2684
+ this.eat("op", ")");
2685
+ return {
2686
+ kind: "PolThrottle",
2687
+ ms
2688
+ };
2689
+ }
2690
+ throw new ParseError(`Unknown policy "${t.value}"`, t.pos);
2691
+ }
2692
+ parseRetry() {
2693
+ const t = this.eat("ident");
2694
+ if (t.value === "none") return { kind: "RetryNone" };
2695
+ if (t.value === "linear") {
2696
+ this.eat("op", "(");
2697
+ const n = this.eat("num").value;
2698
+ this.eat("op", ",");
2699
+ const ms = this.parseDuration();
2700
+ this.eat("op", ")");
2701
+ return {
2702
+ kind: "RetryLinear",
2703
+ n,
2704
+ ms
2705
+ };
2706
+ }
2707
+ if (t.value === "exponential") {
2708
+ this.eat("op", "(");
2709
+ const n = this.eat("num").value;
2710
+ this.eat("op", ",");
2711
+ const ms = this.parseDuration();
2712
+ this.eat("op", ",");
2713
+ const factor = this.eat("num").value;
2714
+ this.eat("op", ")");
2715
+ return {
2716
+ kind: "RetryExp",
2717
+ n,
2718
+ ms,
2719
+ factor
2720
+ };
2721
+ }
2722
+ throw new ParseError(`Unknown retry "${t.value}"`, t.pos);
2723
+ }
2724
+ parseDuration() {
2725
+ const n = this.eat("num").value;
2726
+ const unit = this.eat("ident").value;
2727
+ if (unit === "ms") return n;
2728
+ if (unit === "s") return n * 1e3;
2729
+ if (unit === "m") return n * 60 * 1e3;
2730
+ throw new ParseError(`Unknown duration unit "${unit}"`, this.peek().pos);
2731
+ }
2732
+ parseApp() {
2733
+ const start = this.eat("kw", "app");
2734
+ const name = this.eat("ident").value;
2735
+ let caps = [];
2736
+ let routes = [];
2737
+ let init = [];
2738
+ let theme;
2739
+ while (!this.isAppEnd()) {
2740
+ const ident = this.eat("ident");
2741
+ const k = ident.value;
2742
+ this.eat("op", "=");
2743
+ if (k === "caps") caps = this.parseQualifiedList();
2744
+ else if (k === "routes") routes = this.parseRouteMap();
2745
+ else if (k === "init") init = this.parseInitList();
2746
+ else if (k === "theme") theme = this.eat("ident").value;
2747
+ else if (k === "meta" || k === "http" || k === "indexed-db" || k === "analytics") this.parseExpr();
2748
+ else throw new ParseError(`Unknown app field "${k}"`, ident.pos);
2749
+ }
2750
+ const def = {
2751
+ kind: "AppDef",
2752
+ name,
2753
+ caps,
2754
+ routes,
2755
+ init,
2756
+ pos: start.pos
2757
+ };
2758
+ if (theme) def.theme = theme;
2759
+ return def;
2760
+ }
2761
+ isAppEnd() {
2762
+ const t = this.peek();
2763
+ if (t.kind === "eof") return true;
2764
+ if (t.kind === "kw") return true;
2765
+ return false;
2766
+ }
2767
+ parseQualifiedList() {
2768
+ this.eat("op", "[");
2769
+ const out = [];
2770
+ if (!this.matchOp("]")) {
2771
+ out.push(this.readQualifiedName());
2772
+ while (this.matchOp(",")) {
2773
+ this.next();
2774
+ out.push(this.readQualifiedName());
2775
+ }
2776
+ }
2777
+ this.eat("op", "]");
2778
+ return out;
2779
+ }
2780
+ readQualifiedName() {
2781
+ let name = this.eat("ident").value;
2782
+ while (this.matchOp(".")) {
2783
+ this.next();
2784
+ name += `.${this.eat("ident").value}`;
2785
+ }
2786
+ return name;
2787
+ }
2788
+ parseRouteMap() {
2789
+ this.eat("op", "{");
2790
+ const routes = [];
2791
+ if (!this.matchOp("}")) {
2792
+ routes.push(this.parseRouteEntry());
2793
+ while (this.matchOp(",")) {
2794
+ this.next();
2795
+ routes.push(this.parseRouteEntry());
2796
+ }
2797
+ }
2798
+ this.eat("op", "}");
2799
+ return routes;
2800
+ }
2801
+ parseRouteEntry() {
2802
+ const path = this.eat("str").value;
2803
+ if (this.matchOp("->>")) {
2804
+ this.next();
2805
+ return {
2806
+ path,
2807
+ tile: `>>${this.eat("str").value}`
2808
+ };
2809
+ }
2810
+ this.eat("op", "->");
2811
+ return {
2812
+ path,
2813
+ tile: this.eat("ident").value
2814
+ };
2815
+ }
2816
+ parseInitList() {
2817
+ this.eat("op", "[");
2818
+ const out = [];
2819
+ if (!this.matchOp("]")) {
2820
+ out.push(this.parseExpr());
2821
+ while (this.matchOp(",")) {
2822
+ this.next();
2823
+ out.push(this.parseExpr());
2824
+ }
2825
+ }
2826
+ this.eat("op", "]");
2827
+ return out;
2828
+ }
2829
+ };
2830
+ function parse(tokens) {
2831
+ return new Parser(tokens).parseProgram();
2832
+ }
2833
+ //#endregion
2834
+ //#region src/typecheck.ts
2835
+ const BUILTIN_TILES = new Set([
2836
+ "page",
2837
+ "region",
2838
+ "row",
2839
+ "column",
2840
+ "stack",
2841
+ "grid",
2842
+ "box",
2843
+ "card",
2844
+ "panel",
2845
+ "divider",
2846
+ "scroll",
2847
+ "text",
2848
+ "heading",
2849
+ "link",
2850
+ "code",
2851
+ "markdown",
2852
+ "image",
2853
+ "icon",
2854
+ "video",
2855
+ "button",
2856
+ "input",
2857
+ "textarea",
2858
+ "check",
2859
+ "radio",
2860
+ "select",
2861
+ "slider",
2862
+ "switch",
2863
+ "form",
2864
+ "label",
2865
+ "fieldset",
2866
+ "error",
2867
+ "list",
2868
+ "list-item",
2869
+ "table",
2870
+ "table-head",
2871
+ "table-body",
2872
+ "table-row",
2873
+ "table-cell",
2874
+ "modal",
2875
+ "drawer",
2876
+ "tooltip",
2877
+ "popover",
2878
+ "toast",
2879
+ "spinner",
2880
+ "progress",
2881
+ "skeleton",
2882
+ "route-outlet"
2883
+ ]);
2884
+ const A11Y_CODES = new Set([
2885
+ "E0701",
2886
+ "E0702",
2887
+ "E0703"
2888
+ ]);
2889
+ /** Returns errors with a11y warnings filtered out (unless strict). */
2890
+ function check(program, opts) {
2891
+ const errors = checkAll(program);
2892
+ if (opts?.strictA11y) return errors;
2893
+ return errors.filter((e) => !A11Y_CODES.has(e.code));
2894
+ }
2895
+ function checkAll(program) {
2896
+ const errors = [];
2897
+ const sym = {
2898
+ types: /* @__PURE__ */ new Map(),
2899
+ slots: /* @__PURE__ */ new Map(),
2900
+ reducers: /* @__PURE__ */ new Map(),
2901
+ tiles: /* @__PURE__ */ new Map(),
2902
+ fns: /* @__PURE__ */ new Map(),
2903
+ effects: /* @__PURE__ */ new Map()
2904
+ };
2905
+ for (const def of program.defs) switch (def.kind) {
2906
+ case "TypeDef":
2907
+ sym.types.set(def.name, def);
2908
+ break;
2909
+ case "SlotDef":
2910
+ sym.slots.set(def.name, def);
2911
+ break;
2912
+ case "ReducerDef":
2913
+ sym.reducers.set(def.name, def);
2914
+ break;
2915
+ case "TileDef":
2916
+ sym.tiles.set(def.name, def);
2917
+ break;
2918
+ case "FnDef":
2919
+ sym.fns.set(def.name, def);
2920
+ break;
2921
+ case "EffectDef":
2922
+ sym.effects.set(def.name, def);
2923
+ break;
2924
+ case "AppDef":
2925
+ sym.app = def;
2926
+ break;
2927
+ }
2928
+ for (const def of program.defs) {
2929
+ if (def.kind === "SlotDef") checkSlot(def, sym, errors);
2930
+ if (def.kind === "TileDef") checkTile(def, sym, errors);
2931
+ if (def.kind === "ReducerDef") checkReducer(def, sym, errors);
2932
+ if (def.kind === "FnDef") checkFn(def, sym, errors);
2933
+ if (def.kind === "EffectDef") checkEffect(def, sym, errors);
2934
+ if (def.kind === "AppDef") checkApp(def, sym, errors);
2935
+ }
2936
+ return errors;
2937
+ }
2938
+ function checkSlot(slot, sym, errors) {
2939
+ resolveType(slot.type, sym, errors);
2940
+ checkExpr(slot.init, sym, errors, {
2941
+ kind: "slot-init",
2942
+ localBinds: /* @__PURE__ */ new Set()
2943
+ });
2944
+ }
2945
+ function checkTile(tile, sym, errors) {
2946
+ const ctx = {
2947
+ kind: "tile",
2948
+ localBinds: /* @__PURE__ */ new Set()
2949
+ };
2950
+ if (tile.in) ctx.localBinds.add("$1");
2951
+ checkTileExpr(tile.body, sym, errors, ctx);
2952
+ }
2953
+ function checkA11y(t, errors) {
2954
+ if (t.name === "button") {
2955
+ const hasText = t.args.some((a) => a.name === "text");
2956
+ const hasAria = t.props.some((p) => p.name === "aria-label");
2957
+ if (!hasText && !hasAria) errors.push({
2958
+ code: "E0701",
2959
+ kind: "a11y-button",
2960
+ message: `button must have a text= argument or aria-label prop`,
2961
+ pos: t.pos
2962
+ });
2963
+ }
2964
+ if (t.name === "image") {
2965
+ if (!(t.args.some((a) => a.name === "alt") || t.props.some((p) => p.name === "alt"))) errors.push({
2966
+ code: "E0702",
2967
+ kind: "a11y-image",
2968
+ message: `image must have an alt prop`,
2969
+ pos: t.pos
2970
+ });
2971
+ }
2972
+ if (t.name === "link") {
2973
+ const hasText = t.args.some((a) => a.name === "text") || t.props.some((p) => p.name === "text");
2974
+ const hasAria = t.props.some((p) => p.name === "aria-label");
2975
+ if (!hasText && !hasAria) errors.push({
2976
+ code: "E0703",
2977
+ kind: "a11y-link",
2978
+ message: `link must have inner text or aria-label`,
2979
+ pos: t.pos
2980
+ });
2981
+ }
2982
+ }
2983
+ function checkTileExpr(t, sym, errors, ctx) {
2984
+ switch (t.kind) {
2985
+ case "TileFor": {
2986
+ checkExpr(t.iter, sym, errors, ctx);
2987
+ const inner = {
2988
+ ...ctx,
2989
+ localBinds: new Set(ctx.localBinds)
2990
+ };
2991
+ inner.localBinds.add(t.bind);
2992
+ checkTileExpr(t.body, sym, errors, inner);
2993
+ return;
2994
+ }
2995
+ case "TileWhen":
2996
+ checkExpr(t.cond, sym, errors, ctx);
2997
+ checkTileExpr(t.body, sym, errors, ctx);
2998
+ return;
2999
+ case "TileIf":
3000
+ checkExpr(t.cond, sym, errors, ctx);
3001
+ checkTileExpr(t.consequent, sym, errors, ctx);
3002
+ checkTileExpr(t.alternate, sym, errors, ctx);
3003
+ return;
3004
+ case "TileMatch":
3005
+ checkExpr(t.scrutinee, sym, errors, ctx);
3006
+ for (const arm of t.arms) {
3007
+ const inner = {
3008
+ ...ctx,
3009
+ localBinds: new Set(ctx.localBinds)
3010
+ };
3011
+ if (arm.pattern.kind === "PVariant") for (const b of arm.pattern.binds) inner.localBinds.add(b);
3012
+ if (arm.pattern.kind === "PBind") inner.localBinds.add(arm.pattern.name);
3013
+ checkTileExpr(arm.body, sym, errors, inner);
3014
+ }
3015
+ return;
3016
+ case "TileCall":
3017
+ checkTileCall(t, sym, errors, ctx);
3018
+ return;
3019
+ }
3020
+ }
3021
+ function checkTileCall(t, sym, errors, ctx) {
3022
+ if (!BUILTIN_TILES.has(t.name) && !sym.tiles.has(t.name)) errors.push({
3023
+ code: "E0105",
3024
+ kind: "undef-tile",
3025
+ message: `Reference to undefined tile "${t.name}"`,
3026
+ pos: t.pos
3027
+ });
3028
+ checkA11y(t, errors);
3029
+ const HANDLER_NAMES = new Set([
3030
+ "onClick",
3031
+ "onSubmit",
3032
+ "onChange",
3033
+ "onInput",
3034
+ "onFocus",
3035
+ "onBlur"
3036
+ ]);
3037
+ for (const arg of t.args) {
3038
+ const v = arg.value;
3039
+ if (v.kind === "TileCall" || v.kind === "TileFor" || v.kind === "TileWhen" || v.kind === "TileIf" || v.kind === "TileMatch") {
3040
+ checkTileExpr(v, sym, errors, ctx);
3041
+ continue;
3042
+ }
3043
+ if (arg.name && HANDLER_NAMES.has(arg.name)) {
3044
+ const expr = v;
3045
+ if (expr.kind !== "Ref") errors.push({
3046
+ code: "E0201",
3047
+ kind: "type-mismatch",
3048
+ message: `Event handler arg "${arg.name}" must be a reducer name`,
3049
+ pos: expr.pos
3050
+ });
3051
+ else if (!sym.reducers.has(expr.name)) errors.push({
3052
+ code: "E0102",
3053
+ kind: "undef-reducer",
3054
+ message: `Reference to undefined reducer "${expr.name}"`,
3055
+ pos: expr.pos
3056
+ });
3057
+ continue;
3058
+ }
3059
+ checkExpr(v, sym, errors, ctx);
3060
+ }
3061
+ for (const prop of t.props) if (HANDLER_NAMES.has(prop.name)) {
3062
+ const ref = prop.value;
3063
+ if (ref.kind !== "Ref") errors.push({
3064
+ code: "E0201",
3065
+ kind: "type-mismatch",
3066
+ message: `Event handler prop "${prop.name}" must be a reducer name`,
3067
+ pos: prop.value.pos
3068
+ });
3069
+ else if (!sym.reducers.has(ref.name)) errors.push({
3070
+ code: "E0102",
3071
+ kind: "undef-reducer",
3072
+ message: `Reference to undefined reducer "${ref.name}"`,
3073
+ pos: ref.pos
3074
+ });
3075
+ } else checkExpr(prop.value, sym, errors, ctx);
3076
+ }
3077
+ function checkReducer(r, sym, errors) {
3078
+ const ctx = {
3079
+ kind: "reducer",
3080
+ localBinds: /* @__PURE__ */ new Set(),
3081
+ capsAvailable: new Set(sym.app?.caps ?? [])
3082
+ };
3083
+ if (r.on.kind === "EffectEvent") {
3084
+ for (const b of r.on.binds) if (b !== "_") ctx.localBinds.add(b);
3085
+ }
3086
+ if (r.on.kind === "LifecycleEvent") {
3087
+ if (r.on.name.startsWith("route.")) ctx.localBinds.add("$route");
3088
+ }
3089
+ ctx.localBinds.add("$el");
3090
+ ctx.localBinds.add("$event");
3091
+ ctx.localBinds.add("$route");
3092
+ const writtenRoots = /* @__PURE__ */ new Set();
3093
+ for (const stmt of r.do) checkStmt(stmt, sym, errors, ctx, writtenRoots);
3094
+ }
3095
+ function checkStmt(s, sym, errors, ctx, writtenRoots) {
3096
+ if (s.kind === "ForStmt") {
3097
+ checkExpr(s.iter, sym, errors, ctx);
3098
+ const inner = {
3099
+ ...ctx,
3100
+ localBinds: new Set(ctx.localBinds)
3101
+ };
3102
+ inner.localBinds.add(s.bind);
3103
+ const bodyWrites = new Set(writtenRoots);
3104
+ for (const st of s.body) checkStmt(st, sym, errors, inner, bodyWrites);
3105
+ for (const r of bodyWrites) writtenRoots.add(r);
3106
+ return;
3107
+ }
3108
+ if (s.kind === "IfStmt") {
3109
+ checkExpr(s.cond, sym, errors, ctx);
3110
+ const thenWrites = new Set(writtenRoots);
3111
+ for (const st of s.consequent) checkStmt(st, sym, errors, ctx, thenWrites);
3112
+ const elseWrites = new Set(writtenRoots);
3113
+ for (const st of s.alternate) checkStmt(st, sym, errors, ctx, elseWrites);
3114
+ for (const r of thenWrites) writtenRoots.add(r);
3115
+ for (const r of elseWrites) writtenRoots.add(r);
3116
+ return;
3117
+ }
3118
+ if (s.kind === "MatchStmt") {
3119
+ checkExpr(s.scrutinee, sym, errors, ctx);
3120
+ const armSets = [];
3121
+ for (const arm of s.arms) {
3122
+ const inner = {
3123
+ ...ctx,
3124
+ localBinds: new Set(ctx.localBinds)
3125
+ };
3126
+ if (arm.pattern.kind === "PVariant") {
3127
+ for (const b of arm.pattern.binds) if (b !== "_") inner.localBinds.add(b);
3128
+ }
3129
+ if (arm.pattern.kind === "PBind") inner.localBinds.add(arm.pattern.name);
3130
+ const armWrites = new Set(writtenRoots);
3131
+ for (const st of arm.body) checkStmt(st, sym, errors, inner, armWrites);
3132
+ armSets.push(armWrites);
3133
+ }
3134
+ for (const set of armSets) for (const r of set) writtenRoots.add(r);
3135
+ return;
3136
+ }
3137
+ if (s.kind === "NoopStmt") return;
3138
+ if (s.kind === "LetStmt") {
3139
+ checkExpr(s.rhs, sym, errors, ctx);
3140
+ ctx.localBinds.add(s.name);
3141
+ return;
3142
+ }
3143
+ if (s.kind === "Emit") {
3144
+ const eff = sym.effects.get(s.effect);
3145
+ const isBuiltinNav = s.effect === "navigate" || s.effect === "navigate-replace" || s.effect === "navigate-back" || s.effect === "toast" || s.effect === "log";
3146
+ if (!eff && !isBuiltinNav) errors.push({
3147
+ code: "E0104",
3148
+ kind: "undef-effect",
3149
+ message: `Reference to undefined effect "${s.effect}"`,
3150
+ pos: s.pos
3151
+ });
3152
+ else if (eff && ctx.capsAvailable && !ctx.capsAvailable.has(eff.cap)) errors.push({
3153
+ code: "E0301",
3154
+ kind: "missing-capability",
3155
+ message: `Effect "${s.effect}" requires capability "${eff.cap}" which is not declared in app.caps`,
3156
+ pos: s.pos
3157
+ });
3158
+ for (const a of s.args) checkExpr(a, sym, errors, ctx);
3159
+ return;
3160
+ }
3161
+ const root = lvalueRoot(s.lvalue);
3162
+ if (!sym.slots.has(root)) errors.push({
3163
+ code: "E0103",
3164
+ kind: "undef-slot",
3165
+ message: `Assignment to undefined slot "${root}"`,
3166
+ pos: s.pos
3167
+ });
3168
+ const shape = lvalueShape(s.lvalue);
3169
+ if (writtenRoots.has(shape)) errors.push({
3170
+ code: "E0601",
3171
+ kind: "duplicate-write",
3172
+ message: `Slot path "${shape}" is written more than once in this reducer`,
3173
+ pos: s.pos
3174
+ });
3175
+ writtenRoots.add(shape);
3176
+ checkLvalue(s.lvalue, sym, errors, ctx);
3177
+ checkExpr(s.rhs, sym, errors, ctx);
3178
+ }
3179
+ function lvalueShape(lv) {
3180
+ if (lv.kind === "LSlot") return lv.name;
3181
+ const parts = [];
3182
+ let cur = lv;
3183
+ while (cur.kind !== "LSlot") {
3184
+ if (cur.kind === "LField") parts.unshift(`.${cur.field}`);
3185
+ else parts.unshift("[]");
3186
+ cur = cur.base;
3187
+ }
3188
+ return cur.name + parts.join("");
3189
+ }
3190
+ function lvalueRoot(lv) {
3191
+ while (lv.kind !== "LSlot") lv = lv.base;
3192
+ return lv.name;
3193
+ }
3194
+ function checkLvalue(lv, sym, errors, ctx) {
3195
+ if (lv.kind === "LSlot") return;
3196
+ if (lv.kind === "LIndex") checkExpr(lv.index, sym, errors, ctx);
3197
+ checkLvalue(lv.base, sym, errors, ctx);
3198
+ }
3199
+ function checkExpr(e, sym, errors, ctx) {
3200
+ switch (e.kind) {
3201
+ case "Num":
3202
+ case "Str":
3203
+ case "Bool":
3204
+ case "Unit": return;
3205
+ case "Ref":
3206
+ if (ctx.localBinds.has(e.name)) return;
3207
+ if (sym.slots.has(e.name)) {
3208
+ if (ctx.kind === "fn") errors.push({
3209
+ code: "E0305",
3210
+ kind: "fn-impurity",
3211
+ message: `fn "${currentFnName(ctx)}" must not read slot "${e.name}"`,
3212
+ pos: e.pos
3213
+ });
3214
+ return;
3215
+ }
3216
+ if (sym.fns.has(e.name)) return;
3217
+ if (e.name === "route" || e.name === "now" || e.name === "self") return;
3218
+ errors.push({
3219
+ code: "E0103",
3220
+ kind: "undef-ref",
3221
+ message: `Reference to undefined name "${e.name}"`,
3222
+ pos: e.pos
3223
+ });
3224
+ return;
3225
+ case "Variant":
3226
+ for (const p of e.payload) checkExpr(p, sym, errors, ctx);
3227
+ return;
3228
+ case "BinOp":
3229
+ checkExpr(e.lhs, sym, errors, ctx);
3230
+ checkExpr(e.rhs, sym, errors, ctx);
3231
+ return;
3232
+ case "UnaryOp":
3233
+ checkExpr(e.rhs, sym, errors, ctx);
3234
+ return;
3235
+ case "FieldAccess":
3236
+ checkExpr(e.base, sym, errors, ctx);
3237
+ return;
3238
+ case "Index":
3239
+ checkExpr(e.base, sym, errors, ctx);
3240
+ checkExpr(e.index, sym, errors, ctx);
3241
+ return;
3242
+ case "Call":
3243
+ for (const a of e.args) checkExpr(a, sym, errors, ctx);
3244
+ return;
3245
+ case "MethodCall":
3246
+ if (!KNOWN_METHODS.has(e.method)) errors.push({
3247
+ code: "E0801",
3248
+ kind: "unimplemented-method",
3249
+ message: `Method ".${e.method}" is not implemented by the runtime`,
3250
+ pos: e.pos
3251
+ });
3252
+ checkExpr(e.receiver, sym, errors, ctx);
3253
+ for (const a of e.args) {
3254
+ const inner = {
3255
+ ...ctx,
3256
+ localBinds: new Set(ctx.localBinds)
3257
+ };
3258
+ inner.localBinds.add("$1");
3259
+ inner.localBinds.add("$2");
3260
+ checkExpr(a, sym, errors, inner);
3261
+ }
3262
+ return;
3263
+ case "RecordLit":
3264
+ for (const f of e.fields) checkExpr(f.value, sym, errors, ctx);
3265
+ return;
3266
+ case "ListLit":
3267
+ for (const it of e.items) checkExpr(it, sym, errors, ctx);
3268
+ return;
3269
+ case "MapLit":
3270
+ for (const ent of e.entries) {
3271
+ checkExpr(ent.key, sym, errors, ctx);
3272
+ checkExpr(ent.value, sym, errors, ctx);
3273
+ }
3274
+ return;
3275
+ case "MatchExpr":
3276
+ checkExpr(e.scrutinee, sym, errors, ctx);
3277
+ for (const arm of e.arms) {
3278
+ const inner = {
3279
+ ...ctx,
3280
+ localBinds: new Set(ctx.localBinds)
3281
+ };
3282
+ if (arm.pattern.kind === "PVariant") {
3283
+ for (const b of arm.pattern.binds) if (b !== "_") inner.localBinds.add(b);
3284
+ }
3285
+ if (arm.pattern.kind === "PBind") inner.localBinds.add(arm.pattern.name);
3286
+ checkExpr(arm.body, sym, errors, inner);
3287
+ }
3288
+ return;
3289
+ case "IfExpr":
3290
+ checkExpr(e.cond, sym, errors, ctx);
3291
+ checkExpr(e.consequent, sym, errors, ctx);
3292
+ checkExpr(e.alternate, sym, errors, ctx);
3293
+ return;
3294
+ case "LetIn": {
3295
+ checkExpr(e.value, sym, errors, ctx);
3296
+ const inner = {
3297
+ ...ctx,
3298
+ localBinds: new Set(ctx.localBinds)
3299
+ };
3300
+ inner.localBinds.add(e.name);
3301
+ checkExpr(e.body, sym, errors, inner);
3302
+ return;
3303
+ }
3304
+ }
3305
+ }
3306
+ function currentFnName(ctx) {
3307
+ return ctx.fnName ?? "<fn>";
3308
+ }
3309
+ function checkFn(fn, sym, errors) {
3310
+ const ctx = {
3311
+ kind: "fn",
3312
+ localBinds: /* @__PURE__ */ new Set()
3313
+ };
3314
+ ctx.fnName = fn.name;
3315
+ for (const p of fn.params) ctx.localBinds.add(p.name);
3316
+ ctx.localBinds.add("$1");
3317
+ ctx.localBinds.add("$2");
3318
+ checkExpr(fn.body, sym, errors, ctx);
3319
+ }
3320
+ function checkEffect(eff, sym, errors) {
3321
+ resolveType(eff.inType, sym, errors);
3322
+ resolveType(eff.outType, sym, errors);
3323
+ if (eff.mapRequest) checkExpr(eff.mapRequest, sym, errors, {
3324
+ kind: "slot-init",
3325
+ localBinds: new Set(["$1"])
3326
+ });
3327
+ }
3328
+ function checkApp(app, sym, errors) {
3329
+ let saw404 = false;
3330
+ for (const r of app.routes) {
3331
+ if (r.tile.startsWith(">>")) continue;
3332
+ if (!sym.tiles.has(r.tile)) errors.push({
3333
+ code: "E0105",
3334
+ kind: "undef-tile",
3335
+ message: `Route "${r.path}" targets undefined tile "${r.tile}"`,
3336
+ pos: app.pos
3337
+ });
3338
+ if (r.path === "/404") saw404 = true;
3339
+ }
3340
+ if (!saw404) errors.push({
3341
+ code: "E0001",
3342
+ kind: "missing-404",
3343
+ message: `app.routes must include a "/404" entry`,
3344
+ pos: app.pos
3345
+ });
3346
+ for (const e of app.init) checkExpr(e, sym, errors, {
3347
+ kind: "reducer",
3348
+ localBinds: /* @__PURE__ */ new Set(),
3349
+ capsAvailable: new Set(app.caps)
3350
+ });
3351
+ }
3352
+ function resolveType(t, sym, errors) {
3353
+ switch (t.kind) {
3354
+ case "TypePrim": return;
3355
+ case "TypeRef":
3356
+ if (!sym.types.has(t.name)) return;
3357
+ return;
3358
+ case "TypeApp":
3359
+ for (const a of t.args) resolveType(a, sym, errors);
3360
+ return;
3361
+ case "TypeRecord":
3362
+ for (const f of t.fields) resolveType(f.type, sym, errors);
3363
+ return;
3364
+ case "TypeUnion":
3365
+ for (const v of t.variants) for (const p of v.payloads) resolveType(p, sym, errors);
3366
+ return;
3367
+ case "TypeNominal":
3368
+ resolveType(t.inner, sym, errors);
3369
+ return;
3370
+ case "TypeRefinement":
3371
+ resolveType(t.inner, sym, errors);
3372
+ return;
3373
+ }
3374
+ }
3375
+ //#endregion
3376
+ //#region src/compile.ts
3377
+ /** Inline a runtime bundle into generated module code, stripping the bridging import/export lines. */
3378
+ function inlineRuntime(generatedJs, runtimeBundleJs) {
3379
+ return `${runtimeBundleJs.replace(/^export \{[^}]*\};?\s*$/m, "")}\n${generatedJs.replace(/^import \{[^}]*\} from "[^"]*";\s*$/m, "")}`;
3380
+ }
3381
+ function compile(source, opts) {
3382
+ const program = parse(lex(source));
3383
+ const errors = check(program);
3384
+ if (errors.length > 0) return {
3385
+ kind: "fail",
3386
+ errors
3387
+ };
3388
+ let js = `${RUNTIME_HELPERS}\n${codegen(program, opts)}`;
3389
+ if (opts.bundle) {
3390
+ if (!opts.readRuntimeBundle) throw new Error("compile({ bundle: true }) requires a readRuntimeBundle function (see @kumikijs/compiler/node).");
3391
+ js = inlineRuntime(js, opts.readRuntimeBundle());
3392
+ }
3393
+ return {
3394
+ kind: "ok",
3395
+ js,
3396
+ program
3397
+ };
3398
+ }
3399
+ //#endregion
3400
+ export { ParseError, RUNTIME_HELPERS, check, codegen, compile, inlineRuntime, lex, parse };