Skip to content

Commit d6bd2e5

Browse files
borisbatclaude
andcommitted
class_super: CFG-aware super(...) lint + reject super-skip-intermediate
Examples/wip.das documents three latent footguns in the class system. This PR closes two of them and removes the dormant opt-in lint that was supposed to cover the third. (1) Every derived-class constructor whose parent has a user-defined ctor must call super(...) exactly once on every control-flow path. Replaces the opt-in always_call_super single-bool check with a CFG-aware {lo, hi} walker (countSuperCalls) modeled on exprReturns. Catches missing super, super on only one if/else branch, super in a loop (unbounded count), super called twice on the same path. Handles try/recover: the try block is the normal-flow path; the recover block is fatal-panic-with-diagnostics and is ignored (matched on both the direct ExprTryCatch and the JIT-mode builtin_try_recover rewrite via call->func->module->name == "$" guard). (2) super(...) and super.X() may only walk past an intermediate class that has no user-defined ctor or matching method. Skipping a class with its own user ctor (whose invariants would silently be bypassed) is rejected at the typer with invalid_super_call. Empty-intermediate skip (e.g. the SkipMid pattern in tests/language/super.das) stays legal. The always_call_super option is hard-removed - policy field, lint binding, option-table row, serialize stream byte, and doc references all deleted. Out of scope (PR2): synthesizing a derived default ctor that chains via super() when no user ctor is provided. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dec6363 commit d6bd2e5

18 files changed

Lines changed: 756 additions & 34 deletions

doc/source/reference/language/classes.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,17 @@ up the inheritance chain to the nearest ancestor that does:
169169
170170
Walk-up matches by argument types, so overloaded ``super(args)`` calls pick the closest
171171
ancestor whose constructor or method accepts those arguments. If no ancestor matches, the
172-
call is rejected at compile time.
172+
call is rejected at compile time. The walk-up may only step past a class that has no
173+
user-defined constructor (or method, in the case of ``super.method()``); attempting to
174+
skip a class whose user constructor (or method) would otherwise establish invariants is
175+
rejected — call the immediate parent and let it chain via its own ``super()``.
176+
177+
Every constructor in a derived class whose parent has a user-defined constructor must call
178+
``super(...)`` exactly once on every control-flow path. The lint is unconditional:
179+
180+
* zero ``super(...)`` calls on any reachable path → error;
181+
* two or more ``super(...)`` calls on any path → error;
182+
* ``super(...)`` inside a loop body → error (call count is not bounded to one).
173183

174184
Inside a derived class's finalizer (``operator delete``), ``delete super.self`` runs the
175185
parent's finalizer on the current object:
@@ -197,9 +207,6 @@ functions — see :ref:`Structs <structs>`.
197207
Base-class finalization is explicit, not automatic: a derived finalizer that omits
198208
``delete super.self`` will not run any ancestor finalizer.
199209

200-
The option ``always_call_super`` can be enabled to require ``super()`` in every constructor
201-
(see :ref:`Options <options>`).
202-
203210
Alternatively, the parent's method can be called directly using the backtick syntax:
204211

205212
.. code-block:: das

doc/source/reference/language/options.rst

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,6 @@ Lint Control
9292
- bool
9393
- true
9494
- Disallows writing to nameless (intermediate) variables on the stack.
95-
* - ``always_call_super``
96-
- bool
97-
- false
98-
- Requires ``super()`` to be called in every class constructor.
9995
* - ``no_unused_function_arguments``
10096
- bool
10197
- false

doc/source/reference/tutorials/18_classes.rst

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,67 @@ bypassing virtual dispatch::
7575
The compiler rewrites ``super()`` to ``Parent`Constructor(self, ...)`` and
7676
``super.method()`` to ``Parent`method(self, ...)``.
7777

78+
``super(...)`` is required exactly once per path
79+
================================================
80+
81+
Every constructor in a derived class whose parent has a user-defined constructor
82+
must call ``super(...)`` exactly once on every control-flow path. The lint catches:
83+
84+
::
85+
86+
class Bad : Base {
87+
def Bad(f : bool) {
88+
if (f) {
89+
super() // ERROR: false branch has zero super() calls
90+
}
91+
}
92+
}
93+
94+
class AlsoBad : Base {
95+
def AlsoBad {
96+
super()
97+
super() // ERROR: super() called more than once
98+
}
99+
}
100+
101+
A call to ``super(...)`` inside a loop is also rejected — the count is not
102+
bounded to one. To branch on arguments, both branches must call ``super(...)``::
103+
104+
class OK : Shape {
105+
def OK(big : bool) {
106+
if (big) {
107+
super("Big")
108+
} else {
109+
super("Small")
110+
}
111+
}
112+
}
113+
114+
``super.X()`` cannot skip an intermediate
115+
=========================================
116+
117+
``super.X()`` (whether ``X`` is a method or the name of an ancestor class) may
118+
walk past only intermediate classes that have **no** user-defined constructor or
119+
matching method. Skipping a class whose user code would otherwise establish
120+
invariants is rejected. Empty intermediates are still legal::
121+
122+
class A {
123+
def A { /* ... */ }
124+
}
125+
126+
class B : A {
127+
def B { super() } // B has its own ctor
128+
}
129+
130+
class C : B {
131+
def C {
132+
super.A() // ERROR: would silently skip B's invariants
133+
}
134+
}
135+
136+
If you really want only ``A``'s setup, name the immediate parent:
137+
``super.B()`` (or ``super()``) and let ``B``'s constructor chain to ``A`` itself.
138+
78139
Creating instances
79140
==================
80141

include/daScript/ast/ast.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1567,7 +1567,6 @@ namespace das
15671567
/*option*/ bool no_unsafe_uninitialized_structures = true; // if true, then unsafe uninitialized structures are not allowed
15681568
/*option*/ bool strict_properties = false; // if true, then properties are strict, i.e. a.prop = b does not get promoted to a.prop := b
15691569
/*option*/ bool no_writing_to_nameless = true; // if true, then writing to nameless variables (intermediate on the stack) is not allowed
1570-
/*option*/ bool always_call_super = false; // if true, then super() needs to be called from every class constructor
15711570
// environment
15721571
/*option*/ bool no_optimizations = false; // disable optimizations, regardless of settings
15731572
/*option*/ bool no_infer_time_folding = false; // disable infer-time constant folding

src/ast/ast_infer_type.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,32 @@ namespace das {
1313
// in ast_handle of all places, due to reporting fields
1414
void reportTrait(const TypeDeclPtr &type, const string &prefix, const callable<void(const TypeDeclPtr &, const string &)> &report);
1515

16+
// True if `klass` has any non-generated user-defined function with the given name (mangled
17+
// as `Klass`methodName`). For methodName == klass->name, this detects user ctors; for any
18+
// other name, it detects user methods. Used at super(...) / super.method() walk-up sites to
19+
// reject silent skip of an intermediate class's user-defined invariants.
20+
// The class's ctors/methods live in the same module as the class itself (parser registers
21+
// them together), so we look up by name hash on `klass->module->functionsByName` /
22+
// `genericsByName` instead of scanning the entire library. The `classParent == klass`
23+
// identity check defends against same-named classes in other modules bleeding in via
24+
// generic instantiation (the in-tree corpus already has multiple `ContextStateAgent`).
25+
static bool classHasUserNamedFunction(Program *, Structure * klass, const string & methodName) {
26+
if (!klass || !klass->module) return false;
27+
string funcName = klass->name + "`" + methodName;
28+
uint64_t hName = hash64z(funcName.c_str());
29+
if (auto kv = klass->module->functionsByName.find(hName)) {
30+
for (auto * fn : kv->second) {
31+
if (!fn->generated && fn->classParent == klass) return true;
32+
}
33+
}
34+
if (auto kv = klass->module->genericsByName.find(hName)) {
35+
for (auto * fn : kv->second) {
36+
if (!fn->generated && fn->classParent == klass) return true;
37+
}
38+
}
39+
return false;
40+
}
41+
1642
CaptureLambda::CaptureLambda(bool cm) : inClassMethod(cm) {}
1743
void CaptureLambda::preVisit(ExprVar *expr) {
1844
if (!expr->type) { // trying to capture non-inferred section
@@ -1490,6 +1516,25 @@ namespace das {
14901516
return Visitor::visit(expr);
14911517
}
14921518
}
1519+
// super.<AncestorClassName>() is the legacy form for invoking that
1520+
// ancestor's constructor. Allow only when the named class is the
1521+
// immediate parent or reachable through an unbroken chain of empty
1522+
// intermediates (no user ctors). Skipping a class with a user ctor
1523+
// would silently bypass its invariants.
1524+
for (Structure * anc = baseClass->parent; anc; anc = anc->parent) {
1525+
if (anc->name == eField->name) {
1526+
for (Structure * mid = baseClass; mid != anc; mid = mid->parent) {
1527+
if (classHasUserNamedFunction(program, mid, mid->name)) {
1528+
error("call to super." + eField->name + " in " + func->name
1529+
+ " cannot skip " + mid->name
1530+
+ " which has its own constructor — call super." + mid->name + "(...) instead",
1531+
"", "", expr->at, CompilationError::invalid_super_call);
1532+
return Visitor::visit(expr);
1533+
}
1534+
}
1535+
break;
1536+
}
1537+
}
14931538
vector<TypeDeclPtr> argTypes;
14941539
auto selfType = new TypeDecl(Type::tStructure);
14951540
selfType->structType = func->classParent;
@@ -1515,6 +1560,16 @@ namespace das {
15151560
expr->at, CompilationError::ambiguous_super_call);
15161561
return Visitor::visit(expr);
15171562
}
1563+
// No matching method here. Before walking past, check: does this
1564+
// baseClass have a user-defined method with the same name (different
1565+
// sig)? If yes, we'd silently skip its definition — reject.
1566+
if (classHasUserNamedFunction(program, baseClass, eField->name)) {
1567+
error("call to super." + eField->name + " in " + func->name
1568+
+ " cannot skip " + baseClass->name + " which defines its own "
1569+
+ eField->name + " — adjust args to match " + baseClass->name + "'s overload",
1570+
"", "", expr->at, CompilationError::invalid_super_call);
1571+
return Visitor::visit(expr);
1572+
}
15181573
baseClass = baseClass->parent;
15191574
}
15201575
error("call to super in " + func->name + " is not allowed, no matching super method " + eField->name, "", "",
@@ -5577,6 +5632,9 @@ namespace das {
55775632
// Walks up the inheritance chain looking for a parent class constructor that matches the
55785633
// argument types; this lets `super(...)` call the closest ancestor's constructor when an
55795634
// intermediate class doesn't define its own (mirrors super.method() / delete super.self).
5635+
// The walk-up may only step past a class with NO user ctor — silently skipping a class
5636+
// whose user ctor establishes invariants is rejected (use the right `super(args)` shape
5637+
// to call the immediate parent).
55805638
if (func && func->isClassMethod && func->classParent && expr->name == "super") {
55815639
if (auto baseClass = func->classParent->parent) {
55825640
vector<TypeDeclPtr> argumentTypes;
@@ -5601,6 +5659,14 @@ namespace das {
56015659
expr->at, CompilationError::ambiguous_super_constructor);
56025660
return Visitor::visit(expr);
56035661
}
5662+
// No matching ctor here. Before walking past, check: does this baseClass
5663+
// have ANY user-defined ctor? If yes, we'd silently skip its invariants — reject.
5664+
if (classHasUserNamedFunction(program, baseClass, baseClass->name)) {
5665+
error("call to super in " + func->name + " cannot skip " + baseClass->name
5666+
+ " which has its own constructor — adjust super(...) args to match " + baseClass->name,
5667+
"", "", expr->at, CompilationError::invalid_super_call);
5668+
return Visitor::visit(expr);
5669+
}
56045670
baseClass = baseClass->parent;
56055671
}
56065672
error("call to super in " + func->name + ": no matching super constructor for " + func->classParent->name, "", "",

0 commit comments

Comments
 (0)