diff --git a/doc/source/reference/language/classes.rst b/doc/source/reference/language/classes.rst
index 8b1775ddf..e17381b30 100644
--- a/doc/source/reference/language/classes.rst
+++ b/doc/source/reference/language/classes.rst
@@ -146,6 +146,31 @@ Inside a derived class, ``super()`` calls the parent class constructor:
Both forms are rewritten by the compiler into explicit calls to the parent class function:
``super()`` becomes ``Base`Base(self)`` and ``super.process(x)`` becomes ``Base`process(self, x)``.
+If the immediate parent does not define a matching constructor or method, ``super`` walks
+up the inheritance chain to the nearest ancestor that does:
+
+.. code-block:: das
+
+ class Base {
+ def process(x : int) { /* ... */ }
+ }
+
+ class Mid : Base { // empty intermediate
+ }
+
+ class Leaf : Mid {
+ def Leaf {
+ super() // resolves to Base`Base(self) — Mid is skipped
+ }
+ def override process(x : int) {
+ super.process(x) // resolves to Base`process(self, x) — Mid is skipped
+ }
+ }
+
+Walk-up matches by argument types, so overloaded ``super(args)`` calls pick the closest
+ancestor whose constructor or method accepts those arguments. If no ancestor matches, the
+call is rejected at compile time.
+
Inside a derived class's finalizer (``operator delete``), ``delete super.self`` runs the
parent's finalizer on the current object:
@@ -158,13 +183,19 @@ parent's finalizer on the current object:
}
}
-The compiler rewrites ``delete super.self`` into ``delete cast(self)``. It is only
-valid inside an ``operator delete`` (or equivalently ``def finalize``) of a class that has
-a base class; other uses are rejected at compile time. The same form also works for struct
-finalizers declared as free functions — see :ref:`Structs `.
+The compiler rewrites ``delete super.self`` into ``delete cast(self)``, where ``T``
+is the closest ancestor whose ``finalize`` lookup resolves to a user-defined finalizer.
+For class hierarchies that means walking past intermediate classes that do not define
+their own ``def operator delete``. For struct hierarchies, ``finalize`` resolution honors
+inheritance substitution (a derived struct can be passed where its base is expected), so
+``T`` may be the immediate parent even when only an ancestor defines ``operator delete``.
+``delete super.self`` is only valid inside an ``operator delete`` (or equivalently
+``def finalize``) of a class that has a base with a finalizer; other uses are rejected
+at compile time. The same form also works for struct finalizers declared as free
+functions — see :ref:`Structs `.
Base-class finalization is explicit, not automatic: a derived finalizer that omits
-``delete super.self`` will not run the base finalizer.
+``delete super.self`` will not run any ancestor finalizer.
The option ``always_call_super`` can be enabled to require ``super()`` in every constructor
(see :ref:`Options `).
diff --git a/doc/source/reference/language/structs.rst b/doc/source/reference/language/structs.rst
index 6298ebe00..ae5e40387 100644
--- a/doc/source/reference/language/structs.rst
+++ b/doc/source/reference/language/structs.rst
@@ -238,9 +238,14 @@ whose first argument is ``self``. Inside such a finalizer for a derived struct,
// additional cleanup
}
-The compiler rewrites ``delete super.self`` into ``delete cast(self)``. See
-:ref:`Classes ` for the full description of the ``super`` sugar — it applies
-identically to structs with inheritance.
+The compiler rewrites ``delete super.self`` into ``delete cast(self)``, where ``T`` is
+the closest ancestor struct whose ``finalize`` lookup resolves to a user-defined finalizer.
+Because struct ``finalize`` resolution honors inheritance substitution (a derived struct
+can be passed where its base is expected), ``T`` is typically the immediate parent — even
+when only a more distant ancestor defines ``operator delete``. See :ref:`Classes `
+for the full description of the ``super`` sugar; the behavior is the same except that
+classes lack this substitution and instead skip past empty intermediate classes that
+don't define their own ``operator delete``.
.. _structs_alignment:
diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp
index 5d32f5cc6..5cc07ee2f 100644
--- a/src/ast/ast_infer_type.cpp
+++ b/src/ast/ast_infer_type.cpp
@@ -1400,15 +1400,46 @@ namespace das {
auto eVar = static_cast(eField->value);
if (eVar->name == "super") {
if (auto baseClass = func->classParent->parent) {
- reportAstChanged();
- auto callName = "_::" + baseClass->name + "`" + eField->name;
- auto newCall = new ExprCall(expr->at, callName);
- newCall->atEnclosure = expr->atEnclosure;
- newCall->arguments.push_back(new ExprVar(expr->at, "self"));
+ // We're in argumentsFailedToInfer because `super` itself doesn't resolve.
+ // The actual call args (i >= 2) may also still be uninferred — either
+ // null type, or alias/expr placeholder waiting on a later pass. Defer
+ // in either case (mirrors the allOtherInferred check below at line 1480
+ // and the ExprLooksLikeCall contract that flags both as "not ready").
for (size_t i = 2; i != expr->arguments.size(); ++i) {
- newCall->arguments.push_back(expr->arguments[i]);
+ if (!expr->arguments[i]->type || expr->arguments[i]->type->isAliasOrExpr()) {
+ return Visitor::visit(expr);
+ }
+ }
+ vector argTypes;
+ auto selfType = new TypeDecl(Type::tStructure);
+ selfType->structType = func->classParent;
+ argTypes.push_back(selfType);
+ for (size_t i = 2; i != expr->arguments.size(); ++i) {
+ argTypes.push_back(expr->arguments[i]->type);
+ }
+ while (baseClass) {
+ auto callName = "_::" + baseClass->name + "`" + eField->name;
+ auto fnCandidates = findMatchingFunctions(callName, argTypes, false);
+ if (fnCandidates.size() == 1) {
+ reportAstChanged();
+ auto newCall = new ExprCall(expr->at, callName);
+ newCall->atEnclosure = expr->atEnclosure;
+ newCall->arguments.push_back(new ExprVar(expr->at, "self"));
+ for (size_t i = 2; i != expr->arguments.size(); ++i) {
+ newCall->arguments.push_back(expr->arguments[i]);
+ }
+ return newCall;
+ } else if ( fnCandidates.size() > 1 ) {
+ error("too many candidates for super call " + callName,
+ verbose ? program->describeCandidates(fnCandidates) : "", "",
+ expr->at, CompilationError::function_not_found);
+ return Visitor::visit(expr);
+ }
+ baseClass = baseClass->parent;
}
- return newCall;
+ error("call to super in " + func->name + " is not allowed, no matching super method " + eField->name, "", "",
+ expr->at, CompilationError::function_not_found);
+ return Visitor::visit(expr);
} else {
error("call to super in " + func->name + " is not allowed, no base class for " + func->classParent->name, "", "",
expr->at, CompilationError::function_not_found);
@@ -2510,16 +2541,41 @@ namespace das {
"", "", expr->at, CompilationError::bad_delete);
return Visitor::visit(expr);
}
- reportAstChanged();
- auto selfVar = new ExprVar(expr->at, "self");
- auto castT = new TypeDecl(baseStruct);
- auto castExpr = new ExprCast(expr->at, selfVar, castT);
- auto newDel = new ExprDelete(expr->at, castExpr);
- newDel->alwaysSafe = true;
- newDel->native = expr->native;
- if (expr->sizeexpr)
- newDel->sizeexpr = expr->sizeexpr->clone();
- return newDel;
+ // Walk up the inheritance chain to the nearest ancestor with a user-defined
+ // finalizer (mirrors the super() / super.method() walk-ups). For structs, finalize
+ // substitution via base type matches at the immediate parent even when only an
+ // ancestor defines `def operator delete`. For classes there's no such substitution,
+ // so we must skip empty intermediates and cast to the actual finalizer's class.
+ while (baseStruct) {
+ auto baseType = new TypeDecl(Type::tStructure);
+ baseType->structType = baseStruct;
+ vector argTypes;
+ argTypes.push_back(baseType);
+ auto fnList = findMatchingFunctions("finalize", argTypes, false);
+ // Skip auto-generated finalizers (generateStructureFinalizer / makeClassFinalize):
+ // they don't chain to ancestors. Only stop at a user-defined finalizer (or one
+ // that an ancestor's user finalizer matches via inheritance substitution).
+ bool hasUserFinalize = false;
+ for (auto &f : fnList) {
+ if (!f->generated) { hasUserFinalize = true; break; }
+ }
+ if (hasUserFinalize) {
+ reportAstChanged();
+ auto selfVar = new ExprVar(expr->at, "self");
+ auto castT = new TypeDecl(baseStruct);
+ auto castExpr = new ExprCast(expr->at, selfVar, castT);
+ auto newDel = new ExprDelete(expr->at, castExpr);
+ newDel->alwaysSafe = true;
+ newDel->native = expr->native;
+ if (expr->sizeexpr)
+ newDel->sizeexpr = expr->sizeexpr->clone();
+ return newDel;
+ }
+ baseStruct = baseStruct->parent;
+ }
+ error("delete super.self in " + func->name + ": no ancestor of " + selfStruct->name + " defines a finalizer",
+ "", "", expr->at, CompilationError::bad_delete);
+ return Visitor::visit(expr);
}
}
}
@@ -5323,16 +5379,7 @@ namespace das {
"use let _ = " + call->name + "(...)", "",
call->at, CompilationError::result_discarded);
}
- if (func && func->isClassMethod && func->classParent && call->name == "super") {
- if (auto baseClass = func->classParent->parent) {
- call->name = baseClass->name + "`" + baseClass->name;
- call->arguments.insert(call->arguments.begin(), new ExprVar(call->at, "self"));
- reportAstChanged();
- } else {
- error("call to super in " + func->name + " is not allowed, no base class for " + func->classParent->name, "", "",
- call->at, CompilationError::function_not_found);
- }
- }
+ // super() constructor rewrite is done in visit(ExprCall*), once argument types are inferred
}
void InferTypes::preVisitCallArg(ExprCall *call, Expression *arg, bool last) {
Visitor::preVisitCallArg(call, arg, last);
@@ -5361,6 +5408,45 @@ namespace das {
func->notInferred();
return Visitor::visit(expr);
}
+ // super(args) constructor rewrite — runs in post-visit so argument types are inferred.
+ // Walks up the inheritance chain looking for a parent class constructor that matches the
+ // argument types; this lets `super(...)` call the closest ancestor's constructor when an
+ // intermediate class doesn't define its own (mirrors super.method() / delete super.self).
+ if (func && func->isClassMethod && func->classParent && expr->name == "super") {
+ if (auto baseClass = func->classParent->parent) {
+ vector argumentTypes;
+ auto selfType = new TypeDecl(Type::tStructure);
+ selfType->structType = func->classParent;
+ argumentTypes.reserve(1 + expr->arguments.size());
+ argumentTypes.push_back(selfType);
+ for (auto &arg : expr->arguments) {
+ argumentTypes.push_back(arg->type);
+ }
+ while (baseClass) {
+ auto candidateName = baseClass->name + "`" + baseClass->name;
+ auto matching = findMatchingFunctions(candidateName, argumentTypes, false);
+ if (matching.size() == 1) {
+ expr->name = candidateName;
+ expr->arguments.insert(expr->arguments.begin(), new ExprVar(expr->at, "self"));
+ reportAstChanged();
+ return Visitor::visit(expr);
+ } else if (matching.size() > 1) {
+ error("too many candidates for super constructor " + candidateName,
+ verbose ? program->describeCandidates(matching) : "", "",
+ expr->at, CompilationError::function_not_found);
+ return Visitor::visit(expr);
+ }
+ baseClass = baseClass->parent;
+ }
+ error("call to super in " + func->name + ": no matching super constructor for " + func->classParent->name, "", "",
+ expr->at, CompilationError::function_not_found);
+ return Visitor::visit(expr);
+ } else {
+ error("call to super in " + func->name + " is not allowed, no base class for " + func->classParent->name, "", "",
+ expr->at, CompilationError::function_not_found);
+ return Visitor::visit(expr);
+ }
+ }
if (forceInscopePod) {
bool resolvedBefore = expr->genericFunction && expr->func;
expr->func = inferFunctionCall(expr, InferCallError::functionOrGeneric, expr->genericFunction ? expr->func : nullptr);
diff --git a/tests/language/super.das b/tests/language/super.das
index 7e349939b..8cb9e209b 100644
--- a/tests/language/super.das
+++ b/tests/language/super.das
@@ -59,6 +59,32 @@ class Derived2 : Derived {
}
}
+// Skip-level inheritance: SkipMid is empty (inherits Base's constructor and methods).
+// SkipLeaf calls super() / super.method() — both must walk past SkipMid up to Base.
+class SkipMid : Base {
+}
+
+class SkipLeaf : SkipMid {
+ z : int = 0
+ def SkipLeaf {
+ super()
+ z = 100
+ }
+ def SkipLeaf(val : int) {
+ super(val)
+ z = val + 100
+ }
+ def override get_value() : int {
+ return x + z
+ }
+ def get_super_value() : int {
+ return super.get_value()
+ }
+ def override add(a, b : int) : int {
+ return super.add(a, b) + z
+ }
+}
+
[test]
def test_super(t : T?) {
t |> run("super() calls parent default constructor") @(t : T?) {
@@ -130,4 +156,34 @@ def test_super(t : T?) {
delete d
}
}
+ t |> run("super() walks past empty intermediate class") @(t : T?) {
+ unsafe {
+ // SkipMid has no constructor of its own; super() in SkipLeaf must reach Base.
+ var s = SkipLeaf()
+ t |> equal(s.x, 10) // Base default ctor ran
+ t |> equal(s.z, 100)
+ }
+ }
+ t |> run("super(args) walks past empty intermediate class") @(t : T?) {
+ unsafe {
+ var s = SkipLeaf(7)
+ t |> equal(s.x, 7)
+ t |> equal(s.z, 107)
+ }
+ }
+ t |> run("super.method() walks past empty intermediate class") @(t : T?) {
+ unsafe {
+ var s = SkipLeaf()
+ // SkipLeaf.get_super_value calls super.get_value; SkipMid has no own override,
+ // so the call must resolve to Base.get_value (returns x).
+ t |> equal(s->get_super_value(), 10)
+ }
+ }
+ t |> run("super.method(args) walks past empty intermediate class") @(t : T?) {
+ unsafe {
+ var s = SkipLeaf()
+ // SkipLeaf.add calls super.add(a,b) + z; SkipMid has no add, walks to Base: a+b
+ t |> equal(s->add(3, 4), 107)
+ }
+ }
}
diff --git a/tests/language/super_finalize.das b/tests/language/super_finalize.das
index 09c192ae5..e42dd24d9 100644
--- a/tests/language/super_finalize.das
+++ b/tests/language/super_finalize.das
@@ -62,6 +62,105 @@ def operator delete(var self : StructDerived) {
struct_del_derived ++
}
+// Skip-level inheritance: empty SkipMid in the middle of the chain.
+// `delete super.self` in SkipLeaf must walk past SkipMid and call SkipBase's finalizer.
+var skip_class_base = 0
+var skip_class_leaf = 0
+
+class SkipBase {
+ def operator delete {
+ skip_class_base ++
+ }
+}
+
+class SkipMid : SkipBase {
+}
+
+class SkipLeaf : SkipMid {
+ def operator delete {
+ delete super.self
+ skip_class_leaf ++
+ }
+}
+
+// 4-level skip with two empty intermediates.
+var deep_class_base = 0
+var deep_class_leaf = 0
+
+class DeepBase {
+ def operator delete {
+ deep_class_base ++
+ }
+}
+
+class DeepMid1 : DeepBase {
+}
+
+class DeepMid2 : DeepMid1 {
+}
+
+class DeepLeaf : DeepMid2 {
+ def operator delete {
+ delete super.self
+ deep_class_leaf ++
+ }
+}
+
+// Mid-chain with finalizer: only DeepLeaf and one ancestor have finalizers,
+// the immediate parent does not. Ensures walk-up still chains transitively.
+var mid_class_base = 0
+var mid_class_b = 0
+var mid_class_leaf = 0
+
+class MidBase {
+ def operator delete {
+ mid_class_base ++
+ }
+}
+
+class MidB : MidBase {
+ def operator delete {
+ delete super.self
+ mid_class_b ++
+ }
+}
+
+class MidEmpty : MidB {
+}
+
+class MidLeaf : MidEmpty {
+ def operator delete {
+ delete super.self
+ mid_class_leaf ++
+ }
+}
+
+// Struct skip-level: SkipStructMid has no finalizer (struct case relies on
+// substitution at the immediate parent — we still verify it works).
+var skip_struct_base = 0
+var skip_struct_leaf = 0
+
+struct SkipStructBase {
+ a : int
+}
+
+def operator delete(var self : SkipStructBase) {
+ skip_struct_base ++
+}
+
+struct SkipStructMid : SkipStructBase {
+ m : int
+}
+
+struct SkipStructLeaf : SkipStructMid {
+ z : int
+}
+
+def operator delete(var self : SkipStructLeaf) {
+ delete super.self
+ skip_struct_leaf ++
+}
+
[test]
def test_delete_super_self(t : T?) {
t |> run("class derived operator delete chains to base via super.self") @(t : T?) {
@@ -96,4 +195,47 @@ def test_delete_super_self(t : T?) {
t |> equal(struct_del_base, 1)
t |> equal(struct_del_derived, 1)
}
+ t |> run("delete super.self walks past empty intermediate class") @(t : T?) {
+ skip_class_base = 0
+ skip_class_leaf = 0
+ var s = new SkipLeaf()
+ unsafe {
+ delete s
+ }
+ t |> equal(skip_class_base, 1)
+ t |> equal(skip_class_leaf, 1)
+ }
+ t |> run("delete super.self walks past two empty intermediate classes") @(t : T?) {
+ deep_class_base = 0
+ deep_class_leaf = 0
+ var d = new DeepLeaf()
+ unsafe {
+ delete d
+ }
+ t |> equal(deep_class_base, 1)
+ t |> equal(deep_class_leaf, 1)
+ }
+ t |> run("delete super.self walks past empty mid-chain to nearest finalizer") @(t : T?) {
+ mid_class_base = 0
+ mid_class_b = 0
+ mid_class_leaf = 0
+ var m = new MidLeaf()
+ unsafe {
+ delete m
+ }
+ // MidLeaf -> (skip MidEmpty) -> MidB -> MidBase
+ t |> equal(mid_class_base, 1)
+ t |> equal(mid_class_b, 1)
+ t |> equal(mid_class_leaf, 1)
+ }
+ t |> run("struct delete super.self walks past empty intermediate struct") @(t : T?) {
+ skip_struct_base = 0
+ skip_struct_leaf = 0
+ var s = new SkipStructLeaf()
+ unsafe {
+ delete s
+ }
+ t |> equal(skip_struct_base, 1)
+ t |> equal(skip_struct_leaf, 1)
+ }
}