facebook/redex

IRTypeCheck reports error for inlined constructor

shiqicao opened this issue · 6 comments

MethodInlinePass inlines init for new-instance, so it moves invoke-direct {p0}, Ljava/lang/Object;-><init>()V to caller. However, IRTypeChecker reports

 Variable NEW_INSTANCE Lcom/foo/bar/;initialized with the
wrong type at [0x7f71d521be90] OPCODE: INVOKE_DIRECT v5, Ljava/lang/Object;.<init>:()V in

It seems inliner conflicts IRTypeChecker.

That of course should not happen.

The inliner will indeed inline certain calls, but only when it safe to do so. It performs the checks here https://github.com/facebook/redex/blob/main/service/method-inliner/Inliner.cpp#L403 and here https://github.com/facebook/redex/blob/main/service/method-inliner/Inliner.cpp#L1597

More information about how to reproduce the issue, or the circumstances (is the caller a constructor? Does it contain other related new-instance instructions? is the offending instruction reachable in the CFG?), would be needed...

Inlinee

.class public final Lcom/Bar;
.super ...;
    .method public constructor <init>()V
        .locals 0

        invoke-direct {p0}, Ljava/lang/Object;-><init>()V

        return-void
    .end method
.end class

Caller(before MethodInlinePass)

.method public static bar ()V;
    .locals 3

    new-instance v2, Lcom/Bar;

    invoke-direct {v2}, Lcom/Bar;-><init>()V

    ...
.end method

Caller(after MethodInlinePass)

.method public static bar ()V;
    .locals 3

    new-instance v2, Lcom/Bar;

    invoke-direct {v2}, Ljava/lang/Object;-><init>()V

    ...
.end method

Is the following logic(from inline_callees correct? Should !can_inline_init(...) be can_inline_init(...)?

            if (!can_inline_init(callee)) {
              if (!method::is_init(caller) ||
                  caller->get_class() != callee->get_class() ||
                  !caller->get_code()->editable_cfg_built() ||
                  !constructor_analysis::can_inline_inits_in_same_class(
                      caller, callee, insn)) {
                return editable_cfg_adapter::LOOP_CONTINUE;
              }
            }

Should the above be the following?

            if (can_inline_init(callee)) {
              if (!method::is_init(caller) ||
                  caller->get_class() != callee->get_class() ||
                  !caller->get_code()->editable_cfg_built() ||
                  !constructor_analysis::can_inline_inits_in_same_class(
                      caller, callee, insn)) {
                return editable_cfg_adapter::LOOP_CONTINUE;
              }
            } else {
               return editable_cfg_adapter::LOOP_CONTINUE;
            }

"can_inline_init" isn't the best name for what it checks; more fitting might be "can_always_inline_init". The comment here explains what it checks:

Even when an init cannot be inlined in all possible contexts, then there's still a chance that it might be inlinable in a more specific case, namely into another constructor of the same class. That's what's being checked if the more general "can_inline(_always)init" doesn't succeed.

For your example, both kinds of checks should fail (and the inlining shouldn't happen).

This was recently overhauled in Redex, adding support for relaxed constructor inlining. Feel free to re-open if still is still causing issues.