@@ -2093,6 +2093,157 @@ pub(crate) mod hir_build_tests {
20932093 " ) ;
20942094 }
20952095
2096+ #[ test]
2097+ fn test_send_reloads_referenced_block_param ( ) {
2098+ eval ( "
2099+ def take(x) = x
2100+ def consume = yield
2101+ def test(&block)
2102+ consume { take(block) }
2103+ block
2104+ end
2105+ test { 1 }
2106+ " ) ;
2107+ assert_contains_opcode ( "test" , YARVINSN_send ) ;
2108+ // The block reads `block` (passed as a regular argument), so it references the
2109+ // block param. `getblockparam` is recorded as a read, but reading the block param
2110+ // materializes the captured block into its slot, so the block param must be
2111+ // reloaded after the call (this is the lazy_load_hooks miscompile scenario).
2112+ assert_snapshot ! ( hir_string( "test" ) , @"
2113+ fn test@<compiled>:5:
2114+ bb1():
2115+ EntryPoint interpreter
2116+ v1:BasicObject = LoadSelf
2117+ v2:CPtr = LoadSP
2118+ v3:BasicObject = LoadField v2, :block@0x1000
2119+ Jump bb3(v1, v3)
2120+ bb2():
2121+ EntryPoint JIT(0)
2122+ v6:BasicObject = LoadArg :self@0
2123+ v7:BasicObject = LoadArg :block@1
2124+ Jump bb3(v6, v7)
2125+ bb3(v9:BasicObject, v10:BasicObject):
2126+ v15:BasicObject = Send v9, 0x1008, :consume # SendFallbackReason: Uncategorized(send)
2127+ PatchPoint NoEPEscape(test)
2128+ v18:CPtr = LoadSP
2129+ v19:BasicObject = LoadField v18, :block@0x1000
2130+ v24:CPtr = GetEP 0
2131+ v25:CUInt64 = LoadField v24, :VM_ENV_DATA_INDEX_FLAGS@0x1030
2132+ v26:CBool = IsBlockParamModified v25
2133+ CondBranch v26, bb4(), bb5()
2134+ bb4():
2135+ v28:BasicObject = LoadField v24, :block@0x1031
2136+ Jump bb6(v28)
2137+ bb5():
2138+ v30:BasicObject = GetBlockParam :block, l0, EP@3
2139+ Jump bb6(v30)
2140+ bb6(v23:BasicObject):
2141+ CheckInterrupts
2142+ Return v23
2143+ " ) ;
2144+ }
2145+
2146+ #[ test]
2147+ fn test_send_does_not_reload_unreferenced_block_param ( ) {
2148+ eval ( "
2149+ def consume = yield
2150+ def test(&block)
2151+ a = 1
2152+ consume { a }
2153+ block
2154+ end
2155+ test { 1 }
2156+ " ) ;
2157+ assert_contains_opcode ( "test" , YARVINSN_send ) ;
2158+ // The block only references `a`, never the block param, so the block param
2159+ // cannot have been materialized by the call and is not reloaded. (Before the
2160+ // reload filter was refined, the block param was reloaded after every
2161+ // send-with-block, even when the block could not have touched it.)
2162+ assert_snapshot ! ( hir_string( "test" ) , @"
2163+ fn test@<compiled>:4:
2164+ bb1():
2165+ EntryPoint interpreter
2166+ v1:BasicObject = LoadSelf
2167+ v2:CPtr = LoadSP
2168+ v3:BasicObject = LoadField v2, :block@0x1000
2169+ v4:NilClass = Const Value(nil)
2170+ Jump bb3(v1, v3, v4)
2171+ bb2():
2172+ EntryPoint JIT(0)
2173+ v7:BasicObject = LoadArg :self@0
2174+ v8:BasicObject = LoadArg :block@1
2175+ v9:NilClass = Const Value(nil)
2176+ Jump bb3(v7, v8, v9)
2177+ bb3(v11:BasicObject, v12:BasicObject, v13:NilClass):
2178+ v17:Fixnum[1] = Const Value(1)
2179+ v22:BasicObject = Send v11, 0x1008, :consume # SendFallbackReason: Uncategorized(send)
2180+ PatchPoint NoEPEscape(test)
2181+ v29:CPtr = GetEP 0
2182+ v30:CUInt64 = LoadField v29, :VM_ENV_DATA_INDEX_FLAGS@0x1030
2183+ v31:CBool = IsBlockParamModified v30
2184+ CondBranch v31, bb4(), bb5()
2185+ bb4():
2186+ v33:BasicObject = LoadField v29, :block@0x1031
2187+ Jump bb6(v33)
2188+ bb5():
2189+ v35:BasicObject = GetBlockParam :block, l0, EP@4
2190+ Jump bb6(v35)
2191+ bb6(v28:BasicObject):
2192+ CheckInterrupts
2193+ Return v28
2194+ " ) ;
2195+ }
2196+
2197+ #[ test]
2198+ fn test_send_with_anonymous_block_param ( ) {
2199+ eval ( "
2200+ def consume = yield
2201+ def test(&)
2202+ consume { consume(&) }
2203+ consume(&)
2204+ end
2205+ test { 1 }
2206+ " ) ;
2207+ assert_contains_opcode ( "test" , YARVINSN_send ) ;
2208+ // An anonymous `&` block param can only be forwarded with `&`, which compiles to
2209+ // `getblockparamproxy` and reads the block from the EP. It never materializes the
2210+ // param into its slot, so the block param is read directly from the EP after the
2211+ // call and is not reloaded -- there is nothing a reload could recover.
2212+ assert_snapshot ! ( hir_string( "test" ) , @"
2213+ fn test@<compiled>:4:
2214+ bb1():
2215+ EntryPoint interpreter
2216+ v1:BasicObject = LoadSelf
2217+ v2:CPtr = LoadSP
2218+ v3:BasicObject = LoadField v2, :&@0x1000
2219+ Jump bb3(v1, v3)
2220+ bb2():
2221+ EntryPoint JIT(0)
2222+ v6:BasicObject = LoadArg :self@0
2223+ v7:BasicObject = LoadArg :&@1
2224+ Jump bb3(v6, v7)
2225+ bb3(v9:BasicObject, v10:BasicObject):
2226+ v15:BasicObject = Send v9, 0x1008, :consume # SendFallbackReason: Uncategorized(send)
2227+ PatchPoint NoEPEscape(test)
2228+ v24:CPtr = GetEP 0
2229+ v25:CUInt64 = LoadField v24, :VM_ENV_DATA_INDEX_FLAGS@0x1030
2230+ v26:CBool = IsBlockParamModified v25
2231+ CondBranch v26, bb4(), bb5()
2232+ bb4():
2233+ v28:BasicObject = LoadField v24, :&@0x1031
2234+ Jump bb6(v28, v28)
2235+ bb5():
2236+ v30:CInt64 = LoadField v24, :VM_ENV_DATA_INDEX_SPECVAL@0x1032
2237+ v31:CInt64 = GuardAnyBitSet v30, CUInt64(1) recompile
2238+ v32:ObjectSubclass[BlockParamProxy] = Const Value(VALUE(0x1038))
2239+ Jump bb6(v32, v10)
2240+ bb6(v22:BasicObject, v23:BasicObject):
2241+ v35:BasicObject = Send v9, &block, :consume, v22 # SendFallbackReason: Uncategorized(send)
2242+ CheckInterrupts
2243+ Return v35
2244+ " ) ;
2245+ }
2246+
20962247 #[ test]
20972248 fn test_send_reloads_local_written_by_nested_block ( ) {
20982249 eval ( "
0 commit comments