;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. ;; RUN: foreach %s %t wasm-opt --closed-world --type-refining -all -S -o - | filecheck %s (module ;; A struct with three fields. The first will have no writes, the second one ;; write of the same type, and the last a write of a subtype, which will allow ;; us to specialize that one. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut anyref)) (field (mut (ref i31))) (field (mut (ref i31)))))) (type $struct (sub (struct (field (mut anyref)) (field (mut (ref i31))) (field (mut anyref))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $work (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (struct.set $struct 1 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (ref.i31 ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 2 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (ref.i31 ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.get $struct 2 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (struct.set $struct 1 (local.get $struct) (ref.i31 (i32.const 0)) ) (struct.set $struct 2 (local.get $struct) (ref.i31 (i32.const 0)) ) (drop ;; The type of this struct.get must be updated after the field's type ;; changes, or the validator will complain. (struct.get $struct 2 (local.get $struct) ) ) ) ) (module ;; A struct with a nullable field and a write of a non-nullable value. We ;; must keep the type nullable, unlike in the previous module, due to the ;; default value being null. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut i31ref))))) (type $struct (sub (struct (field (mut anyref))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $work (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new_default $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (ref.i31 ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (drop (struct.new_default $struct) ) (struct.set $struct 0 (local.get $struct) (ref.i31 (i32.const 0)) ) ) ) (module ;; Multiple writes to a field, with a LUB that is not equal to any of them. ;; We can at least improve from structref to a ref of $struct here. Note also ;; that we do so in all three types, not just the parent to which we write ;; (the children have no writes, but must still be updated). (rec ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $struct)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $child-B (sub $struct (struct (field (mut (ref $struct)))))) ;; CHECK: (type $child-A (sub $struct (struct (field (mut (ref $struct)))))) (type $child-A (sub $struct (struct (field (mut structref))))) (type $child-B (sub $struct (struct (field (mut structref))))) ) ;; CHECK: (type $3 (func (param (ref $struct) (ref $child-A) (ref $child-B)))) ;; CHECK: (func $work (type $3) (param $struct (ref $struct)) (param $child-A (ref $child-A)) (param $child-B (ref $child-B)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $child-A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $child-B) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child-A (ref $child-A)) (param $child-B (ref $child-B)) (struct.set $struct 0 (local.get $struct) (local.get $child-A) ) (struct.set $struct 0 (local.get $struct) (local.get $child-B) ) ) ) (module ;; As above, but all writes are of $child-A, which allows more optimization ;; up to that type. (rec ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $child-A)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $child-A (sub $struct (struct (field (mut (ref $child-A)))))) (type $child-A (sub $struct (struct (field (mut structref))))) ;; CHECK: (type $child-B (sub $struct (struct (field (mut (ref $child-A)))))) (type $child-B (sub $struct (struct (field (mut structref))))) ) ;; CHECK: (type $3 (func)) ;; CHECK: (type $4 (func (param (ref $struct) (ref $child-A)))) ;; CHECK: (func $work (type $4) (param $struct (ref $struct)) (param $child-A (ref $child-A)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $child-A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $child-A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child-A (ref $child-A)) (struct.set $struct 0 (local.get $struct) (local.get $child-A) ) (struct.set $struct 0 (local.get $struct) (local.get $child-A) ) ) ;; CHECK: (func $keepalive (type $3) ;; CHECK-NEXT: (local $temp (ref null $child-B)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) (func $keepalive ;; Add a reference to $child-B just to keep it alive in the output for easier ;; comparisons to the previous testcase. Note that $child-B's field will be ;; refined, because its parent $struct forces it to be. (local $temp (ref null $child-B)) ) ) (module ;; Write to the parent a child, and to the child a parent. The write to the ;; child prevents specialization even in the parent and we only improve up to ;; $struct but not to $child. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $struct)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref $struct)))))) (type $child (sub $struct (struct (field (mut structref))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $work (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $child 0 ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child (ref $child)) (struct.set $struct 0 (local.get $struct) (local.get $child) ) (struct.set $child 0 (local.get $child) (local.get $struct) ) ) ) (module ;; As above, but both writes are of $child, so we can optimize. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $child)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref $child)))))) (type $child (sub $struct (struct (field (mut structref))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $work (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $child 0 ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child (ref $child)) (struct.set $struct 0 (local.get $struct) (local.get $child) ) (struct.set $child 0 (local.get $child) (local.get $child) ) ) ) (module ;; As in 2 testcases ago, write to the parent a child, and to the child a ;; parent, but now the writes happen in struct.new. Even with that precise ;; info, however, we can't make the parent field more specific than the ;; child's. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $struct)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref $struct)))))) (type $child (sub $struct (struct (field (mut structref))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $work (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $struct ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $child ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child (ref $child)) (drop (struct.new $struct (local.get $child) ) ) (drop (struct.new $child (local.get $struct) ) ) ) ) (module ;; Write a parent to the parent and a child to the child. We can specialize ;; each of them to contain their own type. This tests that we are aware that ;; a struct.new is of a precise type, which means that seeing a type written ;; to a parent does not limit specialization in a child. ;; ;; (Note that we can't do a similar test with struct.set, as that would ;; imply the fields are mutable, which limits optimization, see the next ;; testcase after this.) ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (ref $struct))))) (type $struct (sub (struct (field structref)))) ;; CHECK: (type $child (sub $struct (struct (field (ref $child))))) (type $child (sub $struct (struct (field structref)))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $work (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $struct ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $child ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child (ref $child)) (drop (struct.new $struct (local.get $struct) ) ) (drop (struct.new $child (local.get $child) ) ) ) ) (module ;; As above, but the fields are mutable. We cannot specialize them to ;; different types in this case, and both will become $struct (still an ;; improvement!) ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $struct)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref $struct)))))) (type $child (sub $struct (struct (field (mut structref))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $work (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $struct ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $child ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child (ref $child)) (drop (struct.new $struct (local.get $struct) ) ) (drop (struct.new $child (local.get $child) ) ) ) ) (module ;; As above, but the child also has a new field that is not in the parent. In ;; that case there is nothing stopping us from specializing that new field ;; to $child. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $struct)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref $struct))) (field (mut (ref $child)))))) (type $child (sub $struct (struct (field (mut structref)) (field (mut structref))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $work (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $struct ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $child ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (param $child (ref $child)) (drop (struct.new $struct (local.get $struct) ) ) (drop (struct.new $child (local.get $child) (local.get $child) ) ) ) ) (module ;; A copy of a field does not prevent optimization (even though it assigns ;; the old type). ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref $struct)))))) (type $struct (sub (struct (field (mut structref))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $work (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (struct.get $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (struct.set $struct 0 (local.get $struct) (local.get $struct) ) (struct.set $struct 0 (local.get $struct) (struct.get $struct 0 (local.get $struct) ) ) ) ) (module (rec ;; CHECK: (rec ;; CHECK-NEXT: (type $A (sub (struct (field (ref $Y))))) ;; CHECK: (type $B (sub $A (struct (field (ref $Y))))) ;; CHECK: (type $X (sub (struct ))) (type $X (sub (struct))) ;; CHECK: (type $Y (sub $X (struct ))) (type $Y (sub $X (struct))) (type $A (sub (struct (field (ref $X))))) ;; CHECK: (type $C (sub $A (struct (field (ref $Y))))) (type $C (sub $A (struct (field (ref $X))))) (type $B (sub $A (struct (field (ref $X))))) ) ;; CHECK: (type $5 (func)) ;; CHECK: (func $foo (type $5) ;; CHECK-NEXT: (local $unused (ref null $C)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $B ;; CHECK-NEXT: (struct.new_default $Y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $foo ;; A use of type $C without ever creating an instance of it. We do still need ;; to update the type if we update the parent type, and we will in fact update ;; the parent $A's field from $X to $Y (see below), so we must do the same in ;; $C. As a result, all the fields with $X in them in all of $A, $B, $C will ;; be improved to contain $Y. (local $unused (ref null $C)) (drop (struct.new $B (struct.new $Y) ;; This value is more specific than the field, which is an ;; opportunity to subtype, which we do for $B. As $A, our ;; parent, has no writes at all, we can propagate this ;; info to there as well, which means we can perform the ;; same optimization in $A as well. ) ) ) ) (module ;; As above, but remove the struct.new to $B, which means $A, $B, $C all have ;; no writes to them. There are no optimizations to do here. (rec ;; CHECK: (rec ;; CHECK-NEXT: (type $X (sub (struct ))) (type $X (sub (struct))) ;; CHECK: (type $Y (sub $X (struct ))) (type $Y (sub $X (struct))) ;; CHECK: (type $A (sub (struct (field (ref $X))))) (type $A (sub (struct (field (ref $X))))) ;; CHECK: (type $B (sub $A (struct (field (ref $X))))) (type $B (sub $A (struct (field (ref $X))))) ;; CHECK: (type $C (sub $A (struct (field (ref $X))))) (type $C (sub $A (struct (field (ref $X))))) ) ;; CHECK: (type $5 (func)) ;; CHECK: (func $foo (type $5) ;; CHECK-NEXT: (local $unused1 (ref null $C)) ;; CHECK-NEXT: (local $unused2 (ref null $B)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new_default $Y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $foo (local $unused1 (ref null $C)) (local $unused2 (ref null $B)) (drop (struct.new $Y)) ) ) (module ;; CHECK: (type $X (sub (struct ))) (type $X (sub (struct))) ;; CHECK: (type $1 (func)) ;; CHECK: (type $A (sub (struct (field (ref $X))))) ;; CHECK: (type $Y (sub $X (struct ))) (type $Y (sub $X (struct))) (type $A (sub (struct (field (ref $X))))) ;; CHECK: (type $B (sub $A (struct (field (ref $Y))))) (type $B (sub $A (struct (field (ref $Y))))) ;; CHECK: (func $foo (type $1) ;; CHECK-NEXT: (local $unused2 (ref null $B)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (struct.new_default $X) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $foo ;; $B begins with its field of type $Y, which is more specific than the ;; field is in the supertype $A. There are no writes to $B, and so we end ;; up looking in the parent to see what to do; we should still emit a ;; reasonable type for $B, and there is no reason to make it *less* ;; specific, so leave things as they are. (local $unused2 (ref null $B)) (drop (struct.new $A (struct.new $X) ) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref null $struct)))))) (type $struct (sub (struct (field (mut (ref null struct)))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $update-null (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $update-null (param $struct (ref $struct)) (struct.set $struct 0 (local.get $struct) ;; Write a $struct to the field. (local.get $struct) ) (struct.set $struct 0 (local.get $struct) ;; This null does not prevent refinement. (ref.null none) ) ) ) (module ;; As above, but now the null is in a child. The result should be the same: ;; refine the field to nullable $struct. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref null $struct)))))) (type $struct (sub (struct (field (mut (ref null struct)))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref null $struct)))))) (type $child (sub $struct (struct (field (mut (ref null struct)))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $update-null (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $child 0 ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $update-null (param $struct (ref $struct)) (param $child (ref $child)) (struct.set $struct 0 (local.get $struct) (local.get $struct) ) (struct.set $child 0 (local.get $child) (ref.null none) ) ) ) (module ;; As above, but now the null is in a parent. The result should be the same. ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref null $struct)))))) (type $struct (sub (struct (field (mut (ref null struct)))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref null $struct)))))) (type $child (sub $struct (struct (field (mut (ref null struct)))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $update-null (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $child 0 ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $update-null (param $struct (ref $struct)) (param $child (ref $child)) (struct.set $struct 0 (local.get $struct) (ref.null none) ) (struct.set $child 0 (local.get $child) (local.get $struct) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut nullref))))) (type $struct (sub (struct (field (mut (ref null struct)))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $work (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new_default $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) ;; The only write to this struct is of a null default value, so we can ;; optimize to nullref. (drop (struct.new_default $struct) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref null $struct)))))) (type $struct (sub (struct (field (mut (ref null struct)))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $work (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new_default $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) (drop (struct.new_default $struct) ) ;; Also write a $struct. The null default should not prevent us from ;; refining the field's type to $struct (but nullable). (struct.set $struct 0 (local.get $struct) (local.get $struct) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref null $struct)))))) (type $struct (sub (struct (field (mut (ref null struct)))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $work (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $struct ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) ;; As before, but instead of new_default, new, and use a null in the given ;; value. (drop (struct.new $struct (ref.null none) ) ) (struct.set $struct 0 (local.get $struct) (local.get $struct) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (sub (struct (field (mut (ref null $child))) (field (mut (ref null $struct)))))) (type $struct (sub (struct (field (mut (ref null struct))) (field (mut (ref null struct)))))) ;; CHECK: (type $child (sub $struct (struct (field (mut (ref null $child))) (field (mut (ref null $struct)))))) (type $child (sub $struct (struct (field (mut (ref null struct))) (field (mut (ref null struct)))))) ;; CHECK: (type $2 (func (param (ref $struct) (ref $child)))) ;; CHECK: (func $update-null (type $2) (param $struct (ref $struct)) (param $child (ref $child)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $struct ;; CHECK-NEXT: (local.get $child) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $struct ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $update-null (param $struct (ref $struct)) (param $child (ref $child)) ;; Update nulls in two fields that are separately optimized to separate ;; values. (drop (struct.new $struct (local.get $child) (ref.null none) ) ) (drop (struct.new $struct (ref.null none) (local.get $struct) ) ) ) ) (module ;; There are two parallel type hierarchies here: "Outer", which are objects ;; that have fields, that contain the "Inner" objects. ;; ;; Root-Outer -> Leaf1-Outer ;; -> Leaf2-Outer ;; ;; Root-Inner -> Leaf1-Inner ;; -> Leaf2-Inner ;; ;; Adding their contents, where X[Y] means X has a field of type Y: ;; ;; Root-Outer[Root-Inner] -> Leaf1-Outer[Leaf1-Inner] ;; -> Leaf2-Outer[Leaf2-Inner] ;; CHECK: (rec ;; CHECK-NEXT: (type $Root-Inner (sub (struct ))) (type $Root-Inner (sub (struct))) ;; CHECK: (type $Leaf1-Inner (sub $Root-Inner (struct (field i32)))) (type $Leaf1-Inner (sub $Root-Inner (struct (field i32)))) ;; CHECK: (type $Root-Outer (sub (struct (field (ref $Leaf2-Inner))))) ;; CHECK: (type $Leaf1-Outer (sub $Root-Outer (struct (field (ref $Leaf2-Inner))))) ;; CHECK: (type $Leaf2-Outer (sub $Root-Outer (struct (field (ref $Leaf2-Inner))))) ;; CHECK: (type $Leaf2-Inner (sub $Root-Inner (struct ))) (type $Leaf2-Inner (sub $Root-Inner (struct ))) (type $Root-Outer (sub (struct (field (ref $Root-Inner))))) (type $Leaf1-Outer (sub $Root-Outer (struct (field (ref $Leaf1-Inner))))) (type $Leaf2-Outer (sub $Root-Outer (struct (field (ref $Leaf2-Inner))))) ;; CHECK: (type $6 (func (param (ref null $Leaf1-Outer)))) ;; CHECK: (func $func (type $6) (param $Leaf1-Outer (ref null $Leaf1-Outer)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $Leaf1-Outer) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $Leaf2-Outer ;; CHECK-NEXT: (struct.new_default $Leaf2-Inner) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $func (param $Leaf1-Outer (ref null $Leaf1-Outer)) (drop ;; The situation here is that we have only a get for some types, and no ;; other constraints. As we ignore gets, we work under no constraints at ;; We then have to pick some type, so we pick the one used by our ;; supertype - and the supertype might have picked up a type from another ;; branch of the type tree, which is not a subtype of ours. ;; ;; In more detail, we never create an instance of $Leaf1-Outer, and we ;; only have a get of its field. This optimization ignores the get (to not ;; be limited by it). It will then optimize $Leaf1-Outer's field of ;; $Leaf1-Inner (another struct for which we have no creation, and only a ;; get) into $Leaf2-Inner, which is driven by the fact that we do have a ;; creation of $Leaf2-Inner. But then this struct.get $Leaf1-Inner on field ;; 0 is no longer valid, as we turn $Leaf1-Inner => $Leaf2-Inner, and ;; $Leaf2-Inner has no field 0. To keep the module validating, we must not ;; emit that. Instead, since there can be no instance of $Leaf1-Inner (as ;; mentioned before, it is never created, nor anything that can be cast to ;; it), we know this code is logically unreachable, and can emit an ;; unreachable here. (struct.get $Leaf1-Inner 0 (struct.get $Leaf1-Outer 0 (local.get $Leaf1-Outer) ) ) ) (drop (struct.new $Leaf2-Outer (struct.new_default $Leaf2-Inner) ) ) ) ) (module ;; CHECK: (type $A (sub (struct (field (mut (ref null $A)))))) (type $A (sub (struct (field (mut (ref null $A)))))) ;; CHECK: (type $1 (func (param (ref $A) (ref null $A)))) ;; CHECK: (func $non-nullability (type $1) (param $nn (ref $A)) (param $A (ref null $A)) ;; CHECK-NEXT: (local $temp (ref null $A)) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: (local.get $nn) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: (local.tee $temp ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (local.tee $temp ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $non-nullability (param $nn (ref $A)) (param $A (ref null $A)) (local $temp (ref null $A)) ;; Set a non-null value to the field. (struct.set $A 0 (local.get $A) (local.get $nn) ) ;; Set a get of the same field to the field - this is a copy. However, the ;; copy goes through a local.tee. Even after we refine the type of the field ;; to non-nullable, the tee will remain nullable since it has the type of ;; the local. We could add casts perhaps, but for now we do not optimize, ;; and type $A's field will remain nullable. (struct.set $A 0 (local.get $A) (local.tee $temp (struct.get $A 0 (local.get $A) ) ) ) ;; The same, but with a struct.new. (drop (struct.new $A (local.tee $temp (struct.get $A 0 (local.get $A) ) ) ) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $A (sub (struct (field (ref null $A))))) (type $A (sub (struct (field (ref null $A))))) ;; CHECK: (type $B (sub $A (struct (field (ref null $B))))) (type $B (sub $A (struct (field (ref null $A))))) ;; CHECK: (type $2 (func (param (ref null $B) (ref null $A)))) ;; CHECK: (func $heap-type (type $2) (param $b (ref null $B)) (param $A (ref null $A)) ;; CHECK-NEXT: (local $a (ref null $A)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $B ;; CHECK-NEXT: (local.get $b) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (local.tee $a ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $heap-type (param $b (ref null $B)) (param $A (ref null $A)) (local $a (ref null $A)) ;; Similar to the above, but instead of non-nullability being the issue, ;; now it is the heap type. We write a B to B's field, so we can trivially ;; refine that, and we want to do a similar refinement to the supertype A. ;; But below we do a copy on A through a tee. As above, the tee's type will ;; not change, so we do not optimize type $A's field (however, we can ;; refine $B's field, which is safe to do). (drop (struct.new $B (local.get $b) ) ) (drop (struct.new $A (local.tee $a (struct.get $A 0 (local.get $A) ) ) ) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $A (sub (struct (field (mut (ref $A)))))) (type $A (sub (struct (field (mut (ref null $A)))))) ;; CHECK: (type $1 (func (param (ref $A) (ref null $A)))) ;; CHECK: (func $non-nullability-block (type $1) (param $nn (ref $A)) (param $A (ref null $A)) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: (local.get $nn) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: (if (result (ref $A)) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (else ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (if (result (ref $A)) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (local.get $A) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (else ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $non-nullability-block (param $nn (ref $A)) (param $A (ref null $A)) (struct.set $A 0 (local.get $A) (local.get $nn) ) ;; As above, but instead of a local.tee fallthrough, use an if. We *can* ;; optimize in this case, as ifs etc do not pose a problem (we'll refinalize ;; the ifs to the proper, non-nullable type, the same as the field). (struct.set $A 0 (local.get $A) (if (result (ref null $A)) (i32.const 1) (then (struct.get $A 0 (local.get $A) ) ) (else (unreachable) ) ) ) (drop (struct.new $A (if (result (ref null $A)) (i32.const 1) (then (struct.get $A 0 (local.get $A) ) ) (else (unreachable) ) ) ) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $struct (struct (field (mut (ref $struct))))) (type $struct (struct (field (mut (ref null struct))))) ;; CHECK: (type $1 (func (param (ref $struct)))) ;; CHECK: (func $work (type $1) (param $struct (ref $struct)) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $struct 0 ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $work (param $struct (ref $struct)) ;; The only set to the field is (ref $struct), so we can refine to that. (struct.set $struct 0 (local.get $struct) (local.get $struct) ) ;; After refining, we must update the get's type properly. ReFinalize by ;; itself would hit a problem here, as it first turns the block's result to ;; a bottom type, after which it can't figure out how to update the ;; struct.get. TypeRefining should handle that internally by updating all ;; struct.gets itself based on the changes it is making. ;; ;; Note that this problem depends on the recursion of a struct.get feeding ;; into a struct.set: after the refining, the struct.set will only ;; validate if we provide it the *refined* type for the field. (struct.set $struct 0 (local.get $struct) (struct.get $struct 0 (block (result (ref null $struct)) (ref.null none) ) ) ) ) ) (module (rec ;; CHECK: (rec ;; CHECK-NEXT: (type $A (struct (field (ref $B)))) (type $A (struct (field (ref struct)))) ;; CHECK: (type $B (struct )) (type $B (struct)) ) ;; CHECK: (type $2 (func (result (ref $A)))) ;; CHECK: (func $0 (type $2) (result (ref $A)) ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (ref.cast (ref $B) ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (struct.new_default $B) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $0 (result (ref $A)) ;; The cast in the middle here will be skipped in the analysis, as we have ;; a copy: a read from $A.0 to a write, with only a cast in the middle ;; (which does not alter the value). While that is valid to do, we need to ;; properly propagate the new output type of the struct.get (which goes from ;; (ref struct) to (ref $B) to the cast, so that the cast has that type as ;; well, as otherwise the struct.new will not validate - we can't write a ;; (ref struct) to a field of (ref $B). (struct.new $A (ref.cast (ref struct) (struct.get $A 0 (struct.new $A (struct.new_default $B) ) ) ) ) ) ) (module (rec ;; CHECK: (rec ;; CHECK-NEXT: (type $A (struct (field (mut nullref)))) (type $A (struct (field (mut anyref)))) ;; CHECK: (type $B (struct (field (mut nullref)))) (type $B (struct (field (mut (ref null $A))))) ) ;; CHECK: (type $2 (func)) ;; CHECK: (func $0 (type $2) ;; CHECK-NEXT: (block ;; (replaces unreachable StructSet we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block ;; (replaces unreachable StructNew we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (struct.get $B 0 ;; CHECK-NEXT: (struct.new_default $B) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $0 (struct.set $A 0 (struct.new $A ;; These two struct.gets will both be refined. That of $B will be ;; refined to a get of a null, at which point the get of $A could get ;; confused and not know what type it is reading from (since in the IR, ;; we depend on the ref for that). The pass should not error here while ;; it refines both the struct type's fields to nullref, after which the ;; code here will be unreachable (since we do a struct.get from a ;; nullref). (struct.get $A 0 (struct.get $B 0 (struct.new_default $B) ) ) ) (ref.null none) ) ) ) (module ;; CHECK: (rec ;; CHECK-NEXT: (type $0 (func (param (ref noextern)))) ;; CHECK: (type $1 (func (param (ref none) (ref noextern)))) ;; CHECK: (type $2 (func (param (ref $A) externref))) ;; CHECK: (type $A (struct (field (mut (ref noextern))))) (type $A (struct (field (mut externref)))) ;; CHECK: (type $4 (func (param externref) (result anyref))) ;; CHECK: (type $5 (func)) ;; CHECK: (type $6 (func)) ;; CHECK: (tag $tag) (tag $tag) ;; CHECK: (func $struct.new (type $4) (param $extern externref) (result anyref) ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (ref.cast (ref noextern) ;; CHECK-NEXT: (try $try (result externref) ;; CHECK-NEXT: (do ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (ref.as_non_null ;; CHECK-NEXT: (ref.null noextern) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (catch $tag ;; CHECK-NEXT: (local.get $extern) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $struct.new (param $extern externref) (result anyref) ;; A noextern is written into the struct field and then read. Note that the ;; try's catch is never reached, since the body cannot throw, so the ;; fallthrough of the try is the struct.get, which leads into a struct.new, so ;; we have a copy of that field. For that reason TypeRefining thinks it can ;; refine the type of the field from externref to noextern. However, the ;; validation rule for try-catch prevents the try from being refined so, ;; since the catch has to be taken into account, and it has a less refined ;; type than the body. ;; ;; In such situations we rely on other optimizations to improve things, like ;; getting rid of the catch in this case. In this pass we add a cast to get ;; things to validate, which should be removable by other passes later on. (struct.new $A (try (result externref) (do (struct.get $A 0 (struct.new $A (ref.as_non_null (ref.null noextern) ) ) ) ) (catch $tag (local.get $extern) ) ) ) ) ;; CHECK: (func $struct.set (type $2) (param $ref (ref $A)) (param $extern externref) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: (ref.cast (ref noextern) ;; CHECK-NEXT: (try $try (result externref) ;; CHECK-NEXT: (do ;; CHECK-NEXT: (struct.get $A 0 ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (ref.as_non_null ;; CHECK-NEXT: (ref.null noextern) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (catch $tag ;; CHECK-NEXT: (local.get $extern) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $struct.set (param $ref (ref $A)) (param $extern externref) (struct.set $A 0 (local.get $ref) (try (result externref) (do (struct.get $A 0 (struct.new $A (ref.as_non_null (ref.null noextern) ) ) ) ) (catch $tag (local.get $extern) ) ) ) ) ;; CHECK: (func $bottom.type (type $1) (param $ref (ref none)) (param $value (ref noextern)) ;; CHECK-NEXT: (block ;; (replaces unreachable StructSet we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $ref) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $value) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $bottom.type (param $ref (ref none)) (param $value (ref noextern)) ;; The reference here is a bottom type, which we should not crash on. (struct.set $A 0 (local.get $ref) (local.get $value) ) ) ;; CHECK: (func $unreachable (type $0) (param $value (ref noextern)) ;; CHECK-NEXT: (block ;; (replaces unreachable StructSet we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $value) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $unreachable (param $value (ref noextern)) ;; The reference here is unreachable, which we should not crash on. (struct.set $A 0 (unreachable) (local.get $value) ) ) )