Skip to content

fix(hir+runtime): Constructor.prototype method dispatch on instances#979

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a19e139f1cbcec083
May 18, 2026
Merged

fix(hir+runtime): Constructor.prototype method dispatch on instances#979
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a19e139f1cbcec083

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • HIR assignment recogniser now accepts computed string-literal keys (Foo.prototype['method'] = fn), not just Foo.prototype.method = fn. Lights up ramda's transducer pattern (XWrap.prototype['@@transducer/step'] = fn) and any pre-ES6 lib that uses keys with dashes/slashes/special chars.
  • Adds read-side support via new HIR variant Expr::GetFunctionPrototypeMethod and runtime helper js_get_function_prototype_method. (Foo.prototype as any).method reads on the function-classic shape now resolve to the registered closure, so the spec idiom typeof Foo.prototype.method === 'function' returns 'function'.
  • Test test-files/test_constructor_prototype_method.ts matches Node byte-for-byte (5/10/function/15).

Background

Perry's existing CLASS_PROTOTYPE_METHODS side-table already powers instance-method dispatch ((new Foo()).method() reaches the registered closure with this bound to the receiver — issue #838 + followups). Two narrow gaps remained:

  1. Assignment side missed computed string-literal keys. The recogniser in lower/expr_assign.rs only matched MemberProp::Ident. ramda's _xwrap.js (and any TypeScript-subset code that uses keys like @@transducer/step) routes through MemberProp::Computed, so the assignment fell through to a generic PropertySet on an unobserved proxy and the instance dispatch found nothing. Fix mirrors the same key-resolution shape lower/expr_object.rs already uses for object-literal computed keys.

  2. Read side was always undefined. Even after a successful Ident-prop registration, (Foo.prototype as any).method evaluated to undefined because no synthetic prototype object was ever materialised. typeof Foo.prototype.method came back 'undefined' despite instances dispatching correctly. New recogniser in lower/expr_member.rs emits Expr::GetFunctionPrototypeMethod, which codegen lowers to a direct lookup into the side-table via the new runtime helper. Skips class C {} blocks (those have a real proto object) and named native imports (module-managed).

Known next blocker

import * as R from 'ramda' (or any named import of a ramda export) still throws TypeError: value is not a function at module init, before any user code runs. Every individual ramda module (import sum from 'ramda/src/sum.js') compiles and runs cleanly — verified by bisecting all 269 entries. So the failure is a cross-module init-time interaction, separate from the prototype-method shape addressed here.

Test plan

  • test-files/test_constructor_prototype_method.ts matches Node line-for-line: 5 / 10 / function / 15
  • Standalone IIFE XWrap mirror returns 15 from new XWrap(add)['@@transducer/step'](10, 5)
  • Direct module path import sum from 'ramda/src/sum.js'; sum([1,2,3,4,5]) returns 15
  • Full import * as R from 'ramda' — known follow-up, unrelated init-time issue

Ramda's transducer scaffolding (`XWrap.prototype['@@transducer/step'] =
fn`) — and any pre-ES6 library that attaches instance methods through a
computed string-literal key — silently fell through to a generic
PropertySet because the assignment-side recogniser only matched the
`MemberProp::Ident` shape. The dispatch hot path was capable of finding
the method via `CLASS_PROTOTYPE_METHODS`; the side-table just never
got populated.

Two coordinated arms:

1. HIR assignment recogniser (`lower/expr_assign.rs`) — extend the
   `<funcDecl>.prototype.<key>` matcher to accept
   `MemberProp::Computed(Lit::Str)` alongside the existing
   `MemberProp::Ident`. Static string-literal keys only; dynamic
   computed keys still fall through to PropertySet.

2. HIR read recogniser (`lower/expr_member.rs`) + new HIR variant
   `Expr::GetFunctionPrototypeMethod` + runtime helper
   `js_get_function_prototype_method`. `(Foo.prototype as any).method`
   reads on a function-classic shape now resolve to the registered
   closure (or `undefined` if none registered), so the spec idiom
   `typeof Foo.prototype.method === 'function'` returns 'function'.

Verified: `test-files/test_constructor_prototype_method.ts` matches
`node --experimental-strip-types` line-for-line (5/10/function/15).
A standalone IIFE XWrap mirror also passes. Individual ramda module
imports work; full `import * as R from 'ramda'` still hits a separate
cross-module init issue (documented as next blocker in CHANGELOG).
@proggeramlug proggeramlug merged commit 7a3f715 into main May 18, 2026
5 of 9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-a19e139f1cbcec083 branch May 18, 2026 02:32
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