diff --git a/Coder.Test/Languages/JavaScriptGeneratorTests.cs b/Coder.Test/Languages/JavaScriptGeneratorTests.cs index f0c3f6d..dac344a 100644 --- a/Coder.Test/Languages/JavaScriptGeneratorTests.cs +++ b/Coder.Test/Languages/JavaScriptGeneratorTests.cs @@ -160,4 +160,96 @@ public void CanGenerate_RejectsUnknownNode() Assert.IsFalse(Generator.CanGenerate(new AstLeafNode(1.0))); Assert.ThrowsExactly(() => Generator.Generate(new AstLeafNode(1.0))); } + /// + /// Builds a Shape base class and a Circle that extends it with a constructor, the + /// shape that throws on construction when the derived constructor never calls super. + /// + /// Adds the constructor's base-call initialiser, if any. + /// A source file holding both classes. + private static SourceFile DerivedClassFile(Action? circleConstructor = null) + { + ClassDeclaration shape = new("Shape"); + shape.Members.Add(new FieldDeclaration("name", "string") { InitialValue = new LiteralExpression("shape") }); + + ClassDeclaration circle = new("Circle") { BaseType = "Shape" }; + circle.Members.Add(new FieldDeclaration("radius", "double")); + FunctionDeclaration constructor = new("Circle") { Kind = FunctionKind.Constructor }; + constructor.Parameters.Add(new Parameter("radius", "double")); + circleConstructor?.Invoke(constructor); + constructor.Initialisers.Add(new MemberInitialiser("radius") { Value = new VariableReference("radius") }); + circle.Members.Add(constructor); + + return new SourceFile("shapes") { Members = { shape, circle } }; + } + + /// + /// Tests that a derived class's constructor calls super() before it assigns to this. + /// + [TestMethod] + public void DerivedConstructor_CallsSuperBeforeInitialisers() + { + string code = Generator.Generate(DerivedClassFile()); + + int superCall = code.IndexOf("super();", StringComparison.Ordinal); + int assignment = code.IndexOf("this.radius = radius;", StringComparison.Ordinal); + Assert.IsGreaterThanOrEqualTo(0, superCall, $"The derived constructor should call super():{Environment.NewLine}{code}"); + Assert.IsLessThan(assignment, superCall, $"super() should come before the first assignment to this:{Environment.NewLine}{code}"); + } + + /// + /// Tests that a class with no base type gets no super call, which would be a syntax error. + /// + [TestMethod] + public void BaseClassConstructor_DoesNotCallSuper() + { + ClassDeclaration shape = new("Shape"); + FunctionDeclaration constructor = new("Shape") { Kind = FunctionKind.Constructor }; + constructor.Initialisers.Add(new MemberInitialiser("name") { Value = new LiteralExpression("shape") }); + shape.Members.Add(constructor); + + string code = Generator.Generate(shape); + + Assert.DoesNotContain("super", code); + } + + /// + /// Tests that an initialiser named after the base, the C++ : Base(args) idiom, passes its + /// value to super instead of being assigned to a member called after the base. + /// + [TestMethod] + public void DerivedConstructor_BaseNamedInitialiser_BecomesTheSuperArgument() + { + string code = Generator.Generate(DerivedClassFile(constructor => + constructor.Initialisers.Add(new MemberInitialiser("Shape") { Value = new VariableReference("radius") }))); + + StringAssert.Contains(code, "super(radius);", StringComparison.Ordinal); + Assert.DoesNotContain("this.Shape", code); + Assert.DoesNotContain("super();", code); + } + + /// + /// Tests that an instance of a derived class can be constructed under Node.js, and that both the + /// base's field and the derived constructor's assignment land on it. + /// + [TestMethod] + public void DerivedClass_ConstructsUnderNode() + { + string? node = ToolchainHarness.FindOnPath("--version", "node"); + if (node is null) + { + Assert.Inconclusive("No Node.js runtime on the path, so nothing was run."); + return; + } + + string generated = Generator.Generate(DerivedClassFile()); + + ToolchainHarness.InTemporaryDirectory(directory => + { + string path = Path.Combine(directory, "shapes.mjs"); + File.WriteAllText(path, generated + "const c = new Circle(2);\nconsole.log(c.radius, c.name);\n"); + (int exitCode, string output) = ToolchainHarness.Run(node, path, directory); + Assert.AreEqual(0, exitCode, $"Node.js could not construct the derived class:{Environment.NewLine}{output}{Environment.NewLine}{generated}"); + StringAssert.Contains(output, "2 shape", StringComparison.Ordinal); + }); + } } diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index 444d062..9ce6799 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -366,7 +366,7 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod switch (member) { case FunctionDeclaration method: - GenerateMethod(method, code); + GenerateMethod(method, classDecl.BaseType, code); break; case EnumDeclaration nested: @@ -416,8 +416,9 @@ private void GenerateField(VariableDeclaration field, CodeBlocker code) /// Emits a function as a class method, which drops the function keyword. /// /// The function to emit as a method. + /// The class the method's class extends, if any. /// The writer to emit into. - private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) + private void GenerateMethod(FunctionDeclaration method, TypeReference? baseType, CodeBlocker code) { WritePreamble(method, code, PreambleSite.Method); @@ -457,9 +458,31 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) return; } - // JavaScript assigns where C++ initialises, before the body's own statements and in the order - // declared, which is what the initialiser means where there is no initialiser list. - foreach (MemberInitialiser initialiser in method.Initialisers) + MemberInitialiser? baseInitialiser = method.Kind == FunctionKind.Constructor && baseType is not null + ? WriteSuperCall(method, baseType, code) + : null; + + WriteInitialiserAssignments(method, baseInitialiser, code); + + foreach (AstNode statement in method.Body) + { + GenerateInternal(statement, code); + } + } + + /// + /// Writes a method's member initialisers as assignments to this. + /// + /// The method whose initialisers to write. + /// The initialiser already passed to super, which is skipped. + /// The writer to emit into. + /// + /// JavaScript assigns where C++ initialises, before the body's own statements and in the order + /// declared, which is what the initialiser means where there is no initialiser list. + /// + private void WriteInitialiserAssignments(FunctionDeclaration method, MemberInitialiser? baseInitialiser, CodeBlocker code) + { + foreach (MemberInitialiser initialiser in method.Initialisers.Where(initialiser => !ReferenceEquals(initialiser, baseInitialiser))) { code.Write($"this.{initialiser.Name} = "); @@ -470,11 +493,36 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) EndStatement(code); } + } - foreach (AstNode statement in method.Body) + /// + /// Emits the base constructor call a derived class's constructor has to make. + /// + /// The constructor being emitted. + /// The class it extends. + /// The writer to emit into. + /// The initialiser that supplied the call's argument, or null when the call passes none. + /// + /// A derived constructor that touches this, or returns at all, without calling super + /// throws, so the call comes first. C++ and C# call the base's default constructor implicitly, and + /// a call with no arguments is the same thing. An initialiser named after the base is the C++ + /// : Base(args) idiom, so its value is passed to the base rather than assigned to a member. + /// + private MemberInitialiser? WriteSuperCall(FunctionDeclaration constructor, TypeReference baseType, CodeBlocker code) + { + MemberInitialiser? baseInitialiser = constructor.Initialisers + .FirstOrDefault(initialiser => string.Equals(initialiser.Name, baseType.Name, StringComparison.Ordinal)); + + code.Write("super("); + + if (baseInitialiser?.Value is not null) { - GenerateInternal(statement, code); + GenerateInternal(baseInitialiser.Value, code); } + + code.Write(")"); + EndStatement(code); + return baseInitialiser; } ///