;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
;; RUN: wasm-opt %s --remove-unused-names --local-subtyping -all -S -o - \
;; RUN: | filecheck %s
;; --remove-unused-names is run to avoid adding names to blocks. Block names
;; can prevent non-nullable local validation (we emit named blocks in the binary
;; format, if we need them, but never emit unnamed ones), which affects some
;; testcases.
(module
(type $"{}" (sub (struct)))
(type $"{i32}" (sub (struct (field i32))))
(type $array (sub (array i8)))
;; CHECK: (type $ret-any (sub (func (result anyref))))
(type $ret-any (sub (func (result anyref))))
;; CHECK: (type $ret-i31 (sub $ret-any (func (result i31ref))))
(type $ret-i31 (sub $ret-any (func (result i31ref))))
;; CHECK: (import "out" "i32" (func $i32 (type $1) (result i32)))
(import "out" "i32" (func $i32 (result i32)))
;; CHECK: (import "out" "i64" (func $i64 (type $6) (result i64)))
(import "out" "i64" (func $i64 (result i64)))
;; Refinalization can find a more specific type, where the declared type was
;; not the optimal LUB.
;; CHECK: (func $refinalize (type $2) (param $x i32)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (if (result (ref i31))
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (ref.i31
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (else
;; CHECK-NEXT: (ref.i31
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block $block (result (ref i31))
;; CHECK-NEXT: (br $block
;; CHECK-NEXT: (ref.i31
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (ref.i31
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $refinalize (param $x i32)
(drop
(if (result anyref)
(local.get $x)
(then
(ref.i31 (i32.const 0))
)
(else
(ref.i31 (i32.const 1))
)
)
)
(drop
(block $block (result anyref)
(br $block
(ref.i31 (i32.const 0))
)
(ref.i31 (i32.const 1))
)
)
)
;; A simple case where a local has a single assignment that we can use as a
;; more specific type. A similar thing with a parameter, however, is not a
;; thing we can optimize. Also, ignore a local with zero assignments.
;; CHECK: (func $simple-local-but-not-param (type $7) (param $x funcref)
;; CHECK-NEXT: (local $y (ref $1))
;; CHECK-NEXT: (local $unused funcref)
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $y
;; CHECK-NEXT: (ref.func $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $simple-local-but-not-param (param $x funcref)
(local $y funcref)
(local $unused funcref)
(local.set $x
(ref.func $i32)
)
(local.set $y
(ref.func $i32)
)
)
;; CHECK: (func $locals-with-multiple-assignments (type $8) (param $struct structref)
;; CHECK-NEXT: (local $x eqref)
;; CHECK-NEXT: (local $y (ref i31))
;; CHECK-NEXT: (local $z structref)
;; CHECK-NEXT: (local $w (ref func))
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.i31
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (local.get $struct)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $y
;; CHECK-NEXT: (ref.i31
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $y
;; CHECK-NEXT: (ref.i31
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $z
;; CHECK-NEXT: (local.get $struct)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $z
;; CHECK-NEXT: (local.get $struct)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $w
;; CHECK-NEXT: (ref.func $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $w
;; CHECK-NEXT: (ref.func $i64)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $locals-with-multiple-assignments (param $struct (ref null struct))
(local $x anyref)
(local $y anyref)
(local $z anyref)
(local $w funcref)
;; x is assigned two different types with a new LUB possible
(local.set $x
(ref.i31 (i32.const 0))
)
(local.set $x
(local.get $struct)
)
;; y and z are assigned the same more specific type twice
(local.set $y
(ref.i31 (i32.const 0))
)
(local.set $y
(ref.i31 (i32.const 1))
)
(local.set $z
(local.get $struct)
)
(local.set $z
(local.get $struct)
)
;; w is assigned two different types *without* a new LUB heap type possible,
;; as it already had the optimal LUB heap type (but it can become non-
;; nullable).
(local.set $w
(ref.func $i32)
)
(local.set $w
(ref.func $i64)
)
)
;; In some cases multiple iterations are necessary, as one inferred new type
;; applies to a get which then allows another inference.
;; CHECK: (func $multiple-iterations (type $0)
;; CHECK-NEXT: (local $x (ref $1))
;; CHECK-NEXT: (local $y (ref $1))
;; CHECK-NEXT: (local $z (ref $1))
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $y
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $z
;; CHECK-NEXT: (local.get $y)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $multiple-iterations
(local $x funcref)
(local $y funcref)
(local $z funcref)
(local.set $x
(ref.func $i32)
)
(local.set $y
(local.get $x)
)
(local.set $z
(local.get $y)
)
)
;; Sometimes a refinalize is necessary in between the iterations.
;; CHECK: (func $multiple-iterations-refinalize (type $2) (param $i i32)
;; CHECK-NEXT: (local $x (ref $1))
;; CHECK-NEXT: (local $y (ref $6))
;; CHECK-NEXT: (local $z (ref func))
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $y
;; CHECK-NEXT: (ref.func $i64)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $z
;; CHECK-NEXT: (select (result (ref func))
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: (local.get $y)
;; CHECK-NEXT: (local.get $i)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $multiple-iterations-refinalize (param $i i32)
(local $x funcref)
(local $y funcref)
(local $z funcref)
(local.set $x
(ref.func $i32)
)
(local.set $y
(ref.func $i64)
)
(local.set $z
(select
(local.get $x)
(local.get $y)
(local.get $i)
)
)
)
;; CHECK: (func $multiple-iterations-refinalize-call-ref (type $0)
;; CHECK-NEXT: (local $f (ref $ret-i31))
;; CHECK-NEXT: (local $x i31ref)
;; CHECK-NEXT: (local.set $f
;; CHECK-NEXT: (ref.func $ret-i31)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (call_ref $ret-i31
;; CHECK-NEXT: (local.get $f)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $multiple-iterations-refinalize-call-ref
(local $f (ref null $ret-any))
(local $x anyref)
(local.set $f
(ref.func $ret-i31)
)
(local.set $x
;; After $f is refined to hold $ret-i31 and the call_ref is refinalized,
;; we will be able to refine $x to i31.
(call_ref $ret-any
(local.get $f)
)
)
)
;; CHECK: (func $multiple-iterations-refinalize-call-ref-bottom (type $0)
;; CHECK-NEXT: (local $f nullfuncref)
;; CHECK-NEXT: (local $x anyref)
;; CHECK-NEXT: (local.set $f
;; CHECK-NEXT: (ref.null nofunc)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (block ;; (replaces unreachable CallRef we can't emit)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $f)
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $multiple-iterations-refinalize-call-ref-bottom
(local $f (ref null $ret-any))
(local $x anyref)
;; Same as above, but now we refine $f to nullfuncref. Check that we don't crash.
(local.set $f
(ref.null nofunc)
)
(local.set $x
;; We can no longer refine $x because there is no result type we can use
;; after refining $f.
(call_ref $ret-any
(local.get $f)
)
)
)
;; CHECK: (func $ret-i31 (type $ret-i31) (result i31ref)
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )
(func $ret-i31 (type $ret-i31) (result i31ref)
(unreachable)
)
;; CHECK: (func $nondefaultable (type $0)
;; CHECK-NEXT: (local $x (tuple funcref funcref))
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (tuple.make 2
;; CHECK-NEXT: (ref.func $i32)
;; CHECK-NEXT: (ref.func $i32)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $nondefaultable
(local $x (tuple funcref funcref))
;; This tuple is assigned non-nullable values, which means the subtype is
;; nondefaultable, and we must not apply it.
(local.set $x
(tuple.make 2
(ref.func $i32)
(ref.func $i32)
)
)
)
;; CHECK: (func $uses-default (type $2) (param $i i32)
;; CHECK-NEXT: (local $x (ref null $2))
;; CHECK-NEXT: (if
;; CHECK-NEXT: (local.get $i)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $uses-default)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $uses-default (param $i i32)
(local $x funcref)
(if
(local.get $i)
;; The only set to this local uses a more specific type than funcref.
(then
(local.set $x (ref.func $uses-default))
)
)
(drop
;; This get may use the default value, but it is ok to have a null of a
;; more refined type in the local.
(local.get $x)
)
)
;; CHECK: (func $unreachables (type $3) (result funcref)
;; CHECK-NEXT: (local $temp (ref $3))
;; CHECK-NEXT: (local.set $temp
;; CHECK-NEXT: (ref.func $unreachables)
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block (result (ref $3))
;; CHECK-NEXT: (local.tee $temp
;; CHECK-NEXT: (ref.func $unreachables)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.get $temp)
;; CHECK-NEXT: )
(func $unreachables (result funcref)
(local $temp funcref)
;; Set a value that allows us to refine the local's type.
(local.set $temp
(ref.func $unreachables)
)
(unreachable)
;; A tee that is not reachable. We must still update its type, and the
;; parents.
(drop
(block (result funcref)
(local.tee $temp
(ref.func $unreachables)
)
)
)
;; A get that is not reachable. We must still update its type.
(local.get $temp)
)
;; CHECK: (func $incompatible-sets (type $1) (result i32)
;; CHECK-NEXT: (local $temp (ref $1))
;; CHECK-NEXT: (local.set $temp
;; CHECK-NEXT: (ref.func $incompatible-sets)
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.tee $temp
;; CHECK-NEXT: (block
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (ref.null nofunc)
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.tee $temp
;; CHECK-NEXT: (block
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (ref.null nofunc)
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )
(func $incompatible-sets (result i32)
(local $temp funcref)
;; Set a value that allows us to specialize the local type.
(local.set $temp
(ref.func $incompatible-sets)
)
;; Make all code unreachable from here.
(unreachable)
;; In unreachable code, assign values that are not compatible with the more
;; specific type we will optimize to. Those cannot be left as they are, and
;; will be fixed up so that they validate. (All we need is validation, as
;; their contents do not matter, given they are not reached.)
(drop
(local.tee $temp
(ref.null func)
)
)
(local.set $temp
(ref.null func)
)
(unreachable)
)
;; CHECK: (func $become-non-nullable (type $0)
;; CHECK-NEXT: (local $x (ref $0))
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $become-non-nullable)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $become-non-nullable
(local $x (ref null func))
(local.set $x
(ref.func $become-non-nullable)
)
(drop
(local.get $x)
)
)
;; CHECK: (func $already-non-nullable (type $0)
;; CHECK-NEXT: (local $x (ref $0))
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $already-non-nullable)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $already-non-nullable
(local $x (ref func))
(local.set $x
(ref.func $already-non-nullable)
)
(drop
(local.get $x)
)
)
;; CHECK: (func $cannot-become-non-nullable (type $0)
;; CHECK-NEXT: (local $x (ref null $0))
;; CHECK-NEXT: (if
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: (then
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $become-non-nullable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $cannot-become-non-nullable
(local $x (ref null func))
;; The set is in a nested scope, so we should not make the local non-
;; nullable, as it would not validate. (We can refine the heap type,
;; though.)
(if
(i32.const 1)
(then
(local.set $x
(ref.func $become-non-nullable)
)
)
)
(drop
(local.get $x)
)
)
;; CHECK: (func $cannot-become-non-nullable-block (type $0)
;; CHECK-NEXT: (local $x (ref null $0))
;; CHECK-NEXT: (block $name
;; CHECK-NEXT: (br_if $name
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $become-non-nullable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $cannot-become-non-nullable-block
(local $x (ref null func))
;; A named block prevents us from optimizing here, the same as above.
(block $name
;; Add a br_if to avoid the name being removed.
(br_if $name
(i32.const 1)
)
(local.set $x
(ref.func $become-non-nullable)
)
)
(drop
(local.get $x)
)
)
;; CHECK: (func $become-non-nullable-block-unnamed (type $0)
;; CHECK-NEXT: (local $x (ref $0))
;; CHECK-NEXT: (block
;; CHECK-NEXT: (local.set $x
;; CHECK-NEXT: (ref.func $become-non-nullable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $x)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $become-non-nullable-block-unnamed
(local $x (ref null func))
;; An named block does *not* prevent us from optimizing here. Unlike above,
;; an unnamed block is never emitted in the binary format, so it does not
;; prevent validation.
(block
(local.set $x
(ref.func $become-non-nullable)
)
)
(drop
(local.get $x)
)
)
)