@kernlang/python 4.0.0 → 4.0.1-canary.224.1.1a92ac0a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, classifyRegexLiteralIndexReadFailClose, classifyRegexLiteralMemberReadFailClose, emitStringKeyArray, expandRegexIFold, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isHostNamespaceRoot, isPostfixMutationOperator, isSupportedAssignOperator, isZeroWidthCapableRegex, KERN_STDLIB_MODULES, lookupStdlibCall, lookupStdlibProperty, lowerRegexAnchorsPython, lowerRegexNamedGroupsPython, needsArgParens, needsBinaryParens, normalizeRegexClasses, parseExpression, parseKeys, REGEX_EXEC_FAILCLOSE, REGEX_HOST_REGEXP_FAILCLOSE, REGEX_MATCHALL_NO_G_FAILCLOSE, REGEX_NONLITERAL_FAILCLOSE, REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE, REGEX_REPLACEALL_NO_G_FAILCLOSE, REGEX_SPLIT_LIMIT_FAILCLOSE, REGEX_SPLIT_ZEROWIDTH_FAILCLOSE, REGEX_TEST_G_FAILCLOSE, regexAstralFailMessage, regexCaptureMeta, regexIFoldFailMessage, regexLiteralReceiverIR, regexMethodRegexArgIdent, scanRegexAstral, suggestStdlibMethod, translateReplStringToPython, unmappedHostNamespaceMessage, unwrapTransparentReceiverIR, validateRegexNamedGroupsPortable, } from '@kernlang/core';
44
+ // Slice 0.9 — the TypeScript-AST closure helpers + classifier live on the Node
45
+ // subpath (the barrel is browser-safe). Python codegen is Node-side and parses
46
+ // block-bodied arrows, so it injects `typescriptClosureClassifier`.
47
+ import { collectFreeIdentifierNames, lowerJsClosureBodyToPython, typescriptClosureClassifier, } from '@kernlang/core/node';
44
48
  import { buildPythonParamList } from './codegen-helpers.js';
45
- import { KERN_FMT_HELPER_PY, 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,7 +1860,7 @@ function emitPyExprCtx(node, ctx) {
1757
1860
  // The top-level wrapper produces `(expr if guard else None)` once
1758
1861
  // (or just `expr` when no `?.` was seen).
1759
1862
  const lowered = lowerChain(node, ctx);
1760
- return wrapGuardIfAny(lowered);
1863
+ return wrapGuardIfAny(lowered, ctx);
1761
1864
  }
1762
1865
  case 'await':
1763
1866
  return `await ${emitPyExprCtx(node.argument, ctx)}`;
@@ -1784,6 +1887,17 @@ function emitPyExprCtx(node, ctx) {
1784
1887
  if (arg.kind === 'ident' && arg.name === 'Error') {
1785
1888
  return 'Exception()';
1786
1889
  }
1890
+ // Slice 2 — `new RegExp(p)` (with or without parens) fails-close BEFORE the
1891
+ // fall-through, with the shared regex message (the TS emitter + IR-validate
1892
+ // throw the same string). Without this the Python `new` case falls through
1893
+ // to a verbatim `RegExp(...)` NameError instead of a compile-time refusal.
1894
+ // Honors user shadowing via `isProvenUserBinding`.
1895
+ if (arg.kind === 'call' && arg.callee.kind === 'ident') {
1896
+ rejectHostRegExpValuePython(arg.callee.name, ctx);
1897
+ }
1898
+ else if (arg.kind === 'ident') {
1899
+ rejectHostRegExpValuePython(arg.name, ctx);
1900
+ }
1787
1901
  return emitPyExprCtx(arg, ctx);
1788
1902
  }
1789
1903
  case 'typeAssert':
@@ -1816,15 +1930,21 @@ function emitPyExprCtx(node, ctx) {
1816
1930
  return out;
1817
1931
  }
1818
1932
  case 'binary': {
1819
- if (node.op === '|' ||
1820
- node.op === '&' ||
1821
- node.op === '^' ||
1822
- node.op === '<<' ||
1823
- node.op === '>>' ||
1824
- node.op === '%') {
1933
+ // Slice 6 — bitwise / shift on the slice-0.75 ToInt32 substrate. Emitted
1934
+ // DIRECTLY as helper-wrapped strings (operands recurse through
1935
+ // `emitPyExprCtx`), so nested bitwise ops compose without the double-
1936
+ // dispatch recursion an AST-rewrite-then-re-emit would cause.
1937
+ if (node.op === '|' || node.op === '&' || node.op === '^' || node.op === '<<' || node.op === '>>') {
1938
+ return emitBitwiseShiftPy(node.op, node.left, node.right, ctx);
1939
+ }
1940
+ if (node.op === '>>>') {
1941
+ return emitUnsignedShiftPy(node.left, node.right, ctx);
1942
+ }
1943
+ if (node.op === '%') {
1944
+ // Modulo keeps the existing `_tmod` lowering (orthogonal to S6).
1825
1945
  const transformed = lowerBitwiseAndModuloAST(node);
1826
1946
  registerHelpers(transformed, ctx);
1827
- return emitPyExprCtx(transformed, ctx);
1947
+ return emitLoweredBitwisePy(transformed, ctx);
1828
1948
  }
1829
1949
  // Slice 2c — arithmetic / comparison / logical lowering for Python.
1830
1950
  // Use precedence-aware paren-wrapping so `a + b * c` doesn't redundantly
@@ -1894,6 +2014,62 @@ function emitPyExprCtx(node, ctx) {
1894
2014
  }
1895
2015
  return `__kern_add(${left}, ${right})`;
1896
2016
  }
2017
+ if (node.op === '&&' || node.op === '||') {
2018
+ // Slice S5 — logical `&&` / `||` RESULT-VALUE semantics on Python.
2019
+ //
2020
+ // KERN `&&`/`||` are operand-selectors, not boolean operators:
2021
+ // `a && b` returns `a` when ToBoolean(a) is false, else `b`.
2022
+ // `a || b` returns `a` when ToBoolean(a) is true, else `b`.
2023
+ // Python's `and`/`or` use Python truthiness (`bool(x)` / `len(x)`), which
2024
+ // diverges from KERN ToBoolean on `[]`, `{}`, `NaN`, `"0"`, `"false"`,
2025
+ // `" "` — e.g. `[] or x` returns `x` in Python but must return `[]` in
2026
+ // KERN. So `and`/`or` are wrong; we lower through the SAME `_kern_truthy`
2027
+ // (KERN ToBoolean) substrate S4 routes `!`/ternary/`if cond=` through.
2028
+ //
2029
+ // Canonical lowering (single-eval, lazy, original-value-returning):
2030
+ // L && R -> (__k_logN if not _kern_truthy(__k_logN := L) else R)
2031
+ // L || R -> (__k_logN if _kern_truthy(__k_logN := L) else R)
2032
+ //
2033
+ // The walrus binds L EXACTLY ONCE (works for calls/indexes/members/
2034
+ // nested logicals — no double-read), `_kern_truthy` decides the branch
2035
+ // on KERN truthiness, and the unselected operand is NEVER evaluated
2036
+ // (Python conditional-expr branches are lazy). NO ident/pure-left fast
2037
+ // path in this slice — conservative walrus for EVERY left operand is the
2038
+ // single-eval law (S4 carry-forward; optimization needs its own oracle).
2039
+ //
2040
+ // Emitted UNCONDITIONALLY for both native (`coerceJsValues`) and Ground/
2041
+ // React layers — mirroring S4's `!`/ternary, which also emit
2042
+ // `_kern_truthy` regardless of the coercion flag. The `[]`/`{}`/`NaN`
2043
+ // divergence is independent of the undefined sentinel, so a Ground-layer
2044
+ // `a and b` would be just as wrong; parity demands one spelling.
2045
+ // `KERN_JS_HELPER_PY` is in Ground's prelude registry, so the helper is
2046
+ // inlined there too.
2047
+ ctx.helpers.add(KERN_JS_HELPER_PY);
2048
+ const tmp = `__k_log${++ctx.gensymCounter}`;
2049
+ // `:=` (PEP 572) binds looser than almost everything, so a low-precedence
2050
+ // left operand (conditional/lambda/walrus-bearing) must be parenthesized
2051
+ // so `__k_logN := L` captures the WHOLE operand. A nested `&&`/`||` is
2052
+ // already self-parenthesized, so this only fires on raw conditional/
2053
+ // lambda left operands.
2054
+ const leftOperand = needsWalrusOperandParens(node.left) ? `(${left})` : left;
2055
+ // The `else` branch is a conditional-expression alternate; a bare
2056
+ // conditional/lambda there reparses ambiguously, so wrap it. A nested
2057
+ // logical R is self-parenthesized; only raw conditional/lambda needs it.
2058
+ const rightOperand = needsConditionalAlternateParens(node.right) ? `(${right})` : right;
2059
+ if (ctx.banWalrus) {
2060
+ // Comprehension-iterable position (see BodyEmitContext.banWalrus):
2061
+ // the walrus form is a SyntaxError here, so bind L as a lambda
2062
+ // parameter instead. Same contract: L evaluated exactly once (as the
2063
+ // call argument), the unselected branch never evaluated (lazy
2064
+ // conditional inside the lambda body), original value returned.
2065
+ const test = `_kern_truthy(${tmp})`;
2066
+ const guarded = node.op === '&&' ? `not ${test}` : test;
2067
+ return `(lambda ${tmp}: (${tmp} if ${guarded} else ${rightOperand}))(${left})`;
2068
+ }
2069
+ const test = `_kern_truthy(${tmp} := ${leftOperand})`;
2070
+ const guarded = node.op === '&&' ? `not ${test}` : test;
2071
+ return `(${tmp} if ${guarded} else ${rightOperand})`;
2072
+ }
1897
2073
  if (node.op === '??') {
1898
2074
  // Slice 4c — nullish coalesce lowering. Two shapes:
1899
2075
  //
@@ -1924,6 +2100,11 @@ function emitPyExprCtx(node, ctx) {
1924
2100
  return `(${left} if ${left} is not None else ${right})`;
1925
2101
  }
1926
2102
  const tmp = `__k_nc${++ctx.gensymCounter}`;
2103
+ if (ctx.banWalrus) {
2104
+ // Comprehension-iterable position — walrus is a SyntaxError here;
2105
+ // lambda-parameter form keeps single-eval + lazy right branch.
2106
+ return `(lambda ${tmp}: (${tmp} if ${tmp} is not None else ${right}))(${left})`;
2107
+ }
1927
2108
  return `(${tmp} if (${tmp} := ${left}) is not None else ${right})`;
1928
2109
  }
1929
2110
  ctx.helpers.add(KERN_FMT_HELPER_PY);
@@ -1931,8 +2112,29 @@ function emitPyExprCtx(node, ctx) {
1931
2112
  return `(${left} if (${left} is not None and ${left} is not _KERN_UNDEFINED) else ${right})`;
1932
2113
  }
1933
2114
  const tmp = `__k_nc${++ctx.gensymCounter}`;
2115
+ if (ctx.banWalrus) {
2116
+ // Comprehension-iterable position — see BodyEmitContext.banWalrus.
2117
+ return `(lambda ${tmp}: (${tmp} if (${tmp} is not None and ${tmp} is not _KERN_UNDEFINED) else ${right}))(${left})`;
2118
+ }
1934
2119
  return `(${tmp} if ((${tmp} := ${left}) is not None and ${tmp} is not _KERN_UNDEFINED) else ${right})`;
1935
2120
  }
2121
+ // Slice S7 — split loose (`==`/`!=`) and strict (`===`/`!==`) equality so
2122
+ // the null/undefined boundary matches JS: `undefined == null` is True (both
2123
+ // nullish), `undefined === null` is False (distinct identities). Pre-S7
2124
+ // both lowered to Python `==`, which on the sentinel/None pair is False —
2125
+ // wrong for the loose op. Routed through the helper pair only in native
2126
+ // (coerce) bodies; the helper-less Ground/React layer keeps the raw
2127
+ // `==`/`!=` mapping (it never materializes the sentinel, so the nullish
2128
+ // crossing cannot arise there). Function-call form sidesteps Python's
2129
+ // comparison-chaining entirely, so no chain-paren handling is needed for
2130
+ // the equality ops themselves; chained comparison CHILDREN still recurse
2131
+ // through `emitPyExprCtx` and self-parenthesize as before.
2132
+ if (ctx.coerceJsValues && (node.op === '===' || node.op === '!==' || node.op === '==' || node.op === '!=')) {
2133
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
2134
+ const fn = node.op === '===' || node.op === '!==' ? '_kern_strict_equal' : '_kern_loose_equal';
2135
+ const call = `${fn}(${left}, ${right})`;
2136
+ return node.op === '!==' || node.op === '!=' ? `(not ${call})` : call;
2137
+ }
1936
2138
  const forceLeft = needsComparisonChainParens(node.left, node.op);
1937
2139
  const forceRight = needsComparisonChainParens(node.right, node.op);
1938
2140
  const lp = forceLeft || needsBinaryParens(node.left, node.op, 'left') ? `(${left})` : left;
@@ -1942,9 +2144,9 @@ function emitPyExprCtx(node, ctx) {
1942
2144
  }
1943
2145
  case 'unary': {
1944
2146
  if (node.op === '~') {
1945
- const transformed = lowerBitwiseAndModuloAST(node);
1946
- registerHelpers(transformed, ctx);
1947
- return emitPyExprCtx(transformed, ctx);
2147
+ // ~a -> _kern_to_int32(~_kern_to_int32(a))
2148
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
2149
+ return `_kern_to_int32(~_kern_to_int32(${emitPyExprCtx(node.argument, ctx)}))`;
1948
2150
  }
1949
2151
  // Slice 2c — `!x` → `not x`, `-x` → `-x`.
1950
2152
  // Slice typeof — expose the now-eligible native KERN `typeof` shape on
@@ -1954,8 +2156,15 @@ function emitPyExprCtx(node, ctx) {
1954
2156
  return emitPyTypeof(node.argument, ctx);
1955
2157
  const arg = emitPyExprCtx(node.argument, ctx);
1956
2158
  const wrapped = needsArgParens(node.argument) ? `(${arg})` : arg;
1957
- if (node.op === '!')
1958
- return `not ${wrapped}`;
2159
+ // Slice S4 — `!x` consumes KERN ToBoolean and returns a real Python bool.
2160
+ // `not _kern_truthy(x)` gives `!""`/`!NaN` → True and `!"0"`/`![]` → False
2161
+ // (bare `not x` would get `![]`/`!{}`/`!NaN` wrong). Wrap unconditionally.
2162
+ if (node.op === '!') {
2163
+ ctx.helpers.add(KERN_JS_HELPER_PY);
2164
+ return `(not _kern_truthy(${arg}))`;
2165
+ }
2166
+ if (node.op === '-' && node.argument.kind === 'numLit' && Number(node.argument.raw) === 0)
2167
+ return '-0.0';
1959
2168
  if (node.op === '-')
1960
2169
  return `-${wrapped}`;
1961
2170
  if (node.op === '+')
@@ -1999,7 +2208,12 @@ function emitPyExprCtx(node, ctx) {
1999
2208
  return emitted;
2000
2209
  }
2001
2210
  };
2002
- return `${wrap(node.consequent, consStr)} if ${wrap(node.test, testStr)} else ${wrap(node.alternate, altStr)}`;
2211
+ // Slice S4 the ternary condition consumes KERN ToBoolean exactly once, so
2212
+ // `{} ? a : b`/`[] ? a : b` take the consequent and `NaN ? a : b` takes the
2213
+ // alternate. `_kern_truthy(...)` already parenthesizes the test, so no extra
2214
+ // `wrap` is needed on it. Wrap unconditionally (no "looks boolean" skip).
2215
+ ctx.helpers.add(KERN_JS_HELPER_PY);
2216
+ return `${wrap(node.consequent, consStr)} if _kern_truthy(${testStr}) else ${wrap(node.alternate, altStr)}`;
2003
2217
  }
2004
2218
  case 'spread':
2005
2219
  return `*${emitPyExprCtx(node.argument, ctx)}`;
@@ -2010,6 +2224,39 @@ function emitPyExprCtx(node, ctx) {
2010
2224
  throw new Error(`emitPyExpression: unsupported expression kind '${node.kind ?? 'unknown'}'.`);
2011
2225
  }
2012
2226
  function emitPyTypeof(argument, ctx) {
2227
+ // Round-6 fix — `typeof <bare host-namespace root>` fails-close on BOTH targets.
2228
+ // The round-5 carve-out lowered `typeof RegExp` to the constant `"function"`
2229
+ // on the theory that it was byte-identical to TS's native `typeof RegExp`. But
2230
+ // the carve-out's siblings on the TS/IR legs blanket-accepted EVERY bare
2231
+ // `typeof` operand, re-opening reserved host roots: `typeof Date`/`typeof
2232
+ // process` lowered HERE to a runtime `isinstance` ladder over the Python names
2233
+ // `Date`/`process`, which do NOT exist → NameError, while TS emits the native
2234
+ // `typeof Date`. That is a genuine TS↔Python divergence, so `typeof <host root>`
2235
+ // must fail-close. `RegExp` keeps the shared regex message (matching the
2236
+ // bare-value reject + the TS/IR legs); every other reserved host root takes the
2237
+ // generic host message with a synthetic `typeof` member. A non-host operand
2238
+ // (`typeof userLocal`, `typeof undeclaredFeatureFlag`) and a proven user binding
2239
+ // fall through to the runtime ladder below, unchanged. `typeof RegExp.prototype`
2240
+ // is a `member` operand and still fails-close via the generic guards.
2241
+ //
2242
+ // Round-7 fix — a WRAPPED operand (`typeof (Date as any)`, `typeof (Date!)`)
2243
+ // arrived as `typeAssert`/`nonNull`, NOT an `ident`, so the round-6 reject was
2244
+ // bypassed and Python lowered a runtime Date/process lookup (NameError) while
2245
+ // the (now-corrected) TS leg emits `typeof Date`. Peel the transparent wrappers
2246
+ // via the round-5 `unwrapTransparentReceiverIR` (fixpoint over
2247
+ // `typeAssert`/`nonNull`) and apply the host-root reject to the UNWRAPPED
2248
+ // operand, identically to the TS-emit + IR-validate legs. The runtime-ladder
2249
+ // fall-through below still operates on the ORIGINAL `argument`, so an accepted
2250
+ // wrapped operand (`typeof (userLocal as any)`) emits the wrapper's value
2251
+ // unchanged.
2252
+ const typeofOperand = unwrapTransparentReceiverIR(argument);
2253
+ if (typeofOperand.kind === 'ident' && !isProvenUserBinding(ctx, typeofOperand.name)) {
2254
+ if (typeofOperand.name === 'RegExp')
2255
+ throw new Error(REGEX_HOST_REGEXP_FAILCLOSE);
2256
+ if (isHostNamespaceRoot(typeofOperand.name)) {
2257
+ throw new Error(unmappedHostNamespaceMessage('Python', typeofOperand.name, 'typeof'));
2258
+ }
2259
+ }
2013
2260
  switch (argument.kind) {
2014
2261
  case 'strLit':
2015
2262
  return '"string"';
@@ -2042,6 +2289,17 @@ function emitPyTypeof(argument, ctx) {
2042
2289
  // pre-slice None-first form.
2043
2290
  if (ctx.coerceJsValues) {
2044
2291
  ctx.helpers.add(KERN_FMT_HELPER_PY);
2292
+ if (ctx.banWalrus) {
2293
+ // Comprehension-iterable position — walrus is a SyntaxError here; bind
2294
+ // the operand as a lambda parameter instead (single-eval preserved).
2295
+ return (`(lambda ${tmp}: ("undefined" if ${tmp} is _KERN_UNDEFINED ` +
2296
+ `else "object" if ${tmp} is None ` +
2297
+ `else "boolean" if isinstance(${tmp}, bool) ` +
2298
+ `else "number" if isinstance(${tmp}, (int, float)) ` +
2299
+ `else "string" if isinstance(${tmp}, str) ` +
2300
+ `else "function" if callable(${tmp}) ` +
2301
+ `else "object"))(${value})`);
2302
+ }
2045
2303
  return (`("undefined" if (${tmp} := ${wrapped}) is _KERN_UNDEFINED ` +
2046
2304
  `else "object" if ${tmp} is None ` +
2047
2305
  `else "boolean" if isinstance(${tmp}, bool) ` +
@@ -2050,6 +2308,15 @@ function emitPyTypeof(argument, ctx) {
2050
2308
  `else "function" if callable(${tmp}) ` +
2051
2309
  `else "object")`);
2052
2310
  }
2311
+ if (ctx.banWalrus) {
2312
+ // Comprehension-iterable position — see BodyEmitContext.banWalrus.
2313
+ return (`(lambda ${tmp}: ("object" if ${tmp} is None ` +
2314
+ `else "boolean" if isinstance(${tmp}, bool) ` +
2315
+ `else "number" if isinstance(${tmp}, (int, float)) ` +
2316
+ `else "string" if isinstance(${tmp}, str) ` +
2317
+ `else "function" if callable(${tmp}) ` +
2318
+ `else "object"))(${value})`);
2319
+ }
2053
2320
  return (`("object" if (${tmp} := ${wrapped}) is None ` +
2054
2321
  `else "boolean" if isinstance(${tmp}, bool) ` +
2055
2322
  `else "number" if isinstance(${tmp}, (int, float)) ` +
@@ -2082,12 +2349,12 @@ function emitLambdaPy(node, ctx) {
2082
2349
  *
2083
2350
  * The closure body is lowered through `lowerJsClosureBodyToPython`, reusing
2084
2351
  * the class-path expression/condition callbacks:
2085
- * - `lowerExpression(raw)` = `emitPyExprCtx(parseExpression(raw), ctx)` —
2352
+ * - `lowerExpression(raw)` = `emitPyExprCtx(parseExpr(raw), ctx)` —
2086
2353
  * identical to every other native-body expression emit, so a captured
2087
2354
  * RENAMED outer variable resolves through `ctx` (the rename stack /
2088
2355
  * symbolMap) exactly as it does outside the closure.
2089
2356
  * - `lowerCondition(raw)` mirrors the class/native if-emitter, which lowers a
2090
- * condition as the bare `emitPyExprCtx(parseExpression(cond), ctx)` (NO
2357
+ * condition as the bare `emitPyExprCtx(parseExpr(cond), ctx)` (NO
2091
2358
  * js_truthy wrapper). Matching it EXACTLY means a condition inside a
2092
2359
  * closure lowers identically to the same condition outside one.
2093
2360
  *
@@ -2101,13 +2368,27 @@ function emitBlockClosurePy(node, names, ctx) {
2101
2368
  const previous = new Set(ctx.shadowedSymbols);
2102
2369
  for (const name of names)
2103
2370
  ctx.shadowedSymbols.add(name);
2371
+ // Slice 2 review-fix parity (round 3) — a block-LOCAL `const`/`let`/function/
2372
+ // class shadows any outer binding of that name WITHIN ITS OWN BLOCK (and
2373
+ // nested blocks) only, so a host-`RegExp` value guard
2374
+ // (`rejectHostRegExpValuePython`) must fire on an OUTER reference but NOT on an
2375
+ // in-scope one. The old code registered EVERY declared name (incl. names
2376
+ // declared only in a NESTED block) into `shadowedSymbols` for the WHOLE
2377
+ // closure — fail-OPEN on `() => { if (ok) { const RegExp = 1; } return RegExp; }`
2378
+ // (the outer `return RegExp` was wrongly treated as the local), DIVERGING from
2379
+ // the TS leg. The lowerer FLATTENS nested blocks, so it now reports block
2380
+ // boundaries via `enterBlockScope`/`exitBlockScope`; we push/pop block-local
2381
+ // shadows exactly like the TS-AST closure walk's `scopes` stack. Only names
2382
+ // NOT already shadowed are pushed/popped, so an outer binding of the same name
2383
+ // survives the inner block's pop.
2384
+ const blockScopeAdded = [];
2104
2385
  try {
2105
2386
  const lowered = lowerJsClosureBodyToPython(node.bodyBlock.raw, {
2106
- lowerExpression: (raw) => emitPyExprCtx(parseExpression(raw), ctx),
2387
+ lowerExpression: (raw) => emitPyExprCtx(parseExpr(raw), ctx),
2107
2388
  // Mirror the native/class if-emitter EXACTLY (bare expression, no
2108
2389
  // js_truthy) so a condition inside the closure matches the same
2109
2390
  // condition outside it.
2110
- lowerCondition: (raw) => emitPyExprCtx(parseExpression(raw), ctx),
2391
+ lowerCondition: (raw) => emitPyExprCtx(parseExpr(raw), ctx),
2111
2392
  // The closure's own params are def-locals, never `nonlocal`: a write to a
2112
2393
  // param (`(x) => { x = x + 1 }`) must not be reported as a written FREE
2113
2394
  // name. The lowerer excludes both params and block-locals.
@@ -2118,6 +2399,25 @@ function emitBlockClosurePy(node, names, ctx) {
2118
2399
  // wrong-values without this). Params/block-locals are never renamed, so
2119
2400
  // the resolver is identity for them.
2120
2401
  lowerAssignTarget: (name) => resolveLocalRename(ctx, name),
2402
+ // Block-scope shadowing — push a block's top-level locals on entry, pop on
2403
+ // exit, so a `RegExp` value reference fails-close ONLY when no in-scope
2404
+ // block-local/param shadows it (byte-aligned with the TS-AST walk).
2405
+ enterBlockScope: (scopeNames) => {
2406
+ const added = new Set();
2407
+ for (const name of scopeNames) {
2408
+ if (!ctx.shadowedSymbols.has(name)) {
2409
+ ctx.shadowedSymbols.add(name);
2410
+ added.add(name);
2411
+ }
2412
+ }
2413
+ blockScopeAdded.push(added);
2414
+ },
2415
+ exitBlockScope: () => {
2416
+ const added = blockScopeAdded.pop();
2417
+ if (added)
2418
+ for (const name of added)
2419
+ ctx.shadowedSymbols.delete(name);
2420
+ },
2121
2421
  });
2122
2422
  if (!lowered.ok) {
2123
2423
  // The commit-A gate already accepted this block, so a lowering failure
@@ -2343,12 +2643,99 @@ function containsLambdaCapturingIdent(node, name) {
2343
2643
  return false;
2344
2644
  }
2345
2645
  }
2646
+ /** Slice S7 — the presence test an optional `?.`/`?.[]` link contributes to the
2647
+ * accumulated chain guard. Native (coerce) bodies test against the full nullish
2648
+ * set (`None` AND the undefined sentinel) so `undefined?.x` short-circuits; the
2649
+ * helper-less Ground/React layer keeps the pre-slice `is not None` test (it
2650
+ * never materializes the sentinel). `ref` may itself be a walrus assignment
2651
+ * expression (`(__k_oc1 := f())`), which Python evaluates exactly once. */
2652
+ function optionalPresenceTest(ref, ctx) {
2653
+ if (ctx.coerceJsValues) {
2654
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
2655
+ return `(not _kern_is_nullish(${ref}))`;
2656
+ }
2657
+ // `x := f() is not None` would bind the BOOLEAN to x; parenthesize a walrus
2658
+ // ref so the receiver (not the comparison) is what gets bound.
2659
+ const wrapped = ref.includes(':=') ? `(${ref})` : ref;
2660
+ return `${wrapped} is not None`;
2661
+ }
2662
+ /** Slice S7 — build the guard + branch-receiver reference for one optional link.
2663
+ * Pure receivers keep the readable double-name form (named in both the guard
2664
+ * and the branch — re-evaluation is side-effect-free). A NON-pure receiver is
2665
+ * bound ONCE via the S4 walrus idiom: the guard carries `(__k_ocN := RECV)` so
2666
+ * the side effect runs exactly once, and the branch references the bound name.
2667
+ * A non-pure receiver that is ITSELF an optional chain (`inner.guard !== null`)
2668
+ * cannot host the walrus and still throws — bind it to a `let` first. */
2669
+ function lowerOptionalLink(inner, objectNode, ctx) {
2670
+ const pure = isReceiverChainPure(objectNode);
2671
+ if (pure) {
2672
+ const presence = optionalPresenceTest(inner.expr, ctx);
2673
+ return {
2674
+ guard: inner.guard === null ? presence : `${inner.guard} and ${presence}`,
2675
+ branchRef: inner.expr,
2676
+ };
2677
+ }
2678
+ if (inner.guard !== null) {
2679
+ throw new Error("Optional chain '?.' on Python target requires a side-effect-free receiver (identifier or pure member chain). " +
2680
+ 'Bind the call/await result to a `let` first, then use `let.field?.next` on the bound name.');
2681
+ }
2682
+ const tmp = `__k_oc${++ctx.gensymCounter}`;
2683
+ if (ctx.banWalrus) {
2684
+ // Comprehension-iterable position (see BodyEmitContext.banWalrus): CPython
2685
+ // rejects the `:=` walrus anywhere inside a comprehension iterable, so the
2686
+ // non-pure receiver cannot be bound via `(__k_ocN := RECV)`. Bind it as a
2687
+ // lambda PARAMETER instead — the presence test + branch reference the bare
2688
+ // `__k_ocN` (the parameter), and `wrapGuardIfAny` wraps the whole guarded
2689
+ // conditional in `(lambda __k_ocN: <conditional>)(RECV)`. Same contract as
2690
+ // the walrus form: RECV evaluated exactly once (as the call argument), the
2691
+ // short-circuit branch yields the sentinel, single-eval preserved.
2692
+ const presence = optionalPresenceTest(tmp, ctx);
2693
+ return { guard: presence, branchRef: tmp, lambdaBind: { param: tmp, arg: inner.expr } };
2694
+ }
2695
+ const presence = optionalPresenceTest(`${tmp} := ${inner.expr}`, ctx);
2696
+ return { guard: presence, branchRef: tmp };
2697
+ }
2346
2698
  function lowerChain(node, ctx) {
2347
2699
  if (node.kind === 'member') {
2348
2700
  const obj = node.object;
2701
+ // Slice 2 — a bare property READ on a DIRECT regex LITERAL (`/x/.source`,
2702
+ // `/x/.flags`) launders the pattern/flags back into a string. Routed through
2703
+ // the SHARED classifier (via the ValueIR adapter) so this site agrees with
2704
+ // the TS emit + IR-validate legs and the closure walk BY CONSTRUCTION. The
2705
+ // portable METHODS (.test/.exec/…) are CALLS routed by the call path before
2706
+ // reaching here, and a LET-BOUND regex read (`r.source`) has an `ident`
2707
+ // object (owned by Slice 3), so only a direct literal read is closed here.
2708
+ // The receiver is UNWRAPPED first so a wrapped read `(/x/ as any).source` /
2709
+ // `(/x/!).source` fails-close identically to the bare `/x/.source` and to the
2710
+ // TS-emit + IR-validate legs (round-5 wrapped-receiver fix).
2711
+ if (regexLiteralReceiverIR(obj) !== null) {
2712
+ const message = classifyRegexLiteralMemberReadFailClose(node);
2713
+ if (message !== null)
2714
+ throw new Error(message);
2715
+ }
2716
+ const stdlibProperty = applyStdlibPropertyLoweringPython(node, ctx);
2717
+ if (stdlibProperty !== null)
2718
+ return { guard: null, expr: stdlibProperty };
2719
+ // Slice H — fail-closed on an UNMAPPED host-namespace member READ. Covers
2720
+ // host CONSTANT reads such as `Math.PI` / `process.env` that are not a call
2721
+ // (the call path guards `Root.member(args)` separately, before descending
2722
+ // here). Only a DIRECT `Root.member` read is inspected: `obj.kind ===
2723
+ // 'ident'`. A host-namespace-shaped, not-user-bound root throws; user
2724
+ // receivers (`user.profile`) and proven-local roots pass through. A host
2725
+ // CALL's callee never reaches this read guard with a host root — the call
2726
+ // path already threw — so this only ever fires on genuine reads.
2727
+ if (obj.kind === 'ident') {
2728
+ rejectUnmappedHostNamespacePython(obj.name, node.property, ctx);
2729
+ }
2730
+ // S5 review fix — a NON-CHAIN root that lowers to a compound expression
2731
+ // must be parenthesized before `.${property}` is appended: a bare ternary
2732
+ // root (`b if t else c`) would otherwise bind the member to its LAST
2733
+ // operand only (`b if t else c.prop` — silently wrong Python). Same
2734
+ // precedence set as index receivers; lowerings that already emit a
2735
+ // self-delimited atom (`(walrus ternary)`, `__kern_add(a, b)`) stay bare.
2349
2736
  const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
2350
2737
  ? lowerChain(obj, ctx)
2351
- : { guard: null, expr: emitPyExprCtx(obj, ctx) };
2738
+ : { guard: null, expr: wrapCompoundRootExpr(obj, emitPyExprCtx(obj, ctx)) };
2352
2739
  // Portable Array *property* read (non-call `.length`) lowers through the
2353
2740
  // SAME shared list-ops hook the route emitter uses, so `this.items.length`
2354
2741
  // emits `len(self.items)` (not invalid `self.items.length`) — identical to
@@ -2356,41 +2743,70 @@ function lowerChain(node, ctx) {
2356
2743
  // link is rewritten; the accumulated optional-chain guard is left UNTOUCHED
2357
2744
  // and still flows through `wrapGuardIfAny`, so `items?.length` stays
2358
2745
  // `(len(items) if items is not None else None)`-shaped.
2746
+ if (node.optional) {
2747
+ // Slice S7 — single-eval optional `?.`. A pure receiver may be named twice
2748
+ // (guard + branch) with no observable effect, so it keeps the readable
2749
+ // double-name form. A NON-pure receiver (e.g. `markReceiver(null)?.x`) is
2750
+ // bound ONCE via the S4 walrus idiom so its side effect runs exactly once;
2751
+ // the BOUND name is then used in both the guard's presence test and the
2752
+ // trailing link. Only a single optional link can host the walrus (the
2753
+ // receiver is not itself an optional chain ⇒ `inner.guard === null`); a
2754
+ // non-pure receiver UNDER an existing optional chain still throws below.
2755
+ const opt = lowerOptionalLink(inner, node.object, ctx);
2756
+ const linkExpr = isSharedPortableArrayProperty(node.property)
2757
+ ? (lowerPortableArrayPropertyPy(opt.branchRef, node.property) ?? `${opt.branchRef}.${node.property}`)
2758
+ : `${opt.branchRef}.${node.property}`;
2759
+ return { guard: opt.guard, expr: linkExpr, lambdaBind: opt.lambdaBind };
2760
+ }
2359
2761
  const linkExpr = isSharedPortableArrayProperty(node.property)
2360
2762
  ? (lowerPortableArrayPropertyPy(inner.expr, node.property) ?? `${inner.expr}.${node.property}`)
2361
2763
  : `${inner.expr}.${node.property}`;
2362
- 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 };
2764
+ return { guard: inner.guard, expr: linkExpr, lambdaBind: inner.lambdaBind };
2374
2765
  }
2375
2766
  if (node.kind === 'index') {
2376
2767
  const obj = node.object;
2768
+ // Slice 2 review fix — the bracket (`index`) form of a regex-literal
2769
+ // property access (`/x/["source"]`, `/x/["flags"]`, `/x/["test"](s)`)
2770
+ // launders the pattern/flags back to a string exactly like the dotted
2771
+ // `/x/.source` member form, so it fails-close identically and BEFORE the
2772
+ // host-ident guard below. A STRING-literal index goes through the same
2773
+ // (empty) portable-property allowlist; a COMPUTED / non-literal index is
2774
+ // unknowable and also fails-close. Mirrors the TS emit + IR-validate index
2775
+ // screens — byte-identical regex message across all three legs. The receiver
2776
+ // is UNWRAPPED first so a wrapped bracket read `(/x/!)["source"]` /
2777
+ // `(/x/ as any)["test"](s)` fails-close identically (round-5 wrapped fix).
2778
+ if (regexLiteralReceiverIR(obj) !== null) {
2779
+ // Routed through the SHARED classifier (a STRING index classifies like the
2780
+ // dotted read; a COMPUTED index is unknowable → fail-close), byte-identical
2781
+ // to the TS emit + IR-validate index screens and the closure walk.
2782
+ const message = classifyRegexLiteralIndexReadFailClose(node);
2783
+ if (message !== null)
2784
+ throw new Error(message);
2785
+ }
2786
+ // Slice H review fix — bracket access must not bypass the fail-closed
2787
+ // guard: `Math["sqrt"]` / `Math["sqrt"](x)` is the same unmapped
2788
+ // host-namespace access as `Math.sqrt`, only spelled as an index node.
2789
+ // Call chains descend through this branch too, so this one site covers
2790
+ // both the read and the call form. Non-literal keys (`Math[k]`) are just
2791
+ // as unmapped — the label degrades to `[computed]`.
2792
+ if (obj.kind === 'ident') {
2793
+ const label = node.index.kind === 'strLit' ? node.index.value : '[computed]';
2794
+ rejectKnownStdlibIndexPython(obj.name, label);
2795
+ rejectUnmappedHostNamespacePython(obj.name, label, ctx);
2796
+ }
2377
2797
  const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
2378
2798
  ? lowerChain(obj, ctx)
2379
2799
  : { guard: null, expr: emitPyExprCtx(obj, ctx) };
2380
2800
  const index = emitPyExprCtx(node.index, ctx);
2381
2801
  if (node.optional) {
2382
- // 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}]` };
2802
+ // Slice S7 single-eval optional `?.[]` (same walrus discipline as `?.`).
2803
+ // A non-pure receiver is bound once; the index expression appears only in
2804
+ // the selected branch, matching JS `?.[]`.
2805
+ const opt = lowerOptionalLink(inner, node.object, ctx);
2806
+ return { guard: opt.guard, expr: `${opt.branchRef}[${index}]`, lambdaBind: opt.lambdaBind };
2391
2807
  }
2392
2808
  const wrapped = needsIndexReceiverParens(node.object) ? `(${inner.expr})` : inner.expr;
2393
- return { guard: inner.guard, expr: `${wrapped}[${index}]` };
2809
+ return { guard: inner.guard, expr: `${wrapped}[${index}]`, lambdaBind: inner.lambdaBind };
2394
2810
  }
2395
2811
  // node.kind === 'call'
2396
2812
  if (node.optional) {
@@ -2441,12 +2857,33 @@ function lowerChain(node, ctx) {
2441
2857
  const errArgs = node.args.map((arg) => emitPyExprCtx(arg, ctx)).join(', ');
2442
2858
  return { guard: null, expr: `Exception(${errArgs})` };
2443
2859
  }
2860
+ // Slice 2 — a bare `RegExp(p, f)` call (callee is an ident, missed by the
2861
+ // member-callee guard below) fails-close with the shared regex message,
2862
+ // BEFORE generic call emission would leak a verbatim `RegExp(...)` NameError.
2863
+ // Honors user shadowing via `isProvenUserBinding`.
2864
+ if (node.callee.kind === 'ident') {
2865
+ rejectHostRegExpValuePython(node.callee.name, ctx);
2866
+ }
2867
+ // Slice H — fail-closed on an UNMAPPED host-namespace member CALL. This runs
2868
+ // AFTER every explicit lowering hook above (stdlib, regex, lambda/array,
2869
+ // portable-array, super/String/Error) and BEFORE generic call emission, so a
2870
+ // `Root.member(args)` call whose root is host-namespace-shaped and not proven
2871
+ // user-bound throws the portable-lowering diagnostic instead of leaking
2872
+ // invalid verbatim Python (`Math.sqrt(x)` → runtime NameError). Canonical
2873
+ // KERN stdlib roots (`Number`/`Json`/…) already returned via the stdlib hook,
2874
+ // so they never reach here; user receivers (`client.send(...)`) and a
2875
+ // proven-local `Math` are not host-namespace-shaped / are user-bound and pass
2876
+ // through. Capitalization-agnostic: equally catches `console.log(...)`,
2877
+ // `Promise.all(...)`, and lowercase host roots such as `process.env`.
2878
+ if (node.callee.kind === 'member' && node.callee.object.kind === 'ident') {
2879
+ rejectUnmappedHostNamespacePython(node.callee.object.name, node.callee.property, ctx);
2880
+ }
2444
2881
  const callee = node.callee;
2445
2882
  const inner = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index'
2446
2883
  ? lowerChain(callee, ctx)
2447
2884
  : { guard: null, expr: emitPyExprCtx(callee, ctx) };
2448
2885
  const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', ');
2449
- return { guard: inner.guard, expr: `${inner.expr}(${args})` };
2886
+ return { guard: inner.guard, expr: `${inner.expr}(${args})`, lambdaBind: inner.lambdaBind };
2450
2887
  }
2451
2888
  /**
2452
2889
  * Lower a portable Array *method call* (e.g. `arr.push(x)`) through the shared
@@ -2482,18 +2919,32 @@ function lowerPortableArrayCallPython(call, ctx) {
2482
2919
  // finding). The optional-chain guard below still applies to ALL methods.
2483
2920
  if (sharedPortableMethodRequiresPureReceiver(callee.property) && !isReceiverChainPure(recvNode))
2484
2921
  return null;
2485
- const recv = recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
2922
+ // S5 review fix these list-ops lowerings embed the receiver in a
2923
+ // comprehension/generator ITERABLE (`join`: `str(__v) for __v in <recv>`;
2924
+ // `flat`: `for __x in <recv>`; `indexOf`: `enumerate(<recv>)` inside a
2925
+ // genexp), where CPython rejects `:=` outright. Emit the receiver under the
2926
+ // walrus ban AND parenthesize compound receivers (a bare ternary in a
2927
+ // generator head reads its `if` as the comprehension filter — SyntaxError).
2928
+ const embedsReceiverInGenexp = callee.property === 'join' || callee.property === 'flat' || callee.property === 'indexOf';
2929
+ const emitRecv = () => recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
2486
2930
  ? lowerChain(recvNode, ctx)
2487
2931
  : { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
2932
+ const recv = embedsReceiverInGenexp ? withWalrusBan(ctx, emitRecv) : emitRecv();
2488
2933
  // A pure receiver can still be an optional chain (`a?.b`), which carries a
2489
2934
  // None-guard the flat shim can't honor — fall through for those too.
2490
2935
  if (recv.guard !== null)
2491
2936
  return null;
2937
+ const recvExpr = embedsReceiverInGenexp ? parenthesizeIterable(recv.expr) : recv.expr;
2492
2938
  const args = call.args.map((a) => (callee.property === 'fill' ? emitPyArrayFillArg(a, ctx) : emitPyExprCtx(a, ctx)));
2493
- const lowered = lowerPortableArrayMethodPy(recv.expr, callee.property, args);
2939
+ const lowered = lowerPortableArrayMethodPy(recvExpr, callee.property, args, { sentinelMiss: ctx.coerceJsValues });
2494
2940
  if (lowered !== null && callee.property === 'fill') {
2495
2941
  ctx.helpers.add(KERN_JS_ARRAY_HELPERS_PY);
2496
2942
  }
2943
+ // Slice S7 — `Array.at` out-of-range yields the undefined sentinel in value
2944
+ // mode; register the helper that defines `_KERN_UNDEFINED`.
2945
+ if (lowered !== null && callee.property === 'at' && ctx.coerceJsValues) {
2946
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
2947
+ }
2497
2948
  return lowered;
2498
2949
  }
2499
2950
  function emitPyArrayFillArg(node, ctx) {
@@ -2636,9 +3087,13 @@ function lowerLambdaArrayCallPython(call, ctx) {
2636
3087
  // single-eval property is what makes M6 (`this.bump().map(...)` runs bump()
2637
3088
  // exactly once) correct.
2638
3089
  const recvNode = callee.object;
2639
- const recv = recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
3090
+ // S5 review fix the receiver lands in the comprehension's generator head
3091
+ // (`for el in <recv>`), where CPython rejects `:=` at any nesting depth, so
3092
+ // it is emitted under the walrus ban (logical/nullish/typeof producers at
3093
+ // any depth switch to their lambda-parameter forms).
3094
+ const recv = withWalrusBan(ctx, () => recvNode.kind === 'member' || recvNode.kind === 'call' || recvNode.kind === 'index'
2640
3095
  ? lowerChain(recvNode, ctx)
2641
- : { guard: null, expr: emitPyExprCtx(recvNode, ctx) };
3096
+ : { guard: null, expr: emitPyExprCtx(recvNode, ctx) });
2642
3097
  // An optional-chain receiver (`a?.b`) carries a None-guard the comprehension
2643
3098
  // can't honor — fall through unchanged for those.
2644
3099
  if (recv.guard !== null)
@@ -2839,43 +3294,196 @@ function lowerRegexCallPython(call, ctx) {
2839
3294
  const callee = call.callee;
2840
3295
  if (callee.kind !== 'member')
2841
3296
  return null;
3297
+ // Slice-3b FIX 4 (parity): a call whose receiver is a KERN-stdlib NAMESPACE
3298
+ // (`Math.match(/a/g)`, `JSON.split(/,/)`) is NOT a string regex method —
3299
+ // defer to `applyStdlibLoweringPython`, which rejects the unknown stdlib
3300
+ // member exactly like the TS emitter (where `applyStdlibLoweringTS` runs
3301
+ // BEFORE the regex lowering). Without this, the regex path mis-claimed the
3302
+ // namespace as the SUBJECT and emitted broken `…finditer("a", Math, …)`
3303
+ // while TS fail-closed — a cross-target divergence.
3304
+ if (callee.object.kind === 'ident' && KERN_STDLIB_MODULES.has(callee.object.name)) {
3305
+ return null;
3306
+ }
3307
+ // Slice-3c DETECT-and-fail-close: a regex method whose regex position holds an
3308
+ // ident KNOWN to be regex-bound (`let re = /…/; s.match(re)`) is NOT portable —
3309
+ // throw the SAME shared `REGEX_NONLITERAL_FAILCLOSE` the TS target throws (see
3310
+ // `assertNoBoundRegexMethodTS` in body-ts.ts). A string-/unknown-bound ident is
3311
+ // NOT flagged: it falls through to the plain host method below (`s.match(needle)`).
3312
+ const regexArgIdent = regexMethodRegexArgIdent(call);
3313
+ if (regexArgIdent !== null && lookupRegexBinding(ctx, regexArgIdent) !== null) {
3314
+ throw new Error(REGEX_NONLITERAL_FAILCLOSE);
3315
+ }
3316
+ // --- Receiver-is-regex shapes: `regex.test(s)`, `regex.exec(s)` ---
2842
3317
  const receiverRegex = resolveRegexExpr(callee.object, ctx);
2843
3318
  if (callee.property === 'test' && receiverRegex !== null) {
2844
3319
  if (call.args.length !== 1)
2845
3320
  return null;
2846
3321
  if (receiverRegex.flags.includes('g')) {
2847
- 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.");
3322
+ // .test(/g) is STATEFUL (lastIndex advances+wraps); no portable re analog.
3323
+ throw new Error(REGEX_TEST_G_FAILCLOSE);
2848
3324
  }
2849
3325
  ctx.imports.add('re');
2850
3326
  return `(__k_re.search(${pyRegexPattern(receiverRegex)}, ${emitPyExprCtx(call.args[0], ctx)}, ${pyRegexFlags(receiverRegex.flags)}) is not None)`;
2851
3327
  }
2852
- 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.');
3328
+ if (callee.property === 'exec' && receiverRegex !== null) {
3329
+ // .exec drives a JS-only stateful `while ((m = re.exec(s)))` loop; fail-close
3330
+ // and redirect to the portable `.matchAll` iteration (D4) rather than silently
3331
+ // rewrite a loop whose body may mutate lastIndex.
3332
+ throw new Error(REGEX_EXEC_FAILCLOSE);
3333
+ }
3334
+ // --- Receiver-is-string shapes: arg[0] is the regex ---
3335
+ const firstArgRegex = call.args.length >= 1 ? resolveRegexExpr(call.args[0], ctx) : null;
3336
+ // `.match(s)` — no /g: canonical {full,groups,index,named}|None shape (the
3337
+ // load-bearing portability fix, D2). With /g: full matches only via
3338
+ // finditer.group(0) (NEVER re.findall, which returns tuples when >1 group),
3339
+ // or None when empty.
3340
+ if (callee.property === 'match' && firstArgRegex !== null && call.args.length === 1) {
3341
+ ctx.imports.add('re');
3342
+ const pat = pyRegexPattern(firstArgRegex);
3343
+ const subject = emitPyExprCtx(callee.object, ctx);
3344
+ if (firstArgRegex.flags.includes('g')) {
3345
+ const flags = pyRegexFlags(firstArgRegex.flags, { allowGlobal: true });
3346
+ return `([__k_m.group(0) for __k_m in __k_re.finditer(${pat}, ${subject}, ${flags})] or None)`;
3347
+ }
3348
+ ctx.helpers.add(KERN_REGEX_MATCH_HELPER_PY);
3349
+ return `_kern_regex_match(${pat}, ${subject}, ${pyRegexFlags(firstArgRegex.flags)})`;
3350
+ }
3351
+ // `.matchAll(s)` — requires /g (a non-global matchAll throws TypeError in JS).
3352
+ // Shapes finditer into [{full,groups,index}, …], incl. zero-width advances.
3353
+ if (callee.property === 'matchAll' && firstArgRegex !== null && call.args.length === 1) {
3354
+ if (!firstArgRegex.flags.includes('g')) {
3355
+ throw new Error(REGEX_MATCHALL_NO_G_FAILCLOSE);
3356
+ }
3357
+ ctx.imports.add('re');
3358
+ ctx.helpers.add(KERN_REGEX_MATCHALL_HELPER_PY);
3359
+ return `_kern_regex_matchall(${pyRegexPattern(firstArgRegex)}, ${emitPyExprCtx(callee.object, ctx)}, ${pyRegexFlags(firstArgRegex.flags, { allowGlobal: true })})`;
3360
+ }
3361
+ // `.split(s)` — IN-CORE for a non-zero-width pattern with NO limit arg (capture
3362
+ // groups interleave portably). FAIL-CLOSE on a zero-width-capable pattern
3363
+ // (empty-edge divergence) or any limit/2nd arg (truncate vs remainder).
3364
+ if (callee.property === 'split' && firstArgRegex !== null) {
3365
+ if (call.args.length > 1) {
3366
+ throw new Error(REGEX_SPLIT_LIMIT_FAILCLOSE);
3367
+ }
3368
+ if (isZeroWidthCapableRegex(firstArgRegex.pattern)) {
3369
+ throw new Error(REGEX_SPLIT_ZEROWIDTH_FAILCLOSE);
2856
3370
  }
2857
3371
  ctx.imports.add('re');
2858
- return `__k_re.search(${pyRegexPattern(matchRegex)}, ${emitPyExprCtx(callee.object, ctx)}, ${pyRegexFlags(matchRegex.flags)})`;
3372
+ return `__k_re.split(${pyRegexPattern(firstArgRegex)}, ${emitPyExprCtx(callee.object, ctx)}, flags=${pyRegexFlags(firstArgRegex.flags, { allowGlobal: true })})`;
2859
3373
  }
3374
+ // `.replace(s, r)` — no /g: FIRST match only (count=1); /g: ALL (count=0).
2860
3375
  const replaceRegex = call.args.length === 2 ? resolveRegexExpr(call.args[0], ctx) : null;
2861
3376
  if (callee.property === 'replace' && replaceRegex !== null) {
2862
3377
  ctx.imports.add('re');
2863
3378
  const count = replaceRegex.flags.includes('g') ? '0' : '1';
2864
- return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${emitPyExprCtx(call.args[1], ctx)}, ${emitPyExprCtx(callee.object, ctx)}, count=${count}, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
3379
+ const repl = emitPyReplArg(call.args[1], replaceRegex, ctx);
3380
+ return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${repl}, ${emitPyExprCtx(callee.object, ctx)}, count=${count}, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
3381
+ }
3382
+ // `.replaceAll(s, r)` — requires /g (a non-global replaceAll throws TypeError in
3383
+ // JS); otherwise identical to `.replace` /g (count=0, ALL matches replaced).
3384
+ if (callee.property === 'replaceAll' && replaceRegex !== null) {
3385
+ if (!replaceRegex.flags.includes('g')) {
3386
+ throw new Error(REGEX_REPLACEALL_NO_G_FAILCLOSE);
3387
+ }
3388
+ ctx.imports.add('re');
3389
+ const repl = emitPyReplArg(call.args[1], replaceRegex, ctx);
3390
+ return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${repl}, ${emitPyExprCtx(callee.object, ctx)}, count=0, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
2865
3391
  }
2866
3392
  return null;
2867
3393
  }
2868
- 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;
3394
+ /**
3395
+ * Milestone C, Slice 4 — emit the Python `re.sub` REPLACEMENT argument for a
3396
+ * `.replace`/`.replaceAll` whose regex position is a literal.
3397
+ *
3398
+ * A STRING-LITERAL replacement is translated from the JS `$`-surface to the
3399
+ * Python repl VALUE via the shared `translateReplStringToPython`, then serialized
3400
+ * back to `.py` SOURCE through a synthetic `strLit` node so the ordinary
3401
+ * string-literal escaper double-escapes the backslashes correctly (gap 6: the
3402
+ * translator yields the runtime VALUE `\g<1>` / `\\` — the serializer turns those
3403
+ * into the source `"\\g<1>"` / `"\\\\"` that evaluates back to that value). A
3404
+ * NON-LITERAL replacement (a variable / computed string) cannot be translated
3405
+ * statically and FAILS-CLOSE symmetrically with the TS target.
3406
+ */
3407
+ function emitPyReplArg(arg, replaceRegex, ctx) {
3408
+ if (arg.kind !== 'strLit') {
3409
+ throw new Error(REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE);
3410
+ }
3411
+ // Capture metadata is read from the KERN/JS `(?<name>)` surface (BEFORE R6's
3412
+ // `(?P<name>)` rewrite), matching the way `$<name>` refs are written.
3413
+ const meta = regexCaptureMeta(replaceRegex.pattern);
3414
+ const pyReplValue = translateReplStringToPython(arg.value, meta);
3415
+ // Serialize the translated VALUE to `.py` source via the normal strLit path
3416
+ // (gap 6 — keep translation and serialization layers separate).
3417
+ const synthetic = { kind: 'strLit', value: pyReplValue, quote: '"' };
3418
+ return emitPyExprCtx(synthetic, ctx);
3419
+ }
3420
+ function resolveRegexExpr(node, _ctx) {
3421
+ // Slice-3c: ONLY a DIRECT regex literal lowers canonically. A let-bound regex
3422
+ // ident no longer RESOLVES to its literal here (the old `lookupRegexBinding`
3423
+ // resolution emitted a STALE pattern when the binding was reassigned and was
3424
+ // fragile to track). The fail-close for a known-regex-bound ident is made by
3425
+ // `lowerRegexCallPython` via the shared `regexMethodRegexArgIdent` detector —
3426
+ // symmetric with the TS target. A string-/unknown-bound ident returns null
3427
+ // and stays a plain host method (the `s.match(stringVar)` case).
3428
+ //
3429
+ // Transparent wrappers (`as`/`!`) are peeled via `regexLiteralReceiverIR` —
3430
+ // mirroring the TS `resolveRegexLitTS` — so a wrapped portable receiver call
3431
+ // `(/x/).test(s)` / `(/x/ as any).test(s)` lowers, and a wrapped non-portable
3432
+ // one `(/x/ as any).exec(s)` fails-close through `lowerRegexCallPython` (round-5
3433
+ // wrapped-receiver fix), identically to the TS leg.
3434
+ return regexLiteralReceiverIR(node);
2874
3435
  }
2875
3436
  function pyRegexPattern(node) {
3437
+ // Slice 5: fail-close any non-BMP (astral) construct in the PATTERN before any
3438
+ // normalization, on the RAW pattern — the IDENTICAL decision + message the TS
3439
+ // emitter makes (the `\/`→`/` un-escape below does not touch any astral
3440
+ // construct, so scanning the raw `node.pattern` here is byte-symmetric with TS).
3441
+ const astral = scanRegexAstral(node.pattern);
3442
+ if (astral !== null)
3443
+ throw new Error(regexAstralFailMessage(astral.char));
3444
+ // FIX 2: a non-portable named group (`(?<café>…)`, `(?<$x>…)`, `(?<>…)`) is
3445
+ // refused on BOTH targets (the same validator runs in the TS regex-literal emit
3446
+ // chokepoints), instead of emitting `(?P<café>…)` / a JS-form `(?<café>…)` that
3447
+ // diverges or crashes Python `re`. Run BEFORE any rewrite so the refusal is over
3448
+ // the original surface (the same surface `regexCaptureMeta` and the TS side see).
3449
+ validateRegexNamedGroupsPortable(node.pattern);
2876
3450
  // JS escapes `/` because it delimits the literal; Python string regexes do not
2877
3451
  // treat `/` specially, so preserve the semantic pattern without that escape.
2878
- return JSON.stringify(node.pattern.replace(/\\\//g, '/'));
3452
+ const unescaped = node.pattern.replace(/\\\//g, '/');
3453
+ // Milestone C, Slice 1 — emission-normalization (shared with the TS emitter
3454
+ // for the class transform, so it is byte-identical across targets):
3455
+ // 1. `\d \w \s` → explicit ASCII classes (same `normalizeRegexClasses` both
3456
+ // targets), so Python's Unicode-aware shorthand matches JS's ASCII.
3457
+ // 2. Slice-/i: class-expand non-ASCII Set(A) letters under /i into explicit
3458
+ // fold classes (Set(B) → throw an identical-to-TS compile error). Runs on
3459
+ // the SAME shared `expandRegexIFold` the TS emitter calls, AFTER class
3460
+ // normalization (Slice-1 classes are pure-ASCII → untouched by the fold
3461
+ // scan) and BEFORE anchor lowering (anchors are ASCII → untouched). Order
3462
+ // is class → fold → anchors; each touches a disjoint character set, so it
3463
+ // is parity-safe and byte-identical to TS. `re.IGNORECASE | re.ASCII` is
3464
+ // kept (handled in pyRegexFlags) — `re.ASCII` is the load-bearing invariant
3465
+ // that makes KEEP-i safe (it suppresses any Python re-fold of the explicit
3466
+ // non-ASCII class members).
3467
+ // 3. Python-only anchor lowering: on the non-`/m` path `$`→`\Z`, `^`→`\A`
3468
+ // so Python anchors match JS's input-end/start semantics (`re.ASCII` and
3469
+ // `re.M` are handled in pyRegexFlags). On the `/m` path anchors are kept.
3470
+ const classed = normalizeRegexClasses(unescaped);
3471
+ const folded = expandRegexIFold(classed, node.flags);
3472
+ if ('failClose' in folded)
3473
+ throw new Error(regexIFoldFailMessage(folded.char, folded.reason));
3474
+ const normalized = lowerRegexAnchorsPython(folded.pattern, node.flags);
3475
+ // Milestone C, Slice 4 — R6: named-group PATTERN syntax (`(?<name>)` /
3476
+ // `\k<name>`) → Python `re` syntax (`(?P<name>)` / `(?P=name)`). PYTHON-ONLY
3477
+ // (JS keeps the original form). Python rejects the JS named-group syntax
3478
+ // outright, so this rewrite is load-bearing for ANY named-group pattern on the
3479
+ // Python target — and required for a `$<name>` repl ref to resolve. Run LAST,
3480
+ // AFTER the `/i` fold expansion: `expandRegexIFold`'s HOLE-1 fail-close depends
3481
+ // on seeing the ORIGINAL `\k<name>` backref token (`/(?<g>é)\k<g>/i` must still
3482
+ // fail-close), so R6 must NOT consume `\k<` before the fold scan. The fold/class/
3483
+ // anchor passes leave the ASCII `(?<` / `\k<` group syntax untouched, so applying
3484
+ // R6 here is order-stable and parity-safe.
3485
+ const named = lowerRegexNamedGroupsPython(normalized);
3486
+ return JSON.stringify(named);
2879
3487
  }
2880
3488
  function pyRegexFlags(flags, options = {}) {
2881
3489
  const unsupported = [...flags].filter((f) => {
@@ -2896,10 +3504,125 @@ function pyRegexFlags(flags, options = {}) {
2896
3504
  parts.push('__k_re.MULTILINE');
2897
3505
  if (flags.includes('s'))
2898
3506
  parts.push('__k_re.DOTALL');
2899
- return parts.length > 0 ? parts.join(' | ') : '0';
3507
+ // Milestone C, Slice 1 — always inject `re.ASCII` so Python `\b` and the
3508
+ // emitted ASCII classes match JS (without `/u`) semantics. This is the
3509
+ // load-bearing flag for word-boundary parity (see the `\bcafé\b` killer row).
3510
+ parts.push('__k_re.ASCII');
3511
+ return parts.join(' | ');
3512
+ }
3513
+ function wrapGuardIfAny(g, ctx) {
3514
+ if (g.guard === null)
3515
+ return g.expr;
3516
+ // Slice S7 — an optional-chain short-circuit yields the undefined SENTINEL in
3517
+ // native (coerce) bodies, so `typeof (undefined?.x)` is "undefined" and the
3518
+ // result participates in `??`/`===` nullish semantics. The helper-less
3519
+ // Ground/React layer (which never materializes the sentinel) keeps the
3520
+ // pre-slice `else None` short-circuit.
3521
+ let conditional;
3522
+ if (ctx.coerceJsValues) {
3523
+ ctx.helpers.add(KERN_NULLISH_HELPER_PY);
3524
+ conditional = `(${g.expr} if ${g.guard} else _KERN_UNDEFINED)`;
3525
+ }
3526
+ else {
3527
+ conditional = `(${g.expr} if ${g.guard} else None)`;
3528
+ }
3529
+ // S7 review fix — in a comprehension-iterable position the non-pure receiver was
3530
+ // bound as a lambda PARAMETER instead of a `:=` walrus (CPython rejects the walrus
3531
+ // anywhere inside a comprehension iterable expression — see BodyEmitContext.banWalrus
3532
+ // and lowerOptionalLink). The guard + branch already reference the bare parameter, so
3533
+ // wrapping the whole conditional in `(lambda <param>: <conditional>)(<arg>)` keeps the
3534
+ // exact contract: <arg> (the receiver) evaluated exactly once, lazy short-circuit branch.
3535
+ if (g.lambdaBind) {
3536
+ return `(lambda ${g.lambdaBind.param}: ${conditional})(${g.lambdaBind.arg})`;
3537
+ }
3538
+ return conditional;
2900
3539
  }
2901
- function wrapGuardIfAny(g) {
2902
- return g.guard === null ? g.expr : `(${g.expr} if ${g.guard} else None)`;
3540
+ /** Slice S5 — does the LEFT operand of a `&&`/`||` walrus binding
3541
+ * (`__k_logN := L`) need parentheses?
3542
+ *
3543
+ * The walrus sits inside a `_kern_truthy(...)` call, whose own parens already
3544
+ * disambiguate `L`, so this is defensive/readability wrapping for the lowest-
3545
+ * precedence operand shapes (a `conditional` or `lambda` left operand). A
3546
+ * nested `&&`/`||` left operand is already self-parenthesized by its own
3547
+ * lowering, and an arithmetic/comparison `binary` binds tighter than `:=`, so
3548
+ * neither is wrapped here. */
3549
+ function needsWalrusOperandParens(child) {
3550
+ return child.kind === 'conditional' || child.kind === 'lambda';
3551
+ }
3552
+ /** S5 review fix — run `fn` with `ctx.banWalrus` set (save/restore), for
3553
+ * emitting an operand that will be interpolated into a comprehension/
3554
+ * generator ITERABLE position, where CPython rejects `:=` outright (see
3555
+ * BodyEmitContext.banWalrus). Nested emissions inherit the flag through the
3556
+ * shared ctx, so a walrus producer at ANY depth of the operand (e.g. the
3557
+ * index expression in `items[i || 0]`) switches to its walrus-free form. */
3558
+ function withWalrusBan(ctx, fn) {
3559
+ const previous = ctx.banWalrus === true;
3560
+ ctx.banWalrus = true;
3561
+ try {
3562
+ return fn();
3563
+ }
3564
+ finally {
3565
+ ctx.banWalrus = previous;
3566
+ }
3567
+ }
3568
+ /** Slice S5 — does the RIGHT operand, emitted in the `else` arm of the lowered
3569
+ * conditional expression, need parentheses? A bare `conditional`/`lambda` in
3570
+ * an `else` arm parses but is ambiguous to read and brittle under further
3571
+ * composition, so wrap those two. A nested `&&`/`||` right operand is already
3572
+ * self-parenthesized. */
3573
+ function needsConditionalAlternateParens(child) {
3574
+ return child.kind === 'conditional' || child.kind === 'lambda';
3575
+ }
3576
+ /** S5 review fix — parenthesize a compound NON-CHAIN member-root expression so
3577
+ * the appended `.prop` link binds to the WHOLE root (`(b if t else c).prop`),
3578
+ * not its last operand (`b if t else c.prop` — silently wrong Python).
3579
+ * Low-precedence node kinds (same set as index receivers) are wrapped UNLESS
3580
+ * the lowering already produced a self-delimited atom: a fully-enclosing
3581
+ * paren pair (the `&&`/`||`/`??` walrus ternaries) or a single helper call
3582
+ * (`__kern_add(a, b)`), which keeps existing pinned bytes stable. */
3583
+ function wrapCompoundRootExpr(obj, emitted) {
3584
+ if (!needsIndexReceiverParens(obj))
3585
+ return emitted;
3586
+ if (isSelfDelimitedPyAtom(emitted))
3587
+ return emitted;
3588
+ return `(${emitted})`;
3589
+ }
3590
+ /** True when `expr` is one self-delimited Python atom: a fully-enclosing
3591
+ * bracket pair (`(...)`, `[...]`) or one identifier-headed call/index chain
3592
+ * whose trailing bracket closes at the very end (`__kern_add(a, b)`). A
3593
+ * leading unary sign (`-a`, `not x`, `await f()`) is NOT an atom. */
3594
+ function isSelfDelimitedPyAtom(expr) {
3595
+ if (expr.length === 0)
3596
+ return false;
3597
+ // Atoms start with an identifier/literal/bracket — a leading operator or
3598
+ // keyword (`-`, `~`, `not `, `await `) always needs wrapping.
3599
+ if (!/^[A-Za-z_0-9"'([{]/.test(expr))
3600
+ return false;
3601
+ if (!expr.includes(' '))
3602
+ return true;
3603
+ // Walk brackets/strings; any top-level space outside brackets means the
3604
+ // expression is compound (`b if t else c`), not an atom.
3605
+ let depth = 0;
3606
+ let quote = null;
3607
+ for (let i = 0; i < expr.length; i++) {
3608
+ const ch = expr[i];
3609
+ if (quote) {
3610
+ if (ch === '\\')
3611
+ i++;
3612
+ else if (ch === quote)
3613
+ quote = null;
3614
+ continue;
3615
+ }
3616
+ if (ch === '"' || ch === "'")
3617
+ quote = ch;
3618
+ else if (ch === '(' || ch === '[' || ch === '{')
3619
+ depth++;
3620
+ else if (ch === ')' || ch === ']' || ch === '}')
3621
+ depth--;
3622
+ else if (ch === ' ' && depth === 0)
3623
+ return false;
3624
+ }
3625
+ return depth === 0;
2903
3626
  }
2904
3627
  function needsIndexReceiverParens(child) {
2905
3628
  return (child.kind === 'binary' ||
@@ -2924,6 +3647,11 @@ function needsIndexReceiverParens(child) {
2924
3647
  function isReceiverChainPure(node) {
2925
3648
  if (node.kind === 'ident')
2926
3649
  return true;
3650
+ // Slice S7 — `undefined`/`null` literals are constant, so an optional chain
3651
+ // rooted at one (`undefined?.x`, `null?.x`) names a side-effect-free value and
3652
+ // takes the readable double-name guard form (short-circuiting to the sentinel).
3653
+ if (node.kind === 'undefLit' || node.kind === 'nullLit')
3654
+ return true;
2927
3655
  if (node.kind === 'member')
2928
3656
  return isReceiverChainPure(node.object);
2929
3657
  if (node.kind === 'index')
@@ -2980,6 +3708,88 @@ function mapBinaryOpToPython(op) {
2980
3708
  return op;
2981
3709
  }
2982
3710
  }
3711
+ /** Slice H — the host-namespace root set and diagnostic now live in core's
3712
+ * shared codegen module so TS and Python reject the same unmapped host roots
3713
+ * in lockstep. RegExp is intentionally exempt in that shared set for
3714
+ * Milestone B. */
3715
+ /** Slice H — is `name` PROVEN to be a user binding in the current expression
3716
+ * context?
3717
+ *
3718
+ * Reuses the emitter's EXISTING scope/binding model — the single source of
3719
+ * truth — rather than building a parallel tracker:
3720
+ * - `lookupLocalBinding` walks `ctx.localScopes` (function params via
3721
+ * `outerBindings`, body `let`/`const`/`cell` via `declareLocalBinding`,
3722
+ * loop vars, block scopes);
3723
+ * - `ctx.shadowedSymbols` records lambda/comprehension parameters that the
3724
+ * ident emitter already treats as user-local (see the `ident` case);
3725
+ * - `ctx.symbolMap` keys are KERN-form param names the FastAPI generator
3726
+ * renamed (e.g. `userId -> user_id`) — a present key means a real param.
3727
+ *
3728
+ * Per tribunal amendment 2, this FAILS TOWARD REFUSE: when the scope model
3729
+ * cannot prove a binding, we return false (→ the host-root guard fires),
3730
+ * never toward verbatim acceptance. Ground/module emitters that carry no
3731
+ * binding information therefore fail closed for reserved host roots, which is
3732
+ * exactly the intent — those are the contexts that previously leaked invalid
3733
+ * Python. (Honoring a *declared* binding — even one whose runtime value would
3734
+ * be `None` — is by design: this slice reuses the lexical scope model, it
3735
+ * does not do runtime value tracking. A user value named `Math` is the
3736
+ * shadowing case the spec explicitly keeps in scope.) */
3737
+ function isProvenUserBinding(ctx, name) {
3738
+ if (lookupLocalBinding(ctx, name) !== undefined)
3739
+ return true;
3740
+ if (ctx.shadowedSymbols.has(name))
3741
+ return true;
3742
+ if (Object.hasOwn(ctx.symbolMap, name))
3743
+ return true;
3744
+ return false;
3745
+ }
3746
+ /** Slice H — the fail-closed guard for unmapped host-namespace member
3747
+ * expressions (the strangler-pattern interim check).
3748
+ *
3749
+ * ACCEPTED DEBT (do not "fix" by relocating): this check lives at the
3750
+ * EMISSION point inside the code generator, not in a standalone validation
3751
+ * pass. That is interim by design — milestone A completes the portable
3752
+ * lowering registry (`KERN_STDLIB_MODULES`) and replaces these refusals with
3753
+ * real lowerings. Because the explicit AST lowering hooks all run BEFORE this
3754
+ * guard, the site is already scheduled to change as that registry grows: the
3755
+ * guard only ever sees the *remaining* unmapped forms. This is the strangler
3756
+ * pattern — the call site is provisioned to shrink, not to be reworked.
3757
+ *
3758
+ * Trigger predicate (capitalization-agnostic; NO {Math,JSON,Object,Date}
3759
+ * allowlist): a host-namespace-shaped root (`isHostNamespaceRoot`) that is
3760
+ * NOT proven user-bound (`isProvenUserBinding`, which fails toward refuse).
3761
+ * Returns `null` (caller proceeds to generic verbatim emission) for any root
3762
+ * that is provably a user binding or is not host-shaped — so user receivers
3763
+ * (`client.send(...)`, `myMath.sqrt(...)`) and proven-local `Math` pass
3764
+ * through unchanged. Throws otherwise. */
3765
+ function rejectUnmappedHostNamespacePython(root, member, ctx) {
3766
+ if (!isHostNamespaceRoot(root))
3767
+ return null;
3768
+ if (isProvenUserBinding(ctx, root))
3769
+ return null;
3770
+ throw new Error(unmappedHostNamespaceMessage('Python', root, member));
3771
+ }
3772
+ /** Slice 2 — host-`RegExp` fail-close (Python emit). Closes the residual RegExp
3773
+ * positions the generic `Module.member` guard above does NOT cover, throwing the
3774
+ * SAME shared `REGEX_HOST_REGEXP_FAILCLOSE` the TS emitter + IR-validate pass
3775
+ * throw (byte-identical across targets):
3776
+ * - a BARE-VALUE reference (`const R = RegExp`, `RegExp` passed as a value),
3777
+ * - a BARE CALL `RegExp(p, f)` (callee is an ident, missed by the member-callee
3778
+ * guard), and
3779
+ * - `new RegExp(p)` (the Python `new` case otherwise falls through to a verbatim
3780
+ * `RegExp(...)` NameError).
3781
+ * Honors user shadowing via `isProvenUserBinding` (fails toward refuse when a
3782
+ * binding cannot be proven, matching the host-namespace guard's intent), so
3783
+ * `const RegExp = myThing; RegExp` is the user value. `RegExp.prototype`/
3784
+ * `RegExp.$1` already fail-close through the generic member guard (one
3785
+ * diagnostic per site). */
3786
+ function rejectHostRegExpValuePython(name, ctx) {
3787
+ if (name !== 'RegExp')
3788
+ return;
3789
+ if (isProvenUserBinding(ctx, name))
3790
+ return;
3791
+ throw new Error(REGEX_HOST_REGEXP_FAILCLOSE);
3792
+ }
2983
3793
  /** Slice 2a — KERN-stdlib dispatch for Python. Returns the lowered Python
2984
3794
  * string when the call matches `<KnownModule>.<method>(args)`, or null when
2985
3795
  * it doesn't. Throws on `<KnownModule>.<unknownMethod>(...)` with a
@@ -2988,6 +3798,31 @@ function mapBinaryOpToPython(op) {
2988
3798
  * Slice 3b — when the matched entry declares `requires.py`, the import
2989
3799
  * identifier is added to the per-handler ctx.imports set so the FastAPI
2990
3800
  * generator can emit `import math` (etc.) at the top of the function body. */
3801
+ function applyStdlibPropertyLoweringPython(member, ctx) {
3802
+ if (member.optional)
3803
+ return null;
3804
+ if (member.object.kind !== 'ident')
3805
+ return null;
3806
+ const moduleName = member.object.name;
3807
+ if (!KERN_STDLIB_MODULES.has(moduleName))
3808
+ return null;
3809
+ const propertyName = member.property;
3810
+ const entry = lookupStdlibProperty(moduleName, propertyName);
3811
+ if (entry === null) {
3812
+ const callEntry = lookupStdlibCall(moduleName, propertyName);
3813
+ if (callEntry !== null) {
3814
+ throw new Error(`KERN-stdlib method '${moduleName}.${propertyName}' cannot be referenced as a value in portable Python lowering; call it directly.`);
3815
+ }
3816
+ throwUnknownStdlibMemberPython(moduleName, propertyName);
3817
+ }
3818
+ registerStdlibRequirementPython(entry.requires?.py, ctx);
3819
+ return entry.py;
3820
+ }
3821
+ function rejectKnownStdlibIndexPython(root, member) {
3822
+ if (!KERN_STDLIB_MODULES.has(root))
3823
+ return;
3824
+ throwUnknownStdlibMemberPython(root, member);
3825
+ }
2991
3826
  function applyStdlibLoweringPython(call, ctx) {
2992
3827
  const callee = call.callee;
2993
3828
  if (callee.kind !== 'member')
@@ -2998,34 +3833,90 @@ function applyStdlibLoweringPython(call, ctx) {
2998
3833
  if (!KERN_STDLIB_MODULES.has(moduleName))
2999
3834
  return null;
3000
3835
  const methodName = callee.property;
3001
- const entry = lookupStdlib(moduleName, methodName);
3836
+ const entry = lookupStdlibCall(moduleName, methodName);
3002
3837
  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}`);
3838
+ const propertyEntry = lookupStdlibProperty(moduleName, methodName);
3839
+ if (propertyEntry !== null) {
3840
+ throw new Error(`KERN-stdlib property '${moduleName}.${methodName}' is not callable.`);
3841
+ }
3842
+ throwUnknownStdlibMemberPython(moduleName, methodName);
3006
3843
  }
3007
3844
  // Slice-2 review fix: enforce declared arity (matches TS-side check).
3008
- 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}.`);
3845
+ validateStdlibCallArityPython(moduleName, methodName, entry, call.args.length);
3846
+ if (moduleName === 'Array' && methodName === 'from' && call.args.some((arg) => arg.kind === 'spread')) {
3847
+ throw new Error('Array.from portable lowering does not accept spread arguments; pass source and mapper directly.');
3010
3848
  }
3011
3849
  const listLambda = lowerListLambdaPython(moduleName, methodName, call, ctx);
3012
3850
  if (listLambda !== null)
3013
3851
  return listLambda;
3014
3852
  // Slice 3b — register required imports (e.g., `Number.floor` ⇒ `import math`).
3015
- if (entry.requires?.py)
3016
- ctx.imports.add(entry.requires.py);
3853
+ registerStdlibRequirementPython(entry.requires?.py, ctx);
3017
3854
  const args = call.args.map((a) => {
3018
3855
  const emitted = emitPyExprCtx(a, ctx);
3019
- return needsArgParens(a) ? `(${emitted})` : emitted;
3856
+ return a.kind !== 'spread' && needsArgParens(a) ? `(${emitted})` : emitted;
3020
3857
  });
3021
- return applyTemplate(entry.py, args);
3858
+ // Slice S7 — `Json.stringify` on Python routes through the sentinel-aware shim
3859
+ // (single-source `KERN_JSON_STRINGIFY_SHIM_PY`) instead of raw `__k_json.dumps`,
3860
+ // so `JSON.stringify(undefined)` is host-undefined, an object key whose value
3861
+ // is the sentinel is omitted, and a sentinel array element becomes JSON null —
3862
+ // matching JS. The shim references `__k_json`, supplied by the table's
3863
+ // `requires.py: 'json'` import registered above (one import shared with parse).
3864
+ if ((moduleName === 'Json' || moduleName === 'JSON') && methodName === 'stringify') {
3865
+ ctx.helpers.add(KERN_JSON_STRINGIFY_SHIM_PY);
3866
+ return `_kern_json_stringify(${args[0]})`;
3867
+ }
3868
+ return typeof entry.py === 'function' ? entry.py(args) : applyTemplate(entry.py, args);
3869
+ }
3870
+ function throwUnknownStdlibMemberPython(moduleName, memberName) {
3871
+ const suggestion = suggestStdlibMethod(moduleName, memberName);
3872
+ const hint = suggestion ? ` Did you mean '${moduleName}.${suggestion}'?` : '';
3873
+ throw new Error(`Unknown KERN-stdlib method/member '${moduleName}.${memberName}'.${hint}`);
3874
+ }
3875
+ function validateStdlibCallArityPython(moduleName, methodName, entry, got) {
3876
+ if (entry.arity !== undefined && got !== entry.arity) {
3877
+ throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes ${entry.arity} arg${entry.arity === 1 ? '' : 's'}, got ${got}.`);
3878
+ }
3879
+ if (entry.minArity !== undefined && got < entry.minArity) {
3880
+ throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes at least ${entry.minArity} args, got ${got}.`);
3881
+ }
3882
+ if (entry.maxArity !== undefined && got > entry.maxArity) {
3883
+ throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes at most ${entry.maxArity} args, got ${got}.`);
3884
+ }
3885
+ }
3886
+ function registerStdlibRequirementPython(requirement, ctx) {
3887
+ if (!requirement)
3888
+ return;
3889
+ if (requirement === 'math-host') {
3890
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
3891
+ ctx.helpers.add(KERN_JS_MATH_HELPERS_PY);
3892
+ return;
3893
+ }
3894
+ if (requirement === 'array-host') {
3895
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
3896
+ ctx.helpers.add(KERN_JS_ARRAY_FROM_HELPER_PY);
3897
+ return;
3898
+ }
3899
+ if (requirement === 'object-host') {
3900
+ ctx.helpers.add(KERN_JS_OBJECT_HELPERS_PY);
3901
+ return;
3902
+ }
3903
+ if (requirement === 'number-host') {
3904
+ ctx.helpers.add(KERN_JS_NUMBER_HELPERS_PY);
3905
+ return;
3906
+ }
3907
+ ctx.imports.add(requirement);
3022
3908
  }
3023
3909
  function lowerListLambdaPython(moduleName, methodName, call, ctx) {
3024
3910
  if (moduleName !== 'List')
3025
3911
  return null;
3026
3912
  if (methodName !== 'map' && methodName !== 'filter')
3027
3913
  return null;
3028
- const source = emitPyExprCtx(call.args[0], ctx);
3914
+ // S5 review fix — the source lands in the comprehension's generator head
3915
+ // (`for x in <source>`): walrus producers must switch to lambda-parameter
3916
+ // forms (CPython rejects `:=` in a comprehension iterable at any depth) and
3917
+ // a compound source (bare ternary) must be parenthesized so its `if` does
3918
+ // not parse as the comprehension filter.
3919
+ const source = parenthesizeIterable(withWalrusBan(ctx, () => emitPyExprCtx(call.args[0], ctx)));
3029
3920
  const callback = call.args[1];
3030
3921
  if (callback.kind !== 'lambda') {
3031
3922
  const fn = emitPyExprCtx(callback, ctx);
@@ -3056,35 +3947,132 @@ function lowerListLambdaPython(moduleName, methodName, call, ctx) {
3056
3947
  ctx.shadowedSymbols = previous;
3057
3948
  }
3058
3949
  }
3950
+ /** Slice 6 — Python bitwise/shift operator precedence (higher binds tighter).
3951
+ * Matches CPython's grammar: `|` < `^` < `&` < shift < unary `~`. Used to
3952
+ * parenthesize the LOWERED tree so the emitted Python reproduces the JS-shaped
3953
+ * grouping (e.g. `a << (b & 31)` must keep the mask parens, since Python's
3954
+ * `<<` binds tighter than `&`). */
3955
+ const PY_BITWISE_PREC = { '|': 1, '^': 2, '&': 3, '<<': 4, '>>': 4 };
3956
+ /**
3957
+ * Slice 6 — emit an ALREADY-LOWERED bitwise/shift tree to a Python string.
3958
+ *
3959
+ * The tree produced by `lowerBitwiseAndModuloAST` contains FINAL Python
3960
+ * operators (`| & ^ << >>` and unary `~`) whose operands are already wrapped in
3961
+ * `_kern_to_int32` / `_kern_to_uint32` calls. Re-routing those operators back
3962
+ * through `emitPyExprCtx`'s bitwise branch would re-lower them and recurse
3963
+ * forever, so this dedicated emitter renders the bitwise/shift `binary` and the
3964
+ * `~` `unary` verbatim (with precedence-correct parens) and delegates every
3965
+ * other node kind (the leaves: calls, idents, literals, …) to `emitPyExprCtx`.
3966
+ */
3967
+ function emitLoweredBitwisePy(node, ctx) {
3968
+ // The lowering's own wrapper calls (`_kern_to_int32` / `_kern_to_uint32` /
3969
+ // `_tmod`) must stay in THIS emit path: their argument is an already-lowered
3970
+ // bitwise/shift subtree, and handing the whole call to `emitPyExprCtx` would
3971
+ // re-route that inner subtree back into the bitwise branch and recurse
3972
+ // forever. Emit the wrapper directly and recurse through its argument here.
3973
+ if (node.kind === 'call' &&
3974
+ node.callee.kind === 'ident' &&
3975
+ (node.callee.name === '_kern_to_int32' || node.callee.name === '_kern_to_uint32' || node.callee.name === '_tmod')) {
3976
+ const args = node.args.map((a) => emitLoweredBitwisePy(a, ctx)).join(', ');
3977
+ return `${node.callee.name}(${args})`;
3978
+ }
3979
+ if (node.kind === 'binary' && node.op in PY_BITWISE_PREC) {
3980
+ const parentPrec = PY_BITWISE_PREC[node.op];
3981
+ const emitChild = (child, isRight) => {
3982
+ const s = emitLoweredBitwisePy(child, ctx);
3983
+ if (child.kind === 'binary' && child.op in PY_BITWISE_PREC) {
3984
+ const childPrec = PY_BITWISE_PREC[child.op];
3985
+ // Lower-precedence child always needs parens; an equal-precedence child
3986
+ // on the RIGHT needs parens to preserve left-associative grouping.
3987
+ if (childPrec < parentPrec || (childPrec === parentPrec && isRight))
3988
+ return `(${s})`;
3989
+ }
3990
+ return s;
3991
+ };
3992
+ return `${emitChild(node.left, false)} ${node.op} ${emitChild(node.right, true)}`;
3993
+ }
3994
+ if (node.kind === 'unary' && node.op === '~') {
3995
+ const inner = emitLoweredBitwisePy(node.argument, ctx);
3996
+ // `~` binds tighter than every binary bitwise/shift op, so a binary argument
3997
+ // must be parenthesized (e.g. `~(a | b)`).
3998
+ const wrapped = node.argument.kind === 'binary' && node.argument.op in PY_BITWISE_PREC ? `(${inner})` : inner;
3999
+ return `~${wrapped}`;
4000
+ }
4001
+ // Leaf / non-bitwise node — hand back to the main expression emitter.
4002
+ return emitPyExprCtx(node, ctx);
4003
+ }
4004
+ /**
4005
+ * Slice 6 — emit a binary bitwise/shift op `a <op> b` (op ∈ `| & ^ << >>`) on
4006
+ * the slice-0.75 ToInt32 substrate, per the S6 emission contract:
4007
+ *
4008
+ * a | b -> _kern_to_int32(_kern_to_int32(a) | _kern_to_int32(b))
4009
+ * a << b -> _kern_to_int32(_kern_to_int32(a) << (_kern_to_uint32(b) & 31))
4010
+ *
4011
+ * Each operand subexpression appears EXACTLY ONCE, so Python evaluates each side
4012
+ * once, left-to-right — matching JS evaluation-once order with no temporaries.
4013
+ */
4014
+ function emitBitwiseShiftPy(op, left, right, ctx) {
4015
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
4016
+ const l = `_kern_to_int32(${emitPyExprCtx(left, ctx)})`;
4017
+ if (op === '<<' || op === '>>') {
4018
+ // Shift count: ToUint32 then `& 31`.
4019
+ const count = `(_kern_to_uint32(${emitPyExprCtx(right, ctx)}) & 31)`;
4020
+ return `_kern_to_int32(${l} ${op} ${count})`;
4021
+ }
4022
+ const r = `_kern_to_int32(${emitPyExprCtx(right, ctx)})`;
4023
+ return `_kern_to_int32(${l} ${op} ${r})`;
4024
+ }
4025
+ /**
4026
+ * Slice 6 — emit `a >>> b` (unsigned/zero-fill right shift):
4027
+ *
4028
+ * a >>> b -> _kern_to_uint32(_kern_to_uint32(a) >> (_kern_to_uint32(b) & 31))
4029
+ *
4030
+ * Both operands are non-negative after `_kern_to_uint32`, so Python's raw `>>`
4031
+ * is zero-fill (it NEVER sign-extends here); the outer `_kern_to_uint32` keeps
4032
+ * the result in the 0..2**32-1 Uint32 codomain. Raw signed `>>` on the original
4033
+ * value would sign-extend and is therefore never used.
4034
+ */
4035
+ function emitUnsignedShiftPy(left, right, ctx) {
4036
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
4037
+ const l = `_kern_to_uint32(${emitPyExprCtx(left, ctx)})`;
4038
+ const count = `(_kern_to_uint32(${emitPyExprCtx(right, ctx)}) & 31)`;
4039
+ return `_kern_to_uint32(${l} >> ${count})`;
4040
+ }
3059
4041
  export function lowerBitwiseAndModuloAST(node) {
3060
4042
  switch (node.kind) {
3061
4043
  case 'binary': {
3062
4044
  const left = lowerBitwiseAndModuloAST(node.left);
3063
4045
  const right = lowerBitwiseAndModuloAST(node.right);
3064
- 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 = {
4046
+ // Slice 6 bitwise / shift on the landed slice-0.75 ToInt32 substrate.
4047
+ // Each operand expression appears EXACTLY ONCE in the lowered form (the
4048
+ // helper call wraps it inline), so Python evaluates each side once,
4049
+ // left-to-right matching JS evaluation-once order without temporaries.
4050
+ if (node.op === '|' || node.op === '&' || node.op === '^') {
4051
+ // a <op> b -> _kern_to_int32(_kern_to_int32(a) <op> _kern_to_int32(b))
4052
+ return wrapInToInt32({ kind: 'binary', op: node.op, left: wrapInToInt32(left), right: wrapInToInt32(right) });
4053
+ }
4054
+ if (node.op === '<<' || node.op === '>>') {
4055
+ // a <op> b -> _kern_to_int32(_kern_to_int32(a) <op> (_kern_to_uint32(b) & 31))
4056
+ return wrapInToInt32({
3080
4057
  kind: 'binary',
3081
4058
  op: node.op,
3082
- left: i32Left,
3083
- right: rewrittenRight,
3084
- };
3085
- return wrapInI32(bitwiseNode);
4059
+ left: wrapInToInt32(left),
4060
+ right: maskShiftCount(right),
4061
+ });
4062
+ }
4063
+ if (node.op === '>>>') {
4064
+ // a >>> b -> _kern_to_uint32(_kern_to_uint32(a) >> (_kern_to_uint32(b) & 31))
4065
+ // Both operands are non-negative after _kern_to_uint32, so Python's raw
4066
+ // `>>` is zero-fill (NEVER sign-extends); the outer _kern_to_uint32 keeps
4067
+ // the result in the 0..2**32-1 Uint32 codomain.
4068
+ return wrapInToUint32({
4069
+ kind: 'binary',
4070
+ op: '>>',
4071
+ left: wrapInToUint32(left),
4072
+ right: maskShiftCount(right),
4073
+ });
3086
4074
  }
3087
- else if (node.op === '%') {
4075
+ if (node.op === '%') {
3088
4076
  return {
3089
4077
  kind: 'call',
3090
4078
  callee: { kind: 'ident', name: '_tmod' },
@@ -3097,13 +4085,8 @@ export function lowerBitwiseAndModuloAST(node) {
3097
4085
  case 'unary': {
3098
4086
  const argument = lowerBitwiseAndModuloAST(node.argument);
3099
4087
  if (node.op === '~') {
3100
- const i32Arg = wrapInI32(argument);
3101
- const unaryNode = {
3102
- kind: 'unary',
3103
- op: '~',
3104
- argument: i32Arg,
3105
- };
3106
- return wrapInI32(unaryNode);
4088
+ // ~a -> _kern_to_int32(~_kern_to_int32(a))
4089
+ return wrapInToInt32({ kind: 'unary', op: '~', argument: wrapInToInt32(argument) });
3107
4090
  }
3108
4091
  return { ...node, argument };
3109
4092
  }
@@ -3156,20 +4139,32 @@ export function lowerBitwiseAndModuloAST(node) {
3156
4139
  return node;
3157
4140
  }
3158
4141
  }
3159
- function wrapInI32(node) {
4142
+ /** Slice 6 — wrap `node` in a `_kern_to_int32(...)` call (the landed slice-0.75
4143
+ * ToInt32 helper). Emits the operator-level coercion the S6 contract mandates. */
4144
+ function wrapInToInt32(node) {
4145
+ return { kind: 'call', callee: { kind: 'ident', name: '_kern_to_int32' }, args: [node], optional: false };
4146
+ }
4147
+ /** Slice 6 — wrap `node` in a `_kern_to_uint32(...)` call (slice-0.75 ToUint32). */
4148
+ function wrapInToUint32(node) {
4149
+ return { kind: 'call', callee: { kind: 'ident', name: '_kern_to_uint32' }, args: [node], optional: false };
4150
+ }
4151
+ /** Slice 6 — JS masks every shift count with `& 31` after ToUint32. Emits
4152
+ * `(_kern_to_uint32(count) & 31)`. The `count` subexpression appears once. */
4153
+ function maskShiftCount(count) {
3160
4154
  return {
3161
- kind: 'call',
3162
- callee: { kind: 'ident', name: '_i32' },
3163
- args: [node],
3164
- optional: false,
4155
+ kind: 'binary',
4156
+ op: '&',
4157
+ left: wrapInToUint32(count),
4158
+ right: { kind: 'numLit', value: 31, raw: '31' },
3165
4159
  };
3166
4160
  }
3167
4161
  export function registerHelpers(node, ctx) {
3168
4162
  switch (node.kind) {
3169
4163
  case 'call':
3170
4164
  if (node.callee.kind === 'ident') {
3171
- if (node.callee.name === '_i32') {
3172
- ctx.helpers.add(KERN_I32_HELPER_PY);
4165
+ if (node.callee.name === '_kern_to_int32' || node.callee.name === '_kern_to_uint32') {
4166
+ // Slice 6 — both helpers live in the single slice-0.75 helper block.
4167
+ ctx.helpers.add(KERN_TO_NUMBER_HELPER_PY);
3173
4168
  }
3174
4169
  else if (node.callee.name === '_tmod') {
3175
4170
  ctx.helpers.add(KERN_TMOD_HELPER_PY);
@@ -3255,7 +4250,7 @@ function emitExpressionV1Py(node, ctx) {
3255
4250
  if (exprSource === undefined || exprSource === '') {
3256
4251
  throw new Error('body-statement `expression-v1` requires `expr=`.');
3257
4252
  }
3258
- const exprIR = parseExpression(exprSource);
4253
+ const exprIR = parseExpr(exprSource);
3259
4254
  declareLocalBinding(ctx, userName, 'const');
3260
4255
  const name = maybeRenameOnShadow(ctx, userName);
3261
4256
  setRegexBinding(ctx, userName, exprIR.kind === 'regexLit' ? exprIR : null);