@kernlang/python 3.5.8-canary.207.1.43495cde → 3.5.8-canary.210.1.239de0e0

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.
@@ -59,4 +59,96 @@ export const KERN_JS_HELPER_PY = [
59
59
  'def js_equals(a, b):',
60
60
  ' return a == b',
61
61
  ].join('\n');
62
+ export const KERN_JS_OBJECT_HELPERS_PY = [
63
+ 'def _kern_js_is_array_index(__k_key):',
64
+ ' __k_s = str(__k_key)',
65
+ ' if not __k_s.isdigit(): return False',
66
+ ' if len(__k_s) > 1 and __k_s[0] == "0": return False',
67
+ ' __k_n = int(__k_s)',
68
+ ' return 0 <= __k_n < 4294967295 and __k_s == str(__k_n)',
69
+ '',
70
+ 'def _kern_js_property_items(__k_obj):',
71
+ ' if __k_obj is None:',
72
+ ' raise TypeError("Cannot convert undefined or null to object")',
73
+ ' if hasattr(__k_obj, "items"):',
74
+ ' __k_raw = list(__k_obj.items())',
75
+ ' else:',
76
+ ' try:',
77
+ ' __k_raw = [(str(__k_i), __k_v) for __k_i, __k_v in enumerate(__k_obj)]',
78
+ ' except TypeError:',
79
+ ' __k_raw = []',
80
+ ' __k_indexed = []',
81
+ ' __k_rest = []',
82
+ ' for __k_pos, (__k_key, __k_val) in enumerate(__k_raw):',
83
+ ' __k_s = str(__k_key)',
84
+ ' if _kern_js_is_array_index(__k_s):',
85
+ ' __k_indexed.append((int(__k_s), __k_s, __k_val))',
86
+ ' else:',
87
+ ' __k_rest.append((__k_pos, __k_s, __k_val))',
88
+ ' return [(__k_s, __k_v) for _, __k_s, __k_v in sorted(__k_indexed, key=lambda __k_item: __k_item[0])] + [(__k_s, __k_v) for _, __k_s, __k_v in __k_rest]',
89
+ '',
90
+ 'def _kern_js_object_keys(__k_obj):',
91
+ ' return [__k_k for __k_k, _ in _kern_js_property_items(__k_obj)]',
92
+ '',
93
+ 'def _kern_js_object_values(__k_obj):',
94
+ ' return [__k_v for _, __k_v in _kern_js_property_items(__k_obj)]',
95
+ '',
96
+ 'def _kern_js_object_entries(__k_obj):',
97
+ ' return [[__k_k, __k_v] for __k_k, __k_v in _kern_js_property_items(__k_obj)]',
98
+ ].join('\n');
99
+ export const KERN_JS_STRING_HELPERS_PY = [
100
+ 'def _kern_js_split_limit(__k_limit):',
101
+ ' if __k_limit is None:',
102
+ ' return None',
103
+ ' try:',
104
+ ' __k_n = float(__k_limit)',
105
+ ' except Exception:',
106
+ ' return 0',
107
+ ' if __k_n != __k_n or __k_n in (float("inf"), float("-inf")):',
108
+ ' return 0',
109
+ ' return int(__k_n) % 4294967296',
110
+ '',
111
+ 'def _kern_js_replacement(__k_repl, __k_match, __k_prefix, __k_suffix):',
112
+ ' __k_repl = str(__k_repl)',
113
+ ' __k_out = []',
114
+ ' __k_i = 0',
115
+ ' while __k_i < len(__k_repl):',
116
+ ' __k_c = __k_repl[__k_i]',
117
+ ' if __k_c == "$" and __k_i + 1 < len(__k_repl):',
118
+ ' __k_n = __k_repl[__k_i + 1]',
119
+ ' if __k_n == "$":',
120
+ ' __k_out.append("$"); __k_i += 2; continue',
121
+ ' if __k_n == "&":',
122
+ ' __k_out.append(__k_match); __k_i += 2; continue',
123
+ ' if __k_n == "`":',
124
+ ' __k_out.append(__k_prefix); __k_i += 2; continue',
125
+ ' if __k_n == "\'":',
126
+ ' __k_out.append(__k_suffix); __k_i += 2; continue',
127
+ ' __k_out.append(__k_c)',
128
+ ' __k_i += 1',
129
+ ' return "".join(__k_out)',
130
+ '',
131
+ 'def _kern_js_replace(__k_s, __k_search, __k_repl, __k_all=False):',
132
+ ' __k_s = str(__k_s)',
133
+ ' __k_search = str(__k_search)',
134
+ ' if not __k_all:',
135
+ ' __k_idx = __k_s.find(__k_search)',
136
+ ' if __k_idx < 0: return __k_s',
137
+ ' __k_end = __k_idx + len(__k_search)',
138
+ ' return __k_s[:__k_idx] + _kern_js_replacement(__k_repl, __k_search, __k_s[:__k_idx], __k_s[__k_end:]) + __k_s[__k_end:]',
139
+ ' if __k_search == "":',
140
+ ' return "".join(_kern_js_replacement(__k_repl, "", __k_s[:__k_i], __k_s[__k_i:]) + (__k_s[__k_i] if __k_i < len(__k_s) else "") for __k_i in range(len(__k_s) + 1))',
141
+ ' __k_parts = []',
142
+ ' __k_pos = 0',
143
+ ' while True:',
144
+ ' __k_idx = __k_s.find(__k_search, __k_pos)',
145
+ ' if __k_idx < 0:',
146
+ ' __k_parts.append(__k_s[__k_pos:])',
147
+ ' break',
148
+ ' __k_end = __k_idx + len(__k_search)',
149
+ ' __k_parts.append(__k_s[__k_pos:__k_idx])',
150
+ ' __k_parts.append(_kern_js_replacement(__k_repl, __k_search, __k_s[:__k_idx], __k_s[__k_end:]))',
151
+ ' __k_pos = __k_end',
152
+ ' return "".join(__k_parts)',
153
+ ].join('\n');
62
154
  //# sourceMappingURL=helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../../src/core/expr/helpers.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,yBAAyB;IACzB,sEAAsE;IACtE,EAAE;IACF,qCAAqC;IACrC,qCAAqC;IACrC,sCAAsC;IACtC,4BAA4B;IAC5B,WAAW;IACX,6CAA6C;IAC7C,4BAA4B;CAC7B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,uBAAuB;IACvB,iCAAiC;IACjC,6CAA6C;IAC7C,uBAAuB;IACvB,uBAAuB;IACvB,uBAAuB;CACxB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,aAAa;IACb,cAAc;IACd,4BAA4B;IAC5B,UAAU;IACV,2CAA2C;IAC3C,kCAAkC;IAClC,uBAAuB;IACvB,cAAc;IACd,4BAA4B;IAC5B,iDAAiD;IACjD,wCAAwC;IACxC,2BAA2B;IAC3B,sBAAsB;IACtB,2DAA2D;CAC5D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,aAAa;IACb,kBAAkB;IAClB,yBAAyB;IACzB,yBAAyB;IACzB,UAAU;IACV,uBAAuB;IACvB,uBAAuB;IACvB,uBAAuB;IACvB,6BAA6B;IAC7B,8DAA8D;IAC9D,4CAA4C;IAC5C,qCAAqC;IACrC,kCAAkC;IAClC,0CAA0C;CAC3C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,mBAAmB;IACnB,8CAA8C;IAC9C,6DAA6D;IAC7D,qDAAqD;IACrD,iBAAiB;IACjB,sBAAsB;IACtB,mBAAmB;CACpB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC"}
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../../src/core/expr/helpers.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared Python expression lowering — framework-agnostic.
3
3
  */
4
- export { KERN_FMT_HELPER_PY, KERN_I32_HELPER_PY, KERN_JS_HELPER_PY, KERN_PAIR_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './helpers.js';
4
+ 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';
5
5
  export declare function quoteObjectKeysOutsideStrings(expr: string): string;
6
6
  export declare function rewriteExpr(expr: string, pathParams: string[], bodyFields?: Set<string>, authUser?: boolean, imports?: Set<string>, hoistedDefs?: string[], closureSeq?: {
7
7
  n: number;
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Shared Python expression lowering — framework-agnostic.
3
3
  */
4
- import { lowerJsClosureBodyToPython } from '@kernlang/core';
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_TMOD_HELPER_PY } from './helpers.js';
7
- export { KERN_FMT_HELPER_PY, KERN_I32_HELPER_PY, KERN_JS_HELPER_PY, KERN_PAIR_HELPERS_PY, KERN_TMOD_HELPER_PY, } from './helpers.js';
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';
8
8
  // Quoted strings absorbed by the alternation; only literal `===`/`!==`
9
9
  // outside strings get rewritten. Both single and double quotes AND
10
10
  // backtick template literals are covered so a message like
@@ -520,6 +520,201 @@ function splitTopLevelArgs(inner) {
520
520
  args.push(inner.slice(start).trim());
521
521
  return args;
522
522
  }
523
+ // ── Host-builtin lowering: Set.has / Date.getTime / logical-not ─────────────
524
+ // Portable-pure constructs lifted from the fitvt/job-central R1 audit. Math/
525
+ // Number/String/Array builtins are handled by the lower*BuiltinCalls passes
526
+ // above; these three were the residual gap (Set membership, epoch-ms dates,
527
+ // `!`). Same balanced-scan approach as the sibling passes.
528
+ function isCustomReceiverChar(c) {
529
+ return !!c && /[\w.]/.test(c);
530
+ }
531
+ function lowerSetOperandMemberRead(expr) {
532
+ const simple = expr.match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
533
+ if (simple) {
534
+ const [, obj, field] = simple;
535
+ return `(${obj}.get("${field}") if isinstance(${obj}, dict) else ${obj}.${field})`;
536
+ }
537
+ const projection = expr.match(/^\[([A-Za-z_]\w*)\.([A-Za-z_]\w*) for \1 in ([\s\S]+)\]$/);
538
+ if (projection) {
539
+ const [, obj, field, source] = projection;
540
+ return `[${obj}.get("${field}") if isinstance(${obj}, dict) else ${obj}.${field} for ${obj} in ${source}]`;
541
+ }
542
+ return expr;
543
+ }
544
+ // new Set(arr).has(x) → (x) in set(arr). Runs AFTER array-method lowering so a
545
+ // `.map(...)` arg is already a comprehension.
546
+ function lowerSetHasCalls(expr, _imports) {
547
+ let out = '';
548
+ let i = 0;
549
+ let quote = null;
550
+ while (i < expr.length) {
551
+ const c = expr[i];
552
+ if (quote) {
553
+ out += c;
554
+ if (c === '\\') {
555
+ out += expr[i + 1] ?? '';
556
+ i += 2;
557
+ continue;
558
+ }
559
+ if (c === quote)
560
+ quote = null;
561
+ i += 1;
562
+ continue;
563
+ }
564
+ if (c === '"' || c === "'" || c === '`') {
565
+ quote = c;
566
+ out += c;
567
+ i += 1;
568
+ continue;
569
+ }
570
+ const m = expr.slice(i).match(/^new\s+Set\s*\(/);
571
+ if (m && !isCustomReceiverChar(expr[i - 1])) {
572
+ const setOpen = i + m[0].length - 1;
573
+ const setClose = matchBalancedParen(expr, setOpen);
574
+ const afterSet = setClose === -1 ? '' : expr.slice(setClose + 1);
575
+ const hasMatch = afterSet.match(/^\s*\.has\s*\(/);
576
+ if (setClose !== -1 && hasMatch) {
577
+ const hasOpen = setClose + 1 + hasMatch[0].length - 1;
578
+ const hasClose = matchBalancedParen(expr, hasOpen);
579
+ if (hasClose !== -1) {
580
+ // Recurse so nested `new Set(...)` inside the args lowers too.
581
+ const setArg = lowerSetOperandMemberRead(lowerSetHasCalls(expr.slice(setOpen + 1, setClose).trim()));
582
+ const hasArg = lowerSetOperandMemberRead(lowerSetHasCalls(expr.slice(hasOpen + 1, hasClose).trim()));
583
+ out += `(${hasArg}) in set(${setArg})`;
584
+ i = hasClose + 1;
585
+ continue;
586
+ }
587
+ }
588
+ }
589
+ out += c;
590
+ i += 1;
591
+ }
592
+ return out;
593
+ }
594
+ // new Date(arg).getTime() → epoch milliseconds. Runs BEFORE Math builtins so a
595
+ // surrounding Math.round sees an integer.
596
+ function lowerDateGetTimeCalls(expr, imports) {
597
+ let out = '';
598
+ let i = 0;
599
+ let quote = null;
600
+ while (i < expr.length) {
601
+ const c = expr[i];
602
+ if (quote) {
603
+ out += c;
604
+ if (c === '\\') {
605
+ out += expr[i + 1] ?? '';
606
+ i += 2;
607
+ continue;
608
+ }
609
+ if (c === quote)
610
+ quote = null;
611
+ i += 1;
612
+ continue;
613
+ }
614
+ if (c === '"' || c === "'" || c === '`') {
615
+ quote = c;
616
+ out += c;
617
+ i += 1;
618
+ continue;
619
+ }
620
+ const m = expr.slice(i).match(/^new\s+Date\s*\(/);
621
+ if (m && !isCustomReceiverChar(expr[i - 1])) {
622
+ const openIdx = i + m[0].length - 1;
623
+ const closeIdx = matchBalancedParen(expr, openIdx);
624
+ if (closeIdx !== -1 && expr.slice(closeIdx + 1).match(/^\s*\.getTime\s*\(\s*\)/)) {
625
+ const tail = expr.slice(closeIdx + 1).match(/^\s*\.getTime\s*\(\s*\)/)[0];
626
+ // Recurse so nested new Date(...).getTime() inside the arg lowers too.
627
+ const arg = lowerDateGetTimeCalls(expr.slice(openIdx + 1, closeIdx).trim(), imports);
628
+ imports?.add('from datetime import datetime, timezone');
629
+ // Branch on the runtime value: JS `new Date(n)` accepts epoch-ms numbers
630
+ // (getTime() returns n), else parse an ISO string. Case-insensitive Z.
631
+ // KNOWN LIMITATIONS (tracked follow-ups, beyond the R1 surface): a
632
+ // date-only string carrying a TZ offset ("2026-06-03Z") and non-ISO
633
+ // formats still raise in fromisoformat.
634
+ out +=
635
+ `(lambda __k_v: int(__k_v) if isinstance(__k_v, (int, float)) ` +
636
+ `else int((lambda __k_dt: (__k_dt if __k_dt.tzinfo is not None else __k_dt.replace(tzinfo=timezone.utc)).timestamp() * 1000)` +
637
+ `(datetime.fromisoformat(str(__k_v).replace("Z", "+00:00").replace("z", "+00:00")))))(${arg})`;
638
+ i = closeIdx + 1 + tail.length;
639
+ continue;
640
+ }
641
+ }
642
+ out += c;
643
+ i += 1;
644
+ }
645
+ return out;
646
+ }
647
+ // `!` → Python `not `. Skips `!=`/`!==`. Runs after the operator/Set passes.
648
+ function lowerLogicalNot(expr, _imports) {
649
+ let out = '';
650
+ let i = 0;
651
+ let quote = null;
652
+ while (i < expr.length) {
653
+ const c = expr[i];
654
+ if (quote) {
655
+ out += c;
656
+ if (c === '\\') {
657
+ out += expr[i + 1] ?? '';
658
+ i += 2;
659
+ continue;
660
+ }
661
+ if (c === quote)
662
+ quote = null;
663
+ i += 1;
664
+ continue;
665
+ }
666
+ if (c === '"' || c === "'" || c === '`') {
667
+ quote = c;
668
+ out += c;
669
+ i += 1;
670
+ continue;
671
+ }
672
+ // KNOWN LIMITATION (tracked follow-up): `not` binds looser than comparison
673
+ // in Python, so `!a < b` (JS: `(!a) < b`) lowers to `not a < b` (Python:
674
+ // `not (a < b)`). Safe for the boolean-connective uses in the R1 surface.
675
+ if (c === '!' && expr[i + 1] !== '=') {
676
+ out += 'not ';
677
+ i += 1;
678
+ while (i < expr.length && /\s/.test(expr[i]))
679
+ i += 1;
680
+ continue;
681
+ }
682
+ out += c;
683
+ i += 1;
684
+ }
685
+ return out;
686
+ }
687
+ const PYTHON_PORTABLE_LOGIC_LOWERINGS = [
688
+ {
689
+ primitive: 'time.epochMs',
690
+ phase: 'beforeMath',
691
+ lower: lowerDateGetTimeCalls,
692
+ },
693
+ {
694
+ primitive: 'collection.has',
695
+ phase: 'afterArrayMethods',
696
+ lower: lowerSetHasCalls,
697
+ },
698
+ {
699
+ primitive: 'logic.not',
700
+ phase: 'final',
701
+ lower: lowerLogicalNot,
702
+ },
703
+ ];
704
+ for (const entry of PYTHON_PORTABLE_LOGIC_LOWERINGS) {
705
+ if (PORTABLE_LOGIC_PRIMITIVES[entry.primitive].targets.python !== 'stable') {
706
+ throw new Error(`Portable logic primitive '${entry.primitive}' is not stable on the Python target.`);
707
+ }
708
+ }
709
+ function lowerPortableLogicPrimitives(expr, imports, phase) {
710
+ let result = expr;
711
+ for (const entry of PYTHON_PORTABLE_LOGIC_LOWERINGS) {
712
+ if (entry.phase !== phase)
713
+ continue;
714
+ result = entry.lower(result, imports);
715
+ }
716
+ return result;
717
+ }
523
718
  // Lower JSON.stringify(...) / JSON.parse(...) to json.dumps/loads. Uses a
524
719
  // balanced, string-aware scan because the single argument can itself contain
525
720
  // commas, nested parens, brackets, braces, or string literals.
@@ -920,7 +1115,7 @@ function lowerStringBuiltinCalls(expr) {
920
1115
  });
921
1116
  }
922
1117
  // Lower the argument-taking JS String methods.
923
- function lowerStringArgMethods(expr) {
1118
+ function lowerStringArgMethods(expr, imports) {
924
1119
  let out = '';
925
1120
  let i = 0;
926
1121
  let quote = null;
@@ -950,9 +1145,18 @@ function lowerStringArgMethods(expr) {
950
1145
  if (closeIdx !== -1) {
951
1146
  const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
952
1147
  if (args.length === 2 && !args[0].trim().startsWith('/')) {
953
- const a0 = lowerStringArgMethods(args[0]).trim();
954
- const a1 = lowerStringArgMethods(args[1]).trim();
955
- out += `.replace(${a0}, ${a1})`;
1148
+ const a0 = lowerStringArgMethods(args[0], imports).trim();
1149
+ const a1 = lowerStringArgMethods(args[1], imports).trim();
1150
+ const receiverStart = findReceiverStart(out);
1151
+ if (receiverStart !== -1 && !isStringLiteralWithoutDollar(a1)) {
1152
+ imports?.add(KERN_JS_STRING_HELPERS_PY);
1153
+ const receiver = out.slice(receiverStart);
1154
+ const pre = out.slice(0, receiverStart);
1155
+ out = `${pre}_kern_js_replace(${receiver}, ${a0}, ${a1}, True)`;
1156
+ }
1157
+ else {
1158
+ out += `.replace(${a0}, ${a1})`;
1159
+ }
956
1160
  i = closeIdx + 1;
957
1161
  continue;
958
1162
  }
@@ -964,9 +1168,18 @@ function lowerStringArgMethods(expr) {
964
1168
  if (closeIdx !== -1) {
965
1169
  const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx));
966
1170
  if (args.length === 2 && !args[0].trim().startsWith('/')) {
967
- const a0 = lowerStringArgMethods(args[0]).trim();
968
- const a1 = lowerStringArgMethods(args[1]).trim();
969
- out += `.replace(${a0}, ${a1}, 1)`;
1171
+ const a0 = lowerStringArgMethods(args[0], imports).trim();
1172
+ const a1 = lowerStringArgMethods(args[1], imports).trim();
1173
+ const receiverStart = findReceiverStart(out);
1174
+ if (receiverStart !== -1 && !isStringLiteralWithoutDollar(a1)) {
1175
+ imports?.add(KERN_JS_STRING_HELPERS_PY);
1176
+ const receiver = out.slice(receiverStart);
1177
+ const pre = out.slice(0, receiverStart);
1178
+ out = `${pre}_kern_js_replace(${receiver}, ${a0}, ${a1}, False)`;
1179
+ }
1180
+ else {
1181
+ out += `.replace(${a0}, ${a1}, 1)`;
1182
+ }
970
1183
  i = closeIdx + 1;
971
1184
  continue;
972
1185
  }
@@ -1031,7 +1244,7 @@ function lowerStringArgMethods(expr) {
1031
1244
  const openIdx = i + '.repeat('.length - 1;
1032
1245
  const closeIdx = matchBalancedParen(expr, openIdx);
1033
1246
  if (closeIdx !== -1) {
1034
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a).trim());
1247
+ const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a, imports).trim());
1035
1248
  const n = args[0] ?? '0';
1036
1249
  const receiverStart = findReceiverStart(out);
1037
1250
  if (receiverStart !== -1) {
@@ -1047,9 +1260,19 @@ function lowerStringArgMethods(expr) {
1047
1260
  const openIdx = i + '.split('.length - 1;
1048
1261
  const closeIdx = matchBalancedParen(expr, openIdx);
1049
1262
  if (closeIdx !== -1) {
1050
- const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a).trim());
1263
+ const args = splitTopLevelArgs(expr.slice(openIdx + 1, closeIdx)).map((a) => lowerStringArgMethods(a, imports).trim());
1264
+ if (args.length >= 1 && isEmptyStringLiteral(args[0])) {
1265
+ const receiverStart = findReceiverStart(out);
1266
+ if (receiverStart !== -1) {
1267
+ const receiver = out.slice(receiverStart);
1268
+ const pre = out.slice(0, receiverStart);
1269
+ out = `${pre}list(${receiver})${args.length === 2 ? `[:${lowerSplitLimit(args[1], imports)}]` : ''}`;
1270
+ i = closeIdx + 1;
1271
+ continue;
1272
+ }
1273
+ }
1051
1274
  if (args.length === 2) {
1052
- out += `.split(${args[0]})[:${args[1]}]`;
1275
+ out += `.split(${args[0]})[:${lowerSplitLimit(args[1], imports)}]`;
1053
1276
  i = closeIdx + 1;
1054
1277
  continue;
1055
1278
  }
@@ -1060,6 +1283,20 @@ function lowerStringArgMethods(expr) {
1060
1283
  }
1061
1284
  return out;
1062
1285
  }
1286
+ function isEmptyStringLiteral(expr) {
1287
+ const t = expr.trim();
1288
+ return t === '""' || t === "''" || t === '``';
1289
+ }
1290
+ function isStringLiteralWithoutDollar(expr) {
1291
+ const t = expr.trim();
1292
+ if (t.includes('$') || t.includes('\\'))
1293
+ return false;
1294
+ return /^(?:"[^"]*"|'[^']*'|`[^`]*`)$/.test(t);
1295
+ }
1296
+ function lowerSplitLimit(limit, imports) {
1297
+ imports?.add(KERN_JS_STRING_HELPERS_PY);
1298
+ return `_kern_js_split_limit(${limit})`;
1299
+ }
1063
1300
  // Lower selected Object/Array/Date host builtins in portable expressions.
1064
1301
  function lowerObjectArrayDateBuiltinCalls(expr, imports) {
1065
1302
  let out = '';
@@ -1129,12 +1366,18 @@ function lowerObjectArrayDateBuiltinCalls(expr, imports) {
1129
1366
  }
1130
1367
  else {
1131
1368
  const arg = lowerObjectArrayDateBuiltinCalls(rawArgs, imports).trim();
1132
- if (method === 'Object.keys')
1133
- out += `list(${arg}.keys())`;
1134
- else if (method === 'Object.values')
1135
- out += `list(${arg}.values())`;
1136
- else if (method === 'Object.entries')
1137
- out += `list(${arg}.items())`;
1369
+ if (method === 'Object.keys') {
1370
+ imports?.add(KERN_JS_OBJECT_HELPERS_PY);
1371
+ out += `_kern_js_object_keys(${arg})`;
1372
+ }
1373
+ else if (method === 'Object.values') {
1374
+ imports?.add(KERN_JS_OBJECT_HELPERS_PY);
1375
+ out += `_kern_js_object_values(${arg})`;
1376
+ }
1377
+ else if (method === 'Object.entries') {
1378
+ imports?.add(KERN_JS_OBJECT_HELPERS_PY);
1379
+ out += `_kern_js_object_entries(${arg})`;
1380
+ }
1138
1381
  else
1139
1382
  out += `isinstance(${arg}, list)`;
1140
1383
  }
@@ -1786,11 +2029,14 @@ export function rewriteExpr(expr, pathParams, bodyFields = new Set(), authUser =
1786
2029
  });
1787
2030
  result = lowerPortableJsOperators(result, imports);
1788
2031
  result = lowerJsonBuiltinCalls(result, imports);
2032
+ result = lowerPortableLogicPrimitives(result, imports, 'beforeMath'); // before Math: Math.round wraps date diffs
1789
2033
  result = lowerMathBuiltinCalls(result, imports);
1790
2034
  result = lowerNumberBuiltinCalls(result, imports);
1791
2035
  result = lowerStringBuiltinCalls(result);
1792
- result = lowerStringArgMethods(result);
2036
+ result = lowerStringArgMethods(result, imports);
1793
2037
  result = lowerObjectArrayDateBuiltinCalls(result, imports);
2038
+ result = lowerPortableLogicPrimitives(result, imports, 'afterArrayMethods'); // Set arg may be a .map() comprehension
2039
+ result = lowerPortableLogicPrimitives(result, imports, 'final'); // applies to lowered membership/boolean
1794
2040
  result = quoteObjectKeysOutsideStrings(result);
1795
2041
  for (const replacement of replacements) {
1796
2042
  result = result.split(replacement.placeholder).join(replacement.lowered);
@@ -1960,7 +2206,7 @@ function parseTokens(tokens) {
1960
2206
  let left = parsePrimary();
1961
2207
  while (true) {
1962
2208
  const next = peek();
1963
- if (!next || next.type !== 'OP')
2209
+ if (next?.type !== 'OP')
1964
2210
  break;
1965
2211
  const opPrecedence = getPrecedence(next.value);
1966
2212
  if (opPrecedence < precedence)