@kernlang/python 4.0.1-canary.224.1.1a92ac0a → 4.0.1-canary.225.1.f5c8d5fe

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.
@@ -40,7 +40,7 @@
40
40
  * threads a `indent` string. The propagation hoist embeds its own 4-space
41
41
  * relative indent on the `return __k_tN` line; the wrapper prepends the
42
42
  * surrounding indent so the post-emit result nests correctly. */
43
- import { applyTemplate, classifyRegexLiteralIndexReadFailClose, classifyRegexLiteralMemberReadFailClose, emitStringKeyArray, expandRegexIFold, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isHostNamespaceRoot, isPostfixMutationOperator, isSupportedAssignOperator, isZeroWidthCapableRegex, KERN_STDLIB_MODULES, lookupStdlibCall, lookupStdlibProperty, lowerRegexAnchorsPython, lowerRegexNamedGroupsPython, needsArgParens, needsBinaryParens, normalizeRegexClasses, parseExpression, parseKeys, REGEX_EXEC_FAILCLOSE, REGEX_HOST_REGEXP_FAILCLOSE, REGEX_MATCHALL_NO_G_FAILCLOSE, REGEX_NONLITERAL_FAILCLOSE, REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE, REGEX_REPLACEALL_NO_G_FAILCLOSE, REGEX_SPLIT_LIMIT_FAILCLOSE, REGEX_SPLIT_ZEROWIDTH_FAILCLOSE, REGEX_TEST_G_FAILCLOSE, regexAstralFailMessage, regexCaptureMeta, regexIFoldFailMessage, regexLiteralReceiverIR, regexMethodRegexArgIdent, scanRegexAstral, suggestStdlibMethod, translateReplStringToPython, unmappedHostNamespaceMessage, unwrapTransparentReceiverIR, validateRegexNamedGroupsPortable, } from '@kernlang/core';
43
+ import { applyTemplate, assertNoDecimalOperator, classifyRegexLiteralIndexReadFailClose, classifyRegexLiteralMemberReadFailClose, decimalBareConstructionFailMessage, emitStringKeyArray, expandRegexIFold, instanceofRhsPythonType, instanceofRhsRejectReasonForName, isHostNamespaceRoot, isPostfixMutationOperator, isSupportedAssignOperator, isZeroWidthCapableRegex, KERN_DECIMAL_OPS_HELPER_PY, KERN_STDLIB_MODULES, lookupStdlibCall, lookupStdlibProperty, lowerRegexAnchorsPython, lowerRegexNamedGroupsPython, needsArgParens, needsBinaryParens, normalizeRegexClasses, parseExpression, parseKeys, REGEX_EXEC_FAILCLOSE, REGEX_HOST_REGEXP_FAILCLOSE, REGEX_MATCHALL_NO_G_FAILCLOSE, REGEX_NONLITERAL_FAILCLOSE, REGEX_REPLACE_NONLITERAL_REPL_FAILCLOSE, REGEX_REPLACEALL_NO_G_FAILCLOSE, REGEX_SPLIT_LIMIT_FAILCLOSE, REGEX_SPLIT_ZEROWIDTH_FAILCLOSE, REGEX_TEST_G_FAILCLOSE, regexAstralFailMessage, regexCaptureMeta, regexIFoldFailMessage, regexLiteralReceiverIR, regexMethodRegexArgIdent, scanRegexAstral, suggestStdlibMethod, translateReplStringToPython, unmappedHostNamespaceMessage, unwrapTransparentReceiverIR, validateDecimalConstructionArg, validateDecimalDivModArgs, validateDecimalOperands, validateDecimalPowArgs, validateRegexNamedGroupsPortable, } from '@kernlang/core';
44
44
  // Slice 0.9 — the TypeScript-AST closure helpers + classifier live on the Node
45
45
  // subpath (the barrel is browser-safe). Python codegen is Node-side and parses
46
46
  // block-bodied arrows, so it injects `typescriptClosureClassifier`.
@@ -1862,8 +1862,10 @@ function emitPyExprCtx(node, ctx) {
1862
1862
  const lowered = lowerChain(node, ctx);
1863
1863
  return wrapGuardIfAny(lowered, ctx);
1864
1864
  }
1865
- case 'await':
1866
- return `await ${emitPyExprCtx(node.argument, ctx)}`;
1865
+ case 'await': {
1866
+ const arg = emitPyExprCtx(node.argument, ctx);
1867
+ return `await ${needsLowPrecedenceOperandParens(node.argument) ? `(${arg})` : arg}`;
1868
+ }
1867
1869
  case 'new': {
1868
1870
  // Host Error mapping (spec §1): `new Error(args)` → `Exception(args)` on
1869
1871
  // Python, since `raise Error(...)` / `isinstance(x, Error)` would
@@ -1930,6 +1932,20 @@ function emitPyExprCtx(node, ctx) {
1930
1932
  return out;
1931
1933
  }
1932
1934
  case 'binary': {
1935
+ // DECIMAL Slice 2 (item 3) — fail closed on `+`/`-`/`*` over a syntactically-
1936
+ // proven Decimal operand (`Decimal.of(...)`/`Decimal.<m>(...)`), the SAME
1937
+ // decision the TS leg makes (shared `assertNoDecimalOperator` + message), so
1938
+ // the refusal is byte-identical across targets. Conservative: a no-op for plain
1939
+ // numeric arithmetic and for every non-`+`/`-`/`*` operator below.
1940
+ //
1941
+ // Slice-2 remediation (Finding 2): the assert is now performed AFTER the
1942
+ // operands are lowered (see below, after `emitPyExprCtx(node.left/right)`),
1943
+ // mirroring the TS leg's lower-then-assert order. A bad Decimal operand — an
1944
+ // unknown member (`Decimal.nope(...)`) or a non-canonical literal
1945
+ // (`Decimal.of("1.10")`) — then throws its OWN specific diagnostic during
1946
+ // lowering instead of being masked by the generic operator error. The blocked
1947
+ // operators are only `+`/`-`/`*`, so deferring the assert past the bitwise /
1948
+ // shift / modulo branches below is safe (they never trip the Decimal check).
1933
1949
  // Slice 6 — bitwise / shift on the slice-0.75 ToInt32 substrate. Emitted
1934
1950
  // DIRECTLY as helper-wrapped strings (operands recurse through
1935
1951
  // `emitPyExprCtx`), so nested bitwise ops compose without the double-
@@ -1958,6 +1974,11 @@ function emitPyExprCtx(node, ctx) {
1958
1974
  // expected `(a == b) < c` evaluation order.
1959
1975
  const left = emitPyExprCtx(node.left, ctx);
1960
1976
  const right = emitPyExprCtx(node.right, ctx);
1977
+ // DECIMAL Slice 2 (item 3) — operator fail-close, now AFTER operand lowering
1978
+ // (Finding-2 remediation) so a bad Decimal operand surfaces its own diagnostic
1979
+ // first. No-op for every operator except `+`/`-`/`*`, so it never affects the
1980
+ // bitwise/shift/modulo paths handled above or the comparison/logical paths below.
1981
+ assertNoDecimalOperator(node);
1961
1982
  if (node.op === 'instanceof') {
1962
1983
  // JS `a instanceof B` → Python `isinstance(a, B)`. Emitting `instanceof`
1963
1984
  // verbatim would be a Python *syntax* error, so this lowering is
@@ -2137,8 +2158,12 @@ function emitPyExprCtx(node, ctx) {
2137
2158
  }
2138
2159
  const forceLeft = needsComparisonChainParens(node.left, node.op);
2139
2160
  const forceRight = needsComparisonChainParens(node.right, node.op);
2140
- const lp = forceLeft || needsBinaryParens(node.left, node.op, 'left') ? `(${left})` : left;
2141
- const rp = forceRight || needsBinaryParens(node.right, node.op, 'right') ? `(${right})` : right;
2161
+ const lp = forceLeft || needsLowPrecedenceOperandParens(node.left) || needsBinaryParens(node.left, node.op, 'left')
2162
+ ? `(${left})`
2163
+ : left;
2164
+ const rp = forceRight || needsLowPrecedenceOperandParens(node.right) || needsBinaryParens(node.right, node.op, 'right')
2165
+ ? `(${right})`
2166
+ : right;
2142
2167
  const op = mapBinaryOpToPython(node.op);
2143
2168
  return `${lp} ${op} ${rp}`;
2144
2169
  }
@@ -2864,6 +2889,13 @@ function lowerChain(node, ctx) {
2864
2889
  if (node.callee.kind === 'ident') {
2865
2890
  rejectHostRegExpValuePython(node.callee.name, ctx);
2866
2891
  }
2892
+ // DECIMAL Slice 1 — bare `Decimal(...)` (ident callee) fail-closes
2893
+ // SYMMETRICALLY with the TS leg. Only `Decimal.of`/`Decimal.add` (member
2894
+ // callees, handled by `applyStdlibLoweringPython` above) are portable. A
2895
+ // proven user binding named `Decimal` is left alone.
2896
+ if (node.callee.kind === 'ident' && node.callee.name === 'Decimal' && !isProvenUserBinding(ctx, 'Decimal')) {
2897
+ throw new Error(decimalBareConstructionFailMessage());
2898
+ }
2867
2899
  // Slice H — fail-closed on an UNMAPPED host-namespace member CALL. This runs
2868
2900
  // AFTER every explicit lowering hook above (stdlib, regex, lambda/array,
2869
2901
  // portable-array, super/String/Error) and BEFORE generic call emission, so a
@@ -2881,7 +2913,13 @@ function lowerChain(node, ctx) {
2881
2913
  const callee = node.callee;
2882
2914
  const inner = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index'
2883
2915
  ? lowerChain(callee, ctx)
2884
- : { guard: null, expr: emitPyExprCtx(callee, ctx) };
2916
+ : {
2917
+ guard: null,
2918
+ expr: (() => {
2919
+ const emitted = emitPyExprCtx(callee, ctx);
2920
+ return needsLowPrecedenceOperandParens(callee) ? `(${emitted})` : emitted;
2921
+ })(),
2922
+ };
2885
2923
  const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', ');
2886
2924
  return { guard: inner.guard, expr: `${inner.expr}(${args})`, lambdaBind: inner.lambdaBind };
2887
2925
  }
@@ -3549,6 +3587,15 @@ function wrapGuardIfAny(g, ctx) {
3549
3587
  function needsWalrusOperandParens(child) {
3550
3588
  return child.kind === 'conditional' || child.kind === 'lambda';
3551
3589
  }
3590
+ /** Low-precedence operand positions (`a <op> b`, `await x`, `<callee>(...)`)
3591
+ * must wrap a conditional child so the surrounding operator/call binds to the
3592
+ * whole operand instead of one ternary arm. */
3593
+ function needsLowPrecedenceOperandParens(child) {
3594
+ let node = child;
3595
+ while (node.kind === 'typeAssert' || node.kind === 'nonNull')
3596
+ node = node.expression;
3597
+ return node.kind === 'conditional';
3598
+ }
3552
3599
  /** S5 review fix — run `fn` with `ctx.banWalrus` set (save/restore), for
3553
3600
  * emitting an operand that will be interpolated into a comprehension/
3554
3601
  * generator ITERABLE position, where CPython rejects `:=` outright (see
@@ -3846,6 +3893,22 @@ function applyStdlibLoweringPython(call, ctx) {
3846
3893
  if (moduleName === 'Array' && methodName === 'from' && call.args.some((arg) => arg.kind === 'spread')) {
3847
3894
  throw new Error('Array.from portable lowering does not accept spread arguments; pass source and mapper directly.');
3848
3895
  }
3896
+ // DECIMAL Slice 1 — `Decimal.of(arg)` string-literal + canonical-scale check,
3897
+ // running the SAME shared-core validator the TS leg runs, so the fail-close is
3898
+ // byte-identical across targets.
3899
+ validateDecimalConstructionArg(moduleName, methodName, call);
3900
+ // DECIMAL Slice 3 — same shared compile-time pow fail-close the TS leg runs, so a
3901
+ // non-integer / non-literal exponent or a negative base is refused byte-identically.
3902
+ validateDecimalPowArgs(moduleName, methodName, call);
3903
+ // DECIMAL Slice 3 (robustness) — same shared non-Decimal-operand fail-close the TS
3904
+ // leg runs: a provably-non-Decimal LITERAL operand (a host number/string/…) passed
3905
+ // to any Decimal op but `of` is refused byte-identically, closing the silent
3906
+ // cross-target divergence a raw `0.1` operand would otherwise emit.
3907
+ validateDecimalOperands(moduleName, methodName, call);
3908
+ // DECIMAL Slice 3 — same shared compile-time zero-divisor fail-close: a literal
3909
+ // `Decimal.of("0")` divisor to `Decimal.div`/`Decimal.mod` is refused byte-
3910
+ // identically with the runtime guard's message (a dynamic zero stays runtime-caught).
3911
+ validateDecimalDivModArgs(moduleName, methodName, call);
3849
3912
  const listLambda = lowerListLambdaPython(moduleName, methodName, call, ctx);
3850
3913
  if (listLambda !== null)
3851
3914
  return listLambda;
@@ -3904,6 +3967,19 @@ function registerStdlibRequirementPython(requirement, ctx) {
3904
3967
  ctx.helpers.add(KERN_JS_NUMBER_HELPERS_PY);
3905
3968
  return;
3906
3969
  }
3970
+ // DECIMAL Slice 3 — `Decimal.div/mod/pow` register the guarded-ops helper block
3971
+ // (single-sourced in `decimal-contract.ts`) AND the `decimal` import (the helper
3972
+ // body references `_KernDecimal` for the `0**0 → 1` special-case). The block's
3973
+ // own `from decimal import Decimal as _KernDecimal` line supplies that binding;
3974
+ // we still register the `decimal` module import so the Decimal VALUES the helper
3975
+ // operates on (`__k_decimal.Decimal(...)` from their `Decimal.of` producers) keep
3976
+ // their existing import path — and so a div/mod/pow used WITHOUT a co-located
3977
+ // `Decimal.of` literal still imports decimal.
3978
+ if (requirement === 'decimal-ops') {
3979
+ ctx.helpers.add(KERN_DECIMAL_OPS_HELPER_PY);
3980
+ ctx.imports.add('decimal');
3981
+ return;
3982
+ }
3907
3983
  ctx.imports.add(requirement);
3908
3984
  }
3909
3985
  function lowerListLambdaPython(moduleName, methodName, call, ctx) {