From f865542c5535e2e6dbb78d7ba1f764d81468d6c3 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 10 Sep 2026 01:01:32 +0300 Subject: [PATCH 1/4] ast: a non-positive fixed-array dimension is not an element count getCountOf64 multiplied the running product by uint64_t(fixedDim) for every node of a fixed-array chain. dimAuto is -1 and dimConst is -2, so an unresolved or already-rejected dimension wrapped the product, and the size that fell out read as enormous rather than unknown: pointer arithmetic on class reached getSizeOf with 18446744073709551568, which is 2^64-48. Such a dimension now contributes no count at all, which is what a dimension of 0 - the other spelling the parser rejects - already did, and what the getSizeOf64(bool&) overload has always answered for the same input. The two disagreed before this: the flagged one said 0, the flagless one 2^64-48. --- src/ast/ast_typedecl.cpp | 2 ++ tests/language/failed_pointer_math_unsized_pointee.das | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 tests/language/failed_pointer_math_unsized_pointee.das diff --git a/src/ast/ast_typedecl.cpp b/src/ast/ast_typedecl.cpp index b66d83d4e1..dce905eb21 100644 --- a/src/ast/ast_typedecl.cpp +++ b/src/ast/ast_typedecl.cpp @@ -3476,6 +3476,8 @@ namespace das uint64_t TypeDecl::getCountOf64() const { uint64_t size = 1; for ( const TypeDecl * t = this; t && t->baseType==Type::tFixedArray; t = t->firstType ) { + // dimAuto (-1) and dimConst (-2) are valid unresolved dims, not counts; uint64_t wraps them + if ( t->fixedDim<=0 ) return 0; size *= uint64_t(t->fixedDim); } return size; diff --git a/tests/language/failed_pointer_math_unsized_pointee.das b/tests/language/failed_pointer_math_unsized_pointee.das new file mode 100644 index 0000000000..6f2d650730 --- /dev/null +++ b/tests/language/failed_pointer_math_unsized_pointee.das @@ -0,0 +1,9 @@ +// Pointer arithmetic whose pointee carries a dimension the parser already rejected. The +// dimension is negative, so the element count wrapped and the size read as 2^64-48. +options gen2 +expect 30109, 30239 + +[export] +def pointer_math_on_unsized_pointee { + var x = new class() - 0 +} From f8b74190dd3628bdffa74c20441fdca19095fe2d Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 10 Sep 2026 01:02:15 +0300 Subject: [PATCH 2/4] lint: an oversized function result is diagnosed, not asserted on The size gate ran on locals, arguments, block arguments, structures, aliases, ascend and new, but never on a function result. A result too big to size therefore reached Program::simulate, where makeFunctionDebugInfo walks it and getSizeOf/getStride assert - and in a build with DAS_NO_ASSERTIONS it walked on with a truncated size instead. preVisit(Function*) now sizes the result the way preVisitArgument sizes an argument, skipping a result that is still auto or an alias, so a generic that has not been instanced is left alone. The fixture carries both shapes: the reported one, whose make-value on the path is diagnosed too, and one where nothing but the result carries the size. --- include/daScript/ast/compilation_errors.h | 2 +- src/ast/ast_lint.cpp | 4 ++++ .../language/failed_result_type_by_ref_too_big.das | 13 +++++++++++++ tests/language/failed_result_type_too_big.das | 13 +++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/language/failed_result_type_by_ref_too_big.das create mode 100644 tests/language/failed_result_type_too_big.das diff --git a/include/daScript/ast/compilation_errors.h b/include/daScript/ast/compilation_errors.h index f1da01ed65..3bcb899697 100644 --- a/include/daScript/ast/compilation_errors.h +++ b/include/daScript/ast/compilation_errors.h @@ -353,7 +353,7 @@ namespace das , exceeds_new_argument = 30509 // 1 site(s) , exceeds_structure = 30510 // 1 site(s) , exceeds_tuple_index = 30511 // 1 site(s) - , exceeds_type = 30512 // 2 site(s) + , exceeds_type = 30512 // 3 site(s) , exceeds_type_alias = 30513 // 1 site(s) , exceeds_typeinfo_sizeof = 30514 // 1 site(s) , exceeds_constant_range = 30515 // 1 site(s) diff --git a/src/ast/ast_lint.cpp b/src/ast/ast_lint.cpp index 6adf6364d3..b66e786856 100644 --- a/src/ast/ast_lint.cpp +++ b/src/ast/ast_lint.cpp @@ -1109,6 +1109,10 @@ namespace das { program->error("[init] is disabled in the options or CodeOfPolicies", "", "", fn->at, CompilationError::cant_function); } + if ( !fn->result->isAutoOrAlias() && fn->result->getSizeOf64()>0x7fffffff ) { + program->error("function result of '" + fn->name + "' is too big", "", "", + fn->at, CompilationError::exceeds_type); + } } virtual FunctionPtr visit ( Function * fn ) override { // Derived class ctor: every CFG path must call super(...) exactly once, diff --git a/tests/language/failed_result_type_by_ref_too_big.das b/tests/language/failed_result_type_by_ref_too_big.das new file mode 100644 index 0000000000..7a5305ad3b --- /dev/null +++ b/tests/language/failed_result_type_by_ref_too_big.das @@ -0,0 +1,13 @@ +// The oversized result on its own, with no other error to stop compilation first: this one +// reaches Program::simulate, where the debug-info walk sizes the result and asserts. +options gen2 +expect 30512 + +[export] +def result_too_big_by_ref(p : int[2000000000]?) : int[2000000000]& { + unsafe { return *p } +} + +[export] +def main { +} diff --git a/tests/language/failed_result_type_too_big.das b/tests/language/failed_result_type_too_big.das new file mode 100644 index 0000000000..13630bda24 --- /dev/null +++ b/tests/language/failed_result_type_too_big.das @@ -0,0 +1,13 @@ +// An oversized function result. The size gate covered locals, arguments, structures and +// aliases but not the result, so the type reached the debug-info walk and asserted there. +options gen2 +expect 30512 + +// the reported shape - the make-value on the path is diagnosed as well. the crash itself is +// in failed_result_type_by_ref_too_big.das, where no earlier error stops the walk +[export] +def result_too_big => <- tuple(uninitialized) + +[export] +def main { +} From 298eecf6a78a2f0f84983b476b3b6b7d70d22a4f Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 10 Sep 2026 01:02:28 +0300 Subject: [PATCH 3/4] infer: a piped call stops padding in a default whose parameter type never resolves A piped call that lands past the first parameter pads the defaults of the parameters it shifts across. isFunctionCompatiblePipedAt admits the candidate on those defaults existing - it type-checks only the arguments the caller actually wrote - and tryPipedCallPadding then splices them into the call. So instancing is handed an argument lookup never matched, the alias loop gives up without progress, and inferGenericType answers null for it. The next statement writes through that null under DAS_ASSERTF, which DAS_NO_ASSERTIONS compiles out, so a stock Release daslang segfaults rather than reporting anything. The padded defaults are now matched once they are resolved, but only where the parameter type is still auto or an alias - a type that never resolves can neither be matched nor instanced, so the candidate is dropped and the call reports a lookup miss. A parameter with a concrete type deliberately stays: its mismatch is a real user error, and the instanced function names it far better (error[30161], both types spelled out) than a lookup miss would. Matching those too was tried and rejected for exactly that reason. With the null's only producer closed off, DAS_ASSERTF(resT, ...) keeps its meaning and stays: if some other path ever yields that null, it is a compiler defect and must be seen as one, not reported to the user as a fault in their code. The other assert on that path goes, because what it claims is false. The no-progress exit is reachable whenever a concrete parameter's default does not match, and falling through is what lets the instanced function produce error[30161]; asserting there fires on a legitimate diagnostic. failed_generic_unknown_arg_type pins the unresolved shape - without the lookup match it aborts on the resT assert - and failed_generic_default_type_mismatch pins the concrete one, which aborts if the deleted assert is put back. --- src/ast/ast_infer_type_function.cpp | 10 +++++++++- .../failed_generic_default_type_mismatch.das | 13 +++++++++++++ tests/language/failed_generic_unknown_arg_type.das | 12 ++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/language/failed_generic_default_type_mismatch.das create mode 100644 tests/language/failed_generic_unknown_arg_type.das diff --git a/src/ast/ast_infer_type_function.cpp b/src/ast/ast_infer_type_function.cpp index f7a786fb53..d16170b847 100644 --- a/src/ast/ast_infer_type_function.cpp +++ b/src/ast/ast_infer_type_function.cpp @@ -557,6 +557,15 @@ namespace das { } padded.push_back(newArg); } + for (int ai = p; ai != k; ++ai) { + const auto & argType = winner->arguments[ai]->type; + if (!argType->isAutoOrAlias()) continue; + AliasMap aliases; + OptionsMap options; + if (!isMatchingArgument(winner, argType, padded[ai - p]->type, useGenerics, true, &aliases, &options)) { + return false; + } + } for (int ai = k - 1; ai >= p; --ai) { expr->arguments.insert(expr->arguments.begin() + p, padded[ai - p]); types.insert(types.begin() + p, padded[ai - p]->type); @@ -1908,7 +1917,6 @@ namespace das { if (!anyFailed) break; if (totalAliases == aliases.size()) { - DAS_ASSERTF(0, "we should not be here. function matched arguments!"); break; } } diff --git a/tests/language/failed_generic_default_type_mismatch.das b/tests/language/failed_generic_default_type_mismatch.das new file mode 100644 index 0000000000..6121c0a327 --- /dev/null +++ b/tests/language/failed_generic_default_type_mismatch.das @@ -0,0 +1,13 @@ +// A piped call pads a default in for the parameter it shifts across, and lookup admits the +// candidate on that default's existence alone. When the parameter type is concrete the +// candidate is still instanced, so the argument-matching loop gives up without progress and +// the instanced function names the real mistake - which is why that exit falls through. +options gen2 +expect 30161, 30341 + +def generic_with_mismatched_default(r : string <- 1, v) => 0 + +[export] +def main { + generic_with_mismatched_default(){} +} diff --git a/tests/language/failed_generic_unknown_arg_type.das b/tests/language/failed_generic_unknown_arg_type.das new file mode 100644 index 0000000000..13ea43315a --- /dev/null +++ b/tests/language/failed_generic_unknown_arg_type.das @@ -0,0 +1,12 @@ +// A generic whose first argument is an unresolved type expression with a default value. The +// call matches at lookup; the alias map then stops growing with that argument still +// unmatched, and the instancing after the loop used to read types the loop never produced. +options gen2 +expect 30341 + +def generic_with_unknown_arg_type(r : l() <- 1, v) => 0 + +[export] +def call_generic_with_unknown_arg_type { + generic_with_unknown_arg_type(){} +} From 92e56e61027e0134c7a04e810e48118ce5570c01 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 10 Sep 2026 01:02:40 +0300 Subject: [PATCH 4/4] aot: cvt_pass is one identity on vec4f instead of an overload set Every vec2/vec3/vec4 flavor converts from vec4f, so the six cvt_pass overloads could not be resolved once their argument was another cvt_'s result - which is what the emitter writes for a conversion chain: int3 v = cvt_pass(cvt_uint3(das_alias::from(getSamplePoint3()))); A single conversion compiled and a constant chain folded away before codegen, so only a runtime value through two conversions reached it. cvt_pass collapses to one function taking and returning vec4f, which is the identity a pass-through conversion should be. Retyping the cvt_ results to their concrete vector type would also resolve the call and is the wrong trade: vec4f is the SIMD register while vec2/vec3/vec4 are structs of scalars, so a concrete return spills the lanes through v_extract_* and the next conversion reloads them - one cvttps2dq becomes a dozen instructions of shuffles and stack traffic. All twelve cvt_ keep their vec4f result. vec_constructors runs the chains on globals the folder cannot reach, two links and three; the AOT lane compiles the file, so the ambiguity is a build failure there rather than a wrong value. --- include/daScript/simulate/REVIEW.md | 7 +++++++ include/daScript/simulate/aot.h | 7 +------ tests/language/vec_constructors.das | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/include/daScript/simulate/REVIEW.md b/include/daScript/simulate/REVIEW.md index fb59b65ea4..6c056c670c 100644 --- a/include/daScript/simulate/REVIEW.md +++ b/include/daScript/simulate/REVIEW.md @@ -24,6 +24,13 @@ C++ half never opens the daslib checklist on its own. - **A diff that changes `KeyHash` (`runtime_table.h`) or `WrapsBuiltinValue` (`cast.h`) states in its own PR description which key types change hash value.** +- **A `cvt_*` inline in `aot.h` takes and returns `vec4f`, and none of them is overloaded.** + `vec4f` is the SIMD register; the `vec2`/`vec3`/`vec4` types are structs of scalars, so a + concrete return spills the lanes through `v_extract_*` and the next conversion reloads them. + Overloading is what forced that once: every flavor converts from `vec4f`, so an overload set + fed a `vec4f` result is ambiguous - the emitter writes `cvt_pass(cvt_uint3(..))` for + `x |> uint3 |> int3`. One name per conversion, `vec4f` throughout, keeps both. + - **A diff that makes the hot path cost more per evaluated expression in the build the repo ships is a defect.** The hot path is a `SimNode::eval*` method, any helper such a method calls on every evaluation, the dispatchers `Context::callOrFastcall` / diff --git a/include/daScript/simulate/aot.h b/include/daScript/simulate/aot.h index 23b5497835..2c53cea7d3 100644 --- a/include/daScript/simulate/aot.h +++ b/include/daScript/simulate/aot.h @@ -4517,12 +4517,7 @@ namespace das { __forceinline vec4f cvt_uint3 ( float3 f ) { return v_cast_vec4f(v_cvt_vec4i(f)); } __forceinline vec4f cvt_uint4 ( float4 f ) { return v_cast_vec4f(v_cvt_vec4i(f)); } - __forceinline vec4f cvt_pass ( int2 i ) { return i; } - __forceinline vec4f cvt_pass ( int3 i ) { return i; } - __forceinline vec4f cvt_pass ( int4 i ) { return i; } - __forceinline vec4f cvt_pass ( uint2 i ) { return i; } - __forceinline vec4f cvt_pass ( uint3 i ) { return i; } - __forceinline vec4f cvt_pass ( uint4 i ) { return i; } + __forceinline vec4f cvt_pass ( vec4f i ) { return i; } __forceinline int32_t class_rtti_size ( void * ptr ) { auto pti = (TypeInfo **)ptr; diff --git a/tests/language/vec_constructors.das b/tests/language/vec_constructors.das index 6068c9e8ef..85ed0f09e8 100644 --- a/tests/language/vec_constructors.das +++ b/tests/language/vec_constructors.das @@ -6,6 +6,12 @@ require dastest/testing_boost public // and type conversions between vector types. // VEC_SEP is a built-in constant (default ",") used in vector printing. +// globals, so the constant folder leaves the conversion chains below to codegen +var g_f2 = float2(1, 2) +var g_f3 = float3(1, 2, 3) +var g_f4 = float4(1, 2, 3, 4) +var g_i3 = int3(1, 2, 3) + def test_vec2(tt : T?; a : auto(vec2) -const -&; isRange = false) { let f2_0 = vec2() let f2_x = vec2(1.0, 2.0) @@ -117,4 +123,19 @@ def test_vec_constructors(t : T?) { t |> equal(uint3(float3(1, 2, 3)), uint3(1u, 2u, 3u)) t |> equal(uint4(float4(1, 2, 3, 4)), uint4(1u, 2u, 3u, 4u)) } + t |> run("chained type conversions") @(t : T?) { + // AOT writes these as cvt_pass(cvt_uint3(..)), which resolves only while each cvt_ + // answers in its vector type - a vec4f argument matches every cvt_pass overload + t |> equal(g_f2 |> uint2 |> int2, int2(1, 2)) + t |> equal(g_f3 |> uint3 |> int3, int3(1, 2, 3)) + t |> equal(g_f4 |> uint4 |> int4, int4(1, 2, 3, 4)) + t |> equal(g_f2 |> int2 |> uint2, uint2(1u, 2u)) + t |> equal(g_f3 |> int3 |> uint3, uint3(1u, 2u, 3u)) + t |> equal(g_f4 |> int4 |> uint4, uint4(1u, 2u, 3u, 4u)) + t |> equal(g_i3 |> float3 |> int3, int3(1, 2, 3)) + t |> equal(g_i3 |> uint3 |> float3, float3(1, 2, 3)) + // three links: cvt_pass(cvt_pass(cvt_uint3(..))), so a cvt_pass feeds a cvt_pass + t |> equal(g_f3 |> uint3 |> int3 |> uint3, uint3(1u, 2u, 3u)) + t |> equal(g_f2 |> int2 |> uint2 |> int2, int2(1, 2)) + } }