Skip to content

Fix compiler crash when a behavior satisfies a non-tag interface method#5191

Draft
SeanTAllen wants to merge 1 commit intomainfrom
fix-reach-be-nontag-interface-crash
Draft

Fix compiler crash when a behavior satisfies a non-tag interface method#5191
SeanTAllen wants to merge 1 commit intomainfrom
fix-reach-be-nontag-interface-crash

Conversation

@SeanTAllen
Copy link
Copy Markdown
Member

Draft for discussion. This fixes the compiler crash, but I'm not confident it's the best layer for the fix — opening to discuss the approach.

The crash. When an actor's be is used to satisfy an interface method with a non-tag receiver cap (fun box / fun ref), the reach pass asserts in genfun_forward. Minimal repro (no lambdas):

interface IFunBox
  fun box apply(s: String)

actor Main
  let _env: Env
  new create(env: Env) =>
    _env = env
    let x: IFunBox = this
    x("hello")

  be apply(s: String) => _env.out.print(s)

This is a valid subtype relationship — a be is fun tag, and box/ref are subcaps of tag, so the contravariant receiver check holds.

Root cause. add_rmethod creates the concrete method entry in n->r_methods keyed by the caller-supplied cap (e.g. box_apply). Later, reach_method looks up the same method but normalizes the cap to n->cap, which is tag for a behavior — so it searches for tag_apply and finds nothing. genfun_forward asserts on the NULL result.

The fix. Normalize the cap to tag in add_rmethod when the method is a behavior, so the concrete entry lands where later lookups expect it. The forwarding entry in n->r_mangled is independent and keeps its interface-cap mangled name, so forwarding stubs still work.

Why I'm not sure this is the right layer. The asymmetry between r_methods and r_mangled lookup is the real smell — this fix papers over it at the insertion site. The right fix might instead be in reach_method's lookup normalization, or somewhere earlier. Happy to pivot based on review.

Tests. Not added yet — I'd rather wait on approach confirmation before writing a test I'm going to throw away.

Relationship to #2922. This PR does not close #2922. The compiler crash was hiding the original 2018 runtime bug: {(s: String) => this(s)} compiles (after this fix) but then infinite-recurses at runtime because this inside the lambda refers to the lambda object itself, not the enclosing actor. That's a separate, deeper problem.

When an actor's `be` is used to satisfy an interface method declared with
a non-tag receiver cap (e.g. `fun box apply`), the reach pass was keying
the new method entry under the interface cap ("box_apply") in
`r_methods`, but later `reach_method` lookups normalize the cap to
`n->cap` — which is `tag` for a behavior — and search for "tag_apply",
finding nothing. `genfun_forward` then asserts on the NULL result.

Normalize to `tag` inside `add_rmethod` for behaviors so the concrete
entry ends up keyed where later lookups expect it. The forwarding entry
in `r_mangled` is independent and keeps its own interface-cap mangled
name.
@SeanTAllen SeanTAllen added the changelog - fixed Automatically add "Fixed" CHANGELOG entry on merge label Apr 8, 2026
@ponylang-main ponylang-main added the discuss during sync Should be discussed during an upcoming sync label Apr 8, 2026
@SeanTAllen
Copy link
Copy Markdown
Member Author

@jemc this PR demonstrates "a solution" to the problem. thoughts on how to address?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog - fixed Automatically add "Fixed" CHANGELOG entry on merge discuss during sync Should be discussed during an upcoming sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid call to this behaviour/function in lambda

2 participants