Skip to content

Do not CSE dynamic dispatch calls with a non-CSE-able callee#1271

Open
mbouaziz wants to merge 1 commit into
mainfrom
fix-dispatch-cse-debug
Open

Do not CSE dynamic dispatch calls with a non-CSE-able callee#1271
mbouaziz wants to merge 1 commit into
mainfrom
fix-dispatch-cse-debug

Conversation

@mbouaziz

Copy link
Copy Markdown
Contributor

Summary

A dynamic dispatch call is no longer CSE'd when any of its possible callees cannot CSE (@debug or untracked).

canCSECall's method branch used a deliberately lax per-callsite rule — "if any implementation can CSE we allow CSE" — so two identical virtual calls were merged even when the receiver could dispatch to a @debug implementation, silently deleting its second execution:

base class B {
  overridable fun m(): Int {
    0
  }
}

class K() extends B {
  @debug
  fun m(): Int {
    print_raw("k");
    0
  }
}

class L() extends B {}

fun twice(b: B): Int {
  b.m() + b.m()
}

fun main(): void {
  _ = twice(K());
  _ = twice(L());
  print_raw("\n")
}

printed k instead of kk (at -O0 --no-inline as well as -O2) — the CSE-able sibling implementation (L inheriting B's tracked m) reaching the same callsite is what flipped the old rule to "allow".

The fix inverts the rule: allow CSE only when every implementation the call might invoke passes funCanCSE. The allMethodImplementations iteration stops at the first implementation that cannot CSE, and the scan only runs for calls that already passed the deep-frozen gates, so the cost is bounded by the concrete subtypes of the receiver's static type at eligible callsites.

Fixes #1270.

Test plan

  • New Runtime/DebugDispatchCse test pins the repro (expects kk)
  • Control re-verified: single-implementation callsites and @debug-only callsites behave as before
  • Full compiler test suite passes locally
  • Compile-time impact measured (prelude release build with fixed vs unfixed compiler)
  • CI passes

🤖 Generated with Claude Code

canCSECall's method branch used a deliberately lax per-callsite rule:
CSE was allowed if any of the possible implementations could CSE. Two
identical virtual calls were therefore merged even when the receiver
could dispatch to a @debug (or untracked) implementation, silently
deleting its second execution. A @debug implementation sharing a
callsite with any CSE-able sibling implementation was enough to
trigger it, at -O0 as well as -O2.

Invert the rule: allow CSE only when every implementation the call
might invoke passes funCanCSE. The allMethodImplementations iteration
stops at the first implementation that cannot CSE, and the scan only
runs for calls that already passed the deep-frozen gates, so there is
no measurable compile-time impact (prelude release build: 17.7s before,
17.3s after).

Fixes #1270

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mbouaziz mbouaziz requested a review from jberdine June 12, 2026 11:10
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.

Dynamic dispatch calls are CSE'd even when a possible callee is @debug, deleting its side effects

1 participant