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