Skip to content

Commit 9905425

Browse files
committed
JIT-AOT host binding mode + green test_jit_aot suite [skip ci]
Compile test bodies jit-aware and aot so the emitted objects bind as SimNode_Jit with run_jit no-op'd. Adds the test_jit_aot harness (macro co-located under tests/jit_aot), AOT-object glob re-resolution, Func-value + cross-module das call/ctor resolution, and interpret-on-miss for anything unresolved. Brings the test_jit_aot suite to a green 286/286. Object-emit correctness: - Optimize and codegen now share one target (use_host_cpu drives both); a host-CPU TTI on SVE-capable aarch64 autovectorized to scalable vectors (vscale) a generic ISel could not select. Adds --aot-object-generic (default true) so a host-specific object is still possible. - Dispose the MCJIT engine + context on the emit_aot_object early return; it skipped finalize_jit and leaked (LeakSanitizer / MCJIT::createJIT).
1 parent d27714d commit 9905425

12 files changed

Lines changed: 262 additions & 49 deletions

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,10 @@ if (NOT ${DAS_TESTS_DISABLED})
13291329
endif()
13301330
if(NOT ${DAS_AOT_EXAMPLES_DISABLED} AND NOT (WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4))
13311331
include(tests/aot/CMakeLists.txt)
1332+
# JIT-AOT (LLVM-backend .o) mirror of test_aot; LLVM-only, opt-in target.
1333+
if(NOT ${DAS_LLVM_DISABLED})
1334+
include(tests/jit_aot/CMakeLists.txt)
1335+
endif()
13321336
endif()
13331337
endif()
13341338

include/daScript/simulate/aot_library.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ namespace das {
3838
// calls it for is_jit entries.
3939
DAS_API SimNode * makeAotJitNode ( Context & ctx, void * publ );
4040

41+
// Run every LLVM-AOT object's recorded glob re-resolution callback for this
42+
// Context. Called at the tail of linkCppAot (LLVM-AOT builds only).
43+
DAS_API void runLlvmAotGlobInits ( Context & ctx );
44+
4145
// Test standalone context
4246

4347
typedef Context * ( * RegisterTestCreator ) ();

modules/dasLLVM/daslib/llvm_exe.das

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,22 @@ class CollectExternVisitor : AstVisitor {
8484

8585
standalone_ctx : LLVMOpaqueValue?
8686

87+
// Offline AOT object linked into a host that already has the runtime: only
88+
// re-resolve builtin-accessor globals (into `builder`); register NO modules
89+
// or functions (the host owns those). See emit_aot_object_globinit.
90+
aot_object : bool
91+
// The globinit ctor's Context* parameter (set by emit_aot_object_globinit),
92+
// used to resolve Func-value globs to the host's SimFunction* at load.
93+
aot_ctx_arg : LLVMOpaqueValue?
94+
8795

8896
def CollectExternVisitor(standalone_context : LLVMOpaqueValue?; _ctx : LLVMContextRef;
8997
_builder : LLVMBuilderRef,
9098
_mod : LLVMOpaqueModule?; var _types : PrimitiveTypes?,
91-
uids : UidNodes?; register_all : bool) {
99+
uids : UidNodes?; register_all : bool; aot_obj : bool = false) {
92100
any_unimplemented = false
93101
needs_whole_lib = false
102+
aot_object = aot_obj
94103
uid := uids
95104
ctx = _ctx
96105
standalone_ctx = standalone_context
@@ -99,6 +108,11 @@ class CollectExternVisitor : AstVisitor {
99108
builder = _builder
100109
register_mod_type = LLVMFunctionType(types.LLVMVoidPtrType(), array<LLVMTypeRef>())
101110

111+
if (aot_object) {
112+
// Glob re-resolution goes into the caller-provided ctor (builder);
113+
// no initialize_modules(), no env, no module registration.
114+
init_builder = _builder
115+
} else {
102116
// Create initialize_modules() function — filled incrementally as we discover needed modules
103117
let init_fn_type = LLVMFunctionType(types.t_void, array<LLVMTypeRef>())
104118
init_fn = LLVMGetNamedFunction(g_mod, "initialize_modules")
@@ -146,6 +160,7 @@ class CollectExternVisitor : AstVisitor {
146160
}
147161
registered_modules["strings"] = true
148162
}
163+
}
149164

150165
// void * get_jit_table_{at,find,erase} ( int32_t baseType, Context * context, LineInfoArg * at )
151166
reg_extern_tab_type = LLVMFunctionType(types.LLVMVoidPtrType(),
@@ -225,6 +240,21 @@ class CollectExternVisitor : AstVisitor {
225240
}
226241

227242
def register_function(fn : Function?) : LLVMOpaqueValue? {
243+
// AOT-into-host: a Func VALUE must be the host Context's SimFunction* (Func
244+
// dispatch goes through SimFunction, not native code). The function is
245+
// already bound in the host by linkCppAot, so look it up by mangled-name
246+
// hash against the ctor's Context* arg -- works for builtins too (which have
247+
// no publ symbol in this object). No standalone-context registration.
248+
if (aot_object) {
249+
let gfType = LLVMFunctionType(types.LLVMVoidPtrType(),
250+
fixed_array<LLVMTypeRef>(types.t_int64, types.LLVMVoidPtrType()))
251+
var gfFn = LLVMGetNamedFunction(g_mod, "das_aot_get_func_by_mnh")
252+
if (gfFn == null) {
253+
gfFn = LLVMAddFunctionWithType(g_mod, "das_aot_get_func_by_mnh", gfType)
254+
}
255+
var gf_args = array<LLVMValueRef>(types.ConstI64(fn.getMangledNameHash), aot_ctx_arg)
256+
return LLVMBuildCall2(builder, gfType, gfFn, gf_args, "")
257+
}
228258
let fnmna = uid.get_dll_fn_name_ptr(fn)
229259
let fn_publ = LLVMGetNamedFunction(g_mod, fnmna.publ())
230260
if (fn_publ == null) return null
@@ -266,6 +296,7 @@ class CollectExternVisitor : AstVisitor {
266296
}
267297

268298
def ensure_module(m : Module?) {
299+
if (aot_object) return // host already has every module registered
269300
if (m == null) return
270301
let mod_name = string(m.name)
271302
// Dynamic (.shared_module) modules are normally registered via the
@@ -576,6 +607,64 @@ class CollectExternVisitor : AstVisitor {
576607
}
577608
}
578609

610+
// Build a load ctor/dtor pair that re-resolves this AOT object's builtin-accessor
611+
// globals to the HOST's real helpers. Offline objects leave those globals null +
612+
// internal (save_address_global / get_or_create_table_fn under emit_aot_object),
613+
// because the codegen-process addresses are meaningless in the host; this ctor
614+
// fills them at load via linker-resolved accessors (get_jit_table_at, das_get_jit_new,
615+
// jit_initialize_extern_function, ...) and points ExprAddr globals at their publ
616+
// symbols. No module/function registration -- the host owns those; functions bind
617+
// through Program::linkCppAot. Returns the (ctor, dtor) for llvm.global_ctors/dtors.
618+
def public emit_aot_object_globinit(ctx : LLVMContextRef; mod : LLVMOpaqueModule?;
619+
var types : PrimitiveTypes?; var funcs : array<FunctionPtr>;
620+
var uids : UidNodes?) : LLVMOpaqueValue? {
621+
// void das_aot_object_globinit_constructor(Context* ctx): ctx is used to
622+
// resolve Func-value globs to the host's SimFunction* (das_aot_get_func_by_mnh).
623+
let fnType = LLVMFunctionType(types.t_void, fixed_array<LLVMTypeRef>(types.LLVMVoidPtrType()))
624+
let ctor = LLVMAddFunctionWithType(g_mod, "das_aot_object_globinit_constructor", fnType)
625+
LLVMSetLinkage(ctor, LLVMLinkage.LLVMPrivateLinkage)
626+
var b = LLVMCreateBuilderInContext(ctx)
627+
LLVMPositionBuilderAtEnd(b, LLVMAppendBasicBlockInContext(ctx, ctor, "entry"))
628+
// The visitor aliases the caller's uids (its `uid` field). We deliberately do
629+
// NOT delete it: deleting cascades into that shared uid, and the caller keeps
630+
// using uids afterward (the das_aot_register loop). It is a gc_node, reclaimed
631+
// by the surrounding ast_gc_guard / process teardown.
632+
var extern_resolver = new CollectExternVisitor(null, ctx, b, mod, types, uids, false, true)
633+
extern_resolver.aot_ctx_arg = LLVMGetParam(ctor, 0u)
634+
make_visitor(*extern_resolver) $(adapter_resolve) {
635+
ast_gc_guard() {
636+
for (fn in funcs) {
637+
visit(fn, adapter_resolve)
638+
}
639+
}
640+
}
641+
// Fill the cross-module call/ctor target globs recorded during codegen with the
642+
// host Context's SimFunction* for each callee (by mangled-name hash).
643+
if (!(g_aot_func_globs |> empty())) {
644+
let gfType = LLVMFunctionType(types.LLVMVoidPtrType(),
645+
fixed_array<LLVMTypeRef>(types.t_int64, types.LLVMVoidPtrType()))
646+
var gfFn = LLVMGetNamedFunction(g_mod, "das_aot_get_func_by_mnh")
647+
if (gfFn == null) {
648+
gfFn = LLVMAddFunctionWithType(g_mod, "das_aot_get_func_by_mnh", gfType)
649+
}
650+
let ctx_arg = LLVMGetParam(ctor, 0u)
651+
for (fg in g_aot_func_globs) {
652+
if (fg.glob == null) {
653+
continue
654+
}
655+
var gf_args = array<LLVMValueRef>(types.ConstI64(fg.mnh), ctx_arg)
656+
var simfn = LLVMBuildCall2(b, gfType, gfFn, gf_args, "")
657+
// The glob is a pointer-to-fn-type; the SimFunction* (void*) bitcasts in.
658+
let elemT = LLVMGlobalGetValueType(fg.glob)
659+
var casted = LLVMBuildPointerCast(b, simfn, elemT, "")
660+
LLVMBuildStore(b, casted, fg.glob)
661+
}
662+
}
663+
LLVMBuildRetVoid(b)
664+
LLVMDisposeBuilder(b)
665+
return ctor
666+
}
667+
579668
def collect_external_functions(standalone_context : LLVMOpaqueValue?; ctx : LLVMContextRef; builder : LLVMBuilderRef,
580669
mod : LLVMOpaqueModule?; var types : PrimitiveTypes?;
581670
var funcs : array<FunctionPtr>; var uids : UidNodes?;

modules/dasLLVM/daslib/llvm_jit.das

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ bitfield public LlvmJitFlags {
218218
gen_exe,
219219
// Is debug info needed. Debug info not fully supported and not tested yet.
220220
need_di,
221+
// Offline AOT object linked into a host that already has the runtime. Like gen_exe,
222+
// codegen-process addresses are meaningless, so builtin-accessor globals are emitted
223+
// internal + null and re-resolved by a load ctor (host owns the modules/functions).
224+
emit_aot_object,
221225
}
222226

223227
// Non-string workhorse key types that get the inline open-addressed find/at (build_workhorse_*):
@@ -1512,32 +1516,35 @@ class public LlvmJitVisitor : AstVisitor {
15121516
var global_addr = LLVMGetNamedGlobal(g_mod, name.glob())
15131517
if (global_addr == null) {
15141518
global_addr = LLVMAddGlobal(g_mod, nonnull_type, name.glob())
1515-
if (flags.gen_exe) {
1519+
if (flags.gen_exe || flags.emit_aot_object) {
15161520
// nolint:STYLE014
1517-
// For standalone exes the JIT-process address is meaningless
1518-
// (different ASLR slide in the new process). Leave the global
1519-
// null at link time; the runtime initialize_modules() walks
1520-
// every used builtin and overwrites this slot via
1521-
// jit_initialize_extern_function. Baking the codegen-time
1522-
// address as a constant initializer would let LLVM
1523-
// constant-fold loads — observed at top-level `let` initializers
1524-
// — to the JIT-process immediate, which the standalone exe then
1525-
// calls and crashes on (dyld loaded the dylib elsewhere).
1521+
// The JIT-process address is meaningless in the host (different ASLR slide),
1522+
// and baking it as a const initializer lets LLVM constant-fold loads to that
1523+
// dead immediate. Leave null + private; a load-time init (initialize_modules
1524+
// for exe, the globinit ctor for the object) fills it via the runtime accessor.
15261525
set_private_linkage(global_addr)
15271526
LLVMSetInitializer(global_addr, LLVMConstPointerNull(nonnull_type))
15281527
} else {
15291528
set_public_linkage(global_addr)
15301529
LLVMSetInitializer(global_addr, types.ConstPtr(address |> intptr, nonnull_type))
15311530
}
1532-
} elif (!flags.gen_exe) {
1533-
// JIT mode codegen's each function once, so a duplicate name is a real bug. Exe mode
1534-
// re-codegens the same global-var initializers in init_globals + init_globals_clone, so the
1535-
// same slot is legitimately re-encountered — reuse silently (idempotent: same name, same slot).
1531+
} elif (!(flags.gen_exe || flags.emit_aot_object)) {
1532+
// JIT codegens each function once, so a duplicate is a real bug (exe/object modes
1533+
// legitimately re-encounter a slot via init_globals + init_globals_clone).
15361534
failed("Name duplicate {name.glob()}\n")
15371535
}
15381536
return load_local_const(global_addr, nonnull_type)
15391537
}
15401538

1539+
// emit-object mode: record a cross-module call/ctor target glob (just created
1540+
// null by save_address_global) so emit_aot_object_globinit fills it with the
1541+
// host Context's SimFunction* (das_aot_get_func_by_mnh) at load.
1542+
def aot_record_func_glob(name : DllName; func : Function?) {
1543+
if (g_emit_aot_object_globals) {
1544+
g_aot_func_globs |> push((glob = LLVMGetNamedGlobal(g_mod, name.glob()), mnh = func.getMangledNameHash))
1545+
}
1546+
}
1547+
15411548
def set_meta_cconv(instr : LLVMOpaqueValue?; conv : string) {
15421549
var values : LLVMOpaqueValue? [1]
15431550
values[0] = LLVMMDStringInContext(g_ctx, conv, uint(length(conv)))
@@ -1712,6 +1719,7 @@ class public LlvmJitVisitor : AstVisitor {
17121719
if (glob_fn_ptr == null) {
17131720
var MNH_ADDR = get_function_addr(jit_context, expr.func)
17141721
glob_fn_ptr = save_address_global(uid.get_dll_fn_name_ptr(expr.func), MNH_ADDR)
1722+
aot_record_func_glob(uid.get_dll_fn_name_ptr(expr.func), expr.func)
17151723
}
17161724
var params : array<LLVMOpaqueValue?> // nolint:STYLE012conditional cmresEval push between fixed args
17171725
params |> push(glob_fn_ptr) // mnh
@@ -4252,6 +4260,7 @@ class public LlvmJitVisitor : AstVisitor {
42524260
if (ctor == null) {
42534261
var MNH_ADDR = get_function_addr(jit_context, expr_func)
42544262
ctor = save_address_global(name, MNH_ADDR)
4263+
aot_record_func_glob(name, expr_func)
42554264
}
42564265
var params <- [
42574266
ctor, // mnh
@@ -7915,16 +7924,11 @@ def public generate_llvm_code(var dll : DLLHandle?; fmna : DllName; dll_enabled
79157924
return code
79167925
}
79177926

7918-
// Creates a single global constructor (run at compiled-object startup) for complex global types
7919-
// like fileInfo that can't be const-initialized.
7927+
// Emit the module's load/unload ctors into llvm.global_ctors/dtors: the FileInfo ctor/dtor,
7928+
// plus -- in offline AOT object mode (emit_aot) -- a ctor calling das_aot_register per function
7929+
// so the .o self-registers into the AOT library at load. funcs/uids/prog read only when emit_aot.
79207930
[macro_function]
7921-
// Emit the module's load/unload constructors into llvm.global_ctors/dtors: the
7922-
// FileInfo ctor/dtor, plus -- in offline AOT object mode (emit_aot) -- a ctor
7923-
// that calls the linker-resolved extern das_aot_register(aotHash, publ) for each
7924-
// of this module's functions, so the emitted .o self-registers into the AOT
7925-
// library at load (reusing the existing AotFactory / linkCppAot path via
7926-
// SimNode_Jit). funcs/uids/prog are only read when emit_aot is true.
7927-
def public generate_global_ctors_dtors(types : PrimitiveTypes?; jit_mode : bool; emit_aot : bool; funcs : array<FunctionPtr>; var uids : UidNodes?; prog : Program?) {
7931+
def public generate_global_ctors_dtors(types : PrimitiveTypes?; jit_mode : bool; emit_aot : bool; funcs : array<FunctionPtr>; var uids : UidNodes?; prog : Program?; glob_ctor : LLVMOpaqueValue? = null) {
79287932
var ctor_dtor <- [
79297933
// FileInfo
79307934
generate_fileinfo_ctor_dtor(*types, active_filenames, jit_mode)
@@ -7938,8 +7942,7 @@ def public generate_global_ctors_dtors(types : PrimitiveTypes?; jit_mode : bool;
79387942
regFn = LLVMAddFunctionWithType(g_mod, "das_aot_register", regType)
79397943
}
79407944
let functionType = LLVMFunctionType(types.t_void, null, 0u, 0)
7941-
// Private: referenced only via llvm.global_ctors, and each object defines it,
7942-
// so public linkage would clash when many objects link into one host.
7945+
// Private: each object defines it, so public linkage would clash when many link together.
79437946
let reg_ctor = LLVMAddFunctionWithType(g_mod, "das_aot_register_constructor", functionType)
79447947
let reg_dtor = LLVMAddFunctionWithType(g_mod, "das_aot_register_destructor", functionType)
79457948
set_private_linkage(reg_ctor)
@@ -7961,6 +7964,19 @@ def public generate_global_ctors_dtors(types : PrimitiveTypes?; jit_mode : bool;
79617964
var reg_args = array<LLVMValueRef>(types.ConstI64(aot_hash), publ_ptr)
79627965
LLVMBuildCall2(ctor_builder, regType, regFn, reg_args, "")
79637966
}
7967+
// Record this object's glob re-resolution callback; the runtime runs it lazily at
7968+
// the first AotLibrary drain (NOT here at static-init, where its accessors would
7969+
// fault on a null bound environment).
7970+
if (glob_ctor != null) {
7971+
let giType = LLVMFunctionType(types.t_void, fixed_array<LLVMTypeRef>(types.LLVMVoidPtrType()))
7972+
var giFn = LLVMGetNamedFunction(g_mod, "das_aot_register_globinit")
7973+
if (giFn == null) {
7974+
giFn = LLVMAddFunctionWithType(g_mod, "das_aot_register_globinit", giType)
7975+
}
7976+
var gi_ptr = LLVMBuildPointerCast(ctor_builder, glob_ctor, types.LLVMVoidPtrType(), "")
7977+
var gi_args = array<LLVMValueRef>(gi_ptr)
7978+
LLVMBuildCall2(ctor_builder, giType, giFn, gi_args, "")
7979+
}
79647980
LLVMBuildRetVoid(ctor_builder)
79657981
LLVMBuildRetVoid(dtor_builder)
79667982
LLVMDisposeBuilder(ctor_builder)

modules/dasLLVM/daslib/llvm_jit_cli.das

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,8 @@ struct public JitCliOptions {
6969
@clarg_name = "jit-register-all-modules"
7070
@clarg_doc = "JIT (-exe only): register all builtin native modules (math, fio, dasbind, ...) at exe startup, so a standalone compiler-driver exe can recompile arbitrary daslang at runtime"
7171
register_all_modules : Option<bool>
72+
73+
@clarg_name = "aot-object-generic"
74+
@clarg_doc = "JIT (--aot-object): emit a portable generic-CPU object (default true). Pass =false for a host-CPU-specific object (host features, e.g. SVE/AVX) that only runs on matching hardware. Optimize + codegen use the same target either way."
75+
aot_object_generic : Option<bool>
7276
}

modules/dasLLVM/daslib/llvm_jit_common.das

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,14 @@ def private get_or_create_table_fn(llvm_module : LLVMOpaqueModule?; var types :
253253

254254
let at_ptr_type = LLVMPointerType(at_type, 0u)
255255
tab_fun = LLVMAddGlobal(llvm_module, LLVMPointerType(at_ptr_type, 0u), fn_name.glob())
256-
add_table_linkage(tab_fun)
257-
LLVMSetInitializer(tab_fun, types.ConstPtr(fn_ptr |> intptr(), at_ptr_type))
256+
if (g_emit_aot_object_globals) {
257+
// Per-object internal + null; the globinit ctor re-resolves it at load.
258+
LLVMSetLinkage(tab_fun, LLVMLinkage.LLVMInternalLinkage)
259+
LLVMSetInitializer(tab_fun, LLVMConstPointerNull(LLVMPointerType(at_ptr_type, 0u)))
260+
} else {
261+
add_table_linkage(tab_fun)
262+
LLVMSetInitializer(tab_fun, types.ConstPtr(fn_ptr |> intptr(), at_ptr_type))
263+
}
258264
} else {
259265
// we already created it
260266
tab_fun = LLVMGetNamedGlobal(llvm_module, fn_name.glob())
@@ -536,6 +542,16 @@ def public LLVMAddFunctionWithType(mod : LLVMModuleRef; name : string; typ : LLV
536542

537543
var g_jit_clopts_parsed = false
538544

545+
// Set while emitting an offline AOT object: builtin-accessor globals are emitted
546+
// internal + null and re-resolved by the emit_aot_object_globinit load ctor,
547+
// instead of a baked codegen-process address. run_jit sets/clears it around codegen.
548+
var public g_emit_aot_object_globals = false
549+
550+
// Emit-object mode: cross-module / non-jit das call + ctor target globs hold a host-
551+
// meaningless SimFunction*. Recorded here (glob + callee mnh) during codegen; the
552+
// globinit ctor fills them from the host Context at load. run_jit clears per compile.
553+
var public g_aot_func_globs : array<tuple<glob : LLVMOpaqueValue?; mnh : uint64>>
554+
539555
// LLVM's unroll / partial-unroll / unroll-and-jam cost-model thresholds are compiled-in cl::opt
540556
// globals; daslang's JIT path never runs cl::ParseCommandLineOptions, so without these the O3
541557
// pipeline lands different (worse) IR shapes than `opt` on the same input (bisected on dasProfile).
@@ -1167,12 +1183,10 @@ def public with_default_target_machine(opt_level : uint; use_host_cpu : bool;
11671183
}
11681184
}
11691185

1170-
// Offline AOT: emit a native object only, no linker step. Used by the
1171-
// jit_emit_object path that produces .o objects statically linked into a host
1172-
// binary. Generic CPU (use_host_cpu=false) so the object runs on any machine the
1173-
// host targets.
1174-
def public emit_object_only(mod : LLVMOpaqueModule?; out_path : string) {
1175-
with_default_target_machine(3u, false) $(targetMachine : LLVMTargetMachineRef) {
1186+
// Offline AOT: emit a native object only, no linker step (jit_emit_object path).
1187+
// use_host_cpu=false (default) targets a generic CPU so the .o is portable; true bakes host features.
1188+
def public emit_object_only(mod : LLVMOpaqueModule?; out_path : string; use_host_cpu : bool = false) {
1189+
with_default_target_machine(3u, use_host_cpu) $(targetMachine : LLVMTargetMachineRef) {
11761190
let error : string?
11771191
let file = add_obj_extension(out_path)
11781192
let filetype = LLVMCodeGenFileType.LLVMObjectFile

0 commit comments

Comments
 (0)