From f556088e5aaa1d4028c0cb6420a70d0711c2e87c Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 5 Aug 2026 12:58:24 +0300 Subject: [PATCH 01/14] math: abs(-0.0) must return +0.0 SimPolicy_MathTT::Abs returns the argument unchanged for -0.0, and v_abs on x86 is max(-a, a), which hands back -0.0 as well. The JIT emits fabs and was already right, so this was a tier divergence; const folding evaluates these policies, so the folded form was wrong in both tiers. The vector fix clears the sign bit directly - include/vecmath is vendored and stays untouched. --- include/daScript/simulate/sim_policy.h | 5 +++-- tests/math/inf_and_nan.das | 30 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/include/daScript/simulate/sim_policy.h b/include/daScript/simulate/sim_policy.h index 496e56f611..96fe2a5ce4 100644 --- a/include/daScript/simulate/sim_policy.h +++ b/include/daScript/simulate/sim_policy.h @@ -179,6 +179,7 @@ namespace das { }; struct SimPolicy_Double : SimPolicy_Type, SimPolicy_MathTT { + static __forceinline double Abs ( double a, Context &, LineInfo * ) { return fabs(a); } static __forceinline double Div ( double a, double b, Context &, LineInfo * ) { return a / b; } static __forceinline void SetDiv ( double & a, double b, Context &, LineInfo * ) { a /= b; } static __forceinline double Mod ( double a, double b, Context &, LineInfo * ) { return fmod(a,b); } @@ -232,7 +233,7 @@ namespace das { struct SimPolicy_MathFloat { static __forceinline float Sign ( float a, Context &, LineInfo * ) { return a == 0.0f ? 0.0f : (a > 0.0f) ? 1.0f : -1.0f; } - static __forceinline float Abs ( float a, Context &, LineInfo * ) { return v_extract_x(v_abs(v_set_x(a))); } + static __forceinline float Abs ( float a, Context &, LineInfo * ) { return fabsf(a); } static __forceinline float Floor ( float a, Context &, LineInfo * ) { return v_extract_x(v_floor(v_set_x(a))); } static __forceinline float Ceil ( float a, Context &, LineInfo * ) { return v_extract_x(v_ceil(v_set_x(a))); } static __forceinline float Round ( float a, Context &, LineInfo * ) { return v_extract_x(v_round(v_set_x(a))); } @@ -284,7 +285,7 @@ namespace das { return v_or(v_and(v_splats(1.0f), v_cmp_gt(a, v_zero())), v_and(v_splats(-1.0f), v_cmp_lt(a, v_zero()))); } - static __forceinline vec4f Abs ( vec4f a, Context &, LineInfo * ) { return v_abs(a); } + static __forceinline vec4f Abs ( vec4f a, Context &, LineInfo * ) { return v_andnot(v_msbit(), a); } static __forceinline vec4f Floor ( vec4f a, Context &, LineInfo * ) { return v_floor(a); } static __forceinline vec4f Ceil ( vec4f a, Context &, LineInfo * ) { return v_ceil(a); } static __forceinline vec4f Fract ( vec4f a, Context &, LineInfo * ) { return v_sub(a, v_floor(a)); } diff --git a/tests/math/inf_and_nan.das b/tests/math/inf_and_nan.das index 79a0ec7982..37c0f9481e 100644 --- a/tests/math/inf_and_nan.das +++ b/tests/math/inf_and_nan.das @@ -44,6 +44,36 @@ def geq(a, b) { return a >= b } +[sideeffects] +def opaque(x) { + return x +} + +[test] +def test_half_subnormals_survive_conversion(t : T?) { + t |> equal(float(half(opaque(3.0517578e-5f))), 3.0517578e-5f) + t |> equal(float(half(opaque(5.9604645e-8f))), 5.9604645e-8f) // min subnormal + t |> equal(float(half(opaque(6.097555e-5f))), 6.097555e-5f) // max subnormal + t |> equal(float(half(opaque(2.9802322e-8f))), 0.0f) // half of min: to zero +} + +[test] +def test_abs_clears_the_sign_of_zero(t : T?) { + t |> run("folded") @@(t : T?) { + t |> equal(unsafe(reinterpret(abs(-0.0f))), 0u) + t |> equal(unsafe(reinterpret(abs(-0.0lf))), 0ul) + t |> equal(unsafe(reinterpret(abs(float4(-0.0f, -0.0f, -0.0f, -0.0f)))), uint4(0u, 0u, 0u, 0u)) + } + t |> run("runtime") @@(t : T?) { + t |> equal(unsafe(reinterpret(abs(opaque(-0.0f)))), 0u) + t |> equal(unsafe(reinterpret(abs(opaque(-0.0lf)))), 0ul) + let mz4 = float4(opaque(-0.0f), opaque(-0.0f), opaque(-0.0f), opaque(-0.0f)) + t |> equal(unsafe(reinterpret(abs(mz4))), uint4(0u, 0u, 0u, 0u)) + let mz2 = float2(opaque(-0.0f), opaque(-0.0f)) + t |> equal(unsafe(reinterpret(abs(mz2))), uint2(0u, 0u)) + } +} + [test] def test_inf_and_nan(t : T?) { if (host_folds_non_finite()) { From beb2002fd828947e419a62e878810c3734d6fe62 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 5 Aug 2026 12:58:25 +0300 Subject: [PATCH 02/14] jit: guard INT_MIN / -1, which LLVM sdiv leaves poison The interpreter throws "division overflow" for INT_MIN / -1 and answers 0 for INT_MIN % -1; the JIT emitted a raw sdiv/srem, which LLVM leaves poison. The division now throws like the interpreter, and the modulo selects a divisor of 1, since INT_MIN % 1 is 0 - the same answer. Signed scalars only: the interpreter does not guard vector division either, and int8/int16 have no division at all. Repins LLVM_JIT_EMITTER_HASH. The interpreter's DAS_FAST_INTEGER_MOD trick had the same divergence in disguise: int32_t(A/B) on the 2^31 quotient truncates to INT_MIN on x86 and saturates to INT_MAX on arm64. Converting the quotient through int64_t keeps every step exact on both, with no extra branch. --- include/daScript/simulate/sim_policy.h | 4 +- modules/dasLLVM/daslib/llvm_jit.das | 35 +++++++++++++ modules/dasLLVM/daslib/llvm_jit_run.das | 4 +- tests/jit_tests/div_overflow.das | 65 +++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/jit_tests/div_overflow.das diff --git a/include/daScript/simulate/sim_policy.h b/include/daScript/simulate/sim_policy.h index 96fe2a5ce4..147f3af7f2 100644 --- a/include/daScript/simulate/sim_policy.h +++ b/include/daScript/simulate/sim_policy.h @@ -142,12 +142,12 @@ namespace das { static __forceinline int32_t Mod ( int32_t a, int32_t b, Context & context, LineInfo * at ) { if ( b==0 ) context.throw_error_at(at, "division by zero in modulo"); double A = a, B = b; - return a - int32_t(A/B)*b; + return int32_t(a - int64_t(A/B)*b); } static __forceinline void SetMod ( int32_t & a, int32_t b, Context & context, LineInfo * at ) { if ( b==0 ) context.throw_error_at(at, "division by zero in modulo"); double A = a, B = b; - a = a - int32_t(A/B)*b; + a = int32_t(a - int64_t(A/B)*b); } }; diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index fb3f1c0d2a..e061999683 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -2982,6 +2982,32 @@ class public LlvmJitVisitor : AstVisitor { LLVMPositionBuilderAtEnd(g_builder, check_end) } + def division_overflow_cond(lval, right : LLVMOpaqueValue?; opType : TypeDeclPtr) : LLVMOpaqueValue? { + if (opType.baseType != Type.tInt && opType.baseType != Type.tInt64) { + panic("division overflow guard on {describe(opType)}") + } + let ty = type_to_llvm_type(opType) + let intMin = 1ul << uint64(LLVMGetIntTypeWidth(ty) - 1u) + var is_min = LLVMBuildICmp(g_builder, LLVMIntPredicate.LLVMIntEQ, lval, + LLVMConstInt(ty, intMin, 0), "cmp_is_intmin") + var is_m1 = LLVMBuildICmp(g_builder, LLVMIntPredicate.LLVMIntEQ, right, + LLVMConstAllOnes(ty), "cmp_is_minus_one") + return LLVMBuildAnd(g_builder, is_min, is_m1, "cmp_div_ovf") + } + + def check_division_overflow(lval, right : LLVMOpaqueValue?; at : LineInfo; opType : TypeDeclPtr) { + var check_ovf = append_basic_block("check_div_ovf") + var check_true = append_basic_block("check_ovf_true") + var check_end = append_basic_block("check_ovf_end") + LLVMBuildBr(g_builder, check_ovf) + LLVMPositionBuilderAtEnd(g_builder, check_ovf) + LLVMBuildCondBr(g_builder, division_overflow_cond(lval, right, opType), check_true, check_end) + LLVMPositionBuilderAtEnd(g_builder, check_true) + build_exception("division overflow", at) + LLVMBuildBr(g_builder, check_end) + LLVMPositionBuilderAtEnd(g_builder, check_end) + } + def isExprIntNZ(expr : ExpressionPtr) { return (((expr is ExprConstInt) && (expr as ExprConstInt).value != 0) || ((expr is ExprConstUInt) && (expr as ExprConstUInt).value != 0u) @@ -3195,6 +3221,15 @@ class public LlvmJitVisitor : AstVisitor { if (!isExprIntNZ(expr.right)) { check_divide_by_0(right, expr.at, opType, string(expr.op)) } + if (opType.isSignedInteger) { + let lval = expr.op == "/" || expr.op == "%" ? left : r2v_left + if (expr.op == "%" || expr.op == "%=") { + right = LLVMBuildSelect(g_builder, division_overflow_cond(lval, right, opType), + LLVMConstInt(type_to_llvm_type(opType), 1ul, 0), right, "mod_ovf_to_1") + } else { + check_division_overflow(lval, right, expr.at, opType) + } + } } if (expr.op == "/") { if (opType.isSignedInteger || (opType.isVectorType && opType.vectorBaseType == Type.tInt)) { diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index c6ac87cd58..2b5dfede21 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -36,11 +36,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 = 0x56ul // darwin in-memory arm emits no dtor list (0x55: handled types iterate and index through the annotation's jit hooks) +let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x57ul // INT_MIN / -1 and % -1 guards on sdiv/srem (0x56: darwin in-memory arm emits no dtor list) // 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 = 0x3140f6c8e0a8738eul +let LLVM_JIT_EMITTER_HASH : uint64 = 0xbe8db6d74086455aul let JIT_FNV_PRIME : uint64 = 1099511628211ul diff --git a/tests/jit_tests/div_overflow.das b/tests/jit_tests/div_overflow.das new file mode 100644 index 0000000000..3e223ee5ef --- /dev/null +++ b/tests/jit_tests/div_overflow.das @@ -0,0 +1,65 @@ +options gen2 +require dastest/testing_boost + +def div_jit(a : int, b : int) { + return a / b +} + +def mod_jit(a : int, b : int) { + return a % b +} + +def div64_jit(a : int64, b : int64) { + return a / b +} + +def mod64_jit(a : int64, b : int64) { + return a % b +} + +def set_div_jit(a : int, b : int) { + var r = a + r /= b + return r +} + +def set_mod_jit(a : int, b : int) { + var r = a + r %= b + return r +} + +def expect_div_panic(t : T?; msg : string; blk : block) { + var failed = false + try { + invoke(blk) + } recover { + failed = true + } + t |> success(failed, msg) +} + +[test] +def test_division_overflow(t : T?) { + t |> run("modulo folds to zero") @@(t : T?) { + t |> equal(mod_jit(INT_MIN, -1), 0, "INT_MIN % -1") + t |> equal(mod64_jit(LONG_MIN, -1l), 0l, "LONG_MIN % -1") + t |> equal(set_mod_jit(INT_MIN, -1), 0, "INT_MIN %= -1") + } + t |> run("division panics") @@(t : T?) { + expect_div_panic(t, "INT_MIN / -1") $ { + print("{div_jit(INT_MIN, -1)}") + } + expect_div_panic(t, "LONG_MIN / -1") $ { + print("{div64_jit(LONG_MIN, -1l)}") + } + expect_div_panic(t, "INT_MIN /= -1") $ { + print("{set_div_jit(INT_MIN, -1)}") + } + } + t |> run("plain values unaffected") @@(t : T?) { + t |> equal(div_jit(-14, 7), -2, "-14 / 7") + t |> equal(mod_jit(-14, 3), -2, "-14 % 3") + t |> equal(div64_jit(-14l, 7l), -2l, "-14 / 7 int64") + } +} From 332aac59db3f49ea78eb3677d83c43ecba27322d Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Fri, 7 Aug 2026 13:48:35 +0300 Subject: [PATCH 03/14] math: convert float to half subnormals instead of flushing them to zero das_float_to_float16's subnormal shift double-counted the 23->10 mantissa reduction, flushing every subnormal to zero; for the smallest inputs it reached 38 - UB, the source of the -nan. Hits any half(x) below the half min normal, 6.104e-5. The right shift is m = base >> (126 - e): base is 1.f << 23, and a half subnormal is m * 2^-24. Co-Authored-By: Claude Fable 5 --- include/daScript/misc/vectypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/daScript/misc/vectypes.h b/include/daScript/misc/vectypes.h index 0bcc0c1b81..6850f7ecac 100644 --- a/include/daScript/misc/vectypes.h +++ b/include/daScript/misc/vectypes.h @@ -201,7 +201,7 @@ namespace das } uint32_t base, sh; if ( em < ((127u - 14u) << 23) ) { // subnormal half - sh = 126u - (em >> 23) + 13u + 1u; + sh = 126u - (em >> 23); base = (em & 0x7fffffu) | 0x800000u; } else { // normal half sh = 13u; From feee73de9e8a897fb5378a4bdfbb360624011e83 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Tue, 25 Aug 2026 19:52:14 +0300 Subject: [PATCH 04/14] aot: break, continue and return out of try/recover The try and recover bodies are C++ lambdas passed to das_try_recover, so an exit aimed outside them stayed inside: break/continue did not compile ("'break' statement not in loop or switch statement"), and a return only left the lambda, silently answering differently than the interpreter -- tests/language/div_by_zero.das has that shape. Each body is scanned for the exits which really leave it; those set a control flag, and the dispatch after das_try_recover turns it back into the real exit -- itself translated, so nesting chains outwards. A returned value rides a slot typed by the function result. The fixtures divide through a global so RunFolding does not fold the calls away and the AOT'd bodies actually run. Fixes #3858 Co-Authored-By: Claude Opus 5 (1M context) --- daslib/aot_cpp.das | 136 +++++++++++++++- tests/language/try_recover_flow.das | 236 ++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 tests/language/try_recover_flow.das diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index b13d93ecdc..4500791fb5 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -1304,6 +1304,60 @@ def public describeCppFunc(fn : FunctionPtr; var collector : BlockVariableCollec } } +enum private ExitKind { None, Break, Continue, Return } + +struct private TryExits { + hasBreak : bool + hasContinue : bool + hasReturn : bool +} + +struct private TryFlowLevel { + isLoop : bool + exits : TryExits + flag : string + slot : string +} + +struct private TryFlowState { + stack : array + ret_slot : string + ret_ptr : bool +} + +class TryExitScanner : AstVisitor { + found : TryExits; + loopDepth : int = 0; + + def override preVisitExprForBody(ffor : ExprFor?) { + loopDepth ++; + } + def override visitExprFor(var ffor : ExprFor?) : ExpressionPtr { + loopDepth --; + return ffor; + } + def override preVisitExprWhileBody(wh : ExprWhile?; body : ExpressionPtr) { + loopDepth ++; + } + def override visitExprWhile(var wh : ExprWhile?) : ExpressionPtr { + loopDepth --; + return wh; + } + def override preVisitExprBreak(that : ExprBreak?) { + found.hasBreak ||= loopDepth == 0; + } + def override preVisitExprContinue(that : ExprContinue?) { + found.hasContinue ||= loopDepth == 0; + } + def override preVisitExprReturn(expr : ExprReturn?) { + found.hasReturn ||= expr.returnFunc != null; + } +}; + +def private anyExit(exits : TryExits) { + return exits.hasBreak || exits.hasContinue || exits.hasReturn; +} + class public CppAot : AstVisitor { //! Main AST visitor that generates C++ ahead-of-time compiled code from daslang AST. @@ -1333,6 +1387,7 @@ class public CppAot : AstVisitor { aotPrefix : table; @do_not_delete local_temp_names : table; @do_not_delete scopes : array; + flow : TryFlowState; prologue : bool = false; solidContext : bool = false; cross_platform : bool = false; @@ -1500,6 +1555,9 @@ class public CppAot : AstVisitor { return !fun.moreFlags.isTemplate; } def override preVisitFunction(fn : FunctionPtr) { + flow.ret_ptr = fn.result.flags.ref; + flow.ret_slot = fn.result.isVoid ? "" : describeCppType(fn.result, DescribeConfig(skip_const = true, + skip_ref = true, cross_platform = cross_platform)) + (flow.ret_ptr ? " *" : ""); if (!empty(aot_filter_function) && (fn.name == aot_filter_function || aotFuncName(fn) == aot_filter_function)) { write(*ss, "\n/*<>*/"); } @@ -2190,8 +2248,15 @@ class public CppAot : AstVisitor { } return that; } + def returnLevel(expr : ExprReturn?) : int { + return expr.returnFunc == null ? -1 : flowLevel(ExitKind.Return); + } def override preVisitExprReturn(expr : ExprReturn?) { - write(*ss, "return "); + let at = returnLevel(expr); + write(*ss, at < 0 ? "return " : "\{ "); + if (at >= 0 && flow.stack[at].slot != "") { + write(*ss, "{flow.stack[at].slot} = {flow.ret_ptr ? "&(" : ""}"); + } if (expr.returnFlags.moveSemantics) { write(*ss, "/* <- */ "); } @@ -2218,17 +2283,24 @@ class public CppAot : AstVisitor { write(*ss, "/* no function/block for return */ "); return expr; } + let at = returnLevel(expr); assume retT = expr.returnFunc != null ? expr.returnFunc.result : expr._block.returnType; if (!retT.isVoid) { write(*ss, ")"); + if (at >= 0 && flow.ret_ptr) { + write(*ss, ")"); + } + } + if (at >= 0) { + write(*ss, "{retT.isVoid ? "" : "; "}{flow.stack[at].flag} = {int(ExitKind.Return)}; return; \}"); } return expr; } def override preVisitExprBreak(that : ExprBreak?) { - write(*ss, "break"); + writeExit(ExitKind.Break, ""); } def override preVisitExprContinue(that : ExprContinue?) { - write(*ss, "continue"); + writeExit(ExitKind.Continue, ""); } def override preVisitExprVar(variable : ExprVar?) { if (variable._type.flags.aotAlias) { @@ -2779,8 +2851,10 @@ class public CppAot : AstVisitor { } def override preVisitExprWhileBody(wh : ExprWhile?; body : ExpressionPtr) { write(*ss, " )\n{tabs()}"); + flow.stack |> push(TryFlowLevel(isLoop = true)); } def override visitExprWhile(var wh : ExprWhile?) : ExpressionPtr { + flow.stack |> pop(); return wh; } def override preVisitExprIfThenElse(ifte : ExprIfThenElse?) { @@ -2986,7 +3060,49 @@ class public CppAot : AstVisitor { write(*ss, ")"); return expr; } + def flowLevel(code : ExitKind) : int { + for (i in range(length(flow.stack))) { + let at = length(flow.stack) - 1 - i; + assume lvl = flow.stack[at]; + if (!lvl.isLoop) return lvl.flag != "" ? at : -1; + if (code != ExitKind.Return) return -1; + } + return -1; + } + def writeExit(code : ExitKind; slot : string) { + let at = flowLevel(code); + if (at >= 0) { + write(*ss, "\{ {flow.stack[at].flag} = {int(code)}; return; \}"); + } elif (code == ExitKind.Break) { + write(*ss, "break"); + } elif (code == ExitKind.Continue) { + write(*ss, "continue"); + } else { + write(*ss, "return {slot == "" ? "" : (flow.ret_ptr ? "*" : "") + slot}"); + } + } + def scanExits(tc : ExprTryCatch?) : TryExits { + var scanner = new TryExitScanner(); + make_visitor(*scanner) $(adp) { + tc.try_block |> visit(adp); + tc.catch_block |> visit(adp); + } + return scanner.found; + } def override preVisitExprTryCatch(tc : ExprTryCatch?) { + var lvl = TryFlowLevel(exits = scanExits(tc)); + if (anyExit(lvl.exits)) { + lvl.flag = "__try_flow_{int(tc.at.line)}_{int(tc.at.column)}"; + write(*ss, "int32_t {lvl.flag} = {int(ExitKind.None)};\n{tabs()}"); + let at = flowLevel(ExitKind.Return); + if (at >= 0) { + lvl.slot = flow.stack[at].slot; + } elif (lvl.exits.hasReturn && flow.ret_slot != "") { + lvl.slot = "__try_ret_{int(tc.at.line)}_{int(tc.at.column)}"; + write(*ss, "{flow.ret_slot} {lvl.slot};\n{tabs()}"); + } + } + flow.stack |> push(lvl); write(*ss, "das_try_recover(__context__, [&]()\n"); write(*ss, "{tabs()}"); } @@ -2996,6 +3112,18 @@ class public CppAot : AstVisitor { } def override visitExprTryCatch(var tc : ExprTryCatch?) : ExpressionPtr { write(*ss, ")"); + let lvl = flow.stack[length(flow.stack) - 1]; + flow.stack |> pop(); + if (!anyExit(lvl.exits)) return tc; + write(*ss, ";"); + let taken = fixed_array(lvl.exits.hasBreak, lvl.exits.hasContinue, lvl.exits.hasReturn); + let codes = fixed_array(ExitKind.Break, ExitKind.Continue, ExitKind.Return); + for (t, code in taken, codes) { + if (!t) continue; + write(*ss, "\n{tabs()}if ( {lvl.flag} == {int(code)} ) \{ "); + writeExit(code, lvl.slot); + write(*ss, "; \}"); + } return tc; } def isDistinctDeref(ptr2ref : ExprPtr2Ref?) { @@ -3932,6 +4060,7 @@ class public CppAot : AstVisitor { write(*ss, "{tabs()}bool {nl} = true;\n"); } def override preVisitExprForBody(ffor : ExprFor?) { + flow.stack |> push(TryFlowLevel(isLoop = true)); let nl = needLoopName(ffor); write(*ss, "{tabs()}for ( ; {nl} ; {nl} = "); for (variable in ffor.iteratorVariables) { @@ -3997,6 +4126,7 @@ class public CppAot : AstVisitor { return that; } def override visitExprFor(var ffor : ExprFor?) : ExpressionPtr { + flow.stack |> pop(); write(*ss, "\n"); for (x in range(ffor.iteratorVariables |> length)) { assume variable = ffor.iteratorVariables[ffor.iteratorVariables |> length - 1 - x]; diff --git a/tests/language/try_recover_flow.das b/tests/language/try_recover_flow.das new file mode 100644 index 0000000000..02fd7ca81a --- /dev/null +++ b/tests/language/try_recover_flow.das @@ -0,0 +1,236 @@ +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 { + var seen = 0 + for (i in range(0, g_rounds)) { + try { + break if (i == 2) + seen ++ + } recover { + } + seen += 10 + } + return seen +} + +[export, no_jit] +def continue_in_while : int { + var seen = 0 + var i = 0 + while (i < g_rounds) { + i ++ + try { + continue if (i % 2 == 0) + seen ++ + } recover { + } + seen += 10 + } + return seen +} + +[export, no_jit] +def break_in_recover : int { + var seen = 0 + for (i in range(0, g_rounds)) { + try { + panic("stop") if (i == 2) + seen ++ + } recover { + break + } + seen += 10 + } + return seen +} + +[export, no_jit] +def nested_try : int { + var seen = 0 + for (i in range(0, g_rounds)) { + try { + try { + continue if (i == 1) + break if (i == 3) + } recover { + } + seen ++ + } recover { + } + seen += 10 + } + return seen +} + +[export, no_jit] +def inner_loop_break : int { + var seen = 0 + for (i in range(0, g_rounds)) { + try { + for (j in range(0, 4)) { + break if (j == 1) + seen ++ + } + break if (i == 2) + } recover { + } + seen += 10 + } + return seen +} + +[export] +def inner_only_break : int { + var seen = 0 + try { + for (j in range(0, 4)) { + break if (j == 2) + seen ++ + } + seen += 100 + } recover { + } + return seen +} + +[export] +def try_without_exit : int { + var seen = 0 + for (_i in range(0, g_rounds)) { + try { + seen ++ + } recover { + } + } + 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 { + return 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") + t |> equal(continue_in_while(), 33, "continue skips the rest of the while iteration") + t |> equal(break_in_recover(), 22, "break in the recover block leaves the loop") + t |> equal(nested_try(), 22, "nested try chains the exit outwards") + 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") +} From 49267f52a2b660969d1fa43f04088c7654db39fa Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 26 Aug 2026 15:28:54 +0300 Subject: [PATCH 05/14] aot: a ref-typed make-local declares its temp as the value The temporary a make-local writes into is returned BY reference, but its declaration went through describeVarLocalCppType, which substitutes a reference with a pointer. A make-local of a reference type therefore declared `Func *` while the lambda around it returned `Func &`: error: non-const lvalue reference to type 'Func' cannot bind to a value of unrelated type 'Func *' Declare the value for a ref result, which is what the return binds to. Nothing else in the tree has a ref-typed make-local temp. Fixes #3858 Co-Authored-By: Claude Opus 5 (1M context) --- daslib/aot_cpp.das | 7 ++++++- tests/aot/test_ref_make_local.das | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/aot/test_ref_make_local.das diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index 4500791fb5..bd5b4b2925 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -1506,7 +1506,12 @@ class public CppAot : AstVisitor { } def write_local_temp_type(tmp : Expression?) : void { write(*ss, tabs()) - describeVarLocalCppType(ss, tmp._type, cross_platform) + if (tmp._type.flags.ref) { + write(*ss, describeCppType(tmp._type, DescribeConfig(skip_ref = true, + skip_const = true, cross_platform = cross_platform))) + } else { + describeVarLocalCppType(ss, tmp._type, cross_platform) + } } def override preVisitGlobalLet(prog : ProgramPtr) { diff --git a/tests/aot/test_ref_make_local.das b/tests/aot/test_ref_make_local.das new file mode 100644 index 0000000000..43455b818f --- /dev/null +++ b/tests/aot/test_ref_make_local.das @@ -0,0 +1,10 @@ +options gen2 +require dastest/testing_boost public + +[export] +def zero_function_ref() => type + +[test] +def test_ref_make_local(t : T?) { + t |> success(zero_function_ref() == null, "zeroed function reference") +} From d604ed358696127cae68cb3207b67a9abf39666f Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 26 Aug 2026 15:36:14 +0300 Subject: [PATCH 06/14] aot: only a pointer to a handle ascends as a handle The ascend emitter picked das_ascend_handle from firstType alone. For a pointer that is the pointee, but for a lambda or block it is the RESULT type, so a lambda returning a C++-bound type took the handle path and instantiated a template with no definition: error: implicit instantiation of undefined template 'das::das_ascend_handle' Gate on the ascended type being a pointer as well. A handled result is an ordinary capture-frame allocation, same as any other result type. No ascend anywhere else in the tree changes. Fixes #3858 Co-Authored-By: Claude Opus 5 (1M context) --- daslib/aot_cpp.das | 2 +- tests/aot/test_lambda_handled_result.das | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/aot/test_lambda_handled_result.das diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index bd5b4b2925..30b50a8602 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -3216,7 +3216,7 @@ class public CppAot : AstVisitor { let info : TypeInfo? = (expr.ascendFlags.needTypeInfo ? helper.helper |> make_type_info(null, expr.subexpr._type) : null); - if (expr._type.firstType.baseType == Type.tHandle) { + if (expr._type.baseType == Type.tPointer && expr._type.firstType.baseType == Type.tHandle) { write(*ss, "das_ascend_handle<{expr._type.flags.smartPtr},{describeCppType(expr._type,DescribeConfig(skip_ref=true,skip_const=true,cross_platform=cross_platform))}>::make(__context__,"); } else { let type_str = describeCppType(expr._type.firstType, DescribeConfig(skip_ref = true, skip_const = true, cross_platform = cross_platform)); diff --git a/tests/aot/test_lambda_handled_result.das b/tests/aot/test_lambda_handled_result.das new file mode 100644 index 0000000000..c849beb5b2 --- /dev/null +++ b/tests/aot/test_lambda_handled_result.das @@ -0,0 +1,15 @@ +options gen2 +require UnitTest +require dastest/testing_boost public + +[export] +def make_handled_lambda { + var a = @ { return makeDummy(); } + unsafe { delete a; } +} + +[test] +def test_lambda_handled_result(t : T?) { + make_handled_lambda() + t |> success(true, "lambda with a handled result builds") +} From 648ad99dd059b2312466966922785e6a427e3664 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 26 Aug 2026 16:32:11 +0300 Subject: [PATCH 07/14] infer: a goto out of a captured block is a compile error findLabel walked every enclosing scope, so a goto inside a captured block resolved against a label in the enclosing function. Nothing downstream can honour that: AOT emits `goto label_50` into the C++ lambda the block became ("use of undeclared label"), the JIT fails to simulate (50503), and the interpreter itself is inconsistent -- it jumps when the label has statements after it and throws "jump to label N failed" when it does not. Stop the search at a closure, which is the rule 'break' and 'continue' already follow (30125 / 30126). The existing "can't find label" diagnostic then reports it, at the goto rather than at the block. Fixes #3858 Co-Authored-By: Claude Opus 5 (1M context) --- src/ast/ast_infer_type.cpp | 1 + tests/language/failed_goto_out_of_block.das | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 tests/language/failed_goto_out_of_block.das diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index b66a602d75..1c89f66aa9 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -1058,6 +1058,7 @@ namespace das { } } } + if (blk->isClosure) break; } return nullptr; } diff --git a/tests/language/failed_goto_out_of_block.das b/tests/language/failed_goto_out_of_block.das new file mode 100644 index 0000000000..2ff56233f3 --- /dev/null +++ b/tests/language/failed_goto_out_of_block.das @@ -0,0 +1,11 @@ +options gen2 +expect 30819 + +[export] +def main { + invoke($() { + goto label 50 + }) + label 50: + pass +} From 6a48df1d65890d35f61d67fb731bfdd12d49752a Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 26 Aug 2026 17:36:01 +0300 Subject: [PATCH 08/14] aot: do not walk into an assume's subexpression An assume keeps its subexpression as a template for the use sites and generates no code of its own -- the emitter already wraps it in `#if 0`. It still descended into it, and asked for type info the nodes there do not carry, so `-aot` on an assume bound to an interpolated string crashed in makeTypeInfo. Only with the optimizer off: otherwise the builder folds away before AOT sees it. The #if 0 region is labeled with the assume's name and expression - describe renders arbitrary text, so it stays behind the preprocessor rather than in a bare line comment. Return false from canVisitWithAliasSubexpression, which is what the lint and verify visitors already do for the same reason. Fixes #3858 Co-Authored-By: Claude Opus 5 (1M context) The jit side had the same walk: ResolveExternVisitor, DisableJitVisitor and llvm_exe's CollectExternVisitor descend into the assume subexpression and touch an ExprOp2 whose func was never inferred. Both get the same canVisitWithAliasSubexpression override the jit codegen visitor already carries. --- daslib/aot_cpp.das | 7 +++++-- modules/dasLLVM/daslib/llvm_exe.das | 4 ++++ modules/dasLLVM/daslib/llvm_jit.das | 8 ++++++++ modules/dasLLVM/daslib/llvm_jit_run.das | 2 +- tests/aot/test_assume_string_builder.das | 14 ++++++++++++++ 5 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/aot/test_assume_string_builder.das diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index 30b50a8602..8e3e21fbd3 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -2838,11 +2838,14 @@ class public CppAot : AstVisitor { } return c; } + def override canVisitWithAliasSubexpression(_expr : ExprAssume?) : bool { + return false; + } def override preVisitExprAssume(expr : ExprAssume?) { - write(*ss, "\n#if 0 // with, note optimizations are off\n"); + write(*ss, "\n#if 0 // assume {expr.alias} = {describe(expr.subexpr) |> replace("\n", " ")}\n"); } def override visitExprAssume(var expr : ExprAssume?) : ExpressionPtr { - write(*ss, "\n#endif\n"); + write(*ss, "\n#endif // assume {expr.alias}\n"); return expr; } def override preVisitExprWith(expr : ExprWith?) { diff --git a/modules/dasLLVM/daslib/llvm_exe.das b/modules/dasLLVM/daslib/llvm_exe.das index cac064ead5..c5f910af45 100644 --- a/modules/dasLLVM/daslib/llvm_exe.das +++ b/modules/dasLLVM/daslib/llvm_exe.das @@ -94,6 +94,10 @@ class public CollectExternVisitor : AstVisitor { // Create the initialize_modules() function, position a builder in it, and emit das_ensure_environment // plus the builtin-module registrations (all native modules if register_all, else $ + strings). Returns the builder. + def override canVisitWithAliasSubexpression(expr : ExprAssume?) : bool { + return false + } + def private setup_init_builder(register_all : bool) : LLVMBuilderRef { let init_fn = LLVMGetNamedFunction(g_mod, "initialize_modules") let init_entry = LLVMAppendBasicBlockInContext(ctx, init_fn, "entry") diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index e061999683..63d3e7ba07 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -7687,6 +7687,10 @@ class public ResolveExternVisitor : AstVisitor { @do_not_delete thisBlock : array uid : UidNodes? + def override canVisitWithAliasSubexpression(expr : ExprAssume?) : bool { + return false + } + def override preVisitFunction(var fun : FunctionPtr) : void { uid.reset(fun) thisFunc = fun @@ -8226,6 +8230,10 @@ class public DisableJitVisitor : AstVisitor { def TypeInfoVisitor { disable = false } + + def override canVisitWithAliasSubexpression(expr : ExprAssume?) : bool { + return false + } // Default-argument inits are inlined into call sites at infer, so the copy left on the // argument is a dead husk LlvmJitVisitor declines too - and ClearUnusedSymbols nulls refs // inside it, so expr.func there is legitimately null. diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 2b5dfede21..f97d51418d 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -40,7 +40,7 @@ let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x57ul // INT_MIN / -1 and % -1 guards // 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 = 0xbe8db6d74086455aul +let LLVM_JIT_EMITTER_HASH : uint64 = 0x636a5a780f5ea1a8ul let JIT_FNV_PRIME : uint64 = 1099511628211ul diff --git a/tests/aot/test_assume_string_builder.das b/tests/aot/test_assume_string_builder.das new file mode 100644 index 0000000000..95f56b0c72 --- /dev/null +++ b/tests/aot/test_assume_string_builder.das @@ -0,0 +1,14 @@ +options gen2 +options optimize = false +require dastest/testing_boost public + +[export] +def assumed_interpolation : string { + assume v = "n={1 + 1}" + return "{v}" +} + +[test] +def test_assume_string_builder(t : T?) { + t |> equal(assumed_interpolation(), "n=2") +} From f9cddc65e27d8c1657425f08d10b933b1b3166d7 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 26 Aug 2026 18:30:59 +0300 Subject: [PATCH 09/14] infer: a structure alias gets the same loop check as a top-level one An alias is structural, so `typedef T = array` has no finite expansion. Top level says so already (31106), but a structure alias is walked by Program::visitStructure through preVisitStructureAlias / visitStructureAlias, neither of which InferTypes overrode -- so the check in visitAlias never ran on one, and struct and class aliases accepted the impossible type silently. The nominal spelling `struct S { x : array }` is unaffected: S is a name, not an expansion. isLoop also learns to look inside typeMacroExpr, which is where a typeMacro keeps its arguments -- `typedef T = Unknown` at top level now reports the loop rather than the missing macro. Co-Authored-By: Claude Opus 5 (1M context) --- include/daScript/ast/ast_infer_type.h | 1 + src/ast/ast_infer_type.cpp | 10 ++++++++++ src/ast/ast_infer_type_helper.cpp | 8 ++++++++ tests/language/failed_struct_alias_loop.das | 11 +++++++++++ 4 files changed, 30 insertions(+) create mode 100644 tests/language/failed_struct_alias_loop.das diff --git a/include/daScript/ast/ast_infer_type.h b/include/daScript/ast/ast_infer_type.h index 0b8f4f656c..1112536bea 100644 --- a/include/daScript/ast/ast_infer_type.h +++ b/include/daScript/ast/ast_infer_type.h @@ -337,6 +337,7 @@ namespace das { // strcuture virtual bool canVisitStructure(Structure *st) override; + virtual void preVisitStructureAlias(Structure *var, const string &name, TypeDecl *at) override; virtual void preVisit(Structure *that) override; virtual void preVisitStructureField(Structure *that, Structure::FieldDeclaration &decl, bool last) override; bool hasSafeWhenUninitialized(const AnnotationArgumentList &args) const; diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 1c89f66aa9..3021d2ec2f 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -307,6 +307,16 @@ namespace das { if ( fatalAliasLoop ) return false; return !st->isTemplate; // we don't do a thing with templates } + void InferTypes::preVisitStructureAlias(Structure *var, const string &name, TypeDecl *at) { + Visitor::preVisitStructureAlias(var, name, at); + vector visited; + visited.push_back(name); + if ( isLoop(visited, at) ) { + fatalAliasLoop = true; + error("alias loop detected: '" + describeType(at) + "'", "", "", + at->at, CompilationError::recursion_type_alias); + } + } void InferTypes::preVisit(Structure *that) { Visitor::preVisit(that); checkEmptyName(that->name, "structure declaration", that->at); diff --git a/src/ast/ast_infer_type_helper.cpp b/src/ast/ast_infer_type_helper.cpp index 09ec3ce206..95fd0aa306 100644 --- a/src/ast/ast_infer_type_helper.cpp +++ b/src/ast/ast_infer_type_helper.cpp @@ -378,6 +378,14 @@ namespace das { return true; } } + for ( auto & tme : decl->typeMacroExpr ) { + if ( tme && tme->rtti_isTypeDecl() ) { + auto te = static_cast(tme); + if ( te->typeexpr && isLoop(visited, te->typeexpr) ) { + return true; + } + } + } if ( decl->baseType == Type::alias ) { visited.pop_back(); } diff --git a/tests/language/failed_struct_alias_loop.das b/tests/language/failed_struct_alias_loop.das new file mode 100644 index 0000000000..47a020e69d --- /dev/null +++ b/tests/language/failed_struct_alias_loop.das @@ -0,0 +1,11 @@ +options gen2 +expect 31106 + +struct S { + typedef T = array +} + +[export] +def main { + pass +} From d941635ae8e0fdac769a2fdf030a88feccb1ddc4 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 26 Aug 2026 19:06:46 +0300 Subject: [PATCH 10/14] infer: check a structure's aliases before walking into them The loop check had to move ahead of the walk. Program::visitStructure calls preVisitStructureAlias and then descends into every alias unconditionally, so reporting from the pre-visit did not stop the descent -- and the descent is what overflowed the stack on an alias which expands into itself (TypeDecl::visit / ExprTypeDecl::visit, ~88k frames). canVisitStructure runs it instead, where returning false skips the walk in the same pass. `struct S { typedef T = Unknown; }` now reports the loop rather than crashing the compiler. Fixes #3858 Co-Authored-By: Claude Opus 5 (1M context) --- include/daScript/ast/ast_infer_type.h | 1 - src/ast/ast_infer_type.cpp | 24 ++++++++++++------- .../failed_struct_alias_typemacro_loop.das | 11 +++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 tests/language/failed_struct_alias_typemacro_loop.das diff --git a/include/daScript/ast/ast_infer_type.h b/include/daScript/ast/ast_infer_type.h index 1112536bea..0b8f4f656c 100644 --- a/include/daScript/ast/ast_infer_type.h +++ b/include/daScript/ast/ast_infer_type.h @@ -337,7 +337,6 @@ namespace das { // strcuture virtual bool canVisitStructure(Structure *st) override; - virtual void preVisitStructureAlias(Structure *var, const string &name, TypeDecl *at) override; virtual void preVisit(Structure *that) override; virtual void preVisitStructureField(Structure *that, Structure::FieldDeclaration &decl, bool last) override; bool hasSafeWhenUninitialized(const AnnotationArgumentList &args) const; diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 3021d2ec2f..5f05dccebd 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -305,17 +305,23 @@ namespace das { } bool InferTypes::canVisitStructure(Structure *st) { if ( fatalAliasLoop ) return false; - return !st->isTemplate; // we don't do a thing with templates - } - void InferTypes::preVisitStructureAlias(Structure *var, const string &name, TypeDecl *at) { - Visitor::preVisitStructureAlias(var, name, at); - vector visited; - visited.push_back(name); - if ( isLoop(visited, at) ) { + if ( st->isTemplate ) return false; // we don't do a thing with templates + bool aliasLoop = false; + st->aliases.foreach([&](const TypeDeclPtr & atype) -> bool { + vector visited; + visited.push_back(atype->alias); + if ( isLoop(visited, atype) ) { + aliasLoop = true; + error("alias loop detected: '" + describeType(atype) + "'", "", "", + atype->at, CompilationError::recursion_type_alias); + } + return true; + }); + if ( aliasLoop ) { fatalAliasLoop = true; - error("alias loop detected: '" + describeType(at) + "'", "", "", - at->at, CompilationError::recursion_type_alias); + return false; } + return true; } void InferTypes::preVisit(Structure *that) { Visitor::preVisit(that); diff --git a/tests/language/failed_struct_alias_typemacro_loop.das b/tests/language/failed_struct_alias_typemacro_loop.das new file mode 100644 index 0000000000..add33b1650 --- /dev/null +++ b/tests/language/failed_struct_alias_typemacro_loop.das @@ -0,0 +1,11 @@ +options gen2 +expect 31106 + +struct S { + typedef T = Unknown +} + +[export] +def main { + pass +} From 3cedb25aceb530e40e7706bc4aae7f6756cbd991 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 27 Aug 2026 08:49:07 +0300 Subject: [PATCH 11/14] infer: a substituted table type checks its key is hashable isTableKeyType rejects the 16/8-bit lattice on purpose - the runtime table key machinery has no rows for it, so it is meant to be a clean compile error. inferGenericType checks it on its table branch, but a table literal lowers to to_table_move(tuple[...]): the key binds from a tuple, that branch never runs, and the table type is built afterwards in inferAlias with no check at all. So `{0h => 1}` produced a table whose key reached makeValueNode - the assert in Debug, a SIGSEGV in Release. The same crash on any use of such a literal, returned or not. Check the key where the table type is substituted. The existing 30254 ("table key has to be declared as a basic 'hashable' type") then reports it. Fixes #3858 Co-Authored-By: Claude Opus 5 (1M context) --- src/ast/ast_infer_type_helper.cpp | 2 ++ tests/language/failed_table_literal_float16_key.das | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 tests/language/failed_table_literal_float16_key.das diff --git a/src/ast/ast_infer_type_helper.cpp b/src/ast/ast_infer_type_helper.cpp index 95fd0aa306..4f9143c271 100644 --- a/src/ast/ast_infer_type_helper.cpp +++ b/src/ast/ast_infer_type_helper.cpp @@ -489,6 +489,8 @@ namespace das { resT->firstType = inferAlias(decl->firstType, fptr, aliases, options, autoToAlias); if (!resT->firstType) return nullptr; + if (!resT->firstType->isAutoOrAlias() && !resT->firstType->isTableKeyType()) + return nullptr; } if (decl->secondType) { resT->secondType = inferAlias(decl->secondType, fptr, aliases, options, autoToAlias); diff --git a/tests/language/failed_table_literal_float16_key.das b/tests/language/failed_table_literal_float16_key.das new file mode 100644 index 0000000000..dc57a2416f --- /dev/null +++ b/tests/language/failed_table_literal_float16_key.das @@ -0,0 +1,7 @@ +options gen2 +expect 30254:2, 30814, 30820 + +[export] +def main { + return {0h => 1} +} From b29367ba8a5dfba5cfad84e73c6b5f52f87f2213 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 27 Aug 2026 15:41:46 +0300 Subject: [PATCH 12/14] infer: an unnamed auto is not an alias lookup `type<...>` marks its payload autoToAlias, so named autos inside a tag participate in alias binding, and the flag inherits down the payload's subtree. An ANONYMOUS auto reaching that mode was looked up by its name - the empty string - and InferTypes::findAlias scans global variables' types, where TypeDecl::findAlias matches `alias == name` without excluding "": the first type node with an empty alias field wins. For variant var9{} var v : var9[ generator < type < lambda <>>>{} ] ; the anonymous auto inside `lambda<>` was substituted with v's own type, dimension expression included. A type that contains itself through its own dimension doubles on every inference pass - each type copy re-clones the dimension expression, which contains the type - until a recursive walk runs out of stack around pass 40. An unnamed auto has nothing to look up by name, whatever the mode; with the lookup gone, the dimension converges to plain 30109 like any other non-constant. Fixes #3858 Co-Authored-By: Claude Fable 5 --- src/ast/ast_infer_type.cpp | 5 +++++ src/ast/ast_infer_type_helper.cpp | 2 +- tests/language/failed_generator_dim.das | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tests/language/failed_generator_dim.das diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 5f05dccebd..32c86e06ef 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -5356,6 +5356,11 @@ namespace das { uint32_t tf = expr->body->getEvalFlags(); if (tf & EvalFlags::yield) { // only unwrap if it has "yield" auto blk = replaceGeneratorFor(expr, func); + if (!blk) { + error("generator for is not fully inferred yet", "", "", + expr->at, CompilationError::not_resolved_yet_block); + return Visitor::visit(expr); + } scopes.back()->needCollapse = true; reportAstChanged(); return blk; diff --git a/src/ast/ast_infer_type_helper.cpp b/src/ast/ast_infer_type_helper.cpp index 4f9143c271..8f1879d45d 100644 --- a/src/ast/ast_infer_type_helper.cpp +++ b/src/ast/ast_infer_type_helper.cpp @@ -397,7 +397,7 @@ namespace das { if (decl->baseType == Type::typeDecl || decl->baseType == Type::typeMacro) { return nullptr; } - if (decl->baseType == Type::autoinfer && !autoToAlias) { // until alias is fully resolved, can't infer + if (decl->baseType == Type::autoinfer && (!autoToAlias || decl->alias.empty())) { return nullptr; } if (decl->baseType == Type::alias || (decl->baseType == Type::autoinfer && autoToAlias)) { diff --git a/tests/language/failed_generator_dim.das b/tests/language/failed_generator_dim.das new file mode 100644 index 0000000000..96b7a48b48 --- /dev/null +++ b/tests/language/failed_generator_dim.das @@ -0,0 +1,11 @@ +options gen2 +expect 30109, 30816 + +variant var9 {} + +var v : var9[generator>> {}] + +[export] +def main { + pass +} From 6c696bddbdc5d8ca560c6c884421f294830e665f Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 27 Aug 2026 13:24:54 +0300 Subject: [PATCH 13/14] generate: a for does not lower while its subtree is still uninferred RenameVar walks a for's iterator NAMES, which the parser fills, and indexes iteratorVariables, which infer fills, with the same index. A nested for that is not inferred yet has names but no variables, so the rename read past the empty vector and crashed on the garbage pointer. Source-reachable: an iterator comprehension lowers while a for inside an interpolated string in its source is still uninferred. Rather than renaming a partial set, the rename reports the subtree not ready and replaceGeneratorFor declines the whole lowering for this pass - generation waits for inference, retrying once the inner for is inferred, or surfacing the real errors if it never is. Fixes #3858 Co-Authored-By: Claude Fable 5 --- include/daScript/ast/ast_generate.h | 2 +- src/ast/ast_generate.cpp | 13 +++++++++++-- .../failed_generator_for_uninferred_rename.das | 6 ++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 tests/language/failed_generator_for_uninferred_rename.das diff --git a/include/daScript/ast/ast_generate.h b/include/daScript/ast/ast_generate.h index f5e09beeca..6f959ff9d1 100644 --- a/include/daScript/ast/ast_generate.h +++ b/include/daScript/ast/ast_generate.h @@ -273,7 +273,7 @@ namespace das { give variables in the scope of 'expr' block unique names only for the top-level block */ - void giveBlockVariablesUniqueNames ( ExpressionPtr expr ); + bool giveBlockVariablesUniqueNames ( ExpressionPtr expr ); /* replace break and continue of a particular loop diff --git a/src/ast/ast_generate.cpp b/src/ast/ast_generate.cpp index 3f1a37a7a3..c9e6eae0d5 100644 --- a/src/ast/ast_generate.cpp +++ b/src/ast/ast_generate.cpp @@ -876,6 +876,10 @@ namespace das { } virtual void preVisit ( ExprFor * expr ) override { Visitor::preVisit(expr); + if ( expr->iterators.size() != expr->iteratorVariables.size() ) { + ready = false; + return; + } if ( scopes.size()==0 ) { // only top level for loop for ( size_t i=0; i!=expr->iterators.size(); ++i ) { auto & varName = expr->iterators[i]; @@ -908,14 +912,17 @@ namespace das { expr->name = it->second; } } + public: + bool ready = true; protected: vector scopes; das_hash_map rename; }; - void giveBlockVariablesUniqueNames ( ExpressionPtr expr ) { + bool giveBlockVariablesUniqueNames ( ExpressionPtr expr ) { RenameVar rename; expr->visit(rename); + return rename.ready; } // rename variable @@ -1442,7 +1449,9 @@ namespace das { if ( expr->body->rtti_isBlock() ) { forCopy = static_cast(expr->clone()); bodyBlock = static_cast(forCopy->body); - giveBlockVariablesUniqueNames(forCopy); + if ( !giveBlockVariablesUniqueNames(forCopy) ) { + return nullptr; + } if ( hasFinally ) { // break -> set flag, goto mid (finally runs, flag check -> end, iterator_close runs) // continue -> mid (finally runs, advances iterator, re-checks) diff --git a/tests/language/failed_generator_for_uninferred_rename.das b/tests/language/failed_generator_for_uninferred_rename.das new file mode 100644 index 0000000000..385ef8c47c --- /dev/null +++ b/tests/language/failed_generator_for_uninferred_rename.das @@ -0,0 +1,6 @@ +options gen2 +expect 30113, 30158, 30166, 30192 + +require UnitTest +[export] +def a{[iterator for((r) in "{({for(i in{0=>0});SomeEnum.})}"); 1] as a; } From 05cc2cd220e63ee5cd9a76180cca2944ec21795a Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 27 Aug 2026 20:46:25 +0300 Subject: [PATCH 14/14] infer: a comprehension skeleton for has no body to unwrap An ExprArrayComprehension keeps its body in subexpr; the exprFor it carries has body == null by design. When the comprehension's source never infers (an empty [] literal), the comprehension never lowers, and its skeleton for reaches the generator unwrap path inside a generator function, which read expr->body->getEvalFlags() unguarded. A for with no body has nothing to yield, so it never unwraps. Fuzzer find, issue #3858. --- src/ast/ast_infer_type.cpp | 2 ++ tests/language/failed_comprehension_source_in_generator.das | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 tests/language/failed_comprehension_source_in_generator.das diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 32c86e06ef..aca70f2f51 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -5346,6 +5346,8 @@ namespace das { return Visitor::visit(expr); } else if (expr->iteratorVariables.size() != expr->sources.size()) { return Visitor::visit(expr); + } else if (!expr->body) { + return Visitor::visit(expr); } // only topmost // which in case of generator is 2, due to diff --git a/tests/language/failed_comprehension_source_in_generator.das b/tests/language/failed_comprehension_source_in_generator.das new file mode 100644 index 0000000000..851d22badc --- /dev/null +++ b/tests/language/failed_comprehension_source_in_generator.das @@ -0,0 +1,4 @@ +options gen2 +expect 30166, 30183, 30192, 30341 + +[export] def a{[iterator for(v in f({for (v in []); (!0)}) => {for (i in (0)); 0; where 0}); {0}]; }