@kernlang/python 4.0.0 → 4.0.1-canary.224.1.1a92ac0a
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 +13 -0
- package/dist/codegen-body-python.js +1140 -145
- package/dist/codegen-body-python.js.map +1 -1
- package/dist/core/emit-models.js +5 -1
- package/dist/core/emit-models.js.map +1 -1
- package/dist/core/expr/helpers.d.ts +45 -0
- package/dist/core/expr/helpers.js +509 -5
- package/dist/core/expr/helpers.js.map +1 -1
- package/dist/core/expr/index.d.ts +1 -1
- package/dist/core/expr/index.js +137 -9
- package/dist/core/expr/index.js.map +1 -1
- package/dist/core/expr/list-ops.d.ts +10 -1
- package/dist/core/expr/list-ops.js +5 -9
- package/dist/core/expr/list-ops.js.map +1 -1
- package/dist/generators/core.js +5 -1
- package/dist/generators/core.js.map +1 -1
- package/dist/generators/ground.d.ts +13 -0
- package/dist/generators/ground.js +117 -12
- package/dist/generators/ground.js.map +1 -1
- package/package.json +2 -2
|
@@ -40,11 +40,24 @@
|
|
|
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,
|
|
43
|
+
import { applyTemplate, classifyRegexLiteralIndexReadFailClose, classifyRegexLiteralMemberReadFailClose, emitStringKeyArray, expandRegexIFold, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isHostNamespaceRoot, isPostfixMutationOperator, isSupportedAssignOperator, isZeroWidthCapableRegex, KERN_STDLIB_MODULES, 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, validateRegexNamedGroupsPortable, } from '@kernlang/core';
|
|
44
|
+
// Slice 0.9 — the TypeScript-AST closure helpers + classifier live on the Node
|
|
45
|
+
// subpath (the barrel is browser-safe). Python codegen is Node-side and parses
|
|
46
|
+
// block-bodied arrows, so it injects `typescriptClosureClassifier`.
|
|
47
|
+
import { collectFreeIdentifierNames, lowerJsClosureBodyToPython, typescriptClosureClassifier, } from '@kernlang/core/node';
|
|
44
48
|
import { buildPythonParamList } from './codegen-helpers.js';
|
|
45
|
-
import { KERN_FMT_HELPER_PY,
|
|
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';
|
|
46
50
|
import { isSharedPortableArrayMethod, isSharedPortableArrayProperty, lowerPortableArrayMethodPy, lowerPortableArrayPropertyPy, sharedPortableMethodRequiresPureReceiver, } from './core/expr/list-ops.js';
|
|
47
51
|
import { mapTsTypeToPython } from './type-map.js';
|
|
52
|
+
/** Parse options for Python codegen — always inject the TypeScript-backed
|
|
53
|
+
* closure classifier so block-bodied arrows parse (slice 0.9). */
|
|
54
|
+
const TS_PARSE_OPTS = { closureClassifier: typescriptClosureClassifier };
|
|
55
|
+
/** Local `parseExpression` binding for Python codegen that always injects the
|
|
56
|
+
* TypeScript closure classifier (slice 0.9). All `parseExpr(...)` calls in this
|
|
57
|
+
* module route through here. */
|
|
58
|
+
function parseExpr(input) {
|
|
59
|
+
return parseExpression(input, TS_PARSE_OPTS);
|
|
60
|
+
}
|
|
48
61
|
const INDENT_STEP = ' ';
|
|
49
62
|
function freshCtx(options) {
|
|
50
63
|
return {
|
|
@@ -350,12 +363,16 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
350
363
|
}
|
|
351
364
|
else if (child.type === 'if') {
|
|
352
365
|
const condRaw = String(child.props?.cond ?? '');
|
|
353
|
-
const condIR =
|
|
366
|
+
const condIR = parseExpr(condRaw);
|
|
354
367
|
// Slice-2 review fix: reject propagation `?` in `if cond=` (parallel to TS side).
|
|
355
368
|
if (condIR.kind === 'propagate') {
|
|
356
369
|
throw new Error("Propagation '?' is not allowed in `if cond=` — bind the call to a `let` first, then test the bound name.");
|
|
357
370
|
}
|
|
358
|
-
|
|
371
|
+
// Slice S4 — `if cond=` consumes KERN ToBoolean, not Python bare
|
|
372
|
+
// truthiness, so `if []:`/`if {}:` are truthy and `if NaN:` is falsy.
|
|
373
|
+
// Wrap UNCONDITIONALLY (no "looks boolean" skip-analysis in v1).
|
|
374
|
+
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
375
|
+
lines.push(`${indent}if _kern_truthy(${emitPyExprCtx(condIR, ctx)}):`);
|
|
359
376
|
const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP);
|
|
360
377
|
if (inner.length === 0)
|
|
361
378
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
@@ -376,11 +393,13 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
376
393
|
if (isChainable) {
|
|
377
394
|
const ifNode = ec[0];
|
|
378
395
|
const nestedCondRaw = String(ifNode.props?.cond ?? '');
|
|
379
|
-
const nestedCondIR =
|
|
396
|
+
const nestedCondIR = parseExpr(nestedCondRaw);
|
|
380
397
|
if (nestedCondIR.kind === 'propagate') {
|
|
381
398
|
throw new Error("Propagation '?' is not allowed in `if cond=` — bind the call to a `let` first, then test the bound name.");
|
|
382
399
|
}
|
|
383
|
-
|
|
400
|
+
// Slice S4 — `else if` chains consume KERN ToBoolean too.
|
|
401
|
+
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
402
|
+
lines.push(`${indent}elif _kern_truthy(${emitPyExprCtx(nestedCondIR, ctx)}):`);
|
|
384
403
|
const ifInner = emitChildrenPy(ifNode.children ?? [], ctx, indent + INDENT_STEP);
|
|
385
404
|
if (ifInner.length === 0)
|
|
386
405
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
@@ -405,7 +424,7 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
405
424
|
}
|
|
406
425
|
else if (child.type === 'while') {
|
|
407
426
|
const condRaw = String(child.props?.cond ?? '');
|
|
408
|
-
const condIR =
|
|
427
|
+
const condIR = parseExpr(condRaw);
|
|
409
428
|
if (condIR.kind === 'propagate') {
|
|
410
429
|
throw new Error("Propagation '?' is not allowed in `while cond=` — bind the call to a `let` first, then test the bound name.");
|
|
411
430
|
}
|
|
@@ -515,7 +534,7 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
515
534
|
// Slice 4c+4d review fix (Codex P1) — read schema-compliant
|
|
516
535
|
// `name`/`in` props (legacy `list`/`as` accepted as fallback).
|
|
517
536
|
const listRaw = String(child.props?.in ?? child.props?.list ?? '[]');
|
|
518
|
-
const listIR =
|
|
537
|
+
const listIR = parseExpr(listRaw);
|
|
519
538
|
const pairKey = child.props?.pairKey;
|
|
520
539
|
const pairValue = child.props?.pairValue;
|
|
521
540
|
const entryKey = child.props?.entryKey;
|
|
@@ -757,9 +776,9 @@ function emitRangeForPy(node, ctx, indent) {
|
|
|
757
776
|
validateIntegerRangeBound(String(rawFrom), 'from');
|
|
758
777
|
validateIntegerRangeBound(String(rawTo), 'to');
|
|
759
778
|
validatePositiveRangeStep(rawStep);
|
|
760
|
-
const fromIR =
|
|
761
|
-
const toIR =
|
|
762
|
-
const stepIR =
|
|
779
|
+
const fromIR = parseExpr(String(rawFrom));
|
|
780
|
+
const toIR = parseExpr(String(rawTo));
|
|
781
|
+
const stepIR = parseExpr(rawStep);
|
|
763
782
|
if (fromIR.kind === 'propagate' || toIR.kind === 'propagate' || stepIR.kind === 'propagate') {
|
|
764
783
|
throw new Error("Propagation '?' is not allowed in `for from=`/`to=`/`step=` — bind the value to a `let` before the loop.");
|
|
765
784
|
}
|
|
@@ -821,7 +840,7 @@ function emitWithPy(node, ctx, indent) {
|
|
|
821
840
|
throw new Error('body-statement `with` requires `cleanup=` (or set `protocol=with` to use __exit__).');
|
|
822
841
|
}
|
|
823
842
|
const name = String(rawName);
|
|
824
|
-
const valueIR =
|
|
843
|
+
const valueIR = parseExpr(String(rawValue));
|
|
825
844
|
if (valueIR.kind === 'propagate') {
|
|
826
845
|
throw new Error("Propagation '?' is not allowed in `with value=` — bind to `let` first.");
|
|
827
846
|
}
|
|
@@ -835,7 +854,7 @@ function emitWithPy(node, ctx, indent) {
|
|
|
835
854
|
lines.push(line);
|
|
836
855
|
return lines;
|
|
837
856
|
}
|
|
838
|
-
const cleanupIR =
|
|
857
|
+
const cleanupIR = parseExpr(String(rawCleanup));
|
|
839
858
|
if (cleanupIR.kind === 'propagate') {
|
|
840
859
|
throw new Error("Propagation '?' is not allowed in `with cleanup=` — bind to `let` first.");
|
|
841
860
|
}
|
|
@@ -882,7 +901,7 @@ function emitBranchPy(node, ctx, indent) {
|
|
|
882
901
|
if (onRaw === '') {
|
|
883
902
|
throw new Error('`branch` requires an `on=` expression in body-statement context.');
|
|
884
903
|
}
|
|
885
|
-
const onIR =
|
|
904
|
+
const onIR = parseExpr(onRaw);
|
|
886
905
|
const subjectVar = `__k_branch_${++ctx.gensymCounter}`;
|
|
887
906
|
const out = [];
|
|
888
907
|
out.push(`${indent}${subjectVar} = ${emitPyExprCtx(onIR, ctx)}`);
|
|
@@ -982,7 +1001,7 @@ function emitCellPy(node, ctx) {
|
|
|
982
1001
|
if (rawInitial === undefined || rawInitial === '') {
|
|
983
1002
|
return [`${pythonName} = None`];
|
|
984
1003
|
}
|
|
985
|
-
const initialIR =
|
|
1004
|
+
const initialIR = parseExpr(String(rawInitial));
|
|
986
1005
|
return [`${pythonName} = ${emitPyExprCtx(initialIR, ctx)}`];
|
|
987
1006
|
}
|
|
988
1007
|
function emitSetPy(node, ctx) {
|
|
@@ -997,7 +1016,7 @@ function emitSetPy(node, ctx) {
|
|
|
997
1016
|
}
|
|
998
1017
|
const name = String(rawName);
|
|
999
1018
|
const pythonName = ctx.symbolMap[name] ?? name;
|
|
1000
|
-
const valueIR =
|
|
1019
|
+
const valueIR = parseExpr(String(rawTo));
|
|
1001
1020
|
if (valueIR.kind === 'propagate') {
|
|
1002
1021
|
throw new Error(`Propagation \`${valueIR.op}\` is not supported in \`set to=\` — bind to \`let\` first, then call set.`);
|
|
1003
1022
|
}
|
|
@@ -1019,7 +1038,7 @@ function emitLetPy(node, ctx) {
|
|
|
1019
1038
|
if (rawValue === undefined || rawValue === '') {
|
|
1020
1039
|
return [`${name} = None`];
|
|
1021
1040
|
}
|
|
1022
|
-
const valueIR =
|
|
1041
|
+
const valueIR = parseExpr(String(rawValue));
|
|
1023
1042
|
setRegexBinding(ctx, userName, valueIR.kind === 'regexLit' ? valueIR : null);
|
|
1024
1043
|
if (valueIR.kind === 'propagate' && valueIR.op === '?') {
|
|
1025
1044
|
rejectPropagationInsideTry(ctx);
|
|
@@ -1107,9 +1126,9 @@ function emitClampPy(node, ctx) {
|
|
|
1107
1126
|
const rawMax = unwrapBodyExpr(props.max);
|
|
1108
1127
|
if (rawMax === undefined || rawMax === '')
|
|
1109
1128
|
throw new Error('body-statement `clamp` requires `max=`.');
|
|
1110
|
-
const valueIR =
|
|
1111
|
-
const minIR =
|
|
1112
|
-
const maxIR =
|
|
1129
|
+
const valueIR = parseExpr(rawValue);
|
|
1130
|
+
const minIR = parseExpr(rawMin);
|
|
1131
|
+
const maxIR = parseExpr(rawMax);
|
|
1113
1132
|
if (valueIR.kind === 'propagate' || minIR.kind === 'propagate' || maxIR.kind === 'propagate') {
|
|
1114
1133
|
throw new Error("Propagation '?' is not allowed in `clamp value=`/`min=`/`max=` — bind the value to a `let` first.");
|
|
1115
1134
|
}
|
|
@@ -1135,17 +1154,39 @@ function emitFirstTruthyPy(node, ctx) {
|
|
|
1135
1154
|
if (values.length < 2)
|
|
1136
1155
|
throw new Error('body-statement `firstTruthy` requires at least two value expressions.');
|
|
1137
1156
|
const emitted = values.map((value) => {
|
|
1138
|
-
const valueIR =
|
|
1157
|
+
const valueIR = parseExpr(value);
|
|
1139
1158
|
if (valueIR.kind === 'propagate') {
|
|
1140
1159
|
throw new Error("Propagation '?' is not allowed in `firstTruthy values=` — bind the value to a `let` first.");
|
|
1141
1160
|
}
|
|
1142
1161
|
return emitFirstTruthyOperandPy(valueIR, ctx);
|
|
1143
1162
|
});
|
|
1144
|
-
|
|
1163
|
+
// Slice S4 — `firstTruthy` selects the first KERN-truthy candidate, NOT the
|
|
1164
|
+
// first Python-truthy one. Bare `a or b` skips `[]`/`{}` (Python-falsy but
|
|
1165
|
+
// JS-truthy) and keeps NaN (Python-truthy but JS-falsy), so it diverges. Lower
|
|
1166
|
+
// to a `_kern_truthy`-gated walrus chain that evaluates each candidate AT MOST
|
|
1167
|
+
// ONCE and only if every earlier candidate was falsy (left-to-right laziness):
|
|
1168
|
+
// (t1 if _kern_truthy(t1 := a) else (t2 if _kern_truthy(t2 := b) else c))
|
|
1169
|
+
// The final candidate needs no temp/guard — it is the fallthrough value.
|
|
1170
|
+
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
1171
|
+
const lines = [`${name} = ${buildFirstTruthyChainPy(emitted, ctx)}`];
|
|
1145
1172
|
if (ctx.traceHooks?.letAssign)
|
|
1146
1173
|
lines.push(letAssignTracePy(name));
|
|
1147
1174
|
return lines;
|
|
1148
1175
|
}
|
|
1176
|
+
/** Build the `_kern_truthy`-gated, single-evaluation, lazy walrus chain for a
|
|
1177
|
+
* `firstTruthy` candidate list. The last candidate is the fallthrough (no
|
|
1178
|
+
* guard); every earlier candidate is bound once via `:=` and tested with
|
|
1179
|
+
* `_kern_truthy`. `gensymCounter`-seeded temp names avoid colliding with user
|
|
1180
|
+
* bindings or each other. */
|
|
1181
|
+
function buildFirstTruthyChainPy(candidates, ctx) {
|
|
1182
|
+
const last = candidates[candidates.length - 1];
|
|
1183
|
+
let chain = last;
|
|
1184
|
+
for (let i = candidates.length - 2; i >= 0; i--) {
|
|
1185
|
+
const tmp = `__k_ft${++ctx.gensymCounter}`;
|
|
1186
|
+
chain = `(${tmp} if _kern_truthy(${tmp} := ${candidates[i]}) else ${chain})`;
|
|
1187
|
+
}
|
|
1188
|
+
return chain;
|
|
1189
|
+
}
|
|
1149
1190
|
function emitCoalescePy(node, ctx) {
|
|
1150
1191
|
const props = (node.props ?? {});
|
|
1151
1192
|
const userName = String(props.name ?? '');
|
|
@@ -1162,7 +1203,7 @@ function emitCoalescePy(node, ctx) {
|
|
|
1162
1203
|
if (values.length < 2)
|
|
1163
1204
|
throw new Error(`body-statement \`${type}\` requires at least two value expressions.`);
|
|
1164
1205
|
const valueIRs = values.map((value) => {
|
|
1165
|
-
const valueIR =
|
|
1206
|
+
const valueIR = parseExpr(value);
|
|
1166
1207
|
if (valueIR.kind === 'propagate') {
|
|
1167
1208
|
throw new Error(`Propagation '?' is not allowed in \`${type} values=\` — bind the value to a \`let\` first.`);
|
|
1168
1209
|
}
|
|
@@ -1203,7 +1244,7 @@ function emitObjectMergePy(node, ctx) {
|
|
|
1203
1244
|
if (source.startsWith('...')) {
|
|
1204
1245
|
throw new Error('body-statement `objectMerge` sources imply spreading; omit leading `...`.');
|
|
1205
1246
|
}
|
|
1206
|
-
const sourceIR =
|
|
1247
|
+
const sourceIR = parseExpr(source);
|
|
1207
1248
|
if (sourceIR.kind === 'propagate') {
|
|
1208
1249
|
throw new Error("Propagation '?' is not allowed in `objectMerge sources=` — bind the value to a `let` first.");
|
|
1209
1250
|
}
|
|
@@ -1229,7 +1270,7 @@ function emitObjectPickPy(node, ctx) {
|
|
|
1229
1270
|
if (rawKeys === undefined || rawKeys === '') {
|
|
1230
1271
|
throw new Error('body-statement `objectPick` requires `keys=`.');
|
|
1231
1272
|
}
|
|
1232
|
-
const inIR =
|
|
1273
|
+
const inIR = parseExpr(rawIn);
|
|
1233
1274
|
if (inIR.kind === 'propagate') {
|
|
1234
1275
|
throw new Error("Propagation '?' is not allowed in `objectPick in=` — bind the value to a `let` first.");
|
|
1235
1276
|
}
|
|
@@ -1258,7 +1299,7 @@ function emitObjectOmitPy(node, ctx) {
|
|
|
1258
1299
|
if (rawKeys === undefined || rawKeys === '') {
|
|
1259
1300
|
throw new Error('body-statement `objectOmit` requires `keys=`.');
|
|
1260
1301
|
}
|
|
1261
|
-
const inIR =
|
|
1302
|
+
const inIR = parseExpr(rawIn);
|
|
1262
1303
|
if (inIR.kind === 'propagate') {
|
|
1263
1304
|
throw new Error("Propagation '?' is not allowed in `objectOmit in=` — bind the value to a `let` first.");
|
|
1264
1305
|
}
|
|
@@ -1298,7 +1339,7 @@ function emitAssignPy(node, ctx) {
|
|
|
1298
1339
|
// mirrors this; the emitter check is defense-in-depth for direct IR.
|
|
1299
1340
|
throw new Error(`body-statement \`assign op="${rawOp}"\` is value-less; remove \`value=\`.`);
|
|
1300
1341
|
}
|
|
1301
|
-
const targetIR =
|
|
1342
|
+
const targetIR = parseExpr(String(rawTarget));
|
|
1302
1343
|
if (!isAssignableTarget(targetIR)) {
|
|
1303
1344
|
throw new Error('body-statement `assign target=` must be an identifier, member access, or index access.');
|
|
1304
1345
|
}
|
|
@@ -1311,11 +1352,20 @@ function emitAssignPy(node, ctx) {
|
|
|
1311
1352
|
const baseOp = rawOp === '++' ? '+=' : '-=';
|
|
1312
1353
|
return [`${emitPyExprCtx(targetIR, ctx)} ${baseOp} 1`];
|
|
1313
1354
|
}
|
|
1314
|
-
const valueIR =
|
|
1355
|
+
const valueIR = parseExpr(String(rawValue));
|
|
1315
1356
|
if (valueIR.kind === 'propagate') {
|
|
1316
1357
|
throw new Error(`Propagation \`${valueIR.op}\` is not supported in \`assign value=\` — bind to \`let\` first, then assign.`);
|
|
1317
1358
|
}
|
|
1359
|
+
// Emit FIRST (its `emitPyExprCtx` lowering fail-closes a regex method on a
|
|
1360
|
+
// bound regex ident) so the RHS is checked against the PRE-reassignment table
|
|
1361
|
+
// (`re = s.match(re)` must still see `re` as a regex). Mirrors the TS leg.
|
|
1318
1362
|
const stmt = `${emitPyExprCtx(targetIR, ctx)} ${rawOp} ${emitPyExprCtx(valueIR, ctx)}`;
|
|
1363
|
+
// Reassign-invalidation (Slice-3c): keep the regex-binding table honest. A
|
|
1364
|
+
// plain `=` to a direct regex literal stays a regex binding (still
|
|
1365
|
+
// fail-closed); any compound op (`+=`, …) or non-regex RHS UNMARKS it.
|
|
1366
|
+
if (targetIR.kind === 'ident') {
|
|
1367
|
+
rebindRegexOnReassign(ctx, targetIR.name, rawOp === '=' ? valueIR : { kind: 'undefLit' });
|
|
1368
|
+
}
|
|
1319
1369
|
// Differential-harness opt-in (see BodyEmitOptions.traceHooks.letAssign): the
|
|
1320
1370
|
// `assign` contract observes a reassignment via the same `{op:"assign"}` event
|
|
1321
1371
|
// a `let` declaration emits. Scoped to identifier targets — the contract
|
|
@@ -1346,6 +1396,22 @@ function lookupRegexBinding(ctx, name) {
|
|
|
1346
1396
|
}
|
|
1347
1397
|
return null;
|
|
1348
1398
|
}
|
|
1399
|
+
/** Reassign-invalidation: when a tracked ident is REASSIGNED (`assign
|
|
1400
|
+
* target=re value=…`), update its regex marking IN THE SCOPE THAT OWNS IT (not
|
|
1401
|
+
* the innermost scope — that would shadow and leak past the inner block).
|
|
1402
|
+
* Reassigned to a direct `regexLit` → stays a regex binding (still fail-closed);
|
|
1403
|
+
* reassigned to anything else → UNMARK. Mirrors the TS `rebindRegexOnReassign`,
|
|
1404
|
+
* so the stale-binding class dies symmetrically on both targets. */
|
|
1405
|
+
function rebindRegexOnReassign(ctx, name, valueIR) {
|
|
1406
|
+
const next = valueIR.kind === 'regexLit' ? valueIR : null;
|
|
1407
|
+
for (let i = ctx.regexScopes.length - 1; i >= 0; i--) {
|
|
1408
|
+
const scope = ctx.regexScopes[i];
|
|
1409
|
+
if (scope.has(name)) {
|
|
1410
|
+
scope.set(name, next);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1349
1415
|
function assertAssignableLocalTarget(target, ctx) {
|
|
1350
1416
|
if (target.kind !== 'ident')
|
|
1351
1417
|
return;
|
|
@@ -1402,7 +1468,7 @@ function emitDestructurePy(node, ctx) {
|
|
|
1402
1468
|
if (rawSource === undefined || rawSource === '') {
|
|
1403
1469
|
throw new Error('body-statement `destructure` requires `source=`.');
|
|
1404
1470
|
}
|
|
1405
|
-
const source = emitPyExprCtx(
|
|
1471
|
+
const source = emitPyExprCtx(parseExpr(String(rawSource)), ctx);
|
|
1406
1472
|
const children = node.children ?? [];
|
|
1407
1473
|
const bindings = children.filter((c) => c.type === 'binding');
|
|
1408
1474
|
const elements = children.filter((c) => c.type === 'element');
|
|
@@ -1415,17 +1481,31 @@ function emitDestructurePy(node, ctx) {
|
|
|
1415
1481
|
if (bindings.length > 0) {
|
|
1416
1482
|
const tmp = `__k_d${++ctx.gensymCounter}`;
|
|
1417
1483
|
const lines = [`${tmp} = ${source}`];
|
|
1484
|
+
// Slice S7 — an ABSENT destructured key is JS `undefined` (the sentinel), so
|
|
1485
|
+
// `typeof missing` is "undefined" not "object". `.get(key, _KERN_UNDEFINED)`
|
|
1486
|
+
// returns the sentinel for an absent key AND preserves a PRESENT key whose
|
|
1487
|
+
// stored value is already the sentinel (`{ a: undefined }`) — the two stay
|
|
1488
|
+
// distinct from a present `None`. Ground/React (non-coerce) keeps `.get(key)`
|
|
1489
|
+
// (None default), since it never materializes the sentinel.
|
|
1490
|
+
const miss = ctx.coerceJsValues ? ', _KERN_UNDEFINED' : '';
|
|
1491
|
+
if (ctx.coerceJsValues)
|
|
1492
|
+
ctx.helpers.add(KERN_NULLISH_HELPER_PY);
|
|
1418
1493
|
for (const child of bindings) {
|
|
1419
1494
|
const cp = (child.props ?? {});
|
|
1420
1495
|
const name = String(cp.name ?? '');
|
|
1421
1496
|
if (!name)
|
|
1422
1497
|
throw new Error('body-statement `binding` requires `name=`.');
|
|
1423
1498
|
const key = cp.key === undefined || cp.key === '' ? name : String(cp.key);
|
|
1424
|
-
lines.push(`${ctx.symbolMap[name] ?? name} = ${tmp}.get(${JSON.stringify(key)})`);
|
|
1499
|
+
lines.push(`${ctx.symbolMap[name] ?? name} = ${tmp}.get(${JSON.stringify(key)}${miss})`);
|
|
1425
1500
|
}
|
|
1426
1501
|
return lines;
|
|
1427
1502
|
}
|
|
1428
1503
|
const tmp = `__k_d${++ctx.gensymCounter}`;
|
|
1504
|
+
// Slice S7 — an out-of-range array-destructure element is JS `undefined` (the
|
|
1505
|
+
// sentinel) in value mode; Ground/React keeps `None`.
|
|
1506
|
+
const arrMiss = ctx.coerceJsValues ? '_KERN_UNDEFINED' : 'None';
|
|
1507
|
+
if (ctx.coerceJsValues && elements.length > 0)
|
|
1508
|
+
ctx.helpers.add(KERN_NULLISH_HELPER_PY);
|
|
1429
1509
|
return [
|
|
1430
1510
|
`${tmp} = ${source}`,
|
|
1431
1511
|
...elements
|
|
@@ -1439,7 +1519,7 @@ function emitDestructurePy(node, ctx) {
|
|
|
1439
1519
|
throw new Error('body-statement `element` requires numeric `index=`.');
|
|
1440
1520
|
return {
|
|
1441
1521
|
index,
|
|
1442
|
-
line: `${ctx.symbolMap[name] ?? name} = (${tmp}[${index}] if len(${tmp}) > ${index} else
|
|
1522
|
+
line: `${ctx.symbolMap[name] ?? name} = (${tmp}[${index}] if len(${tmp}) > ${index} else ${arrMiss})`,
|
|
1443
1523
|
};
|
|
1444
1524
|
})
|
|
1445
1525
|
.sort((a, b) => a.index - b.index)
|
|
@@ -1557,7 +1637,7 @@ function templateToPyFString(template, ctx) {
|
|
|
1557
1637
|
throw new Error('body-statement `fmt`: unterminated `${...}` in template.');
|
|
1558
1638
|
}
|
|
1559
1639
|
const inner = template.slice(i + 2, j);
|
|
1560
|
-
const exprIR =
|
|
1640
|
+
const exprIR = parseExpr(inner);
|
|
1561
1641
|
if (exprIR.kind === 'propagate') {
|
|
1562
1642
|
throw new Error("Propagation '?' is not allowed inside an `fmt` template — bind via `let` first.");
|
|
1563
1643
|
}
|
|
@@ -1608,7 +1688,7 @@ function emitReturnPy(node, ctx) {
|
|
|
1608
1688
|
if (rawValue === undefined || rawValue === '') {
|
|
1609
1689
|
return [`return`];
|
|
1610
1690
|
}
|
|
1611
|
-
const valueIR =
|
|
1691
|
+
const valueIR = parseExpr(String(rawValue));
|
|
1612
1692
|
if (valueIR.kind === 'propagate' && valueIR.op === '?') {
|
|
1613
1693
|
rejectPropagationInsideTry(ctx);
|
|
1614
1694
|
const tmp = `__k_t${++ctx.gensymCounter}`;
|
|
@@ -1624,7 +1704,7 @@ function emitThrowPy(node, ctx) {
|
|
|
1624
1704
|
if (rawValue === undefined || rawValue === '') {
|
|
1625
1705
|
return [`raise Exception()`];
|
|
1626
1706
|
}
|
|
1627
|
-
const valueIR =
|
|
1707
|
+
const valueIR = parseExpr(String(rawValue));
|
|
1628
1708
|
if (valueIR.kind === 'propagate' && valueIR.op === '?') {
|
|
1629
1709
|
rejectPropagationInsideTry(ctx);
|
|
1630
1710
|
const tmp = `__k_t${++ctx.gensymCounter}`;
|
|
@@ -1648,7 +1728,7 @@ function emitDoPy(node, ctx) {
|
|
|
1648
1728
|
if (rawValue === undefined || rawValue === '') {
|
|
1649
1729
|
return [];
|
|
1650
1730
|
}
|
|
1651
|
-
const valueIR =
|
|
1731
|
+
const valueIR = parseExpr(String(rawValue));
|
|
1652
1732
|
if (valueIR.kind === 'propagate' && valueIR.op === '?') {
|
|
1653
1733
|
rejectPropagationInsideTry(ctx);
|
|
1654
1734
|
const tmp = `__k_t${++ctx.gensymCounter}`;
|
|
@@ -1689,6 +1769,20 @@ export function emitPyExpression(node, options) {
|
|
|
1689
1769
|
export function emitPyExpressionWithImports(node, options) {
|
|
1690
1770
|
const ctx = freshCtx(options);
|
|
1691
1771
|
ctx.standaloneExpression = true;
|
|
1772
|
+
// Seed the outer-binding scope into `localScopes` so this standalone-
|
|
1773
|
+
// expression entry point honors `outerBindings` the same way the native-body
|
|
1774
|
+
// entry point does (see `emitNativeKernBodyPythonWithImports`). Before slice
|
|
1775
|
+
// H the expression path silently ignored `outerBindings`; the shadowing rule
|
|
1776
|
+
// (`isProvenUserBinding` consulting the single-source-of-truth scope model)
|
|
1777
|
+
// needs them present so a proven-local root named `Math`/`JSON`/… is treated
|
|
1778
|
+
// as a user value rather than the host namespace. Index-aligned with
|
|
1779
|
+
// `regexScopes`/`renameStack` exactly as the native path does.
|
|
1780
|
+
const outerBindings = options?.outerBindings ?? [];
|
|
1781
|
+
if (outerBindings.length > 0) {
|
|
1782
|
+
ctx.localScopes.push(new Map(outerBindings.map((n) => [n, 'const'])));
|
|
1783
|
+
ctx.regexScopes.push(new Map(outerBindings.map((n) => [n, null])));
|
|
1784
|
+
ctx.renameStack.push(new Map());
|
|
1785
|
+
}
|
|
1692
1786
|
const code = emitPyExprCtx(node, ctx);
|
|
1693
1787
|
return { code, imports: ctx.imports, helpers: ctx.helpers };
|
|
1694
1788
|
}
|
|
@@ -1721,6 +1815,15 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1721
1815
|
ctx.imports.add('re');
|
|
1722
1816
|
return `__k_re.compile(${pyRegexPattern(node)}, ${pyRegexFlags(node.flags, { allowGlobal: true })})`;
|
|
1723
1817
|
case 'ident': {
|
|
1818
|
+
if (node.name === 'NaN')
|
|
1819
|
+
return 'float("nan")';
|
|
1820
|
+
if (node.name === 'Infinity')
|
|
1821
|
+
return 'float("inf")';
|
|
1822
|
+
// Slice 2 — a BARE-VALUE host `RegExp` reference (not a member/call
|
|
1823
|
+
// receiver, which their own cases own) fails-close. This is the
|
|
1824
|
+
// alias-soundness site: `let R = RegExp` is refused at the initializer, so
|
|
1825
|
+
// `R(...)` / `new R(...)` can never silently diverge. Honors user shadowing.
|
|
1826
|
+
rejectHostRegExpValuePython(node.name, ctx);
|
|
1724
1827
|
// Block-scope rename takes precedence — an inner `let x` that shadows
|
|
1725
1828
|
// an outer binding was emitted with a gensym (`__k_shadow_x_N`) and
|
|
1726
1829
|
// every in-block reference must use the same gensym. Walk renameStack
|
|
@@ -1757,7 +1860,7 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1757
1860
|
// The top-level wrapper produces `(expr if guard else None)` once
|
|
1758
1861
|
// (or just `expr` when no `?.` was seen).
|
|
1759
1862
|
const lowered = lowerChain(node, ctx);
|
|
1760
|
-
return wrapGuardIfAny(lowered);
|
|
1863
|
+
return wrapGuardIfAny(lowered, ctx);
|
|
1761
1864
|
}
|
|
1762
1865
|
case 'await':
|
|
1763
1866
|
return `await ${emitPyExprCtx(node.argument, ctx)}`;
|
|
@@ -1784,6 +1887,17 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1784
1887
|
if (arg.kind === 'ident' && arg.name === 'Error') {
|
|
1785
1888
|
return 'Exception()';
|
|
1786
1889
|
}
|
|
1890
|
+
// Slice 2 — `new RegExp(p)` (with or without parens) fails-close BEFORE the
|
|
1891
|
+
// fall-through, with the shared regex message (the TS emitter + IR-validate
|
|
1892
|
+
// throw the same string). Without this the Python `new` case falls through
|
|
1893
|
+
// to a verbatim `RegExp(...)` NameError instead of a compile-time refusal.
|
|
1894
|
+
// Honors user shadowing via `isProvenUserBinding`.
|
|
1895
|
+
if (arg.kind === 'call' && arg.callee.kind === 'ident') {
|
|
1896
|
+
rejectHostRegExpValuePython(arg.callee.name, ctx);
|
|
1897
|
+
}
|
|
1898
|
+
else if (arg.kind === 'ident') {
|
|
1899
|
+
rejectHostRegExpValuePython(arg.name, ctx);
|
|
1900
|
+
}
|
|
1787
1901
|
return emitPyExprCtx(arg, ctx);
|
|
1788
1902
|
}
|
|
1789
1903
|
case 'typeAssert':
|
|
@@ -1816,15 +1930,21 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1816
1930
|
return out;
|
|
1817
1931
|
}
|
|
1818
1932
|
case 'binary': {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
node.op
|
|
1933
|
+
// Slice 6 — bitwise / shift on the slice-0.75 ToInt32 substrate. Emitted
|
|
1934
|
+
// DIRECTLY as helper-wrapped strings (operands recurse through
|
|
1935
|
+
// `emitPyExprCtx`), so nested bitwise ops compose without the double-
|
|
1936
|
+
// dispatch recursion an AST-rewrite-then-re-emit would cause.
|
|
1937
|
+
if (node.op === '|' || node.op === '&' || node.op === '^' || node.op === '<<' || node.op === '>>') {
|
|
1938
|
+
return emitBitwiseShiftPy(node.op, node.left, node.right, ctx);
|
|
1939
|
+
}
|
|
1940
|
+
if (node.op === '>>>') {
|
|
1941
|
+
return emitUnsignedShiftPy(node.left, node.right, ctx);
|
|
1942
|
+
}
|
|
1943
|
+
if (node.op === '%') {
|
|
1944
|
+
// Modulo keeps the existing `_tmod` lowering (orthogonal to S6).
|
|
1825
1945
|
const transformed = lowerBitwiseAndModuloAST(node);
|
|
1826
1946
|
registerHelpers(transformed, ctx);
|
|
1827
|
-
return
|
|
1947
|
+
return emitLoweredBitwisePy(transformed, ctx);
|
|
1828
1948
|
}
|
|
1829
1949
|
// Slice 2c — arithmetic / comparison / logical lowering for Python.
|
|
1830
1950
|
// Use precedence-aware paren-wrapping so `a + b * c` doesn't redundantly
|
|
@@ -1894,6 +2014,62 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1894
2014
|
}
|
|
1895
2015
|
return `__kern_add(${left}, ${right})`;
|
|
1896
2016
|
}
|
|
2017
|
+
if (node.op === '&&' || node.op === '||') {
|
|
2018
|
+
// Slice S5 — logical `&&` / `||` RESULT-VALUE semantics on Python.
|
|
2019
|
+
//
|
|
2020
|
+
// KERN `&&`/`||` are operand-selectors, not boolean operators:
|
|
2021
|
+
// `a && b` returns `a` when ToBoolean(a) is false, else `b`.
|
|
2022
|
+
// `a || b` returns `a` when ToBoolean(a) is true, else `b`.
|
|
2023
|
+
// Python's `and`/`or` use Python truthiness (`bool(x)` / `len(x)`), which
|
|
2024
|
+
// diverges from KERN ToBoolean on `[]`, `{}`, `NaN`, `"0"`, `"false"`,
|
|
2025
|
+
// `" "` — e.g. `[] or x` returns `x` in Python but must return `[]` in
|
|
2026
|
+
// KERN. So `and`/`or` are wrong; we lower through the SAME `_kern_truthy`
|
|
2027
|
+
// (KERN ToBoolean) substrate S4 routes `!`/ternary/`if cond=` through.
|
|
2028
|
+
//
|
|
2029
|
+
// Canonical lowering (single-eval, lazy, original-value-returning):
|
|
2030
|
+
// L && R -> (__k_logN if not _kern_truthy(__k_logN := L) else R)
|
|
2031
|
+
// L || R -> (__k_logN if _kern_truthy(__k_logN := L) else R)
|
|
2032
|
+
//
|
|
2033
|
+
// The walrus binds L EXACTLY ONCE (works for calls/indexes/members/
|
|
2034
|
+
// nested logicals — no double-read), `_kern_truthy` decides the branch
|
|
2035
|
+
// on KERN truthiness, and the unselected operand is NEVER evaluated
|
|
2036
|
+
// (Python conditional-expr branches are lazy). NO ident/pure-left fast
|
|
2037
|
+
// path in this slice — conservative walrus for EVERY left operand is the
|
|
2038
|
+
// single-eval law (S4 carry-forward; optimization needs its own oracle).
|
|
2039
|
+
//
|
|
2040
|
+
// Emitted UNCONDITIONALLY for both native (`coerceJsValues`) and Ground/
|
|
2041
|
+
// React layers — mirroring S4's `!`/ternary, which also emit
|
|
2042
|
+
// `_kern_truthy` regardless of the coercion flag. The `[]`/`{}`/`NaN`
|
|
2043
|
+
// divergence is independent of the undefined sentinel, so a Ground-layer
|
|
2044
|
+
// `a and b` would be just as wrong; parity demands one spelling.
|
|
2045
|
+
// `KERN_JS_HELPER_PY` is in Ground's prelude registry, so the helper is
|
|
2046
|
+
// inlined there too.
|
|
2047
|
+
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
2048
|
+
const tmp = `__k_log${++ctx.gensymCounter}`;
|
|
2049
|
+
// `:=` (PEP 572) binds looser than almost everything, so a low-precedence
|
|
2050
|
+
// left operand (conditional/lambda/walrus-bearing) must be parenthesized
|
|
2051
|
+
// so `__k_logN := L` captures the WHOLE operand. A nested `&&`/`||` is
|
|
2052
|
+
// already self-parenthesized, so this only fires on raw conditional/
|
|
2053
|
+
// lambda left operands.
|
|
2054
|
+
const leftOperand = needsWalrusOperandParens(node.left) ? `(${left})` : left;
|
|
2055
|
+
// The `else` branch is a conditional-expression alternate; a bare
|
|
2056
|
+
// conditional/lambda there reparses ambiguously, so wrap it. A nested
|
|
2057
|
+
// logical R is self-parenthesized; only raw conditional/lambda needs it.
|
|
2058
|
+
const rightOperand = needsConditionalAlternateParens(node.right) ? `(${right})` : right;
|
|
2059
|
+
if (ctx.banWalrus) {
|
|
2060
|
+
// Comprehension-iterable position (see BodyEmitContext.banWalrus):
|
|
2061
|
+
// the walrus form is a SyntaxError here, so bind L as a lambda
|
|
2062
|
+
// parameter instead. Same contract: L evaluated exactly once (as the
|
|
2063
|
+
// call argument), the unselected branch never evaluated (lazy
|
|
2064
|
+
// conditional inside the lambda body), original value returned.
|
|
2065
|
+
const test = `_kern_truthy(${tmp})`;
|
|
2066
|
+
const guarded = node.op === '&&' ? `not ${test}` : test;
|
|
2067
|
+
return `(lambda ${tmp}: (${tmp} if ${guarded} else ${rightOperand}))(${left})`;
|
|
2068
|
+
}
|
|
2069
|
+
const test = `_kern_truthy(${tmp} := ${leftOperand})`;
|
|
2070
|
+
const guarded = node.op === '&&' ? `not ${test}` : test;
|
|
2071
|
+
return `(${tmp} if ${guarded} else ${rightOperand})`;
|
|
2072
|
+
}
|
|
1897
2073
|
if (node.op === '??') {
|
|
1898
2074
|
// Slice 4c — nullish coalesce lowering. Two shapes:
|
|
1899
2075
|
//
|
|
@@ -1924,6 +2100,11 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1924
2100
|
return `(${left} if ${left} is not None else ${right})`;
|
|
1925
2101
|
}
|
|
1926
2102
|
const tmp = `__k_nc${++ctx.gensymCounter}`;
|
|
2103
|
+
if (ctx.banWalrus) {
|
|
2104
|
+
// Comprehension-iterable position — walrus is a SyntaxError here;
|
|
2105
|
+
// lambda-parameter form keeps single-eval + lazy right branch.
|
|
2106
|
+
return `(lambda ${tmp}: (${tmp} if ${tmp} is not None else ${right}))(${left})`;
|
|
2107
|
+
}
|
|
1927
2108
|
return `(${tmp} if (${tmp} := ${left}) is not None else ${right})`;
|
|
1928
2109
|
}
|
|
1929
2110
|
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
@@ -1931,8 +2112,29 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1931
2112
|
return `(${left} if (${left} is not None and ${left} is not _KERN_UNDEFINED) else ${right})`;
|
|
1932
2113
|
}
|
|
1933
2114
|
const tmp = `__k_nc${++ctx.gensymCounter}`;
|
|
2115
|
+
if (ctx.banWalrus) {
|
|
2116
|
+
// Comprehension-iterable position — see BodyEmitContext.banWalrus.
|
|
2117
|
+
return `(lambda ${tmp}: (${tmp} if (${tmp} is not None and ${tmp} is not _KERN_UNDEFINED) else ${right}))(${left})`;
|
|
2118
|
+
}
|
|
1934
2119
|
return `(${tmp} if ((${tmp} := ${left}) is not None and ${tmp} is not _KERN_UNDEFINED) else ${right})`;
|
|
1935
2120
|
}
|
|
2121
|
+
// Slice S7 — split loose (`==`/`!=`) and strict (`===`/`!==`) equality so
|
|
2122
|
+
// the null/undefined boundary matches JS: `undefined == null` is True (both
|
|
2123
|
+
// nullish), `undefined === null` is False (distinct identities). Pre-S7
|
|
2124
|
+
// both lowered to Python `==`, which on the sentinel/None pair is False —
|
|
2125
|
+
// wrong for the loose op. Routed through the helper pair only in native
|
|
2126
|
+
// (coerce) bodies; the helper-less Ground/React layer keeps the raw
|
|
2127
|
+
// `==`/`!=` mapping (it never materializes the sentinel, so the nullish
|
|
2128
|
+
// crossing cannot arise there). Function-call form sidesteps Python's
|
|
2129
|
+
// comparison-chaining entirely, so no chain-paren handling is needed for
|
|
2130
|
+
// the equality ops themselves; chained comparison CHILDREN still recurse
|
|
2131
|
+
// through `emitPyExprCtx` and self-parenthesize as before.
|
|
2132
|
+
if (ctx.coerceJsValues && (node.op === '===' || node.op === '!==' || node.op === '==' || node.op === '!=')) {
|
|
2133
|
+
ctx.helpers.add(KERN_NULLISH_HELPER_PY);
|
|
2134
|
+
const fn = node.op === '===' || node.op === '!==' ? '_kern_strict_equal' : '_kern_loose_equal';
|
|
2135
|
+
const call = `${fn}(${left}, ${right})`;
|
|
2136
|
+
return node.op === '!==' || node.op === '!=' ? `(not ${call})` : call;
|
|
2137
|
+
}
|
|
1936
2138
|
const forceLeft = needsComparisonChainParens(node.left, node.op);
|
|
1937
2139
|
const forceRight = needsComparisonChainParens(node.right, node.op);
|
|
1938
2140
|
const lp = forceLeft || needsBinaryParens(node.left, node.op, 'left') ? `(${left})` : left;
|
|
@@ -1942,9 +2144,9 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1942
2144
|
}
|
|
1943
2145
|
case 'unary': {
|
|
1944
2146
|
if (node.op === '~') {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
return emitPyExprCtx(
|
|
2147
|
+
// ~a -> _kern_to_int32(~_kern_to_int32(a))
|
|
2148
|
+
ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
|
|
2149
|
+
return `_kern_to_int32(~_kern_to_int32(${emitPyExprCtx(node.argument, ctx)}))`;
|
|
1948
2150
|
}
|
|
1949
2151
|
// Slice 2c — `!x` → `not x`, `-x` → `-x`.
|
|
1950
2152
|
// Slice typeof — expose the now-eligible native KERN `typeof` shape on
|
|
@@ -1954,8 +2156,15 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1954
2156
|
return emitPyTypeof(node.argument, ctx);
|
|
1955
2157
|
const arg = emitPyExprCtx(node.argument, ctx);
|
|
1956
2158
|
const wrapped = needsArgParens(node.argument) ? `(${arg})` : arg;
|
|
1957
|
-
|
|
1958
|
-
|
|
2159
|
+
// Slice S4 — `!x` consumes KERN ToBoolean and returns a real Python bool.
|
|
2160
|
+
// `not _kern_truthy(x)` gives `!""`/`!NaN` → True and `!"0"`/`![]` → False
|
|
2161
|
+
// (bare `not x` would get `![]`/`!{}`/`!NaN` wrong). Wrap unconditionally.
|
|
2162
|
+
if (node.op === '!') {
|
|
2163
|
+
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
2164
|
+
return `(not _kern_truthy(${arg}))`;
|
|
2165
|
+
}
|
|
2166
|
+
if (node.op === '-' && node.argument.kind === 'numLit' && Number(node.argument.raw) === 0)
|
|
2167
|
+
return '-0.0';
|
|
1959
2168
|
if (node.op === '-')
|
|
1960
2169
|
return `-${wrapped}`;
|
|
1961
2170
|
if (node.op === '+')
|
|
@@ -1999,7 +2208,12 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1999
2208
|
return emitted;
|
|
2000
2209
|
}
|
|
2001
2210
|
};
|
|
2002
|
-
|
|
2211
|
+
// Slice S4 — the ternary condition consumes KERN ToBoolean exactly once, so
|
|
2212
|
+
// `{} ? a : b`/`[] ? a : b` take the consequent and `NaN ? a : b` takes the
|
|
2213
|
+
// alternate. `_kern_truthy(...)` already parenthesizes the test, so no extra
|
|
2214
|
+
// `wrap` is needed on it. Wrap unconditionally (no "looks boolean" skip).
|
|
2215
|
+
ctx.helpers.add(KERN_JS_HELPER_PY);
|
|
2216
|
+
return `${wrap(node.consequent, consStr)} if _kern_truthy(${testStr}) else ${wrap(node.alternate, altStr)}`;
|
|
2003
2217
|
}
|
|
2004
2218
|
case 'spread':
|
|
2005
2219
|
return `*${emitPyExprCtx(node.argument, ctx)}`;
|
|
@@ -2010,6 +2224,39 @@ function emitPyExprCtx(node, ctx) {
|
|
|
2010
2224
|
throw new Error(`emitPyExpression: unsupported expression kind '${node.kind ?? 'unknown'}'.`);
|
|
2011
2225
|
}
|
|
2012
2226
|
function emitPyTypeof(argument, ctx) {
|
|
2227
|
+
// Round-6 fix — `typeof <bare host-namespace root>` fails-close on BOTH targets.
|
|
2228
|
+
// The round-5 carve-out lowered `typeof RegExp` to the constant `"function"`
|
|
2229
|
+
// on the theory that it was byte-identical to TS's native `typeof RegExp`. But
|
|
2230
|
+
// the carve-out's siblings on the TS/IR legs blanket-accepted EVERY bare
|
|
2231
|
+
// `typeof` operand, re-opening reserved host roots: `typeof Date`/`typeof
|
|
2232
|
+
// process` lowered HERE to a runtime `isinstance` ladder over the Python names
|
|
2233
|
+
// `Date`/`process`, which do NOT exist → NameError, while TS emits the native
|
|
2234
|
+
// `typeof Date`. That is a genuine TS↔Python divergence, so `typeof <host root>`
|
|
2235
|
+
// must fail-close. `RegExp` keeps the shared regex message (matching the
|
|
2236
|
+
// bare-value reject + the TS/IR legs); every other reserved host root takes the
|
|
2237
|
+
// generic host message with a synthetic `typeof` member. A non-host operand
|
|
2238
|
+
// (`typeof userLocal`, `typeof undeclaredFeatureFlag`) and a proven user binding
|
|
2239
|
+
// fall through to the runtime ladder below, unchanged. `typeof RegExp.prototype`
|
|
2240
|
+
// is a `member` operand and still fails-close via the generic guards.
|
|
2241
|
+
//
|
|
2242
|
+
// Round-7 fix — a WRAPPED operand (`typeof (Date as any)`, `typeof (Date!)`)
|
|
2243
|
+
// arrived as `typeAssert`/`nonNull`, NOT an `ident`, so the round-6 reject was
|
|
2244
|
+
// bypassed and Python lowered a runtime Date/process lookup (NameError) while
|
|
2245
|
+
// the (now-corrected) TS leg emits `typeof Date`. Peel the transparent wrappers
|
|
2246
|
+
// via the round-5 `unwrapTransparentReceiverIR` (fixpoint over
|
|
2247
|
+
// `typeAssert`/`nonNull`) and apply the host-root reject to the UNWRAPPED
|
|
2248
|
+
// operand, identically to the TS-emit + IR-validate legs. The runtime-ladder
|
|
2249
|
+
// fall-through below still operates on the ORIGINAL `argument`, so an accepted
|
|
2250
|
+
// wrapped operand (`typeof (userLocal as any)`) emits the wrapper's value
|
|
2251
|
+
// unchanged.
|
|
2252
|
+
const typeofOperand = unwrapTransparentReceiverIR(argument);
|
|
2253
|
+
if (typeofOperand.kind === 'ident' && !isProvenUserBinding(ctx, typeofOperand.name)) {
|
|
2254
|
+
if (typeofOperand.name === 'RegExp')
|
|
2255
|
+
throw new Error(REGEX_HOST_REGEXP_FAILCLOSE);
|
|
2256
|
+
if (isHostNamespaceRoot(typeofOperand.name)) {
|
|
2257
|
+
throw new Error(unmappedHostNamespaceMessage('Python', typeofOperand.name, 'typeof'));
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2013
2260
|
switch (argument.kind) {
|
|
2014
2261
|
case 'strLit':
|
|
2015
2262
|
return '"string"';
|
|
@@ -2042,6 +2289,17 @@ function emitPyTypeof(argument, ctx) {
|
|
|
2042
2289
|
// pre-slice None-first form.
|
|
2043
2290
|
if (ctx.coerceJsValues) {
|
|
2044
2291
|
ctx.helpers.add(KERN_FMT_HELPER_PY);
|
|
2292
|
+
if (ctx.banWalrus) {
|
|
2293
|
+
// Comprehension-iterable position — walrus is a SyntaxError here; bind
|
|
2294
|
+
// the operand as a lambda parameter instead (single-eval preserved).
|
|
2295
|
+
return (`(lambda ${tmp}: ("undefined" if ${tmp} is _KERN_UNDEFINED ` +
|
|
2296
|
+
`else "object" if ${tmp} is None ` +
|
|
2297
|
+
`else "boolean" if isinstance(${tmp}, bool) ` +
|
|
2298
|
+
`else "number" if isinstance(${tmp}, (int, float)) ` +
|
|
2299
|
+
`else "string" if isinstance(${tmp}, str) ` +
|
|
2300
|
+
`else "function" if callable(${tmp}) ` +
|
|
2301
|
+
`else "object"))(${value})`);
|
|
2302
|
+
}
|
|
2045
2303
|
return (`("undefined" if (${tmp} := ${wrapped}) is _KERN_UNDEFINED ` +
|
|
2046
2304
|
`else "object" if ${tmp} is None ` +
|
|
2047
2305
|
`else "boolean" if isinstance(${tmp}, bool) ` +
|
|
@@ -2050,6 +2308,15 @@ function emitPyTypeof(argument, ctx) {
|
|
|
2050
2308
|
`else "function" if callable(${tmp}) ` +
|
|
2051
2309
|
`else "object")`);
|
|
2052
2310
|
}
|
|
2311
|
+
if (ctx.banWalrus) {
|
|
2312
|
+
// Comprehension-iterable position — see BodyEmitContext.banWalrus.
|
|
2313
|
+
return (`(lambda ${tmp}: ("object" if ${tmp} is None ` +
|
|
2314
|
+
`else "boolean" if isinstance(${tmp}, bool) ` +
|
|
2315
|
+
`else "number" if isinstance(${tmp}, (int, float)) ` +
|
|
2316
|
+
`else "string" if isinstance(${tmp}, str) ` +
|
|
2317
|
+
`else "function" if callable(${tmp}) ` +
|
|
2318
|
+
`else "object"))(${value})`);
|
|
2319
|
+
}
|
|
2053
2320
|
return (`("object" if (${tmp} := ${wrapped}) is None ` +
|
|
2054
2321
|
`else "boolean" if isinstance(${tmp}, bool) ` +
|
|
2055
2322
|
`else "number" if isinstance(${tmp}, (int, float)) ` +
|
|
@@ -2082,12 +2349,12 @@ function emitLambdaPy(node, ctx) {
|
|
|
2082
2349
|
*
|
|
2083
2350
|
* The closure body is lowered through `lowerJsClosureBodyToPython`, reusing
|
|
2084
2351
|
* the class-path expression/condition callbacks:
|
|
2085
|
-
* - `lowerExpression(raw)` = `emitPyExprCtx(
|
|
2352
|
+
* - `lowerExpression(raw)` = `emitPyExprCtx(parseExpr(raw), ctx)` —
|
|
2086
2353
|
* identical to every other native-body expression emit, so a captured
|
|
2087
2354
|
* RENAMED outer variable resolves through `ctx` (the rename stack /
|
|
2088
2355
|
* symbolMap) exactly as it does outside the closure.
|
|
2089
2356
|
* - `lowerCondition(raw)` mirrors the class/native if-emitter, which lowers a
|
|
2090
|
-
* condition as the bare `emitPyExprCtx(
|
|
2357
|
+
* condition as the bare `emitPyExprCtx(parseExpr(cond), ctx)` (NO
|
|
2091
2358
|
* js_truthy wrapper). Matching it EXACTLY means a condition inside a
|
|
2092
2359
|
* closure lowers identically to the same condition outside one.
|
|
2093
2360
|
*
|
|
@@ -2101,13 +2368,27 @@ function emitBlockClosurePy(node, names, ctx) {
|
|
|
2101
2368
|
const previous = new Set(ctx.shadowedSymbols);
|
|
2102
2369
|
for (const name of names)
|
|
2103
2370
|
ctx.shadowedSymbols.add(name);
|
|
2371
|
+
// Slice 2 review-fix parity (round 3) — a block-LOCAL `const`/`let`/function/
|
|
2372
|
+
// class shadows any outer binding of that name WITHIN ITS OWN BLOCK (and
|
|
2373
|
+
// nested blocks) only, so a host-`RegExp` value guard
|
|
2374
|
+
// (`rejectHostRegExpValuePython`) must fire on an OUTER reference but NOT on an
|
|
2375
|
+
// in-scope one. The old code registered EVERY declared name (incl. names
|
|
2376
|
+
// declared only in a NESTED block) into `shadowedSymbols` for the WHOLE
|
|
2377
|
+
// closure — fail-OPEN on `() => { if (ok) { const RegExp = 1; } return RegExp; }`
|
|
2378
|
+
// (the outer `return RegExp` was wrongly treated as the local), DIVERGING from
|
|
2379
|
+
// the TS leg. The lowerer FLATTENS nested blocks, so it now reports block
|
|
2380
|
+
// boundaries via `enterBlockScope`/`exitBlockScope`; we push/pop block-local
|
|
2381
|
+
// shadows exactly like the TS-AST closure walk's `scopes` stack. Only names
|
|
2382
|
+
// NOT already shadowed are pushed/popped, so an outer binding of the same name
|
|
2383
|
+
// survives the inner block's pop.
|
|
2384
|
+
const blockScopeAdded = [];
|
|
2104
2385
|
try {
|
|
2105
2386
|
const lowered = lowerJsClosureBodyToPython(node.bodyBlock.raw, {
|
|
2106
|
-
lowerExpression: (raw) => emitPyExprCtx(
|
|
2387
|
+
lowerExpression: (raw) => emitPyExprCtx(parseExpr(raw), ctx),
|
|
2107
2388
|
// Mirror the native/class if-emitter EXACTLY (bare expression, no
|
|
2108
2389
|
// js_truthy) so a condition inside the closure matches the same
|
|
2109
2390
|
// condition outside it.
|
|
2110
|
-
lowerCondition: (raw) => emitPyExprCtx(
|
|
2391
|
+
lowerCondition: (raw) => emitPyExprCtx(parseExpr(raw), ctx),
|
|
2111
2392
|
// The closure's own params are def-locals, never `nonlocal`: a write to a
|
|
2112
2393
|
// param (`(x) => { x = x + 1 }`) must not be reported as a written FREE
|
|
2113
2394
|
// name. The lowerer excludes both params and block-locals.
|
|
@@ -2118,6 +2399,25 @@ function emitBlockClosurePy(node, names, ctx) {
|
|
|
2118
2399
|
// wrong-values without this). Params/block-locals are never renamed, so
|
|
2119
2400
|
// the resolver is identity for them.
|
|
2120
2401
|
lowerAssignTarget: (name) => resolveLocalRename(ctx, name),
|
|
2402
|
+
// Block-scope shadowing — push a block's top-level locals on entry, pop on
|
|
2403
|
+
// exit, so a `RegExp` value reference fails-close ONLY when no in-scope
|
|
2404
|
+
// block-local/param shadows it (byte-aligned with the TS-AST walk).
|
|
2405
|
+
enterBlockScope: (scopeNames) => {
|
|
2406
|
+
const added = new Set();
|
|
2407
|
+
for (const name of scopeNames) {
|
|
2408
|
+
if (!ctx.shadowedSymbols.has(name)) {
|
|
2409
|
+
ctx.shadowedSymbols.add(name);
|
|
2410
|
+
added.add(name);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
blockScopeAdded.push(added);
|
|
2414
|
+
},
|
|
2415
|
+
exitBlockScope: () => {
|
|
2416
|
+
const added = blockScopeAdded.pop();
|
|
2417
|
+
if (added)
|
|
2418
|
+
for (const name of added)
|
|
2419
|
+
ctx.shadowedSymbols.delete(name);
|
|
2420
|
+
},
|
|
2121
2421
|
});
|
|
2122
2422
|
if (!lowered.ok) {
|
|
2123
2423
|
// The commit-A gate already accepted this block, so a lowering failure
|
|
@@ -2343,12 +2643,99 @@ function containsLambdaCapturingIdent(node, name) {
|
|
|
2343
2643
|
return false;
|
|
2344
2644
|
}
|
|
2345
2645
|
}
|
|
2646
|
+
/** Slice S7 — the presence test an optional `?.`/`?.[]` link contributes to the
|
|
2647
|
+
* accumulated chain guard. Native (coerce) bodies test against the full nullish
|
|
2648
|
+
* set (`None` AND the undefined sentinel) so `undefined?.x` short-circuits; the
|
|
2649
|
+
* helper-less Ground/React layer keeps the pre-slice `is not None` test (it
|
|
2650
|
+
* never materializes the sentinel). `ref` may itself be a walrus assignment
|
|
2651
|
+
* expression (`(__k_oc1 := f())`), which Python evaluates exactly once. */
|
|
2652
|
+
function optionalPresenceTest(ref, ctx) {
|
|
2653
|
+
if (ctx.coerceJsValues) {
|
|
2654
|
+
ctx.helpers.add(KERN_NULLISH_HELPER_PY);
|
|
2655
|
+
return `(not _kern_is_nullish(${ref}))`;
|
|
2656
|
+
}
|
|
2657
|
+
// `x := f() is not None` would bind the BOOLEAN to x; parenthesize a walrus
|
|
2658
|
+
// ref so the receiver (not the comparison) is what gets bound.
|
|
2659
|
+
const wrapped = ref.includes(':=') ? `(${ref})` : ref;
|
|
2660
|
+
return `${wrapped} is not None`;
|
|
2661
|
+
}
|
|
2662
|
+
/** Slice S7 — build the guard + branch-receiver reference for one optional link.
|
|
2663
|
+
* Pure receivers keep the readable double-name form (named in both the guard
|
|
2664
|
+
* and the branch — re-evaluation is side-effect-free). A NON-pure receiver is
|
|
2665
|
+
* bound ONCE via the S4 walrus idiom: the guard carries `(__k_ocN := RECV)` so
|
|
2666
|
+
* the side effect runs exactly once, and the branch references the bound name.
|
|
2667
|
+
* A non-pure receiver that is ITSELF an optional chain (`inner.guard !== null`)
|
|
2668
|
+
* cannot host the walrus and still throws — bind it to a `let` first. */
|
|
2669
|
+
function lowerOptionalLink(inner, objectNode, ctx) {
|
|
2670
|
+
const pure = isReceiverChainPure(objectNode);
|
|
2671
|
+
if (pure) {
|
|
2672
|
+
const presence = optionalPresenceTest(inner.expr, ctx);
|
|
2673
|
+
return {
|
|
2674
|
+
guard: inner.guard === null ? presence : `${inner.guard} and ${presence}`,
|
|
2675
|
+
branchRef: inner.expr,
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
if (inner.guard !== null) {
|
|
2679
|
+
throw new Error("Optional chain '?.' on Python target requires a side-effect-free receiver (identifier or pure member chain). " +
|
|
2680
|
+
'Bind the call/await result to a `let` first, then use `let.field?.next` on the bound name.');
|
|
2681
|
+
}
|
|
2682
|
+
const tmp = `__k_oc${++ctx.gensymCounter}`;
|
|
2683
|
+
if (ctx.banWalrus) {
|
|
2684
|
+
// Comprehension-iterable position (see BodyEmitContext.banWalrus): CPython
|
|
2685
|
+
// rejects the `:=` walrus anywhere inside a comprehension iterable, so the
|
|
2686
|
+
// non-pure receiver cannot be bound via `(__k_ocN := RECV)`. Bind it as a
|
|
2687
|
+
// lambda PARAMETER instead — the presence test + branch reference the bare
|
|
2688
|
+
// `__k_ocN` (the parameter), and `wrapGuardIfAny` wraps the whole guarded
|
|
2689
|
+
// conditional in `(lambda __k_ocN: <conditional>)(RECV)`. Same contract as
|
|
2690
|
+
// the walrus form: RECV evaluated exactly once (as the call argument), the
|
|
2691
|
+
// short-circuit branch yields the sentinel, single-eval preserved.
|
|
2692
|
+
const presence = optionalPresenceTest(tmp, ctx);
|
|
2693
|
+
return { guard: presence, branchRef: tmp, lambdaBind: { param: tmp, arg: inner.expr } };
|
|
2694
|
+
}
|
|
2695
|
+
const presence = optionalPresenceTest(`${tmp} := ${inner.expr}`, ctx);
|
|
2696
|
+
return { guard: presence, branchRef: tmp };
|
|
2697
|
+
}
|
|
2346
2698
|
function lowerChain(node, ctx) {
|
|
2347
2699
|
if (node.kind === 'member') {
|
|
2348
2700
|
const obj = node.object;
|
|
2701
|
+
// Slice 2 — a bare property READ on a DIRECT regex LITERAL (`/x/.source`,
|
|
2702
|
+
// `/x/.flags`) launders the pattern/flags back into a string. Routed through
|
|
2703
|
+
// the SHARED classifier (via the ValueIR adapter) so this site agrees with
|
|
2704
|
+
// the TS emit + IR-validate legs and the closure walk BY CONSTRUCTION. The
|
|
2705
|
+
// portable METHODS (.test/.exec/…) are CALLS routed by the call path before
|
|
2706
|
+
// reaching here, and a LET-BOUND regex read (`r.source`) has an `ident`
|
|
2707
|
+
// object (owned by Slice 3), so only a direct literal read is closed here.
|
|
2708
|
+
// The receiver is UNWRAPPED first so a wrapped read `(/x/ as any).source` /
|
|
2709
|
+
// `(/x/!).source` fails-close identically to the bare `/x/.source` and to the
|
|
2710
|
+
// TS-emit + IR-validate legs (round-5 wrapped-receiver fix).
|
|
2711
|
+
if (regexLiteralReceiverIR(obj) !== null) {
|
|
2712
|
+
const message = classifyRegexLiteralMemberReadFailClose(node);
|
|
2713
|
+
if (message !== null)
|
|
2714
|
+
throw new Error(message);
|
|
2715
|
+
}
|
|
2716
|
+
const stdlibProperty = applyStdlibPropertyLoweringPython(node, ctx);
|
|
2717
|
+
if (stdlibProperty !== null)
|
|
2718
|
+
return { guard: null, expr: stdlibProperty };
|
|
2719
|
+
// Slice H — fail-closed on an UNMAPPED host-namespace member READ. Covers
|
|
2720
|
+
// host CONSTANT reads such as `Math.PI` / `process.env` that are not a call
|
|
2721
|
+
// (the call path guards `Root.member(args)` separately, before descending
|
|
2722
|
+
// here). Only a DIRECT `Root.member` read is inspected: `obj.kind ===
|
|
2723
|
+
// 'ident'`. A host-namespace-shaped, not-user-bound root throws; user
|
|
2724
|
+
// receivers (`user.profile`) and proven-local roots pass through. A host
|
|
2725
|
+
// CALL's callee never reaches this read guard with a host root — the call
|
|
2726
|
+
// path already threw — so this only ever fires on genuine reads.
|
|
2727
|
+
if (obj.kind === 'ident') {
|
|
2728
|
+
rejectUnmappedHostNamespacePython(obj.name, node.property, ctx);
|
|
2729
|
+
}
|
|
2730
|
+
// S5 review fix — a NON-CHAIN root that lowers to a compound expression
|
|
2731
|
+
// must be parenthesized before `.${property}` is appended: a bare ternary
|
|
2732
|
+
// root (`b if t else c`) would otherwise bind the member to its LAST
|
|
2733
|
+
// operand only (`b if t else c.prop` — silently wrong Python). Same
|
|
2734
|
+
// precedence set as index receivers; lowerings that already emit a
|
|
2735
|
+
// self-delimited atom (`(walrus ternary)`, `__kern_add(a, b)`) stay bare.
|
|
2349
2736
|
const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
|
|
2350
2737
|
? lowerChain(obj, ctx)
|
|
2351
|
-
: { guard: null, expr: emitPyExprCtx(obj, ctx) };
|
|
2738
|
+
: { guard: null, expr: wrapCompoundRootExpr(obj, emitPyExprCtx(obj, ctx)) };
|
|
2352
2739
|
// Portable Array *property* read (non-call `.length`) lowers through the
|
|
2353
2740
|
// SAME shared list-ops hook the route emitter uses, so `this.items.length`
|
|
2354
2741
|
// emits `len(self.items)` (not invalid `self.items.length`) — identical to
|
|
@@ -2356,41 +2743,70 @@ function lowerChain(node, ctx) {
|
|
|
2356
2743
|
// link is rewritten; the accumulated optional-chain guard is left UNTOUCHED
|
|
2357
2744
|
// and still flows through `wrapGuardIfAny`, so `items?.length` stays
|
|
2358
2745
|
// `(len(items) if items is not None else None)`-shaped.
|
|
2746
|
+
if (node.optional) {
|
|
2747
|
+
// Slice S7 — single-eval optional `?.`. A pure receiver may be named twice
|
|
2748
|
+
// (guard + branch) with no observable effect, so it keeps the readable
|
|
2749
|
+
// double-name form. A NON-pure receiver (e.g. `markReceiver(null)?.x`) is
|
|
2750
|
+
// bound ONCE via the S4 walrus idiom so its side effect runs exactly once;
|
|
2751
|
+
// the BOUND name is then used in both the guard's presence test and the
|
|
2752
|
+
// trailing link. Only a single optional link can host the walrus (the
|
|
2753
|
+
// receiver is not itself an optional chain ⇒ `inner.guard === null`); a
|
|
2754
|
+
// non-pure receiver UNDER an existing optional chain still throws below.
|
|
2755
|
+
const opt = lowerOptionalLink(inner, node.object, ctx);
|
|
2756
|
+
const linkExpr = isSharedPortableArrayProperty(node.property)
|
|
2757
|
+
? (lowerPortableArrayPropertyPy(opt.branchRef, node.property) ?? `${opt.branchRef}.${node.property}`)
|
|
2758
|
+
: `${opt.branchRef}.${node.property}`;
|
|
2759
|
+
return { guard: opt.guard, expr: linkExpr, lambdaBind: opt.lambdaBind };
|
|
2760
|
+
}
|
|
2359
2761
|
const linkExpr = isSharedPortableArrayProperty(node.property)
|
|
2360
2762
|
? (lowerPortableArrayPropertyPy(inner.expr, node.property) ?? `${inner.expr}.${node.property}`)
|
|
2361
2763
|
: `${inner.expr}.${node.property}`;
|
|
2362
|
-
|
|
2363
|
-
// The receiver expression names what we need to test. The expr names
|
|
2364
|
-
// the receiver twice (once in test, once in branch); reject when that
|
|
2365
|
-
// would re-evaluate side-effecting code.
|
|
2366
|
-
if (!isReceiverChainPure(node.object)) {
|
|
2367
|
-
throw new Error("Optional chain '?.' on Python target requires a side-effect-free receiver (identifier or pure member chain). " +
|
|
2368
|
-
'Bind the call/await result to a `let` first, then use `let.field?.next` on the bound name.');
|
|
2369
|
-
}
|
|
2370
|
-
const newGuard = inner.guard === null ? `${inner.expr} is not None` : `${inner.guard} and ${inner.expr} is not None`;
|
|
2371
|
-
return { guard: newGuard, expr: linkExpr };
|
|
2372
|
-
}
|
|
2373
|
-
return { guard: inner.guard, expr: linkExpr };
|
|
2764
|
+
return { guard: inner.guard, expr: linkExpr, lambdaBind: inner.lambdaBind };
|
|
2374
2765
|
}
|
|
2375
2766
|
if (node.kind === 'index') {
|
|
2376
2767
|
const obj = node.object;
|
|
2768
|
+
// Slice 2 review fix — the bracket (`index`) form of a regex-literal
|
|
2769
|
+
// property access (`/x/["source"]`, `/x/["flags"]`, `/x/["test"](s)`)
|
|
2770
|
+
// launders the pattern/flags back to a string exactly like the dotted
|
|
2771
|
+
// `/x/.source` member form, so it fails-close identically and BEFORE the
|
|
2772
|
+
// host-ident guard below. A STRING-literal index goes through the same
|
|
2773
|
+
// (empty) portable-property allowlist; a COMPUTED / non-literal index is
|
|
2774
|
+
// unknowable and also fails-close. Mirrors the TS emit + IR-validate index
|
|
2775
|
+
// screens — byte-identical regex message across all three legs. The receiver
|
|
2776
|
+
// is UNWRAPPED first so a wrapped bracket read `(/x/!)["source"]` /
|
|
2777
|
+
// `(/x/ as any)["test"](s)` fails-close identically (round-5 wrapped fix).
|
|
2778
|
+
if (regexLiteralReceiverIR(obj) !== null) {
|
|
2779
|
+
// Routed through the SHARED classifier (a STRING index classifies like the
|
|
2780
|
+
// dotted read; a COMPUTED index is unknowable → fail-close), byte-identical
|
|
2781
|
+
// to the TS emit + IR-validate index screens and the closure walk.
|
|
2782
|
+
const message = classifyRegexLiteralIndexReadFailClose(node);
|
|
2783
|
+
if (message !== null)
|
|
2784
|
+
throw new Error(message);
|
|
2785
|
+
}
|
|
2786
|
+
// Slice H review fix — bracket access must not bypass the fail-closed
|
|
2787
|
+
// guard: `Math["sqrt"]` / `Math["sqrt"](x)` is the same unmapped
|
|
2788
|
+
// host-namespace access as `Math.sqrt`, only spelled as an index node.
|
|
2789
|
+
// Call chains descend through this branch too, so this one site covers
|
|
2790
|
+
// both the read and the call form. Non-literal keys (`Math[k]`) are just
|
|
2791
|
+
// as unmapped — the label degrades to `[computed]`.
|
|
2792
|
+
if (obj.kind === 'ident') {
|
|
2793
|
+
const label = node.index.kind === 'strLit' ? node.index.value : '[computed]';
|
|
2794
|
+
rejectKnownStdlibIndexPython(obj.name, label);
|
|
2795
|
+
rejectUnmappedHostNamespacePython(obj.name, label, ctx);
|
|
2796
|
+
}
|
|
2377
2797
|
const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
|
|
2378
2798
|
? lowerChain(obj, ctx)
|
|
2379
2799
|
: { guard: null, expr: emitPyExprCtx(obj, ctx) };
|
|
2380
2800
|
const index = emitPyExprCtx(node.index, ctx);
|
|
2381
2801
|
if (node.optional) {
|
|
2382
|
-
//
|
|
2383
|
-
//
|
|
2384
|
-
//
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
'Bind call/await receiver results to `let` first, then index the bound name.');
|
|
2388
|
-
}
|
|
2389
|
-
const newGuard = inner.guard === null ? `${inner.expr} is not None` : `${inner.guard} and ${inner.expr} is not None`;
|
|
2390
|
-
return { guard: newGuard, expr: `${inner.expr}[${index}]` };
|
|
2802
|
+
// Slice S7 — single-eval optional `?.[]` (same walrus discipline as `?.`).
|
|
2803
|
+
// A non-pure receiver is bound once; the index expression appears only in
|
|
2804
|
+
// the selected branch, matching JS `?.[]`.
|
|
2805
|
+
const opt = lowerOptionalLink(inner, node.object, ctx);
|
|
2806
|
+
return { guard: opt.guard, expr: `${opt.branchRef}[${index}]`, lambdaBind: opt.lambdaBind };
|
|
2391
2807
|
}
|
|
2392
2808
|
const wrapped = needsIndexReceiverParens(node.object) ? `(${inner.expr})` : inner.expr;
|
|
2393
|
-
return { guard: inner.guard, expr: `${wrapped}[${index}]
|
|
2809
|
+
return { guard: inner.guard, expr: `${wrapped}[${index}]`, lambdaBind: inner.lambdaBind };
|
|
2394
2810
|
}
|
|
2395
2811
|
// node.kind === 'call'
|
|
2396
2812
|
if (node.optional) {
|
|
@@ -2441,12 +2857,33 @@ function lowerChain(node, ctx) {
|
|
|
2441
2857
|
const errArgs = node.args.map((arg) => emitPyExprCtx(arg, ctx)).join(', ');
|
|
2442
2858
|
return { guard: null, expr: `Exception(${errArgs})` };
|
|
2443
2859
|
}
|
|
2860
|
+
// Slice 2 — a bare `RegExp(p, f)` call (callee is an ident, missed by the
|
|
2861
|
+
// member-callee guard below) fails-close with the shared regex message,
|
|
2862
|
+
// BEFORE generic call emission would leak a verbatim `RegExp(...)` NameError.
|
|
2863
|
+
// Honors user shadowing via `isProvenUserBinding`.
|
|
2864
|
+
if (node.callee.kind === 'ident') {
|
|
2865
|
+
rejectHostRegExpValuePython(node.callee.name, ctx);
|
|
2866
|
+
}
|
|
2867
|
+
// Slice H — fail-closed on an UNMAPPED host-namespace member CALL. This runs
|
|
2868
|
+
// AFTER every explicit lowering hook above (stdlib, regex, lambda/array,
|
|
2869
|
+
// portable-array, super/String/Error) and BEFORE generic call emission, so a
|
|
2870
|
+
// `Root.member(args)` call whose root is host-namespace-shaped and not proven
|
|
2871
|
+
// user-bound throws the portable-lowering diagnostic instead of leaking
|
|
2872
|
+
// invalid verbatim Python (`Math.sqrt(x)` → runtime NameError). Canonical
|
|
2873
|
+
// KERN stdlib roots (`Number`/`Json`/…) already returned via the stdlib hook,
|
|
2874
|
+
// so they never reach here; user receivers (`client.send(...)`) and a
|
|
2875
|
+
// proven-local `Math` are not host-namespace-shaped / are user-bound and pass
|
|
2876
|
+
// through. Capitalization-agnostic: equally catches `console.log(...)`,
|
|
2877
|
+
// `Promise.all(...)`, and lowercase host roots such as `process.env`.
|
|
2878
|
+
if (node.callee.kind === 'member' && node.callee.object.kind === 'ident') {
|
|
2879
|
+
rejectUnmappedHostNamespacePython(node.callee.object.name, node.callee.property, ctx);
|
|
2880
|
+
}
|
|
2444
2881
|
const callee = node.callee;
|
|
2445
2882
|
const inner = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index'
|
|
2446
2883
|
? lowerChain(callee, ctx)
|
|
2447
2884
|
: { guard: null, expr: emitPyExprCtx(callee, ctx) };
|
|
2448
2885
|
const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', ');
|
|
2449
|
-
return { guard: inner.guard, expr: `${inner.expr}(${args})
|
|
2886
|
+
return { guard: inner.guard, expr: `${inner.expr}(${args})`, lambdaBind: inner.lambdaBind };
|
|
2450
2887
|
}
|
|
2451
2888
|
/**
|
|
2452
2889
|
* Lower a portable Array *method call* (e.g. `arr.push(x)`) through the shared
|
|
@@ -2482,18 +2919,32 @@ function lowerPortableArrayCallPython(call, ctx) {
|
|
|
2482
2919
|
// finding). The optional-chain guard below still applies to ALL methods.
|
|
2483
2920
|
if (sharedPortableMethodRequiresPureReceiver(callee.property) && !isReceiverChainPure(recvNode))
|
|
2484
2921
|
return null;
|
|
2485
|
-
|
|
2922
|
+
// S5 review fix — these list-ops lowerings embed the receiver in a
|
|
2923
|
+
// comprehension/generator ITERABLE (`join`: `str(__v) for __v in <recv>`;
|
|
2924
|
+
// `flat`: `for __x in <recv>`; `indexOf`: `enumerate(<recv>)` inside a
|
|
2925
|
+
// genexp), where CPython rejects `:=` outright. Emit the receiver under the
|
|
2926
|
+
// walrus ban AND parenthesize compound receivers (a bare ternary in a
|
|
2927
|
+
// generator head reads its `if` as the comprehension filter — SyntaxError).
|
|
2928
|
+
const embedsReceiverInGenexp = callee.property === 'join' || callee.property === 'flat' || callee.property === 'indexOf';
|
|
2929
|
+
const emitRecv = () => recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
|
|
2486
2930
|
? lowerChain(recvNode, ctx)
|
|
2487
2931
|
: { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
|
|
2932
|
+
const recv = embedsReceiverInGenexp ? withWalrusBan(ctx, emitRecv) : emitRecv();
|
|
2488
2933
|
// A pure receiver can still be an optional chain (`a?.b`), which carries a
|
|
2489
2934
|
// None-guard the flat shim can't honor — fall through for those too.
|
|
2490
2935
|
if (recv.guard !== null)
|
|
2491
2936
|
return null;
|
|
2937
|
+
const recvExpr = embedsReceiverInGenexp ? parenthesizeIterable(recv.expr) : recv.expr;
|
|
2492
2938
|
const args = call.args.map((a) => (callee.property === 'fill' ? emitPyArrayFillArg(a, ctx) : emitPyExprCtx(a, ctx)));
|
|
2493
|
-
const lowered = lowerPortableArrayMethodPy(
|
|
2939
|
+
const lowered = lowerPortableArrayMethodPy(recvExpr, callee.property, args, { sentinelMiss: ctx.coerceJsValues });
|
|
2494
2940
|
if (lowered !== null && callee.property === 'fill') {
|
|
2495
2941
|
ctx.helpers.add(KERN_JS_ARRAY_HELPERS_PY);
|
|
2496
2942
|
}
|
|
2943
|
+
// Slice S7 — `Array.at` out-of-range yields the undefined sentinel in value
|
|
2944
|
+
// mode; register the helper that defines `_KERN_UNDEFINED`.
|
|
2945
|
+
if (lowered !== null && callee.property === 'at' && ctx.coerceJsValues) {
|
|
2946
|
+
ctx.helpers.add(KERN_NULLISH_HELPER_PY);
|
|
2947
|
+
}
|
|
2497
2948
|
return lowered;
|
|
2498
2949
|
}
|
|
2499
2950
|
function emitPyArrayFillArg(node, ctx) {
|
|
@@ -2636,9 +3087,13 @@ function lowerLambdaArrayCallPython(call, ctx) {
|
|
|
2636
3087
|
// single-eval property is what makes M6 (`this.bump().map(...)` runs bump()
|
|
2637
3088
|
// exactly once) correct.
|
|
2638
3089
|
const recvNode = callee.object;
|
|
2639
|
-
|
|
3090
|
+
// S5 review fix — the receiver lands in the comprehension's generator head
|
|
3091
|
+
// (`for el in <recv>`), where CPython rejects `:=` at any nesting depth, so
|
|
3092
|
+
// it is emitted under the walrus ban (logical/nullish/typeof producers at
|
|
3093
|
+
// any depth switch to their lambda-parameter forms).
|
|
3094
|
+
const recv = withWalrusBan(ctx, () => recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
|
|
2640
3095
|
? lowerChain(recvNode, ctx)
|
|
2641
|
-
: { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
|
|
3096
|
+
: { guard: null, expr: emitPyExprCtx(recvNode, ctx) });
|
|
2642
3097
|
// An optional-chain receiver (`a?.b`) carries a None-guard the comprehension
|
|
2643
3098
|
// can't honor — fall through unchanged for those.
|
|
2644
3099
|
if (recv.guard !== null)
|
|
@@ -2839,43 +3294,196 @@ function lowerRegexCallPython(call, ctx) {
|
|
|
2839
3294
|
const callee = call.callee;
|
|
2840
3295
|
if (callee.kind !== 'member')
|
|
2841
3296
|
return null;
|
|
3297
|
+
// Slice-3b FIX 4 (parity): a call whose receiver is a KERN-stdlib NAMESPACE
|
|
3298
|
+
// (`Math.match(/a/g)`, `JSON.split(/,/)`) is NOT a string regex method —
|
|
3299
|
+
// defer to `applyStdlibLoweringPython`, which rejects the unknown stdlib
|
|
3300
|
+
// member exactly like the TS emitter (where `applyStdlibLoweringTS` runs
|
|
3301
|
+
// BEFORE the regex lowering). Without this, the regex path mis-claimed the
|
|
3302
|
+
// namespace as the SUBJECT and emitted broken `…finditer("a", Math, …)`
|
|
3303
|
+
// while TS fail-closed — a cross-target divergence.
|
|
3304
|
+
if (callee.object.kind === 'ident' && KERN_STDLIB_MODULES.has(callee.object.name)) {
|
|
3305
|
+
return null;
|
|
3306
|
+
}
|
|
3307
|
+
// Slice-3c DETECT-and-fail-close: a regex method whose regex position holds an
|
|
3308
|
+
// ident KNOWN to be regex-bound (`let re = /…/; s.match(re)`) is NOT portable —
|
|
3309
|
+
// throw the SAME shared `REGEX_NONLITERAL_FAILCLOSE` the TS target throws (see
|
|
3310
|
+
// `assertNoBoundRegexMethodTS` in body-ts.ts). A string-/unknown-bound ident is
|
|
3311
|
+
// NOT flagged: it falls through to the plain host method below (`s.match(needle)`).
|
|
3312
|
+
const regexArgIdent = regexMethodRegexArgIdent(call);
|
|
3313
|
+
if (regexArgIdent !== null && lookupRegexBinding(ctx, regexArgIdent) !== null) {
|
|
3314
|
+
throw new Error(REGEX_NONLITERAL_FAILCLOSE);
|
|
3315
|
+
}
|
|
3316
|
+
// --- Receiver-is-regex shapes: `regex.test(s)`, `regex.exec(s)` ---
|
|
2842
3317
|
const receiverRegex = resolveRegexExpr(callee.object, ctx);
|
|
2843
3318
|
if (callee.property === 'test' && receiverRegex !== null) {
|
|
2844
3319
|
if (call.args.length !== 1)
|
|
2845
3320
|
return null;
|
|
2846
3321
|
if (receiverRegex.flags.includes('g')) {
|
|
2847
|
-
|
|
3322
|
+
// .test(/g) is STATEFUL (lastIndex advances+wraps); no portable re analog.
|
|
3323
|
+
throw new Error(REGEX_TEST_G_FAILCLOSE);
|
|
2848
3324
|
}
|
|
2849
3325
|
ctx.imports.add('re');
|
|
2850
3326
|
return `(__k_re.search(${pyRegexPattern(receiverRegex)}, ${emitPyExprCtx(call.args[0], ctx)}, ${pyRegexFlags(receiverRegex.flags)}) is not None)`;
|
|
2851
3327
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
3328
|
+
if (callee.property === 'exec' && receiverRegex !== null) {
|
|
3329
|
+
// .exec drives a JS-only stateful `while ((m = re.exec(s)))` loop; fail-close
|
|
3330
|
+
// and redirect to the portable `.matchAll` iteration (D4) rather than silently
|
|
3331
|
+
// rewrite a loop whose body may mutate lastIndex.
|
|
3332
|
+
throw new Error(REGEX_EXEC_FAILCLOSE);
|
|
3333
|
+
}
|
|
3334
|
+
// --- Receiver-is-string shapes: arg[0] is the regex ---
|
|
3335
|
+
const firstArgRegex = call.args.length >= 1 ? resolveRegexExpr(call.args[0], ctx) : null;
|
|
3336
|
+
// `.match(s)` — no /g: canonical {full,groups,index,named}|None shape (the
|
|
3337
|
+
// load-bearing portability fix, D2). With /g: full matches only via
|
|
3338
|
+
// finditer.group(0) (NEVER re.findall, which returns tuples when >1 group),
|
|
3339
|
+
// or None when empty.
|
|
3340
|
+
if (callee.property === 'match' && firstArgRegex !== null && call.args.length === 1) {
|
|
3341
|
+
ctx.imports.add('re');
|
|
3342
|
+
const pat = pyRegexPattern(firstArgRegex);
|
|
3343
|
+
const subject = emitPyExprCtx(callee.object, ctx);
|
|
3344
|
+
if (firstArgRegex.flags.includes('g')) {
|
|
3345
|
+
const flags = pyRegexFlags(firstArgRegex.flags, { allowGlobal: true });
|
|
3346
|
+
return `([__k_m.group(0) for __k_m in __k_re.finditer(${pat}, ${subject}, ${flags})] or None)`;
|
|
3347
|
+
}
|
|
3348
|
+
ctx.helpers.add(KERN_REGEX_MATCH_HELPER_PY);
|
|
3349
|
+
return `_kern_regex_match(${pat}, ${subject}, ${pyRegexFlags(firstArgRegex.flags)})`;
|
|
3350
|
+
}
|
|
3351
|
+
// `.matchAll(s)` — requires /g (a non-global matchAll throws TypeError in JS).
|
|
3352
|
+
// Shapes finditer into [{full,groups,index}, …], incl. zero-width advances.
|
|
3353
|
+
if (callee.property === 'matchAll' && firstArgRegex !== null && call.args.length === 1) {
|
|
3354
|
+
if (!firstArgRegex.flags.includes('g')) {
|
|
3355
|
+
throw new Error(REGEX_MATCHALL_NO_G_FAILCLOSE);
|
|
3356
|
+
}
|
|
3357
|
+
ctx.imports.add('re');
|
|
3358
|
+
ctx.helpers.add(KERN_REGEX_MATCHALL_HELPER_PY);
|
|
3359
|
+
return `_kern_regex_matchall(${pyRegexPattern(firstArgRegex)}, ${emitPyExprCtx(callee.object, ctx)}, ${pyRegexFlags(firstArgRegex.flags, { allowGlobal: true })})`;
|
|
3360
|
+
}
|
|
3361
|
+
// `.split(s)` — IN-CORE for a non-zero-width pattern with NO limit arg (capture
|
|
3362
|
+
// groups interleave portably). FAIL-CLOSE on a zero-width-capable pattern
|
|
3363
|
+
// (empty-edge divergence) or any limit/2nd arg (truncate vs remainder).
|
|
3364
|
+
if (callee.property === 'split' && firstArgRegex !== null) {
|
|
3365
|
+
if (call.args.length > 1) {
|
|
3366
|
+
throw new Error(REGEX_SPLIT_LIMIT_FAILCLOSE);
|
|
3367
|
+
}
|
|
3368
|
+
if (isZeroWidthCapableRegex(firstArgRegex.pattern)) {
|
|
3369
|
+
throw new Error(REGEX_SPLIT_ZEROWIDTH_FAILCLOSE);
|
|
2856
3370
|
}
|
|
2857
3371
|
ctx.imports.add('re');
|
|
2858
|
-
return `__k_re.
|
|
3372
|
+
return `__k_re.split(${pyRegexPattern(firstArgRegex)}, ${emitPyExprCtx(callee.object, ctx)}, flags=${pyRegexFlags(firstArgRegex.flags, { allowGlobal: true })})`;
|
|
2859
3373
|
}
|
|
3374
|
+
// `.replace(s, r)` — no /g: FIRST match only (count=1); /g: ALL (count=0).
|
|
2860
3375
|
const replaceRegex = call.args.length === 2 ? resolveRegexExpr(call.args[0], ctx) : null;
|
|
2861
3376
|
if (callee.property === 'replace' && replaceRegex !== null) {
|
|
2862
3377
|
ctx.imports.add('re');
|
|
2863
3378
|
const count = replaceRegex.flags.includes('g') ? '0' : '1';
|
|
2864
|
-
|
|
3379
|
+
const repl = emitPyReplArg(call.args[1], replaceRegex, ctx);
|
|
3380
|
+
return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${repl}, ${emitPyExprCtx(callee.object, ctx)}, count=${count}, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
|
|
3381
|
+
}
|
|
3382
|
+
// `.replaceAll(s, r)` — requires /g (a non-global replaceAll throws TypeError in
|
|
3383
|
+
// JS); otherwise identical to `.replace` /g (count=0, ALL matches replaced).
|
|
3384
|
+
if (callee.property === 'replaceAll' && replaceRegex !== null) {
|
|
3385
|
+
if (!replaceRegex.flags.includes('g')) {
|
|
3386
|
+
throw new Error(REGEX_REPLACEALL_NO_G_FAILCLOSE);
|
|
3387
|
+
}
|
|
3388
|
+
ctx.imports.add('re');
|
|
3389
|
+
const repl = emitPyReplArg(call.args[1], replaceRegex, ctx);
|
|
3390
|
+
return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${repl}, ${emitPyExprCtx(callee.object, ctx)}, count=0, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
|
|
2865
3391
|
}
|
|
2866
3392
|
return null;
|
|
2867
3393
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
3394
|
+
/**
|
|
3395
|
+
* Milestone C, Slice 4 — emit the Python `re.sub` REPLACEMENT argument for a
|
|
3396
|
+
* `.replace`/`.replaceAll` whose regex position is a literal.
|
|
3397
|
+
*
|
|
3398
|
+
* A STRING-LITERAL replacement is translated from the JS `$`-surface to the
|
|
3399
|
+
* Python repl VALUE via the shared `translateReplStringToPython`, then serialized
|
|
3400
|
+
* back to `.py` SOURCE through a synthetic `strLit` node so the ordinary
|
|
3401
|
+
* string-literal escaper double-escapes the backslashes correctly (gap 6: the
|
|
3402
|
+
* translator yields the runtime VALUE `\g<1>` / `\\` — the serializer turns those
|
|
3403
|
+
* into the source `"\\g<1>"` / `"\\\\"` that evaluates back to that value). A
|
|
3404
|
+
* NON-LITERAL replacement (a variable / computed string) cannot be translated
|
|
3405
|
+
* statically and FAILS-CLOSE symmetrically with the TS target.
|
|
3406
|
+
*/
|
|
3407
|
+
function emitPyReplArg(arg, replaceRegex, ctx) {
|
|
3408
|
+
if (arg.kind !== 'strLit') {
|
|
3409
|
+
throw new Error(REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE);
|
|
3410
|
+
}
|
|
3411
|
+
// Capture metadata is read from the KERN/JS `(?<name>)` surface (BEFORE R6's
|
|
3412
|
+
// `(?P<name>)` rewrite), matching the way `$<name>` refs are written.
|
|
3413
|
+
const meta = regexCaptureMeta(replaceRegex.pattern);
|
|
3414
|
+
const pyReplValue = translateReplStringToPython(arg.value, meta);
|
|
3415
|
+
// Serialize the translated VALUE to `.py` source via the normal strLit path
|
|
3416
|
+
// (gap 6 — keep translation and serialization layers separate).
|
|
3417
|
+
const synthetic = { kind: 'strLit', value: pyReplValue, quote: '"' };
|
|
3418
|
+
return emitPyExprCtx(synthetic, ctx);
|
|
3419
|
+
}
|
|
3420
|
+
function resolveRegexExpr(node, _ctx) {
|
|
3421
|
+
// Slice-3c: ONLY a DIRECT regex literal lowers canonically. A let-bound regex
|
|
3422
|
+
// ident no longer RESOLVES to its literal here (the old `lookupRegexBinding`
|
|
3423
|
+
// resolution emitted a STALE pattern when the binding was reassigned and was
|
|
3424
|
+
// fragile to track). The fail-close for a known-regex-bound ident is made by
|
|
3425
|
+
// `lowerRegexCallPython` via the shared `regexMethodRegexArgIdent` detector —
|
|
3426
|
+
// symmetric with the TS target. A string-/unknown-bound ident returns null
|
|
3427
|
+
// and stays a plain host method (the `s.match(stringVar)` case).
|
|
3428
|
+
//
|
|
3429
|
+
// Transparent wrappers (`as`/`!`) are peeled via `regexLiteralReceiverIR` —
|
|
3430
|
+
// mirroring the TS `resolveRegexLitTS` — so a wrapped portable receiver call
|
|
3431
|
+
// `(/x/).test(s)` / `(/x/ as any).test(s)` lowers, and a wrapped non-portable
|
|
3432
|
+
// one `(/x/ as any).exec(s)` fails-close through `lowerRegexCallPython` (round-5
|
|
3433
|
+
// wrapped-receiver fix), identically to the TS leg.
|
|
3434
|
+
return regexLiteralReceiverIR(node);
|
|
2874
3435
|
}
|
|
2875
3436
|
function pyRegexPattern(node) {
|
|
3437
|
+
// Slice 5: fail-close any non-BMP (astral) construct in the PATTERN before any
|
|
3438
|
+
// normalization, on the RAW pattern — the IDENTICAL decision + message the TS
|
|
3439
|
+
// emitter makes (the `\/`→`/` un-escape below does not touch any astral
|
|
3440
|
+
// construct, so scanning the raw `node.pattern` here is byte-symmetric with TS).
|
|
3441
|
+
const astral = scanRegexAstral(node.pattern);
|
|
3442
|
+
if (astral !== null)
|
|
3443
|
+
throw new Error(regexAstralFailMessage(astral.char));
|
|
3444
|
+
// FIX 2: a non-portable named group (`(?<café>…)`, `(?<$x>…)`, `(?<>…)`) is
|
|
3445
|
+
// refused on BOTH targets (the same validator runs in the TS regex-literal emit
|
|
3446
|
+
// chokepoints), instead of emitting `(?P<café>…)` / a JS-form `(?<café>…)` that
|
|
3447
|
+
// diverges or crashes Python `re`. Run BEFORE any rewrite so the refusal is over
|
|
3448
|
+
// the original surface (the same surface `regexCaptureMeta` and the TS side see).
|
|
3449
|
+
validateRegexNamedGroupsPortable(node.pattern);
|
|
2876
3450
|
// JS escapes `/` because it delimits the literal; Python string regexes do not
|
|
2877
3451
|
// treat `/` specially, so preserve the semantic pattern without that escape.
|
|
2878
|
-
|
|
3452
|
+
const unescaped = node.pattern.replace(/\\\//g, '/');
|
|
3453
|
+
// Milestone C, Slice 1 — emission-normalization (shared with the TS emitter
|
|
3454
|
+
// for the class transform, so it is byte-identical across targets):
|
|
3455
|
+
// 1. `\d \w \s` → explicit ASCII classes (same `normalizeRegexClasses` both
|
|
3456
|
+
// targets), so Python's Unicode-aware shorthand matches JS's ASCII.
|
|
3457
|
+
// 2. Slice-/i: class-expand non-ASCII Set(A) letters under /i into explicit
|
|
3458
|
+
// fold classes (Set(B) → throw an identical-to-TS compile error). Runs on
|
|
3459
|
+
// the SAME shared `expandRegexIFold` the TS emitter calls, AFTER class
|
|
3460
|
+
// normalization (Slice-1 classes are pure-ASCII → untouched by the fold
|
|
3461
|
+
// scan) and BEFORE anchor lowering (anchors are ASCII → untouched). Order
|
|
3462
|
+
// is class → fold → anchors; each touches a disjoint character set, so it
|
|
3463
|
+
// is parity-safe and byte-identical to TS. `re.IGNORECASE | re.ASCII` is
|
|
3464
|
+
// kept (handled in pyRegexFlags) — `re.ASCII` is the load-bearing invariant
|
|
3465
|
+
// that makes KEEP-i safe (it suppresses any Python re-fold of the explicit
|
|
3466
|
+
// non-ASCII class members).
|
|
3467
|
+
// 3. Python-only anchor lowering: on the non-`/m` path `$`→`\Z`, `^`→`\A`
|
|
3468
|
+
// so Python anchors match JS's input-end/start semantics (`re.ASCII` and
|
|
3469
|
+
// `re.M` are handled in pyRegexFlags). On the `/m` path anchors are kept.
|
|
3470
|
+
const classed = normalizeRegexClasses(unescaped);
|
|
3471
|
+
const folded = expandRegexIFold(classed, node.flags);
|
|
3472
|
+
if ('failClose' in folded)
|
|
3473
|
+
throw new Error(regexIFoldFailMessage(folded.char, folded.reason));
|
|
3474
|
+
const normalized = lowerRegexAnchorsPython(folded.pattern, node.flags);
|
|
3475
|
+
// Milestone C, Slice 4 — R6: named-group PATTERN syntax (`(?<name>)` /
|
|
3476
|
+
// `\k<name>`) → Python `re` syntax (`(?P<name>)` / `(?P=name)`). PYTHON-ONLY
|
|
3477
|
+
// (JS keeps the original form). Python rejects the JS named-group syntax
|
|
3478
|
+
// outright, so this rewrite is load-bearing for ANY named-group pattern on the
|
|
3479
|
+
// Python target — and required for a `$<name>` repl ref to resolve. Run LAST,
|
|
3480
|
+
// AFTER the `/i` fold expansion: `expandRegexIFold`'s HOLE-1 fail-close depends
|
|
3481
|
+
// on seeing the ORIGINAL `\k<name>` backref token (`/(?<g>é)\k<g>/i` must still
|
|
3482
|
+
// fail-close), so R6 must NOT consume `\k<` before the fold scan. The fold/class/
|
|
3483
|
+
// anchor passes leave the ASCII `(?<` / `\k<` group syntax untouched, so applying
|
|
3484
|
+
// R6 here is order-stable and parity-safe.
|
|
3485
|
+
const named = lowerRegexNamedGroupsPython(normalized);
|
|
3486
|
+
return JSON.stringify(named);
|
|
2879
3487
|
}
|
|
2880
3488
|
function pyRegexFlags(flags, options = {}) {
|
|
2881
3489
|
const unsupported = [...flags].filter((f) => {
|
|
@@ -2896,10 +3504,125 @@ function pyRegexFlags(flags, options = {}) {
|
|
|
2896
3504
|
parts.push('__k_re.MULTILINE');
|
|
2897
3505
|
if (flags.includes('s'))
|
|
2898
3506
|
parts.push('__k_re.DOTALL');
|
|
2899
|
-
|
|
3507
|
+
// Milestone C, Slice 1 — always inject `re.ASCII` so Python `\b` and the
|
|
3508
|
+
// emitted ASCII classes match JS (without `/u`) semantics. This is the
|
|
3509
|
+
// load-bearing flag for word-boundary parity (see the `\bcafé\b` killer row).
|
|
3510
|
+
parts.push('__k_re.ASCII');
|
|
3511
|
+
return parts.join(' | ');
|
|
3512
|
+
}
|
|
3513
|
+
function wrapGuardIfAny(g, ctx) {
|
|
3514
|
+
if (g.guard === null)
|
|
3515
|
+
return g.expr;
|
|
3516
|
+
// Slice S7 — an optional-chain short-circuit yields the undefined SENTINEL in
|
|
3517
|
+
// native (coerce) bodies, so `typeof (undefined?.x)` is "undefined" and the
|
|
3518
|
+
// result participates in `??`/`===` nullish semantics. The helper-less
|
|
3519
|
+
// Ground/React layer (which never materializes the sentinel) keeps the
|
|
3520
|
+
// pre-slice `else None` short-circuit.
|
|
3521
|
+
let conditional;
|
|
3522
|
+
if (ctx.coerceJsValues) {
|
|
3523
|
+
ctx.helpers.add(KERN_NULLISH_HELPER_PY);
|
|
3524
|
+
conditional = `(${g.expr} if ${g.guard} else _KERN_UNDEFINED)`;
|
|
3525
|
+
}
|
|
3526
|
+
else {
|
|
3527
|
+
conditional = `(${g.expr} if ${g.guard} else None)`;
|
|
3528
|
+
}
|
|
3529
|
+
// S7 review fix — in a comprehension-iterable position the non-pure receiver was
|
|
3530
|
+
// bound as a lambda PARAMETER instead of a `:=` walrus (CPython rejects the walrus
|
|
3531
|
+
// anywhere inside a comprehension iterable expression — see BodyEmitContext.banWalrus
|
|
3532
|
+
// and lowerOptionalLink). The guard + branch already reference the bare parameter, so
|
|
3533
|
+
// wrapping the whole conditional in `(lambda <param>: <conditional>)(<arg>)` keeps the
|
|
3534
|
+
// exact contract: <arg> (the receiver) evaluated exactly once, lazy short-circuit branch.
|
|
3535
|
+
if (g.lambdaBind) {
|
|
3536
|
+
return `(lambda ${g.lambdaBind.param}: ${conditional})(${g.lambdaBind.arg})`;
|
|
3537
|
+
}
|
|
3538
|
+
return conditional;
|
|
2900
3539
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
3540
|
+
/** Slice S5 — does the LEFT operand of a `&&`/`||` walrus binding
|
|
3541
|
+
* (`__k_logN := L`) need parentheses?
|
|
3542
|
+
*
|
|
3543
|
+
* The walrus sits inside a `_kern_truthy(...)` call, whose own parens already
|
|
3544
|
+
* disambiguate `L`, so this is defensive/readability wrapping for the lowest-
|
|
3545
|
+
* precedence operand shapes (a `conditional` or `lambda` left operand). A
|
|
3546
|
+
* nested `&&`/`||` left operand is already self-parenthesized by its own
|
|
3547
|
+
* lowering, and an arithmetic/comparison `binary` binds tighter than `:=`, so
|
|
3548
|
+
* neither is wrapped here. */
|
|
3549
|
+
function needsWalrusOperandParens(child) {
|
|
3550
|
+
return child.kind === 'conditional' || child.kind === 'lambda';
|
|
3551
|
+
}
|
|
3552
|
+
/** S5 review fix — run `fn` with `ctx.banWalrus` set (save/restore), for
|
|
3553
|
+
* emitting an operand that will be interpolated into a comprehension/
|
|
3554
|
+
* generator ITERABLE position, where CPython rejects `:=` outright (see
|
|
3555
|
+
* BodyEmitContext.banWalrus). Nested emissions inherit the flag through the
|
|
3556
|
+
* shared ctx, so a walrus producer at ANY depth of the operand (e.g. the
|
|
3557
|
+
* index expression in `items[i || 0]`) switches to its walrus-free form. */
|
|
3558
|
+
function withWalrusBan(ctx, fn) {
|
|
3559
|
+
const previous = ctx.banWalrus === true;
|
|
3560
|
+
ctx.banWalrus = true;
|
|
3561
|
+
try {
|
|
3562
|
+
return fn();
|
|
3563
|
+
}
|
|
3564
|
+
finally {
|
|
3565
|
+
ctx.banWalrus = previous;
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
/** Slice S5 — does the RIGHT operand, emitted in the `else` arm of the lowered
|
|
3569
|
+
* conditional expression, need parentheses? A bare `conditional`/`lambda` in
|
|
3570
|
+
* an `else` arm parses but is ambiguous to read and brittle under further
|
|
3571
|
+
* composition, so wrap those two. A nested `&&`/`||` right operand is already
|
|
3572
|
+
* self-parenthesized. */
|
|
3573
|
+
function needsConditionalAlternateParens(child) {
|
|
3574
|
+
return child.kind === 'conditional' || child.kind === 'lambda';
|
|
3575
|
+
}
|
|
3576
|
+
/** S5 review fix — parenthesize a compound NON-CHAIN member-root expression so
|
|
3577
|
+
* the appended `.prop` link binds to the WHOLE root (`(b if t else c).prop`),
|
|
3578
|
+
* not its last operand (`b if t else c.prop` — silently wrong Python).
|
|
3579
|
+
* Low-precedence node kinds (same set as index receivers) are wrapped UNLESS
|
|
3580
|
+
* the lowering already produced a self-delimited atom: a fully-enclosing
|
|
3581
|
+
* paren pair (the `&&`/`||`/`??` walrus ternaries) or a single helper call
|
|
3582
|
+
* (`__kern_add(a, b)`), which keeps existing pinned bytes stable. */
|
|
3583
|
+
function wrapCompoundRootExpr(obj, emitted) {
|
|
3584
|
+
if (!needsIndexReceiverParens(obj))
|
|
3585
|
+
return emitted;
|
|
3586
|
+
if (isSelfDelimitedPyAtom(emitted))
|
|
3587
|
+
return emitted;
|
|
3588
|
+
return `(${emitted})`;
|
|
3589
|
+
}
|
|
3590
|
+
/** True when `expr` is one self-delimited Python atom: a fully-enclosing
|
|
3591
|
+
* bracket pair (`(...)`, `[...]`) or one identifier-headed call/index chain
|
|
3592
|
+
* whose trailing bracket closes at the very end (`__kern_add(a, b)`). A
|
|
3593
|
+
* leading unary sign (`-a`, `not x`, `await f()`) is NOT an atom. */
|
|
3594
|
+
function isSelfDelimitedPyAtom(expr) {
|
|
3595
|
+
if (expr.length === 0)
|
|
3596
|
+
return false;
|
|
3597
|
+
// Atoms start with an identifier/literal/bracket — a leading operator or
|
|
3598
|
+
// keyword (`-`, `~`, `not `, `await `) always needs wrapping.
|
|
3599
|
+
if (!/^[A-Za-z_0-9"'([{]/.test(expr))
|
|
3600
|
+
return false;
|
|
3601
|
+
if (!expr.includes(' '))
|
|
3602
|
+
return true;
|
|
3603
|
+
// Walk brackets/strings; any top-level space outside brackets means the
|
|
3604
|
+
// expression is compound (`b if t else c`), not an atom.
|
|
3605
|
+
let depth = 0;
|
|
3606
|
+
let quote = null;
|
|
3607
|
+
for (let i = 0; i < expr.length; i++) {
|
|
3608
|
+
const ch = expr[i];
|
|
3609
|
+
if (quote) {
|
|
3610
|
+
if (ch === '\\')
|
|
3611
|
+
i++;
|
|
3612
|
+
else if (ch === quote)
|
|
3613
|
+
quote = null;
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
if (ch === '"' || ch === "'")
|
|
3617
|
+
quote = ch;
|
|
3618
|
+
else if (ch === '(' || ch === '[' || ch === '{')
|
|
3619
|
+
depth++;
|
|
3620
|
+
else if (ch === ')' || ch === ']' || ch === '}')
|
|
3621
|
+
depth--;
|
|
3622
|
+
else if (ch === ' ' && depth === 0)
|
|
3623
|
+
return false;
|
|
3624
|
+
}
|
|
3625
|
+
return depth === 0;
|
|
2903
3626
|
}
|
|
2904
3627
|
function needsIndexReceiverParens(child) {
|
|
2905
3628
|
return (child.kind === 'binary' ||
|
|
@@ -2924,6 +3647,11 @@ function needsIndexReceiverParens(child) {
|
|
|
2924
3647
|
function isReceiverChainPure(node) {
|
|
2925
3648
|
if (node.kind === 'ident')
|
|
2926
3649
|
return true;
|
|
3650
|
+
// Slice S7 — `undefined`/`null` literals are constant, so an optional chain
|
|
3651
|
+
// rooted at one (`undefined?.x`, `null?.x`) names a side-effect-free value and
|
|
3652
|
+
// takes the readable double-name guard form (short-circuiting to the sentinel).
|
|
3653
|
+
if (node.kind === 'undefLit' || node.kind === 'nullLit')
|
|
3654
|
+
return true;
|
|
2927
3655
|
if (node.kind === 'member')
|
|
2928
3656
|
return isReceiverChainPure(node.object);
|
|
2929
3657
|
if (node.kind === 'index')
|
|
@@ -2980,6 +3708,88 @@ function mapBinaryOpToPython(op) {
|
|
|
2980
3708
|
return op;
|
|
2981
3709
|
}
|
|
2982
3710
|
}
|
|
3711
|
+
/** Slice H — the host-namespace root set and diagnostic now live in core's
|
|
3712
|
+
* shared codegen module so TS and Python reject the same unmapped host roots
|
|
3713
|
+
* in lockstep. RegExp is intentionally exempt in that shared set for
|
|
3714
|
+
* Milestone B. */
|
|
3715
|
+
/** Slice H — is `name` PROVEN to be a user binding in the current expression
|
|
3716
|
+
* context?
|
|
3717
|
+
*
|
|
3718
|
+
* Reuses the emitter's EXISTING scope/binding model — the single source of
|
|
3719
|
+
* truth — rather than building a parallel tracker:
|
|
3720
|
+
* - `lookupLocalBinding` walks `ctx.localScopes` (function params via
|
|
3721
|
+
* `outerBindings`, body `let`/`const`/`cell` via `declareLocalBinding`,
|
|
3722
|
+
* loop vars, block scopes);
|
|
3723
|
+
* - `ctx.shadowedSymbols` records lambda/comprehension parameters that the
|
|
3724
|
+
* ident emitter already treats as user-local (see the `ident` case);
|
|
3725
|
+
* - `ctx.symbolMap` keys are KERN-form param names the FastAPI generator
|
|
3726
|
+
* renamed (e.g. `userId -> user_id`) — a present key means a real param.
|
|
3727
|
+
*
|
|
3728
|
+
* Per tribunal amendment 2, this FAILS TOWARD REFUSE: when the scope model
|
|
3729
|
+
* cannot prove a binding, we return false (→ the host-root guard fires),
|
|
3730
|
+
* never toward verbatim acceptance. Ground/module emitters that carry no
|
|
3731
|
+
* binding information therefore fail closed for reserved host roots, which is
|
|
3732
|
+
* exactly the intent — those are the contexts that previously leaked invalid
|
|
3733
|
+
* Python. (Honoring a *declared* binding — even one whose runtime value would
|
|
3734
|
+
* be `None` — is by design: this slice reuses the lexical scope model, it
|
|
3735
|
+
* does not do runtime value tracking. A user value named `Math` is the
|
|
3736
|
+
* shadowing case the spec explicitly keeps in scope.) */
|
|
3737
|
+
function isProvenUserBinding(ctx, name) {
|
|
3738
|
+
if (lookupLocalBinding(ctx, name) !== undefined)
|
|
3739
|
+
return true;
|
|
3740
|
+
if (ctx.shadowedSymbols.has(name))
|
|
3741
|
+
return true;
|
|
3742
|
+
if (Object.hasOwn(ctx.symbolMap, name))
|
|
3743
|
+
return true;
|
|
3744
|
+
return false;
|
|
3745
|
+
}
|
|
3746
|
+
/** Slice H — the fail-closed guard for unmapped host-namespace member
|
|
3747
|
+
* expressions (the strangler-pattern interim check).
|
|
3748
|
+
*
|
|
3749
|
+
* ACCEPTED DEBT (do not "fix" by relocating): this check lives at the
|
|
3750
|
+
* EMISSION point inside the code generator, not in a standalone validation
|
|
3751
|
+
* pass. That is interim by design — milestone A completes the portable
|
|
3752
|
+
* lowering registry (`KERN_STDLIB_MODULES`) and replaces these refusals with
|
|
3753
|
+
* real lowerings. Because the explicit AST lowering hooks all run BEFORE this
|
|
3754
|
+
* guard, the site is already scheduled to change as that registry grows: the
|
|
3755
|
+
* guard only ever sees the *remaining* unmapped forms. This is the strangler
|
|
3756
|
+
* pattern — the call site is provisioned to shrink, not to be reworked.
|
|
3757
|
+
*
|
|
3758
|
+
* Trigger predicate (capitalization-agnostic; NO {Math,JSON,Object,Date}
|
|
3759
|
+
* allowlist): a host-namespace-shaped root (`isHostNamespaceRoot`) that is
|
|
3760
|
+
* NOT proven user-bound (`isProvenUserBinding`, which fails toward refuse).
|
|
3761
|
+
* Returns `null` (caller proceeds to generic verbatim emission) for any root
|
|
3762
|
+
* that is provably a user binding or is not host-shaped — so user receivers
|
|
3763
|
+
* (`client.send(...)`, `myMath.sqrt(...)`) and proven-local `Math` pass
|
|
3764
|
+
* through unchanged. Throws otherwise. */
|
|
3765
|
+
function rejectUnmappedHostNamespacePython(root, member, ctx) {
|
|
3766
|
+
if (!isHostNamespaceRoot(root))
|
|
3767
|
+
return null;
|
|
3768
|
+
if (isProvenUserBinding(ctx, root))
|
|
3769
|
+
return null;
|
|
3770
|
+
throw new Error(unmappedHostNamespaceMessage('Python', root, member));
|
|
3771
|
+
}
|
|
3772
|
+
/** Slice 2 — host-`RegExp` fail-close (Python emit). Closes the residual RegExp
|
|
3773
|
+
* positions the generic `Module.member` guard above does NOT cover, throwing the
|
|
3774
|
+
* SAME shared `REGEX_HOST_REGEXP_FAILCLOSE` the TS emitter + IR-validate pass
|
|
3775
|
+
* throw (byte-identical across targets):
|
|
3776
|
+
* - a BARE-VALUE reference (`const R = RegExp`, `RegExp` passed as a value),
|
|
3777
|
+
* - a BARE CALL `RegExp(p, f)` (callee is an ident, missed by the member-callee
|
|
3778
|
+
* guard), and
|
|
3779
|
+
* - `new RegExp(p)` (the Python `new` case otherwise falls through to a verbatim
|
|
3780
|
+
* `RegExp(...)` NameError).
|
|
3781
|
+
* Honors user shadowing via `isProvenUserBinding` (fails toward refuse when a
|
|
3782
|
+
* binding cannot be proven, matching the host-namespace guard's intent), so
|
|
3783
|
+
* `const RegExp = myThing; RegExp` is the user value. `RegExp.prototype`/
|
|
3784
|
+
* `RegExp.$1` already fail-close through the generic member guard (one
|
|
3785
|
+
* diagnostic per site). */
|
|
3786
|
+
function rejectHostRegExpValuePython(name, ctx) {
|
|
3787
|
+
if (name !== 'RegExp')
|
|
3788
|
+
return;
|
|
3789
|
+
if (isProvenUserBinding(ctx, name))
|
|
3790
|
+
return;
|
|
3791
|
+
throw new Error(REGEX_HOST_REGEXP_FAILCLOSE);
|
|
3792
|
+
}
|
|
2983
3793
|
/** Slice 2a — KERN-stdlib dispatch for Python. Returns the lowered Python
|
|
2984
3794
|
* string when the call matches `<KnownModule>.<method>(args)`, or null when
|
|
2985
3795
|
* it doesn't. Throws on `<KnownModule>.<unknownMethod>(...)` with a
|
|
@@ -2988,6 +3798,31 @@ function mapBinaryOpToPython(op) {
|
|
|
2988
3798
|
* Slice 3b — when the matched entry declares `requires.py`, the import
|
|
2989
3799
|
* identifier is added to the per-handler ctx.imports set so the FastAPI
|
|
2990
3800
|
* generator can emit `import math` (etc.) at the top of the function body. */
|
|
3801
|
+
function applyStdlibPropertyLoweringPython(member, ctx) {
|
|
3802
|
+
if (member.optional)
|
|
3803
|
+
return null;
|
|
3804
|
+
if (member.object.kind !== 'ident')
|
|
3805
|
+
return null;
|
|
3806
|
+
const moduleName = member.object.name;
|
|
3807
|
+
if (!KERN_STDLIB_MODULES.has(moduleName))
|
|
3808
|
+
return null;
|
|
3809
|
+
const propertyName = member.property;
|
|
3810
|
+
const entry = lookupStdlibProperty(moduleName, propertyName);
|
|
3811
|
+
if (entry === null) {
|
|
3812
|
+
const callEntry = lookupStdlibCall(moduleName, propertyName);
|
|
3813
|
+
if (callEntry !== null) {
|
|
3814
|
+
throw new Error(`KERN-stdlib method '${moduleName}.${propertyName}' cannot be referenced as a value in portable Python lowering; call it directly.`);
|
|
3815
|
+
}
|
|
3816
|
+
throwUnknownStdlibMemberPython(moduleName, propertyName);
|
|
3817
|
+
}
|
|
3818
|
+
registerStdlibRequirementPython(entry.requires?.py, ctx);
|
|
3819
|
+
return entry.py;
|
|
3820
|
+
}
|
|
3821
|
+
function rejectKnownStdlibIndexPython(root, member) {
|
|
3822
|
+
if (!KERN_STDLIB_MODULES.has(root))
|
|
3823
|
+
return;
|
|
3824
|
+
throwUnknownStdlibMemberPython(root, member);
|
|
3825
|
+
}
|
|
2991
3826
|
function applyStdlibLoweringPython(call, ctx) {
|
|
2992
3827
|
const callee = call.callee;
|
|
2993
3828
|
if (callee.kind !== 'member')
|
|
@@ -2998,34 +3833,90 @@ function applyStdlibLoweringPython(call, ctx) {
|
|
|
2998
3833
|
if (!KERN_STDLIB_MODULES.has(moduleName))
|
|
2999
3834
|
return null;
|
|
3000
3835
|
const methodName = callee.property;
|
|
3001
|
-
const entry =
|
|
3836
|
+
const entry = lookupStdlibCall(moduleName, methodName);
|
|
3002
3837
|
if (entry === null) {
|
|
3003
|
-
const
|
|
3004
|
-
|
|
3005
|
-
|
|
3838
|
+
const propertyEntry = lookupStdlibProperty(moduleName, methodName);
|
|
3839
|
+
if (propertyEntry !== null) {
|
|
3840
|
+
throw new Error(`KERN-stdlib property '${moduleName}.${methodName}' is not callable.`);
|
|
3841
|
+
}
|
|
3842
|
+
throwUnknownStdlibMemberPython(moduleName, methodName);
|
|
3006
3843
|
}
|
|
3007
3844
|
// Slice-2 review fix: enforce declared arity (matches TS-side check).
|
|
3008
|
-
|
|
3009
|
-
|
|
3845
|
+
validateStdlibCallArityPython(moduleName, methodName, entry, call.args.length);
|
|
3846
|
+
if (moduleName === 'Array' && methodName === 'from' && call.args.some((arg) => arg.kind === 'spread')) {
|
|
3847
|
+
throw new Error('Array.from portable lowering does not accept spread arguments; pass source and mapper directly.');
|
|
3010
3848
|
}
|
|
3011
3849
|
const listLambda = lowerListLambdaPython(moduleName, methodName, call, ctx);
|
|
3012
3850
|
if (listLambda !== null)
|
|
3013
3851
|
return listLambda;
|
|
3014
3852
|
// Slice 3b — register required imports (e.g., `Number.floor` ⇒ `import math`).
|
|
3015
|
-
|
|
3016
|
-
ctx.imports.add(entry.requires.py);
|
|
3853
|
+
registerStdlibRequirementPython(entry.requires?.py, ctx);
|
|
3017
3854
|
const args = call.args.map((a) => {
|
|
3018
3855
|
const emitted = emitPyExprCtx(a, ctx);
|
|
3019
|
-
return needsArgParens(a) ? `(${emitted})` : emitted;
|
|
3856
|
+
return a.kind !== 'spread' && needsArgParens(a) ? `(${emitted})` : emitted;
|
|
3020
3857
|
});
|
|
3021
|
-
|
|
3858
|
+
// Slice S7 — `Json.stringify` on Python routes through the sentinel-aware shim
|
|
3859
|
+
// (single-source `KERN_JSON_STRINGIFY_SHIM_PY`) instead of raw `__k_json.dumps`,
|
|
3860
|
+
// so `JSON.stringify(undefined)` is host-undefined, an object key whose value
|
|
3861
|
+
// is the sentinel is omitted, and a sentinel array element becomes JSON null —
|
|
3862
|
+
// matching JS. The shim references `__k_json`, supplied by the table's
|
|
3863
|
+
// `requires.py: 'json'` import registered above (one import shared with parse).
|
|
3864
|
+
if ((moduleName === 'Json' || moduleName === 'JSON') && methodName === 'stringify') {
|
|
3865
|
+
ctx.helpers.add(KERN_JSON_STRINGIFY_SHIM_PY);
|
|
3866
|
+
return `_kern_json_stringify(${args[0]})`;
|
|
3867
|
+
}
|
|
3868
|
+
return typeof entry.py === 'function' ? entry.py(args) : applyTemplate(entry.py, args);
|
|
3869
|
+
}
|
|
3870
|
+
function throwUnknownStdlibMemberPython(moduleName, memberName) {
|
|
3871
|
+
const suggestion = suggestStdlibMethod(moduleName, memberName);
|
|
3872
|
+
const hint = suggestion ? ` Did you mean '${moduleName}.${suggestion}'?` : '';
|
|
3873
|
+
throw new Error(`Unknown KERN-stdlib method/member '${moduleName}.${memberName}'.${hint}`);
|
|
3874
|
+
}
|
|
3875
|
+
function validateStdlibCallArityPython(moduleName, methodName, entry, got) {
|
|
3876
|
+
if (entry.arity !== undefined && got !== entry.arity) {
|
|
3877
|
+
throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes ${entry.arity} arg${entry.arity === 1 ? '' : 's'}, got ${got}.`);
|
|
3878
|
+
}
|
|
3879
|
+
if (entry.minArity !== undefined && got < entry.minArity) {
|
|
3880
|
+
throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes at least ${entry.minArity} args, got ${got}.`);
|
|
3881
|
+
}
|
|
3882
|
+
if (entry.maxArity !== undefined && got > entry.maxArity) {
|
|
3883
|
+
throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes at most ${entry.maxArity} args, got ${got}.`);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
function registerStdlibRequirementPython(requirement, ctx) {
|
|
3887
|
+
if (!requirement)
|
|
3888
|
+
return;
|
|
3889
|
+
if (requirement === 'math-host') {
|
|
3890
|
+
ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
|
|
3891
|
+
ctx.helpers.add(KERN_JS_MATH_HELPERS_PY);
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
if (requirement === 'array-host') {
|
|
3895
|
+
ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
|
|
3896
|
+
ctx.helpers.add(KERN_JS_ARRAY_FROM_HELPER_PY);
|
|
3897
|
+
return;
|
|
3898
|
+
}
|
|
3899
|
+
if (requirement === 'object-host') {
|
|
3900
|
+
ctx.helpers.add(KERN_JS_OBJECT_HELPERS_PY);
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
if (requirement === 'number-host') {
|
|
3904
|
+
ctx.helpers.add(KERN_JS_NUMBER_HELPERS_PY);
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
ctx.imports.add(requirement);
|
|
3022
3908
|
}
|
|
3023
3909
|
function lowerListLambdaPython(moduleName, methodName, call, ctx) {
|
|
3024
3910
|
if (moduleName !== 'List')
|
|
3025
3911
|
return null;
|
|
3026
3912
|
if (methodName !== 'map' && methodName !== 'filter')
|
|
3027
3913
|
return null;
|
|
3028
|
-
|
|
3914
|
+
// S5 review fix — the source lands in the comprehension's generator head
|
|
3915
|
+
// (`for x in <source>`): walrus producers must switch to lambda-parameter
|
|
3916
|
+
// forms (CPython rejects `:=` in a comprehension iterable at any depth) and
|
|
3917
|
+
// a compound source (bare ternary) must be parenthesized so its `if` does
|
|
3918
|
+
// not parse as the comprehension filter.
|
|
3919
|
+
const source = parenthesizeIterable(withWalrusBan(ctx, () => emitPyExprCtx(call.args[0], ctx)));
|
|
3029
3920
|
const callback = call.args[1];
|
|
3030
3921
|
if (callback.kind !== 'lambda') {
|
|
3031
3922
|
const fn = emitPyExprCtx(callback, ctx);
|
|
@@ -3056,35 +3947,132 @@ function lowerListLambdaPython(moduleName, methodName, call, ctx) {
|
|
|
3056
3947
|
ctx.shadowedSymbols = previous;
|
|
3057
3948
|
}
|
|
3058
3949
|
}
|
|
3950
|
+
/** Slice 6 — Python bitwise/shift operator precedence (higher binds tighter).
|
|
3951
|
+
* Matches CPython's grammar: `|` < `^` < `&` < shift < unary `~`. Used to
|
|
3952
|
+
* parenthesize the LOWERED tree so the emitted Python reproduces the JS-shaped
|
|
3953
|
+
* grouping (e.g. `a << (b & 31)` must keep the mask parens, since Python's
|
|
3954
|
+
* `<<` binds tighter than `&`). */
|
|
3955
|
+
const PY_BITWISE_PREC = { '|': 1, '^': 2, '&': 3, '<<': 4, '>>': 4 };
|
|
3956
|
+
/**
|
|
3957
|
+
* Slice 6 — emit an ALREADY-LOWERED bitwise/shift tree to a Python string.
|
|
3958
|
+
*
|
|
3959
|
+
* The tree produced by `lowerBitwiseAndModuloAST` contains FINAL Python
|
|
3960
|
+
* operators (`| & ^ << >>` and unary `~`) whose operands are already wrapped in
|
|
3961
|
+
* `_kern_to_int32` / `_kern_to_uint32` calls. Re-routing those operators back
|
|
3962
|
+
* through `emitPyExprCtx`'s bitwise branch would re-lower them and recurse
|
|
3963
|
+
* forever, so this dedicated emitter renders the bitwise/shift `binary` and the
|
|
3964
|
+
* `~` `unary` verbatim (with precedence-correct parens) and delegates every
|
|
3965
|
+
* other node kind (the leaves: calls, idents, literals, …) to `emitPyExprCtx`.
|
|
3966
|
+
*/
|
|
3967
|
+
function emitLoweredBitwisePy(node, ctx) {
|
|
3968
|
+
// The lowering's own wrapper calls (`_kern_to_int32` / `_kern_to_uint32` /
|
|
3969
|
+
// `_tmod`) must stay in THIS emit path: their argument is an already-lowered
|
|
3970
|
+
// bitwise/shift subtree, and handing the whole call to `emitPyExprCtx` would
|
|
3971
|
+
// re-route that inner subtree back into the bitwise branch and recurse
|
|
3972
|
+
// forever. Emit the wrapper directly and recurse through its argument here.
|
|
3973
|
+
if (node.kind === 'call' &&
|
|
3974
|
+
node.callee.kind === 'ident' &&
|
|
3975
|
+
(node.callee.name === '_kern_to_int32' || node.callee.name === '_kern_to_uint32' || node.callee.name === '_tmod')) {
|
|
3976
|
+
const args = node.args.map((a) => emitLoweredBitwisePy(a, ctx)).join(', ');
|
|
3977
|
+
return `${node.callee.name}(${args})`;
|
|
3978
|
+
}
|
|
3979
|
+
if (node.kind === 'binary' && node.op in PY_BITWISE_PREC) {
|
|
3980
|
+
const parentPrec = PY_BITWISE_PREC[node.op];
|
|
3981
|
+
const emitChild = (child, isRight) => {
|
|
3982
|
+
const s = emitLoweredBitwisePy(child, ctx);
|
|
3983
|
+
if (child.kind === 'binary' && child.op in PY_BITWISE_PREC) {
|
|
3984
|
+
const childPrec = PY_BITWISE_PREC[child.op];
|
|
3985
|
+
// Lower-precedence child always needs parens; an equal-precedence child
|
|
3986
|
+
// on the RIGHT needs parens to preserve left-associative grouping.
|
|
3987
|
+
if (childPrec < parentPrec || (childPrec === parentPrec && isRight))
|
|
3988
|
+
return `(${s})`;
|
|
3989
|
+
}
|
|
3990
|
+
return s;
|
|
3991
|
+
};
|
|
3992
|
+
return `${emitChild(node.left, false)} ${node.op} ${emitChild(node.right, true)}`;
|
|
3993
|
+
}
|
|
3994
|
+
if (node.kind === 'unary' && node.op === '~') {
|
|
3995
|
+
const inner = emitLoweredBitwisePy(node.argument, ctx);
|
|
3996
|
+
// `~` binds tighter than every binary bitwise/shift op, so a binary argument
|
|
3997
|
+
// must be parenthesized (e.g. `~(a | b)`).
|
|
3998
|
+
const wrapped = node.argument.kind === 'binary' && node.argument.op in PY_BITWISE_PREC ? `(${inner})` : inner;
|
|
3999
|
+
return `~${wrapped}`;
|
|
4000
|
+
}
|
|
4001
|
+
// Leaf / non-bitwise node — hand back to the main expression emitter.
|
|
4002
|
+
return emitPyExprCtx(node, ctx);
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Slice 6 — emit a binary bitwise/shift op `a <op> b` (op ∈ `| & ^ << >>`) on
|
|
4006
|
+
* the slice-0.75 ToInt32 substrate, per the S6 emission contract:
|
|
4007
|
+
*
|
|
4008
|
+
* a | b -> _kern_to_int32(_kern_to_int32(a) | _kern_to_int32(b))
|
|
4009
|
+
* a << b -> _kern_to_int32(_kern_to_int32(a) << (_kern_to_uint32(b) & 31))
|
|
4010
|
+
*
|
|
4011
|
+
* Each operand subexpression appears EXACTLY ONCE, so Python evaluates each side
|
|
4012
|
+
* once, left-to-right — matching JS evaluation-once order with no temporaries.
|
|
4013
|
+
*/
|
|
4014
|
+
function emitBitwiseShiftPy(op, left, right, ctx) {
|
|
4015
|
+
ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
|
|
4016
|
+
const l = `_kern_to_int32(${emitPyExprCtx(left, ctx)})`;
|
|
4017
|
+
if (op === '<<' || op === '>>') {
|
|
4018
|
+
// Shift count: ToUint32 then `& 31`.
|
|
4019
|
+
const count = `(_kern_to_uint32(${emitPyExprCtx(right, ctx)}) & 31)`;
|
|
4020
|
+
return `_kern_to_int32(${l} ${op} ${count})`;
|
|
4021
|
+
}
|
|
4022
|
+
const r = `_kern_to_int32(${emitPyExprCtx(right, ctx)})`;
|
|
4023
|
+
return `_kern_to_int32(${l} ${op} ${r})`;
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* Slice 6 — emit `a >>> b` (unsigned/zero-fill right shift):
|
|
4027
|
+
*
|
|
4028
|
+
* a >>> b -> _kern_to_uint32(_kern_to_uint32(a) >> (_kern_to_uint32(b) & 31))
|
|
4029
|
+
*
|
|
4030
|
+
* Both operands are non-negative after `_kern_to_uint32`, so Python's raw `>>`
|
|
4031
|
+
* is zero-fill (it NEVER sign-extends here); the outer `_kern_to_uint32` keeps
|
|
4032
|
+
* the result in the 0..2**32-1 Uint32 codomain. Raw signed `>>` on the original
|
|
4033
|
+
* value would sign-extend and is therefore never used.
|
|
4034
|
+
*/
|
|
4035
|
+
function emitUnsignedShiftPy(left, right, ctx) {
|
|
4036
|
+
ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
|
|
4037
|
+
const l = `_kern_to_uint32(${emitPyExprCtx(left, ctx)})`;
|
|
4038
|
+
const count = `(_kern_to_uint32(${emitPyExprCtx(right, ctx)}) & 31)`;
|
|
4039
|
+
return `_kern_to_uint32(${l} >> ${count})`;
|
|
4040
|
+
}
|
|
3059
4041
|
export function lowerBitwiseAndModuloAST(node) {
|
|
3060
4042
|
switch (node.kind) {
|
|
3061
4043
|
case 'binary': {
|
|
3062
4044
|
const left = lowerBitwiseAndModuloAST(node.left);
|
|
3063
4045
|
const right = lowerBitwiseAndModuloAST(node.right);
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
else {
|
|
3076
|
-
rewrittenRight = wrapInI32(right);
|
|
3077
|
-
}
|
|
3078
|
-
const i32Left = wrapInI32(left);
|
|
3079
|
-
const bitwiseNode = {
|
|
4046
|
+
// Slice 6 — bitwise / shift on the landed slice-0.75 ToInt32 substrate.
|
|
4047
|
+
// Each operand expression appears EXACTLY ONCE in the lowered form (the
|
|
4048
|
+
// helper call wraps it inline), so Python evaluates each side once,
|
|
4049
|
+
// left-to-right — matching JS evaluation-once order without temporaries.
|
|
4050
|
+
if (node.op === '|' || node.op === '&' || node.op === '^') {
|
|
4051
|
+
// a <op> b -> _kern_to_int32(_kern_to_int32(a) <op> _kern_to_int32(b))
|
|
4052
|
+
return wrapInToInt32({ kind: 'binary', op: node.op, left: wrapInToInt32(left), right: wrapInToInt32(right) });
|
|
4053
|
+
}
|
|
4054
|
+
if (node.op === '<<' || node.op === '>>') {
|
|
4055
|
+
// a <op> b -> _kern_to_int32(_kern_to_int32(a) <op> (_kern_to_uint32(b) & 31))
|
|
4056
|
+
return wrapInToInt32({
|
|
3080
4057
|
kind: 'binary',
|
|
3081
4058
|
op: node.op,
|
|
3082
|
-
left:
|
|
3083
|
-
right:
|
|
3084
|
-
};
|
|
3085
|
-
|
|
4059
|
+
left: wrapInToInt32(left),
|
|
4060
|
+
right: maskShiftCount(right),
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
if (node.op === '>>>') {
|
|
4064
|
+
// a >>> b -> _kern_to_uint32(_kern_to_uint32(a) >> (_kern_to_uint32(b) & 31))
|
|
4065
|
+
// Both operands are non-negative after _kern_to_uint32, so Python's raw
|
|
4066
|
+
// `>>` is zero-fill (NEVER sign-extends); the outer _kern_to_uint32 keeps
|
|
4067
|
+
// the result in the 0..2**32-1 Uint32 codomain.
|
|
4068
|
+
return wrapInToUint32({
|
|
4069
|
+
kind: 'binary',
|
|
4070
|
+
op: '>>',
|
|
4071
|
+
left: wrapInToUint32(left),
|
|
4072
|
+
right: maskShiftCount(right),
|
|
4073
|
+
});
|
|
3086
4074
|
}
|
|
3087
|
-
|
|
4075
|
+
if (node.op === '%') {
|
|
3088
4076
|
return {
|
|
3089
4077
|
kind: 'call',
|
|
3090
4078
|
callee: { kind: 'ident', name: '_tmod' },
|
|
@@ -3097,13 +4085,8 @@ export function lowerBitwiseAndModuloAST(node) {
|
|
|
3097
4085
|
case 'unary': {
|
|
3098
4086
|
const argument = lowerBitwiseAndModuloAST(node.argument);
|
|
3099
4087
|
if (node.op === '~') {
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
kind: 'unary',
|
|
3103
|
-
op: '~',
|
|
3104
|
-
argument: i32Arg,
|
|
3105
|
-
};
|
|
3106
|
-
return wrapInI32(unaryNode);
|
|
4088
|
+
// ~a -> _kern_to_int32(~_kern_to_int32(a))
|
|
4089
|
+
return wrapInToInt32({ kind: 'unary', op: '~', argument: wrapInToInt32(argument) });
|
|
3107
4090
|
}
|
|
3108
4091
|
return { ...node, argument };
|
|
3109
4092
|
}
|
|
@@ -3156,20 +4139,32 @@ export function lowerBitwiseAndModuloAST(node) {
|
|
|
3156
4139
|
return node;
|
|
3157
4140
|
}
|
|
3158
4141
|
}
|
|
3159
|
-
|
|
4142
|
+
/** Slice 6 — wrap `node` in a `_kern_to_int32(...)` call (the landed slice-0.75
|
|
4143
|
+
* ToInt32 helper). Emits the operator-level coercion the S6 contract mandates. */
|
|
4144
|
+
function wrapInToInt32(node) {
|
|
4145
|
+
return { kind: 'call', callee: { kind: 'ident', name: '_kern_to_int32' }, args: [node], optional: false };
|
|
4146
|
+
}
|
|
4147
|
+
/** Slice 6 — wrap `node` in a `_kern_to_uint32(...)` call (slice-0.75 ToUint32). */
|
|
4148
|
+
function wrapInToUint32(node) {
|
|
4149
|
+
return { kind: 'call', callee: { kind: 'ident', name: '_kern_to_uint32' }, args: [node], optional: false };
|
|
4150
|
+
}
|
|
4151
|
+
/** Slice 6 — JS masks every shift count with `& 31` after ToUint32. Emits
|
|
4152
|
+
* `(_kern_to_uint32(count) & 31)`. The `count` subexpression appears once. */
|
|
4153
|
+
function maskShiftCount(count) {
|
|
3160
4154
|
return {
|
|
3161
|
-
kind: '
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
4155
|
+
kind: 'binary',
|
|
4156
|
+
op: '&',
|
|
4157
|
+
left: wrapInToUint32(count),
|
|
4158
|
+
right: { kind: 'numLit', value: 31, raw: '31' },
|
|
3165
4159
|
};
|
|
3166
4160
|
}
|
|
3167
4161
|
export function registerHelpers(node, ctx) {
|
|
3168
4162
|
switch (node.kind) {
|
|
3169
4163
|
case 'call':
|
|
3170
4164
|
if (node.callee.kind === 'ident') {
|
|
3171
|
-
if (node.callee.name === '
|
|
3172
|
-
|
|
4165
|
+
if (node.callee.name === '_kern_to_int32' || node.callee.name === '_kern_to_uint32') {
|
|
4166
|
+
// Slice 6 — both helpers live in the single slice-0.75 helper block.
|
|
4167
|
+
ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
|
|
3173
4168
|
}
|
|
3174
4169
|
else if (node.callee.name === '_tmod') {
|
|
3175
4170
|
ctx.helpers.add(KERN_TMOD_HELPER_PY);
|
|
@@ -3255,7 +4250,7 @@ function emitExpressionV1Py(node, ctx) {
|
|
|
3255
4250
|
if (exprSource === undefined || exprSource === '') {
|
|
3256
4251
|
throw new Error('body-statement `expression-v1` requires `expr=`.');
|
|
3257
4252
|
}
|
|
3258
|
-
const exprIR =
|
|
4253
|
+
const exprIR = parseExpr(exprSource);
|
|
3259
4254
|
declareLocalBinding(ctx, userName, 'const');
|
|
3260
4255
|
const name = maybeRenameOnShadow(ctx, userName);
|
|
3261
4256
|
setRegexBinding(ctx, userName, exprIR.kind === 'regexLit' ? exprIR : null);
|