Skip to content

Commit 034e609

Browse files
authored
Make world-age increments explicit (JuliaLang#56509)
This PR introduces a new, toplevel-only, syntax form `:worldinc` that semantically represents the effect of raising the current task's world age to the latest world for the remainder of the current toplevel evaluation (that context being an entry to `eval` or a module expression). For detailed motivation on why this is desirable, see JuliaLang#55145, which I won't repeat here, but the gist is that we never really defined when world-age increments and worse are inconsistent about it. This is something we need to figure out now, because the bindings partition work will make world age even more observable via bindings. Having created a mechanism for world age increments, the big question is one of policy, i.e. when should these world age increments be inserted. Several reasonable options exist: 1. After world-age affecting syntax constructs (as proprosed in JuliaLang#55145) 2. Option 1 + some reasonable additional cases that people rely on 3. Before any top level `call` expression 4. Before any expression at toplevel whatsover As an example, case, consider `a == a` at toplevel. Depending on the semantics that could either be the same as in local scope, or each of the four world age dependent lookups (three binding lookups, one method lookup) could (potentially) occur in a different world age. The general tradeoff here is between the risk of exposing the user to confusing world age errors and our ability to optimize top-level code (in general, any `:worldinc` statement will require us to fully pessimize or recompile all following code). This PR basically implements option 2 with the following semantics: 1. The interpreter explicit raises the world age only at `:worldinc` exprs or after `:module` exprs. 2. The frontend inserts `:worldinc` after all struct definitions, method definitions, `using` and `import. 3. The `@eval` macro inserts a worldinc following the call to `eval` if at toplevel 4. A literal (syntactic) call to `include` gains an implicit `worldinc`. Of these the fourth is probably the most questionable, but is necessary to make this non-breaking for most code patterns. Perhaps it would have been better to make `include` a macro from the beginning (esp because it already has semantics that look a little like reaching into the calling module), but that ship has sailed. Unfortunately, I don't see any good intermediate options between this PR and option #3 above. I think option #3 is closest to what we have right now, but if we were to choose it and actually fix the soundness issues, I expect that we would be destroying all performance of global-scope code. For this reason, I would like to try to make the version in this PR work, even if the semantics are a little ugly. The biggest pattern that this PR does not catch is: ``` eval(:(f() = 1)) f() ``` We could apply the same `include` special case to eval, but given the existence of `@eval` which allows addressing this at the macro level, I decided not to. We can decide which way we want to go on this based on what the package ecosystem looks like.
1 parent 6c5f221 commit 034e609

29 files changed

+508
-356
lines changed

Compiler/src/abstractinterpretation.jl

+121-105
Large diffs are not rendered by default.

Compiler/src/inferencestate.jl

+8-1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ to enable flow-sensitive analysis.
209209
"""
210210
const VarTable = Vector{VarState}
211211

212+
struct StatementState
213+
vtypes::Union{VarTable,Nothing}
214+
saw_latestworld::Bool
215+
end
216+
212217
const CACHE_MODE_NULL = 0x00 # not cached, optimization optional
213218
const CACHE_MODE_GLOBAL = 0x01 << 0 # cached globally, optimization required
214219
const CACHE_MODE_LOCAL = 0x01 << 1 # cached locally, optimization required
@@ -260,6 +265,7 @@ mutable struct InferenceState
260265
ssavalue_uses::Vector{BitSet} # ssavalue sparsity and restart info
261266
# TODO: Could keep this sparsely by doing structural liveness analysis ahead of time.
262267
bb_vartables::Vector{Union{Nothing,VarTable}} # nothing if not analyzed yet
268+
bb_saw_latestworld::Vector{Bool}
263269
ssavaluetypes::Vector{Any}
264270
edges::Vector{Any}
265271
stmt_info::Vector{CallInfo}
@@ -320,6 +326,7 @@ mutable struct InferenceState
320326

321327
nslots = length(src.slotflags)
322328
slottypes = Vector{Any}(undef, nslots)
329+
bb_saw_latestworld = Bool[false for i = 1:length(cfg.blocks)]
323330
bb_vartables = Union{Nothing,VarTable}[ nothing for i = 1:length(cfg.blocks) ]
324331
bb_vartable1 = bb_vartables[1] = VarTable(undef, nslots)
325332
argtypes = result.argtypes
@@ -367,7 +374,7 @@ mutable struct InferenceState
367374

368375
this = new(
369376
mi, WorldWithRange(world, valid_worlds), mod, sptypes, slottypes, src, cfg, spec_info,
370-
currbb, currpc, ip, handler_info, ssavalue_uses, bb_vartables, ssavaluetypes, edges, stmt_info,
377+
currbb, currpc, ip, handler_info, ssavalue_uses, bb_vartables, bb_saw_latestworld, ssavaluetypes, edges, stmt_info,
371378
tasks, pclimitations, limitations, cycle_backedges, callstack, parentid, frameid, cycleid,
372379
result, unreachable, bestguess, exc_bestguess, ipo_effects,
373380
restrict_abstract_call_sites, cache_mode, insert_coverage,

Compiler/src/ssair/irinterp.jl

+6-6
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,19 @@ function abstract_eval_invoke_inst(interp::AbstractInterpreter, inst::Instructio
3838
mi_cache = WorldView(code_cache(interp), world)
3939
code = get(mi_cache, mi, nothing)
4040
code === nothing && return Pair{Any,Tuple{Bool,Bool}}(nothing, (false, false))
41-
argtypes = collect_argtypes(interp, stmt.args[2:end], nothing, irsv)
41+
argtypes = collect_argtypes(interp, stmt.args[2:end], StatementState(nothing, false), irsv)
4242
argtypes === nothing && return Pair{Any,Tuple{Bool,Bool}}(Bottom, (false, false))
4343
return concrete_eval_invoke(interp, code, argtypes, irsv)
4444
end
4545

4646
abstract_eval_ssavalue(s::SSAValue, sv::IRInterpretationState) = abstract_eval_ssavalue(s, sv.ir)
4747

4848
function abstract_eval_phi_stmt(interp::AbstractInterpreter, phi::PhiNode, ::Int, irsv::IRInterpretationState)
49-
return abstract_eval_phi(interp, phi, nothing, irsv)
49+
return abstract_eval_phi(interp, phi, StatementState(nothing, false), irsv)
5050
end
5151

52-
function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, irsv::IRInterpretationState)
53-
si = StmtInfo(true) # TODO better job here?
52+
function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, sstate::StatementState, irsv::IRInterpretationState)
53+
si = StmtInfo(true, sstate.saw_latestworld) # TODO better job here?
5454
call = abstract_call(interp, arginfo, si, irsv)::Future
5555
Future{Any}(call, interp, irsv) do call, interp, irsv
5656
irsv.ir.stmts[irsv.curridx][:info] = call.info
@@ -147,7 +147,7 @@ function reprocess_instruction!(interp::AbstractInterpreter, inst::Instruction,
147147
if (head === :call || head === :foreigncall || head === :new || head === :splatnew ||
148148
head === :static_parameter || head === :isdefined || head === :boundscheck)
149149
@assert isempty(irsv.tasks) # TODO: this whole function needs to be converted to a stackless design to be a valid AbsIntState, but this should work here for now
150-
result = abstract_eval_statement_expr(interp, stmt, nothing, irsv)
150+
result = abstract_eval_statement_expr(interp, stmt, StatementState(nothing, false), irsv)
151151
reverse!(irsv.tasks)
152152
while true
153153
if length(irsv.callstack) > irsv.frameid
@@ -302,7 +302,7 @@ populate_def_use_map!(tpdum::TwoPhaseDefUseMap, ir::IRCode) =
302302
function is_all_const_call(@nospecialize(stmt), interp::AbstractInterpreter, irsv::IRInterpretationState)
303303
isexpr(stmt, :call) || return false
304304
@inbounds for i = 2:length(stmt.args)
305-
argtype = abstract_eval_value(interp, stmt.args[i], nothing, irsv)
305+
argtype = abstract_eval_value(interp, stmt.args[i], StatementState(nothing, false), irsv)
306306
is_const_argtype(argtype) || return false
307307
end
308308
return true

Compiler/src/tfuncs.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1426,7 +1426,7 @@ end
14261426
# as well as compute the info for the method matches
14271427
op = unwrapva(argtypes[op_argi])
14281428
v = unwrapva(argtypes[v_argi])
1429-
callinfo = abstract_call(interp, ArgInfo(nothing, Any[op, TF, v]), StmtInfo(true), sv, #=max_methods=#1)
1429+
callinfo = abstract_call(interp, ArgInfo(nothing, Any[op, TF, v]), StmtInfo(true, si.saw_latestworld), sv, #=max_methods=#1)
14301430
TF = Core.Box(TF)
14311431
RT = Core.Box(RT)
14321432
return Future{CallMeta}(callinfo, interp, sv) do callinfo, interp, sv

Compiler/src/types.jl

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ struct StmtInfo
4141
need thus not be computed.
4242
"""
4343
used::Bool
44+
saw_latestworld::Bool
4445
end
4546

4647
struct SpecInfo

Compiler/src/validation.jl

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}(
3939
:using => 1:typemax(Int),
4040
:export => 1:typemax(Int),
4141
:public => 1:typemax(Int),
42+
:latestworld => 0:0,
4243
)
4344

4445
# @enum isn't defined yet, otherwise I'd use it for this

base/boot.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -259,13 +259,14 @@ else
259259
const UInt = UInt32
260260
end
261261

262-
function iterate end
263262
function Typeof end
264263
ccall(:jl_toplevel_eval_in, Any, (Any, Any),
265264
Core, quote
266265
(f::typeof(Typeof))(x) = ($(_expr(:meta,:nospecialize,:x)); isa(x,Type) ? Type{x} : typeof(x))
267266
end)
268267

268+
function iterate end
269+
269270
macro nospecialize(x)
270271
_expr(:meta, :nospecialize, x)
271272
end

base/essentials.jl

+10-2
Original file line numberDiff line numberDiff line change
@@ -467,10 +467,18 @@ Evaluate an expression with values interpolated into it using `eval`.
467467
If two arguments are provided, the first is the module to evaluate in.
468468
"""
469469
macro eval(ex)
470-
return Expr(:escape, Expr(:call, GlobalRef(Core, :eval), __module__, Expr(:quote, ex)))
470+
return Expr(:let, Expr(:(=), :eval_local_result,
471+
Expr(:escape, Expr(:call, GlobalRef(Core, :eval), __module__, Expr(:quote, ex)))),
472+
Expr(:block,
473+
Expr(:var"latestworld-if-toplevel"),
474+
:eval_local_result))
471475
end
472476
macro eval(mod, ex)
473-
return Expr(:escape, Expr(:call, GlobalRef(Core, :eval), mod, Expr(:quote, ex)))
477+
return Expr(:let, Expr(:(=), :eval_local_result,
478+
Expr(:escape, Expr(:call, GlobalRef(Core, :eval), mod, Expr(:quote, ex)))),
479+
Expr(:block,
480+
Expr(:var"latestworld-if-toplevel"),
481+
:eval_local_result))
474482
end
475483

476484
# use `@eval` here to directly form `:new` expressions avoid implicit `convert`s

base/sysimg.jl

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ actually evaluates `mapexpr(expr)`. If it is omitted, `mapexpr` defaults to [`i
3939
4040
Use [`Base.include`](@ref) to evaluate a file into another module.
4141
42+
!!! note
43+
Julia's syntax lowering recognizes an explicit call to a literal `include`
44+
at top-level and inserts an implicit `@Core.latestworld` to make any include'd
45+
definitions visible to subsequent code. Note however that this recognition
46+
is *syntactic*. I.e. assigning `const myinclude = include` may require
47+
and explicit `@Core.latestworld` call after `myinclude`.
48+
4249
!!! compat "Julia 1.5"
4350
Julia 1.5 is required for passing the `mapexpr` argument.
4451
"""

base/tuple.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ end
6060

6161
function _setindex(v, i::Integer, args::Vararg{Any,N}) where {N}
6262
@inline
63-
return ntuple(j -> ifelse(j == i, v, args[j]), Val{N}())
63+
return ntuple(j -> ifelse(j == i, v, args[j]), Val{N}())::NTuple{N, Any}
6464
end
6565

6666

src/codegen.cpp

+19-12
Original file line numberDiff line numberDiff line change
@@ -6721,6 +6721,18 @@ static std::pair<Function*, Function*> get_oc_function(jl_codectx_t &ctx, jl_met
67216721
return std::make_pair(F, specF);
67226722
}
67236723

6724+
static void emit_latestworld(jl_codectx_t &ctx)
6725+
{
6726+
auto world_age_field = get_tls_world_age_field(ctx);
6727+
LoadInst *world = ctx.builder.CreateAlignedLoad(ctx.types().T_size,
6728+
prepare_global_in(jl_Module, jlgetworld_global), ctx.types().alignof_ptr,
6729+
/*isVolatile*/false);
6730+
world->setOrdering(AtomicOrdering::Acquire);
6731+
StoreInst *store_world = ctx.builder.CreateAlignedStore(world, world_age_field,
6732+
ctx.types().alignof_ptr, /*isVolatile*/false);
6733+
(void)store_world;
6734+
}
6735+
67246736
// `expr` is not clobbered in JL_TRY
67256737
JL_GCC_IGNORE_START("-Wclobbered")
67266738
static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr, ssize_t ssaidx_0based)
@@ -7141,6 +7153,10 @@ static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr, ssize_t ssaidx_
71417153
ctx.builder.CreateCall(prepare_call(gc_preserve_end_func), {token.V});
71427154
return jl_cgval_t((jl_value_t*)jl_nothing_type);
71437155
}
7156+
else if (head == jl_latestworld_sym && !jl_is_method(ctx.linfo->def.method)) {
7157+
emit_latestworld(ctx);
7158+
return jl_cgval_t((jl_value_t*)jl_nothing_type);
7159+
}
71447160
else {
71457161
if (jl_is_toplevel_only_expr(expr) &&
71467162
!jl_is_method(ctx.linfo->def.method)) {
@@ -9568,7 +9584,9 @@ static jl_llvm_functions_t
95689584
}
95699585

95709586
mallocVisitStmt(sync_bytes, have_dbg_update);
9571-
if (toplevel || ctx.is_opaque_closure)
9587+
// N.B.: For toplevel thunks, we expect world age restore to be handled
9588+
// by the interpreter which invokes us.
9589+
if (ctx.is_opaque_closure)
95729590
ctx.builder.CreateStore(last_age, world_age_field);
95739591
assert(type_is_ghost(retty) || returninfo.cc == jl_returninfo_t::SRet ||
95749592
retval->getType() == ctx.f->getReturnType());
@@ -9933,17 +9951,6 @@ static jl_llvm_functions_t
99339951
I.setDebugLoc(topdebugloc);
99349952
}
99359953
}
9936-
if (toplevel && !ctx.is_opaque_closure && !in_prologue) {
9937-
// we're at toplevel; insert an atomic barrier between every instruction
9938-
// TODO: inference is invalid if this has any effect (which it often does)
9939-
LoadInst *world = new LoadInst(ctx.types().T_size,
9940-
prepare_global_in(jl_Module, jlgetworld_global), Twine(),
9941-
/*isVolatile*/false, ctx.types().alignof_ptr, /*insertBefore*/&I);
9942-
world->setOrdering(AtomicOrdering::Acquire);
9943-
StoreInst *store_world = new StoreInst(world, world_age_field,
9944-
/*isVolatile*/false, ctx.types().alignof_ptr, /*insertBefore*/&I);
9945-
(void)store_world;
9946-
}
99479954
}
99489955
if (&I == &prologue_end)
99499956
in_prologue = false;

src/interpreter.c

-2
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,6 @@ static jl_value_t *eval_body(jl_array_t *stmts, interpreter_state *s, size_t ip,
463463
s->ip = ip;
464464
if (ip >= ns)
465465
jl_error("`body` expression must terminate in `return`. Use `block` instead.");
466-
if (toplevel)
467-
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
468466
jl_value_t *stmt = jl_array_ptr_ref(stmts, ip);
469467
assert(!jl_is_phinode(stmt));
470468
size_t next_ip = ip + 1;

src/jlfrontend.scm

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139

140140
(define (toplevel-only-expr? e)
141141
(and (pair? e)
142-
(or (memq (car e) '(toplevel line module import using export public
142+
(or (memq (car e) '(toplevel line module export public
143143
error incomplete))
144144
(and (memq (car e) '(global const)) (every symbol? (cdr e))))))
145145

src/julia-syntax.scm

+24-7
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,7 @@
10381038
'())))))
10391039
(call (core _typebody!) ,name (call (core svec) ,@(insert-struct-shim field-types name)))
10401040
(const (globalref (thismodule) ,name) ,name)
1041+
(latestworld)
10411042
(null)))
10421043
;; "inner" constructors
10431044
(scope-block
@@ -1087,6 +1088,7 @@
10871088
(call (core _equiv_typedef) (globalref (thismodule) ,name) ,name))
10881089
(null)
10891090
(const (globalref (thismodule) ,name) ,name))
1091+
(latestworld)
10901092
(null))))))
10911093

10921094
(define (primitive-type-def-expr n name params super)
@@ -1107,6 +1109,7 @@
11071109
(call (core _equiv_typedef) (globalref (thismodule) ,name) ,name))
11081110
(null)
11091111
(const (globalref (thismodule) ,name) ,name))
1112+
(latestworld)
11101113
(null))))))
11111114

11121115
;; take apart a type signature, e.g. T{X} <: S{Y}
@@ -2744,6 +2747,9 @@
27442747
((and (eq? (identifier-name f) '^) (length= e 4) (integer? (cadddr e)))
27452748
(expand-forms
27462749
`(call (top literal_pow) ,f ,(caddr e) (call (call (core apply_type) (top Val) ,(cadddr e))))))
2750+
((eq? f 'include)
2751+
(let ((r (make-ssavalue)))
2752+
`(block (= ,r ,(map expand-forms e)) (latestworld-if-toplevel) ,r)))
27472753
(else
27482754
(map expand-forms e))))
27492755
(map expand-forms e)))
@@ -4125,15 +4131,17 @@ f(x) = yt(x)
41254131
`(lambda ,(cadr lam2)
41264132
(,(clear-capture-bits (car vis))
41274133
,@(cdr vis))
4128-
,body)))))
4134+
,body)))
4135+
(latestworld)))
41294136
(else
41304137
(let* ((exprs (lift-toplevel (convert-lambda lam2 '|#anon| #t '() #f parsed-method-stack)))
41314138
(top-stmts (cdr exprs))
41324139
(newlam (compact-and-renumber (linearize (car exprs)) 'none 0)))
41334140
`(toplevel-butfirst
41344141
(block ,@sp-inits
41354142
(method ,(cadr e) ,(cl-convert sig fname lam namemap defined toplevel interp opaq parsed-method-stack globals locals)
4136-
,(julia-bq-macro newlam)))
4143+
,(julia-bq-macro newlam))
4144+
(latestworld))
41374145
,@top-stmts))))
41384146

41394147
;; local case - lift to a new type at top level
@@ -4272,15 +4280,17 @@ f(x) = yt(x)
42724280
`(toplevel-butfirst
42734281
(null)
42744282
,@sp-inits
4275-
,@mk-method)
4283+
,@mk-method
4284+
(latestworld))
42764285
(begin
42774286
(put! defined name #t)
42784287
`(toplevel-butfirst
42794288
,(convert-assignment name mk-closure fname lam interp opaq parsed-method-stack globals locals)
42804289
,@typedef
42814290
,@(map (lambda (v) `(moved-local ,v)) moved-vars)
42824291
,@sp-inits
4283-
,@mk-method))))))))
4292+
,@mk-method
4293+
(latestworld)))))))))
42844294
((lambda) ;; happens inside (thunk ...) and generated function bodies
42854295
(for-each (lambda (vi) (vinfo:set-asgn! vi #t))
42864296
(list-tail (car (lam:vinfo e)) (length (lam:args e))))
@@ -4513,7 +4523,7 @@ f(x) = yt(x)
45134523
((struct_type) "\"struct\" expression")
45144524
((method) "method definition")
45154525
((set_binding_type!) (string "type declaration for global \"" (deparse (cadr e)) "\""))
4516-
((latestworld) "World age increment")
4526+
((latestworld) "World age increment")
45174527
(else (string "\"" h "\" expression"))))
45184528
(if (not (null? (cadr lam)))
45194529
(error (string (head-to-text (car e)) " not at top level"))))
@@ -4965,7 +4975,12 @@ f(x) = yt(x)
49654975
(else (emit temp)))))
49664976

49674977
;; top level expressions
4968-
((thunk module)
4978+
((thunk)
4979+
(check-top-level e)
4980+
(emit e)
4981+
(if tail (emit-return tail '(null)))
4982+
'(null))
4983+
((module)
49694984
(check-top-level e)
49704985
(emit e)
49714986
(if tail (emit-return tail '(null)))
@@ -4989,7 +5004,9 @@ f(x) = yt(x)
49895004
;; other top level expressions
49905005
((import using export public latestworld)
49915006
(check-top-level e)
4992-
(emit e)
5007+
(if (not (eq? (car e) 'latestworld))
5008+
(emit e))
5009+
(emit `(latestworld))
49935010
(let ((have-ret? (and (pair? code) (pair? (car code)) (eq? (caar code) 'return))))
49945011
(if (and tail (not have-ret?))
49955012
(emit-return tail '(null))))

0 commit comments

Comments
 (0)