Skip to content

Commit a2c9db1

Browse files
committed
LLVM-AOT: self-registering native object mode (--aot-object)
Emit a native .o over the whole used function set with a load ctor that registers each function into the AOT library via das_aot_register, for static linking into a host binary. Program::linkCppAot binds each as a SimNode_Jit and hard-errors on any miss -- same contract as C++ AOT (whole-program emission makes hard-error viable). Wire the run_tests_llvm_aot rail over tests/: whole-program per-test corpus (TEST_AOT_ALL_DAS + jit_tests), msl/metal lattice suites excluded (the LLVM JIT declines fp16/lattice arithmetic, so those interpret and emit no object). Rename DAS_JIT_AOT_LIB -> DAS_LLVM_AOT_LIB; ignore _llvm_aot_generated/.
1 parent 92c5daf commit a2c9db1

18 files changed

Lines changed: 531 additions & 96 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ lib/
4040
modules/**/*.shared_module
4141
examples/**/*.shared_module
4242
_aot_generated/
43+
_llvm_aot_generated/
4344
.vscode/
4445
.cache/
4546

CMakeLists.txt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,60 @@ 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_LLVM_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_llvm_aot_codegen CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/modules/dasLLVM/daslib/*.das)
286+
set(all_outputs "")
287+
# Emit in batches of 32 files per daslang process: the ~52-file dasLLVM compile-time load
288+
# is paid once per batch, not per file (the way run_tests_jit's --batch amortizes it). Any
289+
# file in a batch changing re-emits the whole batch; ninja still runs batches in parallel.
290+
# MACRO args are text substitutions, not variables -- copy to a real var for list(LENGTH).
291+
set(_llvm_aot_inputs ${input_files})
292+
list(LENGTH _llvm_aot_inputs _llvm_aot_total)
293+
set(_llvm_aot_idx 0)
294+
set(_batch_rels "")
295+
set(_batch_srcs "")
296+
set(_batch_objs "")
297+
foreach(input ${_llvm_aot_inputs})
298+
math(EXPR _llvm_aot_idx "${_llvm_aot_idx} + 1")
299+
get_filename_component(input_src ${input} ABSOLUTE)
300+
get_filename_component(input_dir ${input_src} DIRECTORY)
301+
get_filename_component(input_name ${input} NAME)
302+
set(out_dir ${input_dir}/_llvm_aot_generated)
303+
set(out_obj ${out_dir}/${mainTarget}_${input_name}.o)
304+
# The compile-time path is baked into hashed constants (same reason as DAS_AOT_EXT),
305+
# so spell the input source-root-relative to keep the AOT hash in sync with -use-aot.
306+
file(RELATIVE_PATH input_rel ${PROJECT_SOURCE_DIR} ${input_src})
307+
file(MAKE_DIRECTORY ${out_dir})
308+
list(APPEND all_outputs ${out_obj})
309+
list(APPEND ${genList} ${out_obj})
310+
set_source_files_properties(${out_obj} PROPERTIES GENERATED TRUE EXTERNAL_OBJECT TRUE)
311+
list(APPEND _batch_rels ${input_rel})
312+
list(APPEND _batch_srcs ${input_src})
313+
list(APPEND _batch_objs ${out_obj})
314+
list(LENGTH _batch_rels _batch_n)
315+
if(_batch_n EQUAL 32 OR _llvm_aot_idx EQUAL _llvm_aot_total)
316+
ADD_CUSTOM_COMMAND(
317+
OUTPUT ${_batch_objs}
318+
DEPENDS daslang ${_batch_srcs} ${PROJECT_SOURCE_DIR}/utils/jit/main.das ${_das_llvm_aot_codegen}
319+
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
320+
COMMENT "LLVM-AOT compiling ${_batch_n} files"
321+
COMMAND daslang ${PROJECT_SOURCE_DIR}/utils/jit/main.das -- ${_batch_rels} --aot-object --aot-object-prefix ${mainTarget}
322+
)
323+
set(_batch_rels "")
324+
set(_batch_srcs "")
325+
set(_batch_objs "")
326+
endif()
327+
endforeach()
328+
set(custom_name ${mainTarget}_genllvmaot)
329+
ADD_CUSTOM_TARGET(${custom_name} DEPENDS ${all_outputs})
330+
ADD_DEPENDENCIES(${mainTarget} ${custom_name})
331+
ENDMACRO()
332+
279333
SET(UNITZE_BUILD_BATCH_SIZE 10)
280334

281335
MACRO(UNITIZE_BUILD input_dir genList)

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/ast/ast.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,6 +1649,7 @@ namespace das
16491649
bool jit_debug_info = false; // Add debug info to generate binary code
16501650
bool jit_dll_mode = true; // Create if missing and reuse DLL or JIT compile
16511651
bool jit_exe_mode = false; // Create executable
1652+
bool jit_emit_object = false; // Offline AOT: emit a .o (this module only) + a load ctor registering its functions into the AOT library, for static linking into a host
16521653
bool jit_emit_prologue = false; // Emit prologue for all functions and blocks
16531654
string jit_output_path; // Folder to store compiled dll's. By default it'll be _das_root_/.jitted_scripts
16541655
int32_t jit_opt_level = 3u; // Opt level for LLVM to codegen and IR optimizations

include/daScript/simulate/aot_library.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ namespace das {
88
bool is_cmres;
99
void * fn;
1010
vec4f (*wrappedFn)(Context*);
11+
bool is_jit = false;
1112
AotFactory(bool is_cmres, void *fn, vec4f(*wrappedFn)(Context*))
1213
: is_cmres(is_cmres), fn(fn), wrappedFn(wrappedFn) {}
14+
explicit AotFactory(void *jitFn)
15+
: is_cmres(false), fn(jitFn), wrappedFn(nullptr), is_jit(true) {}
1316

1417
SimNode * operator ()(Context &ctx) const;
1518
};
@@ -29,6 +32,11 @@ namespace das {
2932
DAS_API AotLibrary & getGlobalAotLibrary();
3033
DAS_API void clearGlobalAotLibrary();
3134

35+
// makeAotJitNode builds a SimNode_Jit (defined in module_jit.cpp, where it is visible);
36+
SimNode * makeAotJitNode ( Context & ctx, void * publ );
37+
// runLlvmAotGlobInits runs each linked LLVM-AOT object's glob re-resolution callback.
38+
void runLlvmAotGlobInits ( Context & ctx );
39+
3240
// Test standalone context
3341

3442
typedef Context * ( * RegisterTestCreator ) ();

modules/dasLLVM/.das_module

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def initialize(project_path : string) {
77
let daslib_paths = [
88
"llvm_boost", "llvm_debug", "llvm_jit", "llvm_targets",
99
"llvm_jit_intrin", "llvm_jit_common", "llvm_dll_utils",
10-
"llvm_exe", "llvm_macro", "llvm_jit_cli", "llvm_jit_run",
10+
"llvm_exe", "llvm_macro", "llvm_jit_cli", "llvm_jit_run", "llvm_aot",
1111
"aarch64_neon", // public NEON-intrinsic header (portable fallbacks + name-based JIT recognition)
1212
"x64_avx", // public x86-64-intrinsic header (same contract, x64 mirror)
1313
"f16_cvt", // public f16<->f32 convert header (same contract, aarch64 + x64/F16C)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
options gen2
2+
options relaxed_pointer_const // fixed_array<LLVMTypeRef> with const PrimitiveTypes fields needs it
3+
4+
module llvm_aot shared private
5+
6+
require llvm/daslib/llvm_boost
7+
require llvm/daslib/llvm_jit
8+
require llvm/daslib/llvm_jit_common
9+
require llvm/daslib/llvm_exe
10+
require llvm/daslib/llvm_dll_utils
11+
require daslib/ast_boost
12+
require daslib/rtti
13+
14+
//! LLVM-AOT registration pass. After the JIT visitor has generated + optimized the module (as for a
15+
//! normal exe), this adds the offline-AOT-object registration layer so a statically-linked .o binds
16+
//! through Program::linkCppAot: a self-registering das_aot_register ctor plus the builtin/Func-value
17+
//! glob re-resolution ctor (built by llvm_exe's extern resolver). All the AOT-object-only "register
18+
//! it into the host" logic lives here; run_jit calls build_llvm_aot_ctors once, in emit_aot_object mode.
19+
20+
// The das_aot_register ctor: for each emitted function, emit das_aot_register(aotHash, publ) so
21+
// the linked .o self-registers into the AOT library at load; also wire the glob re-resolution callback.
22+
def private emit_aot_register_ctor_dtor(types : PrimitiveTypes?; funcs : array<FunctionPtr>; var uids : UidNodes?; _prog : Program?; glob_ctor : LLVMOpaqueValue?) : tuple<LLVMOpaqueValue?, LLVMOpaqueValue?> {
23+
let regType = LLVMFunctionType(types.t_void,
24+
fixed_array<LLVMTypeRef>(types.t_int64, types.LLVMVoidPtrType()))
25+
var regFn = LLVMGetNamedFunction(g_mod, "das_aot_register")
26+
if (regFn == null) {
27+
regFn = LLVMAddFunctionWithType(g_mod, "das_aot_register", regType) // linker binds it to daScript core
28+
}
29+
let functionType = LLVMFunctionType(types.t_void, null, 0u, 0)
30+
// Private: each object defines it, so public linkage would clash when many link together.
31+
let reg_ctor = LLVMAddFunctionWithType(g_mod, "das_aot_register_constructor", functionType)
32+
let reg_dtor = LLVMAddFunctionWithType(g_mod, "das_aot_register_destructor", functionType)
33+
set_private_linkage(reg_ctor)
34+
set_private_linkage(reg_dtor)
35+
var ctor_builder = LLVMCreateBuilderInContext(g_ctx)
36+
var dtor_builder = LLVMCreateBuilderInContext(g_ctx)
37+
LLVMPositionBuilderAtEnd(ctor_builder, LLVMAppendBasicBlockInContext(g_ctx, reg_ctor, "entry"))
38+
LLVMPositionBuilderAtEnd(dtor_builder, LLVMAppendBasicBlockInContext(g_ctx, reg_dtor, "entry"))
39+
for (fun in funcs) {
40+
var publ = LLVMGetNamedFunction(g_mod, uids->get_dll_fn_name(fun).publ())
41+
if (publ == null) {
42+
continue
43+
}
44+
let aot_hash = get_function_aot_hash(fun)
45+
var publ_ptr = LLVMBuildPointerCast(ctor_builder, publ, types.LLVMVoidPtrType(), "")
46+
var reg_args = array<LLVMValueRef>(types.ConstI64(aot_hash), publ_ptr)
47+
LLVMBuildCall2(ctor_builder, regType, regFn, reg_args, "")
48+
}
49+
// Record the glob re-resolution callback; the runtime runs it lazily at the first AotLibrary drain (not static-init).
50+
if (glob_ctor != null) {
51+
let giType = LLVMFunctionType(types.t_void, fixed_array<LLVMTypeRef>(types.LLVMVoidPtrType()))
52+
var giFn = LLVMGetNamedFunction(g_mod, "das_aot_register_globinit")
53+
if (giFn == null) {
54+
giFn = LLVMAddFunctionWithType(g_mod, "das_aot_register_globinit", giType)
55+
}
56+
var gi_ptr = LLVMBuildPointerCast(ctor_builder, glob_ctor, types.LLVMVoidPtrType(), "")
57+
var gi_args = array<LLVMValueRef>(gi_ptr)
58+
LLVMBuildCall2(ctor_builder, giType, giFn, gi_args, "")
59+
}
60+
LLVMBuildRetVoid(ctor_builder)
61+
LLVMBuildRetVoid(dtor_builder)
62+
LLVMDisposeBuilder(ctor_builder)
63+
LLVMDisposeBuilder(dtor_builder)
64+
return (reg_ctor, reg_dtor)
65+
}
66+
67+
68+
// Build a load ctor that re-resolves this AOT object's builtin-accessor globals to the HOST's real
69+
// helpers (offline objects leave them null+internal since codegen-process addresses are meaningless
70+
// there) and points ExprAddr globals at their publ symbols. No module/function registration.
71+
def public emit_aot_object_globinit(ctx : LLVMContextRef; mod : LLVMOpaqueModule?;
72+
var types : PrimitiveTypes?; var funcs : array<FunctionPtr>;
73+
var uids : UidNodes?) : LLVMOpaqueValue? {
74+
// void das_aot_object_globinit_constructor(Context* ctx): ctx is used to
75+
// resolve Func-value globs to the host's SimFunction* (das_aot_get_func_by_mnh).
76+
let fnType = LLVMFunctionType(types.t_void, fixed_array<LLVMTypeRef>(types.LLVMVoidPtrType()))
77+
let ctor = LLVMAddFunctionWithType(g_mod, "das_aot_object_globinit_constructor", fnType)
78+
LLVMSetLinkage(ctor, LLVMLinkage.LLVMPrivateLinkage)
79+
var b = LLVMCreateBuilderInContext(ctx)
80+
LLVMPositionBuilderAtEnd(b, LLVMAppendBasicBlockInContext(ctx, ctor, "entry"))
81+
// Aliases the caller's uids; do NOT delete it (deletion cascades into that shared uid, which the
82+
// caller still uses). It's a gc_node, reclaimed by the surrounding ast_gc_guard / process teardown.
83+
var extern_resolver = new CollectExternVisitor(null, ctx, b, mod, types, uids, false, LlvmJitMode.LLVM_AOT)
84+
extern_resolver.aot_ctx_arg = LLVMGetParam(ctor, 0u)
85+
make_visitor(*extern_resolver) $(adapter_resolve) {
86+
ast_gc_guard() {
87+
for (fn in funcs) {
88+
visit(fn, adapter_resolve)
89+
}
90+
}
91+
}
92+
// Fill the cross-module call/ctor target globs recorded during codegen with the
93+
// host Context's SimFunction* for each callee (by mangled-name hash).
94+
if (!(g_aot_func_globs |> empty())) {
95+
let ctx_arg = LLVMGetParam(ctor, 0u)
96+
for (fg in g_aot_func_globs) {
97+
if (fg.glob == null) {
98+
continue
99+
}
100+
var simfn = build_aot_get_func_by_mnh(b, types, fg.mnh, ctx_arg)
101+
// The glob is a pointer-to-fn-type; the SimFunction* (void*) bitcasts in.
102+
let elemT = LLVMGlobalGetValueType(fg.glob)
103+
var casted = LLVMBuildPointerCast(b, simfn, elemT, "")
104+
LLVMBuildStore(b, casted, fg.glob)
105+
}
106+
}
107+
LLVMBuildRetVoid(b)
108+
LLVMDisposeBuilder(b)
109+
return ctor
110+
}
111+
112+
113+
//! Build both offline-AOT-object ctors (the extern-resolver globinit + the das_aot_register ctor) and
114+
//! return the (ctor, dtor) pair to append to llvm.global_ctors/dtors. Called by run_jit in emit_aot_object mode.
115+
def public build_llvm_aot_ctors(ctx : LLVMContextRef; mod : LLVMOpaqueModule?; var types : PrimitiveTypes?; var funcs : array<FunctionPtr>; var uids : UidNodes?; prog : Program?) : tuple<LLVMOpaqueValue?, LLVMOpaqueValue?> {
116+
let glob_ctor = emit_aot_object_globinit(ctx, mod, types, funcs, uids)
117+
return emit_aot_register_ctor_dtor(types, funcs, uids, prog, glob_ctor)
118+
}

0 commit comments

Comments
 (0)