Skip to content
Open
59 changes: 59 additions & 0 deletions src/passes/OptimizeInstructions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,65 @@ struct OptimizeInstructions
}
}

void visitResume(Resume* curr) {
skipNonNullCast(curr->cont, curr);
if (trapOnNull(curr, curr->cont)) {
return;
}
if (curr->type == Type::unreachable) {
return;
}

// If this resume operates on a freshly-created continuation of an exact
// function that is known never to suspend, we can turn the resumption into
// a direct call, avoiding the continuation allocation and handler overhead.

// Continuations are single-shot, so resuming a continuation that has
// already been consumed will trap. If traps are assumed never to happen, we
// can assume this continuation was not consumed on another path and look
// through tees and conditional branches. Otherwise, avoid looking through
// them to ensure the continuation cannot be consumed elsewhere.
auto behavior = getPassOptions().trapsNeverHappen
? Properties::FallthroughBehavior::AllowTeeBrIf
: Properties::FallthroughBehavior::NoTeeBrIf;

auto* contExpr = Properties::getFallthrough(
curr->cont, getPassOptions(), *getModule(), behavior);
auto* contNew = contExpr->dynCast<ContNew>();
if (!contNew) {
return;
}
if (contNew->func->type == Type::unreachable) {
return;
}

auto* funcExpr = Properties::getFallthrough(
contNew->func, getPassOptions(), *getModule(), behavior);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
contNew->func, getPassOptions(), *getModule(), behavior);
contNew->func, getPassOptions(), *getModule());

The fallthrough of the function doesn't need to care about other uses and traps and such.

auto* refFunc = funcExpr->dynCast<RefFunc>();
if (!refFunc) {
return;
}

auto* target = getModule()->getFunctionOrNull(refFunc->func);
if (!target || target->imported()) {
return;
}

if (!target->effects || target->effects->suspends) {
return;
}

auto* block =
ChildLocalizer(curr, getFunction(), *getModule(), getPassOptions())
.getChildrenReplacement();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize that this is almost but not quite right. ChildLocalizer considers child effects wrt each other. After the operation, it is safe to reorder and remove them. But moving them across other effects might not be safe.

Unless we know no other effects can be in the middle, here? But it seems like there can be:

(resume
  (OPERANDS)
  (block
    (STUFF A)
    (cont.new
      (block
        (STUFF B)
        (ref.func)
      )
    )
  )
)

=>

(STUFF A)
(STUFF B)
(call
  (OPERANDS)
)

OPERANDS is moved past the STUFFs.

This is simple to handle, though: add a flag to ChildLocalizer to control this behavior. Where it computes

      // Use a local if we need to. That is the case either if this has side
      // effects we can't remove, or if it interacts with other children.

then, we can have a mode where it uses a local for any child with effects, even removable ones.

Builder builder(*getModule());
Type results = target->getResults();
block->list.push_back(
builder.makeCall(target->name, curr->operands, results));
block->type = results;
replaceCurrent(block);
}

// Note on removing casts (which the following utilities, skipNonNullCast and
// skipCast do): removing a cast is potentially dangerous, as it removes
// information from the IR. For example:
Expand Down
Loading
Loading