Conversation
…null assertions In traps-never-happen mode, values flowing into typed slots (call / call_ref / call_indirect arguments, returned values, stored locals/globals, struct and array field/element writes, new allocations, select arms) and the inputs of ref.test often contain downcasts or non-null assertions where the operand already statically satisfies the slot type. Such casts can narrow the value beyond the slot type, and that narrower type is information that refining passes (GUFA, signature-refining, type-refining, local-subtyping, cfp-reftest, ...) can use to refine the slot itself or to resolve a ref.test. Instead of removing them in OptimizeInstructions, which runs throughout the pipeline, add a standalone StripRefiningCasts pass (--strip-refining-casts) meant to run late in the pipeline: - Removes ref.cast and ref.as_non_null on call / call_ref / call_indirect parameters, function results, locals (non-tee), globals, struct.new / struct.set fields, array.new / array.new_fixed / array.set elements, and select arms when the uncast value is already a subtype of the slot type. - Removes ref.cast and ref.as_non_null on ref.test inputs when the outcome of the test is not determined by the input's type (otherwise leaving the test for OptimizeInstructions to resolve). - Preserves descriptor casts and casts on arguments to call.without.effects (which must match the parameters of the actual target rather than those of the intrinsic import).
| } | ||
|
|
||
| void skipCastsOnOperands(ExpressionList& operands, Type params) { | ||
| if (params.size() != operands.size()) { |
There was a problem hiding this comment.
Yeah, I think this can be an assertion.
| } | ||
|
|
||
| void visitGlobalSet(GlobalSet* curr) { | ||
| skipCasts(curr->value, getModule()->getGlobal(curr->name)->type); |
There was a problem hiding this comment.
It looks like these duplicate logic from src/ir/subtype-exprs.h, e.g.
void visitGlobalSet(GlobalSet* curr) {
self()->noteSubtype(curr->value,
self()->getModule()->getGlobal(curr->name)->type);
}Can we instead use that code, implementing a noteSubtype callback that gives us the type of the slot, which is what we need?
tlively
left a comment
There was a problem hiding this comment.
The suggested improvements can just be TODO comments.
Running scripts/experimental/coverage-diff.py shows some missing edge cases in the tests that would be worth fixing.
| // Removing a descriptor cast would also remove the descriptor operand, | ||
| // along with its side effects. |
There was a problem hiding this comment.
This can be worked around by using ChildLocalizer::getChildrenReplacement to get a Block containing the children, then appending cast->ref to the end of the block (where ChildLocalizer will have updated cast->ref to be a LocalGet of a scratch local if necessary). Then the cast can be replaced with the Block.
|
|
||
| // Skips casts on |input| while the uncast value is a subtype of |slotType|. | ||
| void skipCasts(Expression*& input, Type slotType) { | ||
| while (1) { |
There was a problem hiding this comment.
This loop could also look through "fallthrough expressions" like blocks, loops, br_if, local.tee, etc. using Properties::getImmediateFallthroughPtr.
| } | ||
| } | ||
|
|
||
| void visitSelect(Select* curr) { |
There was a problem hiding this comment.
We could even have skipCasts look through both sides of a select (or If, or Try-Catch) and remove casts on all of the joined arms.
| } | ||
|
|
||
| void skipCastsOnOperands(ExpressionList& operands, Type params) { | ||
| if (params.size() != operands.size()) { |
There was a problem hiding this comment.
Yeah, I think this can be an assertion.
| } | ||
|
|
||
| void visitCallRef(CallRef* curr) { | ||
| if (curr->target->type.isSignature()) { |
There was a problem hiding this comment.
It doesn't look like this is covered by the tests.
| } | ||
|
|
||
| void visitCallIndirect(CallIndirect* curr) { | ||
| if (curr->heapType.isSignature()) { |
There was a problem hiding this comment.
I think this can also be an assertion.
kripken
left a comment
There was a problem hiding this comment.
Nice!
lgtn with some last comments. Also
- Please add this pass to
scripts/fuzz_opt.pyaround line 2760, so it gets fuzzed.
| for (auto& [slot, type] : requiredTypes) { | ||
| skipCasts(*slot, type); | ||
| } | ||
| requiredTypes.clear(); |
There was a problem hiding this comment.
No need for manual cleanup in a function-parallel pass.
| requiredTypes.clear(); |
|
Really cool how SubtypingDiscoverer gives us all the core logic here! |
| (local.get $ns) | ||
| ) | ||
| ) | ||
| ;; A local.tee's type is the type of the local, so its cast is also removed. |
There was a problem hiding this comment.
This is not true in Binaryen IR. (We fix things up in the binary writer in cases where it could cause problems.)
There was a problem hiding this comment.
Just double checking before taking action since agent claims otherwise and I don't know any better:
In Binaryen IR, local.tee actually does have the type of the local (FunctionValidator::visitLocalSet enforces curr->type == getFunction()->getLocalType(curr->index)). I think you might be thinking of br_if, which has the type of its value operand in Binaryen IR and gets fixed up in BinaryInstWriter::visitBreak.
There was a problem hiding this comment.
This is something we went back and forth on iirc, so maybe it is confusing, but we use the wasm spec type here. So the tee type is the type of the local (which is not ideal).
Code:
binaryen/src/wasm/wasm-ir-builder.cpp
Line 1603 in f3ab996
There was a problem hiding this comment.
Oh yes, I was confused. It's a little weird that we follow the spec for tee but not br_if.
| ;; while br_if keeps its cast because the br_if's own result type is the | ||
| ;; cast type. |
There was a problem hiding this comment.
The result for br_if could be improved if we optimized through fallthrough expressions.
…null assertions
In traps-never-happen mode, values flowing into typed slots (locals, globals, call parameters, function results, struct fields, array elements, control-flow branches and fallthroughs, etc.) and the inputs of
ref.testoften contain downcasts or non-null assertions where the operand already statically satisfies the required type.Such casts can narrow the value beyond the slot type, and that narrower type is information that refining passes (GUFA, signature-refining, type-refining, local-subtyping, cfp-reftest, ...) can use to refine the slot itself or to resolve a
ref.test. Instead of removing them inOptimizeInstructions, which runs throughout the pipeline, add a standaloneStripRefiningCastspass (--strip-refining-casts) meant to run late in the pipeline:SubtypingDiscovererto find all value-flowing expression slots and removesref.castandref.as_non_nullwhen the uncast value is already a subtype of all types required of that slot.ref.castandref.as_non_nullonref.testinputs when the outcome of the test is not determined by the input's type (otherwise leaving the test forOptimizeInstructionsto resolve).call.without.effects(which must match the parameters of the actual target rather than those of the intrinsic import).