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/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/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_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/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 *) ); 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") 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) {