@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.
- package/LICENSE +678 -0
- package/README.md +26 -0
- package/dist/codegen-body-python.d.ts +152 -0
- package/dist/codegen-body-python.js +1648 -0
- package/dist/codegen-body-python.js.map +1 -0
- package/dist/codegen-helpers.d.ts +21 -0
- package/dist/codegen-helpers.js +352 -0
- package/dist/codegen-helpers.js.map +1 -0
- package/dist/codegen-python.d.ts +17 -0
- package/dist/codegen-python.js +106 -0
- package/dist/codegen-python.js.map +1 -0
- package/dist/fastapi-middleware.d.ts +8 -0
- package/dist/fastapi-middleware.js +87 -0
- package/dist/fastapi-middleware.js.map +1 -0
- package/dist/fastapi-portable.d.ts +9 -0
- package/dist/fastapi-portable.js +295 -0
- package/dist/fastapi-portable.js.map +1 -0
- package/dist/fastapi-raw-handler.d.ts +28 -0
- package/dist/fastapi-raw-handler.js +282 -0
- package/dist/fastapi-raw-handler.js.map +1 -0
- package/dist/fastapi-response.d.ts +13 -0
- package/dist/fastapi-response.js +150 -0
- package/dist/fastapi-response.js.map +1 -0
- package/dist/fastapi-route.d.ts +12 -0
- package/dist/fastapi-route.js +629 -0
- package/dist/fastapi-route.js.map +1 -0
- package/dist/fastapi-types.d.ts +39 -0
- package/dist/fastapi-types.js +5 -0
- package/dist/fastapi-types.js.map +1 -0
- package/dist/fastapi-utils.d.ts +16 -0
- package/dist/fastapi-utils.js +99 -0
- package/dist/fastapi-utils.js.map +1 -0
- package/dist/fastapi-websocket.d.ts +6 -0
- package/dist/fastapi-websocket.js +77 -0
- package/dist/fastapi-websocket.js.map +1 -0
- package/dist/generators/core.d.ts +23 -0
- package/dist/generators/core.js +906 -0
- package/dist/generators/core.js.map +1 -0
- package/dist/generators/data.d.ts +15 -0
- package/dist/generators/data.js +443 -0
- package/dist/generators/data.js.map +1 -0
- package/dist/generators/ground.d.ts +20 -0
- package/dist/generators/ground.js +333 -0
- package/dist/generators/ground.js.map +1 -0
- package/dist/generators/infra.d.ts +8 -0
- package/dist/generators/infra.js +109 -0
- package/dist/generators/infra.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/ir-semantics/python-leg.d.ts +45 -0
- package/dist/ir-semantics/python-leg.js +291 -0
- package/dist/ir-semantics/python-leg.js.map +1 -0
- package/dist/python-stdlib-preamble.d.ts +32 -0
- package/dist/python-stdlib-preamble.js +86 -0
- package/dist/python-stdlib-preamble.js.map +1 -0
- package/dist/transpiler-fastapi.d.ts +8 -0
- package/dist/transpiler-fastapi.js +593 -0
- package/dist/transpiler-fastapi.js.map +1 -0
- package/dist/type-map.d.ts +14 -0
- package/dist/type-map.js +288 -0
- package/dist/type-map.js.map +1 -0
- 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
|