@kernlang/python 3.5.2

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.
Files changed (63) hide show
  1. package/LICENSE +678 -0
  2. package/README.md +26 -0
  3. package/dist/codegen-body-python.d.ts +152 -0
  4. package/dist/codegen-body-python.js +1648 -0
  5. package/dist/codegen-body-python.js.map +1 -0
  6. package/dist/codegen-helpers.d.ts +21 -0
  7. package/dist/codegen-helpers.js +352 -0
  8. package/dist/codegen-helpers.js.map +1 -0
  9. package/dist/codegen-python.d.ts +17 -0
  10. package/dist/codegen-python.js +106 -0
  11. package/dist/codegen-python.js.map +1 -0
  12. package/dist/fastapi-middleware.d.ts +8 -0
  13. package/dist/fastapi-middleware.js +87 -0
  14. package/dist/fastapi-middleware.js.map +1 -0
  15. package/dist/fastapi-portable.d.ts +9 -0
  16. package/dist/fastapi-portable.js +295 -0
  17. package/dist/fastapi-portable.js.map +1 -0
  18. package/dist/fastapi-raw-handler.d.ts +28 -0
  19. package/dist/fastapi-raw-handler.js +282 -0
  20. package/dist/fastapi-raw-handler.js.map +1 -0
  21. package/dist/fastapi-response.d.ts +13 -0
  22. package/dist/fastapi-response.js +150 -0
  23. package/dist/fastapi-response.js.map +1 -0
  24. package/dist/fastapi-route.d.ts +12 -0
  25. package/dist/fastapi-route.js +629 -0
  26. package/dist/fastapi-route.js.map +1 -0
  27. package/dist/fastapi-types.d.ts +39 -0
  28. package/dist/fastapi-types.js +5 -0
  29. package/dist/fastapi-types.js.map +1 -0
  30. package/dist/fastapi-utils.d.ts +16 -0
  31. package/dist/fastapi-utils.js +99 -0
  32. package/dist/fastapi-utils.js.map +1 -0
  33. package/dist/fastapi-websocket.d.ts +6 -0
  34. package/dist/fastapi-websocket.js +77 -0
  35. package/dist/fastapi-websocket.js.map +1 -0
  36. package/dist/generators/core.d.ts +23 -0
  37. package/dist/generators/core.js +906 -0
  38. package/dist/generators/core.js.map +1 -0
  39. package/dist/generators/data.d.ts +15 -0
  40. package/dist/generators/data.js +443 -0
  41. package/dist/generators/data.js.map +1 -0
  42. package/dist/generators/ground.d.ts +20 -0
  43. package/dist/generators/ground.js +333 -0
  44. package/dist/generators/ground.js.map +1 -0
  45. package/dist/generators/infra.d.ts +8 -0
  46. package/dist/generators/infra.js +109 -0
  47. package/dist/generators/infra.js.map +1 -0
  48. package/dist/index.d.ts +6 -0
  49. package/dist/index.js +7 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/ir-semantics/python-leg.d.ts +45 -0
  52. package/dist/ir-semantics/python-leg.js +291 -0
  53. package/dist/ir-semantics/python-leg.js.map +1 -0
  54. package/dist/python-stdlib-preamble.d.ts +32 -0
  55. package/dist/python-stdlib-preamble.js +86 -0
  56. package/dist/python-stdlib-preamble.js.map +1 -0
  57. package/dist/transpiler-fastapi.d.ts +8 -0
  58. package/dist/transpiler-fastapi.js +593 -0
  59. package/dist/transpiler-fastapi.js.map +1 -0
  60. package/dist/type-map.d.ts +14 -0
  61. package/dist/type-map.js +288 -0
  62. package/dist/type-map.js.map +1 -0
  63. package/package.json +37 -0
@@ -0,0 +1,1648 @@
1
+ /** Native KERN handler-body codegen — Python target (slices 1–3).
2
+ *
3
+ * Mirror of `packages/core/src/codegen/body-ts.ts` for the FastAPI/Python
4
+ * target. Walks the children of a handler with `lang=kern` and emits Python
5
+ * body lines. Recognized statements:
6
+ *
7
+ * - `let name=X value="EXPR"` — `X = EXPR` (slice 1)
8
+ * - `return value="EXPR"` / bare `return` (slice 1)
9
+ * - `if cond="EXPR"` / sibling `else` — `if EXPR:\n body\nelse:\n body` (slice 2c).
10
+ * - `while cond="EXPR"` — `while EXPR:\n body`
11
+ * - `for name=i from=0 to="List.length(xs)"` — `for i in range(...)`
12
+ * `else > if(…)` and `else > [if(…), else_inner]` collapse to `elif EXPR:` so
13
+ * raw `elif` chains round-trip byte-equivalent through slice 5b migration.
14
+ *
15
+ * Statement-level propagation `?` lowers to:
16
+ *
17
+ * __k_t1 = await call()
18
+ * if __k_t1.kind == 'err':
19
+ * return __k_t1
20
+ * u = __k_t1.value
21
+ *
22
+ * Slice 3 additions:
23
+ * - Body emit returns `{ code, imports }`. The generator uses the imports
24
+ * set to inject `import math` (etc.) at the top of the function body,
25
+ * so `Number.floor`/`ceil`/`round` lowerings work without surfacing
26
+ * a `NameError: math`.
27
+ * - `BodyEmitOptions.symbolMap` renames KERN identifiers to their
28
+ * Python-form equivalents at codegen time. The FastAPI generator builds
29
+ * a `userId → user_id` map from the param list so KERN bodies that
30
+ * reference `userId` resolve correctly against the snake_cased Python
31
+ * signature. Identifiers absent from the map pass through unchanged.
32
+ * - Optional-chain lowering for `member` (slice 3d): `a?.b` Python-lowers
33
+ * to `(a.b if a is not None else None)`. The receiver must be
34
+ * side-effect-free (ident or pure member chain); calls/awaits in the
35
+ * receiver throw with a let-bind hint to avoid double-evaluation.
36
+ *
37
+ * Indentation: Python is whitespace-significant, so the recursive walk
38
+ * threads a `indent` string. The propagation hoist embeds its own 4-space
39
+ * relative indent on the `return __k_tN` line; the wrapper prepends the
40
+ * surrounding indent so the post-emit result nests correctly. */
41
+ import { applyTemplate, isPostfixMutationOperator, isSupportedAssignOperator, KERN_STDLIB_MODULES, lookupStdlib, needsArgParens, needsBinaryParens, parseExpression, suggestStdlibMethod, } from '@kernlang/core';
42
+ const INDENT_STEP = ' ';
43
+ function freshCtx(options) {
44
+ return {
45
+ gensymCounter: 0,
46
+ imports: new Set(),
47
+ helpers: new Set(),
48
+ symbolMap: options?.symbolMap ?? {},
49
+ shadowedSymbols: new Set(),
50
+ localScopes: [],
51
+ regexScopes: [],
52
+ propagateStyle: options?.propagateStyle ?? 'value',
53
+ usedPropagation: false,
54
+ tryDepth: 0,
55
+ finallyDepth: 0,
56
+ traceHooks: options?.traceHooks,
57
+ };
58
+ }
59
+ /** PR-4 — Python helpers that normalize `each` pair-mode iteration sources.
60
+ * Co-located with the codegen so the production emitter and the differential
61
+ * harness use byte-identical definitions; consumers emit the string at module
62
+ * scope when `BodyEmitResult.helpers` is non-empty.
63
+ *
64
+ * Semantics:
65
+ * - `_kern_pairs(v)`: yields `(k, v)` tuples. Uses `v.items()` when present
66
+ * (Mapping shapes — dict, OrderedDict, custom Mapping subclasses); falls
67
+ * back to `iter(v)` so an iterable of `[k, v]` pairs (list/tuple) also
68
+ * destructures cleanly. Matches JS `for (const [k, v] of arrayOfPairs)`
69
+ * expressiveness — this is the divergence-1 fix from PR-3b audit.
70
+ * - `_kern_async_pairs(v)`: async generator. If `v` has `__aiter__` it
71
+ * forwards each item. Otherwise it falls back to `_kern_pairs(v)` so
72
+ * `async for` over a sync Mapping or array-of-pairs is well-defined —
73
+ * this is the divergence-2/3 fix (`async for` no longer requires an
74
+ * async iterable; sync data is wrapped at iteration entry).
75
+ *
76
+ * Both helpers are pure functions on the input; no captures, no globals. */
77
+ export const KERN_PAIR_HELPERS_PY = [
78
+ 'def _kern_pairs(__k_v):',
79
+ ' return __k_v.items() if hasattr(__k_v, "items") else iter(__k_v)',
80
+ '',
81
+ 'async def _kern_async_pairs(__k_v):',
82
+ ' if hasattr(__k_v, "__aiter__"):',
83
+ ' async for __k_item in __k_v:',
84
+ ' yield __k_item',
85
+ ' else:',
86
+ ' for __k_item in _kern_pairs(__k_v):',
87
+ ' yield __k_item',
88
+ ].join('\n');
89
+ /** Emit the body of a native KERN handler as Python source. Returns the
90
+ * joined body text. Each top-level line is unindented; nested `if`-bodies
91
+ * carry one level of 4-space indent per level of nesting.
92
+ *
93
+ * Legacy slice 1/2 signature — returns just the code string. Callers
94
+ * that also need the import set (slice 3b: `math` etc.) and/or want to
95
+ * pass a symbol map (slice 3a: `userId → user_id`) should use
96
+ * `emitNativeKernBodyPythonWithImports`.
97
+ *
98
+ * Slice 3 review fix (OpenCode + Gemini): if the handler requires imports
99
+ * (e.g. `Number.floor` ⇒ `math`) and the legacy entry point is used,
100
+ * the imports would be silently discarded — the generated Python would
101
+ * reference `__k_math.floor(...)` without the matching `import math as
102
+ * __k_math`, producing a `NameError` at runtime. Throw instead so the
103
+ * caller upgrades to the WithImports variant rather than shipping
104
+ * broken code. */
105
+ export function emitNativeKernBodyPython(handlerNode, options) {
106
+ const result = emitNativeKernBodyPythonWithImports(handlerNode, options);
107
+ if (result.imports.size > 0) {
108
+ const list = [...result.imports].sort().join(', ');
109
+ throw new Error(`emitNativeKernBodyPython: handler requires imports [${list}] which the legacy string-only API silently discards. ` +
110
+ 'Use emitNativeKernBodyPythonWithImports and emit the imports yourself (FastAPI generator does this automatically).');
111
+ }
112
+ // PR-4 — when the body needs runtime helpers (e.g. `_kern_pairs`), prepend
113
+ // them to the returned string. The legacy entry point has no separate
114
+ // helpers channel; folding them inline keeps single-string consumers (PR-3b
115
+ // differential harness) working without an API break.
116
+ if (result.helpers.size > 0) {
117
+ const helpers = [...result.helpers].join('\n\n');
118
+ return result.code ? `${helpers}\n\n${result.code}` : helpers;
119
+ }
120
+ return result.code;
121
+ }
122
+ /** Slice 3e — context-aware variant returning `{ code, imports }`. The
123
+ * FastAPI generator uses this to inject `import math` (etc.) at the top
124
+ * of the function body and to pass the param-rename map (3a) so the body
125
+ * resolves correctly against the snake_cased Python signature.
126
+ *
127
+ * Slice 4a review fix — also returns `usedPropagation` so the route
128
+ * emitter can conditionally add `from fastapi import HTTPException`
129
+ * when `propagateStyle: 'http-exception'` is in effect. */
130
+ export function emitNativeKernBodyPythonWithImports(handlerNode, options) {
131
+ const ctx = freshCtx(options);
132
+ const code = emitChildrenPy(handlerNode.children ?? [], ctx, '').join('\n');
133
+ return { code, imports: ctx.imports, usedPropagation: ctx.usedPropagation, helpers: ctx.helpers };
134
+ }
135
+ function emitChildrenPy(children, ctx, indent, initialBindings = []) {
136
+ const lines = [];
137
+ ctx.localScopes.push(new Map(initialBindings));
138
+ ctx.regexScopes.push(new Map(initialBindings.map(([name]) => [name, null])));
139
+ try {
140
+ for (let i = 0; i < children.length; i++) {
141
+ const child = children[i];
142
+ if (child.type === 'comment') {
143
+ for (const line of emitCommentPy(child))
144
+ lines.push(`${indent}${line}`);
145
+ }
146
+ else if (child.type === 'cell') {
147
+ for (const line of emitCellPy(child, ctx))
148
+ lines.push(`${indent}${line}`);
149
+ }
150
+ else if (child.type === 'set') {
151
+ for (const line of emitSetPy(child, ctx))
152
+ lines.push(`${indent}${line}`);
153
+ }
154
+ else if (child.type === 'let') {
155
+ for (const line of emitLetPy(child, ctx))
156
+ lines.push(`${indent}${line}`);
157
+ }
158
+ else if (child.type === 'assign') {
159
+ for (const line of emitAssignPy(child, ctx))
160
+ lines.push(`${indent}${line}`);
161
+ }
162
+ else if (child.type === 'destructure') {
163
+ for (const line of emitDestructurePy(child, ctx))
164
+ lines.push(`${indent}${line}`);
165
+ }
166
+ else if (child.type === 'fmt') {
167
+ for (const line of emitFmtPy(child, ctx))
168
+ lines.push(`${indent}${line}`);
169
+ }
170
+ else if (child.type === 'return') {
171
+ for (const line of emitReturnPy(child, ctx))
172
+ lines.push(`${indent}${line}`);
173
+ }
174
+ else if (child.type === 'if') {
175
+ const condRaw = String(child.props?.cond ?? '');
176
+ const condIR = parseExpression(condRaw);
177
+ // Slice-2 review fix: reject propagation `?` in `if cond=` (parallel to TS side).
178
+ if (condIR.kind === 'propagate') {
179
+ throw new Error("Propagation '?' is not allowed in `if cond=` — bind the call to a `let` first, then test the bound name.");
180
+ }
181
+ lines.push(`${indent}if ${emitPyExprCtx(condIR, ctx)}:`);
182
+ const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP);
183
+ if (inner.length === 0)
184
+ lines.push(`${indent}${INDENT_STEP}pass`);
185
+ for (const sl of inner)
186
+ lines.push(sl);
187
+ // Walk the `else` chain. Recognised shapes for `else`:
188
+ // 1. else > [if, else_inner] → emit `elif`, recurse on else_inner
189
+ // 2. else > [if] → terminal `elif` with no else
190
+ // 3. else > anything else → plain `else:`, chain ends
191
+ // Mirrors the TS emitter's `else if` collapsing so byte-equivalent
192
+ // raw-body `else if` chains round-trip cleanly through slice 5b.
193
+ let elseCandidate = children[i + 1];
194
+ if (elseCandidate?.type === 'else')
195
+ i++;
196
+ while (elseCandidate && elseCandidate.type === 'else') {
197
+ const ec = elseCandidate.children ?? [];
198
+ const isChainable = ec.length >= 1 && ec[0].type === 'if' && (ec.length === 1 || (ec.length === 2 && ec[1].type === 'else'));
199
+ if (isChainable) {
200
+ const ifNode = ec[0];
201
+ const nestedCondRaw = String(ifNode.props?.cond ?? '');
202
+ const nestedCondIR = parseExpression(nestedCondRaw);
203
+ if (nestedCondIR.kind === 'propagate') {
204
+ throw new Error("Propagation '?' is not allowed in `if cond=` — bind the call to a `let` first, then test the bound name.");
205
+ }
206
+ lines.push(`${indent}elif ${emitPyExprCtx(nestedCondIR, ctx)}:`);
207
+ const ifInner = emitChildrenPy(ifNode.children ?? [], ctx, indent + INDENT_STEP);
208
+ if (ifInner.length === 0)
209
+ lines.push(`${indent}${INDENT_STEP}pass`);
210
+ for (const sl of ifInner)
211
+ lines.push(sl);
212
+ elseCandidate = ec.length === 2 ? ec[1] : undefined;
213
+ }
214
+ else {
215
+ lines.push(`${indent}else:`);
216
+ const elseInner = emitChildrenPy(ec, ctx, indent + INDENT_STEP);
217
+ if (elseInner.length === 0)
218
+ lines.push(`${indent}${INDENT_STEP}pass`);
219
+ for (const el of elseInner)
220
+ lines.push(el);
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ else if (child.type === 'else') {
226
+ // Slice-2 review fix: orphan `else` is a structural error (matches TS side).
227
+ throw new Error('`else` must immediately follow an `if` sibling. Found orphan `else` in handler body.');
228
+ }
229
+ else if (child.type === 'while') {
230
+ const condRaw = String(child.props?.cond ?? '');
231
+ const condIR = parseExpression(condRaw);
232
+ if (condIR.kind === 'propagate') {
233
+ throw new Error("Propagation '?' is not allowed in `while cond=` — bind the call to a `let` first, then test the bound name.");
234
+ }
235
+ lines.push(`${indent}while ${emitPyExprCtx(condIR, ctx)}:`);
236
+ const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP);
237
+ if (inner.length === 0)
238
+ lines.push(`${indent}${INDENT_STEP}pass`);
239
+ for (const sl of inner)
240
+ lines.push(sl);
241
+ }
242
+ else if (child.type === 'for') {
243
+ for (const line of emitRangeForPy(child, ctx, indent))
244
+ lines.push(line);
245
+ }
246
+ else if (child.type === 'with') {
247
+ for (const line of emitWithPy(child, ctx, indent))
248
+ lines.push(line);
249
+ }
250
+ else if (child.type === 'try') {
251
+ // Slice 4c — try/except control flow.
252
+ //
253
+ // Slice 5a deferred-fix (Codex P2-2): mirror the TS-side change to
254
+ // read `catch` as a CHILD of `try`, matching the schema's
255
+ // `try.allowedChildren = ['step', 'handler', 'catch']`. The previous
256
+ // sibling-shape body-emit was unreachable for schema-validated source
257
+ // (the validator rejected it first) and miscompiled when invoked
258
+ // directly with hand-built IR.
259
+ const tryChildren = child.children ?? [];
260
+ const catchChildren = tryChildren.filter((c) => c.type === 'catch');
261
+ const finallyChildren = tryChildren.filter((c) => c.type === 'finally');
262
+ if (catchChildren.length > 1) {
263
+ throw new Error('`try` supports at most one `catch` child — found multiple in handler body.');
264
+ }
265
+ if (finallyChildren.length > 1) {
266
+ throw new Error('`try` supports at most one `finally` child — found multiple in handler body.');
267
+ }
268
+ if (finallyChildren.length > 0 && typeof child.props?.name === 'string' && child.props.name.length > 0) {
269
+ throw new Error('`finally` is only supported on body-statement `try` (inside `handler lang="kern"`). Found `finally` under async-orchestration `try name=…` — move cleanup into the surrounding handler.');
270
+ }
271
+ const catchNode = catchChildren[0] ?? null;
272
+ const finallyNode = finallyChildren[0] ?? null;
273
+ if (catchNode === null && finallyNode === null) {
274
+ throw new Error('`try` must contain a `catch` or `finally` child. Found orphan `try` in handler body.');
275
+ }
276
+ const tryBlockChildren = tryChildren.filter((c) => c.type !== 'catch' && c.type !== 'finally');
277
+ // Slice 5a deferred-fix (Codex): see body-ts.ts for the rationale —
278
+ // `step` / `handler` are valid only inside an async-orchestration
279
+ // `try name=…` block, not inside body-statement try/catch.
280
+ const orchestrationChild = tryBlockChildren.find((c) => c.type === 'step' || c.type === 'handler');
281
+ if (orchestrationChild) {
282
+ throw new Error(`\`${orchestrationChild.type}\` is only valid inside an async-orchestration \`try name=…\` block, not inside a body-statement \`try\`. Move the steps into the surrounding fn or use a structured orchestration block.`);
283
+ }
284
+ lines.push(`${indent}try:`);
285
+ ctx.tryDepth++;
286
+ const inner = emitChildrenPy(tryBlockChildren, ctx, indent + INDENT_STEP);
287
+ ctx.tryDepth--;
288
+ if (inner.length === 0)
289
+ lines.push(`${indent}${INDENT_STEP}pass`);
290
+ for (const sl of inner)
291
+ lines.push(sl);
292
+ if (catchNode !== null) {
293
+ const errName = String(catchNode.props?.name ?? 'e');
294
+ lines.push(`${indent}except Exception as ${errName}:`);
295
+ const catchInner = emitChildrenPy(catchNode.children ?? [], ctx, indent + INDENT_STEP);
296
+ if (catchInner.length === 0)
297
+ lines.push(`${indent}${INDENT_STEP}pass`);
298
+ for (const cl of catchInner)
299
+ lines.push(cl);
300
+ }
301
+ if (finallyNode !== null) {
302
+ lines.push(`${indent}finally:`);
303
+ ctx.finallyDepth++;
304
+ const finallyInner = emitChildrenPy(finallyNode.children ?? [], ctx, indent + INDENT_STEP);
305
+ ctx.finallyDepth--;
306
+ if (finallyInner.length === 0)
307
+ lines.push(`${indent}${INDENT_STEP}pass`);
308
+ for (const fl of finallyInner)
309
+ lines.push(fl);
310
+ }
311
+ }
312
+ else if (child.type === 'catch') {
313
+ throw new Error('`catch` must be a child of `try`. Found top-level `catch` in handler body.');
314
+ }
315
+ else if (child.type === 'finally') {
316
+ throw new Error('`finally` must be a child of `try`. Found top-level `finally` in handler body.');
317
+ }
318
+ else if (child.type === 'throw') {
319
+ for (const line of emitThrowPy(child, ctx))
320
+ lines.push(`${indent}${line}`);
321
+ }
322
+ else if (child.type === 'do') {
323
+ for (const line of emitDoPy(child, ctx))
324
+ lines.push(`${indent}${line}`);
325
+ }
326
+ else if (child.type === 'continue') {
327
+ lines.push(`${indent}continue`);
328
+ }
329
+ else if (child.type === 'break') {
330
+ lines.push(`${indent}break`);
331
+ }
332
+ else if (child.type === 'each') {
333
+ // Slice 4d — each loop.
334
+ // Slice 4c+4d review fix (Codex P1) — read schema-compliant
335
+ // `name`/`in` props (legacy `list`/`as` accepted as fallback).
336
+ const listRaw = String(child.props?.in ?? child.props?.list ?? '[]');
337
+ const listIR = parseExpression(listRaw);
338
+ const pairKey = child.props?.pairKey;
339
+ const pairValue = child.props?.pairValue;
340
+ const entryKey = child.props?.entryKey;
341
+ const entryValue = child.props?.entryValue;
342
+ const isAwait = child.props?.await === true || child.props?.await === 'true';
343
+ const entriesMode = child.props?.entries === true || child.props?.entries === 'true';
344
+ if (isAwait && child.props?.index) {
345
+ throw new Error('body-statement `each await=true` cannot be combined with `index=`.');
346
+ }
347
+ // PR-4 — pair-mode (`pairKey=k pairValue=v`) iterates via the runtime
348
+ // helpers `_kern_pairs` (sync) and `_kern_async_pairs` (async). The
349
+ // helpers normalize Mapping inputs (via `.items()`), array-of-pairs
350
+ // (via `iter()`), and async iterables (forwarded as-is). Goals from
351
+ // PR-3b audit:
352
+ // - sync pair-mode over list-of-[k,v] no longer raises AttributeError
353
+ // - async pair-mode over sync data no longer raises TypeError
354
+ // The helpers are co-located in `KERN_PAIR_HELPERS_PY` so the
355
+ // production emitter and the differential harness share one definition.
356
+ if (pairKey && pairValue) {
357
+ if (entriesMode && isAwait) {
358
+ throw new Error('body-statement `each entries=true` cannot be combined with `await=true`.');
359
+ }
360
+ const k = String(pairKey);
361
+ const v = String(pairValue);
362
+ const sourceExpr = emitPyExprCtx(listIR, ctx);
363
+ ctx.helpers.add(KERN_PAIR_HELPERS_PY);
364
+ const iterableExpr = isAwait ? `_kern_async_pairs(${sourceExpr})` : `_kern_pairs(${sourceExpr})`;
365
+ lines.push(`${indent}${isAwait ? 'async ' : ''}for ${k}, ${v} in ${iterableExpr}:`);
366
+ if (ctx.traceHooks?.eachIterNext) {
367
+ lines.push(`${indent}${INDENT_STEP}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(v)}, "value": ${v}})`);
368
+ }
369
+ const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [
370
+ [k, 'const'],
371
+ [v, 'const'],
372
+ ]);
373
+ if (inner.length === 0 && !ctx.traceHooks?.eachIterNext)
374
+ lines.push(`${indent}${INDENT_STEP}pass`);
375
+ for (const sl of inner)
376
+ lines.push(sl);
377
+ continue;
378
+ }
379
+ if (entryKey || entryValue) {
380
+ if (entriesMode && isAwait) {
381
+ throw new Error('body-statement `each entries=true` cannot be combined with `await=true`.');
382
+ }
383
+ if (!entriesMode) {
384
+ throw new Error('body-statement `each entryKey=`/`entryValue=` requires `entries=true`.');
385
+ }
386
+ if (isAwait) {
387
+ throw new Error('body-statement `each await=true` cannot be combined with `entryKey=`/`entryValue=`.');
388
+ }
389
+ if (entryKey && entryValue) {
390
+ throw new Error('body-statement `each` cannot combine `entryKey=` and `entryValue=`.');
391
+ }
392
+ const sourceExpr = emitPyExprCtx(listIR, ctx);
393
+ if (entryKey) {
394
+ const k = String(entryKey);
395
+ const iterableExpr = `${sourceExpr}.keys()`;
396
+ lines.push(`${indent}for ${k} in ${iterableExpr}:`);
397
+ if (ctx.traceHooks?.eachIterNext) {
398
+ lines.push(`${indent}${INDENT_STEP}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(k)}, "value": ${k}})`);
399
+ }
400
+ const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [[k, 'const']]);
401
+ if (inner.length === 0 && !ctx.traceHooks?.eachIterNext)
402
+ lines.push(`${indent}${INDENT_STEP}pass`);
403
+ for (const sl of inner)
404
+ lines.push(sl);
405
+ }
406
+ else {
407
+ const v = String(entryValue);
408
+ const iterableExpr = `${sourceExpr}.values()`;
409
+ lines.push(`${indent}for ${v} in ${iterableExpr}:`);
410
+ if (ctx.traceHooks?.eachIterNext) {
411
+ lines.push(`${indent}${INDENT_STEP}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(v)}, "value": ${v}})`);
412
+ }
413
+ const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, [[v, 'const']]);
414
+ if (inner.length === 0 && !ctx.traceHooks?.eachIterNext)
415
+ lines.push(`${indent}${INDENT_STEP}pass`);
416
+ for (const sl of inner)
417
+ lines.push(sl);
418
+ }
419
+ continue;
420
+ }
421
+ // Slice 5a deferred-fix: TS `for (const item of xs)` is block-scoped
422
+ // — `item` is undefined after the loop. Python `for item in xs:`
423
+ // leaks: `item` keeps the last iteration value, and a prior outer
424
+ // `item` would have been clobbered. We use a gensym for the
425
+ // iteration variable and unpack into the user-friendly name on each
426
+ // iteration. After the loop the gensym leaks (Python language
427
+ // limitation), but the user-facing `asName` is no worse than before
428
+ // and the inter-loop collision (two `each` with the same `as=`)
429
+ // works because each loop has a fresh gensym + fresh body-local
430
+ // alias. Document the residual leak in the spec.
431
+ //
432
+ // PR-3b — index-mode (`each name=x index=i in=xs`) now lowers to
433
+ // `for i, x in enumerate(xs):`, aligning with the route-handler /
434
+ // ground generators that already supported this shape. Caught by
435
+ // the IR-semantics differential audit (PR-3b).
436
+ const asName = String(child.props?.name ?? child.props?.as ?? 'item');
437
+ const idxName = child.props?.index !== undefined ? String(child.props.index) : null;
438
+ const iterableExpr = emitPyExprCtx(listIR, ctx);
439
+ let primaryBindingPy;
440
+ let initialBindings;
441
+ if (idxName !== null) {
442
+ // `for i, x in enumerate(xs):` — direct destructuring, no gensym
443
+ // unpacking needed because both names are already user-facing.
444
+ lines.push(`${indent}for ${idxName}, ${asName} in enumerate(${iterableExpr}):`);
445
+ primaryBindingPy = asName;
446
+ initialBindings = [
447
+ [idxName, 'const'],
448
+ [asName, 'const'],
449
+ ];
450
+ }
451
+ else {
452
+ const iterVar = `__k_each_${++ctx.gensymCounter}`;
453
+ lines.push(`${indent}${isAwait ? 'async ' : ''}for ${iterVar} in ${iterableExpr}:`);
454
+ lines.push(`${indent}${INDENT_STEP}${asName} = ${iterVar}`);
455
+ primaryBindingPy = asName;
456
+ initialBindings = [[asName, 'const']];
457
+ }
458
+ if (ctx.traceHooks?.eachIterNext) {
459
+ lines.push(`${indent}${INDENT_STEP}_kern_trace({"op": "iter-next", "binding": ${JSON.stringify(primaryBindingPy)}, "value": ${primaryBindingPy}})`);
460
+ }
461
+ const inner = emitChildrenPy(child.children ?? [], ctx, indent + INDENT_STEP, initialBindings);
462
+ // `pass` is needed only when the for-loop body would otherwise be empty:
463
+ // - index-mode path emits NO assignment (direct destructuring), so an
464
+ // empty children list leaves the loop bodyless → IndentationError.
465
+ // Caught by PR-3b agon review (4/6 reviewers).
466
+ // - non-index path emits `${asName} = ${iterVar}` which IS a valid
467
+ // body statement, so `pass` is unnecessary even with empty children.
468
+ // - trace-hook path emits `_kern_trace(...)` as the body, so no pass.
469
+ if (inner.length === 0 && idxName !== null && !ctx.traceHooks?.eachIterNext) {
470
+ lines.push(`${indent}${INDENT_STEP}pass`);
471
+ }
472
+ for (const sl of inner)
473
+ lines.push(sl);
474
+ }
475
+ else if (child.type === 'branch') {
476
+ // 2026-05-06 — body-statement `branch` lowers to a Python
477
+ // `if/elif/else` chain (PEP-634 `match` is deferred). Distinct from
478
+ // any top-level branch codegen — none currently exists on the
479
+ // fastapi target. We gensym the `on=` expression once so it's not
480
+ // double-evaluated across cases.
481
+ for (const line of emitBranchPy(child, ctx, indent))
482
+ lines.push(line);
483
+ }
484
+ }
485
+ }
486
+ finally {
487
+ ctx.localScopes.pop();
488
+ ctx.regexScopes.pop();
489
+ }
490
+ return lines;
491
+ }
492
+ function emitRangeForPy(node, ctx, indent) {
493
+ const props = (node.props ?? {});
494
+ const name = String(props.name ?? '');
495
+ if (!name)
496
+ throw new Error('body-statement `for` requires `name=`.');
497
+ validateRangeLoopIdentifier(name);
498
+ const rawFrom = props.from;
499
+ const rawTo = props.to;
500
+ const rawStep = props.step === undefined || props.step === '' ? '1' : String(props.step);
501
+ if (rawFrom === undefined || rawFrom === '')
502
+ throw new Error('body-statement `for` requires `from=`.');
503
+ if (rawTo === undefined || rawTo === '')
504
+ throw new Error('body-statement `for` requires `to=`.');
505
+ validateIntegerRangeBound(String(rawFrom), 'from');
506
+ validateIntegerRangeBound(String(rawTo), 'to');
507
+ validatePositiveRangeStep(rawStep);
508
+ const fromIR = parseExpression(String(rawFrom));
509
+ const toIR = parseExpression(String(rawTo));
510
+ const stepIR = parseExpression(rawStep);
511
+ if (fromIR.kind === 'propagate' || toIR.kind === 'propagate' || stepIR.kind === 'propagate') {
512
+ throw new Error("Propagation '?' is not allowed in `for from=`/`to=`/`step=` — bind the value to a `let` before the loop.");
513
+ }
514
+ const fromExpr = emitPyExprCtx(fromIR, ctx);
515
+ const toExpr = emitPyExprCtx(toIR, ctx);
516
+ const stepExpr = emitPyExprCtx(stepIR, ctx);
517
+ const rangeArgs = isRangeStepOne(rawStep) ? `${fromExpr}, ${toExpr}` : `${fromExpr}, ${toExpr}, ${stepExpr}`;
518
+ const scopeId = ++ctx.gensymCounter;
519
+ const missingVar = `__k_for_missing_${scopeId}`;
520
+ const prevVar = `__k_for_prev_${scopeId}`;
521
+ const tryIndent = indent + INDENT_STEP;
522
+ const bodyIndent = tryIndent + INDENT_STEP;
523
+ const out = [
524
+ `${indent}${missingVar} = object()`,
525
+ `${indent}${prevVar} = locals().get(${JSON.stringify(name)}, ${missingVar})`,
526
+ `${indent}try:`,
527
+ `${tryIndent}for ${name} in range(${rangeArgs}):`,
528
+ ];
529
+ const inner = emitChildrenPy(node.children ?? [], ctx, bodyIndent, [[name, 'const']]);
530
+ if (inner.length === 0)
531
+ out.push(`${bodyIndent}pass`);
532
+ for (const sl of inner)
533
+ out.push(sl);
534
+ out.push(`${indent}finally:`);
535
+ out.push(`${tryIndent}if ${prevVar} is ${missingVar}:`);
536
+ out.push(`${bodyIndent}try:`);
537
+ out.push(`${bodyIndent}${INDENT_STEP}del ${name}`);
538
+ out.push(`${bodyIndent}except NameError:`);
539
+ out.push(`${bodyIndent}${INDENT_STEP}pass`);
540
+ out.push(`${tryIndent}else:`);
541
+ out.push(`${bodyIndent}${name} = ${prevVar}`);
542
+ return out;
543
+ }
544
+ function emitWithPy(node, ctx, indent) {
545
+ const props = (node.props ?? {});
546
+ const rawName = props.name;
547
+ const rawValue = props.value;
548
+ const rawCleanup = props.cleanup;
549
+ if (rawName === undefined || rawName === '')
550
+ throw new Error('body-statement `with` requires `name=`.');
551
+ if (rawValue === undefined || rawValue === '')
552
+ throw new Error('body-statement `with` requires `value=`.');
553
+ const protocol = props.protocol === undefined || props.protocol === '' ? '' : String(props.protocol);
554
+ if (protocol !== '' && protocol !== 'with') {
555
+ throw new Error('body-statement `with protocol=` supports only `with`.');
556
+ }
557
+ const isAsync = props.async === true || props.async === 'true';
558
+ if (protocol === 'with' && isAsync) {
559
+ throw new Error('body-statement `with async=true protocol=with` is not supported yet — use default protocol (try/finally) for async cleanup.');
560
+ }
561
+ const hasCleanup = rawCleanup !== undefined && rawCleanup !== null && String(rawCleanup) !== '';
562
+ if (protocol === 'with' && hasCleanup) {
563
+ throw new Error("body-statement `with protocol=with` delegates cleanup to the context manager's __exit__ — drop cleanup= or drop protocol=with.");
564
+ }
565
+ if (protocol !== 'with' && !hasCleanup) {
566
+ throw new Error('body-statement `with` requires `cleanup=` (or set `protocol=with` to use __exit__).');
567
+ }
568
+ const name = String(rawName);
569
+ const valueIR = parseExpression(String(rawValue));
570
+ if (valueIR.kind === 'propagate') {
571
+ throw new Error("Propagation '?' is not allowed in `with value=` — bind to `let` first.");
572
+ }
573
+ declareLocalBinding(ctx, name, 'const');
574
+ if (protocol === 'with') {
575
+ const lines = [`${indent}with ${emitPyExprCtx(valueIR, ctx)} as ${name}:`];
576
+ const inner = emitChildrenPy(node.children ?? [], ctx, indent + INDENT_STEP, [[name, 'const']]);
577
+ if (inner.length === 0)
578
+ lines.push(`${indent}${INDENT_STEP}pass`);
579
+ for (const line of inner)
580
+ lines.push(line);
581
+ return lines;
582
+ }
583
+ const cleanupIR = parseExpression(String(rawCleanup));
584
+ if (cleanupIR.kind === 'propagate') {
585
+ throw new Error("Propagation '?' is not allowed in `with cleanup=` — bind to `let` first.");
586
+ }
587
+ const awaitPrefix = isAsync ? 'await ' : '';
588
+ const out = [`${indent}${name} = ${awaitPrefix}${emitPyExprCtx(valueIR, ctx)}`, `${indent}try:`];
589
+ const inner = emitChildrenPy(node.children ?? [], ctx, indent + INDENT_STEP, [[name, 'const']]);
590
+ if (inner.length === 0)
591
+ out.push(`${indent}${INDENT_STEP}pass`);
592
+ for (const line of inner)
593
+ out.push(line);
594
+ out.push(`${indent}finally:`);
595
+ out.push(`${indent}${INDENT_STEP}${awaitPrefix}${emitPyExprCtx(cleanupIR, ctx)}`);
596
+ return out;
597
+ }
598
+ function validatePositiveRangeStep(rawStep) {
599
+ const trimmed = rawStep.trim();
600
+ const numeric = Number(trimmed);
601
+ if (!/^[0-9]+$/.test(trimmed) || !Number.isSafeInteger(numeric) || numeric <= 0) {
602
+ throw new Error('body-statement `for step=` must be a positive integer literal in this cross-target range-loop slice.');
603
+ }
604
+ }
605
+ function validateIntegerRangeBound(rawBound, propName) {
606
+ const trimmed = rawBound.trim();
607
+ const numeric = Number(trimmed);
608
+ if (trimmed !== '' && Number.isFinite(numeric) && !Number.isInteger(numeric)) {
609
+ throw new Error(`body-statement \`for ${propName}=\` must be an integer expression.`);
610
+ }
611
+ }
612
+ function isRangeStepOne(rawStep) {
613
+ const numeric = Number(rawStep.trim());
614
+ return /^[0-9]+$/.test(rawStep.trim()) && Number.isSafeInteger(numeric) && numeric === 1;
615
+ }
616
+ function validateRangeLoopIdentifier(name) {
617
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
618
+ throw new Error('body-statement `for name=` must be a cross-target identifier.');
619
+ }
620
+ }
621
+ function emitBranchPy(node, ctx, indent) {
622
+ const onRaw = String(node.props?.on ?? '');
623
+ if (onRaw === '') {
624
+ throw new Error('`branch` requires an `on=` expression in body-statement context.');
625
+ }
626
+ const onIR = parseExpression(onRaw);
627
+ const subjectVar = `__k_branch_${++ctx.gensymCounter}`;
628
+ const out = [];
629
+ out.push(`${indent}${subjectVar} = ${emitPyExprCtx(onIR, ctx)}`);
630
+ const paths = (node.children ?? []).filter((c) => c.type === 'path');
631
+ // Order matters: every non-default `path` becomes `if`/`elif`; the (at
632
+ // most one) `default` becomes the trailing `else:`. We track whether we
633
+ // already emitted the leading `if` so subsequent paths use `elif`.
634
+ let firstEmitted = false;
635
+ let defaultPath;
636
+ for (const p of paths) {
637
+ if (p.props?.default === true || p.props?.default === 'true') {
638
+ defaultPath = p;
639
+ continue;
640
+ }
641
+ const rawValue = p.props?.value;
642
+ const valueText = rawValue === undefined ? '' : String(rawValue);
643
+ const isIdentifier = !p.__quotedProps?.includes('value');
644
+ // Identifier values pass through verbatim (e.g. `Status.Active`).
645
+ // Quoted strings emit JSON-encoded Python string literals — JSON's
646
+ // ASCII-quoted output is a valid Python str literal subset, so this
647
+ // is correct for both the printable-ASCII and unicode-escaped cases.
648
+ const lit = isIdentifier ? valueText : JSON.stringify(valueText);
649
+ const keyword = firstEmitted ? 'elif' : 'if';
650
+ out.push(`${indent}${keyword} ${subjectVar} == ${lit}:`);
651
+ const inner = emitChildrenPy(p.children ?? [], ctx, indent + INDENT_STEP);
652
+ if (inner.length === 0)
653
+ out.push(`${indent}${INDENT_STEP}pass`);
654
+ for (const sl of inner)
655
+ out.push(sl);
656
+ firstEmitted = true;
657
+ }
658
+ if (defaultPath) {
659
+ if (!firstEmitted) {
660
+ // No regular paths — emit the default body unconditionally. Avoid an
661
+ // `else:` with no preceding `if`, which is a Python syntax error.
662
+ const inner = emitChildrenPy(defaultPath.children ?? [], ctx, indent);
663
+ if (inner.length === 0)
664
+ out.push(`${indent}pass`);
665
+ for (const sl of inner)
666
+ out.push(sl);
667
+ }
668
+ else {
669
+ out.push(`${indent}else:`);
670
+ const inner = emitChildrenPy(defaultPath.children ?? [], ctx, indent + INDENT_STEP);
671
+ if (inner.length === 0)
672
+ out.push(`${indent}${INDENT_STEP}pass`);
673
+ for (const sl of inner)
674
+ out.push(sl);
675
+ }
676
+ }
677
+ return out;
678
+ }
679
+ /** Slice 4c review fix (OpenCode + Gemini critical) — propagation `?`
680
+ * inside `try` has no clean lowering on either propagateStyle: the
681
+ * 'value' style emits `return tmp` (exits the function bypassing
682
+ * except), and the 'http-exception' style emits `raise HTTPException`
683
+ * (caught by the bare `except Exception` we generate, swallowing the
684
+ * err). Reject at codegen with a let-bind hint. Propagation inside
685
+ * `finally` gets a sharper diagnostic because it would override pending
686
+ * control flow from the protected block. */
687
+ function rejectPropagationInsideTry(ctx) {
688
+ if (ctx.tryDepth > 0) {
689
+ throw new Error("Propagation '?' is not allowed inside a `try` block — `return`/`raise` from the err branch interacts incorrectly with the enclosing `except` clause. " +
690
+ 'Bind the call to a `let` outside the try, then use `if x.kind == "err" then throw ...` inside the try, OR use raw `lang=ts`/`lang=python` for the affected handler.');
691
+ }
692
+ if (ctx.finallyDepth > 0) {
693
+ throw new Error("Propagation '?' is not allowed inside a `finally` block — `return`/`raise` from the err branch overrides the pending exception/return from the protected block. " +
694
+ 'Bind the call to a `let` outside the `try` if you need conditional fallthrough, OR use raw `lang=ts`/`lang=python` for the affected handler.');
695
+ }
696
+ }
697
+ function errPropagationLine(tmp, ctx) {
698
+ // Slice 4a review fix (Gemini #5) — when the route emitter requests
699
+ // 'http-exception' propagation style, the err branch raises rather than
700
+ // returns. Without this, FastAPI serializes the err Result as a 200 OK
701
+ // response with `{kind: 'err', error: ...}` body, which silently masks
702
+ // application errors as successful responses.
703
+ if (ctx.propagateStyle === 'http-exception') {
704
+ return ` raise HTTPException(status_code=500, detail=${tmp}.error)`;
705
+ }
706
+ return ` return ${tmp}`;
707
+ }
708
+ function emitCellPy(node, ctx) {
709
+ const props = (node.props ?? {});
710
+ const rawName = props.name;
711
+ if (rawName === undefined || rawName === '') {
712
+ throw new Error('body-statement `cell` requires `name=`.');
713
+ }
714
+ const name = String(rawName);
715
+ declareLocalBinding(ctx, name, 'cell');
716
+ const pythonName = ctx.symbolMap[name] ?? name;
717
+ const rawInitial = props.initial;
718
+ // FastAPI request handlers don't need reactivity — each request resets
719
+ // state. Cell lowers to plain mutable assignment, indistinguishable from
720
+ // `let kind=let` at runtime; the distinction is for cross-target semantic
721
+ // intent (TS+React emits `useState`). Future Python targets (Plotly Dash,
722
+ // Streamlit) can specialize the lowering without changing author code.
723
+ if (rawInitial === undefined || rawInitial === '') {
724
+ return [`${pythonName} = None`];
725
+ }
726
+ const initialIR = parseExpression(String(rawInitial));
727
+ return [`${pythonName} = ${emitPyExprCtx(initialIR, ctx)}`];
728
+ }
729
+ function emitSetPy(node, ctx) {
730
+ const props = (node.props ?? {});
731
+ const rawName = props.name;
732
+ if (rawName === undefined || rawName === '') {
733
+ throw new Error('body-statement `set` requires `name=`.');
734
+ }
735
+ const rawTo = props.to;
736
+ if (rawTo === undefined || rawTo === '') {
737
+ throw new Error('body-statement `set` requires `to=`.');
738
+ }
739
+ const name = String(rawName);
740
+ const pythonName = ctx.symbolMap[name] ?? name;
741
+ const valueIR = parseExpression(String(rawTo));
742
+ if (valueIR.kind === 'propagate') {
743
+ throw new Error(`Propagation \`${valueIR.op}\` is not supported in \`set to=\` — bind to \`let\` first, then call set.`);
744
+ }
745
+ return [`${pythonName} = ${emitPyExprCtx(valueIR, ctx)}`];
746
+ }
747
+ function emitLetPy(node, ctx) {
748
+ const props = (node.props ?? {});
749
+ const name = String(props.name ?? '_');
750
+ validateBodyLetKind(props.kind);
751
+ declareLocalBinding(ctx, name, props.kind === 'let' ? 'let' : 'const');
752
+ const rawValue = props.value;
753
+ if (rawValue === undefined || rawValue === '') {
754
+ return [`${name} = None`];
755
+ }
756
+ const valueIR = parseExpression(String(rawValue));
757
+ setRegexBinding(ctx, name, valueIR.kind === 'regexLit' ? valueIR : null);
758
+ if (valueIR.kind === 'propagate' && valueIR.op === '?') {
759
+ rejectPropagationInsideTry(ctx);
760
+ const tmp = `__k_t${++ctx.gensymCounter}`;
761
+ const inner = emitPyExprCtx(valueIR.argument, ctx);
762
+ ctx.usedPropagation = true;
763
+ return [`${tmp} = ${inner}`, `if ${tmp}.kind == 'err':`, errPropagationLine(tmp, ctx), `${name} = ${tmp}.value`];
764
+ }
765
+ return [`${name} = ${emitPyExprCtx(valueIR, ctx)}`];
766
+ }
767
+ function validateBodyLetKind(rawKind) {
768
+ if (rawKind === undefined || rawKind === '' || rawKind === 'const' || rawKind === 'let')
769
+ return;
770
+ throw new Error('body-statement `let kind=` supports only `const` or `let`.');
771
+ }
772
+ function emitAssignPy(node, ctx) {
773
+ const props = (node.props ?? {});
774
+ const rawTarget = props.target;
775
+ const rawValue = props.value;
776
+ const rawOp = props.op === undefined || props.op === '' ? '=' : String(props.op);
777
+ if (rawTarget === undefined || rawTarget === '') {
778
+ throw new Error('body-statement `assign` requires `target=`.');
779
+ }
780
+ if (!isSupportedAssignOperator(rawOp)) {
781
+ throw new Error(`body-statement \`assign op=\` does not support \`${rawOp}\` on Python.`);
782
+ }
783
+ const isPostfix = isPostfixMutationOperator(rawOp);
784
+ if (!isPostfix && (rawValue === undefined || rawValue === '')) {
785
+ throw new Error('body-statement `assign` requires `value=`.');
786
+ }
787
+ if (isPostfix && rawValue !== undefined) {
788
+ // Reject ANY present `value` — including empty-string. Schema validator
789
+ // mirrors this; the emitter check is defense-in-depth for direct IR.
790
+ throw new Error(`body-statement \`assign op="${rawOp}"\` is value-less; remove \`value=\`.`);
791
+ }
792
+ const targetIR = parseExpression(String(rawTarget));
793
+ if (!isAssignableTarget(targetIR)) {
794
+ throw new Error('body-statement `assign target=` must be an identifier, member access, or index access.');
795
+ }
796
+ assertAssignableLocalTarget(targetIR, ctx);
797
+ // Python lacks `++` / `--`; lower postfix mutation to the canonical compound
798
+ // assignment (`X += 1` / `X -= 1`). The TS round-trip stays byte-equivalent
799
+ // because TS emits `X++;` from the same IR — only the Python target diverges
800
+ // textually, but no round-trip from Python source exists.
801
+ if (isPostfix) {
802
+ const baseOp = rawOp === '++' ? '+=' : '-=';
803
+ return [`${emitPyExprCtx(targetIR, ctx)} ${baseOp} 1`];
804
+ }
805
+ const valueIR = parseExpression(String(rawValue));
806
+ if (valueIR.kind === 'propagate') {
807
+ throw new Error(`Propagation \`${valueIR.op}\` is not supported in \`assign value=\` — bind to \`let\` first, then assign.`);
808
+ }
809
+ return [`${emitPyExprCtx(targetIR, ctx)} ${rawOp} ${emitPyExprCtx(valueIR, ctx)}`];
810
+ }
811
+ function declareLocalBinding(ctx, name, kind) {
812
+ const scope = ctx.localScopes.at(-1);
813
+ if (!scope)
814
+ return;
815
+ if (scope.has(name)) {
816
+ throw new Error(`body-statement local binding \`${name}\` is already declared in this scope.`);
817
+ }
818
+ scope.set(name, kind);
819
+ setRegexBinding(ctx, name, null);
820
+ }
821
+ function setRegexBinding(ctx, name, regex) {
822
+ ctx.regexScopes.at(-1)?.set(name, regex);
823
+ }
824
+ function lookupRegexBinding(ctx, name) {
825
+ for (let i = ctx.regexScopes.length - 1; i >= 0; i--) {
826
+ const scope = ctx.regexScopes[i];
827
+ if (scope.has(name))
828
+ return scope.get(name) ?? null;
829
+ }
830
+ return null;
831
+ }
832
+ function assertAssignableLocalTarget(target, ctx) {
833
+ if (target.kind !== 'ident')
834
+ return;
835
+ const bindingKind = lookupLocalBinding(ctx, target.name);
836
+ if (bindingKind === 'const') {
837
+ throw new Error(`body-statement \`assign target=${target.name}\` cannot reassign immutable \`let name=${target.name}\`; declare it with \`kind=let\`.`);
838
+ }
839
+ }
840
+ function lookupLocalBinding(ctx, name) {
841
+ for (let i = ctx.localScopes.length - 1; i >= 0; i--) {
842
+ const found = ctx.localScopes[i].get(name);
843
+ if (found)
844
+ return found;
845
+ }
846
+ return undefined;
847
+ }
848
+ function isAssignableTarget(node) {
849
+ if (node.kind === 'ident')
850
+ return true;
851
+ if (node.kind === 'member')
852
+ return !node.optional && !containsOptionalAccess(node.object);
853
+ if (node.kind === 'index')
854
+ return !node.optional && !containsOptionalAccess(node.object);
855
+ return false;
856
+ }
857
+ function containsOptionalAccess(node) {
858
+ if (node.kind === 'member')
859
+ return node.optional || containsOptionalAccess(node.object);
860
+ if (node.kind === 'index')
861
+ return node.optional || containsOptionalAccess(node.object);
862
+ if (node.kind === 'call')
863
+ return node.optional || containsOptionalAccess(node.callee);
864
+ if (node.kind === 'nonNull' || node.kind === 'typeAssert')
865
+ return containsOptionalAccess(node.expression);
866
+ return false;
867
+ }
868
+ function emitDestructurePy(node, ctx) {
869
+ const props = (node.props ?? {});
870
+ const rawSource = props.source;
871
+ if (rawSource === undefined || rawSource === '') {
872
+ throw new Error('body-statement `destructure` requires `source=`.');
873
+ }
874
+ const source = emitPyExprCtx(parseExpression(String(rawSource)), ctx);
875
+ const children = node.children ?? [];
876
+ const bindings = children.filter((c) => c.type === 'binding');
877
+ const elements = children.filter((c) => c.type === 'element');
878
+ if (bindings.length === 0 && elements.length === 0) {
879
+ throw new Error('body-statement `destructure` requires `binding` or `element` children.');
880
+ }
881
+ if (bindings.length > 0 && elements.length > 0) {
882
+ throw new Error('body-statement `destructure` cannot mix `binding` and `element` children.');
883
+ }
884
+ if (bindings.length > 0) {
885
+ const tmp = `__k_d${++ctx.gensymCounter}`;
886
+ const lines = [`${tmp} = ${source}`];
887
+ for (const child of bindings) {
888
+ const cp = (child.props ?? {});
889
+ const name = String(cp.name ?? '');
890
+ if (!name)
891
+ throw new Error('body-statement `binding` requires `name=`.');
892
+ const key = cp.key === undefined || cp.key === '' ? name : String(cp.key);
893
+ lines.push(`${ctx.symbolMap[name] ?? name} = ${tmp}.get(${JSON.stringify(key)})`);
894
+ }
895
+ return lines;
896
+ }
897
+ const tmp = `__k_d${++ctx.gensymCounter}`;
898
+ return [
899
+ `${tmp} = ${source}`,
900
+ ...elements
901
+ .map((child) => {
902
+ const cp = (child.props ?? {});
903
+ const name = String(cp.name ?? '');
904
+ if (!name)
905
+ throw new Error('body-statement `element` requires `name=`.');
906
+ const index = Number.parseInt(String(cp.index ?? ''), 10);
907
+ if (Number.isNaN(index))
908
+ throw new Error('body-statement `element` requires numeric `index=`.');
909
+ return {
910
+ index,
911
+ line: `${ctx.symbolMap[name] ?? name} = (${tmp}[${index}] if len(${tmp}) > ${index} else None)`,
912
+ };
913
+ })
914
+ .sort((a, b) => a.index - b.index)
915
+ .map((entry) => entry.line),
916
+ ];
917
+ }
918
+ function emitFmtPy(node, ctx) {
919
+ const props = (node.props ?? {});
920
+ const template = props.template;
921
+ if (template === undefined || template === null) {
922
+ throw new Error('body-statement `fmt` requires `template=`.');
923
+ }
924
+ const fstring = templateToPyFString(String(template), ctx);
925
+ const returnMode = props.return === true || props.return === 'true';
926
+ if (returnMode) {
927
+ if (props.name !== undefined && props.name !== '') {
928
+ throw new Error('body-statement `fmt` with `return=true` must not carry a `name=` prop.');
929
+ }
930
+ return [`return ${fstring}`];
931
+ }
932
+ if (props.name === undefined || props.name === '') {
933
+ throw new Error('body-statement `fmt` requires `name=` (or `return=true` for return-position form). Inline-JSX form is only valid as a direct child of `render`/`group`.');
934
+ }
935
+ const rawName = String(props.name);
936
+ const name = ctx.symbolMap[rawName] ?? rawName;
937
+ return [`${name} = ${fstring}`];
938
+ }
939
+ function templateToPyFString(template, ctx) {
940
+ // The `template=` body is raw TS template-literal source — i.e. the chars
941
+ // between the backticks in the original TS. Backslash escapes (`\n`, `\t`,
942
+ // `\xNN`, `\uNNNN`, `\\`) share semantics between TS and Python f-strings,
943
+ // so they pass through verbatim. Two TS-only escapes need translation:
944
+ //
945
+ // • `` \` `` → `` ` `` (Python doesn't escape backticks; emit literal)
946
+ // • `\${` → `${{` (TS-source escape for literal `${`; in a Python
947
+ // f-string we keep the `$` literal and double-brace
948
+ // the `{` so it renders as `${` at runtime)
949
+ //
950
+ // `${expr}` interpolation is lowered by translating the inner expression
951
+ // and emitting `{pyExpr}`. The brace-depth scanner is string-literal-aware
952
+ // (skips `}` inside `"…"` / `'…'`) to handle interpolations like
953
+ // `${fn("}")}` correctly. (Codex/Gemini/opencode plan-review fixes.)
954
+ let out = 'f"';
955
+ let i = 0;
956
+ while (i < template.length) {
957
+ const c = template[i];
958
+ if (c === '\\' && template[i + 1] !== undefined) {
959
+ const next = template[i + 1];
960
+ if (next === '`') {
961
+ // TS `` \` `` is a TS-source escape for a literal backtick. Python
962
+ // strings don't require this escape — emit the literal backtick.
963
+ out += '`';
964
+ i += 2;
965
+ continue;
966
+ }
967
+ if (next === '$' && template[i + 2] === '{') {
968
+ // TS `\${` escapes the interpolation marker; the runtime value is
969
+ // literal `${`. In a Python f-string, `${` renders by keeping the
970
+ // dollar and doubling the brace.
971
+ out += '${{';
972
+ i += 3;
973
+ continue;
974
+ }
975
+ if (next === '"') {
976
+ // TS `\"` is a literal `"`. Inside a Python `f"…"`, the `"` must be
977
+ // escaped — emit `\"`.
978
+ out += '\\"';
979
+ i += 2;
980
+ continue;
981
+ }
982
+ // All other escapes — `\n`, `\t`, `\r`, `\\`, `\xNN`, `\uNNNN`,
983
+ // `\0`, `\b`, `\f`, `\v` — share semantics. Pass through verbatim.
984
+ out += c + next;
985
+ i += 2;
986
+ continue;
987
+ }
988
+ if (c === '$' && template[i + 1] === '{') {
989
+ let depth = 1;
990
+ let j = i + 2;
991
+ let inString = null;
992
+ while (j < template.length && depth > 0) {
993
+ const ch = template[j];
994
+ if (inString !== null) {
995
+ if (ch === '\\' && j + 1 < template.length) {
996
+ j += 2;
997
+ continue;
998
+ }
999
+ if (ch === inString)
1000
+ inString = null;
1001
+ j++;
1002
+ continue;
1003
+ }
1004
+ if (ch === '"' || ch === "'" || ch === '`') {
1005
+ // Track ` so nested template literals like `${a.b(`x${c}`)}` don't
1006
+ // miscount braces inside the inner template. (Gemini impl-review fix.)
1007
+ inString = ch;
1008
+ j++;
1009
+ continue;
1010
+ }
1011
+ if (ch === '{')
1012
+ depth++;
1013
+ else if (ch === '}') {
1014
+ depth--;
1015
+ if (depth === 0)
1016
+ break;
1017
+ }
1018
+ j++;
1019
+ }
1020
+ if (depth !== 0) {
1021
+ throw new Error('body-statement `fmt`: unterminated `${...}` in template.');
1022
+ }
1023
+ const inner = template.slice(i + 2, j);
1024
+ const exprIR = parseExpression(inner);
1025
+ if (exprIR.kind === 'propagate') {
1026
+ throw new Error("Propagation '?' is not allowed inside an `fmt` template — bind via `let` first.");
1027
+ }
1028
+ out += `{${emitPyExprCtx(exprIR, ctx)}}`;
1029
+ i = j + 1;
1030
+ }
1031
+ else if (c === '{' || c === '}') {
1032
+ out += c + c;
1033
+ i++;
1034
+ }
1035
+ else if (c === '"') {
1036
+ out += '\\"';
1037
+ i++;
1038
+ }
1039
+ else {
1040
+ out += c;
1041
+ i++;
1042
+ }
1043
+ }
1044
+ out += '"';
1045
+ return out;
1046
+ }
1047
+ function emitCommentPy(node) {
1048
+ const props = (node.props ?? {});
1049
+ const raw = props.raw === undefined || props.raw === null ? '' : String(props.raw).trim();
1050
+ if (raw.startsWith('//'))
1051
+ return [`# ${raw.slice(2).trim()}`.trimEnd()];
1052
+ if (raw.startsWith('/*') && raw.endsWith('*/')) {
1053
+ return raw
1054
+ .slice(2, -2)
1055
+ .trim()
1056
+ .split(/\r?\n/)
1057
+ .map((line) => `# ${line.replace(/^\s*\*\s?/, '').trimEnd()}`.trimEnd());
1058
+ }
1059
+ const text = props.text === undefined || props.text === null ? '' : String(props.text);
1060
+ return text.split(/\r?\n/).map((line) => `# ${line}`.trimEnd());
1061
+ }
1062
+ function emitReturnPy(node, ctx) {
1063
+ const props = (node.props ?? {});
1064
+ const rawValue = props.value;
1065
+ if (rawValue === undefined || rawValue === '') {
1066
+ return [`return`];
1067
+ }
1068
+ const valueIR = parseExpression(String(rawValue));
1069
+ if (valueIR.kind === 'propagate' && valueIR.op === '?') {
1070
+ rejectPropagationInsideTry(ctx);
1071
+ const tmp = `__k_t${++ctx.gensymCounter}`;
1072
+ const inner = emitPyExprCtx(valueIR.argument, ctx);
1073
+ ctx.usedPropagation = true;
1074
+ return [`${tmp} = ${inner}`, `if ${tmp}.kind == 'err':`, errPropagationLine(tmp, ctx), `return ${tmp}.value`];
1075
+ }
1076
+ return [`return ${emitPyExprCtx(valueIR, ctx)}`];
1077
+ }
1078
+ function emitThrowPy(node, ctx) {
1079
+ const props = (node.props ?? {});
1080
+ const rawValue = props.value;
1081
+ if (rawValue === undefined || rawValue === '') {
1082
+ return [`raise Exception()`];
1083
+ }
1084
+ const valueIR = parseExpression(String(rawValue));
1085
+ if (valueIR.kind === 'propagate' && valueIR.op === '?') {
1086
+ rejectPropagationInsideTry(ctx);
1087
+ const tmp = `__k_t${++ctx.gensymCounter}`;
1088
+ const inner = emitPyExprCtx(valueIR.argument, ctx);
1089
+ ctx.usedPropagation = true;
1090
+ return [`${tmp} = ${inner}`, `if ${tmp}.kind == 'err':`, errPropagationLine(tmp, ctx), `raise ${tmp}.value`];
1091
+ }
1092
+ // TS allows `throw "msg"` / `throw 42` — Python `raise X` requires X to be
1093
+ // a BaseException subclass, otherwise raises TypeError. Wrap literal
1094
+ // values in `Exception(...)` so the cross-target lowering matches user
1095
+ // expectations. Calls (`new Error(...)`, `MyError(...)`) and identifiers
1096
+ // (could be a caught exception var) pass through unwrapped.
1097
+ if (NON_EXCEPTION_LITERAL_KINDS.has(valueIR.kind)) {
1098
+ return [`raise Exception(${emitPyExprCtx(valueIR, ctx)})`];
1099
+ }
1100
+ return [`raise ${emitPyExprCtx(valueIR, ctx)}`];
1101
+ }
1102
+ function emitDoPy(node, ctx) {
1103
+ const props = (node.props ?? {});
1104
+ const rawValue = props.value;
1105
+ if (rawValue === undefined || rawValue === '') {
1106
+ return [];
1107
+ }
1108
+ const valueIR = parseExpression(String(rawValue));
1109
+ if (valueIR.kind === 'propagate' && valueIR.op === '?') {
1110
+ rejectPropagationInsideTry(ctx);
1111
+ const tmp = `__k_t${++ctx.gensymCounter}`;
1112
+ const inner = emitPyExprCtx(valueIR.argument, ctx);
1113
+ ctx.usedPropagation = true;
1114
+ return [`${tmp} = ${inner}`, `if ${tmp}.kind == 'err':`, errPropagationLine(tmp, ctx)];
1115
+ }
1116
+ return [`${emitPyExprCtx(valueIR, ctx)}`];
1117
+ }
1118
+ /** ValueIR `kind`s that lower to Python literals/values and would trigger
1119
+ * `TypeError: exceptions must derive from BaseException` if `raise`d
1120
+ * directly. Calls / new / member access / identifiers are NOT in this
1121
+ * set — they could legitimately be Exception subclasses. */
1122
+ const NON_EXCEPTION_LITERAL_KINDS = new Set([
1123
+ 'numLit',
1124
+ 'strLit',
1125
+ 'boolLit',
1126
+ 'nullLit',
1127
+ 'undefLit',
1128
+ 'objectLit',
1129
+ 'arrayLit',
1130
+ 'tmplLit',
1131
+ 'regexLit',
1132
+ ]);
1133
+ /** Slice-1 ValueIR → Python expression. Covers the surface that body-ts.ts
1134
+ * emits today; later slices extend per the spec.
1135
+ *
1136
+ * Slice 3 — accepts an `options` bag so callers can supply a `symbolMap`
1137
+ * (3a) without having to construct a `BodyEmitContext` directly. Imports
1138
+ * are still collected during the walk but are not surfaced to the caller
1139
+ * via this entry point — use `emitNativeKernBodyPythonWithImports` when
1140
+ * you need the imports set. The internal recursive callers go through
1141
+ * `emitPyExprCtx` which threads the live ctx (and therefore the live
1142
+ * imports set) end-to-end. */
1143
+ export function emitPyExpression(node, options) {
1144
+ return emitPyExprCtx(node, freshCtx(options));
1145
+ }
1146
+ function emitPyExprCtx(node, ctx) {
1147
+ switch (node.kind) {
1148
+ case 'numLit':
1149
+ return node.raw;
1150
+ case 'strLit': {
1151
+ const escaped = node.value
1152
+ .replace(/\\/g, '\\\\')
1153
+ .replace(/"/g, '\\"')
1154
+ .replace(/\n/g, '\\n')
1155
+ .replace(/\r/g, '\\r')
1156
+ .replace(/\t/g, '\\t');
1157
+ return `"${escaped}"`;
1158
+ }
1159
+ case 'boolLit':
1160
+ return node.value ? 'True' : 'False';
1161
+ case 'nullLit':
1162
+ return 'None';
1163
+ case 'undefLit':
1164
+ return 'None';
1165
+ case 'regexLit':
1166
+ ctx.imports.add('re');
1167
+ return `__k_re.compile(${pyRegexPattern(node)}, ${pyRegexFlags(node.flags, { allowGlobal: true })})`;
1168
+ case 'ident':
1169
+ // Slice 3a — apply symbol-map rename so KERN-form `userId` becomes
1170
+ // Python-form `user_id`. Identifiers not in the map (locals, globals,
1171
+ // module names) pass through unchanged.
1172
+ if (ctx.shadowedSymbols.has(node.name))
1173
+ return node.name;
1174
+ return ctx.symbolMap[node.name] ?? node.name;
1175
+ case 'member':
1176
+ case 'call':
1177
+ case 'index': {
1178
+ // Slice 3d (review fix — Codex critical): optional chains short-circuit
1179
+ // the ENTIRE trailing expression after `?.`, not just the immediate
1180
+ // access. So `user?.profile.name` must lower to
1181
+ // `(user.profile.name if user is not None else None)` — not
1182
+ // `(user.profile if user is not None else None).name`, which would
1183
+ // raise `AttributeError` on a None receiver.
1184
+ //
1185
+ // To carry the trailing chain into the guarded branch, member/call
1186
+ // emit goes through `lowerMemberOrCall` which returns
1187
+ // `{ guard, expr }`. The guard accumulates `is not None` tests
1188
+ // collected from each `?.` link in the receiver chain; the expr
1189
+ // appends each `.prop` / `(...args)` link to the unguarded form.
1190
+ // The top-level wrapper produces `(expr if guard else None)` once
1191
+ // (or just `expr` when no `?.` was seen).
1192
+ const lowered = lowerChain(node, ctx);
1193
+ return wrapGuardIfAny(lowered);
1194
+ }
1195
+ case 'await':
1196
+ return `await ${emitPyExprCtx(node.argument, ctx)}`;
1197
+ case 'new':
1198
+ return emitPyExprCtx(node.argument, ctx);
1199
+ case 'typeAssert':
1200
+ return emitPyExprCtx(node.expression, ctx);
1201
+ case 'nonNull':
1202
+ return emitPyExprCtx(node.expression, ctx);
1203
+ case 'tmplLit': {
1204
+ // Lower TS template literals to Python f-strings.
1205
+ let out = 'f"';
1206
+ for (let i = 0; i < node.quasis.length; i++) {
1207
+ out += node.quasis[i]
1208
+ .replace(/\\/g, '\\\\')
1209
+ .replace(/"/g, '\\"')
1210
+ .replace(/\n/g, '\\n')
1211
+ .replace(/\{/g, '{{')
1212
+ .replace(/\}/g, '}}');
1213
+ if (i < node.expressions.length)
1214
+ out += `{${emitPyExprCtx(node.expressions[i], ctx)}}`;
1215
+ }
1216
+ out += '"';
1217
+ return out;
1218
+ }
1219
+ case 'binary': {
1220
+ // Slice 2c — arithmetic / comparison / logical lowering for Python.
1221
+ // Use precedence-aware paren-wrapping so `a + b * c` doesn't redundantly
1222
+ // wrap the right side (`a + (b * c)`) — same rule as the TS side.
1223
+ //
1224
+ // Slice-2 review fix: Python chains comparisons (`a == b < c` means
1225
+ // `(a == b) and (b < c)`), but TS evaluates left-to-right with strict
1226
+ // precedence. To preserve KERN's TS-flavored AST semantics on the
1227
+ // Python target, force parens around comparison children whose op is
1228
+ // ALSO a comparison — that disables Python's chaining and yields the
1229
+ // expected `(a == b) < c` evaluation order.
1230
+ const left = emitPyExprCtx(node.left, ctx);
1231
+ const right = emitPyExprCtx(node.right, ctx);
1232
+ if (node.op === '??') {
1233
+ // Slice 4c — nullish coalesce lowering. Two shapes:
1234
+ //
1235
+ // (a) Pure left side (ident or non-optional member chain rooted
1236
+ // at ident) — re-evaluating the expression in both the test
1237
+ // and the result branch is side-effect-free, so emit the
1238
+ // readable double-name form:
1239
+ // `(L if L is not None else R)`
1240
+ //
1241
+ // (b) Non-pure left side (call / await / binary / etc.) — single-
1242
+ // eval is required so we use Python's walrus operator
1243
+ // (PEP 572, Python 3.8+) to bind the result inline:
1244
+ // `(__k_nc1 if (__k_nc1 := L) is not None else R)`
1245
+ // Python evaluates the walrus assignment expression FIRST
1246
+ // (single eval of L → bound to __k_nc1), tests for None, and
1247
+ // returns __k_nc1 or R. The gensym counter shares with the
1248
+ // propagation hoist (`__k_t…`) — distinct prefix prevents
1249
+ // any name collision.
1250
+ //
1251
+ // Slice 4c (post-buddy-review) was the easy-win expansion after the
1252
+ // 22.7% empirical-gate scan; this lifts the slice-2 `??` throw and
1253
+ // adds an estimated +7% to native eligibility on Agon-AI bodies.
1254
+ if (isReceiverChainPure(node.left)) {
1255
+ return `(${left} if ${left} is not None else ${right})`;
1256
+ }
1257
+ const tmp = `__k_nc${++ctx.gensymCounter}`;
1258
+ return `(${tmp} if (${tmp} := ${left}) is not None else ${right})`;
1259
+ }
1260
+ const forceLeft = needsComparisonChainParens(node.left, node.op);
1261
+ const forceRight = needsComparisonChainParens(node.right, node.op);
1262
+ const lp = forceLeft || needsBinaryParens(node.left, node.op, 'left') ? `(${left})` : left;
1263
+ const rp = forceRight || needsBinaryParens(node.right, node.op, 'right') ? `(${right})` : right;
1264
+ const op = mapBinaryOpToPython(node.op);
1265
+ return `${lp} ${op} ${rp}`;
1266
+ }
1267
+ case 'unary': {
1268
+ // Slice 2c — `!x` → `not x`, `-x` → `-x`.
1269
+ // Slice typeof — expose the now-eligible native KERN `typeof` shape on
1270
+ // Python too. Dynamic Python values are an approximation of JS typeof:
1271
+ // Python has no runtime `undefined`, `symbol`, or bigint distinction.
1272
+ if (node.op === 'typeof')
1273
+ return emitPyTypeof(node.argument, ctx);
1274
+ const arg = emitPyExprCtx(node.argument, ctx);
1275
+ const wrapped = needsArgParens(node.argument) ? `(${arg})` : arg;
1276
+ if (node.op === '!')
1277
+ return `not ${wrapped}`;
1278
+ if (node.op === '-')
1279
+ return `-${wrapped}`;
1280
+ if (node.op === '+')
1281
+ return `+${wrapped}`;
1282
+ throw new Error(`emitPyExpression: unary op '${node.op}' has no Python equivalent in slice-2c.`);
1283
+ }
1284
+ case 'objectLit': {
1285
+ // Slice 2d — Python dict literal. Keys are ALWAYS double-quoted (no
1286
+ // shorthand-key syntax in Python).
1287
+ const entries = node.entries.map((e) => {
1288
+ if ('kind' in e && e.kind === 'spread') {
1289
+ return `**${emitPyExprCtx(e.argument, ctx)}`;
1290
+ }
1291
+ const prop = e;
1292
+ return `${JSON.stringify(prop.key)}: ${emitPyExprCtx(prop.value, ctx)}`;
1293
+ });
1294
+ return `{${entries.join(', ')}}`;
1295
+ }
1296
+ case 'arrayLit':
1297
+ return `[${node.items.map((i) => emitPyExprCtx(i, ctx)).join(', ')}]`;
1298
+ case 'lambda':
1299
+ return emitLambdaPy(node, ctx);
1300
+ case 'conditional': {
1301
+ // Slice α-2: TS `test ? consequent : alternate` lowers to Python's
1302
+ // expression-form conditional `consequent if test else alternate`
1303
+ // (operand reorder). Lowest-precedence in Python expressions, so
1304
+ // paren-wrap binary/unary children for safety.
1305
+ const testStr = emitPyExprCtx(node.test, ctx);
1306
+ const consStr = emitPyExprCtx(node.consequent, ctx);
1307
+ const altStr = emitPyExprCtx(node.alternate, ctx);
1308
+ const wrap = (child, emitted) => {
1309
+ switch (child.kind) {
1310
+ case 'binary':
1311
+ case 'unary':
1312
+ case 'spread':
1313
+ case 'await':
1314
+ case 'new':
1315
+ case 'conditional':
1316
+ return `(${emitted})`;
1317
+ default:
1318
+ return emitted;
1319
+ }
1320
+ };
1321
+ return `${wrap(node.consequent, consStr)} if ${wrap(node.test, testStr)} else ${wrap(node.alternate, altStr)}`;
1322
+ }
1323
+ case 'spread':
1324
+ return `*${emitPyExprCtx(node.argument, ctx)}`;
1325
+ case 'propagate':
1326
+ throw new Error(`Propagation '${node.op}' is only allowed at statement level (top of \`let value=\` or \`return value=\`). ` +
1327
+ `Mid-expression \`${node.op}\` is rejected — bind the call to a \`let\` first, then use the bound name.`);
1328
+ }
1329
+ }
1330
+ function emitPyTypeof(argument, ctx) {
1331
+ switch (argument.kind) {
1332
+ case 'strLit':
1333
+ return '"string"';
1334
+ case 'boolLit':
1335
+ return '"boolean"';
1336
+ case 'numLit':
1337
+ return argument.bigint ? '"bigint"' : '"number"';
1338
+ case 'undefLit':
1339
+ return '"undefined"';
1340
+ case 'nullLit':
1341
+ return '"object"';
1342
+ case 'lambda':
1343
+ return '"function"';
1344
+ case 'arrayLit':
1345
+ case 'objectLit':
1346
+ case 'regexLit':
1347
+ return '"object"';
1348
+ case 'tmplLit':
1349
+ return '"string"';
1350
+ default:
1351
+ break;
1352
+ }
1353
+ const value = emitPyExprCtx(argument, ctx);
1354
+ const wrapped = needsArgParens(argument) ? `(${value})` : value;
1355
+ const tmp = `__k_typeof${++ctx.gensymCounter}`;
1356
+ return (`("object" if (${tmp} := ${wrapped}) is None ` +
1357
+ `else "boolean" if isinstance(${tmp}, bool) ` +
1358
+ `else "number" if isinstance(${tmp}, (int, float)) ` +
1359
+ `else "string" if isinstance(${tmp}, str) ` +
1360
+ `else "function" if callable(${tmp}) ` +
1361
+ `else "object")`);
1362
+ }
1363
+ function emitLambdaPy(node, ctx) {
1364
+ const names = node.params.map((p) => p.name);
1365
+ const previous = new Set(ctx.shadowedSymbols);
1366
+ for (const name of names)
1367
+ ctx.shadowedSymbols.add(name);
1368
+ try {
1369
+ const params = names.length === 0 ? '' : ` ${names.join(', ')}`;
1370
+ return `lambda${params}: ${emitPyExprCtx(node.body, ctx)}`;
1371
+ }
1372
+ finally {
1373
+ ctx.shadowedSymbols = previous;
1374
+ }
1375
+ }
1376
+ function lowerChain(node, ctx) {
1377
+ if (node.kind === 'member') {
1378
+ const obj = node.object;
1379
+ const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
1380
+ ? lowerChain(obj, ctx)
1381
+ : { guard: null, expr: emitPyExprCtx(obj, ctx) };
1382
+ if (node.optional) {
1383
+ // The receiver expression names what we need to test. The expr names
1384
+ // the receiver twice (once in test, once in branch); reject when that
1385
+ // would re-evaluate side-effecting code.
1386
+ if (!isReceiverChainPure(node.object)) {
1387
+ throw new Error("Optional chain '?.' on Python target requires a side-effect-free receiver (identifier or pure member chain). " +
1388
+ 'Bind the call/await result to a `let` first, then use `let.field?.next` on the bound name.');
1389
+ }
1390
+ const newGuard = inner.guard === null ? `${inner.expr} is not None` : `${inner.guard} and ${inner.expr} is not None`;
1391
+ return { guard: newGuard, expr: `${inner.expr}.${node.property}` };
1392
+ }
1393
+ return { guard: inner.guard, expr: `${inner.expr}.${node.property}` };
1394
+ }
1395
+ if (node.kind === 'index') {
1396
+ const obj = node.object;
1397
+ const inner = obj.kind === 'member' || obj.kind === 'call' || obj.kind === 'index'
1398
+ ? lowerChain(obj, ctx)
1399
+ : { guard: null, expr: emitPyExprCtx(obj, ctx) };
1400
+ const index = emitPyExprCtx(node.index, ctx);
1401
+ if (node.optional) {
1402
+ // The Python lowering names the receiver in the guard and the branch.
1403
+ // Keep that single-eval-safe by requiring a pure receiver. The index
1404
+ // expression appears only in the selected branch, matching JS `?.[]`.
1405
+ if (!isReceiverChainPure(node.object)) {
1406
+ throw new Error("Optional element access '?.[]' on Python target requires a side-effect-free receiver. " +
1407
+ 'Bind call/await receiver results to `let` first, then index the bound name.');
1408
+ }
1409
+ const newGuard = inner.guard === null ? `${inner.expr} is not None` : `${inner.guard} and ${inner.expr} is not None`;
1410
+ return { guard: newGuard, expr: `${inner.expr}[${index}]` };
1411
+ }
1412
+ const wrapped = needsIndexReceiverParens(node.object) ? `(${inner.expr})` : inner.expr;
1413
+ return { guard: inner.guard, expr: `${wrapped}[${index}]` };
1414
+ }
1415
+ // node.kind === 'call'
1416
+ if (node.optional) {
1417
+ throw new Error("Optional call '?.()' is not yet supported on Python target. " +
1418
+ 'Bind the function reference to a `let` first, then test for `none` before calling.');
1419
+ }
1420
+ // Slice 2a — KERN-stdlib dispatch must run on a top-level Module.method
1421
+ // call BEFORE we descend into the callee chain, so `Number.floor(x)`
1422
+ // doesn't degrade into a non-stdlib `Number.floor(x)` Python emit.
1423
+ const regex = lowerRegexCallPython(node, ctx);
1424
+ if (regex !== null)
1425
+ return { guard: null, expr: regex };
1426
+ const stdlib = applyStdlibLoweringPython(node, ctx);
1427
+ if (stdlib !== null)
1428
+ return { guard: null, expr: stdlib };
1429
+ const callee = node.callee;
1430
+ const inner = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index'
1431
+ ? lowerChain(callee, ctx)
1432
+ : { guard: null, expr: emitPyExprCtx(callee, ctx) };
1433
+ const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', ');
1434
+ return { guard: inner.guard, expr: `${inner.expr}(${args})` };
1435
+ }
1436
+ function lowerRegexCallPython(call, ctx) {
1437
+ const callee = call.callee;
1438
+ if (callee.kind !== 'member')
1439
+ return null;
1440
+ const receiverRegex = resolveRegexExpr(callee.object, ctx);
1441
+ if (callee.property === 'test' && receiverRegex !== null) {
1442
+ if (call.args.length !== 1)
1443
+ return null;
1444
+ if (receiverRegex.flags.includes('g')) {
1445
+ 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.");
1446
+ }
1447
+ ctx.imports.add('re');
1448
+ return `(__k_re.search(${pyRegexPattern(receiverRegex)}, ${emitPyExprCtx(call.args[0], ctx)}, ${pyRegexFlags(receiverRegex.flags)}) is not None)`;
1449
+ }
1450
+ const matchRegex = call.args.length === 1 ? resolveRegexExpr(call.args[0], ctx) : null;
1451
+ if (callee.property === 'match' && matchRegex !== null) {
1452
+ if (matchRegex.flags.includes('g')) {
1453
+ 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.');
1454
+ }
1455
+ ctx.imports.add('re');
1456
+ return `__k_re.search(${pyRegexPattern(matchRegex)}, ${emitPyExprCtx(callee.object, ctx)}, ${pyRegexFlags(matchRegex.flags)})`;
1457
+ }
1458
+ const replaceRegex = call.args.length === 2 ? resolveRegexExpr(call.args[0], ctx) : null;
1459
+ if (callee.property === 'replace' && replaceRegex !== null) {
1460
+ ctx.imports.add('re');
1461
+ const count = replaceRegex.flags.includes('g') ? '0' : '1';
1462
+ return `__k_re.sub(${pyRegexPattern(replaceRegex)}, ${emitPyExprCtx(call.args[1], ctx)}, ${emitPyExprCtx(callee.object, ctx)}, count=${count}, flags=${pyRegexFlags(replaceRegex.flags, { allowGlobal: true })})`;
1463
+ }
1464
+ return null;
1465
+ }
1466
+ function resolveRegexExpr(node, ctx) {
1467
+ if (node.kind === 'regexLit')
1468
+ return node;
1469
+ if (node.kind === 'ident')
1470
+ return lookupRegexBinding(ctx, node.name);
1471
+ return null;
1472
+ }
1473
+ function pyRegexPattern(node) {
1474
+ // JS escapes `/` because it delimits the literal; Python string regexes do not
1475
+ // treat `/` specially, so preserve the semantic pattern without that escape.
1476
+ return JSON.stringify(node.pattern.replace(/\\\//g, '/'));
1477
+ }
1478
+ function pyRegexFlags(flags, options = {}) {
1479
+ const unsupported = [...flags].filter((f) => {
1480
+ if (f === 'i' || f === 'm' || f === 's')
1481
+ return false;
1482
+ if (f === 'g' && options.allowGlobal)
1483
+ return false;
1484
+ return true;
1485
+ });
1486
+ if (unsupported.length > 0) {
1487
+ throw new Error(`Python target does not lower regex flag(s) '${unsupported.join('')}'. Supported flags are i, m, s` +
1488
+ (options.allowGlobal ? ', plus g where the call shape gives it JS-compatible meaning.' : '.'));
1489
+ }
1490
+ const parts = [];
1491
+ if (flags.includes('i'))
1492
+ parts.push('__k_re.IGNORECASE');
1493
+ if (flags.includes('m'))
1494
+ parts.push('__k_re.MULTILINE');
1495
+ if (flags.includes('s'))
1496
+ parts.push('__k_re.DOTALL');
1497
+ return parts.length > 0 ? parts.join(' | ') : '0';
1498
+ }
1499
+ function wrapGuardIfAny(g) {
1500
+ return g.guard === null ? g.expr : `(${g.expr} if ${g.guard} else None)`;
1501
+ }
1502
+ function needsIndexReceiverParens(child) {
1503
+ return (child.kind === 'binary' ||
1504
+ child.kind === 'conditional' ||
1505
+ child.kind === 'unary' ||
1506
+ child.kind === 'spread' ||
1507
+ child.kind === 'typeAssert' ||
1508
+ child.kind === 'nonNull' ||
1509
+ child.kind === 'await' ||
1510
+ child.kind === 'lambda');
1511
+ }
1512
+ /** Slice 3d (review fix) — receiver-purity walk for the optional-chain
1513
+ * short-circuit lowering. Pure means: no observable side effects when
1514
+ * re-named twice (once in the `is not None` guard, once in the branch).
1515
+ *
1516
+ * Pure: `ident`, member chains rooted at `ident` (whether optional or
1517
+ * not — repeated attribute access on `None` raises but never silently
1518
+ * side-effects), and index chains rooted at pure receivers with pure index
1519
+ * expressions. NOT pure: `call`, `await`, `binary`, `unary`, `propagate`,
1520
+ * non-index literals (which are technically pure but never sensible
1521
+ * receivers). */
1522
+ function isReceiverChainPure(node) {
1523
+ if (node.kind === 'ident')
1524
+ return true;
1525
+ if (node.kind === 'member')
1526
+ return isReceiverChainPure(node.object);
1527
+ if (node.kind === 'index')
1528
+ return isReceiverChainPure(node.object) && isPureIndexExpression(node.index);
1529
+ return false;
1530
+ }
1531
+ function isPureIndexExpression(node) {
1532
+ switch (node.kind) {
1533
+ case 'ident':
1534
+ case 'numLit':
1535
+ case 'strLit':
1536
+ case 'boolLit':
1537
+ case 'nullLit':
1538
+ case 'undefLit':
1539
+ return true;
1540
+ case 'member':
1541
+ return isReceiverChainPure(node);
1542
+ case 'index':
1543
+ return isReceiverChainPure(node);
1544
+ default:
1545
+ return false;
1546
+ }
1547
+ }
1548
+ const COMPARISON_OPS = new Set(['==', '!=', '===', '!==', '<', '<=', '>', '>=']);
1549
+ /** Slice-2 review fix — Python chains comparisons by default. When the parent
1550
+ * binary op is a comparison and the child is also a (different) comparison
1551
+ * binary, force parens to preserve KERN's TS-flavored left-associative AST. */
1552
+ function needsComparisonChainParens(child, parentOp) {
1553
+ if (!COMPARISON_OPS.has(parentOp))
1554
+ return false;
1555
+ if (child.kind !== 'binary')
1556
+ return false;
1557
+ if (typeof child.op !== 'string')
1558
+ return false;
1559
+ return COMPARISON_OPS.has(child.op);
1560
+ }
1561
+ /** Slice 2c — map KERN/TS-flavored binary ops to Python equivalents.
1562
+ * KERN inherits TS's `===` / `!==` strict-equality syntax; Python uses
1563
+ * `==` / `!=` for the equivalent value-equality semantics on primitives.
1564
+ * `??` (nullish coalesce) has no Python equivalent and slice 3 introduces
1565
+ * a single-eval `(L if L is not None else R)` lowering. Slice 2 throws
1566
+ * rather than emit invalid syntax (review fix). */
1567
+ function mapBinaryOpToPython(op) {
1568
+ switch (op) {
1569
+ case '===':
1570
+ return '==';
1571
+ case '!==':
1572
+ return '!=';
1573
+ case '&&':
1574
+ return 'and';
1575
+ case '||':
1576
+ return 'or';
1577
+ default:
1578
+ return op;
1579
+ }
1580
+ }
1581
+ /** Slice 2a — KERN-stdlib dispatch for Python. Returns the lowered Python
1582
+ * string when the call matches `<KnownModule>.<method>(args)`, or null when
1583
+ * it doesn't. Throws on `<KnownModule>.<unknownMethod>(...)` with a
1584
+ * did-you-mean suggestion. Mirror of `applyStdlibLoweringTS` in core.
1585
+ *
1586
+ * Slice 3b — when the matched entry declares `requires.py`, the import
1587
+ * identifier is added to the per-handler ctx.imports set so the FastAPI
1588
+ * generator can emit `import math` (etc.) at the top of the function body. */
1589
+ function applyStdlibLoweringPython(call, ctx) {
1590
+ const callee = call.callee;
1591
+ if (callee.kind !== 'member')
1592
+ return null;
1593
+ if (callee.object.kind !== 'ident')
1594
+ return null;
1595
+ const moduleName = callee.object.name;
1596
+ if (!KERN_STDLIB_MODULES.has(moduleName))
1597
+ return null;
1598
+ const methodName = callee.property;
1599
+ const entry = lookupStdlib(moduleName, methodName);
1600
+ if (entry === null) {
1601
+ const suggestion = suggestStdlibMethod(moduleName, methodName);
1602
+ const hint = suggestion ? ` Did you mean '${moduleName}.${suggestion}'?` : '';
1603
+ throw new Error(`Unknown KERN-stdlib method '${moduleName}.${methodName}'.${hint}`);
1604
+ }
1605
+ // Slice-2 review fix: enforce declared arity (matches TS-side check).
1606
+ if (call.args.length !== entry.arity) {
1607
+ throw new Error(`KERN-stdlib '${moduleName}.${methodName}' takes ${entry.arity} arg${entry.arity === 1 ? '' : 's'}, got ${call.args.length}.`);
1608
+ }
1609
+ const listLambda = lowerListLambdaPython(moduleName, methodName, call, ctx);
1610
+ if (listLambda !== null)
1611
+ return listLambda;
1612
+ // Slice 3b — register required imports (e.g., `Number.floor` ⇒ `import math`).
1613
+ if (entry.requires?.py)
1614
+ ctx.imports.add(entry.requires.py);
1615
+ const args = call.args.map((a) => {
1616
+ const emitted = emitPyExprCtx(a, ctx);
1617
+ return needsArgParens(a) ? `(${emitted})` : emitted;
1618
+ });
1619
+ return applyTemplate(entry.py, args);
1620
+ }
1621
+ function lowerListLambdaPython(moduleName, methodName, call, ctx) {
1622
+ if (moduleName !== 'List')
1623
+ return null;
1624
+ if (methodName !== 'map' && methodName !== 'filter')
1625
+ return null;
1626
+ const source = emitPyExprCtx(call.args[0], ctx);
1627
+ const callback = call.args[1];
1628
+ if (callback.kind !== 'lambda') {
1629
+ const fn = emitPyExprCtx(callback, ctx);
1630
+ return methodName === 'map' ? `list(map(${fn}, ${source}))` : `list(filter(${fn}, ${source}))`;
1631
+ }
1632
+ if (callback.params.length !== 1) {
1633
+ throw new Error(`List.${methodName} expects a one-parameter lambda on the Python target.`);
1634
+ }
1635
+ const name = callback.params[0].name;
1636
+ const previous = new Set(ctx.shadowedSymbols);
1637
+ ctx.shadowedSymbols.add(name);
1638
+ try {
1639
+ const body = emitPyExprCtx(callback.body, ctx);
1640
+ return methodName === 'map'
1641
+ ? `[${body} for ${name} in ${source}]`
1642
+ : `[${name} for ${name} in ${source} if ${body}]`;
1643
+ }
1644
+ finally {
1645
+ ctx.shadowedSymbols = previous;
1646
+ }
1647
+ }
1648
+ //# sourceMappingURL=codegen-body-python.js.map