Skip to content

perf(ir_passes): inline small statically bound instance methods - #2045

Merged
nahime0 merged 24 commits into
mainfrom
perf/inline-small-methods
Oct 25, 2026
Merged

nahime0 merged 24 commits into
mainfrom
perf/inline-small-methods

Conversation

@nahime0

@nahime0 nahime0 commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Small statically bound instance methods now participate in the EIR inliner. Calls to eligible accessors and predicates are replaced by their bodies, preserving receiver checks, private-property resolution, argument behavior and ownership.

  • Resolve plain non-nullable user-class receivers when the class or method is final, or the method has no vtable slot. Instantiated generic receivers with a slot keep real calls because variance can change the actual instantiation, including final classes and methods.
  • Preserve each transplanted instruction's lexical class in Function::inlined_scopes; scope-sensitive codegen and body copying consume that metadata. Buffer property reuse compares the original instruction scope entries before merging reads, keeping shadowed parent and child private buffers distinct.
  • Borrow $this through an epilogue-excluded temporary and hide transplanted locals from the caller's PHP variable scope. Keep calls whose signatures, frame-dependent operations, backtrace visibility or ownership cannot be reproduced safely.

This branch includes the exact #2044 head d256479dfdb749b06b41774d437bfb3658583ca3 through merge 037b8538b6457b1d91804ece0e76059c08664661. The integration resolves CJ task f3f64c3d, while the original method-inlining work addresses CJ task 5ea2b764. Wait for #2044 to merge before adding this PR to the merge queue. The generic eligibility guard matches the narrow guard in #2070; its additional opcode work is separate.

Regression coverage includes EIR shape on all five supported targets, receiver/null and visibility checks, private-property scopes, generic retained calls, backtraces, defaults, destructor timing and runtime behavior with IR optimization on and off. Original-ID scope matrices cover mismatched scopes and both Some/None directions, plus positive record/descriptor reuse for equal scopes. The natural shadowed-buffer fixture verifies accessor splicing, three distinct property reads and output 273 in both optimizer modes; borrowed receiver temporaries intentionally keep those reads outside the method-local reuse proof. Examples and compiler internals documentation are updated.

The full combined diff from f708b38dfae0cc1cd34308d023b487a690ed9753 received a clean, read-only Claude review on exact head 54cc37bce4fb99f8fab830097a00cdc26aa45084, delegation c8634081-0672-41e9-a2b9-c7d03b6e7780. This includes the loop fixture's missing second_property initializer, which CI exposed on the previous head. Both helper constructors now record the original second property value. git diff --check is clean. No local builds or tests were run, as requested. Fresh CI and Greptile must pass on this head; the prior b0c60a296 review and CI results do not validate this correction.

Codegen and the inliner must agree on when PHP frames are observable, so the
predicate moves from codegen::frame to Module::uses_php_backtrace and
Function::keeps_backtrace_argument_snapshot; frame.rs delegates to it.

Function also gains inlined_scopes, the lexical class of instructions the
inliner splices in from a method body.
FunctionContext::lexical_class answers the scope the current instruction runs
in: the class recorded in Function::inlined_scopes for a spliced method
instruction, the function's own lexical_class otherwise. Every codegen reader
of the frame's lexical class goes through it, and tail-call body cloning carries
the recorded scopes along.
A method_call whose receiver class is final, whose method is final, or whose
class has no vtable slot for it now reaches the small-function inliner. The
splice binds $this as a borrowed argument, keeps the receiver null check
(throw_error with the call's own message), renames the method's locals and
records the method's class scope for each spliced instruction.

Method bodies are limited to frame-independent ops (property reads on $this or
packed records, locals, scalar arithmetic, buffer reads); signatures with
literal false/null, by-reference, variadic or surplus-argument parameters keep
their call, and so does every method call in a module that can observe
backtraces. The recursion analysis now spans functions and methods.
@github-actions github-actions Bot added area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. area:optimizer Touches AST or EIR optimization passes. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:m Medium-sized pull request. type:triage Needs a conventional title or branch prefix before review. labels Oct 24, 2026
…sted

Module::uses_php_backtrace also counts frames that merely collect surplus
arguments, and the core prelude's ErrorException::__construct always does, so
every module that keeps ErrorException disabled method inlining. The inliner
now asks Module::calls_php_backtrace (direct debug_backtrace calls or the eval
bridge); a backtrace reachable any other way gives every frame an argument
snapshot, which already keeps that method body out of the candidates.
Buffers have no automatic cleanup, so the heap-validated example test saw the
table's slot buffer still live at exit in both optimizer modes.
@nahime0
nahime0 marked this pull request as ready for review October 24, 2026 08:23
@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The latest update appears safe to merge, subject to the required CI checks.

Summary

The PR inlines small instance methods when their target is known and preserves each copied instruction's class scope.

  • Eligible instance-method calls now run as part of their callers.
  • Repeated buffer-property reads now share work when the code between them leaves it unchanged.
  • Loop buffer resolution now runs before later invariant loads in the preheader.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Method call] --> B{Safe to inline?}
    B -->|No| C[Keep call]
    B -->|Yes| D[Bind arguments and check receiver]
    D --> E[Copy body with original class scope]
    E --> F[Optimize copied instructions]
Loading

Reviews (4) · Last reviewed commit: "fix(tests): initialize the loop fixture ..." · Reviewed by Greptile

Comment thread src/ir_passes/tests/method_inline_test.rs
…class call

A final class that inherits an accessor binds the parent's body; the spliced
read of the parent's private property must keep the parent's scope although
the subclass shadows the name. Assert the call disappears, the recorded scopes
are Base and Leaf, and both optimizer modes print PHP's 7099.
@github-actions github-actions Bot added size:l Large pull request. and removed size:m Medium-sized pull request. labels Oct 24, 2026
@nahime0
nahime0 added this pull request to the merge queue Oct 25, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Oct 25, 2026
@nahime0
nahime0 added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit cecc91d Oct 25, 2026
148 checks passed
@nahime0
nahime0 deleted the perf/inline-small-methods branch October 25, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. area:optimizer Touches AST or EIR optimization passes. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:l Large pull request. type:triage Needs a conventional title or branch prefix before review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant