Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions doc/source/reference/language/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -158,13 +183,19 @@ parent's finalizer on the current object:
}
}

The compiler rewrites ``delete super.self`` into ``delete cast<Base>(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 <structs>`.
The compiler rewrites ``delete super.self`` into ``delete cast<T>(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 <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 <options>`).
Expand Down
11 changes: 8 additions & 3 deletions doc/source/reference/language/structs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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<Foo>(self)``. See
:ref:`Classes <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<T>(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 <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:

Expand Down
140 changes: 113 additions & 27 deletions src/ast/ast_infer_type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1400,15 +1400,46 @@ namespace das {
auto eVar = static_cast<ExprVar*>(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);
}
}
Comment thread
borisbat marked this conversation as resolved.
vector<TypeDeclPtr> 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);
Comment thread
borisbat marked this conversation as resolved.
if (fnCandidates.size() == 1) {
Comment thread
borisbat marked this conversation as resolved.
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);
Expand Down Expand Up @@ -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<TypeDeclPtr> 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) {
Comment thread
borisbat marked this conversation as resolved.
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);
}
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<TypeDeclPtr> 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);
Comment thread
borisbat marked this conversation as resolved.
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);
Expand Down
56 changes: 56 additions & 0 deletions tests/language/super.das
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand Down Expand Up @@ -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)
}
}
}
Loading
Loading