Skip to content

Commit 954edbc

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 954edbc

14 files changed

Lines changed: 344 additions & 75 deletions

File tree

CMakeLists.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,42 @@ MACRO(DAS_AOT_LIB input genList mainTarget dasAotTool)
276276
DAS_AOT_EXT("${input}" ${genList} ${mainTarget} ${dasAotTool} -aotlib)
277277
ENDMACRO()
278278

279+
# LLVM analog of DAS_AOT_LIB: emit a native .o per .das (this module only) via the LLVM
280+
# backend instead of the C++ proxy. Linked as external objects, so their load ctors
281+
# self-register into the AOT library. Needs LLVM (call only under NOT DAS_LLVM_DISABLED).
282+
MACRO(DAS_JIT_AOT_LIB input_files genList mainTarget)
283+
# The emit-object codegen lives in dasLLVM's daslib (loaded at runtime), so depend
284+
# on it too -- ninja re-emits the .o when the codegen changes, not just the binary.
285+
file(GLOB _das_jit_aot_codegen CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/modules/dasLLVM/daslib/*.das)
286+
set(all_outputs "")
287+
foreach(input ${input_files})
288+
get_filename_component(input_src ${input} ABSOLUTE)
289+
get_filename_component(input_dir ${input_src} DIRECTORY)
290+
get_filename_component(input_name ${input} NAME)
291+
set(out_dir ${input_dir}/_jit_aot_generated)
292+
set(out_prefix ${out_dir}/${mainTarget}_${input_name})
293+
set(out_obj ${out_prefix}.o)
294+
# The compile-time path is baked into hashed constants (same reason as
295+
# DAS_AOT_EXT), so spell the input source-root-relative to keep the AOT
296+
# hash in sync with `-use-aot --test tests`.
297+
file(RELATIVE_PATH input_rel ${PROJECT_SOURCE_DIR} ${input_src})
298+
file(MAKE_DIRECTORY ${out_dir})
299+
list(APPEND all_outputs ${out_obj})
300+
list(APPEND ${genList} ${out_obj})
301+
set_source_files_properties(${out_obj} PROPERTIES GENERATED TRUE EXTERNAL_OBJECT TRUE)
302+
ADD_CUSTOM_COMMAND(
303+
OUTPUT ${out_obj}
304+
DEPENDS daslang ${input_src} ${PROJECT_SOURCE_DIR}/utils/jit/main.das ${_das_jit_aot_codegen}
305+
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
306+
COMMENT "JIT-AOT compiling ${input_rel}"
307+
COMMAND daslang ${PROJECT_SOURCE_DIR}/utils/jit/main.das -- ${input_rel} --aot-object -o ${out_prefix}
308+
)
309+
endforeach()
310+
set(custom_name ${mainTarget}_genjitaot)
311+
ADD_CUSTOM_TARGET(${custom_name} DEPENDS ${all_outputs})
312+
ADD_DEPENDENCIES(${mainTarget} ${custom_name})
313+
ENDMACRO()
314+
279315
SET(UNITZE_BUILD_BATCH_SIZE 10)
280316

281317
MACRO(UNITIZE_BUILD input_dir genList)
@@ -1328,6 +1364,7 @@ if (NOT ${DAS_TESTS_DISABLED})
13281364
include(examples/pathTracer/CMakeLists.txt)
13291365
endif()
13301366
if(NOT ${DAS_AOT_EXAMPLES_DISABLED} AND NOT (WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4))
1367+
# tests/aot also defines the LLVM-only test_jit_aot binary (mirror of test_aot).
13311368
include(tests/aot/CMakeLists.txt)
13321369
endif()
13331370
endif()

doc/source/stdlib/handmade/structure_annotation-rtti-CodeOfPolicies.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ JIT all functions - if enabled, JIT will compile all functions in the module.
9999
JIT debug info - if enabled, JIT will generate debug info for JIT compiled code.
100100
JIT dll mode - if enabled, JIT will generate DLL's into JIT output folder and load them from there.
101101
JIT exe mode - if enabled, JIT will generate standalone executable.
102+
JIT offline AOT object mode - emit a native .o (this module only) with a load constructor registering its functions into the AOT library, for static linking into a host binary.
102103
JIT will always emit function prologues, which allows call-stack in debuggers.
103104
JIT output folder (where JIT compiled code will be stored).
104105
JIT optimization level for compiled code (0-3).

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: 112 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,40 +84,29 @@ class CollectExternVisitor : AstVisitor {
8484

8585
standalone_ctx : LLVMOpaqueValue?
8686

87-
88-
def CollectExternVisitor(standalone_context : LLVMOpaqueValue?; _ctx : LLVMContextRef;
89-
_builder : LLVMBuilderRef,
90-
_mod : LLVMOpaqueModule?; var _types : PrimitiveTypes?,
91-
uids : UidNodes?; register_all : bool) {
92-
any_unimplemented = false
93-
needs_whole_lib = false
94-
uid := uids
95-
ctx = _ctx
96-
standalone_ctx = standalone_context
97-
mod = _mod
98-
types = _types
99-
builder = _builder
100-
register_mod_type = LLVMFunctionType(types.LLVMVoidPtrType(), array<LLVMTypeRef>())
101-
102-
// Create initialize_modules() function — filled incrementally as we discover needed modules
103-
let init_fn_type = LLVMFunctionType(types.t_void, array<LLVMTypeRef>())
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+
95+
96+
// Build initialize_modules(): das_ensure_environment, then register builtins — ALL native
97+
// modules (register_all, force-linked via jit_register_all_builtin_modules) or just $ + strings.
98+
// Marks registered_modules so ensure_module doesn't double-register ("already created").
99+
def private build_initialize_modules(register_all : bool) {
104100
init_fn = LLVMGetNamedFunction(g_mod, "initialize_modules")
105101
let init_entry = LLVMAppendBasicBlockInContext(ctx, init_fn, "entry")
106102
init_builder = LLVMCreateBuilder()
107103
LLVMPositionBuilderAtEnd(init_builder, init_entry)
108-
// Always call das_ensure_environment first
109104
let env_type = LLVMFunctionType(types.t_void, array<LLVMTypeRef>())
110105
var env_fn = LLVMGetNamedFunction(g_mod, "das_ensure_environment")
111106
if (env_fn == null) {
112107
env_fn = LLVMAddFunctionWithType(g_mod, "das_ensure_environment", env_type)
113108
}
114109
LLVMBuildCall2(init_builder, env_type, env_fn, array<LLVMValueRef>(), "")
115-
// Optionally register ALL builtin native modules (math, fio, dasbind, ...)
116-
// so a self-contained compiler-driver exe can recompile arbitrary daslang
117-
// at runtime. Referencing this symbol also force-links those modules in.
118-
// register_builtin_modules() covers exactly the set below, so mark them as
119-
// already-registered to suppress the per-module emission (otherwise each
120-
// would be registered twice -> "Module '...' already created").
121110
if (register_all) {
122111
var reg_all = LLVMGetNamedFunction(g_mod, "jit_register_all_builtin_modules")
123112
if (reg_all == null) {
@@ -130,9 +119,7 @@ class CollectExternVisitor : AstVisitor {
130119
registered_modules[m] = true
131120
}
132121
} else {
133-
// Always register Module_BuiltIn ($) and Module_Strings (strings) — unless
134-
// inject_main's static sweep already emitted the call (the thunks are NOT
135-
// idempotent: a second call dies with "Module '$' already created").
122+
// $ and strings are non-idempotent thunks; skip if inject_main's static sweep already emitted them.
136123
if (!(g_exe_emitted_reg |> key_exists("jit_register_Module_BuiltIn"))) {
137124
g_exe_emitted_reg["jit_register_Module_BuiltIn"] = true
138125
var reg_builtin = LLVMAddFunctionWithType(g_mod, "jit_register_Module_BuiltIn", register_mod_type)
@@ -146,6 +133,30 @@ class CollectExternVisitor : AstVisitor {
146133
}
147134
registered_modules["strings"] = true
148135
}
136+
}
137+
138+
def CollectExternVisitor(standalone_context : LLVMOpaqueValue?; _ctx : LLVMContextRef;
139+
_builder : LLVMBuilderRef,
140+
_mod : LLVMOpaqueModule?; var _types : PrimitiveTypes?,
141+
uids : UidNodes?; register_all : bool; aot_obj : bool = false) {
142+
any_unimplemented = false
143+
needs_whole_lib = false
144+
aot_object = aot_obj
145+
uid := uids
146+
ctx = _ctx
147+
standalone_ctx = standalone_context
148+
mod = _mod
149+
types = _types
150+
builder = _builder
151+
register_mod_type = LLVMFunctionType(types.LLVMVoidPtrType(), array<LLVMTypeRef>())
152+
153+
if (aot_object) {
154+
// Glob re-resolution goes into the caller-provided ctor (builder);
155+
// no initialize_modules(), no env, no module registration.
156+
init_builder = _builder
157+
} else {
158+
build_initialize_modules(register_all)
159+
}
149160

150161
// void * get_jit_table_{at,find,erase} ( int32_t baseType, Context * context, LineInfoArg * at )
151162
reg_extern_tab_type = LLVMFunctionType(types.LLVMVoidPtrType(),
@@ -225,6 +236,21 @@ class CollectExternVisitor : AstVisitor {
225236
}
226237

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

268294
def ensure_module(m : Module?) {
295+
if (aot_object) return // host already has every module registered
269296
if (m == null) return
270297
let mod_name = string(m.name)
271298
// Dynamic (.shared_module) modules are normally registered via the
@@ -576,6 +603,64 @@ class CollectExternVisitor : AstVisitor {
576603
}
577604
}
578605

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

0 commit comments

Comments
 (0)