From 4591c8f4489b13c479f20380ff81d6e80221085e Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 11:16:22 +0300 Subject: [PATCH 1/9] alias: neither the aliasing nor the coverage pass walks a template structure deriveAliases visits every structure's field types, a `struct template`'s included. A template's field type expressions are never inferred, so an unresolved call inside one -- the dim expression of `r[@{0=(0,C())}]>` in the fuzzer's repro -- reaches SourceCollector::preVisit(ExprCall) with a null `func` and dereferences it. The same repro walks into daslib/coverage.das through visit_module: the ExprMakeBlock in that dim expression reaches CoverageMacro.visitExprBlock outside of any function, where `func` is still null and not_inferred() throws "expecting function". Only the extended-checks Coverage step engages that pass, so the test would be green everywhere else and red on one lane. Give AliasMarker and CoverageMacro a canVisitStructure that returns !isTemplate, mirroring AliasMarker's own canVisitFunction isTemplate gate and the ones RunFolding (ast_const_folding.cpp) and ast_verify already carry for the same reason. The test lands in tests/language/type_inference_fuzz.das, which every later fuzzer shape on this branch joins: one file for the shapes that must keep compiling, and a failed_ twin for the ones that must be rejected. The split is the harness, not taste - dastest matches an `expect` list as an exact multiset against one compilation, so a file that compiles cannot also be a file that fails. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- daslib/coverage.das | 6 ++++++ src/ast/ast_derive_alias.cpp | 3 +++ tests/README.md | 1 + tests/language/type_inference_fuzz.das | 17 +++++++++++++++++ 4 files changed, 27 insertions(+) create mode 100644 tests/language/type_inference_fuzz.das diff --git a/daslib/coverage.das b/daslib/coverage.das index 252de8b5e5..6cc3334fd2 100644 --- a/daslib/coverage.das +++ b/daslib/coverage.das @@ -293,6 +293,12 @@ class CoverageMacro : AstVisitor { } } + def override canVisitStructure(st : Structure?) : bool { + //! Skips template structures - their type expressions are never inferred, so a block + //! inside one reaches the instrumentation outside of any function, with a null `func`. + return !st.flags.isTemplate + } + def override canVisitFunction(fun : Function?) : bool { if (fun.flags.init || fun.flags.builtIn) return false for (ann in fun.annotations) { diff --git a/src/ast/ast_derive_alias.cpp b/src/ast/ast_derive_alias.cpp index b3f032b440..06f1681d38 100644 --- a/src/ast/ast_derive_alias.cpp +++ b/src/ast/ast_derive_alias.cpp @@ -321,6 +321,9 @@ namespace das { var->aliasesResolved = isPermanent; return true; } + virtual bool canVisitStructure ( Structure * st ) override { + return !st->isTemplate; + } virtual bool canVisitFunction ( Function * fun ) override { if ( fun->stub ) return false; if ( fun->isTemplate ) return false; diff --git a/tests/README.md b/tests/README.md index b794fd6f06..77c77017a1 100644 --- a/tests/README.md +++ b/tests/README.md @@ -677,6 +677,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | invalid_structure_field_type_ref.das | Ref type in struct field | **expect** `30104` | | invalid_structure_field_type_void.das | Void type in struct field | **expect** `30104` | | invalid_table_type_mix.das | Invalid table key/value types | **expect** `30106:2` `30108` | +| type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a block in a template structure's dim expression | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | | invalid_types.das | Oversized types and arguments | **expect** `30101:2` `30108:4` `30109:2` | | failed_jit_abi.das | JIT ABI correctness - `test_abi_mad` for float2/3/4, function pointers | | diff --git a/tests/language/type_inference_fuzz.das b/tests/language/type_inference_fuzz.das new file mode 100644 index 0000000000..7f9a57241c --- /dev/null +++ b/tests/language/type_inference_fuzz.das @@ -0,0 +1,17 @@ +// Shapes the fuzzer reaches that no hand-written test does. Every block below compiles +// clean and must keep compiling: the file carries no assertions, because a regression is a +// compiler crash or a spurious error, not a wrong value. +options gen2 + +// A template structure's typedef holds a block in the dim expression of its own alias. The +// type expressions of a template are never inferred, so that block reaches every post-infer +// visitor with no type and outside of any function - a visitor keying off its enclosing +// function reads a null one. +struct template r { + def v => 0 + typedef r = r[ @ {0 = (0, C())}]> +} + +[export] +def main { +} From db66049c10af70b9b09255e69e6df096474c4f67 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 11:16:34 +0300 Subject: [PATCH 2/9] parser: only an inherited field takes its type from the parent A sealed or override field declaration sets parentType from `type->isAuto()`, which is right when the field it replaces was copied down from the parent and wrong when the field was declared earlier in the same structure body. Infer then reads parentType, looks the name up in the parent, gets nullptr back and dereferences it. parentType means "this field's type comes from the parent's field", so gate it on the flag that says so. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- src/parser/parser_impl.cpp | 2 +- tests/README.md | 1 + tests/language/failed_type_inference_fuzz.das | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/language/failed_type_inference_fuzz.das diff --git a/src/parser/parser_impl.cpp b/src/parser/parser_impl.cpp index 3525403ffb..91dc287566 100644 --- a/src/parser/parser_impl.cpp +++ b/src/parser/parser_impl.cpp @@ -455,7 +455,7 @@ namespace das { oldFd->init = pDecl->pInit; pDecl->pInit = nullptr; } } - oldFd->parentType = oldFd->type->isAuto(); + oldFd->parentType = oldFd->inherited && oldFd->type->isAuto(); oldFd->privateField = pDecl->isPrivate; oldFd->sealed = pDecl->sealed; oldFd->implemented = true; diff --git a/tests/README.md b/tests/README.md index 77c77017a1..583d41575b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -677,6 +677,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | invalid_structure_field_type_ref.das | Ref type in struct field | **expect** `30104` | | invalid_structure_field_type_void.das | Void type in struct field | **expect** `30104` | | invalid_table_type_mix.das | Invalid table key/value types | **expect** `30106:2` `30108` | +| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - sealed redeclare of an inherited field | **expect** `30320` `30805` `30821` `30826` `30832` | | type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a block in a template structure's dim expression | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | | invalid_types.das | Oversized types and arguments | **expect** `30101:2` `30108:4` `30109:2` | diff --git a/tests/language/failed_type_inference_fuzz.das b/tests/language/failed_type_inference_fuzz.das new file mode 100644 index 0000000000..55cb099afc --- /dev/null +++ b/tests/language/failed_type_inference_fuzz.das @@ -0,0 +1,18 @@ +// Shapes the fuzzer reaches that must be rejected, one diagnostic each. The expect list is +// an exact multiset: an error that stops being reported fails this file just as loudly as a +// new one, which is what keeps a crash from being traded for a silent accept. +options gen2 +expect 30320, 30805, 30821, 30826, 30832 + +// An inherited field redeclared as sealed, in a class deriving from a template. +class template T { +} + +class e : T { + f : r + sealed f : int +} + +[export] +def main { +} From 44918001f8dffd06cfac16ef1b6a269c31068bbc Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 11:16:52 +0300 Subject: [PATCH 3/9] infer: debug of a void expression is a compile error No tier can carry it. Const folding elides a call to an empty side-effect-free function by returning nullptr, which only ExprBlock knows how to drop; as the argument of `debug` the null survives and ExprLooksLikeCall::visit walks into it one pass later -- the fuzzer's SIGSEGV. AOT emits `cast::from(...)`, which has no definition, and the JIT gives up with "failed to get IR". `debug(v())` is the only spelling that puts a void call in a subexpression position, so reject it where a void function argument is already rejected, with the same code and the same wording. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- include/daScript/ast/compilation_errors.h | 2 +- src/ast/ast_infer_type.cpp | 5 +++++ tests/README.md | 2 +- tests/language/failed_type_inference_fuzz.das | 7 ++++++- tests/language/invalid_table_type_mix.das | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/include/daScript/ast/compilation_errors.h b/include/daScript/ast/compilation_errors.h index 199c0b4993..f1da01ed65 100644 --- a/include/daScript/ast/compilation_errors.h +++ b/include/daScript/ast/compilation_errors.h @@ -106,7 +106,7 @@ namespace das , invalid_argument = 30104 // 6 site(s) , invalid_argument_global = 30105 // 4 site(s) , invalid_argument_name = 30106 // 3 site(s) - , invalid_argument_type = 30107 // 3 site(s) + , invalid_argument_type = 30107 // 4 site(s) , invalid_array = 30108 // 6 site(s) , invalid_array_dimension = 30109 // 5 site(s) , invalid_array_dimension_type = 30110 // 1 site(s) diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 671221cab8..c9a9200b02 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -1604,6 +1604,11 @@ namespace das { error("debug comment must be string constant", "", "", expr->at, CompilationError::invalid_debug_comment_type); } + if (expr->arguments[0]->type->isVoid()) { + error("void type is not allowed as argument", "", "", + expr->at, CompilationError::invalid_argument_type); + return Visitor::visit(expr); + } TypeDecl::clone(expr->type, expr->arguments[0]->type); return Visitor::visit(expr); } diff --git a/tests/README.md b/tests/README.md index 583d41575b..70b5f5206c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -677,7 +677,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | invalid_structure_field_type_ref.das | Ref type in struct field | **expect** `30104` | | invalid_structure_field_type_void.das | Void type in struct field | **expect** `30104` | | invalid_table_type_mix.das | Invalid table key/value types | **expect** `30106:2` `30108` | -| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - sealed redeclare of an inherited field | **expect** `30320` `30805` `30821` `30826` `30832` | +| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - sealed redeclare of an inherited field, void argument | **expect** `30107` `30320` `30805` `30821` `30826` `30832` | | type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a block in a template structure's dim expression | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | | invalid_types.das | Oversized types and arguments | **expect** `30101:2` `30108:4` `30109:2` | diff --git a/tests/language/failed_type_inference_fuzz.das b/tests/language/failed_type_inference_fuzz.das index 55cb099afc..f543652895 100644 --- a/tests/language/failed_type_inference_fuzz.das +++ b/tests/language/failed_type_inference_fuzz.das @@ -2,7 +2,11 @@ // an exact multiset: an error that stops being reported fails this file just as loudly as a // new one, which is what keeps a crash from being traded for a silent accept. options gen2 -expect 30320, 30805, 30821, 30826, 30832 +expect 30107, 30320, 30805, 30821, 30826, 30832 + +// A void expression handed to debug(). +def v { +} // An inherited field redeclared as sealed, in a class deriving from a template. class template T { @@ -15,4 +19,5 @@ class e : T { [export] def main { + debug(v()) } diff --git a/tests/language/invalid_table_type_mix.das b/tests/language/invalid_table_type_mix.das index 52e4eafc4e..ed79abf77f 100644 --- a/tests/language/invalid_table_type_mix.das +++ b/tests/language/invalid_table_type_mix.das @@ -2,7 +2,7 @@ // verifies compiler rejects ref keys, non-hashable keys (array), // and void local variables options gen2 -expect 30202, 30248, 30254 // invalid_table_type +expect 30107, 30202, 30248, 30254 // invalid_table_type def test { let a : table // can't have table key ref From 0541a5749d3e7de0a292d8d25c4d7f1e5698f074 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 11:17:04 +0300 Subject: [PATCH 4/9] infer: typeinfo is_argument outside a function answers false `is_argument` asks the enclosing function whether a name is one of its arguments. Reached through a type declaration -- a structure field's array dimension, say -- there is no enclosing function, so `func` is null and findArgument dereferences it. The sibling branch one line down already answers false when the subexpression is not a var; answer false here too. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- src/ast/ast_infer_type.cpp | 2 +- tests/README.md | 2 +- tests/language/failed_type_inference_fuzz.das | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index c9a9200b02..c6791bd6f2 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -2467,7 +2467,7 @@ namespace das { if (expr->subexpr->rtti_isVar()) { auto evar = static_cast(expr->subexpr); reportAstChanged(); - return new ExprConstBool(expr->at, func->findArgument(evar->name) != nullptr); + return new ExprConstBool(expr->at, func && func->findArgument(evar->name) != nullptr); } else { reportAstChanged(); return new ExprConstBool(expr->at, false); diff --git a/tests/README.md b/tests/README.md index 70b5f5206c..fc5b52b5e6 100644 --- a/tests/README.md +++ b/tests/README.md @@ -677,7 +677,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | invalid_structure_field_type_ref.das | Ref type in struct field | **expect** `30104` | | invalid_structure_field_type_void.das | Void type in struct field | **expect** `30104` | | invalid_table_type_mix.das | Invalid table key/value types | **expect** `30106:2` `30108` | -| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - sealed redeclare of an inherited field, void argument | **expect** `30107` `30320` `30805` `30821` `30826` `30832` | +| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - sealed redeclare of an inherited field, void argument, `typeinfo is_argument` outside a function | **expect** `30107` `30110` `30320` `30805` `30821` `30826` `30832` | | type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a block in a template structure's dim expression | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | | invalid_types.das | Oversized types and arguments | **expect** `30101:2` `30108:4` `30109:2` | diff --git a/tests/language/failed_type_inference_fuzz.das b/tests/language/failed_type_inference_fuzz.das index f543652895..da187ae3cc 100644 --- a/tests/language/failed_type_inference_fuzz.das +++ b/tests/language/failed_type_inference_fuzz.das @@ -2,7 +2,7 @@ // an exact multiset: an error that stops being reported fails this file just as loudly as a // new one, which is what keeps a crash from being traded for a silent accept. options gen2 -expect 30107, 30320, 30805, 30821, 30826, 30832 +expect 30107, 30110, 30320, 30805, 30821, 30826, 30832 // A void expression handed to debug(). def v { @@ -17,6 +17,13 @@ class e : T { sealed f : int } +// typeinfo is_argument outside of a function, in the dim expression of a field. +var g_probe = 1 + +struct l { + f : int[typeinfo is_argument(g_probe)] +} + [export] def main { debug(v()) From f81dc359186420fa65aef1df5fd841d0f98b1d08 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 11:17:16 +0300 Subject: [PATCH 5/9] aot: a computed goto only sees labels inside its own block A `goto ` inside a captured block emitted a switch over every label in every enclosing scope, the outer function's included, so the generated lambda jumped to a label that is not in its scope and the C++ did not compile: "use of undeclared label 'label_6'". Failing at runtime there is the intended behavior - the interpreter throws "jump to label N failed" - so AOT has to reach the same runtime failure instead of failing to compile. Stop the label scan at the closure, the rule InferTypes::findLabel already follows. The switch then carries only the labels the lambda can reach, and its default arm throws. The scan also ran outermost-first: the two `reverse()` calls around it resolve to linq's pure `reverse`, which returns a copy, so `scopes` was never reversed. Walk it by index instead, so "stop at the closure" means the innermost one. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- daslib/aot_cpp.das | 8 +++++--- tests/README.md | 2 +- tests/language/type_inference_fuzz.das | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/daslib/aot_cpp.das b/daslib/aot_cpp.das index 8e3e21fbd3..8e862c8309 100644 --- a/daslib/aot_cpp.das +++ b/daslib/aot_cpp.das @@ -1891,16 +1891,18 @@ class public CppAot : AstVisitor { def override visitExprGoto(var that : ExprGoto?) : ExpressionPtr { if (that.subexpr != null) { write(*ss, ") \{\n"); - scopes |> reverse(); - for (blk in scopes) { + var si = length(scopes) - 1; + while (si >= 0) { + let blk = scopes[si]; for (ex in blk.list) { if (ex is ExprLabel) { let lab = ex as ExprLabel; write(*ss, "{tabs()}case {lab.labelName}: goto label_{lab.labelName};\n"); } } + break if (blk.blockFlags.isClosure); + si--; } - scopes |> reverse(); write(*ss, "{tabs()}default: __context__->throw_error(\"invalid label\");\n"); write(*ss, "{tabs()}\}"); } diff --git a/tests/README.md b/tests/README.md index fc5b52b5e6..e69405b898 100644 --- a/tests/README.md +++ b/tests/README.md @@ -678,7 +678,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | invalid_structure_field_type_void.das | Void type in struct field | **expect** `30104` | | invalid_table_type_mix.das | Invalid table key/value types | **expect** `30106:2` `30108` | | failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - sealed redeclare of an inherited field, void argument, `typeinfo is_argument` outside a function | **expect** `30107` `30110` `30320` `30805` `30821` `30826` `30832` | -| type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a block in a template structure's dim expression | | +| type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a block in a template structure's dim expression, a computed goto inside a captured block | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | | invalid_types.das | Oversized types and arguments | **expect** `30101:2` `30108:4` `30109:2` | | failed_jit_abi.das | JIT ABI correctness - `test_abi_mad` for float2/3/4, function pointers | | diff --git a/tests/language/type_inference_fuzz.das b/tests/language/type_inference_fuzz.das index 7f9a57241c..d94a4da45b 100644 --- a/tests/language/type_inference_fuzz.das +++ b/tests/language/type_inference_fuzz.das @@ -12,6 +12,21 @@ struct template r { typedef r = r[ @ {0 = (0, C())}]> } +// A computed goto inside a captured block. Its label lives in the enclosing function, which +// the block's own label table does not carry. +def label_target : int { + return 6 +} + +[export] +def computed_goto_in_a_captured_block { + invoke($ { + goto label_target() + }) + label 6: + pass +} + [export] def main { } From 113a3a0321e95547ef40a42622227582a280c784 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 11:17:57 +0300 Subject: [PATCH 6/9] infer: a template structure's array dimension rejects a block A `struct template` is declined at the top of InferTypes::canVisitStructure, so nothing in it is ever checked: `typedef r = r[@{0=(0,C())}]>` compiles clean and leaves an ExprMakeBlock with no type in the module, where every post-infer visitor meets it outside of any function. A block is a constant at no instantiation, so inference now reports it where it is written - error 30109, the same diagnostic a non-template structure already gets. The walk reaches the dim expression through the alias's typeMacroExpr payload, mirroring isLoop's descent in ast_infer_type_helper.cpp, and is depth-capped because a template's aliases are never resolved. Deliberately narrow. An ast-verify sweep of tests/, daslib/, modules/ and utils/ with the verifier's own !isTemplate gate opened reports un-inferred expressions inside `daslib/option.das`, `daslib/delegate.das` and four typemacro fixtures - all legal, all shipped. A template legitimately carries expressions inference never touched, so "no type inside a template" cannot become an error, and the per-visitor !isTemplate gates stay load-bearing rather than becoming redundant. The shape moves to the failed_ file, and type_inference_fuzz.das takes the legal one that reaches the same crash - a lambda in a template's field initializer - so the coverage gate of the first commit keeps its negative control. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- src/ast/ast_infer_type.cpp | 38 ++++++++++++++++++- tests/README.md | 4 +- tests/language/failed_type_inference_fuzz.das | 9 ++++- tests/language/type_inference_fuzz.das | 13 +++---- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index c6791bd6f2..49e1006c91 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -303,9 +303,45 @@ namespace das { lastEnuValue = nullptr; return Visitor::visit(enu); } + static constexpr int templateDimDepthCapAgainstAliasLoop = 16; + + static void collectTemplateBlockDims ( const TypeDeclPtr & type, int depth, vector & blockDims ) { + if ( !type || depth<=0 ) return; + if ( type->baseType==Type::tFixedArray && type->fixedDim==TypeDecl::dimConst && type->fixedDimExpr ) { + if ( type->fixedDimExpr->rtti_isBlock() || type->fixedDimExpr->rtti_isMakeBlock() ) { + blockDims.push_back(type); + } + } + collectTemplateBlockDims(type->firstType, depth-1, blockDims); + collectTemplateBlockDims(type->secondType, depth-1, blockDims); + for ( auto & argType : type->argTypes ) { + collectTemplateBlockDims(argType, depth-1, blockDims); + } + for ( auto & macroExpr : type->typeMacroExpr ) { + if ( macroExpr && macroExpr->rtti_isTypeDecl() ) { + auto typeExpr = static_cast(macroExpr); + collectTemplateBlockDims(typeExpr->typeexpr, depth-1, blockDims); + } + } + } + bool InferTypes::canVisitStructure(Structure *st) { if ( fatalAliasLoop ) return false; - if ( st->isTemplate ) return false; // we don't do a thing with templates + if ( st->isTemplate ) { + vector blockDims; + st->aliases.foreach([&](const TypeDeclPtr & atype) -> bool { + collectTemplateBlockDims(atype, templateDimDepthCapAgainstAliasLoop, blockDims); + return true; + }); + for ( auto & fi : st->fields ) { + collectTemplateBlockDims(fi.type, templateDimDepthCapAgainstAliasLoop, blockDims); + } + for ( auto & blockDim : blockDims ) { + error("array dimension must be constant", "", "", + blockDim->at, CompilationError::invalid_array_dimension); + } + return false; // we don't do a thing with templates + } bool aliasLoop = false; st->aliases.foreach([&](const TypeDeclPtr & atype) -> bool { vector visited; diff --git a/tests/README.md b/tests/README.md index e69405b898..e9f1a5a86c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -677,8 +677,8 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | invalid_structure_field_type_ref.das | Ref type in struct field | **expect** `30104` | | invalid_structure_field_type_void.das | Void type in struct field | **expect** `30104` | | invalid_table_type_mix.das | Invalid table key/value types | **expect** `30106:2` `30108` | -| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - sealed redeclare of an inherited field, void argument, `typeinfo is_argument` outside a function | **expect** `30107` `30110` `30320` `30805` `30821` `30826` `30832` | -| type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a block in a template structure's dim expression, a computed goto inside a captured block | | +| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - a block in a template structure's dim expression, sealed redeclare of an inherited field, void argument, `typeinfo is_argument` outside a function | **expect** `30107` `30109` `30110` `30320` `30805` `30821` `30826` `30832` | +| type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a lambda in a template structure's field initializer, a computed goto inside a captured block | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | | invalid_types.das | Oversized types and arguments | **expect** `30101:2` `30108:4` `30109:2` | | failed_jit_abi.das | JIT ABI correctness - `test_abi_mad` for float2/3/4, function pointers | | diff --git a/tests/language/failed_type_inference_fuzz.das b/tests/language/failed_type_inference_fuzz.das index da187ae3cc..e62a91dfb4 100644 --- a/tests/language/failed_type_inference_fuzz.das +++ b/tests/language/failed_type_inference_fuzz.das @@ -2,7 +2,14 @@ // an exact multiset: an error that stops being reported fails this file just as loudly as a // new one, which is what keeps a crash from being traded for a silent accept. options gen2 -expect 30107, 30110, 30320, 30805, 30821, 30826, 30832 +expect 30107, 30109, 30110, 30320, 30805, 30821, 30826, 30832 + +// A block in the dim expression of a template structure's own alias. A template's type +// expressions are inferred at instantiation, but a block is a constant at no instantiation. +struct template r { + def v_r => 0 + typedef r = r[ @ {0 = (0, C())}]> +} // A void expression handed to debug(). def v { diff --git a/tests/language/type_inference_fuzz.das b/tests/language/type_inference_fuzz.das index d94a4da45b..62044870e2 100644 --- a/tests/language/type_inference_fuzz.das +++ b/tests/language/type_inference_fuzz.das @@ -3,13 +3,12 @@ // compiler crash or a spurious error, not a wrong value. options gen2 -// A template structure's typedef holds a block in the dim expression of its own alias. The -// type expressions of a template are never inferred, so that block reaches every post-infer -// visitor with no type and outside of any function - a visitor keying off its enclosing -// function reads a null one. -struct template r { - def v => 0 - typedef r = r[ @ {0 = (0, C())}]> +// A template structure's field initializer holds a lambda. The field initializers of a +// template are never inferred, so that block reaches every post-infer visitor with no type +// and outside of any function - a visitor keying off its enclosing function reads a null one. +struct template TL { + f : function<(x : int) : int> = @@(_x : int) : int => 1 + v : T } // A computed goto inside a captured block. Its label lives in the enclosing function, which From 2e62699cabce0700666420b48a6dd5163dd75beb Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 13:24:33 +0300 Subject: [PATCH 7/9] lint: default answers the size check every declaration of it answers `default` parses as an ExprMakeStruct with that makeType, and nothing on the make path sizes it: the same type spelled as a declaration is rejected (`error[30508]: local variable x is too big`), while the default form reaches AllocateStack::preVisit(ExprMakeStruct), where getSizeOf() trips `assertion failed: size <= 0x7fffffff` with 8000000000. A stock Release build compiles the assertion out and hangs instead, so this is not an assertions-only defect. lint already carries this check for a local, an argument, a field, a global, `new` and ascend, and it runs before allocateStack. Give ExprMakeStruct the same one, with the code new and ascend use. The test joins tests/language/invalid_types.das, which is where the too-big diagnostics live - and which already records why an infer-stage variant cannot be in it: an expect file matches one compilation, and inference errors stop the pipeline before lint ever runs. Its README row was stale; it now lists what the file expects. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- src/ast/ast_lint.cpp | 4 ++++ tests/README.md | 2 +- tests/language/invalid_types.das | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ast/ast_lint.cpp b/src/ast/ast_lint.cpp index e48e082d0c..56eee2cbff 100644 --- a/src/ast/ast_lint.cpp +++ b/src/ast/ast_lint.cpp @@ -1202,6 +1202,10 @@ namespace das { } virtual void preVisit ( ExprMakeStruct * mks ) override { Visitor::preVisit(mks); + if ( mks->makeType && mks->makeType->getSizeOf64()>0x7fffffff ) { + program->error("can't make a value of a type that is too big", "", "", + mks->at, CompilationError::exceeds_type); + } if ( mks->constructor && mks->constructor->arguments.size() ) { program->error("default arguments of constructors can't be used in make declarations", "its not yet implemented", "", mks->at, CompilationError::cant_argument_structure); diff --git a/tests/README.md b/tests/README.md index e9f1a5a86c..3d1fbe9695 100644 --- a/tests/README.md +++ b/tests/README.md @@ -680,7 +680,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - a block in a template structure's dim expression, sealed redeclare of an inherited field, void argument, `typeinfo is_argument` outside a function | **expect** `30107` `30109` `30110` `30320` `30805` `30821` `30826` `30832` | | type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a lambda in a template structure's field initializer, a computed goto inside a captured block | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | -| invalid_types.das | Oversized types and arguments | **expect** `30101:2` `30108:4` `30109:2` | +| invalid_types.das | Oversized types and arguments - declarations, `new`, ascend, `default` | **expect** `30500:3` `30508` `30510` `30512:3` `30513` | | failed_jit_abi.das | JIT ABI correctness - `test_abi_mad` for float2/3/4, function pointers | | | labels.das | Labels and goto - control flow, nested loops, labeled break | | | lambda_basic.das | Lambda capture, invoke, null check, addX returning lambda | | diff --git a/tests/language/invalid_types.das b/tests/language/invalid_types.das index 741a3b98b9..e690c01ec2 100644 --- a/tests/language/invalid_types.das +++ b/tests/language/invalid_types.das @@ -1,6 +1,6 @@ options gen2 options disable_auto_inline // this test pins lint diagnostics; splicing would reshape them -expect 30500:3, 30508, 30510, 30512:2, 30513 +expect 30500:3, 30508, 30510, 30512:3, 30513 require dastest/testing_boost public require daslib/lpipe @@ -30,6 +30,7 @@ def test_invalid_types(t : T?) { feint("a = {a}\n") var p = new Foo() // 30109: can't new to a type that is too big var pp = new Foo() // 30109: can't ascend type which is too big + debug(default) // 30512: can't make a value of a type that is too big } // this one happens during infer, and not lint. so we can't have it in the test From 6bbd95e8eec4a2daaf91c3c01375ad693d544d9e Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 13:35:27 +0300 Subject: [PATCH 8/9] infer: a for-source of an [expr] type is not a call to infer yet visitForSource synthesizes `each(source)` for a source that is none of the iterable shapes, and infers that call. It only checked that the source has a type, so a source still carrying an unresolved dim - an `[expr]` type - reached inferArguments, whose own assertion says it: "we are calling infer function call without checking for '[expr]'. do that from where we call up the stack." Every other call path already marks such an argument as failed to infer. Check it here too, so the source waits for the pass that resolves the dim. Release compiles the assertion out and reports the same diagnostics either way, so the shape is only observable in a build with assertions - the Debug lanes. The test is the reported repro, machine-reduced and dense; only its identifiers are renamed, so it does not resolve against the declarations already in the file. Verified both ways in a -DDAS_NO_ASSERTIONS=0 build: it dies on the assertion without this change, and passes with it. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- src/ast/ast_infer_type.cpp | 2 +- tests/README.md | 2 +- tests/language/failed_type_inference_fuzz.das | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 49e1006c91..89e11e868c 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -5349,7 +5349,7 @@ namespace das { } ExpressionPtr InferTypes::visitForSource(ExprFor *expr, Expression *that, bool last) { // now, for the one where we did not find anything - if (that->type) { + if (that->type && !that->type->isExprType()) { if (that->type->baseType != Type::tFixedArray && !that->type->isGoodIteratorType() && !that->type->isGoodArrayType() && diff --git a/tests/README.md b/tests/README.md index 3d1fbe9695..80b3bb6377 100644 --- a/tests/README.md +++ b/tests/README.md @@ -677,7 +677,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | invalid_structure_field_type_ref.das | Ref type in struct field | **expect** `30104` | | invalid_structure_field_type_void.das | Void type in struct field | **expect** `30104` | | invalid_table_type_mix.das | Invalid table key/value types | **expect** `30106:2` `30108` | -| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - a block in a template structure's dim expression, sealed redeclare of an inherited field, void argument, `typeinfo is_argument` outside a function | **expect** `30107` `30109` `30110` `30320` `30805` `30821` `30826` `30832` | +| failed_type_inference_fuzz.das | Fuzzer-reached shapes that must be rejected - a block in a template structure's dim expression, sealed redeclare of an inherited field, void argument, `typeinfo is_argument` outside a function, an `[expr]`-typed for-source | **expect** `30107` `30109:4` `30110` `30149` `30192` `30312` `30320:2` `30341:3` `30804` `30805` `30817` `30821:4` `30826:2` `30832:2` | | type_inference_fuzz.das | Fuzzer-reached shapes that must keep compiling - a lambda in a template structure's field initializer, a computed goto inside a captured block | | | invalid_type_ref_in_table_value.das | Ref type as table value | **expect** `30106` | | invalid_types.das | Oversized types and arguments - declarations, `new`, ascend, `default` | **expect** `30500:3` `30508` `30510` `30512:3` `30513` | diff --git a/tests/language/failed_type_inference_fuzz.das b/tests/language/failed_type_inference_fuzz.das index e62a91dfb4..828669fb61 100644 --- a/tests/language/failed_type_inference_fuzz.das +++ b/tests/language/failed_type_inference_fuzz.das @@ -2,7 +2,7 @@ // an exact multiset: an error that stops being reported fails this file just as loudly as a // new one, which is what keeps a crash from being traded for a silent accept. options gen2 -expect 30107, 30109, 30110, 30320, 30805, 30821, 30826, 30832 +expect 30107, 30109:4, 30110, 30149, 30192, 30312, 30320:2, 30341:3, 30804, 30805, 30817, 30821:4, 30826:2, 30832:2 // A block in the dim expression of a template structure's own alias. A template's type // expressions are inferred at instantiation, but a block is a constant at no instantiation. @@ -35,3 +35,10 @@ struct l { def main { debug(v()) } + +// A for-source whose type is still an [expr] type: synthesizing `each(source)` and inferring +// that call hands the unresolved dim to the argument walk every other call path guards. The +// shape is machine-reduced and dense on purpose - a readable spelling stops at a syntax error +// before reaching it - and only its identifiers are renamed, to keep it out of the way of the +// declarations above. +var {}enum v9{r9 = {for (v9 in $()->t9 < r9 > == & | lambda> [-3] => r9()); ""}}class i9 {l9 : v9()}var e9 : l9[][y9()] From 4500f413c2c94fa2c6118a5e6e3e75ff97e27963 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 3 Sep 2026 19:19:48 +0300 Subject: [PATCH 9/9] aot: a value type's table key compares as its workhorse, not by operator == das keys a table by the value type's workhorse - the type ManagedValueAnnotation derives from WrapType::type - and both tiers that can run such a table compare exactly that: the interpreter through makeTableKeyValueNode's anyArgument case, the JIT through the same Type-keyed runtime helper. AOT was the only tier that mapped the key onto the C++ type and so onto C++ overload resolution, where a bound type's own operator == - or, with none, whatever its implicit conversion happens to yield - decided what das considers the same key. That agreed only by accident. EntityId converts to int32_t, which is a bool comparison of the same 4 bytes its workhorse compares, so it looked fine; BigEntityId converts to vec4f, and the lane-wise result is not a bool at all: runtime_table.h:18:20: error: cannot initialize return object of type 'bool' with an rvalue of type 'int __attribute__((vector_size(16)))' Comparing the workhorse instead removes the second semantics rather than verifying it - the operator takes no part, so a binding cannot disagree with das about key identity by writing one. float2 and float3 also wrap to vec4f but are narrower than it and are workhorse types in their own right, which is what the size test in KeyComparesAsWorkhorse excludes; float4 and Time are captured and compare as they did. The JIT reached the same helper but could not get there: base_type_to_llvm_type had no anyArgument case and JIT_TABLE_FUNCTION's 33 key types were missing it, so a value-type key failed the whole function ("Failed to get IR"). Both now mirror the interpreter's own anyArgument case. LLVM_JIT_CODEGEN_VERSION bumped. test_value_table_key.das grows the vec4f-workhorse arm beside its int32 one and asserts lookups, so the agreement is pinned where it can be observed rather than at compile time: interp 2/2, -jit 2/2, test_aot_subset --use-aot 2/2. Sweeps: interp 13062 tests / 13053 passed, jit 12928 / 12921, 0 failed either. Refs #3892 Co-Authored-By: Claude Opus 5 (1M context) --- include/daScript/simulate/jit_abi.h | 1 + include/daScript/simulate/runtime_table.h | 21 +++++++++++++++++++++ modules/dasLLVM/daslib/llvm_jit_common.das | 2 +- modules/dasLLVM/daslib/llvm_jit_run.das | 2 +- tests/README.md | 2 +- tests/language/test_value_table_key.das | 22 +++++++++++++++++++++- 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/include/daScript/simulate/jit_abi.h b/include/daScript/simulate/jit_abi.h index 8406c5e2d4..2595ba68b4 100644 --- a/include/daScript/simulate/jit_abi.h +++ b/include/daScript/simulate/jit_abi.h @@ -159,6 +159,7 @@ namespace detail { case Type::tString: return detail::TableWrap), TAB_FUN>::get_builtin_address(); \ case Type::tDouble: return detail::TableWrap), TAB_FUN>::get_builtin_address(); \ case Type::tPointer: return detail::TableWrap), TAB_FUN>::get_builtin_address(); \ + case Type::anyArgument: return detail::TableWrap), TAB_FUN>::get_builtin_address(); \ default: context->throw_error_at(at, "unsupported key type %s", das_to_string(Type(baseType)).c_str() ); \ } \ return nullptr; diff --git a/include/daScript/simulate/runtime_table.h b/include/daScript/simulate/runtime_table.h index f494705b4b..31f2ac5f65 100644 --- a/include/daScript/simulate/runtime_table.h +++ b/include/daScript/simulate/runtime_table.h @@ -12,11 +12,32 @@ namespace das DAS_API extern const char * rts_null; + //! A das value type is keyed by its workhorse - the type ManagedValueAnnotation derives + //! from WrapType - so the interpreter and the JIT both compare that, never the C++ + //! operator ==. AOT compares the same thing, which is what this trait selects; a bound + //! type's own == takes no part in what das considers the same key. float2 and float3 wrap + //! to vec4f but are narrower than it and are workhorse types in their own right, which is + //! what the size test excludes. + template + struct KeyComparesAsWorkhorse { + enum { value = WrapType::value + && sizeof(KeyType)==sizeof(typename WrapType::type) + && !is_same::type>::value }; + }; + template struct KeyCompare { __forceinline bool operator () ( const KeyType & a, const KeyType & b ) { + return compare(a, b, integral_constant::value>()); + } + __forceinline bool compare ( const KeyType & a, const KeyType & b, false_type ) { return a == b; } + __forceinline bool compare ( const KeyType & a, const KeyType & b, true_type ) { + typedef typename WrapType::type workhorse; + return KeyCompare()( cast::to(cast::from(a)), + cast::to(cast::from(b)) ); + } }; template <> diff --git a/modules/dasLLVM/daslib/llvm_jit_common.das b/modules/dasLLVM/daslib/llvm_jit_common.das index dd392a1960..f3ffb2158e 100644 --- a/modules/dasLLVM/daslib/llvm_jit_common.das +++ b/modules/dasLLVM/daslib/llvm_jit_common.das @@ -1186,7 +1186,7 @@ def public base_type_to_llvm_type(t : Type) { // nolint:STYLE037 - exact-type d return g_prim_t.LLVMRange64Type() if (t == Type.tRange64 || t == Type.tURange64) return g_prim_t.LLVMFloat2Type() if (t == Type.tFloat2) return g_prim_t.LLVMFloat3Type() if (t == Type.tFloat3) - return g_prim_t.LLVMFloat4Type() if (t == Type.tFloat4) + return g_prim_t.LLVMFloat4Type() if (t == Type.tFloat4 || t == Type.anyArgument) return g_prim_t.get_type_string() if (t == Type.tString) return g_prim_t.LLVMVoidPtrType() if (t == Type.tPointer) // 16/8-bit lattice — packed-lane vectors mirror the das value layout diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index f97d51418d..eaee416740 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -36,7 +36,7 @@ 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 = 0x57ul // INT_MIN / -1 and % -1 guards on sdiv/srem (0x56: darwin in-memory arm emits no dtor list) +let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x58ul // value-type table keys reach the workhorse table helper (0x57: INT_MIN / -1 and % -1 guards on sdiv/srem) // Read by tests-cpp/small/test_jit_emitter_pin.cpp: FNV-1a64 of the emitter sources // (normalized to LF; file list in the test) diff --git a/tests/README.md b/tests/README.md index 80b3bb6377..7af05c97ea 100644 --- a/tests/README.md +++ b/tests/README.md @@ -772,7 +772,7 @@ JIT compilation and code-generation tests. None have `expect` directives. The sl | table.das | Table tombstone handling and iteration | | | table_get_key.das | `get_key(table, value)` - retrieve key by iterator value for int<->float, string<->int, const table, tombstones, empty, single entry | | | table_operations.das | Table find, insert, delete, key_exists, erase collision, lock panic, defaults, modify | | -| test_value_table_key.das | `table` - value-type table key ops, set operations | | +| test_value_table_key.das | Value-type table keys - `table` ops and set operations, plus a vec4f-workhorse key (`BigEntityId`) across all three tiers | | | testing_tools.das | Faker, fuzzer, testing_boost tools | | | to_array.das | `to_array` - from fixed_array, range, each(), static/dynamic arrays | | | to_table.das | `to_table` - from fixed_array of tuples | | diff --git a/tests/language/test_value_table_key.das b/tests/language/test_value_table_key.das index 9c77682cff..c1ab1dbefb 100644 --- a/tests/language/test_value_table_key.das +++ b/tests/language/test_value_table_key.das @@ -52,4 +52,24 @@ def test_value_table_key(t : T?) { t |> success(!(set |> key_exists(EntityId(4)))) } - +// A value type whose workhorse is vec4f rather than an integer: das compares the workhorse, so +// the C++ type needs no operator == of its own, and a key is the same key in all three tiers. +[test] +def test_wide_value_table_key(t : T?) { + let a = unsafe(reinterpret(int4(1, 2, 3, 4))) + let b = unsafe(reinterpret(int4(9, 8, 7, 6))) + var tab : table // nolint:STYLE031 — insert with value keys is under test + tab |> insert(a, "alpha") + tab |> insert(b, "beta") + t |> equal(length(tab), 2) + t |> success(tab |> key_exists(a)) + t |> success(tab |> key_exists(b)) + t |> success(!(tab |> key_exists(BigEntityId()))) + let va = tab ?[a] ?? "none" + t |> equal(va, "alpha") + let vb = tab ?[b] ?? "none" + t |> equal(vb, "beta") + tab |> erase(a) + t |> equal(length(tab), 1) + t |> success(!(tab |> key_exists(a))) +}