@kernlang/core 3.4.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/body-ts.d.ts +68 -0
- package/dist/codegen/body-ts.js +214 -0
- package/dist/codegen/body-ts.js.map +1 -0
- package/dist/codegen/functions.js +19 -2
- package/dist/codegen/functions.js.map +1 -1
- package/dist/codegen/kern-stdlib.d.ts +63 -0
- package/dist/codegen/kern-stdlib.js +160 -0
- package/dist/codegen/kern-stdlib.js.map +1 -0
- package/dist/codegen/stdlib-preamble.d.ts +19 -0
- package/dist/codegen/stdlib-preamble.js +62 -4
- package/dist/codegen/stdlib-preamble.js.map +1 -1
- package/dist/codegen/type-system.js +15 -1
- package/dist/codegen/type-system.js.map +1 -1
- package/dist/codegen-core.js +7 -0
- package/dist/codegen-core.js.map +1 -1
- package/dist/codegen-expression.d.ts +8 -0
- package/dist/codegen-expression.js +111 -5
- package/dist/codegen-expression.js.map +1 -1
- package/dist/decompiler.js +219 -0
- package/dist/decompiler.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/node-props.d.ts +4 -0
- package/dist/node-props.js.map +1 -1
- package/dist/parser-core.d.ts +7 -1
- package/dist/parser-core.js +3 -2
- package/dist/parser-core.js.map +1 -1
- package/dist/parser-diagnostics.js +5 -2
- package/dist/parser-diagnostics.js.map +1 -1
- package/dist/parser-expression.d.ts +8 -3
- package/dist/parser-expression.js +281 -5
- package/dist/parser-expression.js.map +1 -1
- package/dist/parser-keywords.js +16 -0
- package/dist/parser-keywords.js.map +1 -1
- package/dist/parser-validate-propagation.d.ts +105 -0
- package/dist/parser-validate-propagation.js +684 -0
- package/dist/parser-validate-propagation.js.map +1 -0
- package/dist/parser.d.ts +10 -3
- package/dist/parser.js +11 -5
- package/dist/parser.js.map +1 -1
- package/dist/schema.js +199 -13
- package/dist/schema.js.map +1 -1
- package/dist/semantic-validator.js +9 -5
- package/dist/semantic-validator.js.map +1 -1
- package/dist/spec.d.ts +2 -2
- package/dist/spec.js +13 -1
- package/dist/spec.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/value-ir.d.ts +27 -0
- package/dist/value-ir.js +6 -1
- package/dist/value-ir.js.map +1 -1
- 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
|