@kernlang/python 4.0.1-canary.222.1.f06f1a51 → 4.0.1-canary.225.1.f5c8d5fe

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,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, collectFreeIdentifierNames, emitStringKeyArray, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isPostfixMutationOperator, isSupportedAssignOperator, KERN_STDLIB_MODULES, lookupStdlib, lowerJsClosureBodyToPython, needsArgParens, needsBinaryParens, parseExpression, parseKeys, suggestStdlibMethod, } from '@kernlang/core';
43
+ import { applyTemplate, assertNoDecimalOperator, classifyRegexLiteralIndexReadFailClose, classifyRegexLiteralMemberReadFailClose, decimalBareConstructionFailMessage, emitStringKeyArray, expandRegexIFold, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isHostNamespaceRoot, isPostfixMutationOperator, isSupportedAssignOperator, isZeroWidthCapableRegex, KERN_DECIMAL_OPS_HELPER_PY, 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, validateDecimalConstructionArg, validateDecimalDivModArgs, validateDecimalOperands, validateDecimalPowArgs, 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, KERN_I32_HELPER_PY, KERN_JS_ARRAY_HELPERS_PY, KERN_JS_HELPER_PY, KERN_PAIR_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './core/expr/index.js';
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 = parseExpression(condRaw);
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
- lines.push(`${indent}if ${emitPyExprCtx(condIR, ctx)}:`);
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 = parseExpression(nestedCondRaw);
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
- lines.push(`${indent}elif ${emitPyExprCtx(nestedCondIR, ctx)}:`);
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 = parseExpression(condRaw);
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 = parseExpression(listRaw);
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 = parseExpression(String(rawFrom));
761
- const toIR = parseExpression(String(rawTo));
762
- const stepIR = parseExpression(rawStep);
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 = parseExpression(String(rawValue));
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 = parseExpression(String(rawCleanup));
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 = parseExpression(onRaw);
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 = parseExpression(String(rawInitial));
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 = parseExpression(String(rawTo));
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 = parseExpression(String(rawValue));
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 = parseExpression(rawValue);
1111
- const minIR = parseExpression(rawMin);
1112
- const maxIR = parseExpression(rawMax);
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 = parseExpression(value);
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
- const lines = [`${name} = ${emitted.join(' or ')}`];
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 = parseExpression(value);
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 = parseExpression(source);
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 = parseExpression(rawIn);
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 = parseExpression(rawIn);
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 = parseExpression(String(rawTarget));
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 = parseExpression(String(rawValue));
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(parseExpression(String(rawSource)), ctx);
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 None)`,
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 = parseExpression(inner);
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 = parseExpression(String(rawValue));
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 = parseExpression(String(rawValue));
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 = parseExpression(String(rawValue));
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,10 +1860,12 @@ 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);
1864
+ }
1865
+ case 'await': {
1866
+ const arg = emitPyExprCtx(node.argument, ctx);
1867
+ return `await ${needsLowPrecedenceOperandParens(node.argument) ? `(${arg})` : arg}`;
1761
1868
  }
1762
- case 'await':
1763
- return `await ${emitPyExprCtx(node.argument, ctx)}`;
1764
1869
  case 'new': {
1765
1870
  // Host Error mapping (spec §1): `new Error(args)` → `Exception(args)` on
1766
1871
  // Python, since `raise Error(...)` / `isinstance(x, Error)` would
@@ -1784,6 +1889,17 @@ function emitPyExprCtx(node, ctx) {
1784
1889
  if (arg.kind === 'ident' && arg.name === 'Error') {
1785
1890
  return 'Exception()';
1786
1891
  }
1892
+ // Slice 2 — `new RegExp(p)` (with or without parens) fails-close BEFORE the
1893
+ // fall-through, with the shared regex message (the TS emitter + IR-validate
1894
+ // throw the same string). Without this the Python `new` case falls through
1895
+ // to a verbatim `RegExp(...)` NameError instead of a compile-time refusal.
1896
+ // Honors user shadowing via `isProvenUserBinding`.
1897
+ if (arg.kind === 'call' && arg.callee.kind === 'ident') {
1898
+ rejectHostRegExpValuePython(arg.callee.name, ctx);
1899
+ }
1900
+ else if (arg.kind === 'ident') {
1901
+ rejectHostRegExpValuePython(arg.name, ctx);
1902
+ }
1787
1903
  return emitPyExprCtx(arg, ctx);
1788
1904
  }
1789
1905
  case 'typeAssert':
@@ -1816,15 +1932,35 @@ function emitPyExprCtx(node, ctx) {
1816
1932
  return out;
1817
1933
  }
1818
1934
  case 'binary': {
1819
- if (node.op === '|' ||
1820
- node.op === '&' ||
1821
- node.op === '^' ||
1822
- node.op === '<<' ||
1823
- node.op === '>>' ||
1824
- node.op === '%') {
1935
+ // DECIMAL Slice 2 (item 3) fail closed on `+`/`-`/`*` over a syntactically-
1936
+ // proven Decimal operand (`Decimal.of(...)`/`Decimal.<m>(...)`), the SAME
1937
+ // decision the TS leg makes (shared `assertNoDecimalOperator` + message), so
1938
+ // the refusal is byte-identical across targets. Conservative: a no-op for plain
1939
+ // numeric arithmetic and for every non-`+`/`-`/`*` operator below.
1940
+ //
1941
+ // Slice-2 remediation (Finding 2): the assert is now performed AFTER the
1942
+ // operands are lowered (see below, after `emitPyExprCtx(node.left/right)`),
1943
+ // mirroring the TS leg's lower-then-assert order. A bad Decimal operand — an
1944
+ // unknown member (`Decimal.nope(...)`) or a non-canonical literal
1945
+ // (`Decimal.of("1.10")`) — then throws its OWN specific diagnostic during
1946
+ // lowering instead of being masked by the generic operator error. The blocked
1947
+ // operators are only `+`/`-`/`*`, so deferring the assert past the bitwise /
1948
+ // shift / modulo branches below is safe (they never trip the Decimal check).
1949
+ // Slice 6 — bitwise / shift on the slice-0.75 ToInt32 substrate. Emitted
1950
+ // DIRECTLY as helper-wrapped strings (operands recurse through
1951
+ // `emitPyExprCtx`), so nested bitwise ops compose without the double-
1952
+ // dispatch recursion an AST-rewrite-then-re-emit would cause.
1953
+ if (node.op === '|' || node.op === '&' || node.op === '^' || node.op === '<<' || node.op === '>>') {
1954
+ return emitBitwiseShiftPy(node.op, node.left, node.right, ctx);
1955
+ }
1956
+ if (node.op === '>>>') {
1957
+ return emitUnsignedShiftPy(node.left, node.right, ctx);
1958
+ }
1959
+ if (node.op === '%') {
1960
+ // Modulo keeps the existing `_tmod` lowering (orthogonal to S6).
1825
1961
  const transformed = lowerBitwiseAndModuloAST(node);
1826
1962
  registerHelpers(transformed, ctx);
1827
- return emitPyExprCtx(transformed, ctx);
1963
+ return emitLoweredBitwisePy(transformed, ctx);
1828
1964
  }
1829
1965
  // Slice 2c — arithmetic / comparison / logical lowering for Python.
1830
1966
  // Use precedence-aware paren-wrapping so `a + b * c` doesn't redundantly
@@ -1838,6 +1974,11 @@ function emitPyExprCtx(node, ctx) {
1838
1974
  // expected `(a == b) < c` evaluation order.
1839
1975
  const left = emitPyExprCtx(node.left, ctx);
1840
1976
  const right = emitPyExprCtx(node.right, ctx);
1977
+ // DECIMAL Slice 2 (item 3) — operator fail-close, now AFTER operand lowering
1978
+ // (Finding-2 remediation) so a bad Decimal operand surfaces its own diagnostic
1979
+ // first. No-op for every operator except `+`/`-`/`*`, so it never affects the
1980
+ // bitwise/shift/modulo paths handled above or the comparison/logical paths below.
1981
+ assertNoDecimalOperator(node);
1841
1982
  if (node.op === 'instanceof') {
1842
1983
  // JS `a instanceof B` → Python `isinstance(a, B)`. Emitting `instanceof`
1843
1984
  // verbatim would be a Python *syntax* error, so this lowering is
@@ -1894,6 +2035,62 @@ function emitPyExprCtx(node, ctx) {
1894
2035
  }
1895
2036
  return `__kern_add(${left}, ${right})`;
1896
2037
  }
2038
+ if (node.op === '&&' || node.op === '||') {
2039
+ // Slice S5 — logical `&&` / `||` RESULT-VALUE semantics on Python.
2040
+ //
2041
+ // KERN `&&`/`||` are operand-selectors, not boolean operators:
2042
+ // `a && b` returns `a` when ToBoolean(a) is false, else `b`.
2043
+ // `a || b` returns `a` when ToBoolean(a) is true, else `b`.
2044
+ // Python's `and`/`or` use Python truthiness (`bool(x)` / `len(x)`), which
2045
+ // diverges from KERN ToBoolean on `[]`, `{}`, `NaN`, `"0"`, `"false"`,
2046
+ // `" "` — e.g. `[] or x` returns `x` in Python but must return `[]` in
2047
+ // KERN. So `and`/`or` are wrong; we lower through the SAME `_kern_truthy`
2048
+ // (KERN ToBoolean) substrate S4 routes `!`/ternary/`if cond=` through.
2049
+ //
2050
+ // Canonical lowering (single-eval, lazy, original-value-returning):
2051
+ // L && R -> (__k_logN if not _kern_truthy(__k_logN := L) else R)
2052
+ // L || R -> (__k_logN if _kern_truthy(__k_logN := L) else R)
2053
+ //
2054
+ // The walrus binds L EXACTLY ONCE (works for calls/indexes/members/
2055
+ // nested logicals — no double-read), `_kern_truthy` decides the branch
2056
+ // on KERN truthiness, and the unselected operand is NEVER evaluated
2057
+ // (Python conditional-expr branches are lazy). NO ident/pure-left fast
2058
+ // path in this slice — conservative walrus for EVERY left operand is the
2059
+ // single-eval law (S4 carry-forward; optimization needs its own oracle).
2060
+ //
2061
+ // Emitted UNCONDITIONALLY for both native (`coerceJsValues`) and Ground/
2062
+ // React layers — mirroring S4's `!`/ternary, which also emit
2063
+ // `_kern_truthy` regardless of the coercion flag. The `[]`/`{}`/`NaN`
2064
+ // divergence is independent of the undefined sentinel, so a Ground-layer
2065
+ // `a and b` would be just as wrong; parity demands one spelling.
2066
+ // `KERN_JS_HELPER_PY` is in Ground's prelude registry, so the helper is
2067
+ // inlined there too.
2068
+ ctx.helpers.add(KERN_JS_HELPER_PY);
2069
+ const tmp = `__k_log${++ctx.gensymCounter}`;
2070
+ // `:=` (PEP 572) binds looser than almost everything, so a low-precedence
2071
+ // left operand (conditional/lambda/walrus-bearing) must be parenthesized
2072
+ // so `__k_logN := L` captures the WHOLE operand. A nested `&&`/`||` is
2073
+ // already self-parenthesized, so this only fires on raw conditional/
2074
+ // lambda left operands.
2075
+ const leftOperand = needsWalrusOperandParens(node.left) ? `(${left})` : left;
2076
+ // The `else` branch is a conditional-expression alternate; a bare
2077
+ // conditional/lambda there reparses ambiguously, so wrap it. A nested
2078
+ // logical R is self-parenthesized; only raw conditional/lambda needs it.
2079
+ const rightOperand = needsConditionalAlternateParens(node.right) ? `(${right})` : right;
2080
+ if (ctx.banWalrus) {
2081
+ // Comprehension-iterable position (see BodyEmitContext.banWalrus):
2082
+ // the walrus form is a SyntaxError here, so bind L as a lambda
2083
+ // parameter instead. Same contract: L evaluated exactly once (as the
2084
+ // call argument), the unselected branch never evaluated (lazy
2085
+ // conditional inside the lambda body), original value returned.
2086
+ const test = `_kern_truthy(${tmp})`;
2087
+ const guarded = node.op === '&&' ? `not ${test}` : test;
2088
+ return `(lambda ${tmp}: (${tmp} if ${guarded} else ${rightOperand}))(${left})`;
2089
+ }
2090
+ const test = `_kern_truthy(${tmp} := ${leftOperand})`;
2091
+ const guarded = node.op === '&&' ? `not ${test}` : test;
2092
+ return `(${tmp} if ${guarded} else ${rightOperand})`;
2093
+ }
1897
2094
  if (node.op === '??') {
1898
2095
  // Slice 4c — nullish coalesce lowering. Two shapes:
1899
2096
  //
@@ -1924,6 +2121,11 @@ function emitPyExprCtx(node, ctx) {
1924
2121
  return `(${left} if ${left} is not None else ${right})`;
1925
2122
  }
1926
2123
  const tmp = `__k_nc${++ctx.gensymCounter}`;
2124
+ if (ctx.banWalrus) {
2125
+ // Comprehension-iterable position — walrus is a SyntaxError here;
2126
+ // lambda-parameter form keeps single-eval + lazy right branch.
2127
+ return `(lambda ${tmp}: (${tmp} if ${tmp} is not None else ${right}))(${left})`;
2128
+ }
1927
2129
  return `(${tmp} if (${tmp} := ${left}) is not None else ${right})`;
1928
2130
  }
1929
2131
  ctx.helpers.add(KERN_FMT_HELPER_PY);
@@ -1931,20 +2133,45 @@ function emitPyExprCtx(node, ctx) {
1931
2133
  return `(${left} if (${left} is not None and ${left} is not _KERN_UNDEFINED) else ${right})`;
1932
2134
  }
1933
2135
  const tmp = `__k_nc${++ctx.gensymCounter}`;
2136
+ if (ctx.banWalrus) {
2137
+ // Comprehension-iterable position — see BodyEmitContext.banWalrus.
2138
+ return `(lambda ${tmp}: (${tmp} if (${tmp} is not None and ${tmp} is not _KERN_UNDEFINED) else ${right}))(${left})`;
2139
+ }
1934
2140
  return `(${tmp} if ((${tmp} := ${left}) is not None and ${tmp} is not _KERN_UNDEFINED) else ${right})`;
1935
2141
  }
2142
+ // Slice S7 — split loose (`==`/`!=`) and strict (`===`/`!==`) equality so
2143
+ // the null/undefined boundary matches JS: `undefined == null` is True (both
2144
+ // nullish), `undefined === null` is False (distinct identities). Pre-S7
2145
+ // both lowered to Python `==`, which on the sentinel/None pair is False —
2146
+ // wrong for the loose op. Routed through the helper pair only in native
2147
+ // (coerce) bodies; the helper-less Ground/React layer keeps the raw
2148
+ // `==`/`!=` mapping (it never materializes the sentinel, so the nullish
2149
+ // crossing cannot arise there). Function-call form sidesteps Python's
2150
+ // comparison-chaining entirely, so no chain-paren handling is needed for
2151
+ // the equality ops themselves; chained comparison CHILDREN still recurse
2152
+ // through `emitPyExprCtx` and self-parenthesize as before.
2153
+ if (ctx.coerceJsValues && (node.op === '===' || node.op === '!==' || node.op === '==' || node.op === '!=')) {
2154
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
2155
+ const fn = node.op === '===' || node.op === '!==' ? '_kern_strict_equal' : '_kern_loose_equal';
2156
+ const call = `${fn}(${left}, ${right})`;
2157
+ return node.op === '!==' || node.op === '!=' ? `(not ${call})` : call;
2158
+ }
1936
2159
  const forceLeft = needsComparisonChainParens(node.left, node.op);
1937
2160
  const forceRight = needsComparisonChainParens(node.right, node.op);
1938
- const lp = forceLeft || needsBinaryParens(node.left, node.op, 'left') ? `(${left})` : left;
1939
- const rp = forceRight || needsBinaryParens(node.right, node.op, 'right') ? `(${right})` : right;
2161
+ const lp = forceLeft || needsLowPrecedenceOperandParens(node.left) || needsBinaryParens(node.left, node.op, 'left')
2162
+ ? `(${left})`
2163
+ : left;
2164
+ const rp = forceRight || needsLowPrecedenceOperandParens(node.right) || needsBinaryParens(node.right, node.op, 'right')
2165
+ ? `(${right})`
2166
+ : right;
1940
2167
  const op = mapBinaryOpToPython(node.op);
1941
2168
  return `${lp} ${op} ${rp}`;
1942
2169
  }
1943
2170
  case 'unary': {
1944
2171
  if (node.op === '~') {
1945
- const transformed = lowerBitwiseAndModuloAST(node);
1946
- registerHelpers(transformed, ctx);
1947
- return emitPyExprCtx(transformed, ctx);
2172
+ // ~a -> _kern_to_int32(~_kern_to_int32(a))
2173
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
2174
+ return `_kern_to_int32(~_kern_to_int32(${emitPyExprCtx(node.argument, ctx)}))`;
1948
2175
  }
1949
2176
  // Slice 2c — `!x` → `not x`, `-x` → `-x`.
1950
2177
  // Slice typeof — expose the now-eligible native KERN `typeof` shape on
@@ -1954,8 +2181,15 @@ function emitPyExprCtx(node, ctx) {
1954
2181
  return emitPyTypeof(node.argument, ctx);
1955
2182
  const arg = emitPyExprCtx(node.argument, ctx);
1956
2183
  const wrapped = needsArgParens(node.argument) ? `(${arg})` : arg;
1957
- if (node.op === '!')
1958
- return `not ${wrapped}`;
2184
+ // Slice S4 — `!x` consumes KERN ToBoolean and returns a real Python bool.
2185
+ // `not _kern_truthy(x)` gives `!""`/`!NaN` → True and `!"0"`/`![]` → False
2186
+ // (bare `not x` would get `![]`/`!{}`/`!NaN` wrong). Wrap unconditionally.
2187
+ if (node.op === '!') {
2188
+ ctx.helpers.add(KERN_JS_HELPER_PY);
2189
+ return `(not _kern_truthy(${arg}))`;
2190
+ }
2191
+ if (node.op === '-' && node.argument.kind === 'numLit' && Number(node.argument.raw) === 0)
2192
+ return '-0.0';
1959
2193
  if (node.op === '-')
1960
2194
  return `-${wrapped}`;
1961
2195
  if (node.op === '+')
@@ -1999,7 +2233,12 @@ function emitPyExprCtx(node, ctx) {
1999
2233
  return emitted;
2000
2234
  }
2001
2235
  };
2002
- return `${wrap(node.consequent, consStr)} if ${wrap(node.test, testStr)} else ${wrap(node.alternate, altStr)}`;
2236
+ // Slice S4 the ternary condition consumes KERN ToBoolean exactly once, so
2237
+ // `{} ? a : b`/`[] ? a : b` take the consequent and `NaN ? a : b` takes the
2238
+ // alternate. `_kern_truthy(...)` already parenthesizes the test, so no extra
2239
+ // `wrap` is needed on it. Wrap unconditionally (no "looks boolean" skip).
2240
+ ctx.helpers.add(KERN_JS_HELPER_PY);
2241
+ return `${wrap(node.consequent, consStr)} if _kern_truthy(${testStr}) else ${wrap(node.alternate, altStr)}`;
2003
2242
  }
2004
2243
  case 'spread':
2005
2244
  return `*${emitPyExprCtx(node.argument, ctx)}`;
@@ -2010,6 +2249,39 @@ function emitPyExprCtx(node, ctx) {
2010
2249
  throw new Error(`emitPyExpression: unsupported expression kind '${node.kind ?? 'unknown'}'.`);
2011
2250
  }
2012
2251
  function emitPyTypeof(argument, ctx) {
2252
+ // Round-6 fix — `typeof <bare host-namespace root>` fails-close on BOTH targets.
2253
+ // The round-5 carve-out lowered `typeof RegExp` to the constant `"function"`
2254
+ // on the theory that it was byte-identical to TS's native `typeof RegExp`. But
2255
+ // the carve-out's siblings on the TS/IR legs blanket-accepted EVERY bare
2256
+ // `typeof` operand, re-opening reserved host roots: `typeof Date`/`typeof
2257
+ // process` lowered HERE to a runtime `isinstance` ladder over the Python names
2258
+ // `Date`/`process`, which do NOT exist → NameError, while TS emits the native
2259
+ // `typeof Date`. That is a genuine TS↔Python divergence, so `typeof <host root>`
2260
+ // must fail-close. `RegExp` keeps the shared regex message (matching the
2261
+ // bare-value reject + the TS/IR legs); every other reserved host root takes the
2262
+ // generic host message with a synthetic `typeof` member. A non-host operand
2263
+ // (`typeof userLocal`, `typeof undeclaredFeatureFlag`) and a proven user binding
2264
+ // fall through to the runtime ladder below, unchanged. `typeof RegExp.prototype`
2265
+ // is a `member` operand and still fails-close via the generic guards.
2266
+ //
2267
+ // Round-7 fix — a WRAPPED operand (`typeof (Date as any)`, `typeof (Date!)`)
2268
+ // arrived as `typeAssert`/`nonNull`, NOT an `ident`, so the round-6 reject was
2269
+ // bypassed and Python lowered a runtime Date/process lookup (NameError) while
2270
+ // the (now-corrected) TS leg emits `typeof Date`. Peel the transparent wrappers
2271
+ // via the round-5 `unwrapTransparentReceiverIR` (fixpoint over
2272
+ // `typeAssert`/`nonNull`) and apply the host-root reject to the UNWRAPPED
2273
+ // operand, identically to the TS-emit + IR-validate legs. The runtime-ladder
2274
+ // fall-through below still operates on the ORIGINAL `argument`, so an accepted
2275
+ // wrapped operand (`typeof (userLocal as any)`) emits the wrapper's value
2276
+ // unchanged.
2277
+ const typeofOperand = unwrapTransparentReceiverIR(argument);
2278
+ if (typeofOperand.kind === 'ident' && !isProvenUserBinding(ctx, typeofOperand.name)) {
2279
+ if (typeofOperand.name === 'RegExp')
2280
+ throw new Error(REGEX_HOST_REGEXP_FAILCLOSE);
2281
+ if (isHostNamespaceRoot(typeofOperand.name)) {
2282
+ throw new Error(unmappedHostNamespaceMessage('Python', typeofOperand.name, 'typeof'));
2283
+ }
2284
+ }
2013
2285
  switch (argument.kind) {
2014
2286
  case 'strLit':
2015
2287
  return '"string"';
@@ -2042,6 +2314,17 @@ function emitPyTypeof(argument, ctx) {
2042
2314
  // pre-slice None-first form.
2043
2315
  if (ctx.coerceJsValues) {
2044
2316
  ctx.helpers.add(KERN_FMT_HELPER_PY);
2317
+ if (ctx.banWalrus) {
2318
+ // Comprehension-iterable position — walrus is a SyntaxError here; bind
2319
+ // the operand as a lambda parameter instead (single-eval preserved).
2320
+ return (`(lambda ${tmp}: ("undefined" if ${tmp} is _KERN_UNDEFINED ` +
2321
+ `else "object" if ${tmp} is None ` +
2322
+ `else "boolean" if isinstance(${tmp}, bool) ` +
2323
+ `else "number" if isinstance(${tmp}, (int, float)) ` +
2324
+ `else "string" if isinstance(${tmp}, str) ` +
2325
+ `else "function" if callable(${tmp}) ` +
2326
+ `else "object"))(${value})`);
2327
+ }
2045
2328
  return (`("undefined" if (${tmp} := ${wrapped}) is _KERN_UNDEFINED ` +
2046
2329
  `else "object" if ${tmp} is None ` +
2047
2330
  `else "boolean" if isinstance(${tmp}, bool) ` +
@@ -2050,6 +2333,15 @@ function emitPyTypeof(argument, ctx) {
2050
2333
  `else "function" if callable(${tmp}) ` +
2051
2334
  `else "object")`);
2052
2335
  }
2336
+ if (ctx.banWalrus) {
2337
+ // Comprehension-iterable position — see BodyEmitContext.banWalrus.
2338
+ return (`(lambda ${tmp}: ("object" if ${tmp} is None ` +
2339
+ `else "boolean" if isinstance(${tmp}, bool) ` +
2340
+ `else "number" if isinstance(${tmp}, (int, float)) ` +
2341
+ `else "string" if isinstance(${tmp}, str) ` +
2342
+ `else "function" if callable(${tmp}) ` +
2343
+ `else "object"))(${value})`);
2344
+ }
2053
2345
  return (`("object" if (${tmp} := ${wrapped}) is None ` +
2054
2346
  `else "boolean" if isinstance(${tmp}, bool) ` +
2055
2347
  `else "number" if isinstance(${tmp}, (int, float)) ` +
@@ -2082,12 +2374,12 @@ function emitLambdaPy(node, ctx) {
2082
2374
  *
2083
2375
  * The closure body is lowered through `lowerJsClosureBodyToPython`, reusing
2084
2376
  * the class-path expression/condition callbacks:
2085
- * - `lowerExpression(raw)` = `emitPyExprCtx(parseExpression(raw), ctx)` —
2377
+ * - `lowerExpression(raw)` = `emitPyExprCtx(parseExpr(raw), ctx)` —
2086
2378
  * identical to every other native-body expression emit, so a captured
2087
2379
  * RENAMED outer variable resolves through `ctx` (the rename stack /
2088
2380
  * symbolMap) exactly as it does outside the closure.
2089
2381
  * - `lowerCondition(raw)` mirrors the class/native if-emitter, which lowers a
2090
- * condition as the bare `emitPyExprCtx(parseExpression(cond), ctx)` (NO
2382
+ * condition as the bare `emitPyExprCtx(parseExpr(cond), ctx)` (NO
2091
2383
  * js_truthy wrapper). Matching it EXACTLY means a condition inside a
2092
2384
  * closure lowers identically to the same condition outside one.
2093
2385
  *
@@ -2101,13 +2393,27 @@ function emitBlockClosurePy(node, names, ctx) {
2101
2393
  const previous = new Set(ctx.shadowedSymbols);
2102
2394
  for (const name of names)
2103
2395
  ctx.shadowedSymbols.add(name);
2396
+ // Slice 2 review-fix parity (round 3) — a block-LOCAL `const`/`let`/function/
2397
+ // class shadows any outer binding of that name WITHIN ITS OWN BLOCK (and
2398
+ // nested blocks) only, so a host-`RegExp` value guard
2399
+ // (`rejectHostRegExpValuePython`) must fire on an OUTER reference but NOT on an
2400
+ // in-scope one. The old code registered EVERY declared name (incl. names
2401
+ // declared only in a NESTED block) into `shadowedSymbols` for the WHOLE
2402
+ // closure — fail-OPEN on `() => { if (ok) { const RegExp = 1; } return RegExp; }`
2403
+ // (the outer `return RegExp` was wrongly treated as the local), DIVERGING from
2404
+ // the TS leg. The lowerer FLATTENS nested blocks, so it now reports block
2405
+ // boundaries via `enterBlockScope`/`exitBlockScope`; we push/pop block-local
2406
+ // shadows exactly like the TS-AST closure walk's `scopes` stack. Only names
2407
+ // NOT already shadowed are pushed/popped, so an outer binding of the same name
2408
+ // survives the inner block's pop.
2409
+ const blockScopeAdded = [];
2104
2410
  try {
2105
2411
  const lowered = lowerJsClosureBodyToPython(node.bodyBlock.raw, {
2106
- lowerExpression: (raw) => emitPyExprCtx(parseExpression(raw), ctx),
2412
+ lowerExpression: (raw) => emitPyExprCtx(parseExpr(raw), ctx),
2107
2413
  // Mirror the native/class if-emitter EXACTLY (bare expression, no
2108
2414
  // js_truthy) so a condition inside the closure matches the same
2109
2415
  // condition outside it.
2110
- lowerCondition: (raw) => emitPyExprCtx(parseExpression(raw), ctx),
2416
+ lowerCondition: (raw) => emitPyExprCtx(parseExpr(raw), ctx),
2111
2417
  // The closure's own params are def-locals, never `nonlocal`: a write to a
2112
2418
  // param (`(x) => { x = x + 1 }`) must not be reported as a written FREE
2113
2419
  // name. The lowerer excludes both params and block-locals.
@@ -2118,6 +2424,25 @@ function emitBlockClosurePy(node, names, ctx) {
2118
2424
  // wrong-values without this). Params/block-locals are never renamed, so
2119
2425
  // the resolver is identity for them.
2120
2426
  lowerAssignTarget: (name) => resolveLocalRename(ctx, name),
2427
+ // Block-scope shadowing — push a block's top-level locals on entry, pop on
2428
+ // exit, so a `RegExp` value reference fails-close ONLY when no in-scope
2429
+ // block-local/param shadows it (byte-aligned with the TS-AST walk).
2430
+ enterBlockScope: (scopeNames) => {
2431
+ const added = new Set();
2432
+ for (const name of scopeNames) {
2433
+ if (!ctx.shadowedSymbols.has(name)) {
2434
+ ctx.shadowedSymbols.add(name);
2435
+ added.add(name);
2436
+ }
2437
+ }
2438
+ blockScopeAdded.push(added);
2439
+ },
2440
+ exitBlockScope: () => {
2441
+ const added = blockScopeAdded.pop();
2442
+ if (added)
2443
+ for (const name of added)
2444
+ ctx.shadowedSymbols.delete(name);
2445
+ },
2121
2446
  });
2122
2447
  if (!lowered.ok) {
2123
2448
  // The commit-A gate already accepted this block, so a lowering failure
@@ -2343,12 +2668,99 @@ function containsLambdaCapturingIdent(node, name) {
2343
2668
  return false;
2344
2669
  }
2345
2670
  }
2671
+ /** Slice S7 — the presence test an optional `?.`/`?.[]` link contributes to the
2672
+ * accumulated chain guard. Native (coerce) bodies test against the full nullish
2673
+ * set (`None` AND the undefined sentinel) so `undefined?.x` short-circuits; the
2674
+ * helper-less Ground/React layer keeps the pre-slice `is not None` test (it
2675
+ * never materializes the sentinel). `ref` may itself be a walrus assignment
2676
+ * expression (`(__k_oc1 := f())`), which Python evaluates exactly once. */
2677
+ function optionalPresenceTest(ref, ctx) {
2678
+ if (ctx.coerceJsValues) {
2679
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
2680
+ return `(not _kern_is_nullish(${ref}))`;
2681
+ }
2682
+ // `x := f() is not None` would bind the BOOLEAN to x; parenthesize a walrus
2683
+ // ref so the receiver (not the comparison) is what gets bound.
2684
+ const wrapped = ref.includes(':=') ? `(${ref})` : ref;
2685
+ return `${wrapped} is not None`;
2686
+ }
2687
+ /** Slice S7 — build the guard + branch-receiver reference for one optional link.
2688
+ * Pure receivers keep the readable double-name form (named in both the guard
2689
+ * and the branch — re-evaluation is side-effect-free). A NON-pure receiver is
2690
+ * bound ONCE via the S4 walrus idiom: the guard carries `(__k_ocN := RECV)` so
2691
+ * the side effect runs exactly once, and the branch references the bound name.
2692
+ * A non-pure receiver that is ITSELF an optional chain (`inner.guard !== null`)
2693
+ * cannot host the walrus and still throws — bind it to a `let` first. */
2694
+ function lowerOptionalLink(inner, objectNode, ctx) {
2695
+ const pure = isReceiverChainPure(objectNode);
2696
+ if (pure) {
2697
+ const presence = optionalPresenceTest(inner.expr, ctx);
2698
+ return {
2699
+ guard: inner.guard === null ? presence : `${inner.guard} and ${presence}`,
2700
+ branchRef: inner.expr,
2701
+ };
2702
+ }
2703
+ if (inner.guard !== null) {
2704
+ throw new Error("Optional chain '?.' on Python target requires a side-effect-free receiver (identifier or pure member chain). " +
2705
+ 'Bind the call/await result to a `let` first, then use `let.field?.next` on the bound name.');
2706
+ }
2707
+ const tmp = `__k_oc${++ctx.gensymCounter}`;
2708
+ if (ctx.banWalrus) {
2709
+ // Comprehension-iterable position (see BodyEmitContext.banWalrus): CPython
2710
+ // rejects the `:=` walrus anywhere inside a comprehension iterable, so the
2711
+ // non-pure receiver cannot be bound via `(__k_ocN := RECV)`. Bind it as a
2712
+ // lambda PARAMETER instead — the presence test + branch reference the bare
2713
+ // `__k_ocN` (the parameter), and `wrapGuardIfAny` wraps the whole guarded
2714
+ // conditional in `(lambda __k_ocN: <conditional>)(RECV)`. Same contract as
2715
+ // the walrus form: RECV evaluated exactly once (as the call argument), the
2716
+ // short-circuit branch yields the sentinel, single-eval preserved.
2717
+ const presence = optionalPresenceTest(tmp, ctx);
2718
+ return { guard: presence, branchRef: tmp, lambdaBind: { param: tmp, arg: inner.expr } };
2719
+ }
2720
+ const presence = optionalPresenceTest(`${tmp} := ${inner.expr}`, ctx);
2721
+ return { guard: presence, branchRef: tmp };
2722
+ }
2346
2723
  function lowerChain(node, ctx) {
2347
2724
  if (node.kind === 'member') {
2348
2725
  const obj = node.object;
2726
+ // Slice 2 — a bare property READ on a DIRECT regex LITERAL (`/x/.source`,
2727
+ // `/x/.flags`) launders the pattern/flags back into a string. Routed through
2728
+ // the SHARED classifier (via the ValueIR adapter) so this site agrees with
2729
+ // the TS emit + IR-validate legs and the closure walk BY CONSTRUCTION. The
2730
+ // portable METHODS (.test/.exec/…) are CALLS routed by the call path before
2731
+ // reaching here, and a LET-BOUND regex read (`r.source`) has an `ident`
2732
+ // object (owned by Slice 3), so only a direct literal read is closed here.
2733
+ // The receiver is UNWRAPPED first so a wrapped read `(/x/ as any).source` /
2734
+ // `(/x/!).source` fails-close identically to the bare `/x/.source` and to the
2735
+ // TS-emit + IR-validate legs (round-5 wrapped-receiver fix).
2736
+ if (regexLiteralReceiverIR(obj) !== null) {
2737
+ const message = classifyRegexLiteralMemberReadFailClose(node);
2738
+ if (message !== null)
2739
+ throw new Error(message);
2740
+ }
2741
+ const stdlibProperty = applyStdlibPropertyLoweringPython(node, ctx);
2742
+ if (stdlibProperty !== null)
2743
+ return { guard: null, expr: stdlibProperty };
2744
+ // Slice H — fail-closed on an UNMAPPED host-namespace member READ. Covers
2745
+ // host CONSTANT reads such as `Math.PI` / `process.env` that are not a call
2746
+ // (the call path guards `Root.member(args)` separately, before descending
2747
+ // here). Only a DIRECT `Root.member` read is inspected: `obj.kind ===
2748
+ // 'ident'`. A host-namespace-shaped, not-user-bound root throws; user
2749
+ // receivers (`user.profile`) and proven-local roots pass through. A host
2750
+ // CALL's callee never reaches this read guard with a host root — the call
2751
+ // path already threw — so this only ever fires on genuine reads.
2752
+ if (obj.kind === 'ident') {
2753
+ rejectUnmappedHostNamespacePython(obj.name, node.property, ctx);
2754
+ }
2755
+ // S5 review fix — a NON-CHAIN root that lowers to a compound expression
2756
+ // must be parenthesized before `.${property}` is appended: a bare ternary
2757
+ // root (`b if t else c`) would otherwise bind the member to its LAST
2758
+ // operand only (`b if t else c.prop` — silently wrong Python). Same
2759
+ // precedence set as index receivers; lowerings that already emit a
2760
+ // self-delimited atom (`(walrus ternary)`, `__kern_add(a, b)`) stay bare.
2349
2761
  const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
2350
2762
  ? lowerChain(obj, ctx)
2351
- : { guard: null, expr: emitPyExprCtx(obj, ctx) };
2763
+ : { guard: null, expr: wrapCompoundRootExpr(obj, emitPyExprCtx(obj, ctx)) };
2352
2764
  // Portable Array *property* read (non-call `.length`) lowers through the
2353
2765
  // SAME shared list-ops hook the route emitter uses, so `this.items.length`
2354
2766
  // emits `len(self.items)` (not invalid `self.items.length`) — identical to
@@ -2356,41 +2768,70 @@ function lowerChain(node, ctx) {
2356
2768
  // link is rewritten; the accumulated optional-chain guard is left UNTOUCHED
2357
2769
  // and still flows through `wrapGuardIfAny`, so `items?.length` stays
2358
2770
  // `(len(items) if items is not None else None)`-shaped.
2771
+ if (node.optional) {
2772
+ // Slice S7 — single-eval optional `?.`. A pure receiver may be named twice
2773
+ // (guard + branch) with no observable effect, so it keeps the readable
2774
+ // double-name form. A NON-pure receiver (e.g. `markReceiver(null)?.x`) is
2775
+ // bound ONCE via the S4 walrus idiom so its side effect runs exactly once;
2776
+ // the BOUND name is then used in both the guard's presence test and the
2777
+ // trailing link. Only a single optional link can host the walrus (the
2778
+ // receiver is not itself an optional chain ⇒ `inner.guard === null`); a
2779
+ // non-pure receiver UNDER an existing optional chain still throws below.
2780
+ const opt = lowerOptionalLink(inner, node.object, ctx);
2781
+ const linkExpr = isSharedPortableArrayProperty(node.property)
2782
+ ? (lowerPortableArrayPropertyPy(opt.branchRef, node.property) ?? `${opt.branchRef}.${node.property}`)
2783
+ : `${opt.branchRef}.${node.property}`;
2784
+ return { guard: opt.guard, expr: linkExpr, lambdaBind: opt.lambdaBind };
2785
+ }
2359
2786
  const linkExpr = isSharedPortableArrayProperty(node.property)
2360
2787
  ? (lowerPortableArrayPropertyPy(inner.expr, node.property) ?? `${inner.expr}.${node.property}`)
2361
2788
  : `${inner.expr}.${node.property}`;
2362
- if (node.optional) {
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 };
2789
+ return { guard: inner.guard, expr: linkExpr, lambdaBind: inner.lambdaBind };
2374
2790
  }
2375
2791
  if (node.kind === 'index') {
2376
2792
  const obj = node.object;
2793
+ // Slice 2 review fix — the bracket (`index`) form of a regex-literal
2794
+ // property access (`/x/["source"]`, `/x/["flags"]`, `/x/["test"](s)`)
2795
+ // launders the pattern/flags back to a string exactly like the dotted
2796
+ // `/x/.source` member form, so it fails-close identically and BEFORE the
2797
+ // host-ident guard below. A STRING-literal index goes through the same
2798
+ // (empty) portable-property allowlist; a COMPUTED / non-literal index is
2799
+ // unknowable and also fails-close. Mirrors the TS emit + IR-validate index
2800
+ // screens — byte-identical regex message across all three legs. The receiver
2801
+ // is UNWRAPPED first so a wrapped bracket read `(/x/!)["source"]` /
2802
+ // `(/x/ as any)["test"](s)` fails-close identically (round-5 wrapped fix).
2803
+ if (regexLiteralReceiverIR(obj) !== null) {
2804
+ // Routed through the SHARED classifier (a STRING index classifies like the
2805
+ // dotted read; a COMPUTED index is unknowable → fail-close), byte-identical
2806
+ // to the TS emit + IR-validate index screens and the closure walk.
2807
+ const message = classifyRegexLiteralIndexReadFailClose(node);
2808
+ if (message !== null)
2809
+ throw new Error(message);
2810
+ }
2811
+ // Slice H review fix — bracket access must not bypass the fail-closed
2812
+ // guard: `Math["sqrt"]` / `Math["sqrt"](x)` is the same unmapped
2813
+ // host-namespace access as `Math.sqrt`, only spelled as an index node.
2814
+ // Call chains descend through this branch too, so this one site covers
2815
+ // both the read and the call form. Non-literal keys (`Math[k]`) are just
2816
+ // as unmapped — the label degrades to `[computed]`.
2817
+ if (obj.kind === 'ident') {
2818
+ const label = node.index.kind === 'strLit' ? node.index.value : '[computed]';
2819
+ rejectKnownStdlibIndexPython(obj.name, label);
2820
+ rejectUnmappedHostNamespacePython(obj.name, label, ctx);
2821
+ }
2377
2822
  const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
2378
2823
  ? lowerChain(obj, ctx)
2379
2824
  : { guard: null, expr: emitPyExprCtx(obj, ctx) };
2380
2825
  const index = emitPyExprCtx(node.index, ctx);
2381
2826
  if (node.optional) {
2382
- // The Python lowering names the receiver in the guard and the branch.
2383
- // Keep that single-eval-safe by requiring a pure receiver. The index
2384
- // expression appears only in the selected branch, matching JS `?.[]`.
2385
- if (!isReceiverChainPure(node.object)) {
2386
- throw new Error("Optional element access '?.[]' on Python target requires a side-effect-free receiver. " +
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}]` };
2827
+ // Slice S7 single-eval optional `?.[]` (same walrus discipline as `?.`).
2828
+ // A non-pure receiver is bound once; the index expression appears only in
2829
+ // the selected branch, matching JS `?.[]`.
2830
+ const opt = lowerOptionalLink(inner, node.object, ctx);
2831
+ return { guard: opt.guard, expr: `${opt.branchRef}[${index}]`, lambdaBind: opt.lambdaBind };
2391
2832
  }
2392
2833
  const wrapped = needsIndexReceiverParens(node.object) ? `(${inner.expr})` : inner.expr;
2393
- return { guard: inner.guard, expr: `${wrapped}[${index}]` };
2834
+ return { guard: inner.guard, expr: `${wrapped}[${index}]`, lambdaBind: inner.lambdaBind };
2394
2835
  }
2395
2836
  // node.kind === 'call'
2396
2837
  if (node.optional) {
@@ -2441,12 +2882,46 @@ function lowerChain(node, ctx) {
2441
2882
  const errArgs = node.args.map((arg) => emitPyExprCtx(arg, ctx)).join(', ');
2442
2883
  return { guard: null, expr: `Exception(${errArgs})` };
2443
2884
  }
2885
+ // Slice 2 — a bare `RegExp(p, f)` call (callee is an ident, missed by the
2886
+ // member-callee guard below) fails-close with the shared regex message,
2887
+ // BEFORE generic call emission would leak a verbatim `RegExp(...)` NameError.
2888
+ // Honors user shadowing via `isProvenUserBinding`.
2889
+ if (node.callee.kind === 'ident') {
2890
+ rejectHostRegExpValuePython(node.callee.name, ctx);
2891
+ }
2892
+ // DECIMAL Slice 1 — bare `Decimal(...)` (ident callee) fail-closes
2893
+ // SYMMETRICALLY with the TS leg. Only `Decimal.of`/`Decimal.add` (member
2894
+ // callees, handled by `applyStdlibLoweringPython` above) are portable. A
2895
+ // proven user binding named `Decimal` is left alone.
2896
+ if (node.callee.kind === 'ident' && node.callee.name === 'Decimal' && !isProvenUserBinding(ctx, 'Decimal')) {
2897
+ throw new Error(decimalBareConstructionFailMessage());
2898
+ }
2899
+ // Slice H — fail-closed on an UNMAPPED host-namespace member CALL. This runs
2900
+ // AFTER every explicit lowering hook above (stdlib, regex, lambda/array,
2901
+ // portable-array, super/String/Error) and BEFORE generic call emission, so a
2902
+ // `Root.member(args)` call whose root is host-namespace-shaped and not proven
2903
+ // user-bound throws the portable-lowering diagnostic instead of leaking
2904
+ // invalid verbatim Python (`Math.sqrt(x)` → runtime NameError). Canonical
2905
+ // KERN stdlib roots (`Number`/`Json`/…) already returned via the stdlib hook,
2906
+ // so they never reach here; user receivers (`client.send(...)`) and a
2907
+ // proven-local `Math` are not host-namespace-shaped / are user-bound and pass
2908
+ // through. Capitalization-agnostic: equally catches `console.log(...)`,
2909
+ // `Promise.all(...)`, and lowercase host roots such as `process.env`.
2910
+ if (node.callee.kind === 'member' && node.callee.object.kind === 'ident') {
2911
+ rejectUnmappedHostNamespacePython(node.callee.object.name, node.callee.property, ctx);
2912
+ }
2444
2913
  const callee = node.callee;
2445
2914
  const inner = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index'
2446
2915
  ? lowerChain(callee, ctx)
2447
- : { guard: null, expr: emitPyExprCtx(callee, ctx) };
2916
+ : {
2917
+ guard: null,
2918
+ expr: (() => {
2919
+ const emitted = emitPyExprCtx(callee, ctx);
2920
+ return needsLowPrecedenceOperandParens(callee) ? `(${emitted})` : emitted;
2921
+ })(),
2922
+ };
2448
2923
  const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', ');
2449
- return { guard: inner.guard, expr: `${inner.expr}(${args})` };
2924
+ return { guard: inner.guard, expr: `${inner.expr}(${args})`, lambdaBind: inner.lambdaBind };
2450
2925
  }
2451
2926
  /**
2452
2927
  * Lower a portable Array *method call* (e.g. `arr.push(x)`) through the shared
@@ -2482,18 +2957,32 @@ function lowerPortableArrayCallPython(call, ctx) {
2482
2957
  // finding). The optional-chain guard below still applies to ALL methods.
2483
2958
  if (sharedPortableMethodRequiresPureReceiver(callee.property) && !isReceiverChainPure(recvNode))
2484
2959
  return null;
2485
- const recv = recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
2960
+ // S5 review fix these list-ops lowerings embed the receiver in a
2961
+ // comprehension/generator ITERABLE (`join`: `str(__v) for __v in <recv>`;
2962
+ // `flat`: `for __x in <recv>`; `indexOf`: `enumerate(<recv>)` inside a
2963
+ // genexp), where CPython rejects `:=` outright. Emit the receiver under the
2964
+ // walrus ban AND parenthesize compound receivers (a bare ternary in a
2965
+ // generator head reads its `if` as the comprehension filter — SyntaxError).
2966
+ const embedsReceiverInGenexp = callee.property === 'join' || callee.property === 'flat' || callee.property === 'indexOf';
2967
+ const emitRecv = () => recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
2486
2968
  ? lowerChain(recvNode, ctx)
2487
2969
  : { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
2970
+ const recv = embedsReceiverInGenexp ? withWalrusBan(ctx, emitRecv) : emitRecv();
2488
2971
  // A pure receiver can still be an optional chain (`a?.b`), which carries a
2489
2972
  // None-guard the flat shim can't honor — fall through for those too.
2490
2973
  if (recv.guard !== null)
2491
2974
  return null;
2975
+ const recvExpr = embedsReceiverInGenexp ? parenthesizeIterable(recv.expr) : recv.expr;
2492
2976
  const args = call.args.map((a) => (callee.property === 'fill' ? emitPyArrayFillArg(a, ctx) : emitPyExprCtx(a, ctx)));
2493
- const lowered = lowerPortableArrayMethodPy(recv.expr, callee.property, args);
2977
+ const lowered = lowerPortableArrayMethodPy(recvExpr, callee.property, args, { sentinelMiss: ctx.coerceJsValues });
2494
2978
  if (lowered !== null && callee.property === 'fill') {
2495
2979
  ctx.helpers.add(KERN_JS_ARRAY_HELPERS_PY);
2496
2980
  }
2981
+ // Slice S7 — `Array.at` out-of-range yields the undefined sentinel in value
2982
+ // mode; register the helper that defines `_KERN_UNDEFINED`.
2983
+ if (lowered !== null && callee.property === 'at' && ctx.coerceJsValues) {
2984
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
2985
+ }
2497
2986
  return lowered;
2498
2987
  }
2499
2988
  function emitPyArrayFillArg(node, ctx) {
@@ -2636,9 +3125,13 @@ function lowerLambdaArrayCallPython(call, ctx) {
2636
3125
  // single-eval property is what makes M6 (`this.bump().map(...)` runs bump()
2637
3126
  // exactly once) correct.
2638
3127
  const recvNode = callee.object;
2639
- const recv = recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
3128
+ // S5 review fix the receiver lands in the comprehension's generator head
3129
+ // (`for el in <recv>`), where CPython rejects `:=` at any nesting depth, so
3130
+ // it is emitted under the walrus ban (logical/nullish/typeof producers at
3131
+ // any depth switch to their lambda-parameter forms).
3132
+ const recv = withWalrusBan(ctx, () => recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
2640
3133
  ? lowerChain(recvNode, ctx)
2641
- : { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
3134
+ : { guard: null, expr: emitPyExprCtx(recvNode, ctx) });
2642
3135
  // An optional-chain receiver (`a?.b`) carries a None-guard the comprehension
2643
3136
  // can't honor — fall through unchanged for those.
2644
3137
  if (recv.guard !== null)
@@ -2839,43 +3332,196 @@ function lowerRegexCallPython(call, ctx) {
2839
3332
  const callee = call.callee;
2840
3333
  if (callee.kind !== 'member')
2841
3334
  return null;
3335
+ // Slice-3b FIX 4 (parity): a call whose receiver is a KERN-stdlib NAMESPACE
3336
+ // (`Math.match(/a/g)`, `JSON.split(/,/)`) is NOT a string regex method —
3337
+ // defer to `applyStdlibLoweringPython`, which rejects the unknown stdlib
3338
+ // member exactly like the TS emitter (where `applyStdlibLoweringTS` runs
3339
+ // BEFORE the regex lowering). Without this, the regex path mis-claimed the
3340
+ // namespace as the SUBJECT and emitted broken `…finditer("a", Math, …)`
3341
+ // while TS fail-closed — a cross-target divergence.
3342
+ if (callee.object.kind === 'ident' && KERN_STDLIB_MODULES.has(callee.object.name)) {
3343
+ return null;
3344
+ }
3345
+ // Slice-3c DETECT-and-fail-close: a regex method whose regex position holds an
3346
+ // ident KNOWN to be regex-bound (`let re = /…/; s.match(re)`) is NOT portable —
3347
+ // throw the SAME shared `REGEX_NONLITERAL_FAILCLOSE` the TS target throws (see
3348
+ // `assertNoBoundRegexMethodTS` in body-ts.ts). A string-/unknown-bound ident is
3349
+ // NOT flagged: it falls through to the plain host method below (`s.match(needle)`).
3350
+ const regexArgIdent = regexMethodRegexArgIdent(call);
3351
+ if (regexArgIdent !== null && lookupRegexBinding(ctx, regexArgIdent) !== null) {
3352
+ throw new Error(REGEX_NONLITERAL_FAILCLOSE);
3353
+ }
3354
+ // --- Receiver-is-regex shapes: `regex.test(s)`, `regex.exec(s)` ---
2842
3355
  const receiverRegex = resolveRegexExpr(callee.object, ctx);
2843
3356
  if (callee.property === 'test' && receiverRegex !== null) {
2844
3357
  if (call.args.length !== 1)
2845
3358
  return null;
2846
3359
  if (receiverRegex.flags.includes('g')) {
2847
- throw new Error("Python target does not lower RegExp.test with the 'g' flag because JS mutates lastIndex while Python re.search is stateless. Use Regex.contains once the KERN stdlib grows that cross-target shape.");
3360
+ // .test(/g) is STATEFUL (lastIndex advances+wraps); no portable re analog.
3361
+ throw new Error(REGEX_TEST_G_FAILCLOSE);
2848
3362
  }
2849
3363
  ctx.imports.add('re');
2850
3364
  return `(__k_re.search(${pyRegexPattern(receiverRegex)}, ${emitPyExprCtx(call.args[0], ctx)}, ${pyRegexFlags(receiverRegex.flags)}) is not None)`;
2851
3365
  }
2852
- const matchRegex = call.args.length === 1 ? resolveRegexExpr(call.args[0], ctx) : null;
2853
- if (callee.property === 'match' && matchRegex !== null) {
2854
- if (matchRegex.flags.includes('g')) {
2855
- throw new Error('Python target does not lower String.match(/.../g) because JS returns an array of matches while Python re.search returns a Match object. Use Regex.findAll once the KERN stdlib grows that cross-target shape.');
3366
+ if (callee.property === 'exec' && receiverRegex !== null) {
3367
+ // .exec drives a JS-only stateful `while ((m = re.exec(s)))` loop; fail-close
3368
+ // and redirect to the portable `.matchAll` iteration (D4) rather than silently
3369
+ // rewrite a loop whose body may mutate lastIndex.
3370
+ throw new Error(REGEX_EXEC_FAILCLOSE);
3371
+ }
3372
+ // --- Receiver-is-string shapes: arg[0] is the regex ---
3373
+ const firstArgRegex = call.args.length >= 1 ? resolveRegexExpr(call.args[0], ctx) : null;
3374
+ // `.match(s)` — no /g: canonical {full,groups,index,named}|None shape (the
3375
+ // load-bearing portability fix, D2). With /g: full matches only via
3376
+ // finditer.group(0) (NEVER re.findall, which returns tuples when >1 group),
3377
+ // or None when empty.
3378
+ if (callee.property === 'match' && firstArgRegex !== null && call.args.length === 1) {
3379
+ ctx.imports.add('re');
3380
+ const pat = pyRegexPattern(firstArgRegex);
3381
+ const subject = emitPyExprCtx(callee.object, ctx);
3382
+ if (firstArgRegex.flags.includes('g')) {
3383
+ const flags = pyRegexFlags(firstArgRegex.flags, { allowGlobal: true });
3384
+ return `([__k_m.group(0) for __k_m in __k_re.finditer(${pat}, ${subject}, ${flags})] or None)`;
3385
+ }
3386
+ ctx.helpers.add(KERN_REGEX_MATCH_HELPER_PY);
3387
+ return `_kern_regex_match(${pat}, ${subject}, ${pyRegexFlags(firstArgRegex.flags)})`;
3388
+ }
3389
+ // `.matchAll(s)` — requires /g (a non-global matchAll throws TypeError in JS).
3390
+ // Shapes finditer into [{full,groups,index}, …], incl. zero-width advances.
3391
+ if (callee.property === 'matchAll' && firstArgRegex !== null && call.args.length === 1) {
3392
+ if (!firstArgRegex.flags.includes('g')) {
3393
+ throw new Error(REGEX_MATCHALL_NO_G_FAILCLOSE);
3394
+ }
3395
+ ctx.imports.add('re');
3396
+ ctx.helpers.add(KERN_REGEX_MATCHALL_HELPER_PY);
3397
+ return `_kern_regex_matchall(${pyRegexPattern(firstArgRegex)}, ${emitPyExprCtx(callee.object, ctx)}, ${pyRegexFlags(firstArgRegex.flags, { allowGlobal: true })})`;
3398
+ }
3399
+ // `.split(s)` — IN-CORE for a non-zero-width pattern with NO limit arg (capture
3400
+ // groups interleave portably). FAIL-CLOSE on a zero-width-capable pattern
3401
+ // (empty-edge divergence) or any limit/2nd arg (truncate vs remainder).
3402
+ if (callee.property === 'split' && firstArgRegex !== null) {
3403
+ if (call.args.length > 1) {
3404
+ throw new Error(REGEX_SPLIT_LIMIT_FAILCLOSE);
3405
+ }
3406
+ if (isZeroWidthCapableRegex(firstArgRegex.pattern)) {
3407
+ throw new Error(REGEX_SPLIT_ZEROWIDTH_FAILCLOSE);
2856
3408
  }
2857
3409
  ctx.imports.add('re');
2858
- return `__k_re.search(${pyRegexPattern(matchRegex)}, ${emitPyExprCtx(callee.object, ctx)}, ${pyRegexFlags(matchRegex.flags)})`;
3410
+ return `__k_re.split(${pyRegexPattern(firstArgRegex)}, ${emitPyExprCtx(callee.object, ctx)}, flags=${pyRegexFlags(firstArgRegex.flags, { allowGlobal: true })})`;
2859
3411
  }
3412
+ // `.replace(s, r)` — no /g: FIRST match only (count=1); /g: ALL (count=0).
2860
3413
  const replaceRegex = call.args.length === 2 ? resolveRegexExpr(call.args[0], ctx) : null;
2861
3414
  if (callee.property === 'replace' && replaceRegex !== null) {
2862
3415
  ctx.imports.add('re');
2863
3416
  const count = replaceRegex.flags.includes('g') ? '0' : '1';
2864
- return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${emitPyExprCtx(call.args[1], ctx)}, ${emitPyExprCtx(callee.object, ctx)}, count=${count}, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
3417
+ const repl = emitPyReplArg(call.args[1], replaceRegex, ctx);
3418
+ return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${repl}, ${emitPyExprCtx(callee.object, ctx)}, count=${count}, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
3419
+ }
3420
+ // `.replaceAll(s, r)` — requires /g (a non-global replaceAll throws TypeError in
3421
+ // JS); otherwise identical to `.replace` /g (count=0, ALL matches replaced).
3422
+ if (callee.property === 'replaceAll' && replaceRegex !== null) {
3423
+ if (!replaceRegex.flags.includes('g')) {
3424
+ throw new Error(REGEX_REPLACEALL_NO_G_FAILCLOSE);
3425
+ }
3426
+ ctx.imports.add('re');
3427
+ const repl = emitPyReplArg(call.args[1], replaceRegex, ctx);
3428
+ return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${repl}, ${emitPyExprCtx(callee.object, ctx)}, count=0, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
2865
3429
  }
2866
3430
  return null;
2867
3431
  }
2868
- function resolveRegexExpr(node, ctx) {
2869
- if (node.kind === 'regexLit')
2870
- return node;
2871
- if (node.kind === 'ident')
2872
- return lookupRegexBinding(ctx, node.name);
2873
- return null;
3432
+ /**
3433
+ * Milestone C, Slice 4 — emit the Python `re.sub` REPLACEMENT argument for a
3434
+ * `.replace`/`.replaceAll` whose regex position is a literal.
3435
+ *
3436
+ * A STRING-LITERAL replacement is translated from the JS `$`-surface to the
3437
+ * Python repl VALUE via the shared `translateReplStringToPython`, then serialized
3438
+ * back to `.py` SOURCE through a synthetic `strLit` node so the ordinary
3439
+ * string-literal escaper double-escapes the backslashes correctly (gap 6: the
3440
+ * translator yields the runtime VALUE `\g<1>` / `\\` — the serializer turns those
3441
+ * into the source `"\\g<1>"` / `"\\\\"` that evaluates back to that value). A
3442
+ * NON-LITERAL replacement (a variable / computed string) cannot be translated
3443
+ * statically and FAILS-CLOSE symmetrically with the TS target.
3444
+ */
3445
+ function emitPyReplArg(arg, replaceRegex, ctx) {
3446
+ if (arg.kind !== 'strLit') {
3447
+ throw new Error(REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE);
3448
+ }
3449
+ // Capture metadata is read from the KERN/JS `(?<name>)` surface (BEFORE R6's
3450
+ // `(?P<name>)` rewrite), matching the way `$<name>` refs are written.
3451
+ const meta = regexCaptureMeta(replaceRegex.pattern);
3452
+ const pyReplValue = translateReplStringToPython(arg.value, meta);
3453
+ // Serialize the translated VALUE to `.py` source via the normal strLit path
3454
+ // (gap 6 — keep translation and serialization layers separate).
3455
+ const synthetic = { kind: 'strLit', value: pyReplValue, quote: '"' };
3456
+ return emitPyExprCtx(synthetic, ctx);
3457
+ }
3458
+ function resolveRegexExpr(node, _ctx) {
3459
+ // Slice-3c: ONLY a DIRECT regex literal lowers canonically. A let-bound regex
3460
+ // ident no longer RESOLVES to its literal here (the old `lookupRegexBinding`
3461
+ // resolution emitted a STALE pattern when the binding was reassigned and was
3462
+ // fragile to track). The fail-close for a known-regex-bound ident is made by
3463
+ // `lowerRegexCallPython` via the shared `regexMethodRegexArgIdent` detector —
3464
+ // symmetric with the TS target. A string-/unknown-bound ident returns null
3465
+ // and stays a plain host method (the `s.match(stringVar)` case).
3466
+ //
3467
+ // Transparent wrappers (`as`/`!`) are peeled via `regexLiteralReceiverIR` —
3468
+ // mirroring the TS `resolveRegexLitTS` — so a wrapped portable receiver call
3469
+ // `(/x/).test(s)` / `(/x/ as any).test(s)` lowers, and a wrapped non-portable
3470
+ // one `(/x/ as any).exec(s)` fails-close through `lowerRegexCallPython` (round-5
3471
+ // wrapped-receiver fix), identically to the TS leg.
3472
+ return regexLiteralReceiverIR(node);
2874
3473
  }
2875
3474
  function pyRegexPattern(node) {
3475
+ // Slice 5: fail-close any non-BMP (astral) construct in the PATTERN before any
3476
+ // normalization, on the RAW pattern — the IDENTICAL decision + message the TS
3477
+ // emitter makes (the `\/`→`/` un-escape below does not touch any astral
3478
+ // construct, so scanning the raw `node.pattern` here is byte-symmetric with TS).
3479
+ const astral = scanRegexAstral(node.pattern);
3480
+ if (astral !== null)
3481
+ throw new Error(regexAstralFailMessage(astral.char));
3482
+ // FIX 2: a non-portable named group (`(?<café>…)`, `(?<$x>…)`, `(?<>…)`) is
3483
+ // refused on BOTH targets (the same validator runs in the TS regex-literal emit
3484
+ // chokepoints), instead of emitting `(?P<café>…)` / a JS-form `(?<café>…)` that
3485
+ // diverges or crashes Python `re`. Run BEFORE any rewrite so the refusal is over
3486
+ // the original surface (the same surface `regexCaptureMeta` and the TS side see).
3487
+ validateRegexNamedGroupsPortable(node.pattern);
2876
3488
  // JS escapes `/` because it delimits the literal; Python string regexes do not
2877
3489
  // treat `/` specially, so preserve the semantic pattern without that escape.
2878
- return JSON.stringify(node.pattern.replace(/\\\//g, '/'));
3490
+ const unescaped = node.pattern.replace(/\\\//g, '/');
3491
+ // Milestone C, Slice 1 — emission-normalization (shared with the TS emitter
3492
+ // for the class transform, so it is byte-identical across targets):
3493
+ // 1. `\d \w \s` → explicit ASCII classes (same `normalizeRegexClasses` both
3494
+ // targets), so Python's Unicode-aware shorthand matches JS's ASCII.
3495
+ // 2. Slice-/i: class-expand non-ASCII Set(A) letters under /i into explicit
3496
+ // fold classes (Set(B) → throw an identical-to-TS compile error). Runs on
3497
+ // the SAME shared `expandRegexIFold` the TS emitter calls, AFTER class
3498
+ // normalization (Slice-1 classes are pure-ASCII → untouched by the fold
3499
+ // scan) and BEFORE anchor lowering (anchors are ASCII → untouched). Order
3500
+ // is class → fold → anchors; each touches a disjoint character set, so it
3501
+ // is parity-safe and byte-identical to TS. `re.IGNORECASE | re.ASCII` is
3502
+ // kept (handled in pyRegexFlags) — `re.ASCII` is the load-bearing invariant
3503
+ // that makes KEEP-i safe (it suppresses any Python re-fold of the explicit
3504
+ // non-ASCII class members).
3505
+ // 3. Python-only anchor lowering: on the non-`/m` path `$`→`\Z`, `^`→`\A`
3506
+ // so Python anchors match JS's input-end/start semantics (`re.ASCII` and
3507
+ // `re.M` are handled in pyRegexFlags). On the `/m` path anchors are kept.
3508
+ const classed = normalizeRegexClasses(unescaped);
3509
+ const folded = expandRegexIFold(classed, node.flags);
3510
+ if ('failClose' in folded)
3511
+ throw new Error(regexIFoldFailMessage(folded.char, folded.reason));
3512
+ const normalized = lowerRegexAnchorsPython(folded.pattern, node.flags);
3513
+ // Milestone C, Slice 4 — R6: named-group PATTERN syntax (`(?<name>)` /
3514
+ // `\k<name>`) → Python `re` syntax (`(?P<name>)` / `(?P=name)`). PYTHON-ONLY
3515
+ // (JS keeps the original form). Python rejects the JS named-group syntax
3516
+ // outright, so this rewrite is load-bearing for ANY named-group pattern on the
3517
+ // Python target — and required for a `$<name>` repl ref to resolve. Run LAST,
3518
+ // AFTER the `/i` fold expansion: `expandRegexIFold`'s HOLE-1 fail-close depends
3519
+ // on seeing the ORIGINAL `\k<name>` backref token (`/(?<g>é)\k<g>/i` must still
3520
+ // fail-close), so R6 must NOT consume `\k<` before the fold scan. The fold/class/
3521
+ // anchor passes leave the ASCII `(?<` / `\k<` group syntax untouched, so applying
3522
+ // R6 here is order-stable and parity-safe.
3523
+ const named = lowerRegexNamedGroupsPython(normalized);
3524
+ return JSON.stringify(named);
2879
3525
  }
2880
3526
  function pyRegexFlags(flags, options = {}) {
2881
3527
  const unsupported = [...flags].filter((f) => {
@@ -2896,10 +3542,134 @@ function pyRegexFlags(flags, options = {}) {
2896
3542
  parts.push('__k_re.MULTILINE');
2897
3543
  if (flags.includes('s'))
2898
3544
  parts.push('__k_re.DOTALL');
2899
- return parts.length > 0 ? parts.join(' | ') : '0';
3545
+ // Milestone C, Slice 1 — always inject `re.ASCII` so Python `\b` and the
3546
+ // emitted ASCII classes match JS (without `/u`) semantics. This is the
3547
+ // load-bearing flag for word-boundary parity (see the `\bcafé\b` killer row).
3548
+ parts.push('__k_re.ASCII');
3549
+ return parts.join(' | ');
3550
+ }
3551
+ function wrapGuardIfAny(g, ctx) {
3552
+ if (g.guard === null)
3553
+ return g.expr;
3554
+ // Slice S7 — an optional-chain short-circuit yields the undefined SENTINEL in
3555
+ // native (coerce) bodies, so `typeof (undefined?.x)` is "undefined" and the
3556
+ // result participates in `??`/`===` nullish semantics. The helper-less
3557
+ // Ground/React layer (which never materializes the sentinel) keeps the
3558
+ // pre-slice `else None` short-circuit.
3559
+ let conditional;
3560
+ if (ctx.coerceJsValues) {
3561
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
3562
+ conditional = `(${g.expr} if ${g.guard} else _KERN_UNDEFINED)`;
3563
+ }
3564
+ else {
3565
+ conditional = `(${g.expr} if ${g.guard} else None)`;
3566
+ }
3567
+ // S7 review fix — in a comprehension-iterable position the non-pure receiver was
3568
+ // bound as a lambda PARAMETER instead of a `:=` walrus (CPython rejects the walrus
3569
+ // anywhere inside a comprehension iterable expression — see BodyEmitContext.banWalrus
3570
+ // and lowerOptionalLink). The guard + branch already reference the bare parameter, so
3571
+ // wrapping the whole conditional in `(lambda <param>: <conditional>)(<arg>)` keeps the
3572
+ // exact contract: <arg> (the receiver) evaluated exactly once, lazy short-circuit branch.
3573
+ if (g.lambdaBind) {
3574
+ return `(lambda ${g.lambdaBind.param}: ${conditional})(${g.lambdaBind.arg})`;
3575
+ }
3576
+ return conditional;
2900
3577
  }
2901
- function wrapGuardIfAny(g) {
2902
- return g.guard === null ? g.expr : `(${g.expr} if ${g.guard} else None)`;
3578
+ /** Slice S5 — does the LEFT operand of a `&&`/`||` walrus binding
3579
+ * (`__k_logN := L`) need parentheses?
3580
+ *
3581
+ * The walrus sits inside a `_kern_truthy(...)` call, whose own parens already
3582
+ * disambiguate `L`, so this is defensive/readability wrapping for the lowest-
3583
+ * precedence operand shapes (a `conditional` or `lambda` left operand). A
3584
+ * nested `&&`/`||` left operand is already self-parenthesized by its own
3585
+ * lowering, and an arithmetic/comparison `binary` binds tighter than `:=`, so
3586
+ * neither is wrapped here. */
3587
+ function needsWalrusOperandParens(child) {
3588
+ return child.kind === 'conditional' || child.kind === 'lambda';
3589
+ }
3590
+ /** Low-precedence operand positions (`a <op> b`, `await x`, `<callee>(...)`)
3591
+ * must wrap a conditional child so the surrounding operator/call binds to the
3592
+ * whole operand instead of one ternary arm. */
3593
+ function needsLowPrecedenceOperandParens(child) {
3594
+ let node = child;
3595
+ while (node.kind === 'typeAssert' || node.kind === 'nonNull')
3596
+ node = node.expression;
3597
+ return node.kind === 'conditional';
3598
+ }
3599
+ /** S5 review fix — run `fn` with `ctx.banWalrus` set (save/restore), for
3600
+ * emitting an operand that will be interpolated into a comprehension/
3601
+ * generator ITERABLE position, where CPython rejects `:=` outright (see
3602
+ * BodyEmitContext.banWalrus). Nested emissions inherit the flag through the
3603
+ * shared ctx, so a walrus producer at ANY depth of the operand (e.g. the
3604
+ * index expression in `items[i || 0]`) switches to its walrus-free form. */
3605
+ function withWalrusBan(ctx, fn) {
3606
+ const previous = ctx.banWalrus === true;
3607
+ ctx.banWalrus = true;
3608
+ try {
3609
+ return fn();
3610
+ }
3611
+ finally {
3612
+ ctx.banWalrus = previous;
3613
+ }
3614
+ }
3615
+ /** Slice S5 — does the RIGHT operand, emitted in the `else` arm of the lowered
3616
+ * conditional expression, need parentheses? A bare `conditional`/`lambda` in
3617
+ * an `else` arm parses but is ambiguous to read and brittle under further
3618
+ * composition, so wrap those two. A nested `&&`/`||` right operand is already
3619
+ * self-parenthesized. */
3620
+ function needsConditionalAlternateParens(child) {
3621
+ return child.kind === 'conditional' || child.kind === 'lambda';
3622
+ }
3623
+ /** S5 review fix — parenthesize a compound NON-CHAIN member-root expression so
3624
+ * the appended `.prop` link binds to the WHOLE root (`(b if t else c).prop`),
3625
+ * not its last operand (`b if t else c.prop` — silently wrong Python).
3626
+ * Low-precedence node kinds (same set as index receivers) are wrapped UNLESS
3627
+ * the lowering already produced a self-delimited atom: a fully-enclosing
3628
+ * paren pair (the `&&`/`||`/`??` walrus ternaries) or a single helper call
3629
+ * (`__kern_add(a, b)`), which keeps existing pinned bytes stable. */
3630
+ function wrapCompoundRootExpr(obj, emitted) {
3631
+ if (!needsIndexReceiverParens(obj))
3632
+ return emitted;
3633
+ if (isSelfDelimitedPyAtom(emitted))
3634
+ return emitted;
3635
+ return `(${emitted})`;
3636
+ }
3637
+ /** True when `expr` is one self-delimited Python atom: a fully-enclosing
3638
+ * bracket pair (`(...)`, `[...]`) or one identifier-headed call/index chain
3639
+ * whose trailing bracket closes at the very end (`__kern_add(a, b)`). A
3640
+ * leading unary sign (`-a`, `not x`, `await f()`) is NOT an atom. */
3641
+ function isSelfDelimitedPyAtom(expr) {
3642
+ if (expr.length === 0)
3643
+ return false;
3644
+ // Atoms start with an identifier/literal/bracket — a leading operator or
3645
+ // keyword (`-`, `~`, `not `, `await `) always needs wrapping.
3646
+ if (!/^[A-Za-z_0-9"'([{]/.test(expr))
3647
+ return false;
3648
+ if (!expr.includes(' '))
3649
+ return true;
3650
+ // Walk brackets/strings; any top-level space outside brackets means the
3651
+ // expression is compound (`b if t else c`), not an atom.
3652
+ let depth = 0;
3653
+ let quote = null;
3654
+ for (let i = 0; i < expr.length; i++) {
3655
+ const ch = expr[i];
3656
+ if (quote) {
3657
+ if (ch === '\\')
3658
+ i++;
3659
+ else if (ch === quote)
3660
+ quote = null;
3661
+ continue;
3662
+ }
3663
+ if (ch === '"' || ch === "'")
3664
+ quote = ch;
3665
+ else if (ch === '(' || ch === '[' || ch === '{')
3666
+ depth++;
3667
+ else if (ch === ')' || ch === ']' || ch === '}')
3668
+ depth--;
3669
+ else if (ch === ' ' && depth === 0)
3670
+ return false;
3671
+ }
3672
+ return depth === 0;
2903
3673
  }
2904
3674
  function needsIndexReceiverParens(child) {
2905
3675
  return (child.kind === 'binary' ||
@@ -2924,6 +3694,11 @@ function needsIndexReceiverParens(child) {
2924
3694
  function isReceiverChainPure(node) {
2925
3695
  if (node.kind === 'ident')
2926
3696
  return true;
3697
+ // Slice S7 — `undefined`/`null` literals are constant, so an optional chain
3698
+ // rooted at one (`undefined?.x`, `null?.x`) names a side-effect-free value and
3699
+ // takes the readable double-name guard form (short-circuiting to the sentinel).
3700
+ if (node.kind === 'undefLit' || node.kind === 'nullLit')
3701
+ return true;
2927
3702
  if (node.kind === 'member')
2928
3703
  return isReceiverChainPure(node.object);
2929
3704
  if (node.kind === 'index')
@@ -2980,6 +3755,88 @@ function mapBinaryOpToPython(op) {
2980
3755
  return op;
2981
3756
  }
2982
3757
  }
3758
+ /** Slice H — the host-namespace root set and diagnostic now live in core's
3759
+ * shared codegen module so TS and Python reject the same unmapped host roots
3760
+ * in lockstep. RegExp is intentionally exempt in that shared set for
3761
+ * Milestone B. */
3762
+ /** Slice H — is `name` PROVEN to be a user binding in the current expression
3763
+ * context?
3764
+ *
3765
+ * Reuses the emitter's EXISTING scope/binding model — the single source of
3766
+ * truth — rather than building a parallel tracker:
3767
+ * - `lookupLocalBinding` walks `ctx.localScopes` (function params via
3768
+ * `outerBindings`, body `let`/`const`/`cell` via `declareLocalBinding`,
3769
+ * loop vars, block scopes);
3770
+ * - `ctx.shadowedSymbols` records lambda/comprehension parameters that the
3771
+ * ident emitter already treats as user-local (see the `ident` case);
3772
+ * - `ctx.symbolMap` keys are KERN-form param names the FastAPI generator
3773
+ * renamed (e.g. `userId -> user_id`) — a present key means a real param.
3774
+ *
3775
+ * Per tribunal amendment 2, this FAILS TOWARD REFUSE: when the scope model
3776
+ * cannot prove a binding, we return false (→ the host-root guard fires),
3777
+ * never toward verbatim acceptance. Ground/module emitters that carry no
3778
+ * binding information therefore fail closed for reserved host roots, which is
3779
+ * exactly the intent — those are the contexts that previously leaked invalid
3780
+ * Python. (Honoring a *declared* binding — even one whose runtime value would
3781
+ * be `None` — is by design: this slice reuses the lexical scope model, it
3782
+ * does not do runtime value tracking. A user value named `Math` is the
3783
+ * shadowing case the spec explicitly keeps in scope.) */
3784
+ function isProvenUserBinding(ctx, name) {
3785
+ if (lookupLocalBinding(ctx, name) !== undefined)
3786
+ return true;
3787
+ if (ctx.shadowedSymbols.has(name))
3788
+ return true;
3789
+ if (Object.hasOwn(ctx.symbolMap, name))
3790
+ return true;
3791
+ return false;
3792
+ }
3793
+ /** Slice H — the fail-closed guard for unmapped host-namespace member
3794
+ * expressions (the strangler-pattern interim check).
3795
+ *
3796
+ * ACCEPTED DEBT (do not "fix" by relocating): this check lives at the
3797
+ * EMISSION point inside the code generator, not in a standalone validation
3798
+ * pass. That is interim by design — milestone A completes the portable
3799
+ * lowering registry (`KERN_STDLIB_MODULES`) and replaces these refusals with
3800
+ * real lowerings. Because the explicit AST lowering hooks all run BEFORE this
3801
+ * guard, the site is already scheduled to change as that registry grows: the
3802
+ * guard only ever sees the *remaining* unmapped forms. This is the strangler
3803
+ * pattern — the call site is provisioned to shrink, not to be reworked.
3804
+ *
3805
+ * Trigger predicate (capitalization-agnostic; NO {Math,JSON,Object,Date}
3806
+ * allowlist): a host-namespace-shaped root (`isHostNamespaceRoot`) that is
3807
+ * NOT proven user-bound (`isProvenUserBinding`, which fails toward refuse).
3808
+ * Returns `null` (caller proceeds to generic verbatim emission) for any root
3809
+ * that is provably a user binding or is not host-shaped — so user receivers
3810
+ * (`client.send(...)`, `myMath.sqrt(...)`) and proven-local `Math` pass
3811
+ * through unchanged. Throws otherwise. */
3812
+ function rejectUnmappedHostNamespacePython(root, member, ctx) {
3813
+ if (!isHostNamespaceRoot(root))
3814
+ return null;
3815
+ if (isProvenUserBinding(ctx, root))
3816
+ return null;
3817
+ throw new Error(unmappedHostNamespaceMessage('Python', root, member));
3818
+ }
3819
+ /** Slice 2 — host-`RegExp` fail-close (Python emit). Closes the residual RegExp
3820
+ * positions the generic `Module.member` guard above does NOT cover, throwing the
3821
+ * SAME shared `REGEX_HOST_REGEXP_FAILCLOSE` the TS emitter + IR-validate pass
3822
+ * throw (byte-identical across targets):
3823
+ * - a BARE-VALUE reference (`const R = RegExp`, `RegExp` passed as a value),
3824
+ * - a BARE CALL `RegExp(p, f)` (callee is an ident, missed by the member-callee
3825
+ * guard), and
3826
+ * - `new RegExp(p)` (the Python `new` case otherwise falls through to a verbatim
3827
+ * `RegExp(...)` NameError).
3828
+ * Honors user shadowing via `isProvenUserBinding` (fails toward refuse when a
3829
+ * binding cannot be proven, matching the host-namespace guard's intent), so
3830
+ * `const RegExp = myThing; RegExp` is the user value. `RegExp.prototype`/
3831
+ * `RegExp.$1` already fail-close through the generic member guard (one
3832
+ * diagnostic per site). */
3833
+ function rejectHostRegExpValuePython(name, ctx) {
3834
+ if (name !== 'RegExp')
3835
+ return;
3836
+ if (isProvenUserBinding(ctx, name))
3837
+ return;
3838
+ throw new Error(REGEX_HOST_REGEXP_FAILCLOSE);
3839
+ }
2983
3840
  /** Slice 2a — KERN-stdlib dispatch for Python. Returns the lowered Python
2984
3841
  * string when the call matches `<KnownModule>.<method>(args)`, or null when
2985
3842
  * it doesn't. Throws on `<KnownModule>.<unknownMethod>(...)` with a
@@ -2988,6 +3845,31 @@ function mapBinaryOpToPython(op) {
2988
3845
  * Slice 3b — when the matched entry declares `requires.py`, the import
2989
3846
  * identifier is added to the per-handler ctx.imports set so the FastAPI
2990
3847
  * generator can emit `import math` (etc.) at the top of the function body. */
3848
+ function applyStdlibPropertyLoweringPython(member, ctx) {
3849
+ if (member.optional)
3850
+ return null;
3851
+ if (member.object.kind !== 'ident')
3852
+ return null;
3853
+ const moduleName = member.object.name;
3854
+ if (!KERN_STDLIB_MODULES.has(moduleName))
3855
+ return null;
3856
+ const propertyName = member.property;
3857
+ const entry = lookupStdlibProperty(moduleName, propertyName);
3858
+ if (entry === null) {
3859
+ const callEntry = lookupStdlibCall(moduleName, propertyName);
3860
+ if (callEntry !== null) {
3861
+ throw new Error(`KERN-stdlib method '${moduleName}.${propertyName}' cannot be referenced as a value in portable Python lowering; call it directly.`);
3862
+ }
3863
+ throwUnknownStdlibMemberPython(moduleName, propertyName);
3864
+ }
3865
+ registerStdlibRequirementPython(entry.requires?.py, ctx);
3866
+ return entry.py;
3867
+ }
3868
+ function rejectKnownStdlibIndexPython(root, member) {
3869
+ if (!KERN_STDLIB_MODULES.has(root))
3870
+ return;
3871
+ throwUnknownStdlibMemberPython(root, member);
3872
+ }
2991
3873
  function applyStdlibLoweringPython(call, ctx) {
2992
3874
  const callee = call.callee;
2993
3875
  if (callee.kind !== 'member')
@@ -2998,34 +3880,119 @@ function applyStdlibLoweringPython(call, ctx) {
2998
3880
  if (!KERN_STDLIB_MODULES.has(moduleName))
2999
3881
  return null;
3000
3882
  const methodName = callee.property;
3001
- const entry = lookupStdlib(moduleName, methodName);
3883
+ const entry = lookupStdlibCall(moduleName, methodName);
3002
3884
  if (entry === null) {
3003
- const suggestion = suggestStdlibMethod(moduleName, methodName);
3004
- const hint = suggestion ? ` Did you mean '${moduleName}.${suggestion}'?` : '';
3005
- throw new Error(`Unknown KERN-stdlib method '${moduleName}.${methodName}'.${hint}`);
3885
+ const propertyEntry = lookupStdlibProperty(moduleName, methodName);
3886
+ if (propertyEntry !== null) {
3887
+ throw new Error(`KERN-stdlib property '${moduleName}.${methodName}' is not callable.`);
3888
+ }
3889
+ throwUnknownStdlibMemberPython(moduleName, methodName);
3006
3890
  }
3007
3891
  // Slice-2 review fix: enforce declared arity (matches TS-side check).
3008
- if (call.args.length !== entry.arity) {
3009
- throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes ${entry.arity} arg${entry.arity === 1 ? '' : 's'}, got ${call.args.length}.`);
3892
+ validateStdlibCallArityPython(moduleName, methodName, entry, call.args.length);
3893
+ if (moduleName === 'Array' && methodName === 'from' && call.args.some((arg) => arg.kind === 'spread')) {
3894
+ throw new Error('Array.from portable lowering does not accept spread arguments; pass source and mapper directly.');
3010
3895
  }
3896
+ // DECIMAL Slice 1 — `Decimal.of(arg)` string-literal + canonical-scale check,
3897
+ // running the SAME shared-core validator the TS leg runs, so the fail-close is
3898
+ // byte-identical across targets.
3899
+ validateDecimalConstructionArg(moduleName, methodName, call);
3900
+ // DECIMAL Slice 3 — same shared compile-time pow fail-close the TS leg runs, so a
3901
+ // non-integer / non-literal exponent or a negative base is refused byte-identically.
3902
+ validateDecimalPowArgs(moduleName, methodName, call);
3903
+ // DECIMAL Slice 3 (robustness) — same shared non-Decimal-operand fail-close the TS
3904
+ // leg runs: a provably-non-Decimal LITERAL operand (a host number/string/…) passed
3905
+ // to any Decimal op but `of` is refused byte-identically, closing the silent
3906
+ // cross-target divergence a raw `0.1` operand would otherwise emit.
3907
+ validateDecimalOperands(moduleName, methodName, call);
3908
+ // DECIMAL Slice 3 — same shared compile-time zero-divisor fail-close: a literal
3909
+ // `Decimal.of("0")` divisor to `Decimal.div`/`Decimal.mod` is refused byte-
3910
+ // identically with the runtime guard's message (a dynamic zero stays runtime-caught).
3911
+ validateDecimalDivModArgs(moduleName, methodName, call);
3011
3912
  const listLambda = lowerListLambdaPython(moduleName, methodName, call, ctx);
3012
3913
  if (listLambda !== null)
3013
3914
  return listLambda;
3014
3915
  // Slice 3b — register required imports (e.g., `Number.floor` ⇒ `import math`).
3015
- if (entry.requires?.py)
3016
- ctx.imports.add(entry.requires.py);
3916
+ registerStdlibRequirementPython(entry.requires?.py, ctx);
3017
3917
  const args = call.args.map((a) => {
3018
3918
  const emitted = emitPyExprCtx(a, ctx);
3019
- return needsArgParens(a) ? `(${emitted})` : emitted;
3919
+ return a.kind !== 'spread' && needsArgParens(a) ? `(${emitted})` : emitted;
3020
3920
  });
3021
- return applyTemplate(entry.py, args);
3921
+ // Slice S7 — `Json.stringify` on Python routes through the sentinel-aware shim
3922
+ // (single-source `KERN_JSON_STRINGIFY_SHIM_PY`) instead of raw `__k_json.dumps`,
3923
+ // so `JSON.stringify(undefined)` is host-undefined, an object key whose value
3924
+ // is the sentinel is omitted, and a sentinel array element becomes JSON null —
3925
+ // matching JS. The shim references `__k_json`, supplied by the table's
3926
+ // `requires.py: 'json'` import registered above (one import shared with parse).
3927
+ if ((moduleName === 'Json' || moduleName === 'JSON') && methodName === 'stringify') {
3928
+ ctx.helpers.add(KERN_JSON_STRINGIFY_SHIM_PY);
3929
+ return `_kern_json_stringify(${args[0]})`;
3930
+ }
3931
+ return typeof entry.py === 'function' ? entry.py(args) : applyTemplate(entry.py, args);
3932
+ }
3933
+ function throwUnknownStdlibMemberPython(moduleName, memberName) {
3934
+ const suggestion = suggestStdlibMethod(moduleName, memberName);
3935
+ const hint = suggestion ? ` Did you mean '${moduleName}.${suggestion}'?` : '';
3936
+ throw new Error(`Unknown KERN-stdlib method/member '${moduleName}.${memberName}'.${hint}`);
3937
+ }
3938
+ function validateStdlibCallArityPython(moduleName, methodName, entry, got) {
3939
+ if (entry.arity !== undefined && got !== entry.arity) {
3940
+ throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes ${entry.arity} arg${entry.arity === 1 ? '' : 's'}, got ${got}.`);
3941
+ }
3942
+ if (entry.minArity !== undefined && got < entry.minArity) {
3943
+ throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes at least ${entry.minArity} args, got ${got}.`);
3944
+ }
3945
+ if (entry.maxArity !== undefined && got > entry.maxArity) {
3946
+ throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes at most ${entry.maxArity} args, got ${got}.`);
3947
+ }
3948
+ }
3949
+ function registerStdlibRequirementPython(requirement, ctx) {
3950
+ if (!requirement)
3951
+ return;
3952
+ if (requirement === 'math-host') {
3953
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
3954
+ ctx.helpers.add(KERN_JS_MATH_HELPERS_PY);
3955
+ return;
3956
+ }
3957
+ if (requirement === 'array-host') {
3958
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
3959
+ ctx.helpers.add(KERN_JS_ARRAY_FROM_HELPER_PY);
3960
+ return;
3961
+ }
3962
+ if (requirement === 'object-host') {
3963
+ ctx.helpers.add(KERN_JS_OBJECT_HELPERS_PY);
3964
+ return;
3965
+ }
3966
+ if (requirement === 'number-host') {
3967
+ ctx.helpers.add(KERN_JS_NUMBER_HELPERS_PY);
3968
+ return;
3969
+ }
3970
+ // DECIMAL Slice 3 — `Decimal.div/mod/pow` register the guarded-ops helper block
3971
+ // (single-sourced in `decimal-contract.ts`) AND the `decimal` import (the helper
3972
+ // body references `_KernDecimal` for the `0**0 → 1` special-case). The block's
3973
+ // own `from decimal import Decimal as _KernDecimal` line supplies that binding;
3974
+ // we still register the `decimal` module import so the Decimal VALUES the helper
3975
+ // operates on (`__k_decimal.Decimal(...)` from their `Decimal.of` producers) keep
3976
+ // their existing import path — and so a div/mod/pow used WITHOUT a co-located
3977
+ // `Decimal.of` literal still imports decimal.
3978
+ if (requirement === 'decimal-ops') {
3979
+ ctx.helpers.add(KERN_DECIMAL_OPS_HELPER_PY);
3980
+ ctx.imports.add('decimal');
3981
+ return;
3982
+ }
3983
+ ctx.imports.add(requirement);
3022
3984
  }
3023
3985
  function lowerListLambdaPython(moduleName, methodName, call, ctx) {
3024
3986
  if (moduleName !== 'List')
3025
3987
  return null;
3026
3988
  if (methodName !== 'map' && methodName !== 'filter')
3027
3989
  return null;
3028
- const source = emitPyExprCtx(call.args[0], ctx);
3990
+ // S5 review fix — the source lands in the comprehension's generator head
3991
+ // (`for x in <source>`): walrus producers must switch to lambda-parameter
3992
+ // forms (CPython rejects `:=` in a comprehension iterable at any depth) and
3993
+ // a compound source (bare ternary) must be parenthesized so its `if` does
3994
+ // not parse as the comprehension filter.
3995
+ const source = parenthesizeIterable(withWalrusBan(ctx, () => emitPyExprCtx(call.args[0], ctx)));
3029
3996
  const callback = call.args[1];
3030
3997
  if (callback.kind !== 'lambda') {
3031
3998
  const fn = emitPyExprCtx(callback, ctx);
@@ -3056,35 +4023,132 @@ function lowerListLambdaPython(moduleName, methodName, call, ctx) {
3056
4023
  ctx.shadowedSymbols = previous;
3057
4024
  }
3058
4025
  }
4026
+ /** Slice 6 — Python bitwise/shift operator precedence (higher binds tighter).
4027
+ * Matches CPython's grammar: `|` < `^` < `&` < shift < unary `~`. Used to
4028
+ * parenthesize the LOWERED tree so the emitted Python reproduces the JS-shaped
4029
+ * grouping (e.g. `a << (b & 31)` must keep the mask parens, since Python's
4030
+ * `<<` binds tighter than `&`). */
4031
+ const PY_BITWISE_PREC = { '|': 1, '^': 2, '&': 3, '<<': 4, '>>': 4 };
4032
+ /**
4033
+ * Slice 6 — emit an ALREADY-LOWERED bitwise/shift tree to a Python string.
4034
+ *
4035
+ * The tree produced by `lowerBitwiseAndModuloAST` contains FINAL Python
4036
+ * operators (`| & ^ << >>` and unary `~`) whose operands are already wrapped in
4037
+ * `_kern_to_int32` / `_kern_to_uint32` calls. Re-routing those operators back
4038
+ * through `emitPyExprCtx`'s bitwise branch would re-lower them and recurse
4039
+ * forever, so this dedicated emitter renders the bitwise/shift `binary` and the
4040
+ * `~` `unary` verbatim (with precedence-correct parens) and delegates every
4041
+ * other node kind (the leaves: calls, idents, literals, …) to `emitPyExprCtx`.
4042
+ */
4043
+ function emitLoweredBitwisePy(node, ctx) {
4044
+ // The lowering's own wrapper calls (`_kern_to_int32` / `_kern_to_uint32` /
4045
+ // `_tmod`) must stay in THIS emit path: their argument is an already-lowered
4046
+ // bitwise/shift subtree, and handing the whole call to `emitPyExprCtx` would
4047
+ // re-route that inner subtree back into the bitwise branch and recurse
4048
+ // forever. Emit the wrapper directly and recurse through its argument here.
4049
+ if (node.kind === 'call' &&
4050
+ node.callee.kind === 'ident' &&
4051
+ (node.callee.name === '_kern_to_int32' || node.callee.name === '_kern_to_uint32' || node.callee.name === '_tmod')) {
4052
+ const args = node.args.map((a) => emitLoweredBitwisePy(a, ctx)).join(', ');
4053
+ return `${node.callee.name}(${args})`;
4054
+ }
4055
+ if (node.kind === 'binary' && node.op in PY_BITWISE_PREC) {
4056
+ const parentPrec = PY_BITWISE_PREC[node.op];
4057
+ const emitChild = (child, isRight) => {
4058
+ const s = emitLoweredBitwisePy(child, ctx);
4059
+ if (child.kind === 'binary' && child.op in PY_BITWISE_PREC) {
4060
+ const childPrec = PY_BITWISE_PREC[child.op];
4061
+ // Lower-precedence child always needs parens; an equal-precedence child
4062
+ // on the RIGHT needs parens to preserve left-associative grouping.
4063
+ if (childPrec < parentPrec || (childPrec === parentPrec && isRight))
4064
+ return `(${s})`;
4065
+ }
4066
+ return s;
4067
+ };
4068
+ return `${emitChild(node.left, false)} ${node.op} ${emitChild(node.right, true)}`;
4069
+ }
4070
+ if (node.kind === 'unary' && node.op === '~') {
4071
+ const inner = emitLoweredBitwisePy(node.argument, ctx);
4072
+ // `~` binds tighter than every binary bitwise/shift op, so a binary argument
4073
+ // must be parenthesized (e.g. `~(a | b)`).
4074
+ const wrapped = node.argument.kind === 'binary' && node.argument.op in PY_BITWISE_PREC ? `(${inner})` : inner;
4075
+ return `~${wrapped}`;
4076
+ }
4077
+ // Leaf / non-bitwise node — hand back to the main expression emitter.
4078
+ return emitPyExprCtx(node, ctx);
4079
+ }
4080
+ /**
4081
+ * Slice 6 — emit a binary bitwise/shift op `a <op> b` (op ∈ `| & ^ << >>`) on
4082
+ * the slice-0.75 ToInt32 substrate, per the S6 emission contract:
4083
+ *
4084
+ * a | b -> _kern_to_int32(_kern_to_int32(a) | _kern_to_int32(b))
4085
+ * a << b -> _kern_to_int32(_kern_to_int32(a) << (_kern_to_uint32(b) & 31))
4086
+ *
4087
+ * Each operand subexpression appears EXACTLY ONCE, so Python evaluates each side
4088
+ * once, left-to-right — matching JS evaluation-once order with no temporaries.
4089
+ */
4090
+ function emitBitwiseShiftPy(op, left, right, ctx) {
4091
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
4092
+ const l = `_kern_to_int32(${emitPyExprCtx(left, ctx)})`;
4093
+ if (op === '<<' || op === '>>') {
4094
+ // Shift count: ToUint32 then `& 31`.
4095
+ const count = `(_kern_to_uint32(${emitPyExprCtx(right, ctx)}) & 31)`;
4096
+ return `_kern_to_int32(${l} ${op} ${count})`;
4097
+ }
4098
+ const r = `_kern_to_int32(${emitPyExprCtx(right, ctx)})`;
4099
+ return `_kern_to_int32(${l} ${op} ${r})`;
4100
+ }
4101
+ /**
4102
+ * Slice 6 — emit `a >>> b` (unsigned/zero-fill right shift):
4103
+ *
4104
+ * a >>> b -> _kern_to_uint32(_kern_to_uint32(a) >> (_kern_to_uint32(b) & 31))
4105
+ *
4106
+ * Both operands are non-negative after `_kern_to_uint32`, so Python's raw `>>`
4107
+ * is zero-fill (it NEVER sign-extends here); the outer `_kern_to_uint32` keeps
4108
+ * the result in the 0..2**32-1 Uint32 codomain. Raw signed `>>` on the original
4109
+ * value would sign-extend and is therefore never used.
4110
+ */
4111
+ function emitUnsignedShiftPy(left, right, ctx) {
4112
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
4113
+ const l = `_kern_to_uint32(${emitPyExprCtx(left, ctx)})`;
4114
+ const count = `(_kern_to_uint32(${emitPyExprCtx(right, ctx)}) & 31)`;
4115
+ return `_kern_to_uint32(${l} >> ${count})`;
4116
+ }
3059
4117
  export function lowerBitwiseAndModuloAST(node) {
3060
4118
  switch (node.kind) {
3061
4119
  case 'binary': {
3062
4120
  const left = lowerBitwiseAndModuloAST(node.left);
3063
4121
  const right = lowerBitwiseAndModuloAST(node.right);
3064
- if (node.op === '|' || node.op === '&' || node.op === '^' || node.op === '<<' || node.op === '>>') {
3065
- let rewrittenRight = right;
3066
- if (node.op === '<<' || node.op === '>>') {
3067
- const i32Right = wrapInI32(right);
3068
- rewrittenRight = {
3069
- kind: 'binary',
3070
- op: '&',
3071
- left: i32Right,
3072
- right: { kind: 'numLit', value: 31, raw: '31' },
3073
- };
3074
- }
3075
- else {
3076
- rewrittenRight = wrapInI32(right);
3077
- }
3078
- const i32Left = wrapInI32(left);
3079
- const bitwiseNode = {
4122
+ // Slice 6 bitwise / shift on the landed slice-0.75 ToInt32 substrate.
4123
+ // Each operand expression appears EXACTLY ONCE in the lowered form (the
4124
+ // helper call wraps it inline), so Python evaluates each side once,
4125
+ // left-to-right matching JS evaluation-once order without temporaries.
4126
+ if (node.op === '|' || node.op === '&' || node.op === '^') {
4127
+ // a <op> b -> _kern_to_int32(_kern_to_int32(a) <op> _kern_to_int32(b))
4128
+ return wrapInToInt32({ kind: 'binary', op: node.op, left: wrapInToInt32(left), right: wrapInToInt32(right) });
4129
+ }
4130
+ if (node.op === '<<' || node.op === '>>') {
4131
+ // a <op> b -> _kern_to_int32(_kern_to_int32(a) <op> (_kern_to_uint32(b) & 31))
4132
+ return wrapInToInt32({
3080
4133
  kind: 'binary',
3081
4134
  op: node.op,
3082
- left: i32Left,
3083
- right: rewrittenRight,
3084
- };
3085
- return wrapInI32(bitwiseNode);
4135
+ left: wrapInToInt32(left),
4136
+ right: maskShiftCount(right),
4137
+ });
4138
+ }
4139
+ if (node.op === '>>>') {
4140
+ // a >>> b -> _kern_to_uint32(_kern_to_uint32(a) >> (_kern_to_uint32(b) & 31))
4141
+ // Both operands are non-negative after _kern_to_uint32, so Python's raw
4142
+ // `>>` is zero-fill (NEVER sign-extends); the outer _kern_to_uint32 keeps
4143
+ // the result in the 0..2**32-1 Uint32 codomain.
4144
+ return wrapInToUint32({
4145
+ kind: 'binary',
4146
+ op: '>>',
4147
+ left: wrapInToUint32(left),
4148
+ right: maskShiftCount(right),
4149
+ });
3086
4150
  }
3087
- else if (node.op === '%') {
4151
+ if (node.op === '%') {
3088
4152
  return {
3089
4153
  kind: 'call',
3090
4154
  callee: { kind: 'ident', name: '_tmod' },
@@ -3097,13 +4161,8 @@ export function lowerBitwiseAndModuloAST(node) {
3097
4161
  case 'unary': {
3098
4162
  const argument = lowerBitwiseAndModuloAST(node.argument);
3099
4163
  if (node.op === '~') {
3100
- const i32Arg = wrapInI32(argument);
3101
- const unaryNode = {
3102
- kind: 'unary',
3103
- op: '~',
3104
- argument: i32Arg,
3105
- };
3106
- return wrapInI32(unaryNode);
4164
+ // ~a -> _kern_to_int32(~_kern_to_int32(a))
4165
+ return wrapInToInt32({ kind: 'unary', op: '~', argument: wrapInToInt32(argument) });
3107
4166
  }
3108
4167
  return { ...node, argument };
3109
4168
  }
@@ -3156,20 +4215,32 @@ export function lowerBitwiseAndModuloAST(node) {
3156
4215
  return node;
3157
4216
  }
3158
4217
  }
3159
- function wrapInI32(node) {
4218
+ /** Slice 6 — wrap `node` in a `_kern_to_int32(...)` call (the landed slice-0.75
4219
+ * ToInt32 helper). Emits the operator-level coercion the S6 contract mandates. */
4220
+ function wrapInToInt32(node) {
4221
+ return { kind: 'call', callee: { kind: 'ident', name: '_kern_to_int32' }, args: [node], optional: false };
4222
+ }
4223
+ /** Slice 6 — wrap `node` in a `_kern_to_uint32(...)` call (slice-0.75 ToUint32). */
4224
+ function wrapInToUint32(node) {
4225
+ return { kind: 'call', callee: { kind: 'ident', name: '_kern_to_uint32' }, args: [node], optional: false };
4226
+ }
4227
+ /** Slice 6 — JS masks every shift count with `& 31` after ToUint32. Emits
4228
+ * `(_kern_to_uint32(count) & 31)`. The `count` subexpression appears once. */
4229
+ function maskShiftCount(count) {
3160
4230
  return {
3161
- kind: 'call',
3162
- callee: { kind: 'ident', name: '_i32' },
3163
- args: [node],
3164
- optional: false,
4231
+ kind: 'binary',
4232
+ op: '&',
4233
+ left: wrapInToUint32(count),
4234
+ right: { kind: 'numLit', value: 31, raw: '31' },
3165
4235
  };
3166
4236
  }
3167
4237
  export function registerHelpers(node, ctx) {
3168
4238
  switch (node.kind) {
3169
4239
  case 'call':
3170
4240
  if (node.callee.kind === 'ident') {
3171
- if (node.callee.name === '_i32') {
3172
- ctx.helpers.add(KERN_I32_HELPER_PY);
4241
+ if (node.callee.name === '_kern_to_int32' || node.callee.name === '_kern_to_uint32') {
4242
+ // Slice 6 — both helpers live in the single slice-0.75 helper block.
4243
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
3173
4244
  }
3174
4245
  else if (node.callee.name === '_tmod') {
3175
4246
  ctx.helpers.add(KERN_TMOD_HELPER_PY);
@@ -3255,7 +4326,7 @@ function emitExpressionV1Py(node, ctx) {
3255
4326
  if (exprSource === undefined || exprSource === '') {
3256
4327
  throw new Error('body-statement `expression-v1` requires `expr=`.');
3257
4328
  }
3258
- const exprIR = parseExpression(exprSource);
4329
+ const exprIR = parseExpr(exprSource);
3259
4330
  declareLocalBinding(ctx, userName, 'const');
3260
4331
  const name = maybeRenameOnShadow(ctx, userName);
3261
4332
  setRegexBinding(ctx, userName, exprIR.kind === 'regexLit' ? exprIR : null);