Skip to content

Commit 1fb2a44

Browse files
committed
Exclude typedefs, if desired
By default, LoweredCodeUtils will mark the type-definition lines as part of the constructor dependencies. When selectively executed, this leads to attempted redefinition of the type, which generally (but not always) succeeds if the redefined type is the same as the original. Exceptions include key types in Core.Compiler and LinearAlgebra. Hence, it's better to support a mode in which you avoid any type redefinition. This also: - switches to using :global as markers for the beginning and end of a typedef - lowers the optimization level for compilation for reduced latency - does a little bit of code cleanup
1 parent 31a94ef commit 1fb2a44

File tree

5 files changed

+201
-95
lines changed

5 files changed

+201
-95
lines changed

src/LoweredCodeUtils.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
module LoweredCodeUtils
22

3+
if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel"))
4+
@eval Base.Experimental.@optlevel 1
5+
end
6+
37
using Core: SimpleVector, CodeInfo, NewvarNode, GotoNode
48
using Base.Meta: isexpr
59
using JuliaInterpreter

src/codeedges.jl

Lines changed: 79 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,6 @@ function CodeEdges(src::CodeInfo)
391391
end
392392

393393
function CodeEdges(src::CodeInfo, cl::CodeLinks)
394-
function pushall!(dest, src)
395-
for item in src
396-
push!(dest, item)
397-
end
398-
return dest
399-
end
400394
# The main task here is to elide the slot-dependencies and convert
401395
# everything to just ssas & names.
402396
# Hence we "follow" slot links to their non-slot leaves.
@@ -560,86 +554,125 @@ will end up skipping a subset of such statements, perhaps while repeating others
560554
561555
See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
562556
"""
563-
function lines_required(obj::Union{Symbol,GlobalRef}, src::CodeInfo, edges::CodeEdges)
557+
function lines_required(obj::Union{Symbol,GlobalRef}, src::CodeInfo, edges::CodeEdges; kwargs...)
564558
isrequired = falses(length(edges.preds))
565559
objs = Set{Union{Symbol,GlobalRef}}([obj])
566-
return lines_required!(isrequired, objs, src, edges)
560+
return lines_required!(isrequired, objs, src, edges; kwargs...)
567561
end
568562

569-
function lines_required(idx::Int, src::CodeInfo, edges::CodeEdges)
563+
function lines_required(idx::Int, src::CodeInfo, edges::CodeEdges; kwargs...)
570564
isrequired = falses(length(edges.preds))
571565
isrequired[idx] = true
572566
objs = Set{Union{Symbol,GlobalRef}}()
573-
return lines_required!(isrequired, src, edges)
567+
return lines_required!(isrequired, src, edges; kwargs...)
574568
end
575569

576570
"""
577-
lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges)
571+
lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges; exclude_named_typedefs::Bool=false)
578572
579573
Like `lines_required`, but where `isrequired[idx]` has already been set to `true` for all statements
580574
that you know you need to evaluate. All other statements should be marked `false` at entry.
581575
On return, the complete set of required statements will be marked `true`.
576+
577+
Use `exclude_named_typedefs=true` if you're extracting method signatures and not evaluating new definitions.
582578
"""
583-
function lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges)
579+
function lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges; kwargs...)
584580
objs = Set{Union{Symbol,GlobalRef}}()
585-
return lines_required!(isrequired, objs, src, edges)
581+
return lines_required!(isrequired, objs, src, edges; kwargs...)
586582
end
587583

588-
function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo, edges::CodeEdges)
584+
function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo, edges::CodeEdges; exclude_named_typedefs::Bool=false)
589585
# Do a traveral of "numbered" predecessors
590-
function add_preds!(isrequired, idx, edges::CodeEdges)
586+
function add_preds!(isrequired, idx, edges::CodeEdges, norequire)
591587
changed = false
592588
preds = edges.preds[idx]
593589
for p in preds
594590
isrequired[p] && continue
591+
p norequire && continue
595592
isrequired[p] = true
596593
changed = true
597-
add_preds!(isrequired, p, edges)
594+
add_preds!(isrequired, p, edges, norequire)
598595
end
599596
return changed
600597
end
601-
function add_succs!(isrequired, idx, edges::CodeEdges, succs)
598+
function add_succs!(isrequired, idx, edges::CodeEdges, succs, norequire)
602599
changed = false
603600
for p in succs
604601
isrequired[p] && continue
602+
p norequire && continue
605603
isrequired[p] = true
606604
changed = true
607-
add_succs!(isrequired, p, edges, edges.succs[p])
605+
add_succs!(isrequired, p, edges, edges.succs[p], norequire)
608606
end
609607
return changed
610608
end
611-
function add_obj!(isrequired, objs, obj, edges::CodeEdges)
609+
function add_obj!(isrequired, objs, obj, edges::CodeEdges, norequire)
612610
changed = false
613611
for d in edges.byname[obj].assigned
614-
isrequired[d] || add_preds!(isrequired, d, edges)
612+
d norequire && continue
613+
isrequired[d] || add_preds!(isrequired, d, edges, norequire)
615614
isrequired[d] = true
616615
changed = true
617616
end
618617
push!(objs, obj)
619618
return changed
620619
end
621620

621+
# We'll mostly use generic graph traversal to discover all the lines we need,
622+
# but structs are in a bit of a different category (especially on Julia 1.5+).
623+
# It's easiest to discover these at the beginning.
624+
# Moreover, if we're excluding named type definitions, we'll add them to `norequire`
625+
# to prevent them from being marked.
626+
typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
627+
norequire = BitSet()
628+
nstmts = length(src.code)
629+
i = 1
630+
while i <= nstmts
631+
stmt = rhs(src.code[i])
632+
if istypedef(stmt) && !isanonymous_typedef(stmt)
633+
r = typedef_range(src, i)
634+
push!(typedef_blocks, r)
635+
name = stmt.head === :call ? stmt.args[3] : stmt.args[1]
636+
if isa(name, QuoteNode)
637+
name = name.value
638+
end
639+
isa(name, Symbol) || @show src i r stmt
640+
push!(typedef_names, name::Symbol)
641+
i = last(r)+1
642+
if exclude_named_typedefs && !isanonymous_typedef(stmt)
643+
pushall!(norequire, r)
644+
end
645+
else
646+
i += 1
647+
end
648+
end
649+
650+
# Mark any requested objects (their lines of assignment)
622651
objsnew = Set{Union{Symbol,GlobalRef}}()
623652
for obj in objs
624-
add_obj!(isrequired, objsnew, obj, edges)
653+
add_obj!(isrequired, objsnew, obj, edges, norequire)
625654
end
626655
objs = objsnew
656+
657+
# Compute basic blocks, which we'll use to make sure we mark necessary control-flow
627658
bbs = Core.Compiler.compute_basic_blocks(src.code) # needed for control-flow analysis
659+
nblocks = length(bbs.blocks)
660+
628661
changed = true
629662
iter = 0
630663
while changed
631664
changed = false
632665
# Handle ssa predecessors
633-
for idx = 1:length(isrequired)
666+
for idx = 1:nstmts
634667
if isrequired[idx]
635-
changed |= add_preds!(isrequired, idx, edges)
668+
changed |= add_preds!(isrequired, idx, edges, norequire)
636669
end
637670
end
638671
# Handle named dependencies
639672
for (obj, uses) in edges.byname
640673
obj objs && continue
641674
if any(view(isrequired, uses.succs))
642-
changed |= add_obj!(isrequired, objs, obj, edges)
675+
changed |= add_obj!(isrequired, objs, obj, edges, norequire)
643676
end
644677
end
645678
# Add control-flow. For any basic block with an evaluated statement inside it,
@@ -648,22 +681,24 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
648681
for (ibb, bb) in enumerate(bbs.blocks)
649682
r = rng(bb)
650683
if any(view(isrequired, r))
651-
# if !isempty(bb.succs)
652-
if ibb != length(bbs.blocks)
684+
if ibb != nblocks
653685
idxlast = r[end]
686+
idxlast norequire && continue
654687
changed |= !isrequired[idxlast]
655688
isrequired[idxlast] = true
656689
end
657690
for ibbp in bb.preds
658691
rpred = rng(bbs.blocks[ibbp])
659692
idxlast = rpred[end]
693+
idxlast norequire && continue
660694
changed |= !isrequired[idxlast]
661695
isrequired[idxlast] = true
662696
end
663697
for ibbs in bb.succs
664-
ibbs == length(bbs.blocks) && continue
698+
ibbs == nblocks && continue
665699
rpred = rng(bbs.blocks[ibbs])
666700
idxlast = rpred[end]
701+
idxlast norequire && continue
667702
changed |= !isrequired[idxlast]
668703
isrequired[idxlast] = true
669704
end
@@ -674,55 +709,35 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
674709
# statements. If we're evaluating any of them, it's important to evaluate *all* of them.
675710
for (idx, stmt) in enumerate(src.code)
676711
isrequired[idx] || continue
677-
if isexpr(stmt, :(=))
678-
stmt = stmt.args[2]
679-
end
680-
# Is this a struct definition?
681-
if (isa(stmt, Expr) && stmt.head structheads) || # < Julia 1.5
682-
(isexpr(stmt, :call) && callee_matches(stmt.args[1], Core, :_structtype)) # >= Julia 1.5
683-
stmt = stmt::Expr
684-
name = stmt.args[stmt.head === :call ? 3 : 1]
685-
if isa(name, QuoteNode)
686-
name = name.value
687-
end
688-
name = name::NamedVar
689-
# Some lines we need have been marked as successors of this name
690-
for d in edges.byname[name].succs
691-
stmt2 = src.code[d]
692-
if isa(stmt2, Expr)
693-
head = stmt2.head
694-
if head === :method || head === :global || head === :const
695-
changed |= !isrequired[d]
696-
isrequired[d] = true
697-
end
698-
end
699-
# Julia 1.5+: others are successor of a slotnum->ssa load
700-
if isslotnum(stmt2)
701-
for s in edges.succs[d]
702-
stmt3 = src.code[s]
703-
if isexpr(stmt3, :call) && (callee_matches(stmt3.args[1], Core, :_setsuper!) ||
704-
callee_matches(stmt3.args[1], Core, :_typebody!))
705-
changed |= !isrequired[s]
706-
isrequired[s] = true
712+
for (typedefr, typedefn) in zip(typedef_blocks, typedef_names)
713+
if idx typedefr
714+
ireq = view(isrequired, typedefr)
715+
if !all(ireq)
716+
changed = true
717+
ireq .= true
718+
# Also mark any by-type constructor(s) associated with this typedef
719+
var = get(edges.byname, typedefn, nothing)
720+
if var !== nothing
721+
for s in var.succs
722+
s norequire && continue
723+
stmt2 = src.code[s]
724+
if isexpr(stmt2, :method) && (stmt2::Expr).args[1] === false
725+
isrequired[s] = true
726+
end
707727
end
708728
end
709729
end
710-
# Julia 1.5+: for non-parametric types, the Core._setsuper! call happens without the slotname->ssa load
711-
if isexpr(stmt2, :call) && callee_matches((stmt2::Expr).args[1], Core, :_setsuper!)
712-
changed |= !isrequired[d]
713-
isrequired[d] = true
714-
end
715730
end
716731
end
717732
# Anonymous functions may not yet include the method definition
718-
if isexpr(stmt, :thunk) && isanonymous_typedef(stmt.args[1])
733+
if isanonymous_typedef(stmt)
719734
i = idx + 1
720735
while i <= length(src.code) && !ismethod3(src.code[i])
721736
i += 1
722737
end
723738
if i <= length(src.code) && src.code[i].args[1] == false
724739
tpreds = terminal_preds(i, edges)
725-
if minimum(tpreds) == idx
740+
if minimum(tpreds) == idx && i norequire
726741
changed |= !isrequired[i]
727742
isrequired[i] = true
728743
end
@@ -734,14 +749,6 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
734749
return isrequired
735750
end
736751

737-
function callee_matches(f, mod, sym)
738-
is_global_ref(f, mod, sym) && return true
739-
if isdefined(mod, sym)
740-
is_quotenode(f, getfield(mod, sym)) && return true
741-
end
742-
return false
743-
end
744-
745752
"""
746753
selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)
747754

src/signatures.jl

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ function signature(@nospecialize(recurse), frame::Frame, @nospecialize(stmt), pc
4141
mod = moduleof(frame)
4242
lastpc = frame.pc = pc
4343
while !isexpr(stmt, :method, 3) # wait for the 3-arg version
44-
if isexpr(stmt, :thunk) && isanonymous_typedef(stmt.args[1])
45-
lastpc = pc = define_anonymous(recurse, frame, stmt)
44+
if isanonymous_typedef(stmt)
45+
lastpc = pc = step_through_methoddef(recurse, frame, stmt) # define an anonymous function
4646
elseif isexpr(stmt, :call) && is_quotenode(stmt.args[1], Core.Typeof) &&
4747
(sym = stmt.args[2]; isa(sym, Symbol) && !isdefined(mod, sym))
4848
return nothing, pc
@@ -78,25 +78,7 @@ function signature_top(frame, stmt::Expr, pc)
7878
return minid(stmt.args[2], frame.framecode.src.code, pc)
7979
end
8080

81-
##
82-
## Detecting anonymous functions. These start with a :thunk expr and have a characteristic CodeInfo
83-
##
84-
function isanonymous_typedef(src::CodeInfo)
85-
length(src.code) >= 4 || return false
86-
if VERSION >= v"1.5.0-DEV.702"
87-
stmt = src.code[end-1]
88-
(isexpr(stmt, :call) && is_global_ref(stmt.args[1], Core, :_typebody!)) || return false
89-
name = stmt.args[2]::Symbol
90-
return startswith(String(name), "#")
91-
else
92-
stmt = src.code[end-1]
93-
isexpr(stmt, :struct_type) || return false
94-
name = stmt.args[1]::Symbol
95-
return startswith(String(name), "#")
96-
end
97-
end
98-
99-
function define_anonymous(@nospecialize(recurse), frame, @nospecialize(stmt))
81+
function step_through_methoddef(@nospecialize(recurse), frame, @nospecialize(stmt))
10082
while !isexpr(stmt, :method)
10183
pc = step_expr!(recurse, frame, stmt, true)
10284
stmt = pc_expr(frame, pc)

0 commit comments

Comments
 (0)