From 3e1022e64d61ffcb7e6e8702c2d2f19fa745e7c5 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 22 Oct 2026 18:27:01 -0700 Subject: [PATCH 1/8] [OptimizeInstructions] Remove redundant sign-exts Relax isSignExted to check signExtBits <= bits, teach getSignExtBits about signed narrow loads and i64.extend_i32_s, remove unary sign-extensions when maxBits < bits, strip wider inner sign-extensions inside narrower outer ones, and fold i64.extend32_s(i64.extend_i32_u(x)) to i64.extend_i32_s(x). --- src/passes/OptimizeInstructions.cpp | 100 ++++++++-- .../lit/passes/optimize-instructions-mvp.wast | 177 ++++++++++++++++++ .../optimize-instructions-sign_ext.wast | 164 +++++++++++++++- 3 files changed, 419 insertions(+), 22 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index a5f702d1d45..961807f1a6b 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -84,6 +84,30 @@ struct LocalInfo { Index maxBits = -1; Index signExtBits = 0; + + // Returns the number of bits an expression is sign-extended (or 0 if it is + // not), ignoring local.get lookups. + static Index getExpressionSignExtBits(Expression* curr) { + Index bits = 0; + if (auto* unary = curr->dynCast(); + unary && unary->op == ExtendSInt32) { + // i64.extend_i32_s sign-extends from 32 bits, or from a narrower width if + // its i32 input is itself sign-extended (since any i32 sign extension is + // narrower than 32 bits and extending to i64 replicates the same sign + // bit). + bits = 32; + curr = unary->value; + } + if (Properties::getSignExtValue(curr)) { + return Properties::getSignExtBits(curr); + } + if (auto* load = curr->dynCast()) { + if (LoadUtils::isSignRelevant(load) && load->signed_) { + return load->bytes * 8; + } + } + return bits; + } }; struct LocalScanner : PostWalker { @@ -132,13 +156,9 @@ struct LocalScanner : PostWalker { auto& info = localInfo[curr->index]; info.maxBits = std::max(info.maxBits, Bits::getMaxBits(value, this)); info.maxBits = std::min(info.maxBits, getBitsForType(type)); - auto signExtBits = LocalInfo::kUnknown; - if (Properties::getSignExtValue(value)) { - signExtBits = Properties::getSignExtBits(value); - } else if (auto* load = value->dynCast()) { - if (LoadUtils::isSignRelevant(load) && load->signed_) { - signExtBits = load->bytes * 8; - } + auto signExtBits = LocalInfo::getExpressionSignExtBits(value); + if (signExtBits == 0) { + signExtBits = LocalInfo::kUnknown; } if (info.signExtBits == 0) { info.signExtBits = signExtBits; // first info we see @@ -615,10 +635,22 @@ struct OptimizeInstructions if (isSignExted(ext, bits) && extraLeftShifts == 0) { return replaceCurrent(removeAlmostSignExt(curr)); } + // A narrower outer shift sign-extension can strip a wider inner one. + if (auto* innerExt = Properties::getSignExtValue(ext)) { + if (bits - extraLeftShifts <= Properties::getSignExtBits(ext)) { + curr->left->cast()->left = innerExt; + return replaceCurrent(curr); + } + } } else if (curr->op == EqInt32 || curr->op == NeInt32 || curr->op == EqInt64 || curr->op == NeInt64) { if (auto* c = curr->right->dynCast()) { if (auto* ext = Properties::getSignExtValue(curr->left)) { + // TODO: Reexamine whether replacing unary sign-extend instructions + // (as opposed to shift pairs) with zero-extend masks here is + // worthwhile, since an and with a constant is larger than a unary + // extend. + // // We are comparing a sign extend to a constant, which means we can // use a cheaper zero-extend in some cases. That is, // (x << S) >> S ==/!= C => x & T ==/!= C @@ -674,6 +706,10 @@ struct OptimizeInstructions if (Properties::getSignExtBits(curr->right) == bits) { // we are comparing two sign-exts with the same bits, so we may as // well replace both with cheaper zexts + // TODO: Reexamine whether replacing unary sign-extend instructions + // (as opposed to shift pairs) with zero-extend masks here is + // worthwhile, since an and with a constant is larger than a unary + // extend. curr->left = makeZeroExt(left, bits); curr->right = makeZeroExt(right, bits); return replaceCurrent(curr); @@ -1045,19 +1081,35 @@ struct OptimizeInstructions } if (auto* ext = Properties::getSignExtValue(curr)) { + using namespace Match; auto bits = Properties::getSignExtBits(curr); if (trySignExtLoad(ext, bits)) { return replaceCurrent(ext); } - // Simple sign extends can be removed if the value is already - // sign-extended. Note that we can handle the case of |curr| having a - // larger sign-extend: if we have an 8-bit value in 32-bit, then there are - // 24 sign bits, and doing a sign-extend to 16 will only affect 16 of - // those 24, and the effect is to leave them as they are. - auto signExtBits = getSignExtBits(ext); - if (signExtBits > 0 && signExtBits <= bits) { + // Simple sign extends can be removed if the input cannot have a sign bit + // or is already sign-extended. Note that we can handle the case of |curr| + // having a larger sign-extend: if we have an 8-bit value in 32-bit, then + // there are 24 sign bits, and doing a sign-extend to 16 will only affect + // 16 of those 24, and the effect is to leave them as they are. + if (Bits::getMaxBits(ext, this) < bits || isSignExted(ext, bits)) { return replaceCurrent(ext); } + // A narrower unary sign-extension can strip a wider inner one. Since + // isSignExted(ext, bits) was false above, any inner sign-extension is + // strictly wider than |bits|. + if (auto* innerExt = Properties::getSignExtValue(ext)) { + assert(Properties::getSignExtBits(ext) > bits); + curr->value = innerExt; + return replaceCurrent(curr); + } + // Sign-extending the low 32 bits of a zero-extended i32 is a signed i32 + // extension. + Unary* inner; + if (matches(curr, + unary(ExtendS32Int64, unary(&inner, ExtendUInt32, any())))) { + inner->op = ExtendSInt32; + return replaceCurrent(inner); + } } if (Abstract::hasAnyReinterpret(curr->op)) { @@ -1097,6 +1149,9 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(curr->value)) { // we are comparing a sign extend to a constant, which means we can // use a cheaper zext + // TODO: Reexamine whether replacing unary sign-extend instructions (as + // opposed to shift pairs) with zero-extend masks here is worthwhile, + // since an and with a constant is larger than a unary extend. auto bits = Properties::getSignExtBits(curr->value); curr->value = makeZeroExt(ext, bits); return replaceCurrent(curr); @@ -3448,6 +3503,10 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(binary)) { // use a cheaper zero-extent, we just care about the boolean value // anyhow + // TODO: Reexamine whether replacing unary sign-extend instructions (as + // opposed to shift pairs) with zero-extend masks in boolean contexts + // is worthwhile, since an and with a constant is larger than a unary + // extend. return makeZeroExt(ext, Properties::getSignExtBits(binary)); } } else if (auto* block = boolean->dynCast()) { @@ -4442,22 +4501,25 @@ struct OptimizeInstructions return false; } - // Check if an expression is already sign-extended to an exact number of bits. + // Check if an expression is already sign-extended to at most |bits| bits. bool isSignExted(Expression* curr, Index bits) { - return getSignExtBits(curr) == bits; + auto signExtBits = getSignExtBits(curr); + return signExtBits > 0 && signExtBits <= bits; } // Returns the number of bits an expression is sign-extended (or 0 if it is // not). Index getSignExtBits(Expression* curr) { - if (Properties::getSignExtValue(curr)) { - return Properties::getSignExtBits(curr); + if (auto* unary = curr->dynCast(); + unary && unary->op == ExtendSInt32) { + auto innerBits = getSignExtBits(unary->value); + return innerBits > 0 ? innerBits : 32; } if (auto* get = curr->dynCast()) { // Check what we know about the local. return localInfo[get->index].signExtBits; } - return 0; + return LocalInfo::getExpressionSignExtBits(curr); } // optimize trivial math operations, given that the right side of a binary diff --git a/test/lit/passes/optimize-instructions-mvp.wast b/test/lit/passes/optimize-instructions-mvp.wast index 6208f178215..0463a19f2b5 100644 --- a/test/lit/passes/optimize-instructions-mvp.wast +++ b/test/lit/passes/optimize-instructions-mvp.wast @@ -5999,6 +5999,183 @@ ) ) ) + + ;; CHECK: (func $redundant-shift-sign-ext (param $x32 i32) (param $x64 i64) (param $addr i32) + ;; CHECK-NEXT: (local $t64 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.load8_s + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $t64 + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $t64) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $redundant-shift-sign-ext (param $x32 i32) (param $x64 i64) (param $addr i32) + (local $t64 i64) + ;; Outer shift sign-extension is redundant when the inner value is already + ;; sign-extended to fewer or equal bits. + (drop + (i32.shr_s + (i32.shl + (i32.shr_s (i32.shl (local.get $x32) (i32.const 24)) (i32.const 24)) + (i32.const 16) + ) + (i32.const 16) + ) + ) + ;; Signed narrow load is already sign-extended to 8 bits. + (drop + (i32.shr_s + (i32.shl (i32.load8_s (local.get $addr)) (i32.const 16)) + (i32.const 16) + ) + ) + ;; i64.extend_i32_s is already sign-extended to 32 bits. + (drop + (i64.shr_s + (i64.shl (i64.extend_i32_s (local.get $x32)) (i64.const 32)) + (i64.const 32) + ) + ) + ;; i64.extend_i32_s of an 8-bit sign-extended i32 is sign-extended to 8 bits. + (drop + (i64.shr_s + (i64.shl + (i64.extend_i32_s + (i32.shr_s (i32.shl (local.get $x32) (i32.const 24)) (i32.const 24)) + ) + (i64.const 48) + ) + (i64.const 48) + ) + ) + ;; Local initialized with i64.extend_i32_s is known to be 32-bit sign-extended. + (local.set $t64 (i64.extend_i32_s (local.get $x32))) + (drop + (i64.shr_s + (i64.shl (local.get $t64) (i64.const 32)) + (i64.const 32) + ) + ) + ;; Negative case: full-width load is not sign-extended to 8 bits. + (drop + (i32.shr_s + (i32.shl (i32.load (local.get $addr)) (i32.const 24)) + (i32.const 24) + ) + ) + ) + + ;; CHECK: (func $nested-shift-sign-ext (param $x32 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: (i32.const 26) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 24) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 16) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $nested-shift-sign-ext (param $x32 i32) + ;; Narrower outer shift sign-extension strips wider inner shift sign-extension. + (drop + (i32.shr_s + (i32.shl + (i32.shr_s (i32.shl (local.get $x32) (i32.const 16)) (i32.const 16)) + (i32.const 24) + ) + (i32.const 24) + ) + ) + ;; Also strips when the outer shift is an almost-sign-ext with extra left shifts + ;; that still only read bits inside the inner sign-extended width. + (drop + (i32.shr_s + (i32.shl + (i32.shr_s (i32.shl (local.get $x32) (i32.const 24)) (i32.const 24)) + (i32.const 26) + ) + (i32.const 24) + ) + ) + ;; Negative case: extra left shifts keep sign-extended bits from the inner shift. + (drop + (i32.shr_s + (i32.shl + (i32.shr_s (i32.shl (local.get $x32) (i32.const 24)) (i32.const 24)) + (i32.const 16) + ) + (i32.const 8) + ) + ) + ) ;; CHECK: (func $compare-load-s-sign-extend (param $0 i32) (param $1 i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.eq diff --git a/test/lit/passes/optimize-instructions-sign_ext.wast b/test/lit/passes/optimize-instructions-sign_ext.wast index a90c5d53e70..d33b495d590 100644 --- a/test/lit/passes/optimize-instructions-sign_ext.wast +++ b/test/lit/passes/optimize-instructions-sign_ext.wast @@ -365,6 +365,164 @@ ) ) + ;; CHECK: (func $redundant-unary-sign-ext-max-bits (type $2) (param $x32 i32) (param $x64 i64) (param $addr i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: (i32.const 127) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.load8_u + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.and + ;; CHECK-NEXT: (local.get $x64) + ;; CHECK-NEXT: (i64.const 32767) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.load16_u + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.extend8_s + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: (i32.const 255) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $redundant-unary-sign-ext-max-bits (param $x32 i32) (param $x64 i64) (param $addr i32) + ;; Eliminate unary sign-extend when maxBits < bits (the sign bit is known 0). + (drop (i32.extend8_s (i32.and (local.get $x32) (i32.const 0x7f)))) + (drop (i32.extend16_s (i32.load8_u (local.get $addr)))) + (drop (i64.extend16_s (i64.and (local.get $x64) (i64.const 0x7fff)))) + (drop (i64.extend32_s (i64.load16_u (local.get $addr)))) + ;; Negative case: maxBits == bits (the sign bit may be 1). + (drop (i32.extend8_s (i32.and (local.get $x32) (i32.const 0xff)))) + ) + + ;; CHECK: (func $redundant-unary-sign-ext-already-signed (type $4) (param $x32 i32) (param $addr i32) + ;; CHECK-NEXT: (local $t64 i64) + ;; CHECK-NEXT: (local $t64_8 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.load8_s + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.load8_s + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.load16_s + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (i32.extend8_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $t64 + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $t64) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $t64_8 + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (i32.extend8_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $t64_8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.extend8_s + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (local.get $addr) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $redundant-unary-sign-ext-already-signed (param $x32 i32) (param $addr i32) + (local $t64 i64) + (local $t64_8 i64) + ;; Eliminate unary sign-extend when input is already sign-extended to <= bits + ;; via signed loads, i64.extend_i32_s, or locals. + (drop (i32.extend8_s (i32.load8_s (local.get $addr)))) + (drop (i32.extend16_s (i32.load8_s (local.get $addr)))) + (drop (i64.extend32_s (i64.load16_s (local.get $addr)))) + (drop (i64.extend32_s (i64.extend_i32_s (local.get $x32)))) + (drop (i64.extend16_s (i64.extend_i32_s (i32.extend8_s (local.get $x32))))) + ;; Local initialized with i64.extend_i32_s is known to be 32-bit sign-extended. + (local.set $t64 (i64.extend_i32_s (local.get $x32))) + (drop (i64.extend32_s (local.get $t64))) + ;; Local initialized with i64.extend_i32_s(i32.extend8_s) is 8-bit sign-extended. + (local.set $t64_8 (i64.extend_i32_s (i32.extend8_s (local.get $x32)))) + (drop (i64.extend16_s (local.get $t64_8))) + ;; Negative case: full-width load is not sign-extended. + (drop (i32.extend8_s (i32.load (local.get $addr)))) + ) + + ;; CHECK: (func $nested-unary-sign-ext (type $3) (param $x32 i32) (param $x64 i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.extend8_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend8_s + ;; CHECK-NEXT: (local.get $x64) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend16_s + ;; CHECK-NEXT: (local.get $x64) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend16_s + ;; CHECK-NEXT: (i64.extend_i32_u + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $nested-unary-sign-ext (param $x32 i32) (param $x64 i64) + ;; Strip wider inner sign-extension inside narrower outer sign-extension. + (drop (i32.extend8_s (i32.extend16_s (local.get $x32)))) + (drop (i64.extend8_s (i64.extend16_s (local.get $x64)))) + (drop (i64.extend16_s (i64.extend32_s (local.get $x64)))) + ;; Fold i64.extend32_s(i64.extend_i32_u(x)) => i64.extend_i32_s(x). + (drop (i64.extend32_s (i64.extend_i32_u (local.get $x32)))) + ;; Negative case: narrower extend of i64.extend_i32_u cannot be folded to extend_i32_s. + (drop (i64.extend16_s (i64.extend_i32_u (local.get $x32)))) + ) + ;; CHECK: (func $unary-sign-ext-loads (type $1) (param $addr i32) ;; CHECK-NEXT: (local $t32 i32) ;; CHECK-NEXT: (local $t64 i64) @@ -489,7 +647,7 @@ (drop (i64.extend16_s (i64.load32_u (local.get $addr)))) ) - ;; CHECK: (func $unary-sign-ext-eq-const (type $2) (param $x32 i32) (param $x64 i64) + ;; CHECK: (func $unary-sign-ext-eq-const (type $3) (param $x32 i32) (param $x64 i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.eq ;; CHECK-NEXT: (i32.and @@ -563,7 +721,7 @@ (drop (i64.ne (i64.extend32_s (local.get $x64)) (i64.const 0x80000000))) ) - ;; CHECK: (func $unary-sign-ext-eq-both (type $3) (param $x32 i32) (param $y32 i32) (param $x64 i64) (param $y64 i64) + ;; CHECK: (func $unary-sign-ext-eq-both (type $5) (param $x32 i32) (param $y32 i32) (param $x64 i64) (param $y64 i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.eq ;; CHECK-NEXT: (i32.and @@ -619,7 +777,7 @@ (drop (i64.eq (i64.extend8_s (local.get $x64)) (i64.extend16_s (local.get $y64)))) ) - ;; CHECK: (func $unary-sign-ext-eq-load (type $4) (param $x32 i32) (param $x64 i64) (param $addr i32) + ;; CHECK: (func $unary-sign-ext-eq-load (type $2) (param $x32 i32) (param $x64 i64) (param $addr i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.eq ;; CHECK-NEXT: (i32.load16_u From 82413348f5a624200339f8e91434486a49ed0dfd Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 29 Oct 2026 21:33:38 -0700 Subject: [PATCH 2/8] update tests --- test/lit/passes/optimize-instructions-sign_ext.wast | 2 +- test/passes/optimize-instructions_fuzz-exec.txt | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/lit/passes/optimize-instructions-sign_ext.wast b/test/lit/passes/optimize-instructions-sign_ext.wast index 502436c7b2c..d81d133a608 100644 --- a/test/lit/passes/optimize-instructions-sign_ext.wast +++ b/test/lit/passes/optimize-instructions-sign_ext.wast @@ -819,7 +819,7 @@ (drop (i64.ne (i64.load8_s (local.get $addr)) (i64.extend8_s (local.get $x64)))) ) - ;; CHECK: (func $unary-sign-ext-newly-unreachable (type $5) (result i32) + ;; CHECK: (func $unary-sign-ext-newly-unreachable (type $6) (result i32) ;; CHECK-NEXT: (i32.eqz ;; CHECK-NEXT: (i32.extend8_s ;; CHECK-NEXT: (unreachable) diff --git a/test/passes/optimize-instructions_fuzz-exec.txt b/test/passes/optimize-instructions_fuzz-exec.txt index 141b9a94390..9d01b97fe01 100644 --- a/test/passes/optimize-instructions_fuzz-exec.txt +++ b/test/passes/optimize-instructions_fuzz-exec.txt @@ -262,9 +262,7 @@ (call $log (i32.shr_s (i32.shl - (i32.extend8_s - (local.get $0) - ) + (local.get $0) (i32.const 30) ) (i32.const 24) From 10557726ca2f83b52dbfaa9b1740b32d238177ad Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 4 Nov 2026 08:18:23 -0800 Subject: [PATCH 3/8] [NFC] Consolidate and rename sign-ext bit helpers Merge OptimizeInstructions::getSignExtBits into Properties::getSignExtBits using an optional LocalInfoProvider (following Bits::getMaxBits), and rename the sign/zero-extension bit width helpers from get*ExtBits to get*ExtKeptBits to clarify that they return the number of low bits kept before extension rather than the number of extended sign/zero bits. --- src/ir/bits.h | 5 +- src/ir/properties.h | 63 +++++++++++++--------- src/passes/OptimizeInstructions.cpp | 81 ++++++++++++++--------------- src/passes/PickLoadSigns.cpp | 4 +- 4 files changed, 84 insertions(+), 69 deletions(-) diff --git a/src/ir/bits.h b/src/ir/bits.h index 7f3c1f4b2cf..a4493e79930 100644 --- a/src/ir/bits.h +++ b/src/ir/bits.h @@ -145,7 +145,8 @@ inline Expression* makePackedFieldGet(Expression* value, return builder.makeBinary(AndInt32, value, builder.makeConst(int32_t(mask))); } -// getMaxBits() helper that has pessimistic results for the bits used in locals. +// LocalInfoProvider helper that has pessimistic results for the bits used in +// locals. struct DummyLocalInfoProvider { Index getMaxBitsForLocal(LocalGet* get) { if (get->type == Type::i32) { @@ -155,6 +156,8 @@ struct DummyLocalInfoProvider { } WASM_UNREACHABLE("type has no integer bit size"); } + + Index getSignExtKeptBitsForLocal(LocalGet* get) { return 0; } }; // Returns the maximum amount of bits used in an integer expression diff --git a/src/ir/properties.h b/src/ir/properties.h index 96d9786960f..790f0551f62 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -186,26 +186,39 @@ inline Expression* getSignExtValue(Expression* curr) { return nullptr; } -// gets the size of the sign-extended value -inline Index getSignExtBits(Expression* curr) { - assert(curr->type.isInteger()); - if (auto* unary = curr->dynCast()) { - switch (unary->op) { - case ExtendS8Int32: - case ExtendS8Int64: - return 8; - case ExtendS16Int32: - case ExtendS16Int64: - return 16; - case ExtendS32Int64: - return 32; - default: - WASM_UNREACHABLE("invalid unary operation"); +// If |curr| is known to be sign-extended, returns the number of low bits kept +// before sign-extending into the upper bits, or 0 otherwise. LocalInfoProvider +// is an optional class that can provide answers about local.get. +template +inline Index +getSignExtKeptBits(Expression* curr, + LocalInfoProvider* localInfoProvider = nullptr) { + if (getSignExtValue(curr)) { + if (auto* unary = curr->dynCast()) { + switch (unary->op) { + case ExtendS8Int32: + case ExtendS8Int64: + return 8; + case ExtendS16Int32: + case ExtendS16Int64: + return 16; + case ExtendS32Int64: + return 32; + default: + WASM_UNREACHABLE("invalid unary operation"); + } + } else { + auto* rightShift = curr->cast()->right; + return curr->type.getByteSize() * 8 - + Bits::getEffectiveShifts(rightShift); + } + } + if (auto* get = curr->dynCast()) { + if (localInfoProvider) { + return localInfoProvider->getSignExtKeptBitsForLocal(get); } - } else { - auto* rightShift = curr->cast()->right; - return curr->type.getByteSize() * 8 - Bits::getEffectiveShifts(rightShift); } + return 0; } // Check if an expression is almost a sign-extend: perhaps the inner shift @@ -229,14 +242,16 @@ inline Expression* getAlmostSignExt(Expression* curr) { return nullptr; } -// gets the size of the almost sign-extended value, as well as the -// extra shifts, if any -inline Index getAlmostSignExtBits(Expression* curr, Index& extraLeftShifts) { +// Returns the number of low bits kept by an almost-sign-extend (including any +// extra left-shifted zero bits), and sets |extraLeftShifts| to the number of +// extra left shifts. +inline Index getAlmostSignExtKeptBits(Expression* curr, + Index& extraLeftShifts) { auto* leftShift = curr->cast()->left->cast()->right; auto* rightShift = curr->cast()->right; extraLeftShifts = Bits::getEffectiveShifts(leftShift) - Bits::getEffectiveShifts(rightShift); - return getSignExtBits(curr); + return curr->type.getByteSize() * 8 - Bits::getEffectiveShifts(rightShift); } // Check if an expression is a zero-extend, and if so, returns the value @@ -256,8 +271,8 @@ inline Expression* getZeroExtValue(Expression* curr) { return nullptr; } -// gets the size of the sign-extended value -inline Index getZeroExtBits(Expression* curr) { +// Returns the number of low bits kept by a zero-extend operation. +inline Index getZeroExtKeptBits(Expression* curr) { assert(curr->type == Type::i32); int32_t mask = curr->cast()->right->cast()->value.geti32(); return Bits::getMaskedBits(mask); diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index cfeb478acaa..84a4e7991cc 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -83,7 +83,7 @@ struct LocalInfo { static const Index kUnknown = Index(-1); Index maxBits = -1; - Index signExtBits = 0; + Index signExtKeptBits = 0; }; struct LocalScanner : PostWalker { @@ -101,9 +101,10 @@ struct LocalScanner : PostWalker { auto& info = localInfo[i]; if (func->isParam(i)) { info.maxBits = getBitsForType(func->getLocalType(i)); // worst-case - info.signExtBits = LocalInfo::kUnknown; // we will never know anything + info.signExtKeptBits = + LocalInfo::kUnknown; // we will never know anything } else { - info.maxBits = info.signExtBits = 0; // we are open to learning + info.maxBits = info.signExtKeptBits = 0; // we are open to learning } } // walk @@ -111,8 +112,8 @@ struct LocalScanner : PostWalker { // finalize for (Index i = 0; i < func->getNumLocals(); i++) { auto& info = localInfo[i]; - if (info.signExtBits == LocalInfo::kUnknown) { - info.signExtBits = 0; + if (info.signExtKeptBits == LocalInfo::kUnknown) { + info.signExtKeptBits = 0; } } } @@ -132,19 +133,20 @@ struct LocalScanner : PostWalker { auto& info = localInfo[curr->index]; info.maxBits = std::max(info.maxBits, Bits::getMaxBits(value, this)); info.maxBits = std::min(info.maxBits, getBitsForType(type)); - auto signExtBits = LocalInfo::kUnknown; - if (Properties::getSignExtValue(value)) { - signExtBits = Properties::getSignExtBits(value); - } else if (auto* load = value->dynCast()) { - if (LoadUtils::isSignRelevant(load) && load->signed_) { - signExtBits = load->bytes * 8; + auto keptBits = Properties::getSignExtKeptBits(value); + if (keptBits == 0) { + if (auto* load = value->dynCast(); + load && LoadUtils::isSignRelevant(load) && load->signed_) { + keptBits = load->bytes * 8; + } else { + keptBits = LocalInfo::kUnknown; } } - if (info.signExtBits == 0) { - info.signExtBits = signExtBits; // first info we see - } else if (info.signExtBits != signExtBits) { + if (info.signExtKeptBits == 0) { + info.signExtKeptBits = keptBits; // first info we see + } else if (info.signExtKeptBits != keptBits) { // contradictory information, give up - info.signExtBits = LocalInfo::kUnknown; + info.signExtKeptBits = LocalInfo::kUnknown; } } @@ -598,7 +600,7 @@ struct OptimizeInstructions } if (auto* ext = Properties::getAlmostSignExt(curr)) { Index extraLeftShifts; - auto bits = Properties::getAlmostSignExtBits(curr, extraLeftShifts); + auto bits = Properties::getAlmostSignExtKeptBits(curr, extraLeftShifts); if (extraLeftShifts == 0 && trySignExtLoad(ext, bits)) { return replaceCurrent(ext); } @@ -631,7 +633,7 @@ struct OptimizeInstructions // sign-extended, that is, its effective sign bit is spread to all // the upper bits, which means that the relevant bits on the left // side are either all 0, or all 1. - auto bits = Properties::getSignExtBits(curr->left); + auto bits = Properties::getSignExtKeptBits(curr->left); uint64_t right = c->value.getUnsigned(); Index totalBits = c->type.getByteSize() * 8; Index numRelevantBits = totalBits - bits + 1; @@ -670,8 +672,8 @@ struct OptimizeInstructions } } else if (auto* left = Properties::getSignExtValue(curr->left)) { if (auto* right = Properties::getSignExtValue(curr->right)) { - auto bits = Properties::getSignExtBits(curr->left); - if (Properties::getSignExtBits(curr->right) == bits) { + auto bits = Properties::getSignExtKeptBits(curr->left); + if (Properties::getSignExtKeptBits(curr->right) == bits) { // we are comparing two sign-exts with the same bits, so we may as // well replace both with cheaper zexts curr->left = makeZeroExt(left, bits); @@ -681,7 +683,7 @@ struct OptimizeInstructions } else if (auto* load = curr->right->dynCast()) { // we are comparing a load to a sign-ext, we may be able to switch // to zext - auto leftBits = Properties::getSignExtBits(curr->left); + auto leftBits = Properties::getSignExtKeptBits(curr->left); if (load->signed_ && leftBits == load->bytes * 8) { load->signed_ = false; curr->left = makeZeroExt(left, leftBits); @@ -692,7 +694,7 @@ struct OptimizeInstructions if (auto* right = Properties::getSignExtValue(curr->right)) { // we are comparing a load to a sign-ext, we may be able to switch // to zext - auto rightBits = Properties::getSignExtBits(curr->right); + auto rightBits = Properties::getSignExtKeptBits(curr->right); if (load->signed_ && rightBits == load->bytes * 8) { load->signed_ = false; curr->right = makeZeroExt(right, rightBits); @@ -1045,7 +1047,7 @@ struct OptimizeInstructions } if (auto* ext = Properties::getSignExtValue(curr)) { - auto bits = Properties::getSignExtBits(curr); + auto bits = Properties::getSignExtKeptBits(curr); if (trySignExtLoad(ext, bits)) { return replaceCurrent(ext); } @@ -1054,8 +1056,8 @@ struct OptimizeInstructions // larger sign-extend: if we have an 8-bit value in 32-bit, then there are // 24 sign bits, and doing a sign-extend to 16 will only affect 16 of // those 24, and the effect is to leave them as they are. - auto signExtBits = getSignExtBits(ext); - if (signExtBits > 0 && signExtBits <= bits) { + auto keptBits = Properties::getSignExtKeptBits(ext, this); + if (keptBits > 0 && keptBits <= bits) { return replaceCurrent(ext); } } @@ -1097,7 +1099,7 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(curr->value)) { // we are comparing a sign extend to a constant, which means we can // use a cheaper zext - auto bits = Properties::getSignExtBits(curr->value); + auto bits = Properties::getSignExtKeptBits(curr->value); curr->value = makeZeroExt(ext, bits); return replaceCurrent(curr); } @@ -1313,7 +1315,7 @@ struct OptimizeInstructions // if sign extending the exact bit size we store, we can skip the // extension if extending something bigger, then we just alter bits we // don't save anyhow - if (Properties::getSignExtBits(binary) >= Index(bytes) * 8) { + if (Properties::getSignExtKeptBits(binary) >= Index(bytes) * 8) { value = ext; } } @@ -2990,6 +2992,13 @@ struct OptimizeInstructions return getBitsForType(get->type); } + Index getSignExtKeptBitsForLocal(LocalGet* get) { + if (get->index < localInfo.size()) { + return localInfo[get->index].signExtKeptBits; + } + return 0; + } + private: // Information about our locals std::vector localInfo; @@ -3449,7 +3458,7 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(binary)) { // use a cheaper zero-extent, we just care about the boolean value // anyhow - return makeZeroExt(ext, Properties::getSignExtBits(binary)); + return makeZeroExt(ext, Properties::getSignExtKeptBits(binary)); } } else if (auto* block = boolean->dynCast()) { if (block->type == Type::i32 && block->list.size() > 0) { @@ -4443,22 +4452,10 @@ struct OptimizeInstructions return false; } - // Check if an expression is already sign-extended to an exact number of bits. + // Check if an expression is already sign-extended with an exact number of + // kept bits. bool isSignExted(Expression* curr, Index bits) { - return getSignExtBits(curr) == bits; - } - - // Returns the number of bits an expression is sign-extended (or 0 if it is - // not). - Index getSignExtBits(Expression* curr) { - if (Properties::getSignExtValue(curr)) { - return Properties::getSignExtBits(curr); - } - if (auto* get = curr->dynCast()) { - // Check what we know about the local. - return localInfo[get->index].signExtBits; - } - return 0; + return Properties::getSignExtKeptBits(curr, this) == bits; } // optimize trivial math operations, given that the right side of a binary diff --git a/src/passes/PickLoadSigns.cpp b/src/passes/PickLoadSigns.cpp index f04f8451ca0..f419ef7a8eb 100644 --- a/src/passes/PickLoadSigns.cpp +++ b/src/passes/PickLoadSigns.cpp @@ -74,7 +74,7 @@ struct PickLoadSigns : public WalkerPass> { auto* parent = expressionStack[expressionStack.size() - i]; if (Properties::getZeroExtValue(parent) == curr) { - auto bits = Properties::getZeroExtBits(parent); + auto bits = Properties::getZeroExtKeptBits(parent); if (usage.unsignedUsages == 0) { usage.unsignedBits = bits; } else if (usage.unsignedBits != bits) { @@ -82,7 +82,7 @@ struct PickLoadSigns : public WalkerPass> { } usage.unsignedUsages++; } else if (Properties::getSignExtValue(parent) == curr) { - auto bits = Properties::getSignExtBits(parent); + auto bits = Properties::getSignExtKeptBits(parent); if (usage.signedUsages == 0) { usage.signedBits = bits; } else if (usage.signedBits != bits) { From 0052db2cb99a8afcbe74ee002ff21ba82d49803f Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 4 Nov 2026 16:01:36 -0800 Subject: [PATCH 4/8] update comments --- src/passes/OptimizeInstructions.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index c047f774442..f57bb7d90c0 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -605,8 +605,8 @@ struct OptimizeInstructions if (Bits::getMaxBits(ext, this) + extraLeftShifts < bits) { return replaceCurrent(removeAlmostSignExt(curr)); } - // We also don't need it if it already has an identical-sized sign - // extend applied to it. That is, if it is already a sign-extended + // We also don't need it if it already has an identical-sized or larger + // sign extend applied to it. That is, if it is already a sign-extended // value, then another sign extend will do nothing. We do need to be // careful of the extra shifts, though. if (isSignExted(ext, bits) && extraLeftShifts == 0) { @@ -625,7 +625,7 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(curr->left)) { // TODO: Reexamine whether replacing unary sign-extend instructions // (as opposed to shift pairs) with zero-extend masks here is - // worthwhile, since an and with a constant is larger than a unary + // worthwhile, since an `and` with a constant is larger than a unary // extend. // // We are comparing a sign extend to a constant, which means we can @@ -685,7 +685,7 @@ struct OptimizeInstructions // well replace both with cheaper zexts // TODO: Reexamine whether replacing unary sign-extend instructions // (as opposed to shift pairs) with zero-extend masks here is - // worthwhile, since an and with a constant is larger than a unary + // worthwhile, since an `and` with a constant is larger than a unary // extend. curr->left = makeZeroExt(left, bits); curr->right = makeZeroExt(right, bits); @@ -1128,7 +1128,7 @@ struct OptimizeInstructions // use a cheaper zext // TODO: Reexamine whether replacing unary sign-extend instructions (as // opposed to shift pairs) with zero-extend masks here is worthwhile, - // since an and with a constant is larger than a unary extend. + // since an `and` with a constant is larger than a unary extend. auto bits = Properties::getSignExtKeptBits(curr->value); curr->value = makeZeroExt(ext, bits); return replaceCurrent(curr); @@ -3488,10 +3488,6 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(binary)) { // use a cheaper zero-extent, we just care about the boolean value // anyhow - // TODO: Reexamine whether replacing unary sign-extend instructions (as - // opposed to shift pairs) with zero-extend masks in boolean contexts - // is worthwhile, since an and with a constant is larger than a unary - // extend. return makeZeroExt(ext, Properties::getSignExtKeptBits(binary)); } } else if (auto* block = boolean->dynCast()) { From 58ed7107df3fca1541bbfb15b96a5abca60709fb Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 4 Nov 2026 16:08:19 -0800 Subject: [PATCH 5/8] Add missing opt and split conditions --- src/passes/OptimizeInstructions.cpp | 23 ++++++-- .../lit/passes/optimize-instructions-mvp.wast | 55 +++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index f57bb7d90c0..6c9d8380bf2 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -619,6 +619,13 @@ struct OptimizeInstructions return replaceCurrent(curr); } } + // Sign-extending the low 32 bits of a zero-extended i32 is a signed + // i32 extension (plus any remaining left shifts). + if (auto* unary = ext->dynCast(); + unary && unary->op == ExtendUInt32 && bits - extraLeftShifts == 32) { + unary->op = ExtendSInt32; + return replaceCurrent(removeAlmostSignExt(curr)); + } } else if (curr->op == EqInt32 || curr->op == NeInt32 || curr->op == EqInt64 || curr->op == NeInt64) { if (auto* c = curr->right->dynCast()) { @@ -1063,12 +1070,16 @@ struct OptimizeInstructions if (trySignExtLoad(ext, bits)) { return replaceCurrent(ext); } - // Simple sign extends can be removed if the input cannot have a sign bit - // or is already sign-extended. Note that we can handle the case of |curr| - // having a larger sign-extend: if we have an 8-bit value in 32-bit, then - // there are 24 sign bits, and doing a sign-extend to 16 will only affect - // 16 of those 24, and the effect is to leave them as they are. - if (Bits::getMaxBits(ext, this) < bits || isSignExted(ext, bits)) { + // If the sign-extend input cannot have a sign bit, we don't need it. + if (Bits::getMaxBits(ext, this) < bits) { + return replaceCurrent(ext); + } + // Simple sign extends can also be removed if the input is already + // sign-extended. Note that we can handle the case of |curr| having a + // larger sign-extend: if we have an 8-bit value in 32-bit, then there + // are 24 sign bits, and doing a sign-extend to 16 will only affect 16 + // of those 24, and the effect is to leave them as they are. + if (isSignExted(ext, bits)) { return replaceCurrent(ext); } // A narrower unary sign-extension can strip a wider inner one. Since diff --git a/test/lit/passes/optimize-instructions-mvp.wast b/test/lit/passes/optimize-instructions-mvp.wast index 0463a19f2b5..8da998935ae 100644 --- a/test/lit/passes/optimize-instructions-mvp.wast +++ b/test/lit/passes/optimize-instructions-mvp.wast @@ -6142,6 +6142,30 @@ ;; CHECK-NEXT: (i32.const 8) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.shl + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.shr_s + ;; CHECK-NEXT: (i64.shl + ;; CHECK-NEXT: (i64.extend_i32_u + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 48) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 48) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $nested-shift-sign-ext (param $x32 i32) ;; Narrower outer shift sign-extension strips wider inner shift sign-extension. @@ -6175,6 +6199,37 @@ (i32.const 8) ) ) + ;; Sign-extending the low 32 bits of a zero-extended i32 is a signed i32 extension. + (drop + (i64.shr_s + (i64.shl + (i64.extend_i32_u (local.get $x32)) + (i64.const 32) + ) + (i64.const 32) + ) + ) + ;; Also applies when there are remaining left shifts after shifting the i32 + ;; sign bit into the i64 sign bit. + (drop + (i64.shr_s + (i64.shl + (i64.extend_i32_u (local.get $x32)) + (i64.const 32) + ) + (i64.const 24) + ) + ) + ;; Negative case: shifting by more than 32 bits sign-extends a narrower width. + (drop + (i64.shr_s + (i64.shl + (i64.extend_i32_u (local.get $x32)) + (i64.const 48) + ) + (i64.const 48) + ) + ) ) ;; CHECK: (func $compare-load-s-sign-extend (param $0 i32) (param $1 i32) ;; CHECK-NEXT: (drop From ede79b6ec4373c2503eac8e15028e782d54d0f53 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 4 Nov 2026 08:18:23 -0800 Subject: [PATCH 6/8] [NFC] Consolidate and rename sign-ext bit helpers Merge OptimizeInstructions::getSignExtBits into Properties::getSignExtBits using an optional LocalInfoProvider (following Bits::getMaxBits), and rename the sign/zero-extension bit width helpers from get*ExtBits to get*ExtKeptBits to clarify that they return the number of low bits kept before extension rather than the number of extended sign/zero bits. --- src/ir/bits.h | 5 +- src/ir/properties.h | 63 +++++++++++++-------- src/passes/OptimizeInstructions.cpp | 85 ++++++++++++++--------------- src/passes/PickLoadSigns.cpp | 4 +- 4 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/ir/bits.h b/src/ir/bits.h index 7f3c1f4b2cf..a4493e79930 100644 --- a/src/ir/bits.h +++ b/src/ir/bits.h @@ -145,7 +145,8 @@ inline Expression* makePackedFieldGet(Expression* value, return builder.makeBinary(AndInt32, value, builder.makeConst(int32_t(mask))); } -// getMaxBits() helper that has pessimistic results for the bits used in locals. +// LocalInfoProvider helper that has pessimistic results for the bits used in +// locals. struct DummyLocalInfoProvider { Index getMaxBitsForLocal(LocalGet* get) { if (get->type == Type::i32) { @@ -155,6 +156,8 @@ struct DummyLocalInfoProvider { } WASM_UNREACHABLE("type has no integer bit size"); } + + Index getSignExtKeptBitsForLocal(LocalGet* get) { return 0; } }; // Returns the maximum amount of bits used in an integer expression diff --git a/src/ir/properties.h b/src/ir/properties.h index 96d9786960f..790f0551f62 100644 --- a/src/ir/properties.h +++ b/src/ir/properties.h @@ -186,26 +186,39 @@ inline Expression* getSignExtValue(Expression* curr) { return nullptr; } -// gets the size of the sign-extended value -inline Index getSignExtBits(Expression* curr) { - assert(curr->type.isInteger()); - if (auto* unary = curr->dynCast()) { - switch (unary->op) { - case ExtendS8Int32: - case ExtendS8Int64: - return 8; - case ExtendS16Int32: - case ExtendS16Int64: - return 16; - case ExtendS32Int64: - return 32; - default: - WASM_UNREACHABLE("invalid unary operation"); +// If |curr| is known to be sign-extended, returns the number of low bits kept +// before sign-extending into the upper bits, or 0 otherwise. LocalInfoProvider +// is an optional class that can provide answers about local.get. +template +inline Index +getSignExtKeptBits(Expression* curr, + LocalInfoProvider* localInfoProvider = nullptr) { + if (getSignExtValue(curr)) { + if (auto* unary = curr->dynCast()) { + switch (unary->op) { + case ExtendS8Int32: + case ExtendS8Int64: + return 8; + case ExtendS16Int32: + case ExtendS16Int64: + return 16; + case ExtendS32Int64: + return 32; + default: + WASM_UNREACHABLE("invalid unary operation"); + } + } else { + auto* rightShift = curr->cast()->right; + return curr->type.getByteSize() * 8 - + Bits::getEffectiveShifts(rightShift); + } + } + if (auto* get = curr->dynCast()) { + if (localInfoProvider) { + return localInfoProvider->getSignExtKeptBitsForLocal(get); } - } else { - auto* rightShift = curr->cast()->right; - return curr->type.getByteSize() * 8 - Bits::getEffectiveShifts(rightShift); } + return 0; } // Check if an expression is almost a sign-extend: perhaps the inner shift @@ -229,14 +242,16 @@ inline Expression* getAlmostSignExt(Expression* curr) { return nullptr; } -// gets the size of the almost sign-extended value, as well as the -// extra shifts, if any -inline Index getAlmostSignExtBits(Expression* curr, Index& extraLeftShifts) { +// Returns the number of low bits kept by an almost-sign-extend (including any +// extra left-shifted zero bits), and sets |extraLeftShifts| to the number of +// extra left shifts. +inline Index getAlmostSignExtKeptBits(Expression* curr, + Index& extraLeftShifts) { auto* leftShift = curr->cast()->left->cast()->right; auto* rightShift = curr->cast()->right; extraLeftShifts = Bits::getEffectiveShifts(leftShift) - Bits::getEffectiveShifts(rightShift); - return getSignExtBits(curr); + return curr->type.getByteSize() * 8 - Bits::getEffectiveShifts(rightShift); } // Check if an expression is a zero-extend, and if so, returns the value @@ -256,8 +271,8 @@ inline Expression* getZeroExtValue(Expression* curr) { return nullptr; } -// gets the size of the sign-extended value -inline Index getZeroExtBits(Expression* curr) { +// Returns the number of low bits kept by a zero-extend operation. +inline Index getZeroExtKeptBits(Expression* curr) { assert(curr->type == Type::i32); int32_t mask = curr->cast()->right->cast()->value.geti32(); return Bits::getMaskedBits(mask); diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index cfeb478acaa..79702ec3597 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -83,7 +83,7 @@ struct LocalInfo { static const Index kUnknown = Index(-1); Index maxBits = -1; - Index signExtBits = 0; + Index signExtKeptBits = 0; }; struct LocalScanner : PostWalker { @@ -101,9 +101,10 @@ struct LocalScanner : PostWalker { auto& info = localInfo[i]; if (func->isParam(i)) { info.maxBits = getBitsForType(func->getLocalType(i)); // worst-case - info.signExtBits = LocalInfo::kUnknown; // we will never know anything + // We will never know anything. + info.signExtKeptBits = LocalInfo::kUnknown; } else { - info.maxBits = info.signExtBits = 0; // we are open to learning + info.maxBits = info.signExtKeptBits = 0; // we are open to learning } } // walk @@ -111,8 +112,8 @@ struct LocalScanner : PostWalker { // finalize for (Index i = 0; i < func->getNumLocals(); i++) { auto& info = localInfo[i]; - if (info.signExtBits == LocalInfo::kUnknown) { - info.signExtBits = 0; + if (info.signExtKeptBits == LocalInfo::kUnknown) { + info.signExtKeptBits = 0; } } } @@ -132,19 +133,20 @@ struct LocalScanner : PostWalker { auto& info = localInfo[curr->index]; info.maxBits = std::max(info.maxBits, Bits::getMaxBits(value, this)); info.maxBits = std::min(info.maxBits, getBitsForType(type)); - auto signExtBits = LocalInfo::kUnknown; - if (Properties::getSignExtValue(value)) { - signExtBits = Properties::getSignExtBits(value); - } else if (auto* load = value->dynCast()) { - if (LoadUtils::isSignRelevant(load) && load->signed_) { - signExtBits = load->bytes * 8; + auto keptBits = Properties::getSignExtKeptBits(value); + if (keptBits == 0) { + if (auto* load = value->dynCast(); + load && LoadUtils::isSignRelevant(load) && load->signed_) { + keptBits = load->bytes * 8; + } else { + keptBits = LocalInfo::kUnknown; } } - if (info.signExtBits == 0) { - info.signExtBits = signExtBits; // first info we see - } else if (info.signExtBits != signExtBits) { + if (info.signExtKeptBits == 0) { + info.signExtKeptBits = keptBits; // first info we see + } else if (info.signExtKeptBits != keptBits) { // contradictory information, give up - info.signExtBits = LocalInfo::kUnknown; + info.signExtKeptBits = LocalInfo::kUnknown; } } @@ -598,7 +600,7 @@ struct OptimizeInstructions } if (auto* ext = Properties::getAlmostSignExt(curr)) { Index extraLeftShifts; - auto bits = Properties::getAlmostSignExtBits(curr, extraLeftShifts); + auto bits = Properties::getAlmostSignExtKeptBits(curr, extraLeftShifts); if (extraLeftShifts == 0 && trySignExtLoad(ext, bits)) { return replaceCurrent(ext); } @@ -612,7 +614,7 @@ struct OptimizeInstructions // extend applied to it. That is, if it is already a sign-extended // value, then another sign extend will do nothing. We do need to be // careful of the extra shifts, though. - if (isSignExted(ext, bits) && extraLeftShifts == 0) { + if (hasSignExtKeptBits(ext, bits) && extraLeftShifts == 0) { return replaceCurrent(removeAlmostSignExt(curr)); } } else if (curr->op == EqInt32 || curr->op == NeInt32 || @@ -631,7 +633,7 @@ struct OptimizeInstructions // sign-extended, that is, its effective sign bit is spread to all // the upper bits, which means that the relevant bits on the left // side are either all 0, or all 1. - auto bits = Properties::getSignExtBits(curr->left); + auto bits = Properties::getSignExtKeptBits(curr->left); uint64_t right = c->value.getUnsigned(); Index totalBits = c->type.getByteSize() * 8; Index numRelevantBits = totalBits - bits + 1; @@ -670,8 +672,8 @@ struct OptimizeInstructions } } else if (auto* left = Properties::getSignExtValue(curr->left)) { if (auto* right = Properties::getSignExtValue(curr->right)) { - auto bits = Properties::getSignExtBits(curr->left); - if (Properties::getSignExtBits(curr->right) == bits) { + auto bits = Properties::getSignExtKeptBits(curr->left); + if (Properties::getSignExtKeptBits(curr->right) == bits) { // we are comparing two sign-exts with the same bits, so we may as // well replace both with cheaper zexts curr->left = makeZeroExt(left, bits); @@ -681,7 +683,7 @@ struct OptimizeInstructions } else if (auto* load = curr->right->dynCast()) { // we are comparing a load to a sign-ext, we may be able to switch // to zext - auto leftBits = Properties::getSignExtBits(curr->left); + auto leftBits = Properties::getSignExtKeptBits(curr->left); if (load->signed_ && leftBits == load->bytes * 8) { load->signed_ = false; curr->left = makeZeroExt(left, leftBits); @@ -692,7 +694,7 @@ struct OptimizeInstructions if (auto* right = Properties::getSignExtValue(curr->right)) { // we are comparing a load to a sign-ext, we may be able to switch // to zext - auto rightBits = Properties::getSignExtBits(curr->right); + auto rightBits = Properties::getSignExtKeptBits(curr->right); if (load->signed_ && rightBits == load->bytes * 8) { load->signed_ = false; curr->right = makeZeroExt(right, rightBits); @@ -1045,7 +1047,7 @@ struct OptimizeInstructions } if (auto* ext = Properties::getSignExtValue(curr)) { - auto bits = Properties::getSignExtBits(curr); + auto bits = Properties::getSignExtKeptBits(curr); if (trySignExtLoad(ext, bits)) { return replaceCurrent(ext); } @@ -1054,8 +1056,8 @@ struct OptimizeInstructions // larger sign-extend: if we have an 8-bit value in 32-bit, then there are // 24 sign bits, and doing a sign-extend to 16 will only affect 16 of // those 24, and the effect is to leave them as they are. - auto signExtBits = getSignExtBits(ext); - if (signExtBits > 0 && signExtBits <= bits) { + auto keptBits = Properties::getSignExtKeptBits(ext, this); + if (keptBits > 0 && keptBits <= bits) { return replaceCurrent(ext); } } @@ -1097,7 +1099,7 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(curr->value)) { // we are comparing a sign extend to a constant, which means we can // use a cheaper zext - auto bits = Properties::getSignExtBits(curr->value); + auto bits = Properties::getSignExtKeptBits(curr->value); curr->value = makeZeroExt(ext, bits); return replaceCurrent(curr); } @@ -1313,7 +1315,7 @@ struct OptimizeInstructions // if sign extending the exact bit size we store, we can skip the // extension if extending something bigger, then we just alter bits we // don't save anyhow - if (Properties::getSignExtBits(binary) >= Index(bytes) * 8) { + if (Properties::getSignExtKeptBits(binary) >= Index(bytes) * 8) { value = ext; } } @@ -2990,6 +2992,13 @@ struct OptimizeInstructions return getBitsForType(get->type); } + Index getSignExtKeptBitsForLocal(LocalGet* get) { + if (get->index < localInfo.size()) { + return localInfo[get->index].signExtKeptBits; + } + return 0; + } + private: // Information about our locals std::vector localInfo; @@ -3449,7 +3458,7 @@ struct OptimizeInstructions if (auto* ext = Properties::getSignExtValue(binary)) { // use a cheaper zero-extent, we just care about the boolean value // anyhow - return makeZeroExt(ext, Properties::getSignExtBits(binary)); + return makeZeroExt(ext, Properties::getSignExtKeptBits(binary)); } } else if (auto* block = boolean->dynCast()) { if (block->type == Type::i32 && block->list.size() > 0) { @@ -4443,22 +4452,10 @@ struct OptimizeInstructions return false; } - // Check if an expression is already sign-extended to an exact number of bits. - bool isSignExted(Expression* curr, Index bits) { - return getSignExtBits(curr) == bits; - } - - // Returns the number of bits an expression is sign-extended (or 0 if it is - // not). - Index getSignExtBits(Expression* curr) { - if (Properties::getSignExtValue(curr)) { - return Properties::getSignExtBits(curr); - } - if (auto* get = curr->dynCast()) { - // Check what we know about the local. - return localInfo[get->index].signExtBits; - } - return 0; + // Check if an expression is already sign-extended with an exact number of + // kept bits. + bool hasSignExtKeptBits(Expression* curr, Index bits) { + return Properties::getSignExtKeptBits(curr, this) == bits; } // optimize trivial math operations, given that the right side of a binary diff --git a/src/passes/PickLoadSigns.cpp b/src/passes/PickLoadSigns.cpp index f04f8451ca0..f419ef7a8eb 100644 --- a/src/passes/PickLoadSigns.cpp +++ b/src/passes/PickLoadSigns.cpp @@ -74,7 +74,7 @@ struct PickLoadSigns : public WalkerPass> { auto* parent = expressionStack[expressionStack.size() - i]; if (Properties::getZeroExtValue(parent) == curr) { - auto bits = Properties::getZeroExtBits(parent); + auto bits = Properties::getZeroExtKeptBits(parent); if (usage.unsignedUsages == 0) { usage.unsignedBits = bits; } else if (usage.unsignedBits != bits) { @@ -82,7 +82,7 @@ struct PickLoadSigns : public WalkerPass> { } usage.unsignedUsages++; } else if (Properties::getSignExtValue(parent) == curr) { - auto bits = Properties::getSignExtBits(parent); + auto bits = Properties::getSignExtKeptBits(parent); if (usage.signedUsages == 0) { usage.signedBits = bits; } else if (usage.signedBits != bits) { From ca304dbc763d5a79e95aa0f11a355e07863c7699 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 4 Nov 2026 18:30:30 -0800 Subject: [PATCH 7/8] Generalize almost-sign-ext outer elimination --- src/passes/OptimizeInstructions.cpp | 22 ++++++++++--------- .../lit/passes/optimize-instructions-mvp.wast | 17 +++++++------- .../optimize-instructions-sign_ext.wast | 11 ++++++++++ 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/passes/OptimizeInstructions.cpp b/src/passes/OptimizeInstructions.cpp index 6370ba9f955..96725fd6336 100644 --- a/src/passes/OptimizeInstructions.cpp +++ b/src/passes/OptimizeInstructions.cpp @@ -605,19 +605,21 @@ struct OptimizeInstructions if (Bits::getMaxBits(ext, this) + extraLeftShifts < bits) { return replaceCurrent(removeAlmostSignExt(curr)); } - // We also don't need it if it already has an identical-sized or larger - // sign extend applied to it. That is, if it is already a sign-extended - // value, then another sign extend will do nothing. We do need to be - // careful of the extra shifts, though. - if (hasSignExtKeptBits(ext, bits) && extraLeftShifts == 0) { + // A wider (or equal) outer shift sign-extension is redundant around a + // narrower inner sign-extension: if |ext| is already sign-extended with + // at most |bits - extraLeftShifts| kept bits, shifting it left by + // |extraLeftShifts| and sign-extending from |bits| bits only re-extends + // bits that are already copies of the sign bit, so we can remove the + // outer sign-extension (leaving any |extraLeftShifts|). + if (hasSignExtKeptBits(ext, bits - extraLeftShifts)) { return replaceCurrent(removeAlmostSignExt(curr)); } - // A narrower outer shift sign-extension can strip a wider inner one. + // Conversely, when the inner sign-extension is wider than the outer shift + // sign-extension, we can strip the redundant inner sign-extension. if (auto* innerExt = Properties::getSignExtValue(ext)) { - if (bits - extraLeftShifts <= Properties::getSignExtKeptBits(ext)) { - curr->left->cast()->left = innerExt; - return replaceCurrent(curr); - } + assert(Properties::getSignExtKeptBits(ext) > bits - extraLeftShifts); + curr->left->cast()->left = innerExt; + return replaceCurrent(curr); } // Sign-extending the low 32 bits of a zero-extended i32 is a signed // i32 extension (plus any remaining left shifts). diff --git a/test/lit/passes/optimize-instructions-mvp.wast b/test/lit/passes/optimize-instructions-mvp.wast index 8da998935ae..442b7fedc9a 100644 --- a/test/lit/passes/optimize-instructions-mvp.wast +++ b/test/lit/passes/optimize-instructions-mvp.wast @@ -6128,16 +6128,13 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.shr_s - ;; CHECK-NEXT: (i32.shl - ;; CHECK-NEXT: (i32.shr_s - ;; CHECK-NEXT: (i32.shl - ;; CHECK-NEXT: (local.get $x32) - ;; CHECK-NEXT: (i32.const 24) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (i32.shr_s + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (local.get $x32) ;; CHECK-NEXT: (i32.const 24) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 16) + ;; CHECK-NEXT: (i32.const 24) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.const 8) ;; CHECK-NEXT: ) @@ -6189,7 +6186,9 @@ (i32.const 24) ) ) - ;; Negative case: extra left shifts keep sign-extended bits from the inner shift. + ;; Conversely, when the outer almost-sign-ext is wider than the inner + ;; sign-extension, the outer sign-extension is stripped (leaving any extra + ;; left shifts). (drop (i32.shr_s (i32.shl diff --git a/test/lit/passes/optimize-instructions-sign_ext.wast b/test/lit/passes/optimize-instructions-sign_ext.wast index d81d133a608..a75587eb163 100644 --- a/test/lit/passes/optimize-instructions-sign_ext.wast +++ b/test/lit/passes/optimize-instructions-sign_ext.wast @@ -511,6 +511,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.shl + ;; CHECK-NEXT: (i32.extend8_s + ;; CHECK-NEXT: (local.get $x32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $nested-unary-sign-ext (param $x32 i32) (param $x64 i64) ;; Strip wider inner sign-extension inside narrower outer sign-extension. @@ -521,6 +529,9 @@ (drop (i64.extend32_s (i64.extend_i32_u (local.get $x32)))) ;; Negative case: narrower extend of i64.extend_i32_u cannot be folded to extend_i32_s. (drop (i64.extend16_s (i64.extend_i32_u (local.get $x32)))) + ;; Strip wider outer almost-sign-ext around narrower inner unary sign-ext, + ;; leaving the extra left shifts. + (drop (i32.shr_s (i32.shl (i32.extend8_s (local.get $x32)) (i32.const 16)) (i32.const 8))) ) ;; CHECK: (func $unary-sign-ext-loads (type $1) (param $addr i32) From 071133e2999a2116b2cfbcbca28ec4c720c4ca03 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 6 Nov 2026 17:03:54 -0800 Subject: [PATCH 8/8] guard against unreachable --- src/ir/bits.h | 5 ++++- .../optimize-instructions-sign_ext.wast | 20 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/ir/bits.h b/src/ir/bits.h index 4d1a8977723..7289f7de2a4 100644 --- a/src/ir/bits.h +++ b/src/ir/bits.h @@ -607,8 +607,11 @@ class Provider { // If |curr| is known to be sign-extended, returns the number of low bits kept // before sign-extending into the upper bits, or 0 otherwise. Index getSignExtKeptBits(Expression* curr) const { + if (!curr->type.isInteger()) { + return 0; + } if (auto* unary = curr->dynCast(); - unary && unary->op == ExtendSInt32) { + unary && unary->op == ExtendSInt32 && unary->value->type.isInteger()) { // i64.extend_i32_s sign-extends from 32 bits, or from a narrower width if // its i32 input is itself sign-extended (since any i32 sign extension is // narrower than 32 bits and extending to i64 replicates the same sign diff --git a/test/lit/passes/optimize-instructions-sign_ext.wast b/test/lit/passes/optimize-instructions-sign_ext.wast index a75587eb163..a5361d89782 100644 --- a/test/lit/passes/optimize-instructions-sign_ext.wast +++ b/test/lit/passes/optimize-instructions-sign_ext.wast @@ -831,6 +831,13 @@ ) ;; CHECK: (func $unary-sign-ext-newly-unreachable (type $6) (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i64.extend32_s + ;; CHECK-NEXT: (i64.extend_i32_s + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: (i32.eqz ;; CHECK-NEXT: (i32.extend8_s ;; CHECK-NEXT: (unreachable) @@ -839,8 +846,17 @@ ;; CHECK-NEXT: ) (func $unary-sign-ext-newly-unreachable (result i32) ;; The array.len traps on null and is replaced with unreachable before - ;; refinalization updates the enclosing i32.extend8_s. Do not treat the - ;; newly-unreachable operand as an integer sign-extended value. + ;; refinalization updates the enclosing sign extensions. Do not treat the + ;; newly-unreachable operands as integer sign-extended values. + (drop + (i64.extend32_s + (i64.extend_i32_s + (array.len + (ref.null array) + ) + ) + ) + ) (i32.eqz (i32.extend8_s (array.len