diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index 8e3e21fbd3..dd195149df 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -3798,7 +3798,7 @@ class public CppAot : AstVisitor { } return empty(bif.cppName); } - if (func.flags.noAot || func.flags.aotHybrid) return true; + if (func.flags.noAot || func.flags.aotHybrid || func.moreFlags.requestJit) return true; return func._module != program.getThisModule; } def needsArgPassType(argType : TypeDeclPtr) { diff --git a/doc/reflections/das2rst.das b/doc/reflections/das2rst.das index 3aafcfcadf..712e0e939e 100644 --- a/doc/reflections/das2rst.das +++ b/doc/reflections/das2rst.das @@ -207,7 +207,7 @@ get_value|insert_clone|emplace_new|insert_default|emplace_default|get_with_defau group_by_regex("Bit operations", mod, %regex~(popcnt|clz|ctz|mul128|__bit_set)$%%), group_by_regex("Intervals", mod, %regex~(interval)$%%), group_by_regex("RTTI", mod, %regex~(class_rtti_size)$%%), - hide_group(group_by_regex("Jit", mod,%regex~(invoke_code|.*jit.*)$%%)), + hide_group(group_by_regex("Jit", mod,%regex~(invoke_code|is_aot_function|.*jit.*)$%%)), group_by_regex("Initialization and finalization", mod, %regex~(using|clone|finalize)$%%), group_by_regex("Algorithms", mod, %regex~(swap|iter_range|count|ucount|long_iter_range)$%%), group_by_regex("Memset", mod, %regex~(memset.*)$%%), diff --git a/include/daScript/simulate/ARCHITECTURE.md b/include/daScript/simulate/ARCHITECTURE.md index 6b6932976e..6045109fba 100644 --- a/include/daScript/simulate/ARCHITECTURE.md +++ b/include/daScript/simulate/ARCHITECTURE.md @@ -16,6 +16,14 @@ the hot set; its cost is judged against the allocate/copy/rehash it rides. The ledger the checklist's hot-path rule routes to. Each entry: what was added, where, why correctness required it, and the alternative that was rejected. +- **The `adBySid` memo** (`simulate.h`: `adBySidMemo`, `Context::AdMemo`) - jitted code resolves a + block's annotation data by sid on every call, once per tick for an entity system; it measured + 212 cycles a call on enlisted's act stage. A 16-slot direct-mapped memo answers the repeats. + Cost: one load and one predictable branch comparing `adMemo.owner` against `tabAdLookup.get()`, + plus 256 bytes per `Context`, placed last so no offset the emitter bakes moves. Rejected + alternative: clearing the memo at every site that assigns `tabAdLookup` - cheaper to read, but + an invariant spread across four writers, one of which had already been missed. + - **CRT scalar transcendentals** (`sim_policy.h`) - the scalar float arms of `Exp`, `Exp2`, `Log2` and `Pow` call the CRT; the `vec4f` arms stay on the vecmath polynomials, where four lanes amortize the setup. The lane trick's `v_set_x`/`v_extract_x` round-trip is a diff --git a/include/daScript/simulate/aot_builtin_jit.h b/include/daScript/simulate/aot_builtin_jit.h index c689cfe6ea..699c649bde 100644 --- a/include/daScript/simulate/aot_builtin_jit.h +++ b/include/daScript/simulate/aot_builtin_jit.h @@ -18,6 +18,7 @@ namespace das { float4 das_invoke_code ( void * pfun, vec4f anything, void * cmres, Context * context ); bool das_is_jit_function ( const Func func ); + bool das_is_aot_function ( const Func func ); bool das_has_jit_fastpath ( const Func func ); bool das_remove_jit ( const Func func ); bool das_instrument_jit ( void * pfun, const Func func, const LineInfo & info, Context & context ); diff --git a/include/daScript/simulate/simulate.h b/include/daScript/simulate/simulate.h index 677cc17aa3..b50c0146fb 100644 --- a/include/daScript/simulate/simulate.h +++ b/include/daScript/simulate/simulate.h @@ -537,6 +537,24 @@ namespace das DAS_ASSERT(it!=tabAdLookup->end()); return it->second; } + // Jitted code resolves a block's annotation data by sid on every call - once per ES tick - + // and the map misses cache on each distinct sid. It is immutable after buildADLookup and a + // Context is single-threaded, so a direct-mapped memo answers the repeat lookups. + __forceinline uint64_t adBySidMemo ( uint64_t sid ) { + if ( !sid || !tabAdLookup ) return 0; + if ( adMemo.owner != tabAdLookup.get() ) { + memset(adMemo.sid, 0, sizeof(adMemo.sid)); + memset(adMemo.val, 0, sizeof(adMemo.val)); + adMemo.owner = tabAdLookup.get(); + } + uint32_t slot = uint32_t((sid * 0x9E3779B97F4A7C15ull) >> (64u - AdMemo::BITS)); + if ( adMemo.sid[slot]==sid ) return adMemo.val[slot]; + auto it = tabAdLookup->find(sid); + uint64_t ad = it!=tabAdLookup->end() ? it->second : 0; + adMemo.sid[slot] = sid; + adMemo.val[slot] = ad; + return ad; + } __forceinline SimFunction * fnByMangledName ( uint64_t mnh ) { if ( mnh==0 ) return nullptr; auto it = tabMnLookup->find(mnh); @@ -922,6 +940,14 @@ namespace das void *llvm_context; }; JitContext deleteJITOnFinish = {}; + // last on purpose: the emitter bakes the offsets above into every jitted function + struct AdMemo { + static constexpr uint32_t BITS = 4; + const void * owner = nullptr; + uint64_t sid[1u< deleteUponFinish; }; diff --git a/modules/dasLLVM/daslib/llvm_aot.das b/modules/dasLLVM/daslib/llvm_aot.das index e5e3370f5a..dffbaba566 100644 --- a/modules/dasLLVM/daslib/llvm_aot.das +++ b/modules/dasLLVM/daslib/llvm_aot.das @@ -10,6 +10,8 @@ require llvm/daslib/llvm_exe require llvm/daslib/llvm_dll_utils require daslib/ast_boost require daslib/rtti +require daslib/safe_addr +require strings //! LLVM-AOT registration pass. After the JIT visitor has generated + optimized the module (as for a //! normal exe), this adds the offline-AOT-object registration layer so a statically-linked .o binds @@ -101,3 +103,37 @@ def public build_llvm_aot_ctors(ctx : LLVMContextRef; mod : LLVMOpaqueModule?; v let glob_ctor = emit_aot_object_globinit(ctx, mod, types, funcs, uids) return emit_aot_register_ctor_dtor(types, funcs, uids, prog, glob_ctor) } + +//! Names, for the caller to panic on, the DllName.glob() address globals nothing fills at load: LLVM +//! folds their loads to null, proves the body UB and drops it to a zero-length `unreachable` that +//! shares an address with the next function, so das_aot_register binds that hash to alien code. +def public collect_unresolved_address_globals(mod : LLVMOpaqueModule?) : array { + var bad : array + var glob = LLVMGetFirstGlobal(mod) + while (glob != null) { + let init = LLVMGetInitializer(glob) + if (init != null && LLVMIsNull(init) != 0) { + var name_len = 0ul + let name = LLVMGetValueName2(glob, safe_addr(name_len)) + if (name |> ends_with(" glob")) { + var loaded = false + var escaped = false + var use = LLVMGetFirstUse(glob) + while (use != null) { + let user = LLVMGetUser(use) + if (LLVMIsALoadInst(user) != null) { + loaded = true + } else { + escaped = true // stored into, or the address itself is passed on + } + use = LLVMGetNextUse(use) + } + if (loaded && !escaped) { + bad |> push(clone_string(name)) + } + } + } + glob = LLVMGetNextGlobal(glob) + } + return <- bad +} diff --git a/modules/dasLLVM/daslib/llvm_boost.das b/modules/dasLLVM/daslib/llvm_boost.das index 653232fc0b..341ee7e2a2 100644 --- a/modules/dasLLVM/daslib/llvm_boost.das +++ b/modules/dasLLVM/daslib/llvm_boost.das @@ -183,27 +183,6 @@ def StructType(types : PrimitiveTypes?; var fields : array) { } -def LLVMBuildSRemInt32(builder : LLVMOpaqueBuilder?; var types : PrimitiveTypes?; lhs, rhs : LLVMOpaqueValue?; name : string) { - // convert lhs % rhs to lhs - int(double(lhs)/double(rhs))*rhs - let l = LLVMBuildSIToFP(builder, lhs, types.t_double, "") - let r = LLVMBuildSIToFP(builder, rhs, types.t_double, "") - let d = LLVMBuildFDiv(builder, l, r, "") - let i = LLVMBuildFPToSI(builder, d, types.t_int32, "") - let m = LLVMBuildMul(builder, i, rhs, "") - return LLVMBuildSub(builder, lhs, m, name) -} - - -def LLVMBuildURemUInt32(builder : LLVMOpaqueBuilder?; var types : PrimitiveTypes?; lhs, rhs : LLVMOpaqueValue?; name : string) { - // convert lhs % rhs to lhs - uint(double(lhs)/double(rhs))*rhs - let l = LLVMBuildUIToFP(builder, lhs, types.t_double, "") - let r = LLVMBuildUIToFP(builder, rhs, types.t_double, "") - let d = LLVMBuildFDiv(builder, l, r, "") - let i = LLVMBuildFPToUI(builder, d, types.t_int32, "") - let m = LLVMBuildMul(builder, i, rhs, "") - return LLVMBuildSub(builder, lhs, m, name) -} - def LLVMIsVector3(typ : LLVMOpaqueType?) : bool { return (LLVMGetTypeKind(typ) == LLVMTypeKind.LLVMVectorTypeKind) && (LLVMGetVectorSize(typ) == 3u) } diff --git a/modules/dasLLVM/daslib/llvm_exe.das b/modules/dasLLVM/daslib/llvm_exe.das index c5f910af45..1dc887ec8e 100644 --- a/modules/dasLLVM/daslib/llvm_exe.das +++ b/modules/dasLLVM/daslib/llvm_exe.das @@ -413,6 +413,18 @@ class public CollectExternVisitor : AstVisitor { } } + // `new [[Handle...]]` allocates through the annotation the same way ExprNew does, and the + // emitter names that slot after the ascend node - so it needs its own resolution. + def override preVisitExprAscend(expr : ExprAscend?) { + assume subT = expr.subexpr._type + if (subT.isHandle) { + let name = uid.get_ascend_new(expr) + if (initialize(name)) { + register_new_from_annotation(name, subT.annotation) + } + } + } + def override preVisitExprNew(expr : ExprNew?) { if (expr.typeexpr.isHandle) { let name = uid.get_new(expr.typeexpr) diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 9afa96a429..9702dbf54f 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -3256,17 +3256,9 @@ class public LlvmJitVisitor : AstVisitor { } } elif (expr.op == "%") { if (opType.isSignedInteger || (opType.isVectorType && opType.vectorBaseType == Type.tInt)) { - if (opType.baseType == Type.tInt) { - setE(expr, LLVMBuildSRemInt32(g_builder, types, left, right, "")) - } else { - setE(expr, LLVMBuildSRem(g_builder, left, right, "")) - } + setE(expr, LLVMBuildSRem(g_builder, left, right, "")) } elif (opType.isUnsignedInteger || (opType.isVectorType && opType.vectorBaseType == Type.tUInt)) { - if (opType.baseType == Type.tUInt) { - setE(expr, LLVMBuildURemUInt32(g_builder, types, left, right, "")) - } else { - setE(expr, LLVMBuildURem(g_builder, left, right, "")) - } + setE(expr, LLVMBuildURem(g_builder, left, right, "")) } elif (opType.isFloatOrDouble || (opType.isVectorType && opType.vectorBaseType == Type.tFloat)) { setE(expr, LLVMBuildFRem(g_builder, left, right, "")) } else { @@ -3274,17 +3266,9 @@ class public LlvmJitVisitor : AstVisitor { } } elif (expr.op == "%=") { if (opType.isSignedInteger || (opType.isVectorType && opType.vectorBaseType == Type.tInt)) { - if (opType.baseType == Type.tInt) { - setE(expr, LLVMBuildStoreAligned(g_builder, LLVMBuildSRemInt32(g_builder, types, r2v_left, right, ""), left, uint(expr.left._type.alignOf))) - } else { - setE(expr, LLVMBuildStoreAligned(g_builder, LLVMBuildSRem(g_builder, r2v_left, right, ""), left, uint(expr.left._type.alignOf))) - } + setE(expr, LLVMBuildStoreAligned(g_builder, LLVMBuildSRem(g_builder, r2v_left, right, ""), left, uint(expr.left._type.alignOf))) } elif (opType.isUnsignedInteger || (opType.isVectorType && opType.vectorBaseType == Type.tUInt)) { - if (opType.baseType == Type.tUInt) { - setE(expr, LLVMBuildStoreAligned(g_builder, LLVMBuildURemUInt32(g_builder, types, r2v_left, right, ""), left, uint(expr.left._type.alignOf))) - } else { - setE(expr, LLVMBuildStoreAligned(g_builder, LLVMBuildURem(g_builder, r2v_left, right, ""), left, uint(expr.left._type.alignOf))) - } + setE(expr, LLVMBuildStoreAligned(g_builder, LLVMBuildURem(g_builder, r2v_left, right, ""), left, uint(expr.left._type.alignOf))) } elif (opType.isFloatOrDouble || (opType.isVectorType && opType.vectorBaseType == Type.tFloat)) { setE(expr, LLVMBuildStoreAligned(g_builder, LLVMBuildFRem(g_builder, r2v_left, right, ""), left, uint(expr.left._type.alignOf))) } else { diff --git a/modules/dasLLVM/daslib/llvm_jit_code.das b/modules/dasLLVM/daslib/llvm_jit_code.das index 2cc9f5b840..4cf992501d 100644 --- a/modules/dasLLVM/daslib/llvm_jit_code.das +++ b/modules/dasLLVM/daslib/llvm_jit_code.das @@ -1,6 +1,7 @@ options gen2 options indenting = 4 options strict_smart_pointers = false +options no_global_variables = false module llvm_jit_code shared private diff --git a/modules/dasLLVM/daslib/llvm_jit_intrin.das b/modules/dasLLVM/daslib/llvm_jit_intrin.das index 63a36cb51b..b40a72438d 100644 --- a/modules/dasLLVM/daslib/llvm_jit_intrin.das +++ b/modules/dasLLVM/daslib/llvm_jit_intrin.das @@ -7,6 +7,7 @@ options unsafe_table_lookup = false // intrinsic emitters carry per-function lowering contracts (gates, exactness preconditions, // saturation bounds) that don't compress to the 3-line cap — keep them at the emitters options _comment_hygiene = false +options no_global_variables = false module llvm_jit_intrin shared private diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index ca7780097e..db7a7c2a2a 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -2,6 +2,7 @@ options gen2 options indenting = 4 options strict_smart_pointers = false options stack = 4_194_304 +options no_global_variables = false require llvm/daslib/llvm_boost require llvm/daslib/llvm_dll_utils @@ -36,11 +37,11 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // invalidates cached DLLs (e.g. edits to llvm_jit.das, llvm_macro.das, llvm_jit_common.das, // runtime helper ABI, default target triple). Cache filenames fold this in, so a bump // makes every previously written DLL miss the cache on the next run and get GC'd. -let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x59ul // aarch64 hosts append +i8mm when cpu_supports says so (0x58: [hint(unsafe_division_check)] drops the sdiv/srem guards) +let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x5aul // srem/urem for 32-bit %, Context grew the adBySid memo (0x59: aarch64 hosts append +i8mm when cpu_supports says so) // Read by tests-cpp/small/test_jit_emitter_pin.cpp: FNV-1a64 of the emitter sources // (normalized to LF; file list in the test) -let LLVM_JIT_EMITTER_HASH : uint64 = 0xfcc23a40234a227ul +let LLVM_JIT_EMITTER_HASH : uint64 = 0x5a3d11a87421b38eul let JIT_FNV_PRIME : uint64 = 1099511628211ul @@ -187,6 +188,8 @@ def jit_env_salt(opt_level : int; size_level : int; emit_prologue : bool; debug_ } h = (h ^ uint64(CONTEXT_OFFSET_OF_GLOBALS)) * JIT_FNV_PRIME h = (h ^ uint64(CONTEXT_OFFSET_OF_SHARED)) * JIT_FNV_PRIME + h = (h ^ uint64(CONTEXT_OFFSET_OF_STOP_FLAGS)) * JIT_FNV_PRIME + h = (h ^ uint64(CONTEXT_OFFSET_OF_EVAL_TOP)) * JIT_FNV_PRIME // full host-CPU identity when the TargetMachine bakes it - the cpuid bits below cover only the matrix-tier features; a wider box's object is an illegal instruction on a narrower one if (use_host_cpu) { let host_cpu = LLVMGetHostCPUName() @@ -687,17 +690,22 @@ def private run_split_codegen(prog : Program?; ctx : Context?; funcs : array var disabled : array var visitorDisabled = 0 @@ -821,7 +829,7 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL // In object mode emit the whole used, non-noAot set (all modules + // per-program generic instantiations), mirroring C++ AOT whole-program // coverage; noAot functions interpret as Program::linkCppAot skips them. - if (fun.flags.used && (jit_all_functions || rqj) && (!emit_aot_object || !fun.flags.noAot)) { + if (fun.flags.used && (!emit_aot_object || !fun.flags.noAot) && jit_selects(fun, rqj, aot_host, jit_all_functions, ctx)) { if (!fun.moreFlags.requestNoJit) { disableJitVisitor.disable = false // if DisableJitVisitor ever grows a preVisitFunction check again, it must @@ -965,6 +973,14 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL aot_dtor = pair._1 } generate_global_ctors_dtors(g_prim_t, emit_aot_object ? false : !(use_dll || gen_exe), aot_ctor, aot_dtor) + if (emit_aot_object) { + // before optimization: the folding this catches also deletes the evidence + let unresolved = collect_unresolved_address_globals(g_mod) + if (!(unresolved |> empty())) { + reset_jit_globals_after_failure() + panic("Internal jit error. AOT address globals left unresolved by the globinit ctor: {join(unresolved, ", ")}\n") + } + } if (g_jit_fast_math) { apply_fast_math_to_module(g_mod) // EXPERIMENT: stamp all FP ops fast (non-bit-exact ceiling) } @@ -1051,9 +1067,8 @@ def public run_jit(prog : Program?; var ctx : Context?) : bool { // nolint:STYL jit_log_report(funcs, recompile_prog, opt_level, tier_known, jit_split, split_parts, log_jit_time, totalTime, t_hash, t_init, t_declare, t_probe, t_irgen, t_opt, t_emit, t_install, t_finalize) } else { - // a -jit run that jitted NOTHING is indistinguishable from jit working (just slower) - - // the whole-program interpret must be loud, not inferred from timings - to_log(LOG_ERROR, "LLVM JIT: 0 functions to jit ({length(disabled)} marked no_jit, {visitorDisabled} disabled by content) - the WHOLE program runs interpreted\n") + // a -jit run that jitted NOTHING looks like jit working, only slower, so say it + to_log(LOG_INFO, "LLVM JIT: 0 functions to jit ({length(disabled)} marked no_jit, {visitorDisabled} disabled by content) - the WHOLE program runs interpreted\n") } return true } diff --git a/modules/dasLLVM/daslib/llvm_tune.das b/modules/dasLLVM/daslib/llvm_tune.das index cd3aef2f45..2d83e602d3 100644 --- a/modules/dasLLVM/daslib/llvm_tune.das +++ b/modules/dasLLVM/daslib/llvm_tune.das @@ -1,6 +1,10 @@ options gen2 options indenting = 4 options no_aot = true +options no_global_variables = false +// the sidecar reader hands back a table built from json values, which the aliasing pass sees as +// reaching json's null sentinel - a host that compiles with no_aliasing must still load the tuner +options no_aliasing = false module llvm_tune shared public diff --git a/modules/dasLLVM/daslib/llvm_user_modules.das b/modules/dasLLVM/daslib/llvm_user_modules.das index 593bd4307f..5a6ff6ec41 100644 --- a/modules/dasLLVM/daslib/llvm_user_modules.das +++ b/modules/dasLLVM/daslib/llvm_user_modules.das @@ -1,6 +1,7 @@ options gen2 options indenting = 4 options _dasllama_internal = true +options no_global_variables = false module llvm_user_modules shared private diff --git a/modules/dasUnitTest/test_handles.cpp b/modules/dasUnitTest/test_handles.cpp index 2351a980f6..43d33c03b6 100644 --- a/modules/dasUnitTest/test_handles.cpp +++ b/modules/dasUnitTest/test_handles.cpp @@ -370,6 +370,86 @@ bool tempArrayExample( const TArray & arr, return (arr.size == 1) && (strcmp(arr[0], "one") == 0); } +// A keyed handle whose index yields a POINTER, and null for a missing key - the shape an +// embedder's keyed container has (an ecs Object, a json object). Every annotation that overrides +// makeIndexType in tree yields a ref, so without this one nothing can reach the jit's pointer arm: +// a jit that loads through the returned pointer hands back the pointee instead, and dereferences +// null on a miss. +struct PtrSlots { + int a = 7; + int b = 9; +}; + +MAKE_TYPE_FACTORY(PtrSlots,PtrSlots); + +// the at`handle contract: ( handle, index natively typed, Context *, LineInfoArg * ) +static char * ptr_slots_at ( void * pSlots, char * key, Context *, LineInfoArg * ) { + auto * slots = (PtrSlots *) pSlots; + if ( !slots || !key ) return nullptr; + if ( key[0]=='a' && key[1]==0 ) return (char *) &slots->a; + if ( key[0]=='b' && key[1]==0 ) return (char *) &slots->b; + return nullptr; +} + +struct PtrSlotsAnnotation final : ManagedStructureAnnotation { + struct SimNode_PtrSlotsAt : SimNode_At { + DAS_PTR_NODE; + SimNode_PtrSlotsAt ( const LineInfo & at, SimNode * rv, SimNode * idx, uint32_t ofs ) + : SimNode_At(at, rv, idx, 0, ofs, 0, "PtrSlots[key]") {} + __forceinline char * compute ( Context & context ) { + auto * slots = value->evalPtr(context); + char * key = cast::to(index->eval(context)); + char * res = ptr_slots_at(slots, key, &context, nullptr); + return res ? res + offset : nullptr; + } + virtual SimNode * visit ( SimVisitor & vis ) override { + using TT = PtrSlots; + V_BEGIN(); + V_OP_TT(AtPtrSlots); + V_SUB(value); + V_SUB(index); + V_END(); + } + }; + PtrSlotsAnnotation ( ModuleLibrary & ml ) : ManagedStructureAnnotation("PtrSlots", ml, "PtrSlots") { + addField("a"); + addField("b"); + atType = makeType(ml); + } + virtual bool isIndexable ( const TypeDeclPtr & indexType ) const override { + return indexType->isSimpleType(Type::tString); + } + virtual TypeDeclPtr makeIndexType ( ExpressionPtr, ExpressionPtr ) const override { + return new TypeDecl(*atType); + } + virtual SimNode * simulateGetAt ( Context & context, const LineInfo & at, const TypeDeclPtr &, + ExpressionPtr rv, ExpressionPtr idx, uint32_t ofs ) const override { + return context.code->makeNode(at, simulateExpression(context, rv), + simulateExpression(context, idx), ofs); + } + virtual void * jitGetAt ( Type indexType ) const override { + // one index type only: the key. The jit passes it natively typed, same as the ref-yielding + // annotations do for their int indices. + return indexType==Type::tString ? (void *) &ptr_slots_at : nullptr; + } + virtual void gc_collect ( gc_root * target, gc_root * from ) override { + ManagedStructureAnnotation::gc_collect(target, from); + if ( atType ) atType->gc_collect(target, from); + } + virtual void visitTypeDecls ( const function & callback ) override { + ManagedStructureAnnotation::visitTypeDecls(callback); + if ( atType ) callback(atType); + } + TypeDeclPtr atType = nullptr; +}; + +void testPtrSlots(const TBlock & blk, Context * context, LineInfoArg * at) { + PtrSlots slots; + vec4f args[1]; + args[0] = cast::from(&slots); + context->invoke(blk, args, nullptr, at); +} + void testPoint3Array(const TBlock & blk, Context * context, LineInfoArg * at) { Point3Array arr; for (int32_t x = 0; x != 10; ++x) { @@ -605,6 +685,10 @@ Module_UnitTest::Module_UnitTest() : Module("UnitTest") { addCtorAndUsing(*this, lib, "Point3Array", "Point3Array"); addExtern(*this, lib, "testPoint3Array", SideEffects::modifyExternal, "testPoint3Array"); + // keyed handle whose index yields a pointer - the jit's pointer arm (see PtrSlotsAnnotation) + addAnnotation(new PtrSlotsAnnotation(lib)); + addExtern(*this, lib, "testPtrSlots", + SideEffects::modifyExternal, "testPtrSlots"); addExtern(*this, lib, "testNotLocalObject", SideEffects::modifyExternal, "testNotLocalObject"); addExtern(*this, lib, "testCMRES", diff --git a/src/builtin/module_builtin_runtime.cpp b/src/builtin/module_builtin_runtime.cpp index b52a590b8e..4cb467cece 100644 --- a/src/builtin/module_builtin_runtime.cpp +++ b/src/builtin/module_builtin_runtime.cpp @@ -1981,6 +1981,13 @@ namespace das return simfn->code && simfn->code->rtti_node_isJit(); } + // linkCppAot binds every function it can from the AOT library before the simulate macros run, + // so this separates the AOT-covered set from what is left to interpret or jit. + bool das_is_aot_function ( const Func func ) { + auto simfn = func.PTR; + return simfn && simfn->aot; + } + bool das_jit_enabled ( Context * context, LineInfoArg * at ) { if ( !context->thisProgram ) context->throw_error_at(at, "can only query for jit during compilation"); return context->thisProgram->policies.jit_enabled; @@ -2847,6 +2854,9 @@ namespace das addExternInline(*this, lib, "is_jit_function", SideEffects::worstDefault, "das_is_jit_function") ->args({"function"}); + addExternInline(*this, lib, "is_aot_function", + SideEffects::worstDefault, "das_is_aot_function") + ->args({"function"}); addExternInline(*this, lib, "jit_enabled", SideEffects::none, "das_jit_enabled") ->args({"context","at"}); diff --git a/src/builtin/module_jit.cpp b/src/builtin/module_jit.cpp index 2ac6d3ab1e..7199fdbf10 100644 --- a/src/builtin/module_jit.cpp +++ b/src/builtin/module_jit.cpp @@ -743,9 +743,8 @@ extern "C" { } 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; + if ( !context ) return 0; + return context->adBySidMemo(sid); } DAS_API void jit_debug ( vec4f res, TypeInfo * typeInfo, char * message, Context * context, LineInfoArg * at ) { diff --git a/tests-cpp/small/test_ad_memo_invalidation.cpp b/tests-cpp/small/test_ad_memo_invalidation.cpp new file mode 100644 index 0000000000..7d26e0526d --- /dev/null +++ b/tests-cpp/small/test_ad_memo_invalidation.cpp @@ -0,0 +1,21 @@ +#include +#include "daScript/daScript.h" + +using namespace das; + +TEST_CASE("adBySidMemo invalidates itself when tabAdLookup is replaced") { + Context ctx(1024); + const uint64_t sid = 0x1234567890abcdefull; + + ctx.tabAdLookup = make_shared>(); + (*ctx.tabAdLookup)[sid] = 111; + CHECK_EQ(ctx.adBySidMemo(sid), 111ull); + CHECK_EQ(ctx.adBySidMemo(sid), 111ull); + + auto firstMap = ctx.tabAdLookup; + ctx.tabAdLookup = make_shared>(); + (*ctx.tabAdLookup)[sid] = 222; + CHECK_EQ(ctx.adBySidMemo(sid), 222ull); + + CHECK_EQ(ctx.adBySidMemo(sid ^ 1ull), 0ull); +} diff --git a/tests/jit_tests/handle_ptr_index.das b/tests/jit_tests/handle_ptr_index.das new file mode 100644 index 0000000000..0bd0281578 --- /dev/null +++ b/tests/jit_tests/handle_ptr_index.das @@ -0,0 +1,28 @@ +options gen2 +require dastest/testing_boost + +require UnitTest + +//! A keyed handle (UnitTest::PtrSlots) indexes by string and yields `int?`, null for a missing key. +//! The jit's at`handle hook returns that pointer as the value of the expression: loading through it +//! hands back the pointee instead, and dereferences null on a miss. Every other annotation in tree +//! yields a ref from makeIndexType, so this is the only test that reaches the pointer arm. + +def check_slots(t : T?; var slots : PtrSlots) { + t |> success(!jit_enabled() || is_jit_function(@@ < (t : T?; var slots : PtrSlots) : void > check_slots)) + let pa = slots["a"] + let pb = slots["b"] + t |> success(pa != null) + t |> success(pb != null) + t |> equal(7, *pa) + t |> equal(9, *pb) + // a missing key is a null pointer, and evaluating the expression must not read through it + t |> success(slots["zz"] == null) +} + +[test] +def test_handle_ptr_index(t : T?) { + testPtrSlots() $(var slots : PtrSlots) { + check_slots(t, slots) + } +}