Skip to content

Feat/kern core runtime#397

Merged
cukas merged 68 commits into
mainfrom
feat/kern-core-runtime
Jun 9, 2026
Merged

Feat/kern core runtime#397
cukas merged 68 commits into
mainfrom
feat/kern-core-runtime

Conversation

@cukas

@cukas cukas commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What

Why

How

Checklist

  • tsc -b passes
  • pnpm test passes
  • pnpm test:kern passes
  • pnpm lint passes
  • kern review packages/ --recursive checked

cukas and others added 30 commits June 6, 2026 17:21
* Add portable expression logic foundation

* Format portable expression foundation changes
Bumps the minor-and-patch group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.1` | `25.9.2` |
| [next](https://github.com/vercel/next.js) | `16.2.6` | `16.2.7` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.7` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.15` | `19.2.17` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.7` |


Updates `@types/node` from 25.9.1 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `next` from 16.2.6 to 16.2.7
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](vercel/next.js@v16.2.6...v16.2.7)

Updates `react` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react)

Updates `@types/react` from 19.2.15 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

Updates `@types/react` from 19.2.15 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: next
  dependency-version: 16.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: react
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Add portable expression logic foundation

* Format portable expression foundation changes
# Conflicts:
#	packages/core/src/core-runtime/index.ts
#	packages/core/src/semantic-validator.ts
#	packages/core/tests/class-semantics.test.ts
cukas and others added 28 commits June 9, 2026 07:33
A static setter whose param snake-cased to `cls` emitted invalid Python
(`def label(cls, cls):`). The body collision guard reserved only `self`,
never the injected metaclass receiver. Reserve the actual receiver (`cls`
for static accessors, `self` for instance members) and fail codegen early
with a clear message instead of producing a SyntaxError.

Closes the codex (0.96) finding from the PR 2.1 static-accessor agon review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Class-method bodies emitted `this.items.push(x)` verbatim — invalid Python,
since lists have no `.push` — while the route emitter already lowered it. The
two Python emitters had drifted: each open-coded its own array dispatch.

Extract the portable-array lowering into one shared module
(core/expr/list-ops.ts) that BOTH the route emitter and the class-method body
emitter delegate to, so `arr.push(x)` lowers identically (TS native; Python
`(recv.append(x) or len(recv))`, matching JS push's new-length return)
wherever it appears — parity by construction, no per-path drift.

The class-body hook lowers only a single-arg call on a pure, guard-free
receiver; the shim names the receiver twice, so an impure receiver such as
`makeBag().items.push(x)` would run its effects twice on Python and break JS
parity — those fall through unchanged.

Agon-planned (6-engine brainstorm, unanimous Approach C) and agon-reviewed
(codex 0.97 receiver double-evaluation closed via the isReceiverChainPure gate;
the method-name set is kept private behind a predicate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- class-conformance.mjs: differential fixture proving per-instance list
  isolation AND push return-parity — two Bags, append twice to one, the other's
  first push returns 1 (not 3). Kills shared-mutable-default, unlowered push
  (Python crash, ts != py), and push without JS-return parity (None != 1).
  Also refresh the stale "list mutation excluded" scope comment.
- class-python.test.ts: pure-receiver push lowers to the shared append+len
  shim; impure-receiver push does NOT lower (locks the codex double-eval fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…il-fast stubs

KERN classes can be `abstract` and `implements` an interface, but Python codegen
dropped both and TS only carried the keywords. Define KERN's semantics and emit
them with TS<->Python parity by construction (Approach B+, agon-chosen):

- `abstract` is ERASED at runtime on both targets (a plain, instantiable class) —
  matching how TS `abstract` is erased in compiled JS. A handler-less member
  under an abstract class lowers to a fail-fast body: `raise NotImplementedError`
  (Python) / `throw new Error` (TS), so an un-overridden abstract member fails
  identically on both. TS keeps the class-level `abstract` keyword (tsc still
  rejects `new X()`); Python emits a plain class (no ABC/metaclass). Covers
  instance methods, getters, setters, AND static accessors (the metaclass path).
- `implements` is erased on Python (the semantic validator owns conformance) and
  left as a `# implements: Y` marker comment; the single-inheritance base is
  unchanged. TS keeps its existing `implements` emission.

Abstract enforcement (reject `new Abstract()`, require concrete overrides) lives
in KERN's validator, not the host type-checker — a deferred follow-up.

Agon-planned (6-engine brainstorm, Approach B+) and agon-reviewed (codex 0.99:
abstract static accessors must raise too, now fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- class-conformance.mjs (now 12/12): abstract polymorphic dispatch (Shape/Square),
  abstract template method reading an inherited field default (Formatter), and an
  abstract static accessor override dispatched through a chained metaclass.
- class-python.test.ts: abstract instance method + abstract static accessor each
  emit the fail-fast `raise` (not a silent body); `implements` is erased to a
  marker comment with no runtime base.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or semantics)

A derived class whose explicit constructor touches `this`/`self` without calling
super() crashed on TS ("must call super constructor before accessing this") but
worked on Python — the JS "super-before-this" rule leaking into KERN as a parity
break. KERN now OWNS the rule: a derived constructor that omits super() gets an
implicit no-arg super() / super().__init__() injected as the FIRST statement on
BOTH targets (Java/Python ergonomics), threaded so field defaults and the body
still run after it. The author writes explicit super(args) only to pass args up.

Agon-planned (6-engine brainstorm, unanimous Option C). This is C's codegen arm
(implicit injection). C's safety arm — a validator diagnostic when the base
constructor REQUIRES args and a derived ctor omits super(args) — is a follow-up in
the consolidated validator-enforcement slice (with the deferred abstract
concrete-must-override check); until then that edge injects a no-arg super() that
fails identically on both targets (parity preserved, just without a friendly
compile-time error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…guards

- class-conformance.mjs (now 13/13): a derived class whose ctor omits super() and
  reads an inherited field default -> get() == 7 + 1 identically on both targets.
  Kills no-super-injected (TS crash), super-after-this (TS crash), base-init
  skipped (this.tag undefined -> NaN/AttributeError), field-defaults-before-super.
- class-python.test.ts: derived ctor injects super().__init__() before field
  defaults and body; a non-derived ctor gets none.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… runtime, codegen

PR4 Part 1 made codegen inject an implicit super() for derived constructors that
omit it, but the semantic validator and the in-process KERN runtime still enforced
the old "explicit super is mandatory" rule — so a program legal in generated
TS/Python was rejected by the validator and threw in the interpreter (a
target-vs-runtime split, flagged blocking by review).

This lands Option C as one coherent semantic across all four layers, behind a
single source of truth — a new canonical predicate `hasDirectSuperCtorCall(ctor)`
(packages/core/src/constructor-super.ts). It answers structurally, from the IR,
the one question every layer must answer identically: does the constructor make a
direct super(...) call (counting if-branches, not lambdas, not super.member)?
This replaces three divergent textual heuristics — the validator's own walk plus
the codegens' emitted-text scans (`/\bsuper\s*\(/` and `"super().__init__"`) that
5/6 reviewers flagged as false-matching `super(` inside strings/comments.

- Validator: a derived ctor with no direct super is now LEGAL (implicit base init);
  the only error is class-constructor-implicit-super-needs-args, raised when the
  base ctor requires arguments an arg-less implicit super can't supply. A direct
  super keeps the full discipline (double / conditional / this-before-super).
  class-constructor-missing-super is retired from the user path. The descriptive
  superStatus substrate fact is left untouched (still reports 'missing' etc.).
- Runtime: initializeClassLayer initializes the base FIRST, then derived field
  defaults, then the body for implicit-mode ctors (frame pre-marked superCalled so
  this/super access is unguarded; a stray late super still trips double-init).
- TS + Python codegen: the inject/splice decision now consumes the shared predicate
  instead of scanning emitted text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- constructor-super.test.ts: pins hasDirectSuperCtorCall on the canonical cases
  (omit, straight-line super, super-in-if, lambda-only, super.member, double) so
  all four layers can't drift from the single source of truth.
- class-semantics: flips the omitted-super and lambda-only cases to legal; the
  super.kind() (member, no ctor super) case drops to implicit-mode legal
  (this-before-super 2 -> 1); adds coverage for the new
  class-constructor-implicit-super-needs-args arity diagnostic and that an explicit
  super(args) satisfies an arg-requiring base.
- core-runtime: a lambda-only super now attempts implicit base init (fails an
  arg-requiring base with "missing required argument" — proving the lambda super
  isn't counted); adds the positive implicit-base-init case mirroring the
  class-conformance Box/Base fixture inside the interpreter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er arity check

Review (codex, confidence 1.0, verified repro) found baseConstructorRequiresArgs
only inspected the immediate base's own constructor. For `C extends B extends A`
where B has no constructor and A requires arguments, the validator accepted C's
omitted super() while the runtime threw `A.constructor missing required argument`
— re-opening the exact validator/runtime split this reconciliation set out to
close (initializeClassLayer forwards [] through constructor-less bases to A).

Walk up the inheritance chain through constructor-less bases to the first ancestor
that actually declares a constructor — the one implicit init reaches with no args —
and test ITS required params (with a cycle guard). This matches the runtime's
forwarding precisely. Adds regression coverage for the transitive arg-requiring
base (flagged) and the transitive no-arg base (accepted).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nostic

Re-review (codex, confidence 1.0) noted the transitive arity check rejected the
right constructor but the message still named the immediate base. For
`C extends B extends A` (B constructor-less, A requires args) it read "base class
'B' has a constructor that requires arguments" — but B has no constructor; A does.

argRequiringEffectiveBaseName now returns the name of the class whose constructor
is actually reached and requires args (undefined when implicit init succeeds), and
the diagnostic names that class. Test asserts the transitive message names 'A',
not 'B'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
origin/main carried #396 — a squash-merge of an EARLIER snapshot of this
branch's Python class-codegen work (#396 touched only package.json,
packages/python/*, and scripts/class-conformance.mjs). Because the squash
hid the shared lineage, git saw the branch's continued history as divergent
and flagged conflicts in the four Python-side files.

Resolution: take the branch side for all four. Verified each is the strict
evolution of #396's snapshot — origin/main contributed zero lines the branch
lacks except code the branch deliberately superseded:
  - codegen-body-python.ts / class-python.test.ts: 0 main-only lines (superset)
  - class-conformance.mjs: only a stale "needs list-append lowering" scope note
  - generators/data.ts: main's unique lines are the OLD superIdx-existence super
    decision + pre-hasDirectSuperCtorCall import that PR4 replaced with the
    canonical hasDirectSuperCtorCall(ctor) predicate + Option-C implicit-super
    injection + abstract-member raise stubs.

The full class-conformance + python + core gates re-run post-merge to confirm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KERN now owns its abstract contract at the VALIDATOR layer (codegen/runtime
stay the loud backstop), so TS and Python reject the same programs by
construction — the validator runs before codegen, making enforcement
parity-free. Closes the soundness/asymmetry gap where `new Abstract()` and
unimplemented abstract members only failed at runtime (and only tsc, not
Python, caught the direct instantiation).

Two rules:
  - class-abstract-instantiation: reject `new <AbstractClass>(...)` anywhere,
    including the abstract class's own static factory (matches TS). Module-wide
    pass over BODY_EXPRESSION_PROPS; resolves the constructor target by
    descending the postfix chain to its head ident (so `new Shape().area()`
    is attributed to Shape per JS `new` precedence) and skips qualified
    (`pkg.Shape`), non-ident, and unresolved callees conservatively.
  - class-abstract-member-unimplemented: a CONCRETE class must implement every
    inherited abstract member. Driven by a dedicated collectAbstractObligations
    lineage walker keyed by (static, name, kind) — NOT effectiveClassMemberFacts,
    which collapses members by name+static and would let a getter-only override
    silently erase the sibling abstract setter (or a same-name different-kind
    member erase an abstract method). Getter/setter are independent obligations;
    a same-name different-kind collision stays owned by class-member-conflict.

Multi-level abstract chains require the override only at the concrete leaf;
abstract subclasses may carry/inherit abstract members. Runtime `new Abstract()`
guard deferred (validator is the gate; the codegen raise/throw stub remains the
backstop) — panel-unanimous.

Design via 6-engine agon brainstorm; the effectiveClassMemberFacts soundness
hole was flagged independently by codex/kimi/minimax and verified in code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validator unit tests for both abstract rules:
  - instantiation: direct `new Abstract()` rejected; concrete subclass accepted;
    rejected inside the abstract class's own static factory; concrete/unresolved
    `new` not flagged.
  - concrete-must-override: missing override rejected (message names class +
    kind + member + declaring ancestor); full override accepted; abstract
    subclass may leave it unimplemented; multi-level chain requires it only at
    the concrete leaf (names the original abstract declarer).
  - getter/setter pair: overriding only the getter still flags the abstract
    setter (the soundness case); overriding both is accepted.
  - same-name different-kind (abstract method vs field) does not satisfy the
    method obligation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review (codex 0.98, kimi, agy 0.85 — 3-engine convergence) found the
class-abstract-instantiation pass scanned only BODY_EXPRESSION_PROPS, which
excludes `default`. So `new Abstract()` in a field `default=` or `param
default=` initializer was silently un-checked — a real soundness gap, since
field initializers treat `value` and `default` equivalently and both lower to
runtime code. Verified empirically: `field s default="new Shape()"` was MISSED
while `value={{ new Shape() }}` was caught.

Scan `default` too, local to this pass (INSTANTIATION_EXPRESSION_PROPS) so the
shared super-detection / shape-usage walks are unaffected. Regression test added.

Also documented the multi-root visibility-union behavior (codex 0.86): the pass
resolves classes across roots via the same union extends/implements already use;
all production callers validate a single root, so it is not a false-positive
surface in practice. The cycle-short-circuit (minimax 0.75) only affects programs
that already carry a class-inheritance-cycle diagnostic; deferred as the cycle is
the primary error. Remaining review items were nits or self-disproven.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The conformance-bad-cases suite asserted a `class-constructor-missing-super`
("constructor does not call super") violation, but Option-C implicit-super
(commits f0d9d1c/62855ebf) intentionally retired that rule: omitting
`super(...)` is now legal because KERN injects base init. `class-semantics`
unit tests already assert the rule is absent, so the conformance fixture was
stale and CI's `pnpm test:kern` failed on it.

Give `ProtocolBase` a required-arg constructor so `MissingSuper` (which omits
super) now trips the *replacement* detector
`class-constructor-implicit-super-needs-args`, and reword the assertion to
match. Preserves detector coverage; the derived classes that call `super('u1')`
explicitly are unaffected. Full test:kern green (233/233, coverage 100%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frozen oracle for the value→string coercion-parity slice. 14 differential
fixtures (compile to TS + Python, run both, assert ts==python==expected):
10 RED-at-base (Python diverges: bool→"True", null→"None", undefined→"None",
1.0→"1.0", arrays→"[1, 2, 3]", str+x→TypeError) force the implementation; 4
GREEN guards (2+3==5, 5/null/undefined ?? 9) must stay green so an over-eager
`+` coercion or a non-nullish `undefined` sentinel is caught. Expected values
are JS/TS truth → correct by construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KERN is one language emitted to BOTH TS and Python; coercion must be parity
by construction. JS coerces values to strings (true→"true", null→"null",
undefined→"undefined", 1.0→"1", [1,2,3]→"1,2,3", "a"+true→"atrue",
Infinity→"Infinity") in ways Python's str()/+ do not. This lowers those
semantics on the Python target (TS already IS JS).

Scope: native KERN handler bodies only, gated by `coerceJsValues` (default
true; helpers inject function-locally there). The helper-less Ground/React
declarative layer opts out (GROUND_EMIT) and keeps the pre-slice forms, so
its output is byte-identical — no regression, no NameError from undefined
helper references.

- helpers.ts: _kern_fmt extended (str/bool/null/int-float/array/dict/
  Infinity/NaN); _KERN_UNDEFINED sentinel with __bool__→False so JS
  undefined stays falsy (!undefined, ternary, ||, if); __kern_add for the
  JS `+` overload (string concat if string-ish incl tuple, else ToNumber
  add null→0/undefined→NaN/bool→0,1, try/except fallback for exotic hosts).
- codegen-body-python.ts: undefLit→sentinel; tmplLit interpolation wrapped
  in _kern_fmt; binary `+` → __kern_add / _kern_fmt; `??` excludes the
  sentinel; dynamic typeof reports "undefined" for a stored sentinel. All
  gated on coerceJsValues.
- ground.ts: 6 emitPyExpression calls opt out via GROUND_EMIT.
- coercion-conformance.mjs: 20 discriminating fixtures (ts==python==
  expected), incl Infinity/-Infinity via float overflow (KERN has no `e`
  notation).

Verified: oracle 20/20, class 13/13, expr 273/273, full monorepo test
green (# fail 0), lint clean.

Follow-ups (documented, out of slice scope): extend the fmt IR-semantics
contract to the coercion superset; sentinel JSON-serialization at the route
boundary; cross-function sentinel identity (function-local helper injection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cukas cukas merged commit 5a6c9af into main Jun 9, 2026
4 checks passed
@cukas cukas deleted the feat/kern-core-runtime branch June 9, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant