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) + } }