From 897f22ba125840099ad7cb6fbca3d776ed9699dd Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 12 Oct 2026 16:20:54 +0100 Subject: [PATCH] A derived class's implicit constructor passes its arguments to the base TypeScript gives a derived class with no constructor an implicit `constructor(...args) { super(...args); }`. tslang generated one only for a class with field initializers, and as `constructor() { super(); }`, so `new Derived(2, 3)` failed with "Expected 1-2 arguments, but got 0" when the base constructor takes parameters. The generated constructor now takes the base constructor's parameters, typed as its function is (a generic base's already specialized; optional and rest parameters included), and passes them to `super`; the base applies its defaults. That needed `super(a, ...rest)` to work: a super call's arguments were generated without the constructor's parameters (the callee is the base class), so a spread had no count to fill and the base got an empty rest parameter. They are generated against the base constructor now. Errors in the generated constructor and in the assignments generated for field initializers were reported at 1:1, as those nodes had no position. They are reported at the class and at the field now: `argv: Ref = null` says 6:5, not 1:1. Found while porting native_ts_graphics samples (TSLANG_ISSUES.md 1, 2 and the location in 4). Co-Authored-By: Claude Opus 5.5 --- tslang/lib/TypeScript/MLIRGenClasses.cpp | 98 ++++++++++++++++- tslang/lib/TypeScript/MLIRGenExpressions.cpp | 23 +++- tslang/test/tester/CMakeLists.txt | 20 ++++ .../diagnostic-location/field_initializer.ts | 7 ++ .../implicit_constructor.ts | 11 ++ .../tests/00class_implicit_constructor.ts | 103 ++++++++++++++++++ 6 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 tslang/test/tester/diagnostic-location/field_initializer.ts create mode 100644 tslang/test/tester/diagnostic-location/implicit_constructor.ts create mode 100644 tslang/test/tester/tests/00class_implicit_constructor.ts diff --git a/tslang/lib/TypeScript/MLIRGenClasses.cpp b/tslang/lib/TypeScript/MLIRGenClasses.cpp index f49da68ca..baeddaf80 100644 --- a/tslang/lib/TypeScript/MLIRGenClasses.cpp +++ b/tslang/lib/TypeScript/MLIRGenClasses.cpp @@ -1525,18 +1525,98 @@ genContext); NodeFactory nf(NodeFactoryFlags::None); + // errors in the generated code are reported at the class + auto atClass = [&](auto node) { + node->pos = classDeclarationAST->pos; + node->_end = classDeclarationAST->_end; + return node; + }; + + NodeArray parameters; NodeArray statements; if (!newClassPtr->baseClasses.empty()) { - auto superExpr = nf.createToken(SyntaxKind::SuperKeyword); - auto callSuper = nf.createCallExpression(superExpr, undefined, undefined); - statements.push_back(nf.createExpressionStatement(callSuper)); + // TypeScript's implicit `constructor(...args) { super(...args); }`: it takes the + // parameters of the base constructor, typed as its function is (a generic base's + // already specialized), and passes them on; the base applies its defaults + mlir_ts::FunctionType baseConstructorType; + for (auto baseClass = newClassPtr->baseClasses.front(); baseClass; + baseClass = baseClass->baseClasses.empty() ? ClassInfo::TypePtr() : baseClass->baseClasses.front()) + { + if (baseClass->hasConstructor) + { + auto index = baseClass->getMethodIndex(CONSTRUCTOR_NAME); + if (index >= 0) + { + baseConstructorType = baseClass->methods[index].funcType; + } + + break; + } + } + + NodeArray superArguments; + if (baseConstructorType) + { + auto inputs = baseConstructorType.getInputs(); + // the first input is `this` + for (auto index = 1u; index < inputs.size(); index++) + { + auto paramType = inputs[index]; + auto isRest = baseConstructorType.isVarArg() && index == inputs.size() - 1; + auto isOptional = false; + if (auto optionalType = dyn_cast(paramType)) + { + paramType = optionalType.getElementType(); + isOptional = true; + } + + // not a name the field initializers, run in this constructor, could mean + auto paramName = ".arg" + std::to_string(index - 1); + + std::string paramTypeAlias; + paramTypeAlias += "."; + paramTypeAlias += newClassPtr->fullName.str(); + paramTypeAlias += "."; + paramTypeAlias += CONSTRUCTOR_NAME; + paramTypeAlias += paramName; + getTypeAliasMap()[paramTypeAlias] = { paramType, undefined }; + + DotDotDotToken dotDotDotToken; + if (isRest) + { + dotDotDotToken = nf.createToken(SyntaxKind::DotDotDotToken); + } + + QuestionToken questionToken; + if (isOptional) + { + questionToken = nf.createToken(SyntaxKind::QuestionToken); + } + + parameters.push_back(atClass(nf.createParameterDeclaration( + undefined, dotDotDotToken, nf.createIdentifier(stows(paramName)), questionToken, + nf.createTypeReferenceNode(nf.createIdentifier(stows(paramTypeAlias)), undefined)))); + + Expression argument = atClass(nf.createIdentifier(stows(paramName))); + if (isRest) + { + argument = atClass(nf.createSpreadElement(argument)); + } + + superArguments.push_back(argument); + } + } + + auto superExpr = atClass(nf.createToken(SyntaxKind::SuperKeyword)); + auto callSuper = atClass(nf.createCallExpression(superExpr, undefined, superArguments)); + statements.push_back(atClass(nf.createExpressionStatement(callSuper))); } - auto body = nf.createBlock(statements, /*multiLine*/ false); + auto body = atClass(nf.createBlock(statements, /*multiLine*/ false)); - auto generatedConstructor = nf.createConstructorDeclaration(undefined, undefined, body); + auto generatedConstructor = atClass(nf.createConstructorDeclaration(undefined, parameters, body)); newClassPtr->extraMembers.push_back(generatedConstructor); } @@ -2806,6 +2886,14 @@ genContext); propertyDeclaration->initializer); auto expr_statement = nf.createExpressionStatement(_this_name_equal); + // errors in the assignment (the initializer's cast to the field type) are reported at + // the field, not at the start of the file + for (Node node : {Node(_this), Node(_name), Node(_this_name), Node(_this_name_equal), Node(expr_statement)}) + { + node->pos = propertyDeclaration->pos; + node->_end = propertyDeclaration->_end; + } + // NOTE: upward mailbox: drained when the constructor body is generated - see A7 const_cast(genContext).generatedStatements.push_back(expr_statement.as()); } diff --git a/tslang/lib/TypeScript/MLIRGenExpressions.cpp b/tslang/lib/TypeScript/MLIRGenExpressions.cpp index f76a1d590..8c7532eab 100644 --- a/tslang/lib/TypeScript/MLIRGenExpressions.cpp +++ b/tslang/lib/TypeScript/MLIRGenExpressions.cpp @@ -1080,7 +1080,28 @@ namespace mlirgen { offsetArgs = 2; } - if (mlir::failed(mlirGenOperands(callExpression->arguments, operands, funcResult.getType(), genContext, offsetArgs, noReceiverTypesForGenericCall))) + + // `super(...)`: the callee is the base class, and the arguments are its constructor's. Without + // the constructor's parameters a spread had no count to fill and passed nothing: `super(a, ...r)` + // gave the base an empty rest parameter. + auto operandsFuncType = funcResult.getType(); + if (auto classStorageType = dyn_cast(funcType)) + { + for (auto classInfo = getClassInfoByFullName(classStorageType.getName().getValue()); classInfo; + classInfo = classInfo->baseClasses.empty() ? ClassInfo::TypePtr() : classInfo->baseClasses.front()) + { + auto index = classInfo->getMethodIndex(CONSTRUCTOR_NAME); + if (index >= 0) + { + operandsFuncType = classInfo->methods[index].funcType; + // after `this` + offsetArgs = 1; + break; + } + } + } + + if (mlir::failed(mlirGenOperands(callExpression->arguments, operands, operandsFuncType, genContext, offsetArgs, noReceiverTypesForGenericCall))) { return mlir::failure(); } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index f1d97fc0d..a3d505115 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -527,6 +527,7 @@ tslang_add_test(NAME test-compile-00-class-accessor-2 COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-class-accessor-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") tslang_add_test(NAME test-compile-00-class-accessor-virtual COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts") tslang_add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") +tslang_add_test(NAME test-compile-00-class-implicit-constructor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_implicit_constructor.ts") tslang_add_test(NAME test-compile-00-class-super-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") tslang_add_test(NAME test-compile-00-class-default-constructor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_def_constr.ts") tslang_add_test(NAME test-compile-00-class-constructor-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_constr_fields.ts") @@ -1055,6 +1056,7 @@ tslang_add_test(NAME test-jit-00-class-accessor-2 COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-class-accessor-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") tslang_add_test(NAME test-jit-00-class-accessor-virtual COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_virtual.ts") tslang_add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") +tslang_add_test(NAME test-jit-00-class-implicit-constructor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_implicit_constructor.ts") tslang_add_test(NAME test-jit-00-class-super-static COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") tslang_add_test(NAME test-jit-00-class-default-constructor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_def_constr.ts") tslang_add_test(NAME test-jit-00-class-static-constructor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_static_constr.ts") @@ -1837,6 +1839,7 @@ set(TSLANG_CORPUS 00class_structural_extends.ts 00class_super_static.ts 00class_super.ts + 00class_implicit_constructor.ts 00class_symbol_iterator.ts 00class_var_args.ts 00class_virtual_call.ts @@ -2845,6 +2848,23 @@ set_tests_properties(test-compile-diagnostic-location-generated-cast PROPERTIES PASS_REGULAR_EXPRESSION "generated_cast\\.ts:6:23: error: can't cast from 'null' to 'string'" FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|generated_cast\\.ts:1:") +# An error in a field initializer, or in a derived class's implicit constructor, is reported at the +# field or the class - not at 1:1, where the code the compiler generates for them had put it. +add_test(NAME test-compile-diagnostic-location-field-initializer + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/diagnostic-location/field_initializer.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/diagnostic-location-field-initializer.obj") +set_tests_properties(test-compile-diagnostic-location-field-initializer + PROPERTIES PASS_REGULAR_EXPRESSION "field_initializer\\.ts:6:5: error: can't cast from 'null' to 'Reference'" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|field_initializer\\.ts:1:") +add_test(NAME test-compile-diagnostic-location-implicit-constructor + COMMAND $ --emit=obj --no-default-lib -mm=none + "${PROJECT_SOURCE_DIR}/test/tester/diagnostic-location/implicit_constructor.ts" + -o "${CMAKE_CURRENT_BINARY_DIR}/diagnostic-location-implicit-constructor.obj") +set_tests_properties(test-compile-diagnostic-location-implicit-constructor + PROPERTIES PASS_REGULAR_EXPRESSION "implicit_constructor\\.ts:9:1: error: Class constructor is not accessable" + FAIL_REGULAR_EXPRESSION "Stack dump|Assertion failed|implicit_constructor\\.ts:1:") + # A call to the collector's API (GC_gcollect, or the weak-link functions DefaultLib's WeakRef uses) # is an error under every model but gc, which alone links the collector - it used to reach the # linker as an unresolved symbol. Under gc the same file compiles. diff --git a/tslang/test/tester/diagnostic-location/field_initializer.ts b/tslang/test/tester/diagnostic-location/field_initializer.ts new file mode 100644 index 000000000..81709f2e3 --- /dev/null +++ b/tslang/test/tester/diagnostic-location/field_initializer.ts @@ -0,0 +1,7 @@ +// An error in a field's initializer was reported at the start of the file: the assignment the +// compiler generates for it in the constructor had no position. + +class Args { + count = 1; + protected argv: Ref = null; +} diff --git a/tslang/test/tester/diagnostic-location/implicit_constructor.ts b/tslang/test/tester/diagnostic-location/implicit_constructor.ts new file mode 100644 index 000000000..ba12e6d55 --- /dev/null +++ b/tslang/test/tester/diagnostic-location/implicit_constructor.ts @@ -0,0 +1,11 @@ +// An error in a derived class's implicit constructor was reported at the start of the file. +// Here the base's constructor is private, so the implicit one can't call it. + +class Base { + private constructor(protected a: int) { + } +} + +class Derived extends Base { + extra = 1; +} diff --git a/tslang/test/tester/tests/00class_implicit_constructor.ts b/tslang/test/tester/tests/00class_implicit_constructor.ts new file mode 100644 index 000000000..924ff8c3e --- /dev/null +++ b/tslang/test/tester/tests/00class_implicit_constructor.ts @@ -0,0 +1,103 @@ +// regression test: a derived class without a constructor gets TypeScript's implicit one, +// `constructor(...args) { super(...args); }`. +// +// bugs found and fixed: +// - a derived class with field initializers (and no constructor) was given a generated +// `constructor() { super(); }`, so `new Derived(2, 3)` failed with "Expected 1-2 arguments, +// but got 0", reported at line 1. It now takes the base constructor's parameters (optional +// ones and a rest parameter included) and passes them on. +// - `super(a, ...rest)` passed an empty rest parameter: the arguments of a super call were +// generated without the constructor's parameters, so a spread had nothing to fill. + +class Base { + constructor(protected a: int, protected b: int = 10, public c?: string, ...rest: int[]) { + this.restLength = rest.length; + } + + restLength: int; + + sum(): int { + return this.a + this.b; + } +} + +class Derived extends Base { + extra = 2; +} + +// the implicit constructor of a class whose base has one too +class MoreDerived extends Derived { + more = 3; +} + +class SpreadSuper extends Base { + constructor(a: int, ...rest: int[]) { + super(a, 1, "s", ...rest); + } +} + +class Generic { + constructor(public v: T) { + } +} + +class FromSpecialized extends Generic { + w = 5; +} + +class FromGeneric extends Generic { + w = 6; +} + +class NoConstructorBase { + x = 1; +} + +class NoConstructorDerived extends NoConstructorBase { + y = 2; +} + +// a field initializer runs in the implicit constructor, and still sees the outer `arg0` +const arg0 = 99; +class Shadow extends Base { + z = arg0; +} + +function main() { + const d = new Derived(2, 3, "s", 7, 8); + assert(d.sum() == 5); + assert(d.extra == 2); + assert(d.c == "s"); + assert(d.restLength == 2); + + // the base's default and optional parameters + const d2 = new Derived(4); + assert(d2.sum() == 14); + assert(d2.c == undefined); + assert(d2.restLength == 0); + + const m = new MoreDerived(1, 2, "t", 9); + assert(m.sum() == 3); + assert(m.extra == 2); + assert(m.more == 3); + assert(m.restLength == 1); + + const s = new SpreadSuper(1, 7, 8, 9); + assert(s.restLength == 3); + + const f = new FromSpecialized("hello"); + assert(f.v == "hello"); + assert(f.w == 5); + + const g = new FromGeneric(1.5); + assert(g.v == 1.5); + assert(g.w == 6); + + const n = new NoConstructorDerived(); + assert(n.x == 1); + assert(n.y == 2); + + assert(new Shadow(1).z == 99); + + print("done."); +}