@kernlang/python 4.0.1-canary.224.1.1a92ac0a → 4.1.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.
- package/dist/codegen-body-python.d.ts +10 -0
- package/dist/codegen-body-python.js +115 -6
- package/dist/codegen-body-python.js.map +1 -1
- package/dist/core/expr/helpers.d.ts +32 -0
- package/dist/core/expr/helpers.js +75 -1
- package/dist/core/expr/helpers.js.map +1 -1
- package/dist/core/expr/index.d.ts +1 -1
- package/dist/core/expr/index.js +3 -1
- package/dist/core/expr/index.js.map +1 -1
- package/package.json +5 -2
|
@@ -171,6 +171,16 @@ interface BodyEmitContext {
|
|
|
171
171
|
/** Depth of nested `finally` blocks. Propagation from finally would
|
|
172
172
|
* override pending control flow, so it gets a finally-specific error. */
|
|
173
173
|
finallyDepth: number;
|
|
174
|
+
/** Error-substrate Slice 1 — names currently bound by an enclosing
|
|
175
|
+
* `except Exception as <name>:` (a body-statement `catch name=<name>`). A
|
|
176
|
+
* caught binding's `.message` read lowers to `str(<name>)` (Python exceptions
|
|
177
|
+
* have no `.message` attribute; `str(Exception("x"))` === TS
|
|
178
|
+
* `Error("x").message`). Populated on catch-body entry, restored on exit
|
|
179
|
+
* (a `Set`, so nested catches with distinct names stack; a re-bound same name
|
|
180
|
+
* is idempotent). Only `.message` is rewritten — `.name`/`.stack`/etc. are NOT
|
|
181
|
+
* in the parity domain and emit verbatim (the runner abstains on them, so they
|
|
182
|
+
* never reach a certified differential row). */
|
|
183
|
+
caughtBindings: Set<string>;
|
|
174
184
|
standaloneExpression: boolean;
|
|
175
185
|
/** When true, helper-dependent JS value→string coercion is emitted
|
|
176
186
|
* (`__kern_add`, `_kern_fmt`-wrapped templates, the `_KERN_UNDEFINED`
|
|
@@ -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`.
|
|
@@ -75,6 +75,7 @@ function freshCtx(options) {
|
|
|
75
75
|
usedPropagation: false,
|
|
76
76
|
tryDepth: 0,
|
|
77
77
|
finallyDepth: 0,
|
|
78
|
+
caughtBindings: new Set(),
|
|
78
79
|
standaloneExpression: false,
|
|
79
80
|
coerceJsValues: options?.coerceJsValues ?? true,
|
|
80
81
|
traceHooks: options?.traceHooks,
|
|
@@ -310,6 +311,13 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
310
311
|
lines.push(`${indent}${line}`);
|
|
311
312
|
}
|
|
312
313
|
else if (child.type === 'let') {
|
|
314
|
+
// Error-substrate Slice 1 (codex review: shadowing) — a `let` that rebinds
|
|
315
|
+
// a caught-error name SHADOWS the catch binding, so from here on its
|
|
316
|
+
// `.message` is an ordinary property access (matching TS), NOT `str(e)`.
|
|
317
|
+
// Drop it from the caught set so the rewrite no longer fires.
|
|
318
|
+
const letName = child.props?.name;
|
|
319
|
+
if (typeof letName === 'string')
|
|
320
|
+
ctx.caughtBindings.delete(letName);
|
|
313
321
|
for (const line of emitLetPy(child, ctx))
|
|
314
322
|
lines.push(`${indent}${line}`);
|
|
315
323
|
}
|
|
@@ -322,6 +330,11 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
322
330
|
lines.push(line);
|
|
323
331
|
}
|
|
324
332
|
else if (child.type === 'assign') {
|
|
333
|
+
// Same shadowing guard for a bare-identifier assignment target: after
|
|
334
|
+
// `assign target="e" …`, `e` is no longer the caught error.
|
|
335
|
+
const tgt = child.props?.target;
|
|
336
|
+
if (typeof tgt === 'string' && /^[A-Za-z_$][\w$]*$/.test(tgt))
|
|
337
|
+
ctx.caughtBindings.delete(tgt);
|
|
325
338
|
for (const line of emitAssignPy(child, ctx))
|
|
326
339
|
lines.push(`${indent}${line}`);
|
|
327
340
|
}
|
|
@@ -492,7 +505,16 @@ function emitChildrenPy(children, ctx, indent, initialBindings = [], isLoopBody
|
|
|
492
505
|
if (catchNode !== null) {
|
|
493
506
|
const errName = String(catchNode.props?.name ?? 'e');
|
|
494
507
|
lines.push(`${indent}except Exception as ${errName}:`);
|
|
508
|
+
// Error-substrate Slice 1 — mark `errName` as a caught binding for the
|
|
509
|
+
// duration of the catch body so a `<errName>.message` read lowers to
|
|
510
|
+
// `str(<errName>)` (see `lowerChain`'s member case). Restore the prior
|
|
511
|
+
// membership on exit so a SHADOWED outer catch binding of the same name
|
|
512
|
+
// is unaffected and the flag never leaks past the catch block.
|
|
513
|
+
const hadCaught = ctx.caughtBindings.has(errName);
|
|
514
|
+
ctx.caughtBindings.add(errName);
|
|
495
515
|
const catchInner = emitChildrenPy(catchNode.children ?? [], ctx, indent + INDENT_STEP);
|
|
516
|
+
if (!hadCaught)
|
|
517
|
+
ctx.caughtBindings.delete(errName);
|
|
496
518
|
if (catchInner.length === 0)
|
|
497
519
|
lines.push(`${indent}${INDENT_STEP}pass`);
|
|
498
520
|
for (const cl of catchInner)
|
|
@@ -1862,8 +1884,10 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1862
1884
|
const lowered = lowerChain(node, ctx);
|
|
1863
1885
|
return wrapGuardIfAny(lowered, ctx);
|
|
1864
1886
|
}
|
|
1865
|
-
case 'await':
|
|
1866
|
-
|
|
1887
|
+
case 'await': {
|
|
1888
|
+
const arg = emitPyExprCtx(node.argument, ctx);
|
|
1889
|
+
return `await ${needsLowPrecedenceOperandParens(node.argument) ? `(${arg})` : arg}`;
|
|
1890
|
+
}
|
|
1867
1891
|
case 'new': {
|
|
1868
1892
|
// Host Error mapping (spec §1): `new Error(args)` → `Exception(args)` on
|
|
1869
1893
|
// Python, since `raise Error(...)` / `isinstance(x, Error)` would
|
|
@@ -1930,6 +1954,20 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1930
1954
|
return out;
|
|
1931
1955
|
}
|
|
1932
1956
|
case 'binary': {
|
|
1957
|
+
// DECIMAL Slice 2 (item 3) — fail closed on `+`/`-`/`*` over a syntactically-
|
|
1958
|
+
// proven Decimal operand (`Decimal.of(...)`/`Decimal.<m>(...)`), the SAME
|
|
1959
|
+
// decision the TS leg makes (shared `assertNoDecimalOperator` + message), so
|
|
1960
|
+
// the refusal is byte-identical across targets. Conservative: a no-op for plain
|
|
1961
|
+
// numeric arithmetic and for every non-`+`/`-`/`*` operator below.
|
|
1962
|
+
//
|
|
1963
|
+
// Slice-2 remediation (Finding 2): the assert is now performed AFTER the
|
|
1964
|
+
// operands are lowered (see below, after `emitPyExprCtx(node.left/right)`),
|
|
1965
|
+
// mirroring the TS leg's lower-then-assert order. A bad Decimal operand — an
|
|
1966
|
+
// unknown member (`Decimal.nope(...)`) or a non-canonical literal
|
|
1967
|
+
// (`Decimal.of("1.10")`) — then throws its OWN specific diagnostic during
|
|
1968
|
+
// lowering instead of being masked by the generic operator error. The blocked
|
|
1969
|
+
// operators are only `+`/`-`/`*`, so deferring the assert past the bitwise /
|
|
1970
|
+
// shift / modulo branches below is safe (they never trip the Decimal check).
|
|
1933
1971
|
// Slice 6 — bitwise / shift on the slice-0.75 ToInt32 substrate. Emitted
|
|
1934
1972
|
// DIRECTLY as helper-wrapped strings (operands recurse through
|
|
1935
1973
|
// `emitPyExprCtx`), so nested bitwise ops compose without the double-
|
|
@@ -1958,6 +1996,11 @@ function emitPyExprCtx(node, ctx) {
|
|
|
1958
1996
|
// expected `(a == b) < c` evaluation order.
|
|
1959
1997
|
const left = emitPyExprCtx(node.left, ctx);
|
|
1960
1998
|
const right = emitPyExprCtx(node.right, ctx);
|
|
1999
|
+
// DECIMAL Slice 2 (item 3) — operator fail-close, now AFTER operand lowering
|
|
2000
|
+
// (Finding-2 remediation) so a bad Decimal operand surfaces its own diagnostic
|
|
2001
|
+
// first. No-op for every operator except `+`/`-`/`*`, so it never affects the
|
|
2002
|
+
// bitwise/shift/modulo paths handled above or the comparison/logical paths below.
|
|
2003
|
+
assertNoDecimalOperator(node);
|
|
1961
2004
|
if (node.op === 'instanceof') {
|
|
1962
2005
|
// JS `a instanceof B` → Python `isinstance(a, B)`. Emitting `instanceof`
|
|
1963
2006
|
// verbatim would be a Python *syntax* error, so this lowering is
|
|
@@ -2137,8 +2180,12 @@ function emitPyExprCtx(node, ctx) {
|
|
|
2137
2180
|
}
|
|
2138
2181
|
const forceLeft = needsComparisonChainParens(node.left, node.op);
|
|
2139
2182
|
const forceRight = needsComparisonChainParens(node.right, node.op);
|
|
2140
|
-
const lp = forceLeft || needsBinaryParens(node.left, node.op, 'left')
|
|
2141
|
-
|
|
2183
|
+
const lp = forceLeft || needsLowPrecedenceOperandParens(node.left) || needsBinaryParens(node.left, node.op, 'left')
|
|
2184
|
+
? `(${left})`
|
|
2185
|
+
: left;
|
|
2186
|
+
const rp = forceRight || needsLowPrecedenceOperandParens(node.right) || needsBinaryParens(node.right, node.op, 'right')
|
|
2187
|
+
? `(${right})`
|
|
2188
|
+
: right;
|
|
2142
2189
|
const op = mapBinaryOpToPython(node.op);
|
|
2143
2190
|
return `${lp} ${op} ${rp}`;
|
|
2144
2191
|
}
|
|
@@ -2698,6 +2745,17 @@ function lowerOptionalLink(inner, objectNode, ctx) {
|
|
|
2698
2745
|
function lowerChain(node, ctx) {
|
|
2699
2746
|
if (node.kind === 'member') {
|
|
2700
2747
|
const obj = node.object;
|
|
2748
|
+
// Error-substrate Slice 1 — a `<caughtBinding>.message` read lowers to
|
|
2749
|
+
// `str(<caughtBinding>)`. Python exceptions have NO `.message` attribute
|
|
2750
|
+
// (a bare `e.message` raises AttributeError), but `str(Exception("x"))`
|
|
2751
|
+
// === V8 `Error("x").message`, so this restores cross-target parity for the
|
|
2752
|
+
// ONE admitted error-binding read. Scoped exactly: a NON-optional `.message`
|
|
2753
|
+
// on a bare ident that is currently a caught binding. `e?.message`, a member
|
|
2754
|
+
// chain root (`e.cause.message`), or any non-caught ident is NOT rewritten
|
|
2755
|
+
// and emits verbatim — out of the parity domain (the runner abstains there).
|
|
2756
|
+
if (!node.optional && node.property === 'message' && obj.kind === 'ident' && ctx.caughtBindings.has(obj.name)) {
|
|
2757
|
+
return { guard: null, expr: `str(${emitPyExprCtx(obj, ctx)})` };
|
|
2758
|
+
}
|
|
2701
2759
|
// Slice 2 — a bare property READ on a DIRECT regex LITERAL (`/x/.source`,
|
|
2702
2760
|
// `/x/.flags`) launders the pattern/flags back into a string. Routed through
|
|
2703
2761
|
// the SHARED classifier (via the ValueIR adapter) so this site agrees with
|
|
@@ -2864,6 +2922,13 @@ function lowerChain(node, ctx) {
|
|
|
2864
2922
|
if (node.callee.kind === 'ident') {
|
|
2865
2923
|
rejectHostRegExpValuePython(node.callee.name, ctx);
|
|
2866
2924
|
}
|
|
2925
|
+
// DECIMAL Slice 1 — bare `Decimal(...)` (ident callee) fail-closes
|
|
2926
|
+
// SYMMETRICALLY with the TS leg. Only `Decimal.of`/`Decimal.add` (member
|
|
2927
|
+
// callees, handled by `applyStdlibLoweringPython` above) are portable. A
|
|
2928
|
+
// proven user binding named `Decimal` is left alone.
|
|
2929
|
+
if (node.callee.kind === 'ident' && node.callee.name === 'Decimal' && !isProvenUserBinding(ctx, 'Decimal')) {
|
|
2930
|
+
throw new Error(decimalBareConstructionFailMessage());
|
|
2931
|
+
}
|
|
2867
2932
|
// Slice H — fail-closed on an UNMAPPED host-namespace member CALL. This runs
|
|
2868
2933
|
// AFTER every explicit lowering hook above (stdlib, regex, lambda/array,
|
|
2869
2934
|
// portable-array, super/String/Error) and BEFORE generic call emission, so a
|
|
@@ -2881,7 +2946,13 @@ function lowerChain(node, ctx) {
|
|
|
2881
2946
|
const callee = node.callee;
|
|
2882
2947
|
const inner = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index'
|
|
2883
2948
|
? lowerChain(callee, ctx)
|
|
2884
|
-
: {
|
|
2949
|
+
: {
|
|
2950
|
+
guard: null,
|
|
2951
|
+
expr: (() => {
|
|
2952
|
+
const emitted = emitPyExprCtx(callee, ctx);
|
|
2953
|
+
return needsLowPrecedenceOperandParens(callee) ? `(${emitted})` : emitted;
|
|
2954
|
+
})(),
|
|
2955
|
+
};
|
|
2885
2956
|
const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', ');
|
|
2886
2957
|
return { guard: inner.guard, expr: `${inner.expr}(${args})`, lambdaBind: inner.lambdaBind };
|
|
2887
2958
|
}
|
|
@@ -3549,6 +3620,15 @@ function wrapGuardIfAny(g, ctx) {
|
|
|
3549
3620
|
function needsWalrusOperandParens(child) {
|
|
3550
3621
|
return child.kind === 'conditional' || child.kind === 'lambda';
|
|
3551
3622
|
}
|
|
3623
|
+
/** Low-precedence operand positions (`a <op> b`, `await x`, `<callee>(...)`)
|
|
3624
|
+
* must wrap a conditional child so the surrounding operator/call binds to the
|
|
3625
|
+
* whole operand instead of one ternary arm. */
|
|
3626
|
+
function needsLowPrecedenceOperandParens(child) {
|
|
3627
|
+
let node = child;
|
|
3628
|
+
while (node.kind === 'typeAssert' || node.kind === 'nonNull')
|
|
3629
|
+
node = node.expression;
|
|
3630
|
+
return node.kind === 'conditional';
|
|
3631
|
+
}
|
|
3552
3632
|
/** S5 review fix — run `fn` with `ctx.banWalrus` set (save/restore), for
|
|
3553
3633
|
* emitting an operand that will be interpolated into a comprehension/
|
|
3554
3634
|
* generator ITERABLE position, where CPython rejects `:=` outright (see
|
|
@@ -3846,6 +3926,22 @@ function applyStdlibLoweringPython(call, ctx) {
|
|
|
3846
3926
|
if (moduleName === 'Array' && methodName === 'from' && call.args.some((arg) => arg.kind === 'spread')) {
|
|
3847
3927
|
throw new Error('Array.from portable lowering does not accept spread arguments; pass source and mapper directly.');
|
|
3848
3928
|
}
|
|
3929
|
+
// DECIMAL Slice 1 — `Decimal.of(arg)` string-literal + canonical-scale check,
|
|
3930
|
+
// running the SAME shared-core validator the TS leg runs, so the fail-close is
|
|
3931
|
+
// byte-identical across targets.
|
|
3932
|
+
validateDecimalConstructionArg(moduleName, methodName, call);
|
|
3933
|
+
// DECIMAL Slice 3 — same shared compile-time pow fail-close the TS leg runs, so a
|
|
3934
|
+
// non-integer / non-literal exponent or a negative base is refused byte-identically.
|
|
3935
|
+
validateDecimalPowArgs(moduleName, methodName, call);
|
|
3936
|
+
// DECIMAL Slice 3 (robustness) — same shared non-Decimal-operand fail-close the TS
|
|
3937
|
+
// leg runs: a provably-non-Decimal LITERAL operand (a host number/string/…) passed
|
|
3938
|
+
// to any Decimal op but `of` is refused byte-identically, closing the silent
|
|
3939
|
+
// cross-target divergence a raw `0.1` operand would otherwise emit.
|
|
3940
|
+
validateDecimalOperands(moduleName, methodName, call);
|
|
3941
|
+
// DECIMAL Slice 3 — same shared compile-time zero-divisor fail-close: a literal
|
|
3942
|
+
// `Decimal.of("0")` divisor to `Decimal.div`/`Decimal.mod` is refused byte-
|
|
3943
|
+
// identically with the runtime guard's message (a dynamic zero stays runtime-caught).
|
|
3944
|
+
validateDecimalDivModArgs(moduleName, methodName, call);
|
|
3849
3945
|
const listLambda = lowerListLambdaPython(moduleName, methodName, call, ctx);
|
|
3850
3946
|
if (listLambda !== null)
|
|
3851
3947
|
return listLambda;
|
|
@@ -3904,6 +4000,19 @@ function registerStdlibRequirementPython(requirement, ctx) {
|
|
|
3904
4000
|
ctx.helpers.add(KERN_JS_NUMBER_HELPERS_PY);
|
|
3905
4001
|
return;
|
|
3906
4002
|
}
|
|
4003
|
+
// DECIMAL Slice 3 — `Decimal.div/mod/pow` register the guarded-ops helper block
|
|
4004
|
+
// (single-sourced in `decimal-contract.ts`) AND the `decimal` import (the helper
|
|
4005
|
+
// body references `_KernDecimal` for the `0**0 → 1` special-case). The block's
|
|
4006
|
+
// own `from decimal import Decimal as _KernDecimal` line supplies that binding;
|
|
4007
|
+
// we still register the `decimal` module import so the Decimal VALUES the helper
|
|
4008
|
+
// operates on (`__k_decimal.Decimal(...)` from their `Decimal.of` producers) keep
|
|
4009
|
+
// their existing import path — and so a div/mod/pow used WITHOUT a co-located
|
|
4010
|
+
// `Decimal.of` literal still imports decimal.
|
|
4011
|
+
if (requirement === 'decimal-ops') {
|
|
4012
|
+
ctx.helpers.add(KERN_DECIMAL_OPS_HELPER_PY);
|
|
4013
|
+
ctx.imports.add('decimal');
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
3907
4016
|
ctx.imports.add(requirement);
|
|
3908
4017
|
}
|
|
3909
4018
|
function lowerListLambdaPython(moduleName, methodName, call, ctx) {
|