Skip to content

Commit

Permalink
WIP: Make world-age increments explicit
Browse files Browse the repository at this point in the history
This PR introduces uses the new, toplevel-only, syntax form `:latestworld`
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 #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 #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 in 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 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 `:latestworld`
   exprs, after `:module` exprs, or at the beginning of the top-level
   exprs inside `:toplevel` and `:module`.
2. The frontend inserts `:latestworld` 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 closes 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:
```
begin
    eval(:(f() = 1))
    f()
end
```

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.
  • Loading branch information
Keno committed Nov 20, 2024
1 parent 4ed8814 commit 703fe86
Show file tree
Hide file tree
Showing 17 changed files with 234 additions and 164 deletions.
1 change: 1 addition & 0 deletions Compiler/src/validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}(
:using => 1:typemax(Int),
:export => 1:typemax(Int),
:public => 1:typemax(Int),
:latestworld => 0:0,
)

# @enum isn't defined yet, otherwise I'd use it for this
Expand Down
3 changes: 2 additions & 1 deletion base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,14 @@ else
const UInt = UInt32
end

function iterate end
function Typeof end
ccall(:jl_toplevel_eval_in, Any, (Any, Any),
Core, quote
(f::typeof(Typeof))(x) = ($(_expr(:meta,:nospecialize,:x)); isa(x,Type) ? Type{x} : typeof(x))
end)

function iterate end

macro nospecialize(x)
_expr(:meta, :nospecialize, x)
end
Expand Down
14 changes: 12 additions & 2 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,20 @@ Evaluate an expression with values interpolated into it using `eval`.
If two arguments are provided, the first is the module to evaluate in.
"""
macro eval(ex)
return Expr(:escape, Expr(:call, GlobalRef(Core, :eval), __module__, Expr(:quote, ex)))
g = ccall(:jl_gensym, Ref{Symbol}, ())
return Expr(:let, Expr(:(=), g,
Expr(:escape, Expr(:call, GlobalRef(Core, :eval), __module__, Expr(:quote, ex)))),
Expr(:block,
Expr(:var"latestworld-if-toplevel"),
g))
end
macro eval(mod, ex)
return Expr(:escape, Expr(:call, GlobalRef(Core, :eval), mod, Expr(:quote, ex)))
g = ccall(:jl_gensym, Ref{Symbol}, ())
return Expr(:let, Expr(:(=), g,
Expr(:escape, Expr(:call, GlobalRef(Core, :eval), mod, Expr(:quote, ex)))),
Expr(:block,
Expr(:var"latestworld-if-toplevel"),
g))
end

# use `@eval` here to directly form `:new` expressions avoid implicit `convert`s
Expand Down
7 changes: 7 additions & 0 deletions base/sysimg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ actually evaluates `mapexpr(expr)`. If it is omitted, `mapexpr` defaults to [`i
Use [`Base.include`](@ref) to evaluate a file into another module.
!!! note
Julia's syntax lowering recognizes an explicit call to a literal `include`
at top-level and inserts an implicit `@Core.latestworld` to make any include'd
definitions visible to subsequent code. Note however that this recognition
is *syntactic*. I.e. assigning `const myinclude = include` may require
and explicit `@Core.latestworld` call after `myinclude`.
!!! compat "Julia 1.5"
Julia 1.5 is required for passing the `mapexpr` argument.
"""
Expand Down
2 changes: 1 addition & 1 deletion base/tuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ end

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


Expand Down
5 changes: 0 additions & 5 deletions src/interpreter.c
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,6 @@ static jl_value_t *eval_body(jl_array_t *stmts, interpreter_state *s, size_t ip,
s->ip = ip;
if (ip >= ns)
jl_error("`body` expression must terminate in `return`. Use `block` instead.");
if (toplevel)
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
jl_value_t *stmt = jl_array_ptr_ref(stmts, ip);
assert(!jl_is_phinode(stmt));
size_t next_ip = ip + 1;
Expand Down Expand Up @@ -888,10 +886,7 @@ jl_value_t *NOINLINE jl_interpret_toplevel_thunk(jl_module_t *m, jl_code_info_t
s->mi = NULL;
s->ci = NULL;
JL_GC_ENABLEFRAME(s);
jl_task_t *ct = jl_current_task;
size_t last_age = ct->world_age;
jl_value_t *r = eval_body(stmts, s, 0, 1);
ct->world_age = last_age;
JL_GC_POP();
return r;
}
Expand Down
2 changes: 1 addition & 1 deletion src/jlfrontend.scm
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@

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

Expand Down
33 changes: 25 additions & 8 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,7 @@
'())))))
(call (core _typebody!) ,name (call (core svec) ,@(insert-struct-shim field-types name)))
(const (globalref (thismodule) ,name) ,name)
(latestworld)
(null)))
;; "inner" constructors
(scope-block
Expand Down Expand Up @@ -1087,6 +1088,7 @@
(call (core _equiv_typedef) (globalref (thismodule) ,name) ,name))
(null)
(const (globalref (thismodule) ,name) ,name))
(latestworld)
(null))))))

(define (primitive-type-def-expr n name params super)
Expand All @@ -1107,6 +1109,7 @@
(call (core _equiv_typedef) (globalref (thismodule) ,name) ,name))
(null)
(const (globalref (thismodule) ,name) ,name))
(latestworld)
(null))))))

;; take apart a type signature, e.g. T{X} <: S{Y}
Expand Down Expand Up @@ -1221,7 +1224,7 @@
(cond ((and (length= e 2) (or (symbol? name) (globalref? name)))
(if (not (valid-name? name))
(error (string "invalid function name \"" name "\"")))
`(method ,name))
`(block (method ,name) (latestworld) (unnecessary ,name)))
((not (pair? name)) e)
((eq? (car name) 'call)
(let* ((raw-typevars (or where '()))
Expand Down Expand Up @@ -2744,6 +2747,9 @@
((and (eq? (identifier-name f) '^) (length= e 4) (integer? (cadddr e)))
(expand-forms
`(call (top literal_pow) ,f ,(caddr e) (call (call (core apply_type) (top Val) ,(cadddr e))))))
((eq? f 'include)
(let ((r (make-ssavalue)))
`(block (= ,r ,(map expand-forms e)) (latestworld-if-toplevel) ,r)))
(else
(map expand-forms e))))
(map expand-forms e)))
Expand Down Expand Up @@ -4125,15 +4131,17 @@ f(x) = yt(x)
`(lambda ,(cadr lam2)
(,(clear-capture-bits (car vis))
,@(cdr vis))
,body)))))
,body)))
(latestworld)))
(else
(let* ((exprs (lift-toplevel (convert-lambda lam2 '|#anon| #t '() #f parsed-method-stack)))
(top-stmts (cdr exprs))
(newlam (compact-and-renumber (linearize (car exprs)) 'none 0)))
`(toplevel-butfirst
(block ,@sp-inits
(method ,(cadr e) ,(cl-convert sig fname lam namemap defined toplevel interp opaq parsed-method-stack globals locals)
,(julia-bq-macro newlam)))
,(julia-bq-macro newlam))
(latestworld))
,@top-stmts))))

;; local case - lift to a new type at top level
Expand Down Expand Up @@ -4272,15 +4280,17 @@ f(x) = yt(x)
`(toplevel-butfirst
(null)
,@sp-inits
,@mk-method)
,@mk-method
(latestworld))
(begin
(put! defined name #t)
`(toplevel-butfirst
,(convert-assignment name mk-closure fname lam interp opaq parsed-method-stack globals locals)
,@typedef
,@(map (lambda (v) `(moved-local ,v)) moved-vars)
,@sp-inits
,@mk-method))))))))
,@mk-method
(latestworld)))))))))
((lambda) ;; happens inside (thunk ...) and generated function bodies
(for-each (lambda (vi) (vinfo:set-asgn! vi #t))
(list-tail (car (lam:vinfo e)) (length (lam:args e))))
Expand Down Expand Up @@ -4513,7 +4523,7 @@ f(x) = yt(x)
((struct_type) "\"struct\" expression")
((method) "method definition")
((set_binding_type!) (string "type declaration for global \"" (deparse (cadr e)) "\""))
((latestworld) "World age increment")
((latestworld) "World age increment")
(else (string "\"" h "\" expression"))))
(if (not (null? (cadr lam)))
(error (string (head-to-text (car e)) " not at top level"))))
Expand Down Expand Up @@ -4965,7 +4975,12 @@ f(x) = yt(x)
(else (emit temp)))))

;; top level expressions
((thunk module)
((thunk)
(check-top-level e)
(emit e)
(if tail (emit-return tail '(null)))
'(null))
((module)
(check-top-level e)
(emit e)
(if tail (emit-return tail '(null)))
Expand All @@ -4989,7 +5004,9 @@ f(x) = yt(x)
;; other top level expressions
((import using export public latestworld)
(check-top-level e)
(emit e)
(if (not (eq? (car e) 'latestworld))
(emit e))
(emit `(latestworld))
(let ((have-ret? (and (pair? code) (pair? (car code)) (eq? (caar code) 'return))))
(if (and tail (not have-ret?))
(emit-return tail '(null))))
Expand Down
21 changes: 18 additions & 3 deletions src/toplevel.c
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,8 @@ static jl_value_t *jl_eval_module_expr(jl_module_t *parent_module, jl_expr_t *ex

for (int i = 0; i < jl_array_nrows(exprs); i++) {
// process toplevel form
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
ct->world_age = jl_atomic_load_relaxed(&jl_world_counter);
form = jl_expand_stmt_with_loc(jl_array_ptr_ref(exprs, i), newm, filename, lineno);
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
(void)jl_toplevel_eval_flex(newm, form, 1, 1, &filename, &lineno);
}
ct->world_age = last_age;
Expand Down Expand Up @@ -865,6 +864,7 @@ JL_DLLEXPORT jl_value_t *jl_toplevel_eval_flex(jl_module_t *JL_NONNULL m, jl_val

if (head == jl_module_sym) {
jl_value_t *val = jl_eval_module_expr(m, ex);
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
JL_GC_POP();
return val;
}
Expand Down Expand Up @@ -920,6 +920,9 @@ JL_DLLEXPORT jl_value_t *jl_toplevel_eval_flex(jl_module_t *JL_NONNULL m, jl_val
jl_eval_errorf(m, *toplevel_filename, *toplevel_lineno,
"syntax: malformed \"using\" statement");
}
if (!expanded) {
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
}
JL_GC_POP();
return jl_nothing;
}
Expand Down Expand Up @@ -968,6 +971,12 @@ JL_DLLEXPORT jl_value_t *jl_toplevel_eval_flex(jl_module_t *JL_NONNULL m, jl_val
jl_eval_errorf(m, *toplevel_filename, *toplevel_lineno,
"syntax: malformed \"import\" statement");
}
if (!expanded) {
// To avoid having to roundtrip every `using` expression through
// lowering, just to add the world-age increment effect, do it
// manually here.
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
}
JL_GC_POP();
return jl_nothing;
}
Expand Down Expand Up @@ -1002,6 +1011,7 @@ JL_DLLEXPORT jl_value_t *jl_toplevel_eval_flex(jl_module_t *JL_NONNULL m, jl_val
jl_value_t *res = jl_nothing;
int i;
for (i = 0; i < jl_array_nrows(ex->args); i++) {
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
res = jl_toplevel_eval_flex(m, jl_array_ptr_ref(ex->args, i), fast, 0, toplevel_filename, toplevel_lineno);
}
JL_GC_POP();
Expand Down Expand Up @@ -1112,8 +1122,11 @@ JL_DLLEXPORT jl_value_t *jl_toplevel_eval_in(jl_module_t *m, jl_value_t *ex)
jl_value_t *v = NULL;
int last_lineno = jl_lineno;
const char *last_filename = jl_filename;
jl_task_t *ct = jl_current_task;
jl_lineno = 1;
jl_filename = "none";
size_t last_age = ct->world_age;
ct->world_age = jl_atomic_load_relaxed(&jl_world_counter);
JL_TRY {
v = jl_toplevel_eval(m, ex);
}
Expand All @@ -1124,6 +1137,7 @@ JL_DLLEXPORT jl_value_t *jl_toplevel_eval_in(jl_module_t *m, jl_value_t *ex)
}
jl_lineno = last_lineno;
jl_filename = last_filename;
ct->world_age = last_age;
assert(v);
return v;
}
Expand Down Expand Up @@ -1171,6 +1185,7 @@ static jl_value_t *jl_parse_eval_all(jl_module_t *module, jl_value_t *text,
int last_lineno = jl_lineno;
const char *last_filename = jl_filename;
size_t last_age = ct->world_age;
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
int lineno = 0;
jl_lineno = 0;
const char *filename_str = jl_string_data(filename);
Expand All @@ -1186,9 +1201,9 @@ static jl_value_t *jl_parse_eval_all(jl_module_t *module, jl_value_t *text,
jl_lineno = lineno;
continue;
}
ct->world_age = jl_atomic_load_relaxed(&jl_world_counter);
expression = jl_expand_with_loc_warn(expression, module,
jl_string_data(filename), lineno);
ct->world_age = jl_atomic_load_acquire(&jl_world_counter);
result = jl_toplevel_eval_flex(module, expression, 1, 1, &filename_str, &lineno);
}
}
Expand Down
2 changes: 1 addition & 1 deletion stdlib/Logging/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ end
AboveMaxLevel === Logging.AboveMaxLevel
end
""")
@test m.run()
@test invokelatest(m.run)
end

@testset "custom log macro" begin
Expand Down
Loading

0 comments on commit 703fe86

Please sign in to comment.