@kernlang/python 3.5.9-canary.220.1.c398cd95 → 4.0.1-canary.222.1.f06f1a51
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-python.d.ts +86 -0
- package/dist/codegen-body-python.js +894 -28
- package/dist/codegen-body-python.js.map +1 -1
- package/dist/codegen-helpers.d.ts +1 -0
- package/dist/codegen-helpers.js +19 -6
- package/dist/codegen-helpers.js.map +1 -1
- package/dist/codegen-python.d.ts +1 -1
- package/dist/codegen-python.js +6 -2
- package/dist/codegen-python.js.map +1 -1
- package/dist/core/expr/helpers.d.ts +1 -0
- package/dist/core/expr/helpers.js +109 -2
- package/dist/core/expr/helpers.js.map +1 -1
- package/dist/core/expr/index.d.ts +1 -1
- package/dist/core/expr/index.js +214 -87
- package/dist/core/expr/index.js.map +1 -1
- package/dist/core/expr/list-ops.d.ts +81 -0
- package/dist/core/expr/list-ops.js +228 -0
- package/dist/core/expr/list-ops.js.map +1 -0
- package/dist/core/handlers/index.js +1 -0
- package/dist/core/handlers/index.js.map +1 -1
- package/dist/fastapi-portable.js +1 -0
- package/dist/fastapi-portable.js.map +1 -1
- package/dist/fastapi-route.js +132 -24
- package/dist/fastapi-route.js.map +1 -1
- package/dist/generators/data.d.ts +2 -0
- package/dist/generators/data.js +417 -14
- package/dist/generators/data.js.map +1 -1
- package/dist/generators/ground.js +52 -14
- package/dist/generators/ground.js.map +1 -1
- package/dist/targets/python.js +10 -0
- package/dist/targets/python.js.map +1 -1
- package/dist/type-map.d.ts +15 -0
- package/dist/type-map.js +46 -0
- package/dist/type-map.js.map +1 -1
- package/package.json +2 -2
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
* threads a `indent` string. The propagation hoist embeds its own 4-space
|
|
41
41
|
* relative indent on the `return __k_tN` line; the wrapper prepends the
|
|
42
42
|
* surrounding indent so the post-emit result nests correctly. */
|
|
43
|
-
import { applyTemplate, emitStringKeyArray, isPostfixMutationOperator, isSupportedAssignOperator, KERN_STDLIB_MODULES, lookupStdlib, needsArgParens, needsBinaryParens, parseExpression, parseKeys, suggestStdlibMethod, } from '@kernlang/core';
|
|
43
|
+
import { applyTemplate, collectFreeIdentifierNames, emitStringKeyArray, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isPostfixMutationOperator, isSupportedAssignOperator, KERN_STDLIB_MODULES, lookupStdlib, lowerJsClosureBodyToPython, needsArgParens, needsBinaryParens, parseExpression, parseKeys, suggestStdlibMethod, } from '@kernlang/core';
|
|
44
44
|
import { buildPythonParamList } from './codegen-helpers.js';
|
|
45
|
-
import { KERN_FMT_HELPER_PY, KERN_I32_HELPER_PY, KERN_PAIR_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './core/expr/index.js';
|
|
45
|
+
import { KERN_FMT_HELPER_PY, KERN_I32_HELPER_PY, KERN_JS_ARRAY_HELPERS_PY, KERN_JS_HELPER_PY, KERN_PAIR_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './core/expr/index.js';
|
|
46
|
+
import { isSharedPortableArrayMethod, isSharedPortableArrayProperty, lowerPortableArrayMethodPy, lowerPortableArrayPropertyPy, sharedPortableMethodRequiresPureReceiver, } from './core/expr/list-ops.js';
|
|
46
47
|
import { mapTsTypeToPython } from './type-map.js';
|
|
47
48
|
const INDENT_STEP = ' ';
|
|
48
49
|
function freshCtx(options) {
|
|
@@ -51,6 +52,8 @@ function freshCtx(options) {
|
|
|
51
52
|
imports: new Set(),
|
|
52
53
|
helpers: new Set(),
|
|
53
54
|
symbolMap: options?.symbolMap ?? {},
|
|
55
|
+
inClassBody: options?.inClassBody ?? false,
|
|
56
|
+
inConstructor: options?.inConstructor ?? false,
|
|
54
57
|
shadowedSymbols: new Set(),
|
|
55
58
|
localScopes: [],
|
|
56
59
|
regexScopes: [],
|
|
@@ -60,7 +63,12 @@ function freshCtx(options) {
|
|
|
60
63
|
tryDepth: 0,
|
|
61
64
|
finallyDepth: 0,
|
|
62
65
|
standaloneExpression: false,
|
|
66
|
+
coerceJsValues: options?.coerceJsValues ?? true,
|
|
63
67
|
traceHooks: options?.traceHooks,
|
|
68
|
+
pendingHoists: [],
|
|
69
|
+
closureSeq: 0,
|
|
70
|
+
loopScopeIndexes: [],
|
|
71
|
+
loopLaterAssignFrames: [],
|
|
64
72
|
};
|
|
65
73
|
}
|
|
66
74
|
/** PR-4 — Python helpers that normalize `each` pair-mode iteration sources.
|
|
@@ -151,6 +159,13 @@ export function emitNativeKernBodyPythonWithImports(handlerNode, options) {
|
|
|
151
159
|
}
|
|
152
160
|
try {
|
|
153
161
|
const code = emitChildrenPy(handlerNode.children ?? [], ctx, '').join('\n');
|
|
162
|
+
// Slices 0+1 — a hoisted closure def left un-flushed means some statement
|
|
163
|
+
// emitter produced a block arrow without routing through emitChildrenPy's
|
|
164
|
+
// flush point. That would silently drop the def → NameError at runtime.
|
|
165
|
+
// Fail loud instead.
|
|
166
|
+
if (ctx.pendingHoists.length > 0) {
|
|
167
|
+
throw new Error('Internal codegen error: block-arrow closure def(s) were not flushed (a statement emitter bypassed the emitChildrenPy hoist point).');
|
|
168
|
+
}
|
|
154
169
|
return { code, imports: ctx.imports, usedPropagation: ctx.usedPropagation, helpers: ctx.helpers };
|
|
155
170
|
}
|
|
156
171
|
finally {
|
|
@@ -192,15 +207,83 @@ function trailingCommentToPy(raw) {
|
|
|
192
207
|
return `# ${raw.slice(2, -2).trim()}`.trimEnd();
|
|
193
208
|
return `# ${raw}`.trimEnd();
|
|
194
209
|
}
|
|
195
|
-
|
|
210
|
+
/** Slice-2 fix — map each bare-identifier write target inside a loop body to the
|
|
211
|
+
* LAST top-level child index whose subtree writes it. Recurses into nested
|
|
212
|
+
* statements (if/else branches, nested loops, try bodies) but attributes every
|
|
213
|
+
* write to the TOP-LEVEL child containing it (the granularity
|
|
214
|
+
* `loopLaterAssignFrames.current` tracks). Member/index targets (`this.x`,
|
|
215
|
+
* `a[i]`) are excluded — mutating a captured OBJECT is by-reference in both
|
|
216
|
+
* languages and never pinned.
|
|
217
|
+
*
|
|
218
|
+
* Covered write node types (the body-statement emitters that can rebind a bare
|
|
219
|
+
* name): `assign` (its `target=` prop, INCLUDING the compound `op="+="`/`-=`/…
|
|
220
|
+
* and postfix `op="++"`/`--` forms — all the same node type, distinguished only
|
|
221
|
+
* by `op=`, all rebinding `target=`) and `set` (its `name=` prop, a bare-name
|
|
222
|
+
* cell write). `let`/`cell` are DECLARATIONS, not reassignments, so they are
|
|
223
|
+
* not scanned (the binding they create is the thing being pinned). */
|
|
224
|
+
function collectLoopAssignLastIndexes(children) {
|
|
225
|
+
const last = new Map();
|
|
226
|
+
const scan = (node, topIdx) => {
|
|
227
|
+
if (node.type === 'assign') {
|
|
228
|
+
// `target=` is the bare name (or member/index) being rebound, regardless of
|
|
229
|
+
// `op=` (plain `=`, compound `+=`, or postfix `++`/`--`).
|
|
230
|
+
const target = String(node.props?.target ?? '');
|
|
231
|
+
if (target && !target.includes('.') && !target.includes('['))
|
|
232
|
+
last.set(target, topIdx);
|
|
233
|
+
}
|
|
234
|
+
else if (node.type === 'set') {
|
|
235
|
+
// `set name=… to=…` rebinds a bare-name cell; the target is `name=`.
|
|
236
|
+
const target = String(node.props?.name ?? '');
|
|
237
|
+
if (target && !target.includes('.') && !target.includes('['))
|
|
238
|
+
last.set(target, topIdx);
|
|
239
|
+
}
|
|
240
|
+
for (const child of node.children ?? [])
|
|
241
|
+
scan(child, topIdx);
|
|
242
|
+
};
|
|
243
|
+
for (let i = 0; i < children.length; i++)
|
|
244
|
+
scan(children[i], i);
|
|
245
|
+
return last;
|
|
246
|
+
}
|
|
247
|
+
function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody = false) {
|
|
196
248
|
const lines = [];
|
|
197
249
|
ctx.localScopes.push(new Map(initialBindings));
|
|
198
250
|
ctx.regexScopes.push(new Map(initialBindings.map(([name]) => [name, null])));
|
|
199
251
|
ctx.renameStack.push(new Map());
|
|
252
|
+
// Slice-2 loop-variable pinning. When this recursion is a loop BODY, record
|
|
253
|
+
// the just-pushed scope's index so `emitBlockClosurePy` can decide whether a
|
|
254
|
+
// captured name resolves at-or-inside the enclosing loop body (→ pin). Only
|
|
255
|
+
// loop bodies mark a scope here — if/else/try/branch/with bodies do not, so a
|
|
256
|
+
// closure inside an `if` that is itself inside a loop still pins via the
|
|
257
|
+
// outer loop's recorded index (the `if` body's own scope index is >= it).
|
|
258
|
+
if (isLoopBody)
|
|
259
|
+
ctx.loopScopeIndexes.push(ctx.localScopes.length - 1);
|
|
260
|
+
// Slice-2 fix — pre-scan this loop body for bare-name assign targets so the
|
|
261
|
+
// closure emitter can reject a pin whose binding is reassigned AFTER the
|
|
262
|
+
// closure-creating statement (see loopLaterAssignFrames doc).
|
|
263
|
+
const loopFrame = isLoopBody ? { assignLast: collectLoopAssignLastIndexes(children), current: -1 } : null;
|
|
264
|
+
if (loopFrame)
|
|
265
|
+
ctx.loopLaterAssignFrames.push(loopFrame);
|
|
266
|
+
// Slices 0+1 fix (agon review, claude 0.7) — isolate the hoist buffer per
|
|
267
|
+
// recursion level. A statement emitter that lowers a HEADER expression (an
|
|
268
|
+
// `if`/`while` condition, an `each`/`for` iterable, a `branch` scrutinee)
|
|
269
|
+
// pushes that expression's closure defs into the buffer BEFORE recursing
|
|
270
|
+
// into its body via this function. Without isolation, the body-level
|
|
271
|
+
// per-child flush below would steal those defs and splice them INSIDE the
|
|
272
|
+
// body — after the header line already referenced the def name (runtime
|
|
273
|
+
// NameError: `if __kern_closure_0(2):` with the def indented under it).
|
|
274
|
+
// Saving/clearing here means a header def survives untouched until the
|
|
275
|
+
// PARENT level's per-child flush, which splices it before the entire
|
|
276
|
+
// statement — defs bind once, so before-the-header placement is correct for
|
|
277
|
+
// every header position including `elif` chains (the def simply precedes
|
|
278
|
+
// the whole if/elif chain).
|
|
279
|
+
const outerHoists = ctx.pendingHoists;
|
|
280
|
+
ctx.pendingHoists = [];
|
|
200
281
|
try {
|
|
201
282
|
for (let i = 0; i < children.length; i++) {
|
|
202
283
|
const child = children[i];
|
|
203
|
-
|
|
284
|
+
if (loopFrame)
|
|
285
|
+
loopFrame.current = i;
|
|
286
|
+
let trailStart = lines.length;
|
|
204
287
|
if (child.type === 'comment') {
|
|
205
288
|
for (const line of emitCommentPy(child))
|
|
206
289
|
lines.push(`${indent}${line}`);
|
|
@@ -327,7 +410,11 @@ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
|
|
|
327
410
|
throw new Error("Propagation '?' is not allowed in `while cond=` — bind the call to a `let` first, then test the bound name.");
|
|
328
411
|
}
|
|
329
412
|
lines.push(`${indent}while ${emitPyExprCtx(condIR, ctx)}:`);
|
|
330
|
-
|
|
413
|
+
// Slice-2: a `while` body is a loop body — per-iteration locals declared
|
|
414
|
+
// INSIDE it (JS re-binds block-scoped lets each iteration) must pin.
|
|
415
|
+
// The condition var, declared OUTSIDE, resolves below the loop scope and
|
|
416
|
+
// stays late-bound (by-reference, JS-parity-correct).
|
|
417
|
+
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [], true);
|
|
331
418
|
if (inner.length === 0)
|
|
332
419
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
333
420
|
for (const sl of inner)
|
|
@@ -463,7 +550,7 @@ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
|
|
|
463
550
|
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [
|
|
464
551
|
[k, 'const'],
|
|
465
552
|
[v, 'const'],
|
|
466
|
-
]);
|
|
553
|
+
], true);
|
|
467
554
|
if (inner.length === 0 && !ctx.traceHooks?.eachIterNext)
|
|
468
555
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
469
556
|
for (const sl of inner)
|
|
@@ -491,7 +578,7 @@ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
|
|
|
491
578
|
if (ctx.traceHooks?.eachIterNext) {
|
|
492
579
|
lines.push(`${indent}${INDENT_STEP}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(k)}, "value": ${k}})`);
|
|
493
580
|
}
|
|
494
|
-
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [[k, 'const']]);
|
|
581
|
+
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [[k, 'const']], true);
|
|
495
582
|
if (inner.length === 0 && !ctx.traceHooks?.eachIterNext)
|
|
496
583
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
497
584
|
for (const sl of inner)
|
|
@@ -504,7 +591,7 @@ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
|
|
|
504
591
|
if (ctx.traceHooks?.eachIterNext) {
|
|
505
592
|
lines.push(`${indent}${INDENT_STEP}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(v)}, "value": ${v}})`);
|
|
506
593
|
}
|
|
507
|
-
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [[v, 'const']]);
|
|
594
|
+
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [[v, 'const']], true);
|
|
508
595
|
if (inner.length === 0 && !ctx.traceHooks?.eachIterNext)
|
|
509
596
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
510
597
|
for (const sl of inner)
|
|
@@ -552,7 +639,7 @@ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
|
|
|
552
639
|
if (ctx.traceHooks?.eachIterNext) {
|
|
553
640
|
lines.push(`${indent}${INDENT_STEP}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(primaryBindingPy)}, "value": ${primaryBindingPy}})`);
|
|
554
641
|
}
|
|
555
|
-
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, initialBindings);
|
|
642
|
+
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, initialBindings, true);
|
|
556
643
|
// `pass` is needed only when the for-loop body would otherwise be empty:
|
|
557
644
|
// - index-mode path emits NO assignment (direct destructuring), so an
|
|
558
645
|
// empty children list leaves the loop bodyless → IndentationError.
|
|
@@ -575,6 +662,24 @@ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
|
|
|
575
662
|
for (const line of emitBranchPy(child, ctx, indent))
|
|
576
663
|
lines.push(line);
|
|
577
664
|
}
|
|
665
|
+
// Slices 0+1 — flush hoisted block-arrow closure defs. `emitLambdaPy`
|
|
666
|
+
// pushed each `def __kern_closure_N(...):` block into `ctx.pendingHoists`
|
|
667
|
+
// when it lowered a block arrow used by THIS child. Splice them in at the
|
|
668
|
+
// current indent IMMEDIATELY BEFORE the child's own lines so the def
|
|
669
|
+
// precedes its use — works at any nesting level because every nested
|
|
670
|
+
// emission funnels through emitChildrenPy. Bump `trailStart` past the
|
|
671
|
+
// spliced defs so the trailing-comment check below still measures only
|
|
672
|
+
// the child's own line count.
|
|
673
|
+
if (ctx.pendingHoists.length > 0) {
|
|
674
|
+
const hoistLines = [];
|
|
675
|
+
for (const def of ctx.pendingHoists) {
|
|
676
|
+
for (const dl of def)
|
|
677
|
+
hoistLines.push(`${indent}${dl}`);
|
|
678
|
+
}
|
|
679
|
+
lines.splice(trailStart, 0, ...hoistLines);
|
|
680
|
+
trailStart += hoistLines.length;
|
|
681
|
+
ctx.pendingHoists = [];
|
|
682
|
+
}
|
|
578
683
|
// W1 — re-attach an inline same-line trailing comment (captured by the
|
|
579
684
|
// migrator as `trailingComment=`) to the simple statement's last line,
|
|
580
685
|
// converted to a Python `#` comment.
|
|
@@ -588,9 +693,18 @@ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
|
|
|
588
693
|
}
|
|
589
694
|
}
|
|
590
695
|
finally {
|
|
696
|
+
if (isLoopBody)
|
|
697
|
+
ctx.loopScopeIndexes.pop();
|
|
698
|
+
if (loopFrame)
|
|
699
|
+
ctx.loopLaterAssignFrames.pop();
|
|
591
700
|
ctx.localScopes.pop();
|
|
592
701
|
ctx.regexScopes.pop();
|
|
593
702
|
ctx.renameStack.pop();
|
|
703
|
+
// Restore the parent level's hoist buffer (see the isolation comment at
|
|
704
|
+
// entry). Any defs THIS level's last child left behind are appended so the
|
|
705
|
+
// parent's flush (or the defensive end-of-body throw) still sees them —
|
|
706
|
+
// hoists are never silently dropped.
|
|
707
|
+
ctx.pendingHoists = outerHoists.concat(ctx.pendingHoists);
|
|
594
708
|
}
|
|
595
709
|
return lines;
|
|
596
710
|
}
|
|
@@ -667,7 +781,7 @@ function emitRangeForPy(node, ctx, indent) {
|
|
|
667
781
|
if (ctx.traceHooks?.forIterNext) {
|
|
668
782
|
out.push(`${bodyIndent}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(name)}, "value": ${name}})`);
|
|
669
783
|
}
|
|
670
|
-
const inner = emitChildrenPy(node.children ?? [], ctx, bodyIndent, [[name, 'const']]);
|
|
784
|
+
const inner = emitChildrenPy(node.children ?? [], ctx, bodyIndent, [[name, 'const']], true);
|
|
671
785
|
if (inner.length === 0 && !ctx.traceHooks?.forIterNext)
|
|
672
786
|
out.push(`${bodyIndent}pass`);
|
|
673
787
|
for (const sl of inner)
|
|
@@ -1248,6 +1362,20 @@ function lookupLocalBinding(ctx, name) {
|
|
|
1248
1362
|
}
|
|
1249
1363
|
return undefined;
|
|
1250
1364
|
}
|
|
1365
|
+
/** Slice-2 — the index into `ctx.localScopes` of the innermost scope that
|
|
1366
|
+
* binds `name`, or `null` if no scope binds it (an unresolved/host name).
|
|
1367
|
+
* Used by `emitBlockClosurePy` to decide loop-variable pinning: a captured
|
|
1368
|
+
* name pins IFF its binding index is at-or-inside the outermost enclosing
|
|
1369
|
+
* loop body (`>= ctx.loopScopeIndexes[0]`). Walks innermost→outermost so a
|
|
1370
|
+
* shadowing inner re-declaration wins over an outer binding of the same name,
|
|
1371
|
+
* matching the rename resolution the body emission uses. */
|
|
1372
|
+
function findBindingScopeIndex(ctx, name) {
|
|
1373
|
+
for (let i = ctx.localScopes.length - 1; i >= 0; i--) {
|
|
1374
|
+
if (ctx.localScopes[i].has(name))
|
|
1375
|
+
return i;
|
|
1376
|
+
}
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1251
1379
|
function isAssignableTarget(node) {
|
|
1252
1380
|
if (node.kind === 'ident')
|
|
1253
1381
|
return true;
|
|
@@ -1556,9 +1684,13 @@ const NON_EXCEPTION_LITERAL_KINDS = new Set([
|
|
|
1556
1684
|
* `emitPyExprCtx` which threads the live ctx (and therefore the live
|
|
1557
1685
|
* imports set) end-to-end. */
|
|
1558
1686
|
export function emitPyExpression(node, options) {
|
|
1687
|
+
return emitPyExpressionWithImports(node, options).code;
|
|
1688
|
+
}
|
|
1689
|
+
export function emitPyExpressionWithImports(node, options) {
|
|
1559
1690
|
const ctx = freshCtx(options);
|
|
1560
1691
|
ctx.standaloneExpression = true;
|
|
1561
|
-
|
|
1692
|
+
const code = emitPyExprCtx(node, ctx);
|
|
1693
|
+
return { code, imports: ctx.imports, helpers: ctx.helpers };
|
|
1562
1694
|
}
|
|
1563
1695
|
function emitPyExprCtx(node, ctx) {
|
|
1564
1696
|
switch (node.kind) {
|
|
@@ -1578,7 +1710,13 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1578
1710
|
case 'nullLit':
|
|
1579
1711
|
return 'None';
|
|
1580
1712
|
case 'undefLit':
|
|
1581
|
-
|
|
1713
|
+
// Ground/React layer (no helper channel) keeps the pre-slice collapse to
|
|
1714
|
+
// None; native bodies materialize the sentinel so `${undefined}` renders
|
|
1715
|
+
// "undefined" (vs null's "null") and `?? `/`typeof` can distinguish it.
|
|
1716
|
+
if (!ctx.coerceJsValues)
|
|
1717
|
+
return 'None';
|
|
1718
|
+
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
1719
|
+
return '_KERN_UNDEFINED';
|
|
1582
1720
|
case 'regexLit':
|
|
1583
1721
|
ctx.imports.add('re');
|
|
1584
1722
|
return `__k_re.compile(${pyRegexPattern(node)}, ${pyRegexFlags(node.flags, { allowGlobal: true })})`;
|
|
@@ -1597,6 +1735,8 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1597
1735
|
// module names) pass through unchanged.
|
|
1598
1736
|
if (ctx.shadowedSymbols.has(node.name))
|
|
1599
1737
|
return node.name;
|
|
1738
|
+
if (ctx.inClassBody && node.name === 'super')
|
|
1739
|
+
return 'super()';
|
|
1600
1740
|
return ctx.symbolMap[node.name] ?? node.name;
|
|
1601
1741
|
}
|
|
1602
1742
|
case 'member':
|
|
@@ -1621,14 +1761,44 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1621
1761
|
}
|
|
1622
1762
|
case 'await':
|
|
1623
1763
|
return `await ${emitPyExprCtx(node.argument, ctx)}`;
|
|
1624
|
-
case 'new':
|
|
1625
|
-
|
|
1764
|
+
case 'new': {
|
|
1765
|
+
// Host Error mapping (spec §1): `new Error(args)` → `Exception(args)` on
|
|
1766
|
+
// Python, since `raise Error(...)` / `isinstance(x, Error)` would
|
|
1767
|
+
// NameError (Python has no global `Error`). The mapping also covers
|
|
1768
|
+
// `let e = new Error(x)` and `throw new Error(x)` (both flow through the
|
|
1769
|
+
// `new` expression). `new TypeError(...)` is intentionally NOT mapped —
|
|
1770
|
+
// Python has a native `TypeError`, so it emits as-is.
|
|
1771
|
+
// TODO(kern): RangeError/SyntaxError/ReferenceError/EvalError/URIError
|
|
1772
|
+
// have NO same-named Python builtins and emit as-is → runtime NameError;
|
|
1773
|
+
// map or reject in the v2 builtin-errors slice. A user KERN class
|
|
1774
|
+
// literally named `Error` would be shadowed by this mapping —
|
|
1775
|
+
// registry-precedence hardening is also v2.
|
|
1776
|
+
const arg = node.argument;
|
|
1777
|
+
if (arg.kind === 'call' && arg.callee.kind === 'ident' && arg.callee.name === 'Error') {
|
|
1778
|
+
const remapped = { ...arg, callee: { ...arg.callee, name: 'Exception' } };
|
|
1779
|
+
return emitPyExprCtx(remapped, ctx);
|
|
1780
|
+
}
|
|
1781
|
+
// `new Error` WITHOUT parens is valid JS (≡ `new Error()`) and parses as
|
|
1782
|
+
// a bare ident argument — remap it too, else it emits a bare `Error`
|
|
1783
|
+
// NameError (agon review, claude/zai convergence).
|
|
1784
|
+
if (arg.kind === 'ident' && arg.name === 'Error') {
|
|
1785
|
+
return 'Exception()';
|
|
1786
|
+
}
|
|
1787
|
+
return emitPyExprCtx(arg, ctx);
|
|
1788
|
+
}
|
|
1626
1789
|
case 'typeAssert':
|
|
1627
1790
|
return emitPyExprCtx(node.expression, ctx);
|
|
1628
1791
|
case 'nonNull':
|
|
1629
1792
|
return emitPyExprCtx(node.expression, ctx);
|
|
1630
1793
|
case 'tmplLit': {
|
|
1631
|
-
// Lower TS template literals to Python f-strings.
|
|
1794
|
+
// Lower TS template literals to Python f-strings. In native bodies, wrap
|
|
1795
|
+
// each interpolation in _kern_fmt so JS value→string coercion semantics
|
|
1796
|
+
// (true→"true", null→"null", undefined→"undefined", 1.0→"1", arrays→
|
|
1797
|
+
// comma-joined, objects→"[object Object]") are preserved. The helper-less
|
|
1798
|
+
// Ground/React layer keeps the pre-slice raw f-string interpolation.
|
|
1799
|
+
const coerce = ctx.coerceJsValues;
|
|
1800
|
+
if (coerce)
|
|
1801
|
+
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
1632
1802
|
let out = 'f"';
|
|
1633
1803
|
for (let i = 0; i < node.quasis.length; i++) {
|
|
1634
1804
|
out += node.quasis[i]
|
|
@@ -1637,8 +1807,10 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1637
1807
|
.replace(/\n/g, '\\n')
|
|
1638
1808
|
.replace(/\{/g, '{{')
|
|
1639
1809
|
.replace(/\}/g, '}}');
|
|
1640
|
-
if (i < node.expressions.length)
|
|
1641
|
-
|
|
1810
|
+
if (i < node.expressions.length) {
|
|
1811
|
+
const inner = emitPyExprCtx(node.expressions[i], ctx);
|
|
1812
|
+
out += coerce ? `{_kern_fmt(${inner})}` : `{${inner}}`;
|
|
1813
|
+
}
|
|
1642
1814
|
}
|
|
1643
1815
|
out += '"';
|
|
1644
1816
|
return out;
|
|
@@ -1669,11 +1841,58 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1669
1841
|
if (node.op === 'instanceof') {
|
|
1670
1842
|
// JS `a instanceof B` → Python `isinstance(a, B)`. Emitting `instanceof`
|
|
1671
1843
|
// verbatim would be a Python *syntax* error, so this lowering is
|
|
1672
|
-
// mandatory (unlike raw host methods, which emit verbatim).
|
|
1673
|
-
//
|
|
1674
|
-
//
|
|
1675
|
-
//
|
|
1676
|
-
|
|
1844
|
+
// mandatory (unlike raw host methods, which emit verbatim).
|
|
1845
|
+
//
|
|
1846
|
+
// RHS handling (spec §2, shared table in core/instanceof-rhs.ts):
|
|
1847
|
+
// - accepted host global → mapped Python type: `Array`→`list`,
|
|
1848
|
+
// `Error`→`Exception` (so `e instanceof Error` ≡
|
|
1849
|
+
// `isinstance(e, Exception)`, mirroring `new Error(...)` →
|
|
1850
|
+
// `Exception(...)`; Python `except Exception as e` + this check ≡
|
|
1851
|
+
// JS catch + `e instanceof Error` for KERN-thrown errors).
|
|
1852
|
+
// - rejected RHS (wrapper-parity trap / unsupported builtin /
|
|
1853
|
+
// non-type-name) → THROW fail-closed. This is defense in depth:
|
|
1854
|
+
// the eligibility gate already rejects these bodies, so this throw
|
|
1855
|
+
// can only fire on directly-built IR, never on gate-passed source —
|
|
1856
|
+
// gate and lowerer share core/instanceof-rhs.ts and cannot drift.
|
|
1857
|
+
// - any other ident/member RHS → emit as-is (user classes work; an
|
|
1858
|
+
// unknown name fails loud at Python runtime with NameError —
|
|
1859
|
+
// registry-precedence hardening is v2).
|
|
1860
|
+
const rhs = node.right;
|
|
1861
|
+
if (rhs.kind === 'ident') {
|
|
1862
|
+
const rejectReason = instanceofRhsRejectReasonForName(rhs.name);
|
|
1863
|
+
if (rejectReason !== null) {
|
|
1864
|
+
throw new Error(`instanceof RHS '${rhs.name}' has no Python lowering (${rejectReason}). ` +
|
|
1865
|
+
'JS primitive-wrapper / unmapped-builtin instanceof has no isinstance parity; ' +
|
|
1866
|
+
'this body is ineligible for native KERN.');
|
|
1867
|
+
}
|
|
1868
|
+
const mapped = instanceofRhsPythonType(rhs.name);
|
|
1869
|
+
if (mapped !== null)
|
|
1870
|
+
return `isinstance(${left}, ${mapped})`;
|
|
1871
|
+
return `isinstance(${left}, ${right})`;
|
|
1872
|
+
}
|
|
1873
|
+
if (rhs.kind === 'member') {
|
|
1874
|
+
return `isinstance(${left}, ${right})`;
|
|
1875
|
+
}
|
|
1876
|
+
throw new Error('instanceof RHS is not a type name (instanceof-rhs-not-a-type-name); ' +
|
|
1877
|
+
'only a class identifier or qualified member name can lower to isinstance().');
|
|
1878
|
+
}
|
|
1879
|
+
if (node.op === '+' && ctx.coerceJsValues) {
|
|
1880
|
+
// JS `+` is overloaded: string concat if either operand is string-ish,
|
|
1881
|
+
// numeric addition otherwise. Python has no implicit coercion, so we
|
|
1882
|
+
// lower based on syntactic hints:
|
|
1883
|
+
// - If either operand is syntactically string-producing (strLit/tmplLit),
|
|
1884
|
+
// emit _kern_fmt(left) + _kern_fmt(right) for JS string concat.
|
|
1885
|
+
// - Otherwise (idents/calls/members/numbers — type unknown at emit time),
|
|
1886
|
+
// emit __kern_add(left, right) so numeric + stays additive and dynamic
|
|
1887
|
+
// string concat is coerced at runtime.
|
|
1888
|
+
// The helper-less Ground/React layer skips this and falls through to the
|
|
1889
|
+
// generic raw `+` path below (pre-slice behavior, zero regression).
|
|
1890
|
+
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
1891
|
+
const isStr = (n) => n.kind === 'strLit' || n.kind === 'tmplLit';
|
|
1892
|
+
if (isStr(node.left) || isStr(node.right)) {
|
|
1893
|
+
return `_kern_fmt(${left}) + _kern_fmt(${right})`;
|
|
1894
|
+
}
|
|
1895
|
+
return `__kern_add(${left}, ${right})`;
|
|
1677
1896
|
}
|
|
1678
1897
|
if (node.op === '??') {
|
|
1679
1898
|
// Slice 4c — nullish coalesce lowering. Two shapes:
|
|
@@ -1697,11 +1916,22 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1697
1916
|
// Slice 4c (post-buddy-review) was the easy-win expansion after the
|
|
1698
1917
|
// 22.7% empirical-gate scan; this lifts the slice-2 `??` throw and
|
|
1699
1918
|
// adds an estimated +7% to native eligibility on Agon-AI bodies.
|
|
1919
|
+
// Ground/React layer keeps the pre-slice None-only nullish test (no
|
|
1920
|
+
// sentinel, no helper). Native bodies also exclude the undefined
|
|
1921
|
+
// sentinel so `undefined ?? x` coalesces.
|
|
1922
|
+
if (!ctx.coerceJsValues) {
|
|
1923
|
+
if (isReceiverChainPure(node.left)) {
|
|
1924
|
+
return `(${left} if ${left} is not None else ${right})`;
|
|
1925
|
+
}
|
|
1926
|
+
const tmp = `__k_nc${++ctx.gensymCounter}`;
|
|
1927
|
+
return `(${tmp} if (${tmp} := ${left}) is not None else ${right})`;
|
|
1928
|
+
}
|
|
1929
|
+
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
1700
1930
|
if (isReceiverChainPure(node.left)) {
|
|
1701
|
-
return `(${left} if ${left} is not None else ${right})`;
|
|
1931
|
+
return `(${left} if (${left} is not None and ${left} is not _KERN_UNDEFINED) else ${right})`;
|
|
1702
1932
|
}
|
|
1703
1933
|
const tmp = `__k_nc${++ctx.gensymCounter}`;
|
|
1704
|
-
return `(${tmp} if (${tmp} := ${left}) is not None else ${right})`;
|
|
1934
|
+
return `(${tmp} if ((${tmp} := ${left}) is not None and ${tmp} is not _KERN_UNDEFINED) else ${right})`;
|
|
1705
1935
|
}
|
|
1706
1936
|
const forceLeft = needsComparisonChainParens(node.left, node.op);
|
|
1707
1937
|
const forceRight = needsComparisonChainParens(node.right, node.op);
|
|
@@ -1805,6 +2035,21 @@ function emitPyTypeof(argument, ctx) {
|
|
|
1805
2035
|
const value = emitPyExprCtx(argument, ctx);
|
|
1806
2036
|
const wrapped = needsArgParens(argument) ? `(${value})` : value;
|
|
1807
2037
|
const tmp = `__k_typeof${++ctx.gensymCounter}`;
|
|
2038
|
+
// Native bodies: a runtime value holding the undefined sentinel reports
|
|
2039
|
+
// "undefined" (JS `typeof undefined`), not "object". The walrus binds in the
|
|
2040
|
+
// first test so the sentinel branch is checked before the None branch. The
|
|
2041
|
+
// helper-less Ground layer never materializes the sentinel, so it keeps the
|
|
2042
|
+
// pre-slice None-first form.
|
|
2043
|
+
if (ctx.coerceJsValues) {
|
|
2044
|
+
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
2045
|
+
return (`("undefined" if (${tmp} := ${wrapped}) is _KERN_UNDEFINED ` +
|
|
2046
|
+
`else "object" if ${tmp} is None ` +
|
|
2047
|
+
`else "boolean" if isinstance(${tmp}, bool) ` +
|
|
2048
|
+
`else "number" if isinstance(${tmp}, (int, float)) ` +
|
|
2049
|
+
`else "string" if isinstance(${tmp}, str) ` +
|
|
2050
|
+
`else "function" if callable(${tmp}) ` +
|
|
2051
|
+
`else "object")`);
|
|
2052
|
+
}
|
|
1808
2053
|
return (`("object" if (${tmp} := ${wrapped}) is None ` +
|
|
1809
2054
|
`else "boolean" if isinstance(${tmp}, bool) ` +
|
|
1810
2055
|
`else "number" if isinstance(${tmp}, (int, float)) ` +
|
|
@@ -1814,6 +2059,9 @@ function emitPyTypeof(argument, ctx) {
|
|
|
1814
2059
|
}
|
|
1815
2060
|
function emitLambdaPy(node, ctx) {
|
|
1816
2061
|
const names = node.params.map((p) => p.name);
|
|
2062
|
+
if (node.bodyBlock) {
|
|
2063
|
+
return emitBlockClosurePy(node, names, ctx);
|
|
2064
|
+
}
|
|
1817
2065
|
const previous = new Set(ctx.shadowedSymbols);
|
|
1818
2066
|
for (const name of names)
|
|
1819
2067
|
ctx.shadowedSymbols.add(name);
|
|
@@ -1825,6 +2073,180 @@ function emitLambdaPy(node, ctx) {
|
|
|
1825
2073
|
ctx.shadowedSymbols = previous;
|
|
1826
2074
|
}
|
|
1827
2075
|
}
|
|
2076
|
+
/** Slices 0+1 — lower a block-bodied arrow (`x => { ... }`) to a hoisted local
|
|
2077
|
+
* Python `def`. Pushes `def __kern_closure_N(params): <body>` into
|
|
2078
|
+
* `ctx.pendingHoists` (flushed by emitChildrenPy immediately before the
|
|
2079
|
+
* enclosing statement) and RETURNS the def name as the expression string, so
|
|
2080
|
+
* `let scale = (x) => {...}` lowers to `scale = __kern_closure_0` with the def
|
|
2081
|
+
* hoisted above it.
|
|
2082
|
+
*
|
|
2083
|
+
* The closure body is lowered through `lowerJsClosureBodyToPython`, reusing
|
|
2084
|
+
* the class-path expression/condition callbacks:
|
|
2085
|
+
* - `lowerExpression(raw)` = `emitPyExprCtx(parseExpression(raw), ctx)` —
|
|
2086
|
+
* identical to every other native-body expression emit, so a captured
|
|
2087
|
+
* RENAMED outer variable resolves through `ctx` (the rename stack /
|
|
2088
|
+
* symbolMap) exactly as it does outside the closure.
|
|
2089
|
+
* - `lowerCondition(raw)` mirrors the class/native if-emitter, which lowers a
|
|
2090
|
+
* condition as the bare `emitPyExprCtx(parseExpression(cond), ctx)` (NO
|
|
2091
|
+
* js_truthy wrapper). Matching it EXACTLY means a condition inside a
|
|
2092
|
+
* closure lowers identically to the same condition outside one.
|
|
2093
|
+
*
|
|
2094
|
+
* Closure PARAMS shadow outer renames while the body is lowered (same
|
|
2095
|
+
* `shadowedSymbols` save/restore as the expression-lambda branch) — params
|
|
2096
|
+
* must NOT be renamed, while captures of renamed outer vars still resolve
|
|
2097
|
+
* through ctx. The v1 gate (commit A) guarantees the lowering succeeds;
|
|
2098
|
+
* gate/lowerer drift (`ok:false`) is a loud bug. */
|
|
2099
|
+
function emitBlockClosurePy(node, names, ctx) {
|
|
2100
|
+
const closureName = `__kern_closure_${ctx.closureSeq++}`;
|
|
2101
|
+
const previous = new Set(ctx.shadowedSymbols);
|
|
2102
|
+
for (const name of names)
|
|
2103
|
+
ctx.shadowedSymbols.add(name);
|
|
2104
|
+
try {
|
|
2105
|
+
const lowered = lowerJsClosureBodyToPython(node.bodyBlock.raw, {
|
|
2106
|
+
lowerExpression: (raw) => emitPyExprCtx(parseExpression(raw), ctx),
|
|
2107
|
+
// Mirror the native/class if-emitter EXACTLY (bare expression, no
|
|
2108
|
+
// js_truthy) so a condition inside the closure matches the same
|
|
2109
|
+
// condition outside it.
|
|
2110
|
+
lowerCondition: (raw) => emitPyExprCtx(parseExpression(raw), ctx),
|
|
2111
|
+
// The closure's own params are def-locals, never `nonlocal`: a write to a
|
|
2112
|
+
// param (`(x) => { x = x + 1 }`) must not be reported as a written FREE
|
|
2113
|
+
// name. The lowerer excludes both params and block-locals.
|
|
2114
|
+
paramNames: names,
|
|
2115
|
+
// Bare write TARGETS resolve through the SAME rename machinery reads use
|
|
2116
|
+
// — a write to a shadow-renamed capture must target the renamed binding
|
|
2117
|
+
// (`__k_shadow_x_N = …`), not the outer one (probe-verified silent
|
|
2118
|
+
// wrong-values without this). Params/block-locals are never renamed, so
|
|
2119
|
+
// the resolver is identity for them.
|
|
2120
|
+
lowerAssignTarget: (name) => resolveLocalRename(ctx, name),
|
|
2121
|
+
});
|
|
2122
|
+
if (!lowered.ok) {
|
|
2123
|
+
// The commit-A gate already accepted this block, so a lowering failure
|
|
2124
|
+
// here is gate/lowerer drift — surface it loudly.
|
|
2125
|
+
throw new Error(`Internal codegen error: block-arrow closure passed the v1 gate but failed to lower (${lowered.reason ?? 'unknown'}).`);
|
|
2126
|
+
}
|
|
2127
|
+
// Slice-2 loop-variable pinning. JS closures capture variables BY
|
|
2128
|
+
// REFERENCE; a binding created PER-ITERATION (an each/for loop var, or any
|
|
2129
|
+
// let/const declared inside a loop body) is re-bound each iteration, so
|
|
2130
|
+
// each closure sees its own iteration's value. A naive Python hoisted def
|
|
2131
|
+
// late-binds → every closure sees the LAST value (the classic 0,1,2 vs
|
|
2132
|
+
// 2,2,2 bug). FIX: pin such captures via a default arg
|
|
2133
|
+
// (`def __kern_closure_N(p, x=x):`) — Python evaluates defaults at def
|
|
2134
|
+
// time = the hoist point before the enclosing statement = exactly the
|
|
2135
|
+
// per-iteration snapshot JS produces.
|
|
2136
|
+
//
|
|
2137
|
+
// RULE: pin a captured name IFF its binding resolves at-or-inside the
|
|
2138
|
+
// OUTERMOST loop body enclosing the closure (scope index >=
|
|
2139
|
+
// loopScopeIndexes[0]). A binding declared OUTSIDE every loop (a function
|
|
2140
|
+
// param, an accumulator, a `while` condition var) resolves below that
|
|
2141
|
+
// index and stays late-bound — JS sees its CURRENT value at call time, and
|
|
2142
|
+
// Python late binding is already parity-correct for it. Over-pinning those
|
|
2143
|
+
// would WRONGLY freeze a value JS does not freeze.
|
|
2144
|
+
const pinParams = [];
|
|
2145
|
+
// The user-facing names that got PINNED (per-iteration loop captures). A
|
|
2146
|
+
// WRITTEN free name that is also pinned is unlowerable in v1 (the def-time
|
|
2147
|
+
// pin freezes the value; a `nonlocal` write would target the wrong binding)
|
|
2148
|
+
// — it throws `closure-pinned-write` below. Tracked here so the throw and
|
|
2149
|
+
// the disjointness assertion can consult it.
|
|
2150
|
+
const pinnedNames = new Set();
|
|
2151
|
+
if (ctx.loopScopeIndexes.length > 0) {
|
|
2152
|
+
const free = collectFreeIdentifierNames(node.bodyBlock.raw, names);
|
|
2153
|
+
const outermostLoopScope = ctx.loopScopeIndexes[0];
|
|
2154
|
+
// Alphabetical by user-facing name for deterministic emission order.
|
|
2155
|
+
for (const name of [...free].sort()) {
|
|
2156
|
+
const scopeIndex = findBindingScopeIndex(ctx, name);
|
|
2157
|
+
if (scopeIndex === null || scopeIndex < outermostLoopScope)
|
|
2158
|
+
continue;
|
|
2159
|
+
// Slice-2 fix (agon review, claude 0.7) — a pin FREEZES the value at
|
|
2160
|
+
// def time, but JS captures by reference: if the pinned binding is
|
|
2161
|
+
// REASSIGNED in a later sibling statement of any enclosing loop body,
|
|
2162
|
+
// the JS closure sees the mutation and the frozen default does not
|
|
2163
|
+
// (`let t = 0; fns.push(() => t); t = t + x` → JS [1,2], pinned
|
|
2164
|
+
// Python [0,0]). Fail closed instead of emitting silent divergence.
|
|
2165
|
+
// Assignments in a STRICTLY-LOWER top-level child (`< current`) run
|
|
2166
|
+
// before the closure and are captured by the pin — those are fine. The
|
|
2167
|
+
// `>=` (not `>`) rejects an assignment in the SAME top-level child as the
|
|
2168
|
+
// closure too: within-child statement order is not tracked (the whole
|
|
2169
|
+
// child shares one index), so a same-child closure+reassignment cannot be
|
|
2170
|
+
// proven safe — fail closed beats the silent divergence (kimi 0.85).
|
|
2171
|
+
for (const frame of ctx.loopLaterAssignFrames) {
|
|
2172
|
+
const lastAssign = frame.assignLast.get(name);
|
|
2173
|
+
if (lastAssign !== undefined && lastAssign >= frame.current) {
|
|
2174
|
+
throw new Error(`Closure captures loop-local '${name}' which is reassigned after the closure is created — ` +
|
|
2175
|
+
`the per-iteration pin would freeze a value JS does not freeze. ` +
|
|
2176
|
+
`Bind the final value to a fresh const before creating the closure (v1 limitation).`);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// Resolve to the SAME Python name the body emission uses. A captured
|
|
2180
|
+
// loop-body binding goes through the block-scope rename stack
|
|
2181
|
+
// (`resolveLocalRename`) — identical to the `ident` emit path — so a
|
|
2182
|
+
// shadow-renamed inner `x` pins as `__k_shadow_x_N=__k_shadow_x_N`.
|
|
2183
|
+
// (symbolMap is param-only and never names a loop-body-scoped binding,
|
|
2184
|
+
// so it is intentionally not consulted here.)
|
|
2185
|
+
const renamed = resolveLocalRename(ctx, name);
|
|
2186
|
+
// Defensive (agon review, claude 0.3 nit): a renamed pin equal to a
|
|
2187
|
+
// closure param would emit `def f(p, p=p)` — a Python SyntaxError.
|
|
2188
|
+
// Renames are __k_shadow_*/gensym forms, so this is theoretical; fail
|
|
2189
|
+
// loud rather than emit invalid Python if it ever happens.
|
|
2190
|
+
if (names.includes(renamed)) {
|
|
2191
|
+
throw new Error(`Closure parameter '${renamed}' collides with the rename of captured '${name}' — rename the parameter.`);
|
|
2192
|
+
}
|
|
2193
|
+
pinParams.push(`${renamed}=${renamed}`);
|
|
2194
|
+
pinnedNames.add(name);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
// Mutation v1 — free-variable WRITES (`nonlocal`). A bare write to a free
|
|
2198
|
+
// capture (one that is neither a closure param nor a block-local — the
|
|
2199
|
+
// lowerer already excluded those) needs a `nonlocal` declaration so Python
|
|
2200
|
+
// rebinds the OUTER binding instead of creating a def-local shadow (without
|
|
2201
|
+
// it: `UnboundLocalError` for read+write, or a silent dead local for
|
|
2202
|
+
// write-only). Member/index writes never appear in `writtenFreeNames` —
|
|
2203
|
+
// they mutate a captured object by reference and need no `nonlocal`.
|
|
2204
|
+
//
|
|
2205
|
+
// The eligibility≢lowerability gap (documented on the gate): a write to a
|
|
2206
|
+
// PINNED per-iteration capture cannot be lowered. The def-time default-arg
|
|
2207
|
+
// pin freezes the value, and a `nonlocal` on that name would rebind the
|
|
2208
|
+
// enclosing loop-body binding — neither matches JS's per-iteration
|
|
2209
|
+
// by-reference capture. The gate cannot see the enclosing loop from a
|
|
2210
|
+
// single statement, so we fail closed LOUDLY here.
|
|
2211
|
+
const nonlocalNames = [];
|
|
2212
|
+
for (const name of [...lowered.writtenFreeNames].sort()) {
|
|
2213
|
+
if (pinnedNames.has(name)) {
|
|
2214
|
+
throw new Error(`Closure writes to per-iteration loop capture '${name}', which v1 cannot lower: ` +
|
|
2215
|
+
`mutation of a per-iteration loop capture needs cell-boxing (v2). ` +
|
|
2216
|
+
`Bind to an outer variable or restructure.`);
|
|
2217
|
+
}
|
|
2218
|
+
nonlocalNames.push(name);
|
|
2219
|
+
}
|
|
2220
|
+
// Council's riskiest-thing defensive assertion: a name cannot be both
|
|
2221
|
+
// pinned (a per-iteration loop capture → default-arg) AND nonlocal (an
|
|
2222
|
+
// outer free write). By construction they are disjoint — pinning requires
|
|
2223
|
+
// the binding to resolve AT-OR-INSIDE the outermost loop, while a
|
|
2224
|
+
// nonlocal-written name is precisely one that did NOT (the pinned case
|
|
2225
|
+
// throws above). If this ever fires, the pin condition and the written-free
|
|
2226
|
+
// classification have drifted — a DESIGN error, not a user error.
|
|
2227
|
+
for (const name of nonlocalNames) {
|
|
2228
|
+
if (pinnedNames.has(name)) {
|
|
2229
|
+
throw new Error(`Internal codegen invariant violated: '${name}' is both pinned and nonlocal in closure ${closureName}.`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
const params = [...names, ...pinParams].join(', ');
|
|
2233
|
+
// `lowered.lines` are body lines at 4-space indent (the lowerer's own
|
|
2234
|
+
// convention); they nest directly under the `def` header. A `nonlocal` line
|
|
2235
|
+
// (if any) is the def's FIRST body statement (Python requires it before any
|
|
2236
|
+
// use of the name). The declared names are RENAME-RESOLVED — the body's
|
|
2237
|
+
// write targets went through `lowerAssignTarget` (same resolver), so the
|
|
2238
|
+
// nonlocal declaration, the writes, and the enclosing binding all agree on
|
|
2239
|
+
// the renamed Python name for shadow-renamed captures.
|
|
2240
|
+
const nonlocalLine = nonlocalNames.length > 0
|
|
2241
|
+
? [` nonlocal ${nonlocalNames.map((n) => resolveLocalRename(ctx, n)).join(', ')}`]
|
|
2242
|
+
: [];
|
|
2243
|
+
ctx.pendingHoists.push([`def ${closureName}(${params}):`, ...nonlocalLine, ...lowered.lines]);
|
|
2244
|
+
return closureName;
|
|
2245
|
+
}
|
|
2246
|
+
finally {
|
|
2247
|
+
ctx.shadowedSymbols = previous;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
1828
2250
|
function valueReferencesIdent(node, name) {
|
|
1829
2251
|
switch (node.kind) {
|
|
1830
2252
|
case 'ident':
|
|
@@ -1864,16 +2286,26 @@ function valueReferencesIdent(node, name) {
|
|
|
1864
2286
|
case 'lambda':
|
|
1865
2287
|
if (node.params.some((p) => p.name === name))
|
|
1866
2288
|
return false;
|
|
2289
|
+
if (node.bodyBlock)
|
|
2290
|
+
return rawBlockReferencesIdent(node.bodyBlock.raw, name);
|
|
1867
2291
|
return valueReferencesIdent(node.body, name);
|
|
1868
2292
|
default:
|
|
1869
2293
|
return false;
|
|
1870
2294
|
}
|
|
1871
2295
|
}
|
|
2296
|
+
/** Conservative word-boundary check for an identifier inside a raw closure
|
|
2297
|
+
* block. Used when a block-bodied arrow (slices 0+1) has no expression `body`
|
|
2298
|
+
* to recurse. Over-matching is safe for the capture analyses that consume it. */
|
|
2299
|
+
function rawBlockReferencesIdent(raw, name) {
|
|
2300
|
+
return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(raw);
|
|
2301
|
+
}
|
|
1872
2302
|
function containsLambdaCapturingIdent(node, name) {
|
|
1873
2303
|
switch (node.kind) {
|
|
1874
2304
|
case 'lambda':
|
|
1875
2305
|
if (node.params.some((p) => p.name === name))
|
|
1876
2306
|
return false;
|
|
2307
|
+
if (node.bodyBlock)
|
|
2308
|
+
return rawBlockReferencesIdent(node.bodyBlock.raw, name);
|
|
1877
2309
|
return valueReferencesIdent(node.body, name);
|
|
1878
2310
|
case 'member':
|
|
1879
2311
|
return containsLambdaCapturingIdent(node.object, name);
|
|
@@ -1917,6 +2349,16 @@ function lowerChain(node, ctx) {
|
|
|
1917
2349
|
const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
|
|
1918
2350
|
? lowerChain(obj, ctx)
|
|
1919
2351
|
: { guard: null, expr: emitPyExprCtx(obj, ctx) };
|
|
2352
|
+
// Portable Array *property* read (non-call `.length`) lowers through the
|
|
2353
|
+
// SAME shared list-ops hook the route emitter uses, so `this.items.length`
|
|
2354
|
+
// emits `len(self.items)` (not invalid `self.items.length`) — identical to
|
|
2355
|
+
// a route handler's `arr.length` by construction. Only the trailing `.prop`
|
|
2356
|
+
// link is rewritten; the accumulated optional-chain guard is left UNTOUCHED
|
|
2357
|
+
// and still flows through `wrapGuardIfAny`, so `items?.length` stays
|
|
2358
|
+
// `(len(items) if items is not None else None)`-shaped.
|
|
2359
|
+
const linkExpr = isSharedPortableArrayProperty(node.property)
|
|
2360
|
+
? (lowerPortableArrayPropertyPy(inner.expr, node.property) ?? `${inner.expr}.${node.property}`)
|
|
2361
|
+
: `${inner.expr}.${node.property}`;
|
|
1920
2362
|
if (node.optional) {
|
|
1921
2363
|
// The receiver expression names what we need to test. The expr names
|
|
1922
2364
|
// the receiver twice (once in test, once in branch); reject when that
|
|
@@ -1926,9 +2368,9 @@ function lowerChain(node, ctx) {
|
|
|
1926
2368
|
'Bind the call/await result to a `let` first, then use `let.field?.next` on the bound name.');
|
|
1927
2369
|
}
|
|
1928
2370
|
const newGuard = inner.guard === null ? `${inner.expr} is not None` : `${inner.guard} and ${inner.expr} is not None`;
|
|
1929
|
-
return { guard: newGuard, expr:
|
|
2371
|
+
return { guard: newGuard, expr: linkExpr };
|
|
1930
2372
|
}
|
|
1931
|
-
return { guard: inner.guard, expr:
|
|
2373
|
+
return { guard: inner.guard, expr: linkExpr };
|
|
1932
2374
|
}
|
|
1933
2375
|
if (node.kind === 'index') {
|
|
1934
2376
|
const obj = node.object;
|
|
@@ -1964,6 +2406,23 @@ function lowerChain(node, ctx) {
|
|
|
1964
2406
|
const stdlib = applyStdlibLoweringPython(node, ctx);
|
|
1965
2407
|
if (stdlib !== null)
|
|
1966
2408
|
return { guard: null, expr: stdlib };
|
|
2409
|
+
// Lambda-bearing array methods (`map`/`filter`/`some`/`every`) lower to a
|
|
2410
|
+
// call-by-name comprehension. Peeked BEFORE the portable-array shim because
|
|
2411
|
+
// none of these four are in that shim's set; gating here keeps the two paths
|
|
2412
|
+
// independent and lets a non-matching shape fall through unchanged.
|
|
2413
|
+
const lambdaArray = lowerLambdaArrayCallPython(node, ctx);
|
|
2414
|
+
if (lambdaArray !== null)
|
|
2415
|
+
return { guard: null, expr: lambdaArray };
|
|
2416
|
+
// Portable array methods (e.g. `arr.push(x)`) lower through the SAME shared
|
|
2417
|
+
// helper the route emitter uses, so a class method's `this.items.push(x)`
|
|
2418
|
+
// matches a route handler's `arr.push(x)` by construction (no per-path drift).
|
|
2419
|
+
const portableArray = lowerPortableArrayCallPython(node, ctx);
|
|
2420
|
+
if (portableArray !== null)
|
|
2421
|
+
return { guard: null, expr: portableArray };
|
|
2422
|
+
if (ctx.inConstructor && node.callee.kind === 'ident' && node.callee.name === 'super') {
|
|
2423
|
+
const superArgs = node.args.map((arg) => emitPyExprCtx(arg, ctx)).join(', ');
|
|
2424
|
+
return { guard: null, expr: `super().__init__(${superArgs})` };
|
|
2425
|
+
}
|
|
1967
2426
|
if (node.callee.kind === 'ident' && node.callee.name === 'String') {
|
|
1968
2427
|
if (node.args.length !== 1) {
|
|
1969
2428
|
throw new Error('String() portable coercion expects exactly one argument on Python target.');
|
|
@@ -1974,6 +2433,14 @@ function lowerChain(node, ctx) {
|
|
|
1974
2433
|
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
1975
2434
|
return { guard: null, expr: `_kern_fmt(${arg})` };
|
|
1976
2435
|
}
|
|
2436
|
+
// Host Error mapping, call-without-new form: JS `Error("x")` (no `new`)
|
|
2437
|
+
// constructs an error too — remap like `new Error(...)`, else it emits a
|
|
2438
|
+
// bare `Error(...)` NameError on Python (agon review, kimi 0.7). The same
|
|
2439
|
+
// documented user-class-named-Error shadowing edge applies (v2 hardening).
|
|
2440
|
+
if (node.callee.kind === 'ident' && node.callee.name === 'Error') {
|
|
2441
|
+
const errArgs = node.args.map((arg) => emitPyExprCtx(arg, ctx)).join(', ');
|
|
2442
|
+
return { guard: null, expr: `Exception(${errArgs})` };
|
|
2443
|
+
}
|
|
1977
2444
|
const callee = node.callee;
|
|
1978
2445
|
const inner = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index'
|
|
1979
2446
|
? lowerChain(callee, ctx)
|
|
@@ -1981,6 +2448,393 @@ function lowerChain(node, ctx) {
|
|
|
1981
2448
|
const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', ');
|
|
1982
2449
|
return { guard: inner.guard, expr: `${inner.expr}(${args})` };
|
|
1983
2450
|
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Lower a portable Array *method call* (e.g. `arr.push(x)`) through the shared
|
|
2453
|
+
* `list-ops` module, so a class-method body and a route handler lower the same
|
|
2454
|
+
* portable subset to identical Python. Returns `null` — and the caller falls
|
|
2455
|
+
* through to the generic call emission — for anything that is not a bare,
|
|
2456
|
+
* non-optional member call of a shared portable method on a guard-free
|
|
2457
|
+
* receiver. Mirrors the peek-then-emit shape of `lowerRegexCallPython`.
|
|
2458
|
+
*/
|
|
2459
|
+
function lowerPortableArrayCallPython(call, ctx) {
|
|
2460
|
+
const callee = call.callee;
|
|
2461
|
+
if (callee.kind !== 'member' || callee.optional)
|
|
2462
|
+
return null;
|
|
2463
|
+
// Gate on method name BEFORE emitting receiver/args, so a non-shared call
|
|
2464
|
+
// falls through without any duplicated emission. Arity is NOT gated here —
|
|
2465
|
+
// the shared `lowerPortableArrayMethodPy` validates the arg count per method
|
|
2466
|
+
// (push/concat are single-arg, slice takes 0/1/2) and returns null for shapes
|
|
2467
|
+
// it can't lower, so a malformed call falls through unchanged. A blanket
|
|
2468
|
+
// `args.length !== 1` guard here would have wrongly blocked `slice()` /
|
|
2469
|
+
// `slice(1, 3)` (a push-shaped assumption — see spec 3a).
|
|
2470
|
+
if (!isSharedPortableArrayMethod(callee.property))
|
|
2471
|
+
return null;
|
|
2472
|
+
const recvNode = callee.object;
|
|
2473
|
+
// Per-method purity contract (scalar-method sweep). Multi-eval / mutating
|
|
2474
|
+
// methods (push/reverse/at/lastIndexOf) name the receiver more than once
|
|
2475
|
+
// (`(recv.append(x) or len(recv))`, `(recv.reverse() or recv)`), so a
|
|
2476
|
+
// side-effectful receiver — `makeBag().items.push(x)`, `bags[idx()].reverse()`
|
|
2477
|
+
// — would run those effects twice on Python and break JS parity; lower only a
|
|
2478
|
+
// provably-pure receiver for those. Single-eval methods
|
|
2479
|
+
// (slice/includes/indexOf/join/flat/concat/fill) name the receiver once, so they
|
|
2480
|
+
// accept an impure receiver — the old blanket `isReceiverChainPure` guard
|
|
2481
|
+
// wrongly skipped `makeBox().items.slice(1)` (the prior agon-review 0.97
|
|
2482
|
+
// finding). The optional-chain guard below still applies to ALL methods.
|
|
2483
|
+
if (sharedPortableMethodRequiresPureReceiver(callee.property) && !isReceiverChainPure(recvNode))
|
|
2484
|
+
return null;
|
|
2485
|
+
const recv = recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
|
|
2486
|
+
? lowerChain(recvNode, ctx)
|
|
2487
|
+
: { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
|
|
2488
|
+
// A pure receiver can still be an optional chain (`a?.b`), which carries a
|
|
2489
|
+
// None-guard the flat shim can't honor — fall through for those too.
|
|
2490
|
+
if (recv.guard !== null)
|
|
2491
|
+
return null;
|
|
2492
|
+
const args = call.args.map((a) => (callee.property === 'fill' ? emitPyArrayFillArg(a, ctx) : emitPyExprCtx(a, ctx)));
|
|
2493
|
+
const lowered = lowerPortableArrayMethodPy(recv.expr, callee.property, args);
|
|
2494
|
+
if (lowered !== null && callee.property === 'fill') {
|
|
2495
|
+
ctx.helpers.add(KERN_JS_ARRAY_HELPERS_PY);
|
|
2496
|
+
}
|
|
2497
|
+
return lowered;
|
|
2498
|
+
}
|
|
2499
|
+
function emitPyArrayFillArg(node, ctx) {
|
|
2500
|
+
if (node.kind === 'undefLit')
|
|
2501
|
+
return '_KERN_UNDEFINED';
|
|
2502
|
+
return emitPyExprCtx(node, ctx);
|
|
2503
|
+
}
|
|
2504
|
+
/** Methods this peek lowers to a call-by-name comprehension. `reduce`/
|
|
2505
|
+
* `reduceRight` share the call-by-name callback resolution but lower to
|
|
2506
|
+
* `functools.reduce` (a different arg-count/arity contract), so they live in
|
|
2507
|
+
* `REDUCE_ARRAY_METHODS` and route through `lowerReduceArrayCallPython`.
|
|
2508
|
+
* `sort`-with-comparator remains DEFERRED (see slice report). */
|
|
2509
|
+
const LAMBDA_ARRAY_METHODS = new Set([
|
|
2510
|
+
'map',
|
|
2511
|
+
'filter',
|
|
2512
|
+
'some',
|
|
2513
|
+
'every',
|
|
2514
|
+
// find-family + flatMap: same call-by-name comprehension architecture as
|
|
2515
|
+
// map/filter (callback 1 or 2 params; the 2-param form binds the index via
|
|
2516
|
+
// enumerate). The find-family wraps the predicate in `js_truthy`.
|
|
2517
|
+
'find',
|
|
2518
|
+
'findIndex',
|
|
2519
|
+
'findLast',
|
|
2520
|
+
'findLastIndex',
|
|
2521
|
+
'flatMap',
|
|
2522
|
+
]);
|
|
2523
|
+
/** reduce/reduceRight take a callback of EXACTLY 2 params (acc, cur) and the
|
|
2524
|
+
* CALL itself carries 1 arg (cb) or 2 (cb, seed) — a different arg-count and
|
|
2525
|
+
* callback-arity contract than the enumerate-comprehension methods above, so
|
|
2526
|
+
* they are dispatched on their own path inside `lowerLambdaArrayCallPython`. */
|
|
2527
|
+
const REDUCE_ARRAY_METHODS = new Set(['reduce', 'reduceRight']);
|
|
2528
|
+
/**
|
|
2529
|
+
* Lower a lambda-bearing Array method call (`recv.map(cb)` / `.filter` / `.some`
|
|
2530
|
+
* / `.every`) on the class/native-body Python path to a call-by-name
|
|
2531
|
+
* comprehension. Returns `null` — and the caller falls through to the generic
|
|
2532
|
+
* call emission — for any shape this peek does not own. Peeked at the same
|
|
2533
|
+
* dispatch point as `lowerPortableArrayCallPython` (these four methods are NOT
|
|
2534
|
+
* in that shim's set).
|
|
2535
|
+
*
|
|
2536
|
+
* GATE (all required): a non-optional `member` callee, method ∈
|
|
2537
|
+
* {map,filter,some,every}, exactly one call arg, and that arg is a `lambda`
|
|
2538
|
+
* (expression- or block-bodied) OR a bare `ident`.
|
|
2539
|
+
*
|
|
2540
|
+
* Callback shapes resolved to a Python NAME `cb`:
|
|
2541
|
+
* - block lambda → `emitPyExprCtx` returns the hoisted `def __kern_closure_N`
|
|
2542
|
+
* name (closures v1), and the def is pushed into
|
|
2543
|
+
* `ctx.pendingHoists` (flushed by emitChildrenPy before the
|
|
2544
|
+
* enclosing statement).
|
|
2545
|
+
* - expr lambda → emit `lambda x: <expr>`, hoist ONE assignment
|
|
2546
|
+
* `__kern_cb_N = <lambda>` into the SAME `ctx.pendingHoists`
|
|
2547
|
+
* buffer; `cb` = `__kern_cb_N`.
|
|
2548
|
+
* - bare ident → the rename-resolved identifier as-is. Arity is unknown for
|
|
2549
|
+
* a named callback, so it is always called single-arg. JS
|
|
2550
|
+
* would pass `(el, idx, arr)`; a named callback that reads a
|
|
2551
|
+
* 2nd/3rd param diverges — an ACCEPTED edge for this slice.
|
|
2552
|
+
*
|
|
2553
|
+
* MEMBER-EXPRESSION callbacks (`this.items.map(this.fmt)`) FALL THROUGH verbatim
|
|
2554
|
+
* (the arg is a `member`, not `lambda`/`ident`, so the gate rejects it). This is
|
|
2555
|
+
* deliberate: JS `.map(this.fmt)` passes the method UNBOUND (`this` is undefined
|
|
2556
|
+
* inside it), so the TS target is already broken for such code. Lowering it on
|
|
2557
|
+
* Python would create works-on-Python / breaks-on-TS anti-parity; until KERN
|
|
2558
|
+
* defines a bound-method story there is nothing to be parity-correct WITH.
|
|
2559
|
+
*
|
|
2560
|
+
* TRUTHINESS: filter/some/every wrap the predicate result in `js_truthy(...)`.
|
|
2561
|
+
* This is JS-CORRECT — JS keeps `[]`/`{}` truthy while bare Python drops them.
|
|
2562
|
+
* Two VERIFIED pre-existing divergences are intentionally NOT fixed here:
|
|
2563
|
+
* (1) the ROUTE path's filter/find-family predicates are BARE (`if ${body}`,
|
|
2564
|
+
* core/expr/index.ts ~:294) — a pre-existing route truthiness bug, a
|
|
2565
|
+
* follow-up for the deferral-sweep slice;
|
|
2566
|
+
* (2) the class path's `if cond=` lowering is bare (no js_truthy wrapper) — a
|
|
2567
|
+
* separate follow-up.
|
|
2568
|
+
* `js_truthy` here matches JS semantics for the lambda-array predicates only.
|
|
2569
|
+
*/
|
|
2570
|
+
function lowerLambdaArrayCallPython(call, ctx) {
|
|
2571
|
+
// SCOPE GATE (do not remove). This lowering is for the class/native-body
|
|
2572
|
+
// statement path (`emitNativeKernBodyPythonWithImports`, standaloneExpression
|
|
2573
|
+
// = false). The standalone-expression entry point (`emitPyExpression`) is the
|
|
2574
|
+
// Ground/React declarative + expression-unit surface; it emits array methods
|
|
2575
|
+
// VERBATIM (e.g. `values.filter(lambda value: ...)`) by design, so do NOT
|
|
2576
|
+
// intercept there. The FastAPI ROUTE path lowers `.map`/`.filter` through a
|
|
2577
|
+
// SEPARATE string-rewrite (`core/expr` `rewriteExpr`/`lowerJsArrayMethods`),
|
|
2578
|
+
// never this IR `lowerChain` peek, so routes are untouched.
|
|
2579
|
+
if (ctx.standaloneExpression)
|
|
2580
|
+
return null;
|
|
2581
|
+
const callee = call.callee;
|
|
2582
|
+
if (callee.kind !== 'member' || callee.optional)
|
|
2583
|
+
return null;
|
|
2584
|
+
const method = callee.property;
|
|
2585
|
+
const isReduce = REDUCE_ARRAY_METHODS.has(method);
|
|
2586
|
+
if (!LAMBDA_ARRAY_METHODS.has(method) && !isReduce)
|
|
2587
|
+
return null;
|
|
2588
|
+
// reduce/reduceRight have a DISTINCT contract (callback EXACTLY 2 params; the
|
|
2589
|
+
// call carries 1 arg [cb] or 2 [cb, seed]) and lower to `functools.reduce`,
|
|
2590
|
+
// not a comprehension. Handle them on their own path before the shared
|
|
2591
|
+
// enumerate-comprehension machinery below. `callee.object` is the receiver.
|
|
2592
|
+
if (isReduce)
|
|
2593
|
+
return lowerReduceArrayCallPython(call, callee.object, method, ctx);
|
|
2594
|
+
if (call.args.length !== 1)
|
|
2595
|
+
return null;
|
|
2596
|
+
const arg = call.args[0];
|
|
2597
|
+
if (arg.kind !== 'lambda' && arg.kind !== 'ident')
|
|
2598
|
+
return null;
|
|
2599
|
+
// Resolve the callback to a NAME. For a lambda the arg arity decides the
|
|
2600
|
+
// comprehension form (enumerate when 2 params); a bare ident is arity-unknown
|
|
2601
|
+
// and always called single-arg (documented divergence above).
|
|
2602
|
+
let cb;
|
|
2603
|
+
let twoArity = false;
|
|
2604
|
+
if (arg.kind === 'lambda') {
|
|
2605
|
+
// The enumerate comprehension only supplies (el, i); a callback declaring a
|
|
2606
|
+
// 3rd param (`(el, i, arr) => …`) would be DEFINED with 3 params but CALLED
|
|
2607
|
+
// with 2 → runtime TypeError. Fall through verbatim (the pre-slice status quo
|
|
2608
|
+
// for that shape) rather than emit a broken lowering.
|
|
2609
|
+
if (arg.params.length > 2)
|
|
2610
|
+
return null;
|
|
2611
|
+
twoArity = arg.params.length >= 2;
|
|
2612
|
+
const emitted = emitLambdaPy(arg, ctx);
|
|
2613
|
+
if (arg.bodyBlock) {
|
|
2614
|
+
// Block lambda → `emitLambdaPy` already pushed the hoisted def and
|
|
2615
|
+
// returned its bare name; use it directly as the callback name.
|
|
2616
|
+
cb = emitted;
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
// Expression lambda → `emitted` is a `lambda x: <expr>`. Hoist ONE
|
|
2620
|
+
// assignment into the SAME buffer as closure defs (flushed before the
|
|
2621
|
+
// enclosing statement) and call it by name, so the comprehension names
|
|
2622
|
+
// the callback exactly once.
|
|
2623
|
+
cb = `__kern_cb_${ctx.closureSeq++}`;
|
|
2624
|
+
ctx.pendingHoists.push([`${cb} = ${emitted}`]);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
else {
|
|
2628
|
+
// Bare LOCAL ident callback (`let f = …; recv.map(f)`). Emit through the
|
|
2629
|
+
// ident path so a renamed binding resolves to its Python name.
|
|
2630
|
+
cb = emitPyExprCtx(arg, ctx);
|
|
2631
|
+
}
|
|
2632
|
+
// Receiver: lowered ONCE. Every template below names `recv` EXACTLY ONCE, so
|
|
2633
|
+
// there is NO purity gate here (deliberate contrast with the multi-eval
|
|
2634
|
+
// list-ops methods, which DO gate — see `lowerPortableArrayCallPython`).
|
|
2635
|
+
// FUTURE READER: do not add a redundant `isReceiverChainPure` gate; the
|
|
2636
|
+
// single-eval property is what makes M6 (`this.bump().map(...)` runs bump()
|
|
2637
|
+
// exactly once) correct.
|
|
2638
|
+
const recvNode = callee.object;
|
|
2639
|
+
const recv = recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
|
|
2640
|
+
? lowerChain(recvNode, ctx)
|
|
2641
|
+
: { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
|
|
2642
|
+
// An optional-chain receiver (`a?.b`) carries a None-guard the comprehension
|
|
2643
|
+
// can't honor — fall through unchanged for those.
|
|
2644
|
+
if (recv.guard !== null)
|
|
2645
|
+
return null;
|
|
2646
|
+
// A compound receiver (a ternary `xs if c else ys`, a binary expression)
|
|
2647
|
+
// dropped bare into a generator head parses WRONG: Python reads the `if`
|
|
2648
|
+
// as the comprehension filter (`for el in xs if c else ys` → SyntaxError) —
|
|
2649
|
+
// agon review, agy 1.0. Call-arg positions (`enumerate(recv)`) are immune;
|
|
2650
|
+
// the bare `for … in recv` heads and `recv[::-1]` are not. Space-heuristic
|
|
2651
|
+
// parens: atoms (`self.items`, `makeBox().items`, `[1, 2]`?) — note a
|
|
2652
|
+
// bracketed literal contains ', ' but leading `[`/`(` already self-delimits;
|
|
2653
|
+
// anything else with top-level spaces gets wrapped. Parens are never wrong,
|
|
2654
|
+
// the heuristic only preserves byte-identical output for the common shapes.
|
|
2655
|
+
const recvExpr = parenthesizeIterable(recv.expr);
|
|
2656
|
+
// Hygienic loop vars, fresh per call.
|
|
2657
|
+
const seq = ctx.gensymCounter++;
|
|
2658
|
+
const el = `__kern_el_${seq}`;
|
|
2659
|
+
const ix = `__kern_ix_${seq}`;
|
|
2660
|
+
const callCb = twoArity ? `${cb}(${el}, ${ix})` : `${cb}(${el})`;
|
|
2661
|
+
const head = twoArity ? `for ${ix}, ${el} in enumerate(${recvExpr})` : `for ${el} in ${recvExpr}`;
|
|
2662
|
+
if (method === 'map') {
|
|
2663
|
+
return `[${callCb} ${head}]`;
|
|
2664
|
+
}
|
|
2665
|
+
if (method === 'flatMap') {
|
|
2666
|
+
// map, then flatten ONE level — JS flatMap only flattens arrays, so a
|
|
2667
|
+
// scalar/string callback result is appended as a single element. The
|
|
2668
|
+
// callback is bound to a fresh `__kern_r_N` via a one-element `for`, so it
|
|
2669
|
+
// is called EXACTLY ONCE per element. This is deliberately BETTER than the
|
|
2670
|
+
// FastAPI route's body-substitution lowering (core/expr/index.ts ~:336),
|
|
2671
|
+
// which textually re-emits the callback body twice (`__x` substituted in
|
|
2672
|
+
// both the isinstance test and the fallback) and double-evaluates a
|
|
2673
|
+
// side-effecting callback — a tracked route follow-up. flatMap has no
|
|
2674
|
+
// predicate, so NO js_truthy here.
|
|
2675
|
+
const r = `__kern_r_${seq}`;
|
|
2676
|
+
const y = `__kern_y_${seq}`;
|
|
2677
|
+
return `[${y} ${head} for ${r} in [${callCb}] for ${y} in (${r} if isinstance(${r}, list) else [${r}])]`;
|
|
2678
|
+
}
|
|
2679
|
+
// filter + find-family predicates wrap in `js_truthy` (JS-correct; map/flatMap
|
|
2680
|
+
// do not need it). The helper lands once via the helper Set.
|
|
2681
|
+
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
2682
|
+
if (method === 'filter') {
|
|
2683
|
+
return `[${el} ${head} if js_truthy(${callCb})]`;
|
|
2684
|
+
}
|
|
2685
|
+
if (method === 'some') {
|
|
2686
|
+
return `any(js_truthy(${callCb}) ${head})`;
|
|
2687
|
+
}
|
|
2688
|
+
if (method === 'every') {
|
|
2689
|
+
return `all(js_truthy(${callCb}) ${head})`;
|
|
2690
|
+
}
|
|
2691
|
+
// find-family — `next((<gen>), <miss>)` never raises. find/findLast yield the
|
|
2692
|
+
// ELEMENT (miss → None); findIndex/findLastIndex yield the INDEX (miss → -1).
|
|
2693
|
+
// The *Last variants scan a reversed view; with a 2-param callback the index
|
|
2694
|
+
// must come from enumerate, so they reverse `list(enumerate(recv))` (mirrors
|
|
2695
|
+
// the route's findLast/findLastIndex shape).
|
|
2696
|
+
if (method === 'find') {
|
|
2697
|
+
return `next((${el} ${head} if js_truthy(${callCb})), None)`;
|
|
2698
|
+
}
|
|
2699
|
+
if (method === 'findIndex') {
|
|
2700
|
+
// The index is always bound here, so iterate enumerate(recv) even for a
|
|
2701
|
+
// 1-param callback (which is called single-arg on the element).
|
|
2702
|
+
const enumHead = `for ${ix}, ${el} in enumerate(${recvExpr})`;
|
|
2703
|
+
return `next((${ix} ${enumHead} if js_truthy(${callCb})), -1)`;
|
|
2704
|
+
}
|
|
2705
|
+
if (method === 'findLast') {
|
|
2706
|
+
const revHead = twoArity
|
|
2707
|
+
? `for ${ix}, ${el} in reversed(list(enumerate(${recvExpr})))`
|
|
2708
|
+
: `for ${el} in reversed(${recvExpr})`;
|
|
2709
|
+
return `next((${el} ${revHead} if js_truthy(${callCb})), None)`;
|
|
2710
|
+
}
|
|
2711
|
+
// method === 'findLastIndex'
|
|
2712
|
+
const revIndexHead = `for ${ix}, ${el} in reversed(list(enumerate(${recvExpr})))`;
|
|
2713
|
+
return `next((${ix} ${revIndexHead} if js_truthy(${callCb})), -1)`;
|
|
2714
|
+
}
|
|
2715
|
+
/** Lower `recv.reduce(cb)` / `.reduce(cb, seed)` / `.reduceRight(...)` on the
|
|
2716
|
+
* class/native-body Python path to `functools.reduce`. Returns `null` (caller
|
|
2717
|
+
* falls through to verbatim emission) for any shape this peek does not own.
|
|
2718
|
+
*
|
|
2719
|
+
* CONTRACT (all required): a non-optional `member` callee already checked by
|
|
2720
|
+
* the caller; the call carries 1 arg (cb) or 2 (cb, seed); the callback is a
|
|
2721
|
+
* `lambda` with EXACTLY 2 params (acc, cur) OR a bare `ident` (arity unknown —
|
|
2722
|
+
* accepted, called with two args by functools.reduce). A 1- or 3+-param lambda
|
|
2723
|
+
* callback (e.g. an idx-reading `(a, c, i) =>`) FALLS THROUGH verbatim (status
|
|
2724
|
+
* quo) rather than emit a callback functools.reduce would call with the wrong
|
|
2725
|
+
* arity.
|
|
2726
|
+
*
|
|
2727
|
+
* JS `reduce((acc, cur) => …)` arg order == `functools.reduce(fn(acc, cur), …)`,
|
|
2728
|
+
* so NO adaptation is needed (asserted by the order-sensitive FR7 fixture).
|
|
2729
|
+
* reduceRight is the same callback over the reversed sequence (`recv[::-1]`).
|
|
2730
|
+
*
|
|
2731
|
+
* PARITY NOTE: JS `[].reduce(cb)` (no seed, empty array) throws TypeError;
|
|
2732
|
+
* Python `functools.reduce(cb, [])` raises TypeError too — same failure mode,
|
|
2733
|
+
* documented parity (not a divergence).
|
|
2734
|
+
*
|
|
2735
|
+
* IMPORT: `functools` is registered via `ctx.imports.add('functools')`, which
|
|
2736
|
+
* the fn/method generators render as `import functools as __k_functools` (the
|
|
2737
|
+
* same alias convention as `re` → `__k_re`); the template names
|
|
2738
|
+
* `__k_functools.reduce`. The Set dedupes, so the import lands exactly once per
|
|
2739
|
+
* body. (The FastAPI route path uses a SEPARATE imports channel that emits a
|
|
2740
|
+
* bare `import functools`; this class path is untouched by that.) */
|
|
2741
|
+
function lowerReduceArrayCallPython(call, recvNode, method, ctx) {
|
|
2742
|
+
if (call.args.length !== 1 && call.args.length !== 2)
|
|
2743
|
+
return null;
|
|
2744
|
+
const cbArg = call.args[0];
|
|
2745
|
+
if (cbArg.kind !== 'lambda' && cbArg.kind !== 'ident')
|
|
2746
|
+
return null;
|
|
2747
|
+
// Member-expression callbacks fall through verbatim (same unbound-this policy
|
|
2748
|
+
// as the comprehension methods) — the gate above already rejects them.
|
|
2749
|
+
let cb;
|
|
2750
|
+
if (cbArg.kind === 'lambda') {
|
|
2751
|
+
// EXACTLY 2 params (acc, cur). A 1-param or 3+-param (idx-reading) callback
|
|
2752
|
+
// would be mis-called by functools.reduce — fall through verbatim.
|
|
2753
|
+
if (cbArg.params.length !== 2)
|
|
2754
|
+
return null;
|
|
2755
|
+
const emitted = emitLambdaPy(cbArg, ctx);
|
|
2756
|
+
if (cbArg.bodyBlock) {
|
|
2757
|
+
// Block lambda → hoisted def name from emitLambdaPy; use it directly.
|
|
2758
|
+
cb = emitted;
|
|
2759
|
+
}
|
|
2760
|
+
else {
|
|
2761
|
+
// Expression lambda → hoist `__kern_cb_N = lambda a, c: <expr>` and pass
|
|
2762
|
+
// the name, so functools.reduce names the callback exactly once.
|
|
2763
|
+
cb = `__kern_cb_${ctx.closureSeq++}`;
|
|
2764
|
+
ctx.pendingHoists.push([`${cb} = ${emitted}`]);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
else {
|
|
2768
|
+
// Bare LOCAL ident callback — resolve through the ident path.
|
|
2769
|
+
cb = emitPyExprCtx(cbArg, ctx);
|
|
2770
|
+
}
|
|
2771
|
+
// Receiver: lowered ONCE (same single-eval property as the comprehension
|
|
2772
|
+
// methods — no purity gate). `recvNode` is the caller's already-narrowed
|
|
2773
|
+
// `callee.object`.
|
|
2774
|
+
const recv = recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
|
|
2775
|
+
? lowerChain(recvNode, ctx)
|
|
2776
|
+
: { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
|
|
2777
|
+
if (recv.guard !== null)
|
|
2778
|
+
return null;
|
|
2779
|
+
// reduceRight reverses the sequence; reduce uses it as-is. The slice binds
|
|
2780
|
+
// TIGHTER than ternary/binary receivers (`xs if c else ys[::-1]` slices only
|
|
2781
|
+
// `ys` — agon review, agy 1.0), so the receiver is parenthesized when
|
|
2782
|
+
// compound. The plain-reduce position is a call argument (self-delimiting),
|
|
2783
|
+
// but wrap uniformly so both methods agree on the receiver text.
|
|
2784
|
+
const recvExpr = parenthesizeIterable(recv.expr);
|
|
2785
|
+
const seq = method === 'reduceRight' ? `${recvExpr}[::-1]` : recvExpr;
|
|
2786
|
+
ctx.imports.add('functools');
|
|
2787
|
+
if (call.args.length === 2) {
|
|
2788
|
+
const seed = emitPyExprCtx(call.args[1], ctx);
|
|
2789
|
+
return `__k_functools.reduce(${cb}, ${seq}, ${seed})`;
|
|
2790
|
+
}
|
|
2791
|
+
return `__k_functools.reduce(${cb}, ${seq})`;
|
|
2792
|
+
}
|
|
2793
|
+
/** Parenthesize a lowered receiver for generator-head (`for el in <recv>`) and
|
|
2794
|
+
* slice (`<recv>[::-1]`) positions, where a compound expression parses wrong
|
|
2795
|
+
* (a bare ternary's `if` reads as the comprehension filter — SyntaxError; a
|
|
2796
|
+
* slice binds only the rightmost operand). Space heuristic: an expression
|
|
2797
|
+
* with no top-level spaces is an atom/chain (`self.items`, `makeBox().items`,
|
|
2798
|
+
* `xs[1:]`) and stays bare (byte-identical to pre-fix output); one that
|
|
2799
|
+
* starts with a self-delimiting bracket also stays bare; anything else wraps.
|
|
2800
|
+
* Parens are never semantically wrong — the heuristic only preserves
|
|
2801
|
+
* idiomatic output for the common shapes. */
|
|
2802
|
+
function parenthesizeIterable(expr) {
|
|
2803
|
+
if (!expr.includes(' '))
|
|
2804
|
+
return expr;
|
|
2805
|
+
// A leading bracket is only self-delimiting if it CLOSES at the very end
|
|
2806
|
+
// (`[1, 2, 3]` yes; `[1] if c else [2]` no — same opener, not enclosing).
|
|
2807
|
+
const open = expr[0];
|
|
2808
|
+
const close = open === '[' ? ']' : open === '(' ? ')' : open === '{' ? '}' : null;
|
|
2809
|
+
if (close !== null && expr[expr.length - 1] === close) {
|
|
2810
|
+
let depth = 0;
|
|
2811
|
+
let quote = null;
|
|
2812
|
+
for (let i = 0; i < expr.length; i++) {
|
|
2813
|
+
const ch = expr[i];
|
|
2814
|
+
if (quote) {
|
|
2815
|
+
if (ch === '\\')
|
|
2816
|
+
i++;
|
|
2817
|
+
else if (ch === quote)
|
|
2818
|
+
quote = null;
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
if (ch === '"' || ch === "'")
|
|
2822
|
+
quote = ch;
|
|
2823
|
+
else if (ch === '[' || ch === '(' || ch === '{')
|
|
2824
|
+
depth++;
|
|
2825
|
+
else if (ch === ']' || ch === ')' || ch === '}') {
|
|
2826
|
+
depth--;
|
|
2827
|
+
// Depth returns to 0 before the end → the leading bracket does NOT
|
|
2828
|
+
// enclose the whole expression.
|
|
2829
|
+
if (depth === 0 && i < expr.length - 1)
|
|
2830
|
+
return `(${expr})`;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
if (depth === 0)
|
|
2834
|
+
return expr;
|
|
2835
|
+
}
|
|
2836
|
+
return `(${expr})`;
|
|
2837
|
+
}
|
|
1984
2838
|
function lowerRegexCallPython(call, ctx) {
|
|
1985
2839
|
const callee = call.callee;
|
|
1986
2840
|
if (callee.kind !== 'member')
|
|
@@ -2180,6 +3034,12 @@ function lowerListLambdaPython(moduleName, methodName, call, ctx) {
|
|
|
2180
3034
|
if (callback.params.length !== 1) {
|
|
2181
3035
|
throw new Error(`List.${methodName} expects a one-parameter lambda on the Python target.`);
|
|
2182
3036
|
}
|
|
3037
|
+
// Block-bodied arrow callback: the comprehension lowering only handles an
|
|
3038
|
+
// expression `body`. Fall through (null) so the lambda routes through the
|
|
3039
|
+
// default call path → `emitLambdaPy` (which fails closed in commit A and
|
|
3040
|
+
// hoists a local def in commit B).
|
|
3041
|
+
if (!callback.body)
|
|
3042
|
+
return null;
|
|
2183
3043
|
const name = callback.params[0].name;
|
|
2184
3044
|
const previous = new Set(ctx.shadowedSymbols);
|
|
2185
3045
|
ctx.shadowedSymbols.add(name);
|
|
@@ -2260,7 +3120,10 @@ export function lowerBitwiseAndModuloAST(node) {
|
|
|
2260
3120
|
args: node.args.map(lowerBitwiseAndModuloAST),
|
|
2261
3121
|
};
|
|
2262
3122
|
case 'lambda':
|
|
2263
|
-
|
|
3123
|
+
// Block-bodied arrows carry raw text, not an expression `body`. The raw
|
|
3124
|
+
// is re-parsed and lowered during closure emission (commit B), so leave
|
|
3125
|
+
// it untouched here.
|
|
3126
|
+
return node.bodyBlock ? node : { ...node, body: lowerBitwiseAndModuloAST(node.body) };
|
|
2264
3127
|
case 'spread':
|
|
2265
3128
|
return { ...node, argument: lowerBitwiseAndModuloAST(node.argument) };
|
|
2266
3129
|
case 'await':
|
|
@@ -2337,7 +3200,10 @@ export function registerHelpers(node, ctx) {
|
|
|
2337
3200
|
registerHelpers(node.index, ctx);
|
|
2338
3201
|
break;
|
|
2339
3202
|
case 'lambda':
|
|
2340
|
-
|
|
3203
|
+
// Block-bodied arrows have no expression `body`; helpers referenced by a
|
|
3204
|
+
// block body are registered during closure emission (commit B).
|
|
3205
|
+
if (node.body)
|
|
3206
|
+
registerHelpers(node.body, ctx);
|
|
2341
3207
|
break;
|
|
2342
3208
|
case 'spread':
|
|
2343
3209
|
registerHelpers(node.argument, ctx);
|