Skip to content

Commit 9982420

Browse files
borisbatclaude
andcommitted
class_super: synth derived ctor chains super + suppress 0-arg when user has ctor
Follow-up to #2892. Closes the synth-ctor side of the silent-bypass family. PR1 made super(...) mandatory on every CFG path of a user-defined derived ctor and hard-rejected super.X() skipping a user-coded intermediate. But it left two gaps: (a) a fully empty class Derived : Base {} still got the pre-existing 0-arg field-init synth ctor that never called Base's ctor, so new Derived() silently bypassed Base's invariants; (b) a class with an args-only user ctor (def Foo(int)) also got a pre-existing 0-arg synth that silently bypassed the user ctor on new Foo(). Both close here. S2 Synth-chain ctor for implicit-default-derived classes - InferTypes::visit(Structure*) now fires tryMakeStructureCtor when any ancestor has a user ctor (not just on field-init), so empty intermediates get a chain ctor that lets further-derived classes reach the deepest user code in one call. - makeConstructor emits a let-self / call ChainTarget-ChainTarget(self) / return self body when findChainCtorAncestor() returns non-null. The walk-past skips empty intermediates so the call lands on the closest user-ctored ancestor; that's required because intermediate synth ctors only register the factory (no (self : Mid) method form to walk to). S2d Hard-error when no default ancestor ctor exists - If a synth-needed class's closest user-ctored ancestor has no 0-arg signature (or all-defaulted args), error 30322 (missing_super_call) fires with a fix-it pointing at the offending ancestor by name. New tests/language/failed_parent_no_default* cover the direct and skip-empty walks. Option-A 0-arg synth suppression (the part Boris asked for) - tryMakeStructureCtor skips emission entirely when var->isClass and the class has any user ctor. Pre-PR2 daslang auto-synthesized a 0-arg field- init ctor whenever no 0-arg user ctor existed, which let new Class() silently bypass an args-only user ctor's invariants. Matches C++/Java/C# convention: providing any ctor opts out of the compiler-generated default. Structs keep the old behavior (no inheritance, no super to bypass). One in-tree fixture (cant_access_private_members.das) used the silent-bypass form and is updated to pass the required arg. Lint walks ancestors - PR1's lint only triggered when fn->classParent->parent had a user ctor. With chain synth in place, an empty intermediate would propagate the requirement: C : B : A where B is empty and A has user ctor must lint C's user ctor for missing super(). lint now uses findChainCtorAncestor. Suggestion text and countSuperCalls target both update to the resolved ancestor name (matches what super(...) actually resolves to via the walk-up in ast_infer_type.cpp:5682). Helper: findChainCtorAncestor (ast_generate.{h,cpp}) - Shared by lint, gate, and makeConstructor - single source of truth for "closest ancestor with a user ctor, skipping empty intermediates." Replaces three open-coded for-loops and the unused parentHasUserCtor() shim. Helper methods on Structure - Structure::hasUserConstructor() - any non-generated Klass-Klass exists - Structure::hasUserDefaultConstructor() - 0-arg-callable user ctor exists - Both look up by name hash via the class's own module (ctors are always co-located with their class), with a classParent == this identity check that defends against same-named classes in other modules (the in-tree corpus already has multiple ContextStateAgent classes). Tests - tests/language/super.das: ImplicitChain, two-level MidImplicit/Leaf- Implicit (regression for the chain-through-synth-intermediate case where the previous straight-Parent-Parent(self) approach failed because the synth intermediate has no method form), and DerivedWithDefault (chain resolves to parent ctor with default arg). - tests/language/failed_parent_no_default.das: B : A where A has only def A(int), expect 30322 + 30318/30341 cascade. - tests/language/failed_parent_no_default_skip_empty.das: C : B : A where B is empty, A has only def A(int). Error 30322 fires for both B and C, naming A as the offending ancestor in each. - tests/language/failed_super_skip_via_empty_intermediate.das: lint must flag C's missing super even though immediate parent B is empty. Docs - doc/source/reference/language/classes.rst: implicit-chain section + Option-A suppression note. - doc/source/reference/tutorials/18_classes.rst + tutorials/language/ 18_classes.das: implicit-chain example with class Pet : Animal {}, plus the suppression rule and the parent-no-default error case. AOT impact - Per plan S8: AOT hash for every class that gets a new synth chain ctor changes. Expect AOT hash desync on first CI; AOT cache rebuilds on next test_aot build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c0ce6ad commit 9982420

16 files changed

Lines changed: 390 additions & 47 deletions

doc/source/reference/language/classes.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,22 @@ Every constructor in a derived class whose parent has a user-defined constructor
181181
* two or more ``super(...)`` calls on any path → error;
182182
* ``super(...)`` inside a loop body → error (call count is not bounded to one).
183183

184+
When the derived class has *no* constructor of its own and the parent has a user
185+
constructor callable with no arguments (or with all arguments default-initialized), the
186+
compiler synthesizes a default constructor for the derived class that chains ``super()``
187+
automatically — so ``new Derived()`` runs the parent's ctor body instead of silently
188+
skipping it. If the parent's user constructors all require arguments, the derived class
189+
must declare its own constructor; otherwise compilation fails with
190+
``missing_super_call``.
191+
192+
Declaring *any* user constructor on a class — even one that only takes arguments —
193+
suppresses the synthesized 0-arg constructor entirely. ``new Class()`` then requires the
194+
user to also declare ``def Class()`` explicitly; otherwise lookup fails with
195+
``no matching function``. This matches the C++/Java/C# convention: a user-defined
196+
constructor opts out of the compiler-generated default. Pre-PR2 behavior auto-synthesized
197+
a 0-arg field-init ctor whenever the user omitted one, which silently bypassed any user
198+
ctor's invariants on ``new Class()`` — that bug is now an error.
199+
184200
Inside a derived class's finalizer (``operator delete``), ``delete super.self`` runs the
185201
parent's finalizer on the current object:
186202

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

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

78+
Implicit ``super()`` chain — no constructor needed
79+
==================================================
80+
81+
If the parent has a user-defined constructor that's callable with no
82+
arguments, the compiler synthesizes a default constructor for the derived
83+
class that chains ``super()`` automatically::
84+
85+
class Animal {
86+
def Animal { print("Animal init\n") } // 0-arg user ctor
87+
}
88+
89+
class Pet : Animal {
90+
name : string = "rex"
91+
}
92+
93+
new Pet() // prints "Animal init" — synth ctor calls super()
94+
95+
The synthesis runs only when:
96+
97+
- the parent has a user-defined constructor (any signature),
98+
- the derived class has **no** constructor of its own (none at all), and
99+
- the parent ctor is 0-arg-callable (no arguments, or all arguments have
100+
defaults).
101+
102+
Defining **any** user constructor — even one that only takes arguments —
103+
suppresses the 0-arg synthesis. ``new Derived()`` then fails to resolve
104+
unless the user also declares ``def Derived()``::
105+
106+
class Pet : Animal {
107+
def Pet(name : string) { super(); this.name = name }
108+
}
109+
new Pet() // ERROR: no matching ctor _::Pet()
110+
new Pet("rex") // OK
111+
112+
If the parent has only constructors that require arguments, the derived
113+
class must declare its own constructor::
114+
115+
class A { def A(val : int) { /* ... */ } }
116+
class B : A {} // ERROR: no 0-arg parent ctor to chain
117+
class B : A { def B { super(0) } } // OK
118+
78119
``super(...)`` is required exactly once per path
79120
================================================
80121

include/daScript/ast/ast.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ namespace das
288288
string describe() const { return name; }
289289
string getMangledName() const;
290290
bool hasAnyInitializers() const;
291+
bool hasUserConstructor() const;
292+
bool hasUserDefaultConstructor() const;
291293
void serialize( AstSerializer & ser );
292294
void gc_collect ( gc_root * target, gc_root * from );
293295
uint64_t getOwnSemanticHash(HashBuilder & hb,das_set<Structure *> & dep, das_set<Annotation *> & adep) const;

include/daScript/ast/ast_generate.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ namespace das {
8383
*/
8484
DAS_API FunctionPtr makeConstructor ( Structure * str, bool isPrivate );
8585

86+
// Walk `cls`'s inheritance chain starting from cls->parent, return the closest
87+
// ancestor with a user-defined ctor (Klass`Klass not generated). Used by the
88+
// synth-ctor body emitter (skip past empty intermediates straight to the deepest
89+
// user code) and by the lint (super(...) walk-up matches the same target).
90+
// Returns nullptr if `cls` isn't a class, has no parent, or no ancestor has a
91+
// user ctor.
92+
DAS_API Structure * findChainCtorAncestor ( Structure * cls );
93+
8694
/*
8795
def clone(var a:STRUCT_NAME; b:STRUCT_NAME)
8896
a.f1 := b.f1

include/daScript/ast/compilation_errors.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ namespace das
320320
, missing_result = 30319 // 1 site(s)
321321
, missing_structure_field = 30320 // 1 site(s)
322322
, missing_typeinfo_subexpression = 30321 // 4 site(s)
323+
, missing_super_call = 30322 // class ctor must call super(); or parent ctor missing default
323324

324325
// mismatching_*
325326

src/ast/ast.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,57 @@ namespace das {
291291
return false;
292292
}
293293

294+
// True if this structure has any non-generated ctor method (Klass`Klass).
295+
// Ctor methods always live in the same module as the class (parser registers
296+
// them together), so we look up by name hash via functionsByName / genericsByName.
297+
// The `classParent == this` identity check defends against same-named classes
298+
// in other modules bleeding in through generic instantiation.
299+
bool Structure::hasUserConstructor() const {
300+
if ( !module ) return false;
301+
string ctorName = name + "`" + name;
302+
uint64_t hName = hash64z(ctorName.c_str());
303+
if ( auto kv = module->functionsByName.find(hName) ) {
304+
for ( auto * fn : kv->second ) {
305+
if ( !fn->generated && fn->classParent == this ) return true;
306+
}
307+
}
308+
if ( auto kv = module->genericsByName.find(hName) ) {
309+
for ( auto * fn : kv->second ) {
310+
if ( !fn->generated && fn->classParent == this ) return true;
311+
}
312+
}
313+
return false;
314+
}
315+
316+
// True if this structure has a non-generated 0-arg-callable ctor (no args, or
317+
// all args default-initialized). Used by the synth-derived-ctor gate to verify
318+
// the parent has a default constructor reachable as Parent`Parent(self).
319+
bool Structure::hasUserDefaultConstructor() const {
320+
if ( !module ) return false;
321+
string ctorName = name + "`" + name;
322+
uint64_t hName = hash64z(ctorName.c_str());
323+
auto matches = [this]( Function * fn ) -> bool {
324+
if ( fn->generated ) return false;
325+
if ( fn->classParent != this ) return false;
326+
// arg 0 is always 'self'; arg 1+ must be default-initialized
327+
for ( size_t i=1; i<fn->arguments.size(); ++i ) {
328+
if ( !fn->arguments[i]->init ) return false;
329+
}
330+
return true;
331+
};
332+
if ( auto kv = module->functionsByName.find(hName) ) {
333+
for ( auto * fn : kv->second ) {
334+
if ( matches(fn) ) return true;
335+
}
336+
}
337+
if ( auto kv = module->genericsByName.find(hName) ) {
338+
for ( auto * fn : kv->second ) {
339+
if ( matches(fn) ) return true;
340+
}
341+
}
342+
return false;
343+
}
344+
294345
bool Structure::canCopy(bool tempMatters) const {
295346
if ( circularGuard ) return true;
296347
circularGuard = true;

src/ast/ast_generate.cpp

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,16 @@ namespace das {
292292
return del;
293293
}
294294

295-
// return [[t()]]
295+
Structure * findChainCtorAncestor ( Structure * cls ) {
296+
if ( !cls || !cls->isClass || !cls->parent ) return nullptr;
297+
for ( auto * anc = cls->parent; anc; anc = anc->parent ) {
298+
if ( anc->hasUserConstructor() ) return anc;
299+
}
300+
return nullptr;
301+
}
302+
303+
// return [[t()]] -- or, for a class derived from a parent with a user ctor,
304+
// emit the let-self / call Parent`Parent(self) / return self chain body.
296305
FunctionPtr makeConstructor ( Structure * str, bool isPrivate ) {
297306
auto fn = new Function();
298307
fn->generated = true;
@@ -308,21 +317,61 @@ namespace das {
308317
if ( str->macroInterface ) fn->macroFunction = true;
309318
auto block = new ExprBlock();
310319
block->at = str->at;
311-
auto makeT = new ExprMakeStruct(str->at);
312-
if ( str->isClass ) makeT->alwaysSafe = true;
313-
makeT->useInitializer = false;
314-
makeT->nativeClassInitializer = true;
315-
for ( auto & f : str->fields ) {
316-
if ( f.init ) {
317-
makeT->useInitializer = true;
318-
break;
320+
// For synth ctors of derived classes whose ancestor chain has a user ctor,
321+
// emit a chain body that calls the deepest USER ctor directly. The synth
322+
// walks past intermediate synth-ctored classes since those have no user
323+
// code -- only the user ctor's invariants need running.
324+
Structure * chainTarget = findChainCtorAncestor(str);
325+
if ( chainTarget ) {
326+
// let self = [[Derived()]]
327+
auto makeT = new ExprMakeStruct(str->at);
328+
makeT->alwaysSafe = true;
329+
makeT->useInitializer = true;
330+
makeT->nativeClassInitializer = true;
331+
makeT->makeType = new TypeDecl(str);
332+
makeT->structs.push_back(new MakeStruct());
333+
auto letS = new ExprLet();
334+
letS->at = str->at;
335+
letS->atInit = str->at;
336+
letS->visibility = str->at;
337+
letS->alwaysSafe = true; // local class variable
338+
auto argT = new TypeDecl(str);
339+
argT->constant = false;
340+
auto argV = new Variable();
341+
argV->name = "self";
342+
argV->type = argT;
343+
argV->at = str->at;
344+
argV->generated = true;
345+
argV->capture_as_ref = true;
346+
argV->init = makeT;
347+
argV->init_via_move = true;
348+
letS->variables.push_back(argV);
349+
block->list.push_back(letS);
350+
// call ChainTarget`ChainTarget(self) -- closest ancestor with user ctor
351+
auto cll = new ExprCall(str->at, chainTarget->name + "`" + chainTarget->name);
352+
cll->arguments.push_back(new ExprVar(str->at, "self"));
353+
block->list.push_back(cll);
354+
// return self
355+
auto returnDecl = new ExprReturn(str->at, new ExprVar(str->at, "self"));
356+
returnDecl->moveSemantics = true;
357+
block->list.push_back(returnDecl);
358+
} else {
359+
auto makeT = new ExprMakeStruct(str->at);
360+
if ( str->isClass ) makeT->alwaysSafe = true;
361+
makeT->useInitializer = false;
362+
makeT->nativeClassInitializer = true;
363+
for ( auto & f : str->fields ) {
364+
if ( f.init ) {
365+
makeT->useInitializer = true;
366+
break;
367+
}
319368
}
369+
makeT->makeType = new TypeDecl(str);
370+
makeT->structs.push_back(new MakeStruct());
371+
auto returnDecl = new ExprReturn(str->at,makeT);
372+
returnDecl->moveSemantics = true;
373+
block->list.push_back(returnDecl);
320374
}
321-
makeT->makeType = new TypeDecl(str);
322-
makeT->structs.push_back(new MakeStruct());
323-
auto returnDecl = new ExprReturn(str->at,makeT);
324-
returnDecl->moveSemantics = true;
325-
block->list.push_back(returnDecl);
326375
fn->body = block;
327376
verifyGenerated(fn->body);
328377
return fn;

src/ast/ast_infer_type.cpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,11 +401,26 @@ namespace das {
401401
}
402402
StructurePtr InferTypes::visit(Structure *var) {
403403
if (!var->genCtor) {
404-
if (var->hasAnyInitializers()) {
404+
// For a class whose ancestor chain contains a user ctor, synthesize a
405+
// default ctor that chains super() to the immediate parent -- otherwise
406+
// the user ctor's invariants get silently skipped on new Derived().
407+
// findChainCtorAncestor walks past empty intermediates so the chain
408+
// reaches the deepest user ctor in one call. Skipped when `var` already
409+
// has any user ctor: the user is responsible for chaining via super(...),
410+
// and the lint enforces that on every path.
411+
Structure * chainTargetAnc = var->hasUserConstructor() ? nullptr : findChainCtorAncestor(var);
412+
bool parentNeedsChain = chainTargetAnc != nullptr;
413+
bool missingParentDefault = parentNeedsChain && !chainTargetAnc->hasUserDefaultConstructor();
414+
if (missingParentDefault) {
415+
error("class " + var->name + " inherits from " + chainTargetAnc->name +
416+
" which has no default constructor; provide an explicit def " + var->name + "()", "", "",
417+
var->at, CompilationError::missing_super_call);
418+
}
419+
if (!missingParentDefault && (var->hasAnyInitializers() || parentNeedsChain)) {
405420
if (tryMakeStructureCtor(var, var->privateStructure, false)) {
406421
var->genCtor = true;
407422
}
408-
} else {
423+
} else if (!missingParentDefault) {
409424
getOrCreateDummy(thisModule);
410425
}
411426
}

src/ast/ast_infer_type_helper.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,15 @@ namespace das {
735735
return result;
736736
}
737737
bool InferTypes::tryMakeStructureCtor(Structure *var, bool isPrivate, bool fromGeneric) {
738+
// For classes, defining ANY user ctor (even args-only) suppresses the 0-arg
739+
// synth. The pre-PR2 behavior auto-synthesized a 0-arg ctor whenever no 0-arg
740+
// user ctor existed, which silently bypassed user invariants for `new Class()`
741+
// when only an args-taking user ctor was defined. Matches C++/Java/C# semantics
742+
// -- if the user wants `new Class()`, they must write `def Class()` explicitly.
743+
// Structs keep the old behavior (no inheritance, no super to bypass).
744+
if (var->isClass && var->hasUserConstructor()) {
745+
return false;
746+
}
738747
if (!hasDefaultUserConstructor(var->name)) {
739748
auto ctor = makeConstructor(var, isPrivate);
740749
if (fromGeneric) {

src/ast/ast_lint.cpp

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -317,30 +317,6 @@ namespace das {
317317
return {lo, hi};
318318
}
319319

320-
// True if `klass` has any non-generated user-defined ctor method (Klass`Klass).
321-
// A class's ctor methods always live in the same module as the class itself
322-
// (parser registers them together), so we jump straight to `klass->module` and
323-
// look up by name hash via functionsByName / genericsByName. The `classParent ==
324-
// klass` identity check defends against same-named classes in other modules
325-
// bleeding in through generic instantiation (the in-tree corpus already has
326-
// multiple `ContextStateAgent` classes).
327-
bool parentHasUserCtor ( Program *, Structure * klass ) {
328-
if ( !klass || !klass->module ) return false;
329-
string ctorName = klass->name + "`" + klass->name;
330-
uint64_t hName = hash64z(ctorName.c_str());
331-
if ( auto kv = klass->module->functionsByName.find(hName) ) {
332-
for ( auto * fn : kv->second ) {
333-
if ( !fn->generated && fn->classParent == klass ) return true;
334-
}
335-
}
336-
if ( auto kv = klass->module->genericsByName.find(hName) ) {
337-
for ( auto * fn : kv->second ) {
338-
if ( !fn->generated && fn->classParent == klass ) return true;
339-
}
340-
}
341-
return false;
342-
}
343-
344320
bool needAvoidNullPtr ( const TypeDeclPtr & type, bool allowDim ) {
345321
if ( !type ) {
346322
return false;
@@ -1060,17 +1036,19 @@ namespace das {
10601036
}
10611037
virtual FunctionPtr visit ( Function * fn ) override {
10621038
// Derived class ctor: every CFG path must call super(...) exactly once,
1063-
// and only when the parent has a user-defined ctor to chain to.
1039+
// and only when ANY ancestor has a user-defined ctor to chain to. The
1040+
// walk-up matches super(...)'s own walk-up semantics — an empty
1041+
// intermediate (synth chain ctor) propagates the invariant requirement.
10641042
if ( fn->isClassMethod && fn->classParent && fn->classParent->parent
10651043
&& !fn->generated
10661044
&& fn->name == fn->classParent->name + "`" + fn->classParent->name ) {
1067-
Structure * parent = fn->classParent->parent;
1068-
if ( parentHasUserCtor(program.get(), parent) ) {
1069-
string parentMangled = parent->name + "`" + parent->name;
1070-
auto count = reduceSuperCount(countSuperCalls(fn->body, parentMangled));
1045+
Structure * chainAncestor = findChainCtorAncestor(fn->classParent);
1046+
if ( chainAncestor ) {
1047+
string chainMangled = chainAncestor->name + "`" + chainAncestor->name;
1048+
auto count = reduceSuperCount(countSuperCalls(fn->body, chainMangled));
10711049
if ( count.lo == 0 ) {
10721050
program->error("class constructor " + fn->name + " does not call super(...) on every control-flow path",
1073-
"", "call super(...) (or super." + parent->name + "(...)) exactly once per path",
1051+
"", "call super(...) (or super." + chainAncestor->name + "(...)) exactly once per path",
10741052
fn->at, CompilationError::missing_function_body);
10751053
} else if ( count.hi != count.lo || count.hi > 1 ) {
10761054
program->error("class constructor " + fn->name + " calls super(...) more than once on some path",

0 commit comments

Comments
 (0)