From d9427f70d2002be58e80ade55320f14cd9a59079 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sat, 5 Aug 2023 02:38:14 +0000 Subject: [PATCH 01/14] Refine effects based on optimizer-derived information The optimizer may be able to derive information that is not available to inference. For example, it may SROA a mutable value to derive additional constant information. Additionally, some effects, like :consistent are path-dependent and should ideally be scanned once all optimizations are done. Now, there is a bit of a complication that we have generally so far taken the position that the optimizer may do non-IPO-safe optimizations, although in practice we never actually implemented any. This was a sensible choice, because we weren't really doing anything with the post-optimized IR other than feeding it into codegen anyway. However, with irinterp and this change, there's now two consumers of IPO-safely optimized IR. I do still think we may at some point want to run passes that allow IPO-unsafe optimizations, but we can always add them at the end of the pipeline. With these changes, the effect analysis is a lot more precise. For example, we can now derive :consistent for these functions: ``` function f1(b) if Base.inferencebarrier(b) error() end return b end function f3(x) @fastmath sqrt(x) return x end ``` and we can derive `:nothrow` for this function: ``` function f2() if Ref(false)[] error() end return true end ``` --- base/compiler/abstractinterpretation.jl | 4 + base/compiler/bootstrap.jl | 2 +- base/compiler/optimize.jl | 173 ++++++++++++++++++- base/compiler/ssair/inlining.jl | 6 +- base/compiler/ssair/ir.jl | 24 ++- base/compiler/ssair/irinterp.jl | 215 +++++++++++++----------- base/compiler/ssair/slot2ssa.jl | 11 +- base/compiler/ssair/verify.jl | 2 +- base/compiler/typeinfer.jl | 2 +- base/reflection.jl | 4 +- test/compiler/effects.jl | 26 ++- test/compiler/inference.jl | 2 +- test/compiler/invalidation.jl | 2 +- test/compiler/irpasses.jl | 2 +- 14 files changed, 350 insertions(+), 125 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 0eb4631e2c7dd..9b73a4cd763c5 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -2667,6 +2667,10 @@ function handle_global_assignment!(interp::AbstractInterpreter, frame::Inference effect_free = ALWAYS_FALSE nothrow = global_assignment_nothrow(lhs.mod, lhs.name, newty) inaccessiblememonly = ALWAYS_FALSE + if !nothrow + sub_curr_ssaflag!(frame, IR_FLAG_NOTHROW) + end + sub_curr_ssaflag!(frame, IR_FLAG_EFFECT_FREE) merge_effects!(interp, frame, Effects(EFFECTS_TOTAL; effect_free, nothrow, inaccessiblememonly)) return nothing end diff --git a/base/compiler/bootstrap.jl b/base/compiler/bootstrap.jl index 1f62d21c9d2d9..6575ad4f72ede 100644 --- a/base/compiler/bootstrap.jl +++ b/base/compiler/bootstrap.jl @@ -13,7 +13,7 @@ let interp = NativeInterpreter() fs = Any[ # we first create caches for the optimizer, because they contain many loop constructions # and they're better to not run in interpreter even during bootstrapping - #=analyze_escapes_tt,=# run_passes, + #=analyze_escapes_tt,=# run_passes_ipo_safe, # then we create caches for inference entries typeinf_ext, typeinf, typeinf_edge, ] diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 53463f13d5faf..a51b4eba9920b 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -331,8 +331,10 @@ function stmt_effect_flags(𝕃ₒ::AbstractLattice, @nospecialize(stmt), @nospe return (false, false, false) end return (false, true, true) - elseif head === :isdefined || head === :the_exception || head === :copyast || head === :inbounds || head === :boundscheck + elseif head === :isdefined || head === :the_exception || head === :copyast || head === :inbounds return (true, true, true) + elseif head === :boundscheck + return (false, true, true) else # e.g. :loopinfo return (false, false, false) @@ -453,9 +455,172 @@ function finish(interp::AbstractInterpreter, opt::OptimizationState, return nothing end +function visit_bb_phis!(callback, ir::IRCode, bb::Int) + stmts = ir.cfg.blocks[bb].stmts + for idx in stmts + stmt = ir[SSAValue(idx)][:inst] + if !isa(stmt, PhiNode) + if !is_valid_phiblock_stmt(stmt) + return + end + else + callback(idx, stmt) + end + end +end + +function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result::InferenceResult) + inconsistent = BitSet() + inconsistent_bbs = BitSet() + tpdum = TwoPhaseDefUseMap(length(ir.stmts)) + + all_effect_free = true # TODO refine using EscapeAnalysis + all_nothrow = true + all_retpaths_consistent = true + had_trycatch = false + + scanner = BBScanner(ir) + + cfg = nothing + domtree = nothing + function get_augmented_domtree() + if cfg !== nothing + return (cfg, domtree) + end + cfg = copy(ir.cfg) + # Add a virtual basic block to represent the exit + push!(cfg.blocks, BasicBlock(StmtRange(0:-1))) + + for bb = 1:(length(cfg.blocks)-1) + terminator = ir[SSAValue(last(cfg.blocks[bb].stmts))][:inst] + if isa(terminator, ReturnNode) && isdefined(terminator, :val) + cfg_insert_edge!(cfg, bb, length(cfg.blocks)) + end + end + + domtree = construct_domtree(cfg.blocks) + return (cfg, domtree) + end + + + function scan_non_dataflow_flags!(flag) + all_effect_free &= (flag & IR_FLAG_EFFECT_FREE) != 0 + all_nothrow &= (flag & IR_FLAG_NOTHROW) != 0 + end + + function scan_inconsistency!(flag, idx, @nospecialize(stmt)) + stmt_inconsistent = (flag & IR_FLAG_CONSISTENT) == 0 + for ur in userefs(stmt) + val = ur[] + if isa(val, SSAValue) + stmt_inconsistent |= val.id in inconsistent + count!(tpdum, val) + elseif isexpr(val, :boundscheck) + stmt_inconsistent = true + end + end + stmt_inconsistent && push!(inconsistent, idx) + return stmt_inconsistent + end + + function scan_stmt!(inst, idx, lstmt, bb) + stmt = inst[:inst] + flag = inst[:flag] + + if isexpr(stmt, :enter) + # try/catch not yet modeled + had_trycatch = true + return true + end + + scan_non_dataflow_flags!(flag) + stmt_inconsistent = scan_inconsistency!(flag, idx, stmt) + + if idx == lstmt + if isa(stmt, ReturnNode) && isdefined(stmt, :val) && stmt_inconsistent + all_retpaths_consistent = false + elseif isa(stmt, GotoIfNot) && stmt_inconsistent + # Conditional Branch with inconsistent condition. + sa, sb = ir.cfg.blocks[bb].succs + (cfg, domtree) = get_augmented_domtree() + for succ in iterated_dominance_frontier(cfg, BlockLiveness((sa, sb), nothing), domtree) + if succ == length(cfg.blocks) + # Phi node in the virtual exit -> We have a conditional + # return. TODO: Check if all the retvals are egal. + all_retpaths_consistent = false + else + visit_bb_phis!(ir, succ) do phiidx::Int, phi::PhiNode + push!(inconsistent, phiidx) + end + end + end + end + end + + return all_retpaths_consistent + end + if !scan!(scan_stmt!, scanner, true) + if !all_retpaths_consistent + # No longer any dataflow concerns, just scan the flags + scan!(scanner, false) do inst, idx, lstmt, bb + flag = inst[:flag] + scan_non_dataflow_flags!(flag) + end + else + scan!(scan_stmt!, scanner, true) + complete!(tpdum); push!(scanner.bb_ip, 1) + populate_def_use_map!(tpdum, scanner) + for def in inconsistent + for use in tpdum[def] + if !(use in inconsistent) + push!(inconsistent, use) + append!(stmt_ip, tpdum[use]) + end + end + end + + stmt_ip = BitSetBoundedMinPrioritySet(length(ir.stmts)) + while !isempty(stmt_ip) + idx = popfirst!(stmt_ip) + inst = ir[SSAValue(idx)] + stmt = inst[:inst] + if isa(stmt, ReturnNode) + all_retpaths_consistent = false + else isa(stmt, GotoIfNot) + bb = block_for_inst(ir, idx) + sa, sb = ir.cfg.blocks[bb].succs + for succ in iterated_dominance_frontier(ir.cfg, BlockLiveness((sa, sb), nothing), get!(lazydomtree)) + visit_bb_phis!(ir, succ) do (phiidx, phi) + push!(inconsistent, phiidx) + push!(stmt_ip, phiidx) + end + end + end + all_retpaths_consistent || break + append!(inconsistent, tpdum[idx]) + append!(stmt_ip, tpdum[idx]) + end + end + end + + had_trycatch && return + effects = result.ipo_effects + if all_effect_free + effects = Effects(effects; effect_free = true) + end + if all_nothrow + effects = Effects(effects; nothrow = true) + end + if all_retpaths_consistent + effects = Effects(effects; consistent = ALWAYS_TRUE) + end + result.ipo_effects = effects +end + # run the optimization work function optimize(interp::AbstractInterpreter, opt::OptimizationState, caller::InferenceResult) - @timeit "optimizer" ir = run_passes(opt.src, opt, caller) + @timeit "optimizer" ir = run_passes_ipo_safe(opt.src, opt, caller) + ipo_dataflow_analysis!(interp, ir, caller) return finish(interp, opt, ir, caller) end @@ -500,7 +665,7 @@ matchpass(optimize_until::Int, stage, _) = optimize_until == stage matchpass(optimize_until::String, _, name) = optimize_until == name matchpass(::Nothing, _, _) = false -function run_passes( +function run_passes_ipo_safe( ci::CodeInfo, sv::OptimizationState, caller::InferenceResult, @@ -517,7 +682,7 @@ function run_passes( @pass "compact 2" ir = compact!(ir) @pass "SROA" ir = sroa_pass!(ir, sv.inlining) @pass "ADCE" ir = adce_pass!(ir, sv.inlining) - @pass "compact 3" ir = compact!(ir) + @pass "compact 3" ir = compact!(ir, true) if JLOptions().debug_level == 2 @timeit "verify 3" (verify_ir(ir, true, false, optimizer_lattice(sv.inlining.interp)); verify_linetable(ir.linetable)) end diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 5343daa2646c1..3f20edc6bfb34 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1397,7 +1397,7 @@ function compute_inlining_cases(@nospecialize(info::CallInfo), flag::UInt8, sig: fully_covered &= split_fully_covered end - fully_covered || (joint_effects = Effects(joint_effects; nothrow=false)) + (handled_all_cases && fully_covered) || (joint_effects = Effects(joint_effects; nothrow=false)) if handled_all_cases && revisit_idx !== nothing # we handled everything except one match with unmatched sparams, @@ -1733,7 +1733,9 @@ function early_inline_special_case( isa(setting, Const) || return nothing setting = setting.val isa(setting, Symbol) || return nothing - setting === :const || setting === :conditional || setting === :type || return nothing + # setting === :const || setting === :type barrier const evaluation, + # so they can't be eliminated at IPO time + setting === :conditional || return nothing # barriered successfully already, eliminate it return SomeCase(stmt.args[3]) elseif f === Core.ifelse && length(argtypes) == 4 diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index b5b9d2a67f799..6eb902d2ab871 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -144,14 +144,30 @@ function compute_basic_blocks(stmts::Vector{Any}) end # this function assumes insert position exists +function is_valid_phiblock_stmt(@nospecialize(stmt)) + isa(stmt, PhiNode) && return true + isa(stmt, Union{UpsilonNode, PhiCNode, SSAValue}) && return false + isa(stmt, Expr) && return is_value_pos_expr_head(stmt.head) + return true +end + function first_insert_for_bb(code::Vector{Any}, cfg::CFG, block::Int) - for idx in cfg.blocks[block].stmts + stmts = cfg.blocks[block].stmts + lastnonphiidx = first(stmts) + for idx in stmts stmt = code[idx] if !isa(stmt, PhiNode) - return idx + if !is_valid_phiblock_stmt(stmt) + return lastnonphiidx + end + else + lastnonphiidx = idx + 1 end end - error("any insert position isn't found") + if lastnonphiidx > last(stmts) + error("any insert position isn't found") + end + return lastnonphiidx end # SSA values that need renaming @@ -1390,7 +1406,7 @@ function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instr if cfg_transforms_enabled # Rename phi node edges let bb_rename_pred=bb_rename_pred - map!(i::Int32->bb_rename_pred[i], stmt.edges, stmt.edges) + map!(i::Int32->i == 0 ? 0 : bb_rename_pred[i], stmt.edges, stmt.edges) end # Remove edges and values associated with dead blocks. Entries in diff --git a/base/compiler/ssair/irinterp.jl b/base/compiler/ssair/irinterp.jl index ec3b769a9992a..2bbd802e8d539 100644 --- a/base/compiler/ssair/irinterp.jl +++ b/base/compiler/ssair/irinterp.jl @@ -178,12 +178,8 @@ function reprocess_instruction!(interp::AbstractInterpreter, idx::Int, bb::Union end # Process the terminator and add the successor to `bb_ip`. Returns whether a backedge was seen. -function process_terminator!(ir::IRCode, @nospecialize(stmt), idx::Int, bb::Int, - all_rets::Vector{Int}, bb_ip::BitSetBoundedMinPrioritySet) +function process_terminator!(@nospecialize(stmt), bb::Int, bb_ip::BitSetBoundedMinPrioritySet) if isa(stmt, ReturnNode) - if isdefined(stmt, :val) - push!(all_rets, idx) - end return false elseif isa(stmt, GotoNode) backedge = stmt.label <= bb @@ -206,134 +202,151 @@ function process_terminator!(ir::IRCode, @nospecialize(stmt), idx::Int, bb::Int, end end -function _ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IRInterpretationState; - externally_refined::Union{Nothing,BitSet} = nothing) - interp = switch_to_irinterp(interp) - - (; ir, tpdum, ssa_refined) = irsv +struct BBScanner + ir::IRCode + bb_ip::BitSetBoundedMinPrioritySet +end +function BBScanner(ir::IRCode) bbs = ir.cfg.blocks bb_ip = BitSetBoundedMinPrioritySet(length(bbs)) push!(bb_ip, 1) - all_rets = Int[] + return BBScanner(ir, bb_ip) +end - # Fast path: Scan both use counts and refinement in one single pass of - # of the instructions. In the absence of backedges, this will - # converge. +function scan!(callback, scanner::BBScanner, forwards_only::Bool) + (; bb_ip, ir) = scanner + bbs = ir.cfg.blocks while !isempty(bb_ip) bb = popfirst!(bb_ip) stmts = bbs[bb].stmts lstmt = last(stmts) for idx = stmts - irsv.curridx = idx inst = ir[SSAValue(idx)] - stmt = inst[:stmt] - typ = inst[:type] - flag = inst[:flag] - any_refined = false - if (flag & IR_FLAG_REFINED) != 0 - any_refined = true - inst[:flag] &= ~IR_FLAG_REFINED - end - for ur in userefs(stmt) - val = ur[] - if isa(val, Argument) - any_refined |= irsv.argtypes_refined[val.n] - elseif isa(val, SSAValue) - any_refined |= val.id in ssa_refined - count!(tpdum, val) - end - end - if isa(stmt, PhiNode) && idx in ssa_refined - any_refined = true - delete!(ssa_refined, idx) - end - is_terminator_or_phi = isa(stmt, PhiNode) || isa(stmt, GotoNode) || isa(stmt, GotoIfNot) || isa(stmt, ReturnNode) || isexpr(stmt, :enter) - if typ === Bottom && (idx != lstmt || !is_terminator_or_phi) - continue + ret = callback(inst, idx, lstmt, bb) + ret === false && break + idx == lstmt && process_terminator!(inst[:inst], bb, bb_ip) && forwards_only && return false + end + end + return true +end + +function populate_def_use_map!(tpdum::TwoPhaseDefUseMap, scanner::BBScanner) + scan!(scanner, false) do inst, idx, lstmt, bb + for ur in userefs(inst) + val = ur[] + if isa(val, SSAValue) + push!(tpdum[val.id], idx) end - if (any_refined && reprocess_instruction!(interp, - idx, bb, stmt, typ, irsv)) || - (externally_refined !== nothing && idx in externally_refined) - push!(ssa_refined, idx) - stmt = inst[:stmt] - typ = inst[:type] + end + return true + end +end +populate_def_use_map!(tpdum::TwoPhaseDefUseMap, ir::IRCode) = + populate_def_use_map!(tpdum, BBScanner(ir)) + +function _ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IRInterpretationState; + externally_refined::Union{Nothing,BitSet} = nothing) + interp = switch_to_irinterp(interp) + + (; ir, tpdum, ssa_refined) = irsv + + all_rets = Int[] + scanner = BBScanner(ir) + + check_ret!(@nospecialize(stmt), idx::Int) = isa(stmt, ReturnNode) && isdefined(stmt, :val) && push!(all_rets, idx) + + # Fast path: Scan both use counts and refinement in one single pass of + # of the instructions. In the absence of backedges, this will + # converge. + completed_scan = scan!(scanner, true) do inst, idx, lstmt, bb + irsv.curridx = idx + stmt = ir.stmts[idx][:inst] + typ = ir.stmts[idx][:type] + flag = ir.stmts[idx][:flag] + any_refined = false + if (flag & IR_FLAG_REFINED) != 0 + any_refined = true + ir.stmts[idx][:flag] &= ~IR_FLAG_REFINED + end + for ur in userefs(stmt) + val = ur[] + if isa(val, Argument) + any_refined |= irsv.argtypes_refined[val.n] + elseif isa(val, SSAValue) + any_refined |= val.id in ssa_refined + count!(tpdum, val) end - if typ === Bottom && !is_terminator_or_phi - kill_terminator_edges!(irsv, lstmt, bb) - if idx != lstmt - for idx2 in (idx+1:lstmt-1) - ir[SSAValue(idx2)] = nothing - end - ir[SSAValue(lstmt)][:stmt] = ReturnNode() + end + if isa(stmt, PhiNode) && idx in ssa_refined + any_refined = true + delete!(ssa_refined, idx) + end + check_ret!(stmt, idx) + is_terminator_or_phi = isa(stmt, PhiNode) || isa(stmt, GotoNode) || isa(stmt, GotoIfNot) || isa(inst, ReturnNode) || isexpr(inst, :enter) + if typ === Bottom && (idx != lstmt || !is_terminator_or_phi) + return true + end + if (any_refined && reprocess_instruction!(interp, + idx, bb, stmt, typ, irsv)) || + (externally_refined !== nothing && idx in externally_refined) + push!(ssa_refined, idx) + stmt = ir.stmts[idx][:inst] + typ = ir.stmts[idx][:type] + end + if typ === Bottom && !is_terminator_or_phi + kill_terminator_edges!(irsv, lstmt, bb) + if idx != lstmt + for idx2 in (idx+1:lstmt-1) + ir[SSAValue(idx2)] = nothing end - break - end - if idx == lstmt - process_terminator!(ir, stmt, idx, bb, all_rets, bb_ip) && @goto residual_scan + ir[SSAValue(lstmt)][:inst] = ReturnNode() end + return false end + return true end - @goto compute_rt - # Slow path - begin @label residual_scan + if !completed_scan + # Slow path stmt_ip = BitSetBoundedMinPrioritySet(length(ir.stmts)) # Slow Path Phase 1.A: Complete use scanning - while !isempty(bb_ip) - bb = popfirst!(bb_ip) - stmts = bbs[bb].stmts - lstmt = last(stmts) - for idx = stmts - irsv.curridx = idx - inst = ir[SSAValue(idx)] - stmt = inst[:stmt] - flag = inst[:flag] - if (flag & IR_FLAG_REFINED) != 0 - inst[:flag] &= ~IR_FLAG_REFINED - push!(stmt_ip, idx) - end - for ur in userefs(stmt) - val = ur[] - if isa(val, Argument) - if irsv.argtypes_refined[val.n] - push!(stmt_ip, idx) - end - elseif isa(val, SSAValue) - count!(tpdum, val) + scan!(scanner, false) do inst, idx, lstmt, bb + irsv.curridx = idx + stmt = inst[:inst] + flag = inst[:flag] + if (flag & IR_FLAG_REFINED) != 0 + inst[:flag] &= ~IR_FLAG_REFINED + push!(stmt_ip, idx) + end + check_ret!(stmt, idx) + for ur in userefs(stmt) + val = ur[] + if isa(val, Argument) + if irsv.argtypes_refined[val.n] + push!(stmt_ip, idx) end + elseif isa(val, SSAValue) + count!(tpdum, val) end - idx == lstmt && process_terminator!(ir, stmt, idx, bb, all_rets, bb_ip) end + return true end # Slow Path Phase 1.B: Assemble def-use map - complete!(tpdum) - push!(bb_ip, 1) - while !isempty(bb_ip) - bb = popfirst!(bb_ip) - stmts = bbs[bb].stmts - lstmt = last(stmts) - for idx = stmts - irsv.curridx = idx - inst = ir[SSAValue(idx)] - stmt = inst[:stmt] - for ur in userefs(stmt) - val = ur[] - if isa(val, SSAValue) - push!(tpdum[val.id], idx) - end - end - idx == lstmt && process_terminator!(ir, stmt, idx, bb, all_rets, bb_ip) - end - end + complete!(tpdum); push!(scanner.bb_ip, 1) + populate_def_use_map!(tpdum, scanner) # Slow Path Phase 2: Use def-use map to converge cycles. # TODO: It would be possible to return to the fast path after converging # each cycle, but that's somewhat complicated. for val in ssa_refined - append!(stmt_ip, tpdum[val]) + for use in tpdum[val] + if !(use in ssa_refined) + push!(stmt_ip, use) + end + end end while !isempty(stmt_ip) idx = popfirst!(stmt_ip) diff --git a/base/compiler/ssair/slot2ssa.jl b/base/compiler/ssair/slot2ssa.jl index 4d5bc20403d98..90d11036e6d30 100644 --- a/base/compiler/ssair/slot2ssa.jl +++ b/base/compiler/ssair/slot2ssa.jl @@ -216,9 +216,9 @@ function typ_for_val(@nospecialize(x), ci::CodeInfo, ir::IRCode, idx::Int, slott return Const(x) end -struct BlockLiveness - def_bbs::Vector{Int} - live_in_bbs::Vector{Int} +struct BlockLiveness{D, L <: Union{Vector{Int}, Nothing}} + def_bbs::D + live_in_bbs::L end """ @@ -281,7 +281,8 @@ function iterated_dominance_frontier(cfg::CFG, liveness::BlockLiveness, domtree: push!(worklist, node) while !isempty(worklist) active = pop!(worklist) - for succ in cfg.blocks[active].succs + succs = cfg.blocks[active].succs + for succ in succs # Check whether the current root (`node`) dominates succ. # We are guaranteed that `node` dominates `active`, since # we've arrived at `active` by following dominator tree edges. @@ -296,7 +297,7 @@ function iterated_dominance_frontier(cfg::CFG, liveness::BlockLiveness, domtree: # unless liveness said otherwise. succ in processed && continue push!(processed, succ) - if !(succ in liveness.live_in_bbs) + if liveness.live_in_bbs !== nothing && !(succ in liveness.live_in_bbs) continue end push!(phiblocks, succ) diff --git a/base/compiler/ssair/verify.jl b/base/compiler/ssair/verify.jl index 11a0144529453..2f211fad44a6b 100644 --- a/base/compiler/ssair/verify.jl +++ b/base/compiler/ssair/verify.jl @@ -292,7 +292,7 @@ function verify_ir(ir::IRCode, print::Bool=true, continue end - if is_phinode_block && isa(stmt, Union{Expr, UpsilonNode, PhiCNode, SSAValue}) + if is_phinode_block && !is_valid_phiblock_stmt(stmt) if !isa(stmt, Expr) || !is_value_pos_expr_head(stmt.head) # Go back and check that all non-PhiNodes are valid value-position for validate_idx in firstidx:(lastphi-1) diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 641ff7962a266..45e4fbc8c7a7c 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -919,7 +919,7 @@ function typeinf_ircode( end (; result) = frame opt = OptimizationState(frame, interp) - ir = run_passes(opt.src, opt, result, optimize_until) + ir = run_passes_ipo_safe(opt.src, opt, result, optimize_until) rt = widenconst(ignorelimited(result.result)) ccall(:jl_typeinf_timing_end, Cvoid, (UInt64,), start_time) return ir, rt diff --git a/base/reflection.jl b/base/reflection.jl index 3cf7e0ad06ffb..16c165eddbb2f 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -1648,9 +1648,9 @@ function infer_effects(@nospecialize(f), @nospecialize(types=default_tt(f)); for match in matches.matches match = match::Core.MethodMatch frame = Core.Compiler.typeinf_frame(interp, - match.method, match.spec_types, match.sparams, #=run_optimizer=#false) + match.method, match.spec_types, match.sparams, #=run_optimizer=#true) frame === nothing && return Core.Compiler.Effects() - effects = Core.Compiler.merge_effects(effects, frame.ipo_effects) + effects = Core.Compiler.merge_effects(effects, frame.result.ipo_effects) end return effects end diff --git a/test/compiler/effects.jl b/test/compiler/effects.jl index 2346d8b408196..4a31bb71c72d5 100644 --- a/test/compiler/effects.jl +++ b/test/compiler/effects.jl @@ -890,7 +890,8 @@ end |> Core.Compiler.is_foldable_nothrow return 1 end |> Core.Compiler.is_foldable_nothrow -@test Base.infer_effects(Tuple{Int64}) do i +# TODO: Needs noub split +@test_broken Base.infer_effects(Tuple{Int64}) do i @inbounds (1,2,3)[i] end |> !Core.Compiler.is_noub @@ -1028,3 +1029,26 @@ f2_compilerbarrier(b) = Base.compilerbarrier(:conditional, b) @test !Core.Compiler.is_consistent(Base.infer_effects(f1_compilerbarrier, (Bool,))) @test Core.Compiler.is_consistent(Base.infer_effects(f2_compilerbarrier, (Bool,))) + +# Optimizer-refined effects +function f1_optrefine(b) + if Base.inferencebarrier(b) + error() + end + return b +end +@test Core.Compiler.is_consistent(Base.infer_effects(f1_optrefine, (Bool,))) + +function f2_optrefine() + if Ref(false)[] + error() + end + return true +end +@test Core.Compiler.is_nothrow(Base.infer_effects(f2_optrefine)) + +function f3_optrefine(x) + @fastmath sqrt(x) + return x +end +@test Core.Compiler.is_consistent(Base.infer_effects(f3_optrefine)) diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index 750fc0fdcea1b..152d806ccea85 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -4705,7 +4705,7 @@ end |> only === Union{Int,Nothing} @test Base.return_types((Symbol,Int)) do setting, val compilerbarrier(setting, val) end |> only === Any # XXX we may want to have "compile-time" error for this instead -for setting = (:type, :const, :conditional) +for setting = (#=:type, :const,=# :conditional,) # a successful barrier on abstract interpretation should be eliminated at the optimization @test @eval fully_eliminated((Int,)) do a compilerbarrier($(QuoteNode(setting)), 42) diff --git a/test/compiler/invalidation.jl b/test/compiler/invalidation.jl index 20ab2483aa378..f0527e2c9f503 100644 --- a/test/compiler/invalidation.jl +++ b/test/compiler/invalidation.jl @@ -181,7 +181,7 @@ begin take!(GLOBAL_BUFFER) let rt = only(Base.return_types(pr48932_callee_inferrable, (Any,))) @test rt === Nothing effects = Base.infer_effects(pr48932_callee_inferrable, (Any,)) - @test Core.Compiler.Effects(effects; noinbounds=false) == Core.Compiler.Effects() + @test_broken Core.Compiler.Effects(effects; noinbounds=false) == Core.Compiler.Effects() end # run inference on both `pr48932_caller` and `pr48932_callee`: diff --git a/test/compiler/irpasses.jl b/test/compiler/irpasses.jl index 3638218e0508a..258647311ae55 100644 --- a/test/compiler/irpasses.jl +++ b/test/compiler/irpasses.jl @@ -688,7 +688,7 @@ let nt = (a=1, b=2) end # Expr(:new) annotated as PartialStruct -struct FooPartial +struct FooPartialNew x y global f_partial From 23755dd19d7fb7d495d9b0bf1d56269ef7cb4240 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 20 Aug 2023 04:17:33 +0000 Subject: [PATCH 02/14] Don't delete code that may be needed by irinterp I was seeing a fair number of inference instabilities on master and tracked this down to us deleting the inferred code after codegen making it unavailable to irinterp. --- src/codegen.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/codegen.cpp b/src/codegen.cpp index a582a1e7aeaa4..e268bd35b9ab3 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -8890,6 +8890,15 @@ static jl_llvm_functions_t jl_emit_oc_wrapper(orc::ThreadSafeModule &m, jl_codeg return declarations; } +static int effects_foldable(uint32_t effects) +{ + // N.B.: This needs to be kept in sync with Core.Compiler.is_foldable + return ((effects & 0x7) == 0) && // is_consistent(effects) + ((effects >> 10) & 0x01) && // is_noub(effects) + (((effects >> 3) & 0x03) == 0) && // is_effect_free + ((effects >> 6) & 0x01); // is_terminates +} + jl_llvm_functions_t jl_emit_codeinst( orc::ThreadSafeModule &m, jl_code_instance_t *codeinst, @@ -8963,6 +8972,7 @@ jl_llvm_functions_t jl_emit_codeinst( // Julia-level optimization will never need to see it else if (jl_is_method(def) && // don't delete toplevel code inferred != jl_nothing && // and there is something to delete (test this before calling jl_ir_inlining_cost) + !effects_foldable(codeinst->ipo_purity_bits) && // don't delete code we may want for irinterp ((jl_ir_inlining_cost(inferred) == UINT16_MAX) || // don't delete inlineable code jl_atomic_load_relaxed(&codeinst->invoke) == jl_fptr_const_return_addr) && // unless it is constant !(params.imaging_mode || jl_options.incremental)) { // don't delete code when generating a precompile file From 3a1424affe8ac1ae8ad05b7af411315fe33a0872 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 20 Aug 2023 04:18:28 +0000 Subject: [PATCH 03/14] Correctly taint :nothrow effect on GotoIfNot with non-Bool argument We were usually doing this correctly, but not in the corner case where both branches ended up in the same place. --- base/compiler/abstractinterpretation.jl | 51 +++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 9b73a4cd763c5..149c14b3efb7f 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -3008,7 +3008,12 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end condval = maybe_extract_const_bool(condt) nothrow = (condval !== nothing) || ⊑(𝕃ᵢ, orig_condt, Bool) - nothrow && add_curr_ssaflag!(frame, IR_FLAG_NOTHROW) + if nothrow + add_curr_ssaflag!(frame, IR_FLAG_NOTHROW) + else + merge_effects!(interp, frame, EFFECTS_THROWS) + end + if !isempty(frame.pclimitations) # we can't model the possible effect of control # dependencies on the return @@ -3020,6 +3025,13 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) if condval === true @goto fallthrough else + if condval !== false && !nothrow + if !hasintersect(widenconst(orig_condt), Bool) + ssavaluetypes[currpc] = Bottom + @goto find_next_bb + end + end + succs = bbs[currbb].succs if length(succs) == 1 @assert condval === false || (stmt.dest === currpc + 1) @@ -3042,29 +3054,28 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) end end - # We continue with the true branch, but process the false - # branch here. - if isa(condt, Conditional) - else_change = conditional_change(𝕃ᵢ, currstate, condt.elsetype, condt.slot) - if else_change !== nothing - false_vartable = stoverwrite1!(copy(currstate), else_change) - else - false_vartable = currstate - end - changed = update_bbstate!(𝕃ᵢ, frame, falsebb, false_vartable) - then_change = conditional_change(𝕃ᵢ, currstate, condt.thentype, condt.slot) - if then_change !== nothing - stoverwrite1!(currstate, then_change) - end + # We continue with the true branch, but process the false + # branch here. + if isa(condt, Conditional) + else_change = conditional_change(𝕃ᵢ, currstate, condt.elsetype, condt.slot) + if else_change !== nothing + false_vartable = stoverwrite1!(copy(currstate), else_change) else - changed = update_bbstate!(𝕃ᵢ, frame, falsebb, currstate) + false_vartable = currstate end - if changed - handle_control_backedge!(interp, frame, currpc, stmt.dest) - push!(W, falsebb) + changed = update_bbstate!(𝕃ᵢ, frame, falsebb, false_vartable) + then_change = conditional_change(𝕃ᵢ, currstate, condt.thentype, condt.slot) + if then_change !== nothing + stoverwrite1!(currstate, then_change) end - @goto fallthrough + else + changed = update_bbstate!(𝕃ᵢ, frame, falsebb, currstate) end + if changed + handle_control_backedge!(interp, frame, currpc, stmt.dest) + push!(W, falsebb) + end + @goto fallthrough end elseif isa(stmt, ReturnNode) rt = abstract_eval_value(interp, stmt.val, currstate, frame) From fa0333a985cc937439c3f1152d2facaaaff77cba Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 20 Aug 2023 04:19:39 +0000 Subject: [PATCH 04/14] Fixup --- base/compiler/abstractinterpretation.jl | 1 + base/compiler/optimize.jl | 3 +-- test/compiler/effects.jl | 3 +-- test/compiler/invalidation.jl | 7 +++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 149c14b3efb7f..8350f8156e804 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -785,6 +785,7 @@ end function abstract_call_method_with_const_args(interp::AbstractInterpreter, result::MethodCallResult, @nospecialize(f), arginfo::ArgInfo, si::StmtInfo, match::MethodMatch, sv::AbsIntState, invokecall::Union{Nothing,InvokeCall}=nothing) + if !const_prop_enabled(interp, sv, match) return nothing end diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index a51b4eba9920b..91f2ac5bd46bf 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -502,7 +502,6 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: return (cfg, domtree) end - function scan_non_dataflow_flags!(flag) all_effect_free &= (flag & IR_FLAG_EFFECT_FREE) != 0 all_nothrow &= (flag & IR_FLAG_NOTHROW) != 0 @@ -557,7 +556,7 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: end end - return all_retpaths_consistent + return true end if !scan!(scan_stmt!, scanner, true) if !all_retpaths_consistent diff --git a/test/compiler/effects.jl b/test/compiler/effects.jl index 4a31bb71c72d5..0d92510877896 100644 --- a/test/compiler/effects.jl +++ b/test/compiler/effects.jl @@ -890,8 +890,7 @@ end |> Core.Compiler.is_foldable_nothrow return 1 end |> Core.Compiler.is_foldable_nothrow -# TODO: Needs noub split -@test_broken Base.infer_effects(Tuple{Int64}) do i +@test Base.infer_effects(Tuple{Int64}) do i @inbounds (1,2,3)[i] end |> !Core.Compiler.is_noub diff --git a/test/compiler/invalidation.jl b/test/compiler/invalidation.jl index f0527e2c9f503..2bef61910ac2d 100644 --- a/test/compiler/invalidation.jl +++ b/test/compiler/invalidation.jl @@ -173,15 +173,14 @@ end # we can avoid adding backedge even if the callee's return type is not the top # when the return value is not used within the caller begin take!(GLOBAL_BUFFER) - - pr48932_callee_inferrable(x) = (print(GLOBAL_BUFFER, x); nothing) + pr48932_callee_inferrable(x) = (print(GLOBAL_BUFFER, x); Base.inferencebarrier(1)::Int) pr48932_caller_unuse(x) = (pr48932_callee_inferrable(Base.inferencebarrier(x)); nothing) # assert that type and effects information inferred from `pr48932_callee(::Any)` are the top let rt = only(Base.return_types(pr48932_callee_inferrable, (Any,))) - @test rt === Nothing + @test rt === Int effects = Base.infer_effects(pr48932_callee_inferrable, (Any,)) - @test_broken Core.Compiler.Effects(effects; noinbounds=false) == Core.Compiler.Effects() + @test Core.Compiler.Effects(effects; noinbounds=false) == Core.Compiler.Effects() end # run inference on both `pr48932_caller` and `pr48932_callee`: From ee01fbfb011dff7b5dd5fdedd3fd71251f3a2552 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 20 Aug 2023 04:53:27 +0000 Subject: [PATCH 05/14] Fixup unstable --- src/codegen.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codegen.cpp b/src/codegen.cpp index e268bd35b9ab3..73541a4255867 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -8895,8 +8895,8 @@ static int effects_foldable(uint32_t effects) // N.B.: This needs to be kept in sync with Core.Compiler.is_foldable return ((effects & 0x7) == 0) && // is_consistent(effects) ((effects >> 10) & 0x01) && // is_noub(effects) - (((effects >> 3) & 0x03) == 0) && // is_effect_free - ((effects >> 6) & 0x01); // is_terminates + (((effects >> 3) & 0x03) == 0) && // is_effect_free(effects) + ((effects >> 6) & 0x01); // is_terminates(effects) } jl_llvm_functions_t jl_emit_codeinst( From 04a711640f87ff10fed37b5ec958f49e6ee920d7 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Mon, 21 Aug 2023 16:50:36 +0000 Subject: [PATCH 06/14] Respect termination consistent-cy --- base/compiler/optimize.jl | 58 ++++++++++++++++++++++++++++++++------- test/compiler/effects.jl | 15 +++++++++- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 91f2ac5bd46bf..e793da09d506b 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -469,10 +469,37 @@ function visit_bb_phis!(callback, ir::IRCode, bb::Int) end end +function any_stmt_may_throw(ir::IRCode, bb) + for stmt in ir.cfg.blocks[bb].stmts + if (ir[SSAValue(stmt)][:flag] & IR_FLAG_NOTHROW) != 0 + return true + end + end + return false +end + +function conditional_successors_may_throw(lazypostdomtree::LazyPostDomtree, ir::IRCode, bb::Int) + visited = BitSet((bb,)) + worklist = Int[bb] + postdomtree = get!(lazypostdomtree) + while !isempty(worklist) + thisbb = pop!(worklist) + for succ in ir.cfg.blocks[thisbb].succs + succ in visited && continue + push!(visited, succ) + postdominates(postdomtree, succ, thisbb) && continue + any_stmt_may_throw(ir, succ) && return true + push!(worklist, succ) + end + end + return false +end + function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result::InferenceResult) inconsistent = BitSet() inconsistent_bbs = BitSet() tpdum = TwoPhaseDefUseMap(length(ir.stmts)) + lazypostdomtree = LazyPostDomtree(ir) all_effect_free = true # TODO refine using EscapeAnalysis all_nothrow = true @@ -481,6 +508,8 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: scanner = BBScanner(ir) + effects = result.ipo_effects + cfg = nothing domtree = nothing function get_augmented_domtree() @@ -541,15 +570,25 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: elseif isa(stmt, GotoIfNot) && stmt_inconsistent # Conditional Branch with inconsistent condition. sa, sb = ir.cfg.blocks[bb].succs - (cfg, domtree) = get_augmented_domtree() - for succ in iterated_dominance_frontier(cfg, BlockLiveness((sa, sb), nothing), domtree) - if succ == length(cfg.blocks) - # Phi node in the virtual exit -> We have a conditional - # return. TODO: Check if all the retvals are egal. - all_retpaths_consistent = false - else - visit_bb_phis!(ir, succ) do phiidx::Int, phi::PhiNode - push!(inconsistent, phiidx) + # If we do not know this function terminates, taint consistency, now, + # :consistent requires consistent termination. TODO: Just look at the + # inconsistent region. + if !effects.terminates + all_retpaths_consistent = false + # Check if there are potential throws that require + elseif conditional_successors_may_throw(lazypostdomtree, ir, bb) + all_retpaths_consistent = false + else + (cfg, domtree) = get_augmented_domtree() + for succ in iterated_dominance_frontier(cfg, BlockLiveness((sa, sb), nothing), domtree) + if succ == length(cfg.blocks) + # Phi node in the virtual exit -> We have a conditional + # return. TODO: Check if all the retvals are egal. + all_retpaths_consistent = false + else + visit_bb_phis!(ir, succ) do phiidx::Int, phi::PhiNode + push!(inconsistent, phiidx) + end end end end @@ -603,7 +642,6 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: end had_trycatch && return - effects = result.ipo_effects if all_effect_free effects = Effects(effects; effect_free = true) end diff --git a/test/compiler/effects.jl b/test/compiler/effects.jl index 0d92510877896..eaf760e6823a7 100644 --- a/test/compiler/effects.jl +++ b/test/compiler/effects.jl @@ -1036,7 +1036,7 @@ function f1_optrefine(b) end return b end -@test Core.Compiler.is_consistent(Base.infer_effects(f1_optrefine, (Bool,))) +@test !Core.Compiler.is_consistent(Base.infer_effects(f1_optrefine, (Bool,))) function f2_optrefine() if Ref(false)[] @@ -1051,3 +1051,16 @@ function f3_optrefine(x) return x end @test Core.Compiler.is_consistent(Base.infer_effects(f3_optrefine)) + +# Check that :consistent is properly modeled for throwing statements +const GLOBAL_MUTABLE_SWITCH = Ref{Bool}(false) + +check_switch(switch::Base.RefValue{Bool}) = (switch[] && error(); return nothing) +check_switch2() = check_switch(GLOBAL_MUTABLE_SWITCH) + +@test (Base.return_types(check_switch2) |> only) === Nothing +GLOBAL_MUTABLE_SWITCH[] = true +# Check that flipping the switch doesn't accidentally change the return type +@test (Base.return_types(check_switch2) |> only) === Nothing + +@test !Core.Compiler.is_consistent(Base.infer_effects(check_switch, (Base.RefValue{Bool},))) From e13298e70c072a0bb005559834add10c2c607da2 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Mon, 21 Aug 2023 19:26:10 +0000 Subject: [PATCH 07/14] Delay :boundscheck resolution until codegen time --- base/compiler/ssair/inlining.jl | 26 +++++++++++++++++--------- base/compiler/ssair/verify.jl | 2 +- base/compiler/validation.jl | 2 +- src/codegen.cpp | 3 +++ src/julia-syntax.scm | 2 +- src/julia_internal.h | 2 ++ src/method.c | 6 +++++- test/boundscheck_exec.jl | 5 ++--- 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index 3f20edc6bfb34..c7d1416599e8c 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -386,6 +386,19 @@ function ir_prepare_inlining!(insert_node!::Inserter, inline_target::Union{IRCod return SSASubstitute(mi, argexprs, spvals_ssa, linetable_offset) end +function adjust_boundscheck!(inline_compact, idx′, stmt, boundscheck) + if boundscheck === :off + if length(stmt.args) == 0 + inline_compact[SSAValue(idx′)][:flag] |= IR_FLAG_INBOUNDS + end + elseif boundscheck !== :propagate + if (inline_compact[SSAValue(idx′)][:flag] & IR_FLAG_INBOUNDS) == 0 + # Prevent future inlining passes from setting IR_FLAG_INBOUNDS + length(stmt.args) == 0 && push!(stmt.args, true) + end + end +end + function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector{Any}, item::InliningTodo, boundscheck::Symbol, todo_bbs::Vector{Tuple{Int, Int}}) # Ok, do the inlining here @@ -424,6 +437,8 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector # Everything legal in value position is guaranteed to be effect free in stmt position inline_compact.result[idx′][:flag] = IR_FLAG_EFFECT_FREE | IR_FLAG_NOTHROW break + elseif isexpr(stmt′, :boundscheck) + adjust_boundscheck!(inline_compact, idx′, stmt′, boundscheck) end inline_compact[idx′] = stmt′ end @@ -460,6 +475,8 @@ function ir_inline_item!(compact::IncrementalCompact, idx::Int, argexprs::Vector stmt′ = GotoIfNot(stmt′.cond, stmt′.dest + bb_offset) elseif isa(stmt′, PhiNode) stmt′ = PhiNode(Int32[edge+bb_offset for edge in stmt′.edges], stmt′.values) + elseif isexpr(stmt′, :boundscheck) + adjust_boundscheck!(inline_compact, idx′, stmt′, boundscheck) end inline_compact[idx′] = stmt′ end @@ -1805,7 +1822,6 @@ end function ssa_substitute!(insert_node!::Inserter, subst_inst::Instruction, @nospecialize(val), ssa_substitute::SSASubstitute, boundscheck::Symbol) - subst_inst[:flag] &= ~IR_FLAG_INBOUNDS subst_inst[:line] += ssa_substitute.linetable_offset return ssa_substitute_op!(insert_node!, subst_inst, val, ssa_substitute, boundscheck) end @@ -1875,14 +1891,6 @@ function ssa_substitute_op!(insert_node!::Inserter, subst_inst::Instruction, @no for argt in e.args[3]::SimpleVector ]...) end end - elseif head === :boundscheck - if boundscheck === :off # inbounds == true - return false - elseif boundscheck === :propagate - return e - else # on or default - return true - end end end isa(val, Union{SSAValue, NewSSAValue}) && return val # avoid infinite loop diff --git a/base/compiler/ssair/verify.jl b/base/compiler/ssair/verify.jl index 2f211fad44a6b..50e83625da54e 100644 --- a/base/compiler/ssair/verify.jl +++ b/base/compiler/ssair/verify.jl @@ -20,7 +20,7 @@ if !isdefined(@__MODULE__, Symbol("@verify_error")) end end -is_value_pos_expr_head(head::Symbol) = head === :boundscheck +is_value_pos_expr_head(head::Symbol) = false function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int, use_idx::Int, printed_use_idx::Int, print::Bool, isforeigncall::Bool, arg_idx::Int, allow_frontend_forms::Bool) if isa(op, SSAValue) if op.id > length(ir.stmts) diff --git a/base/compiler/validation.jl b/base/compiler/validation.jl index 4f407475b7190..8aebc53035ca5 100644 --- a/base/compiler/validation.jl +++ b/base/compiler/validation.jl @@ -234,7 +234,7 @@ is_valid_lvalue(@nospecialize(x)) = isa(x, SlotNumber) || isa(x, GlobalRef) function is_valid_argument(@nospecialize(x)) if isa(x, SlotNumber) || isa(x, Argument) || isa(x, SSAValue) || - isa(x, GlobalRef) || isa(x, QuoteNode) || isexpr(x, (:static_parameter, :boundscheck)) || + isa(x, GlobalRef) || isa(x, QuoteNode) || is_value_pos_expr_head(x) || isa(x, Number) || isa(x, AbstractString) || isa(x, AbstractChar) || isa(x, Tuple) || isa(x, Type) || isa(x, Core.Box) || isa(x, Module) || x === nothing return true diff --git a/src/codegen.cpp b/src/codegen.cpp index 73541a4255867..f7ae6b0a00680 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -5151,6 +5151,9 @@ static void emit_ssaval_assign(jl_codectx_t &ctx, ssize_t ssaidx_0based, jl_valu it = ctx.phic_slots.emplace(ssaidx_0based, jl_varinfo_t(ctx.builder.getContext())).first; } slot = emit_varinfo(ctx, it->second, jl_symbol("phic")); + } else if (jl_is_expr(r) && ((jl_expr_t*)r)->head == jl_boundscheck_sym) { + uint8_t flag = jl_array_uint8_ref(ctx.source->ssaflags, ssaidx_0based); + slot = mark_julia_const(ctx, bounds_check_enabled(ctx, (flag & IR_FLAG_INBOUNDS) ? jl_false : jl_true) ? jl_true : jl_false); } else { slot = emit_expr(ctx, r, ssaidx_0based); // slot could be a jl_value_t (unboxed) or jl_value_t* (ispointer) } diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index cca513e4646f6..cc43f6400bb12 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -4256,7 +4256,7 @@ f(x) = yt(x) (or (simple-atom? e) (symbol? e) (and (pair? e) (memq (car e) '(quote inert top core globalref outerref - slot static_parameter boundscheck))))) + slot static_parameter))))) (define (valid-ir-rvalue? lhs e) (or (ssavalue? lhs) diff --git a/src/julia_internal.h b/src/julia_internal.h index 1a0373f0f9131..52eb639fa6845 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -1662,6 +1662,8 @@ JL_DLLEXPORT uint32_t jl_crc32c(uint32_t crc, const char *buf, size_t len); // -- exports from codegen -- // +#define IR_FLAG_INBOUNDS 0x01 + JL_DLLIMPORT jl_code_instance_t *jl_generate_fptr(jl_method_instance_t *mi JL_PROPAGATES_ROOT, size_t world); JL_DLLIMPORT void jl_generate_fptr_for_unspecialized(jl_code_instance_t *unspec); JL_DLLIMPORT void jl_generate_fptr_for_oc_wrapper(jl_code_instance_t *unspec); diff --git a/src/method.c b/src/method.c index 00eae940f9f88..55d6ec0f9d815 100644 --- a/src/method.c +++ b/src/method.c @@ -381,6 +381,10 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) } bd[j] = jl_nothing; } + else if (jl_is_expr(st) && ((jl_expr_t*)st)->head == jl_boundscheck_sym) { + // Don't set IR_FLAG_INBOUNDS on boundscheck at the same level + is_flag_stmt = 1; + } else if (jl_is_expr(st) && ((jl_expr_t*)st)->head == jl_return_sym) { jl_array_ptr_set(body, j, jl_new_struct(jl_returnnode_type, jl_exprarg(st, 0))); } @@ -392,7 +396,7 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) else { uint8_t flag = 0; if (inbounds_depth > 0) - flag |= 1 << 0; + flag |= IR_FLAG_INBOUNDS; if (inline_flags->len > 0) { void* inline_flag = inline_flags->items[inline_flags->len - 1]; flag |= 1 << (inline_flag ? 1 : 2); diff --git a/test/boundscheck_exec.jl b/test/boundscheck_exec.jl index f2eb2ea630893..10f46eb4a8031 100644 --- a/test/boundscheck_exec.jl +++ b/test/boundscheck_exec.jl @@ -252,10 +252,9 @@ end # Boundschecking removal of indices with different type, see #40281 getindex_40281(v, a, b, c) = @inbounds getindex(v, a, b, c) -typed_40281 = sprint((io, args...) -> code_warntype(io, args...; optimize=true), getindex_40281, Tuple{Array{Float64, 3}, Int, UInt8, Int}) +llvm_40281 = sprint((io, args...) -> code_llvm(io, args...; optimize=true), getindex_40281, Tuple{Array{Float64, 3}, Int, UInt8, Int}) if bc_opt == bc_default || bc_opt == bc_off - @test occursin("arrayref(false", typed_40281) - @test !occursin("arrayref(true", typed_40281) + @test !occursin("call void @ijl_bounds_error_ints", llvm_40281) end # Given this is a sub-processed test file, not using @testsets avoids From 7aa2b9193613ab9727f26a920d2b243b0be12008 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Thu, 24 Aug 2023 18:31:24 +0000 Subject: [PATCH 08/14] Corrections to UB behavior, get rid of inbounds effects Also include UB in optimizer-refinement and get rid of the inbounds effect, replacing it by a conditional ub effect, which does much the same thing, but with more precise, path-dependent semantics. --- base/compiler/abstractinterpretation.jl | 85 ++++++----------- base/compiler/effects.jl | 49 +++++----- base/compiler/inferencestate.jl | 8 +- base/compiler/optimize.jl | 109 ++++++++++++++++------ base/compiler/ssair/inlining.jl | 40 ++++---- base/compiler/ssair/ir.jl | 26 +++--- base/compiler/ssair/show.jl | 2 - base/compiler/ssair/verify.jl | 2 +- base/compiler/tfuncs.jl | 9 +- base/compiler/typeinfer.jl | 8 +- base/compiler/validation.jl | 4 +- src/codegen.cpp | 2 +- src/ircode.c | 2 +- src/jl_exported_data.inc | 1 + src/jltypes.c | 4 +- src/julia-syntax.scm | 4 +- src/julia.h | 13 +++ src/method.c | 6 +- src/staticdata.c | 3 +- stdlib/Serialization/src/Serialization.jl | 6 +- test/compiler/AbstractInterpreter.jl | 4 +- test/compiler/effects.jl | 5 + test/compiler/invalidation.jl | 6 +- test/compiler/ssair.jl | 22 +++-- 24 files changed, 241 insertions(+), 179 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 8350f8156e804..7ad6970e7a188 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -464,13 +464,6 @@ function add_call_backedges!(interp::AbstractInterpreter, @nospecialize(rettype) if !isoverlayed(method_table(interp)) all_effects = Effects(all_effects; nonoverlayed=false) end - if (# ignore the `:noinbounds` property if `:consistent`-cy is tainted already - (sv isa InferenceState && sv.ipo_effects.consistent === ALWAYS_FALSE) || - all_effects.consistent === ALWAYS_FALSE || - # or this `:noinbounds` doesn't taint it - !stmt_taints_inbounds_consistency(sv)) - all_effects = Effects(all_effects; noinbounds=false) - end all_effects === Effects() && return nothing end for edge in edges @@ -854,14 +847,8 @@ function concrete_eval_eligible(interp::AbstractInterpreter, return :none end end - if !effects.noinbounds && stmt_taints_inbounds_consistency(sv) - # If the current statement is @inbounds or we propagate inbounds, - # the call's :consistent-cy is tainted and not consteval eligible. - add_remark!(interp, sv, "[constprop] Concrete evel disabled for inbounds") - return :none - end mi = result.edge - if mi !== nothing && is_foldable(effects) + if mi !== nothing && is_foldable(effects, !stmt_taints_inbounds_consistency(sv)) if f !== nothing && is_all_const_arg(arginfo, #=start=#2) if is_nonoverlayed(mi.def::Method) && (!isoverlayed(method_table(interp)) || is_nonoverlayed(effects)) return :concrete_eval @@ -2000,17 +1987,6 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), end rt = abstract_call_builtin(interp, f, arginfo, sv) effects = builtin_effects(𝕃ᵢ, f, arginfo, rt) - if (isa(sv, InferenceState) && f === getfield && fargs !== nothing && - isexpr(fargs[end], :boundscheck) && !is_nothrow(effects)) - # As a special case, we delayed tainting `noinbounds` for `getfield` calls - # in case we can prove in-boundedness indepedently. - # Here we need to put that back in other cases. - # N.B. This isn't about the effects of the call itself, - # but a delayed contribution of the :boundscheck statement, - # so we need to merge this directly into sv, rather than modifying the effects. - merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; noinbounds=false, - consistent = iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) ? ALWAYS_TRUE : ALWAYS_FALSE)) - end return CallMeta(rt, effects, NoCallInfo()) elseif isa(f, Core.OpaqueClosure) # calling an OpaqueClosure about which we have no information returns no information @@ -2227,24 +2203,7 @@ function abstract_eval_value_expr(interp::AbstractInterpreter, e::Expr, vtypes:: merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; nothrow)) return rt elseif head === :boundscheck - if isa(sv, InferenceState) - stmt = sv.src.code[sv.currpc] - if isexpr(stmt, :call) - f = abstract_eval_value(interp, stmt.args[1], vtypes, sv) - if f isa Const && f.val === getfield - # boundscheck of `getfield` call is analyzed by tfunc potentially without - # tainting :noinbounds or :noub when it's known to be nothrow - return Bool - end - end - # If there is no particular `@inbounds` for this function, then we only taint `:noinbounds`, - # which will subsequently taint `:consistent` if this function is called from another - # function that uses `@inbounds`. However, if this `:boundscheck` is itself within an - # `@inbounds` region, its value depends on `--check-bounds`, so we need to taint - # `:consistent`-cy here also. - merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; noinbounds=false, - consistent = iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) ? ALWAYS_TRUE : ALWAYS_FALSE)) - end + merge_effects!(interp, sv, Effects(EFFECTS_TOTAL; consistent = ALWAYS_FALSE)) return Bool elseif head === :inbounds @assert false "Expected `:inbounds` expression to have been moved into SSA flags" @@ -2329,6 +2288,11 @@ function mark_curr_effect_flags!(sv::AbsIntState, effects::Effects) else sub_curr_ssaflag!(sv, IR_FLAG_CONSISTENT) end + if is_noub(effects, false) + add_curr_ssaflag!(sv, IR_FLAG_NOUB) + else + sub_curr_ssaflag!(sv, IR_FLAG_NOUB) + end end end @@ -2365,8 +2329,8 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp elseif ehead === :new t, isexact = instanceof_tfunc(abstract_eval_value(interp, e.args[1], vtypes, sv)) ut = unwrap_unionall(t) - consistent = ALWAYS_FALSE - nothrow = noub = false + consistent = noub = ALWAYS_FALSE + nothrow = false if isa(ut, DataType) && !isabstracttype(ut) ismutable = ismutabletype(ut) fcount = datatype_fieldcount(ut) @@ -2379,10 +2343,10 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp # mutable object isn't `:consistent`, but we still have a chance that # return type information later refines the `:consistent`-cy of the method consistent = CONSISTENT_IF_NOTRETURNED - noub = true + noub = ALWAYS_TRUE else consistent = ALWAYS_TRUE - noub = true + noub = ALWAYS_TRUE end if isconcretedispatch(t) nothrow = true @@ -2530,6 +2494,20 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp end end end + elseif ehead === :throw_undef_if_not + condt = argextype(stmt.args[2], ir) + condval = maybe_extract_const_bool(condt) + t = Nothing + effects = EFFECTS_THROWS + if condval isa Bool + if condval + effects = EFFECTS_TOTAL + else + t = Union{} + end + elseif !hasintersect(condt, Bool) + t = Union{} + end elseif false @label always_throw t = Bottom @@ -2576,7 +2554,7 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, vtypes: terminates = override.terminates_globally ? true : effects.terminates, notaskstate = override.notaskstate ? true : effects.notaskstate, inaccessiblememonly = override.inaccessiblememonly ? ALWAYS_TRUE : effects.inaccessiblememonly, - noub = override.noub ? true : effects.noub) + noub = override.noub ? ALWAYS_TRUE : effects.noub) end return RTEffects(t, effects) end @@ -2605,14 +2583,13 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), return abstract_eval_special_value(interp, e, vtypes, sv) end (; rt, effects) = abstract_eval_statement_expr(interp, e, vtypes, sv) - if !effects.noinbounds - if !propagate_inbounds(sv) + if effects.noub === NOUB_IF_NOINBOUNDS + if !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) + effects = Effects(effects; noub=ALWAYS_FALSE) + elseif !propagate_inbounds(sv) # The callee read our inbounds flag, but unless we propagate inbounds, # we ourselves don't read our parent's inbounds. - effects = Effects(effects; noinbounds=true) - end - if !iszero(get_curr_ssaflag(sv) & IR_FLAG_INBOUNDS) - effects = Effects(effects; consistent=ALWAYS_FALSE, noub=false) + effects = Effects(effects; noub=ALWAYS_TRUE) end end merge_effects!(interp, sv, effects) diff --git a/base/compiler/effects.jl b/base/compiler/effects.jl index a8f5596a0af9e..81b658dfe037e 100644 --- a/base/compiler/effects.jl +++ b/base/compiler/effects.jl @@ -100,9 +100,8 @@ struct Effects terminates::Bool notaskstate::Bool inaccessiblememonly::UInt8 - noub::Bool + noub::UInt8 nonoverlayed::Bool - noinbounds::Bool function Effects( consistent::UInt8, effect_free::UInt8, @@ -110,9 +109,8 @@ struct Effects terminates::Bool, notaskstate::Bool, inaccessiblememonly::UInt8, - noub::Bool, - nonoverlayed::Bool, - noinbounds::Bool) + noub::UInt8, + nonoverlayed::Bool) return new( consistent, effect_free, @@ -121,8 +119,7 @@ struct Effects notaskstate, inaccessiblememonly, noub, - nonoverlayed, - noinbounds) + nonoverlayed) end end @@ -139,10 +136,13 @@ const EFFECT_FREE_IF_INACCESSIBLEMEMONLY = 0x01 << 1 # :inaccessiblememonly bits const INACCESSIBLEMEM_OR_ARGMEMONLY = 0x01 << 1 -const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, true, true, true) -const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, true, true, true) -const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, false, true, true) # unknown mostly, but it's not overlayed and noinbounds at least (e.g. it's not a call) -const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, false, false, false) # unknown really +# :noub bits +const NOUB_IF_NOINBOUNDS = 0x01 << 1 + +const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, true) +const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, true) +const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, true) # unknown mostly, but it's not overlayed and noinbounds at least (e.g. it's not a call) +const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, false) # unknown really function Effects(effects::Effects = _EFFECTS_UNKNOWN; consistent::UInt8 = effects.consistent, @@ -151,9 +151,8 @@ function Effects(effects::Effects = _EFFECTS_UNKNOWN; terminates::Bool = effects.terminates, notaskstate::Bool = effects.notaskstate, inaccessiblememonly::UInt8 = effects.inaccessiblememonly, - noub::Bool = effects.noub, - nonoverlayed::Bool = effects.nonoverlayed, - noinbounds::Bool = effects.noinbounds) + noub::UInt8 = effects.noub, + nonoverlayed::Bool = effects.nonoverlayed) return Effects( consistent, effect_free, @@ -162,8 +161,7 @@ function Effects(effects::Effects = _EFFECTS_UNKNOWN; notaskstate, inaccessiblememonly, noub, - nonoverlayed, - noinbounds) + nonoverlayed) end function merge_effects(old::Effects, new::Effects) @@ -175,8 +173,7 @@ function merge_effects(old::Effects, new::Effects) merge_effectbits(old.notaskstate, new.notaskstate), merge_effectbits(old.inaccessiblememonly, new.inaccessiblememonly), merge_effectbits(old.noub, new.noub), - merge_effectbits(old.nonoverlayed, new.nonoverlayed), - merge_effectbits(old.noinbounds, new.noinbounds)) + merge_effectbits(old.nonoverlayed, new.nonoverlayed)) end function merge_effectbits(old::UInt8, new::UInt8) @@ -193,18 +190,18 @@ is_nothrow(effects::Effects) = effects.nothrow is_terminates(effects::Effects) = effects.terminates is_notaskstate(effects::Effects) = effects.notaskstate is_inaccessiblememonly(effects::Effects) = effects.inaccessiblememonly === ALWAYS_TRUE -is_noub(effects::Effects) = effects.noub +is_noub(effects::Effects, noinbounds=true) = effects.noub === ALWAYS_TRUE || (noinbounds && effects.noub === NOUB_IF_NOINBOUNDS) is_nonoverlayed(effects::Effects) = effects.nonoverlayed # implies `is_notaskstate` & `is_inaccessiblememonly`, but not explicitly checked here -is_foldable(effects::Effects) = +is_foldable(effects::Effects, noinbounds=true) = is_consistent(effects) && - is_noub(effects) && + is_noub(effects, noinbounds) && is_effect_free(effects) && is_terminates(effects) -is_foldable_nothrow(effects::Effects) = - is_foldable(effects) && +is_foldable_nothrow(effects::Effects, noinbounds=true) = + is_foldable(effects, noinbounds) && is_nothrow(effects) # TODO add `is_noub` here? @@ -232,8 +229,7 @@ function encode_effects(e::Effects) ((e.notaskstate % UInt32) << 7) | ((e.inaccessiblememonly % UInt32) << 8) | ((e.noub % UInt32) << 10) | - ((e.nonoverlayed % UInt32) << 11) | - ((e.noinbounds % UInt32) << 12) + ((e.nonoverlayed % UInt32) << 12) end function decode_effects(e::UInt32) @@ -244,8 +240,7 @@ function decode_effects(e::UInt32) _Bool((e >> 6) & 0x01), _Bool((e >> 7) & 0x01), UInt8((e >> 8) & 0x03), - _Bool((e >> 10) & 0x01), - _Bool((e >> 11) & 0x01), + UInt8((e >> 10) & 0x03), _Bool((e >> 12) & 0x01)) end diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 86d6c046b4553..a63634226ff03 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -811,11 +811,11 @@ end get_curr_ssaflag(sv::InferenceState) = sv.src.ssaflags[sv.currpc] get_curr_ssaflag(sv::IRInterpretationState) = sv.ir.stmts[sv.curridx][:flag] -add_curr_ssaflag!(sv::InferenceState, flag::UInt8) = sv.src.ssaflags[sv.currpc] |= flag -add_curr_ssaflag!(sv::IRInterpretationState, flag::UInt8) = sv.ir.stmts[sv.curridx][:flag] |= flag +add_curr_ssaflag!(sv::InferenceState, flag::UInt32) = sv.src.ssaflags[sv.currpc] |= flag +add_curr_ssaflag!(sv::IRInterpretationState, flag::UInt32) = sv.ir.stmts[sv.curridx][:flag] |= flag -sub_curr_ssaflag!(sv::InferenceState, flag::UInt8) = sv.src.ssaflags[sv.currpc] &= ~flag -sub_curr_ssaflag!(sv::IRInterpretationState, flag::UInt8) = sv.ir.stmts[sv.curridx][:flag] &= ~flag +sub_curr_ssaflag!(sv::InferenceState, flag::UInt32) = sv.src.ssaflags[sv.currpc] &= ~flag +sub_curr_ssaflag!(sv::IRInterpretationState, flag::UInt32) = sv.ir.stmts[sv.curridx][:flag] &= ~flag merge_effects!(::AbstractInterpreter, caller::InferenceState, effects::Effects) = caller.ipo_effects = merge_effects(caller.ipo_effects, effects) diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index e793da09d506b..d3983cfd517c8 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -15,27 +15,29 @@ const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError # NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c -const IR_FLAG_NULL = 0x00 +const IR_FLAG_NULL = UInt32(1) # This statement is marked as @inbounds by user. # Ff replaced by inlining, any contained boundschecks may be removed. -const IR_FLAG_INBOUNDS = 0x01 << 0 +const IR_FLAG_INBOUNDS = UInt32(1) << 0 # This statement is marked as @inline by user -const IR_FLAG_INLINE = 0x01 << 1 +const IR_FLAG_INLINE = UInt32(1) << 1 # This statement is marked as @noinline by user -const IR_FLAG_NOINLINE = 0x01 << 2 -const IR_FLAG_THROW_BLOCK = 0x01 << 3 +const IR_FLAG_NOINLINE = UInt32(1) << 2 +const IR_FLAG_THROW_BLOCK = UInt32(1) << 3 # This statement may be removed if its result is unused. In particular, # it must be both :effect_free and :nothrow. # TODO: Separate these out. -const IR_FLAG_EFFECT_FREE = 0x01 << 4 +const IR_FLAG_EFFECT_FREE = UInt32(1) << 4 # This statement was proven not to throw -const IR_FLAG_NOTHROW = 0x01 << 5 +const IR_FLAG_NOTHROW = UInt32(1) << 5 # This is :consistent -const IR_FLAG_CONSISTENT = 0x01 << 6 +const IR_FLAG_CONSISTENT = UInt32(1) << 6 # An optimization pass has updated this statement in a way that may # have exposed information that inference did not see. Re-running # inference on this statement may be profitable. -const IR_FLAG_REFINED = 0x01 << 7 +const IR_FLAG_REFINED = UInt32(1) << 7 +# This is :noub == ALWAYS_TRUE +const IR_FLAG_NOUB = UInt32(1) << 8 const TOP_TUPLE = GlobalRef(Core, :tuple) @@ -70,7 +72,7 @@ is_source_inferred(@nospecialize src::MaybeCompressed) = ccall(:jl_ir_flag_inferred, Bool, (Any,), src) function inlining_policy(interp::AbstractInterpreter, - @nospecialize(src), @nospecialize(info::CallInfo), stmt_flag::UInt8, mi::MethodInstance, + @nospecialize(src), @nospecialize(info::CallInfo), stmt_flag::UInt32, mi::MethodInstance, argtypes::Vector{Any}) if isa(src, MaybeCompressed) is_source_inferred(src) || return nothing @@ -221,9 +223,9 @@ end _topmod(sv::OptimizationState) = _topmod(sv.mod) -is_stmt_inline(stmt_flag::UInt8) = stmt_flag & IR_FLAG_INLINE ≠ 0 -is_stmt_noinline(stmt_flag::UInt8) = stmt_flag & IR_FLAG_NOINLINE ≠ 0 -is_stmt_throw_block(stmt_flag::UInt8) = stmt_flag & IR_FLAG_THROW_BLOCK ≠ 0 +is_stmt_inline(stmt_flag::UInt32) = stmt_flag & IR_FLAG_INLINE ≠ 0 +is_stmt_noinline(stmt_flag::UInt32) = stmt_flag & IR_FLAG_NOINLINE ≠ 0 +is_stmt_throw_block(stmt_flag::UInt32) = stmt_flag & IR_FLAG_THROW_BLOCK ≠ 0 function new_expr_effect_flags(𝕃ₒ::AbstractLattice, args::Vector{Any}, src::Union{IRCode,IncrementalCompact}, pattern_match=nothing) Targ = args[1] @@ -504,6 +506,8 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: all_effect_free = true # TODO refine using EscapeAnalysis all_nothrow = true all_retpaths_consistent = true + all_noub = true + any_conditional_ub = false had_trycatch = false scanner = BBScanner(ir) @@ -531,20 +535,58 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: return (cfg, domtree) end - function scan_non_dataflow_flags!(flag) + function is_getfield_boundscheck(inst::Instruction) + stmt = inst[:stmt] + is_known_call(stmt, getfield, ir) || return false + length(stmt.args) < 4 && return false + boundscheck = stmt.args[end] + argextype(boundscheck, ir) === Bool || return false + isa(boundscheck, SSAValue) || return false + return true + end + + function is_conditional_noub(inst::Instruction) + # Special case: `:boundscheck` into `getfield` + is_getfield_boundscheck(inst) || return false + barg = inst[:stmt].args[end] + isexpr(ir[barg][:stmt], :boundscheck) || return false + # If IR_FLAG_INBOUNDS is already set, no more conditional ub + (ir[barg][:flag] & IR_FLAG_INBOUNDS) != 0 && return false + any_conditional_ub = true + return true + end + + function scan_non_dataflow_flags!(inst::Instruction) + flag = inst[:flag] all_effect_free &= (flag & IR_FLAG_EFFECT_FREE) != 0 all_nothrow &= (flag & IR_FLAG_NOTHROW) != 0 + if (flag & IR_FLAG_NOUB) == 0 + if !is_conditional_noub(inst) + all_noub = false + end + end end - function scan_inconsistency!(flag, idx, @nospecialize(stmt)) + function scan_inconsistency!(inst::Instruction, idx) + flag = inst[:flag] stmt_inconsistent = (flag & IR_FLAG_CONSISTENT) == 0 - for ur in userefs(stmt) - val = ur[] - if isa(val, SSAValue) - stmt_inconsistent |= val.id in inconsistent - count!(tpdum, val) - elseif isexpr(val, :boundscheck) - stmt_inconsistent = true + stmt = inst[:stmt] + # Special case: For getfield, we allow inconsistency of the :boundscheck argument + if is_getfield_boundscheck(inst) + for i = 1:(length(stmt.args)-1) + val = stmt.args[i] + if isa(val, SSAValue) + stmt_inconsistent |= val.id in inconsistent + count!(tpdum, val) + end + end + else + for ur in userefs(stmt) + val = ur[] + if isa(val, SSAValue) + stmt_inconsistent |= val.id in inconsistent + count!(tpdum, val) + end end end stmt_inconsistent && push!(inconsistent, idx) @@ -561,8 +603,8 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: return true end - scan_non_dataflow_flags!(flag) - stmt_inconsistent = scan_inconsistency!(flag, idx, stmt) + scan_non_dataflow_flags!(inst) + stmt_inconsistent = scan_inconsistency!(inst, idx) if idx == lstmt if isa(stmt, ReturnNode) && isdefined(stmt, :val) && stmt_inconsistent @@ -601,8 +643,8 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: if !all_retpaths_consistent # No longer any dataflow concerns, just scan the flags scan!(scanner, false) do inst, idx, lstmt, bb - flag = inst[:flag] - scan_non_dataflow_flags!(flag) + scan_non_dataflow_flags!(inst) + return true end else scan!(scan_stmt!, scanner, true) @@ -622,7 +664,17 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: idx = popfirst!(stmt_ip) inst = ir[SSAValue(idx)] stmt = inst[:inst] - if isa(stmt, ReturnNode) + if is_getfield_boundscheck(inst) + any_non_boundscheck_inconsistent = false + for i = 1:(length(stmt.args)-1) + val = stmt.args[i] + if isa(val, SSAValue) + any_non_boundscheck_inconsistent |= val.id in inconsistent + any_non_boundscheck_inconsistent && break + end + end + any_non_boundscheck_inconsistent || continue + elseif isa(stmt, ReturnNode) all_retpaths_consistent = false else isa(stmt, GotoIfNot) bb = block_for_inst(ir, idx) @@ -651,6 +703,9 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: if all_retpaths_consistent effects = Effects(effects; consistent = ALWAYS_TRUE) end + if all_noub + effects = Effects(effects; noub = any_conditional_ub ? NOUB_IF_NOINBOUNDS : ALWAYS_TRUE) + end result.ipo_effects = effects end diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index c7d1416599e8c..e70864922f60b 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -669,7 +669,7 @@ function batch_inline!(ir::IRCode, todo::Vector{Pair{Int,Any}}, propagate_inboun end finish_cfg_inline!(state) - boundscheck = inbounds_option() + boundscheck = :default if boundscheck === :default && propagate_inbounds boundscheck = :propagate end @@ -874,7 +874,7 @@ end # the general resolver for usual and const-prop'ed calls function resolve_todo(mi::MethodInstance, result::Union{MethodMatch,InferenceResult}, - argtypes::Vector{Any}, @nospecialize(info::CallInfo), flag::UInt8, + argtypes::Vector{Any}, @nospecialize(info::CallInfo), flag::UInt32, state::InliningState; invokesig::Union{Nothing,Vector{Any}}=nothing) et = InliningEdgeTracker(state, invokesig) @@ -915,7 +915,7 @@ end # the special resolver for :invoke-d call function resolve_todo(mi::MethodInstance, argtypes::Vector{Any}, - @nospecialize(info::CallInfo), flag::UInt8, state::InliningState) + @nospecialize(info::CallInfo), flag::UInt32, state::InliningState) if !OptimizationParams(state.interp).inlining || is_stmt_noinline(flag) return nothing end @@ -953,7 +953,7 @@ function may_have_fcalls(m::Method) end function analyze_method!(match::MethodMatch, argtypes::Vector{Any}, - @nospecialize(info::CallInfo), flag::UInt8, state::InliningState; + @nospecialize(info::CallInfo), flag::UInt32, state::InliningState; allow_typevars::Bool, invokesig::Union{Nothing,Vector{Any}}=nothing) method = match.method spec_types = match.spec_types @@ -994,7 +994,7 @@ retrieve_ir_for_inlining(mi::MethodInstance, src::CodeInfo) = inflate_ir(src, mi retrieve_ir_for_inlining(mi::MethodInstance, ir::IRCode) = copy(ir) function flags_for_effects(effects::Effects) - flags::UInt8 = 0 + flags::UInt32 = 0 if is_consistent(effects) flags |= IR_FLAG_CONSISTENT end @@ -1178,7 +1178,7 @@ function is_builtin(𝕃ₒ::AbstractLattice, s::Signature) end function handle_invoke_call!(todo::Vector{Pair{Int,Any}}, - ir::IRCode, idx::Int, stmt::Expr, info::InvokeCallInfo, flag::UInt8, + ir::IRCode, idx::Int, stmt::Expr, info::InvokeCallInfo, flag::UInt32, sig::Signature, state::InliningState) match = info.match if !match.fully_covers @@ -1247,6 +1247,14 @@ function check_effect_free!(ir::IRCode, idx::Int, @nospecialize(stmt), @nospecia elseif nothrow ir.stmts[idx][:flag] |= IR_FLAG_NOTHROW end + if !isexpr(stmt, :call) && !isexpr(stmt, :invoke) + # There is a bit of a subtle point here, which is that some non-call + # statements (e.g. PiNode) can be UB:, however, we consider it + # illegal to introduce such statements that actually cause UB (for any + # input). Ideally that'd be handled at insertion time (TODO), but for + # the time being just do that here. + ir.stmts[idx][:flag] |= IR_FLAG_NOUB + end return effect_free_and_nothrow end @@ -1317,7 +1325,7 @@ end function handle_any_const_result!(cases::Vector{InliningCase}, @nospecialize(result), match::MethodMatch, argtypes::Vector{Any}, - @nospecialize(info::CallInfo), flag::UInt8, state::InliningState; + @nospecialize(info::CallInfo), flag::UInt32, state::InliningState; allow_abstract::Bool, allow_typevars::Bool) if isa(result, ConcreteResult) return handle_concrete_result!(cases, result, info, state) @@ -1355,7 +1363,7 @@ function info_effects(@nospecialize(result), match::MethodMatch, state::Inlining end end -function compute_inlining_cases(@nospecialize(info::CallInfo), flag::UInt8, sig::Signature, +function compute_inlining_cases(@nospecialize(info::CallInfo), flag::UInt32, sig::Signature, state::InliningState) nunion = nsplit(info) nunion === nothing && return nothing @@ -1451,7 +1459,7 @@ function compute_inlining_cases(@nospecialize(info::CallInfo), flag::UInt8, sig: end function handle_call!(todo::Vector{Pair{Int,Any}}, - ir::IRCode, idx::Int, stmt::Expr, @nospecialize(info::CallInfo), flag::UInt8, sig::Signature, + ir::IRCode, idx::Int, stmt::Expr, @nospecialize(info::CallInfo), flag::UInt32, sig::Signature, state::InliningState) cases = compute_inlining_cases(info, flag, sig, state) cases === nothing && return nothing @@ -1461,7 +1469,7 @@ function handle_call!(todo::Vector{Pair{Int,Any}}, end function handle_match!(cases::Vector{InliningCase}, - match::MethodMatch, argtypes::Vector{Any}, @nospecialize(info::CallInfo), flag::UInt8, + match::MethodMatch, argtypes::Vector{Any}, @nospecialize(info::CallInfo), flag::UInt32, state::InliningState; allow_abstract::Bool, allow_typevars::Bool) spec_types = match.spec_types @@ -1478,7 +1486,7 @@ end function handle_const_prop_result!(cases::Vector{InliningCase}, result::ConstPropResult, argtypes::Vector{Any}, @nospecialize(info::CallInfo), - flag::UInt8, state::InliningState; + flag::UInt32, state::InliningState; allow_abstract::Bool, allow_typevars::Bool) mi = result.result.linfo spec_types = mi.specTypes @@ -1493,7 +1501,7 @@ function handle_const_prop_result!(cases::Vector{InliningCase}, end function semiconcrete_result_item(result::SemiConcreteResult, - @nospecialize(info::CallInfo), flag::UInt8, state::InliningState) + @nospecialize(info::CallInfo), flag::UInt32, state::InliningState) mi = result.mi if !OptimizationParams(state.interp).inlining || is_stmt_noinline(flag) et = InliningEdgeTracker(state) @@ -1505,7 +1513,7 @@ function semiconcrete_result_item(result::SemiConcreteResult, end function handle_semi_concrete_result!(cases::Vector{InliningCase}, result::SemiConcreteResult, - @nospecialize(info::CallInfo), flag::UInt8, state::InliningState; + @nospecialize(info::CallInfo), flag::UInt32, state::InliningState; allow_abstract::Bool) mi = result.mi spec_types = mi.specTypes @@ -1560,7 +1568,7 @@ end function handle_opaque_closure_call!(todo::Vector{Pair{Int,Any}}, ir::IRCode, idx::Int, stmt::Expr, info::OpaqueClosureCallInfo, - flag::UInt8, sig::Signature, state::InliningState) + flag::UInt32, sig::Signature, state::InliningState) result = info.result if isa(result, ConstPropResult) mi = result.result.linfo @@ -1621,7 +1629,7 @@ function handle_finalizer_call!(ir::IRCode, idx::Int, stmt::Expr, info::Finalize argtypes[2] = argextype(stmt.args[3], ir) sig = Signature(f, ft, argtypes) - cases = compute_inlining_cases(info.info, #=flag=#UInt8(0), sig, state) + cases = compute_inlining_cases(info.info, #=flag=#UInt32(0), sig, state) cases === nothing && return nothing cases, all_covered, _ = cases if all_covered && length(cases) == 1 @@ -1643,7 +1651,7 @@ function handle_finalizer_call!(ir::IRCode, idx::Int, stmt::Expr, info::Finalize end function handle_invoke_expr!(todo::Vector{Pair{Int,Any}}, - idx::Int, stmt::Expr, @nospecialize(info::CallInfo), flag::UInt8, sig::Signature, state::InliningState) + idx::Int, stmt::Expr, @nospecialize(info::CallInfo), flag::UInt32, sig::Signature, state::InliningState) mi = stmt.args[1]::MethodInstance case = resolve_todo(mi, sig.argtypes, info, flag, state) if case !== nothing diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index 6eb902d2ab871..ba7ac1752a229 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -206,7 +206,7 @@ struct InstructionStream type::Vector{Any} info::Vector{CallInfo} line::Vector{Int32} - flag::Vector{UInt8} + flag::Vector{UInt32} end function InstructionStream(len::Int) stmts = Vector{Any}(undef, len) @@ -307,9 +307,9 @@ struct NewInstruction type::Any info::CallInfo line::Union{Int32,Nothing} # if nothing, copy the line from previous statement in the insertion location - flag::Union{UInt8,Nothing} # if nothing, IR flags will be recomputed on insertion + flag::Union{UInt32,Nothing} # if nothing, IR flags will be recomputed on insertion function NewInstruction(@nospecialize(stmt), @nospecialize(type), @nospecialize(info::CallInfo), - line::Union{Int32,Nothing}, flag::Union{UInt8,Nothing}) + line::Union{Int32,Nothing}, flag::Union{UInt32,Nothing}) return new(stmt, type, info, line, flag) end end @@ -322,7 +322,7 @@ function NewInstruction(newinst::NewInstruction; type::Any=newinst.type, info::CallInfo=newinst.info, line::Union{Int32,Nothing}=newinst.line, - flag::Union{UInt8,Nothing}=newinst.flag) + flag::Union{UInt32,Nothing}=newinst.flag) return NewInstruction(stmt, type, info, line, flag) end function NewInstruction(inst::Instruction; @@ -330,19 +330,19 @@ function NewInstruction(inst::Instruction; type::Any=inst[:type], info::CallInfo=inst[:info], line::Union{Int32,Nothing}=inst[:line], - flag::Union{UInt8,Nothing}=inst[:flag]) + flag::Union{UInt32,Nothing}=inst[:flag]) return NewInstruction(stmt, type, info, line, flag) end @specialize effect_free_and_nothrow(newinst::NewInstruction) = NewInstruction(newinst; flag=add_flag(newinst, IR_FLAG_EFFECT_FREE | IR_FLAG_NOTHROW)) -with_flags(newinst::NewInstruction, flags::UInt8) = NewInstruction(newinst; flag=add_flag(newinst, flags)) -without_flags(newinst::NewInstruction, flags::UInt8) = NewInstruction(newinst; flag=sub_flag(newinst, flags)) -function add_flag(newinst::NewInstruction, newflag::UInt8) +with_flags(newinst::NewInstruction, flags::UInt32) = NewInstruction(newinst; flag=add_flag(newinst, flags)) +without_flags(newinst::NewInstruction, flags::UInt32) = NewInstruction(newinst; flag=sub_flag(newinst, flags)) +function add_flag(newinst::NewInstruction, newflag::UInt32) flag = newinst.flag flag === nothing && return newflag return flag | newflag end -function sub_flag(newinst::NewInstruction, newflag::UInt8) +function sub_flag(newinst::NewInstruction, newflag::UInt32) flag = newinst.flag flag === nothing && return IR_FLAG_NULL return flag & ~newflag @@ -827,7 +827,7 @@ function add_pending!(compact::IncrementalCompact, pos::Int, attach_after::Bool) end function inst_from_newinst!(node::Instruction, newinst::NewInstruction, - newline::Int32=newinst.line::Int32, newflag::UInt8=newinst.flag::UInt8) + newline::Int32=newinst.line::Int32, newflag::UInt32=newinst.flag::UInt32) node[:stmt] = newinst.stmt node[:type] = newinst.type node[:info] = newinst.info @@ -850,6 +850,10 @@ function recompute_inst_flag(newinst::NewInstruction, src::Union{IRCode,Incremen elseif nothrow flag |= IR_FLAG_NOTHROW end + if !isexpr(newinst.stmt, :call) && !isexpr(newinst.stmt, :invoke) + # See comment in check_effect_free! + flag |= IR_FLAG_NOUB + end return flag end @@ -1100,7 +1104,7 @@ end # N.B.: Don't make this <: Function to avoid ::Function deopt struct Refiner - result_flags::Vector{UInt8} + result_flags::Vector{UInt32} result_idx::Int end (this::Refiner)() = (this.result_flags[this.result_idx] |= IR_FLAG_REFINED; nothing) diff --git a/base/compiler/ssair/show.jl b/base/compiler/ssair/show.jl index 1b1bdaf7e9e9e..e9e2aa75f755e 100644 --- a/base/compiler/ssair/show.jl +++ b/base/compiler/ssair/show.jl @@ -1017,8 +1017,6 @@ function Base.show(io::IO, e::Effects) printstyled(io, effectbits_letter(e, :inaccessiblememonly, 'm'); color=effectbits_color(e, :inaccessiblememonly)) print(io, ',') printstyled(io, effectbits_letter(e, :noub, 'u'); color=effectbits_color(e, :noub)) - print(io, ',') - printstyled(io, effectbits_letter(e, :noinbounds, 'i'); color=effectbits_color(e, :noinbounds)) print(io, ')') e.nonoverlayed || printstyled(io, '′'; color=:red) end diff --git a/base/compiler/ssair/verify.jl b/base/compiler/ssair/verify.jl index 50e83625da54e..f6b94eac53897 100644 --- a/base/compiler/ssair/verify.jl +++ b/base/compiler/ssair/verify.jl @@ -20,7 +20,7 @@ if !isdefined(@__MODULE__, Symbol("@verify_error")) end end -is_value_pos_expr_head(head::Symbol) = false +is_value_pos_expr_head(head::Symbol) = head === :static_parameter function check_op(ir::IRCode, domtree::DomTree, @nospecialize(op), use_bb::Int, use_idx::Int, printed_use_idx::Int, print::Bool, isforeigncall::Bool, arg_idx::Int, allow_frontend_forms::Bool) if isa(op, SSAValue) if op.id > length(ir.stmts) diff --git a/base/compiler/tfuncs.jl b/base/compiler/tfuncs.jl index 20996be4faaed..650a2d38b2ccd 100644 --- a/base/compiler/tfuncs.jl +++ b/base/compiler/tfuncs.jl @@ -944,7 +944,6 @@ end function getfield_nothrow(𝕃::AbstractLattice, arginfo::ArgInfo, boundscheck::Symbol=getfield_boundscheck(arginfo)) (;argtypes) = arginfo - boundscheck === :unknown && return false ordering = Const(:not_atomic) if length(argtypes) == 4 isvarargtype(argtypes[4]) && return false @@ -2310,7 +2309,7 @@ function getfield_effects(𝕃::AbstractLattice, arginfo::ArgInfo, @nospecialize consistent=CONSISTENT_IF_INACCESSIBLEMEMONLY, nothrow=false, inaccessiblememonly=ALWAYS_FALSE, - noub=false) + noub=ALWAYS_FALSE) end # :consistent if the argtype is immutable consistent = (is_immutable_argtype(obj) || is_mutation_free_argtype(obj)) ? @@ -2325,11 +2324,11 @@ function getfield_effects(𝕃::AbstractLattice, arginfo::ArgInfo, @nospecialize if !(length(argtypes) ≥ 3 && getfield_notundefined(obj, argtypes[3])) consistent = ALWAYS_FALSE end - noub = true + noub = ALWAYS_TRUE bcheck = getfield_boundscheck(arginfo) nothrow = getfield_nothrow(𝕃, arginfo, bcheck) if !nothrow - if !(bcheck === :on || bcheck === :boundscheck) + if bcheck !== :on # If we cannot independently prove inboundsness, taint `:noub`. # The inbounds-ness assertion requires dynamic reachability, # while `:noub` needs to be true for all input values. @@ -2338,7 +2337,7 @@ function getfield_effects(𝕃::AbstractLattice, arginfo::ArgInfo, @nospecialize # based on the `:noinbounds` effect. # N.B. We do not taint for `--check-bounds=no` here. # That is handled in concrete evaluation. - noub = false + noub = ALWAYS_FALSE end end if hasintersect(widenconst(obj), Module) diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 45e4fbc8c7a7c..f1ef36413303e 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -440,7 +440,7 @@ function adjust_effects(sv::InferenceState) # that is currently modeled in a flow-insensitive way: ideally we want to model it # with a proper dataflow analysis instead rt = sv.bestguess - if ipo_effects.noinbounds && rt === Bottom + if rt === Bottom # always throwing an error counts or never returning both count as consistent ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE) end @@ -498,7 +498,7 @@ function adjust_effects(sv::InferenceState) ipo_effects = Effects(ipo_effects; inaccessiblememonly=ALWAYS_TRUE) end if is_effect_overridden(override, :noub) - ipo_effects = Effects(ipo_effects; noub=true) + ipo_effects = Effects(ipo_effects; noub=ALWAYS_TRUE) end end @@ -952,11 +952,11 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance) tree.code = Any[ ReturnNode(quoted(rettype_const)) ] nargs = Int(method.nargs) tree.slotnames = ccall(:jl_uncompress_argnames, Vector{Symbol}, (Any,), method.slot_syms) - tree.slotflags = fill(IR_FLAG_NULL, nargs) + tree.slotflags = fill(0x00, nargs) tree.ssavaluetypes = 1 tree.codelocs = Int32[1] tree.linetable = LineInfoNode[LineInfoNode(method.module, method.name, method.file, method.line, Int32(0))] - tree.ssaflags = UInt8[0] + tree.ssaflags = UInt32[0] set_inlineable!(tree, true) tree.parent = mi tree.rettype = Core.Typeof(rettype_const) diff --git a/base/compiler/validation.jl b/base/compiler/validation.jl index 8aebc53035ca5..3105e42111bd1 100644 --- a/base/compiler/validation.jl +++ b/base/compiler/validation.jl @@ -19,7 +19,7 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}( :inbounds => 1:1, :inline => 1:1, :noinline => 1:1, - :boundscheck => 0:0, + :boundscheck => 0:1, :copyast => 1:1, :meta => 0:typemax(Int), :global => 1:1, @@ -234,7 +234,7 @@ is_valid_lvalue(@nospecialize(x)) = isa(x, SlotNumber) || isa(x, GlobalRef) function is_valid_argument(@nospecialize(x)) if isa(x, SlotNumber) || isa(x, Argument) || isa(x, SSAValue) || - isa(x, GlobalRef) || isa(x, QuoteNode) || is_value_pos_expr_head(x) || + isa(x, GlobalRef) || isa(x, QuoteNode) || (isa(x, Expr) && is_value_pos_expr_head(x.head)) || isa(x, Number) || isa(x, AbstractString) || isa(x, AbstractChar) || isa(x, Tuple) || isa(x, Type) || isa(x, Core.Box) || isa(x, Module) || x === nothing return true diff --git a/src/codegen.cpp b/src/codegen.cpp index f7ae6b0a00680..10296d9794afc 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -5152,7 +5152,7 @@ static void emit_ssaval_assign(jl_codectx_t &ctx, ssize_t ssaidx_0based, jl_valu } slot = emit_varinfo(ctx, it->second, jl_symbol("phic")); } else if (jl_is_expr(r) && ((jl_expr_t*)r)->head == jl_boundscheck_sym) { - uint8_t flag = jl_array_uint8_ref(ctx.source->ssaflags, ssaidx_0based); + uint32_t flag = jl_array_uint32_ref(ctx.source->ssaflags, ssaidx_0based); slot = mark_julia_const(ctx, bounds_check_enabled(ctx, (flag & IR_FLAG_INBOUNDS) ? jl_false : jl_true) ? jl_true : jl_false); } else { slot = emit_expr(ctx, r, ssaidx_0based); // slot could be a jl_value_t (unboxed) or jl_value_t* (ispointer) diff --git a/src/ircode.c b/src/ircode.c index bc5cc61e7f892..508326c96314a 100644 --- a/src/ircode.c +++ b/src/ircode.c @@ -1112,7 +1112,7 @@ void jl_init_serializer(void) jl_methtable_type, jl_typemap_level_type, jl_voidpointer_type, jl_newvarnode_type, jl_abstractstring_type, jl_array_symbol_type, jl_anytuple_type, jl_tparam0(jl_anytuple_type), - jl_emptytuple_type, jl_array_uint8_type, jl_code_info_type, + jl_emptytuple_type, jl_array_uint8_type, jl_array_uint32_type, jl_code_info_type, jl_typeofbottom_type, jl_typeofbottom_type->super, jl_namedtuple_type, jl_array_int32_type, jl_uint32_type, jl_uint64_type, diff --git a/src/jl_exported_data.inc b/src/jl_exported_data.inc index 9c1a454020406..2acde218a104c 100644 --- a/src/jl_exported_data.inc +++ b/src/jl_exported_data.inc @@ -17,6 +17,7 @@ XX(jl_array_type) \ XX(jl_array_typename) \ XX(jl_array_uint8_type) \ + XX(jl_array_uint32_type) \ XX(jl_array_uint64_type) \ XX(jl_atomicerror_type) \ XX(jl_base_module) \ diff --git a/src/jltypes.c b/src/jltypes.c index f3273ae936db3..75bf828949329 100644 --- a/src/jltypes.c +++ b/src/jltypes.c @@ -2871,6 +2871,7 @@ void jl_init_types(void) JL_GC_DISABLED jl_array_any_type = jl_apply_type2((jl_value_t*)jl_array_type, (jl_value_t*)jl_any_type, jl_box_long(1)); jl_array_symbol_type = jl_apply_type2((jl_value_t*)jl_array_type, (jl_value_t*)jl_symbol_type, jl_box_long(1)); jl_array_uint8_type = jl_apply_type2((jl_value_t*)jl_array_type, (jl_value_t*)jl_uint8_type, jl_box_long(1)); + jl_array_uint32_type = jl_apply_type2((jl_value_t*)jl_array_type, (jl_value_t*)jl_uint32_type, jl_box_long(1)); jl_array_int32_type = jl_apply_type2((jl_value_t*)jl_array_type, (jl_value_t*)jl_int32_type, jl_box_long(1)); jl_array_uint64_type = jl_apply_type2((jl_value_t*)jl_array_type, (jl_value_t*)jl_uint64_type, jl_box_long(1)); jl_an_empty_vec_any = (jl_value_t*)jl_alloc_vec_any(0); // used internally @@ -2988,7 +2989,7 @@ void jl_init_types(void) JL_GC_DISABLED jl_array_any_type, jl_array_int32_type, jl_any_type, - jl_array_uint8_type, + jl_array_uint32_type, jl_any_type, jl_any_type, jl_array_symbol_type, @@ -3356,6 +3357,7 @@ void jl_init_types(void) JL_GC_DISABLED ((jl_datatype_t*)jl_array_any_type)->ismutationfree = 0; ((jl_datatype_t*)jl_array_symbol_type)->ismutationfree = 0; ((jl_datatype_t*)jl_array_uint8_type)->ismutationfree = 0; + ((jl_datatype_t*)jl_array_uint32_type)->ismutationfree = 0; ((jl_datatype_t*)jl_array_int32_type)->ismutationfree = 0; ((jl_datatype_t*)jl_array_uint64_type)->ismutationfree = 0; diff --git a/src/julia-syntax.scm b/src/julia-syntax.scm index cc43f6400bb12..82f0f91271bb1 100644 --- a/src/julia-syntax.scm +++ b/src/julia-syntax.scm @@ -4400,7 +4400,7 @@ f(x) = yt(x) (if (null? lst) '() (let ((simple? (every (lambda (x) (or (simple-atom? x) (symbol? x) (and (pair? x) - (memq (car x) '(quote inert top core globalref outerref boundscheck))))) + (memq (car x) '(quote inert top core globalref outerref))))) lst))) (let loop ((lst lst) (vals '())) @@ -4415,7 +4415,7 @@ f(x) = yt(x) (not (simple-atom? arg)) (not (simple-atom? aval)) (not (and (pair? arg) - (memq (car arg) '(quote inert top core boundscheck)))) + (memq (car arg) '(quote inert top core)))) (not (and (symbol? aval) ;; function args are immutable and always assigned (memq aval (lam:args lam)))) (not (and (or (symbol? arg) diff --git a/src/julia.h b/src/julia.h index a96b4a1f5e562..604433711478c 100644 --- a/src/julia.h +++ b/src/julia.h @@ -858,6 +858,7 @@ extern JL_DLLIMPORT jl_value_t *jl_array_uint8_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_value_t *jl_array_any_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_value_t *jl_array_symbol_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_value_t *jl_array_int32_type JL_GLOBALLY_ROOTED; +extern JL_DLLIMPORT jl_value_t *jl_array_uint32_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_value_t *jl_array_uint64_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_datatype_t *jl_expr_type JL_GLOBALLY_ROOTED; extern JL_DLLIMPORT jl_datatype_t *jl_binding_type JL_GLOBALLY_ROOTED; @@ -1131,6 +1132,18 @@ STATIC_INLINE void jl_array_uint8_set(void *a, size_t i, uint8_t x) JL_NOTSAFEPO assert(jl_typetagis(a, jl_array_uint8_type)); ((uint8_t*)(jl_array_data(a)))[i] = x; } +STATIC_INLINE uint8_t jl_array_uint32_ref(void *a, size_t i) JL_NOTSAFEPOINT +{ + assert(i < jl_array_len(a)); + assert(jl_typetagis(a, jl_array_uint32_type)); + return ((uint32_t*)(jl_array_data(a)))[i]; +} +STATIC_INLINE void jl_array_uint32_set(void *a, size_t i, uint8_t x) JL_NOTSAFEPOINT +{ + assert(i < jl_array_len(a)); + assert(jl_typetagis(a, jl_array_uint32_type)); + ((uint32_t*)(jl_array_data(a)))[i] = x; +} #define jl_exprarg(e,n) jl_array_ptr_ref(((jl_expr_t*)(e))->args, n) #define jl_exprargset(e, n, v) jl_array_ptr_set(((jl_expr_t*)(e))->args, n, v) diff --git a/src/method.c b/src/method.c index 55d6ec0f9d815..9f8482f394674 100644 --- a/src/method.c +++ b/src/method.c @@ -299,7 +299,7 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) jl_gc_wb(li, li->code); size_t n = jl_array_len(body); jl_value_t **bd = (jl_value_t**)jl_array_ptr_data((jl_array_t*)li->code); - li->ssaflags = jl_alloc_array_1d(jl_array_uint8_type, n); + li->ssaflags = jl_alloc_array_1d(jl_array_uint32_type, n); jl_gc_wb(li, li->ssaflags); int inbounds_depth = 0; // number of stacked inbounds // isempty(inline_flags): no user annotation @@ -392,7 +392,7 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) li->has_fcall = 1; } if (is_flag_stmt) - jl_array_uint8_set(li->ssaflags, j, 0); + jl_array_uint32_set(li->ssaflags, j, 0); else { uint8_t flag = 0; if (inbounds_depth > 0) @@ -401,7 +401,7 @@ static void jl_code_info_set_ir(jl_code_info_t *li, jl_expr_t *ir) void* inline_flag = inline_flags->items[inline_flags->len - 1]; flag |= 1 << (inline_flag ? 1 : 2); } - jl_array_uint8_set(li->ssaflags, j, flag); + jl_array_uint32_set(li->ssaflags, j, flag); } } assert(inline_flags->len == 0); // malformed otherwise diff --git a/src/staticdata.c b/src/staticdata.c index c05422fd10969..0fce6108f53fd 100644 --- a/src/staticdata.c +++ b/src/staticdata.c @@ -99,7 +99,7 @@ extern "C" { // TODO: put WeakRefs on the weak_refs list during deserialization // TODO: handle finalizers -#define NUM_TAGS 158 +#define NUM_TAGS 159 // An array of references that need to be restored from the sysimg // This is a manually constructed dual of the gvars array, which would be produced by codegen for Julia code, for C. @@ -177,6 +177,7 @@ jl_value_t **const*const get_tags(void) { INSERT_TAG(jl_emptytuple_type); INSERT_TAG(jl_array_symbol_type); INSERT_TAG(jl_array_uint8_type); + INSERT_TAG(jl_array_uint32_type); INSERT_TAG(jl_array_int32_type); INSERT_TAG(jl_array_uint64_type); INSERT_TAG(jl_int32_type); diff --git a/stdlib/Serialization/src/Serialization.jl b/stdlib/Serialization/src/Serialization.jl index 7c1043f33bdfe..fbd02cc1a78dd 100644 --- a/stdlib/Serialization/src/Serialization.jl +++ b/stdlib/Serialization/src/Serialization.jl @@ -80,7 +80,7 @@ const TAGS = Any[ const NTAGS = length(TAGS) @assert NTAGS == 255 -const ser_version = 24 # do not make changes without bumping the version #! +const ser_version = 25 # do not make changes without bumping the version #! format_version(::AbstractSerializer) = ser_version format_version(s::Serializer) = s.version @@ -1160,7 +1160,9 @@ function deserialize(s::AbstractSerializer, ::Type{CodeInfo}) if length(ssaflags) ≠ length(code) # make sure the length of `ssaflags` matches that of `code` # so that the latest inference doesn't throw on IRs serialized from old versions - ssaflags = UInt8[0x00 for _ in 1:length(code)] + ssaflags = UInt32[0x00 for _ in 1:length(code)] + elseif eltype(ssaflags) != UInt32 + ssaflags = map(UInt32, ssaflags) end ci.ssaflags = ssaflags if pre_12 diff --git a/test/compiler/AbstractInterpreter.jl b/test/compiler/AbstractInterpreter.jl index 3af79d3bfc0b6..679c6d86d20ed 100644 --- a/test/compiler/AbstractInterpreter.jl +++ b/test/compiler/AbstractInterpreter.jl @@ -334,13 +334,13 @@ function CC.abstract_call(interp::NoinlineInterpreter, return ret end function CC.inlining_policy(interp::NoinlineInterpreter, - @nospecialize(src), @nospecialize(info::CallInfo), stmt_flag::UInt8, mi::MethodInstance, + @nospecialize(src), @nospecialize(info::CallInfo), stmt_flag::UInt32, mi::MethodInstance, argtypes::Vector{Any}) if isa(info, NoinlineCallInfo) return nothing end return @invoke CC.inlining_policy(interp::CC.AbstractInterpreter, - src::Any, info::CallInfo, stmt_flag::UInt8, mi::MethodInstance, + src::Any, info::CallInfo, stmt_flag::UInt32, mi::MethodInstance, argtypes::Vector{Any}) end diff --git a/test/compiler/effects.jl b/test/compiler/effects.jl index eaf760e6823a7..be3e4da9eee37 100644 --- a/test/compiler/effects.jl +++ b/test/compiler/effects.jl @@ -833,6 +833,11 @@ for op = Any[ end end +# tuple indexing +# -------------- + +@test Core.Compiler.is_foldable(Base.infer_effects(iterate, Tuple{Tuple{Int, Int}, Int})) + # end to end # ---------- diff --git a/test/compiler/invalidation.jl b/test/compiler/invalidation.jl index 2bef61910ac2d..848d475b85b69 100644 --- a/test/compiler/invalidation.jl +++ b/test/compiler/invalidation.jl @@ -138,7 +138,7 @@ begin take!(GLOBAL_BUFFER) let rt = only(Base.return_types(pr48932_callee, (Any,))) @test rt === Any effects = Base.infer_effects(pr48932_callee, (Any,)) - @test Core.Compiler.Effects(effects; noinbounds=false) == Core.Compiler.Effects() + @test Core.Compiler.Effects(effects) == Core.Compiler.Effects() end # run inference on both `pr48932_caller` and `pr48932_callee` @@ -180,7 +180,7 @@ begin take!(GLOBAL_BUFFER) let rt = only(Base.return_types(pr48932_callee_inferrable, (Any,))) @test rt === Int effects = Base.infer_effects(pr48932_callee_inferrable, (Any,)) - @test Core.Compiler.Effects(effects; noinbounds=false) == Core.Compiler.Effects() + @test Core.Compiler.Effects(effects) == Core.Compiler.Effects() end # run inference on both `pr48932_caller` and `pr48932_callee`: @@ -224,7 +224,7 @@ begin take!(GLOBAL_BUFFER) let rt = only(Base.return_types(pr48932_callee_inlined, (Any,))) @test rt === Any effects = Base.infer_effects(pr48932_callee_inlined, (Any,)) - @test Core.Compiler.Effects(effects; noinbounds=false) == Core.Compiler.Effects() + @test Core.Compiler.Effects(effects) == Core.Compiler.Effects() end # run inference on `pr48932_caller_inlined` and `pr48932_callee_inlined` diff --git a/test/compiler/ssair.jl b/test/compiler/ssair.jl index 625ee45cf6b57..b654ad5923b01 100644 --- a/test/compiler/ssair.jl +++ b/test/compiler/ssair.jl @@ -145,7 +145,8 @@ end # Test for bug caused by renaming blocks improperly, related to PR #32145 let ci = make_ci([ # block 1 - Core.Compiler.GotoIfNot(Expr(:boundscheck), 6), + Expr(:boundscheck), + Core.Compiler.GotoIfNot(SSAValue(1), 6), # block 2 Expr(:call, GlobalRef(Base, :size), Core.Compiler.Argument(3)), Core.Compiler.ReturnNode(), @@ -155,12 +156,12 @@ let ci = make_ci([ # block 4 GlobalRef(Main, :something), GlobalRef(Main, :somethingelse), - Expr(:call, Core.SSAValue(6), Core.SSAValue(7)), - Core.Compiler.GotoIfNot(Core.SSAValue(8), 11), + Expr(:call, Core.SSAValue(7), Core.SSAValue(8)), + Core.Compiler.GotoIfNot(Core.SSAValue(9), 12), # block 5 - Core.Compiler.ReturnNode(Core.SSAValue(8)), + Core.Compiler.ReturnNode(Core.SSAValue(9)), # block 6 - Core.Compiler.ReturnNode(Core.SSAValue(8)) + Core.Compiler.ReturnNode(Core.SSAValue(9)) ]) ir = Core.Compiler.inflate_ir(ci) ir = Core.Compiler.compact!(ir, true) @@ -569,19 +570,20 @@ end # Issue #50379 - insert_node!(::IncrementalCompact, ...) at end of basic block let ci = make_ci([ # block 1 - #= %1: =# Core.Compiler.GotoIfNot(Expr(:boundscheck), 3), + #= %1: =# Expr(:boundscheck), + #= %2: =# Core.Compiler.GotoIfNot(SSAValue(1), 4), # block 2 - #= %2: =# Expr(:call, println, Argument(1)), + #= %3: =# Expr(:call, println, Argument(1)), # block 3 - #= %3: =# Core.PhiNode(), - #= %4: =# Core.Compiler.ReturnNode(), + #= %4: =# Core.PhiNode(), + #= %5: =# Core.Compiler.ReturnNode(), ]) ir = Core.Compiler.inflate_ir(ci) # Insert another call at end of "block 2" compact = Core.Compiler.IncrementalCompact(ir) new_inst = NewInstruction(Expr(:call, println, Argument(1)), Nothing) - insert_node!(compact, SSAValue(2), new_inst, #= attach_after =# true) + insert_node!(compact, SSAValue(3), new_inst, #= attach_after =# true) # Complete iteration x = Core.Compiler.iterate(compact) From 0cb5ef929090493294373c1c78f3dc32a8036abb Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Sun, 27 Aug 2023 01:37:36 +0000 Subject: [PATCH 09/14] Emit compilerbarrier as identity in codegen --- src/codegen.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/codegen.cpp b/src/codegen.cpp index 10296d9794afc..c000c21742e7a 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -4203,6 +4203,12 @@ static bool emit_builtin_call(jl_codectx_t &ctx, jl_cgval_t *ret, jl_value_t *f, return true; } + else if (f == jl_builtin_compilerbarrier && (nargs == 2)) { + emit_typecheck(ctx, argv[1], (jl_value_t*)jl_symbol_type, "compilerbarrier"); + *ret = argv[2]; + return true; + } + return false; } From 4bef7d3aecd7eca98b04cf53fef14ea59c735305 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Tue, 29 Aug 2023 04:04:55 +0000 Subject: [PATCH 10/14] Address review and fix rebase mistakes --- base/compiler/abstractinterpretation.jl | 21 +++-------- base/compiler/effects.jl | 8 ++-- base/compiler/optimize.jl | 50 +++++++++++++------------ base/compiler/ssair/slot2ssa.jl | 6 +-- src/codegen.cpp | 4 +- 5 files changed, 42 insertions(+), 47 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 7ad6970e7a188..3e52b77efd020 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -848,7 +848,7 @@ function concrete_eval_eligible(interp::AbstractInterpreter, end end mi = result.edge - if mi !== nothing && is_foldable(effects, !stmt_taints_inbounds_consistency(sv)) + if mi !== nothing && is_foldable(effects) if f !== nothing && is_all_const_arg(arginfo, #=start=#2) if is_nonoverlayed(mi.def::Method) && (!isoverlayed(method_table(interp)) || is_nonoverlayed(effects)) return :concrete_eval @@ -2505,7 +2505,7 @@ function abstract_eval_statement_expr(interp::AbstractInterpreter, e::Expr, vtyp else t = Union{} end - elseif !hasintersect(condt, Bool) + elseif !hasintersect(windenconst(condt), Bool) t = Union{} end elseif false @@ -3003,11 +3003,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) if condval === true @goto fallthrough else - if condval !== false && !nothrow - if !hasintersect(widenconst(orig_condt), Bool) - ssavaluetypes[currpc] = Bottom - @goto find_next_bb - end + if !nothrow && !hasintersect(widenconst(orig_condt), Bool) + ssavaluetypes[currpc] = Bottom + @goto find_next_bb end succs = bbs[currbb].succs @@ -3023,14 +3021,7 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) nextbb = falsebb handle_control_backedge!(interp, frame, currpc, stmt.dest) @goto branch - else - if !nothrow - merge_effects!(interp, frame, EFFECTS_THROWS) - if !hasintersect(widenconst(orig_condt), Bool) - ssavaluetypes[currpc] = Bottom - @goto find_next_bb - end - end + end # We continue with the true branch, but process the false # branch here. diff --git a/base/compiler/effects.jl b/base/compiler/effects.jl index 81b658dfe037e..efe22bb39bc24 100644 --- a/base/compiler/effects.jl +++ b/base/compiler/effects.jl @@ -141,7 +141,7 @@ const NOUB_IF_NOINBOUNDS = 0x01 << 1 const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, ALWAYS_TRUE, true) const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, ALWAYS_TRUE, true) -const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, true) # unknown mostly, but it's not overlayed and noinbounds at least (e.g. it's not a call) +const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, true) # unknown mostly, but it's not overlayed at least (e.g. it's not a call) const _EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_FALSE, false, false, false, ALWAYS_FALSE, ALWAYS_FALSE, false) # unknown really function Effects(effects::Effects = _EFFECTS_UNKNOWN; @@ -190,17 +190,17 @@ is_nothrow(effects::Effects) = effects.nothrow is_terminates(effects::Effects) = effects.terminates is_notaskstate(effects::Effects) = effects.notaskstate is_inaccessiblememonly(effects::Effects) = effects.inaccessiblememonly === ALWAYS_TRUE -is_noub(effects::Effects, noinbounds=true) = effects.noub === ALWAYS_TRUE || (noinbounds && effects.noub === NOUB_IF_NOINBOUNDS) +is_noub(effects::Effects, noinbounds::Bool=true) = effects.noub === ALWAYS_TRUE || (noinbounds && effects.noub === NOUB_IF_NOINBOUNDS) is_nonoverlayed(effects::Effects) = effects.nonoverlayed # implies `is_notaskstate` & `is_inaccessiblememonly`, but not explicitly checked here -is_foldable(effects::Effects, noinbounds=true) = +is_foldable(effects::Effects, noinbounds::Bool=true) = is_consistent(effects) && is_noub(effects, noinbounds) && is_effect_free(effects) && is_terminates(effects) -is_foldable_nothrow(effects::Effects, noinbounds=true) = +is_foldable_nothrow(effects::Effects, noinbounds::Bool=true) = is_foldable(effects, noinbounds) && is_nothrow(effects) diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index d3983cfd517c8..53efec0af94f4 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -15,7 +15,7 @@ const SLOT_USEDUNDEF = 32 # slot has uses that might raise UndefVarError # NOTE make sure to sync the flag definitions below with julia.h and `jl_code_info_set_ir` in method.c -const IR_FLAG_NULL = UInt32(1) +const IR_FLAG_NULL = UInt32(0) # This statement is marked as @inbounds by user. # Ff replaced by inlining, any contained boundschecks may be removed. const IR_FLAG_INBOUNDS = UInt32(1) << 0 @@ -466,12 +466,12 @@ function visit_bb_phis!(callback, ir::IRCode, bb::Int) return end else - callback(idx, stmt) + callback(idx) end end end -function any_stmt_may_throw(ir::IRCode, bb) +function any_stmt_may_throw(ir::IRCode, bb::Int) for stmt in ir.cfg.blocks[bb].stmts if (ir[SSAValue(stmt)][:flag] & IR_FLAG_NOTHROW) != 0 return true @@ -497,6 +497,11 @@ function conditional_successors_may_throw(lazypostdomtree::LazyPostDomtree, ir:: return false end +struct AugmentedDomtree + cfg::CFG + domtree::DomTree +end + function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result::InferenceResult) inconsistent = BitSet() inconsistent_bbs = BitSet() @@ -514,11 +519,10 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: effects = result.ipo_effects - cfg = nothing - domtree = nothing + agdomtree = nothing function get_augmented_domtree() - if cfg !== nothing - return (cfg, domtree) + if agdomtree !== nothing + return agdomtree end cfg = copy(ir.cfg) # Add a virtual basic block to represent the exit @@ -532,10 +536,11 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: end domtree = construct_domtree(cfg.blocks) - return (cfg, domtree) + agdomtree = AugmentedDomtree(cfg, domtree) + return agdomtree end - function is_getfield_boundscheck(inst::Instruction) + function is_getfield_with_boundscheck_arg(inst::Instruction) stmt = inst[:stmt] is_known_call(stmt, getfield, ir) || return false length(stmt.args) < 4 && return false @@ -547,7 +552,7 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: function is_conditional_noub(inst::Instruction) # Special case: `:boundscheck` into `getfield` - is_getfield_boundscheck(inst) || return false + is_getfield_with_boundscheck_arg(inst) || return false barg = inst[:stmt].args[end] isexpr(ir[barg][:stmt], :boundscheck) || return false # If IR_FLAG_INBOUNDS is already set, no more conditional ub @@ -567,12 +572,12 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: end end - function scan_inconsistency!(inst::Instruction, idx) + function scan_inconsistency!(inst::Instruction, idx::Int) flag = inst[:flag] stmt_inconsistent = (flag & IR_FLAG_CONSISTENT) == 0 stmt = inst[:stmt] # Special case: For getfield, we allow inconsistency of the :boundscheck argument - if is_getfield_boundscheck(inst) + if is_getfield_with_boundscheck_arg(inst) for i = 1:(length(stmt.args)-1) val = stmt.args[i] if isa(val, SSAValue) @@ -600,7 +605,7 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: if isexpr(stmt, :enter) # try/catch not yet modeled had_trycatch = true - return true + return false end scan_non_dataflow_flags!(inst) @@ -611,7 +616,6 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: all_retpaths_consistent = false elseif isa(stmt, GotoIfNot) && stmt_inconsistent # Conditional Branch with inconsistent condition. - sa, sb = ir.cfg.blocks[bb].succs # If we do not know this function terminates, taint consistency, now, # :consistent requires consistent termination. TODO: Just look at the # inconsistent region. @@ -621,14 +625,14 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: elseif conditional_successors_may_throw(lazypostdomtree, ir, bb) all_retpaths_consistent = false else - (cfg, domtree) = get_augmented_domtree() - for succ in iterated_dominance_frontier(cfg, BlockLiveness((sa, sb), nothing), domtree) + (; cfg, domtree) = get_augmented_domtree() + for succ in iterated_dominance_frontier(cfg, BlockLiveness(ir.cfg.blocks[bb].succs, nothing), domtree) if succ == length(cfg.blocks) # Phi node in the virtual exit -> We have a conditional # return. TODO: Check if all the retvals are egal. all_retpaths_consistent = false else - visit_bb_phis!(ir, succ) do phiidx::Int, phi::PhiNode + visit_bb_phis!(ir, succ) do phiidx::Int push!(inconsistent, phiidx) end end @@ -642,7 +646,7 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: if !scan!(scan_stmt!, scanner, true) if !all_retpaths_consistent # No longer any dataflow concerns, just scan the flags - scan!(scanner, false) do inst, idx, lstmt, bb + scan!(scanner, false) do inst::Instruction, idx::Int, lstmt::Int, bb::Int scan_non_dataflow_flags!(inst) return true end @@ -664,7 +668,7 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: idx = popfirst!(stmt_ip) inst = ir[SSAValue(idx)] stmt = inst[:inst] - if is_getfield_boundscheck(inst) + if is_getfield_with_boundscheck_arg(inst) any_non_boundscheck_inconsistent = false for i = 1:(length(stmt.args)-1) val = stmt.args[i] @@ -678,9 +682,8 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: all_retpaths_consistent = false else isa(stmt, GotoIfNot) bb = block_for_inst(ir, idx) - sa, sb = ir.cfg.blocks[bb].succs - for succ in iterated_dominance_frontier(ir.cfg, BlockLiveness((sa, sb), nothing), get!(lazydomtree)) - visit_bb_phis!(ir, succ) do (phiidx, phi) + for succ in iterated_dominance_frontier(ir.cfg, BlockLiveness(ir.cfg.blocks[bb].succs, nothing), get!(lazydomtree)) + visit_bb_phis!(ir, succ) do phiidx push!(inconsistent, phiidx) push!(stmt_ip, phiidx) end @@ -800,8 +803,9 @@ function convert_to_ircode(ci::CodeInfo, sv::OptimizationState) # - typeassert if must-throw block = block_for_inst(sv.cfg, i) if ssavaluetypes[i] === Bottom + destblock = block_for_inst(sv.cfg, expr.dest) cfg_delete_edge!(sv.cfg, block, block + 1) - cfg_delete_edge!(sv.cfg, block, block_for_inst(sv.cfg, expr.dest)) + ((block + 1) != destblock) && cfg_delete_edge!(sv.cfg, block, destblock) expr = Expr(:call, Core.typeassert, expr.cond, Bool) elseif i + 1 in sv.unreachable @assert (ci.ssaflags[i] & IR_FLAG_NOTHROW) != 0 diff --git a/base/compiler/ssair/slot2ssa.jl b/base/compiler/ssair/slot2ssa.jl index 90d11036e6d30..07c190fff4211 100644 --- a/base/compiler/ssair/slot2ssa.jl +++ b/base/compiler/ssair/slot2ssa.jl @@ -216,9 +216,9 @@ function typ_for_val(@nospecialize(x), ci::CodeInfo, ir::IRCode, idx::Int, slott return Const(x) end -struct BlockLiveness{D, L <: Union{Vector{Int}, Nothing}} - def_bbs::D - live_in_bbs::L +struct BlockLiveness + def_bbs::Vector{Int} + live_in_bbs::Union{Vector{Int}, Nothing} end """ diff --git a/src/codegen.cpp b/src/codegen.cpp index c000c21742e7a..c5af2fcaa68d5 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -8901,9 +8901,9 @@ static jl_llvm_functions_t jl_emit_oc_wrapper(orc::ThreadSafeModule &m, jl_codeg static int effects_foldable(uint32_t effects) { - // N.B.: This needs to be kept in sync with Core.Compiler.is_foldable + // N.B.: This needs to be kept in sync with Core.Compiler.is_foldable(effects, true) return ((effects & 0x7) == 0) && // is_consistent(effects) - ((effects >> 10) & 0x01) && // is_noub(effects) + (((effects >> 10) & 0x03) == 0) && // is_noub(effects) (((effects >> 3) & 0x03) == 0) && // is_effect_free(effects) ((effects >> 6) & 0x01); // is_terminates(effects) } From 3b92b5db5c8f26fe1238123fc0c179fa1a5abefb Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Tue, 29 Aug 2023 05:11:09 +0000 Subject: [PATCH 11/14] Switch :boundscheck to fully use extra Expr arg Per in-person review from Jameson. --- base/compiler/ssair/inlining.jl | 9 ++------- src/codegen.cpp | 6 ++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index e70864922f60b..e989ba2a87c2c 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -388,14 +388,9 @@ end function adjust_boundscheck!(inline_compact, idx′, stmt, boundscheck) if boundscheck === :off - if length(stmt.args) == 0 - inline_compact[SSAValue(idx′)][:flag] |= IR_FLAG_INBOUNDS - end + length(stmt.args) == 0 && push!(stmt.args, false) elseif boundscheck !== :propagate - if (inline_compact[SSAValue(idx′)][:flag] & IR_FLAG_INBOUNDS) == 0 - # Prevent future inlining passes from setting IR_FLAG_INBOUNDS - length(stmt.args) == 0 && push!(stmt.args, true) - end + length(stmt.args) == 0 && push!(stmt.args, true) end end diff --git a/src/codegen.cpp b/src/codegen.cpp index c5af2fcaa68d5..cd129edbaf1be 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -5157,9 +5157,6 @@ static void emit_ssaval_assign(jl_codectx_t &ctx, ssize_t ssaidx_0based, jl_valu it = ctx.phic_slots.emplace(ssaidx_0based, jl_varinfo_t(ctx.builder.getContext())).first; } slot = emit_varinfo(ctx, it->second, jl_symbol("phic")); - } else if (jl_is_expr(r) && ((jl_expr_t*)r)->head == jl_boundscheck_sym) { - uint32_t flag = jl_array_uint32_ref(ctx.source->ssaflags, ssaidx_0based); - slot = mark_julia_const(ctx, bounds_check_enabled(ctx, (flag & IR_FLAG_INBOUNDS) ? jl_false : jl_true) ? jl_true : jl_false); } else { slot = emit_expr(ctx, r, ssaidx_0based); // slot could be a jl_value_t (unboxed) or jl_value_t* (ispointer) } @@ -5836,7 +5833,8 @@ static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr, ssize_t ssaidx_ jl_errorf("Expr(:%s) in value position", jl_symbol_name(head)); } else if (head == jl_boundscheck_sym) { - return mark_julia_const(ctx, bounds_check_enabled(ctx, jl_true) ? jl_true : jl_false); + jl_value_t *def = nargs == 0 ? jl_true : args[0]; + return mark_julia_const(ctx, bounds_check_enabled(ctx, def) ? jl_true : jl_false); } else if (head == jl_gc_preserve_begin_sym) { SmallVector argv(nargs); From fa2255c0c98b4cdb51f498d5e89c4c642aeac74b Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Tue, 29 Aug 2023 06:33:36 +0000 Subject: [PATCH 12/14] Support noub refinement in irinterp --- base/compiler/abstractinterpretation.jl | 5 ++++- base/compiler/optimize.jl | 5 +++-- base/compiler/ssair/inlining.jl | 3 +++ base/compiler/ssair/irinterp.jl | 25 +++++++++++++++---------- src/codegen.cpp | 2 +- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 3e52b77efd020..90b73d066ba39 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1150,7 +1150,7 @@ function semi_concrete_eval_call(interp::AbstractInterpreter, irsv = IRInterpretationState(interp, code, mi, arginfo.argtypes, world) if irsv !== nothing irsv.parent = sv - rt, nothrow = ir_abstract_constant_propagation(interp, irsv) + rt, (nothrow, noub) = ir_abstract_constant_propagation(interp, irsv) @assert !(rt isa Conditional || rt isa MustAlias) "invalid lattice element returned from irinterp" if !(isa(rt, Type) && hasintersect(rt, Bool)) ir = irsv.ir @@ -1162,6 +1162,9 @@ function semi_concrete_eval_call(interp::AbstractInterpreter, if !is_nothrow(effects) effects = Effects(effects; nothrow) end + if noub + effects = Effects(effects; noub = ALWAYS_TRUE) + end return ConstCallResults(rt, SemiConcreteResult(mi, ir, effects), effects, mi) end end diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 53efec0af94f4..604a9a719c7b6 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -554,9 +554,10 @@ function ipo_dataflow_analysis!(interp::AbstractInterpreter, ir::IRCode, result: # Special case: `:boundscheck` into `getfield` is_getfield_with_boundscheck_arg(inst) || return false barg = inst[:stmt].args[end] - isexpr(ir[barg][:stmt], :boundscheck) || return false + bstmt = ir[barg][:stmt] + isexpr(bstmt, :boundscheck) || return false # If IR_FLAG_INBOUNDS is already set, no more conditional ub - (ir[barg][:flag] & IR_FLAG_INBOUNDS) != 0 && return false + (length(bstmt.args) != 0 && bstmt.args[1] === false) && return false any_conditional_ub = true return true end diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index e989ba2a87c2c..c681522745a9b 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -999,6 +999,9 @@ function flags_for_effects(effects::Effects) if is_nothrow(effects) flags |= IR_FLAG_NOTHROW end + if is_noub(effects, false) + flags |= IR_FLAG_NOUB + end return flags end diff --git a/base/compiler/ssair/irinterp.jl b/base/compiler/ssair/irinterp.jl index 2bbd802e8d539..4d1b03079e01f 100644 --- a/base/compiler/ssair/irinterp.jl +++ b/base/compiler/ssair/irinterp.jl @@ -10,9 +10,9 @@ function concrete_eval_invoke(interp::AbstractInterpreter, world = frame_world(irsv) mi_cache = WorldView(code_cache(interp), world) code = get(mi_cache, mi, nothing) - code === nothing && return Pair{Any,Bool}(nothing, false) + code === nothing && return Pair{Any,Tuple{Bool, Bool}}(nothing, (false, false)) argtypes = collect_argtypes(interp, inst.args[2:end], nothing, irsv) - argtypes === nothing && return Pair{Any,Bool}(Bottom, false) + argtypes === nothing && return Pair{Any,Tuple{Bool, Bool}}(Bottom, (false, false)) effects = decode_effects(code.ipo_purity_bits) if (is_foldable(effects) && is_all_const_arg(argtypes, #=start=#1) && is_nonoverlayed(effects) && is_nonoverlayed(mi.def::Method)) @@ -21,20 +21,20 @@ function concrete_eval_invoke(interp::AbstractInterpreter, try Core._call_in_world_total(world, args...) catch - return Pair{Any,Bool}(Bottom, false) + return Pair{Any,Tuple{Bool, Bool}}(Bottom, (false, is_noub(effects, false))) end end - return Pair{Any,Bool}(Const(value), true) + return Pair{Any,Tuple{Bool, Bool}}(Const(value), (true, true)) else if is_constprop_edge_recursed(mi, irsv) - return Pair{Any,Bool}(nothing, is_nothrow(effects)) + return Pair{Any,Tuple{Bool, Bool}}(nothing, (is_nothrow(effects), is_noub(effects, false))) end newirsv = IRInterpretationState(interp, code, mi, argtypes, world) if newirsv !== nothing newirsv.parent = irsv return ir_abstract_constant_propagation(interp, newirsv) end - return Pair{Any,Bool}(nothing, is_nothrow(effects)) + return Pair{Any,Tuple{Bool, Bool}}(nothing, (is_nothrow(effects), is_noub(effects, false))) end end @@ -129,10 +129,13 @@ function reprocess_instruction!(interp::AbstractInterpreter, idx::Int, bb::Union (; rt, effects) = abstract_eval_statement_expr(interp, stmt, nothing, irsv) inst[:flag] |= flags_for_effects(effects) elseif head === :invoke - rt, nothrow = concrete_eval_invoke(interp, stmt, stmt.args[1]::MethodInstance, irsv) + rt, (nothrow, noub) = concrete_eval_invoke(interp, stmt, stmt.args[1]::MethodInstance, irsv) if nothrow inst[:flag] |= IR_FLAG_NOTHROW end + if noub + inst[:flag] |= IR_FLAG_NOUB + end elseif head === :throw_undef_if_not condval = maybe_extract_const_bool(argextype(stmt.args[2], ir)) condval isa Bool || return false @@ -375,11 +378,13 @@ function _ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IR end end - nothrow = true + nothrow = noub = true for idx = 1:length(ir.stmts) if (ir[SSAValue(idx)][:flag] & IR_FLAG_NOTHROW) == 0 nothrow = false - break + end + if (ir[SSAValue(idx)][:flag] & IR_FLAG_NOUB) == 0 + noub = false end end @@ -389,7 +394,7 @@ function _ir_abstract_constant_propagation(interp::AbstractInterpreter, irsv::IR store_backedges(frame_instance(irsv), irsv.edges) end - return Pair{Any,Bool}(maybe_singleton_const(ultimate_rt), nothrow) + return Pair{Any,Tuple{Bool, Bool}}(maybe_singleton_const(ultimate_rt), (nothrow, noub)) end function ir_abstract_constant_propagation(interp::NativeInterpreter, irsv::IRInterpretationState) diff --git a/src/codegen.cpp b/src/codegen.cpp index cd129edbaf1be..c68110407b6e5 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -5833,7 +5833,7 @@ static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr, ssize_t ssaidx_ jl_errorf("Expr(:%s) in value position", jl_symbol_name(head)); } else if (head == jl_boundscheck_sym) { - jl_value_t *def = nargs == 0 ? jl_true : args[0]; + jl_value_t *def = (nargs == 0) ? jl_true : args[0]; return mark_julia_const(ctx, bounds_check_enabled(ctx, def) ? jl_true : jl_false); } else if (head == jl_gc_preserve_begin_sym) { From dac139e542be653a1bf881c10c68bcb018c38cfa Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Wed, 30 Aug 2023 00:13:22 +0000 Subject: [PATCH 13/14] Update doctest --- doc/src/devdocs/inference.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/doc/src/devdocs/inference.md b/doc/src/devdocs/inference.md index b6614d060a0c8..0ec40378e792c 100644 --- a/doc/src/devdocs/inference.md +++ b/doc/src/devdocs/inference.md @@ -96,18 +96,20 @@ Each statement gets analyzed for its total cost in a function called as follows: ```jldoctest; filter=r"tuple.jl:\d+" julia> Base.print_statement_costs(stdout, map, (typeof(sqrt), Tuple{Int},)) # map(sqrt, (2,)) -map(f, t::Tuple{Any}) @ Base tuple.jl:273 - 0 1 ─ %1 = Base.getfield(_3, 1, true)::Int64 - 1 │ %2 = Base.sitofp(Float64, %1)::Float64 - 2 │ %3 = Base.lt_float(%2, 0.0)::Bool - 0 └── goto #3 if not %3 - 0 2 ─ invoke Base.Math.throw_complex_domainerror(:sqrt::Symbol, %2::Float64)::Union{} +map(f, t::Tuple{Any}) @ Base tuple.jl:291 + 0 1 ─ %1 = $(Expr(:boundscheck, true))::Bool + 0 │ %2 = Base.getfield(_3, 1, %1)::Int64 + 1 │ %3 = Base.sitofp(Float64, %2)::Float64 + 2 │ %4 = Base.lt_float(%3, 0.0)::Bool + 0 └── goto #3 if not %4 + 0 2 ─ invoke Base.Math.throw_complex_domainerror(:sqrt::Symbol, %3::Float64)::Union{} 0 └── unreachable - 20 3 ─ %7 = Base.Math.sqrt_llvm(%2)::Float64 + 20 3 ─ %8 = Base.Math.sqrt_llvm(%3)::Float64 0 └── goto #4 0 4 ─ goto #5 - 0 5 ─ %10 = Core.tuple(%7)::Tuple{Float64} - 0 └── return %10 + 0 5 ─ %11 = Core.tuple(%8)::Tuple{Float64} + 0 └── return %11 + ``` The line costs are in the left column. This includes the consequences of inlining and other forms of optimization. From c84f526f14da874d378575092da7881ffdf6fa2c Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Wed, 30 Aug 2023 02:48:52 +0000 Subject: [PATCH 14/14] Fix IncrementalCompact of PhiCNode with cfg mod --- base/compiler/ssair/ir.jl | 16 +++++++++++++++- test/compiler/irpasses.jl | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/base/compiler/ssair/ir.jl b/base/compiler/ssair/ir.jl index ba7ac1752a229..fa857b4f70ea7 100644 --- a/base/compiler/ssair/ir.jl +++ b/base/compiler/ssair/ir.jl @@ -1469,7 +1469,21 @@ function process_node!(compact::IncrementalCompact, result_idx::Int, inst::Instr end elseif isa(stmt, PhiCNode) ssa_rename[idx] = SSAValue(result_idx) - result[result_idx][:stmt] = PhiCNode(process_phinode_values(stmt.values, late_fixup, processed_idx, result_idx, ssa_rename, used_ssas, new_new_used_ssas, do_rename_ssa, mark_refined!)) + values = stmt.values + if cfg_transforms_enabled + # Filter arguments that come from dead blocks + values = Any[] + for value in stmt.values + if isa(value, SSAValue) + blk = block_for_inst(compact.ir.cfg, value.id) + if bb_rename_pred[blk] < 0 + continue + end + end + push!(values, value) + end + end + result[result_idx][:stmt] = PhiCNode(process_phinode_values(values, late_fixup, processed_idx, result_idx, ssa_rename, used_ssas, new_new_used_ssas, do_rename_ssa, mark_refined!)) result_idx += 1 else if isa(stmt, SSAValue) diff --git a/test/compiler/irpasses.jl b/test/compiler/irpasses.jl index 258647311ae55..4869def43324c 100644 --- a/test/compiler/irpasses.jl +++ b/test/compiler/irpasses.jl @@ -1452,3 +1452,42 @@ end fully_eliminated(; retval=Core.Argument(2)) do x::Float64 return ifelse(isa(x, Float64), x, exp(x)) end + +# PhiC fixup of compact! with cfg modification +@inline function big_dead_throw_catch() + x = 1 + try + x = 2 + if Ref{Bool}(false)[] + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + Base.donotdelete(x) + x = 3 + end + catch + return x + end +end + +function call_big_dead_throw_catch() + if Ref{Bool}(false)[] + return big_dead_throw_catch() + end + return 4 +end