@kernlang/python 3.5.9-canary.220.1.c398cd95 → 4.0.0

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.
@@ -3,8 +3,9 @@
3
3
  */
4
4
  import { lowerJsClosureBodyToPython, PORTABLE_LOGIC_PRIMITIVES } from '@kernlang/core';
5
5
  import { toSnakeCase } from '../../type-map.js';
6
- import { KERN_I32_HELPER_PY, KERN_JS_HELPER_PY, KERN_JS_OBJECT_HELPERS_PY, KERN_JS_STRING_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './helpers.js';
7
- export { KERN_FMT_HELPER_PY, KERN_I32_HELPER_PY, KERN_JS_HELPER_PY, KERN_JS_OBJECT_HELPERS_PY, KERN_JS_STRING_HELPERS_PY, KERN_PAIR_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './helpers.js';
6
+ import { KERN_I32_HELPER_PY, KERN_JS_ARRAY_HELPERS_PY, KERN_JS_HELPER_PY, KERN_JS_OBJECT_HELPERS_PY, KERN_JS_STRING_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './helpers.js';
7
+ import { isSharedPortableArrayMethod, isSharedPortableArrayProperty, lowerPortableArrayMethodPy, lowerPortableArrayPropertyPy, } from './list-ops.js';
8
+ export { KERN_FMT_HELPER_PY, KERN_I32_HELPER_PY, KERN_JS_ARRAY_HELPERS_PY, KERN_JS_HELPER_PY, KERN_JS_OBJECT_HELPERS_PY, KERN_JS_STRING_HELPERS_PY, KERN_PAIR_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './helpers.js';
8
9
  // Quoted strings absorbed by the alternation; only literal `===`/`!==`
9
10
  // outside strings get rewritten. Both single and double quotes AND
10
11
  // backtick template literals are covered so a message like
@@ -194,12 +195,28 @@ function lowerArrowBlockClosure(arrow, ctx) {
194
195
  const result = lowerJsClosureBodyToPython(arrow.body, {
195
196
  lowerExpression: (raw) => rewriteExpr(lowerDictMemberAccess(raw, arrow.params[0]), ctx.pathParams, ctx.bodyFields, ctx.authUser, ctx.imports, undefined, ctx.closureSeq),
196
197
  lowerCondition: (raw) => `js_truthy(${rewriteExpr(lowerDictMemberAccess(raw, arrow.params[0]), ctx.pathParams, ctx.bodyFields, ctx.authUser, ctx.imports, undefined, ctx.closureSeq)})`,
198
+ // Closure params are def-locals (never `nonlocal`); the lowerer excludes
199
+ // them and block-locals from the written-free set.
200
+ paramNames: arrow.params,
197
201
  });
198
202
  if (!result.ok)
199
203
  return null;
200
204
  ctx.imports?.add(KERN_JS_HELPER_PY);
201
205
  const params = arrow.params.join(', ');
202
- const def = [`def ${name}(${params}):`, ...(result.lines.length > 0 ? result.lines : [' pass'])].join('\n');
206
+ // Mutation v1 free-variable WRITES need `nonlocal`. The route hoisted def
207
+ // nests INSIDE the route handler function, so a free capture that the closure
208
+ // writes is a handler-local (a `derive`/method-local). Unlike the class/
209
+ // native path (`emitBlockClosurePy`), the route path has NO loop-pinning
210
+ // concept — every written free name is an outer handler binding and ALL of
211
+ // them get a `nonlocal` declaration. Without it the def shadows the name and
212
+ // raises `UnboundLocalError` (read+write) or silently writes a dead local
213
+ // (write-only) — a live route bug this fixes. `nonlocal` is the def's FIRST
214
+ // body statement (Python requires it before any use). Member/index writes
215
+ // never appear in `writtenFreeNames` (by-reference mutation needs no decl).
216
+ const sortedFreeWrites = [...result.writtenFreeNames].sort();
217
+ const nonlocalLines = sortedFreeWrites.length > 0 ? [` nonlocal ${sortedFreeWrites.join(', ')}`] : [];
218
+ const bodyLines = result.lines.length > 0 ? result.lines : [' pass'];
219
+ const def = [`def ${name}(${params}):`, ...nonlocalLines, ...bodyLines].join('\n');
203
220
  if (ctx.hoistedDefs) {
204
221
  ctx.hoistedDefs.push(def);
205
222
  }
@@ -255,29 +272,35 @@ function lowerJsArrayMethods(expr, ctx) {
255
272
  // name.
256
273
  const loopTarget = idxVar ? `${idxVar}, ${elemVar}` : elemVar;
257
274
  const source = idxVar ? `enumerate(${receiver})` : receiver;
275
+ // filter/find-family predicates wrap the body in `js_truthy(...)`:
276
+ // a predicate that yields a JS-truthy empty container ([] / {}) must be
277
+ // KEPT, but Python treats [] / {} as falsy, so a bare `if body` would
278
+ // wrongly drop it. `js_truthy` restores JS truthiness. (`map`/`flatMap`
279
+ // have no predicate and are left untouched.) The helper lands once.
280
+ ctx.imports?.add(KERN_JS_HELPER_PY);
258
281
  let lowered;
259
282
  if (method === 'filter') {
260
- lowered = `[${elemVar} for ${loopTarget} in ${source} if ${body}]`;
283
+ lowered = `[${elemVar} for ${loopTarget} in ${source} if js_truthy(${body})]`;
261
284
  }
262
285
  else if (method === 'find') {
263
- lowered = `next((${elemVar} for ${loopTarget} in ${source} if ${body}), None)`;
286
+ lowered = `next((${elemVar} for ${loopTarget} in ${source} if js_truthy(${body})), None)`;
264
287
  }
265
288
  else if (method === 'findIndex') {
266
289
  // index of the first match, or -1 (never raises). Bind the user's
267
290
  // own index var when the callback has one, so `(x, i) => …i…` works.
268
291
  const ix = idxVar ?? '__i';
269
- lowered = `next((${ix} for ${ix}, ${elemVar} in enumerate(${receiver}) if ${body}), -1)`;
292
+ lowered = `next((${ix} for ${ix}, ${elemVar} in enumerate(${receiver}) if js_truthy(${body})), -1)`;
270
293
  }
271
294
  else if (method === 'findLast') {
272
295
  // last matching element, or None
273
296
  lowered = idxVar
274
- ? `next((${elemVar} for ${idxVar}, ${elemVar} in reversed(list(enumerate(${receiver}))) if ${body}), None)`
275
- : `next((${elemVar} for ${elemVar} in reversed(${receiver}) if ${body}), None)`;
297
+ ? `next((${elemVar} for ${idxVar}, ${elemVar} in reversed(list(enumerate(${receiver}))) if js_truthy(${body})), None)`
298
+ : `next((${elemVar} for ${elemVar} in reversed(${receiver}) if js_truthy(${body})), None)`;
276
299
  }
277
300
  else if (method === 'findLastIndex') {
278
301
  // index of the last match, or -1
279
302
  const ix = idxVar ?? '__i';
280
- lowered = `next((${ix} for ${ix}, ${elemVar} in reversed(list(enumerate(${receiver}))) if ${body}), -1)`;
303
+ lowered = `next((${ix} for ${ix}, ${elemVar} in reversed(list(enumerate(${receiver}))) if js_truthy(${body})), -1)`;
281
304
  }
282
305
  else if (method === 'flatMap') {
283
306
  // map, then flatten ONE level — JS flatMap only flattens arrays, so
@@ -303,57 +326,29 @@ function lowerJsArrayMethods(expr, ctx) {
303
326
  if (closeIdx !== -1 && recvStart !== -1) {
304
327
  const receiver = out.slice(recvStart);
305
328
  const pre = out.slice(0, recvStart);
306
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerJsArrayMethods(a.trim(), ctx));
329
+ const rawArgs = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
330
+ const args = rawArgs.map((a) => method === 'fill' ? lowerJsFillArgument(a.trim(), ctx) : lowerJsArrayMethods(a.trim(), ctx));
307
331
  let lowered = null;
308
- if (method === 'includes') {
309
- const needle = args[0] ?? '';
310
- lowered = `(${needle} in ${receiver})`;
311
- }
312
- else if (method === 'indexOf') {
313
- const needle = args[0] ?? '';
314
- const fromIndex = args[1] ?? null;
315
- if (fromIndex) {
316
- lowered = `(next((__i for __i, __v in enumerate(${receiver}) if __i >= ${fromIndex} and __v == ${needle}), -1))`;
317
- }
318
- else {
319
- lowered = `(next((__i for __i, __v in enumerate(${receiver}) if __v == ${needle}), -1))`;
332
+ if (isSharedPortableArrayMethod(method)) {
333
+ // Delegate the argument-shape (non-lambda) scalar methods —
334
+ // push/slice/concat/includes/indexOf/join/flat/reverse/at/fill/
335
+ // lastIndexOf — to the single shared list-ops lowering (also used by
336
+ // the class-method body emitter) so routes and class methods can't
337
+ // drift. The shared helper returns null for arg-count shapes it
338
+ // doesn't support (e.g. multi-arg concat), and `lowered` stays null so
339
+ // the caller falls through unchanged — the same gap as the pre-sweep
340
+ // inline branches. The method names here are disjoint from the
341
+ // lambda-bearing some/every/reduce/reduceRight/sort branches below, so
342
+ // chain order does not matter.
343
+ const imports = ctx.imports;
344
+ const portable = method === 'fill' && !imports ? null : lowerPortableArrayMethodPy(receiver, method, args);
345
+ if (portable !== null) {
346
+ if (method === 'fill') {
347
+ imports?.add(KERN_JS_ARRAY_HELPERS_PY);
348
+ }
349
+ lowered = portable;
320
350
  }
321
351
  }
322
- else if (method === 'push') {
323
- // JS Array.push mutates AND returns the new length. Python list.append
324
- // returns None, so emit `(recv.append(x) or len(recv))` for exact parity
325
- // (mutate + length). Single-arg only; varargs push left unsupported.
326
- if (args.length === 1)
327
- lowered = `(${receiver}.append(${args[0]}) or len(${receiver}))`;
328
- }
329
- else if (method === 'reverse') {
330
- // JS Array.reverse mutates AND returns the (same, reversed) array; Python
331
- // list.reverse returns None -> `(recv.reverse() or recv)` mutates + returns it.
332
- lowered = `(${receiver}.reverse() or ${receiver})`;
333
- }
334
- else if (method === 'concat') {
335
- // JS Array.concat returns a NEW array; an array arg is spread, a scalar arg
336
- // is appended. Mirror with `recv + (x if isinstance(x, list) else [x])`.
337
- // Single-arg only; varargs concat left unsupported.
338
- if (args.length === 1)
339
- lowered = `(${receiver} + (${args[0]} if isinstance(${args[0]}, list) else [${args[0]}]))`;
340
- }
341
- else if (method === 'join') {
342
- const sep = args[0] ?? '","';
343
- lowered = `${sep}.join(str(__v) for __v in ${receiver})`;
344
- }
345
- else if (method === 'slice') {
346
- const start = args[0];
347
- const end = args[1];
348
- if (!start && !end)
349
- lowered = `${receiver}[:]`;
350
- else if (start && !end)
351
- lowered = `${receiver}[${start}:]`;
352
- else if (!start && end)
353
- lowered = `${receiver}[:${end}]`;
354
- else
355
- lowered = `${receiver}[${start}:${end}]`;
356
- }
357
352
  else if (method === 'some' || method === 'every') {
358
353
  const arrow = parseArrowCallback(expr.slice(openIdx + 1, closeIdx));
359
354
  if (arrow && arrow.params.length >= 1) {
@@ -365,10 +360,17 @@ function lowerJsArrayMethods(expr, ctx) {
365
360
  const pred = blockClosure ?? lowerJsArrayMethods(lowerDictMemberAccess(arrow.body, elemVar), ctx);
366
361
  const loopTarget = idxVar ? `${idxVar}, ${elemVar}` : elemVar;
367
362
  const source = idxVar ? `enumerate(${receiver})` : receiver;
363
+ // Wrap the predicate in `js_truthy(...)` for JS truthiness parity (a
364
+ // predicate yielding [] / {} is JS-truthy but Python-falsy). Skip the
365
+ // wrap when `pred` is ALREADY a js_truthy(...) call — the block-closure
366
+ // path's `lowerCondition` can emit one — to avoid emit-noise double
367
+ // wrapping (harmless but ugly). The helper lands once.
368
+ const wrappedPred = pred.startsWith('js_truthy(') ? pred : `js_truthy(${pred})`;
369
+ ctx.imports?.add(KERN_JS_HELPER_PY);
368
370
  lowered =
369
371
  method === 'some'
370
- ? `any(${pred} for ${loopTarget} in ${source})`
371
- : `all(${pred} for ${loopTarget} in ${source})`;
372
+ ? `any(${wrappedPred} for ${loopTarget} in ${source})`
373
+ : `all(${wrappedPred} for ${loopTarget} in ${source})`;
372
374
  }
373
375
  }
374
376
  else if (method === 'reduce') {
@@ -425,33 +427,6 @@ function lowerJsArrayMethods(expr, ctx) {
425
427
  lowered = `sorted(${receiver}, key=lambda __v${LAMBDA_COLON_PLACEHOLDER} str(__v))`;
426
428
  }
427
429
  }
428
- else if (method === 'flat') {
429
- // one level: flatten nested lists, keep scalars
430
- lowered = `[__y for __x in ${receiver} for __y in (__x if isinstance(__x, list) else [__x])]`;
431
- }
432
- else if (method === 'at') {
433
- const n = args[0] ?? '0';
434
- lowered = `(${receiver}[${n}] if -len(${receiver}) <= ${n} < len(${receiver}) else None)`;
435
- }
436
- else if (method === 'fill') {
437
- const v = args[0] ?? 'None';
438
- if (args.length <= 1) {
439
- lowered = `[${v} for __ in ${receiver}]`;
440
- }
441
- else {
442
- // fill(value, start, end) fills [start, end) with JS negative-index
443
- // normalization; untouched positions keep their original element.
444
- const s = args[1];
445
- const e = args[2] ?? `len(${receiver})`;
446
- lowered = `[(${v} if (${s} if ${s} >= 0 else ${s} + len(${receiver})) <= __i < (${e} if ${e} >= 0 else ${e} + len(${receiver})) else __x) for __i, __x in enumerate(${receiver})]`;
447
- }
448
- }
449
- else if (method === 'lastIndexOf') {
450
- const needle = args[0] ?? '';
451
- // String receivers use rfind (correct for multi-char substrings, -1
452
- // when absent); array receivers reverse-scan by element equality.
453
- lowered = `(${receiver}.rfind(${needle}) if isinstance(${receiver}, str) else (len(${receiver}) - 1 - ${receiver}[::-1].index(${needle}) if ${needle} in ${receiver} else -1))`;
454
- }
455
430
  if (lowered) {
456
431
  out = `${pre}${lowered}`;
457
432
  i = closeIdx + 1;
@@ -459,6 +434,26 @@ function lowerJsArrayMethods(expr, ctx) {
459
434
  }
460
435
  }
461
436
  }
437
+ // Portable Array *property* access (non-call `.length`). Matched only when
438
+ // NOT immediately followed by `(` (the method scan above owns call forms),
439
+ // so `arr.length` lowers to `len(arr)` while a hypothetical `arr.length(...)`
440
+ // call is left for the method path. Receiver taken from already-emitted
441
+ // `out` like the method path, so chained forms (`arr.slice(1).length`)
442
+ // compose naturally.
443
+ const mProp = expr.slice(i).match(/^\.([A-Za-z]\w*)(?!\s*\()/);
444
+ if (mProp && isSharedPortableArrayProperty(mProp[1])) {
445
+ const recvStart = findReceiverStart(out);
446
+ if (recvStart !== -1) {
447
+ const receiver = out.slice(recvStart);
448
+ const pre = out.slice(0, recvStart);
449
+ const lowered = lowerPortableArrayPropertyPy(receiver, mProp[1]);
450
+ if (lowered !== null) {
451
+ out = `${pre}${lowered}`;
452
+ i += mProp[0].length;
453
+ continue;
454
+ }
455
+ }
456
+ }
462
457
  out += c;
463
458
  i += 1;
464
459
  }
@@ -490,6 +485,134 @@ function matchBalancedParen(expr, openIdx) {
490
485
  }
491
486
  return -1;
492
487
  }
488
+ function stripOuterParens(raw) {
489
+ let trimmed = raw.trim();
490
+ while (trimmed.startsWith('(')) {
491
+ const close = matchBalancedParen(trimmed, 0);
492
+ if (close !== trimmed.length - 1)
493
+ break;
494
+ trimmed = trimmed.slice(1, -1).trim();
495
+ }
496
+ return trimmed;
497
+ }
498
+ function exactVoidOperand(raw) {
499
+ const trimmed = stripOuterParens(raw);
500
+ if (!trimmed.startsWith('void'))
501
+ return null;
502
+ const rest = trimmed.slice('void'.length);
503
+ if (rest === '' || (!/^\s/.test(rest) && !rest.startsWith('(')))
504
+ return null;
505
+ const operand = rest.trim();
506
+ if (!operand)
507
+ return null;
508
+ if (operand.startsWith('(')) {
509
+ const close = matchBalancedParen(operand, 0);
510
+ return close === operand.length - 1 ? operand : null;
511
+ }
512
+ let depth = 0;
513
+ let quote = null;
514
+ for (let i = 0; i < operand.length; i += 1) {
515
+ const c = operand[i];
516
+ if (quote) {
517
+ if (c === '\\')
518
+ i += 1;
519
+ else if (c === quote)
520
+ quote = null;
521
+ continue;
522
+ }
523
+ if (c === '"' || c === "'" || c === '`') {
524
+ quote = c;
525
+ continue;
526
+ }
527
+ if (c === '(' || c === '[' || c === '{') {
528
+ depth += 1;
529
+ continue;
530
+ }
531
+ if (c === ')' || c === ']' || c === '}') {
532
+ depth -= 1;
533
+ if (depth < 0)
534
+ return null;
535
+ continue;
536
+ }
537
+ if (depth === 0 && /[,+\-*/%|&^?:<>=]/.test(c) && !(i === 0 && /[+-]/.test(c)))
538
+ return null;
539
+ }
540
+ return depth === 0 && !quote ? operand : null;
541
+ }
542
+ function rewriteExprInContext(raw, ctx) {
543
+ return rewriteExpr(raw, ctx.pathParams, ctx.bodyFields, ctx.authUser, ctx.imports, ctx.hoistedDefs, ctx.closureSeq);
544
+ }
545
+ function lowerLogicalAndOr(expr) {
546
+ let out = '';
547
+ let i = 0;
548
+ let quote = null;
549
+ while (i < expr.length) {
550
+ const c = expr[i];
551
+ if (quote) {
552
+ out += c;
553
+ if (c === '\\') {
554
+ out += expr[i + 1] ?? '';
555
+ i += 2;
556
+ continue;
557
+ }
558
+ if (c === quote)
559
+ quote = null;
560
+ i += 1;
561
+ continue;
562
+ }
563
+ if (c === '"' || c === "'" || c === '`') {
564
+ quote = c;
565
+ out += c;
566
+ i += 1;
567
+ continue;
568
+ }
569
+ if (expr.startsWith('&&', i)) {
570
+ out += ' and ';
571
+ i += 2;
572
+ continue;
573
+ }
574
+ if (expr.startsWith('||', i)) {
575
+ out += ' or ';
576
+ i += 2;
577
+ continue;
578
+ }
579
+ out += c;
580
+ i += 1;
581
+ }
582
+ return out;
583
+ }
584
+ function lowerJsVoidExpression(raw, ctx) {
585
+ const voidOperand = exactVoidOperand(raw);
586
+ if (voidOperand === null) {
587
+ throw new Error('Array.fill void argument lowering expects an exact unary void expression; bind complex void expressions first.');
588
+ }
589
+ return `(${lowerJsVoidOperand(voidOperand, ctx)}, _KERN_UNDEFINED)[1]`;
590
+ }
591
+ function lowerJsVoidOperand(raw, ctx) {
592
+ const nestedVoidOperand = exactVoidOperand(raw);
593
+ if (nestedVoidOperand !== null) {
594
+ return lowerJsVoidExpression(raw, ctx);
595
+ }
596
+ return lowerLogicalAndOr(rewriteExprInContext(raw, ctx));
597
+ }
598
+ function lowerJsFillArgument(raw, ctx) {
599
+ const trimmed = stripOuterParens(raw);
600
+ if (trimmed === 'undefined')
601
+ return '_KERN_UNDEFINED';
602
+ const voidOperand = exactVoidOperand(trimmed);
603
+ if (voidOperand !== null) {
604
+ return `(${lowerJsVoidOperand(voidOperand, ctx)}, _KERN_UNDEFINED)[1]`;
605
+ }
606
+ // Fail-closed ONLY on a true `void` OPERATOR with a complex operand
607
+ // (exactVoidOperand already rejected it above). A bare identifier that
608
+ // merely STARTS with "void" (voidValue, voidFn()) is ordinary code and
609
+ // must lower normally — `void` is followed by whitespace or `(` when it
610
+ // is the operator.
611
+ if (/^void(?:\s|\()/.test(trimmed)) {
612
+ return lowerJsVoidExpression(trimmed, ctx);
613
+ }
614
+ return rewriteExprInContext(trimmed, ctx);
615
+ }
493
616
  // Split a call's inner argument text on top-level commas, ignoring commas
494
617
  // inside nested ()[]{} or string literals.
495
618
  function splitTopLevelArgs(inner) {
@@ -1969,7 +2092,11 @@ export function rewriteExpr(expr, pathParams, bodyFields = new Set(), authUser =
1969
2092
  const tokens = tokenizeJSExpr(expr);
1970
2093
  const comparisonProbe = expr.replace(/>>>|>>|<</g, '');
1971
2094
  const hasLooseComparison = /(?:===|!==|==|!=|<=|>=|<|>)/.test(comparisonProbe);
1972
- const hasBitwiseOrModulo = !expr.includes('=>') && !hasLooseComparison && tokens.some((t) => t.type === 'UNARY' || t.type === 'OP');
2095
+ const hasVoidOperator = /\bvoid\b/.test(expr);
2096
+ const hasBitwiseOrModulo = !hasVoidOperator &&
2097
+ !expr.includes('=>') &&
2098
+ !hasLooseComparison &&
2099
+ tokens.some((t) => t.type === 'UNARY' || t.type === 'OP');
1973
2100
  if (hasBitwiseOrModulo) {
1974
2101
  const ast = parseTokens(tokens);
1975
2102
  expr = codegenASTToPython(ast, imports);