Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Outline potentially undefined globals during lowering #51970

Merged
merged 13 commits into from
Nov 11, 2023
16 changes: 10 additions & 6 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function from_interprocedural!(interp::AbstractInterpreter, @nospecialize(rt), s
arginfo::ArgInfo, @nospecialize(maybecondinfo))
rt = collect_limitations!(rt, sv)
if isa(rt, InterMustAlias)
rt = from_intermustalias(rt, arginfo)
rt = from_intermustalias(rt, arginfo, sv)
elseif is_lattice_bool(ipo_lattice(interp), rt)
if maybecondinfo === nothing
rt = widenconditional(rt)
Expand All @@ -340,10 +340,10 @@ function collect_limitations!(@nospecialize(typ), sv::InferenceState)
return typ
end

function from_intermustalias(rt::InterMustAlias, arginfo::ArgInfo)
function from_intermustalias(rt::InterMustAlias, arginfo::ArgInfo, sv::AbsIntState)
fargs = arginfo.fargs
if fargs !== nothing && 1 ≤ rt.slot ≤ length(fargs)
arg = fargs[rt.slot]
arg = ssa_def_slot(fargs[rt.slot], sv)
if isa(arg, SlotNumber)
argtyp = widenslotwrapper(arginfo.argtypes[rt.slot])
if rt.vartyp ⊑ argtyp
Expand Down Expand Up @@ -1334,6 +1334,9 @@ function ssa_def_slot(@nospecialize(arg), sv::InferenceState)
return arg
end

# No slots in irinterp
ssa_def_slot(@nospecialize(arg), sv::IRInterpretationState) = nothing

struct AbstractIterationResult
cti::Vector{Any}
info::MaybeAbstractIterationInfo
Expand Down Expand Up @@ -1743,7 +1746,7 @@ function abstract_call_builtin(interp::AbstractInterpreter, f::Builtin, (; fargs
a3 = argtypes[3]
if isa(a3, Const)
if rt !== Bottom && !isalreadyconst(rt)
var = fargs[2]
var = ssa_def_slot(fargs[2], sv)
if isa(var, SlotNumber)
vartyp = widenslotwrapper(argtypes[2])
fldidx = maybe_const_fldidx(vartyp, a3.val)
Expand Down Expand Up @@ -3047,17 +3050,18 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState)
@goto branch
elseif isa(stmt, GotoIfNot)
condx = stmt.cond
condxslot = ssa_def_slot(condx, frame)
condt = abstract_eval_value(interp, condx, currstate, frame)
if condt === Bottom
ssavaluetypes[currpc] = Bottom
empty!(frame.pclimitations)
@goto find_next_bb
end
orig_condt = condt
if !(isa(condt, Const) || isa(condt, Conditional)) && isa(condx, SlotNumber)
if !(isa(condt, Const) || isa(condt, Conditional)) && isa(condxslot, SlotNumber)
# if this non-`Conditional` object is a slot, we form and propagate
# the conditional constraint on it
condt = Conditional(condx, Const(true), Const(false))
condt = Conditional(condxslot, Const(true), Const(false))
end
condval = maybe_extract_const_bool(condt)
nothrow = (condval !== nothing) || ⊑(𝕃ᵢ, orig_condt, Bool)
Expand Down
7 changes: 5 additions & 2 deletions base/compiler/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,12 @@ function find_ssavalue_uses(e::PhiNode, uses::Vector{BitSet}, line::Int)
end
end

function is_throw_call(e::Expr)
function is_throw_call(e::Expr, code::Vector{Any})
if e.head === :call
f = e.args[1]
if isa(f, SSAValue)
f = code[f.id]
end
if isa(f, GlobalRef)
ff = abstract_eval_globalref_type(f)
if isa(ff, Const) && ff.val === Core.throw
Expand Down Expand Up @@ -478,7 +481,7 @@ function find_throw_blocks(code::Vector{Any}, handler_at::Vector{Int})
end
elseif s.head === :return
# see `ReturnNode` handling
elseif is_throw_call(s)
elseif is_throw_call(s, code)
if handler_at[i] == 0
push!(stmts, i)
end
Expand Down
2 changes: 2 additions & 0 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,8 @@ function bodyfunction(basemethod::Method)
else
return nothing
end
elseif isa(fsym, Core.SSAValue)
fsym = ast.code[fsym.id]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to add the ability to annotate uses as nothrow, for auto-generated globals we know at lowering time will be defined before they're used.

else
return nothing
end
Expand Down
2 changes: 1 addition & 1 deletion doc/src/base/reflection.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Finally, the [`Meta.lower`](@ref) function gives the `lowered` form of any expre
particular interest for understanding how language constructs map to primitive operations such
as assignments, branches, and calls:

```jldoctest
```jldoctest; setup = (using Base: +, sin)
julia> Meta.lower(@__MODULE__, :( [1+2, sin(0.5)] ))
:($(Expr(:thunk, CodeInfo(
@ none within `top-level scope`
Expand Down
61 changes: 46 additions & 15 deletions src/ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,61 @@ static jl_value_t *scm_to_julia(fl_context_t *fl_ctx, value_t e, jl_module_t *mo
static value_t julia_to_scm(fl_context_t *fl_ctx, jl_value_t *v);
static jl_value_t *jl_expand_macros(jl_value_t *expr, jl_module_t *inmodule, struct macroctx_stack *macroctx, int onelevel, size_t world, int throw_load_error);

static jl_sym_t *scmsym_to_julia(fl_context_t *fl_ctx, value_t s)
{
assert(issymbol(s));
if (fl_isgensym(fl_ctx, s)) {
char gsname[16];
char *n = uint2str(&gsname[1], sizeof(gsname)-1,
((gensym_t*)ptr(s))->id, 10);
*(--n) = '#';
return jl_symbol(n);
}
return jl_symbol(symbol_name(fl_ctx, s));
}

static value_t fl_defined_julia_global(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
// tells whether a var is defined in and *by* the current module
argcount(fl_ctx, "defined-julia-global", nargs, 1);
(void)tosymbol(fl_ctx, args[0], "defined-julia-global");
jl_ast_context_t *ctx = jl_ast_ctx(fl_ctx);
jl_sym_t *var = jl_symbol(symbol_name(fl_ctx, args[0]));
jl_sym_t *var = scmsym_to_julia(fl_ctx, args[0]);
jl_binding_t *b = jl_get_module_binding(ctx->module, var, 0);
return (b != NULL && jl_atomic_load_relaxed(&b->owner) == b) ? fl_ctx->T : fl_ctx->F;
}

static value_t fl_nothrow_julia_global(fl_context_t *fl_ctx, value_t *args, uint32_t nargs)
{
// tells whether a var is defined, in the sense that accessing it is nothrow
// can take either a symbol or a module and a symbol
jl_ast_context_t *ctx = jl_ast_ctx(fl_ctx);
jl_module_t *mod = ctx->module;
jl_sym_t *var = NULL;
if (nargs == 1) {
(void)tosymbol(fl_ctx, args[0], "nothrow-julia-global");
var = scmsym_to_julia(fl_ctx, args[0]);
}
else {
argcount(fl_ctx, "nothrow-julia-global", nargs, 2);
value_t argmod = args[0];
if (iscvalue(argmod) && cv_class((cvalue_t*)ptr(argmod)) == jl_ast_ctx(fl_ctx)->jvtype) {
mod = *(jl_module_t**)cv_data((cvalue_t*)ptr(argmod));
JL_GC_PROMISE_ROOTED(mod);
} else {
(void)tosymbol(fl_ctx, argmod, "nothrow-julia-global");
if (scmsym_to_julia(fl_ctx, argmod) != jl_thismodule_sym) {
lerrorf(fl_ctx, fl_ctx->ArgError, "nothrow-julia-global: Unknown globalref module kind");
}
}
(void)tosymbol(fl_ctx, args[1], "nothrow-julia-global");
var = scmsym_to_julia(fl_ctx, args[1]);
}
jl_binding_t *b = jl_get_module_binding(mod, var, 0);
b = b ? jl_atomic_load_relaxed(&b->owner) : NULL;
return b != NULL && jl_atomic_load_relaxed(&b->value) != NULL ? fl_ctx->T : fl_ctx->F;
}

static value_t fl_current_module_counter(fl_context_t *fl_ctx, value_t *args, uint32_t nargs) JL_NOTSAFEPOINT
{
jl_ast_context_t *ctx = jl_ast_ctx(fl_ctx);
Expand Down Expand Up @@ -210,6 +254,7 @@ static jl_value_t *scm_to_julia_(fl_context_t *fl_ctx, value_t e, jl_module_t *m

static const builtinspec_t julia_flisp_ast_ext[] = {
{ "defined-julia-global", fl_defined_julia_global }, // TODO: can we kill this safepoint
{ "nothrow-julia-global", fl_nothrow_julia_global },
{ "current-julia-module-counter", fl_current_module_counter },
{ "julia-scalar?", fl_julia_scalar },
{ "julia-current-file", fl_julia_current_file },
Expand Down Expand Up @@ -421,20 +466,6 @@ JL_DLLEXPORT void fl_profile(const char *fname)
jl_ast_ctx_leave(ctx);
}


static jl_sym_t *scmsym_to_julia(fl_context_t *fl_ctx, value_t s)
{
assert(issymbol(s));
if (fl_isgensym(fl_ctx, s)) {
char gsname[16];
char *n = uint2str(&gsname[1], sizeof(gsname)-1,
((gensym_t*)ptr(s))->id, 10);
*(--n) = '#';
return jl_symbol(n);
}
return jl_symbol(symbol_name(fl_ctx, s));
}

static jl_value_t *scm_to_julia(fl_context_t *fl_ctx, value_t e, jl_module_t *mod)
{
jl_value_t *v = NULL;
Expand Down
1 change: 1 addition & 0 deletions src/jlfrontend.scm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

;; this is overwritten when we run in actual julia
(define (defined-julia-global v) #f)
(define (nothrow-julia-global v) #f)
(define (julia-current-file) 'none)
(define (julia-current-line) 0)

Expand Down
87 changes: 51 additions & 36 deletions src/julia-syntax.scm
Original file line number Diff line number Diff line change
Expand Up @@ -4254,17 +4254,21 @@ f(x) = yt(x)
(else (for-each linearize (cdr e))))
e)

;; N.B.: This assumes that resolve-scopes has run, so outerref is equivalent to
;; a global in the current scope.
(define (valid-ir-argument? e)
(or (simple-atom? e) (symbol? e)
(or (simple-atom? e)
(and (outerref? e) (nothrow-julia-global (cadr e)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothrow-julia-global should be able to accept a globalref as well.

(and (globalref? e) (nothrow-julia-global (cadr e) (caddr e)))
(and (pair? e)
(memq (car e) '(quote inert top core globalref outerref
(memq (car e) '(quote inert top core
slot static_parameter)))))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove static_parameter from here since it can also throw?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I suppose, but for some reason I always see static parameters getting moved out anyway.


(define (valid-ir-rvalue? lhs e)
(or (ssavalue? lhs)
(valid-ir-argument? e)
(and (symbol? lhs) (pair? e)
(memq (car e) '(new splatnew the_exception isdefined call invoke foreigncall cfunction gc_preserve_begin copyast new_opaque_closure)))))
(memq (car e) '(new splatnew the_exception isdefined call invoke foreigncall cfunction gc_preserve_begin copyast new_opaque_closure globalref outerref)))))

(define (valid-ir-return? e)
;; returning lambda directly is needed for @generated
Expand Down Expand Up @@ -4410,48 +4414,59 @@ f(x) = yt(x)
(else (string "\"" h "\" expression"))))
(if (not (null? (cadr lam)))
(error (string (head-to-text (car e)) " not at top level"))))
(define (valid-body-ir-argument? aval)
(or (valid-ir-argument? aval)
(and (symbol? aval) ; Arguments are always defined slots
(or (memq aval (lam:args lam))
(let ((vi (get vinfo-table aval #f)))
(and vi (vinfo:never-undef vi)))))))
(define (single-assign-var? aval)
(and (symbol? aval) ; Arguments are always sa
(or (memq aval (lam:args lam))
(let ((vi (get vinfo-table aval #f)))
(and vi (vinfo:sa vi))))))
;; TODO: We could also allow const globals here
(define (const-read-arg? x)
;; Even if we have side effects, we know that singly-assigned
;; locals cannot be affected them, so we can inline them anyway.
(or (simple-atom? x) (single-assign-var? x)
(and (pair? x)
(memq (car x) '(quote inert top core)))))
;; evaluate the arguments of a call, creating temporary locations as needed
(define (compile-args lst break-labels)
(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)))))
lst)))
(let loop ((lst lst)
(vals '()))
(if (null? lst)
(reverse! vals)
(let* ((arg (car lst))
(aval (or (compile arg break-labels #t #f)
;; TODO: argument exprs that don't yield a value?
'(null))))
(loop (cdr lst)
(cons (if (and (not simple?)
(not (simple-atom? arg))
(not (simple-atom? aval))
(not (and (pair? arg)
(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)
(and (pair? arg)
(memq (car arg) '(globalref outerref))))
(or (null? (cdr lst))
(null? vals)))))
(let ((tmp (make-ssavalue)))
(emit `(= ,tmp ,aval))
tmp)
aval)
vals))))))))
;; First check if all the arguments as simple (and therefore side-effect free).
;; Otherwise, we need to use ssa values for all arguments to ensure proper
;; left-to-right evaluation semantics.
(let ((simple? (every (lambda (x) (or (simple-atom? x) (symbol? x)
(and (pair? x)
(memq (car x) '(quote inert top core globalref outerref)))))
lst)))
(let loop ((lst lst)
(vals '()))
(if (null? lst)
(reverse! vals)
(let* ((arg (car lst))
(aval (or (compile arg break-labels #t #f)
;; TODO: argument exprs that don't yield a value?
'(null))))
(loop (cdr lst)
(cons (if (and
(or simple? (const-read-arg? aval))
(valid-body-ir-argument? aval))
aval
(let ((tmp (make-ssavalue)))
(emit `(= ,tmp ,aval))
tmp))
vals))))))))
(define (compile-cond ex break-labels)
(let ((cnd (or (compile ex break-labels #t #f)
;; TODO: condition exprs that don't yield a value?
'(null))))
(if (not (valid-ir-argument? cnd))
(if (valid-body-ir-argument? cnd) cnd
(let ((tmp (make-ssavalue)))
(emit `(= ,tmp ,cnd))
tmp)
cnd)))
tmp))))
(define (emit-cond cnd break-labels endl)
(let* ((cnd (if (and (pair? cnd) (eq? (car cnd) 'block))
(flatten-ex 'block cnd)
Expand Down
11 changes: 7 additions & 4 deletions src/toplevel.c
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ JL_DLLEXPORT jl_module_t *jl_base_relative_to(jl_module_t *m)
return jl_top_module;
}

static void expr_attributes(jl_value_t *v, int *has_ccall, int *has_defs, int *has_opaque)
static void expr_attributes(jl_value_t *v, jl_array_t *body, int *has_ccall, int *has_defs, int *has_opaque)
{
if (!jl_is_expr(v))
return;
Expand Down Expand Up @@ -390,6 +390,9 @@ static void expr_attributes(jl_value_t *v, int *has_ccall, int *has_defs, int *h
else if (head == jl_call_sym && jl_expr_nargs(e) > 0) {
jl_value_t *called = NULL;
jl_value_t *f = jl_exprarg(e, 0);
if (jl_is_ssavalue(f)) {
f = jl_array_ptr_ref(body, ((jl_ssavalue_t*)f)->id - 1);
}
if (jl_is_globalref(f)) {
jl_module_t *mod = jl_globalref_mod(f);
jl_sym_t *name = jl_globalref_name(f);
Expand Down Expand Up @@ -417,7 +420,7 @@ static void expr_attributes(jl_value_t *v, int *has_ccall, int *has_defs, int *h
for (i = 0; i < jl_array_nrows(e->args); i++) {
jl_value_t *a = jl_exprarg(e, i);
if (jl_is_expr(a))
expr_attributes(a, has_ccall, has_defs, has_opaque);
expr_attributes(a, body, has_ccall, has_defs, has_opaque);
}
}

Expand All @@ -431,7 +434,7 @@ int jl_code_requires_compiler(jl_code_info_t *src, int include_force_compile)
return 1;
for(i=0; i < jl_array_nrows(body); i++) {
jl_value_t *stmt = jl_array_ptr_ref(body,i);
expr_attributes(stmt, &has_ccall, &has_defs, &has_opaque);
expr_attributes(stmt, body, &has_ccall, &has_defs, &has_opaque);
if (has_ccall)
return 1;
}
Expand All @@ -454,7 +457,7 @@ static void body_attributes(jl_array_t *body, int *has_ccall, int *has_defs, int
*has_loops = 1;
}
}
expr_attributes(stmt, has_ccall, has_defs, has_opaque);
expr_attributes(stmt, body, has_ccall, has_defs, has_opaque);
}
*forced_compile = jl_has_meta(body, jl_force_compile_sym);
}
Expand Down
2 changes: 1 addition & 1 deletion stdlib/Test/src/Test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1814,7 +1814,7 @@ matches the inferred type modulo `AllowedType`, or when the return type is a sub
`AllowedType`. This is useful when testing type stability of functions returning a small
union such as `Union{Nothing, T}` or `Union{Missing, T}`.

```jldoctest; setup = :(using InteractiveUtils), filter = r"begin\\n(.|\\n)*end"
```jldoctest; setup = :(using InteractiveUtils; using Base: >), filter = r"begin\\n(.|\\n)*end"
julia> f(a) = a > 1 ? 1 : 1.0
f (generic function with 1 method)

Expand Down
Loading