From 64a4d0c433ad78d55e0b8f76c37b7d7eee5bb2fd Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Fri, 11 Sep 2026 14:51:27 +0300 Subject: [PATCH 1/3] jit: the runtime entry points JIT code calls move to their own file src/builtin/jit_runtime.cpp is not a module - it is the set of extern "C" symbols jitted and LLVM-AOT-emitted code links against, plus the address takers the jit module binds. module_jit.cpp keeps the das module: bindings, linker invocation and emit orchestration. --- CMakeLists.txt | 1 + include/daScript/simulate/aot_builtin_jit.h | 36 - include/daScript/simulate/aot_library.h | 2 +- src/builtin/jit_runtime.cpp | 1280 +++++++++++++++++++ src/builtin/jit_runtime.h | 77 ++ src/builtin/module_builtin_fio.cpp | 2 +- src/builtin/module_jit.cpp | 1234 +----------------- tests-cpp/small/test_jit_module_resolve.cpp | 2 +- 8 files changed, 1362 insertions(+), 1272 deletions(-) create mode 100644 src/builtin/jit_runtime.cpp create mode 100644 src/builtin/jit_runtime.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 54e213c677..55e991d21e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1000,6 +1000,7 @@ src/builtin/module_builtin_rtti.cpp src/builtin/module_builtin_ast.h src/builtin/module_builtin_uriparser.h src/builtin/module_builtin_uriparser.cpp +src/builtin/jit_runtime.cpp src/builtin/module_jit.cpp src/builtin/module_builtin_fio.cpp src/builtin/module_builtin_dasbind.cpp diff --git a/include/daScript/simulate/aot_builtin_jit.h b/include/daScript/simulate/aot_builtin_jit.h index 699c649bde..f570cd43af 100644 --- a/include/daScript/simulate/aot_builtin_jit.h +++ b/include/daScript/simulate/aot_builtin_jit.h @@ -23,41 +23,5 @@ namespace das { bool das_remove_jit ( const Func func ); bool das_instrument_jit ( void * pfun, const Func func, const LineInfo & info, Context & context ); void * das_instrument_line_info ( const LineInfo & info, Context * context, LineInfoArg * at ); - void * das_get_jit_exception (); - void * das_get_jit_call_or_fastcall (); - void * das_get_jit_call_with_cmres ( ); - void * das_get_jit_invoke_block ( ); - void * das_get_jit_invoke_block_with_cmres ( ); - void * das_get_jit_string_builder (); - void * das_get_jit_string_builder_temp (); - void * das_get_jit_get_global_mnh (); - void * das_get_jit_alloc_heap (); - void * das_get_jit_alloc_persistent (); - void * das_get_jit_free_heap (); - void * das_get_jit_free_persistent (); - void * das_get_jit_array_lock (); - void * das_get_jit_array_unlock (); - void * das_get_jit_table_lock (); - void * das_get_jit_table_unlock (); - void * das_get_jit_array_resize (); - void * das_get_jit_table_at ( int32_t baseType, Context * context, LineInfoArg * at ); - void * das_get_jit_table_erase ( int32_t baseType, Context * context, LineInfoArg * at ); - void * das_get_jit_table_find ( int32_t baseType, Context * context, LineInfoArg * at ); - void * das_get_jit_str_cmp (); - void * das_get_jit_str_cat (); - void * das_get_jit_ast_typedecl (); - void * das_get_jit_prologue (); - void * das_get_jit_epilogue (); - void * das_get_jit_make_block (); - void * das_get_jit_ad_by_sid (); - void * das_get_jit_debug (); - void * das_get_jit_iterator_iterate(); - void * das_get_jit_iterator_delete(); - void * das_get_jit_iterator_close(); - void * das_get_jit_iterator_first(); - void * das_get_jit_iterator_next(); void * das_get_builtin_function_address ( Function * fn, Context * context, LineInfoArg * at ); - void * das_get_jit_debug_enter (); - void * das_get_jit_debug_exit (); - void * das_get_jit_debug_line (); } diff --git a/include/daScript/simulate/aot_library.h b/include/daScript/simulate/aot_library.h index 2f6b4dd239..ee75463a57 100644 --- a/include/daScript/simulate/aot_library.h +++ b/include/daScript/simulate/aot_library.h @@ -32,7 +32,7 @@ namespace das { DAS_API AotLibrary & getGlobalAotLibrary(); DAS_API void clearGlobalAotLibrary(); - // makeAotJitNode builds a SimNode_Jit (defined in module_jit.cpp, where it is visible); + // makeAotJitNode builds a SimNode_Jit (defined in jit_runtime.cpp, where it is visible); SimNode * makeAotJitNode ( Context & ctx, void * publ ); } diff --git a/src/builtin/jit_runtime.cpp b/src/builtin/jit_runtime.cpp new file mode 100644 index 0000000000..1f131cc534 --- /dev/null +++ b/src/builtin/jit_runtime.cpp @@ -0,0 +1,1280 @@ +#include "daScript/daScriptModule.h" +#include "daScript/misc/platform.h" + +#include "daScript/misc/sysos.h" + +#ifdef DAS_ENABLE_DYN_INCLUDES +#include "daScript/ast/dyn_modules.h" +#endif + +#include "daScript/ast/ast.h" +#include "daScript/ast/ast_handle.h" + +#include "daScript/simulate/aot.h" +#include "daScript/simulate/aot_builtin_jit.h" +#include "daScript/simulate/aot_builtin.h" +#include "daScript/simulate/aot_builtin_rtti.h" +#include "daScript/simulate/aot_library.h" +#include "daScript/simulate/debug_info.h" +#include "daScript/simulate/debug_print.h" +#include "daScript/simulate/hash.h" +#include "daScript/simulate/heap.h" +#include "daScript/simulate/simulate.h" +#include "daScript/simulate/simulate_visit_op.h" + +#include "daScript/misc/fpe.h" +#include "misc/include_fmt.h" +#include "jit_runtime.h" + +#include +#include +#include +#include +#include + +#if !DAS_NO_FILEIO +#include +#endif + +namespace das { + // forward declarations from module_builtin_fio.cpp + void * register_dynamic_module(const char *, const char *, int, Context *, LineInfoArg *); + void register_native_path(const char *, const char *, const char *, Context *, LineInfoArg *); + bool builtin_fexist ( const char * path ); + DAS_API void retry_pending_dynamic_modules(); + DAS_API int report_pending_dynamic_modules(); + + // JitFunction typedef lives in arraytype.h (the SimFunction::jitFunction mirror shares it) + + struct SimNode_Jit : SimNode { + SimNode_Jit ( const LineInfo & at, JitFunction eval ) + : SimNode(at), func(eval) {} + virtual SimNode * visit ( SimVisitor & vis ) override; + DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; + virtual bool rtti_node_isJit() const override { return true; } + JitFunction func = nullptr; + // saved original node + SimNode * saved_code = nullptr; + bool saved_aot = false; + void * saved_aot_function = nullptr; + }; + + static void runLlvmAotGlobInitOf ( Context & ctx, void * publ ); + + SimNode * makeAotJitNode ( Context & ctx, void * publ ) { + runLlvmAotGlobInitOf(ctx, publ); + return ctx.code->makeNode(LineInfo(), (JitFunction)publ); + } + + extern "C" void * das_aot_get_func_by_mnh ( uint64_t mnh, Context * ctx ) { + return builtin_getFunctionByMnh(mnh, ctx).PTR; + } + + static vector> & llvmAotEntries() { + static vector> entries; + return entries; + } + + static vector & llvmAotGlobInits() { + static vector inits; + return inits; + } + + // publ -> index into llvmAotGlobInits, so linking one function can init the object it came from. + static das_hash_map & llvmAotGlobInitOfPubl() { + static das_hash_map ofPubl; + return ofPubl; + } + + // An object registers its functions first, then its glob-init; the functions wait here until + // the glob-init arrives and claims them. + static vector & llvmAotPendingPubl() { + static vector pending; + return pending; + } + + // Runs an object's glob-init once: the slot is cleared as it is consumed. + static void runLlvmAotGlobInitOf ( Context & ctx, void * publ ) { + auto it = llvmAotGlobInitOfPubl().find(publ); + if ( it==llvmAotGlobInitOfPubl().end() ) return; + auto & fn = llvmAotGlobInits()[it->second]; + if ( !fn ) return; + auto todo = fn; + fn = nullptr; + todo(&ctx); + } + static void registerLlvmAotFunctions ( AotLibrary & lib ) { + for ( auto & e : llvmAotEntries() ) { + lib.emplace(e.first, AotFactory(e.second)); + } + } + static AotListBase g_llvmAotList(registerLlvmAotFunctions); + + extern "C" void das_aot_register ( uint64_t aotHash, void * publ ) { + llvmAotEntries().emplace_back(aotHash, publ); + llvmAotPendingPubl().push_back(publ); + } + + extern "C" void das_aot_register_globinit ( void (*fn)(Context*) ) { + uint32_t index = uint32_t(llvmAotGlobInits().size()); + llvmAotGlobInits().push_back(fn); + for ( auto publ : llvmAotPendingPubl() ) { + llvmAotGlobInitOfPubl()[publ] = index; + } + llvmAotPendingPubl().clear(); + } + + struct SimNode_JitBlock; + + struct JitBlock : Block { + vec4f node[10]; + }; + + struct SimNode_JitBlock : SimNode_ClosureBlock { + SimNode_JitBlock ( const LineInfo & at, JitBlockFunction eval, Block * bptr, uint64_t ad ) + : SimNode_ClosureBlock(at,false,false,ad), func(eval), blockPtr(bptr) {} + virtual SimNode * visit ( SimVisitor & vis ) override; + DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; + JitBlockFunction func = nullptr; + Block * blockPtr = nullptr; + }; + static_assert(sizeof(SimNode_JitBlock)<=sizeof(JitBlock().node),"jit block node must fit under node size"); + + + DAS_SUPPRESS_UB vec4f SimNode_Jit::eval ( Context & context ) { + auto result = func(&context, context.abiArg, context.abiCMRES); + context.result = result; + return result; + } + + SimNode * SimNode_JitBlock::visit ( SimVisitor & vis ) { + uint64_t fptr = (uint64_t) func; + V_BEGIN(); + V_OP(JitBlock); + V_ARG(fptr); + V_END(); + } + + DAS_SUPPRESS_UB vec4f SimNode_JitBlock::eval ( Context & context ) { + auto ba = (BlockArguments *) ( context.stack.bottom() + blockPtr->argumentsOffset ); + return func(&context, ba->arguments, ba->copyOrMoveResult, blockPtr ); + } + + SimNode * SimNode_Jit::visit ( SimVisitor & vis ) { + uint64_t fptr = (uint64_t) func; + V_BEGIN(); + V_OP(Jit); + V_ARG(fptr); + V_END(); + } + + float4 das_invoke_code ( void * pfun, vec4f anything, void * cmres, Context * context ) { + vec4f * arguments = cast::to(anything); + vec4f (*fun)(Context *, vec4f *, void *) = (vec4f(*)(Context *, vec4f *, void *)) pfun; + vec4f res = fun ( context, arguments, cmres ); + return res; + } + + bool das_remove_jit ( const Func func ) { + auto simfn = func.PTR; + if ( !simfn ) return false; + if ( simfn->code && simfn->code->rtti_node_isJit() ) { + auto jitNode = static_cast(simfn->code); + simfn->code = jitNode->saved_code; + simfn->aot = jitNode->saved_aot; + simfn->aotFunction = jitNode->saved_aot_function; + simfn->jitFunction = nullptr; // pre-instrument mirror is always null (aot/jit exclusive) + simfn->jit = false; + return true; + } else { + return false; + } + } + + bool das_has_jit_fastpath ( const Func func ) { + // test rail: is the invoke fastpath armed for this function (SimFunction::jitFunction mirror set)? + auto simfn = func.PTR; + return simfn && simfn->jitFunction; + } + + bool das_instrument_jit ( void * pfun, const Func func, const LineInfo & lineInfo, Context & context ) { + + auto simfn = func.PTR; + if ( !simfn ) return false; + if ( simfn->code && simfn->code->rtti_node_isJit() ) { + auto jitNode = static_cast(simfn->code); + jitNode->func = (JitFunction) pfun; + jitNode->debugInfo = lineInfo; + simfn->jitFunction = pfun; + } else { + auto node = context.code->makeNode(lineInfo, (JitFunction)pfun); + node->saved_code = simfn->code; + node->saved_aot = simfn->aot; + node->saved_aot_function = simfn->aotFunction; + simfn->code = node; + simfn->aot = false; + simfn->aotFunction = nullptr; + simfn->jitFunction = pfun; // the invoke-fastpath mirror of node->func + simfn->jit = true; + } + return true; + } + +extern "C" { + DAS_API void jit_exception ( const char * text, Context * context, LineInfoArg * at ) { + context->throw_error_at(at, "%s", text ? text : ""); + } + + DAS_API vec4f jit_call_or_fastcall ( SimFunction * fn, vec4f * args, Context * context ) { + if ( !fn ) context->throw_error("jit_call_or_fastcall: null function (unresolved LLVM-AOT call target)"); + return context->callOrFastcall(fn, args, nullptr); + } + + DAS_API vec4f jit_invoke_block ( const Block & blk, vec4f * args, Context * context ) { + return context->invoke(blk, args, nullptr, nullptr); + } + + DAS_API vec4f jit_invoke_block_with_cmres ( const Block & blk, vec4f * args, void * cmres, Context * context ) { + return context->invoke(blk, args, cmres, nullptr); + } + + DAS_API vec4f jit_call_with_cmres ( SimFunction * fn, vec4f * args, void * cmres, Context * context ) { + if ( !fn ) context->throw_error("jit_call_with_cmres: null function (unresolved LLVM-AOT call target)"); + return context->callWithCopyOnReturn(fn, args, cmres, nullptr); + } + + DAS_API char * jit_string_builder ( Context & context, int32_t nArgs, TypeInfo ** types, LineInfoArg * at, vec4f * args ) { + StringBuilderWriter writer; + DebugDataWalker walker(writer, PrintFlags::string_builder); + for ( int i = 0; i < nArgs; ++i ) + walker.walk(args[i], types[i]); + auto length = writer.tellp(); + if ( length ) { + return context.allocateString(writer.c_str(), uint32_t(length), at); + } else { + return nullptr; + } + } + + DAS_API char * jit_string_builder_temp ( Context & context, int32_t nArgs, TypeInfo ** types, LineInfoArg * at, vec4f * args ) { + StringBuilderWriter writer; + DebugDataWalker walker(writer, PrintFlags::string_builder); + for ( int i = 0; i < nArgs; ++i ) + walker.walk(args[i], types[i]); + auto length = writer.tellp(); + if ( length ) { + auto str = context.allocateTempString(writer.c_str(), uint32_t(length), at); + context.freeTempString(str, at); + return str; + } else { + return nullptr; + } + } + + DAS_API void jit_simnode_interop(void *ptr, int argCount, TypeInfo **types) { + auto res = new(ptr) SimNode_AotInteropBase(); + res->argumentValues = nullptr; + res->nArguments = argCount; + res->types = types; + } + + DAS_API void jit_free_simnode_interop(SimNode_AotInteropBase *ptr) { + ptr->~SimNode_AotInteropBase(); + } + + DAS_API void *das_get_jit_simnode_interop() { + return (void *) &jit_simnode_interop; + } + + DAS_API void *das_get_jit_free_simnode_interop() { + return (void *) &jit_free_simnode_interop; + } + + // We need it to bypass protected/private. + class JitContext : public Context { + public: + ~JitContext() = default; + JitContext(size_t totalVariables, size_t totalFunctions, size_t globalStringHeapSize, + size_t globSize, size_t shrSize, bool pinvoke, uint32_t stackSize = 16*1024, + bool persistentHeap = false, bool gcEnabled = false) + : Context(stackSize) { + auto &context = *this; + CodeOfPolicies policies; + policies.debugger = false; + // standalone exe skips Program::simulate (which would read `options + // persistent_heap` / `options gc`), so set the heap mode here. Without a + // persistent heap + gcEnabled, heap_collect() throws at runtime. + policies.persistent_heap = persistentHeap; + context.setup(totalVariables, globalStringHeapSize, policies, {}); + context.gcEnabled = gcEnabled; + context.globalsSize = globSize; + context.sharedSize = shrSize; + context.sharedOwner = true; + for (size_t i = 0; i < totalVariables; i++) { + globalVariables[i] = GlobalVariable{}; + } + context.allocateGlobalsAndShared(); + // UBSAN forbid memset(null, 0, 0) + if (context.globalsSize) memset(context.globals, 0, context.globalsSize); + if (context.sharedSize) memset(context.shared, 0, context.sharedSize); + if ( pinvoke ) { + context.contextMutex = new recursive_mutex; + } + } + + void allocFunctions ( uint64_t count ) { + functions = (SimFunction *) code->allocate(count * sizeof(SimFunction)); + memset(functions, 0, count * sizeof(SimFunction)); + totalFunctions = (int) count; + // Allocate stub debugInfo for all function slots so that + // runShutdownScript can safely iterate them. + auto stubInfo = (FuncInfo *) code->allocate(count * sizeof(FuncInfo)); + memset(stubInfo, 0, count * sizeof(FuncInfo)); + for ( uint64_t i = 0; i < count; i++ ) { + stubInfo[i].name = (char *) "unimplemented"; + functions[i].name = (char *) "unimplemented"; + functions[i].debugInfo = &stubInfo[i]; + } + functionLookup = make_shared(); + variableLookup = make_shared(); + } + + void *registerJitFunction ( uint64_t index, const char * funcName, const char * mangledName, + uint64_t mnh, uint32_t stackSize, void * fnPtr, + bool cmres, bool fastcall, bool pinvoke, uint32_t nArguments ) { + DAS_ASSERT(index < (uint64_t) totalFunctions); + auto & fn = functions[index]; + fn.name = code->allocateName(funcName); + fn.mangledName = code->allocateName(mangledName); + fn.mangledNameHash = mnh; + fn.stackSize = stackSize; + fn.flags = 0; + fn.cmres = cmres; + fn.fastcall = fastcall; + fn.pinvoke = pinvoke; + fn.jit = true; + auto finfo = (FuncInfo *) code->allocate(sizeof(FuncInfo)); + memset(finfo, 0, sizeof(FuncInfo)); + finfo->name = fn.name; + finfo->stackSize = stackSize; + finfo->count = nArguments; + fn.debugInfo = finfo; + auto node = code->makeNode(LineInfo{}, (JitFunction) fnPtr); + fn.code = node; + fn.jitFunction = fnPtr; // the invoke-fastpath mirror + return &fn; + } + + void registerJitGlobalVariable(uint64_t index, const char * name, uint64_t mnh, size_t offset, bool shared) { + DAS_ASSERT(index < (uint64_t) totalVariables); + auto & gv = globalVariables[index]; + gv.name = code->allocateName(name); + gv.mangledNameHash = mnh; + gv.offset = (uint32_t) offset; + gv.flags = shared ? 1u : 0u; + } + + // the exe carries both lookups as constant data the emitter sealed; nothing is built or owned here + void adoptLookups(const NameLookup::StaticTable * functions, const NameLookup::StaticTable * variables) { + functionLookup->adopt(*functions); + variableLookup->adopt(*variables); + } + + // registerJitGlobalVariable fills name, hash, offset and the shared flag; the + // debugInfo is the exe-resident TypeInfo emitted by create_type_info_global, which + // only the JIT'd init function can wire. collectHeap walks globalVariables[i] via + // .offset/.debugInfo/.shared, so a NULL debugInfo is a GC crash. + void setStandaloneGlobalInfo(uint64_t index, uint64_t offset, void* debugInfo, int shared) { + DAS_ASSERT(index < (uint64_t) totalVariables); + auto & gv = globalVariables[index]; + gv.offset = (uint32_t) offset; + gv.debugInfo = (VarInfo *) debugInfo; + gv.flags = shared ? 1u : 0u; + } + + void initFunctionAddr ( uint64_t index, void * globPtr ) { + DAS_ASSERT(index < (uint64_t) totalFunctions); + *((SimFunction **) globPtr) = &functions[index]; + } + }; + + // Note: this function called in runtime, before main. + // When we built executable from das. + DAS_API Context * jit_create_standalone_ctx ( uint64_t totalVariables, + uint64_t totalFunctions, + uint64_t globalStringHeapSize, + uint64_t globalsSize, + uint64_t sharedSize, + bool pinvoke, + uint64_t stackSize) { + Context *context = new JitContext(totalVariables, totalFunctions, globalStringHeapSize, + globalsSize, sharedSize, pinvoke, + stackSize ? (uint32_t)stackSize : 16*1024, + /*persistentHeap*/ true, /*gcEnabled*/ true); + static_cast(context)->allocFunctions(totalFunctions); + return context; + } + + DAS_API void *jit_register_standalone_function ( Context * ctx, uint64_t index, + const char * name, const char * mangledName, + uint64_t mnh, uint32_t stackSize, + void * fnPtr, + bool cmres, bool fastcall, bool pinvoke, + uint32_t nArguments ) { + return static_cast(ctx)->registerJitFunction(index, name, mangledName, mnh, stackSize, + fnPtr, cmres, fastcall, pinvoke, nArguments); + } + + DAS_API void jit_register_standalone_variable ( Context * ctx, uint64_t index, const char * name, uint64_t mangledNameHash, uint64_t offset, int shared ) { + static_cast(ctx)->registerJitGlobalVariable(index, name, mangledNameHash, offset, shared != 0); + } + + DAS_API void jit_adopt_standalone_lookups ( Context * ctx, const void * functions, const void * variables ) { + static_cast(ctx)->adoptLookups((const NameLookup::StaticTable *) functions, (const NameLookup::StaticTable *) variables); + } + + // Populate globalVariables[index] so the GC can trace standalone-exe globals. + // Emitted by generate_globals_initialization_fn into the init function (debugInfo + // is the exe-resident TypeInfo, so it can only be wired at codegen time). + DAS_API void jit_set_global_var ( Context * ctx, uint64_t index, uint64_t offset, void* debugInfo, int shared ) { + static_cast(ctx)->setStandaloneGlobalInfo(index, offset, debugInfo, shared); + } + + DAS_API void jit_set_init_script ( Context * ctx, Context::JitInitScriptFn fn ) { + ctx->jitInitScript = fn; + } + + DAS_API void jit_init_function_addr ( Context * ctx, uint64_t index, void * globPtr ) { + static_cast(ctx)->initFunctionAddr(index, globPtr); + } + + // A missing MODULE and a missing FUNCTION die on the same lookup — distinguish them, + // because the former is a load failure (dlopen), not a signature mismatch. + static bool jit_module_is_registered ( const char * moduleName ) { + bool exists = false; + Module::foreach([&](Module * module) -> bool { + if ( module->name != moduleName ) return true; + exists = true; + return false; + }); + return exists; + } + + DAS_API void jit_init_extern_function ( const char * moduleName, + const char * funcMangledName, + void ** dllGlobal ) { + bool found = false; + Module::foreach([&](Module * module) -> bool { + if ( module->name != moduleName ) return true; + auto fn = module->findFunction(funcMangledName); + if ( fn && fn->builtIn ) { + *dllGlobal = static_cast(fn)->getBuiltinAddress(); + found = *dllGlobal != nullptr; + return !found; + } + return true; + }); + if ( !found && strcmp(moduleName, "dasbind") == 0 && strncmp(funcMangledName, "@dasbind::__dasbind__", 21) == 0 ) { + Module::foreach([&](Module * module) -> bool { + if ( module->name != "dasbind" ) return true; + auto resolverFn = module->findUniqueFunction("__dasbind_resolve"); + if ( resolverFn && resolverFn->builtIn ) { + auto resolver = (void * (*)(const char *)) + static_cast(resolverFn)->getBuiltinAddress(); + if ( resolver ) { + *dllGlobal = resolver(funcMangledName); + found = *dllGlobal != nullptr; + } + } + return false; + }); + } + if (!found) { + if ( !jit_module_is_registered(moduleName) ) { + DAS_FATAL_ERROR("Failed to find %s: module %s is not registered (its .shared_module may have failed to load - see errors above).\n", funcMangledName, moduleName); + } + DAS_FATAL_ERROR("Failed to find %s in module %s.\n", funcMangledName, moduleName); + } + } + + DAS_API Annotation *jit_get_annotation ( const char * moduleName, + const char * annName ) { + Annotation *result = nullptr; + Module::foreach([&](Module * module) -> bool { + if ( module->name != moduleName ) return true; + result = module->findAnnotation(annName); + return false; + }); + if (!result) { + if ( !jit_module_is_registered(moduleName) ) { + DAS_FATAL_ERROR("Failed to find annotation %s: module %s is not registered (its .shared_module may have failed to load - see errors above).\n", annName, moduleName); + } + DAS_FATAL_ERROR("Failed to find annotation %s in module %s.\n", annName, moduleName); + } + return result; + } + + DAS_API void jit_trap() { + DAS_FATAL_ERROR("FATAL: Unresolved dynamic function call in compiled code. This indicates a missing JIT symbol. Disable `strict` mode or remove this call.\n"); + } + + DAS_API void jit_set_command_line_arguments( int argc, char * argv[] ) { + setCommandLineArguments(argc, argv); + } + + DAS_API void *das_get_jit_init_extern_function() { + return (void *) &jit_init_extern_function; + } + + DAS_API void * jit_get_global_mnh ( uint64_t mnh, Context & context ) { + return context.globals + context.globalOffsetByMangledName(mnh); + } + + DAS_API void * jit_get_shared_mnh ( uint64_t mnh, Context & context ) { + return context.shared + context.globalOffsetByMangledName(mnh); + } + + // Resolve a handled-type (C++) field offset at runtime. The JIT bakes field + // offsets via the HOST annotation's offsetof, which is wrong when cross-compiling + // to a target whose C++ ABI lays the struct out differently — even at equal pointer + // width (e.g. an MSVC host vs a clang/wasm64 target: vptr + multiple-base ordering + // diverge for polymorphic classes like Context). The runtime archive (built for the + // target) registers each handled type's annotation with the right offsetof, so + // resolve by (module, type, field) here. Called once per offset-global at init, + // after initialize_modules() has registered the modules. + DAS_API uint32_t jit_get_handled_field_offset ( const char * moduleName, + const char * typeName, + const char * fieldName ) { + uint32_t offset = (uint32_t)-1; + Module::foreach([&](Module * module) -> bool { + if ( module->name != moduleName ) return true; + auto ann = module->findAnnotation(typeName); + // handled-type annotations only — StructureAnnotation isn't a TypeAnnotation (see note in + // jit_find_handled_annotation); the JIT only resolves offsets for handled-type fields. + if ( ann && ann->rtti_isHandledTypeAnnotation() ) { + offset = ((TypeAnnotation *)ann)->getFieldOffset(fieldName); + return false; // stop iterating + } + return true; + }); + // Compiler only requests offsets for fields it resolved on the host annotation; a + // miss = target runtime annotation diverges (ABI/module mismatch). Fail loud, not -1. + if ( offset == (uint32_t)-1 ) { + DAS_FATAL_ERROR("jit: unresolved handled-type field offset %s::%s.%s (target runtime annotation diverges from the host).\n", + moduleName, typeName, fieldName); + } + return offset; + } + + // ---- ABI safe-check (opt-in via --jit-check-abi) ------------------------ + // Cross-compiling from an MSVC host to a wasm/clang target can bake a wrong + // handled-type (C++ struct) SIZE or field OFFSET when the two C++ ABIs lay the + // struct out differently (vptr/base ordering, or #ifdef WIN32/EMSCRIPTEN- + // conditional members). The codegen emits, at startup, one check call per used + // handled type (size) and per field (offset) carrying the HOST-baked value; + // each compares against the TARGET runtime annotation and records every + // divergence. jit_handled_abi_check_report() dumps them all at once and aborts, + // so a single run reveals the full magnitude of the layout disaster. + static string g_abi_check_report; + static int g_abi_check_count = 0; // mismatches + static int g_abi_types_checked = 0; // type-size checks where the target annotation was found + static int g_abi_types_skipped = 0; // ... not registered on target (module not linked here) + static int g_abi_fields_checked = 0; + static int g_abi_fields_skipped = 0; + + static TypeAnnotation * jit_find_handled_annotation ( const char * moduleName, const char * typeName ) { + TypeAnnotation * found = nullptr; + Module::foreach([&](Module * module) -> bool { + if ( module->name != moduleName ) return true; + auto ann = module->findAnnotation(typeName); + // handled-type annotations only: StructureAnnotation derives from Annotation (not + // TypeAnnotation), so casting one to TypeAnnotation* would be UB. The ABI sweep only + // emits checks for handled (BasicStructureAnnotation) types, so this is also exact. + if ( ann && ann->rtti_isHandledTypeAnnotation() ) { + found = (TypeAnnotation *) ann; + return false; // stop iterating + } + return true; + }); + return found; + } + + DAS_API void jit_check_handled_type_size ( const char * moduleName, const char * typeName, uint32_t hostSize ) { + auto ann = jit_find_handled_annotation(moduleName, typeName); + if ( !ann ) { g_abi_types_skipped ++; return; } // not registered on target -> module not linked here + g_abi_types_checked ++; + uint32_t targetSize = uint32_t(ann->getSizeOf()); + if ( targetSize != hostSize ) { + char buf[256]; + snprintf(buf, sizeof(buf), " size %s::%s host=%u target=%u\n", + moduleName, typeName, hostSize, targetSize); + g_abi_check_report += buf; + g_abi_check_count ++; + } + } + + DAS_API void jit_check_handled_field_offset ( const char * moduleName, const char * typeName, + const char * fieldName, uint32_t hostOffset ) { + auto ann = jit_find_handled_annotation(moduleName, typeName); + if ( !ann ) { g_abi_fields_skipped ++; return; } + g_abi_fields_checked ++; + uint32_t targetOffset = ann->getFieldOffset(fieldName); + if ( targetOffset != hostOffset ) { + char buf[256]; + if ( targetOffset == (uint32_t)-1 ) // field absent on the target annotation + snprintf(buf, sizeof(buf), " offset %s::%s.%s host=%u target=MISSING\n", + moduleName, typeName, fieldName, hostOffset); + else + snprintf(buf, sizeof(buf), " offset %s::%s.%s host=%u target=%u\n", + moduleName, typeName, fieldName, hostOffset, targetOffset); + g_abi_check_report += buf; + g_abi_check_count ++; + } + } + + DAS_API void jit_handled_abi_check_report () { + DAS_FATAL_LOG("JIT ABI CHECK: types %d checked / %d skipped, fields %d checked / %d skipped, %d mismatch(es)\n", + g_abi_types_checked, g_abi_types_skipped, g_abi_fields_checked, g_abi_fields_skipped, g_abi_check_count); + if ( g_abi_check_count==0 ) { + // reset so a subsequent sweep in the same process reports only its own results + g_abi_types_checked = g_abi_types_skipped = g_abi_fields_checked = g_abi_fields_skipped = 0; + g_abi_check_report.clear(); + return; + } + DAS_FATAL_ERROR("JIT ABI CHECK: %d handled-type layout mismatch(es) (host-baked vs target runtime):\n%s", + g_abi_check_count, g_abi_check_report.c_str()); + } + + DAS_API void * jit_alloc_heap ( uint32_t bytes, Context * context ) { + return context->allocate(bytes); + } + + DAS_API void * jit_alloc_persistent ( uint32_t bytes, Context * context ) { + if ( !bytes ) context->throw_out_of_memory(false, bytes); + return das_aligned_alloc16(bytes); + } + + DAS_API void jit_free_heap ( void * bytes, uint32_t size, Context * context ) { + context->free((char *)bytes,size); + } + + DAS_API void jit_free_persistent ( void * bytes, Context * ) { + das_aligned_free16(bytes); + } + + DAS_API void jit_array_lock ( const Array & arr, Context * context, LineInfoArg * at ) { + builtin_array_lock_mutable(arr, context, at); + } + + DAS_API void jit_array_unlock ( const Array & arr, Context * context, LineInfoArg * at ) { + builtin_array_unlock_mutable(arr, context, at); + } + + DAS_API void jit_table_lock ( Table & tab, Context * context, LineInfoArg * at ) { + builtin_table_lock(tab, context, at); + } + + DAS_API void jit_table_unlock ( Table & tab, Context * context, LineInfoArg * at ) { + builtin_table_unlock(tab, context, at); + } + + DAS_API void jit_array_resize ( Array & arr, int newSize, int stride, Context * context, LineInfoArg * at ) { + builtin_array_resize(arr, newSize, stride, context, at); + } + + DAS_API int32_t jit_str_cmp ( char * a, char * b ) { + return strcmp(a ? a : "",b ? b : ""); + } + + DAS_API char * jit_str_cat ( const char * sA, const char * sB, Context * context, LineInfoArg * at ) { + sA = sA ? sA : ""; + sB = sB ? sB : ""; + auto la = stringLength(*context, sA); + auto lb = stringLength(*context, sB); + uint32_t commonLength = la + lb; + if ( !commonLength ) { + return nullptr; + } else { + char * sAB = (char * ) context->allocateString(nullptr, commonLength, at); + memcpy ( sAB, sA, la ); + memcpy ( sAB+la, sB, lb+1 ); + context->stringHeap->recognize(sAB); + return sAB; + } + } + + struct JitStackState { + char * EP; + char * SP; + }; + + DAS_API void jit_prologue ( const char *funcName, void * funcLineInfo, + int32_t stackSize, JitStackState * stackState, + Context * context, LineInfoArg * at ) { + if (!context->stack.push(stackSize, stackState->EP, stackState->SP)) { + context->throw_error_at(at, "stack overflow"); + } +#if DAS_ENABLE_STACK_WALK + Prologue * pp = (Prologue *)context->stack.sp(); + pp->info = nullptr; + pp->fileName = funcName; + pp->functionLine = (LineInfo *) funcLineInfo; + pp->stackSize = stackSize; + pp->is_jit = true; +#endif + } + + DAS_API void jit_epilogue ( JitStackState * stackState, Context * context ) { + context->stack.pop(stackState->EP, stackState->SP); + } + + DAS_API void jit_make_block ( Block * blk, int32_t argStackTop, uint64_t ad, void * bodyNode, void * jitImpl, void * funcInfo, void * lineInfo, Context * context ) { + DAS_ASSERTF(lineInfo != nullptr, "Line info should not be null"); + + JitBlock * block = (JitBlock *) blk; + block->stackOffset = context->stack.spi(); + block->argumentsOffset = argStackTop ? (context->stack.spi() + argStackTop) : 0; + block->body = (SimNode *)(void*) block->node; + block->aotFunction = nullptr; + block->jitFunction = jitImpl; + block->functionArguments = context->abiArguments(); + block->info = (FuncInfo *) funcInfo; + new (block->node) SimNode_JitBlock(*static_cast(lineInfo), (JitBlockFunction) bodyNode, blk, ad); + } + + DAS_API uint64_t jit_ad_by_sid ( uint64_t sid, Context * context ) { + if ( !context || !context->tabAdLookup ) return 0; + auto it = context->tabAdLookup->find(sid); + return it != context->tabAdLookup->end() ? it->second : 0; + } + + DAS_API void jit_debug ( vec4f res, TypeInfo * typeInfo, char * message, Context * context, LineInfoArg * at ) { + FPE_DISABLE; + TextWriter ssw; + if ( message ) ssw << message << " "; + ssw << debug_type(typeInfo) << " = " << debug_value(res, typeInfo, PrintFlags::debugger) << "\n"; + context->to_out(at, ssw.str().c_str()); + } + + DAS_API bool jit_iterator_iterate ( das::Sequence &it, void *data, das::Context *context ) { + return builtin_iterator_iterate(it, data, context); + } + + DAS_API void jit_iterator_delete ( das::Sequence &it, das::Context *context ) { + return builtin_iterator_delete(it, context); + } + + DAS_API void jit_iterator_close ( das::Sequence &it, void *data, das::Context *context ) { + return builtin_iterator_close(it, data, context); + } + + DAS_API bool jit_iterator_first ( das::Sequence &it, void *data, das::Context *context, das::LineInfoArg *at ) { + return builtin_iterator_first(it, data, context, at); + } + + DAS_API bool jit_iterator_next ( das::Sequence &it, void *data, das::Context *context, das::LineInfoArg *at ) { + return builtin_iterator_next(it, data, context, at); + } + + DAS_API void jit_debug_enter ( char * message, Context * context, LineInfoArg * at ) { + TextWriter tw; + tw << string(context->fnDepth, '\t'); context->fnDepth ++; + tw << ">>"; + if ( !context->name.empty() ) tw << "(" << context->name << ")"; + tw << ": "; + if ( message ) tw << message; + if ( at && at->line ) tw << " at " << at->describe(); + tw << "\n"; + context->to_out(at, tw.str().c_str()); + } + + DAS_API void jit_debug_exit ( char * message, Context * context, LineInfoArg * at ) { + TextWriter tw; + context->fnDepth --; tw << string(context->fnDepth, '\t'); + tw << " -"; + if ( !context->name.empty() ) tw << "(" << context->name << ")"; + tw << ": "; + if ( message ) tw << message; + if ( at && at->line ) tw << " at " << at->describe(); + tw << "\n"; + context->to_out(at, tw.str().c_str()); + } + + DAS_API void jit_debug_line ( char * message, Context * context, LineInfoArg * at ) { + TextWriter tw; + tw << string(context->fnDepth + 1, '\t'); + tw << ">>"; + if ( !context->name.empty() ) tw << "(" << context->name << ")"; + tw << ": "; + if ( message ) tw << message; + if ( at && at->line ) tw << " at " << at->describe(); + tw << "\n"; + context->to_out(at, tw.str().c_str()); + } + + DAS_API void jit_initialize_fileinfo ( void * dummy, const char *filename ) { + new (dummy) FileInfo(); + auto fileInfoPtr = reinterpret_cast(dummy); + fileInfoPtr->name = filename; + } + + DAS_API void jit_free_fileinfo ( void * dummy ) { + reinterpret_cast(dummy)->~FileInfo(); + } + + DAS_API void * jit_ast_typedecl ( uint64_t hash, Context * context, LineInfoArg * at ) { + if ( !context->thisProgram ) context->throw_error_at(at, "can't get ast_typeinfo, no program. is 'options rtti' missing?"); + auto ti = context->thisProgram->astTypeInfo.find(hash); + if ( ti==context->thisProgram->astTypeInfo.end() ) { + context->throw_error_at(at, "can't find ast_typeinfo for hash %" PRIx64, hash); + } + auto info = ti->second; + return (void*) new TypeDecl(*info); + } +} + + void *das_get_jit_exception() { return (void *)&jit_exception; } + void *das_get_jit_call_or_fastcall() { return (void *)&jit_call_or_fastcall; } + void *das_get_jit_invoke_block() { return (void *)&jit_invoke_block; } + void *das_get_jit_invoke_block_with_cmres() { return (void *)&jit_invoke_block_with_cmres; } + void *das_get_jit_call_with_cmres() { return (void *)&jit_call_with_cmres; } + void *das_get_jit_string_builder() { return (void *)&jit_string_builder; } + void *das_get_jit_string_builder_temp() { return (void *)&jit_string_builder_temp; } + void *das_get_jit_get_global_mnh() { return (void *)&jit_get_global_mnh; } + void *das_get_jit_get_shared_mnh() { return (void *)&jit_get_shared_mnh; } + void *das_get_jit_get_handled_field_offset() { return (void *)&jit_get_handled_field_offset; } + void *das_get_jit_check_handled_type_size() { return (void *)&jit_check_handled_type_size; } + void *das_get_jit_check_handled_field_offset() { return (void *)&jit_check_handled_field_offset; } + void *das_get_jit_handled_abi_check_report() { return (void *)&jit_handled_abi_check_report; } + void *das_get_jit_alloc_heap() { return (void *)&jit_alloc_heap; } + void *das_get_jit_alloc_persistent() { return (void *)&jit_alloc_persistent; } + void *das_get_jit_free_heap() { return (void *)&jit_free_heap; } + void *das_get_jit_free_persistent() { return (void *)&jit_free_persistent; } + void *das_get_jit_array_lock() { return (void *)&builtin_array_lock; } + void *das_get_jit_array_unlock() { return (void *)&builtin_array_unlock; } + void *das_get_jit_table_lock() { return (void *)&builtin_table_lock; } + void *das_get_jit_table_unlock() { return (void *)&builtin_table_unlock; } + void *das_get_jit_array_resize() { return (void *)&builtin_array_resize; } + + void *das_get_jit_str_cmp() { return (void *)&jit_str_cmp; } + void *das_get_jit_prologue() { return (void *)&jit_prologue; } + void *das_get_jit_epilogue() { return (void *)&jit_epilogue; } + void *das_get_jit_make_block() { return (void *)&jit_make_block; } + void *das_get_jit_ad_by_sid() { return (void *)&jit_ad_by_sid; } + void *das_get_jit_debug() { return (void *)&jit_debug; } + void *das_get_jit_iterator_iterate() { return (void *)&builtin_iterator_iterate; } + void *das_get_jit_iterator_delete() { return (void *)&builtin_iterator_delete; } + void *das_get_jit_iterator_close() { return (void *)&builtin_iterator_close; } + void *das_get_jit_iterator_first() { return (void *)&builtin_iterator_first; } + void *das_get_jit_iterator_next() { return (void *)&builtin_iterator_next; } + void *das_get_jit_str_cat() { return (void *)&jit_str_cat; } + void *das_get_jit_debug_enter() { return (void *)&jit_debug_enter; } + void *das_get_jit_debug_exit() { return (void *)&jit_debug_exit; } + void *das_get_jit_debug_line() { return (void *)&jit_debug_line; } + void *das_get_jit_initialize_fileinfo () { return (void*)&jit_initialize_fileinfo; } + void *das_get_jit_free_fileinfo () { return (void*)&jit_free_fileinfo; } + void *das_get_jit_ast_typedecl () { return (void*)&jit_ast_typedecl; } + + template + int32_t jit_table_at ( Table * tab, KeyType key, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { + if ( tab->isLocked() ) context->throw_error_at(at, "can't insert to a locked table"); + TableHash thh(context,valueTypeSize); + auto hfn = hash_function(*context, key); + int64_t idx = thh.reserve(*tab, key, hfn, at); + // TODO Phase 7: widen JIT helper return to int64_t. Until then, guard the narrowing + // so JIT-emitted code never gets a wrapped-negative slot index for huge tables. + if ( idx > int64_t(INT32_MAX) ) context->throw_error_at(at, "JIT table slot index %lld exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots", (long long)idx); + return (int32_t) idx; + } + + void * das_get_jit_table_at ( int32_t baseType, Context * context, LineInfoArg * at ) { + JIT_TABLE_FUNCTION(&jit_table_at); + } + + template + bool jit_table_erase ( Table * tab, KeyType key, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { + if ( tab->isLocked() ) context->throw_error_at(at, "can't erase from locked table"); + TableHash thh(context,valueTypeSize); + auto hfn = hash_function(*context, key); + return thh.erase(*tab, key, hfn) != -1; + } + + void * das_get_jit_table_erase ( int32_t baseType, Context * context, LineInfoArg * at ) { + JIT_TABLE_FUNCTION(&jit_table_erase); + } + + template + int32_t jit_table_find ( Table * tab, KeyType key, int32_t valueTypeSize, Context * context ) { + TableHash thh(context,valueTypeSize); + auto hfn = hash_function(*context, key); + int64_t idx = thh.find(*tab, key, hfn); + // TODO Phase 7: widen JIT helper return to int64_t. Until then, guard the narrowing + // so JIT-emitted code never gets a wrapped-negative slot index for huge tables. + if ( idx > int64_t(INT32_MAX) ) context->throw_error("JIT table slot index exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots"); + return (int32_t) idx; + } + + void * das_get_jit_table_find ( int32_t baseType, Context * context, LineInfoArg * at ) { + JIT_TABLE_FUNCTION(&jit_table_find); + } + + // String-key at that takes a precomputed hash, so the JIT emits the hash inline (foldable to an + // immediate for a constant key) instead of recomputing it in C++. String-only — no per-baseType + // matrix; the grow fallback for the inline large-string at (string find is fully inline). + int32_t jit_string_table_at_with_hash ( Table * tab, char * key, uint64_t hfn, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { + if ( tab->isLocked() ) context->throw_error_at(at, "can't insert to a locked table"); + TableHash thh(context,valueTypeSize); + int64_t idx = thh.reserve(*tab, key, hfn, at); + if ( idx > int64_t(INT32_MAX) ) context->throw_error_at(at, "JIT table slot index %lld exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots", (long long)idx); + return (int32_t) idx; + } + + void * das_get_jit_string_table_at_with_hash ( ) { + return (void*)&jit_string_table_at_with_hash; + } + + // Insert after the JIT's inline packed find already proved the key absent from a packed + // table — skips the C++ packed dedup. See TableHash::reserveAfterPackedMiss (which checks + // the lock itself). String form takes the JIT-computed hash; the non-string template + // computes the (cheap) integer hash here rather than threading it from the JIT. + int32_t jit_string_table_at_after_packed_miss ( Table * tab, char * key, uint64_t hfn, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { + TableHash thh(context,valueTypeSize); + int64_t idx = thh.reserveAfterPackedMiss(*tab, key, hfn, at); + if ( idx > int64_t(INT32_MAX) ) context->throw_error_at(at, "JIT table slot index %lld exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots", (long long)idx); + return (int32_t) idx; + } + + void * das_get_jit_string_table_at_after_packed_miss ( ) { + return (void*)&jit_string_table_at_after_packed_miss; + } + + extern "C" { + DAS_API void * get_jit_table_find ( int32_t baseType, Context * context, LineInfoArg * at ) { + return das_get_jit_table_find(baseType, context, at); + } + DAS_API void * get_jit_table_at ( int32_t baseType, Context * context, LineInfoArg * at ) { + return das_get_jit_table_at(baseType, context, at); + } + DAS_API void * get_jit_table_erase ( int32_t baseType, Context * context, LineInfoArg * at ) { + return das_get_jit_table_erase(baseType, context, at); + } + // String-key at routes through this precomputed-hash global (see llvm_jit.das + // build_string_table_at_*); the standalone-exe glob baking needs it as a linkable symbol. + // (String find is fully inline — no C++ helper.) + DAS_API void * get_jit_string_table_at_with_hash ( ) { + return das_get_jit_string_table_at_with_hash(); + } + // Insert-after-packed-miss globals: the JIT's inline packed find calls these on a miss. + // The standalone-exe baker (llvm_exe.das add_table_at_call) stores the in-exe address + // here; without it the glob keeps its compile-process address and faults under ASLR. + DAS_API void * get_jit_string_table_at_after_packed_miss ( ) { + return das_get_jit_string_table_at_after_packed_miss(); + } + DAS_API void * das_get_jit_new ( TypeAnnotation *annotation ) { + return annotation->jitGetNew(); + } + DAS_API void * das_get_jit_delete ( TypeAnnotation *annotation ) { + return annotation->jitGetDelete(); + } + DAS_API void * das_get_jit_clone ( TypeAnnotation *annotation ) { + return annotation->jitGetClone(); + } + DAS_API void * das_get_jit_each ( TypeAnnotation *annotation ) { + return annotation->jitGetEach(); + } + DAS_API void * das_get_jit_at ( TypeAnnotation *annotation, int32_t indexType ) { + return annotation->jitGetAt(Type(indexType)); + } + } +} + +// Test seam: unit tests override the exe-path source so resolution can be +// exercised with synthetic layouts. nullptr = use real getExecutableFileName. +// Returned char* must remain valid for the duration of one resolve call. +static const char * (*g_jit_exe_file_for_test)() = nullptr; + +// Test predicate "does this path exist?" (default: real filesystem). Tests +// can swap in a mock predicate to exercise resolution without touching disk. +static bool (*g_jit_path_exists_for_test)(const char *) = nullptr; + +#if !DAS_NO_FILEIO +static bool jit_path_exists ( const char * p ) { + return g_jit_path_exists_for_test ? g_jit_path_exists_for_test(p) : das::builtin_fexist(p); +} + +// Try a candidate dir / rel_path; if it (or its _debug variant in debug +// builds, gated on DAS_NO_ASSERTIONS to match register_dynamic_module's +// rewrite at module_builtin_fio.cpp:1099) exists, return the path to load. +// Empty string = miss. +// +// Returns the non-_debug name even when the _debug variant is what exists — +// register_dynamic_module applies the _debug rewrite itself in debug builds, +// so we mustn't double-rewrite. std::filesystem::path::operator/ handles +// trailing-separator and root cases correctly (POSIX `/` + `modules/X` → +// `/modules/X`, not `//modules/X`). +static das::string pick_at_dir ( const std::filesystem::path & dir, const char * rel_path ) { + namespace fs = std::filesystem; + if ( dir.empty() || !rel_path || !*rel_path ) return ""; + fs::path candidate = dir / rel_path; + das::string candidate_str = candidate.string().c_str(); + if ( jit_path_exists(candidate_str.c_str()) ) return candidate_str; +#ifndef DAS_NO_ASSERTIONS + if ( candidate.extension() == ".shared_module" ) { + fs::path dbg = candidate.parent_path() / (candidate.stem().string() + "_debug" + candidate.extension().string()); + if ( jit_path_exists(dbg.string().c_str()) ) return candidate_str; + } +#endif + return ""; +} + +static das::string jit_exe_file () { + if ( g_jit_exe_file_for_test ) { + const char * s = g_jit_exe_file_for_test(); + return s ? das::string(s) : das::string(); + } + return das::getExecutableFileName(); +} + +// Pure resolution: pick the path to load, in priority order: +// 1. / — daspkg release bundle layout (modules sit next to the exe) +// 2. / — SDK install layout AND local dev build +// 3. — baked-at-codegen absolute path (legacy / paths without /modules/ segment) +// +// Tiers 1+2 are skipped when rel_path is empty/null. Returns the chosen path; +// tier 3 is unconditional, so the result is never empty when fallback is set. +static das::string resolve_dynamic_module_path ( const char * rel_path, const char * fallback_abs_path ) { + namespace fs = std::filesystem; + // tier 1 — exe_dir (parent_path correctly preserves filesystem root: "/myapp" → "/", "C:\myapp.exe" → "C:\") + das::string exeFile = jit_exe_file(); + if ( !exeFile.empty() ) { + fs::path exeDir = fs::path(exeFile.c_str()).parent_path(); + if ( exeDir.empty() ) exeDir = "."; // exe is a bare filename relative to cwd + das::string p = pick_at_dir(exeDir, rel_path); + if ( !p.empty() ) return p; + } + // tier 2 — das_root + { + das::string p = pick_at_dir(fs::path(das::getDasRoot().c_str()), rel_path); + if ( !p.empty() ) return p; + } + // tier 3 — baked absolute (legacy fallback) + return fallback_abs_path ? fallback_abs_path : ""; +} + +// Same three tiers for a native-path template's destination. It may be a PATTERN +// ("modules/dasLLVM/daslib/{path}.das"), so the probe is its directory prefix and the +// winner is that base with the whole pattern re-rooted onto it. Empty rel_pattern (no +// /modules/ segment at codegen) → tier 3. +static das::string resolve_native_path_dst ( const char * rel_pattern, const char * fallback_abs ) { + namespace fs = std::filesystem; + if ( rel_pattern && *rel_pattern ) { + std::string rel = rel_pattern; + size_t firstBrace = rel.find('{'); // npos → rfind searches the whole string + size_t dirEnd = rel.rfind('/', firstBrace); + std::string dirRel = dirEnd == std::string::npos ? "" : rel.substr(0, dirEnd); + das::vector bases; + das::string exeFile = jit_exe_file(); + if ( !exeFile.empty() ) { + fs::path exeDir = fs::path(exeFile.c_str()).parent_path(); + bases.push_back(exeDir.empty() ? fs::path(".") : exeDir); + } + das::string dasRoot = das::getDasRoot(); + if ( !dasRoot.empty() ) bases.push_back(fs::path(dasRoot.c_str())); + for ( auto & base : bases ) { + fs::path dir = dirRel.empty() ? base : base / dirRel; + if ( jit_path_exists(dir.string().c_str()) ) { + return das::string((base / rel).string().c_str()); + } + } + } + return fallback_abs ? fallback_abs : ""; +} +#else +// No file IO (DAS_NO_FILEIO): there is no filesystem to resolve a dynamic +// module against, and a build with no fio can't dlopen one anyway. Calling +// this is a logic error on such a platform, so abort loudly. +static das::string resolve_dynamic_module_path ( const char *, const char * ) { + std::abort(); +} +// Unlike a dynamic module, a native-path entry is an inert string-table registration +// (register_native_path is a stubbed no-op under DAS_NO_FILEIO), so a standalone exe +// that registered one must keep starting - hand back the baked path, never abort. +static das::string resolve_native_path_dst ( const char *, const char * fallback_abs ) { + return fallback_abs ? fallback_abs : ""; +} +#endif + +// Standalone-exe browser lifecycle (matches the interpreter's WebLoop in +// utils/daslang/main.cpp). A cross-compiled wasm graphics app exports +// init/update/shutdown, but its `main` is the blocking desktop driver. On the web +// the generated entry (llvm_exe.das) runs init() then calls jit_run_web_lifecycle +// instead of main: it installs an rAF loop on update() and runs shutdown() when +// update() returns false. updateFn/shutdownFn are jitted `RetT(Context*)` pointers; +// updateReturnsValue selects the void vs bool/int call signature (bool/int both +// return wasm i32, so they share one signature). +#ifdef __EMSCRIPTEN__ +#include +namespace { + struct JitWebLifecycle { + das::Context * ctx; + void * updateFn; + bool updateReturnsValue; + void * shutdownFn; // nullable + }; + void jit_web_lifecycle_tick ( void * arg ) { + auto * lc = (JitWebLifecycle *) arg; + bool keepGoing = true; + if ( lc->updateReturnsValue ) { + keepGoing = ((int32_t(*)(das::Context*))lc->updateFn)(lc->ctx) != 0; + } else { + ((void(*)(das::Context*))lc->updateFn)(lc->ctx); + } + if ( keepGoing ) lc->ctx->collectHeapIfMostlyFree(); + if ( !keepGoing ) { + emscripten_cancel_main_loop(); + if ( lc->shutdownFn ) ((void(*)(das::Context*))lc->shutdownFn)(lc->ctx); + } + } +} +#endif + +extern "C" { +// See JitWebLifecycle note above. Defined for every target so the symbol always +// links; only the emscripten build installs the rAF loop (others block, but the +// generated entry only emits this call on the wasm target). +DAS_API void jit_run_web_lifecycle ( das::Context * ctx, void * updateFn, + int32_t updateReturnsValue, void * shutdownFn ) { +#ifdef __EMSCRIPTEN__ + // arg leaks by design (lives the whole program). 0 = browser rAF cadence; + // true = simulate_infinite_loop, so this never returns and the entry's + // jit_shutdown() stays unreachable — the runtime persists for the rAF callbacks. + auto * lc = new JitWebLifecycle{ ctx, updateFn, updateReturnsValue != 0, shutdownFn }; + emscripten_set_main_loop_arg(jit_web_lifecycle_tick, lc, 0, true); +#else + bool keepGoing = true; + while ( keepGoing ) { + if ( updateReturnsValue ) keepGoing = ((int32_t(*)(das::Context*))updateFn)(ctx) != 0; + else ((void(*)(das::Context*))updateFn)(ctx); + } + if ( shutdownFn ) ((void(*)(das::Context*))shutdownFn)(ctx); +#endif +} + +// Standalone-exe main guard: the generated entry (llvm_exe.das) routes das main through this +// so a runtime exception prints and exits nonzero instead of unwinding out of the entry with +// no message (hosted runs get this boundary from their runWithCatch call sites; a bare exe +// had none — fix for the silent-exit-127 class). resultKind: 0 = void main, 1 = int main +// (value = exit code), 2 = bool main (true -> 0, false -> 1). +DAS_API int32_t jit_run_main_guarded ( das::Context * ctx, void * mainFn, int32_t resultKind ) { + int32_t rc = 0; + bool ok = ctx->runWithCatch([&]() { + if ( resultKind == 1 ) { + rc = ((int32_t(*)(das::Context*))mainFn)(ctx); + } else if ( resultKind == 2 ) { + rc = ((bool(*)(das::Context*))mainFn)(ctx) ? 0 : 1; + } else { + ((void(*)(das::Context*))mainFn)(ctx); + } + }); + if ( !ok ) { + das::TextPrinter tp; + tp << "EXCEPTION: " << (ctx->getException() ? ctx->getException() : "unknown") << "\n"; + tp.output(); // TextWriter's dtor only frees its buffer — output() is what prints + return 1; + } + return rc; +} + +DAS_API void das_ensure_environment () { + das::daScriptEnvironment::ensure(); +} + +DAS_API void jit_initialize_modules () { + // No need to initialize modules. JIT will generate required calls. + das::daScriptEnvironment::ensure(); +} + +DAS_API void jit_initialize_modules_done () { + das::Module::Initialize(); +} + +// Standalone-exe teardown. Emitted by inject_main right before main returns, +// so debug agents and modules drain while the runtime is alive. Without this, +// the static g_DebugAgents map dtor races ref_count_mutex during +// __cxa_finalize_ranges and terminate() fires (issue #2583). +DAS_API void jit_shutdown () { + das::Module::ShutdownStandalone(); +} + +DAS_API void * jit_register_dynamic_module ( const char * path, const char * mod_name ) { + return das::register_dynamic_module(path, mod_name, 0/*Quiet*/, nullptr, nullptr); +} + +DAS_API void jit_set_exe_file_for_test_( const char * (*fn)() ) { + g_jit_exe_file_for_test = fn; +} + +DAS_API void jit_set_path_exists_for_test_( bool (*fn)(const char *) ) { + g_jit_path_exists_for_test = fn; +} + +// Test entry point: invoke pure resolution and return the chosen path via +// caller-owned buffer. Returns true on success (buf populated, NUL-terminated), +// false if buffer is too small. Unit tests use this to assert resolution order +// without dlopening anything. +DAS_API bool jit_resolve_dynamic_module_path_for_test_ ( const char * rel_path, + const char * fallback_abs_path, + char * out_buf, + size_t buf_size ) { + das::string r = resolve_dynamic_module_path(rel_path, fallback_abs_path); + if ( r.size() + 1 > buf_size ) return false; + memcpy(out_buf, r.c_str(), r.size() + 1); + return true; +} + +DAS_API bool jit_resolve_native_path_dst_for_test_ ( const char * rel_pattern, + const char * fallback_abs, + char * out_buf, + size_t buf_size ) { + das::string r = resolve_native_path_dst(rel_pattern, fallback_abs); + if ( r.size() + 1 > buf_size ) return false; + memcpy(out_buf, r.c_str(), r.size() + 1); + return true; +} + +// Resolve and load a dynamic module. Used by standalone exes (emitted by +// inject_main in llvm_exe.das). +DAS_API void * jit_register_dynamic_module_resolve ( const char * rel_path, + const char * fallback_abs_path, + const char * mod_name ) { + das::string chosen = resolve_dynamic_module_path(rel_path, fallback_abs_path); + return das::register_dynamic_module(chosen.c_str(), mod_name, 0/*Quiet*/, nullptr, nullptr); +} + +// Emitted by inject_main after the per-module jit_register_dynamic_module_resolve calls. +// Those load Quiet (sibling DT_NEEDED ordering makes a first-attempt failure normal), so +// run the fixed-point retry, then treat anything still unloadable as fatal: every module +// the exe registers is required by its program, and continuing only defers the death to +// the first extern lookup, whose message no longer names the real cause. +DAS_API void jit_finalize_dynamic_modules () { + das::retry_pending_dynamic_modules(); + if ( int failed = das::report_pending_dynamic_modules() ) { + DAS_FATAL_ERROR("%d dynamic module(s) failed to load (see above).\n", failed); + } +} + +// ABI shim: -exe binaries emitted before the resolving form link this runtime dynamically +// and still import the 3-argument name. +DAS_API void jit_register_native_path ( const char * mod_name, const char * src_path, const char * dst_path ) { + das::register_native_path(mod_name, src_path, dst_path, nullptr, nullptr); +} + +// Emitted by inject_main (llvm_exe.das) for every native path the program compiled +// against: the destination is re-rooted onto / at run time, so a +// bundle built elsewhere finds its own modules/ instead of the build machine's. +DAS_API void jit_register_native_path_resolve ( const char * mod_name, const char * src_path, + const char * rel_dst, const char * fallback_abs_dst ) { + das::string chosen = resolve_native_path_dst(rel_dst, fallback_abs_dst); + das::register_native_path(mod_name, src_path, chosen.c_str(), nullptr, nullptr); +} +} diff --git a/src/builtin/jit_runtime.h b/src/builtin/jit_runtime.h new file mode 100644 index 0000000000..6c876c3ed1 --- /dev/null +++ b/src/builtin/jit_runtime.h @@ -0,0 +1,77 @@ +#pragma once + +#include "daScript/misc/platform.h" + +// Keep this list short. Every entry is a call out of jitted code into the runtime, which costs a +// call boundary the optimizer cannot see through. An intrinsic emitted as LLVM IR inlines and +// folds with its neighbours, so implement one in IR whenever the operation can be expressed there, +// and reach for a runtime entry point only when it cannot. + +namespace das { + + class Context; + struct LineInfoArg; + struct TypeAnnotation; + + // The JIT runtime entry points live in jit_runtime.cpp; these hand their addresses to the + // emitter, and module_jit.cpp binds them. They run in the compiling process only, so they + // are not in aot_builtin_jit.h, which Module_Jit::aotRequire injects into generated AOT C++. + void * das_get_jit_exception (); + void * das_get_jit_call_or_fastcall (); + void * das_get_jit_call_with_cmres (); + void * das_get_jit_invoke_block (); + void * das_get_jit_invoke_block_with_cmres (); + void * das_get_jit_string_builder (); + void * das_get_jit_string_builder_temp (); + void * das_get_jit_get_global_mnh (); + void * das_get_jit_get_shared_mnh (); + void * das_get_jit_get_handled_field_offset (); + void * das_get_jit_check_handled_type_size (); + void * das_get_jit_check_handled_field_offset (); + void * das_get_jit_handled_abi_check_report (); + void * das_get_jit_alloc_heap (); + void * das_get_jit_alloc_persistent (); + void * das_get_jit_free_heap (); + void * das_get_jit_free_persistent (); + void * das_get_jit_array_lock (); + void * das_get_jit_array_unlock (); + void * das_get_jit_table_lock (); + void * das_get_jit_table_unlock (); + void * das_get_jit_array_resize (); + void * das_get_jit_table_at ( int32_t baseType, Context * context, LineInfoArg * at ); + void * das_get_jit_table_erase ( int32_t baseType, Context * context, LineInfoArg * at ); + void * das_get_jit_table_find ( int32_t baseType, Context * context, LineInfoArg * at ); + void * das_get_jit_string_table_at_with_hash (); + void * das_get_jit_string_table_at_after_packed_miss (); + void * das_get_jit_str_cmp (); + void * das_get_jit_str_cat (); + void * das_get_jit_ast_typedecl (); + void * das_get_jit_prologue (); + void * das_get_jit_epilogue (); + void * das_get_jit_make_block (); + void * das_get_jit_ad_by_sid (); + void * das_get_jit_debug (); + void * das_get_jit_debug_enter (); + void * das_get_jit_debug_exit (); + void * das_get_jit_debug_line (); + void * das_get_jit_iterator_iterate (); + void * das_get_jit_iterator_delete (); + void * das_get_jit_iterator_close (); + void * das_get_jit_iterator_first (); + void * das_get_jit_iterator_next (); + void * das_get_jit_initialize_fileinfo (); + void * das_get_jit_free_fileinfo (); + + // llvm_exe.das declares these into the emitted module by name (LLVMAddFunctionWithType), + // so a standalone exe links them undecorated - C linkage, not C++ mangling. + extern "C" { + DAS_API void * das_get_jit_simnode_interop (); + DAS_API void * das_get_jit_free_simnode_interop (); + DAS_API void * das_get_jit_init_extern_function (); + DAS_API void * das_get_jit_new ( TypeAnnotation * annotation ); + DAS_API void * das_get_jit_delete ( TypeAnnotation * annotation ); + DAS_API void * das_get_jit_clone ( TypeAnnotation * annotation ); + DAS_API void * das_get_jit_each ( TypeAnnotation * annotation ); + DAS_API void * das_get_jit_at ( TypeAnnotation * annotation, int32_t indexType ); + } +} diff --git a/src/builtin/module_builtin_fio.cpp b/src/builtin/module_builtin_fio.cpp index 042cedbf0d..620bf606b1 100644 --- a/src/builtin/module_builtin_fio.cpp +++ b/src/builtin/module_builtin_fio.cpp @@ -2859,7 +2859,7 @@ namespace das { } // 3-tier directory resolver mirroring the shared-module resolution policy - // from PR #2579 (module_jit.cpp::resolve_dynamic_module_path), but for + // from PR #2579 (jit_runtime.cpp::resolve_dynamic_module_path), but for // source-side asset directories. Given a baked source-file path captured // at macro expansion (e.g. ".../modules/das-cards/cards/card_mesh.das"), // returns the directory where that module's runtime assets currently live: diff --git a/src/builtin/module_jit.cpp b/src/builtin/module_jit.cpp index 44cf63fbae..1d2af19fc8 100644 --- a/src/builtin/module_jit.cpp +++ b/src/builtin/module_jit.cpp @@ -29,6 +29,7 @@ #include "misc/include_fmt.h" #include "module_builtin_rtti.h" +#include "jit_runtime.h" #include #include @@ -57,909 +58,7 @@ namespace das { DAS_API void retry_pending_dynamic_modules(); DAS_API int report_pending_dynamic_modules(); - // JitFunction typedef lives in arraytype.h (the SimFunction::jitFunction mirror shares it) - struct SimNode_Jit : SimNode { - SimNode_Jit ( const LineInfo & at, JitFunction eval ) - : SimNode(at), func(eval) {} - virtual SimNode * visit ( SimVisitor & vis ) override; - DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; - virtual bool rtti_node_isJit() const override { return true; } - JitFunction func = nullptr; - // saved original node - SimNode * saved_code = nullptr; - bool saved_aot = false; - void * saved_aot_function = nullptr; - }; - - static void runLlvmAotGlobInitOf ( Context & ctx, void * publ ); - - SimNode * makeAotJitNode ( Context & ctx, void * publ ) { - runLlvmAotGlobInitOf(ctx, publ); - return ctx.code->makeNode(LineInfo(), (JitFunction)publ); - } - - extern "C" void * das_aot_get_func_by_mnh ( uint64_t mnh, Context * ctx ) { - return builtin_getFunctionByMnh(mnh, ctx).PTR; - } - - static vector> & llvmAotEntries() { - static vector> entries; - return entries; - } - - static vector & llvmAotGlobInits() { - static vector inits; - return inits; - } - - // publ -> index into llvmAotGlobInits, so linking one function can init the object it came from. - static das_hash_map & llvmAotGlobInitOfPubl() { - static das_hash_map ofPubl; - return ofPubl; - } - - // An object registers its functions first, then its glob-init; the functions wait here until - // the glob-init arrives and claims them. - static vector & llvmAotPendingPubl() { - static vector pending; - return pending; - } - - // Runs an object's glob-init once: the slot is cleared as it is consumed. - static void runLlvmAotGlobInitOf ( Context & ctx, void * publ ) { - auto it = llvmAotGlobInitOfPubl().find(publ); - if ( it==llvmAotGlobInitOfPubl().end() ) return; - auto & fn = llvmAotGlobInits()[it->second]; - if ( !fn ) return; - auto todo = fn; - fn = nullptr; - todo(&ctx); - } - static void registerLlvmAotFunctions ( AotLibrary & lib ) { - for ( auto & e : llvmAotEntries() ) { - lib.emplace(e.first, AotFactory(e.second)); - } - } - static AotListBase g_llvmAotList(registerLlvmAotFunctions); - - extern "C" void das_aot_register ( uint64_t aotHash, void * publ ) { - llvmAotEntries().emplace_back(aotHash, publ); - llvmAotPendingPubl().push_back(publ); - } - - extern "C" void das_aot_register_globinit ( void (*fn)(Context*) ) { - uint32_t index = uint32_t(llvmAotGlobInits().size()); - llvmAotGlobInits().push_back(fn); - for ( auto publ : llvmAotPendingPubl() ) { - llvmAotGlobInitOfPubl()[publ] = index; - } - llvmAotPendingPubl().clear(); - } - - struct SimNode_JitBlock; - - struct JitBlock : Block { - vec4f node[10]; - }; - - struct SimNode_JitBlock : SimNode_ClosureBlock { - SimNode_JitBlock ( const LineInfo & at, JitBlockFunction eval, Block * bptr, uint64_t ad ) - : SimNode_ClosureBlock(at,false,false,ad), func(eval), blockPtr(bptr) {} - virtual SimNode * visit ( SimVisitor & vis ) override; - DAS_EVAL_ABI virtual vec4f eval ( Context & context ) override; - JitBlockFunction func = nullptr; - Block * blockPtr = nullptr; - }; - static_assert(sizeof(SimNode_JitBlock)<=sizeof(JitBlock().node),"jit block node must fit under node size"); - - - DAS_SUPPRESS_UB vec4f SimNode_Jit::eval ( Context & context ) { - auto result = func(&context, context.abiArg, context.abiCMRES); - context.result = result; - return result; - } - - SimNode * SimNode_JitBlock::visit ( SimVisitor & vis ) { - uint64_t fptr = (uint64_t) func; - V_BEGIN(); - V_OP(JitBlock); - V_ARG(fptr); - V_END(); - } - - DAS_SUPPRESS_UB vec4f SimNode_JitBlock::eval ( Context & context ) { - auto ba = (BlockArguments *) ( context.stack.bottom() + blockPtr->argumentsOffset ); - return func(&context, ba->arguments, ba->copyOrMoveResult, blockPtr ); - } - - SimNode * SimNode_Jit::visit ( SimVisitor & vis ) { - uint64_t fptr = (uint64_t) func; - V_BEGIN(); - V_OP(Jit); - V_ARG(fptr); - V_END(); - } - - float4 das_invoke_code ( void * pfun, vec4f anything, void * cmres, Context * context ) { - vec4f * arguments = cast::to(anything); - vec4f (*fun)(Context *, vec4f *, void *) = (vec4f(*)(Context *, vec4f *, void *)) pfun; - vec4f res = fun ( context, arguments, cmres ); - return res; - } - - bool das_remove_jit ( const Func func ) { - auto simfn = func.PTR; - if ( !simfn ) return false; - if ( simfn->code && simfn->code->rtti_node_isJit() ) { - auto jitNode = static_cast(simfn->code); - simfn->code = jitNode->saved_code; - simfn->aot = jitNode->saved_aot; - simfn->aotFunction = jitNode->saved_aot_function; - simfn->jitFunction = nullptr; // pre-instrument mirror is always null (aot/jit exclusive) - simfn->jit = false; - return true; - } else { - return false; - } - } - - bool das_has_jit_fastpath ( const Func func ) { - // test rail: is the invoke fastpath armed for this function (SimFunction::jitFunction mirror set)? - auto simfn = func.PTR; - return simfn && simfn->jitFunction; - } - - bool das_instrument_jit ( void * pfun, const Func func, const LineInfo & lineInfo, Context & context ) { - - auto simfn = func.PTR; - if ( !simfn ) return false; - if ( simfn->code && simfn->code->rtti_node_isJit() ) { - auto jitNode = static_cast(simfn->code); - jitNode->func = (JitFunction) pfun; - jitNode->debugInfo = lineInfo; - simfn->jitFunction = pfun; - } else { - auto node = context.code->makeNode(lineInfo, (JitFunction)pfun); - node->saved_code = simfn->code; - node->saved_aot = simfn->aot; - node->saved_aot_function = simfn->aotFunction; - simfn->code = node; - simfn->aot = false; - simfn->aotFunction = nullptr; - simfn->jitFunction = pfun; // the invoke-fastpath mirror of node->func - simfn->jit = true; - } - return true; - } - -extern "C" { - DAS_API void jit_exception ( const char * text, Context * context, LineInfoArg * at ) { - context->throw_error_at(at, "%s", text ? text : ""); - } - - DAS_API vec4f jit_call_or_fastcall ( SimFunction * fn, vec4f * args, Context * context ) { - if ( !fn ) context->throw_error("jit_call_or_fastcall: null function (unresolved LLVM-AOT call target)"); - return context->callOrFastcall(fn, args, nullptr); - } - - DAS_API vec4f jit_invoke_block ( const Block & blk, vec4f * args, Context * context ) { - return context->invoke(blk, args, nullptr, nullptr); - } - - DAS_API vec4f jit_invoke_block_with_cmres ( const Block & blk, vec4f * args, void * cmres, Context * context ) { - return context->invoke(blk, args, cmres, nullptr); - } - - DAS_API vec4f jit_call_with_cmres ( SimFunction * fn, vec4f * args, void * cmres, Context * context ) { - if ( !fn ) context->throw_error("jit_call_with_cmres: null function (unresolved LLVM-AOT call target)"); - return context->callWithCopyOnReturn(fn, args, cmres, nullptr); - } - - DAS_API char * jit_string_builder ( Context & context, int32_t nArgs, TypeInfo ** types, LineInfoArg * at, vec4f * args ) { - StringBuilderWriter writer; - DebugDataWalker walker(writer, PrintFlags::string_builder); - for ( int i = 0; i < nArgs; ++i ) - walker.walk(args[i], types[i]); - auto length = writer.tellp(); - if ( length ) { - return context.allocateString(writer.c_str(), uint32_t(length), at); - } else { - return nullptr; - } - } - - DAS_API char * jit_string_builder_temp ( Context & context, int32_t nArgs, TypeInfo ** types, LineInfoArg * at, vec4f * args ) { - StringBuilderWriter writer; - DebugDataWalker walker(writer, PrintFlags::string_builder); - for ( int i = 0; i < nArgs; ++i ) - walker.walk(args[i], types[i]); - auto length = writer.tellp(); - if ( length ) { - auto str = context.allocateTempString(writer.c_str(), uint32_t(length), at); - context.freeTempString(str, at); - return str; - } else { - return nullptr; - } - } - - DAS_API void jit_simnode_interop(void *ptr, int argCount, TypeInfo **types) { - auto res = new(ptr) SimNode_AotInteropBase(); - res->argumentValues = nullptr; - res->nArguments = argCount; - res->types = types; - } - - DAS_API void jit_free_simnode_interop(SimNode_AotInteropBase *ptr) { - ptr->~SimNode_AotInteropBase(); - } - - DAS_API void *das_get_jit_simnode_interop() { - return (void *) &jit_simnode_interop; - } - - DAS_API void *das_get_jit_free_simnode_interop() { - return (void *) &jit_free_simnode_interop; - } - - // We need it to bypass protected/private. - class JitContext : public Context { - public: - ~JitContext() = default; - JitContext(size_t totalVariables, size_t totalFunctions, size_t globalStringHeapSize, - size_t globSize, size_t shrSize, bool pinvoke, uint32_t stackSize = 16*1024, - bool persistentHeap = false, bool gcEnabled = false) - : Context(stackSize) { - auto &context = *this; - CodeOfPolicies policies; - policies.debugger = false; - // standalone exe skips Program::simulate (which would read `options - // persistent_heap` / `options gc`), so set the heap mode here. Without a - // persistent heap + gcEnabled, heap_collect() throws at runtime. - policies.persistent_heap = persistentHeap; - context.setup(totalVariables, globalStringHeapSize, policies, {}); - context.gcEnabled = gcEnabled; - context.globalsSize = globSize; - context.sharedSize = shrSize; - context.sharedOwner = true; - for (size_t i = 0; i < totalVariables; i++) { - globalVariables[i] = GlobalVariable{}; - } - context.allocateGlobalsAndShared(); - // UBSAN forbid memset(null, 0, 0) - if (context.globalsSize) memset(context.globals, 0, context.globalsSize); - if (context.sharedSize) memset(context.shared, 0, context.sharedSize); - if ( pinvoke ) { - context.contextMutex = new recursive_mutex; - } - } - - void allocFunctions ( uint64_t count ) { - functions = (SimFunction *) code->allocate(count * sizeof(SimFunction)); - memset(functions, 0, count * sizeof(SimFunction)); - totalFunctions = (int) count; - // Allocate stub debugInfo for all function slots so that - // runShutdownScript can safely iterate them. - auto stubInfo = (FuncInfo *) code->allocate(count * sizeof(FuncInfo)); - memset(stubInfo, 0, count * sizeof(FuncInfo)); - for ( uint64_t i = 0; i < count; i++ ) { - stubInfo[i].name = (char *) "unimplemented"; - functions[i].name = (char *) "unimplemented"; - functions[i].debugInfo = &stubInfo[i]; - } - functionLookup = make_shared(); - variableLookup = make_shared(); - } - - void *registerJitFunction ( uint64_t index, const char * funcName, const char * mangledName, - uint64_t mnh, uint32_t stackSize, void * fnPtr, - bool cmres, bool fastcall, bool pinvoke, uint32_t nArguments ) { - DAS_ASSERT(index < (uint64_t) totalFunctions); - auto & fn = functions[index]; - fn.name = code->allocateName(funcName); - fn.mangledName = code->allocateName(mangledName); - fn.mangledNameHash = mnh; - fn.stackSize = stackSize; - fn.flags = 0; - fn.cmres = cmres; - fn.fastcall = fastcall; - fn.pinvoke = pinvoke; - fn.jit = true; - auto finfo = (FuncInfo *) code->allocate(sizeof(FuncInfo)); - memset(finfo, 0, sizeof(FuncInfo)); - finfo->name = fn.name; - finfo->stackSize = stackSize; - finfo->count = nArguments; - fn.debugInfo = finfo; - auto node = code->makeNode(LineInfo{}, (JitFunction) fnPtr); - fn.code = node; - fn.jitFunction = fnPtr; // the invoke-fastpath mirror - return &fn; - } - - void registerJitGlobalVariable(uint64_t index, const char * name, uint64_t mnh, size_t offset, bool shared) { - DAS_ASSERT(index < (uint64_t) totalVariables); - auto & gv = globalVariables[index]; - gv.name = code->allocateName(name); - gv.mangledNameHash = mnh; - gv.offset = (uint32_t) offset; - gv.flags = shared ? 1u : 0u; - } - - // the exe carries both lookups as constant data the emitter sealed; nothing is built or owned here - void adoptLookups(const NameLookup::StaticTable * functions, const NameLookup::StaticTable * variables) { - functionLookup->adopt(*functions); - variableLookup->adopt(*variables); - } - - // registerJitGlobalVariable fills name, hash, offset and the shared flag; the - // debugInfo is the exe-resident TypeInfo emitted by create_type_info_global, which - // only the JIT'd init function can wire. collectHeap walks globalVariables[i] via - // .offset/.debugInfo/.shared, so a NULL debugInfo is a GC crash. - void setStandaloneGlobalInfo(uint64_t index, uint64_t offset, void* debugInfo, int shared) { - DAS_ASSERT(index < (uint64_t) totalVariables); - auto & gv = globalVariables[index]; - gv.offset = (uint32_t) offset; - gv.debugInfo = (VarInfo *) debugInfo; - gv.flags = shared ? 1u : 0u; - } - - void initFunctionAddr ( uint64_t index, void * globPtr ) { - DAS_ASSERT(index < (uint64_t) totalFunctions); - *((SimFunction **) globPtr) = &functions[index]; - } - }; - - // Note: this function called in runtime, before main. - // When we built executable from das. - DAS_API Context * jit_create_standalone_ctx ( uint64_t totalVariables, - uint64_t totalFunctions, - uint64_t globalStringHeapSize, - uint64_t globalsSize, - uint64_t sharedSize, - bool pinvoke, - uint64_t stackSize) { - Context *context = new JitContext(totalVariables, totalFunctions, globalStringHeapSize, - globalsSize, sharedSize, pinvoke, - stackSize ? (uint32_t)stackSize : 16*1024, - /*persistentHeap*/ true, /*gcEnabled*/ true); - static_cast(context)->allocFunctions(totalFunctions); - return context; - } - - DAS_API void *jit_register_standalone_function ( Context * ctx, uint64_t index, - const char * name, const char * mangledName, - uint64_t mnh, uint32_t stackSize, - void * fnPtr, - bool cmres, bool fastcall, bool pinvoke, - uint32_t nArguments ) { - return static_cast(ctx)->registerJitFunction(index, name, mangledName, mnh, stackSize, - fnPtr, cmres, fastcall, pinvoke, nArguments); - } - - DAS_API void jit_register_standalone_variable ( Context * ctx, uint64_t index, const char * name, uint64_t mangledNameHash, uint64_t offset, int shared ) { - static_cast(ctx)->registerJitGlobalVariable(index, name, mangledNameHash, offset, shared != 0); - } - - DAS_API void jit_adopt_standalone_lookups ( Context * ctx, const void * functions, const void * variables ) { - static_cast(ctx)->adoptLookups((const NameLookup::StaticTable *) functions, (const NameLookup::StaticTable *) variables); - } - - // Populate globalVariables[index] so the GC can trace standalone-exe globals. - // Emitted by generate_globals_initialization_fn into the init function (debugInfo - // is the exe-resident TypeInfo, so it can only be wired at codegen time). - DAS_API void jit_set_global_var ( Context * ctx, uint64_t index, uint64_t offset, void* debugInfo, int shared ) { - static_cast(ctx)->setStandaloneGlobalInfo(index, offset, debugInfo, shared); - } - - DAS_API void jit_set_init_script ( Context * ctx, Context::JitInitScriptFn fn ) { - ctx->jitInitScript = fn; - } - - DAS_API void jit_init_function_addr ( Context * ctx, uint64_t index, void * globPtr ) { - static_cast(ctx)->initFunctionAddr(index, globPtr); - } - - // A missing MODULE and a missing FUNCTION die on the same lookup — distinguish them, - // because the former is a load failure (dlopen), not a signature mismatch. - static bool jit_module_is_registered ( const char * moduleName ) { - bool exists = false; - Module::foreach([&](Module * module) -> bool { - if ( module->name != moduleName ) return true; - exists = true; - return false; - }); - return exists; - } - - DAS_API void jit_init_extern_function ( const char * moduleName, - const char * funcMangledName, - void ** dllGlobal ) { - bool found = false; - Module::foreach([&](Module * module) -> bool { - if ( module->name != moduleName ) return true; - auto fn = module->findFunction(funcMangledName); - if ( fn && fn->builtIn ) { - *dllGlobal = static_cast(fn)->getBuiltinAddress(); - found = *dllGlobal != nullptr; - return !found; - } - return true; - }); - if ( !found && strcmp(moduleName, "dasbind") == 0 && strncmp(funcMangledName, "@dasbind::__dasbind__", 21) == 0 ) { - Module::foreach([&](Module * module) -> bool { - if ( module->name != "dasbind" ) return true; - auto resolverFn = module->findUniqueFunction("__dasbind_resolve"); - if ( resolverFn && resolverFn->builtIn ) { - auto resolver = (void * (*)(const char *)) - static_cast(resolverFn)->getBuiltinAddress(); - if ( resolver ) { - *dllGlobal = resolver(funcMangledName); - found = *dllGlobal != nullptr; - } - } - return false; - }); - } - if (!found) { - if ( !jit_module_is_registered(moduleName) ) { - DAS_FATAL_ERROR("Failed to find %s: module %s is not registered (its .shared_module may have failed to load - see errors above).\n", funcMangledName, moduleName); - } - DAS_FATAL_ERROR("Failed to find %s in module %s.\n", funcMangledName, moduleName); - } - } - - DAS_API Annotation *jit_get_annotation ( const char * moduleName, - const char * annName ) { - Annotation *result = nullptr; - Module::foreach([&](Module * module) -> bool { - if ( module->name != moduleName ) return true; - result = module->findAnnotation(annName); - return false; - }); - if (!result) { - if ( !jit_module_is_registered(moduleName) ) { - DAS_FATAL_ERROR("Failed to find annotation %s: module %s is not registered (its .shared_module may have failed to load - see errors above).\n", annName, moduleName); - } - DAS_FATAL_ERROR("Failed to find annotation %s in module %s.\n", annName, moduleName); - } - return result; - } - - DAS_API void jit_trap() { - DAS_FATAL_ERROR("FATAL: Unresolved dynamic function call in compiled code. This indicates a missing JIT symbol. Disable `strict` mode or remove this call.\n"); - } - - DAS_API void jit_set_command_line_arguments( int argc, char * argv[] ) { - setCommandLineArguments(argc, argv); - } - - DAS_API void *das_get_jit_init_extern_function() { - return (void *) &jit_init_extern_function; - } - - DAS_API void * jit_get_global_mnh ( uint64_t mnh, Context & context ) { - return context.globals + context.globalOffsetByMangledName(mnh); - } - - DAS_API void * jit_get_shared_mnh ( uint64_t mnh, Context & context ) { - return context.shared + context.globalOffsetByMangledName(mnh); - } - - // Resolve a handled-type (C++) field offset at runtime. The JIT bakes field - // offsets via the HOST annotation's offsetof, which is wrong when cross-compiling - // to a target whose C++ ABI lays the struct out differently — even at equal pointer - // width (e.g. an MSVC host vs a clang/wasm64 target: vptr + multiple-base ordering - // diverge for polymorphic classes like Context). The runtime archive (built for the - // target) registers each handled type's annotation with the right offsetof, so - // resolve by (module, type, field) here. Called once per offset-global at init, - // after initialize_modules() has registered the modules. - DAS_API uint32_t jit_get_handled_field_offset ( const char * moduleName, - const char * typeName, - const char * fieldName ) { - uint32_t offset = (uint32_t)-1; - Module::foreach([&](Module * module) -> bool { - if ( module->name != moduleName ) return true; - auto ann = module->findAnnotation(typeName); - // handled-type annotations only — StructureAnnotation isn't a TypeAnnotation (see note in - // jit_find_handled_annotation); the JIT only resolves offsets for handled-type fields. - if ( ann && ann->rtti_isHandledTypeAnnotation() ) { - offset = ((TypeAnnotation *)ann)->getFieldOffset(fieldName); - return false; // stop iterating - } - return true; - }); - // Compiler only requests offsets for fields it resolved on the host annotation; a - // miss = target runtime annotation diverges (ABI/module mismatch). Fail loud, not -1. - if ( offset == (uint32_t)-1 ) { - DAS_FATAL_ERROR("jit: unresolved handled-type field offset %s::%s.%s (target runtime annotation diverges from the host).\n", - moduleName, typeName, fieldName); - } - return offset; - } - - // ---- ABI safe-check (opt-in via --jit-check-abi) ------------------------ - // Cross-compiling from an MSVC host to a wasm/clang target can bake a wrong - // handled-type (C++ struct) SIZE or field OFFSET when the two C++ ABIs lay the - // struct out differently (vptr/base ordering, or #ifdef WIN32/EMSCRIPTEN- - // conditional members). The codegen emits, at startup, one check call per used - // handled type (size) and per field (offset) carrying the HOST-baked value; - // each compares against the TARGET runtime annotation and records every - // divergence. jit_handled_abi_check_report() dumps them all at once and aborts, - // so a single run reveals the full magnitude of the layout disaster. - static string g_abi_check_report; - static int g_abi_check_count = 0; // mismatches - static int g_abi_types_checked = 0; // type-size checks where the target annotation was found - static int g_abi_types_skipped = 0; // ... not registered on target (module not linked here) - static int g_abi_fields_checked = 0; - static int g_abi_fields_skipped = 0; - - static TypeAnnotation * jit_find_handled_annotation ( const char * moduleName, const char * typeName ) { - TypeAnnotation * found = nullptr; - Module::foreach([&](Module * module) -> bool { - if ( module->name != moduleName ) return true; - auto ann = module->findAnnotation(typeName); - // handled-type annotations only: StructureAnnotation derives from Annotation (not - // TypeAnnotation), so casting one to TypeAnnotation* would be UB. The ABI sweep only - // emits checks for handled (BasicStructureAnnotation) types, so this is also exact. - if ( ann && ann->rtti_isHandledTypeAnnotation() ) { - found = (TypeAnnotation *) ann; - return false; // stop iterating - } - return true; - }); - return found; - } - - DAS_API void jit_check_handled_type_size ( const char * moduleName, const char * typeName, uint32_t hostSize ) { - auto ann = jit_find_handled_annotation(moduleName, typeName); - if ( !ann ) { g_abi_types_skipped ++; return; } // not registered on target -> module not linked here - g_abi_types_checked ++; - uint32_t targetSize = uint32_t(ann->getSizeOf()); - if ( targetSize != hostSize ) { - char buf[256]; - snprintf(buf, sizeof(buf), " size %s::%s host=%u target=%u\n", - moduleName, typeName, hostSize, targetSize); - g_abi_check_report += buf; - g_abi_check_count ++; - } - } - - DAS_API void jit_check_handled_field_offset ( const char * moduleName, const char * typeName, - const char * fieldName, uint32_t hostOffset ) { - auto ann = jit_find_handled_annotation(moduleName, typeName); - if ( !ann ) { g_abi_fields_skipped ++; return; } - g_abi_fields_checked ++; - uint32_t targetOffset = ann->getFieldOffset(fieldName); - if ( targetOffset != hostOffset ) { - char buf[256]; - if ( targetOffset == (uint32_t)-1 ) // field absent on the target annotation - snprintf(buf, sizeof(buf), " offset %s::%s.%s host=%u target=MISSING\n", - moduleName, typeName, fieldName, hostOffset); - else - snprintf(buf, sizeof(buf), " offset %s::%s.%s host=%u target=%u\n", - moduleName, typeName, fieldName, hostOffset, targetOffset); - g_abi_check_report += buf; - g_abi_check_count ++; - } - } - - DAS_API void jit_handled_abi_check_report () { - DAS_FATAL_LOG("JIT ABI CHECK: types %d checked / %d skipped, fields %d checked / %d skipped, %d mismatch(es)\n", - g_abi_types_checked, g_abi_types_skipped, g_abi_fields_checked, g_abi_fields_skipped, g_abi_check_count); - if ( g_abi_check_count==0 ) { - // reset so a subsequent sweep in the same process reports only its own results - g_abi_types_checked = g_abi_types_skipped = g_abi_fields_checked = g_abi_fields_skipped = 0; - g_abi_check_report.clear(); - return; - } - DAS_FATAL_ERROR("JIT ABI CHECK: %d handled-type layout mismatch(es) (host-baked vs target runtime):\n%s", - g_abi_check_count, g_abi_check_report.c_str()); - } - - DAS_API void * jit_alloc_heap ( uint32_t bytes, Context * context ) { - return context->allocate(bytes); - } - - DAS_API void * jit_alloc_persistent ( uint32_t bytes, Context * context ) { - if ( !bytes ) context->throw_out_of_memory(false, bytes); - return das_aligned_alloc16(bytes); - } - - DAS_API void jit_free_heap ( void * bytes, uint32_t size, Context * context ) { - context->free((char *)bytes,size); - } - - DAS_API void jit_free_persistent ( void * bytes, Context * ) { - das_aligned_free16(bytes); - } - - DAS_API void jit_array_lock ( const Array & arr, Context * context, LineInfoArg * at ) { - builtin_array_lock_mutable(arr, context, at); - } - - DAS_API void jit_array_unlock ( const Array & arr, Context * context, LineInfoArg * at ) { - builtin_array_unlock_mutable(arr, context, at); - } - - DAS_API void jit_table_lock ( Table & tab, Context * context, LineInfoArg * at ) { - builtin_table_lock(tab, context, at); - } - - DAS_API void jit_table_unlock ( Table & tab, Context * context, LineInfoArg * at ) { - builtin_table_unlock(tab, context, at); - } - - DAS_API void jit_array_resize ( Array & arr, int newSize, int stride, Context * context, LineInfoArg * at ) { - builtin_array_resize(arr, newSize, stride, context, at); - } - - DAS_API int32_t jit_str_cmp ( char * a, char * b ) { - return strcmp(a ? a : "",b ? b : ""); - } - - DAS_API char * jit_str_cat ( const char * sA, const char * sB, Context * context, LineInfoArg * at ) { - sA = sA ? sA : ""; - sB = sB ? sB : ""; - auto la = stringLength(*context, sA); - auto lb = stringLength(*context, sB); - uint32_t commonLength = la + lb; - if ( !commonLength ) { - return nullptr; - } else { - char * sAB = (char * ) context->allocateString(nullptr, commonLength, at); - memcpy ( sAB, sA, la ); - memcpy ( sAB+la, sB, lb+1 ); - context->stringHeap->recognize(sAB); - return sAB; - } - } - - struct JitStackState { - char * EP; - char * SP; - }; - - DAS_API void jit_prologue ( const char *funcName, void * funcLineInfo, - int32_t stackSize, JitStackState * stackState, - Context * context, LineInfoArg * at ) { - if (!context->stack.push(stackSize, stackState->EP, stackState->SP)) { - context->throw_error_at(at, "stack overflow"); - } -#if DAS_ENABLE_STACK_WALK - Prologue * pp = (Prologue *)context->stack.sp(); - pp->info = nullptr; - pp->fileName = funcName; - pp->functionLine = (LineInfo *) funcLineInfo; - pp->stackSize = stackSize; - pp->is_jit = true; -#endif - } - - DAS_API void jit_epilogue ( JitStackState * stackState, Context * context ) { - context->stack.pop(stackState->EP, stackState->SP); - } - - DAS_API void jit_make_block ( Block * blk, int32_t argStackTop, uint64_t ad, void * bodyNode, void * jitImpl, void * funcInfo, void * lineInfo, Context * context ) { - DAS_ASSERTF(lineInfo != nullptr, "Line info should not be null"); - - JitBlock * block = (JitBlock *) blk; - block->stackOffset = context->stack.spi(); - block->argumentsOffset = argStackTop ? (context->stack.spi() + argStackTop) : 0; - block->body = (SimNode *)(void*) block->node; - block->aotFunction = nullptr; - block->jitFunction = jitImpl; - block->functionArguments = context->abiArguments(); - block->info = (FuncInfo *) funcInfo; - new (block->node) SimNode_JitBlock(*static_cast(lineInfo), (JitBlockFunction) bodyNode, blk, ad); - } - - DAS_API uint64_t jit_ad_by_sid ( uint64_t sid, Context * context ) { - if ( !context || !context->tabAdLookup ) return 0; - auto it = context->tabAdLookup->find(sid); - return it != context->tabAdLookup->end() ? it->second : 0; - } - - DAS_API void jit_debug ( vec4f res, TypeInfo * typeInfo, char * message, Context * context, LineInfoArg * at ) { - FPE_DISABLE; - TextWriter ssw; - if ( message ) ssw << message << " "; - ssw << debug_type(typeInfo) << " = " << debug_value(res, typeInfo, PrintFlags::debugger) << "\n"; - context->to_out(at, ssw.str().c_str()); - } - - DAS_API bool jit_iterator_iterate ( das::Sequence &it, void *data, das::Context *context ) { - return builtin_iterator_iterate(it, data, context); - } - - DAS_API void jit_iterator_delete ( das::Sequence &it, das::Context *context ) { - return builtin_iterator_delete(it, context); - } - - DAS_API void jit_iterator_close ( das::Sequence &it, void *data, das::Context *context ) { - return builtin_iterator_close(it, data, context); - } - - DAS_API bool jit_iterator_first ( das::Sequence &it, void *data, das::Context *context, das::LineInfoArg *at ) { - return builtin_iterator_first(it, data, context, at); - } - - DAS_API bool jit_iterator_next ( das::Sequence &it, void *data, das::Context *context, das::LineInfoArg *at ) { - return builtin_iterator_next(it, data, context, at); - } - - DAS_API void jit_debug_enter ( char * message, Context * context, LineInfoArg * at ) { - TextWriter tw; - tw << string(context->fnDepth, '\t'); context->fnDepth ++; - tw << ">>"; - if ( !context->name.empty() ) tw << "(" << context->name << ")"; - tw << ": "; - if ( message ) tw << message; - if ( at && at->line ) tw << " at " << at->describe(); - tw << "\n"; - context->to_out(at, tw.str().c_str()); - } - - DAS_API void jit_debug_exit ( char * message, Context * context, LineInfoArg * at ) { - TextWriter tw; - context->fnDepth --; tw << string(context->fnDepth, '\t'); - tw << " -"; - if ( !context->name.empty() ) tw << "(" << context->name << ")"; - tw << ": "; - if ( message ) tw << message; - if ( at && at->line ) tw << " at " << at->describe(); - tw << "\n"; - context->to_out(at, tw.str().c_str()); - } - - DAS_API void jit_debug_line ( char * message, Context * context, LineInfoArg * at ) { - TextWriter tw; - tw << string(context->fnDepth + 1, '\t'); - tw << ">>"; - if ( !context->name.empty() ) tw << "(" << context->name << ")"; - tw << ": "; - if ( message ) tw << message; - if ( at && at->line ) tw << " at " << at->describe(); - tw << "\n"; - context->to_out(at, tw.str().c_str()); - } - - DAS_API void jit_initialize_fileinfo ( void * dummy, const char *filename ) { - new (dummy) FileInfo(); - auto fileInfoPtr = reinterpret_cast(dummy); - fileInfoPtr->name = filename; - } - - DAS_API void jit_free_fileinfo ( void * dummy ) { - reinterpret_cast(dummy)->~FileInfo(); - } - - DAS_API void * jit_ast_typedecl ( uint64_t hash, Context * context, LineInfoArg * at ) { - if ( !context->thisProgram ) context->throw_error_at(at, "can't get ast_typeinfo, no program. is 'options rtti' missing?"); - auto ti = context->thisProgram->astTypeInfo.find(hash); - if ( ti==context->thisProgram->astTypeInfo.end() ) { - context->throw_error_at(at, "can't find ast_typeinfo for hash %" PRIx64, hash); - } - auto info = ti->second; - return (void*) new TypeDecl(*info); - } -} - - void *das_get_jit_exception() { return (void *)&jit_exception; } - void *das_get_jit_call_or_fastcall() { return (void *)&jit_call_or_fastcall; } - void *das_get_jit_invoke_block() { return (void *)&jit_invoke_block; } - void *das_get_jit_invoke_block_with_cmres() { return (void *)&jit_invoke_block_with_cmres; } - void *das_get_jit_call_with_cmres() { return (void *)&jit_call_with_cmres; } - void *das_get_jit_string_builder() { return (void *)&jit_string_builder; } - void *das_get_jit_string_builder_temp() { return (void *)&jit_string_builder_temp; } - void *das_get_jit_get_global_mnh() { return (void *)&jit_get_global_mnh; } - void *das_get_jit_get_shared_mnh() { return (void *)&jit_get_shared_mnh; } - void *das_get_jit_get_handled_field_offset() { return (void *)&jit_get_handled_field_offset; } - void *das_get_jit_check_handled_type_size() { return (void *)&jit_check_handled_type_size; } - void *das_get_jit_check_handled_field_offset() { return (void *)&jit_check_handled_field_offset; } - void *das_get_jit_handled_abi_check_report() { return (void *)&jit_handled_abi_check_report; } - void *das_get_jit_alloc_heap() { return (void *)&jit_alloc_heap; } - void *das_get_jit_alloc_persistent() { return (void *)&jit_alloc_persistent; } - void *das_get_jit_free_heap() { return (void *)&jit_free_heap; } - void *das_get_jit_free_persistent() { return (void *)&jit_free_persistent; } - void *das_get_jit_array_lock() { return (void *)&builtin_array_lock; } - void *das_get_jit_array_unlock() { return (void *)&builtin_array_unlock; } - void *das_get_jit_table_lock() { return (void *)&builtin_table_lock; } - void *das_get_jit_table_unlock() { return (void *)&builtin_table_unlock; } - void *das_get_jit_array_resize() { return (void *)&builtin_array_resize; } - - void *das_get_jit_str_cmp() { return (void *)&jit_str_cmp; } - void *das_get_jit_prologue() { return (void *)&jit_prologue; } - void *das_get_jit_epilogue() { return (void *)&jit_epilogue; } - void *das_get_jit_make_block() { return (void *)&jit_make_block; } - void *das_get_jit_ad_by_sid() { return (void *)&jit_ad_by_sid; } - void *das_get_jit_debug() { return (void *)&jit_debug; } - void *das_get_jit_iterator_iterate() { return (void *)&builtin_iterator_iterate; } - void *das_get_jit_iterator_delete() { return (void *)&builtin_iterator_delete; } - void *das_get_jit_iterator_close() { return (void *)&builtin_iterator_close; } - void *das_get_jit_iterator_first() { return (void *)&builtin_iterator_first; } - void *das_get_jit_iterator_next() { return (void *)&builtin_iterator_next; } - void *das_get_jit_str_cat() { return (void *)&jit_str_cat; } - void *das_get_jit_debug_enter() { return (void *)&jit_debug_enter; } - void *das_get_jit_debug_exit() { return (void *)&jit_debug_exit; } - void *das_get_jit_debug_line() { return (void *)&jit_debug_line; } - void *das_get_jit_initialize_fileinfo () { return (void*)&jit_initialize_fileinfo; } - void *das_get_jit_free_fileinfo () { return (void*)&jit_free_fileinfo; } - void *das_get_jit_ast_typedecl () { return (void*)&jit_ast_typedecl; } - - template - int32_t jit_table_at ( Table * tab, KeyType key, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { - if ( tab->isLocked() ) context->throw_error_at(at, "can't insert to a locked table"); - TableHash thh(context,valueTypeSize); - auto hfn = hash_function(*context, key); - int64_t idx = thh.reserve(*tab, key, hfn, at); - // TODO Phase 7: widen JIT helper return to int64_t. Until then, guard the narrowing - // so JIT-emitted code never gets a wrapped-negative slot index for huge tables. - if ( idx > int64_t(INT32_MAX) ) context->throw_error_at(at, "JIT table slot index %lld exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots", (long long)idx); - return (int32_t) idx; - } - - void * das_get_jit_table_at ( int32_t baseType, Context * context, LineInfoArg * at ) { - JIT_TABLE_FUNCTION(&jit_table_at); - } - - template - bool jit_table_erase ( Table * tab, KeyType key, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { - if ( tab->isLocked() ) context->throw_error_at(at, "can't erase from locked table"); - TableHash thh(context,valueTypeSize); - auto hfn = hash_function(*context, key); - return thh.erase(*tab, key, hfn) != -1; - } - - void * das_get_jit_table_erase ( int32_t baseType, Context * context, LineInfoArg * at ) { - JIT_TABLE_FUNCTION(&jit_table_erase); - } - - template - int32_t jit_table_find ( Table * tab, KeyType key, int32_t valueTypeSize, Context * context ) { - TableHash thh(context,valueTypeSize); - auto hfn = hash_function(*context, key); - int64_t idx = thh.find(*tab, key, hfn); - // TODO Phase 7: widen JIT helper return to int64_t. Until then, guard the narrowing - // so JIT-emitted code never gets a wrapped-negative slot index for huge tables. - if ( idx > int64_t(INT32_MAX) ) context->throw_error("JIT table slot index exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots"); - return (int32_t) idx; - } - - void * das_get_jit_table_find ( int32_t baseType, Context * context, LineInfoArg * at ) { - JIT_TABLE_FUNCTION(&jit_table_find); - } - - // String-key at that takes a precomputed hash, so the JIT emits the hash inline (foldable to an - // immediate for a constant key) instead of recomputing it in C++. String-only — no per-baseType - // matrix; the grow fallback for the inline large-string at (string find is fully inline). - int32_t jit_string_table_at_with_hash ( Table * tab, char * key, uint64_t hfn, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { - if ( tab->isLocked() ) context->throw_error_at(at, "can't insert to a locked table"); - TableHash thh(context,valueTypeSize); - int64_t idx = thh.reserve(*tab, key, hfn, at); - if ( idx > int64_t(INT32_MAX) ) context->throw_error_at(at, "JIT table slot index %lld exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots", (long long)idx); - return (int32_t) idx; - } - - void * das_get_jit_string_table_at_with_hash ( ) { - return (void*)&jit_string_table_at_with_hash; - } - - // Insert after the JIT's inline packed find already proved the key absent from a packed - // table — skips the C++ packed dedup. See TableHash::reserveAfterPackedMiss (which checks - // the lock itself). String form takes the JIT-computed hash; the non-string template - // computes the (cheap) integer hash here rather than threading it from the JIT. - int32_t jit_string_table_at_after_packed_miss ( Table * tab, char * key, uint64_t hfn, int32_t valueTypeSize, Context * context, LineInfoArg * at ) { - TableHash thh(context,valueTypeSize); - int64_t idx = thh.reserveAfterPackedMiss(*tab, key, hfn, at); - if ( idx > int64_t(INT32_MAX) ) context->throw_error_at(at, "JIT table slot index %lld exceeds INT32_MAX; JIT does not yet support tables past INT_MAX slots", (long long)idx); - return (int32_t) idx; - } - - void * das_get_jit_string_table_at_after_packed_miss ( ) { - return (void*)&jit_string_table_at_after_packed_miss; - } uint64_t das_get_global_variable_offset( const Context * ctx, int id ) { return ctx->getGlobalVariable(id).offset; @@ -989,46 +88,6 @@ extern "C" { return ctx->getSharedSize(); } - extern "C" { - DAS_API void * get_jit_table_find ( int32_t baseType, Context * context, LineInfoArg * at ) { - return das_get_jit_table_find(baseType, context, at); - } - DAS_API void * get_jit_table_at ( int32_t baseType, Context * context, LineInfoArg * at ) { - return das_get_jit_table_at(baseType, context, at); - } - DAS_API void * get_jit_table_erase ( int32_t baseType, Context * context, LineInfoArg * at ) { - return das_get_jit_table_erase(baseType, context, at); - } - // String-key at routes through this precomputed-hash global (see llvm_jit.das - // build_string_table_at_*); the standalone-exe glob baking needs it as a linkable symbol. - // (String find is fully inline — no C++ helper.) - DAS_API void * get_jit_string_table_at_with_hash ( ) { - return das_get_jit_string_table_at_with_hash(); - } - // Insert-after-packed-miss globals: the JIT's inline packed find calls these on a miss. - // The standalone-exe baker (llvm_exe.das add_table_at_call) stores the in-exe address - // here; without it the glob keeps its compile-process address and faults under ASLR. - DAS_API void * get_jit_string_table_at_after_packed_miss ( ) { - return das_get_jit_string_table_at_after_packed_miss(); - } - DAS_API void * das_get_jit_new ( TypeAnnotation *annotation ) { - return annotation->jitGetNew(); - } - DAS_API void * das_get_jit_delete ( TypeAnnotation *annotation ) { - return annotation->jitGetDelete(); - } - DAS_API void * das_get_jit_clone ( TypeAnnotation *annotation ) { - return annotation->jitGetClone(); - } - DAS_API void * das_get_jit_each ( TypeAnnotation *annotation ) { - return annotation->jitGetEach(); - } - DAS_API void * das_get_jit_at ( TypeAnnotation *annotation, int32_t indexType ) { - return annotation->jitGetAt(Type(indexType)); - } - } - - void * das_instrument_line_info ( const LineInfo & info, Context * context, LineInfoArg * at ) { LineInfo * info_ptr = (LineInfo *) context->code->allocate(sizeof(LineInfo)); if ( !info_ptr ) context->throw_error_at(at, "can't instrument line info, out of code heap"); @@ -1727,294 +786,3 @@ extern "C" { } REGISTER_MODULE_IN_NAMESPACE(Module_Jit,das); - -// Test seam: unit tests override the exe-path source so resolution can be -// exercised with synthetic layouts. nullptr = use real getExecutableFileName. -// Returned char* must remain valid for the duration of one resolve call. -static const char * (*g_jit_exe_file_for_test)() = nullptr; - -// Test predicate "does this path exist?" (default: real filesystem). Tests -// can swap in a mock predicate to exercise resolution without touching disk. -static bool (*g_jit_path_exists_for_test)(const char *) = nullptr; - -#if !DAS_NO_FILEIO -static bool jit_path_exists ( const char * p ) { - return g_jit_path_exists_for_test ? g_jit_path_exists_for_test(p) : das::builtin_fexist(p); -} - -// Try a candidate dir / rel_path; if it (or its _debug variant in debug -// builds, gated on DAS_NO_ASSERTIONS to match register_dynamic_module's -// rewrite at module_builtin_fio.cpp:1099) exists, return the path to load. -// Empty string = miss. -// -// Returns the non-_debug name even when the _debug variant is what exists — -// register_dynamic_module applies the _debug rewrite itself in debug builds, -// so we mustn't double-rewrite. std::filesystem::path::operator/ handles -// trailing-separator and root cases correctly (POSIX `/` + `modules/X` → -// `/modules/X`, not `//modules/X`). -static das::string pick_at_dir ( const std::filesystem::path & dir, const char * rel_path ) { - namespace fs = std::filesystem; - if ( dir.empty() || !rel_path || !*rel_path ) return ""; - fs::path candidate = dir / rel_path; - das::string candidate_str = candidate.string().c_str(); - if ( jit_path_exists(candidate_str.c_str()) ) return candidate_str; -#ifndef DAS_NO_ASSERTIONS - if ( candidate.extension() == ".shared_module" ) { - fs::path dbg = candidate.parent_path() / (candidate.stem().string() + "_debug" + candidate.extension().string()); - if ( jit_path_exists(dbg.string().c_str()) ) return candidate_str; - } -#endif - return ""; -} - -static das::string jit_exe_file () { - if ( g_jit_exe_file_for_test ) { - const char * s = g_jit_exe_file_for_test(); - return s ? das::string(s) : das::string(); - } - return das::getExecutableFileName(); -} - -// Pure resolution: pick the path to load, in priority order: -// 1. / — daspkg release bundle layout (modules sit next to the exe) -// 2. / — SDK install layout AND local dev build -// 3. — baked-at-codegen absolute path (legacy / paths without /modules/ segment) -// -// Tiers 1+2 are skipped when rel_path is empty/null. Returns the chosen path; -// tier 3 is unconditional, so the result is never empty when fallback is set. -static das::string resolve_dynamic_module_path ( const char * rel_path, const char * fallback_abs_path ) { - namespace fs = std::filesystem; - // tier 1 — exe_dir (parent_path correctly preserves filesystem root: "/myapp" → "/", "C:\myapp.exe" → "C:\") - das::string exeFile = jit_exe_file(); - if ( !exeFile.empty() ) { - fs::path exeDir = fs::path(exeFile.c_str()).parent_path(); - if ( exeDir.empty() ) exeDir = "."; // exe is a bare filename relative to cwd - das::string p = pick_at_dir(exeDir, rel_path); - if ( !p.empty() ) return p; - } - // tier 2 — das_root - { - das::string p = pick_at_dir(fs::path(das::getDasRoot().c_str()), rel_path); - if ( !p.empty() ) return p; - } - // tier 3 — baked absolute (legacy fallback) - return fallback_abs_path ? fallback_abs_path : ""; -} - -// Same three tiers for a native-path template's destination. It may be a PATTERN -// ("modules/dasLLVM/daslib/{path}.das"), so the probe is its directory prefix and the -// winner is that base with the whole pattern re-rooted onto it. Empty rel_pattern (no -// /modules/ segment at codegen) → tier 3. -static das::string resolve_native_path_dst ( const char * rel_pattern, const char * fallback_abs ) { - namespace fs = std::filesystem; - if ( rel_pattern && *rel_pattern ) { - std::string rel = rel_pattern; - size_t firstBrace = rel.find('{'); // npos → rfind searches the whole string - size_t dirEnd = rel.rfind('/', firstBrace); - std::string dirRel = dirEnd == std::string::npos ? "" : rel.substr(0, dirEnd); - das::vector bases; - das::string exeFile = jit_exe_file(); - if ( !exeFile.empty() ) { - fs::path exeDir = fs::path(exeFile.c_str()).parent_path(); - bases.push_back(exeDir.empty() ? fs::path(".") : exeDir); - } - das::string dasRoot = das::getDasRoot(); - if ( !dasRoot.empty() ) bases.push_back(fs::path(dasRoot.c_str())); - for ( auto & base : bases ) { - fs::path dir = dirRel.empty() ? base : base / dirRel; - if ( jit_path_exists(dir.string().c_str()) ) { - return das::string((base / rel).string().c_str()); - } - } - } - return fallback_abs ? fallback_abs : ""; -} -#else -// No file IO (DAS_NO_FILEIO): there is no filesystem to resolve a dynamic -// module against, and a build with no fio can't dlopen one anyway. Calling -// this is a logic error on such a platform, so abort loudly. -static das::string resolve_dynamic_module_path ( const char *, const char * ) { - std::abort(); -} -// Unlike a dynamic module, a native-path entry is an inert string-table registration -// (register_native_path is a stubbed no-op under DAS_NO_FILEIO), so a standalone exe -// that registered one must keep starting - hand back the baked path, never abort. -static das::string resolve_native_path_dst ( const char *, const char * fallback_abs ) { - return fallback_abs ? fallback_abs : ""; -} -#endif - -// Standalone-exe browser lifecycle (matches the interpreter's WebLoop in -// utils/daslang/main.cpp). A cross-compiled wasm graphics app exports -// init/update/shutdown, but its `main` is the blocking desktop driver. On the web -// the generated entry (llvm_exe.das) runs init() then calls jit_run_web_lifecycle -// instead of main: it installs an rAF loop on update() and runs shutdown() when -// update() returns false. updateFn/shutdownFn are jitted `RetT(Context*)` pointers; -// updateReturnsValue selects the void vs bool/int call signature (bool/int both -// return wasm i32, so they share one signature). -#ifdef __EMSCRIPTEN__ -#include -namespace { - struct JitWebLifecycle { - das::Context * ctx; - void * updateFn; - bool updateReturnsValue; - void * shutdownFn; // nullable - }; - void jit_web_lifecycle_tick ( void * arg ) { - auto * lc = (JitWebLifecycle *) arg; - bool keepGoing = true; - if ( lc->updateReturnsValue ) { - keepGoing = ((int32_t(*)(das::Context*))lc->updateFn)(lc->ctx) != 0; - } else { - ((void(*)(das::Context*))lc->updateFn)(lc->ctx); - } - if ( keepGoing ) lc->ctx->collectHeapIfMostlyFree(); - if ( !keepGoing ) { - emscripten_cancel_main_loop(); - if ( lc->shutdownFn ) ((void(*)(das::Context*))lc->shutdownFn)(lc->ctx); - } - } -} -#endif - -extern "C" { -// See JitWebLifecycle note above. Defined for every target so the symbol always -// links; only the emscripten build installs the rAF loop (others block, but the -// generated entry only emits this call on the wasm target). -DAS_API void jit_run_web_lifecycle ( das::Context * ctx, void * updateFn, - int32_t updateReturnsValue, void * shutdownFn ) { -#ifdef __EMSCRIPTEN__ - // arg leaks by design (lives the whole program). 0 = browser rAF cadence; - // true = simulate_infinite_loop, so this never returns and the entry's - // jit_shutdown() stays unreachable — the runtime persists for the rAF callbacks. - auto * lc = new JitWebLifecycle{ ctx, updateFn, updateReturnsValue != 0, shutdownFn }; - emscripten_set_main_loop_arg(jit_web_lifecycle_tick, lc, 0, true); -#else - bool keepGoing = true; - while ( keepGoing ) { - if ( updateReturnsValue ) keepGoing = ((int32_t(*)(das::Context*))updateFn)(ctx) != 0; - else ((void(*)(das::Context*))updateFn)(ctx); - } - if ( shutdownFn ) ((void(*)(das::Context*))shutdownFn)(ctx); -#endif -} - -// Standalone-exe main guard: the generated entry (llvm_exe.das) routes das main through this -// so a runtime exception prints and exits nonzero instead of unwinding out of the entry with -// no message (hosted runs get this boundary from their runWithCatch call sites; a bare exe -// had none — fix for the silent-exit-127 class). resultKind: 0 = void main, 1 = int main -// (value = exit code), 2 = bool main (true -> 0, false -> 1). -DAS_API int32_t jit_run_main_guarded ( das::Context * ctx, void * mainFn, int32_t resultKind ) { - int32_t rc = 0; - bool ok = ctx->runWithCatch([&]() { - if ( resultKind == 1 ) { - rc = ((int32_t(*)(das::Context*))mainFn)(ctx); - } else if ( resultKind == 2 ) { - rc = ((bool(*)(das::Context*))mainFn)(ctx) ? 0 : 1; - } else { - ((void(*)(das::Context*))mainFn)(ctx); - } - }); - if ( !ok ) { - das::TextPrinter tp; - tp << "EXCEPTION: " << (ctx->getException() ? ctx->getException() : "unknown") << "\n"; - tp.output(); // TextWriter's dtor only frees its buffer — output() is what prints - return 1; - } - return rc; -} - -DAS_API void das_ensure_environment () { - das::daScriptEnvironment::ensure(); -} - -DAS_API void jit_initialize_modules () { - // No need to initialize modules. JIT will generate required calls. - das::daScriptEnvironment::ensure(); -} - -DAS_API void jit_initialize_modules_done () { - das::Module::Initialize(); -} - -// Standalone-exe teardown. Emitted by inject_main right before main returns, -// so debug agents and modules drain while the runtime is alive. Without this, -// the static g_DebugAgents map dtor races ref_count_mutex during -// __cxa_finalize_ranges and terminate() fires (issue #2583). -DAS_API void jit_shutdown () { - das::Module::ShutdownStandalone(); -} - -DAS_API void * jit_register_dynamic_module ( const char * path, const char * mod_name ) { - return das::register_dynamic_module(path, mod_name, 0/*Quiet*/, nullptr, nullptr); -} - -DAS_API void jit_set_exe_file_for_test_( const char * (*fn)() ) { - g_jit_exe_file_for_test = fn; -} - -DAS_API void jit_set_path_exists_for_test_( bool (*fn)(const char *) ) { - g_jit_path_exists_for_test = fn; -} - -// Test entry point: invoke pure resolution and return the chosen path via -// caller-owned buffer. Returns true on success (buf populated, NUL-terminated), -// false if buffer is too small. Unit tests use this to assert resolution order -// without dlopening anything. -DAS_API bool jit_resolve_dynamic_module_path_for_test_ ( const char * rel_path, - const char * fallback_abs_path, - char * out_buf, - size_t buf_size ) { - das::string r = resolve_dynamic_module_path(rel_path, fallback_abs_path); - if ( r.size() + 1 > buf_size ) return false; - memcpy(out_buf, r.c_str(), r.size() + 1); - return true; -} - -DAS_API bool jit_resolve_native_path_dst_for_test_ ( const char * rel_pattern, - const char * fallback_abs, - char * out_buf, - size_t buf_size ) { - das::string r = resolve_native_path_dst(rel_pattern, fallback_abs); - if ( r.size() + 1 > buf_size ) return false; - memcpy(out_buf, r.c_str(), r.size() + 1); - return true; -} - -// Resolve and load a dynamic module. Used by standalone exes (emitted by -// inject_main in llvm_exe.das). -DAS_API void * jit_register_dynamic_module_resolve ( const char * rel_path, - const char * fallback_abs_path, - const char * mod_name ) { - das::string chosen = resolve_dynamic_module_path(rel_path, fallback_abs_path); - return das::register_dynamic_module(chosen.c_str(), mod_name, 0/*Quiet*/, nullptr, nullptr); -} - -// Emitted by inject_main after the per-module jit_register_dynamic_module_resolve calls. -// Those load Quiet (sibling DT_NEEDED ordering makes a first-attempt failure normal), so -// run the fixed-point retry, then treat anything still unloadable as fatal: every module -// the exe registers is required by its program, and continuing only defers the death to -// the first extern lookup, whose message no longer names the real cause. -DAS_API void jit_finalize_dynamic_modules () { - das::retry_pending_dynamic_modules(); - if ( int failed = das::report_pending_dynamic_modules() ) { - DAS_FATAL_ERROR("%d dynamic module(s) failed to load (see above).\n", failed); - } -} - -// ABI shim: -exe binaries emitted before the resolving form link this runtime dynamically -// and still import the 3-argument name. -DAS_API void jit_register_native_path ( const char * mod_name, const char * src_path, const char * dst_path ) { - das::register_native_path(mod_name, src_path, dst_path, nullptr, nullptr); -} - -// Emitted by inject_main (llvm_exe.das) for every native path the program compiled -// against: the destination is re-rooted onto / at run time, so a -// bundle built elsewhere finds its own modules/ instead of the build machine's. -DAS_API void jit_register_native_path_resolve ( const char * mod_name, const char * src_path, - const char * rel_dst, const char * fallback_abs_dst ) { - das::string chosen = resolve_native_path_dst(rel_dst, fallback_abs_dst); - das::register_native_path(mod_name, src_path, chosen.c_str(), nullptr, nullptr); -} -} diff --git a/tests-cpp/small/test_jit_module_resolve.cpp b/tests-cpp/small/test_jit_module_resolve.cpp index d6298a99e4..a30d6aae8e 100644 --- a/tests-cpp/small/test_jit_module_resolve.cpp +++ b/tests-cpp/small/test_jit_module_resolve.cpp @@ -7,7 +7,7 @@ #include #include -// Test entry points exposed by src/builtin/module_jit.cpp. +// Test entry points exposed by src/builtin/jit_runtime.cpp. extern "C" { DAS_API void jit_set_exe_file_for_test_( const char * (*fn)() ); DAS_API void jit_set_path_exists_for_test_( bool (*fn)(const char *) ); From 708a8b216c40c135fc78b0a25c5bcdae1879d278 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 10 Sep 2026 12:32:59 +0300 Subject: [PATCH 2/3] aot: the C++ emitter qualifies a name, escapes a literal, and keeps a field's module A member-pointer expression spelled its structure with the module's own name rather than aotModuleName, so a namespaced module - every standalone context - emitted `&Node::next` against an undeclared Node. A raw `?` in an emitted literal read as a trigraph. dumpAnnotationInfo clears its name table before writing rather than after. A used structure keeps its field types' modules alive: a handled field is the only thing reaching the module that declares it, and dropping that module dropped its aot_require include. A standalone context emits its own module's structures whether or not code reaches them, so an unreferenced one is walked too. --- daslib/aot_cpp.das | 59 ++++++++++++++++--- tests/aot/_standalone_emit_paths_fixture.das | 18 ++++++ .../aot/_standalone_orphan_struct_fixture.das | 15 +++++ tests/aot/test_standalone_emit.das | 31 ++++++++++ 4 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 tests/aot/_standalone_emit_paths_fixture.das create mode 100644 tests/aot/_standalone_orphan_struct_fixture.das diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index 0a9cebdea3..7f30d55520 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -764,11 +764,17 @@ class public DebugVarCache { } +// `\?` is `?` to a C compiler and hides the ??-sequences it would otherwise read as trigraphs +def private c_literal(s : string implicit) : string { + return escape(s) |> replace("?", "\\?") +} + + def private writeAnnotationArgInit(var sb : StringBuilderWriter; arg : AnnotationArgumentInfo) { if (arg.basicType == Type.tBool) { write(sb, "AnnotationArgumentInfo(\"{arg.name}\", {arg.bValue})") } elif (arg.basicType == Type.tString) { - write(sb, "AnnotationArgumentInfo(\"{arg.name}\", \"{escape(arg.sValue)}\")") + write(sb, "AnnotationArgumentInfo(\"{arg.name}\", \"{c_literal(arg.sValue)}\")") } elif (arg.basicType == Type.tInt) { write(sb, "AnnotationArgumentInfo(\"{arg.name}\", {arg.iValue})") } elif (arg.basicType == Type.tFloat) { @@ -855,6 +861,7 @@ class public AotDebugInfoHelper { def str() { return build_string() $(var writer) { verify(info2Name.empty() && info2TypeName.empty()) + annInfoNames |> clear() helper |> debug_helper_iter_structs($(_name, ti) { write(writer, "extern StructInfo {structInfoName(ti)};\n"); }); @@ -897,7 +904,6 @@ class public AotDebugInfoHelper { write(writer, "\}\n\n") info2Name.clear(); info2TypeName.clear(); - annInfoNames |> clear() } } @@ -2480,7 +2486,8 @@ class public CppAot : AstVisitor { } write(*ss, ", {vtype.get_variant_field_offset(field.fieldIndex)}, {field.fieldIndex}>::get("); } else { - let mod_name = (vtype.structType._module.name.empty() ? "" : string(vtype.structType._module.name) + "::"); + let ns = aotModuleName(vtype.structType._module); + let mod_name = (ns |> empty() ? "" : "{ns}::"); write(*ss, ",&{mod_name}{aotStructName(vtype.structType)}::{aotFieldName(string(field.name))}>::get("); } } @@ -2771,7 +2778,7 @@ class public CppAot : AstVisitor { write(*ss, "nullptr"); } else { peek(c.value) $(sv) { - write(*ss, "((char *) \"{escape(sv)}\")"); + write(*ss, "((char *) \"{c_literal(sv)}\")"); } } return c; @@ -4477,6 +4484,41 @@ def private canAotModule(mod : Module?) : bool { return ok } +def private markTypeModules(var used : table; var seen : table; t : TypeDeclPtr) { + if (t == null) { + return + } + if (t.annotation != null && t.annotation._module != null) { + used |> insert(t.annotation._module) + } + if (t.enumType != null && t.enumType._module != null) { + used |> insert(t.enumType._module) + } + markStructModules(used, seen, t.structType) + markTypeModules(used, seen, t.firstType) + markTypeModules(used, seen, t.secondType) + for (a in t.argTypes) { + markTypeModules(used, seen, a) + } +} + + +//! A used structure keeps its field types' modules alive: a field of a handled type is the only +//! thing that reaches the module owning it, and dropping that module drops its aot_require include. +def private markStructModules(var used : table; var seen : table; st : Structure?) { + if (st == null || (seen |> key_exists(st))) { + return + } + seen |> insert(st) + if (st._module != null) { + used |> insert(st._module) + } + for (fld in st.fields) { + markTypeModules(used, seen, fld._type) + } +} + + def private collectUsedModules(program : ProgramPtr) : table { //! Modules owning a function (externs included), struct, enum or handled type the runtime //! program still reaches. @@ -4496,11 +4538,14 @@ def private collectUsedModules(program : ProgramPtr) : table { visit(fn, adapter) } } + var seenStructs : table for (st in keys(utm.useStructs)) { - if (st._module != null) { - used |> insert(st._module) - } + markStructModules(used, seenStructs, st) } + // a standalone context emits its own module's structures even where nothing reaches them + program.getThisModule |> for_each_structure($(ps) { + markStructModules(used, seenStructs, ps) + }) for (en in keys(utm.useEnums)) { if (en._module != null) { used |> insert(en._module) diff --git a/tests/aot/_standalone_emit_paths_fixture.das b/tests/aot/_standalone_emit_paths_fixture.das new file mode 100644 index 0000000000..6d35318409 --- /dev/null +++ b/tests/aot/_standalone_emit_paths_fixture.das @@ -0,0 +1,18 @@ +options gen2 + +// safe navigation on a this-module struct, and a literal whose `??>` is a trigraph to a C +// compiler + +struct Node { + next : Node? +} + +[export] +def has_next(n : Node?) : bool { + return (n?.next) != null +} + +[export] +def puzzled() : string { + return "what??>" +} diff --git a/tests/aot/_standalone_orphan_struct_fixture.das b/tests/aot/_standalone_orphan_struct_fixture.das new file mode 100644 index 0000000000..2955196b88 --- /dev/null +++ b/tests/aot/_standalone_orphan_struct_fixture.das @@ -0,0 +1,15 @@ +options gen2 + +// a standalone context emits its own module's structures even where nothing reaches them, so +// an unreferenced one still has to keep its field's module alive + +require daslib/fio + +struct Orphan { + handle : FILE? +} + +[export] +def answer() : int { + return 42 +} diff --git a/tests/aot/test_standalone_emit.das b/tests/aot/test_standalone_emit.das index b16097914a..e33a5e0b4c 100644 --- a/tests/aot/test_standalone_emit.das +++ b/tests/aot/test_standalone_emit.das @@ -280,6 +280,37 @@ def test_standalone_emit(t : T?) { // nolint:STYLE038 - flat list of emit subc t |> success(find(source, "DECLARE_MODULE(Module_FIO);") >= 0, "Module_FIO is declared") } + t |> run("a structure nothing reaches still keeps its field's module") @(t : T?) { + let source = generate_standalone_source(t, "_standalone_orphan_struct_fixture", out_dir) + t |> success(find(source, " // require fio_core\n") >= 0, "an unreferenced struct's handled field still links fio_core") + } + + t |> run("a member pointer names a namespace the file declares") @(t : T?) { + let source = generate_standalone_source(t, "_standalone_emit_paths_fixture", out_dir) + // the qualifier the member pointer uses, whatever the entry module ends up being called + let at = find(source, "das_safe_navigation_ptr<") + t |> success(at >= 0, "the safe navigation emitted its member-pointer form") + if (at >= 0) { + let tail = slice(source, at) + let amp = find(tail, ",&") + let end = find(tail, "::Node::next>") + t |> success(amp >= 0 && end > amp, "the member pointer is spelled in the expected shape") + if (amp >= 0 && end > amp) { + let ns = slice(tail, amp + 2, end) + t |> success(!empty(ns), "the member pointer is qualified") + t |> success(find(source, "namespace {ns} \{") >= 0, + "`{ns}` is a namespace the file declares - the das module name is not one") + } + } + } + + t |> run("an emitted literal's trigraph is escaped") @(t : T?) { + let source = generate_standalone_source(t, "_standalone_emit_paths_fixture", out_dir) + t |> success(find(source, "\"what\\?\\?>\"") >= 0, "a literal's ?? is escaped, so no C compiler reads it as a trigraph") + t |> success(find(source, "\"what??>\"") < 0, "the raw sequence does not survive") + t |> equal(brace_balance(source), 0, "unbalanced braces in the generated C++") + } + t |> run("options gc and persistent_heap reach the context, so heap_collect is allowed there") @(t : T?) { let source = generate_standalone_source(t, "_standalone_gc_fixture", out_dir) t |> success(find(source, "policies.persistent_heap = true;") >= 0, "the persistent heap policy is on") From f79bacfc935c4d587c169ceff8ec2900ba20af7b Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 9 Sep 2026 02:00:23 +0300 Subject: [PATCH 3/3] dasbind: a bound extern has no body for any tier to run `daslang -aot script.das out.cpp -aot-macros` core dumped whenever the module graph carried a dasbind [extern], which covers every dasLLVM binding: Should be unreachable. We handled it in transformCall. Failed on: c_strlen. -aot-macros sets export_all, which force-marks every function of every module and skips removeUnusedSymbols, so an [extern] declaration - which has no body - reached simulate. Simulating has to succeed there, so the annotation answers a node that throws if anything actually calls it: transformCall rewrites every CALL site, so only an address-taken extern reaches the body, and an empty one would answer uninitialized stack. That placeholder is no more compilable than it is runnable, so the extern asks for no JIT the way it already asks for no AOT - taking its address otherwise walks the JIT emitter into a declaration it cannot type. --- src/builtin/module_builtin_dasbind.cpp | 52 ++++++++------------------ tests/dasbind/test_extern_abi.das | 12 ++++++ 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/builtin/module_builtin_dasbind.cpp b/src/builtin/module_builtin_dasbind.cpp index fea1f07b61..77f54c65d4 100644 --- a/src/builtin/module_builtin_dasbind.cpp +++ b/src/builtin/module_builtin_dasbind.cpp @@ -618,6 +618,7 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { fun->stub = true; fun->userScenario = true; fun->noAot = true; // TODO: generate custom C++ to invoke the call directly + fun->requestNoJit = true; // the body is a placeholder - transformCall rewrites the call sites // parse annotation arguments auto [is_ok, ba] = parseExternArgs(args, err); if ( !is_ok ) { @@ -699,42 +700,21 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { } return newCallExpr; } - virtual SimNode * simulate ( Context * /*context*/, Function * fun, const AnnotationArgumentList & /*args*/, string & /*err*/ ) override { - if (is_in_completion()) return nullptr; - DAS_FATAL_ERROR("Should be unreachable. We handled it in transformCall. Failed on: %s.", fun->name.c_str()); - // // All validation is in apply(). This path is only reached for late/opengl functions. - // auto [is_ok, ba] = parseExternArgs(args, err); - // DAS_ASSERTF(is_ok, "Should have failed in apply"); - // void * libhandle = nullptr; - // if ( !ba.library.empty() ) { - // libhandle = bindDynamicLibrary(ba.library); - // if ( !libhandle && !ba.late && ba.api!=ApiType::api_opengl ) { - // err = "can't load library " + ba.library; - // return nullptr; - // } - // } - // void * funptr = nullptr; - // if ( !ba.late ) { - // if ( ba.api==ApiType::api_opengl ) { - // funptr = openGlGetFunctionAddress(ba.fn_name.c_str()); - // } - // if ( !funptr ) { - // funptr = getFunctionAddress(libhandle, ba.fn_name.c_str()); - // } - // if ( !funptr ) { - // err = "can't find function " + ba.fn_name + " in library " + ba.library; - // return nullptr; - // } - // } - // uint64_t code = lateBind(ba.fn_name, ba.library, funptr); - // auto wrp = computeWrapper(fun); - // if ( ba.api==ApiType::api_opengl ) { - // return context->code->makeNode(fun->at,code,wrp,funptr); - // } - // if ( ba.late ) { - // return context->code->makeNode(fun->at,code,wrp,funptr); - // } - // return context->code->makeNode(fun->at,code,wrp,funptr); + // transformCall rewrites every CALL to the bound extern, so the body is reached only + // without a call site to rewrite - an address-taken extern. Simulating still has to + // SUCCEED (-aot-macros marks and simulates every function), but the body must refuse: + // an empty one answers uninitialized stack. + virtual SimNode * simulate ( Context * context, Function * fun, const AnnotationArgumentList &, string & ) override { + if ( is_in_completion() ) return nullptr; + struct SimNode_NoIndirect : SimNode { + SimNode_NoIndirect ( const LineInfo & at ) : SimNode(at) {} + virtual vec4f eval ( Context & ctx ) override { + ctx.throw_error_at(debugInfo, "a dasbind extern has no body to call - it is reached " + "through its call site, which transformCall rewrites, not through a function pointer"); + return v_zero(); + } + }; + return context->code->makeNode(fun->at); } #endif }; diff --git a/tests/dasbind/test_extern_abi.das b/tests/dasbind/test_extern_abi.das index 7071b03a35..428bf0c40b 100644 --- a/tests/dasbind/test_extern_abi.das +++ b/tests/dasbind/test_extern_abi.das @@ -83,6 +83,18 @@ def test_extern_argument_passing(t : T?) { let fv = 1.5 t |> equal(probe_ref_result(1l, 2l, 3l, 4l, 5l, 6l, 7l, fv), 1.5) } + t |> run("an address-taken extern refuses at eval - transformCall rewrites call sites, not pointers") @(t : T?) { + let fp = @@probe_regs7 + var caught = false + try { + invoke(fp, int8(1), int16(2), 3, 4l, 0.5, 0.25lf, 6) + } recover { + caught = true + } + t |> success(caught, "the body throws rather than answering uninitialized stack") + t |> equal(probe_regs7(int8(1), int16(2), 3, 4l, 0.5, 0.25lf, 6), 4367l, "and the call site still goes through the binding") + } + t |> run("the call survives the extern being applied again by another program in the process") @(t : T?) { var inscope access <- make_file_access("") using() $(var mg : ModuleGroup) {