@kernlang/python 4.1.1-canary.228.1.41c150f2 → 4.1.1-canary.232.1.c7f7511c
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.
|
@@ -40,7 +40,7 @@
|
|
|
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, assertNoDecimalOperator, classifyRegexLiteralIndexReadFailClose, classifyRegexLiteralMemberReadFailClose, decimalBareConstructionFailMessage, emitStringKeyArray, expandRegexIFold, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isHostNamespaceRoot, isPostfixMutationOperator, isSupportedAssignOperator, isZeroWidthCapableRegex, KERN_DECIMAL_OPS_HELPER_PY, KERN_STDLIB_MODULES, KERN_TEXT_OPS_HELPER_PY, lookupStdlibCall, lookupStdlibProperty, lowerRegexAnchorsPython, lowerRegexNamedGroupsPython, needsArgParens, needsBinaryParens, normalizeRegexClasses, parseExpression, parseKeys, REGEX_EXEC_FAILCLOSE, REGEX_HOST_REGEXP_FAILCLOSE, REGEX_MATCHALL_NO_G_FAILCLOSE, REGEX_NONLITERAL_FAILCLOSE, REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE, REGEX_REPLACEALL_NO_G_FAILCLOSE, REGEX_SPLIT_LIMIT_FAILCLOSE, REGEX_SPLIT_ZEROWIDTH_FAILCLOSE, REGEX_TEST_G_FAILCLOSE, regexAstralFailMessage, regexCaptureMeta, regexIFoldFailMessage, regexLiteralReceiverIR, regexMethodRegexArgIdent, scanRegexAstral, suggestStdlibMethod, translateReplStringToPython, unmappedHostNamespaceMessage, unwrapTransparentReceiverIR, validateDecimalConstructionArg, validateDecimalDivModArgs, validateDecimalOperands, validateDecimalPowArgs, validateRegexNamedGroupsPortable, } from '@kernlang/core';
|
|
43
|
+
import { applyTemplate, assertNoDecimalOperator, classifyRegexLiteralIndexReadFailClose, classifyRegexLiteralMemberReadFailClose, decimalBareConstructionFailMessage, emitStringKeyArray, expandRegexIFold, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isHostNamespaceRoot, isParenthesized, isPostfixMutationOperator, isSafeIntegerLiteralIndex, isSupportedAssignOperator, isZeroWidthCapableRegex, KERN_DECIMAL_OPS_HELPER_PY, KERN_STDLIB_MODULES, KERN_TEXT_OPS_HELPER_PY, lookupStdlibCall, lookupStdlibProperty, lowerRegexAnchorsPython, lowerRegexNamedGroupsPython, needsArgParens, needsBinaryParens, normalizeRegexClasses, parseExpression, parseKeys, REGEX_EXEC_FAILCLOSE, REGEX_HOST_REGEXP_FAILCLOSE, REGEX_MATCHALL_NO_G_FAILCLOSE, REGEX_NONLITERAL_FAILCLOSE, REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE, REGEX_REPLACEALL_NO_G_FAILCLOSE, REGEX_SPLIT_LIMIT_FAILCLOSE, REGEX_SPLIT_ZEROWIDTH_FAILCLOSE, REGEX_TEST_G_FAILCLOSE, regexAstralFailMessage, regexCaptureMeta, regexIFoldFailMessage, regexLiteralReceiverIR, regexMethodRegexArgIdent, scanRegexAstral, suggestStdlibMethod, translateReplStringToPython, unmappedHostNamespaceMessage, unwrapTransparentReceiverIR, validateDecimalConstructionArg, validateDecimalDivModArgs, validateDecimalOperands, validateDecimalPowArgs, validateRegexNamedGroupsPortable, } from '@kernlang/core';
|
|
44
44
|
// Slice 0.9 — the TypeScript-AST closure helpers + classifier live on the Node
|
|
45
45
|
// subpath (the barrel is browser-safe). Python codegen is Node-side and parses
|
|
46
46
|
// block-bodied arrows, so it injects `typescriptClosureClassifier`.
|
|
@@ -48,6 +48,7 @@ import { collectFreeIdentifierNames, lowerJsClosureBodyToPython, typescriptClosu
|
|
|
48
48
|
import { buildPythonParamList } from './codegen-helpers.js';
|
|
49
49
|
import { KERN_FMT_HELPER_PY, KERN_JS_ARRAY_FROM_HELPER_PY, KERN_JS_ARRAY_HELPERS_PY, KERN_JS_HELPER_PY, KERN_JS_MATH_HELPERS_PY, KERN_JS_NUMBER_HELPERS_PY, KERN_JS_OBJECT_HELPERS_PY, KERN_JSON_STRINGIFY_SHIM_PY, KERN_NULLISH_HELPER_PY, KERN_PAIR_HELPERS_PY, KERN_REGEX_MATCH_HELPER_PY, KERN_REGEX_MATCHALL_HELPER_PY, KERN_TMOD_HELPER_PY, KERN_TO_NUMBER_HELPER_PY, } from './core/expr/index.js';
|
|
50
50
|
import { isSharedPortableArrayMethod, isSharedPortableArrayProperty, lowerPortableArrayMethodPy, lowerPortableArrayPropertyPy, sharedPortableMethodRequiresPureReceiver, } from './core/expr/list-ops.js';
|
|
51
|
+
import { DOT_DICT_SHIM_PY } from './targets/python.js';
|
|
51
52
|
import { mapTsTypeToPython } from './type-map.js';
|
|
52
53
|
/** Parse options for Python codegen — always inject the TypeScript-backed
|
|
53
54
|
* closure classifier so block-bodied arrows parse (slice 0.9). */
|
|
@@ -58,6 +59,42 @@ const TS_PARSE_OPTS = { closureClassifier: typescriptClosureClassifier };
|
|
|
58
59
|
function parseExpr(input) {
|
|
59
60
|
return parseExpression(input, TS_PARSE_OPTS);
|
|
60
61
|
}
|
|
62
|
+
/** Property-specific fail-close for a non-length read/call on a nested record
|
|
63
|
+
* array field — message LOCKSTEP with the TS twin (`nestedArrayMemberThrowTS`
|
|
64
|
+
* in codegen-expression.ts): 'has no portable property "<name>"'. */
|
|
65
|
+
const KERN_NESTED_NO_PROPERTY_HELPER_PY = `def _kern_nested_no_property(path, prop):
|
|
66
|
+
raise Exception('portable: nested array field "' + path + '" has no portable property "' + prop + '"')
|
|
67
|
+
`;
|
|
68
|
+
const KERN_NESTED_ARRAY_HELPER_PY = `def _kern_nested_array_value(record, field, index=None):
|
|
69
|
+
if not isinstance(record, dict) or field not in record:
|
|
70
|
+
raise Exception("portable: nested array receiver must be a record field")
|
|
71
|
+
value = record[field]
|
|
72
|
+
if not isinstance(value, list):
|
|
73
|
+
raise Exception("portable: nested record field must be an array")
|
|
74
|
+
if index is None:
|
|
75
|
+
return len(value)
|
|
76
|
+
if not isinstance(index, int) or isinstance(index, bool) or index < 0 or index >= len(value):
|
|
77
|
+
raise Exception("portable: nested array index must be an in-bounds non-negative safe integer")
|
|
78
|
+
out = value[index]
|
|
79
|
+
if not (out is None or isinstance(out, str) or isinstance(out, bool) or (isinstance(out, (int, float)) and not isinstance(out, bool) and out == out and out != float("inf") and out != float("-inf"))):
|
|
80
|
+
raise Exception("portable: nested array element must be a portable scalar")
|
|
81
|
+
return out
|
|
82
|
+
`;
|
|
83
|
+
const KERN_NESTED_ARRAY_REF_HELPER_PY = `def _kern_nested_array_ref(record, field):
|
|
84
|
+
if not isinstance(record, dict) or field not in record:
|
|
85
|
+
raise Exception("portable: nested array receiver must be a record field")
|
|
86
|
+
value = record[field]
|
|
87
|
+
if not isinstance(value, list):
|
|
88
|
+
raise Exception("portable: nested record field must be an array")
|
|
89
|
+
return value
|
|
90
|
+
`;
|
|
91
|
+
const KERN_NESTED_ARRAY_ITER_HELPER_PY = `def _kern_nested_array_iter(record, field):
|
|
92
|
+
value = _kern_nested_array_ref(record, field)
|
|
93
|
+
for out in value:
|
|
94
|
+
if not (out is None or isinstance(out, str) or isinstance(out, bool) or (isinstance(out, (int, float)) and not isinstance(out, bool) and out == out and out != float("inf") and out != float("-inf"))):
|
|
95
|
+
raise Exception("portable: nested array element must be a portable scalar")
|
|
96
|
+
return value
|
|
97
|
+
`;
|
|
61
98
|
const INDENT_STEP = ' ';
|
|
62
99
|
function freshCtx(options) {
|
|
63
100
|
return {
|
|
@@ -70,6 +107,12 @@ function freshCtx(options) {
|
|
|
70
107
|
shadowedSymbols: new Set(),
|
|
71
108
|
localScopes: [],
|
|
72
109
|
regexScopes: [],
|
|
110
|
+
recordScopes: [],
|
|
111
|
+
recordArrayFieldScopes: [],
|
|
112
|
+
recordScalarArrayFieldScopes: [],
|
|
113
|
+
maybeRecordArrayFieldScopes: [],
|
|
114
|
+
arrayBindingScopes: [],
|
|
115
|
+
scalarArrayBindingScopes: [],
|
|
73
116
|
renameStack: [],
|
|
74
117
|
propagateStyle: options?.propagateStyle ?? 'value',
|
|
75
118
|
usedPropagation: false,
|
|
@@ -169,6 +212,12 @@ export function emitNativeKernBodyPythonWithImports(handlerNode, options) {
|
|
|
169
212
|
// no regex literal was assigned to it). Mirroring it here keeps regex
|
|
170
213
|
// and local-binding scope stacks index-aligned.
|
|
171
214
|
ctx.regexScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
215
|
+
ctx.recordScopes.push(new Map(outerBindings.map((n) => [n, false])));
|
|
216
|
+
ctx.recordArrayFieldScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
217
|
+
ctx.recordScalarArrayFieldScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
218
|
+
ctx.maybeRecordArrayFieldScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
219
|
+
ctx.arrayBindingScopes.push(new Map());
|
|
220
|
+
ctx.scalarArrayBindingScopes.push(new Map());
|
|
172
221
|
ctx.renameStack.push(new Map());
|
|
173
222
|
}
|
|
174
223
|
try {
|
|
@@ -186,6 +235,12 @@ export function emitNativeKernBodyPythonWithImports(handlerNode, options) {
|
|
|
186
235
|
if (outerBindings.length > 0) {
|
|
187
236
|
ctx.localScopes.pop();
|
|
188
237
|
ctx.regexScopes.pop();
|
|
238
|
+
ctx.recordScopes.pop();
|
|
239
|
+
ctx.recordArrayFieldScopes.pop();
|
|
240
|
+
ctx.recordScalarArrayFieldScopes.pop();
|
|
241
|
+
ctx.maybeRecordArrayFieldScopes.pop();
|
|
242
|
+
ctx.arrayBindingScopes.pop();
|
|
243
|
+
ctx.scalarArrayBindingScopes.pop();
|
|
189
244
|
ctx.renameStack.pop();
|
|
190
245
|
}
|
|
191
246
|
}
|
|
@@ -259,10 +314,24 @@ function collectLoopAssignLastIndexes(children) {
|
|
|
259
314
|
scan(children[i], i);
|
|
260
315
|
return last;
|
|
261
316
|
}
|
|
317
|
+
function childrenCanFallThrough(children) {
|
|
318
|
+
for (const child of children) {
|
|
319
|
+
if (child.type === 'return' || child.type === 'throw' || child.type === 'break' || child.type === 'continue') {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
262
325
|
function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody = false) {
|
|
263
326
|
const lines = [];
|
|
264
327
|
ctx.localScopes.push(new Map(initialBindings));
|
|
265
328
|
ctx.regexScopes.push(new Map(initialBindings.map(([name]) => [name, null])));
|
|
329
|
+
ctx.recordScopes.push(new Map(initialBindings.map(([name]) => [name, false])));
|
|
330
|
+
ctx.recordArrayFieldScopes.push(new Map(initialBindings.map(([name]) => [name, null])));
|
|
331
|
+
ctx.recordScalarArrayFieldScopes.push(new Map(initialBindings.map(([name]) => [name, null])));
|
|
332
|
+
ctx.maybeRecordArrayFieldScopes.push(new Map(initialBindings.map(([name]) => [name, null])));
|
|
333
|
+
ctx.arrayBindingScopes.push(new Map());
|
|
334
|
+
ctx.scalarArrayBindingScopes.push(new Map());
|
|
266
335
|
ctx.renameStack.push(new Map());
|
|
267
336
|
// Slice-2 loop-variable pinning. When this recursion is a loop BODY, record
|
|
268
337
|
// the just-pushed scope's index so `emitBlockClosurePy` can decide whether a
|
|
@@ -330,6 +399,9 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
330
399
|
for (const line of emitFnPy(child, ctx, indent))
|
|
331
400
|
lines.push(line);
|
|
332
401
|
}
|
|
402
|
+
else if (child.type === 'capability') {
|
|
403
|
+
throw new Error('capability nodes are not supported in emitted TypeScript/Python until an emitted capability ABI exists');
|
|
404
|
+
}
|
|
333
405
|
else if (child.type === 'assign') {
|
|
334
406
|
// Same shadowing guard for a bare-identifier assignment target: after
|
|
335
407
|
// `assign target="e" …`, `e` is no longer the caught error.
|
|
@@ -391,11 +463,18 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
391
463
|
// Wrap UNCONDITIONALLY (no "looks boolean" skip-analysis in v1).
|
|
392
464
|
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
393
465
|
lines.push(`${indent}if _kern_truthy(${emitPyExprCtx(condIR, ctx)}):`);
|
|
466
|
+
const branchBase = cloneBranchBindingScopes(ctx);
|
|
467
|
+
const branchOutcomes = [];
|
|
468
|
+
let remainingBranchReachable = !isStaticBooleanLiteral(condIR, true);
|
|
469
|
+
restoreBranchBindingScopes(ctx, branchBase);
|
|
394
470
|
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP);
|
|
395
471
|
if (inner.length === 0)
|
|
396
472
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
397
473
|
for (const sl of inner)
|
|
398
474
|
lines.push(sl);
|
|
475
|
+
if (!isStaticBooleanLiteral(condIR, false) && childrenCanFallThrough(child.children ?? [])) {
|
|
476
|
+
branchOutcomes.push(cloneBranchBindingScopes(ctx));
|
|
477
|
+
}
|
|
399
478
|
// Walk the `else` chain. Recognised shapes for `else`:
|
|
400
479
|
// 1. else > [if, else_inner] → emit `elif`, recurse on else_inner
|
|
401
480
|
// 2. else > [if] → terminal `elif` with no else
|
|
@@ -405,10 +484,12 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
405
484
|
let elseCandidate = children[i + 1];
|
|
406
485
|
if (elseCandidate?.type === 'else')
|
|
407
486
|
i++;
|
|
487
|
+
let hasTerminalElse = false;
|
|
408
488
|
while (elseCandidate && elseCandidate.type === 'else') {
|
|
409
489
|
const ec = elseCandidate.children ?? [];
|
|
410
490
|
const isChainable = ec.length >= 1 && ec[0].type === 'if' && (ec.length === 1 || (ec.length === 2 && ec[1].type === 'else'));
|
|
411
491
|
if (isChainable) {
|
|
492
|
+
restoreBranchBindingScopes(ctx, branchBase);
|
|
412
493
|
const ifNode = ec[0];
|
|
413
494
|
const nestedCondRaw = String(ifNode.props?.cond ?? '');
|
|
414
495
|
const nestedCondIR = parseExpr(nestedCondRaw);
|
|
@@ -423,18 +504,33 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
423
504
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
424
505
|
for (const sl of ifInner)
|
|
425
506
|
lines.push(sl);
|
|
507
|
+
if (remainingBranchReachable &&
|
|
508
|
+
!isStaticBooleanLiteral(nestedCondIR, false) &&
|
|
509
|
+
childrenCanFallThrough(ifNode.children ?? [])) {
|
|
510
|
+
branchOutcomes.push(cloneBranchBindingScopes(ctx));
|
|
511
|
+
}
|
|
512
|
+
if (isStaticBooleanLiteral(nestedCondIR, true))
|
|
513
|
+
remainingBranchReachable = false;
|
|
426
514
|
elseCandidate = ec.length === 2 ? ec[1] : undefined;
|
|
427
515
|
}
|
|
428
516
|
else {
|
|
517
|
+
restoreBranchBindingScopes(ctx, branchBase);
|
|
429
518
|
lines.push(`${indent}else:`);
|
|
430
519
|
const elseInner = emitChildrenPy(ec, ctx, indent + INDENT_STEP);
|
|
431
520
|
if (elseInner.length === 0)
|
|
432
521
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
433
522
|
for (const el of elseInner)
|
|
434
523
|
lines.push(el);
|
|
524
|
+
if (remainingBranchReachable && childrenCanFallThrough(ec))
|
|
525
|
+
branchOutcomes.push(cloneBranchBindingScopes(ctx));
|
|
526
|
+
hasTerminalElse = true;
|
|
527
|
+
remainingBranchReachable = false;
|
|
435
528
|
break;
|
|
436
529
|
}
|
|
437
530
|
}
|
|
531
|
+
if (!hasTerminalElse && remainingBranchReachable)
|
|
532
|
+
branchOutcomes.push(branchBase);
|
|
533
|
+
mergeBranchBindingSnapshots(ctx, branchBase, branchOutcomes);
|
|
438
534
|
}
|
|
439
535
|
else if (child.type === 'else') {
|
|
440
536
|
// Slice-2 review fix: orphan `else` is a structural error (matches TS side).
|
|
@@ -586,7 +682,8 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
586
682
|
}
|
|
587
683
|
const k = String(pairKey);
|
|
588
684
|
const v = String(pairValue);
|
|
589
|
-
|
|
685
|
+
assertNoKeyedNestedRecordReceiverPy(listIR, ctx);
|
|
686
|
+
const sourceExpr = emitEachIterablePy(listIR, ctx);
|
|
590
687
|
ctx.helpers.add(KERN_PAIR_HELPERS_PY);
|
|
591
688
|
const iterableExpr = isAwait ? `_kern_async_pairs(${sourceExpr})` : `_kern_pairs(${sourceExpr})`;
|
|
592
689
|
lines.push(`${indent}${isAwait ? 'async ' : ''}for ${k}, ${v} in ${iterableExpr}:`);
|
|
@@ -616,7 +713,8 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
616
713
|
if (entryKey && entryValue) {
|
|
617
714
|
throw new Error('body-statement `each` cannot combine `entryKey=` and `entryValue=`.');
|
|
618
715
|
}
|
|
619
|
-
|
|
716
|
+
assertNoKeyedNestedRecordReceiverPy(listIR, ctx);
|
|
717
|
+
const sourceExpr = emitEachIterablePy(listIR, ctx);
|
|
620
718
|
if (entryKey) {
|
|
621
719
|
const k = String(entryKey);
|
|
622
720
|
const iterableExpr = `${sourceExpr}.keys()`;
|
|
@@ -662,7 +760,7 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
662
760
|
// the IR-semantics differential audit (PR-3b).
|
|
663
761
|
const asName = String(child.props?.name ?? child.props?.as ?? 'item');
|
|
664
762
|
const idxName = child.props?.index !== undefined ? String(child.props.index) : null;
|
|
665
|
-
const iterableExpr =
|
|
763
|
+
const iterableExpr = emitEachIterablePy(listIR, ctx);
|
|
666
764
|
let primaryBindingPy;
|
|
667
765
|
let initialBindings;
|
|
668
766
|
if (idxName !== null) {
|
|
@@ -745,6 +843,12 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
745
843
|
ctx.loopLaterAssignFrames.pop();
|
|
746
844
|
ctx.localScopes.pop();
|
|
747
845
|
ctx.regexScopes.pop();
|
|
846
|
+
ctx.recordScopes.pop();
|
|
847
|
+
ctx.recordArrayFieldScopes.pop();
|
|
848
|
+
ctx.recordScalarArrayFieldScopes.pop();
|
|
849
|
+
ctx.maybeRecordArrayFieldScopes.pop();
|
|
850
|
+
ctx.arrayBindingScopes.pop();
|
|
851
|
+
ctx.scalarArrayBindingScopes.pop();
|
|
748
852
|
ctx.renameStack.pop();
|
|
749
853
|
// Restore the parent level's hoist buffer (see the isolation comment at
|
|
750
854
|
// entry). Any defs THIS level's last child left behind are appended so the
|
|
@@ -768,6 +872,17 @@ function resolveLocalRename(ctx, name) {
|
|
|
768
872
|
}
|
|
769
873
|
return name;
|
|
770
874
|
}
|
|
875
|
+
function emitScopedIdentPy(ctx, name) {
|
|
876
|
+
// Only custom nested-record rewrites need this helper; ordinary expression
|
|
877
|
+
// emission descends through the ident case, which already resolves block
|
|
878
|
+
// renames before applying symbol-map names.
|
|
879
|
+
const blockRename = resolveLocalRename(ctx, name);
|
|
880
|
+
if (blockRename !== name)
|
|
881
|
+
return blockRename;
|
|
882
|
+
if (ctx.shadowedSymbols.has(name))
|
|
883
|
+
return name;
|
|
884
|
+
return ctx.symbolMap[name] ?? name;
|
|
885
|
+
}
|
|
771
886
|
/** Returns the renamed name if `let name=` here would shadow a binding in
|
|
772
887
|
* any OUTER scope; otherwise returns `name` unchanged. Used by `emitLetPy`
|
|
773
888
|
* to give an inner-block shadow a unique Python name + record the rename
|
|
@@ -1067,6 +1182,8 @@ function emitLetPy(node, ctx) {
|
|
|
1067
1182
|
}
|
|
1068
1183
|
const valueIR = parseExpr(String(rawValue));
|
|
1069
1184
|
setRegexBinding(ctx, userName, valueIR.kind === 'regexLit' ? valueIR : null);
|
|
1185
|
+
setRecordBinding(ctx, userName, valueIR.kind === 'objectLit', recordArrayFieldsForValue(valueIR, ctx), recordScalarArrayFieldsForValue(valueIR, ctx));
|
|
1186
|
+
bindArrayStatusFromLet(ctx, userName, valueIR);
|
|
1070
1187
|
if (valueIR.kind === 'propagate' && valueIR.op === '?') {
|
|
1071
1188
|
rejectPropagationInsideTry(ctx);
|
|
1072
1189
|
const tmp = `__k_t${++ctx.gensymCounter}`;
|
|
@@ -1082,11 +1199,22 @@ function emitLetPy(node, ctx) {
|
|
|
1082
1199
|
lines.push(letAssignTracePy(name));
|
|
1083
1200
|
return lines;
|
|
1084
1201
|
}
|
|
1085
|
-
const lines = [`${name} = ${
|
|
1202
|
+
const lines = [`${name} = ${emitLetInitializerPy(valueIR, ctx)}`];
|
|
1086
1203
|
if (ctx.traceHooks?.letAssign)
|
|
1087
1204
|
lines.push(letAssignTracePy(name));
|
|
1088
1205
|
return lines;
|
|
1089
1206
|
}
|
|
1207
|
+
/** RECORD-binding initializer wrap (nested-values slice-1): a `let` bound to a
|
|
1208
|
+
* DIRECT object literal becomes a `__DotDict` so certified field reads
|
|
1209
|
+
* (`r.a`, `r.length`) resolve on the Python leg. ONLY this position wraps —
|
|
1210
|
+
* inline/argument/return object literals keep base plain-dict emission. */
|
|
1211
|
+
function emitLetInitializerPy(valueIR, ctx) {
|
|
1212
|
+
const rhs = emitPyExprCtx(valueIR, ctx);
|
|
1213
|
+
if (valueIR.kind !== 'objectLit')
|
|
1214
|
+
return rhs;
|
|
1215
|
+
ctx.helpers.add(DOT_DICT_SHIM_PY);
|
|
1216
|
+
return `__DotDict(${rhs})`;
|
|
1217
|
+
}
|
|
1090
1218
|
function unwrapBodyExpr(value) {
|
|
1091
1219
|
if (value === undefined || value === null)
|
|
1092
1220
|
return undefined;
|
|
@@ -1371,6 +1499,7 @@ function emitAssignPy(node, ctx) {
|
|
|
1371
1499
|
throw new Error('body-statement `assign target=` must be an identifier, member access, or index access.');
|
|
1372
1500
|
}
|
|
1373
1501
|
assertAssignableLocalTarget(targetIR, ctx);
|
|
1502
|
+
applyArrayMutationAssignPy(targetIR, ctx);
|
|
1374
1503
|
// Python lacks `++` / `--`; lower postfix mutation to the canonical compound
|
|
1375
1504
|
// assignment (`X += 1` / `X -= 1`). The TS round-trip stays byte-equivalent
|
|
1376
1505
|
// because TS emits `X++;` from the same IR — only the Python target diverges
|
|
@@ -1386,12 +1515,20 @@ function emitAssignPy(node, ctx) {
|
|
|
1386
1515
|
// Emit FIRST (its `emitPyExprCtx` lowering fail-closes a regex method on a
|
|
1387
1516
|
// bound regex ident) so the RHS is checked against the PRE-reassignment table
|
|
1388
1517
|
// (`re = s.match(re)` must still see `re` as a regex). Mirrors the TS leg.
|
|
1389
|
-
|
|
1518
|
+
// A plain-`=` ident reassignment to a DIRECT object literal takes the SAME
|
|
1519
|
+
// __DotDict wrap as a record `let` initializer — `rebindRecordOnReassign`
|
|
1520
|
+
// below re-marks the binding as a record, so the VALUE must be record-
|
|
1521
|
+
// shaped too or later emitted field reads (attribute access) would hit a
|
|
1522
|
+
// plain dict. Lockstep with `emitLetInitializerPy`.
|
|
1523
|
+
const rhs = rawOp === '=' && targetIR.kind === 'ident' ? emitLetInitializerPy(valueIR, ctx) : emitPyExprCtx(valueIR, ctx);
|
|
1524
|
+
const stmt = `${emitPyExprCtx(targetIR, ctx)} ${rawOp} ${rhs}`;
|
|
1390
1525
|
// Reassign-invalidation (Slice-3c): keep the regex-binding table honest. A
|
|
1391
1526
|
// plain `=` to a direct regex literal stays a regex binding (still
|
|
1392
1527
|
// fail-closed); any compound op (`+=`, …) or non-regex RHS UNMARKS it.
|
|
1393
1528
|
if (targetIR.kind === 'ident') {
|
|
1394
1529
|
rebindRegexOnReassign(ctx, targetIR.name, rawOp === '=' ? valueIR : { kind: 'undefLit' });
|
|
1530
|
+
rebindRecordOnReassign(ctx, targetIR.name, rawOp === '=' ? valueIR : { kind: 'undefLit' });
|
|
1531
|
+
rebindArrayOnReassign(ctx, targetIR.name, rawOp === '=' ? valueIR : { kind: 'undefLit' });
|
|
1395
1532
|
}
|
|
1396
1533
|
// Differential-harness opt-in (see BodyEmitOptions.traceHooks.letAssign): the
|
|
1397
1534
|
// `assign` contract observes a reassignment via the same `{op:"assign"}` event
|
|
@@ -1411,6 +1548,8 @@ function declareLocalBinding(ctx, name, kind) {
|
|
|
1411
1548
|
}
|
|
1412
1549
|
scope.set(name, kind);
|
|
1413
1550
|
setRegexBinding(ctx, name, null);
|
|
1551
|
+
setRecordBinding(ctx, name, false);
|
|
1552
|
+
setArrayBindingStatus(ctx, name, null);
|
|
1414
1553
|
}
|
|
1415
1554
|
function setRegexBinding(ctx, name, regex) {
|
|
1416
1555
|
ctx.regexScopes.at(-1)?.set(name, regex);
|
|
@@ -1439,6 +1578,360 @@ function rebindRegexOnReassign(ctx, name, valueIR) {
|
|
|
1439
1578
|
}
|
|
1440
1579
|
}
|
|
1441
1580
|
}
|
|
1581
|
+
/** Record whether `name` is bound to a DIRECT record literal in the current scope. */
|
|
1582
|
+
function setRecordBinding(ctx, name, isRecord, arrayFields = null, scalarArrayFields = null) {
|
|
1583
|
+
ctx.recordScopes.at(-1)?.set(name, isRecord);
|
|
1584
|
+
ctx.recordArrayFieldScopes.at(-1)?.set(name, isRecord ? arrayFields : null);
|
|
1585
|
+
ctx.recordScalarArrayFieldScopes.at(-1)?.set(name, isRecord ? scalarArrayFields : null);
|
|
1586
|
+
ctx.maybeRecordArrayFieldScopes.at(-1)?.set(name, isRecord ? arrayFields : null);
|
|
1587
|
+
}
|
|
1588
|
+
function recordArrayFieldsForValue(valueIR, ctx) {
|
|
1589
|
+
if (valueIR.kind !== 'objectLit')
|
|
1590
|
+
return null;
|
|
1591
|
+
const fields = new Set();
|
|
1592
|
+
for (const entry of valueIR.entries) {
|
|
1593
|
+
if ('kind' in entry)
|
|
1594
|
+
continue;
|
|
1595
|
+
if (entry.value.kind === 'arrayLit') {
|
|
1596
|
+
assertPortableArrayLiteralElementsPy(entry.value);
|
|
1597
|
+
fields.add(entry.key);
|
|
1598
|
+
}
|
|
1599
|
+
else if (entry.value.kind === 'ident') {
|
|
1600
|
+
const status = lookupArrayBindingStatus(ctx, entry.value.name);
|
|
1601
|
+
if (status === 'fresh' || status === 'fresh-push' || status === 'captured')
|
|
1602
|
+
fields.add(entry.key);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return fields;
|
|
1606
|
+
}
|
|
1607
|
+
function recordScalarArrayFieldsForValue(valueIR, ctx) {
|
|
1608
|
+
if (valueIR.kind !== 'objectLit')
|
|
1609
|
+
return null;
|
|
1610
|
+
const fields = new Set();
|
|
1611
|
+
for (const entry of valueIR.entries) {
|
|
1612
|
+
if ('kind' in entry)
|
|
1613
|
+
continue;
|
|
1614
|
+
if (entry.value.kind === 'arrayLit' && arrayLiteralHasOnlyScalarElements(entry.value))
|
|
1615
|
+
fields.add(entry.key);
|
|
1616
|
+
else if (entry.value.kind === 'ident') {
|
|
1617
|
+
const status = lookupArrayBindingStatus(ctx, entry.value.name);
|
|
1618
|
+
if ((status === 'fresh' || status === 'fresh-push' || status === 'captured') &&
|
|
1619
|
+
lookupScalarArrayBinding(ctx, entry.value.name)) {
|
|
1620
|
+
fields.add(entry.key);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return fields;
|
|
1625
|
+
}
|
|
1626
|
+
function assertPortableArrayLiteralElementsPy(valueIR) {
|
|
1627
|
+
for (const item of valueIR.items) {
|
|
1628
|
+
if (item.kind === 'arrayLit') {
|
|
1629
|
+
assertPortableArrayLiteralElementsPy(item);
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
if (item.kind === 'strLit' || item.kind === 'boolLit' || item.kind === 'nullLit')
|
|
1633
|
+
continue;
|
|
1634
|
+
if (item.kind === 'numLit' && item.bigint !== true && Number.isFinite(item.value)) {
|
|
1635
|
+
if (isIntegerValuedFloatLiteralPy(item))
|
|
1636
|
+
throw new Error('portable: float literal has an integer value (float/int divergence)');
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
throw new Error('portable-array: array literal fields must contain only portable scalar or array elements');
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
function arrayLiteralHasOnlyScalarElements(valueIR) {
|
|
1643
|
+
return valueIR.items.every((item) => (item.kind === 'numLit' &&
|
|
1644
|
+
item.bigint !== true &&
|
|
1645
|
+
Number.isFinite(item.value) &&
|
|
1646
|
+
!isIntegerValuedFloatLiteralPy(item)) ||
|
|
1647
|
+
item.kind === 'strLit' ||
|
|
1648
|
+
item.kind === 'boolLit' ||
|
|
1649
|
+
item.kind === 'nullLit');
|
|
1650
|
+
}
|
|
1651
|
+
function setArrayBindingStatus(ctx, name, status) {
|
|
1652
|
+
const scope = ctx.arrayBindingScopes.at(-1);
|
|
1653
|
+
if (!scope)
|
|
1654
|
+
return;
|
|
1655
|
+
if (status === null)
|
|
1656
|
+
scope.delete(name);
|
|
1657
|
+
else
|
|
1658
|
+
scope.set(name, status);
|
|
1659
|
+
}
|
|
1660
|
+
function setScalarArrayBinding(ctx, name, scalar) {
|
|
1661
|
+
const scope = ctx.scalarArrayBindingScopes.at(-1);
|
|
1662
|
+
if (!scope)
|
|
1663
|
+
return;
|
|
1664
|
+
if (scalar)
|
|
1665
|
+
scope.set(name, true);
|
|
1666
|
+
else
|
|
1667
|
+
scope.delete(name);
|
|
1668
|
+
}
|
|
1669
|
+
function lookupArrayBindingStatus(ctx, name) {
|
|
1670
|
+
for (let i = ctx.localScopes.length - 1; i >= 0; i--) {
|
|
1671
|
+
if (!ctx.localScopes[i].has(name))
|
|
1672
|
+
continue;
|
|
1673
|
+
return ctx.arrayBindingScopes[i]?.get(name) ?? null;
|
|
1674
|
+
}
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
function lookupScalarArrayBinding(ctx, name) {
|
|
1678
|
+
for (let i = ctx.localScopes.length - 1; i >= 0; i--) {
|
|
1679
|
+
if (!ctx.localScopes[i].has(name))
|
|
1680
|
+
continue;
|
|
1681
|
+
return ctx.scalarArrayBindingScopes[i]?.get(name) === true;
|
|
1682
|
+
}
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
function setDeclaringArrayBindingStatus(ctx, name, status) {
|
|
1686
|
+
for (let i = ctx.localScopes.length - 1; i >= 0; i--) {
|
|
1687
|
+
if (!ctx.localScopes[i].has(name))
|
|
1688
|
+
continue;
|
|
1689
|
+
const scope = ctx.arrayBindingScopes[i];
|
|
1690
|
+
if (status === null)
|
|
1691
|
+
scope?.delete(name);
|
|
1692
|
+
else
|
|
1693
|
+
scope?.set(name, status);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function setDeclaringScalarArrayBinding(ctx, name, scalar) {
|
|
1698
|
+
for (let i = ctx.localScopes.length - 1; i >= 0; i--) {
|
|
1699
|
+
if (!ctx.localScopes[i].has(name))
|
|
1700
|
+
continue;
|
|
1701
|
+
const scope = ctx.scalarArrayBindingScopes[i];
|
|
1702
|
+
if (scalar)
|
|
1703
|
+
scope?.set(name, true);
|
|
1704
|
+
else
|
|
1705
|
+
scope?.delete(name);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
function assertFreshArrayCaptureNotInRepeatableLoopPy(ctx, name) {
|
|
1710
|
+
const loopScopeIndex = ctx.loopScopeIndexes.at(-1);
|
|
1711
|
+
if (loopScopeIndex === undefined)
|
|
1712
|
+
return;
|
|
1713
|
+
const scopeIndex = findBindingScopeIndex(ctx, name);
|
|
1714
|
+
if (scopeIndex !== null && scopeIndex < loopScopeIndex) {
|
|
1715
|
+
throw new Error(`fresh array binding "${name}" cannot be captured inside a repeatable loop body`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function isStaticBooleanLiteral(node, value) {
|
|
1719
|
+
return node.kind === 'boolLit' && node.value === value;
|
|
1720
|
+
}
|
|
1721
|
+
function cloneFieldScopes(scopes) {
|
|
1722
|
+
return scopes.map((scope) => new Map([...scope.entries()].map(([name, fields]) => [name, fields === null ? null : new Set(fields)])));
|
|
1723
|
+
}
|
|
1724
|
+
function cloneBranchBindingScopes(ctx) {
|
|
1725
|
+
return {
|
|
1726
|
+
array: ctx.arrayBindingScopes.map((scope) => new Map(scope)),
|
|
1727
|
+
scalarArray: ctx.scalarArrayBindingScopes.map((scope) => new Map(scope)),
|
|
1728
|
+
record: ctx.recordScopes.map((scope) => new Map(scope)),
|
|
1729
|
+
recordArrayField: cloneFieldScopes(ctx.recordArrayFieldScopes),
|
|
1730
|
+
recordScalarArrayField: cloneFieldScopes(ctx.recordScalarArrayFieldScopes),
|
|
1731
|
+
maybeRecordArrayField: cloneFieldScopes(ctx.maybeRecordArrayFieldScopes),
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
function restoreBranchBindingScopes(ctx, snapshot) {
|
|
1735
|
+
ctx.arrayBindingScopes = snapshot.array.map((scope) => new Map(scope));
|
|
1736
|
+
ctx.scalarArrayBindingScopes = snapshot.scalarArray.map((scope) => new Map(scope));
|
|
1737
|
+
ctx.recordScopes = snapshot.record.map((scope) => new Map(scope));
|
|
1738
|
+
ctx.recordArrayFieldScopes = cloneFieldScopes(snapshot.recordArrayField);
|
|
1739
|
+
ctx.recordScalarArrayFieldScopes = cloneFieldScopes(snapshot.recordScalarArrayField);
|
|
1740
|
+
ctx.maybeRecordArrayFieldScopes = cloneFieldScopes(snapshot.maybeRecordArrayField);
|
|
1741
|
+
}
|
|
1742
|
+
function strongestArrayBindingStatus(statuses) {
|
|
1743
|
+
if (statuses.includes('captured'))
|
|
1744
|
+
return 'captured';
|
|
1745
|
+
if (statuses.includes('stale'))
|
|
1746
|
+
return 'stale';
|
|
1747
|
+
if (statuses.includes('fresh'))
|
|
1748
|
+
return 'fresh';
|
|
1749
|
+
if (statuses.includes('fresh-push'))
|
|
1750
|
+
return 'fresh-push';
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
function mergeArrayScopes(base, outcomes) {
|
|
1754
|
+
const merged = base.map((scope) => new Map(scope));
|
|
1755
|
+
for (let scopeIndex = 0; scopeIndex < merged.length; scopeIndex++) {
|
|
1756
|
+
const names = new Set(merged[scopeIndex].keys());
|
|
1757
|
+
for (const outcome of outcomes)
|
|
1758
|
+
for (const name of outcome[scopeIndex]?.keys() ?? [])
|
|
1759
|
+
names.add(name);
|
|
1760
|
+
for (const name of names) {
|
|
1761
|
+
const status = strongestArrayBindingStatus(outcomes.map((outcome) => outcome[scopeIndex]?.get(name) ?? null));
|
|
1762
|
+
if (status === null)
|
|
1763
|
+
merged[scopeIndex].delete(name);
|
|
1764
|
+
else
|
|
1765
|
+
merged[scopeIndex].set(name, status);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return merged;
|
|
1769
|
+
}
|
|
1770
|
+
function mergeScalarArrayScopes(base, outcomes) {
|
|
1771
|
+
const merged = base.map((scope) => new Map(scope));
|
|
1772
|
+
for (let scopeIndex = 0; scopeIndex < merged.length; scopeIndex++) {
|
|
1773
|
+
const names = new Set(merged[scopeIndex].keys());
|
|
1774
|
+
for (const outcome of outcomes)
|
|
1775
|
+
for (const name of outcome[scopeIndex]?.keys() ?? [])
|
|
1776
|
+
names.add(name);
|
|
1777
|
+
for (const name of names) {
|
|
1778
|
+
const scalar = outcomes.length > 0 && outcomes.every((outcome) => outcome[scopeIndex]?.get(name) === true);
|
|
1779
|
+
if (scalar)
|
|
1780
|
+
merged[scopeIndex].set(name, true);
|
|
1781
|
+
else
|
|
1782
|
+
merged[scopeIndex].delete(name);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return merged;
|
|
1786
|
+
}
|
|
1787
|
+
function mergeRecordScopes(base, outcomes) {
|
|
1788
|
+
const merged = base.map((scope) => new Map(scope));
|
|
1789
|
+
for (let scopeIndex = 0; scopeIndex < merged.length; scopeIndex++) {
|
|
1790
|
+
const names = new Set(merged[scopeIndex].keys());
|
|
1791
|
+
for (const outcome of outcomes)
|
|
1792
|
+
for (const name of outcome[scopeIndex]?.keys() ?? [])
|
|
1793
|
+
names.add(name);
|
|
1794
|
+
for (const name of names) {
|
|
1795
|
+
merged[scopeIndex].set(name, outcomes.length > 0 && outcomes.every((outcome) => outcome[scopeIndex]?.get(name) === true));
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return merged;
|
|
1799
|
+
}
|
|
1800
|
+
function mergeFieldScopes(base, outcomes, mode) {
|
|
1801
|
+
const merged = base.map((scope) => new Map(scope));
|
|
1802
|
+
for (let scopeIndex = 0; scopeIndex < merged.length; scopeIndex++) {
|
|
1803
|
+
const names = new Set(merged[scopeIndex].keys());
|
|
1804
|
+
for (const outcome of outcomes)
|
|
1805
|
+
for (const name of outcome[scopeIndex]?.keys() ?? [])
|
|
1806
|
+
names.add(name);
|
|
1807
|
+
for (const name of names) {
|
|
1808
|
+
const outcomeFields = outcomes.map((outcome) => outcome[scopeIndex]?.get(name) ?? null);
|
|
1809
|
+
const fields = new Set();
|
|
1810
|
+
if (mode === 'union') {
|
|
1811
|
+
for (const set of outcomeFields)
|
|
1812
|
+
if (set)
|
|
1813
|
+
for (const field of set)
|
|
1814
|
+
fields.add(field);
|
|
1815
|
+
}
|
|
1816
|
+
else if (outcomeFields.length > 0 && outcomeFields.every((set) => set !== null)) {
|
|
1817
|
+
for (const field of outcomeFields[0] ?? []) {
|
|
1818
|
+
if (outcomeFields.every((set) => set?.has(field) === true))
|
|
1819
|
+
fields.add(field);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
merged[scopeIndex].set(name, fields.size > 0 ? fields : null);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return merged;
|
|
1826
|
+
}
|
|
1827
|
+
function mergeBranchBindingSnapshots(ctx, base, outcomes) {
|
|
1828
|
+
ctx.arrayBindingScopes = mergeArrayScopes(base.array, outcomes.map((outcome) => outcome.array));
|
|
1829
|
+
ctx.scalarArrayBindingScopes = mergeScalarArrayScopes(base.scalarArray, outcomes.map((outcome) => outcome.scalarArray));
|
|
1830
|
+
ctx.recordScopes = mergeRecordScopes(base.record, outcomes.map((outcome) => outcome.record));
|
|
1831
|
+
ctx.recordArrayFieldScopes = mergeFieldScopes(base.recordArrayField, outcomes.map((outcome) => outcome.recordArrayField), 'intersection');
|
|
1832
|
+
ctx.recordScalarArrayFieldScopes = mergeFieldScopes(base.recordScalarArrayField, outcomes.map((outcome) => outcome.recordScalarArrayField), 'intersection');
|
|
1833
|
+
ctx.maybeRecordArrayFieldScopes = mergeFieldScopes(base.maybeRecordArrayField, outcomes.map((outcome) => outcome.maybeRecordArrayField), 'union');
|
|
1834
|
+
}
|
|
1835
|
+
function lookupRecordBinding(ctx, name) {
|
|
1836
|
+
for (let i = ctx.recordScopes.length - 1; i >= 0; i--) {
|
|
1837
|
+
const scope = ctx.recordScopes[i];
|
|
1838
|
+
if (scope.has(name))
|
|
1839
|
+
return scope.get(name) === true;
|
|
1840
|
+
}
|
|
1841
|
+
return false;
|
|
1842
|
+
}
|
|
1843
|
+
function lookupRecordArrayField(ctx, name, field) {
|
|
1844
|
+
for (let i = ctx.recordArrayFieldScopes.length - 1; i >= 0; i--) {
|
|
1845
|
+
const scope = ctx.recordArrayFieldScopes[i];
|
|
1846
|
+
if (!scope.has(name))
|
|
1847
|
+
continue;
|
|
1848
|
+
return scope.get(name)?.has(field) === true;
|
|
1849
|
+
}
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
function lookupRecordScalarArrayField(ctx, name, field) {
|
|
1853
|
+
for (let i = ctx.recordScalarArrayFieldScopes.length - 1; i >= 0; i--) {
|
|
1854
|
+
const scope = ctx.recordScalarArrayFieldScopes[i];
|
|
1855
|
+
if (!scope.has(name))
|
|
1856
|
+
continue;
|
|
1857
|
+
return scope.get(name)?.has(field) === true;
|
|
1858
|
+
}
|
|
1859
|
+
return false;
|
|
1860
|
+
}
|
|
1861
|
+
function lookupMaybeRecordArrayField(ctx, name, field) {
|
|
1862
|
+
for (let i = ctx.maybeRecordArrayFieldScopes.length - 1; i >= 0; i--) {
|
|
1863
|
+
const scope = ctx.maybeRecordArrayFieldScopes[i];
|
|
1864
|
+
if (!scope.has(name))
|
|
1865
|
+
continue;
|
|
1866
|
+
return scope.get(name)?.has(field) === true;
|
|
1867
|
+
}
|
|
1868
|
+
return false;
|
|
1869
|
+
}
|
|
1870
|
+
function bindArrayStatusFromLet(ctx, name, valueIR) {
|
|
1871
|
+
if (valueIR.kind === 'arrayLit') {
|
|
1872
|
+
setArrayBindingStatus(ctx, name, valueIR.items.length === 0 ? 'fresh-push' : 'fresh');
|
|
1873
|
+
setScalarArrayBinding(ctx, name, arrayLiteralHasOnlyScalarElements(valueIR));
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (valueIR.kind === 'ident') {
|
|
1877
|
+
const sourceStatus = lookupArrayBindingStatus(ctx, valueIR.name);
|
|
1878
|
+
const sourceScalar = lookupScalarArrayBinding(ctx, valueIR.name);
|
|
1879
|
+
if (sourceStatus === 'fresh' || sourceStatus === 'fresh-push') {
|
|
1880
|
+
setDeclaringArrayBindingStatus(ctx, valueIR.name, 'stale');
|
|
1881
|
+
setDeclaringScalarArrayBinding(ctx, valueIR.name, false);
|
|
1882
|
+
setArrayBindingStatus(ctx, name, 'stale');
|
|
1883
|
+
setScalarArrayBinding(ctx, name, false);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (sourceStatus === 'captured') {
|
|
1887
|
+
setArrayBindingStatus(ctx, name, 'captured');
|
|
1888
|
+
setScalarArrayBinding(ctx, name, sourceScalar);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (sourceStatus === 'stale') {
|
|
1892
|
+
setArrayBindingStatus(ctx, name, 'stale');
|
|
1893
|
+
setScalarArrayBinding(ctx, name, false);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (valueIR.kind === 'member' &&
|
|
1898
|
+
valueIR.object.kind === 'ident' &&
|
|
1899
|
+
lookupRecordArrayField(ctx, valueIR.object.name, valueIR.property)) {
|
|
1900
|
+
setArrayBindingStatus(ctx, name, 'captured');
|
|
1901
|
+
setScalarArrayBinding(ctx, name, lookupRecordScalarArrayField(ctx, valueIR.object.name, valueIR.property));
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
setArrayBindingStatus(ctx, name, null);
|
|
1905
|
+
setScalarArrayBinding(ctx, name, false);
|
|
1906
|
+
}
|
|
1907
|
+
function rebindArrayOnReassign(ctx, name, valueIR) {
|
|
1908
|
+
if (valueIR.kind === 'arrayLit') {
|
|
1909
|
+
setDeclaringArrayBindingStatus(ctx, name, 'stale');
|
|
1910
|
+
setDeclaringScalarArrayBinding(ctx, name, false);
|
|
1911
|
+
}
|
|
1912
|
+
else {
|
|
1913
|
+
setDeclaringArrayBindingStatus(ctx, name, null);
|
|
1914
|
+
setDeclaringScalarArrayBinding(ctx, name, false);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
/** Reassign-invalidation for the record table — mirrors `rebindRegexOnReassign`
|
|
1918
|
+
* (and the TS emitter's `rebindRecordOnReassign`): owning-scope update, no
|
|
1919
|
+
* stale record classification after `assign target=r value=<non-record>`. */
|
|
1920
|
+
function rebindRecordOnReassign(ctx, name, valueIR) {
|
|
1921
|
+
const next = valueIR.kind === 'objectLit';
|
|
1922
|
+
const arrayFields = recordArrayFieldsForValue(valueIR, ctx);
|
|
1923
|
+
const scalarArrayFields = recordScalarArrayFieldsForValue(valueIR, ctx);
|
|
1924
|
+
for (let i = ctx.recordScopes.length - 1; i >= 0; i--) {
|
|
1925
|
+
const scope = ctx.recordScopes[i];
|
|
1926
|
+
if (scope.has(name)) {
|
|
1927
|
+
scope.set(name, next);
|
|
1928
|
+
ctx.recordArrayFieldScopes[i]?.set(name, next ? arrayFields : null);
|
|
1929
|
+
ctx.recordScalarArrayFieldScopes[i]?.set(name, next ? scalarArrayFields : null);
|
|
1930
|
+
ctx.maybeRecordArrayFieldScopes[i]?.set(name, next ? arrayFields : null);
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1442
1935
|
function assertAssignableLocalTarget(target, ctx) {
|
|
1443
1936
|
if (target.kind !== 'ident')
|
|
1444
1937
|
return;
|
|
@@ -1777,7 +2270,7 @@ function emitDoPy(node, ctx) {
|
|
|
1777
2270
|
ctx.usedPropagation = true;
|
|
1778
2271
|
return [`${tmp} = ${inner}`, `if ${tmp}.kind == 'err':`, errPropagationLine(tmp, ctx)];
|
|
1779
2272
|
}
|
|
1780
|
-
return [`${emitPyExprCtx(valueIR, ctx)}`];
|
|
2273
|
+
return [`${emitPyExprCtx(valueIR, ctx, { preserveFreshPush: true })}`];
|
|
1781
2274
|
}
|
|
1782
2275
|
/** ValueIR `kind`s that lower to Python literals/values and would trigger
|
|
1783
2276
|
* `TypeError: exceptions must derive from BaseException` if `raise`d
|
|
@@ -1822,12 +2315,213 @@ export function emitPyExpressionWithImports(node, options) {
|
|
|
1822
2315
|
if (outerBindings.length > 0) {
|
|
1823
2316
|
ctx.localScopes.push(new Map(outerBindings.map((n) => [n, 'const'])));
|
|
1824
2317
|
ctx.regexScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
2318
|
+
ctx.recordScopes.push(new Map(outerBindings.map((n) => [n, false])));
|
|
2319
|
+
ctx.recordArrayFieldScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
2320
|
+
ctx.recordScalarArrayFieldScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
2321
|
+
ctx.maybeRecordArrayFieldScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
2322
|
+
ctx.arrayBindingScopes.push(new Map());
|
|
2323
|
+
ctx.scalarArrayBindingScopes.push(new Map());
|
|
1825
2324
|
ctx.renameStack.push(new Map());
|
|
1826
2325
|
}
|
|
1827
2326
|
const code = emitPyExprCtx(node, ctx);
|
|
1828
2327
|
return { code, imports: ctx.imports, helpers: ctx.helpers };
|
|
1829
2328
|
}
|
|
1830
|
-
|
|
2329
|
+
const MUTATING_ARRAY_METHODS = new Set([
|
|
2330
|
+
'copyWithin',
|
|
2331
|
+
'fill',
|
|
2332
|
+
'pop',
|
|
2333
|
+
'push',
|
|
2334
|
+
'reverse',
|
|
2335
|
+
'shift',
|
|
2336
|
+
'sort',
|
|
2337
|
+
'splice',
|
|
2338
|
+
'unshift',
|
|
2339
|
+
]);
|
|
2340
|
+
function captureFreshArrayRecordSourcesPy(node, ctx) {
|
|
2341
|
+
const freshSources = new Set();
|
|
2342
|
+
for (const entry of node.entries) {
|
|
2343
|
+
if ('kind' in entry)
|
|
2344
|
+
continue;
|
|
2345
|
+
if (entry.value.kind === 'member' &&
|
|
2346
|
+
entry.value.object.kind === 'ident' &&
|
|
2347
|
+
lookupMaybeRecordArrayField(ctx, entry.value.object.name, entry.value.property)) {
|
|
2348
|
+
throw new Error(`record field "${entry.value.object.name}.${entry.value.property}" cannot be captured by another record field`);
|
|
2349
|
+
}
|
|
2350
|
+
if (entry.value.kind !== 'ident')
|
|
2351
|
+
continue;
|
|
2352
|
+
const status = lookupArrayBindingStatus(ctx, entry.value.name);
|
|
2353
|
+
if (status === 'fresh' || status === 'fresh-push') {
|
|
2354
|
+
if (freshSources.has(entry.value.name)) {
|
|
2355
|
+
throw new Error(`fresh array binding "${entry.value.name}" can be captured only once`);
|
|
2356
|
+
}
|
|
2357
|
+
assertFreshArrayCaptureNotInRepeatableLoopPy(ctx, entry.value.name);
|
|
2358
|
+
freshSources.add(entry.value.name);
|
|
2359
|
+
}
|
|
2360
|
+
else if (status === 'captured') {
|
|
2361
|
+
throw new Error(`fresh array binding "${entry.value.name}" was already captured by a record field`);
|
|
2362
|
+
}
|
|
2363
|
+
else if (status === 'stale') {
|
|
2364
|
+
throw new Error(`stale array binding "${entry.value.name}" cannot be captured by a record field`);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
for (const name of freshSources)
|
|
2368
|
+
setDeclaringArrayBindingStatus(ctx, name, 'captured');
|
|
2369
|
+
}
|
|
2370
|
+
function rootIdentName(node) {
|
|
2371
|
+
if (node.kind === 'ident')
|
|
2372
|
+
return node.name;
|
|
2373
|
+
if (node.kind === 'member' || node.kind === 'index')
|
|
2374
|
+
return rootIdentName(node.object);
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
function recordArrayFieldMutationTarget(node, ctx) {
|
|
2378
|
+
if (node.kind === 'member' &&
|
|
2379
|
+
node.object.kind === 'ident' &&
|
|
2380
|
+
lookupMaybeRecordArrayField(ctx, node.object.name, node.property)) {
|
|
2381
|
+
return { recordName: node.object.name, fieldName: node.property };
|
|
2382
|
+
}
|
|
2383
|
+
if (node.kind === 'member' || node.kind === 'index')
|
|
2384
|
+
return recordArrayFieldMutationTarget(node.object, ctx);
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
function isIntegerValuedFloatLiteralPy(node) {
|
|
2388
|
+
return (node.raw.includes('.') || /[eE]/.test(node.raw)) && Number.isInteger(node.value);
|
|
2389
|
+
}
|
|
2390
|
+
function isFreshnessPreservingPushElementPy(node) {
|
|
2391
|
+
if (node.kind === 'strLit' || node.kind === 'boolLit' || node.kind === 'nullLit')
|
|
2392
|
+
return true;
|
|
2393
|
+
if (node.kind !== 'numLit')
|
|
2394
|
+
return false;
|
|
2395
|
+
if (node.bigint || !Number.isFinite(node.value))
|
|
2396
|
+
return false;
|
|
2397
|
+
if (isIntegerValuedFloatLiteralPy(node))
|
|
2398
|
+
throw new Error('portable: float literal has an integer value (float/int divergence)');
|
|
2399
|
+
return true;
|
|
2400
|
+
}
|
|
2401
|
+
function pushMutationTargetPy(node) {
|
|
2402
|
+
if (node.kind !== 'call' || node.optional || node.args.length !== 1)
|
|
2403
|
+
return null;
|
|
2404
|
+
const callee = node.callee;
|
|
2405
|
+
if (callee.kind !== 'member' || callee.optional || callee.property !== 'push')
|
|
2406
|
+
return null;
|
|
2407
|
+
return { target: callee.object, element: node.args[0] };
|
|
2408
|
+
}
|
|
2409
|
+
function applyArrayMutationTargetPy(target, ctx, preserveFreshPush = false) {
|
|
2410
|
+
const recordField = recordArrayFieldMutationTarget(target, ctx);
|
|
2411
|
+
if (recordField !== null) {
|
|
2412
|
+
throw new Error(`record array field "${recordField.recordName}.${recordField.fieldName}" cannot be mutated after capture`);
|
|
2413
|
+
}
|
|
2414
|
+
const targetName = rootIdentName(target);
|
|
2415
|
+
if (targetName === null)
|
|
2416
|
+
return;
|
|
2417
|
+
const status = lookupArrayBindingStatus(ctx, targetName);
|
|
2418
|
+
if (status === 'captured') {
|
|
2419
|
+
throw new Error(`fresh array binding "${targetName}" was already captured by a record field`);
|
|
2420
|
+
}
|
|
2421
|
+
if (status === 'fresh-push' && preserveFreshPush)
|
|
2422
|
+
return;
|
|
2423
|
+
if (status === 'fresh' || status === 'fresh-push')
|
|
2424
|
+
setDeclaringArrayBindingStatus(ctx, targetName, 'stale');
|
|
2425
|
+
}
|
|
2426
|
+
function applyArrayMutationDoPy(node, ctx, preserveFreshPush = false) {
|
|
2427
|
+
const pushTarget = pushMutationTargetPy(node);
|
|
2428
|
+
if (pushTarget !== null) {
|
|
2429
|
+
const keepFresh = preserveFreshPush && isFreshnessPreservingPushElementPy(pushTarget.element);
|
|
2430
|
+
applyArrayMutationTargetPy(pushTarget.target, ctx, keepFresh);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
if (node.kind !== 'call')
|
|
2434
|
+
return;
|
|
2435
|
+
const callee = node.callee;
|
|
2436
|
+
if (callee.kind === 'member' && MUTATING_ARRAY_METHODS.has(callee.property))
|
|
2437
|
+
applyArrayMutationTargetPy(callee.object, ctx);
|
|
2438
|
+
}
|
|
2439
|
+
function applyArrayMutationAssignPy(target, ctx) {
|
|
2440
|
+
if (target.kind === 'ident') {
|
|
2441
|
+
const status = lookupArrayBindingStatus(ctx, target.name);
|
|
2442
|
+
if (status === 'captured') {
|
|
2443
|
+
throw new Error(`fresh array binding "${target.name}" was already captured by a record field`);
|
|
2444
|
+
}
|
|
2445
|
+
if (status === 'fresh' || status === 'fresh-push' || status === 'stale') {
|
|
2446
|
+
throw new Error(`array binding "${target.name}" cannot be reassigned by portable assign`);
|
|
2447
|
+
}
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
applyArrayMutationTargetPy(target, ctx);
|
|
2451
|
+
}
|
|
2452
|
+
function forEachValueIRChildPy(node, visit) {
|
|
2453
|
+
if (node.kind === 'member')
|
|
2454
|
+
visit(node.object);
|
|
2455
|
+
else if (node.kind === 'index') {
|
|
2456
|
+
visit(node.object);
|
|
2457
|
+
visit(node.index);
|
|
2458
|
+
}
|
|
2459
|
+
else if (node.kind === 'call') {
|
|
2460
|
+
visit(node.callee);
|
|
2461
|
+
for (const arg of node.args)
|
|
2462
|
+
visit(arg);
|
|
2463
|
+
}
|
|
2464
|
+
else if (node.kind === 'binary') {
|
|
2465
|
+
visit(node.left);
|
|
2466
|
+
visit(node.right);
|
|
2467
|
+
}
|
|
2468
|
+
else if (node.kind === 'unary' ||
|
|
2469
|
+
node.kind === 'spread' ||
|
|
2470
|
+
node.kind === 'await' ||
|
|
2471
|
+
node.kind === 'new' ||
|
|
2472
|
+
node.kind === 'propagate') {
|
|
2473
|
+
visit(node.argument);
|
|
2474
|
+
}
|
|
2475
|
+
else if (node.kind === 'typeAssert' || node.kind === 'nonNull')
|
|
2476
|
+
visit(node.expression);
|
|
2477
|
+
else if (node.kind === 'conditional') {
|
|
2478
|
+
visit(node.test);
|
|
2479
|
+
visit(node.consequent);
|
|
2480
|
+
visit(node.alternate);
|
|
2481
|
+
}
|
|
2482
|
+
else if (node.kind === 'tmplLit')
|
|
2483
|
+
for (const expr of node.expressions)
|
|
2484
|
+
visit(expr);
|
|
2485
|
+
else if (node.kind === 'objectLit') {
|
|
2486
|
+
for (const entry of node.entries) {
|
|
2487
|
+
if ('kind' in entry && entry.kind === 'spread')
|
|
2488
|
+
visit(entry.argument);
|
|
2489
|
+
else
|
|
2490
|
+
visit(entry.value);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
else if (node.kind === 'arrayLit')
|
|
2494
|
+
for (const item of node.items)
|
|
2495
|
+
visit(item);
|
|
2496
|
+
else if (node.kind === 'lambda' && node.body)
|
|
2497
|
+
visit(node.body);
|
|
2498
|
+
}
|
|
2499
|
+
function applyArrayMutationsInExpressionPy(node, ctx, preserveFreshPush = false) {
|
|
2500
|
+
applyArrayMutationDoPy(node, ctx, preserveFreshPush);
|
|
2501
|
+
// Only a direct `do xs.push(<scalar>)` statement certifies the push-built freshness chain.
|
|
2502
|
+
// Nested pushes inside a larger expression are still mutation expressions and must stale.
|
|
2503
|
+
forEachValueIRChildPy(node, (child) => applyArrayMutationsInExpressionPy(child, ctx));
|
|
2504
|
+
}
|
|
2505
|
+
function assertRecordArrayFieldReadsProvenPy(node, ctx) {
|
|
2506
|
+
if (node.kind === 'member' &&
|
|
2507
|
+
node.object.kind === 'ident' &&
|
|
2508
|
+
lookupMaybeRecordArrayField(ctx, node.object.name, node.property) &&
|
|
2509
|
+
(node.optional || isParenthesized(node.object))) {
|
|
2510
|
+
throw new Error(`record array field "${node.object.name}.${node.property}" must use a bare non-optional receiver`);
|
|
2511
|
+
}
|
|
2512
|
+
if (node.kind === 'member' &&
|
|
2513
|
+
node.object.kind === 'ident' &&
|
|
2514
|
+
lookupMaybeRecordArrayField(ctx, node.object.name, node.property) &&
|
|
2515
|
+
!lookupRecordArrayField(ctx, node.object.name, node.property)) {
|
|
2516
|
+
throw new Error(`record array field "${node.object.name}.${node.property}" is not proven on every branch`);
|
|
2517
|
+
}
|
|
2518
|
+
forEachValueIRChildPy(node, (child) => assertRecordArrayFieldReadsProvenPy(child, ctx));
|
|
2519
|
+
}
|
|
2520
|
+
function emitPyExprCtx(node, ctx, options = {}) {
|
|
2521
|
+
applyArrayMutationsInExpressionPy(node, ctx, options.preserveFreshPush === true);
|
|
2522
|
+
assertRecordArrayFieldReadsProvenPy(node, ctx);
|
|
2523
|
+
if (node.kind === 'objectLit')
|
|
2524
|
+
captureFreshArrayRecordSourcesPy(node, ctx);
|
|
1831
2525
|
switch (node.kind) {
|
|
1832
2526
|
case 'numLit':
|
|
1833
2527
|
return node.raw;
|
|
@@ -2255,7 +2949,10 @@ function emitPyExprCtx(node, ctx) {
|
|
|
2255
2949
|
}
|
|
2256
2950
|
case 'objectLit': {
|
|
2257
2951
|
// Slice 2d — Python dict literal. Keys are ALWAYS double-quoted (no
|
|
2258
|
-
// shorthand-key syntax in Python).
|
|
2952
|
+
// shorthand-key syntax in Python). Emits the BASE plain-dict form —
|
|
2953
|
+
// record-binding `let` initializers (the ONLY position where field
|
|
2954
|
+
// reads are certified) wrap in __DotDict at the let site, so every
|
|
2955
|
+
// other object-literal position keeps its pre-slice bytes.
|
|
2259
2956
|
const entries = node.entries.map((e) => {
|
|
2260
2957
|
if ('kind' in e && e.kind === 'spread') {
|
|
2261
2958
|
return `**${emitPyExprCtx(e.argument, ctx)}`;
|
|
@@ -2777,9 +3474,118 @@ function lowerOptionalLink(inner, objectNode, ctx) {
|
|
|
2777
3474
|
const presence = optionalPresenceTest(`${tmp} := ${inner.expr}`, ctx);
|
|
2778
3475
|
return { guard: presence, branchRef: tmp };
|
|
2779
3476
|
}
|
|
3477
|
+
/** Nested-record-field receiver, LOCKSTEP with the TS twin
|
|
3478
|
+
* (`nestedRecordFieldReceiver` in codegen-expression.ts): fires ONLY when the
|
|
3479
|
+
* binding table proves the SOURCE-named ident is a record literal; any other
|
|
3480
|
+
* two-level chain (`this.data.filter(...)`, object params, unproven idents,
|
|
3481
|
+
* parenthesized receivers) keeps its base lowering. INVARIANT: the lookup is
|
|
3482
|
+
* by SOURCE name, but the EMITTED name goes through `ctx.symbolMap` — Python's
|
|
3483
|
+
* flat function scope renames shadowed bindings, while the TS leg never
|
|
3484
|
+
* renames (real block scoping), which is why the TS twin has no remap. */
|
|
3485
|
+
function provenRecordFieldReceiverPy(node, ctx) {
|
|
3486
|
+
if (node.kind !== 'member' || node.optional)
|
|
3487
|
+
return null;
|
|
3488
|
+
if (node.object.kind !== 'ident')
|
|
3489
|
+
return null;
|
|
3490
|
+
if (!lookupRecordBinding(ctx, node.object.name))
|
|
3491
|
+
return null;
|
|
3492
|
+
if (isParenthesized(node.object))
|
|
3493
|
+
return null;
|
|
3494
|
+
return { record: emitScopedIdentPy(ctx, node.object.name), field: node.property };
|
|
3495
|
+
}
|
|
3496
|
+
function nestedRecordFieldReceiverPy(node, ctx) {
|
|
3497
|
+
if (node.kind !== 'member' || node.object.kind !== 'ident')
|
|
3498
|
+
return null;
|
|
3499
|
+
const proven = provenRecordFieldReceiverPy(node, ctx);
|
|
3500
|
+
if (proven === null)
|
|
3501
|
+
return null;
|
|
3502
|
+
if (!lookupRecordArrayField(ctx, node.object.name, node.property))
|
|
3503
|
+
return null;
|
|
3504
|
+
return proven;
|
|
3505
|
+
}
|
|
3506
|
+
function nestedRecordFieldNonArrayReceiverPy(node, ctx) {
|
|
3507
|
+
if (node.kind !== 'member' || node.object.kind !== 'ident')
|
|
3508
|
+
return null;
|
|
3509
|
+
const proven = provenRecordFieldReceiverPy(node, ctx);
|
|
3510
|
+
if (proven === null)
|
|
3511
|
+
return null;
|
|
3512
|
+
if (lookupRecordArrayField(ctx, node.object.name, node.property))
|
|
3513
|
+
return null;
|
|
3514
|
+
return proven;
|
|
3515
|
+
}
|
|
3516
|
+
function emitEachIterablePy(node, ctx) {
|
|
3517
|
+
if (node.kind === 'member' && node.object.kind === 'ident') {
|
|
3518
|
+
if (!lookupRecordBinding(ctx, node.object.name))
|
|
3519
|
+
return emitPyExprCtx(node, ctx);
|
|
3520
|
+
if (node.optional || isParenthesized(node.object)) {
|
|
3521
|
+
throw new Error(`each nested record-array receiver "${node.object.name}.${node.property}" is not proven`);
|
|
3522
|
+
}
|
|
3523
|
+
if (lookupMaybeRecordArrayField(ctx, node.object.name, node.property) &&
|
|
3524
|
+
!lookupRecordArrayField(ctx, node.object.name, node.property)) {
|
|
3525
|
+
throw new Error(`record array field "${node.object.name}.${node.property}" is not proven on every branch`);
|
|
3526
|
+
}
|
|
3527
|
+
if (lookupRecordArrayField(ctx, node.object.name, node.property) &&
|
|
3528
|
+
!lookupRecordScalarArrayField(ctx, node.object.name, node.property)) {
|
|
3529
|
+
throw new Error(`record array field "${node.object.name}.${node.property}" elements are not proven portable scalars`);
|
|
3530
|
+
}
|
|
3531
|
+
ctx.helpers.add(KERN_NESTED_ARRAY_REF_HELPER_PY);
|
|
3532
|
+
ctx.helpers.add(KERN_NESTED_ARRAY_ITER_HELPER_PY);
|
|
3533
|
+
const record = emitScopedIdentPy(ctx, node.object.name);
|
|
3534
|
+
return `_kern_nested_array_iter(${record}, ${JSON.stringify(node.property)})`;
|
|
3535
|
+
}
|
|
3536
|
+
return emitPyExprCtx(node, ctx);
|
|
3537
|
+
}
|
|
3538
|
+
function assertNoKeyedNestedRecordReceiverPy(node, ctx) {
|
|
3539
|
+
if (node.kind !== 'member' || node.object.kind !== 'ident')
|
|
3540
|
+
return;
|
|
3541
|
+
if (!lookupRecordBinding(ctx, node.object.name))
|
|
3542
|
+
return;
|
|
3543
|
+
if (node.optional || isParenthesized(node.object))
|
|
3544
|
+
return;
|
|
3545
|
+
throw new Error(`keyed iteration over nested record field "${node.object.name}.${node.property}" is outside the portable domain`);
|
|
3546
|
+
}
|
|
2780
3547
|
function lowerChain(node, ctx) {
|
|
2781
3548
|
if (node.kind === 'member') {
|
|
2782
3549
|
const obj = node.object;
|
|
3550
|
+
const directArrayField = nestedRecordFieldReceiverPy(node, ctx);
|
|
3551
|
+
if (directArrayField !== null) {
|
|
3552
|
+
ctx.helpers.add(KERN_NESTED_ARRAY_REF_HELPER_PY);
|
|
3553
|
+
return {
|
|
3554
|
+
guard: null,
|
|
3555
|
+
expr: `_kern_nested_array_ref(${directArrayField.record}, ${JSON.stringify(directArrayField.field)})`,
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
const nested = nestedRecordFieldReceiverPy(obj, ctx);
|
|
3559
|
+
if (nested !== null) {
|
|
3560
|
+
if (node.optional)
|
|
3561
|
+
throw new Error('portable: optional nested member access is outside the portable domain');
|
|
3562
|
+
if (node.property !== 'length') {
|
|
3563
|
+
ctx.helpers.add(KERN_NESTED_NO_PROPERTY_HELPER_PY);
|
|
3564
|
+
return {
|
|
3565
|
+
guard: null,
|
|
3566
|
+
expr: `_kern_nested_no_property(${JSON.stringify(`${nested.record}.${nested.field}`)}, ${JSON.stringify(node.property)})`,
|
|
3567
|
+
};
|
|
3568
|
+
}
|
|
3569
|
+
ctx.helpers.add(KERN_NESTED_ARRAY_HELPER_PY);
|
|
3570
|
+
return { guard: null, expr: `_kern_nested_array_value(${nested.record}, ${JSON.stringify(nested.field)})` };
|
|
3571
|
+
}
|
|
3572
|
+
const nonArrayNested = nestedRecordFieldNonArrayReceiverPy(obj, ctx);
|
|
3573
|
+
if (nonArrayNested !== null) {
|
|
3574
|
+
if (node.optional)
|
|
3575
|
+
throw new Error('portable: optional nested member access is outside the portable domain');
|
|
3576
|
+
if (node.property !== 'length') {
|
|
3577
|
+
ctx.helpers.add(KERN_NESTED_NO_PROPERTY_HELPER_PY);
|
|
3578
|
+
return {
|
|
3579
|
+
guard: null,
|
|
3580
|
+
expr: `_kern_nested_no_property(${JSON.stringify(`${nonArrayNested.record}.${nonArrayNested.field}`)}, ${JSON.stringify(node.property)})`,
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
ctx.helpers.add(KERN_NESTED_ARRAY_HELPER_PY);
|
|
3584
|
+
return {
|
|
3585
|
+
guard: null,
|
|
3586
|
+
expr: `_kern_nested_array_value(${nonArrayNested.record}, ${JSON.stringify(nonArrayNested.field)})`,
|
|
3587
|
+
};
|
|
3588
|
+
}
|
|
2783
3589
|
// Error-substrate Slice 1 — a `<caughtBinding>.message` read lowers to
|
|
2784
3590
|
// `str(<caughtBinding>)`. Python exceptions have NO `.message` attribute
|
|
2785
3591
|
// (a bare `e.message` raises AttributeError), but `str(Exception("x"))`
|
|
@@ -2829,6 +3635,11 @@ function lowerChain(node, ctx) {
|
|
|
2829
3635
|
const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
|
|
2830
3636
|
? lowerChain(obj, ctx)
|
|
2831
3637
|
: { guard: null, expr: wrapCompoundRootExpr(obj, emitPyExprCtx(obj, ctx)) };
|
|
3638
|
+
// Nested-values slice-1 — a PROVEN record binding's single-level member
|
|
3639
|
+
// read must stay an ATTRIBUTE read even for shared array property names
|
|
3640
|
+
// (`r.length` is the scalar FIELD "length" on a __DotDict, never len(r));
|
|
3641
|
+
// every other receiver keeps the base shared list-ops lowering below.
|
|
3642
|
+
const recordReceiver = obj.kind === 'ident' && lookupRecordBinding(ctx, obj.name);
|
|
2832
3643
|
// Portable Array *property* read (non-call `.length`) lowers through the
|
|
2833
3644
|
// SAME shared list-ops hook the route emitter uses, so `this.items.length`
|
|
2834
3645
|
// emits `len(self.items)` (not invalid `self.items.length`) — identical to
|
|
@@ -2846,18 +3657,52 @@ function lowerChain(node, ctx) {
|
|
|
2846
3657
|
// receiver is not itself an optional chain ⇒ `inner.guard === null`); a
|
|
2847
3658
|
// non-pure receiver UNDER an existing optional chain still throws below.
|
|
2848
3659
|
const opt = lowerOptionalLink(inner, node.object, ctx);
|
|
2849
|
-
const linkExpr = isSharedPortableArrayProperty(node.property)
|
|
3660
|
+
const linkExpr = !recordReceiver && isSharedPortableArrayProperty(node.property)
|
|
2850
3661
|
? (lowerPortableArrayPropertyPy(opt.branchRef, node.property) ?? `${opt.branchRef}.${node.property}`)
|
|
2851
3662
|
: `${opt.branchRef}.${node.property}`;
|
|
2852
3663
|
return { guard: opt.guard, expr: linkExpr, lambdaBind: opt.lambdaBind };
|
|
2853
3664
|
}
|
|
2854
|
-
const linkExpr = isSharedPortableArrayProperty(node.property)
|
|
3665
|
+
const linkExpr = !recordReceiver && isSharedPortableArrayProperty(node.property)
|
|
2855
3666
|
? (lowerPortableArrayPropertyPy(inner.expr, node.property) ?? `${inner.expr}.${node.property}`)
|
|
2856
3667
|
: `${inner.expr}.${node.property}`;
|
|
2857
3668
|
return { guard: inner.guard, expr: linkExpr, lambdaBind: inner.lambdaBind };
|
|
2858
3669
|
}
|
|
2859
3670
|
if (node.kind === 'index') {
|
|
2860
3671
|
const obj = node.object;
|
|
3672
|
+
const nested = nestedRecordFieldReceiverPy(obj, ctx);
|
|
3673
|
+
if (nested !== null) {
|
|
3674
|
+
if (node.optional)
|
|
3675
|
+
throw new Error('portable: optional nested index access is outside the portable domain');
|
|
3676
|
+
ctx.helpers.add(KERN_NESTED_ARRAY_HELPER_PY);
|
|
3677
|
+
if (!isSafeIntegerLiteralIndex(node.index)) {
|
|
3678
|
+
return {
|
|
3679
|
+
guard: null,
|
|
3680
|
+
expr: `_kern_nested_array_value(${nested.record}, ${JSON.stringify(nested.field)}, "__kern_invalid_index__")`,
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
const literalIndex = node.index;
|
|
3684
|
+
return {
|
|
3685
|
+
guard: null,
|
|
3686
|
+
expr: `_kern_nested_array_value(${nested.record}, ${JSON.stringify(nested.field)}, ${literalIndex.raw})`,
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
const nonArrayNested = nestedRecordFieldNonArrayReceiverPy(obj, ctx);
|
|
3690
|
+
if (nonArrayNested !== null) {
|
|
3691
|
+
if (node.optional)
|
|
3692
|
+
throw new Error('portable: optional nested index access is outside the portable domain');
|
|
3693
|
+
ctx.helpers.add(KERN_NESTED_ARRAY_HELPER_PY);
|
|
3694
|
+
if (!isSafeIntegerLiteralIndex(node.index)) {
|
|
3695
|
+
return {
|
|
3696
|
+
guard: null,
|
|
3697
|
+
expr: `_kern_nested_array_value(${nonArrayNested.record}, ${JSON.stringify(nonArrayNested.field)}, "__kern_invalid_index__")`,
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
const literalIndex = node.index;
|
|
3701
|
+
return {
|
|
3702
|
+
guard: null,
|
|
3703
|
+
expr: `_kern_nested_array_value(${nonArrayNested.record}, ${JSON.stringify(nonArrayNested.field)}, ${literalIndex.raw})`,
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
2861
3706
|
// Slice 2 review fix — the bracket (`index`) form of a regex-literal
|
|
2862
3707
|
// property access (`/x/["source"]`, `/x/["flags"]`, `/x/["test"](s)`)
|
|
2863
3708
|
// launders the pattern/flags back to a string exactly like the dotted
|
|
@@ -2906,6 +3751,28 @@ function lowerChain(node, ctx) {
|
|
|
2906
3751
|
throw new Error("Optional call '?.()' is not yet supported on Python target. " +
|
|
2907
3752
|
'Bind the function reference to a `let` first, then test for `none` before calling.');
|
|
2908
3753
|
}
|
|
3754
|
+
// Nested-values slice-1 CHOKE POINT (lockstep with the TS call case's
|
|
3755
|
+
// identical guard): ANY method call on a PROVEN record binding's array
|
|
3756
|
+
// field fails closed BEFORE any per-method lowering (regex / lambda-array /
|
|
3757
|
+
// portable-array) can admit it — the runner abstains on every such program.
|
|
3758
|
+
if (node.callee.kind === 'member' && !node.callee.optional) {
|
|
3759
|
+
const nestedRecv = nestedRecordFieldReceiverPy(node.callee.object, ctx);
|
|
3760
|
+
if (nestedRecv !== null) {
|
|
3761
|
+
ctx.helpers.add(KERN_NESTED_NO_PROPERTY_HELPER_PY);
|
|
3762
|
+
return {
|
|
3763
|
+
guard: null,
|
|
3764
|
+
expr: `_kern_nested_no_property(${JSON.stringify(`${nestedRecv.record}.${nestedRecv.field}`)}, ${JSON.stringify(node.callee.property)})`,
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
const nonArrayNested = nestedRecordFieldNonArrayReceiverPy(node.callee.object, ctx);
|
|
3768
|
+
if (nonArrayNested !== null) {
|
|
3769
|
+
ctx.helpers.add(KERN_NESTED_NO_PROPERTY_HELPER_PY);
|
|
3770
|
+
return {
|
|
3771
|
+
guard: null,
|
|
3772
|
+
expr: `_kern_nested_no_property(${JSON.stringify(`${nonArrayNested.record}.${nonArrayNested.field}`)}, ${JSON.stringify(node.callee.property)})`,
|
|
3773
|
+
};
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
2909
3776
|
// Slice 2a — KERN-stdlib dispatch must run on a top-level Module.method
|
|
2910
3777
|
// call BEFORE we descend into the callee chain, so `Number.floor(x)`
|
|
2911
3778
|
// doesn't degrade into a non-stdlib `Number.floor(x)` Python emit.
|
|
@@ -4407,7 +5274,9 @@ function emitExpressionV1Py(node, ctx) {
|
|
|
4407
5274
|
declareLocalBinding(ctx, userName, 'const');
|
|
4408
5275
|
const name = maybeRenameOnShadow(ctx, userName);
|
|
4409
5276
|
setRegexBinding(ctx, userName, exprIR.kind === 'regexLit' ? exprIR : null);
|
|
4410
|
-
|
|
5277
|
+
setRecordBinding(ctx, userName, exprIR.kind === 'objectLit', recordArrayFieldsForValue(exprIR, ctx), recordScalarArrayFieldsForValue(exprIR, ctx));
|
|
5278
|
+
bindArrayStatusFromLet(ctx, userName, exprIR);
|
|
5279
|
+
const lines = [`${name} = ${emitLetInitializerPy(exprIR, ctx)}`];
|
|
4411
5280
|
if (ctx.traceHooks?.letAssign)
|
|
4412
5281
|
lines.push(letAssignTracePy(name));
|
|
4413
5282
|
return lines;
|