@kernlang/core 3.4.0 → 3.4.2-canary.3.1.6b4cbb13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/codegen/body-ts.d.ts +68 -0
  2. package/dist/codegen/body-ts.js +233 -0
  3. package/dist/codegen/body-ts.js.map +1 -0
  4. package/dist/codegen/functions.js +19 -2
  5. package/dist/codegen/functions.js.map +1 -1
  6. package/dist/codegen/kern-stdlib.d.ts +63 -0
  7. package/dist/codegen/kern-stdlib.js +160 -0
  8. package/dist/codegen/kern-stdlib.js.map +1 -0
  9. package/dist/codegen/stdlib-preamble.d.ts +19 -0
  10. package/dist/codegen/stdlib-preamble.js +62 -4
  11. package/dist/codegen/stdlib-preamble.js.map +1 -1
  12. package/dist/codegen/type-system.js +15 -1
  13. package/dist/codegen/type-system.js.map +1 -1
  14. package/dist/codegen-core.js +7 -0
  15. package/dist/codegen-core.js.map +1 -1
  16. package/dist/codegen-expression.d.ts +8 -0
  17. package/dist/codegen-expression.js +111 -5
  18. package/dist/codegen-expression.js.map +1 -1
  19. package/dist/decompiler.js +219 -0
  20. package/dist/decompiler.js.map +1 -1
  21. package/dist/index.d.ts +14 -1
  22. package/dist/index.js +16 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/native-eligibility.d.ts +69 -0
  25. package/dist/native-eligibility.js +155 -0
  26. package/dist/native-eligibility.js.map +1 -0
  27. package/dist/node-props.d.ts +4 -0
  28. package/dist/node-props.js.map +1 -1
  29. package/dist/parser-core.d.ts +7 -1
  30. package/dist/parser-core.js +5 -2
  31. package/dist/parser-core.js.map +1 -1
  32. package/dist/parser-diagnostics.js +6 -2
  33. package/dist/parser-diagnostics.js.map +1 -1
  34. package/dist/parser-expression.d.ts +8 -3
  35. package/dist/parser-expression.js +293 -5
  36. package/dist/parser-expression.js.map +1 -1
  37. package/dist/parser-keywords.js +16 -0
  38. package/dist/parser-keywords.js.map +1 -1
  39. package/dist/parser-validate-native-eligible.d.ts +21 -0
  40. package/dist/parser-validate-native-eligible.js +46 -0
  41. package/dist/parser-validate-native-eligible.js.map +1 -0
  42. package/dist/parser-validate-propagation.d.ts +105 -0
  43. package/dist/parser-validate-propagation.js +684 -0
  44. package/dist/parser-validate-propagation.js.map +1 -0
  45. package/dist/parser.d.ts +10 -3
  46. package/dist/parser.js +11 -5
  47. package/dist/parser.js.map +1 -1
  48. package/dist/schema.js +199 -13
  49. package/dist/schema.js.map +1 -1
  50. package/dist/semantic-validator.js +9 -5
  51. package/dist/semantic-validator.js.map +1 -1
  52. package/dist/spec.d.ts +2 -2
  53. package/dist/spec.js +13 -1
  54. package/dist/spec.js.map +1 -1
  55. package/dist/types.d.ts +1 -1
  56. package/dist/value-ir.d.ts +27 -0
  57. package/dist/value-ir.js +6 -1
  58. package/dist/value-ir.js.map +1 -1
  59. package/package.json +1 -1
@@ -0,0 +1,684 @@
1
+ /** Slice 7 — `?` and `!` propagation operators.
2
+ *
3
+ * Walks every `fn` / `method` node whose handler body uses a postfix `?`
4
+ * or `!` after a call to a Result/Option-producing function, and rewrites
5
+ * the body with statement-level hoisting:
6
+ *
7
+ * const u = parseUser(raw)?; → const __k_t1 = parseUser(raw);
8
+ * if (__k_t1.kind === 'err') return __k_t1;
9
+ * const u = __k_t1.value;
10
+ *
11
+ * const u = parseUser(raw)!; → const __k_t1 = parseUser(raw);
12
+ * if (__k_t1.kind === 'err') throw new KernUnwrapError(__k_t1);
13
+ * const u = __k_t1.value;
14
+ *
15
+ * IIFE wrappers were tried in v0 and rejected: their `return` exits the
16
+ * IIFE, not the enclosing handler, so propagation fell through silently.
17
+ *
18
+ * Recognition is **name-anchored** at call expressions and accepts a tiny
19
+ * statement grammar:
20
+ * 1. `(const|let|var) <name>(:<type>)? = <call><op>;`
21
+ * 2. `return <call><op>;`
22
+ * 3. `<call><op>;`
23
+ * Everything else (mid-expression, `await call()?`, ternary surroundings,
24
+ * for-headers, JSX attributes, template `${…}`) is rejected with a clear
25
+ * diagnostic. Bare `obj.prop!` (TypeScript non-null assertion) is preserved
26
+ * verbatim because pass B skips member-access call sites.
27
+ *
28
+ * The failure discriminator (`'err'` vs `'none'`) comes from the CALLEE's
29
+ * kind, not the enclosing function's return type — `Option.some(x)?` always
30
+ * branches on `'none'` regardless of whether the enclosing fn returns
31
+ * Result or Option. Mixed cases (`?` on Option callee inside a Result fn)
32
+ * are rejected.
33
+ *
34
+ * Diagnostics:
35
+ * - INVALID_PROPAGATION — `?` outside a Result/Option fn,
36
+ * mismatched callee/container kind,
37
+ * closure-nested, mid-expression, or
38
+ * `await` in front of the call
39
+ * - UNSAFE_UNWRAP_IN_RESULT_FN — soft warning when `!` lives inside
40
+ * a Result/Option-returning fn (`?`
41
+ * keeps the rich error shape)
42
+ * - NESTED_PROPAGATION — `expr??` chains rejected; bind to
43
+ * a let between steps */
44
+ import { emitDiagnostic } from './parser-diagnostics.js';
45
+ /** The full return string must be exactly `Result<…>` (or `Option<…>`).
46
+ * Nested generics like `Promise<Result<…>>` or unions like `Result<…> | null`
47
+ * do NOT classify as Result/Option for propagation purposes — those are
48
+ * out-of-scope for slice 7 v1 (await? fusion is deferred to v2). */
49
+ const RESULT_RETURN_RE = /^Result<[\s\S]*>$/;
50
+ const OPTION_RETURN_RE = /^Option<[\s\S]*>$/;
51
+ /** Companion-object helpers from slice 4 that actually propagate (return
52
+ * Result / Option). The non-propagating helpers (`isOk`, `isErr`, `isSome`,
53
+ * `isNone`, `unwrapOr`) return booleans / unwrapped values and must NOT be
54
+ * recognised as propagation targets — `Result.isOk(r) ? a : b` is a ternary,
55
+ * `Result.unwrapOr(null, r)!` is a TS non-null on a plain value. */
56
+ const RESULT_PROPAGATING_HELPERS = new Set(['ok', 'err', 'map', 'mapErr', 'andThen']);
57
+ const OPTION_PROPAGATING_HELPERS = new Set(['some', 'none', 'map', 'andThen']);
58
+ /** Strip an outer `Promise<…>` wrapper if present, returning the inner
59
+ * text and a flag. Used by `classifyReturn` and the cross-module
60
+ * registry's identical classifier. */
61
+ function unwrapPromise(returns) {
62
+ const trimmed = returns.trim();
63
+ if (trimmed.startsWith('Promise<') && trimmed.endsWith('>')) {
64
+ return { inner: trimmed.slice('Promise<'.length, -1).trim(), wasPromise: true };
65
+ }
66
+ return { inner: trimmed, wasPromise: false };
67
+ }
68
+ function classifyReturn(returns, isAsync = false) {
69
+ if (typeof returns !== 'string')
70
+ return 'other';
71
+ const { inner, wasPromise } = unwrapPromise(returns);
72
+ const effectivelyAsync = wasPromise || isAsync;
73
+ if (RESULT_RETURN_RE.test(inner))
74
+ return effectivelyAsync ? 'asyncResult' : 'result';
75
+ if (OPTION_RETURN_RE.test(inner))
76
+ return effectivelyAsync ? 'asyncOption' : 'option';
77
+ return 'other';
78
+ }
79
+ /** Inner-kind helper — strips async to its underlying Result/Option family,
80
+ * used when checking that a callee's kind matches its container. */
81
+ function innerKind(k) {
82
+ if (k === 'result' || k === 'asyncResult')
83
+ return 'result';
84
+ if (k === 'option' || k === 'asyncOption')
85
+ return 'option';
86
+ return 'other';
87
+ }
88
+ /** Strip JS comments and string literals while preserving byte offsets —
89
+ * replaces contents with same-length whitespace so positions in the cleaned
90
+ * string still index back into the original. Mirrors the slice 6 walker. */
91
+ function stripCommentsAndStrings(code) {
92
+ return code
93
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
94
+ .replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length))
95
+ .replace(/"(?:[^"\\\n]|\\.)*"/g, (m) => `"${' '.repeat(Math.max(0, m.length - 2))}"`)
96
+ .replace(/'(?:[^'\\\n]|\\.)*'/g, (m) => `'${' '.repeat(Math.max(0, m.length - 2))}'`)
97
+ .replace(/`(?:[^`\\]|\\.)*`/g, (m) => `\`${' '.repeat(Math.max(0, m.length - 2))}\``);
98
+ }
99
+ /** Find the matching `)` for the `(` at `openIdx`. */
100
+ function findMatchingClose(cleaned, openIdx) {
101
+ let depth = 0;
102
+ for (let i = openIdx; i < cleaned.length; i++) {
103
+ const ch = cleaned[i];
104
+ if (ch === '(' || ch === '[' || ch === '{')
105
+ depth++;
106
+ else if (ch === ')' || ch === ']' || ch === '}') {
107
+ depth--;
108
+ if (depth === 0 && ch === ')')
109
+ return i;
110
+ }
111
+ }
112
+ return -1;
113
+ }
114
+ /** Scan forward from `qPos+1` at depth 0 to determine whether the `?` at
115
+ * `qPos` is the start of a ternary expression (`expr ? a : b`). Returns
116
+ * true if a `:` appears at the same paren/brace depth before any `;`/`}`. */
117
+ function isTernaryQuestion(cleaned, qPos) {
118
+ let depth = 0;
119
+ for (let i = qPos + 1; i < cleaned.length; i++) {
120
+ const c = cleaned[i];
121
+ if (c === '(' || c === '[' || c === '{')
122
+ depth++;
123
+ else if (c === ')' || c === ']' || c === '}') {
124
+ if (depth === 0)
125
+ return false;
126
+ depth--;
127
+ }
128
+ else if (depth === 0) {
129
+ if (c === ';')
130
+ return false;
131
+ if (c === ':')
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+ /** Names that must NOT be treated as method-shorthand bodies even when they
138
+ * precede a `(...)<ws>{` shape. Control-flow constructs use parentheses
139
+ * for the test/init expression and braces for the body, but those bodies
140
+ * are NOT closures — they share the enclosing fn's scope. */
141
+ const NOT_A_METHOD_NAME = new Set(['if', 'else', 'while', 'for', 'switch', 'catch', 'with', 'do', 'try', 'finally']);
142
+ /** Inspect the `{` at `lbracePos` to decide whether it opens a function
143
+ * body (one of: `function …() {…}`, `<id>() {…}` method shorthand,
144
+ * getter/setter `<id>() {…}`). Arrow bodies are handled separately at
145
+ * `=>` since the back-scan from `{` lands on `>` rather than `)`. */
146
+ function looksLikeFunctionBody(cleaned, lbracePos) {
147
+ let j = lbracePos - 1;
148
+ while (j >= 0 && /\s/.test(cleaned[j]))
149
+ j--;
150
+ if (j < 0 || cleaned[j] !== ')')
151
+ return false;
152
+ let openParen = -1;
153
+ {
154
+ let pd = 1;
155
+ for (let k = j - 1; k >= 0; k--) {
156
+ const c = cleaned[k];
157
+ if (c === ')')
158
+ pd++;
159
+ else if (c === '(') {
160
+ pd--;
161
+ if (pd === 0) {
162
+ openParen = k;
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ if (openParen < 0)
169
+ return false;
170
+ let k = openParen - 1;
171
+ while (k >= 0 && /\s/.test(cleaned[k]))
172
+ k--;
173
+ // Optional generic params `<…>` — skip.
174
+ if (k >= 0 && cleaned[k] === '>') {
175
+ let gd = 1;
176
+ k--;
177
+ while (k >= 0 && gd > 0) {
178
+ if (cleaned[k] === '>')
179
+ gd++;
180
+ else if (cleaned[k] === '<')
181
+ gd--;
182
+ k--;
183
+ }
184
+ while (k >= 0 && /\s/.test(cleaned[k]))
185
+ k--;
186
+ }
187
+ const nameEnd = k + 1;
188
+ while (k >= 0 && /[A-Za-z0-9_$]/.test(cleaned[k]))
189
+ k--;
190
+ const name = cleaned.slice(k + 1, nameEnd);
191
+ if (!name)
192
+ return false; // `(args) {` standalone — not a recognised shape
193
+ if (name === 'function')
194
+ return true;
195
+ if (NOT_A_METHOD_NAME.has(name))
196
+ return false;
197
+ // Method shorthand, getter, setter, named function expression, etc.
198
+ return true;
199
+ }
200
+ /** Track function-nesting depth to detect "inside a closure". A `?` inside
201
+ * a nested arrow / function-expression / method body cannot propagate to
202
+ * the outer fn — its `return` would belong to the inner closure. */
203
+ function buildClosureMap(cleaned) {
204
+ const depthAt = new Array(cleaned.length).fill(false);
205
+ let depth = 0;
206
+ let braceDepth = 0;
207
+ const closingBraceForFn = [];
208
+ for (let i = 0; i < cleaned.length; i++) {
209
+ const c = cleaned[i];
210
+ if (c === '{') {
211
+ braceDepth++;
212
+ // Decide whether this brace opens a function body (closure) by
213
+ // looking back at the surrounding shape.
214
+ if (looksLikeFunctionBody(cleaned, i)) {
215
+ depth++;
216
+ closingBraceForFn.push(braceDepth);
217
+ }
218
+ }
219
+ else if (c === '}') {
220
+ if (closingBraceForFn.length > 0 && closingBraceForFn[closingBraceForFn.length - 1] === braceDepth) {
221
+ depth--;
222
+ closingBraceForFn.pop();
223
+ }
224
+ braceDepth--;
225
+ }
226
+ // Arrow function: `=>` (with or without preceding `()`/`x`).
227
+ if (c === '=' && cleaned[i + 1] === '>') {
228
+ let j = i + 2;
229
+ while (j < cleaned.length && /\s/.test(cleaned[j]))
230
+ j++;
231
+ if (cleaned[j] === '{') {
232
+ // Arrow body is a block — the `{` will be processed in this same
233
+ // loop at index j, but `looksLikeFunctionBody` returns false there
234
+ // (back-scan finds `>` not `)`), so we explicitly mark the body's
235
+ // expected closing brace here.
236
+ depth++;
237
+ closingBraceForFn.push(braceDepth + 1);
238
+ }
239
+ else {
240
+ // `=> expr` form — body extends until the next unmatched `,`/`;`/closer
241
+ // at the same depth. Mark the whole expression as inside-closure.
242
+ let k = j;
243
+ let pd = 0;
244
+ while (k < cleaned.length) {
245
+ const ch = cleaned[k];
246
+ if (ch === '(' || ch === '[' || ch === '{')
247
+ pd++;
248
+ else if (ch === ')' || ch === ']' || ch === '}') {
249
+ if (pd === 0)
250
+ break;
251
+ pd--;
252
+ }
253
+ else if ((ch === ',' || ch === ';') && pd === 0)
254
+ break;
255
+ k++;
256
+ }
257
+ for (let m = j; m < k; m++)
258
+ depthAt[m] = true;
259
+ }
260
+ }
261
+ if (depth > 0)
262
+ depthAt[i] = true;
263
+ }
264
+ return depthAt;
265
+ }
266
+ /** Find the bounds of the statement containing `opPos`. Statement starts
267
+ * after the previous `;` at depth 0, or after an opening `{` (block start),
268
+ * or at start-of-body. Ends at the next `;` at depth 0 (inclusive of the
269
+ * `;`) or end-of-body. If the back-scan or forward-scan encounters an
270
+ * unmatched `(`/`[` or `)`/`]`, the site is inside a sub-expression. */
271
+ function statementBounds(cleaned, opPos) {
272
+ let start = 0;
273
+ let insideGrouping = false;
274
+ {
275
+ let depth = 0;
276
+ for (let i = opPos - 1; i >= 0; i--) {
277
+ const c = cleaned[i];
278
+ if (c === ')' || c === ']')
279
+ depth++;
280
+ else if (c === '(' || c === '[') {
281
+ if (depth === 0) {
282
+ start = i + 1;
283
+ insideGrouping = true;
284
+ break;
285
+ }
286
+ depth--;
287
+ }
288
+ else if (c === '}')
289
+ depth++;
290
+ else if (c === '{') {
291
+ if (depth === 0) {
292
+ start = i + 1;
293
+ break;
294
+ }
295
+ depth--;
296
+ }
297
+ else if (depth === 0 && c === ';') {
298
+ start = i + 1;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ let end = cleaned.length;
304
+ {
305
+ let depth = 0;
306
+ for (let i = opPos + 1; i < cleaned.length; i++) {
307
+ const c = cleaned[i];
308
+ if (c === '(' || c === '[')
309
+ depth++;
310
+ else if (c === ')' || c === ']') {
311
+ if (depth === 0) {
312
+ end = i;
313
+ insideGrouping = true;
314
+ break;
315
+ }
316
+ depth--;
317
+ }
318
+ else if (c === '{')
319
+ depth++;
320
+ else if (c === '}') {
321
+ if (depth === 0) {
322
+ end = i;
323
+ break;
324
+ }
325
+ depth--;
326
+ }
327
+ else if (depth === 0 && c === ';') {
328
+ end = i + 1;
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ return { start, end, insideGrouping };
334
+ }
335
+ /** Classify the statement around the propagation site. Returns either a
336
+ * recognised shape or null with a reason for the diagnostic. The optional
337
+ * `hasAwait` flag indicates whether the call was preceded by `await` — set
338
+ * by stripping a trailing `await` token from the head before re-matching
339
+ * the accepted statement grammar. */
340
+ function classifyStatement(cleaned, callStart, opPos, bounds) {
341
+ if (bounds.insideGrouping) {
342
+ return { ok: false, reason: 'mid-expression — propagation operator inside a parenthesised sub-expression' };
343
+ }
344
+ // Tail must be empty (or just `;`).
345
+ let tailStart = opPos + 1;
346
+ while (tailStart < bounds.end && /\s/.test(cleaned[tailStart]))
347
+ tailStart++;
348
+ const tail = cleaned.slice(tailStart, bounds.end).replace(/;\s*$/, '').trim();
349
+ if (tail.length > 0) {
350
+ return { ok: false, reason: 'mid-expression — characters between operator and statement end' };
351
+ }
352
+ // Head is everything before the call within the statement.
353
+ const headRaw = cleaned.slice(bounds.start, callStart).trim();
354
+ // Slice 7 v2.1 — strip a trailing `await` token (or a sole `await`) so the
355
+ // shape matchers can run against the same accepted grammar as the sync
356
+ // forms. `hasAwait` is propagated to the site for validation + lowering.
357
+ let hasAwait = false;
358
+ let head = headRaw;
359
+ const awaitSuffix = /(?:^|\s)await$/;
360
+ if (awaitSuffix.test(head)) {
361
+ hasAwait = true;
362
+ head = head.replace(awaitSuffix, '').trim();
363
+ }
364
+ if (head === '')
365
+ return { ok: true, shape: { kind: 'exprStmt' }, hasAwait };
366
+ if (head === 'return')
367
+ return { ok: true, shape: { kind: 'return' }, hasAwait };
368
+ const declMatch = head.match(/^(const|let|var)\s+([A-Za-z_$][\w$]*)(\s*:\s*[\s\S]+?)?\s*=$/);
369
+ if (declMatch) {
370
+ return {
371
+ ok: true,
372
+ shape: {
373
+ kind: 'declInit',
374
+ declKw: declMatch[1],
375
+ declId: declMatch[2],
376
+ typeAnnot: declMatch[3] ?? '',
377
+ },
378
+ hasAwait,
379
+ };
380
+ }
381
+ return {
382
+ ok: false,
383
+ reason: `unsupported statement shape — only declaration init, \`return\`, and expression-statement are accepted (got: "${headRaw}")`,
384
+ };
385
+ }
386
+ /** Walk the cleaned handler body looking for recognised `<callee>(…)<op>`
387
+ * patterns and classify each. Recognition negatives (optional chaining,
388
+ * ternary `?:`, comparison `!=`, member access, non-propagating Result/Option
389
+ * helpers) are SILENTLY skipped. Recognition positives that fail downstream
390
+ * validation (mid-expression, await, closure-nested) carry a reject reason
391
+ * for the diagnostic. */
392
+ function findPropagationSites(original, cleaned, ctx) {
393
+ const sites = [];
394
+ const closureMap = buildClosureMap(cleaned);
395
+ const reA = /\b(Result|Option)\.(\w+)\s*\(/g;
396
+ const reB = /\b(\w+)\s*\(/g;
397
+ function maybeRecord(callStart, openParen, calleeText, calleeKind) {
398
+ const closeParen = findMatchingClose(cleaned, openParen);
399
+ if (closeParen < 0)
400
+ return;
401
+ let after = closeParen + 1;
402
+ while (after < cleaned.length && /\s/.test(cleaned[after]))
403
+ after++;
404
+ const ch = cleaned[after];
405
+ if (ch !== '?' && ch !== '!')
406
+ return;
407
+ const opPos = after;
408
+ // Recognition negatives — silent skip.
409
+ if (ch === '?' && cleaned[opPos + 1] === '.')
410
+ return; // `?.` optional chaining
411
+ if (ch === '!' && cleaned[opPos + 1] === '=')
412
+ return; // `!=` / `!==` comparison
413
+ if (ch === '?' && isTernaryQuestion(cleaned, opPos))
414
+ return; // `expr ? a : b`
415
+ const op = ch;
416
+ const chained = op === '?' && cleaned[opPos + 1] === '?';
417
+ const insideClosure = !!closureMap[opPos];
418
+ const bounds = statementBounds(cleaned, opPos);
419
+ const cls = classifyStatement(cleaned, callStart, opPos, bounds);
420
+ sites.push({
421
+ callStart,
422
+ callEnd: closeParen,
423
+ opPos,
424
+ op,
425
+ callExpr: original.slice(callStart, closeParen + 1),
426
+ callee: calleeText,
427
+ calleeKind,
428
+ bounds,
429
+ shape: cls.ok ? cls.shape : null,
430
+ insideClosure,
431
+ chained,
432
+ hasAwait: cls.ok ? cls.hasAwait : false,
433
+ rejectReason: cls.ok ? null : cls.reason,
434
+ });
435
+ }
436
+ // Pass A — Result.<helper>(…) / Option.<helper>(…), whitelisted to the
437
+ // helpers that actually return Result / Option.
438
+ for (const m of cleaned.matchAll(reA)) {
439
+ const start = m.index ?? 0;
440
+ const ns = m[1];
441
+ const helper = m[2];
442
+ const propagating = ns === 'Result' ? RESULT_PROPAGATING_HELPERS.has(helper) : OPTION_PROPAGATING_HELPERS.has(helper);
443
+ if (!propagating)
444
+ continue;
445
+ const openParen = start + m[0].length - 1;
446
+ const calleeText = original.slice(start, openParen).trim();
447
+ maybeRecord(start, openParen, calleeText, ns === 'Result' ? 'result' : 'option');
448
+ }
449
+ // Pass B — bare identifier calls, restricted to known result/option fns
450
+ // (sync or async) and excluding member-access (preceded by `.`) so
451
+ // `obj.parse(x)?` is skipped instead of producing an invalid
452
+ // `obj.(() => …)()` rewrite.
453
+ for (const m of cleaned.matchAll(reB)) {
454
+ const start = m.index ?? 0;
455
+ const ident = m[1];
456
+ if (ident === 'Result' || ident === 'Option')
457
+ continue;
458
+ if (start > 0 && cleaned[start - 1] === '.')
459
+ continue;
460
+ let calleeKind = null;
461
+ if (ctx.asyncResultFns?.has(ident))
462
+ calleeKind = 'asyncResult';
463
+ else if (ctx.asyncOptionFns?.has(ident))
464
+ calleeKind = 'asyncOption';
465
+ else if (ctx.resultFns.has(ident))
466
+ calleeKind = 'result';
467
+ else if (ctx.optionFns.has(ident))
468
+ calleeKind = 'option';
469
+ if (!calleeKind)
470
+ continue;
471
+ if (sites.some((s) => s.callStart === start))
472
+ continue;
473
+ const openParen = start + m[0].length - 1;
474
+ const calleeText = original.slice(start, openParen).trim();
475
+ maybeRecord(start, openParen, calleeText, calleeKind);
476
+ }
477
+ sites.sort((a, b) => a.callStart - b.callStart);
478
+ return sites;
479
+ }
480
+ let gensymCounter = 0;
481
+ function nextGensym() {
482
+ gensymCounter += 1;
483
+ return `__k_t${gensymCounter}`;
484
+ }
485
+ /** Build the hoisted lowering for a single site. */
486
+ function buildLowering(site, tmp) {
487
+ const calleeInner = innerKind(site.calleeKind);
488
+ const failureKind = calleeInner === 'option' ? 'none' : 'err';
489
+ const failBranch = site.op === '?' ? `return ${tmp};` : `throw new KernUnwrapError(${tmp});`;
490
+ // Slice 7 v2.1 — preserve `await` on the awaited call expression so the
491
+ // hoisted temp resolves the Promise BEFORE the discriminant check.
492
+ const callRhs = site.hasAwait ? `await ${site.callExpr}` : site.callExpr;
493
+ const hoist = `const ${tmp} = ${callRhs};\nif (${tmp}.kind === '${failureKind}') ${failBranch}`;
494
+ const shape = site.shape;
495
+ if (!shape)
496
+ return hoist; // shouldn't reach if shape was null we skipped earlier
497
+ switch (shape.kind) {
498
+ case 'declInit':
499
+ return `${hoist}\n${shape.declKw} ${shape.declId}${shape.typeAnnot ?? ''} = ${tmp}.value;`;
500
+ case 'return':
501
+ return `${hoist}\nreturn ${tmp}.value;`;
502
+ case 'exprStmt':
503
+ return hoist;
504
+ }
505
+ }
506
+ /** Rewrite a single handler body. Public entry exported for tests. */
507
+ export function rewritePropagationInBody(code, fnReturn, ctx, emit) {
508
+ gensymCounter = 0; // deterministic temp names per handler
509
+ const cleaned = stripCommentsAndStrings(code);
510
+ const sites = findPropagationSites(code, cleaned, ctx);
511
+ if (sites.length === 0)
512
+ return { code, usedUnwrap: false };
513
+ let usedUnwrap = false;
514
+ const applied = [];
515
+ for (const site of sites) {
516
+ let apply = true;
517
+ if (site.chained) {
518
+ emit('NESTED_PROPAGATION', `Chained \`??\` is not supported — bind \`${site.callExpr}\` to a \`const\`/\`let\` between propagations.`);
519
+ apply = false;
520
+ }
521
+ else if (site.insideClosure) {
522
+ emit('INVALID_PROPAGATION', `\`${site.op}\` after \`${site.callExpr}\` sits inside a nested closure — its early-return would belong to the inner function. Lift the propagation outside the closure or use \`match\`.`);
523
+ apply = false;
524
+ }
525
+ else if (!site.shape) {
526
+ emit('INVALID_PROPAGATION', `\`${site.op}\` after \`${site.callExpr}\` is rejected: ${site.rejectReason ?? 'unsupported context'}. Slice 7 v1 supports only \`<call>${site.op};\`, \`return <call>${site.op};\`, and \`(const|let|var) name = <call>${site.op};\`.`);
527
+ apply = false;
528
+ }
529
+ else {
530
+ // Slice 7 v2.1 — async/await checks BEFORE the kind-match checks so a
531
+ // missing-`await` or stray-`await` site gets a clearer diagnostic
532
+ // (otherwise the kind-match error would dominate).
533
+ const calleeIsAsync = site.calleeKind === 'asyncResult' || site.calleeKind === 'asyncOption';
534
+ const containerIsAsync = fnReturn === 'asyncResult' || fnReturn === 'asyncOption';
535
+ const calleeInner = innerKind(site.calleeKind);
536
+ const containerInner = innerKind(fnReturn);
537
+ const calleeLabel = calleeInner === 'result' ? 'Result' : 'Option';
538
+ if (site.hasAwait && !calleeIsAsync) {
539
+ emit('INVALID_PROPAGATION', `\`await\` before \`${site.callee}(...)\` is unnecessary — \`${site.callee}\` returns a sync ${calleeLabel}, not a Promise.`);
540
+ apply = false;
541
+ }
542
+ else if (!site.hasAwait && calleeIsAsync) {
543
+ emit('INVALID_PROPAGATION', `\`${site.callee}(...)\` returns Promise<${calleeLabel}<…>> — write \`await ${site.callee}(...)${site.op}\` so the discriminant check sees the resolved value.`);
544
+ apply = false;
545
+ }
546
+ else if (site.hasAwait && !containerIsAsync) {
547
+ emit('INVALID_PROPAGATION', `\`await\` is only valid inside an \`async=true\` fn or one whose \`returns\` is \`Promise<…>\`. Mark the containing fn async or drop the \`await\`.`);
548
+ apply = false;
549
+ }
550
+ else if (site.op === '?') {
551
+ if (containerInner === 'other') {
552
+ emit('INVALID_PROPAGATION', `\`?\` requires the containing fn to return Result<T, E> or Option<T> — got a fn whose \`returns\` does not match. Use \`!\` to panic, or change the fn's return type.`);
553
+ apply = false;
554
+ }
555
+ else if (containerInner !== calleeInner) {
556
+ const containerLabel = containerInner === 'result' ? 'Result' : 'Option';
557
+ emit('INVALID_PROPAGATION', `\`?\` on a ${calleeLabel} call cannot propagate from a ${containerLabel}-returning fn. Use \`!\` to panic, or convert with \`match\`.`);
558
+ apply = false;
559
+ }
560
+ }
561
+ else if (site.op === '!' && containerInner === calleeInner) {
562
+ emit('UNSAFE_UNWRAP_IN_RESULT_FN', `\`${site.callExpr}!\` panics inside a fn that returns ${calleeLabel} — use \`?\` to propagate the error/none case instead of throwing.`);
563
+ // Soft warning — still rewrite.
564
+ }
565
+ }
566
+ const tmp = apply ? nextGensym() : '';
567
+ applied.push({ ...site, tmp, apply });
568
+ if (apply && site.op === '!')
569
+ usedUnwrap = true;
570
+ }
571
+ // Apply rewrites in REVERSE statement-bound order so earlier offsets stay
572
+ // valid. Each rewrite replaces the entire enclosing statement
573
+ // [bounds.start, bounds.end) with the lowering.
574
+ let outCode = code;
575
+ for (let i = applied.length - 1; i >= 0; i--) {
576
+ const site = applied[i];
577
+ if (!site.apply)
578
+ continue;
579
+ const lowering = buildLowering(site, site.tmp);
580
+ outCode = outCode.slice(0, site.bounds.start) + lowering + outCode.slice(site.bounds.end);
581
+ }
582
+ return { code: outCode, usedUnwrap };
583
+ }
584
+ /** Walk the IR collecting fn/method names whose `returns` is Result/Option.
585
+ * When `resolveImport` is supplied, also walks `use` nodes and merges in
586
+ * exported fn signatures from imported KERN modules — `from name=parseUser`
587
+ * contributes `parseUser` (or its `as=alias` if present) to the local
588
+ * resultFns/optionFns set. Imports the resolver returns `null` for are
589
+ * skipped silently. */
590
+ function collectKnownFns(root, resolveImport) {
591
+ const resultFns = new Set();
592
+ const optionFns = new Set();
593
+ const asyncResultFns = new Set();
594
+ const asyncOptionFns = new Set();
595
+ function walk(node) {
596
+ if (node.type === 'fn' || node.type === 'method') {
597
+ const props = node.props || {};
598
+ const name = typeof props.name === 'string' ? props.name : null;
599
+ const returns = props.returns;
600
+ const isAsync = props.async === true || props.async === 'true';
601
+ if (name && typeof returns === 'string') {
602
+ const cls = classifyReturn(returns, isAsync);
603
+ if (cls === 'result')
604
+ resultFns.add(name);
605
+ else if (cls === 'option')
606
+ optionFns.add(name);
607
+ else if (cls === 'asyncResult')
608
+ asyncResultFns.add(name);
609
+ else if (cls === 'asyncOption')
610
+ asyncOptionFns.add(name);
611
+ }
612
+ }
613
+ else if (node.type === 'use' && resolveImport) {
614
+ const path = node.props?.path;
615
+ if (typeof path === 'string') {
616
+ const exports = resolveImport(path);
617
+ if (exports) {
618
+ for (const child of node.children || []) {
619
+ if (child.type !== 'from')
620
+ continue;
621
+ const importedName = child.props?.name;
622
+ if (typeof importedName !== 'string')
623
+ continue;
624
+ const aliasRaw = child.props?.as;
625
+ const localName = typeof aliasRaw === 'string' && aliasRaw ? aliasRaw : importedName;
626
+ if (exports.resultFns.has(importedName))
627
+ resultFns.add(localName);
628
+ if (exports.optionFns.has(importedName))
629
+ optionFns.add(localName);
630
+ if (exports.asyncResultFns?.has(importedName))
631
+ asyncResultFns.add(localName);
632
+ if (exports.asyncOptionFns?.has(importedName))
633
+ asyncOptionFns.add(localName);
634
+ }
635
+ }
636
+ }
637
+ }
638
+ if (node.children)
639
+ for (const child of node.children)
640
+ walk(child);
641
+ }
642
+ walk(root);
643
+ return { resultFns, optionFns, asyncResultFns, asyncOptionFns };
644
+ }
645
+ /** Walk the IR and rewrite every fn/method handler body in place. Returns
646
+ * the set of nodes whose handlers used `!` so the codegen can decide
647
+ * whether to add `KernUnwrapError` to the auto-emitted preamble.
648
+ *
649
+ * Slice 7 v2 — when `resolveImport` is supplied, fn names imported via
650
+ * `use path="…"` get merged into the recognised set so cross-module
651
+ * `parseUser(raw)?` calls propagate. The CLI builds the resolver from a
652
+ * project-wide pre-pass; pure-parse callers omit it and cross-module
653
+ * recognition is disabled. */
654
+ export function validateAndRewritePropagation(state, root, resolveImport) {
655
+ const ctx = collectKnownFns(root, resolveImport);
656
+ let unwrapUsedAnywhere = false;
657
+ function walk(node) {
658
+ if (node.type === 'fn' || node.type === 'method') {
659
+ const isAsync = node.props?.async === true || node.props?.async === 'true';
660
+ const fnReturn = classifyReturn(node.props?.returns, isAsync);
661
+ for (const child of node.children || []) {
662
+ if (child.type !== 'handler')
663
+ continue;
664
+ const code = child.props?.code;
665
+ if (typeof code !== 'string')
666
+ continue;
667
+ const out = rewritePropagationInBody(code, fnReturn, ctx, (codeName, message) => {
668
+ emitDiagnostic(state, codeName, codeName === 'UNSAFE_UNWRAP_IN_RESULT_FN' ? 'warning' : 'error', message, node.loc?.line ?? 0, node.loc?.col ?? 0);
669
+ });
670
+ if (out.usedUnwrap)
671
+ unwrapUsedAnywhere = true;
672
+ if (out.code !== code) {
673
+ child.props.code = out.code;
674
+ }
675
+ }
676
+ }
677
+ if (node.children)
678
+ for (const child of node.children)
679
+ walk(child);
680
+ }
681
+ walk(root);
682
+ return { unwrapUsedAnywhere };
683
+ }
684
+ //# sourceMappingURL=parser-validate-propagation.js.map