From 69c5ac21cf71080f2874cd2a021ea0d5a38e1086 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 14:27:12 +0000 Subject: [PATCH] Write a positional construction as a Go composite literal (closes #68) [patch] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go's conversion syntax takes exactly one operand, so a ConstructionExpression with a type and two or more plain arguments came out as Wrapper(1, 2) — which the compiler rejects with "too many arguments in conversion to Wrapper" rather than reading as a longer conversion. The branch only asked whether any argument named a member, conflating a conversion with the other composite literal Go has: the one listing a struct's fields in declaration order. Route anything but a single argument to the brace list, and reserve the parenthesised form for the one-operand case that is actually a conversion. The exemplar the Go compile test builds now holds a positionally constructed value, so the shape is checked by a real toolchain rather than only pinned as text: the table lists its members by name and the assertion constructs nothing, so this was the one construction nothing compiled. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013HjQ6mUh5eNFYPYWU3GpD2 --- .../GoGeneratedSourceCompilesTests.cs | 32 ++++++++++++++++++- Coder.Test/Languages/GoGeneratorTests.cs | 25 +++++++++++++++ Coder/Languages/GoGenerator.cs | 27 +++++++--------- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs index ae8e517..1833d9f 100644 --- a/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs +++ b/Coder.Test/Languages/GoGeneratedSourceCompilesTests.cs @@ -83,7 +83,7 @@ func use() int { // circle.Sum is Point's, reached through the embedded field rather than inherited. return built.Sum() + zeroed.y + first.x + named.x + circle.Sum() + built.Pick() + - built.Add(zeroed).x + built.Negate().x + + built.Add(zeroed).x + built.Negate().x + Corner.y + int(built.ToFloat64()) + int(shape.area()) + int(ColourGreen) + measure("a", []int{1}) } @@ -160,6 +160,7 @@ private static SourceFile Exemplar() file.Members.Add(DescribesPoint()); file.Members.Add(CompiledExemplar.OriginAlias()); file.Members.Add(CompiledExemplar.OriginTable()); + file.Members.Add(Corner()); file.Members.Add(CompiledExemplar.Measure()); file.Members.Add(new CompileTimeAssertion { @@ -170,6 +171,35 @@ private static SourceFile Exemplar() return file; } + /// + /// Builds a value constructed from a type and two plain arguments, which is the shape neither of + /// the other two composite literals here has. + /// + /// The declaration. + /// + /// This is here because it is the one construction Go cannot write as a call: a conversion takes + /// one operand, so Point(1, 2) is too many arguments in conversion to Point rather + /// than a longer conversion, and only a compiler says so. The table above lists its members by + /// name and the assertion below constructs nothing at all, so without this the positional + /// literal is the one shape nothing compiles. + /// + private static FieldDeclaration Corner() + { + ConstructionExpression built = new(new TypeReference("Point")); + built.Arguments.Add(new LiteralExpression(1)); + built.Arguments.Add(new LiteralExpression(2)); + + FieldDeclaration corner = new() + { + Name = "Corner", + Type = new TypeReference("Point"), + InitialValue = built, + }; + corner.Documentation.Add("The far corner, built from its members in order."); + + return corner; + } + /// /// Builds the destructor, which becomes the Close the caller has to call. /// diff --git a/Coder.Test/Languages/GoGeneratorTests.cs b/Coder.Test/Languages/GoGeneratorTests.cs index 03a014c..b3a267b 100644 --- a/Coder.Test/Languages/GoGeneratorTests.cs +++ b/Coder.Test/Languages/GoGeneratorTests.cs @@ -641,6 +641,31 @@ public void BareListInitialiser_TakesTheDeclarationsType() Assert.AreEqual($"var Sizes []int = []int{{1, 2}}{NewLine}", Generator.Generate(sizes)); } + /// + /// Tests that a construction is written as a conversion only where Go has one — a type and + /// exactly one operand — and as a composite literal everywhere else. A conversion taking two + /// arguments is not a longer conversion but a compile error, so the argument count is what parts + /// the two shapes rather than whether any argument names a member. + /// + [TestMethod] + public void Construction_IsAConversionOnlyWhereItTakesOneValue() + { + ConstructionExpression named = new(new TypeReference("Point")); + named.Arguments.Add(new MemberInitialiser("X") { Value = Literal.Number(1) }); + + ConstructionExpression converted = new(new TypeReference("int64")); + converted.Arguments.Add(new VariableReference("n")); + + ConstructionExpression positional = new(new TypeReference("Wrapper")); + positional.Arguments.Add(Literal.Number(1)); + positional.Arguments.Add(Literal.Number(2)); + + Assert.AreEqual("Point{X: 1}", Generator.Generate(named)); + Assert.AreEqual("int64(n)", Generator.Generate(converted)); + Assert.AreEqual("Wrapper{1, 2}", Generator.Generate(positional)); + Assert.AreEqual("Point{}", Generator.Generate(new ConstructionExpression(new TypeReference("Point")))); + } + /// /// Tests that a list whose elements are themselves lists is written one per line, so that adding /// a row to a generated table touches one line. diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs index ee5034c..8318dba 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -1583,9 +1583,16 @@ private static string BranchType(ConditionalExpression conditional) => /// /// Three shapes, and which one is written depends on what the expression is rather than on where /// it stands: a construction naming its members is a composite literal, one with no type at all - /// is a composite literal whose type the declaration around it supplies, and one that names - /// neither is a call — which in Go is a conversion when it takes one argument, since that is - /// what int64(n) is. + /// is a composite literal whose type the declaration around it supplies, and one naming a type + /// and handing it exactly one value is a call — which in Go is a conversion, since that is what + /// int64(n) is. + /// + /// The argument count is what parts the last two, and it has to be: a conversion takes one + /// operand and nothing else, so Wrapper(1, 2) is not a longer conversion but a compile + /// error — too many arguments in conversion to Wrapper. A type handed two values + /// positionally is the other kind of composite literal, the one that lists a struct's fields in + /// declaration order, so anything but one argument is written in braces. + /// /// protected override void GenerateConstructionExpression(ConstructionExpression construction, CodeBlocker code) { @@ -1600,7 +1607,7 @@ protected override void GenerateConstructionExpression(ConstructionExpression co string type = SpellType(construction.Type); - if (construction.Arguments.Count == 0 || construction.Arguments.Any(argument => argument is MemberInitialiser)) + if (construction.Arguments.Count != 1 || construction.Arguments.Any(argument => argument is MemberInitialiser)) { code.Write(type); WriteElementList(construction, code, "{", "}", "{}"); @@ -1608,17 +1615,7 @@ protected override void GenerateConstructionExpression(ConstructionExpression co } code.Write($"{type}("); - - for (int index = 0; index < construction.Arguments.Count; index++) - { - if (index > 0) - { - code.Write(", "); - } - - GenerateInternal(construction.Arguments[index], code); - } - + GenerateInternal(construction.Arguments[0], code); code.Write(")"); }