From 0e93e8a9c696ec876a63d4e3cfca0e7dad25a47b Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Mon, 7 Sep 2026 21:23:32 -0700 Subject: [PATCH] the JIT parks a statement after a terminator in its own dead block: a nested block's return / break / continue, or the constant-condition if the compiler unfolds even unoptimized, left the tail after the ret in the same basic block (LLVM "Terminator found in the middle of a basic block", surfaced by test_step_smoothstep's HOST_FAST_MATH skip arm under options optimize = false); tests/jit_tests/terminated_block_tail.das pins every shape; the dim-expr folding fixture halves its vector by a multiply, since a -ffast-math host divides by a runtime splat through rcpps one ULP off; codegen version 0x76, emitter pin re-hashed Co-Authored-By: Claude Fable 5.1 --- modules/dasLLVM/daslib/llvm_jit.das | 9 ++- modules/dasLLVM/daslib/llvm_jit_run.das | 4 +- tests/jit_tests/terminated_block_tail.das | 75 +++++++++++++++++++++++ tests/lint/_fixture_dim_expr_folding.das | 3 +- 4 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 tests/jit_tests/terminated_block_tail.das diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 1248008497..4b5775f8c0 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -1070,10 +1070,13 @@ class public LlvmJitVisitor : AstVisitor { } def override preVisitExprBlockExpression(blk : ExprBlock?; expr : ExpressionPtr) : void { + if (current_block_terminates()) { + //! a statement after a terminator - a nested block's return / break / continue, or the + //! constant-condition if the compiler unfolds even unoptimized - lands in its own dead block + LLVMPositionBuilderAtEnd(g_builder, append_basic_block("dead_at_line_{int(expr.at.line)}_")) + } if (LLVM_DEBUG_TRACES && LLVM_DEBUG_LINE_TRACES) { - if (!current_block_terminates()) { - build_debug_trace(expr.at, FN_JIT_DEBUG_LINE) - } + build_debug_trace(expr.at, FN_JIT_DEBUG_LINE) } } diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 17e850d45e..7806e05c47 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -38,11 +38,11 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // invalidates cached DLLs (e.g. edits to llvm_jit.das, llvm_macro.das, llvm_jit_common.das, // runtime helper ABI, default target triple). Cache filenames fold this in, so a bump // makes every previously written DLL miss the cache on the next run and get GC'd. -let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x75ul // 0x75: the global-offset lookup is memory(none) and emitted at its use site - LLVM dedups and hoists it, an untaken branch never pays it; a solid-context global resolves once per function at entry (0x74: a runtime-only exe emits no register_native_path rows, and a whole-lib exe emits them once (0x73: computed goto lowers to one switch with the trap as its default, not an icmp chain (0x72: policies.fast_math defaults to the host's float flags, so a fast-math host now JITs fast-math (0x71: a CPU class row's cpu is the arch's bare baseline, so a DAS_JIT_BASELINE build enables the row's set and nothing a level implies (0x70: the wasm feature string drops +relaxed-simd and the idot family keeps only the exact extmul + extadd_pairwise lowering on wasm SIMD128 (0x6f: the first wasm idot lowering; 0x6e: the aarch64 SDOT / SMMLA tables gate on DotProd / i8mm, not the arch alone, and the force env reaches the generic exe machine (0x6d: the inline polynomial rail carries NaN: tanh selects the operand back over its ordered clamp, and the sincos quadrant / tan octant convert through llvm.fptosi.sat instead of poisoning on NaN and out-of-range (0x6c: aarch64 vector tan/exp2/log2/log/pow join the inline polynomial rail bit-exactly with the interpreter, sinh/cosh/tanh ride the exp one; 0x6b: aarch64 vector sin/cos ride the inline polynomial; 0x6a: srem/urem for 32-bit %; 0x69: every string argument of an extern is substituted, not just the ones which asked) +let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x76ul // 0x76: a statement after a terminator in the same block list lands in its own dead block instead of after the ret (0x75: the global-offset lookup is memory(none) and emitted at its use site - LLVM dedups and hoists it, an untaken branch never pays it; a solid-context global resolves once per function at entry (0x74: a runtime-only exe emits no register_native_path rows, and a whole-lib exe emits them once (0x73: computed goto lowers to one switch with the trap as its default, not an icmp chain (0x72: policies.fast_math defaults to the host's float flags, so a fast-math host now JITs fast-math (0x71: a CPU class row's cpu is the arch's bare baseline, so a DAS_JIT_BASELINE build enables the row's set and nothing a level implies (0x70: the wasm feature string drops +relaxed-simd and the idot family keeps only the exact extmul + extadd_pairwise lowering on wasm SIMD128 (0x6f: the first wasm idot lowering; 0x6e: the aarch64 SDOT / SMMLA tables gate on DotProd / i8mm, not the arch alone, and the force env reaches the generic exe machine (0x6d: the inline polynomial rail carries NaN: tanh selects the operand back over its ordered clamp, and the sincos quadrant / tan octant convert through llvm.fptosi.sat instead of poisoning on NaN and out-of-range (0x6c: aarch64 vector tan/exp2/log2/log/pow join the inline polynomial rail bit-exactly with the interpreter, sinh/cosh/tanh ride the exp one; 0x6b: aarch64 vector sin/cos ride the inline polynomial; 0x6a: srem/urem for 32-bit %; 0x69: every string argument of an extern is substituted, not just the ones which asked) // 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 = 0xe10a9a4a639d1e19ul +let LLVM_JIT_EMITTER_HASH : uint64 = 0xe08c02674b3653d9ul let JIT_FNV_PRIME : uint64 = 1099511628211ul diff --git a/tests/jit_tests/terminated_block_tail.das b/tests/jit_tests/terminated_block_tail.das new file mode 100644 index 0000000000..9372fe64d0 --- /dev/null +++ b/tests/jit_tests/terminated_block_tail.das @@ -0,0 +1,75 @@ +options gen2 +options optimize = false +require dastest/testing_boost + +//! every shape here leaves a statement after a terminator inside one block list - the +//! optimizer would delete the tail, so the file compiles unoptimized to keep it + +var g_tail_ran = 0 + +def bare_block_return() : int { + { + return 1 + } + g_tail_ran++ + return 2 +} + +def constant_if_return() : int { + if (true) { // nolint:STYLE010 - the constant-condition if is the shape under test + return 3 + } + g_tail_ran++ + return 4 +} + +def break_in_nested_block() : int { + var seen = 0 + for (_i in range(10)) { + seen++ + { + break + } + g_tail_ran++ + } + return seen +} + +def continue_in_nested_block() : int { + var seen = 0 + for (_i in range(3)) { + { + seen++ + continue + } + g_tail_ran++ + } + return seen +} + +def void_bare_block_return() { + { + return + } + g_tail_ran++ +} + +[test] +def test_statements_after_a_terminated_nested_block(t : T?) { + g_tail_ran = 0 + t |> equal(bare_block_return(), 1) + t |> equal(constant_if_return(), 3) + t |> equal(break_in_nested_block(), 1) + t |> equal(continue_in_nested_block(), 3) + void_bare_block_return() + t |> equal(g_tail_ran, 0, "a statement after a terminator never runs") +} + +[test] +def test_skip_shape_returns_early(t : T?) { + if (true) { // nolint:STYLE010 - the gated-skip shape a test file writes + t |> skip("the early return is the shape under test") + return + } + t |> failure("the tail after the early return ran") +} diff --git a/tests/lint/_fixture_dim_expr_folding.das b/tests/lint/_fixture_dim_expr_folding.das index d1caa62b4e..dba1b95ea2 100644 --- a/tests/lint/_fixture_dim_expr_folding.das +++ b/tests/lint/_fixture_dim_expr_folding.das @@ -17,7 +17,8 @@ let SPAN = ROWS * STEP // A vector constructor is a call, so this one folds through neither the operators nor // an already-constant node -- the shape a shader spells a basis matrix's column in. -let VEC_HALF = float4(1.0, 2.0, 3.0, 4.0) / 2.0 +//! a multiply, not a divide: a -ffast-math host divides a vector by a runtime splat through rcpps, one ULP off +let VEC_HALF = float4(1.0, 2.0, 3.0, 4.0) * 0.5 // A flat sum nests one operator per term, so this one is deeper than the bound that // stops a cycle of consts defined off each other. Depth in the AST is not a cycle.