@kernlang/python 3.5.6 → 3.5.8-canary.204.1.43495cde

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 (37) hide show
  1. package/dist/adapters/django.d.ts +49 -0
  2. package/dist/adapters/django.js +151 -0
  3. package/dist/adapters/django.js.map +1 -0
  4. package/dist/adapters/fastapi.d.ts +43 -0
  5. package/dist/adapters/fastapi.js +139 -0
  6. package/dist/adapters/fastapi.js.map +1 -0
  7. package/dist/codegen-body-python.d.ts +17 -5
  8. package/dist/codegen-body-python.js +83 -67
  9. package/dist/codegen-body-python.js.map +1 -1
  10. package/dist/core/expr/helpers.d.ts +5 -0
  11. package/dist/core/expr/helpers.js +62 -0
  12. package/dist/core/expr/helpers.js.map +1 -0
  13. package/dist/core/expr/index.d.ts +9 -0
  14. package/dist/core/expr/index.js +2046 -0
  15. package/dist/core/expr/index.js.map +1 -0
  16. package/dist/core/handlers/index.d.ts +74 -0
  17. package/dist/core/handlers/index.js +462 -0
  18. package/dist/core/handlers/index.js.map +1 -0
  19. package/dist/fastapi-portable.js +3 -2
  20. package/dist/fastapi-portable.js.map +1 -1
  21. package/dist/fastapi-response.d.ts +0 -6
  22. package/dist/fastapi-response.js +2 -2217
  23. package/dist/fastapi-response.js.map +1 -1
  24. package/dist/fastapi-route.js +24 -3
  25. package/dist/fastapi-route.js.map +1 -1
  26. package/dist/fastapi-utils.d.ts +2 -1
  27. package/dist/fastapi-utils.js +2 -58
  28. package/dist/fastapi-utils.js.map +1 -1
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/ir-semantics/python-leg.js +5 -0
  33. package/dist/ir-semantics/python-leg.js.map +1 -1
  34. package/dist/targets/python.d.ts +27 -0
  35. package/dist/targets/python.js +130 -4
  36. package/dist/targets/python.js.map +1 -1
  37. package/package.json +2 -2
@@ -2,14 +2,10 @@
2
2
  * Response helpers for the FastAPI transpiler.
3
3
  *
4
4
  * generateRespondFastAPI — IR respond node → Python return/raise statements
5
- * rewriteFastAPIExpr — rewrite portable request references to FastAPI equivalents
6
- * extractExprCode — extract expression code from IR prop
7
5
  * addRespondImports — add necessary imports for respond node
8
6
  */
9
- import { getProps, lowerJsClosureBodyToPython } from '@kernlang/core';
10
- import { KERN_I32_HELPER_PY, KERN_JS_HELPER_PY, KERN_TMOD_HELPER_PY } from './codegen-body-python.js';
11
- import { escapePyStr, quoteObjectKeysOutsideStrings } from './fastapi-utils.js';
12
- import { toSnakeCase } from './type-map.js';
7
+ import { getProps } from '@kernlang/core';
8
+ import { escapePyStr } from './fastapi-utils.js';
13
9
  export function generateRespondFastAPI(respondNode, indent) {
14
10
  const p = getProps(respondNode);
15
11
  const status = typeof p.status === 'number' ? p.status : undefined;
@@ -43,1971 +39,6 @@ export function generateRespondFastAPI(respondNode, indent) {
43
39
  }
44
40
  return [`${indent}return Response(status_code=200)`];
45
41
  }
46
- // Quoted strings absorbed by the alternation; only literal `===`/`!==`
47
- // outside strings get rewritten. Both single and double quotes AND
48
- // backtick template literals are covered so a message like
49
- // `` `use ===` `` is preserved (review fix — Codex+Gemini on 0ddfcc3d
50
- // flagged backticks as missing). Escape sequences are honored so
51
- // `"\""` / `` `\`` `` etc. don't terminate the string early.
52
- const STRING_LITERAL_ALT = '"(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\'|`(?:[^`\\\\]|\\\\.)*`';
53
- const STRICT_EQ_RE = new RegExp(`${STRING_LITERAL_ALT}|===|!==`, 'g');
54
- // Same trick for JS-literal lowering: any literal text inside a quoted
55
- // string OR after a `.` (property accessor — `obj.true` must NOT become
56
- // `obj.True`, which is a Python SyntaxError) is preserved untouched.
57
- // Variable-width lookbehind `(?<!\.\s*)` handles both tight (`obj.true`)
58
- // and loose (`obj . true`) forms; the latter caught by Codex review on
59
- // commit 68565826.
60
- const JS_LITERAL_RE = new RegExp(`${STRING_LITERAL_ALT}|(?<!\\.\\s*)\\b(?:undefined|null|true|false)\\b`, 'g');
61
- // Within an arrow body/predicate, rewrite member access on the bound element
62
- // variable to dict-subscript form so iterating a list of dicts works at
63
- // runtime: `x.n` → `x["n"]`, `x.meta.tag` → `x["meta"]["tag"]`. A chain that is
64
- // immediately followed by `(` is a METHOD call (`x.toUpperCase()`) and is left
65
- // untouched for the string-method pass. String-aware (literal `"x.n"` is kept)
66
- // and skips a chain that is itself a property of something else (`body.x.n`).
67
- // Manual scan (no RegExp sticky matching) so single-char fields like `.n` are
68
- // handled — the prior regex required two-plus-char field names.
69
- function lowerDictMemberAccess(text, varName) {
70
- let out = '';
71
- let i = 0;
72
- let quote = null;
73
- while (i < text.length) {
74
- const c = text[i];
75
- if (quote) {
76
- out += c;
77
- if (c === '\\') {
78
- out += text[i + 1] ?? '';
79
- i += 2;
80
- continue;
81
- }
82
- if (c === quote)
83
- quote = null;
84
- i += 1;
85
- continue;
86
- }
87
- if (c === '"' || c === "'" || c === '`') {
88
- quote = c;
89
- out += c;
90
- i += 1;
91
- continue;
92
- }
93
- const prev = text[i - 1];
94
- const boundaryOk = !(prev && /[\w.$]/.test(prev));
95
- const afterVar = text[i + varName.length] ?? '';
96
- if (boundaryOk && text.startsWith(varName, i) && !/[\w$]/.test(afterVar)) {
97
- let k = i + varName.length;
98
- const fields = [];
99
- while (text[k] === '.') {
100
- const fm = text.slice(k + 1).match(/^[A-Za-z_$]\w*/);
101
- if (!fm)
102
- break;
103
- fields.push(fm[0]);
104
- k += 1 + fm[0].length;
105
- }
106
- if (fields.length > 0) {
107
- if (text[k] === '(') {
108
- // Method call: subscript the leading DATA fields but keep the final
109
- // segment as attribute access (the method name) so the string-method
110
- // / nested-array passes still see it — `x.name.toUpperCase()` →
111
- // `x["name"].toUpperCase()`, `x.tags.map(...)` → `x["tags"].map(...)`
112
- // (codex review of ab192611). A lone `x.method()` is unchanged.
113
- const dataFields = fields.slice(0, -1);
114
- const methodField = fields[fields.length - 1];
115
- out += `${varName + dataFields.map((field) => `[${JSON.stringify(field)}]`).join('')}.${methodField}`;
116
- }
117
- else {
118
- out += varName + fields.map((field) => `[${JSON.stringify(field)}]`).join('');
119
- }
120
- i = k;
121
- continue;
122
- }
123
- }
124
- out += c;
125
- i += 1;
126
- }
127
- return out;
128
- }
129
- // A statement body that is EXACTLY `{ return E; }` is semantically identical to the
130
- // expression body `E`, so unwrap it to reuse the expression-bodied lowering (slice 1 of
131
- // native closures, #5). Richer statement bodies (locals, control flow, side effects
132
- // before the return) need full closure lowering (hoisted nested defs) and are NOT handled
133
- // here — they stay untouched (still unsupported) rather than mis-lowered. The scan is
134
- // string/bracket-aware so `{ return f({a:1}); }` unwraps but `{ return a; more(); }` does not.
135
- function unwrapSingleReturnBlock(body) {
136
- const t = body.trim();
137
- if (t.length < 2 || t[0] !== '{' || t[t.length - 1] !== '}')
138
- return body;
139
- const topLevelBreaks = (s, breakOnSemicolon) => {
140
- let depth = 0;
141
- let inStr = null;
142
- for (let i = 0; i < s.length; i++) {
143
- const c = s[i];
144
- if (inStr) {
145
- if (c === inStr && s[i - 1] !== '\\')
146
- inStr = null;
147
- continue;
148
- }
149
- if (c === '"' || c === "'" || c === '`')
150
- inStr = c;
151
- else if (c === '{' || c === '(' || c === '[')
152
- depth++;
153
- else if (c === '}' || c === ')' || c === ']') {
154
- depth--;
155
- // the opening brace must match the FINAL char, else `{..}{..}` etc.
156
- if (!breakOnSemicolon && depth === 0 && i !== s.length - 1)
157
- return true;
158
- }
159
- else if (breakOnSemicolon && c === ';' && depth === 0)
160
- return true;
161
- }
162
- return false;
163
- };
164
- if (topLevelBreaks(t, false))
165
- return body; // outer braces don't span the whole body
166
- let inner = t.slice(1, -1).trim();
167
- if (!/^return\b/.test(inner))
168
- return body;
169
- inner = inner.slice(6).trim();
170
- if (inner.endsWith(';'))
171
- inner = inner.slice(0, -1).trim();
172
- if (!inner || topLevelBreaks(inner, true))
173
- return body; // empty or multi-statement
174
- return inner;
175
- }
176
- // Parse an arrow callback's argument text into `{ params, body }`, or null when
177
- // it isn't a single arrow function (e.g. `.map(fn)` with a bare reference, which
178
- // is left unchanged). Handles `(p) => body`, `p => body`, and `(p, i) => body`.
179
- function parseArrowCallback(inner) {
180
- const trimmed = inner.trim();
181
- if (trimmed.startsWith('(')) {
182
- const close = matchBalancedParen(trimmed, 0);
183
- if (close === -1)
184
- return null;
185
- const after = trimmed.slice(close + 1).trim();
186
- if (!after.startsWith('=>'))
187
- return null;
188
- const params = splitTopLevelArgs(trimmed.slice(1, close))
189
- .map((s) => s.trim())
190
- .filter(Boolean);
191
- return { params, body: unwrapSingleReturnBlock(after.slice(2).trim()) };
192
- }
193
- const m = trimmed.match(/^([A-Za-z_$][\w$]*)\s*=>\s*([\s\S]+)$/);
194
- if (!m)
195
- return null;
196
- return { params: [m[1]], body: unwrapSingleReturnBlock(m[2].trim()) };
197
- }
198
- // Lower JS arrow-callback array methods to Python comprehensions:
199
- // arr.filter((x) => pred) -> [x for x in arr if pred]
200
- // arr.map((x) => body) -> [body for x in arr]
201
- // arr.map((x, i) => body) -> [body for i, x in enumerate(arr)]
202
- // arr.find((x) => pred) -> next((x for x in arr if pred), None)
203
- // Balanced + string-aware scan (NOT regex): the receiver is taken from the
204
- // already-emitted output via findReceiverStart, so chained calls compose
205
- // naturally (`arr.filter(...).map(...)` nests one comprehension inside the
206
- // next) and the quotes/brackets of a lowered comprehension can never desync the
207
- // receiver — the failure mode of the prior regex form. Member access on the
208
- // bound element is dict-subscripted so a list-of-dicts iterates correctly.
209
- const ARROW_ARRAY_METHODS = new Set(['filter', 'map', 'find', 'findIndex', 'findLast', 'findLastIndex', 'flatMap']);
210
- const PORTABLE_ARRAY_METHODS = new Set([
211
- 'includes',
212
- 'indexOf',
213
- 'join',
214
- 'slice',
215
- 'some',
216
- 'every',
217
- 'reduce',
218
- 'sort',
219
- 'flat',
220
- 'at',
221
- 'push',
222
- 'reverse',
223
- 'concat',
224
- 'fill',
225
- 'lastIndexOf',
226
- 'reduceRight',
227
- ]);
228
- const LAMBDA_COLON_PLACEHOLDER = '__KERN_LAMBDA_COLON__';
229
- function lowerArrowBlockClosure(arrow, ctx) {
230
- if (!arrow.body.trim().startsWith('{'))
231
- return null;
232
- const seq = ctx.closureSeq ?? { n: 0 };
233
- const name = `__kern_closure_${seq.n++}`;
234
- if (!ctx.closureSeq)
235
- ctx.closureSeq = seq;
236
- const result = lowerJsClosureBodyToPython(arrow.body, {
237
- lowerExpression: (raw) => rewriteFastAPIExpr(lowerDictMemberAccess(raw, arrow.params[0]), ctx.pathParams, ctx.bodyFields, ctx.authUser, ctx.imports, undefined, ctx.closureSeq),
238
- lowerCondition: (raw) => `js_truthy(${rewriteFastAPIExpr(lowerDictMemberAccess(raw, arrow.params[0]), ctx.pathParams, ctx.bodyFields, ctx.authUser, ctx.imports, undefined, ctx.closureSeq)})`,
239
- });
240
- if (!result.ok)
241
- return null;
242
- ctx.imports?.add(KERN_JS_HELPER_PY);
243
- const params = arrow.params.join(', ');
244
- const def = [`def ${name}(${params}):`, ...(result.lines.length > 0 ? result.lines : [' pass'])].join('\n');
245
- if (ctx.hoistedDefs) {
246
- ctx.hoistedDefs.push(def);
247
- }
248
- else {
249
- ctx.imports?.add(def);
250
- }
251
- return `${name}(${arrow.params.join(', ')})`;
252
- }
253
- function lowerJsArrayMethods(expr, ctx) {
254
- let out = '';
255
- let i = 0;
256
- let quote = null;
257
- while (i < expr.length) {
258
- const c = expr[i];
259
- if (quote) {
260
- out += c;
261
- if (c === '\\') {
262
- out += expr[i + 1] ?? '';
263
- i += 2;
264
- continue;
265
- }
266
- if (c === quote)
267
- quote = null;
268
- i += 1;
269
- continue;
270
- }
271
- if (c === '"' || c === "'" || c === '`') {
272
- quote = c;
273
- out += c;
274
- i += 1;
275
- continue;
276
- }
277
- const m = expr.slice(i).match(/^\.([A-Za-z]\w*)\(/);
278
- if (m && ARROW_ARRAY_METHODS.has(m[1])) {
279
- const method = m[1];
280
- const openIdx = i + m[0].length - 1;
281
- const closeIdx = matchBalancedParen(expr, openIdx);
282
- const recvStart = findReceiverStart(out);
283
- if (closeIdx !== -1 && recvStart !== -1) {
284
- const arrow = parseArrowCallback(expr.slice(openIdx + 1, closeIdx));
285
- if (arrow && arrow.params.length >= 1) {
286
- const receiver = out.slice(recvStart);
287
- const pre = out.slice(0, recvStart);
288
- const elemVar = arrow.params[0];
289
- const idxVar = arrow.params[1];
290
- // Recurse for nested array methods in the body; subscript the element
291
- // var's member access. The index var (if any) stays a bare int.
292
- const blockClosure = lowerArrowBlockClosure(arrow, ctx);
293
- const body = blockClosure ?? lowerJsArrayMethods(lowerDictMemberAccess(arrow.body, elemVar), ctx);
294
- // A second callback param is the element index — bind it via
295
- // enumerate() for every method, not just map, so a predicate that
296
- // references the index (`(x, i) => i > 0`) doesn't emit an unbound
297
- // name (codex review of ab192611).
298
- const loopTarget = idxVar ? `${idxVar}, ${elemVar}` : elemVar;
299
- const source = idxVar ? `enumerate(${receiver})` : receiver;
300
- let lowered;
301
- if (method === 'filter') {
302
- lowered = `[${elemVar} for ${loopTarget} in ${source} if ${body}]`;
303
- }
304
- else if (method === 'find') {
305
- lowered = `next((${elemVar} for ${loopTarget} in ${source} if ${body}), None)`;
306
- }
307
- else if (method === 'findIndex') {
308
- // index of the first match, or -1 (never raises). Bind the user's
309
- // own index var when the callback has one, so `(x, i) => …i…` works.
310
- const ix = idxVar ?? '__i';
311
- lowered = `next((${ix} for ${ix}, ${elemVar} in enumerate(${receiver}) if ${body}), -1)`;
312
- }
313
- else if (method === 'findLast') {
314
- // last matching element, or None
315
- lowered = idxVar
316
- ? `next((${elemVar} for ${idxVar}, ${elemVar} in reversed(list(enumerate(${receiver}))) if ${body}), None)`
317
- : `next((${elemVar} for ${elemVar} in reversed(${receiver}) if ${body}), None)`;
318
- }
319
- else if (method === 'findLastIndex') {
320
- // index of the last match, or -1
321
- const ix = idxVar ?? '__i';
322
- lowered = `next((${ix} for ${ix}, ${elemVar} in reversed(list(enumerate(${receiver}))) if ${body}), -1)`;
323
- }
324
- else if (method === 'flatMap') {
325
- // map, then flatten ONE level — JS flatMap only flattens arrays, so
326
- // a scalar/string callback result is appended as a single element.
327
- lowered = `[__y for ${loopTarget} in ${source} for __y in (${body} if isinstance(${body}, list) else [${body}])]`;
328
- }
329
- else {
330
- lowered = `[${body} for ${loopTarget} in ${source}]`;
331
- }
332
- out = `${pre}${lowered}`;
333
- i = closeIdx + 1;
334
- continue;
335
- }
336
- }
337
- }
338
- const mArray = expr.slice(i).match(/^\.([A-Za-z]\w*)\(/);
339
- if (mArray && PORTABLE_ARRAY_METHODS.has(mArray[1])) {
340
- const method = mArray[1];
341
- const openIdx = i + mArray[0].length - 1;
342
- const closeIdx = matchBalancedParen(expr, openIdx);
343
- const recvStart = findReceiverStart(out);
344
- if (closeIdx !== -1 && recvStart !== -1) {
345
- const receiver = out.slice(recvStart);
346
- const pre = out.slice(0, recvStart);
347
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerJsArrayMethods(a.trim(), ctx));
348
- let lowered = null;
349
- if (method === 'includes') {
350
- const needle = args[0] ?? '';
351
- lowered = `(${needle} in ${receiver})`;
352
- }
353
- else if (method === 'indexOf') {
354
- const needle = args[0] ?? '';
355
- const fromIndex = args[1] ?? null;
356
- if (fromIndex) {
357
- lowered = `(next((__i for __i, __v in enumerate(${receiver}) if __i >= ${fromIndex} and __v == ${needle}), -1))`;
358
- }
359
- else {
360
- lowered = `(next((__i for __i, __v in enumerate(${receiver}) if __v == ${needle}), -1))`;
361
- }
362
- }
363
- else if (method === 'push') {
364
- // JS Array.push mutates AND returns the new length. Python list.append
365
- // returns None, so emit `(recv.append(x) or len(recv))` for exact parity
366
- // (mutate + length). Single-arg only; varargs push left unsupported.
367
- if (args.length === 1)
368
- lowered = `(${receiver}.append(${args[0]}) or len(${receiver}))`;
369
- }
370
- else if (method === 'reverse') {
371
- // JS Array.reverse mutates AND returns the (same, reversed) array; Python
372
- // list.reverse returns None -> `(recv.reverse() or recv)` mutates + returns it.
373
- lowered = `(${receiver}.reverse() or ${receiver})`;
374
- }
375
- else if (method === 'concat') {
376
- // JS Array.concat returns a NEW array; an array arg is spread, a scalar arg
377
- // is appended. Mirror with `recv + (x if isinstance(x, list) else [x])`.
378
- // Single-arg only; varargs concat left unsupported.
379
- if (args.length === 1)
380
- lowered = `(${receiver} + (${args[0]} if isinstance(${args[0]}, list) else [${args[0]}]))`;
381
- }
382
- else if (method === 'join') {
383
- const sep = args[0] ?? '","';
384
- lowered = `${sep}.join(str(__v) for __v in ${receiver})`;
385
- }
386
- else if (method === 'slice') {
387
- const start = args[0];
388
- const end = args[1];
389
- if (!start && !end)
390
- lowered = `${receiver}[:]`;
391
- else if (start && !end)
392
- lowered = `${receiver}[${start}:]`;
393
- else if (!start && end)
394
- lowered = `${receiver}[:${end}]`;
395
- else
396
- lowered = `${receiver}[${start}:${end}]`;
397
- }
398
- else if (method === 'some' || method === 'every') {
399
- const arrow = parseArrowCallback(expr.slice(openIdx + 1, closeIdx));
400
- if (arrow && arrow.params.length >= 1) {
401
- const elemVar = arrow.params[0];
402
- const idxVar = arrow.params[1];
403
- // Only the element var is dict-subscripted; the index var stays a
404
- // bare int and must be bound via enumerate() when present.
405
- const blockClosure = lowerArrowBlockClosure(arrow, ctx);
406
- const pred = blockClosure ?? lowerJsArrayMethods(lowerDictMemberAccess(arrow.body, elemVar), ctx);
407
- const loopTarget = idxVar ? `${idxVar}, ${elemVar}` : elemVar;
408
- const source = idxVar ? `enumerate(${receiver})` : receiver;
409
- lowered =
410
- method === 'some'
411
- ? `any(${pred} for ${loopTarget} in ${source})`
412
- : `all(${pred} for ${loopTarget} in ${source})`;
413
- }
414
- }
415
- else if (method === 'reduce') {
416
- const rawArgs = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
417
- const arrow = parseArrowCallback(rawArgs[0] ?? '');
418
- if (arrow && arrow.params.length >= 2) {
419
- const accVar = arrow.params[0];
420
- const elemVar = arrow.params[1];
421
- let body = lowerDictMemberAccess(arrow.body, accVar);
422
- body = lowerDictMemberAccess(body, elemVar);
423
- const loweredBody = lowerJsArrayMethods(body, ctx);
424
- ctx.imports?.add('import functools');
425
- if (rawArgs.length >= 2) {
426
- const seed = lowerJsArrayMethods(rawArgs[1].trim(), ctx);
427
- lowered = `functools.reduce(lambda ${accVar}, ${elemVar}${LAMBDA_COLON_PLACEHOLDER} ${loweredBody}, ${receiver}, ${seed})`;
428
- }
429
- else {
430
- lowered = `functools.reduce(lambda ${accVar}, ${elemVar}${LAMBDA_COLON_PLACEHOLDER} ${loweredBody}, ${receiver})`;
431
- }
432
- }
433
- }
434
- else if (method === 'reduceRight') {
435
- // reduce from the right: same callback (acc, cur), reversed sequence.
436
- const rawArgs = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
437
- const arrow = parseArrowCallback(rawArgs[0] ?? '');
438
- if (arrow && arrow.params.length >= 2) {
439
- const accVar = arrow.params[0];
440
- const elemVar = arrow.params[1];
441
- let body = lowerDictMemberAccess(arrow.body, accVar);
442
- body = lowerDictMemberAccess(body, elemVar);
443
- const loweredBody = lowerJsArrayMethods(body, ctx);
444
- ctx.imports?.add('import functools');
445
- if (rawArgs.length >= 2) {
446
- const seed = lowerJsArrayMethods(rawArgs[1].trim(), ctx);
447
- lowered = `functools.reduce(lambda ${accVar}, ${elemVar}${LAMBDA_COLON_PLACEHOLDER} ${loweredBody}, ${receiver}[::-1], ${seed})`;
448
- }
449
- else {
450
- lowered = `functools.reduce(lambda ${accVar}, ${elemVar}${LAMBDA_COLON_PLACEHOLDER} ${loweredBody}, ${receiver}[::-1])`;
451
- }
452
- }
453
- }
454
- else if (method === 'sort') {
455
- // JS default sort is LEXICOGRAPHIC and returns a NEW array (Python's
456
- // list.sort() is numeric AND in-place → None). A 2-arg comparator
457
- // sorts numerically; anything else falls back to the string key.
458
- const arrow = parseArrowCallback(expr.slice(openIdx + 1, closeIdx));
459
- if (arrow && arrow.params.length >= 2) {
460
- const a = arrow.params[0];
461
- const b = arrow.params[1];
462
- const body = lowerJsArrayMethods(arrow.body, ctx);
463
- ctx.imports?.add('import functools');
464
- lowered = `sorted(${receiver}, key=functools.cmp_to_key(lambda ${a}, ${b}${LAMBDA_COLON_PLACEHOLDER} ${body}))`;
465
- }
466
- else {
467
- lowered = `sorted(${receiver}, key=lambda __v${LAMBDA_COLON_PLACEHOLDER} str(__v))`;
468
- }
469
- }
470
- else if (method === 'flat') {
471
- // one level: flatten nested lists, keep scalars
472
- lowered = `[__y for __x in ${receiver} for __y in (__x if isinstance(__x, list) else [__x])]`;
473
- }
474
- else if (method === 'at') {
475
- const n = args[0] ?? '0';
476
- lowered = `(${receiver}[${n}] if -len(${receiver}) <= ${n} < len(${receiver}) else None)`;
477
- // NB: `reverse` and `concat` are handled earlier in this chain (they mutate /
478
- // accept scalar args per JS) — main's later array-only duplicates were dropped
479
- // in the roadmap-stack merge (they were dead + concat broke on scalar args).
480
- }
481
- else if (method === 'fill') {
482
- const v = args[0] ?? 'None';
483
- if (args.length <= 1) {
484
- lowered = `[${v} for __ in ${receiver}]`;
485
- }
486
- else {
487
- // fill(value, start, end) fills [start, end) with JS negative-index
488
- // normalization; untouched positions keep their original element.
489
- const s = args[1];
490
- const e = args[2] ?? `len(${receiver})`;
491
- lowered = `[(${v} if (${s} if ${s} >= 0 else ${s} + len(${receiver})) <= __i < (${e} if ${e} >= 0 else ${e} + len(${receiver})) else __x) for __i, __x in enumerate(${receiver})]`;
492
- }
493
- }
494
- else if (method === 'lastIndexOf') {
495
- const needle = args[0] ?? '';
496
- // String receivers use rfind (correct for multi-char substrings, -1
497
- // when absent); array receivers reverse-scan by element equality.
498
- lowered = `(${receiver}.rfind(${needle}) if isinstance(${receiver}, str) else (len(${receiver}) - 1 - ${receiver}[::-1].index(${needle}) if ${needle} in ${receiver} else -1))`;
499
- }
500
- if (lowered) {
501
- out = `${pre}${lowered}`;
502
- i = closeIdx + 1;
503
- continue;
504
- }
505
- }
506
- }
507
- out += c;
508
- i += 1;
509
- }
510
- return out;
511
- }
512
- // Index of the bracket that closes the one at `openIdx`, tracking ()[]{} depth
513
- // and skipping string/template literals. -1 if unbalanced.
514
- function matchBalancedParen(expr, openIdx) {
515
- let depth = 0;
516
- let quote = null;
517
- for (let i = openIdx; i < expr.length; i++) {
518
- const c = expr[i];
519
- if (quote) {
520
- if (c === '\\')
521
- i += 1;
522
- else if (c === quote)
523
- quote = null;
524
- continue;
525
- }
526
- if (c === '"' || c === "'" || c === '`')
527
- quote = c;
528
- else if (c === '(' || c === '[' || c === '{')
529
- depth += 1;
530
- else if (c === ')' || c === ']' || c === '}') {
531
- depth -= 1;
532
- if (depth === 0)
533
- return i;
534
- }
535
- }
536
- return -1;
537
- }
538
- // Split a call's inner argument text on top-level commas, ignoring commas
539
- // inside nested ()[]{} or string literals.
540
- function splitTopLevelArgs(inner) {
541
- const args = [];
542
- let depth = 0;
543
- let quote = null;
544
- let start = 0;
545
- for (let i = 0; i < inner.length; i++) {
546
- const c = inner[i];
547
- if (quote) {
548
- if (c === '\\')
549
- i += 1;
550
- else if (c === quote)
551
- quote = null;
552
- continue;
553
- }
554
- if (c === '"' || c === "'" || c === '`')
555
- quote = c;
556
- else if (c === '(' || c === '[' || c === '{')
557
- depth += 1;
558
- else if (c === ')' || c === ']' || c === '}')
559
- depth -= 1;
560
- else if (c === ',' && depth === 0) {
561
- args.push(inner.slice(start, i).trim());
562
- start = i + 1;
563
- }
564
- }
565
- args.push(inner.slice(start).trim());
566
- return args;
567
- }
568
- // Lower JSON.stringify(...) / JSON.parse(...) to json.dumps/loads. Uses a
569
- // balanced, string-aware scan because the single argument can itself contain
570
- // commas, nested parens, brackets, braces, or string literals — which regex
571
- // cannot reliably capture (three regex iterations were each holed by review).
572
- // Skips occurrences inside string literals and those that are a property of
573
- // another receiver (e.g. `myJSON.stringify`). Handles the pretty-print form
574
- // `JSON.stringify(x, null, n)` → `json.dumps(x, indent=n)`.
575
- function lowerJsonBuiltinCalls(expr, imports) {
576
- let out = '';
577
- let i = 0;
578
- let quote = null;
579
- while (i < expr.length) {
580
- const c = expr[i];
581
- if (quote) {
582
- out += c;
583
- if (c === '\\') {
584
- out += expr[i + 1] ?? '';
585
- i += 2;
586
- continue;
587
- }
588
- if (c === quote)
589
- quote = null;
590
- i += 1;
591
- continue;
592
- }
593
- if (c === '"' || c === "'" || c === '`') {
594
- quote = c;
595
- out += c;
596
- i += 1;
597
- continue;
598
- }
599
- const m = expr.slice(i).match(/^JSON\.(stringify|parse)\(/);
600
- const prev = expr[i - 1];
601
- if (m && !(prev && /[\w.]/.test(prev))) {
602
- const method = m[1];
603
- const openIdx = i + m[0].length - 1;
604
- const closeIdx = matchBalancedParen(expr, openIdx);
605
- if (closeIdx !== -1) {
606
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
607
- // Recurse so a nested builtin in the argument is lowered too, e.g.
608
- // JSON.stringify(JSON.parse(x)) → json.dumps(json.loads(x)) (Codex
609
- // review on 9d8ed8d0). Terminates: the argument is strictly shorter.
610
- const a0 = lowerJsonBuiltinCalls(args[0] ?? '', imports);
611
- imports?.add('import json');
612
- if (method === 'parse') {
613
- out += `json.loads(${a0})`;
614
- }
615
- else if (args.length >= 3 && /^(None|null)$/.test(args[1]) && /^\d+$/.test(args[2])) {
616
- out += `json.dumps(${a0}, indent=${args[2]})`;
617
- }
618
- else {
619
- out += `json.dumps(${a0})`;
620
- }
621
- i = closeIdx + 1;
622
- continue;
623
- }
624
- }
625
- out += c;
626
- i += 1;
627
- }
628
- return out;
629
- }
630
- // Lower Number/Math arithmetic builtins used in portable expressions.
631
- // String-aware + balanced-paren scan, so nested calls/expressions survive:
632
- // Number.floor(a + b) -> __k_math.floor(a + b)
633
- // Number.round(x) -> __k_math.floor(x + 0.5) (JS Math.round parity)
634
- // Math.max(a, b, c) -> max(a, b, c)
635
- // Guards skip member calls on custom receivers (e.g. myNumber.floor(x)).
636
- function lowerMathBuiltinCalls(expr, imports) {
637
- let out = '';
638
- let i = 0;
639
- let quote = null;
640
- while (i < expr.length) {
641
- const c = expr[i];
642
- if (quote) {
643
- out += c;
644
- if (c === '\\') {
645
- out += expr[i + 1] ?? '';
646
- i += 2;
647
- continue;
648
- }
649
- if (c === quote)
650
- quote = null;
651
- i += 1;
652
- continue;
653
- }
654
- if (c === '"' || c === "'" || c === '`') {
655
- quote = c;
656
- out += c;
657
- i += 1;
658
- continue;
659
- }
660
- const m = expr
661
- .slice(i)
662
- .match(/^(?:(?:Number|Math)\.(floor|ceil|round|abs|trunc|isFinite|isNaN)|Math\.(min|max|pow|sqrt|hypot|random|sign|log10|log2|log|exp|sin|cos|atan2))\(/);
663
- const prev = expr[i - 1];
664
- // Math.PI / Math.E are constants (no call), so the call regex never sees
665
- // them — handle them here before the method dispatch.
666
- const cm = expr.slice(i).match(/^Math\.(PI|E)\b/);
667
- if (cm && !(prev && /[\w.]/.test(prev))) {
668
- imports?.add('import math as __k_math');
669
- out += cm[1] === 'PI' ? '__k_math.pi' : '__k_math.e';
670
- i += cm[0].length;
671
- continue;
672
- }
673
- if (m && !(prev && /[\w.]/.test(prev))) {
674
- const method = m[1] ?? m[2];
675
- const openIdx = i + m[0].length - 1;
676
- const closeIdx = matchBalancedParen(expr, openIdx);
677
- if (closeIdx !== -1) {
678
- const inner = expr.slice(openIdx + 1, closeIdx);
679
- const rawArgs = inner.trim() === '' ? [] : splitTopLevelArgs(inner);
680
- const loweredArgs = rawArgs.map((a) => lowerMathBuiltinCalls(a, imports).trim());
681
- const arg = loweredArgs[0] ?? '';
682
- switch (method) {
683
- case 'floor':
684
- imports?.add('import math as __k_math');
685
- out += `__k_math.floor(${arg})`;
686
- break;
687
- case 'ceil':
688
- imports?.add('import math as __k_math');
689
- out += `__k_math.ceil(${arg})`;
690
- break;
691
- case 'round':
692
- imports?.add('import math as __k_math');
693
- out += `__k_math.floor(${arg} + 0.5)`;
694
- break;
695
- case 'abs':
696
- out += `abs(${arg})`;
697
- break;
698
- case 'trunc':
699
- // JS Math.trunc truncates toward zero; math.trunc matches.
700
- imports?.add('import math as __k_math');
701
- out += `__k_math.trunc(${arg})`;
702
- break;
703
- case 'isFinite':
704
- imports?.add('import math as __k_math');
705
- // Type guard: Number.isFinite returns false for non-numbers; math.isfinite raises TypeError
706
- out += `(isinstance(${arg}, (int, float)) and __k_math.isfinite(${arg}))`;
707
- break;
708
- case 'isNaN':
709
- imports?.add('import math as __k_math');
710
- // Type guard: Number.isNaN returns false for non-numbers; math.isnan raises TypeError
711
- out += `(isinstance(${arg}, (int, float)) and __k_math.isnan(${arg}))`;
712
- break;
713
- case 'min':
714
- // JS Math.min(): 0 args → +Infinity; 1 arg → that value (Python
715
- // min(x) treats a lone arg as an iterable and raises).
716
- if (loweredArgs.length === 0)
717
- out += 'float("inf")';
718
- else if (loweredArgs.length === 1)
719
- out += `(${arg})`;
720
- else
721
- out += `min(${loweredArgs.join(', ')})`;
722
- break;
723
- case 'max':
724
- if (loweredArgs.length === 0)
725
- out += 'float("-inf")';
726
- else if (loweredArgs.length === 1)
727
- out += `(${arg})`;
728
- else
729
- out += `max(${loweredArgs.join(', ')})`;
730
- break;
731
- case 'pow':
732
- // JS Math.pow(a, b) === a ** b; fewer than 2 args is NaN in JS.
733
- out += loweredArgs.length >= 2 ? `(${loweredArgs[0]} ** ${loweredArgs[1]})` : 'float("nan")';
734
- break;
735
- case 'sqrt':
736
- imports?.add('import math as __k_math');
737
- out += `__k_math.sqrt(${arg})`;
738
- break;
739
- case 'hypot':
740
- imports?.add('import math as __k_math');
741
- out += `__k_math.hypot(${loweredArgs.join(', ')})`;
742
- break;
743
- case 'random':
744
- imports?.add('import random as __k_random');
745
- out += '__k_random.random()';
746
- break;
747
- case 'sign':
748
- // JS Math.sign returns -1, 0, or 1; 0 args → NaN.
749
- out += loweredArgs.length === 0 ? 'float("nan")' : `(1 if ${arg} > 0 else (-1 if ${arg} < 0 else 0))`;
750
- break;
751
- // Math.cbrt is intentionally NOT lowered: V8's Math.cbrt and the
752
- // platform libm cbrt disagree in the last ulp (Linux: cbrt(27) =
753
- // 3.0000000000000004, V8 = 3), so no Python expression reproduces it
754
- // bit-for-bit — same out-of-scope reason as toPrecision.
755
- case 'log':
756
- imports?.add('import math as __k_math');
757
- out += `__k_math.log(${arg})`;
758
- break;
759
- case 'log2':
760
- imports?.add('import math as __k_math');
761
- out += `__k_math.log2(${arg})`;
762
- break;
763
- case 'log10':
764
- imports?.add('import math as __k_math');
765
- out += `__k_math.log10(${arg})`;
766
- break;
767
- case 'exp':
768
- imports?.add('import math as __k_math');
769
- out += `__k_math.exp(${arg})`;
770
- break;
771
- case 'sin':
772
- imports?.add('import math as __k_math');
773
- out += `__k_math.sin(${arg})`;
774
- break;
775
- case 'cos':
776
- imports?.add('import math as __k_math');
777
- out += `__k_math.cos(${arg})`;
778
- break;
779
- case 'atan2':
780
- // JS Math.atan2(y, x) needs BOTH args; fewer → NaN.
781
- imports?.add('import math as __k_math');
782
- out += loweredArgs.length >= 2 ? `__k_math.atan2(${loweredArgs[0]}, ${loweredArgs[1]})` : 'float("nan")';
783
- break;
784
- default:
785
- out += expr.slice(i, closeIdx + 1);
786
- break;
787
- }
788
- i = closeIdx + 1;
789
- continue;
790
- }
791
- }
792
- out += c;
793
- i += 1;
794
- }
795
- return out;
796
- }
797
- // Find the start of the JS expression that ends just before the current position.
798
- // Uses a balanced-scan (backwards) to skip over () [] {}.
799
- function findReceiverStart(s) {
800
- let j = s.length - 1;
801
- while (j >= 0 && /\s/.test(s[j]))
802
- j--;
803
- if (j < 0)
804
- return -1;
805
- let depth = 0;
806
- while (j >= 0) {
807
- const c = s[j];
808
- if (c === '"' || c === "'" || c === '`') {
809
- // A string literal is one atom — scan back to its matching open quote,
810
- // honoring backslash escapes, so `"a.b(".charAt(1)` or `data["k"].at(0)`
811
- // don't desync the receiver. (The old scan stopped AT the quote, leaving
812
- // an empty receiver and emitting broken Python.)
813
- const q = c;
814
- let k = j - 1;
815
- while (k >= 0) {
816
- if (s[k] === q) {
817
- let b = 0;
818
- let p = k - 1;
819
- while (p >= 0 && s[p] === '\\') {
820
- b++;
821
- p--;
822
- }
823
- if (b % 2 === 0)
824
- break;
825
- }
826
- k--;
827
- }
828
- if (depth === 0)
829
- return k < 0 ? 0 : k; // the literal is the receiver atom
830
- j = k - 1; // literal sits inside brackets — skip it and keep scanning
831
- continue;
832
- }
833
- if (c === ')' || c === ']' || c === '}') {
834
- depth++;
835
- }
836
- else if (c === '(' || c === '[' || c === '{') {
837
- depth--;
838
- if (depth < 0)
839
- return j + 1;
840
- }
841
- else if (depth === 0) {
842
- // At top level, we stop at anything that isn't part of an identifier,
843
- // property access, or indexed access.
844
- if (!/[\w.$]/.test(c))
845
- return j + 1;
846
- }
847
- j--;
848
- }
849
- return 0;
850
- }
851
- // Lower Number parsing and formatting builtins:
852
- // parseInt(x) / parseInt(x, 10) -> int(x)
853
- // parseFloat(x) -> float(x)
854
- // Number.isInteger(x) -> (isinstance(x, int) and not isinstance(x, bool))
855
- // Number(x) -> float(x) (best-effort coercion)
856
- // (n).toFixed(d) -> f"{n:.{d}f}"
857
- // String-aware + balanced-paren scan so nested calls survive.
858
- function lowerNumberBuiltinCalls(expr, imports) {
859
- let out = '';
860
- let i = 0;
861
- let quote = null;
862
- while (i < expr.length) {
863
- const c = expr[i];
864
- if (quote) {
865
- out += c;
866
- if (c === '\\') {
867
- out += expr[i + 1] ?? '';
868
- i += 2;
869
- continue;
870
- }
871
- if (c === quote)
872
- quote = null;
873
- i += 1;
874
- continue;
875
- }
876
- if (c === '"' || c === "'" || c === '`') {
877
- quote = c;
878
- out += c;
879
- i += 1;
880
- continue;
881
- }
882
- const m = expr
883
- .slice(i)
884
- .match(/^(?:Number\.isInteger|Number\.isSafeInteger|Number\.parseInt|Number\.parseFloat|Number|parseInt|parseFloat|isNaN|isFinite)\(/);
885
- const prev = expr[i - 1];
886
- if (m && !(prev && /[\w.]/.test(prev))) {
887
- const match = m[0];
888
- const method = match.slice(0, -1);
889
- const openIdx = i + match.length - 1;
890
- const closeIdx = matchBalancedParen(expr, openIdx);
891
- if (closeIdx !== -1) {
892
- const inner = expr.slice(openIdx + 1, closeIdx);
893
- const args = splitTopLevelArgs(inner);
894
- const a0 = lowerNumberBuiltinCalls(args[0] ?? '', imports).trim();
895
- if (method === 'parseInt' || method === 'Number.parseInt') {
896
- if (args.length === 1 || (args.length === 2 && args[1].trim() === '10')) {
897
- out += `int(${a0})`;
898
- }
899
- else {
900
- const a1 = args[1] ? lowerNumberBuiltinCalls(args[1], imports).trim() : '';
901
- out += `int(${a0}, ${a1})`;
902
- }
903
- }
904
- else if (method === 'parseFloat' || method === 'Number.parseFloat') {
905
- out += `float(${a0})`;
906
- }
907
- else if (method === 'Number.isInteger') {
908
- out += `(isinstance(${a0}, int) and not isinstance(${a0}, bool))`;
909
- }
910
- else if (method === 'Number.isSafeInteger') {
911
- // JS: an integer-valued finite number within ±(2^53 − 1). Whole floats
912
- // count too; bool is not a number on the JS side.
913
- imports?.add('import math as __k_math');
914
- out += `(isinstance(${a0}, (int, float)) and not isinstance(${a0}, bool) and __k_math.isfinite(${a0}) and __k_math.floor(${a0}) == ${a0} and abs(${a0}) <= 9007199254740991)`;
915
- }
916
- else if (method === 'isNaN') {
917
- // GLOBAL isNaN (not Number.isNaN) — numeric inputs only (full JS
918
- // string-coercion is out of scope); raises on non-numbers.
919
- imports?.add('import math as __k_math');
920
- out += `__k_math.isnan(${a0})`;
921
- }
922
- else if (method === 'isFinite') {
923
- imports?.add('import math as __k_math');
924
- out += `__k_math.isfinite(${a0})`;
925
- }
926
- else if (method === 'Number') {
927
- out += `float(${a0})`;
928
- }
929
- i = closeIdx + 1;
930
- continue;
931
- }
932
- }
933
- if (expr.startsWith('.toFixed(', i)) {
934
- const openIdx = i + '.toFixed('.length - 1;
935
- const closeIdx = matchBalancedParen(expr, openIdx);
936
- if (closeIdx !== -1) {
937
- const inner = expr.slice(openIdx + 1, closeIdx);
938
- const args = splitTopLevelArgs(inner);
939
- const precision = args[0] ? lowerNumberBuiltinCalls(args[0], imports).trim() : '0';
940
- const receiverStart = findReceiverStart(out);
941
- if (receiverStart !== -1) {
942
- const receiver = out.slice(receiverStart);
943
- const pre = out.slice(0, receiverStart);
944
- // Quote-safe: a nested f-string `f"{receiver:.{p}f}"` is a SyntaxError
945
- // on CPython <3.12 when the receiver contains `"` (e.g. data["k"]).
946
- // `format(x, '.' + str(p) + 'f')` keeps the receiver as a bare arg.
947
- out = `${pre}format(${receiver}, '.' + str(${precision}) + 'f')`;
948
- i = closeIdx + 1;
949
- continue;
950
- }
951
- }
952
- }
953
- // (n).toString(radix) → format/str. Only the explicit radix form is lowered
954
- // (radix 2/8/16 via format spec, 10 via str); a no-arg .toString() is left
955
- // raw because on a non-number receiver it would mean the wrong thing.
956
- if (expr.startsWith('.toString(', i)) {
957
- const openIdx = i + '.toString('.length - 1;
958
- const closeIdx = matchBalancedParen(expr, openIdx);
959
- if (closeIdx !== -1) {
960
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
961
- const radix = (args[0] ?? '').trim();
962
- const spec = radix === '2' ? 'b' : radix === '8' ? 'o' : radix === '16' ? 'x' : null;
963
- const receiverStart = findReceiverStart(out);
964
- if (receiverStart !== -1 && (spec || radix === '10')) {
965
- const receiver = out.slice(receiverStart);
966
- const pre = out.slice(0, receiverStart);
967
- out = spec ? `${pre}format(${receiver}, '${spec}')` : `${pre}str(${receiver})`;
968
- i = closeIdx + 1;
969
- continue;
970
- }
971
- }
972
- }
973
- // (n).toExponential(d) → '%.<d>e' then strip the leading zero(s) JS omits in
974
- // the exponent: Python gives 1.23e+03, JS gives 1.23e+3.
975
- if (expr.startsWith('.toExponential(', i)) {
976
- const openIdx = i + '.toExponential('.length - 1;
977
- const closeIdx = matchBalancedParen(expr, openIdx);
978
- if (closeIdx !== -1) {
979
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
980
- const digits = args[0] ? lowerNumberBuiltinCalls(args[0], imports).trim() : '';
981
- const receiverStart = findReceiverStart(out);
982
- if (receiverStart !== -1 && digits !== '') {
983
- const receiver = out.slice(receiverStart);
984
- const pre = out.slice(0, receiverStart);
985
- imports?.add('import re as __k_re');
986
- out = `${pre}__k_re.sub(r"e([+-])0*(\\d)", r"e\\1\\2", ('%.' + str(${digits}) + 'e') % (${receiver}))`;
987
- i = closeIdx + 1;
988
- continue;
989
- }
990
- }
991
- }
992
- out += c;
993
- i += 1;
994
- }
995
- return out;
996
- }
997
- // Lower JS string builtins to Python methods:
998
- // x.toUpperCase() -> x.upper()
999
- // x.toLowerCase() -> x.lower()
1000
- // x.trim() -> x.strip()
1001
- // x.padStart(n[, fill]) -> x.rjust(n[, fill]) (JS/Python both default to " ")
1002
- // x.padEnd(n[, fill]) -> x.ljust(n[, fill])
1003
- // Skip string literals so text like "a.toUpperCase()" / ".padStart(" stays raw.
1004
- // pad*/startsWith/endsWith take args, so only the method+`(` is matched and the
1005
- // argument list flows through to Python unchanged. Note: JS pad* accept a
1006
- // multi-char fill while Python rjust/ljust require a single fill char — only the
1007
- // 1-char fixture form is in scope; a multi-char fill is left to raise on Python.
1008
- function lowerStringBuiltinCalls(expr) {
1009
- return expr.replace(new RegExp(`${STRING_LITERAL_ALT}|\\.toUpperCase\\(\\)|\\.toLowerCase\\(\\)|\\.trim\\(\\)|\\.startsWith\\(|\\.endsWith\\(|\\.padStart\\(|\\.padEnd\\(`, 'g'), (match) => {
1010
- if (match === '.toUpperCase()')
1011
- return '.upper()';
1012
- if (match === '.toLowerCase()')
1013
- return '.lower()';
1014
- if (match === '.trim()')
1015
- return '.strip()';
1016
- // startsWith/endsWith take args; match only the method+`(` so the
1017
- // argument list passes through to Python's str.startswith/endswith.
1018
- if (match === '.startsWith(')
1019
- return '.startswith(';
1020
- if (match === '.endsWith(')
1021
- return '.endswith(';
1022
- if (match === '.padStart(')
1023
- return '.rjust(';
1024
- if (match === '.padEnd(')
1025
- return '.ljust(';
1026
- return match;
1027
- });
1028
- }
1029
- // Lower the argument-taking JS String methods that need more than a bare method
1030
- // rename (those are handled by lowerStringBuiltinCalls). One string-aware,
1031
- // balanced scan reused for all of them; the shared matchBalancedParen /
1032
- // splitTopLevelArgs / findReceiverStart helpers carve out args and receiver so
1033
- // no new char-loop matcher is introduced:
1034
- // s.replace("a", "b") -> s.replace("a", "b", 1) (JS replaces FIRST only,
1035
- // Python str.replace replaces ALL — pin count=1)
1036
- // s.substring(a, b) -> s[a:b] (and s.substring(a) -> s[a:])
1037
- // s.repeat(n) -> (s * n)
1038
- // s.split(sep, limit) -> s.split(sep)[:limit] THE TRAP: Python's 2nd
1039
- // arg is maxsplit, which KEEPS the remainder; JS
1040
- // keeps only the first `limit` parts. The no-limit
1041
- // s.split(sep) form is left raw (Python matches JS).
1042
- // replace: only the 2-arg, non-regex form is lowered (`s.replace(/re/, b)` is
1043
- // out of scope); `.replaceAll(` never matches. A quoted `".repeat("` etc. is
1044
- // skipped by the quote tracking, so string-literal text stays raw.
1045
- // substring edge (scoped out): JS substring clamps negative args to 0 and SWAPS
1046
- // them when a > b; Python slicing does neither. Only the simple non-negative
1047
- // fixture case is lowered — a negative/swapped substring would diverge.
1048
- function lowerStringArgMethods(expr) {
1049
- let out = '';
1050
- let i = 0;
1051
- let quote = null;
1052
- while (i < expr.length) {
1053
- const c = expr[i];
1054
- if (quote) {
1055
- out += c;
1056
- if (c === '\\') {
1057
- out += expr[i + 1] ?? '';
1058
- i += 2;
1059
- continue;
1060
- }
1061
- if (c === quote)
1062
- quote = null;
1063
- i += 1;
1064
- continue;
1065
- }
1066
- if (c === '"' || c === "'" || c === '`') {
1067
- quote = c;
1068
- out += c;
1069
- i += 1;
1070
- continue;
1071
- }
1072
- if (expr.startsWith('.replaceAll(', i)) {
1073
- const openIdx = i + '.replaceAll('.length - 1;
1074
- const closeIdx = matchBalancedParen(expr, openIdx);
1075
- if (closeIdx !== -1) {
1076
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
1077
- if (args.length === 2 && !args[0].trim().startsWith('/')) {
1078
- const a0 = lowerStringArgMethods(args[0]).trim();
1079
- const a1 = lowerStringArgMethods(args[1]).trim();
1080
- // Python str.replace already replaces ALL occurrences (no count arg).
1081
- out += `.replace(${a0}, ${a1})`;
1082
- i = closeIdx + 1;
1083
- continue;
1084
- }
1085
- }
1086
- }
1087
- if (expr.startsWith('.replace(', i)) {
1088
- const openIdx = i + '.replace('.length - 1;
1089
- const closeIdx = matchBalancedParen(expr, openIdx);
1090
- if (closeIdx !== -1) {
1091
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
1092
- if (args.length === 2 && !args[0].trim().startsWith('/')) {
1093
- const a0 = lowerStringArgMethods(args[0]).trim();
1094
- const a1 = lowerStringArgMethods(args[1]).trim();
1095
- out += `.replace(${a0}, ${a1}, 1)`;
1096
- i = closeIdx + 1;
1097
- continue;
1098
- }
1099
- }
1100
- }
1101
- // trimStart/trimEnd → lstrip/rstrip (method→method, receiver trails in `out`).
1102
- if (expr.startsWith('.trimStart()', i)) {
1103
- out += '.lstrip()';
1104
- i += '.trimStart()'.length;
1105
- continue;
1106
- }
1107
- if (expr.startsWith('.trimEnd()', i)) {
1108
- out += '.rstrip()';
1109
- i += '.trimEnd()'.length;
1110
- continue;
1111
- }
1112
- // charAt(i) → char or "" out of range (JS never raises; negative → "").
1113
- if (expr.startsWith('.charAt(', i)) {
1114
- const openIdx = i + '.charAt('.length - 1;
1115
- const closeIdx = matchBalancedParen(expr, openIdx);
1116
- if (closeIdx !== -1) {
1117
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a).trim());
1118
- const idx = args[0] ?? '0';
1119
- const receiverStart = findReceiverStart(out);
1120
- if (receiverStart !== -1) {
1121
- const receiver = out.slice(receiverStart);
1122
- const pre = out.slice(0, receiverStart);
1123
- out = `${pre}(${receiver}[${idx}] if 0 <= ${idx} < len(${receiver}) else "")`;
1124
- i = closeIdx + 1;
1125
- continue;
1126
- }
1127
- }
1128
- }
1129
- // charCodeAt(i)/codePointAt(i) → ord of the char (in-range; BMP only).
1130
- if (expr.startsWith('.charCodeAt(', i) || expr.startsWith('.codePointAt(', i)) {
1131
- const tok = expr.startsWith('.charCodeAt(', i) ? '.charCodeAt(' : '.codePointAt(';
1132
- const openIdx = i + tok.length - 1;
1133
- const closeIdx = matchBalancedParen(expr, openIdx);
1134
- if (closeIdx !== -1) {
1135
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a).trim());
1136
- const idx = args[0] ?? '0';
1137
- const receiverStart = findReceiverStart(out);
1138
- if (receiverStart !== -1) {
1139
- const receiver = out.slice(receiverStart);
1140
- const pre = out.slice(0, receiverStart);
1141
- // JS charCodeAt/codePointAt never raise: out-of-range (incl. negative)
1142
- // → NaN/undefined, which both become JSON null. Guard to avoid IndexError.
1143
- out = `${pre}(ord(${receiver}[${idx}]) if 0 <= ${idx} < len(${receiver}) else None)`;
1144
- i = closeIdx + 1;
1145
- continue;
1146
- }
1147
- }
1148
- }
1149
- if (expr.startsWith('.substring(', i)) {
1150
- const openIdx = i + '.substring('.length - 1;
1151
- const closeIdx = matchBalancedParen(expr, openIdx);
1152
- if (closeIdx !== -1) {
1153
- // Receiver is already in `out`; `s.substring(a, b)` and `s[a:b]` both
1154
- // trail the receiver, so just append the slice — no receiver surgery.
1155
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a).trim());
1156
- const start = args[0] ?? '';
1157
- const end = args[1] ?? '';
1158
- out += `[${start}:${end}]`;
1159
- i = closeIdx + 1;
1160
- continue;
1161
- }
1162
- }
1163
- if (expr.startsWith('.repeat(', i)) {
1164
- const openIdx = i + '.repeat('.length - 1;
1165
- const closeIdx = matchBalancedParen(expr, openIdx);
1166
- if (closeIdx !== -1) {
1167
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a).trim());
1168
- const n = args[0] ?? '0';
1169
- const receiverStart = findReceiverStart(out);
1170
- if (receiverStart !== -1) {
1171
- const receiver = out.slice(receiverStart);
1172
- const pre = out.slice(0, receiverStart);
1173
- out = `${pre}(${receiver} * ${n})`;
1174
- i = closeIdx + 1;
1175
- continue;
1176
- }
1177
- }
1178
- }
1179
- if (expr.startsWith('.split(', i)) {
1180
- const openIdx = i + '.split('.length - 1;
1181
- const closeIdx = matchBalancedParen(expr, openIdx);
1182
- if (closeIdx !== -1) {
1183
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a).trim());
1184
- // Only the 2-arg limit form needs rewriting; the no-limit form is left
1185
- // raw (falls through) because Python str.split(sep) already matches JS.
1186
- if (args.length === 2) {
1187
- out += `.split(${args[0]})[:${args[1]}]`;
1188
- i = closeIdx + 1;
1189
- continue;
1190
- }
1191
- }
1192
- }
1193
- out += c;
1194
- i += 1;
1195
- }
1196
- return out;
1197
- }
1198
- // Lower selected Object/Array/Date host builtins in portable expressions:
1199
- // Object.keys(x) -> list(x.keys())
1200
- // Object.values(x) -> list(x.values())
1201
- // Object.entries(x) -> list(x.items())
1202
- // Object.assign(t,s,…) -> {**t, **s, ...}
1203
- // Object.fromEntries(p) -> dict(p)
1204
- // Array.isArray(x) -> isinstance(x, list)
1205
- // Date.now() -> int(datetime.now(timezone.utc).timestamp() * 1000)
1206
- // Uses the same string-aware balanced scan as other builtin lowerers.
1207
- function lowerObjectArrayDateBuiltinCalls(expr, imports) {
1208
- let out = '';
1209
- let i = 0;
1210
- let quote = null;
1211
- while (i < expr.length) {
1212
- const c = expr[i];
1213
- if (quote) {
1214
- out += c;
1215
- if (c === '\\') {
1216
- out += expr[i + 1] ?? '';
1217
- i += 2;
1218
- continue;
1219
- }
1220
- if (c === quote)
1221
- quote = null;
1222
- i += 1;
1223
- continue;
1224
- }
1225
- if (c === '"' || c === "'" || c === '`') {
1226
- quote = c;
1227
- out += c;
1228
- i += 1;
1229
- continue;
1230
- }
1231
- const m = expr
1232
- .slice(i)
1233
- .match(/^(Object\.(keys|values|entries|assign|fromEntries)|Array\.(isArray|of)|String\.fromCharCode)\(/);
1234
- const prev = expr[i - 1];
1235
- if (m && !(prev && /[\w.]/.test(prev))) {
1236
- const method = m[1];
1237
- const openIdx = i + m[0].length - 1;
1238
- const closeIdx = matchBalancedParen(expr, openIdx);
1239
- if (closeIdx !== -1) {
1240
- const rawArgs = expr.slice(openIdx + 1, closeIdx);
1241
- if (method === 'Object.assign') {
1242
- const args = splitTopLevelArgs(rawArgs)
1243
- .map((a) => lowerObjectArrayDateBuiltinCalls(a, imports).trim())
1244
- .filter(Boolean);
1245
- if (args.length >= 1) {
1246
- // The request `body` is a Pydantic BaseModel, not a mapping, so
1247
- // `{**body}` raises TypeError. Unpack its dict form instead.
1248
- out += `{${args.map((a) => (a === 'body' ? '**body.model_dump()' : `**${a}`)).join(', ')}}`;
1249
- }
1250
- else {
1251
- out += '{}';
1252
- }
1253
- }
1254
- else if (method === 'Object.fromEntries') {
1255
- const arg = lowerObjectArrayDateBuiltinCalls(rawArgs, imports).trim();
1256
- out += `dict(${arg})`;
1257
- }
1258
- else if (method === 'Array.of') {
1259
- // Array.of(...items) is a plain list of its args (NOT Array(n) length).
1260
- const args = rawArgs.trim() === ''
1261
- ? []
1262
- : splitTopLevelArgs(rawArgs).map((a) => lowerObjectArrayDateBuiltinCalls(a, imports).trim());
1263
- out += `[${args.join(', ')}]`;
1264
- }
1265
- else if (method === 'String.fromCharCode') {
1266
- // chr() per code unit; join when there are several.
1267
- const args = rawArgs.trim() === ''
1268
- ? []
1269
- : splitTopLevelArgs(rawArgs).map((a) => lowerObjectArrayDateBuiltinCalls(a, imports).trim());
1270
- out +=
1271
- args.length === 0
1272
- ? '""'
1273
- : args.length === 1
1274
- ? `chr(${args[0]})`
1275
- : `''.join(chr(__c) for __c in [${args.join(', ')}])`;
1276
- }
1277
- else {
1278
- const arg = lowerObjectArrayDateBuiltinCalls(rawArgs, imports).trim();
1279
- if (method === 'Object.keys')
1280
- out += `list(${arg}.keys())`;
1281
- else if (method === 'Object.values')
1282
- out += `list(${arg}.values())`;
1283
- else if (method === 'Object.entries')
1284
- out += `list(${arg}.items())`;
1285
- else
1286
- out += `isinstance(${arg}, list)`;
1287
- }
1288
- i = closeIdx + 1;
1289
- continue;
1290
- }
1291
- }
1292
- if (expr.startsWith('Date.now()', i) && !(expr[i - 1] && /[\w.]/.test(expr[i - 1]))) {
1293
- imports?.add('from datetime import datetime, timezone');
1294
- out += 'int(datetime.now(timezone.utc).timestamp() * 1000)';
1295
- i += 'Date.now()'.length;
1296
- continue;
1297
- }
1298
- out += c;
1299
- i += 1;
1300
- }
1301
- return out;
1302
- }
1303
- // Build the Python comprehension for one `Array.from(...)` call's argument list,
1304
- // or return null if the call isn't a lowerable length-form. Uses the balanced
1305
- // helpers (not regex) so a length value or arrow params containing braces/parens
1306
- // don't desync (codex/gemini review of cd7c40ae).
1307
- function tryLowerArrayFrom(args) {
1308
- if (args.length < 2)
1309
- return null;
1310
- // arg0 must be an object literal whose `length` property gives the count.
1311
- const arg0 = args[0].trim();
1312
- if (!arg0.startsWith('{') || matchBalancedParen(arg0, 0) !== arg0.length - 1)
1313
- return null;
1314
- let count = null;
1315
- for (const prop of splitTopLevelArgs(arg0.slice(1, -1))) {
1316
- const mm = prop.match(/^(?:length|["']length["'])\s*:\s*([\s\S]+)$/);
1317
- if (mm) {
1318
- count = mm[1].trim();
1319
- break;
1320
- }
1321
- }
1322
- if (count === null)
1323
- return null;
1324
- // arg1 must be an arrow `(params) => body` or `param => body`.
1325
- const arrowStr = args[1].trim();
1326
- let params;
1327
- let body;
1328
- if (arrowStr.startsWith('(')) {
1329
- const pClose = matchBalancedParen(arrowStr, 0);
1330
- if (pClose === -1)
1331
- return null;
1332
- const after = arrowStr.slice(pClose + 1).trim();
1333
- if (!after.startsWith('=>'))
1334
- return null;
1335
- params = splitTopLevelArgs(arrowStr.slice(1, pClose))
1336
- .map((s) => s.trim())
1337
- .filter(Boolean);
1338
- body = after.slice(2).trim();
1339
- }
1340
- else {
1341
- const am = arrowStr.match(/^([A-Za-z_$][\w$]*)\s*=>\s*([\s\S]+)$/);
1342
- if (!am)
1343
- return null;
1344
- params = [am[1]];
1345
- body = am[2].trim();
1346
- }
1347
- // Loop var = the INDEX (2nd param). The 1st param is the element, which is
1348
- // undefined for the length form, so it is NOT promoted to the loop variable
1349
- // (doing so would diverge from JS — `(x) => x` is [undefined…], not [0,1,…]).
1350
- // A non-simple index (destructuring) isn't a valid Python loop target → bail.
1351
- const idxVar = params[1] || '_';
1352
- if (!/^[A-Za-z_$][\w$]*$/.test(idxVar))
1353
- return null;
1354
- // `(_, i) => ({...})` parenthesizes the object body to disambiguate it from a
1355
- // block; unwrap ONLY when the enclosed body is an object literal, so a comma
1356
- // operator `(1, 2)` or grouped expr isn't mis-stripped (codex review).
1357
- if (body.startsWith('(') && matchBalancedParen(body, 0) === body.length - 1) {
1358
- const inner = body.slice(1, -1).trim();
1359
- if (inner.startsWith('{'))
1360
- body = inner;
1361
- }
1362
- // Recurse so a nested Array.from in the count or body is lowered too.
1363
- return `[${lowerArrayFromCalls(body)} for ${idxVar} in range(${lowerArrayFromCalls(count)})]`;
1364
- }
1365
- // Expand JS object-literal shorthand properties to explicit `key: key` so the
1366
- // dict-key quoting pass can quote them: `{ items, page }` → `{ items: items,
1367
- // page: page }`. Bracket/string-aware: only an object-literal entry that is a
1368
- // bare identifier is expanded; `key: value`, `**spread`, computed keys, and
1369
- // array/comprehension contents (`[]`) are left alone, and nested objects are
1370
- // handled by recursing into each entry. Runs just before key quoting.
1371
- function expandObjectShorthand(expr) {
1372
- let out = '';
1373
- let i = 0;
1374
- let quote = null;
1375
- while (i < expr.length) {
1376
- const c = expr[i];
1377
- if (quote) {
1378
- out += c;
1379
- if (c === '\\') {
1380
- out += expr[i + 1] ?? '';
1381
- i += 2;
1382
- continue;
1383
- }
1384
- if (c === quote)
1385
- quote = null;
1386
- i += 1;
1387
- continue;
1388
- }
1389
- if (c === '"' || c === "'" || c === '`') {
1390
- quote = c;
1391
- out += c;
1392
- i += 1;
1393
- continue;
1394
- }
1395
- if (c === '{') {
1396
- const close = matchBalancedParen(expr, i);
1397
- if (close !== -1) {
1398
- const rebuilt = splitTopLevelArgs(expr.slice(i + 1, close)).map((entry) => {
1399
- const t = entry.trim();
1400
- if (t === '')
1401
- return entry;
1402
- if (/^[A-Za-z_$][\w$]*$/.test(t))
1403
- return `${t}: ${t}`;
1404
- return expandObjectShorthand(entry);
1405
- });
1406
- out += `{${rebuilt.join(', ')}}`;
1407
- i = close + 1;
1408
- continue;
1409
- }
1410
- }
1411
- out += c;
1412
- i += 1;
1413
- }
1414
- return out;
1415
- }
1416
- // Lower `Array.from({ length: N }, (_, i) => BODY)` to a Python list
1417
- // comprehension `[BODY for i in range(N)]` (Express keeps Array.from — valid
1418
- // JS). Balanced, string-aware scan; runs BEFORE the ref/key/template passes so
1419
- // they lower N and BODY in place. Only the `{ length: N }` form is handled;
1420
- // `Array.from(iterable, fn)` (map form) is left untouched. A call immediately
1421
- // followed by a method chain (`.map`, `.filter`, …) is left raw rather than
1422
- // lowered, because the array-method pass cannot consume a comprehension
1423
- // receiver and would emit malformed Python (codex review of cd7c40ae).
1424
- function lowerArrayFromCalls(expr) {
1425
- let out = '';
1426
- let i = 0;
1427
- let quote = null;
1428
- while (i < expr.length) {
1429
- const c = expr[i];
1430
- if (quote) {
1431
- out += c;
1432
- if (c === '\\') {
1433
- out += expr[i + 1] ?? '';
1434
- i += 2;
1435
- continue;
1436
- }
1437
- if (c === quote)
1438
- quote = null;
1439
- i += 1;
1440
- continue;
1441
- }
1442
- if (c === '"' || c === "'" || c === '`') {
1443
- quote = c;
1444
- out += c;
1445
- i += 1;
1446
- continue;
1447
- }
1448
- const m = expr.slice(i).match(/^Array\.from\(/);
1449
- const prev = expr[i - 1];
1450
- if (m && !(prev && /[\w.]/.test(prev))) {
1451
- const openIdx = i + m[0].length - 1;
1452
- const closeIdx = matchBalancedParen(expr, openIdx);
1453
- if (closeIdx !== -1 && expr[closeIdx + 1] !== '.') {
1454
- const lowered = tryLowerArrayFrom(splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)));
1455
- if (lowered !== null) {
1456
- out += lowered;
1457
- i = closeIdx + 1;
1458
- continue;
1459
- }
1460
- }
1461
- }
1462
- out += c;
1463
- i += 1;
1464
- }
1465
- return out;
1466
- }
1467
- function scanQuotedString(expr, startIndex, quote) {
1468
- for (let i = startIndex + 1; i < expr.length; i++) {
1469
- if (expr[i] === '\\') {
1470
- i += 1;
1471
- continue;
1472
- }
1473
- if (expr[i] === quote)
1474
- return i;
1475
- }
1476
- return -1;
1477
- }
1478
- function scanTemplateInterpolationEnd(expr, startIndex) {
1479
- let depth = 1;
1480
- for (let i = startIndex; i < expr.length; i++) {
1481
- const c = expr[i];
1482
- if (c === '"' || c === "'") {
1483
- const quotedEnd = scanQuotedString(expr, i, c);
1484
- if (quotedEnd === -1)
1485
- return -1;
1486
- i = quotedEnd;
1487
- continue;
1488
- }
1489
- if (c === '`') {
1490
- const templateEnd = scanTemplateLiteralEnd(expr, i);
1491
- if (templateEnd === -1)
1492
- return -1;
1493
- i = templateEnd;
1494
- continue;
1495
- }
1496
- if (c === '{')
1497
- depth += 1;
1498
- else if (c === '}') {
1499
- depth -= 1;
1500
- if (depth === 0)
1501
- return i;
1502
- }
1503
- }
1504
- return -1;
1505
- }
1506
- function scanTemplateLiteralEnd(expr, startIndex) {
1507
- for (let i = startIndex + 1; i < expr.length; i++) {
1508
- const c = expr[i];
1509
- if (c === '\\') {
1510
- i += 1;
1511
- continue;
1512
- }
1513
- if (c === '`')
1514
- return i;
1515
- if (c === '$' && expr[i + 1] === '{') {
1516
- const interpolationEnd = scanTemplateInterpolationEnd(expr, i + 2);
1517
- if (interpolationEnd === -1)
1518
- return -1;
1519
- i = interpolationEnd;
1520
- }
1521
- }
1522
- return -1;
1523
- }
1524
- function parseTemplateLiteral(expr, startIndex) {
1525
- const textParts = [];
1526
- const interpolationParts = [];
1527
- let text = '';
1528
- for (let i = startIndex + 1; i < expr.length;) {
1529
- const c = expr[i];
1530
- if (c === '\\') {
1531
- text += c;
1532
- if (i + 1 < expr.length)
1533
- text += expr[i + 1];
1534
- i += 2;
1535
- continue;
1536
- }
1537
- if (c === '`') {
1538
- textParts.push(text);
1539
- return { endIndex: i, textParts, interpolationParts };
1540
- }
1541
- if (c === '$' && expr[i + 1] === '{') {
1542
- textParts.push(text);
1543
- text = '';
1544
- const interpolationEnd = scanTemplateInterpolationEnd(expr, i + 2);
1545
- if (interpolationEnd === -1)
1546
- return undefined;
1547
- interpolationParts.push(expr.slice(i + 2, interpolationEnd));
1548
- i = interpolationEnd + 1;
1549
- continue;
1550
- }
1551
- text += c;
1552
- i += 1;
1553
- }
1554
- return undefined;
1555
- }
1556
- // Re-encode JS-template literal text (kept raw by parseTemplateLiteral, with `\x`
1557
- // as two characters) for a Python double-quoted string. Most JS escapes are
1558
- // ALSO valid Python escapes (`\n \t \r \b \f \v \\ \" \uXXXX \xXX \0`), so they
1559
- // are preserved verbatim — decoding then re-encoding them only risks corrupting
1560
- // the exotic ones (Codex reviews on 678e6bc1 and the escape-decoder commit).
1561
- // Only the JS-specific escapes that Python does not recognise are converted to
1562
- // the bare character: `\`` → backtick, `\$` → `$`, `\'` → `'`. A bare `"` (or a
1563
- // bare trailing backslash, or raw control char) is escaped so the literal stays
1564
- // valid.
1565
- function escapeJsTemplateTextForPy(raw) {
1566
- let out = '';
1567
- for (let i = 0; i < raw.length; i++) {
1568
- const c = raw[i];
1569
- if (c === '\\' && i + 1 < raw.length) {
1570
- const next = raw[i + 1];
1571
- if (next === '`' || next === '$' || next === "'") {
1572
- out += next; // JS-only escape → bare char (Python has no such escape)
1573
- }
1574
- else {
1575
- out += `\\${next}`; // valid Python escape (\n, \uXXXX, \0, ...) — keep
1576
- }
1577
- i += 1;
1578
- continue;
1579
- }
1580
- if (c === '\\')
1581
- out += '\\\\'; // lone trailing backslash
1582
- else if (c === '"')
1583
- out += '\\"';
1584
- else if (c === '\n')
1585
- out += '\\n';
1586
- else if (c === '\r')
1587
- out += '\\r';
1588
- else if (c === '\t')
1589
- out += '\\t';
1590
- else
1591
- out += c;
1592
- }
1593
- return out;
1594
- }
1595
- function escapePythonTemplateText(text, forFormatTemplate) {
1596
- const escaped = escapeJsTemplateTextForPy(text);
1597
- if (!forFormatTemplate)
1598
- return escaped;
1599
- // str.format treats { } as field markers, so literal braces must be doubled.
1600
- return escaped.replace(/{/g, '{{').replace(/}/g, '}}');
1601
- }
1602
- function lowerTemplateLiteralToPython(parsed, pathParams, bodyFields, authUser, imports) {
1603
- if (parsed.interpolationParts.length === 0) {
1604
- return `"${escapePythonTemplateText(parsed.textParts.join(''), false)}"`;
1605
- }
1606
- const rewrittenInterpolations = parsed.interpolationParts.map((part) => rewriteFastAPIExpr(part.trim(), pathParams, bodyFields, authUser, imports));
1607
- let fmt = '';
1608
- for (let i = 0; i < parsed.textParts.length; i++) {
1609
- fmt += escapePythonTemplateText(parsed.textParts[i], true);
1610
- if (i < parsed.interpolationParts.length)
1611
- fmt += '{}';
1612
- }
1613
- return `"${fmt}".format(${rewrittenInterpolations.join(', ')})`;
1614
- }
1615
- function extractTemplateLiterals(expr, pathParams, bodyFields, authUser, imports) {
1616
- let maskedExpr = '';
1617
- const replacements = [];
1618
- let quote = null;
1619
- for (let i = 0; i < expr.length;) {
1620
- const c = expr[i];
1621
- if (quote) {
1622
- maskedExpr += c;
1623
- if (c === '\\') {
1624
- maskedExpr += expr[i + 1] ?? '';
1625
- i += 2;
1626
- continue;
1627
- }
1628
- if (c === quote)
1629
- quote = null;
1630
- i += 1;
1631
- continue;
1632
- }
1633
- if (c === '"' || c === "'") {
1634
- quote = c;
1635
- maskedExpr += c;
1636
- i += 1;
1637
- continue;
1638
- }
1639
- if (c === '`') {
1640
- const parsed = parseTemplateLiteral(expr, i);
1641
- if (!parsed) {
1642
- maskedExpr += c;
1643
- i += 1;
1644
- continue;
1645
- }
1646
- const placeholder = `__KERN_TEMPLATE_${replacements.length}__`;
1647
- const lowered = lowerTemplateLiteralToPython(parsed, pathParams, bodyFields, authUser, imports);
1648
- replacements.push({ placeholder, lowered });
1649
- maskedExpr += placeholder;
1650
- i = parsed.endIndex + 1;
1651
- continue;
1652
- }
1653
- maskedExpr += c;
1654
- i += 1;
1655
- }
1656
- return { maskedExpr, replacements };
1657
- }
1658
- // Lower JS spread elements to Python unpacking, choosing the operator from the
1659
- // enclosing bracket: `{...x}` → `{**x}`, `[...x]` / `f(...x)` → `[*x]` / `f(*x)`.
1660
- // Bracket-aware (a stack) and string-aware (skips quoted contents) so a literal
1661
- // "..." inside a string is left intact. Runs BEFORE the request-ref rewrites so
1662
- // that, e.g., `...user.roles` becomes `*user.roles` and the auth rewrite's
1663
- // `(?<!\.)` lookbehind no longer sees the spread's trailing dot.
1664
- function lowerSpreadElements(expr) {
1665
- let out = '';
1666
- const stack = [];
1667
- let i = 0;
1668
- while (i < expr.length) {
1669
- const ch = expr[i];
1670
- if (ch === '"' || ch === "'") {
1671
- const q = ch;
1672
- out += ch;
1673
- i++;
1674
- while (i < expr.length) {
1675
- out += expr[i];
1676
- if (expr[i] === '\\') {
1677
- i++;
1678
- if (i < expr.length)
1679
- out += expr[i];
1680
- i++;
1681
- continue;
1682
- }
1683
- if (expr[i] === q) {
1684
- i++;
1685
- break;
1686
- }
1687
- i++;
1688
- }
1689
- continue;
1690
- }
1691
- if (ch === '{' || ch === '[' || ch === '(') {
1692
- stack.push(ch);
1693
- out += ch;
1694
- i++;
1695
- continue;
1696
- }
1697
- if (ch === '}' || ch === ']' || ch === ')') {
1698
- stack.pop();
1699
- out += ch;
1700
- i++;
1701
- continue;
1702
- }
1703
- if (ch === '.' && expr[i + 1] === '.' && expr[i + 2] === '.') {
1704
- out += stack[stack.length - 1] === '{' ? '**' : '*';
1705
- i += 3;
1706
- // Collapse whitespace after the operator so `{ ... body }` yields tight
1707
- // `{**body}` — the model_dump pass matches `**body`, not `** body` (Codex).
1708
- while (i < expr.length && /\s/.test(expr[i]))
1709
- i++;
1710
- continue;
1711
- }
1712
- out += ch;
1713
- i++;
1714
- }
1715
- return out;
1716
- }
1717
- // ── Portable JS operators whose Python spelling diverges or doesn't exist ────
1718
- // `%` (JS follows the DIVIDEND sign; Python's follows the divisor), `>>>` (no
1719
- // Python equivalent — emit a 32-bit-masked unsigned shift), and `??` (null-only
1720
- // coalesce, NOT falsy). Operand boundaries are found by a balanced, sign-aware
1721
- // scan over precedence "stop" chars; findNextJsOperator skips string literals.
1722
- function isUnarySign(expr, index) {
1723
- const c = expr[index];
1724
- if (c !== '+' && c !== '-')
1725
- return false;
1726
- let j = index - 1;
1727
- while (j >= 0 && /\s/.test(expr[j]))
1728
- j--;
1729
- return j < 0 || /[({[,:?+\-*/%<>=!&|^~]/.test(expr[j]);
1730
- }
1731
- function findLeftOperandStart(expr, opIndex, stopChars) {
1732
- let j = opIndex - 1;
1733
- while (j >= 0 && /\s/.test(expr[j]))
1734
- j--;
1735
- let depth = 0;
1736
- for (; j >= 0; j--) {
1737
- const c = expr[j];
1738
- if (c === '"' || c === "'" || c === '`') {
1739
- // A string operand is one atom — skip back to its opening quote (escape-
1740
- // aware) so a stop char inside the literal isn't mistaken for a boundary.
1741
- const q = c;
1742
- let k = j - 1;
1743
- while (k >= 0) {
1744
- if (expr[k] === q) {
1745
- let b = 0;
1746
- let p = k - 1;
1747
- while (p >= 0 && expr[p] === '\\') {
1748
- b++;
1749
- p--;
1750
- }
1751
- if (b % 2 === 0)
1752
- break;
1753
- }
1754
- k--;
1755
- }
1756
- j = k; // loop's j-- moves past the opening quote next
1757
- continue;
1758
- }
1759
- if (c === ')' || c === ']' || c === '}') {
1760
- depth++;
1761
- continue;
1762
- }
1763
- if (c === '(' || c === '[' || c === '{') {
1764
- if (depth === 0)
1765
- return j + 1;
1766
- depth--;
1767
- continue;
1768
- }
1769
- if (depth === 0 && stopChars.includes(c) && !isUnarySign(expr, j))
1770
- return j + 1;
1771
- }
1772
- return 0;
1773
- }
1774
- function findRightOperandEnd(expr, startIndex, stopChars) {
1775
- let j = startIndex;
1776
- while (j < expr.length && /\s/.test(expr[j]))
1777
- j++;
1778
- let depth = 0;
1779
- let quote = null;
1780
- for (; j < expr.length; j++) {
1781
- const c = expr[j];
1782
- if (quote) {
1783
- if (c === '\\') {
1784
- j++;
1785
- continue;
1786
- }
1787
- if (c === quote)
1788
- quote = null;
1789
- continue;
1790
- }
1791
- if (c === '"' || c === "'" || c === '`') {
1792
- quote = c;
1793
- continue;
1794
- }
1795
- if (c === '(' || c === '[' || c === '{') {
1796
- depth++;
1797
- continue;
1798
- }
1799
- if (c === ')' || c === ']' || c === '}') {
1800
- if (depth === 0)
1801
- return j;
1802
- depth--;
1803
- continue;
1804
- }
1805
- if (depth === 0 && stopChars.includes(c) && !isUnarySign(expr, j))
1806
- return j;
1807
- }
1808
- return expr.length;
1809
- }
1810
- function findNextJsOperator(expr, op, from) {
1811
- let quote = null;
1812
- for (let i = 0; i < expr.length; i++) {
1813
- const c = expr[i];
1814
- if (quote) {
1815
- if (c === '\\')
1816
- i += 1;
1817
- else if (c === quote)
1818
- quote = null;
1819
- continue;
1820
- }
1821
- if (c === '"' || c === "'" || c === '`') {
1822
- quote = c;
1823
- continue;
1824
- }
1825
- if (i >= from && expr.startsWith(op, i))
1826
- return i;
1827
- }
1828
- return -1;
1829
- }
1830
- function replaceJsOperator(expr, op, stopChars, lower) {
1831
- let result = expr;
1832
- let from = 0;
1833
- while (true) {
1834
- const opIndex = findNextJsOperator(result, op, from);
1835
- if (opIndex === -1)
1836
- return result;
1837
- const leftStart = findLeftOperandStart(result, opIndex, stopChars);
1838
- const rightEnd = findRightOperandEnd(result, opIndex + op.length, stopChars);
1839
- const left = result.slice(leftStart, opIndex).trim();
1840
- const right = result.slice(opIndex + op.length, rightEnd).trim();
1841
- if (!left || !right) {
1842
- // Not a binary use here — skip past this occurrence to avoid a loop.
1843
- from = opIndex + op.length;
1844
- continue;
1845
- }
1846
- const lowered = lower(left, right);
1847
- result = `${result.slice(0, leftStart)}${lowered}${result.slice(rightEnd)}`;
1848
- from = leftStart + lowered.length;
1849
- }
1850
- }
1851
- function lowerPortableJsOperators(expr, imports) {
1852
- // `%` binds at multiplicative precedence; `>>>` and `??` are loose (low).
1853
- const multiplicativeStops = ',:?+-*/%<>=!&|^';
1854
- const looseBinaryStops = ',:?';
1855
- let result = expr;
1856
- // >>> first (longest operator); mask the dividend to 32 bits and the shift
1857
- // count to 5 bits, mirroring JS ToUint32 + (count & 31).
1858
- result = replaceJsOperator(result, '>>>', looseBinaryStops, (l, r) => `((${l} & 0xFFFFFFFF) >> (${r} & 31))`);
1859
- // `%`: math.fmod follows the DIVIDEND sign like JS (and keeps the fractional
1860
- // part for float operands — int() would wrongly truncate 5.5 % 2 → 1).
1861
- result = replaceJsOperator(result, '%', multiplicativeStops, (l, r) => {
1862
- imports?.add('import math as __k_math');
1863
- return `__k_math.fmod(${l}, ${r})`;
1864
- });
1865
- result = replaceJsOperator(result, '??', looseBinaryStops, (l, r) => `(${l} if ${l} is not None else ${r})`);
1866
- return result;
1867
- }
1868
- export function rewriteFastAPIExpr(expr, pathParams, bodyFields = new Set(), authUser = false, imports, hoistedDefs, closureSeq) {
1869
- try {
1870
- const tokens = tokenizeJSExpr(expr);
1871
- const comparisonProbe = expr.replace(/>>>|>>|<</g, '');
1872
- const hasLooseComparison = /(?:===|!==|==|!=|<=|>=|<|>)/.test(comparisonProbe);
1873
- const hasBitwiseOrModulo = !expr.includes('=>') && !hasLooseComparison && tokens.some((t) => t.type === 'UNARY' || t.type === 'OP');
1874
- if (hasBitwiseOrModulo) {
1875
- const ast = parseTokens(tokens);
1876
- expr = codegenASTToPython(ast, imports);
1877
- }
1878
- }
1879
- catch (_err) {
1880
- // Graceful fallback to original expr string if parsing/emission fails
1881
- }
1882
- const { maskedExpr, replacements } = extractTemplateLiterals(expr, pathParams, bodyFields, authUser, imports);
1883
- let result = maskedExpr;
1884
- // Spread → unpacking first, so the request-ref rewrites below see clean
1885
- // operands (e.g. `*user.roles`, not `...user.roles`).
1886
- result = lowerSpreadElements(result);
1887
- // Expand object shorthand BEFORE Array.from lowering, so a shorthand length
1888
- // object `Array.from({ length }, …)` becomes `{ length: length }` and is
1889
- // recognised (codex review of d75a9d05). No later pass creates new object
1890
- // literals, so this single early pass covers length objects, arrow bodies,
1891
- // and every other object.
1892
- result = expandObjectShorthand(result);
1893
- // Array.from(length, arrow) → list comprehension. Runs before the ref/key
1894
- // passes so they lower the count and body of the produced comprehension.
1895
- result = lowerArrayFromCalls(result);
1896
- // params.X → X (function param) for path params
1897
- for (const param of pathParams) {
1898
- result = result.replace(new RegExp(`\\bparams\\.${param}\\b`, 'g'), param);
1899
- }
1900
- // Fallback: any remaining params.X → X (for query params not in pathParams)
1901
- result = result.replace(/\bparams\.([A-Za-z_]\w*)/g, '$1');
1902
- // user.X → user["X"]: with auth, `user` is the decoded JWT payload (a dict
1903
- // returned by auth_required/auth_optional), so attribute access would raise
1904
- // AttributeError. Only applied when the route declares auth (Codex review).
1905
- // Skip text inside string literals so `{{"user.id"}}` isn't corrupted to
1906
- // `"user["id"]"` (Codex review on 02ecb2fa), and require `user` NOT be a
1907
- // property of something else (negative lookbehind `(?<!\.)`) so a nested
1908
- // body access like `body.user.id` is left intact (Kimi review on 02ecb2fa).
1909
- if (authUser) {
1910
- const USER_FIELD_RE = new RegExp(`${STRING_LITERAL_ALT}|(?<!\\.)\\buser\\.([A-Za-z_]\\w*)`, 'g');
1911
- result = result.replace(USER_FIELD_RE, (match, field) => (field ? `user["${field}"]` : match));
1912
- }
1913
- // body.X → body.<snake_case(X)>: the generated Pydantic model snake-cases
1914
- // every field, so a camelCase access would raise AttributeError at runtime.
1915
- // Only remap fields the model actually declares; leave unknown `body.X`
1916
- // (e.g. external validate schemas) untouched.
1917
- result = result.replace(/\bbody\.([A-Za-z_]\w*)/g, (match, field) => bodyFields.has(field) ? `body.${toSnakeCase(field)}` : match);
1918
- // Spreading the whole request body: `{**body}` raises TypeError because a
1919
- // Pydantic model is not a mapping, so unpack its dict form instead. This is
1920
- // unconditional: whenever the `body` symbol exists it is a Pydantic model
1921
- // (inline `RequestBody`, or an external `validate` schema typed `body: X` for
1922
- // POST/PUT/PATCH) — there is no `body: dict` codegen path, so model_dump() is
1923
- // always correct. Keying on bodyFields would wrongly skip external schemas
1924
- // (their field names are unknown but the param is still a model). A
1925
- // `**body.field` member spread is left alone via the `(?!\s*\.)` guard.
1926
- result = result.replace(/\*\*body\b(?!\s*\.)/g, '**body.model_dump()');
1927
- // query.X → X (function param)
1928
- result = result.replace(/\bquery\.([A-Za-z_]\w*)/g, '$1');
1929
- // headers.X → request.headers.get("X")
1930
- result = result.replace(/\bheaders\.([A-Za-z_][\w-]*)/g, (_m, key) => `request.headers.get("${key}")`);
1931
- // effectName.result → effect_name (effect variables hold the result directly, snake_cased)
1932
- result = result.replace(/\b([A-Za-z_]\w*)\.result\b/g, (_m, name) => toSnakeCase(name));
1933
- // ── JS-to-Python expression lowerings ─────────────────────────────────
1934
- // Array methods first (so any `===` inside an arrow body is hoisted into
1935
- // a list-comprehension predicate that the strict-equality pass below
1936
- // then catches).
1937
- result = lowerJsArrayMethods(result, { pathParams, bodyFields, authUser, imports, hoistedDefs, closureSeq });
1938
- // Strict equality: skip text inside quoted strings so a user message
1939
- // like `"use === for strict equality"` doesn't get mangled to `==`.
1940
- result = result.replace(STRICT_EQ_RE, (match) => {
1941
- if (match === '===')
1942
- return '==';
1943
- if (match === '!==')
1944
- return '!=';
1945
- return match; // quoted string — return unchanged
1946
- });
1947
- // JS literals → Python equivalents. Same string-skip trick — a message
1948
- // like `"undefined behavior"` must not be rewritten to `"None behavior"`.
1949
- result = result.replace(JS_LITERAL_RE, (match) => {
1950
- if (match === 'undefined' || match === 'null')
1951
- return 'None';
1952
- if (match === 'true')
1953
- return 'True';
1954
- if (match === 'false')
1955
- return 'False';
1956
- return match; // quoted string
1957
- });
1958
- // ── Host-builtin lowering (JS globals → Python stdlib) ────────────────
1959
- // crypto / Date are fixed forms matched by regex with a `(?<![\w.])` guard so
1960
- // a custom receiver (`some.crypto.randomUUID()`) is left untouched. The JSON
1961
- // calls need balanced argument parsing (regex can't), so they go through the
1962
- // string-aware scanner `lowerJsonBuiltinCalls`.
1963
- // crypto.randomUUID() → str(uuid.uuid4())
1964
- result = result.replace(new RegExp(`${STRING_LITERAL_ALT}|(?<![\\w.])crypto\\.randomUUID\\(\\)`, 'g'), (match) => {
1965
- if (match === 'crypto.randomUUID()') {
1966
- imports?.add('import uuid');
1967
- return 'str(uuid.uuid4())';
1968
- }
1969
- return match; // string literal — leave untouched
1970
- });
1971
- // new Date().toISOString() → datetime.now(timezone.utc).isoformat()
1972
- result = result.replace(new RegExp(`${STRING_LITERAL_ALT}|(?<![\\w.])new Date\\(\\)\\.toISOString\\(\\)`, 'g'), (match) => {
1973
- if (match === 'new Date().toISOString()') {
1974
- imports?.add('from datetime import datetime, timezone');
1975
- return 'datetime.now(timezone.utc).isoformat()';
1976
- }
1977
- return match;
1978
- });
1979
- // Portable operators whose Python spelling diverges (`%`, `??`) or is absent
1980
- // (`>>>`) — lower before the number builtins emit any `%`-format strings.
1981
- result = lowerPortableJsOperators(result, imports);
1982
- // JSON.stringify(...) → json.dumps(...) / JSON.parse(...) → json.loads(...)
1983
- result = lowerJsonBuiltinCalls(result, imports);
1984
- // Number/Math arithmetic builtins in portable expressions.
1985
- result = lowerMathBuiltinCalls(result, imports);
1986
- // Number parsing and formatting builtins.
1987
- result = lowerNumberBuiltinCalls(result, imports);
1988
- // String builtins in portable expressions (bare renames + pad → rjust/ljust).
1989
- result = lowerStringBuiltinCalls(result);
1990
- // Argument-taking string methods: replace (first-only), substring → slice,
1991
- // repeat → `*`, and the split(sep, limit) maxsplit trap.
1992
- result = lowerStringArgMethods(result);
1993
- // Object/Array/Date host builtins in portable expressions.
1994
- result = lowerObjectArrayDateBuiltinCalls(result, imports);
1995
- // Object-literal keys → quoted Python dict keys (`{userId: x}` →
1996
- // `{"userId": x}`). Applied last, mirroring the raw `res.json(...)` path's
1997
- // outer quote-after-lower order; runs after array-method lowering so dicts
1998
- // produced inside list comprehensions are quoted too.
1999
- result = quoteObjectKeysOutsideStrings(result);
2000
- for (const replacement of replacements) {
2001
- result = result.split(replacement.placeholder).join(replacement.lowered);
2002
- }
2003
- result = result.split(LAMBDA_COLON_PLACEHOLDER).join(':');
2004
- return result;
2005
- }
2006
- export function extractExprCode(prop) {
2007
- if (typeof prop === 'object' && prop !== null && prop.__expr)
2008
- return prop.code;
2009
- return typeof prop === 'string' ? prop : '';
2010
- }
2011
42
  export function addRespondImports(respondNode, imports) {
2012
43
  const rp = getProps(respondNode);
2013
44
  if (rp.redirect)
@@ -2021,250 +52,4 @@ export function addRespondImports(respondNode, imports) {
2021
52
  if (rp.error)
2022
53
  imports.add('from fastapi import HTTPException');
2023
54
  }
2024
- function tokenizeJSExpr(expr) {
2025
- const tokens = [];
2026
- let i = 0;
2027
- while (i < expr.length) {
2028
- while (i < expr.length && /\s/.test(expr[i])) {
2029
- i++;
2030
- }
2031
- if (i >= expr.length)
2032
- break;
2033
- const char = expr[i];
2034
- if (char === '(') {
2035
- tokens.push({ type: 'LP' });
2036
- i++;
2037
- continue;
2038
- }
2039
- if (char === ')') {
2040
- tokens.push({ type: 'RP' });
2041
- i++;
2042
- continue;
2043
- }
2044
- if (char === '~') {
2045
- tokens.push({ type: 'UNARY', value: '~' });
2046
- i++;
2047
- continue;
2048
- }
2049
- if (char === '"' || char === "'" || char === '`') {
2050
- const quote = char;
2051
- let val = quote;
2052
- i++;
2053
- while (i < expr.length) {
2054
- const c = expr[i];
2055
- val += c;
2056
- if (c === '\\') {
2057
- val += expr[i + 1] ?? '';
2058
- i += 2;
2059
- continue;
2060
- }
2061
- if (c === quote) {
2062
- i++;
2063
- break;
2064
- }
2065
- i++;
2066
- }
2067
- tokens.push({ type: 'TEXT', value: val });
2068
- continue;
2069
- }
2070
- if (char === '&') {
2071
- if (expr[i + 1] === '&') {
2072
- // Fall through to TEXT
2073
- }
2074
- else {
2075
- tokens.push({ type: 'OP', value: '&' });
2076
- i++;
2077
- continue;
2078
- }
2079
- }
2080
- if (char === '|') {
2081
- if (expr[i + 1] === '|') {
2082
- // Fall through to TEXT
2083
- }
2084
- else {
2085
- tokens.push({ type: 'OP', value: '|' });
2086
- i++;
2087
- continue;
2088
- }
2089
- }
2090
- if (char === '^' || char === '%') {
2091
- tokens.push({ type: 'OP', value: char });
2092
- i++;
2093
- continue;
2094
- }
2095
- if (char === '<' && expr[i + 1] === '<') {
2096
- tokens.push({ type: 'OP', value: '<<' });
2097
- i += 2;
2098
- continue;
2099
- }
2100
- // `>>>` (unsigned right shift) is intentionally OUT of the int32 AST path
2101
- // (deferred — see the numbermodel oracle note). Bail the whole expr out of
2102
- // the AST path by throwing (caught in rewriteFastAPIExpr) so the downstream
2103
- // string-level operator lowering — which 32-bit-masks `>>>` correctly —
2104
- // handles it. Must be checked BEFORE `>>`, else `>>` greedily eats two of
2105
- // the three `>` and leaves a stray `>` that mangles the operand.
2106
- if (char === '>' && expr[i + 1] === '>' && expr[i + 2] === '>') {
2107
- throw new Error('unsupported-operator: >>> (defer to string lowering)');
2108
- }
2109
- if (char === '>' && expr[i + 1] === '>') {
2110
- tokens.push({ type: 'OP', value: '>>' });
2111
- i += 2;
2112
- continue;
2113
- }
2114
- let text = '';
2115
- while (i < expr.length) {
2116
- const c = expr[i];
2117
- if (c === '(' || c === ')' || c === '~' || c === '^' || c === '%') {
2118
- break;
2119
- }
2120
- if (c === '"' || c === "'" || c === '`') {
2121
- break;
2122
- }
2123
- if (c === '&') {
2124
- if (expr[i + 1] === '&') {
2125
- text += '&&';
2126
- i += 2;
2127
- continue;
2128
- }
2129
- else {
2130
- break;
2131
- }
2132
- }
2133
- if (c === '|') {
2134
- if (expr[i + 1] === '|') {
2135
- text += '||';
2136
- i += 2;
2137
- continue;
2138
- }
2139
- else {
2140
- break;
2141
- }
2142
- }
2143
- if (c === '<' && expr[i + 1] === '<') {
2144
- break;
2145
- }
2146
- if (c === '>' && expr[i + 1] === '>') {
2147
- break;
2148
- }
2149
- text += c;
2150
- i++;
2151
- }
2152
- if (text) {
2153
- tokens.push({ type: 'TEXT', value: text.trimEnd() });
2154
- }
2155
- }
2156
- return tokens;
2157
- }
2158
- function parseTokens(tokens) {
2159
- let index = 0;
2160
- function peek() {
2161
- return tokens[index];
2162
- }
2163
- function consume() {
2164
- return tokens[index++];
2165
- }
2166
- function getPrecedence(op) {
2167
- switch (op) {
2168
- case '|':
2169
- return 1;
2170
- case '^':
2171
- return 2;
2172
- case '&':
2173
- return 3;
2174
- case '<<':
2175
- case '>>':
2176
- return 4;
2177
- case '%':
2178
- return 5;
2179
- default:
2180
- return 0;
2181
- }
2182
- }
2183
- function parseExpression(precedence) {
2184
- let left = parsePrimary();
2185
- while (true) {
2186
- const next = peek();
2187
- if (!next || next.type !== 'OP')
2188
- break;
2189
- const opPrecedence = getPrecedence(next.value);
2190
- if (opPrecedence < precedence)
2191
- break;
2192
- consume();
2193
- const right = parseExpression(opPrecedence + 1);
2194
- left = { type: 'binary', op: next.value, left, right };
2195
- }
2196
- return left;
2197
- }
2198
- function parsePrimary() {
2199
- const t = peek();
2200
- if (!t)
2201
- throw new Error('Unexpected EOF');
2202
- if (t.type === 'UNARY') {
2203
- consume();
2204
- const arg = parseExpression(6);
2205
- return { type: 'unary', op: t.value, arg };
2206
- }
2207
- if (t.type === 'LP') {
2208
- consume();
2209
- const inner = parseExpression(0);
2210
- const next = peek();
2211
- if (next && next.type === 'RP') {
2212
- consume();
2213
- }
2214
- return { type: 'group', arg: inner };
2215
- }
2216
- if (t.type === 'TEXT') {
2217
- consume();
2218
- return { type: 'text', value: t.value };
2219
- }
2220
- consume();
2221
- return { type: 'text', value: t.type === 'OP' ? t.value : '' };
2222
- }
2223
- return parseExpression(0);
2224
- }
2225
- function codegenASTToPython(node, imports) {
2226
- switch (node.type) {
2227
- case 'text':
2228
- return node.value;
2229
- case 'group':
2230
- return `(${codegenASTToPython(node.arg, imports)})`;
2231
- case 'unary': {
2232
- const argStr = codegenASTToPython(node.arg, imports);
2233
- if (node.op === '~') {
2234
- imports?.add(KERN_I32_HELPER_PY);
2235
- return `_i32(~_i32(${argStr}))`;
2236
- }
2237
- return `${node.op}${argStr}`;
2238
- }
2239
- case 'binary': {
2240
- const leftStr = codegenASTToPython(node.left, imports);
2241
- const rightStr = codegenASTToPython(node.right, imports);
2242
- if (node.op === '|') {
2243
- imports?.add(KERN_I32_HELPER_PY);
2244
- return `_i32(_i32(${leftStr}) | _i32(${rightStr}))`;
2245
- }
2246
- if (node.op === '&') {
2247
- imports?.add(KERN_I32_HELPER_PY);
2248
- return `_i32(_i32(${leftStr}) & _i32(${rightStr}))`;
2249
- }
2250
- if (node.op === '^') {
2251
- imports?.add(KERN_I32_HELPER_PY);
2252
- return `_i32(_i32(${leftStr}) ^ _i32(${rightStr}))`;
2253
- }
2254
- if (node.op === '<<') {
2255
- imports?.add(KERN_I32_HELPER_PY);
2256
- return `_i32(_i32(${leftStr}) << (_i32(${rightStr}) & 31))`;
2257
- }
2258
- if (node.op === '>>') {
2259
- imports?.add(KERN_I32_HELPER_PY);
2260
- return `_i32(_i32(${leftStr}) >> (_i32(${rightStr}) & 31))`;
2261
- }
2262
- if (node.op === '%') {
2263
- imports?.add(KERN_TMOD_HELPER_PY);
2264
- return `_tmod(${leftStr}, ${rightStr})`;
2265
- }
2266
- return `${leftStr} ${node.op} ${rightStr}`;
2267
- }
2268
- }
2269
- }
2270
55
  //# sourceMappingURL=fastapi-response.js.map