Skip to content

Commit

Permalink
codegen: start to remove the ability to call back into inference (#54655
Browse files Browse the repository at this point in the history
)

Continuing the development in #53219, according to the plan in
https://hackmd.io/@vtjnash/codeinstances, this further separates the
meaning behind CodeInfo and CodeInstance, such that CodeInstance can
only be used as a call target, and cannot be used for code generation,
while CodeInfo can only be used to generate code (or for reflection on
what code would be generated), and cannot be used as a call target.
Basically, the eventual idea is that CodeInfo will only show up now as
an input (e.g. for doing inlining or codegen) and is ephemeral, while a
CodeInstance is what shows up in a cache (e.g. as a callable object).
  • Loading branch information
vtjnash authored Jun 6, 2024
2 parents 5cb1107 + a4cc6c8 commit ec32170
Show file tree
Hide file tree
Showing 21 changed files with 196 additions and 291 deletions.
31 changes: 11 additions & 20 deletions base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,9 @@ function finish(me::InferenceState, interp::AbstractInterpreter)
me.result.result = bestguess
ipo_effects = me.result.ipo_effects = me.ipo_effects = adjust_effects(me)
me.result.exc_result = me.exc_bestguess = refine_exception_type(me.exc_bestguess, ipo_effects)
me.src.rettype = widenconst(ignorelimited(bestguess))
me.src.min_world = first(me.valid_worlds)
me.src.max_world = last(me.valid_worlds)

if limited_ret
# a parent may be cached still, but not this intermediate work:
Expand Down Expand Up @@ -933,6 +936,7 @@ function codeinfo_for_const(interp::AbstractInterpreter, mi::MethodInstance, @no
tree.ssavaluetypes = 1
tree.debuginfo = DebugInfo(mi)
tree.ssaflags = UInt32[0]
tree.rettype = Core.Typeof(val)
set_inlineable!(tree, true)
tree.parent = mi
return tree
Expand Down Expand Up @@ -965,15 +969,13 @@ typeinf_code(interp::AbstractInterpreter, method::Method, @nospecialize(atype),
typeinf_code(interp, specialize_method(method, atype, sparams), run_optimizer)
function typeinf_code(interp::AbstractInterpreter, mi::MethodInstance, run_optimizer::Bool)
frame = typeinf_frame(interp, mi, run_optimizer)
frame === nothing && return nothing, Any
is_inferred(frame) || return nothing, Any
frame === nothing && return nothing
is_inferred(frame) || return nothing
if result_is_constabi(interp, frame.result, run_optimizer)
rt = frame.result.result::Const
return codeinfo_for_const(interp, frame.linfo, rt.val), widenconst(rt)
return codeinfo_for_const(interp, frame.linfo, rt.val)
end
code = frame.src
rt = widenconst(ignorelimited(frame.result.result))
return code, rt
return frame.src
end

"""
Expand Down Expand Up @@ -1063,15 +1065,6 @@ N.B.: The same caching considerations as SOURCE_MODE_ABI apply.
"""
const SOURCE_MODE_FORCE_SOURCE = 0x2

"""
SOURCE_MODE_FORCE_SOURCE_UNCACHED
Like `SOURCE_MODE_FORCE_SOURCE`, but ensures that the resulting code instance is
not part of the cache hierarchy, so the `->inferred` field may be safely used
without the possibility of deletion by the compiler.
"""
const SOURCE_MODE_FORCE_SOURCE_UNCACHED = 0x3

function ci_has_source(code::CodeInstance)
inf = @atomic :monotonic code.inferred
return isa(inf, CodeInfo) || isa(inf, String)
Expand All @@ -1093,7 +1086,6 @@ function ci_meets_requirement(code::CodeInstance, source_mode::UInt8, ci_is_cach
source_mode == SOURCE_MODE_NOT_REQUIRED && return true
source_mode == SOURCE_MODE_ABI && return ci_has_abi(code)
source_mode == SOURCE_MODE_FORCE_SOURCE && return ci_has_source(code)
source_mode == SOURCE_MODE_FORCE_SOURCE_UNCACHED && return (!ci_is_cached && ci_has_source(code))
return false
end

Expand All @@ -1106,7 +1098,7 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance, source_mod
code = get(code_cache(interp), mi, nothing)
if code isa CodeInstance
# see if this code already exists in the cache
if source_mode in (SOURCE_MODE_FORCE_SOURCE, SOURCE_MODE_FORCE_SOURCE_UNCACHED) && use_const_api(code)
if source_mode == SOURCE_MODE_FORCE_SOURCE && use_const_api(code)
code = codeinstance_for_const_with_code(interp, code)
ccall(:jl_typeinf_timing_end, Cvoid, (UInt64,), start_time)
return code
Expand All @@ -1128,7 +1120,7 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance, source_mod
end
lock_mi_inference(interp, mi)
result = InferenceResult(mi, typeinf_lattice(interp))
frame = InferenceState(result, #=cache_mode=#source_mode == SOURCE_MODE_FORCE_SOURCE_UNCACHED ? :volatile : :global, interp)
frame = InferenceState(result, #=cache_mode=#:global, interp)
frame === nothing && return nothing
typeinf(interp, frame)
ccall(:jl_typeinf_timing_end, Cvoid, (UInt64,), start_time)
Expand All @@ -1147,14 +1139,13 @@ function typeinf_ext(interp::AbstractInterpreter, mi::MethodInstance, source_mod
# store the source in the cache, but the caller wanted it anyway (e.g. for reflection).
# We construct a new CodeInstance for it that is not part of the cache hierarchy.
can_discard_trees = source_mode SOURCE_MODE_FORCE_SOURCE &&
source_mode SOURCE_MODE_FORCE_SOURCE_UNCACHED &&
is_result_constabi_eligible(result)
code = CodeInstance(interp, result; can_discard_trees)

# If the caller cares about the code and this is constabi, still use our synthesis function
# anyway, because we will have not finished inferring the code inside the CodeInstance once
# we realized it was constabi, but we want reflection to pretend that we did.
if use_const_api(code) && source_mode in (SOURCE_MODE_FORCE_SOURCE, SOURCE_MODE_FORCE_SOURCE_UNCACHED)
if use_const_api(code) && source_mode == SOURCE_MODE_FORCE_SOURCE
return codeinstance_for_const_with_code(interp, code)
end
@assert ci_meets_requirement(code, source_mode, false)
Expand Down
1 change: 1 addition & 0 deletions base/opaque_closure.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function Core.OpaqueClosure(ir::IRCode, @nospecialize env...;
src.isva = isva
src.nargs = nargtypes
src = Core.Compiler.ir_to_codeinf!(src, ir)
src.rettype = rt
return generate_opaque_closure(sig, Union{}, rt, src, nargs, isva, env...; kwargs...)
end

Expand Down
15 changes: 10 additions & 5 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1660,12 +1660,12 @@ function code_typed_by_type(@nospecialize(tt::Type);
asts = []
for match in matches.matches
match = match::Core.MethodMatch
(code, ty) = Core.Compiler.typeinf_code(interp, match, optimize)
code = Core.Compiler.typeinf_code(interp, match, optimize)
if code === nothing
push!(asts, match.method => Any)
else
debuginfo === :none && remove_linenums!(code)
push!(asts, code => ty)
push!(asts, code => code.rettype)
end
end
return asts
Expand All @@ -1682,14 +1682,19 @@ function get_oc_code_rt(oc::Core.OpaqueClosure, types, optimize::Bool)
tt = Tuple{typeof(oc.captures), to_tuple_type(types).parameters...}
mi = Core.Compiler.specialize_method(m, tt, Core.svec())
interp = Core.Compiler.NativeInterpreter(m.primary_world)
return Core.Compiler.typeinf_code(interp, mi, optimize)
code = Core.Compiler.typeinf_code(interp, mi, optimize)
if code isa CodeInfo
return Pair{CodeInfo, Any}(code, code.rettype)
end
error("inference not successful")
else
code = _uncompressed_ir(m)
return Pair{CodeInfo,Any}(code, typeof(oc).parameters[2])
return Pair{CodeInfo, Any}(code, typeof(oc).parameters[2])
end
else
# OC constructed from optimized IR
codeinst = m.specializations.cache
# XXX: the inferred field is not normally a CodeInfo, but this assumes it is guaranteed to be always
return Pair{CodeInfo, Any}(codeinst.inferred, codeinst.rettype)
end
else
Expand Down Expand Up @@ -2209,7 +2214,7 @@ function print_statement_costs(io::IO, @nospecialize(tt::Type);
for match in matches.matches
match = match::Core.MethodMatch
println(io, match.method)
(code, ty) = Core.Compiler.typeinf_code(interp, match, true)
code = Core.Compiler.typeinf_code(interp, match, true)
if code === nothing
println(io, " inference not successful")
else
Expand Down
34 changes: 15 additions & 19 deletions doc/src/devdocs/ast.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ for important details on how to modify these fields safely.

### CodeInfo

A (usually temporary) container for holding lowered source code.
A (usually temporary) container for holding lowered (and possibly inferred) source code.

* `code`

Expand Down Expand Up @@ -691,25 +691,18 @@ A (usually temporary) container for holding lowered source code.
Statement-level 32 bits flags for each expression in the function.
See the definition of `jl_code_info_t` in julia.h for more details.

These are only populated after inference (or by generated functions in some cases):

* `debuginfo`

An object to retrieve source information for each statements, see
[How to interpret line numbers in a `CodeInfo` object](@ref).

Optional Fields:

* `slottypes`

An array of types for the slots.

* `rettype`

The inferred return type of the lowered form (IR). Default value is `Any`.

* `method_for_inference_limit_heuristics`

The `method_for_inference_heuristics` will expand the given method's generator if
necessary during inference.
The inferred return type of the lowered form (IR). Default value is `Any`. This is
mostly present for convenience, as (due to the way OpaqueClosures work) it is not
necessarily the rettype used by codegen.

* `parent`

Expand All @@ -723,16 +716,19 @@ Optional Fields:

The range of world ages for which this code was valid at the time when it had been inferred.

Optional Fields:

Boolean properties:
* `slottypes`

* `inferred`
An array of types for the slots.

Whether this has been produced by type inference.
* `method_for_inference_limit_heuristics`

* `inlineable`
The `method_for_inference_heuristics` will expand the given method's generator if
necessary during inference.

Whether this should be eligible for inlining.

Boolean properties:

* `propagate_inbounds`

Expand All @@ -742,7 +738,7 @@ Boolean properties:

`UInt8` settings:

* `constprop`
* `constprop`, `inlineable`

* 0 = use heuristic
* 1 = aggressive
Expand Down
14 changes: 10 additions & 4 deletions doc/src/devdocs/compiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,16 @@ Use appropriate care when copying.

## Specialized Calling Convention Signature Representation

A `jl_returninfo_t` object describes the calling convention details of any callable.
A `jl_returninfo_t` object describes the specialized calling convention details of any
callable. It can be generated from any (specTypes, rettype) pair, such as a CodeInstance, or
other place they are declared. This is the expected calling convention for specptr, but
other data may be stored there. Only if the function pointer stored there has the
expected specialized calling convention will the corresponding flag be set in specsigflags
to indicate it is useable.

If any of the arguments or return type of a method can be represented unboxed,
and the method is not varargs, it'll be given an optimized calling convention
signature based on its `specTypes` and `rettype` fields.
If any of the arguments or return type of a method can be represented unboxed, and none are
unable to be represented unboxed (such as an unbounded vararg), it will be given an
optimized calling convention signature based on the `specTypes` and `rettype` values.

The general principles are that:

Expand All @@ -112,4 +117,5 @@ The total logic for this is implemented by `get_specsig_function` and `deserves_

Additionally, if the return type is a union, it may be returned as a pair of values (a pointer and a tag).
If the union values can be stack-allocated, then sufficient space to store them will also be passed as a hidden first argument.
If the struct to return needs gc roots, space for those will be passed as a hidden second argument.
It is up to the callee whether the returned pointer will point to this space, a boxed object, or even other constant memory.
91 changes: 26 additions & 65 deletions src/aotcompile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1929,97 +1929,62 @@ void addTargetPasses(legacy::PassManagerBase *PM, const Triple &triple, TargetIR
PM->add(createTargetTransformInfoWrapperPass(std::move(analysis)));
}

// sometimes in GDB you want to find out what code was created from a mi
// sometimes in GDB you want to find out what code would be created from a mi
extern "C" JL_DLLEXPORT_CODEGEN jl_code_info_t *jl_gdbdumpcode(jl_method_instance_t *mi)
{
jl_llvmf_dump_t llvmf_dump;
size_t world = jl_current_task->world_age;
JL_STREAM *stream = (JL_STREAM*)STDERR_FILENO;

jl_code_info_t *src = jl_gdbcodetyped1(mi, world);
JL_GC_PUSH1(&src);

jl_printf(stream, "---- dumping IR for ----\n");
jl_static_show(stream, (jl_value_t*)mi);
jl_printf(stream, "\n----\n");

jl_printf(stream, "\n---- unoptimized IR ----");
jl_get_llvmf_defn(&llvmf_dump, mi, world, 0, false, jl_default_cgparams);
jl_printf(stream, "\n---- unoptimized IR ----\n");
jl_get_llvmf_defn(&llvmf_dump, mi, src, 0, false, jl_default_cgparams);
if (llvmf_dump.F) {
jl_value_t *ir = jl_dump_function_ir(&llvmf_dump, 0, 1, "source");
jl_static_show(stream, ir);
if (ir != NULL && jl_is_string(ir))
jl_printf(stream, "%s", jl_string_data(ir));
}
jl_printf(stream, "----\n");
jl_printf(stream, "\n----\n");

jl_printf(stream, "\n---- optimized IR ----");
jl_get_llvmf_defn(&llvmf_dump, mi, world, 0, true, jl_default_cgparams);
jl_printf(stream, "\n---- optimized IR ----\n");
jl_get_llvmf_defn(&llvmf_dump, mi, src, 0, true, jl_default_cgparams);
if (llvmf_dump.F) {
jl_value_t *ir = jl_dump_function_ir(&llvmf_dump, 0, 1, "source");
jl_static_show(stream, ir);
if (ir != NULL && jl_is_string(ir))
jl_printf(stream, "%s", jl_string_data(ir));
}
jl_printf(stream, "----\n");
jl_printf(stream, "\n----\n");

jl_printf(stream, "\n---- assembly ----");
jl_get_llvmf_defn(&llvmf_dump, mi, world, 0, true, jl_default_cgparams);
jl_printf(stream, "\n---- assembly ----\n");
jl_get_llvmf_defn(&llvmf_dump, mi, src, 0, true, jl_default_cgparams);
if (llvmf_dump.F) {
jl_value_t *ir = jl_dump_function_asm(&llvmf_dump, 0, "", "source", 0, true);
jl_static_show(stream, ir);
if (ir != NULL && jl_is_string(ir))
jl_printf(stream, "%s", jl_string_data(ir));
}
jl_printf(stream, "----\n");
jl_printf(stream, "\n----\n");
JL_GC_POP();

jl_code_info_t *src = NULL;
jl_value_t *ci = jl_default_cgparams.lookup(mi, world, world);
if (ci == jl_nothing) {
ci = (jl_value_t*)jl_type_infer(mi, world, 0, SOURCE_MODE_FORCE_SOURCE_UNCACHED);
} else {
ci = NULL;
}
if (ci) {
jl_code_instance_t *codeinst = (jl_code_instance_t*)ci;
src = (jl_code_info_t*)jl_atomic_load_relaxed(&codeinst->inferred);
if ((jl_value_t*)src != jl_nothing && !jl_is_code_info(src) && jl_is_method(mi->def.method)) {
JL_GC_PUSH2(&codeinst, &src);
src = jl_uncompress_ir(mi->def.method, codeinst, (jl_value_t*)src);
JL_GC_POP();
}
}
return src;
}

// --- native code info, and dump function to IR and ASM ---
// Get pointer to llvm::Function instance, compiling if necessary
// for use in reflection from Julia.
// This is paired with jl_dump_function_ir, jl_dump_function_asm, jl_dump_method_asm in particular ways:
// misuse will leak memory or cause read-after-free
// This is paired with jl_dump_function_ir and jl_dump_function_asm, either of which will free all memory allocated here
extern "C" JL_DLLEXPORT_CODEGEN
void jl_get_llvmf_defn_impl(jl_llvmf_dump_t* dump, jl_method_instance_t *mi, size_t world, char getwrapper, char optimize, const jl_cgparams_t params)
void jl_get_llvmf_defn_impl(jl_llvmf_dump_t* dump, jl_method_instance_t *mi, jl_code_info_t *src, char getwrapper, char optimize, const jl_cgparams_t params)
{
if (jl_is_method(mi->def.method) && mi->def.method->source == NULL &&
mi->def.method->generator == NULL && !mi->def.method->is_for_opaque_closure) {
// not a generic function
dump->F = NULL;
return;
}

// get the source code for this function
jl_code_info_t *src = NULL;
jl_code_instance_t *codeinst = NULL;
JL_GC_PUSH2(&src, &codeinst);
jl_value_t *ci = params.lookup(mi, world, world);
if (ci && ci != jl_nothing) {
codeinst = (jl_code_instance_t*)ci;
src = (jl_code_info_t*)jl_atomic_load_relaxed(&codeinst->inferred);
}
if (!src || (jl_value_t*)src == jl_nothing) {
codeinst = jl_type_infer(mi, world, 0, SOURCE_MODE_FORCE_SOURCE_UNCACHED);
if (codeinst) {
src = (jl_code_info_t*)jl_atomic_load_relaxed(&codeinst->inferred);
}
}
if (src) {
if ((jl_value_t*)src != jl_nothing && !jl_is_code_info(src) && jl_is_method(mi->def.method))
src = jl_uncompress_ir(mi->def.method, codeinst, (jl_value_t*)src);
}

// emit this function into a new llvm module
if (codeinst && src && jl_is_code_info(src)) {
dump->F = nullptr;
dump->TSM = nullptr;
if (src && jl_is_code_info(src)) {
auto ctx = jl_ExecutionEngine->getContext();
orc::ThreadSafeModule m = jl_create_ts_module(name_from_method_instance(mi), *ctx);
uint64_t compiler_start_time = 0;
Expand All @@ -2040,7 +2005,7 @@ void jl_get_llvmf_defn_impl(jl_llvmf_dump_t* dump, jl_method_instance_t *mi, siz
// This would also be nice, but it seems to cause OOMs on the windows32 builder
// To get correct names in the IR this needs to be at least 2
output.debug_level = params.debug_info_level;
auto decls = jl_emit_code(m, mi, src, codeinst->rettype, output, jl_atomic_load_relaxed(&codeinst->min_world), jl_atomic_load_relaxed(&codeinst->max_world));
auto decls = jl_emit_code(m, mi, src, output);
JL_UNLOCK(&jl_codegen_lock); // Might GC

Function *F = NULL;
Expand Down Expand Up @@ -2091,7 +2056,6 @@ void jl_get_llvmf_defn_impl(jl_llvmf_dump_t* dump, jl_method_instance_t *mi, siz
fname = &decls.functionObject;
F = cast<Function>(m.getModuleUnlocked()->getNamedValue(*fname));
}
JL_GC_POP();
if (measure_compile_time_enabled) {
auto end = jl_hrtime();
jl_atomic_fetch_add_relaxed(&jl_cumulative_compile_time, end - compiler_start_time);
Expand All @@ -2102,7 +2066,4 @@ void jl_get_llvmf_defn_impl(jl_llvmf_dump_t* dump, jl_method_instance_t *mi, siz
return;
}
}

const char *mname = name_from_method_instance(mi);
jl_errorf("unable to compile source for function %s", mname);
}
Loading

0 comments on commit ec32170

Please sign in to comment.