From fc7b94e6f193aec3ac81ab6b267bde629723a121 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 17:01:03 -0800 Subject: [PATCH 01/11] jit: a reinterpret between bool and integer is the byte, not a bitcast The cast emitted `bitcast i1 to i8`, which reads true as 0xff and fails the verifier when it feeds a call. A bool source is zero-extended to the integer width; an integer source compares its low byte with zero. Fixes #4471 Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/daslib/llvm_jit.das | 9 +++- modules/dasLLVM/daslib/llvm_jit_plan.das | 4 +- modules/dasLLVM/daslib/llvm_jit_run.das | 2 +- tests/jit_tests/bool_reinterpret_byte.das | 51 +++++++++++++++++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 tests/jit_tests/bool_reinterpret_byte.das diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index e0ce20ab71..18c576bc7e 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -5800,7 +5800,14 @@ class public LlvmJitVisitor : AstVisitor { } elif ((ect.isPointer || ect.isString) && subT.isInteger) { res = LLVMBuildIntToPtr(g_builder, subE, type_to_llvm_type(ect), "cast_i_p") } elif (!ect.isRefType && !subT.isRefType) { - if (ect.sizeOf == subT.sizeOf) { + if (ect.isBool && subT.isInteger) { + let low_byte = (subT.sizeOf > 1 + ? LLVMBuildTrunc(g_builder, subE, types.t_int8, "trunc_cast") : subE) + res = LLVMBuildICmp(g_builder, LLVMIntPredicate.LLVMIntNE, low_byte, + LLVMConstNull(LLVMTypeOf(low_byte)), "cast_bool") + } elif (subT.isBool && ect.isInteger) { + res = LLVMBuildZExt(g_builder, subE, type_to_llvm_type(ect), "zext_cast") + } elif (ect.sizeOf == subT.sizeOf) { res = LLVMBuildBitCast(g_builder, subE, type_to_llvm_type(ect), "cast_r") } elif (ect.sizeOf > subT.sizeOf) { if (subT.isSignedInteger) { diff --git a/modules/dasLLVM/daslib/llvm_jit_plan.das b/modules/dasLLVM/daslib/llvm_jit_plan.das index a09d1191b0..c7a10ce451 100644 --- a/modules/dasLLVM/daslib/llvm_jit_plan.das +++ b/modules/dasLLVM/daslib/llvm_jit_plan.das @@ -31,8 +31,8 @@ require jit // 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. -// Current entry: module-cache dependencies carry a FileAccess flag. -let public LLVM_JIT_CODEGEN_VERSION : uint64 = 0xA5ul +// Current entry: a reinterpret between bool and an integer is its byte, not a bitcast. +let public LLVM_JIT_CODEGEN_VERSION : uint64 = 0xA6ul let public JIT_FNV_PRIME : uint64 = 1099511628211ul diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 66095be9ab..058013ed03 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -37,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // 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 = 0xd6cc1b27dd0ba41dul +let LLVM_JIT_EMITTER_HASH : uint64 = 0x9cb413735cd3c57cul [arch(at = "../ARCHITECTURE_FAST_MATH.md#never-fast-math")] def private apply_fast_math_to_module(m : LLVMOpaqueModule?) { diff --git a/tests/jit_tests/bool_reinterpret_byte.das b/tests/jit_tests/bool_reinterpret_byte.das new file mode 100644 index 0000000000..f7c3d0cae4 --- /dev/null +++ b/tests/jit_tests/bool_reinterpret_byte.das @@ -0,0 +1,51 @@ +options gen2 +require dastest/testing_boost + +def to_byte(v : bool) : uint8 => unsafe(reinterpret(v)) + +def to_bool(v : uint8) : bool => unsafe(reinterpret(v)) + +def int_to_bool(v : int) : bool => unsafe(reinterpret(v)) + +var g_byte_source = 0u8 +var g_int_source = 0 + +def store(var x : auto(TT)&; v : TT) { + unsafe { + memset8(addr(x), reinterpret(v), 1) + } +} + +[test] +def test_a_bool_reinterpreted_to_uint8_is_its_byte(t : T?) { + t |> equal(1, int(to_byte(true))) + t |> equal(0, int(to_byte(false))) + t |> equal(true, to_bool(1u8)) + t |> equal(false, to_bool(0u8)) + var b = false + store(b, true) + t |> equal(true, b) + var u = 0u8 + store(u, 7u8) + t |> equal(7, int(u)) +} + +//! a bool's truth by branching on it, not by comparing its byte +def truth(b : bool) : int => b ? 1 : 0 + +//! a bool byte other than 0 or 1 is defined only under the JIT, which +//! compares the low byte with zero; the interpreter and AOT read it +//! as a C++ bool +[test] +def test_a_runtime_integer_reinterpreted_to_bool_reads_its_low_byte(t : T?) { + g_int_source = 256 + t |> equal(0, truth(int_to_bool(g_int_source))) + g_int_source = 1 + t |> equal(1, truth(int_to_bool(g_int_source))) + if (jit_enabled()) { + g_byte_source = 2u8 + t |> equal(1, truth(to_bool(g_byte_source))) + g_int_source = 2 + t |> equal(1, truth(int_to_bool(g_int_source))) + } +} From 468304860b3da15f67837baf852c62efdaea461a Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 17:40:22 -0800 Subject: [PATCH 02/11] infer: a return inside a try or recover arm is a compile error The JIT compiles the two arms as block functions called through jit_try_recover, so a return in an arm left the arm and the function answered wrong, while the interpreter returned from the function. There is no lowering for the shape without a return slot threaded through the arm's captures, so the front end now rejects it on every tier (error 30299, naming the fix: set a variable in the arm and return after the pair). A closure's own return inside the arm is the closure's and stays allowed. The flow test keeps its break and continue cases; the return cases are gone with the semantics. Fixes #4462 Co-Authored-By: Claude Fable 5.1 --- doc/source/reference/language/statements.rst | 4 + include/daScript/ast/ast_infer_type.h | 3 + include/daScript/ast/ast_serializer.h | 2 +- include/daScript/ast/compilation_errors.h | 1 + modules/dasLLVM/daslib/llvm_jit.das | 4 +- modules/dasLLVM/daslib/llvm_jit_run.das | 4 +- .../integration/test_dynamic_rendering.das | 5 +- skills/daslang/SKILL.md | 7 +- src/ast/ast_infer_type.cpp | 10 ++ src/ast/ast_infer_type_op.cpp | 5 + tests/jit_tests/return_in_try_recover.das | 20 ++++ tests/language/div_by_zero.das | 10 +- tests/language/failed_return_in_recover.das | 16 +++ tests/language/failed_return_in_try.das | 16 +++ tests/language/try_recover_flow.das | 113 ------------------ tree-sitter-daslang/examples/example.das | 4 +- .../gen1-to-gen2/tests/_fixture_gen1_stmt.das | 5 +- 17 files changed, 100 insertions(+), 129 deletions(-) create mode 100644 tests/jit_tests/return_in_try_recover.das create mode 100644 tests/language/failed_return_in_recover.das create mode 100644 tests/language/failed_return_in_try.das diff --git a/doc/source/reference/language/statements.rst b/doc/source/reference/language/statements.rst index fb25a6579c..157ba1f9fa 100644 --- a/doc/source/reference/language/statements.rst +++ b/doc/source/reference/language/statements.rst @@ -773,6 +773,10 @@ dereference or out-of-bounds array access: print("recovered from panic\n") } +A ``return`` inside the ``try`` block or the ``recover`` block is a compile error: set a +variable in the arm and return after the pair. A closure's own ``return`` inside the arm is +not affected. + .. warning:: ``try``/``recover`` is **not** a general error-handling mechanism and should not be used for control flow. It is designed for catching runtime panics (similar to Go's diff --git a/include/daScript/ast/ast_infer_type.h b/include/daScript/ast/ast_infer_type.h index d22860611c..ba55251563 100644 --- a/include/daScript/ast/ast_infer_type.h +++ b/include/daScript/ast/ast_infer_type.h @@ -50,6 +50,8 @@ namespace das { vector assumeStack; vector assumeTypeStack; vector inFinally; + int tryDepth = 0; + vector tryDepthAtClosure; vector inInfer; ExprReturn * oneReturn; int32_t returnCount = 0; @@ -551,6 +553,7 @@ namespace das { virtual ExpressionPtr visit(ExprClone *expr) override; // ExprTryCatch void preVisit(ExprTryCatch *expr) override; + ExpressionPtr visit(ExprTryCatch *expr) override; // ExprReturn bool inferReturnType(TypeDeclPtr &resType, ExprReturn *expr); bool isVoidReturnValueSettled(Expression *subexpr) const; diff --git a/include/daScript/ast/ast_serializer.h b/include/daScript/ast/ast_serializer.h index db327d9b21..852c5cd638 100644 --- a/include/daScript/ast/ast_serializer.h +++ b/include/daScript/ast/ast_serializer.h @@ -362,7 +362,7 @@ namespace das { AstSerializer & serializeModule ( Module & module, bool already_exists ); static constexpr uint32_t getVersion () { - return 245; // 245: inline splices are typed and no infer pass follows them + return 246; // 246: a return inside a try or recover arm no longer infers } void serializeProgram ( ProgramPtr program, ModuleGroup & libGroup ) noexcept; diff --git a/include/daScript/ast/compilation_errors.h b/include/daScript/ast/compilation_errors.h index a4d2df472a..5f4828e761 100644 --- a/include/daScript/ast/compilation_errors.h +++ b/include/daScript/ast/compilation_errors.h @@ -297,6 +297,7 @@ namespace das , invalid_empty_name = 30296 // AST node with empty name; usually a macro emission bug , invalid_distinct_type = 30297 // distinct underlying must be a workhorse type, not another distinct , invalid_with_module = 30298 // with (module foo) names a module that is not part of the program + , return_in_try_recover = 30299 // a return inside a try or recover arm // missing_* diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 18c576bc7e..64fa08dd24 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -6534,7 +6534,9 @@ class public LlvmJitVisitor : AstVisitor { def override preVisitExprReturn(expr : ExprReturn?) { if (thisBlock != null) { if (expr.returnFunc != null) { - to_log(LOG_WARNING, "LLVM JIT: return inside try/recover leaves the try arm, not the function. at {describe(expr.at)}\n") + let fix = "move the return after the try" + failed_E(expr, + "a return inside a try/recover arm is not supported under -jit; {fix}") } pre_visit_expr_return(expr, isCMRESType(thisBlock.returnType)) } elif (thisFunc != null) { diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 058013ed03..06be891171 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -37,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // 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 = 0x9cb413735cd3c57cul +let LLVM_JIT_EMITTER_HASH : uint64 = 0xd98e7cd2c75d9a66ul [arch(at = "../ARCHITECTURE_FAST_MATH.md#never-fast-math")] def private apply_fast_math_to_module(m : LLVMOpaqueModule?) { @@ -732,7 +732,7 @@ def public run_jit(prog : Program?; var ctx : Context?; cli_opts : JitCliOptions g_dynamic_lib_handle = get_dll_by_path(output_path) let (missing, disabling) = get_dll_missing(funcs, disabled, g_dynamic_lib_handle, uids) let dll_changed = !(missing |> empty() && disabling |> empty()) - recompile_prog = dll_changed || dump_ir + recompile_prog = dll_changed || dump_ir || plan.remarks if (recompile_prog) { g_dynamic_lib_handle.reset() to_log(LOG_INFO, "LLVM JIT: DLL cache miss, codegen for {output_path}.dll\n") diff --git a/modules/dasVulkan/tests/integration/test_dynamic_rendering.das b/modules/dasVulkan/tests/integration/test_dynamic_rendering.das index af41597413..7aedd86f3e 100644 --- a/modules/dasVulkan/tests/integration/test_dynamic_rendering.das +++ b/modules/dasVulkan/tests/integration/test_dynamic_rendering.das @@ -13,12 +13,13 @@ require vulkan require vulkan/vulkan_boost def require_dynamic_rendering_panics() : bool { + var panicked = false try { require_dynamic_rendering("gate test") } recover { - return true + panicked = true } - return false + return panicked } [test] diff --git a/skills/daslang/SKILL.md b/skills/daslang/SKILL.md index 079e5a00cc..dd8b29d2ef 100644 --- a/skills/daslang/SKILL.md +++ b/skills/daslang/SKILL.md @@ -227,9 +227,10 @@ first when the pointers are non-owning. function), unwinds to the nearest enclosing `try { } recover { }`; the `recover` block runs and execution continues after it, in every execution tier. `recover` binds nothing - there is no exception value to match on; the message is `this_context().last_exception` - (`require daslib/rtti`), with a trailing newline. Under the JIT the pair compiles to a - builtin call, and a `return` inside the `try` block is a compile error there (the - interpreter allows it). With no enclosing `try` the error unwinds out of the script: the + (`require daslib/rtti`), with a trailing newline. A `return` inside the `try` or the + `recover` block is a compile error on every tier (a closure's own `return` inside the arm + is fine): set a variable in the arm and return after the pair. With no enclosing `try` the + error unwinds out of the script: the daslang CLI prints it and exits nonzero, an embedding host gets it back as a context error. Never design APIs around panic-as-control-flow. - `assert(cond, "msg")` may be stripped in release; `verify(cond, "msg")` always runs. The diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index aab82587ca..5b92fc0b46 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -3773,6 +3773,8 @@ namespace das { } resetBlockFlow(block); if (block->isClosure) { + tryDepthAtClosure.push_back(tryDepth); + tryDepth = 0; if (block->returnType) { blocks.push_back(block); TypeDecl::clone(block->type, block->returnType); @@ -3923,6 +3925,10 @@ namespace das { popVarStack(); scopes.pop_back(); inFinally.pop_back(); + if (block->isClosure) { + tryDepth = tryDepthAtClosure.back(); + tryDepthAtClosure.pop_back(); + } if (block->isClosure && block->returnType) { blocks.pop_back(); if (block->list.size()) { @@ -4720,6 +4726,10 @@ namespace das { } } ExpressionPtr InferTypes::visit(ExprReturn *expr) { + if (tryDepth > 0) { + error("return inside a try/recover arm; set a variable in the arm and return after the pair", "", "", + expr->at, CompilationError::return_in_try_recover); + } if (blocks.size()) { ExprBlock *block = blocks.back(); expr->block = block; diff --git a/src/ast/ast_infer_type_op.cpp b/src/ast/ast_infer_type_op.cpp index 9d16587264..d10b41cb3f 100644 --- a/src/ast/ast_infer_type_op.cpp +++ b/src/ast/ast_infer_type_op.cpp @@ -1127,8 +1127,13 @@ namespace das { } void InferTypes::preVisit(ExprTryCatch *expr) { Visitor::preVisit(expr); + tryDepth++; if (func) func->hasTryRecover = true; } + ExpressionPtr InferTypes::visit(ExprTryCatch *expr) { + tryDepth--; + return Visitor::visit(expr); + } } diff --git a/tests/jit_tests/return_in_try_recover.das b/tests/jit_tests/return_in_try_recover.das new file mode 100644 index 0000000000..cc1f346ce9 --- /dev/null +++ b/tests/jit_tests/return_in_try_recover.das @@ -0,0 +1,20 @@ +options gen2 +require dastest/testing_boost public + +def returns_from_a_block_inside_try : int { + var r = 0 + try { + let f = $() : int { + return 3 + } + r = invoke(f) + } recover { + pass + } + return r +} + +[test] +def test_a_return_from_a_closure_inside_a_try_arm_is_the_closures_own(t : T?) { + t |> equal(returns_from_a_block_inside_try(), 3) +} diff --git a/tests/language/div_by_zero.das b/tests/language/div_by_zero.das index d30c47c4a8..421abbe9da 100644 --- a/tests/language/div_by_zero.das +++ b/tests/language/div_by_zero.das @@ -5,25 +5,27 @@ require dastest/testing_boost public [sideeffects, no_jit] def div_by_zero(num : auto(numT)) : numT { var ex = false + var res = num try { - return num / numT(0) + res = num / numT(0) } recover { ex = true } assert(ex) - return num + return ex ? num : res } [sideeffects, no_jit] def mod_by_zero(num : auto(numT)) : numT { var ex = false + var res = num try { - return num % numT(0) + res = num % numT(0) } recover { ex = true } assert(ex) - return num + return ex ? num : res } [test] diff --git a/tests/language/failed_return_in_recover.das b/tests/language/failed_return_in_recover.das new file mode 100644 index 0000000000..8d5cbcffa8 --- /dev/null +++ b/tests/language/failed_return_in_recover.das @@ -0,0 +1,16 @@ +options gen2 +expect 30299 + +def f(n : int) : int { + try { + panic("x") + } recover { + return -1 + } + return n +} + +[export] +def main { + print("{f(1)}\n") +} diff --git a/tests/language/failed_return_in_try.das b/tests/language/failed_return_in_try.das new file mode 100644 index 0000000000..87232e3a22 --- /dev/null +++ b/tests/language/failed_return_in_try.das @@ -0,0 +1,16 @@ +options gen2 +expect 30299 + +def f(n : int) : int { + try { + return n * 2 + } recover { + pass + } + return n +} + +[export] +def main { + print("{f(1)}\n") +} diff --git a/tests/language/try_recover_flow.das b/tests/language/try_recover_flow.das index 4edf91a0af..8103d75a3d 100644 --- a/tests/language/try_recover_flow.das +++ b/tests/language/try_recover_flow.das @@ -2,8 +2,6 @@ options gen2 require dastest/testing_boost public var g_rounds = 5 -var g_void_seen = 0 -var g_refs <- [11, 22] [export, no_jit] def break_in_for : int { @@ -111,107 +109,6 @@ def try_without_exit : int { return seen } -[export, no_jit] -def return_in_try : int { - try { - return g_rounds * 2 - } recover { - } - return -1 -} - -[export, no_jit] -def void_return_in_try { - try { - g_void_seen = 1 - return - } recover { - } - g_void_seen = 2 -} - -[export, no_jit] -def return_from_loop_in_try : int { - try { - var s = 0 - for (i in range(0, g_rounds)) { - s += i - return s if (s > 2) - } - } recover { - } - return -1 -} - -[export, no_jit] -def return_from_nested_try : int { - try { - try { - return g_rounds + 4 - } recover { - } - } recover { - } - return -1 -} - -[export, no_jit] -def continue_and_return : int { - var acc = 0 - for (i in range(0, g_rounds)) { - try { - continue if (i == 1) - return acc if (i == 3) - acc += i - } recover { - } - acc += 100 - } - return -1 -} - -[no_jit] -def moved_array : array { - try { - return <- [1, 2, g_rounds] - } recover { - } - var e : array - return <- e -} - -[export] -def return_moved_array : int { - let got <- moved_array() - return length(got) -} - -def call_it(blk : block<(x : int) : int>) : int => invoke(blk, 5) - -[export, no_jit] -def return_ref_in_try : int & { - try { - unsafe { - return g_refs[0] - } - } recover { - } - unsafe { - return g_refs[1] - } -} - -[export, no_jit] -def block_return_in_try : int { - try { - return call_it() $(x : int) : int { - return x * 2 - } - } recover { - } - return -1 -} - [test] def test_try_recover_control_flow(t : T?) { t |> equal(break_in_for(), 22, "break leaves the for loop") @@ -221,14 +118,4 @@ def test_try_recover_control_flow(t : T?) { t |> equal(inner_loop_break(), 23, "break in a loop inside the try stays inner") t |> equal(inner_only_break(), 102, "a loop break with no loop around the try needs no exit") t |> equal(try_without_exit(), 5, "try with no exit is unaffected") - t |> equal(return_in_try(), 10, "return carries its value out") - void_return_in_try() - t |> equal(g_void_seen, 1, "a void return needs no slot") - t |> equal(return_from_loop_in_try(), 3, "return crosses a loop inside the try") - t |> equal(return_from_nested_try(), 9, "nested try shares the return slot") - t |> equal(continue_and_return(), 202, "one try with continue and return") - t |> equal(return_moved_array(), 3, "moved array survives the return slot") - t |> equal(block_return_in_try(), 10, "a block return is not the function's") - return_ref_in_try() = 99 - t |> equal(g_refs[0], 99, "a returned reference rides the slot as a pointer to the same storage") } diff --git a/tree-sitter-daslang/examples/example.das b/tree-sitter-daslang/examples/example.das index 12222c3f60..2382a4d259 100644 --- a/tree-sitter-daslang/examples/example.das +++ b/tree-sitter-daslang/examples/example.das @@ -67,12 +67,14 @@ def main(args : array; verbose = false) : int { counter++ } let shape = new Shape(origin = Point(x = 1.0, y = 2.0)) + var failed = false try { unsafe { delete shape } } recover { - return -1 + failed = true } + return -1 if (failed) return total if (total > 0) else return 0 } diff --git a/utils/gen1-to-gen2/tests/_fixture_gen1_stmt.das b/utils/gen1-to-gen2/tests/_fixture_gen1_stmt.das index 2530f7f18b..ce548acec0 100644 --- a/utils/gen1-to-gen2/tests/_fixture_gen1_stmt.das +++ b/utils/gen1-to-gen2/tests/_fixture_gen1_stmt.das @@ -12,11 +12,12 @@ def classify(n : int) return total def risky() + var rc = 0 try classify(3) // may fail recover - return -1 - return 0 + rc = -1 + return rc def commented(n : int) // what this one is for return n + 1 // and what it gives back From 2e0f5d1b7eeec23415d3ddfb1efebc6df4031d6a Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 17:23:57 -0800 Subject: [PATCH 03/11] jit: a pointer reinterprets to any integer width through ptrtoint reinterpret(p) built `trunc ptr to i32`, which the verifier refuses; only the 64-bit target went through ptrtoint. Every integer target does now, and ptrtoint to a narrower width keeps the low bits, as the interpreter does. The JIT half of #4108; the interpreter half is the reinterpret eval-slot fix on the optimizer branch. Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/daslib/llvm_jit.das | 2 +- modules/dasLLVM/daslib/llvm_jit_plan.das | 5 ++++- modules/dasLLVM/daslib/llvm_jit_run.das | 2 +- tests/jit_tests/reinterpret_ptr_to_uint.das | 16 ++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/jit_tests/reinterpret_ptr_to_uint.das diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 64fa08dd24..1af58ab6ab 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -5781,7 +5781,7 @@ class public LlvmJitVisitor : AstVisitor { res = LLVMBuildPointerCast(g_builder, subE, type_to_llvm_type(ect), "cast_p") } elif (ect.baseType == Type.tFixedArray && subT.isPointer) { res = LLVMBuildPointerCast(g_builder, subE, LLVMPointerType(type_to_llvm_type(ect), 0u), "") - } elif (is_int64_or_uint64(ect.baseType) && subT.isPointer) { + } elif (ect.isInteger && subT.isPointer) { res = LLVMBuildPtrToInt(g_builder, subE, type_to_llvm_type(ect), "cast_p_i") } elif (subT.baseType == Type.tFunction && (ect.isPointer || is_int64_or_uint64(ect.baseType))) { // reinterpret(f) diff --git a/modules/dasLLVM/daslib/llvm_jit_plan.das b/modules/dasLLVM/daslib/llvm_jit_plan.das index c7a10ce451..a994a56770 100644 --- a/modules/dasLLVM/daslib/llvm_jit_plan.das +++ b/modules/dasLLVM/daslib/llvm_jit_plan.das @@ -31,7 +31,10 @@ require jit // 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. -// Current entry: a reinterpret between bool and an integer is its byte, not a bitcast. +// Current entry: bool<->integer reinterprets are the byte, a +// pointer reinterprets to any integer width through ptrtoint, a +// pointer difference is inline arithmetic, a global lookup is +// speculatable. let public LLVM_JIT_CODEGEN_VERSION : uint64 = 0xA6ul let public JIT_FNV_PRIME : uint64 = 1099511628211ul diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 06be891171..3d0f19f0f0 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -37,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // 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 = 0xd98e7cd2c75d9a66ul +let LLVM_JIT_EMITTER_HASH : uint64 = 0x32084e5fcb9ed3b5ul [arch(at = "../ARCHITECTURE_FAST_MATH.md#never-fast-math")] def private apply_fast_math_to_module(m : LLVMOpaqueModule?) { diff --git a/tests/jit_tests/reinterpret_ptr_to_uint.das b/tests/jit_tests/reinterpret_ptr_to_uint.das new file mode 100644 index 0000000000..2cb70a286e --- /dev/null +++ b/tests/jit_tests/reinterpret_ptr_to_uint.das @@ -0,0 +1,16 @@ +options gen2 +require dastest/testing_boost + +def low_bits(p : uint8?) : uint => unsafe(reinterpret(p)) + +def all_bits(p : uint8?) : uint64 => unsafe(reinterpret(p)) + +[test] +def test_a_pointer_reinterpreted_to_a_32_bit_integer_is_its_low_bits(t : T?) { + var data : uint8[64] + let p = unsafe(addr(data[0])) + let q : uint8? = unsafe(p + 4) + t |> equal(all_bits(q) - all_bits(p), 4ul) + t |> equal(low_bits(q) - low_bits(p), 4u) + t |> equal(low_bits(p), uint(all_bits(p) & 0xfffffffful)) +} From ca25a00466b04eb7fa9402e7a65c0221bdc52ebc Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 21:04:37 -0800 Subject: [PATCH 04/11] jit: default assigned into a fixed array zeroes the whole array The make-struct arm zeroed max(elements, 1) strides; for default there are no elements and a stride is one element, so only the first element was cleared and `a = default` left the rest. The arm now zeroes the declared shape's size when no element is provided and reads the element type through the fixed-array chain for the empty-tuple case, as the interpreter does. Found by the regex rewrite: a set reset through default<> under -jit kept its old bits. Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/daslib/llvm_jit.das | 8 +++- modules/dasLLVM/daslib/llvm_jit_run.das | 2 +- .../jit_tests/default_fixed_array_assign.das | 40 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 tests/jit_tests/default_fixed_array_assign.das diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 1af58ab6ab..389211e825 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -4535,11 +4535,15 @@ class public LlvmJitVisitor : AstVisitor { } } let stride = expr.makeType.stride - let emptyEmbeddedTuple = expr.makeType.baseType == Type.tTuple && empty(expr.structs) + var mkBaseT = expr.makeType + while (mkBaseT.baseType == Type.tFixedArray && mkBaseT.firstType != null) { + mkBaseT = mkBaseT.firstType + } + let emptyEmbeddedTuple = mkBaseT.baseType == Type.tTuple && empty(expr.structs) let partiallyInitStruct = !expr.makeFlags.doesNotNeedInit && !expr.makeFlags.initAllFields if ((partiallyInitStruct || emptyEmbeddedTuple) && stride != 0) { let total = expr.structs |> length - let bytes = max(total, 1) * stride + let bytes = total != 0 ? total * stride : expr.makeType.sizeOf let align = uint(expr.makeType.alignOf) LLVMBuildMemSet(g_builder, mks_ptr, 0ul, uint64(bytes), align) } diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 3d0f19f0f0..09ecfda4ae 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -37,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // 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 = 0x32084e5fcb9ed3b5ul +let LLVM_JIT_EMITTER_HASH : uint64 = 0xcd033aadf59a4e5dul [arch(at = "../ARCHITECTURE_FAST_MATH.md#never-fast-math")] def private apply_fast_math_to_module(m : LLVMOpaqueModule?) { diff --git a/tests/jit_tests/default_fixed_array_assign.das b/tests/jit_tests/default_fixed_array_assign.das new file mode 100644 index 0000000000..e88eff40c0 --- /dev/null +++ b/tests/jit_tests/default_fixed_array_assign.das @@ -0,0 +1,40 @@ +options gen2 +require dastest/testing_boost + +def zero_through_reference(var a : uint[8]) { + a = default +} + +[test] +def test_assigning_default_zeroes_the_whole_fixed_array(t : T?) { + t |> run("a local") @(t : T?) { + var a = fixed_array(1, 2, 3, 4) + a = default + t |> equal(a[0], 0) + t |> equal(a[1], 0, "every element, not only the first") + t |> equal(a[3], 0) + } + t |> run("through a var parameter") @(t : T?) { + var a : uint[8] + for (w in a) { + w = 0xffffffffu + } + zero_through_reference(a) + t |> equal(a[0], 0u) + t |> equal(a[7], 0u, "the last element too") + } + t |> run("a tuple element and a nested dimension") @(t : T?) { + var ts : tuple[2] + ts[1]._0 = 3 + ts = default[2]> + t |> equal(ts[1]._0, 0) + var ss : int[2][3] + ss[1][2] = 9 + ss = default + t |> equal(ss[1][2], 0) + } + t |> run("initializing from default is the control") @(t : T?) { + let a = default + t |> equal(a[3], 0) + } +} From 61ca72f5ab9e2ee3e49c28c836d1a15a3cce4d25 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 17:04:54 -0800 Subject: [PATCH 05/11] jit: a pointer difference is inline arithmetic, not a slot call `p - q` called i_das_ptr_diff through the builtin's slot with the stride as a run-time argument, so the division never folded; the intrinsic row emits ptrtoint, sub and a division by the constant stride. The issue's loop goes from 0.32 s to 0.0045 s. Fixes #4469 Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/CMakeLists.txt | 1 + modules/dasLLVM/daslib/llvm_jit_intrin.das | 10 +++ modules/dasLLVM/daslib/llvm_jit_run.das | 4 +- modules/dasLLVM/tests/_exe_test_common.das | 42 ++++++++++++ modules/dasLLVM/tests/_ptr_diff_root.das | 24 +++++++ modules/dasLLVM/tests/llvm_jit_debug_info.das | 65 +++++-------------- .../tests/llvm_jit_never_fast_math.das | 22 +------ .../tests/llvm_jit_ptr_diff_inline.das | 21 ++++++ 8 files changed, 116 insertions(+), 73 deletions(-) create mode 100644 modules/dasLLVM/tests/_ptr_diff_root.das create mode 100644 modules/dasLLVM/tests/llvm_jit_ptr_diff_inline.das diff --git a/modules/dasLLVM/CMakeLists.txt b/modules/dasLLVM/CMakeLists.txt index 2bd9e0f4ed..f0712d5d48 100644 --- a/modules/dasLLVM/CMakeLists.txt +++ b/modules/dasLLVM/CMakeLists.txt @@ -186,6 +186,7 @@ if(NOT DAS_TOOLS_DISABLED) modules/dasLLVM/tests/llvm_class_target_gates.das modules/dasLLVM/tests/llvm_gep_inbounds.das modules/dasLLVM/tests/llvm_jit_baseline.das modules/dasLLVM/tests/llvm_jit_debug_info.das modules/dasLLVM/tests/llvm_jit_global_lookup.das modules/dasLLVM/tests/llvm_jit_link.das + modules/dasLLVM/tests/llvm_jit_ptr_diff_inline.das modules/dasLLVM/tests/llvm_jit_sanitize.das modules/dasLLVM/tests/llvm_layout_key.das modules/dasLLVM/tests/llvm_second_context.das) das_lane_dastest_target(run_tests_llvm_tune COMMENT "Testing the llvm_tune framework" diff --git a/modules/dasLLVM/daslib/llvm_jit_intrin.das b/modules/dasLLVM/daslib/llvm_jit_intrin.das index 103214a585..abfe9e4a8b 100644 --- a/modules/dasLLVM/daslib/llvm_jit_intrin.das +++ b/modules/dasLLVM/daslib/llvm_jit_intrin.das @@ -37,6 +37,7 @@ let g_intrin_lookup <- { "$::i_das_ptr_sub" => @@intrinsic_das_ptr_sub, "$::i_das_ptr_inc" => @@intrinsic_das_ptr_inc, "$::i_das_ptr_dec" => @@intrinsic_das_ptr_dec, + "$::i_das_ptr_diff" => @@intrinsic_das_ptr_diff, "$::memcpy" => @@intrinsic_memcpy, "$::memmove" => @@intrinsic_memmove, "$::memset8" => @@intrinsic_memset8, @@ -1973,6 +1974,15 @@ def intrinsic_das_ptr_sub(var ctx : JitCtx; expr : ExprCallFunc?; arguments : ar return intrinsic_das_ptr_add(ctx, expr, args) } +def intrinsic_das_ptr_diff(var ctx : JitCtx; _expr : ExprCallFunc?; // nolint:LINT014 - table shape + arguments : array) : LLVMOpaqueValue? { + let a = LLVMBuildPtrToInt(ctx.builder, arguments[0], ctx.types.t_int64, "") + let b = LLVMBuildPtrToInt(ctx.builder, arguments[1], ctx.types.t_int64, "") + let bytes = LLVMBuildSub(ctx.builder, a, b, "") + let stride = LLVMBuildSExt(ctx.builder, arguments[2], ctx.types.t_int64, "") + return LLVMBuildSDiv(ctx.builder, bytes, stride, "ptr_diff") +} + def intrinsic_memcpy(var ctx : JitCtx; _expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { // nolint:LINT014 - mutable context matches the intrinsic table signature. return LLVMBuildMemCpy(ctx.builder, arguments[0], 1u, arguments[1], 1u, arguments[2]) } diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 09ecfda4ae..80c4b272bf 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -37,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // 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 = 0xcd033aadf59a4e5dul +let LLVM_JIT_EMITTER_HASH : uint64 = 0x352c33a08c4bfedbul [arch(at = "../ARCHITECTURE_FAST_MATH.md#never-fast-math")] def private apply_fast_math_to_module(m : LLVMOpaqueModule?) { @@ -732,7 +732,7 @@ def public run_jit(prog : Program?; var ctx : Context?; cli_opts : JitCliOptions g_dynamic_lib_handle = get_dll_by_path(output_path) let (missing, disabling) = get_dll_missing(funcs, disabled, g_dynamic_lib_handle, uids) let dll_changed = !(missing |> empty() && disabling |> empty()) - recompile_prog = dll_changed || dump_ir || plan.remarks + recompile_prog = dll_changed || dump_ir if (recompile_prog) { g_dynamic_lib_handle.reset() to_log(LOG_INFO, "LLVM JIT: DLL cache miss, codegen for {output_path}.dll\n") diff --git a/modules/dasLLVM/tests/_exe_test_common.das b/modules/dasLLVM/tests/_exe_test_common.das index e1ed6214b6..cfe9fb2aae 100644 --- a/modules/dasLLVM/tests/_exe_test_common.das +++ b/modules/dasLLVM/tests/_exe_test_common.das @@ -18,6 +18,48 @@ def das_exe() : string { def tail(s : string) : string => slice(s, max(0, length(s) - 400)) +//! the dumped IR of the function whose quoted name ends in +//! `name_end`; signature to closing brace +def function_ir(dump : string; name_end : string) : string { + let start = find(dump, "{name_end}\"(") + return "" if (start < 0) + let body = slice(dump, start) + let close = find(body, "\n}") + return close < 0 ? body : slice(body, 0, close) +} + +//! the entry block of a dumped function: everything before its +//! first branch +def entry_block_ir(fn_ir : string) : string { + let br = find(fn_ir, "\n br ") + return br < 0 ? fn_ir : slice(fn_ir, 0, br) +} + +//! one labelled basic block of a dumped function, label line +//! through the blank line after it +def named_block_ir(fn_ir : string; block_name : string) : string { + let start = find(fn_ir, "\n{block_name}:") + return "" if (start < 0) + let body = slice(fn_ir, start + 1) + let close = find(body, "\n\n") + return close < 0 ? body : slice(body, 0, close) +} + +//! the output of `root` run under -jit with --jit-dump and `extra` +//! after the separator, checked to exit 0; -jit-no-cache, since a +//! cache hit runs no codegen and prints no dump +def jit_dump(t : T?; root : string; extra : array) : string { + var args <- [das_exe(), "-no-module-cache", "-jit", "-jit-no-cache", root, "--", "--jit-dump"] + args |> push_from(extra) + var out = "" + var rc = 0 + with_child_log_info() { + rc = run_and_capture(args, out, 300.0) + } + t |> equal(0, rc, "the fixture runs under -jit: {tail(out)}") + return out +} + def run_capture(cmd : string; var out : string&; timeout = 300.0) : int { return unsafe(popen_timeout(cmd, timeout, $(f) { if (f != null) { diff --git a/modules/dasLLVM/tests/_ptr_diff_root.das b/modules/dasLLVM/tests/_ptr_diff_root.das new file mode 100644 index 0000000000..7fcabf087a --- /dev/null +++ b/modules/dasLLVM/tests/_ptr_diff_root.das @@ -0,0 +1,24 @@ +options gen2 + +struct S { + a : int64 + b : int64 +} + +//! the element distance between two pointers into one array +[never_inline, hint(noinline)] +def distance(p : S?; base : S?) : int => int(p - base) + +[never_inline, hint(noinline)] +def distance64(p : S?; base : S?) : int64 => p - base + +[export] +def main { + var arr : S[64] + unsafe { + let fwd = distance(addr(arr[5]), addr(arr[0])) + let back = distance(addr(arr[2]), addr(arr[7])) + print("DIST {fwd} {back}\n") + print("DIST64 {distance64(addr(arr[2]), addr(arr[7]))}\n") + } +} diff --git a/modules/dasLLVM/tests/llvm_jit_debug_info.das b/modules/dasLLVM/tests/llvm_jit_debug_info.das index 4588be7b3d..fa8384010d 100644 --- a/modules/dasLLVM/tests/llvm_jit_debug_info.das +++ b/modules/dasLLVM/tests/llvm_jit_debug_info.das @@ -1,21 +1,16 @@ options gen2 require dastest/testing_boost public require daslib/fio -require _spawn_common require daslib/strings_boost require strings require math +require _exe_test_common // The --jit-debug rail's DWARF, read off the emitted IR rather than off a debugger. // Shape and reasons: ARCHITECTURE_DEBUG_INFO.md sec.12. let private ROOT = "modules/dasLLVM/tests/_debug_info_root.das" -def private das_exe() : string { - let argv <- get_command_line_arguments() - return find(to_generic_path(argv[0]), "/") < 0 ? argv[0] : get_full_file_name(normalize(argv[0])) -} - def private out_tail(s : string) : string => slice(s, max(0, length(s) - 600)) def private count_of(s : string; needle : string) : int { @@ -28,21 +23,6 @@ def private count_of(s : string; needle : string) : int { return n } -//! the dumped IR of one function, by its dumped name - signature line to closing brace -def private function_ir(dump : string; name : string) : string { - let start = find(dump, "`{name}\"(") - return "" if (start < 0) - let body = slice(dump, start) - let close = find(body, "\n}") - return close < 0 ? body : slice(body, 0, close) -} - -//! the entry block of a dumped function: everything before its first branch -def private entry_block_ir(fn_ir : string) : string { - let br = find(fn_ir, "\n br ") - return br < 0 ? fn_ir : slice(fn_ir, 0, br) -} - //! the fixture's own DIFile line, whichever metadata number it landed on def private fixture_difile(dump : string) : string { var lines <- split(dump, "\n") @@ -54,7 +34,7 @@ def private fixture_difile(dump : string) : string { return "" } -let private HOT_IMPL = "hot Ci Cf CS`Lt`Point`Gt` CE`Lt`::Mode`Gt` Cs implementation" +let private HOT_IMPL = "`hot Ci Cf CS`Lt`Point`Gt` CE`Lt`::Mode`Gt` Cs implementation" let private FIXTURE_FOR_STATEMENT = "for (i in range(n))" @@ -76,15 +56,6 @@ def private fixture_line_of(needle : string) : int { let private FIXTURE_ARGS = ["n", "scale", "p", "m", "tag"] let private FIXTURE_LOCALS = ["acc", "f4", "i", "inner", "acc_ref", "spin", "dim3", "xs", "pair", "head", "node", "blocked"] -//! one labelled basic block of a dumped function, label line through the blank line after it -def private named_block_ir(fn_ir : string; block_name : string) : string { - let start = find(fn_ir, "\n{block_name}:") - return "" if (start < 0) - let body = slice(fn_ir, start + 1) - let close = find(body, "\n\n") - return close < 0 ? body : slice(body, 0, close) -} - //! the source line of the first !dbg attachment in a piece of IR, -1 when it carries none def private dbg_line_of(dump : string; ir : string) : int { let at = find(ir, ", !dbg !") @@ -99,18 +70,12 @@ def private dbg_line_of(dump : string; ir : string) : int { return comma < 0 ? -1 : to_int(slice(after, 0, comma)) } -def private jit_dump(t : T?; debug_info : bool) : string { - var args <- [das_exe(), "-no-module-cache", "-jit", "-jit-no-cache", ROOT, "--", - "--jit-opt-level=0", "--jit-dump"] +def private debug_info_dump(t : T?; debug_info : bool) : string { + var extra <- ["--jit-opt-level=0"] if (debug_info) { - args |> push("--jit-debug") - } - var out = "" - var rc = 0 - with_child_log_info() { - rc = run_and_capture(args, out, 300.0) + extra |> push("--jit-debug") } - t |> equal(0, rc, "the fixture runs under -jit: {out_tail(out)}") + let out = jit_dump(t, ROOT, extra) t |> success(find(out, "DEBUG_INFO_ROOT ") >= 0, "the fixture printed its witness: {out_tail(out)}") return out } @@ -118,7 +83,7 @@ def private jit_dump(t : T?; debug_info : bool) : string { [test] def test_debug_info_is_off_by_default(t : T?) { t |> run("no --jit-debug means no debug metadata at all") @(t : T?) { - let out = jit_dump(t, false) + let out = debug_info_dump(t, false) t |> equal(0, count_of(out, "!DICompileUnit"), "a default run emits no compile unit") t |> equal(0, count_of(out, "!DILocalVariable"), "a default run emits no variable metadata") t |> equal(0, count_of(out, "#dbg_declare("), "a default run emits no dbg records") @@ -128,7 +93,7 @@ def test_debug_info_is_off_by_default(t : T?) { [test] def test_one_compile_unit_per_module(t : T?) { t |> run("the whole module shares one compile unit, tagged C++") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) t |> equal(1, count_of(out, "distinct !DICompileUnit"), "one compile unit per module - a unit per function hides an inlined instance from a debugger's name lookup") t |> success(find(out, "language: DW_LANG_C_plus_plus") >= 0, @@ -139,7 +104,7 @@ def test_one_compile_unit_per_module(t : T?) { [test] def test_subprograms_carry_the_das_name(t : T?) { t |> run("a das function is named as its source names it, with no linkage name") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) t |> equal(2, count_of(out, "!DISubprogram(name: \"hot\""), "both halves of the function pair carry the das name:\n{out_tail(out)}") t |> equal(0, count_of(out, "linkageName:"), @@ -151,7 +116,7 @@ def test_subprograms_carry_the_das_name(t : T?) { [test] def test_locals_and_arguments_are_described(t : T?) { t |> run("every argument and local has a DILocalVariable in a lexical scope") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) for (arg, ix in FIXTURE_ARGS, count(1)) { t |> success(find(out, "!DILocalVariable(name: \"{arg}\", arg: {ix}") >= 0, "argument {arg} is described as parameter {ix}:\n{out_tail(out)}") @@ -170,7 +135,7 @@ def test_locals_and_arguments_are_described(t : T?) { [test] def test_a_global_is_described(t : T?) { t |> run("a das global reads in every function that touches it") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) //! a global has no static address - it is described at the pointer the function computed //! from its own ctx, so it lands as a variable of that function (arch doc sec.12.4) t |> success(find(out, "!DILocalVariable(name: \"g_calls\"") >= 0, @@ -183,7 +148,7 @@ def test_a_global_is_described(t : T?) { [test] def test_types_are_described(t : T?) { t |> run("a struct, an enum, a string and a vector each reach DWARF as themselves") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) t |> success(find(out, "tag: DW_TAG_structure_type, name: \"Point") >= 0, "the struct is a real structure type, not an opaque blob:\n{out_tail(out)}") t |> success(find(out, "tag: DW_TAG_member, name: \"x\"") >= 0, "member x is named") @@ -218,7 +183,7 @@ def test_types_are_described(t : T?) { [test] def test_source_paths_are_absolute(t : T?) { t |> run("a DIFile names an absolute path, so a debugger lists the source from any cwd") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) let line = fixture_difile(out) t |> success(!empty(line), "the dump carries a DIFile for the fixture:\n{out_tail(out)}") let open = find(line, "\"") @@ -233,7 +198,7 @@ def test_source_paths_are_absolute(t : T?) { [test] def test_hoisted_instructions_carry_no_stale_line(t : T?) { t |> run("the prologue carries no line, and every argument is declared in it") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) let ir = function_ir(out, HOT_IMPL) t |> success(!empty(ir), "the dump carries hot's implementation:\n{out_tail(out)}") let entry = entry_block_ir(ir) @@ -243,7 +208,7 @@ def test_hoisted_instructions_carry_no_stale_line(t : T?) { "all five arguments are declared in the entry block:\n{entry}") } t |> run("a loop latch carries the loop's own line, not its body's last statement") @(t : T?) { - let out = jit_dump(t, true) + let out = debug_info_dump(t, true) let ir = function_ir(out, HOT_IMPL) t |> success(!empty(ir), "the dump carries hot's implementation:\n{out_tail(out)}") let latch = named_block_ir(ir, "for_continue") diff --git a/modules/dasLLVM/tests/llvm_jit_never_fast_math.das b/modules/dasLLVM/tests/llvm_jit_never_fast_math.das index 9cd3bd6d89..52b4919d7b 100644 --- a/modules/dasLLVM/tests/llvm_jit_never_fast_math.das +++ b/modules/dasLLVM/tests/llvm_jit_never_fast_math.das @@ -1,7 +1,5 @@ options gen2 require dastest/testing_boost public -require daslib/fio -require _spawn_common require strings require daslib/rtti require daslib/ast @@ -12,17 +10,6 @@ require _exe_test_common // (modules/dasLLVM/ARCHITECTURE_FAST_MATH.md#never-fast-math). The check reads the dumped IR, so it // holds on every host - the numeric failure it prevents shows only on an AVX-512 target. -//! the dumped IR of the function whose quoted name ends in `name_end`; signature to closing brace -def private function_ir(dump : string; name_end : string) : string { - let start = find(dump, "{name_end}\"(") - if (start < 0) { - return "" - } - let body = slice(dump, start) - let close = find(body, "\n}") - return close < 0 ? body : slice(body, 0, close) -} - def private expect_unstamped(t : T?; ir, what : string) { t |> success(!empty(ir), "the dump carries {what}") t |> success(find(ir, "fmul double") >= 0, "{what} keeps its multiply:\n{ir}") @@ -31,14 +18,7 @@ def private expect_unstamped(t : T?; ir, what : string) { [test] def test_never_fast_math_keeps_flags_off(t : T?) { - let root = "modules/dasLLVM/tests/_never_fast_math_root.das" - var out = "" - var rc = 0 - with_child_log_info() { - // -jit-no-cache: a cache hit runs no codegen, so it prints no dump - rc = run_and_capture([das_exe(), "-no-module-cache", "-jit", "-jit-no-cache", root, "--", "--jit-dump"], out, 300.0) - } - t |> equal(0, rc, "the fixture runs under -jit: {tail(out)}") + let out = jit_dump(t, "modules/dasLLVM/tests/_never_fast_math_root.das", []) t |> success(find(out, "MACS 4 4 4") >= 0, "the fixture printed its three results: {tail(out)}") let fast = function_ir(out, "`fast_mac Cd Cd Cd implementation") t |> success(!empty(fast), "the dump carries fast_mac: {tail(out)}") diff --git a/modules/dasLLVM/tests/llvm_jit_ptr_diff_inline.das b/modules/dasLLVM/tests/llvm_jit_ptr_diff_inline.das new file mode 100644 index 0000000000..15708a74f6 --- /dev/null +++ b/modules/dasLLVM/tests/llvm_jit_ptr_diff_inline.das @@ -0,0 +1,21 @@ +options gen2 +require dastest/testing_boost public +require strings +require _exe_test_common + +//! a pointer difference lowers to i_das_ptr_diff(a, b, stride), +//! which the interpreter and the AOT inline; the dump shows the +//! JIT's form +[test] +def test_a_pointer_difference_is_arithmetic_not_a_call(t : T?) { + let out = jit_dump(t, "modules/dasLLVM/tests/_ptr_diff_root.das", []) + t |> success(find(out, "DIST 5 -5") >= 0, "the fixture printed both distances: {tail(out)}") + t |> success(find(out, "DIST64 -5") >= 0, + "the unnarrowed backward distance divides signed: {tail(out)}") + let ir = function_ir(out, + "`distance C1`Lt`S`Lt`S`Gt``Gt`? C1`Lt`S`Lt`S`Gt``Gt`? implementation") + t |> success(!empty(ir), "the dump carries distance: {tail(out)}") + t |> equal(-1, find(ir, "i_das_ptr_diff"), + "the difference is inline arithmetic, not a call through the i_das_ptr_diff slot:\n{ir}") + t |> success(find(ir, "sub i64") >= 0, "the difference subtracts the two addresses:\n{ir}") +} From 98793cbc356389be48f0ed10dcc64a4921f42c75 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 17:09:07 -0800 Subject: [PATCH 06/11] jit: a global lookup is speculatable and hoists out of a loop jit_get_global_mnh depends on the name hash and the context alone and cannot trap, so it carries `speculatable`; LLVM then hoists a lookup under a branch inside a loop to the preheader. The issue's loop goes from 0.405 s to 0.042 s, beside 0.035 s for the solid_context build. Fixes #4467 Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/ARCHITECTURE.md | 10 ++- modules/dasLLVM/daslib/llvm_jit_common.das | 9 ++- modules/dasLLVM/daslib/llvm_jit_run.das | 2 +- modules/dasLLVM/tests/_global_lookup_root.das | 36 +++++++++ .../dasLLVM/tests/llvm_jit_global_lookup.das | 79 ++++++++++--------- 5 files changed, 92 insertions(+), 44 deletions(-) diff --git a/modules/dasLLVM/ARCHITECTURE.md b/modules/dasLLVM/ARCHITECTURE.md index c785f4a653..9c06c430b3 100644 --- a/modules/dasLLVM/ARCHITECTURE.md +++ b/modules/dasLLVM/ARCHITECTURE.md @@ -195,10 +195,12 @@ shared one): the mangled-name hash is static, and the offset it names is added t globals base, which is what a cross-context call needs - the same JIT function runs on any context of the program, each with its own base, so the address cannot be baked. The emitter (`visitExprVar`, `daslib/llvm_jit.das`) emits the call where the variable is used and declares it -`memory(none)` (`daslib/llvm_jit_common.das`): the result depends on nothing but its arguments for -the lifetime of the context, so LLVM CSEs one use against a dominating one, hoists a loop's lookup -into the preheader, and - the call not being `speculatable` - never moves it ahead of a branch, so -a global written on one branch is looked up on that branch only. Every access to a global in a +`memory(none)` and `speculatable` (`daslib/llvm_jit_common.das`): the result depends on nothing but +its arguments for the lifetime of the context, and the probe neither traps nor writes, so LLVM CSEs +one use against a dominating one and hoists a loop's lookup into the preheader even when the access +sits on one arm of a branch inside the loop - one probe per call instead of one per taken iteration. +Outside a loop the optimizer does not speculate a call, so a global written on one branch of +straight-line code is still looked up on that branch only. Every access to a global in a function therefore shares one base pointer, which is what lets LLVM see `xs[j]` and `xs[j + 1]` as adjacent. Under `options solid_context` the address is instead `context->globals + stackTop`, computed once per function in the entry block. diff --git a/modules/dasLLVM/daslib/llvm_jit_common.das b/modules/dasLLVM/daslib/llvm_jit_common.das index 4099d28f4c..ce526b04b4 100644 --- a/modules/dasLLVM/daslib/llvm_jit_common.das +++ b/modules/dasLLVM/daslib/llvm_jit_common.das @@ -111,6 +111,7 @@ class public Attributes { nofree : LLVMOpaqueAttributeRef? norecurse : LLVMOpaqueAttributeRef? noalias : LLVMOpaqueAttributeRef? + speculatable : LLVMOpaqueAttributeRef? sext : LLVMOpaqueAttributeRef? zext : LLVMOpaqueAttributeRef? // memory(inaccessiblemem: readwrite) — for allocator-style runtime calls @@ -138,6 +139,7 @@ class public Attributes { nofree = ctx |> LLVMGetEnumAttribute("nofree") norecurse = ctx |> LLVMGetEnumAttribute("norecurse") noalias = ctx |> LLVMGetEnumAttribute("noalias") + speculatable = ctx |> LLVMGetEnumAttribute("speculatable") sext = ctx |> LLVMGetEnumAttribute("signext") zext = ctx |> LLVMGetEnumAttribute("zeroext") let memory_kind = LLVMGetEnumAttributeKindForName("memory") @@ -821,6 +823,7 @@ def public init_jit_state(cg_opt_level : uint; target_triple : string; host_feat assume memory_inaccessible_rw = attrs.memory_inaccessible_rw assume memory_arg_inaccessible_rw = attrs.memory_arg_inaccessible_rw assume memory_none = attrs.memory_none + assume speculatable = attrs.speculatable build_jit_types() // cold: opt sinks the failure block off the hot path; pairs with noreturn. @@ -842,9 +845,11 @@ def public init_jit_state(cg_opt_level : uint; target_triple : string; host_feat jit_fn_type($(ctx : void?; nArgs : int; types, at, args : void?) : void? {}), get_jit_string_builder_temp(), fixed_array(nounwind, willreturn)) jit_add_extern(FN_JIT_GET_GLOBAL_MNH, jit_fn_type($(mnh : int64; ctx : void?) : void? {}), - get_jit_get_global_mnh(), fixed_array(nounwind, willreturn, memory_none), (nocapture, [1])) + get_jit_get_global_mnh(), fixed_array(nounwind, willreturn, memory_none, speculatable), + (nocapture, [1])) jit_add_extern(FN_JIT_GET_SHARED_MNH, jit_fn_type($(mnh : int64; ctx : void?) : void? {}), - get_jit_get_shared_mnh(), fixed_array(nounwind, willreturn, memory_none), (nocapture, [1])) + get_jit_get_shared_mnh(), fixed_array(nounwind, willreturn, memory_none, speculatable), + (nocapture, [1])) // Wasm-only: resolves a handled-type (C++) field offset against the TARGET runtime's annotation // registry, into per-field globals at init. Declared unconditionally; only used when g_target_is_wasm. jit_add_extern(FN_JIT_GET_HANDLED_FIELD_OFFSET, diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 80c4b272bf..f17e35f0e7 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -37,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // 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 = 0x352c33a08c4bfedbul +let LLVM_JIT_EMITTER_HASH : uint64 = 0x15e51cb6a7e1e2e2ul [arch(at = "../ARCHITECTURE_FAST_MATH.md#never-fast-math")] def private apply_fast_math_to_module(m : LLVMOpaqueModule?) { diff --git a/modules/dasLLVM/tests/_global_lookup_root.das b/modules/dasLLVM/tests/_global_lookup_root.das index 95a9c1f577..9cd74639aa 100644 --- a/modules/dasLLVM/tests/_global_lookup_root.das +++ b/modules/dasLLVM/tests/_global_lookup_root.das @@ -35,6 +35,37 @@ def branchy(foo : bool) { } } +var lut : int[256] +var scale = 3 + +//! two globals read on one arm of a branch inside a loop: each +//! lookup leaves the loop +[export, sideeffects] +def sum_scaled(n : int) : int { + var s = 0 + for (i in range(n)) { + if ((i & 3) != 0) { + s += lut[i & 255] * scale + } + } + return s +} + +let shared shared_scale <- [3] + +//! a shared global read on one arm of a branch inside a loop: its +//! lookup leaves the loop too +[export, sideeffects] +def sum_shared_scaled(n : int) : int { + var s = 0 + for (i in range(n)) { + if ((i & 3) != 0) { + s += i * shared_scale[0] + } + } + return s +} + [export] def main { for (j in range(N)) { @@ -45,4 +76,9 @@ def main { branchy(true) branchy(false) print("BRANCHY {ga} {gb}\n") + for (i in range(256)) { + lut[i] = i * 7 + } + print("SUM_SCALED {sum_scaled(1024)}\n") + print("SUM_SHARED_SCALED {sum_shared_scaled(1024)}\n") } diff --git a/modules/dasLLVM/tests/llvm_jit_global_lookup.das b/modules/dasLLVM/tests/llvm_jit_global_lookup.das index 58dc844a32..97cac12d00 100644 --- a/modules/dasLLVM/tests/llvm_jit_global_lookup.das +++ b/modules/dasLLVM/tests/llvm_jit_global_lookup.das @@ -1,40 +1,16 @@ options gen2 require dastest/testing_boost public -require daslib/fio require _spawn_common require daslib/strings_boost require strings require math +require _exe_test_common // A das global reaches JIT code through a jit_get_global_mnh lookup at function entry. The emitter // resolves each global ONCE per function: every access then shares one base pointer, so LLVM sees // xs[j] and xs[j + 1] as adjacent (vectorizable, alias-analyzable) and the runtime pays one hash // lookup per global per call instead of one per access. -def private das_exe() : string { - let argv <- get_command_line_arguments() - return find(to_generic_path(argv[0]), "/") < 0 ? argv[0] : get_full_file_name(normalize(argv[0])) -} - -def private tail(s : string) : string => slice(s, max(0, length(s) - 400)) - -//! the dumped IR of one function, by its dumped name - mangled (`branchy Cb`) unless the signature is nullary; signature to closing brace -def private function_ir(dump : string; name : string) : string { - let start = find(dump, "`{name}\"(") - if (start < 0) { - return "" - } - let body = slice(dump, start) - let close = find(body, "\n}") - return close < 0 ? body : slice(body, 0, close) -} - -//! the entry block of a dumped function: everything before its first branch -def private entry_block_ir(fn_ir : string) : string { - let br = find(fn_ir, "\n br ") - return br < 0 ? fn_ir : slice(fn_ir, 0, br) -} - def private count_of(s : string; needle : string) : int { var n = 0 var at = find(s, needle, 0) @@ -45,24 +21,34 @@ def private count_of(s : string; needle : string) : int { return n } +//! the `callee` lookups of a dumped function outside its entry +//! block and its loop preheaders +def private lookups_inside_loop(fn_ir : string; callee = "jit_get_global_mnh") : int { + var n = 0 + let blocks <- split(fn_ir, "\n\n") + for (blk, ix in blocks, count()) { + let block_label = slice(blk, 0, max(0, find(blk, ":"))) + if (ix > 0 && find(block_label, ".preheader") < 0) { + n += count_of(blk, "@{callee}(") + } + } + return n +} + [test] def test_global_lookup_once_per_function(t : T?) { with_child_log_info() { t |> run("four accesses to two globals emit one lookup per global") @(t : T?) { - let root = "modules/dasLLVM/tests/_global_lookup_root.das" - var out = "" - //! -jit-no-cache: the dump only exists when codegen runs; -no-module-cache: nothing is written beside the fixture - let rc = run_and_capture([das_exe(), "-no-module-cache", "-jit", "-jit-no-cache", root, "--", "--jit-dump"], out, 300.0) - t |> equal(0, rc, "the fixture runs under -jit: {tail(out)}") + let out = jit_dump(t, "modules/dasLLVM/tests/_global_lookup_root.das", []) t |> success(find(out, "FOUR_SUMS ") >= 0, "the fixture printed its sum: {tail(out)}") - let ir = function_ir(out, "four_sums") + let ir = function_ir(out, "`four_sums") t |> success(!empty(ir), "the dump carries four_sums: {tail(out)}") let lookups = count_of(ir, "@jit_get_global_mnh(") t |> equal(2, lookups, "one jit_get_global_mnh per global (xs, ys) in four_sums, not one per access:\n{ir}") //! the lazy half: a global written on one branch only is never looked up ahead of the branch - the //! entry block carries no constant-hash lookup (LLVM may fold both branches into ONE lookup whose //! hash is a select, which still probes once per call and never for the untaken global) - let bir = function_ir(out, "branchy Cb") + let bir = function_ir(out, "`branchy Cb") t |> success(!empty(bir), "the dump carries branchy: {tail(out)}") let total = count_of(bir, "@jit_get_global_mnh(") t |> success(total >= 1 && total <= 2, "at most one lookup per branch global (ga, gb) in branchy, got {total}:\n{bir}") @@ -70,13 +56,32 @@ def test_global_lookup_once_per_function(t : T?) { let hoisted = count_of(entry, "@jit_get_global_mnh(") - count_of(entry, "@jit_get_global_mnh(i64 %") t |> equal(0, hoisted, "no constant-hash lookup in branchy's entry block - an untaken branch pays nothing:\n{bir}") } + t |> run("a lookup under a branch in a loop leaves the loop") @(t : T?) { + let out = jit_dump(t, "modules/dasLLVM/tests/_global_lookup_root.das", []) + t |> success(find(out, "SUM_SCALED ") >= 0, + "the fixture printed its scaled sum: {tail(out)}") + let ir = function_ir(out, "`sum_scaled Ci") + t |> success(!empty(ir), "the dump carries sum_scaled: {tail(out)}") + t |> equal(2, count_of(ir, "@jit_get_global_mnh("), + "one lookup per global (lut, scale) in sum_scaled:\n{ir}") + t |> equal(0, lookups_inside_loop(ir), + "no lookup is left inside the loop or the branch:\n{ir}") + } + t |> run("a shared-global lookup under a branch in a loop leaves the loop") @(t : T?) { + let out = jit_dump(t, "modules/dasLLVM/tests/_global_lookup_root.das", []) + t |> success(find(out, "SUM_SHARED_SCALED ") >= 0, + "the fixture printed its shared scaled sum: {tail(out)}") + let ir = function_ir(out, "`sum_shared_scaled Ci") + t |> success(!empty(ir), "the dump carries sum_shared_scaled: {tail(out)}") + t |> equal(1, count_of(ir, "call ptr @jit_get_shared_mnh("), + "one lookup of shared_scale in sum_shared_scaled:\n{ir}") + t |> equal(0, lookups_inside_loop(ir, "jit_get_shared_mnh"), + "no shared lookup is left inside the loop or the branch:\n{ir}") + } t |> run("a solid-context global is one base load per function and no lookup") @(t : T?) { - let root = "modules/dasLLVM/tests/_global_lookup_solid_root.das" - var out = "" - let rc = run_and_capture([das_exe(), "-no-module-cache", "-jit", "-jit-no-cache", root, "--", "--jit-dump"], out, 300.0) - t |> equal(0, rc, "the solid fixture runs under -jit: {tail(out)}") + let out = jit_dump(t, "modules/dasLLVM/tests/_global_lookup_solid_root.das", []) t |> success(find(out, "FOUR_SUMS ") >= 0, "the solid fixture printed its sum: {tail(out)}") - let ir = function_ir(out, "four_sums") + let ir = function_ir(out, "`four_sums") t |> success(!empty(ir), "the dump carries four_sums: {tail(out)}") t |> equal(0, count_of(ir, "@jit_get_global_mnh("), "a solid-context global never goes through the hash lookup:\n{ir}") t |> equal(1, count_of(ir, "load ptr, ptr"), "one load of the context's globals base serves every access of both globals:\n{ir}") From 74ee4a534c142d299c1d32da3abc82aedc439f6a Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 17:17:10 -0800 Subject: [PATCH 07/11] jit: --jit-remarks flags print LLVM's optimization remarks --jit-remarks=, --jit-remarks-missed= and --jit-remarks-analysis= take a pass regex, set LLVM's -pass-remarks options once per process and force codegen past the DLL and object caches; under --jit-debug each remark carries the .das file and line. Fixes #4470 Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/ARCHITECTURE.md | 2 +- modules/dasLLVM/CMakeLists.txt | 1 + modules/dasLLVM/daslib/llvm_jit_cli.das | 12 +++++ modules/dasLLVM/daslib/llvm_jit_common.das | 12 +++++ modules/dasLLVM/daslib/llvm_jit_link.das | 2 +- modules/dasLLVM/daslib/llvm_jit_plan.das | 9 +++- modules/dasLLVM/daslib/llvm_jit_run.das | 4 +- modules/dasLLVM/tests/_remarks_root.das | 34 ++++++++++++ modules/dasLLVM/tests/llvm_jit_remarks.das | 60 ++++++++++++++++++++++ 9 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 modules/dasLLVM/tests/_remarks_root.das create mode 100644 modules/dasLLVM/tests/llvm_jit_remarks.das diff --git a/modules/dasLLVM/ARCHITECTURE.md b/modules/dasLLVM/ARCHITECTURE.md index 9c06c430b3..a90c378289 100644 --- a/modules/dasLLVM/ARCHITECTURE.md +++ b/modules/dasLLVM/ARCHITECTURE.md @@ -112,7 +112,7 @@ bitcode and the link runs lld LTO - a dev probe artifact), `DAS_JIT_X64_FORCE_FE and `cpu_supports`-based tune eligibility all follow), `DAS_JIT_BASELINE` (build for a CPU class instead of the box - the machine, the gates, the tune ladder and the cache keys all follow; `ARCHITECTURE_TARGET_FEATURES.md` sec.10), `--jit-debug` / `-g` (emit DWARF or CodeView debug -info and promote every argument to a stack slot for it; `ARCHITECTURE_DEBUG_INFO.md` sec.12), `--jit-compile-only` / `options jit_compile_only` (build, verify and optimize, then write, load and install nothing - the program runs interpreted; under a `--jit-target` cross triple it also keeps the `[tune]` grid a cross artifact would stamp as reference bodies, which is what the emission dump inspects), `--jit-sanitize` / `options jit_sanitize` (`address` runs LLVM's `asan` pass after the optimizer on functions marked `sanitize_address`; `undefined` runs `bounds-checking`; a sanitizer host defaults to its own, `none` to nothing; an exe or a library links with `-fsanitize=` for the host's sanitizers and the requested ones, through `clang++` unless a linker is named; an ASan host lowers every array push to the runtime call, which keeps the buffer's container annotation - `src/misc/ARCHITECTURE.md` sec.10), and the +info and promote every argument to a stack slot for it; `ARCHITECTURE_DEBUG_INFO.md` sec.12), `--jit-remarks` / `--jit-remarks-missed` / `--jit-remarks-analysis` (LLVM's `-pass-remarks*` regexes, parsed once per process with the other JIT cl::opts: the matching passes' optimization remarks print to stderr, and codegen runs past the DLL and obj caches so every function's passes run), `--jit-compile-only` / `options jit_compile_only` (build, verify and optimize, then write, load and install nothing - the program runs interpreted; under a `--jit-target` cross triple it also keeps the `[tune]` grid a cross artifact would stamp as reference bodies, which is what the emission dump inspects), `--jit-sanitize` / `options jit_sanitize` (`address` runs LLVM's `asan` pass after the optimizer on functions marked `sanitize_address`; `undefined` runs `bounds-checking`; a sanitizer host defaults to its own, `none` to nothing; an exe or a library links with `-fsanitize=` for the host's sanitizers and the requested ones, through `clang++` unless a linker is named; an ASan host lowers every array push to the runtime call, which keeps the buffer's container annotation - `src/misc/ARCHITECTURE.md` sec.10), and the runtime escape API `tune_suppress_mint(knob)` (a library `[init]` suppresses the auto/restart mint; the caller passes the knob name it acts for). The announce contract: an override announces at the point it CHANGES THE OUTCOME - at least one line naming the knob (its env spelling, or the caller-supplied knob diff --git a/modules/dasLLVM/CMakeLists.txt b/modules/dasLLVM/CMakeLists.txt index f0712d5d48..5bee5c6438 100644 --- a/modules/dasLLVM/CMakeLists.txt +++ b/modules/dasLLVM/CMakeLists.txt @@ -187,6 +187,7 @@ if(NOT DAS_TOOLS_DISABLED) modules/dasLLVM/tests/llvm_jit_baseline.das modules/dasLLVM/tests/llvm_jit_debug_info.das modules/dasLLVM/tests/llvm_jit_global_lookup.das modules/dasLLVM/tests/llvm_jit_link.das modules/dasLLVM/tests/llvm_jit_ptr_diff_inline.das + modules/dasLLVM/tests/llvm_jit_remarks.das modules/dasLLVM/tests/llvm_jit_sanitize.das modules/dasLLVM/tests/llvm_layout_key.das modules/dasLLVM/tests/llvm_second_context.das) das_lane_dastest_target(run_tests_llvm_tune COMMENT "Testing the llvm_tune framework" diff --git a/modules/dasLLVM/daslib/llvm_jit_cli.das b/modules/dasLLVM/daslib/llvm_jit_cli.das index 8ca3038385..4699fef286 100644 --- a/modules/dasLLVM/daslib/llvm_jit_cli.das +++ b/modules/dasLLVM/daslib/llvm_jit_cli.das @@ -49,6 +49,18 @@ struct public JitCliOptions { @clarg_doc = "JIT: print the generated LLVM IR, or with =PATH write it to that file" dump_ir : Option + @clarg_name = "jit-remarks" + @clarg_doc = "JIT: print LLVM optimization remarks of passes matching the regex" + remarks : Option + + @clarg_name = "jit-remarks-missed" + @clarg_doc = "JIT: as --jit-remarks, for the transformations a matching pass declined" + remarks_missed : Option + + @clarg_name = "jit-remarks-analysis" + @clarg_doc = "JIT: as --jit-remarks, for the analysis behind a matching pass's decision" + remarks_analysis : Option + @clarg_name = "jit-target" @clarg_doc = "JIT: cross-compile target triple (use with -exe; e.g. wasm32-unknown-emscripten)" target : Option diff --git a/modules/dasLLVM/daslib/llvm_jit_common.das b/modules/dasLLVM/daslib/llvm_jit_common.das index ce526b04b4..1782d53b27 100644 --- a/modules/dasLLVM/daslib/llvm_jit_common.das +++ b/modules/dasLLVM/daslib/llvm_jit_common.das @@ -13,6 +13,7 @@ module llvm_jit_common shared private require llvm/daslib/llvm_boost require llvm/daslib/llvm_dll_utils require llvm/daslib/llvm_jit_plan public +require llvm/daslib/llvm_jit_cli require llvm/daslib/llvm_targets require llvm/daslib/llvm_dsl @@ -654,6 +655,17 @@ def public init_jit_clopts() { "-enable-unroll-and-jam=true", "-allow-unroll-and-jam=true" ] + let cli = jit_cli_options_or_default() + for (flag, llvm_opt, regex in + fixed_array("jit-remarks", "jit-remarks-missed", "jit-remarks-analysis"), + fixed_array("pass-remarks", "pass-remarks-missed", "pass-remarks-analysis"), + fixed_array(cli.remarks, cli.remarks_missed, cli.remarks_analysis)) { + if (regex |> is_some) { + let passes = regex |> unwrap + argv |> push("-{llvm_opt}={passes}") + to_log(LOG_INFO, "LLVM JIT: --{flag}={passes} prints LLVM's {llvm_opt} to stderr\n") + } + } LLVMParseCommandLineOptions(length(argv), unsafe(addr(argv[0])), "daslang JIT cl::opts") diff --git a/modules/dasLLVM/daslib/llvm_jit_link.das b/modules/dasLLVM/daslib/llvm_jit_link.das index da1a9bfa06..f119a82be1 100644 --- a/modules/dasLLVM/daslib/llvm_jit_link.das +++ b/modules/dasLLVM/daslib/llvm_jit_link.das @@ -71,7 +71,7 @@ def public run_jit_linked(prog : Program?; var ctx : Context?) : bool { } return false if (report_collect_errors(plan)) let t_hash = get_time_usec(phase_tm) - if (!plan.use_dll || plan.dump_ir) { + if (!plan.use_dll || plan.dump_ir || plan.remarks) { return run_jit_in_emitter(prog, ctx, plan.log_jit_time, t_hash, 0) } if (empty(plan.candidates) && !plan.gen_lib) { diff --git a/modules/dasLLVM/daslib/llvm_jit_plan.das b/modules/dasLLVM/daslib/llvm_jit_plan.das index a994a56770..a3a354ca2d 100644 --- a/modules/dasLLVM/daslib/llvm_jit_plan.das +++ b/modules/dasLLVM/daslib/llvm_jit_plan.das @@ -837,6 +837,9 @@ struct public JitPlan { debug_info : bool dump_ir : bool dump_ir_path : string + //! an optimization-remark flag is set, so codegen runs past every + //! cache + remarks : bool gen_exe : bool gen_lib : bool lib_static : bool @@ -914,7 +917,7 @@ def private resolve_split_options(var plan : JitPlan; cli_opts : JitCliOptions; let obj_cache_asked = (cli_opts.obj_cache |> unwrap_or(obj_cache_default ? 1 : 0)) != 0 let pinned_path = !(string(prog.policies.jit_output_path) |> empty()) plan.obj_cache = (plan.jit_split && plan.use_dll && !pinned_path && obj_cache_asked - && !plan.dump_ir) + && !plan.dump_ir && !plan.remarks) if (announce && (cli_opts.obj_cache |> is_some()) && !(plan.jit_split && plan.use_dll)) { to_log(LOG_WARNING, "LLVM JIT: --jit-obj-cache applies under --jit-split-modules dll mode only - ignored for this run\n") } @@ -1007,7 +1010,9 @@ def public make_jit_plan(prog : Program?; var ctx : Context?; announce : bool; c jit_all_functions = prog.policies.jit_jit_all_functions, debug_info = cli_opts.debug_info |> unwrap_or(prog.policies.jit_debug_info), dump_ir = cli_opts.dump_ir |> is_some, - dump_ir_path = cli_opts.dump_ir |> unwrap_or("")) + dump_ir_path = cli_opts.dump_ir |> unwrap_or(""), + remarks = ((cli_opts.remarks |> is_some) || (cli_opts.remarks_missed |> is_some) + || (cli_opts.remarks_analysis |> is_some))) if (announce && prog.policies.jit_dll_mode && !das_is_dll_build()) { // a legitimate configuration (static daslang), but the cost is worth naming: no DLL // cache exists, so every -jit run pays full in-memory codegen diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index f17e35f0e7..74fff1f1aa 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -37,7 +37,7 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // 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 = 0x15e51cb6a7e1e2e2ul +let LLVM_JIT_EMITTER_HASH : uint64 = 0x74c7da0c49eda027ul [arch(at = "../ARCHITECTURE_FAST_MATH.md#never-fast-math")] def private apply_fast_math_to_module(m : LLVMOpaqueModule?) { @@ -732,7 +732,7 @@ def public run_jit(prog : Program?; var ctx : Context?; cli_opts : JitCliOptions g_dynamic_lib_handle = get_dll_by_path(output_path) let (missing, disabling) = get_dll_missing(funcs, disabled, g_dynamic_lib_handle, uids) let dll_changed = !(missing |> empty() && disabling |> empty()) - recompile_prog = dll_changed || dump_ir + recompile_prog = dll_changed || dump_ir || plan.remarks if (recompile_prog) { g_dynamic_lib_handle.reset() to_log(LOG_INFO, "LLVM JIT: DLL cache miss, codegen for {output_path}.dll\n") diff --git a/modules/dasLLVM/tests/_remarks_root.das b/modules/dasLLVM/tests/_remarks_root.das new file mode 100644 index 0000000000..77d59ebfbe --- /dev/null +++ b/modules/dasLLVM/tests/_remarks_root.das @@ -0,0 +1,34 @@ +options gen2 + +//! a counted in-place loop: every host's vectorizer takes it +[never_inline, hint(noinline)] +def halve(var fa : float?; n : int) { + unsafe { + for (i in range(n)) { + fa[i] = fa[i] * 0.5f + 1.0f + } + } +} + +//! a loop whose exit reads the data it writes: no vectorizer takes it +[never_inline, hint(noinline)] +def halve_until_zero(var fa : float?) : int { + var i = 0 + unsafe { + while (fa[i] != 0.0f) { + fa[i] = fa[i] * 0.5f + i++ + } + } + return i +} + +[export] +def main { + var fa : array + fa |> resize(64) + unsafe { + halve(addr(fa[0]), 63) + print("HALVED {fa[0]} {halve_until_zero(addr(fa[0]))}\n") + } +} diff --git a/modules/dasLLVM/tests/llvm_jit_remarks.das b/modules/dasLLVM/tests/llvm_jit_remarks.das new file mode 100644 index 0000000000..dc16cdc4b9 --- /dev/null +++ b/modules/dasLLVM/tests/llvm_jit_remarks.das @@ -0,0 +1,60 @@ +options gen2 +require dastest/testing_boost public +require daslib/fio +require strings +require _exe_test_common + +//! the remarks are stderr lines of a child: a second child in the +//! same directory proves they print past the DLL cache the first one +//! minted +[test] +def test_jit_remarks_print_the_vectorizer_decisions(t : T?) { + var terr : string + let dir = create_temp_directory("llvm_jit_remarks", terr) + if (empty(dir)) { + t |> failure("create_temp_directory: {terr}") + return + } + let root = get_full_file_name("modules/dasLLVM/tests/_remarks_root.das") + let noenv : array + var out = "" + var rc = run_and_capture([das_exe(), "-no-module-cache", "-jit", root], dir, noenv, out, 300.0) + t |> equal(0, rc, "the fixture runs under -jit: {tail(out)}") + t |> success(find(out, "HALVED 1 ") >= 0, "the fixture printed its result: {tail(out)}") + t |> equal(-1, find(out, "remark:"), "no remark prints without a remark flag: {tail(out)}") + rc = run_and_capture([das_exe(), "-no-module-cache", "-jit", root, "--", + "--jit-remarks=loop-vectorize"], dir, noenv, out, 300.0) + t |> equal(0, rc, "the fixture runs under -jit --jit-remarks alone: {tail(out)}") + t |> success(find(out, "remark: :0:0: vectorized loop") >= 0, + "--jit-remarks alone prints past the cache: {tail(out)}") + rc = run_and_capture([das_exe(), "-no-module-cache", "-jit", root, "--", + "--jit-remarks-missed=loop-vectorize"], dir, noenv, out, 300.0) + t |> equal(0, rc, "the fixture runs under -jit --jit-remarks-missed alone: {tail(out)}") + t |> success(find(out, "remark: :0:0: loop not vectorized") >= 0, + "--jit-remarks-missed alone prints past the cache: {tail(out)}") + t |> equal(-1, find(out, "loop not vectorized:"), + "--jit-remarks-missed prints no analysis remark: {tail(out)}") + rc = run_and_capture([das_exe(), "-no-module-cache", "-jit", root, "--", + "--jit-remarks-analysis=loop-vectorize"], dir, noenv, out, 300.0) + t |> equal(0, rc, "the fixture runs under -jit --jit-remarks-analysis alone: {tail(out)}") + t |> success(find(out, "remark: :0:0: loop not vectorized: ") >= 0, + "--jit-remarks-analysis alone prints the analysis remarks past the cache: {tail(out)}") + rc = run_and_capture( + [das_exe(), "-no-module-cache", "-jit", root, "--", "--jit-remarks=loop-vectorize", + "--jit-remarks-missed=loop-vectorize"], + dir, noenv, out, 300.0) + t |> equal(0, rc, "the fixture runs under -jit with remarks: {tail(out)}") + t |> success(find(out, "remark: :0:0: vectorized loop") >= 0, + "the counted loop's remark prints past the cache: {tail(out)}") + t |> success(find(out, "remark: :0:0: loop not vectorized") >= 0, + "the data-exit loop's missed remark prints: {tail(out)}") + rc = run_and_capture( + [das_exe(), "-no-module-cache", "-jit", root, "--", "--jit-debug", + "--jit-remarks-missed=loop-vectorize"], + dir, noenv, out, 300.0) + t |> equal(0, rc, "the fixture runs under -jit --jit-debug with remarks: {tail(out)}") + t |> success(find(out, "_remarks_root.das:18:") >= 0, + "under --jit-debug a remark names the loop's .das line: {tail(out)}") + t |> equal(-1, find(out, "vectorized loop"), "only the missed remarks print: {tail(out)}") + rmdir_rec(dir, terr) +} From 5a148619cabc0f94489bec51ef007e0e45b098ce Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 17:17:35 -0800 Subject: [PATCH 08/11] jit: the typed-pointer aliasing check lands skipped The emitter attaches no alias information to typed pointers, by the language's own rule that a pointer is an address; the test documents the shape and stays skipped behind a flag. Refs #4446 Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/CMakeLists.txt | 1 + .../dasLLVM/tests/_pointer_aliasing_root.das | 29 +++++++++++++++++++ .../tests/llvm_jit_pointer_aliasing.das | 29 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 modules/dasLLVM/tests/_pointer_aliasing_root.das create mode 100644 modules/dasLLVM/tests/llvm_jit_pointer_aliasing.das diff --git a/modules/dasLLVM/CMakeLists.txt b/modules/dasLLVM/CMakeLists.txt index 5bee5c6438..0bed5a599a 100644 --- a/modules/dasLLVM/CMakeLists.txt +++ b/modules/dasLLVM/CMakeLists.txt @@ -188,6 +188,7 @@ if(NOT DAS_TOOLS_DISABLED) modules/dasLLVM/tests/llvm_jit_global_lookup.das modules/dasLLVM/tests/llvm_jit_link.das modules/dasLLVM/tests/llvm_jit_ptr_diff_inline.das modules/dasLLVM/tests/llvm_jit_remarks.das + modules/dasLLVM/tests/llvm_jit_pointer_aliasing.das modules/dasLLVM/tests/llvm_jit_sanitize.das modules/dasLLVM/tests/llvm_layout_key.das modules/dasLLVM/tests/llvm_second_context.das) das_lane_dastest_target(run_tests_llvm_tune COMMENT "Testing the llvm_tune framework" diff --git a/modules/dasLLVM/tests/_pointer_aliasing_root.das b/modules/dasLLVM/tests/_pointer_aliasing_root.das new file mode 100644 index 0000000000..c22262d754 --- /dev/null +++ b/modules/dasLLVM/tests/_pointer_aliasing_root.das @@ -0,0 +1,29 @@ +options gen2 + +//! a loop over float? bounded and fed through two int? - the C +//! shape `for (i = 0; i < *n; i++) fa[i] = fa[i] * 0.5f + *k` +[never_inline, hint(noinline)] +def scale(var fa : float?; n_ptr : int?; k_ptr : int?) { + unsafe { + var i = 0 + while (i < *n_ptr) { + fa[i] = fa[i] * 0.5f + float(*k_ptr) + i++ + } + } +} + +[export] +def main { + var fa : array + fa |> resize(64) + var ctl : int[2] + ctl[0] = 64 + ctl[1] = 3 + for (_p in range(3)) { + unsafe { + scale(addr(fa[0]), addr(ctl[0]), addr(ctl[1])) + } + } + print("SCALE {fa[7]} {fa[63]}\n") +} diff --git a/modules/dasLLVM/tests/llvm_jit_pointer_aliasing.das b/modules/dasLLVM/tests/llvm_jit_pointer_aliasing.das new file mode 100644 index 0000000000..e2a0f7620f --- /dev/null +++ b/modules/dasLLVM/tests/llvm_jit_pointer_aliasing.das @@ -0,0 +1,29 @@ +options gen2 +require dastest/testing_boost public +require strings +require _exe_test_common + +//! flips to true when the emitter gives LLVM alias information for +//! typed pointers (#4446): no !tbaa and no parameter noalias exist +//! yet +let private TYPED_POINTER_ALIAS_INFO = false + +//! a float store cannot change what an int? points at, so the bound +//! and the addend of the fixture's loop are read outside it +[test] +def test_loop_invariant_reads_through_pointers_leave_the_loop(t : T?) { + if (!TYPED_POINTER_ALIAS_INFO) { + t |> skip("the JIT emits no alias information for typed pointers yet (#4446)") + return + } + let out = jit_dump(t, "modules/dasLLVM/tests/_pointer_aliasing_root.das", []) + t |> success(find(out, "SCALE 5.25 5.25") >= 0, "the fixture printed its result: {tail(out)}") + let ir = function_ir(out, + "`scale 1`Lt`f`Gt`? C1`Lt`i`Gt`? C1`Lt`i`Gt`? implementation") + t |> success(!empty(ir), "the dump carries scale: {tail(out)}") + let body = named_block_ir(ir, "while_body") + t |> success(!empty(body), "scale keeps a while_body block:\n{ir}") + t |> success(find(body, "store float") >= 0, "the loop body stores the float:\n{body}") + t |> equal(-1, find(body, "load i32"), + "the bound and the addend are read outside the loop, not after each float store:\n{body}") +} From 1a94a3f1cd926cf8147dfa4c3ebb09862587c6c4 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 21:24:52 -0800 Subject: [PATCH 09/11] review: two checklists name their triggers and share one rule format The tests checklist's AOT-run rule described what test_aot compiles in a way that read as an example, and its opt-out rule named two of the four ways a file leaves the AOT set; a test that starts a daslang child now has its cache flags spelled out instead of sending the reader into the host's source. The dasLLVM checklist's routing rules take the list form the rest of the file uses, its fused tune rule splits into its two triggers, the child-artifact rule accepts a working directory inside the test's own directory, and the twinned-emitter definition is the closed list with a scalar criterion. Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/REVIEW.md | 45 ++++++++++++++++++++++----------------- tests/REVIEW.md | 35 +++++++++++++++++++----------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/modules/dasLLVM/REVIEW.md b/modules/dasLLVM/REVIEW.md index f9ab6cfab9..be35912be3 100644 --- a/modules/dasLLVM/REVIEW.md +++ b/modules/dasLLVM/REVIEW.md @@ -5,18 +5,22 @@ docs: `ARCHITECTURE.md`, `ARCHITECTURE_CODEGEN_IDENTITY.md`, `ARCHITECTURE_DEBUG `ARCHITECTURE_EXE.md`, `ARCHITECTURE_JIT_ENTRY.md`, `ARCHITECTURE_LIB.md`, `ARCHITECTURE_TARGET_FEATURES.md`, `ARCHITECTURE_VECTOR_MATH.md`. Planned work: `LEDGER.md`. -**A `[test]` file under this module that carries a `require dasllama/...` line of its own answers -to `modules/dasLLAMA/REVIEW.md` (repo root) as well.** +- **A `[test]` file under this module that carries a `require dasllama/...` line of its own + answers to `modules/dasLLAMA/REVIEW.md` (repo root) as well.** -**A diff that changes how the compile handles a function because it carries `[tune]`, -`[tune_perm]`, `[tune_companion]`, `[tune_scope]`, `[tune_policy]` or `[llvm_code]` - the body -it emits for it, which permutation it picks for it - or what a read or a save of `.tune.json` -takes from a saved sidecar that is stale (its mtime is older than the running binary) or that -another machine wrote, is reviewed with `skills/tune.md`.** +- **A diff that changes how the compile handles a function because it carries `[tune]`, + `[tune_perm]`, `[tune_companion]`, `[tune_scope]`, `[tune_policy]` or `[llvm_code]` - the body + it emits for it, which permutation it picks for it - is reviewed with `skills/tune.md`.** -**A change to the tune framework - `daslib/llvm_tune.das`, a test under `tests/` here that requires -`llvm/daslib/llvm_tune` or spawns a child that does, or the descriptor and C++ rows that join it to -a program (`.das_module`, `src/dasLLVM.cpp`) - is reviewed with `skills/internal/llvm_tune_internals.md`.** +- **A diff that changes what a read or a save of `.tune.json` takes from an + `.tune.json` it finds stale (its mtime is older than the running binary) or written by + another machine is reviewed with `skills/tune.md`.** + +- **A change to the tune framework - `daslib/llvm_tune.das`, a test under `tests/` here that + requires `llvm/daslib/llvm_tune` or spawns a child that does, the + `registerModuleGroupMember("tune_framework", ...)` row in `src/dasLLVM.cpp`, or the + `register_module_group("tune_framework", ...)` row in `.das_module` - is reviewed with + `skills/internal/llvm_tune_internals.md`.** - **A change to any file under this module but a `.md` runs the module-owned suite** (command and build gate: `tests/README.md` here). CI runs some of its test files only nightly, so before @@ -37,12 +41,12 @@ a program (`.das_module`, `src/dasLLVM.cpp`) - is reviewed with `skills/internal temp directory), and removes them** - a shared path under `build/` is one two concurrent runs collide on. -- **A test under `tests/` here that spawns a daslang child keeps the child's artifacts inside a - directory it created uniquely for this process: `-output /...` for a `-exe` build, - `-no-module-cache` or `-module-cache /...` for a run that compiles through the front-end - cache, and `-jit-no-cache` or a pinned `jit_output_path` for a `-jit` run that executes the - script.** A child writes its caches relative to the cwd otherwise, which is the tree two - concurrent runs share. +- **A test under `tests/` here that spawns a daslang child keeps every path the child writes + inside a directory the test created uniquely for this process: `-output /...` for a + `-exe` build; `-no-module-cache` or `-module-cache /...` for a run that compiles through + the front-end cache; `-jit-no-cache` or `-output /...` for a `-jit` run that executes the + script; a working directory inside `` covers the module cache and the JIT cache.** A child + writes its caches relative to the cwd otherwise, which is the tree two concurrent runs share. - **A diff that changes which arm a non-host triple selects in a branch keyed on the target triple (a key added, changed or removed), or changes code only a cross target's arm reaches, @@ -202,10 +206,11 @@ a program (`.das_module`, `src/dasLLVM.cpp`) - is reviewed with `skills/internal - **A diff that changes the machine code a twinned emitter produces - its body, which of its arms a call selects, or the feature set its output is lowered under - leaves it covered by a cell comparing the emitted result with the interpreted result over the operand range that - emitter serves (every vector width for a vector emitter, every pair of int8 operands for a - dot), adding the cell in the same change when none covers that range.** A twinned emitter is - one the interpreter also computes, through a das body or a builtin: every `build_vector_*` in - `daslib/llvm_jit_intrin.das` and every `[llvm_code]` generator under this module. + emitter serves (every vector width for a vector emitter, every operand value for one over + 8-bit operands, each operand class its arms branch on otherwise), adding the cell in the same + change when none covers that range.** A twinned emitter is a `build_vector_*` in + `daslib/llvm_jit_intrin.das` or an `[llvm_code]` generator under this module: the interpreter + computes the same answer through a das body or a builtin, so the two can drift apart. - **A test that searches the text of the LLVM IR the JIT emits matches an instruction by its opcode and flags (`fmul reassoc`), never by a numbered register (`%12`).** The numbering shifts diff --git a/tests/REVIEW.md b/tests/REVIEW.md index a22c6834c5..442bc91c93 100644 --- a/tests/REVIEW.md +++ b/tests/REVIEW.md @@ -41,20 +41,31 @@ under a working directory or project root it passes that program - writes it und at once, and only those two directories are one run's own. The reviewer checks every write `REVIEW.das` does not flag; weakening that check is a defect. +**A test that starts a daslang child passes it `-no-module-cache` or `-module-cache /...`, +and for a `-jit` child also `-jit-no-cache` or `-output /...`, or starts it in a working +directory inside ``, where `` is `test_temp_dir()` or a `create_temp_directory` +directory.** A child writes its front-end module cache and its JIT DLL cache relative to its +working directory otherwise, which is the tree two concurrent runs share. + **A diff that widens the `dasbind` skip in `.das_test` or drops one of the `[extern]` probe functions or any of its assertions from `dasbind/test_extern_abi.das` is a defect.** The suite is the only check of which register or stack slot an interpreted `[extern]` call puts each argument in - the JIT never takes that path - so coverage lost there reports green on every lane. -**A diff that changes a `.das` file under `tests/` (repo root) not marked `options no_aot` - one an -`AOT_*_FILES` variable of `tests/aot/CMakeLists.txt` holds after its filters, or one in a -`tests//` folder `DAS_AOT_SUITES` lists - names in the PR body the run that showed -`test_aot` pass: a local `daslang utils/internal/preflight/main.das -- --full` (repo root), a -`--target run_tests_aot` build, or a CI job link.** Per-PR CI builds only the AOT subset binary -(the test_aot_subset target), so the PR's own checks do not prove `test_aot`. - -**A diff that marks a `.das` file under `tests/` (repo root) `options no_aot`, or filters one out of an -`AOT_*_FILES` variable of `tests/aot/CMakeLists.txt`, states in a comment beside the option or -the filter what stops `test_aot` compiling or running the file, why an AOT build of it tests -nothing, or which `AOT_*_FILES` variable compiles it instead.** Without a stated reason no later -diff can tell whether the opt-out is still needed. +**A diff that changes a `.das` file under `tests/` (repo root) the `test_aot` binary compiles +and runs names in the PR body the run that showed `test_aot` pass: a local +`daslang utils/internal/preflight/main.das -- --full` (repo root), a `--target run_tests_aot` +build, or a CI job link.** `test_aot` compiles a file when an `AOT_*_FILES` variable of +`tests/aot/CMakeLists.txt` holds it after its filters (a folder `DAS_AOT_SUITES` lists globs +into its own such variable), and runs it unless `tests/.das_test` skips its folder under +`--use-aot` or the file says `options no_aot`. Per-PR CI builds only the AOT subset binary (the +test_aot_subset target), so the PR's own checks do not prove `test_aot`. + +**A diff that takes a `.das` file under `tests/` (repo root) out of what `test_aot` compiles or +runs - `options no_aot` in the file, a filter on an `AOT_*_FILES` variable of +`tests/aot/CMakeLists.txt`, a `--use-aot` skip of its folder in `tests/.das_test`, or a new +`tests//` folder whose files neither `DAS_AOT_SUITES` nor any `AOT_*_FILES` variable +compiles - states beside the opt-out (for a folder, beside the `DAS_AOT_SUITES` list) what stops +`test_aot` compiling or running the file, why an AOT build of it tests nothing, or which other +`AOT_*_FILES` variable compiles it instead.** Without a stated reason no later diff can tell +whether the opt-out is still needed. From 4b25125251354b9ff59db7dfa13e0306213290dc Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 23:31:16 -0800 Subject: [PATCH 10/11] review: the version-bump rule covers a pass that rejects source A pass that newly rejects a shape leaves a cached module untouched, so the src/ast checklist's getVersion rule names that trigger beside the output change it already named. The AST-headers checklist cites its architecture section by anchor, drops the version-comment rule the root CLAUDE.md already carries, and narrows the binding-name rule to the names ast_handle.h itself computes. Co-Authored-By: Claude Fable 5.1 --- include/daScript/ast/REVIEW.md | 35 ++++++++++++++------------------ src/ast/REVIEW.md | 37 ++++++++++++++++------------------ 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/include/daScript/ast/REVIEW.md b/include/daScript/ast/REVIEW.md index b3d8867150..e2e26dde6e 100644 --- a/include/daScript/ast/REVIEW.md +++ b/include/daScript/ast/REVIEW.md @@ -3,12 +3,14 @@ **Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** Architecture doc: `src/ast/ARCHITECTURE.md` (repo root). -- **A diff that changes the module an existing bind registers into, or the name it registers - under (`vectorHomeModule`, `typeFactory>::make`, `registerVectorFunctions`, the name - a `ManagedVectorAnnotation` or `ManagedStructureAnnotation` takes - all in `ast_handle.h`), - bumps `LLVM_JIT_CODEGEN_VERSION` in `modules/dasLLVM/daslib/llvm_jit_plan.das` (repo root), in - the same change.** The JIT's DLL cache key folds the codegen version and each function's AST - hash, never the module an extern lives in, so a cache hit binds the old name and crashes. +- **A diff that changes the module or the name `ast_handle.h` itself registers an existing C++ + binding under (`vectorHomeModule`, `typeFactory>::make`, `registerVectorFunctions`, + and any name those templates compute) bumps `LLVM_JIT_CODEGEN_VERSION` in + `modules/dasLLVM/daslib/llvm_jit_plan.das` (repo root), in the same change.** The JIT's DLL + cache key folds the codegen version and each function's AST hash, never the module an extern + lives in, so a cache hit binds the old name and crashes. The name string a + `ManagedVectorAnnotation` or `ManagedStructureAnnotation` constructor takes is chosen where the + annotation is created, so that rename answers to the creating folder's checklist. - **A diff that changes the memory layout of a C++ type declared under this folder that some `Managed*Annotation` is instantiated over - a data member added, removed, reordered or retyped @@ -24,17 +26,10 @@ `skills/internal/abi_break_sweep.md` (repo root) too.** An out-of-tree subclass that omits `override` keeps compiling and silently stops being called. -- **A diff that adds a field to `Function` or `Variable` (`ast.h`) holding something one - program's compile decides - whether the program uses it, the slot it holds in that program's - context, whether this program's JIT selected it - is a defect; the value goes into `Program`'s - symbol tables (`usedFunctions`, `functionIndices`, `usedVariables`, `variableIndices`, - `jitSelected`) instead.** A shared module's objects are one instance for every program in the - process, and a compile that runs inside another would otherwise overwrite the outer program's - answer mid-simulate. - `src/ast/ARCHITECTURE.md` sec.4 lists the per-compile fields that remain and why each is - tolerated. - -- **A diff changing `getVersion()` in `include/daScript/ast/ast_serializer.h` - replaces its version comment instead of appending to it.** The comment describes the current - version rather than earlier versions. A diff that keeps the old note in a parenthesis is a defect; git carries the - history. +- **A diff that adds a field to `Function` or `Variable` (`ast.h`) whose value one compiled + program decides, so two programs sharing the module would want different values, is a defect; + the value goes into a per-program table on `Program` instead.** A shared module's objects are + one instance for every program in the process, and a compile that runs inside another would + otherwise overwrite the outer program's answer before the outer program has finished reading + it. `src/ast/ARCHITECTURE.md#program-scoped-symbol-state` lists the per-compile fields + `Function` and `Variable` carry and why each is safe. diff --git a/src/ast/REVIEW.md b/src/ast/REVIEW.md index 6c502166b6..18d9af44bf 100644 --- a/src/ast/REVIEW.md +++ b/src/ast/REVIEW.md @@ -10,7 +10,6 @@ docs: `ARCHITECTURE.md`, `ARCHITECTURE_INFER.md`, `ARCHITECTURE_MODULE_SCAN.md`. `trySerializeProgramModule` (`ast_parse.cpp`, `ARCHITECTURE.md#module-cache-read`) or of the module-cache read diagnostics elsewhere in that file, or a finding text that no longer names what failed. - What the gate enforces is read from the gate itself. - **A diff that adds a module-cache read diagnostic outside `trySerializeProgramModule` (`ast_parse.cpp`), or moves one out of it - into a helper it calls, or another function in this @@ -46,11 +45,11 @@ docs: `ARCHITECTURE.md`, `ARCHITECTURE_INFER.md`, `ARCHITECTURE_MODULE_SCAN.md`. skipped, teaches every site that decides it the same spelling and the same skip-or-take decision, in the same change: the text collector `getAllRequireReq` (`ast_parse.cpp`), the parser's `ast_requireGuardAvailable` (`src/parser/parser_impl.cpp`), and the member walk - `moduleGroupMemberAvailable` (`src/builtin/module_builtin_rtti.cpp`).** The prerequisite walk collects - requires from the source text before any parse, so a spelling only the parser reads is a - module the walk never compiles, a decision the two make differently is a require the parse - takes with no module behind it, and a member the calls take but the require did not is a call - into a module that is not there. + `moduleGroupMemberAvailable` (`src/builtin/module_builtin_rtti.cpp`).** The prerequisite walk + collects requires from the source text before any parse, so a spelling only the parser reads + is a module the walk never compiles, a decision the collector and the parser make differently + is a require the parse takes with no module behind it, and a member the walk takes but the + require did not is a call into a module that is not there. - **A diff that makes a compile entry point in `ast_parse.cpp` rebind a `daScriptEnvironment` field has that entry point hold a `CompileEnvScope` for the whole compile and puts that field in @@ -66,12 +65,12 @@ docs: `ARCHITECTURE.md`, `ARCHITECTURE_INFER.md`, `ARCHITECTURE_MODULE_SCAN.md`. change.** A reader accepts a stream whose stored version equals `getVersion()`, so without the bump an older cache decodes the changed header as the old one with no diagnostic. -- **A diff that changes what a pass in this folder leaves in a compiled module - an inferred - flag, or a node folded, inlined or rewritten - bumps `AstSerializer::getVersion()` - (`include/daScript/ast/ast_serializer.h`, repo root) in the same change.** The default cache - stamps only the host executable, which a rebuild of the runtime library alone leaves - unchanged, and a `-module-cache ` file stamps no binary, so without the bump a rebuilt - `daslang` serves modules the old pass produced. +- **A diff that makes a pass in this folder reject source it accepted before, or change what it + leaves in a compiled module - an inferred flag, or a node folded, inlined or rewritten - bumps + `AstSerializer::getVersion()` (`include/daScript/ast/ast_serializer.h`, repo root) in the same + change.** A cached module never runs through the passes again, and the cache key misses a + rebuild of the runtime library alone (a `-module-cache ` file records no binary at all), + so without the bump a rebuilt `daslang` serves modules the old pass accepted or produced. - **The `setDeferredModuleLoader` call in `require_dynamic_modules` (`dyn_modules.cpp`) stays, and stays above the first `init_modules_for_folder` call - removing it or moving it below is a @@ -85,11 +84,9 @@ docs: `ARCHITECTURE.md`, `ARCHITECTURE_INFER.md`, `ARCHITECTURE_MODULE_SCAN.md`. never the name an extern binds under, so a cached DLL binds the old name and crashes on the hit. - **A diff that builds an `ExprCopy`, `ExprMove` or `ExprClone` standing in for no `=`, `<-` - or `:=` the user wrote - an inliner result store, a `let` relocated into an assignment, a - loop counter or control flag, a yield, finally-return or capture store - passes `true` as the - constructor's `no_promo` argument, or sets `no_promotion` from the node it rewrites - (`ARCHITECTURE_INFER.md#assign-operator-lookup`).** A store that performs a user-written `:=` - one field at a time, as a generated clone does, is the user's store and passes no flag. A - compiler-made - store that reaches the user's `operator =` runs it at a site the user never wrote, and only - in the build configuration that builds the store. + or `:=` the user wrote passes `true` as the constructor's `no_promo` argument, or sets + `no_promotion` from the node it rewrites (`ARCHITECTURE_INFER.md#assign-operator-lookup`).** A + store that performs a user-written `:=` one field at a time, as a generated clone does, is the + user's store and passes no flag. A compiler-made store that reaches the user's `operator =` + runs it at a site the user never wrote, and only in the build configuration that builds the + store. From 0a65dfe2fda2845d5a02ace6a3fe4d8fcee49edd Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Fri, 6 Nov 2026 23:34:42 -0800 Subject: [PATCH 11/11] ci: the tree-sitter parser test runs on ubuntu-24.04 ubuntu-latest moved to 26.04, where the swift toolchain the parser test action installs fails to load libxml2.so.2 and swift-build exits 127. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/tree_sitter_daslang.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tree_sitter_daslang.yml b/.github/workflows/tree_sitter_daslang.yml index d21f1310ea..cbee4d4270 100644 --- a/.github/workflows/tree_sitter_daslang.yml +++ b/.github/workflows/tree_sitter_daslang.yml @@ -25,8 +25,10 @@ jobs: runs-on: ${{matrix.os}} strategy: fail-fast: false + # The swift toolchain parser-test-action installs is built for ubuntu 24.04 and does not + # load on ubuntu-latest (26.04): swift-build fails to find libxml2.so.2. matrix: - os: [ubuntu-latest, windows-latest, macos-15] + os: [ubuntu-24.04, windows-latest, macos-15] steps: - name: Check out the grammar uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1