From 3aef48d83fac612f6ac6c03beb168bcf7c6d9689 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 17 Sep 2026 00:23:22 +0300 Subject: [PATCH 1/3] infer: a class whose parent was rejected generates its own __rtti `struct A; class B : A; class C : B;` crashed in makeClassRtti. The `class B : A` error is correct, and it returns from ast_structureDeclaration before B gets its generated __rtti, so the accepted `class C : B` wrote through a findField("__rtti") answering null. makeClassFinalize had the identical defect one call later and would have been the next crash. Both now let the parent lookup pick the branch: a parent that never got the field means the class declares its own. --- src/ast/ast_generate.cpp | 8 ++++---- tests/language/failed_class_parent_rejected.das | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 tests/language/failed_class_parent_rejected.das diff --git a/src/ast/ast_generate.cpp b/src/ast/ast_generate.cpp index 05e350cb37..fa7c227ae6 100644 --- a/src/ast/ast_generate.cpp +++ b/src/ast/ast_generate.cpp @@ -2296,8 +2296,8 @@ namespace das { auto rttiType = new TypeDecl(baseClass); rttiType->at = baseClass->at; ExpressionPtr finit = new ExprTypeInfo(baseClass->at, "rtti_classinfo", rttiType); - if ( baseClass->parent ) { - auto fd = (Structure::FieldDeclaration *) baseClass->findField("__rtti"); + auto fd = baseClass->parent ? (Structure::FieldDeclaration *) baseClass->findField("__rtti") : nullptr; + if ( fd ) { fd->init = finit; fd->parentType = fd->type->isAuto(); fd->generated = true; @@ -2323,8 +2323,8 @@ namespace das { // template classes stay on open "_::": their stamped instances infer in the consumer // module while the stamped finalizer may live elsewhere, so a strict pin can't see it ExpressionPtr finit = new ExprAddr(baseClass->at, (baseClass->isTemplate ? "_::" : "__::") + fname); - if ( baseClass->parent ) { - auto fd = (Structure::FieldDeclaration *) baseClass->findField("__finalize"); + auto fd = baseClass->parent ? (Structure::FieldDeclaration *) baseClass->findField("__finalize") : nullptr; + if ( fd ) { auto castT = new TypeDecl(Type::autoinfer, baseClass->at); fd->init = new ExprCast(baseClass->at, finit, castT); fd->parentType = fd->type->isAuto(); diff --git a/tests/language/failed_class_parent_rejected.das b/tests/language/failed_class_parent_rejected.das new file mode 100644 index 0000000000..16328231aa --- /dev/null +++ b/tests/language/failed_class_parent_rejected.das @@ -0,0 +1,13 @@ +// A class deriving from a class whose own declaration is rejected. The `class B : A` error +// returns before B gets its generated __rtti and __finalize fields, so `class C : B` - which is +// accepted - reaches field generation with a parent that carries neither. +options gen2 +expect 30134 + +struct A; +class B : A; +class C : B; + +[export] +def main { +} From 9159b0a61793dbca82823a5c3bd26ec6323bc587 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 17 Sep 2026 01:15:38 +0300 Subject: [PATCH 2/3] infer: a safe_* typeinfo fold tells infer to run again `1 &&= typeinfo safe_has_field(typeinfo is_ref(1))` crashed in buildAccessFlags on a null ExprOp2::func. The missing guard is real, but it was covering an inference bug. Root cause: four safe_* typeinfo traits returned a replacement node without reportAstChanged - safe_has_field, struct_safe_has_annotation, and both arms of struct_safe_has_annotation_argument. Every sibling branch signals; these four did not. The replacement arrives untyped, the enclosing operator takes its "failed to infer" early-out, and with no signal that pass is the last one, so func stays null with no diagnostic - which is also why the reported input never reached operator resolution and never got its 30341. It rejected valid code too: `b &&= typeinfo safe_has_field(typeinfo is_ref(1))` did not compile. Guard: preVisit of ExprOp1, ExprOp2 and ExprOp3 in the access-flag pass now return early on a null func, the way ExprCall right below them already did. buildAccessFlags runs before the pass that owns the 50613 diagnostic, so without it any unsignalled rewrite crashes before it can be reported. A fifth site, is_iterable, looked like the same defect but already signals three lines above its return - left alone. The fixture holds all four traits and the reported input, and must stay free of anything that keeps signalling - a print's string builder is enough, and it grants the enclosing operator a repair pass that hides the defect. Reverting the four calls turns the expected 30341 into 50613 five times over. --- src/ast/ast_infer_type.cpp | 4 +++ src/ast/ast_unused.cpp | 13 +++++---- .../failed_op2_typeinfo_safe_traits.das | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 tests/language/failed_op2_typeinfo_safe_traits.das diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index 1ca09e8dbb..b7a9455f27 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -2716,6 +2716,7 @@ namespace das { return new ExprConstBool(expr->at, ft != nullptr); } else { if (expr->trait == "safe_has_field") { + reportAstChanged(); return new ExprConstBool(expr->at, false); } else { error("typeinfo(has_field<" + expr->subtrait + "> ...) is only defined for structures and handled types, " + describeType(expr->typeexpr), "", "", @@ -2730,6 +2731,7 @@ namespace das { return new ExprConstBool(expr->at, it != ann.end()); } else { if (expr->trait == "struct_safe_has_annotation") { + reportAstChanged(); return new ExprConstBool(expr->at, false); } else { error("typeinfo(struct_has_annotation<" + expr->subtrait + "> ...) is only defined for structures, " + describeType(expr->typeexpr), "", "", @@ -2742,6 +2744,7 @@ namespace das { auto it = find_if(ann.begin(), ann.end(), [&](const AnnotationDeclarationPtr &pa) { return pa->annotation->name == expr->subtrait; }); if (it == ann.end()) { if (expr->trait == "struct_safe_has_annotation_argument") { + reportAstChanged(); return new ExprConstBool(expr->at, false); } else { error("typeinfo(struct_has_annotation_argument<" + expr->subtrait + ";" + expr->extratrait + "> ...) annotation not found ", "", "", @@ -2755,6 +2758,7 @@ namespace das { } } else { if (expr->trait == "struct_safe_has_annotation_argument") { + reportAstChanged(); return new ExprConstBool(expr->at, false); } else { error("typeinfo(struct_has_annotation_argument<" + expr->subtrait + "> ...) is only defined for structures, " + describeType(expr->typeexpr), "", "", diff --git a/src/ast/ast_unused.cpp b/src/ast/ast_unused.cpp index 0915625701..e3da3ecfa0 100644 --- a/src/ast/ast_unused.cpp +++ b/src/ast/ast_unused.cpp @@ -690,6 +690,7 @@ namespace das { // Op1 virtual void preVisit ( ExprOp1 * expr ) override { Visitor::preVisit(expr); + if ( !expr->func ) return; auto sef = getSideEffects(expr->func); markPassMutableOperand(expr->func, 0, expr->subexpr); if ( sef & uint32_t(SideEffects::modifyArgument) ) { @@ -699,6 +700,7 @@ namespace das { // Op2 virtual void preVisit ( ExprOp2 * expr ) override { Visitor::preVisit(expr); + if ( !expr->func ) return; auto sef = getSideEffects(expr->func); markPassMutableOperand(expr->func, 0, expr->left); markPassMutableOperand(expr->func, 1, expr->right); @@ -716,12 +718,11 @@ namespace das { // Op3 virtual void preVisit ( ExprOp3 * expr ) override { Visitor::preVisit(expr); - auto sef = expr->func ? getSideEffects(expr->func) : 0; - if ( expr->func ) { - markPassMutableOperand(expr->func, 0, expr->subexpr); - markPassMutableOperand(expr->func, 1, expr->left); - markPassMutableOperand(expr->func, 2, expr->right); - } + if ( !expr->func ) return; + auto sef = getSideEffects(expr->func); + markPassMutableOperand(expr->func, 0, expr->subexpr); + markPassMutableOperand(expr->func, 1, expr->left); + markPassMutableOperand(expr->func, 2, expr->right); if ( sef & uint32_t(SideEffects::modifyArgument) ) { auto condT = expr->subexpr->type; if ( condT->isRefOrPointer() && !condT->constant ) { diff --git a/tests/language/failed_op2_typeinfo_safe_traits.das b/tests/language/failed_op2_typeinfo_safe_traits.das new file mode 100644 index 0000000000..5df736b235 --- /dev/null +++ b/tests/language/failed_op2_typeinfo_safe_traits.das @@ -0,0 +1,28 @@ +// A safe_* typeinfo trait that answers false replaces its node, so it tells infer to run again. +// Each arm takes its value from a nested typeinfo, or from an index that folds, so the trait +// resolves one pass late - in a pass where nothing else changes, which is the pass that catches a +// missing signal. Keep this file free of anything that keeps signalling (a print's string builder +// will do it): that grants the enclosing operator a repair pass and the arms stop proving anything. +// Without the signal every arm reports 50613 instead, the reported input included. +options gen2 +expect 30341 + +[comment(i = 1)] +struct TsrAnnotated { + x : int +} + +var g_tsr_annotated : TsrAnnotated[8] + +[export] +def main { + var a = true + a &&= typeinfo safe_has_field < no_such_field > (typeinfo is_ref(1)) + var b = true + b &&= typeinfo struct_safe_has_annotation < no_such_annotation > (typeinfo is_ref(1)) + var c = true + c &&= typeinfo struct_safe_has_annotation_argument < no_such_annotation; no_such_argument > (typeinfo is_ref(1)) + var d = true + d &&= typeinfo struct_safe_has_annotation_argument < no_such_annotation; no_such_argument > (g_tsr_annotated[typeinfo sizeof(type)]) + 1 &&= typeinfo safe_has_field < no_such_type > (typeinfo is_ref(1)) +} From 73c420fe3dd97b6e25e8c3c2609302e941e754c0 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Thu, 17 Sep 2026 00:52:53 +0300 Subject: [PATCH 3/3] infer: a global's dimension does not adopt the type it dimensions `let zz : int[zz][zz]` ate 26.8 GB and died to the OOM killer. The dimension expression lives inside the type it dimensions, so giving the dimension's ExprVar the variable's own type gives it a type containing that same expression - with two dimensions each infer pass squares the previous pass's copy, 4^50. InferTypes::visit(ExprVar*) no longer adopts a global's type while its dimensions are unresolved (isExprType). This also covers two shapes the report does not name, both of which OOM on master and neither of which the globalVar self-reference guard the issue suggests would catch: let aa : int[bb][bb] // mutual let bb : int[aa][aa] let aa : int[length(bb)][length(bb)] // through a call let bb : int[length(aa)][length(aa)] All of them now report error[30109] in under half a second. A forward reference that resolves in a later pass (`let b : int[MM]` before `let MM = 3`, and a function reading b) keeps working. --- src/ast/ast_infer_type.cpp | 3 +++ .../language/failed_global_dim_self_reference.das | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/language/failed_global_dim_self_reference.das diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index b7a9455f27..d78f4eaacc 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -4604,6 +4604,9 @@ namespace das { return Visitor::visit(expr); } expr->variable = var; + if (var->type->isExprType()) { + return Visitor::visit(expr); + } TypeDecl::clone(expr->type, var->type); expr->type->ref = true; return Visitor::visit(expr); diff --git a/tests/language/failed_global_dim_self_reference.das b/tests/language/failed_global_dim_self_reference.das new file mode 100644 index 0000000000..00350f5d3d --- /dev/null +++ b/tests/language/failed_global_dim_self_reference.das @@ -0,0 +1,14 @@ +// A global whose fixed-array dimension names the variable it declares. The dimension expression +// sits inside the type it dimensions, so a reference that takes the variable's own type takes a +// type holding that reference; with two dimensions each infer pass squares the last one's copy. +// The array clone in main is what keeps the passes coming. +options gen2 +expect 30109:2 + +let zz : int[zz][zz] + +[export] +def main { + var cb : array + cb := cb +}