@kernlang/python 3.5.6 → 3.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/django.d.ts +49 -0
- package/dist/adapters/django.js +151 -0
- package/dist/adapters/django.js.map +1 -0
- package/dist/adapters/fastapi.d.ts +43 -0
- package/dist/adapters/fastapi.js +139 -0
- package/dist/adapters/fastapi.js.map +1 -0
- package/dist/codegen-body-python.d.ts +17 -5
- package/dist/codegen-body-python.js +83 -67
- package/dist/codegen-body-python.js.map +1 -1
- package/dist/core/expr/helpers.d.ts +5 -0
- package/dist/core/expr/helpers.js +62 -0
- package/dist/core/expr/helpers.js.map +1 -0
- package/dist/core/expr/index.d.ts +9 -0
- package/dist/core/expr/index.js +2046 -0
- package/dist/core/expr/index.js.map +1 -0
- package/dist/core/handlers/index.d.ts +74 -0
- package/dist/core/handlers/index.js +462 -0
- package/dist/core/handlers/index.js.map +1 -0
- package/dist/fastapi-portable.js +3 -2
- package/dist/fastapi-portable.js.map +1 -1
- package/dist/fastapi-response.d.ts +0 -6
- package/dist/fastapi-response.js +2 -2217
- package/dist/fastapi-response.js.map +1 -1
- package/dist/fastapi-route.js +24 -3
- package/dist/fastapi-route.js.map +1 -1
- package/dist/fastapi-utils.d.ts +2 -1
- package/dist/fastapi-utils.js +2 -58
- package/dist/fastapi-utils.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ir-semantics/python-leg.js +5 -0
- package/dist/ir-semantics/python-leg.js.map +1 -1
- package/dist/targets/python.d.ts +27 -0
- package/dist/targets/python.js +130 -4
- package/dist/targets/python.js.map +1 -1
- package/package.json +2 -2
package/dist/fastapi-response.js
CHANGED
|
@@ -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
|
|
10
|
-
import {
|
|
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
|