From ddcac740117084ac8a028ce89edcc267c454fba8 Mon Sep 17 00:00:00 2001 From: "agon (KERN)" <292465531+KERN-Agon@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:24:20 +0200 Subject: [PATCH 1/4] test(codegen): ternary-precedence RED-at-base oracle (conditional operand must be parenthesized) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed-systemic bug (agon codex audit + verified on both legs): KERN's expression emitter drops the parens a `conditional` operand needs under a tighter operator, so `(true ? 2 : 3) * 5` silently compiles to `true ? 2 : 3 * 5` (=2, not 10) on BOTH the TS and Python legs — and breaks TS↔Python byte-parity for the Python call-callee + await cases (TS wraps, Python doesn't). This is the frozen spec for the fix. RED-at-base (4 failing fixtures): `(ternary)*x`, `x*(ternary)` on both legs, `(ternary)(x)` + `await (ternary)` on Python. GREEN guards pin already-correct output (TS call/await, member receiver, plain binary chains) so the fix can't over-wrap or regress. `*` is used (raw infix on both legs) because Python lowers `+` to a call-delimited `__kern_add(...)` that is already safe. Root cause = ad-hoc parenthesization, not one precedence-aware operand policy; TS emitter is codegen-expression.ts, Python is codegen-body-python.ts. ⚔️ Forged by [Agon](https://github.com/KERNlang/agon) Co-Authored-By: agon (KERN) <292465531+KERN-Agon@users.noreply.github.com> --- .../tests/ternary-precedence-codegen.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/python/tests/ternary-precedence-codegen.test.ts diff --git a/packages/python/tests/ternary-precedence-codegen.test.ts b/packages/python/tests/ternary-precedence-codegen.test.ts new file mode 100644 index 00000000..01980b08 --- /dev/null +++ b/packages/python/tests/ternary-precedence-codegen.test.ts @@ -0,0 +1,64 @@ +/** TERNARY-PRECEDENCE codegen fix — the discriminating RED-at-base oracle. + * + * BUG (confirmed systemic, agon codex audit 2026-06-16 + verified empirically on both + * legs): KERN's expression emitter drops the parentheses a `conditional` operand needs + * when it sits under a higher-precedence operator. `?:` (TS) / `... if ... else ...` + * (Python) bind LOOSER than `*`/`await`/call, so an unwrapped conditional operand + * silently re-associates — `(true ? 2 : 3) * 5` compiles to `true ? 2 : 3 * 5` (= 2, + * not 10). It also breaks TS↔Python byte-parity (the Python call-callee + await cases + * emit unwrapped while TS wraps). + * + * ROOT CAUSE: ad-hoc parenthesization instead of one precedence-aware "subexpression + * operand" policy. TS emitter = `packages/core/src/codegen-expression.ts`; Python = + * `packages/python/src/codegen-body-python.ts`. The fix must wrap a `conditional` + * child in EVERY low-precedence operand position on BOTH legs — and must NOT + * over-wrap (the guards below pin already-correct output so a "wrap everything" cheat + * fails). + * + * Python note: `+` lowers to `__kern_add(...)` (call-delimited, inherently safe), so + * these fixtures use `*` (raw infix on BOTH legs) where the bug actually bites. */ + +import { emitExpression, parseExpression } from '@kernlang/core'; +import { emitPyExpression } from '../src/codegen-body-python.js'; + +const ts = (src: string): string => emitExpression(parseExpression(src)); +const py = (src: string): string => emitPyExpression(parseExpression(src)); + +// ── RED at base — a `conditional` operand under a tighter operator must be wrapped ── +describe('ternary-precedence — conditional operand is parenthesized (currently RED)', () => { + // #1 generic infix (binary `*`) — WRONG on BOTH legs at base. + test('(ternary) * x — left operand, both legs', () => { + expect(ts('(true ? 2 : 3) * 5')).toBe('(true ? 2 : 3) * 5'); + expect(py('(true ? 2 : 3) * 5')).toBe('(2 if _kern_truthy(True) else 3) * 5'); + }); + test('x * (ternary) — right operand, both legs', () => { + expect(ts('5 * (false ? 2 : 3)')).toBe('5 * (false ? 2 : 3)'); + expect(py('5 * (false ? 2 : 3)')).toBe('5 * (2 if _kern_truthy(False) else 3)'); + }); + // #2 call callee — WRONG on Python (TS already wraps → also a parity divergence). + test('(ternary)(x) — call callee, Python', () => { + expect(py('(ok ? f : g)(x)')).toBe('(f if _kern_truthy(ok) else g)(x)'); + }); + // #3 await operand — WRONG on Python. + test('await (ternary) — Python', () => { + expect(py('await (ok ? a() : b())')).toBe('await (a() if _kern_truthy(ok) else b())'); + }); +}); + +// ── GREEN guards — pin already-correct output so the fix can't OVER-wrap / regress ── +describe('ternary-precedence — guards (must stay byte-identical, no spurious parens)', () => { + test('TS call callee + await already wrap correctly — must not break', () => { + expect(ts('(ok ? f : g)(x)')).toBe('(ok ? f : g)(x)'); + expect(ts('await (ok ? a() : b())')).toBe('await (ok ? a() : b())'); + }); + test('member receiver of a ternary is wrapped on both legs — must stay', () => { + expect(ts('(a ? b : c).d')).toBe('(a ? b : c).d'); + expect(py('(a ? b : c).d')).toBe('(b if _kern_truthy(a) else c).d'); + }); + test('plain binary chains gain NO spurious parentheses', () => { + expect(ts('2 * 3 + 4')).toBe('2 * 3 + 4'); + expect(py('2 * 3 + 4')).toBe('__kern_add(2 * 3, 4)'); + expect(ts('2 * 5')).toBe('2 * 5'); + expect(py('a + b')).toBe('__kern_add(a, b)'); + }); +}); From 6fc4fd8e83ba452926e5c8e8a162c69241e92227 Mon Sep 17 00:00:00 2001 From: "agon (KERN)" <292465531+KERN-Agon@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:18:10 +0200 Subject: [PATCH 2/4] fix(codegen): precedence-aware wrap of conditional operands (binary / call / await, both legs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the systemic ternary-precedence miscompile for the three direct operand positions: - TS (codegen-expression.ts): `needsParens` now returns true for a `conditional` child, so a ternary binary operand is parenthesized (`(true ? 2 : 3) * 5`, not `true ? 2 : 3 * 5`). - Python (codegen-body-python.ts): a shared `needsLowPrecedenceOperandParens` predicate wraps a `conditional` child in binary operands (left+right), `await` operands, and call callees. Makes the original RED oracle green (4 fixtures, both legs) with no ripple — full emission/codegen/ golden/ternary/portable-expr suites stay green. Does NOT over-wrap (plain binary chains unchanged). Transparent-wrapper case (`(ternary as T)`) is handled in the follow-up commit. ⚔️ Forged by [Agon](https://github.com/KERNlang/agon) Co-Authored-By: agon (KERN) <292465531+KERN-Agon@users.noreply.github.com> --- packages/core/src/codegen-expression.ts | 2 +- packages/python/src/codegen-body-python.ts | 31 ++++++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/core/src/codegen-expression.ts b/packages/core/src/codegen-expression.ts index 186ea5ca..02e6aa7f 100644 --- a/packages/core/src/codegen-expression.ts +++ b/packages/core/src/codegen-expression.ts @@ -639,7 +639,7 @@ export function needsBinaryParens(child: ValueIR, parentOp: string, side: 'left' } function needsParens(child: ValueIR, parentOp: string, side: 'left' | 'right'): boolean { - if (child.kind === 'typeAssert') return true; + if (child.kind === 'typeAssert' || child.kind === 'conditional') return true; return needsBinaryParens(child, parentOp, side); } diff --git a/packages/python/src/codegen-body-python.ts b/packages/python/src/codegen-body-python.ts index ffa4a0d4..d56ca3b6 100644 --- a/packages/python/src/codegen-body-python.ts +++ b/packages/python/src/codegen-body-python.ts @@ -2112,8 +2112,10 @@ function emitPyExprCtx(node: ValueIR, ctx: BodyEmitContext): string { const lowered = lowerChain(node, ctx); return wrapGuardIfAny(lowered, ctx); } - case 'await': - return `await ${emitPyExprCtx(node.argument, ctx)}`; + case 'await': { + const arg = emitPyExprCtx(node.argument, ctx); + return `await ${needsLowPrecedenceOperandParens(node.argument) ? `(${arg})` : arg}`; + } case 'new': { // Host Error mapping (spec §1): `new Error(args)` → `Exception(args)` on // Python, since `raise Error(...)` / `isinstance(x, Error)` would @@ -2414,8 +2416,14 @@ function emitPyExprCtx(node: ValueIR, ctx: BodyEmitContext): string { const forceLeft = needsComparisonChainParens(node.left, node.op); const forceRight = needsComparisonChainParens(node.right, node.op); - const lp = forceLeft || needsBinaryParens(node.left, node.op, 'left') ? `(${left})` : left; - const rp = forceRight || needsBinaryParens(node.right, node.op, 'right') ? `(${right})` : right; + const lp = + forceLeft || needsLowPrecedenceOperandParens(node.left) || needsBinaryParens(node.left, node.op, 'left') + ? `(${left})` + : left; + const rp = + forceRight || needsLowPrecedenceOperandParens(node.right) || needsBinaryParens(node.right, node.op, 'right') + ? `(${right})` + : right; const op = mapBinaryOpToPython(node.op); return `${lp} ${op} ${rp}`; } @@ -3225,7 +3233,13 @@ function lowerChain(node: ChainNode, ctx: BodyEmitContext): GuardedExpr { const inner: GuardedExpr = callee.kind === 'member' || callee.kind === 'call' || callee.kind === 'index' ? lowerChain(callee, ctx) - : { guard: null, expr: emitPyExprCtx(callee, ctx) }; + : { + guard: null, + expr: (() => { + const emitted = emitPyExprCtx(callee, ctx); + return needsLowPrecedenceOperandParens(callee) ? `(${emitted})` : emitted; + })(), + }; const args = node.args.map((a) => emitPyExprCtx(a, ctx)).join(', '); return { guard: inner.guard, expr: `${inner.expr}(${args})`, lambdaBind: inner.lambdaBind }; } @@ -3904,6 +3918,13 @@ function needsWalrusOperandParens(child: ValueIR): boolean { return child.kind === 'conditional' || child.kind === 'lambda'; } +/** Low-precedence operand positions (`a b`, `await x`, `(...)`) + * must wrap a conditional child so the surrounding operator/call binds to the + * whole operand instead of one ternary arm. */ +function needsLowPrecedenceOperandParens(child: ValueIR): boolean { + return child.kind === 'conditional'; +} + /** S5 review fix — run `fn` with `ctx.banWalrus` set (save/restore), for * emitting an operand that will be interpolated into a comprehension/ * generator ITERABLE position, where CPython rejects `:=` outright (see From c6c91b3991f87c5febd54fb0902cdcdc982dc9ab Mon Sep 17 00:00:00 2001 From: "agon (KERN)" <292465531+KERN-Agon@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:18:10 +0200 Subject: [PATCH 3/4] =?UTF-8?q?test(codegen):=20extend=20ternary=20oracle?= =?UTF-8?q?=20=E2=80=94=20transparent-wrapped=20conditional=20(D1,=20RED)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agon impl-review (full roster) surfaced a real gap the first oracle missed: Python ERASES transparent wrappers (`as T` / `!`), so `((true ? 2 : 3) as any) * 5` emits a BARE conditional into a tight position → `2 if _kern_truthy(True) else 3 * 5` (=2), while TS keeps `(... as any)` and is correct — a miscompile AND a TS↔Python byte-divergence. RED fixtures for the binary, call- callee, and await positions; member-object guard (already wrapped) pins no-regress. The operand predicate must peel transparent wrappers and test the inner kind. ⚔️ Forged by [Agon](https://github.com/KERNlang/agon) Co-Authored-By: agon (KERN) <292465531+KERN-Agon@users.noreply.github.com> --- .../tests/ternary-precedence-codegen.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/python/tests/ternary-precedence-codegen.test.ts b/packages/python/tests/ternary-precedence-codegen.test.ts index 01980b08..0132fbf5 100644 --- a/packages/python/tests/ternary-precedence-codegen.test.ts +++ b/packages/python/tests/ternary-precedence-codegen.test.ts @@ -62,3 +62,25 @@ describe('ternary-precedence — guards (must stay byte-identical, no spurious p expect(py('a + b')).toBe('__kern_add(a, b)'); }); }); + +// ── TRANSPARENT-WRAPPER blindness (D1) — Python ERASES `as T` / `!`, so a wrapped +// conditional becomes a BARE conditional in a tight position. The operand predicate must +// peel transparent wrappers (typeAssert/nonNull) and test the INNER kind. TS keeps the +// wrapper (`(x as T)` self-parenthesizes) so TS is already correct — this is a Python-only +// fix AND a TS↔Python byte-divergence until closed. (The recurring wrapper-bypass class.) +describe('ternary-precedence — transparent-wrapped conditional operand (currently RED, Python)', () => { + test('(ternary as T) under a binary op — Python wraps the erased conditional', () => { + expect(py('((true ? 2 : 3) as any) * 5')).toBe('(2 if _kern_truthy(True) else 3) * 5'); + // TS guard: already correct (keeps the cast, wraps the whole operand) — must not regress. + expect(ts('((true ? 2 : 3) as any) * 5')).toBe('((true ? 2 : 3) as any) * 5'); + }); + test('(ternary as T) as a call callee — Python', () => { + expect(py('((ok ? f : g) as any)(x)')).toBe('(f if _kern_truthy(ok) else g)(x)'); + }); + test('await (ternary as T) — Python', () => { + expect(py('await ((ok ? a() : b()) as any)')).toBe('await (a() if _kern_truthy(ok) else b())'); + }); + test('member object of a wrapped ternary is already wrapped — must stay (guard)', () => { + expect(py('((a ? b : c) as any).d')).toBe('(b if _kern_truthy(a) else c).d'); + }); +}); From 240cf4605f7ed2f04874cbfdd9f8d53e4e502d44 Mon Sep 17 00:00:00 2001 From: "agon (KERN)" <292465531+KERN-Agon@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:05:46 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix(codegen):=20close=20D1=20=E2=80=94=20un?= =?UTF-8?q?wrap=20transparent=20wrappers=20in=20the=20Python=20operand=20p?= =?UTF-8?q?redicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `needsLowPrecedenceOperandParens` now peels `typeAssert`/`nonNull` (`.expression`) before testing for an inner `conditional`, so a wrapped ternary in a low-precedence operand position is parenthesized on the Python leg too. Fixes `((true ? 2 : 3) as any) * 5` → `(2 if _kern_truthy(True) else 3) * 5` (was `2 if ... else 3 * 5`), closing the miscompile AND the TS↔Python byte-divergence (TS already kept `(... as any)`). One shared helper → all three positions (binary/await/call) covered. Gate green: full ternary oracle (incl. the D1 block) + codegen/golden/ternary/emission suites; no over-wrap, no ripple. TS emitter untouched (already correct). ⚔️ Forged by [Agon](https://github.com/KERNlang/agon) Co-Authored-By: agon (KERN) <292465531+KERN-Agon@users.noreply.github.com> --- packages/python/src/codegen-body-python.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/python/src/codegen-body-python.ts b/packages/python/src/codegen-body-python.ts index d56ca3b6..5d5b4249 100644 --- a/packages/python/src/codegen-body-python.ts +++ b/packages/python/src/codegen-body-python.ts @@ -3922,7 +3922,9 @@ function needsWalrusOperandParens(child: ValueIR): boolean { * must wrap a conditional child so the surrounding operator/call binds to the * whole operand instead of one ternary arm. */ function needsLowPrecedenceOperandParens(child: ValueIR): boolean { - return child.kind === 'conditional'; + let node = child; + while (node.kind === 'typeAssert' || node.kind === 'nonNull') node = node.expression; + return node.kind === 'conditional'; } /** S5 review fix — run `fn` with `ctx.banWalrus` set (save/restore), for