From eed73d6d5414c4a11f9e66c9a62c7854a05f495e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 22:20:44 +0000 Subject: [PATCH 1/3] feat: add a C language generator [minor] C is the first target with no classes, namespaces, overloading or generics, so CGenerator writes what C uses in their place rather than dropping them: - a type is a `typedef struct` keeping its tag - a member function is a free `Type_name(Type* self, ...)`, and a `const` member function takes a pointer to const - a constructor returns the value it built from a designated initialiser - an interface is a struct of function pointers taking an untyped receiver - a base type is the first member, which is what makes the two layout-compatible - an enumeration's members are qualified by its name, since a C enumeration is unscoped - a namespace is a comment over flat members: folding the name into the declarations would rename them without renaming the references to them The dialect is C99 plus C11's `_Static_assert`, so a pure function gets no `[[nodiscard]]` and an enumeration no fixed underlying type. What C cannot say - a member's initial value, a default argument, a deleted declaration - is written as a note rather than dropped. CGeneratedSourceCompilesTests compiles a generated header with a real C compiler, including it twice from one translation unit, because C's rules about linkage, empty parameter lists and what may initialise an object with static storage duration are not visible in the text. It is inconclusive where no compiler is on the path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7 --- CLAUDE.md | 23 +- Coder.Test/Ast/CompileTimeAssertionTests.cs | 19 +- Coder.Test/Editor/CoderEditorAppTests.cs | 2 +- Coder.Test/Editor/EditorWiringTests.cs | 2 +- .../Editor/GeneratedCodeHighlightingTests.cs | 2 +- .../CGeneratedSourceCompilesTests.cs | 236 ++++ Coder.Test/Languages/CGeneratorTests.cs | 603 +++++++++ .../Languages/GeneratedLineEndingTests.cs | 2 + .../ServiceCollectionExtensionsTests.cs | 3 +- Coder/Ast/CompileTimeAssertion.cs | 6 +- Coder/Ast/UnaryExpression.cs | 2 +- Coder/Languages/CGenerator.cs | 1189 +++++++++++++++++ Coder/ServiceCollectionExtensions.cs | 1 + README.md | 19 +- docs/design.md | 6 +- 15 files changed, 2090 insertions(+), 25 deletions(-) create mode 100644 Coder.Test/Languages/CGeneratedSourceCompilesTests.cs create mode 100644 Coder.Test/Languages/CGeneratorTests.cs create mode 100644 Coder/Languages/CGenerator.cs diff --git a/CLAUDE.md b/CLAUDE.md index e857732..170f3cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ dotnet run --project Coder.Editor ## Project Structure `ktsu.Coder` represents code as a language-agnostic AST, round-trips it through YAML, and generates -source in four target languages. The solution uses: +source in five target languages. The solution uses: - **ktsu.Sdk** — custom SDK providing shared build configuration - **MSTest.Sdk** — test project SDK with Microsoft Testing Platform @@ -78,17 +78,30 @@ source in four target languages. The solution uses: that it is an array, with no bound, because where the brackets go is the generator's business and C++ is the one language here that puts them on the declarator rather than the type. `IsConstant` is the intent rather than the keyword: C++ writes `inline constexpr` at namespace - scope and `static constexpr` inside a type, C# writes `static readonly`, and a language with no - spelling for it omits it the way it omits an indirection. + scope and `static constexpr` inside a type, C# writes `static readonly`, C writes `static const` + at file scope and a note inside a struct, having no static data member at all, and a language with + no spelling for it omits it the way it omits an indirection. - `Coder/Ast/CompileTimeAssertion.cs` — what a generated type promises that the type itself cannot say. Its `Condition` is text for the same reason `SourceFile.Imports` are: a compile-time predicate is language-specific in a way most of the AST is not, and there is no shared idea underneath - `std::is_trivially_copyable_v` to model. Only C++ has one; the others write a comment, because a - file that quietly loses a guarantee looks like one that still makes it. + `std::is_trivially_copyable_v` to model. Only C++ and C have one — `static_assert` and + `_Static_assert`, the second of which requires a message, so an assertion with none is given its + own condition; the others write a comment, because a file that quietly loses a guarantee looks + like one that still makes it. - `Coder/Languages/LanguageGeneratorBase.cs` — the emitters every generator shares. - `Coder/Languages/StandardLanguageGenerator.cs` — owns the node dispatch, so a derived generator supplies only the syntax its language does not share. `CSharpGenerator` deliberately does not derive from it. +- `Coder/Languages/CGenerator.cs` — the target with the least to map onto, and so the one whose + decisions are worth reading. C has no classes, namespaces, overloading or generics, so a type is a + `typedef struct`, a member function is a free function taking the instance, an interface is a + struct of function pointers, a base type is the first member (which is what makes the two + layout-compatible), and a namespace is a comment — folding its name into the declarations would + rename them without renaming the references to them. The dialect is C99 plus C11's + `_Static_assert`, which is why a pure function gets no `[[nodiscard]]` and an enumeration no fixed + underlying type. `Coder.Test/Languages/CGeneratedSourceCompilesTests.cs` compiles what it writes, + because C's rules about linkage, empty parameter lists and what may initialise an object with + static storage duration are not visible in the text. - `Coder.Graph/AstSchema.cs` — the uniform view of the AST's parent/child structure, hand-written rather than reflective. Adding a node type means adding it here. - `Coder.Graph/AstFields.cs` — a node's editable properties as named fields of a kind, which is what diff --git a/Coder.Test/Ast/CompileTimeAssertionTests.cs b/Coder.Test/Ast/CompileTimeAssertionTests.cs index 481cbd6..9514a20 100644 --- a/Coder.Test/Ast/CompileTimeAssertionTests.cs +++ b/Coder.Test/Ast/CompileTimeAssertionTests.cs @@ -13,9 +13,9 @@ namespace ktsu.Coder.Test.Ast; /// cannot say. /// /// -/// Only C++ has anything checked before the program runs, so this is the clearest case of the rule -/// the whole AST follows — say what is true, and let each language say as much of it as it can. The -/// other three write a comment rather than dropping it, because a file that quietly loses a +/// Only C++ and C have anything checked before the program runs, so this is the clearest case of the +/// rule the whole AST follows — say what is true, and let each language say as much of it as it can. +/// The other three write a comment rather than dropping it, because a file that quietly loses a /// guarantee looks exactly like one that still makes it. /// [TestClass] @@ -59,6 +59,19 @@ public void Cpp_EscapesTheMessage() Assert.Contains("\\\"T\\\"", new CppGenerator().Generate(assertion), StringComparison.Ordinal); } + /// + /// C writes it too, under C11's own spelling — and always with a message, which C11 requires and + /// which an assertion that carries none is given from its own condition. + /// + [TestMethod] + public void C_WritesTheAssertionWithAMessageEitherWay() + { + Assert.AreEqual( + "_Static_assert(sizeof(Handle) == 8,\n" + + " \"sizeof(Handle) == 8\");\n", + new CGenerator().Generate(new CompileTimeAssertion("sizeof(Handle) == 8")).ReplaceLineEndings("\n")); + } + /// /// The other three have nothing checked before the program runs, so they say what was asserted /// rather than dropping it. diff --git a/Coder.Test/Editor/CoderEditorAppTests.cs b/Coder.Test/Editor/CoderEditorAppTests.cs index 76dffcc..b489076 100644 --- a/Coder.Test/Editor/CoderEditorAppTests.cs +++ b/Coder.Test/Editor/CoderEditorAppTests.cs @@ -61,7 +61,7 @@ private static DocumentStore NewStore() => private string PathIn(string name) => Path.Combine(root, name + DocumentStore.Extension); private static CoderEditorApp NewApp(DocumentStore store, EditorSettings? settings = null) => - new(store, [new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new JavaScriptGenerator()], + new(store, [new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new CGenerator(), new JavaScriptGenerator()], settings ?? new EditorSettings()); /// diff --git a/Coder.Test/Editor/EditorWiringTests.cs b/Coder.Test/Editor/EditorWiringTests.cs index 8b58811..72a1b37 100644 --- a/Coder.Test/Editor/EditorWiringTests.cs +++ b/Coder.Test/Editor/EditorWiringTests.cs @@ -24,7 +24,7 @@ public sealed class EditorWiringTests { private const string ConfigHomeVariable = "XDG_CONFIG_HOME"; - private static readonly string[] ExpectedLanguageIds = ["python", "csharp", "javascript", "cpp"]; + private static readonly string[] ExpectedLanguageIds = ["python", "csharp", "javascript", "cpp", "c"]; private static readonly string[] ExpectedRecentFiles = ["/work/second.coder.yaml", "/work/first.coder.yaml"]; private string root = string.Empty; diff --git a/Coder.Test/Editor/GeneratedCodeHighlightingTests.cs b/Coder.Test/Editor/GeneratedCodeHighlightingTests.cs index b0eff9e..198e6ca 100644 --- a/Coder.Test/Editor/GeneratedCodeHighlightingTests.cs +++ b/Coder.Test/Editor/GeneratedCodeHighlightingTests.cs @@ -36,7 +36,7 @@ private static FunctionDeclaration SampleFunction() } private static ILanguageGenerator[] Generators() => - [new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new JavaScriptGenerator()]; + [new CSharpGenerator(), new PythonGenerator(), new CppGenerator(), new CGenerator(), new JavaScriptGenerator()]; /// /// Tests that every generator's language id is one the highlighter recognises, by generating real diff --git a/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs new file mode 100644 index 0000000..09a8b63 --- /dev/null +++ b/Coder.Test/Languages/CGeneratedSourceCompilesTests.cs @@ -0,0 +1,236 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using System.Diagnostics; +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Compiles what writes, with a real C compiler. +/// +/// +/// Every other test here pins a spelling, and a spelling can be pinned and still be wrong: C's rules +/// about what may initialise an object with static storage duration, what an empty parameter list +/// declares, and what a file-scope const links as are not visible in the text at all. The only +/// thing that knows them is a compiler. +/// +/// The header is included twice on purpose. That is what a header is for, and it is what makes +/// #pragma once and the static const spelling of a constant load-bearing rather than +/// stylistic. +/// +/// +/// The test is inconclusive rather than failing where no compiler is on the path, which is the +/// honest result: nothing was checked. A hosted Linux or macOS runner has one; a Windows one usually +/// does not, and the rest of the suite covers what the text says there. +/// +/// +[TestClass] +public class CGeneratedSourceCompilesTests +{ + /// + /// The compilers to look for, in the order a C project would. + /// + private static readonly string[] Compilers = ["cc", "gcc", "clang"]; + + /// + /// The consumer of the generated header, written the way a person would write one. + /// + /// + /// It uses every declaration the header makes, so a declaration that compiles on its own but + /// cannot be used — a constant that is not a constant expression, a function pointer whose + /// signature does not match what implements it — fails here rather than passing quietly. + /// + private const string Driver = """ + #include "exemplar.h" + #include "exemplar.h" + + static double circle_area(const void* self) + { + (void)self; + return 1.0; + } + + static void circle_draw(void* self, double scale) + { + (void)self; + (void)scale; + } + + int main(void) + { + Point built = Point_create(1, 2); + Point zeroed = Point_zero(); + Origin first = origins[0]; + Shape shape = { .draw = circle_draw, .area = circle_area }; + Colour colour = Colour_Green; + + shape.draw(&built, 2.0); + + return (built.x + zeroed.y + first.y + (int)shape.area(&first) + (int)colour) * 0; + } + + """; + + /// + /// Tests that a header holding every kind of declaration the generator writes compiles, and that + /// a translation unit including it twice compiles too. + /// + [TestMethod] + public void GeneratedHeader_Compiles() + { + string? compiler = FindCompiler(); + if (compiler is null) + { + Assert.Inconclusive("No C compiler on the path, so nothing was compiled."); + return; + } + + string directory = Path.Combine(Path.GetTempPath(), $"coder-c-{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + + try + { + File.WriteAllText( + Path.Combine(directory, "exemplar.h"), + new CGenerator().Generate(Exemplar())); + File.WriteAllText(Path.Combine(directory, "driver.c"), Driver); + + (int exitCode, string output) = Run( + compiler, + $"-std=c11 -Wall -Wextra -pedantic -c driver.c -o driver.o", + directory); + + Assert.AreEqual(0, exitCode, $"{compiler} rejected the generated header:{Environment.NewLine}{output}"); + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + /// + /// Builds a header holding one of everything the generator has a spelling for. + /// + /// The file to generate. + private static SourceFile Exemplar() + { + SourceFile file = new("exemplar") { IsHeader = true }; + file.HeaderComment.Add("Generated by Coder. Do not edit."); + file.Imports.Add(""); + + EnumDeclaration colour = new("Colour") { UnderlyingType = "int" }; + colour.Documentation.Add("What something is coloured."); + colour.Members.Add(new EnumMember("Red") { Value = "1" }); + colour.Members.Add(new EnumMember("Green")); + + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + point.Documentation.Add("Somewhere on a surface."); + point.Members.Add(new VariableDeclaration("x", "int")); + point.Members.Add(new VariableDeclaration("y", "int", new LiteralExpression(1))); + + FunctionDeclaration create = new("Point") { Kind = FunctionKind.Constructor }; + create.Parameters.Add(new Parameter("x", "int")); + create.Parameters.Add(new Parameter("y", "int") { IsOptional = true, DefaultValue = "0" }); + create.Initialisers.Add(new MemberInitialiser("x") { Value = new VariableReference("x") }); + create.Initialisers.Add(new MemberInitialiser("y") { Value = new VariableReference("y") }); + point.Members.Add(create); + + ConstructionExpression origin = new(new TypeReference("Point")); + origin.Arguments.Add(new MemberInitialiser("x") { Value = new LiteralExpression(0) }); + origin.Arguments.Add(new MemberInitialiser("y") { Value = new LiteralExpression(0) }); + + FunctionDeclaration zero = new("zero") { ReturnType = "Point", IsStatic = true, IsPure = true }; + zero.Body.Add(new ReturnStatement(origin)); + point.Members.Add(zero); + + ClassDeclaration shape = new("Shape") { Kind = TypeDeclarationKind.Interface }; + shape.Documentation.Add("What every shape can do."); + FunctionDeclaration draw = new("draw") { IsAbstract = true }; + draw.Parameters.Add(new Parameter("scale", "double")); + shape.Members.Add(draw); + shape.Members.Add(new FunctionDeclaration("area") + { + ReturnType = "double", + IsReadOnly = true, + IsAbstract = true, + }); + + UsingAlias origins = new() { Name = "Origin", AliasedType = "Point" }; + + ConstructionExpression table = new(type: null); + foreach (int offset in new[] { 0, 1 }) + { + ConstructionExpression row = new(new TypeReference("Point")); + row.Arguments.Add(new MemberInitialiser("x") { Value = new LiteralExpression(offset) }); + row.Arguments.Add(new MemberInitialiser("y") { Value = new LiteralExpression(offset) }); + table.Arguments.Add(row); + } + + FieldDeclaration origin_table = new() + { + Name = "origins", + Type = new TypeReference("Point") { IsArray = true, IsReadOnly = true }, + IsConstant = true, + InitialValue = table, + }; + origin_table.Documentation.Add("Where each shape starts."); + + file.Members.Add(colour); + file.Members.Add(point); + file.Members.Add(shape); + file.Members.Add(origins); + file.Members.Add(origin_table); + file.Members.Add(new CompileTimeAssertion + { + Condition = "sizeof(Point) == 2 * sizeof(int)", + Message = "Point must stay two ints", + }); + + return file; + } + + /// + /// Finds the first compiler on the path. + /// + /// Its name, or null when there is none. + private static string? FindCompiler() => + Compilers.FirstOrDefault(compiler => Run(compiler, "--version", Path.GetTempPath()).ExitCode == 0); + + /// + /// Runs a command, waiting for it to finish. + /// + /// The executable to run. + /// Its arguments. + /// Where to run it. + /// What it exited with, and everything it wrote. + private static (int ExitCode, string Output) Run(string command, string arguments, string workingDirectory) + { + ProcessStartInfo start = new(command, arguments) + { + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + + try + { + using Process? process = Process.Start(start); + if (process is null) + { + return (-1, string.Empty); + } + + string output = process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd(); + process.WaitForExit(); + return (process.ExitCode, output); + } + catch (System.ComponentModel.Win32Exception) + { + // The command is not on the path, which is the answer rather than a failure. + return (-1, string.Empty); + } + } +} diff --git a/Coder.Test/Languages/CGeneratorTests.cs b/Coder.Test/Languages/CGeneratorTests.cs new file mode 100644 index 0000000..fe6957d --- /dev/null +++ b/Coder.Test/Languages/CGeneratorTests.cs @@ -0,0 +1,603 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using ktsu.CodeBlocker; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for . +/// +/// +/// C is the one target with no classes, no namespaces, no overloading and no generics, so most of +/// what is pinned here is what it writes in their place — and that each of those substitutions is +/// the one C code written by hand uses, rather than a comment apologising for the language. +/// +[TestClass] +public class CGeneratorTests +{ + private CGenerator Generator { get; } = new(); + + private static string NewLine => CodeBlocker.DefaultNewLineString; + + /// + /// Tests that the generator reports the identity the registry and file writer rely on. + /// + [TestMethod] + public void Identity_IsC() + { + Assert.AreEqual("c", Generator.LanguageId); + Assert.AreEqual("C", Generator.DisplayName); + Assert.AreEqual("c", Generator.FileExtension); + } + + /// + /// Tests that a function with no parameters says so. An empty list in C declares a function whose + /// parameters are unspecified, which is the opposite of what the declaration means. + /// + [TestMethod] + public void EmptyFunction_TakesVoidAndReturnsVoid() + { + FunctionDeclaration function = new("doNothing"); + + string code = Generator.Generate(function); + + Assert.AreEqual($"void doNothing(void){NewLine}{{{NewLine}}}{NewLine}", code); + } + + /// + /// Tests that the AST's language-neutral type names are mapped to C spellings, in both the return + /// position and the parameter list. + /// + [TestMethod] + public void Types_AreMappedToCSpellings() + { + FunctionDeclaration function = new("greet") { ReturnType = "str" }; + function.Parameters.Add(new Parameter("name", "str")); + function.Parameters.Add(new Parameter("times", "long")); + + string code = Generator.Generate(function); + + StringAssert.Contains(code, "const char* greet(const char* name, long long times)", StringComparison.Ordinal); + } + + /// + /// Tests that an unrecognized type name is passed through, so a caller can name a real C type. + /// + [TestMethod] + public void UnknownType_IsPassedThrough() + { + FunctionDeclaration function = new("make") { ReturnType = "struct Widget" }; + + StringAssert.Contains(Generator.Generate(function), "struct Widget make(void)", StringComparison.Ordinal); + } + + /// + /// Tests that a list becomes a pointer to its element. C has no container type, and the length is + /// a second thing the program carries beside it. + /// + [TestMethod] + public void ListType_BecomesAPointerToItsElement() + { + FunctionDeclaration function = new("names") + { + ReturnType = new TypeReference("list") { TypeArguments = { new TypeReference("int") } }, + }; + + StringAssert.Contains(Generator.Generate(function), "int* names(void)", StringComparison.Ordinal); + } + + /// + /// Tests that a type's arguments are folded into its name, which is what the macro that generated + /// such a type in C would have called it. + /// + [TestMethod] + public void GenericType_FoldsItsArgumentsIntoTheName() + { + FunctionDeclaration function = new("first") + { + ReturnType = new TypeReference("Vector") { TypeArguments = { new TypeReference("int") } }, + }; + + StringAssert.Contains(Generator.Generate(function), "Vector_int first(void)", StringComparison.Ordinal); + } + + /// + /// Tests that a reference is a pointer. C has no references, and the one thing the distinction + /// carries cannot be said about a C parameter either way. + /// + [TestMethod] + public void Reference_IsAPointer() + { + FunctionDeclaration function = new("touch"); + function.Parameters.Add(new Parameter("value") + { + Type = new TypeReference("Widget") { Indirection = TypeIndirection.Reference }, + }); + + StringAssert.Contains(Generator.Generate(function), "void touch(Widget* value)", StringComparison.Ordinal); + } + + /// + /// Tests that a member function becomes a free function named for its type and taking the + /// instance as its first parameter, and that a member that promises not to modify what it is + /// called on receives a pointer to const. + /// + [TestMethod] + public void MemberFunction_TakesTheInstanceAsItsFirstParameter() + { + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + FunctionDeclaration translate = new("translate"); + translate.Parameters.Add(new Parameter("dx", "int")); + point.Members.Add(translate); + point.Members.Add(new FunctionDeclaration("length") { ReturnType = "double", IsReadOnly = true }); + + string code = Generator.Generate(point); + + StringAssert.Contains(code, "void Point_translate(Point* self, int dx)", StringComparison.Ordinal); + StringAssert.Contains(code, "double Point_length(const Point* self)", StringComparison.Ordinal); + } + + /// + /// Tests that a static member acts on no instance, so it takes none — and that a member function + /// with nothing else to take says void. + /// + [TestMethod] + public void StaticMember_TakesNoInstance() + { + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + point.Members.Add(new FunctionDeclaration("origin") { ReturnType = "Point", IsStatic = true }); + + StringAssert.Contains(Generator.Generate(point), "Point Point_origin(void)", StringComparison.Ordinal); + } + + /// + /// Tests that a constructor becomes a function returning the value it built, with the initialiser + /// list written as the designated initialiser C invented. + /// + [TestMethod] + public void Constructor_ReturnsTheValueItBuilt() + { + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + FunctionDeclaration create = new("Point") { Kind = FunctionKind.Constructor }; + create.Parameters.Add(new Parameter("x", "int")); + create.Initialisers.Add(new MemberInitialiser("x") { Value = new VariableReference("x") }); + point.Members.Add(create); + + string code = Generator.Generate(point); + + StringAssert.Contains(code, "Point Point_create(int x)", StringComparison.Ordinal); + StringAssert.Contains(code, " Point self = { .x = x };", StringComparison.Ordinal); + StringAssert.Contains(code, " return self;", StringComparison.Ordinal); + } + + /// + /// Tests that a constructor with nothing to initialise starts from a zeroed value, so every + /// member of what it returns has a value whether or not anybody named it. + /// + [TestMethod] + public void Constructor_WithNoInitialisers_StartsFromZero() + { + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + point.Members.Add(new FunctionDeclaration("Point") { Kind = FunctionKind.Constructor }); + + StringAssert.Contains(Generator.Generate(point), " Point self = {0};", StringComparison.Ordinal); + } + + /// + /// Tests that a destructor is a function acting on an instance, since C frees nothing by itself. + /// + [TestMethod] + public void Destructor_ActsOnTheInstance() + { + ClassDeclaration buffer = new("Buffer"); + buffer.Members.Add(new FunctionDeclaration("Buffer") { Kind = FunctionKind.Destructor }); + + StringAssert.Contains(Generator.Generate(buffer), "void Buffer_destroy(Buffer* self)", StringComparison.Ordinal); + } + + /// + /// Tests that an operator, which C cannot declare as one, is named by the word for what it does. + /// + [TestMethod] + public void Operator_IsNamedByItsWord() + { + ClassDeclaration vector = new("Vec") { Kind = TypeDeclarationKind.Struct }; + FunctionDeclaration plus = new("+") { Kind = FunctionKind.Operator, ReturnType = "Vec", IsReadOnly = true }; + plus.Parameters.Add(new Parameter("other", "Vec")); + vector.Members.Add(plus); + + StringAssert.Contains( + Generator.Generate(vector), + "Vec Vec_add(const Vec* self, Vec other)", + StringComparison.Ordinal); + } + + /// + /// Tests that a conversion is named for what it converts to, which is the only part of it C can + /// keep. + /// + [TestMethod] + public void ConversionOperator_IsNamedForItsTarget() + { + ClassDeclaration weight = new("Weight") { Kind = TypeDeclarationKind.Struct }; + weight.Members.Add(new FunctionDeclaration("ignored") + { + Kind = FunctionKind.ConversionOperator, + ReturnType = "double", + IsReadOnly = true, + }); + + StringAssert.Contains( + Generator.Generate(weight), + "double Weight_to_double(const Weight* self)", + StringComparison.Ordinal); + } + + /// + /// Tests that a private function takes internal linkage, which is the only privacy C has. + /// + [TestMethod] + public void PrivateFunction_IsStatic() + { + FunctionDeclaration function = new("helper") { Visibility = Visibility.Private }; + + StringAssert.StartsWith(Generator.Generate(function), "static void helper(void)", StringComparison.Ordinal); + } + + /// + /// Tests that a declaration with no definition — abstract, or one the language would have + /// supplied — becomes the prototype that is all C has to offer for either. + /// + [TestMethod] + public void DeclarationWithoutADefinition_IsAPrototype() + { + FunctionDeclaration abstractFunction = new("draw") { IsAbstract = true }; + FunctionDeclaration defaulted = new("copy") { Definition = FunctionDefinition.Defaulted }; + + Assert.AreEqual($"void draw(void);{NewLine}", Generator.Generate(abstractFunction)); + Assert.AreEqual($"void copy(void);{NewLine}", Generator.Generate(defaulted)); + } + + /// + /// Tests that a deleted declaration is a note rather than a prototype. C cannot refuse a call, so + /// writing the prototype would make legal exactly what the declaration exists to forbid. + /// + [TestMethod] + public void DeletedFunction_IsANoteRatherThanAPrototype() + { + ClassDeclaration handle = new("Handle") { Kind = TypeDeclarationKind.Struct }; + handle.Members.Add(new FunctionDeclaration("Handle") + { + Kind = FunctionKind.Constructor, + Definition = FunctionDefinition.Deleted, + }); + + string code = Generator.Generate(handle); + + StringAssert.Contains(code, "// Handle_create is deleted", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("Handle Handle_create", StringComparison.Ordinal)); + } + + /// + /// Tests that an interface becomes a struct of function pointers, each taking the instance as an + /// untyped pointer — which is how C dispatches dynamically, and what an interface is for. + /// + [TestMethod] + public void Interface_BecomesAStructOfFunctionPointers() + { + ClassDeclaration shape = new("Shape") { Kind = TypeDeclarationKind.Interface }; + FunctionDeclaration draw = new("draw") { IsAbstract = true }; + draw.Parameters.Add(new Parameter("scale", "double")); + shape.Members.Add(draw); + shape.Members.Add(new FunctionDeclaration("area") { ReturnType = "double", IsReadOnly = true, IsAbstract = true }); + + string code = Generator.Generate(shape); + + StringAssert.Contains(code, "typedef struct Shape", StringComparison.Ordinal); + StringAssert.Contains(code, " void (*draw)(void* self, double scale);", StringComparison.Ordinal); + StringAssert.Contains(code, " double (*area)(const void* self);", StringComparison.Ordinal); + StringAssert.Contains(code, "} Shape;", StringComparison.Ordinal); + } + + /// + /// Tests that a base type becomes the first member. A struct whose first member is another is + /// layout-compatible with it, so the position is what makes the substitution legal rather than + /// merely conventional. + /// + [TestMethod] + public void BaseType_BecomesTheFirstMember() + { + ClassDeclaration circle = new("Circle") { BaseType = "Shape" }; + circle.Members.Add(new VariableDeclaration("radius", "double")); + + string code = Generator.Generate(circle); + + int baseAt = code.IndexOf("Shape base;", StringComparison.Ordinal); + int radiusAt = code.IndexOf("double radius;", StringComparison.Ordinal); + + Assert.IsTrue(baseAt >= 0, "the base should be a member"); + Assert.IsTrue(baseAt < radiusAt, "the base should come first"); + } + + /// + /// Tests that a type is written as a typedef of a struct that also keeps its tag, so a caller + /// writes Point rather than struct Point and a struct can still point at its own + /// kind. + /// + [TestMethod] + public void Struct_IsTypedefedAndKeepsItsTag() + { + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + point.Members.Add(new VariableDeclaration("x", "int")); + + string code = Generator.Generate(point); + + Assert.AreEqual( + $"typedef struct Point{NewLine}{{{NewLine} int x;{NewLine}}} Point;{NewLine}", + code); + } + + /// + /// Tests that a member's initial value becomes a note. C has no default member initialisers, and + /// whoever writes the initialiser for the struct is the one who needs to know what it was. + /// + [TestMethod] + public void StructMember_Initialiser_IsANote() + { + ClassDeclaration point = new("Point") { Kind = TypeDeclarationKind.Struct }; + point.Members.Add(new VariableDeclaration("x", "int", new LiteralExpression(3))); + + StringAssert.Contains(Generator.Generate(point), "int x; // defaults to 3", StringComparison.Ordinal); + } + + /// + /// Tests that an enumeration is typedefed and its members qualified by its name. A C enumeration + /// is unscoped, so two enumerations with a None each would otherwise be one name declared + /// twice. + /// + [TestMethod] + public void Enum_IsTypedefedAndItsMembersQualified() + { + EnumDeclaration colour = new("Colour"); + colour.Members.Add(new EnumMember("Red") { Value = "1" }); + colour.Members.Add(new EnumMember("Green")); + + string code = Generator.Generate(colour); + + Assert.AreEqual( + $"typedef enum Colour{NewLine}{{{NewLine} Colour_Red = 1,{NewLine} Colour_Green,{NewLine}}} Colour;{NewLine}", + code); + } + + /// + /// Tests that a member already named for its enumeration is left alone, rather than qualified a + /// second time by a generator that cannot tell it has already been done. + /// + [TestMethod] + public void Enum_AlreadyQualifiedMember_IsNotQualifiedTwice() + { + EnumDeclaration colour = new("Colour"); + colour.Members.Add(new EnumMember("Colour_Red")); + + string code = Generator.Generate(colour); + + StringAssert.Contains(code, " Colour_Red,", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("Colour_Colour_Red", StringComparison.Ordinal)); + } + + /// + /// Tests that a fixed underlying type is written as a note rather than as syntax, since the + /// spelling that pins it is C23's and the guarantee is better asserted than assumed. + /// + [TestMethod] + public void Enum_UnderlyingType_IsANote() + { + EnumDeclaration colour = new("Colour") { UnderlyingType = "int" }; + colour.Members.Add(new EnumMember("Red")); + + string code = Generator.Generate(colour); + + StringAssert.StartsWith(code, "// underlying type int", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("enum Colour : int", StringComparison.Ordinal)); + } + + /// + /// Tests that a constant is static const. A file-scope const in C has external + /// linkage, so a header declaring one and included twice is the same object defined twice. + /// + [TestMethod] + public void ConstantField_IsStaticConst() + { + FieldDeclaration limit = new() + { + Name = "MaxItems", + Type = new TypeReference("int") { IsReadOnly = true }, + IsConstant = true, + InitialValue = new LiteralExpression(16), + }; + + string code = Generator.Generate(limit); + + Assert.AreEqual($"static const int MaxItems = 16;{NewLine}", code); + } + + /// + /// Tests that a constant table is written as an initialiser rather than as a compound literal. + /// Only the braced form is a constant expression, which is what an object with static storage + /// duration has to be initialised by. + /// + [TestMethod] + public void ConstantTable_IsAnInitialiserRatherThanACompoundLiteral() + { + ConstructionExpression rows = new(type: null); + ConstructionExpression row = new(new TypeReference("Point")); + row.Arguments.Add(new MemberInitialiser("x") { Value = new LiteralExpression(1) }); + rows.Arguments.Add(row); + + FieldDeclaration table = new() + { + Name = "origins", + Type = new TypeReference("Point") { IsArray = true }, + IsConstant = true, + InitialValue = rows, + }; + + string code = Generator.Generate(table); + + StringAssert.Contains(code, "static const Point origins[] = {", StringComparison.Ordinal); + StringAssert.Contains(code, " { .x = 1 },", StringComparison.Ordinal); + Assert.IsFalse(code.Contains("(Point){", StringComparison.Ordinal)); + } + + /// + /// Tests that a construction standing where a value is wanted keeps its type, since that is the + /// one position where C needs a compound literal to know what is being built. + /// + [TestMethod] + public void Construction_AsAValue_IsACompoundLiteral() + { + ConstructionExpression origin = new(new TypeReference("Point")); + origin.Arguments.Add(new MemberInitialiser("x") { Value = new LiteralExpression(1) }); + + FunctionDeclaration make = new("make") { ReturnType = "Point" }; + make.Body.Add(new ReturnStatement(origin)); + + StringAssert.Contains(Generator.Generate(make), "return (Point){ .x = 1 };", StringComparison.Ordinal); + } + + /// + /// Tests that an assertion uses C11's own spelling and always carries a message, since C11 + /// requires one and the condition is a better default than nothing. + /// + [TestMethod] + public void CompileTimeAssertion_UsesStaticAssertAndAlwaysHasAMessage() + { + CompileTimeAssertion withMessage = new() + { + Condition = "sizeof(Point) == 8", + Message = "Point must stay two ints", + }; + CompileTimeAssertion bare = new() { Condition = "sizeof(Point) == 8" }; + + StringAssert.Contains( + Generator.Generate(withMessage), + "_Static_assert(sizeof(Point) == 8,", + StringComparison.Ordinal); + StringAssert.Contains( + Generator.Generate(withMessage), + "\"Point must stay two ints\");", + StringComparison.Ordinal); + StringAssert.Contains(Generator.Generate(bare), "\"sizeof(Point) == 8\");", StringComparison.Ordinal); + } + + /// + /// Tests that an alias is a typedef, and that an alias for an array puts its brackets after the + /// name being declared rather than after the type. + /// + [TestMethod] + public void UsingAlias_IsATypedef() + { + UsingAlias alias = new() + { + Name = "Radii", + AliasedType = new TypeReference("double") { IsArray = true }, + }; + + Assert.AreEqual($"typedef double Radii[];{NewLine}", Generator.Generate(alias)); + } + + /// + /// Tests that a parameter's default value is written beside it as a comment. C has no default + /// arguments, so the caller has to pass one — and the value the declaration chose is what they + /// need in order to pass the same thing. + /// + [TestMethod] + public void OptionalParameter_KeepsItsDefaultAsAComment() + { + FunctionDeclaration function = new("repeat"); + function.Parameters.Add(new Parameter("times", "int") { IsOptional = true, DefaultValue = "1" }); + + StringAssert.Contains(Generator.Generate(function), "void repeat(int times /* = 1 */)", StringComparison.Ordinal); + } + + /// + /// Tests that the entry point is C's main, saying void where it takes nothing. + /// + [TestMethod] + public void EntryPoint_IsMain() + { + EntryPoint bare = new(); + EntryPoint withArguments = new() { AcceptsArguments = true }; + + StringAssert.Contains(Generator.Generate(bare), "int main(void)", StringComparison.Ordinal); + StringAssert.Contains(Generator.Generate(withArguments), "int main(int argc, char* argv[])", StringComparison.Ordinal); + } + + /// + /// Tests that a namespace is written as a comment over flat members. Folding the name into every + /// declaration would rename the declarations without renaming the references to them. + /// + [TestMethod] + public void Namespace_IsACommentOverFlatMembers() + { + NamespaceDeclaration ns = new("geo.shapes"); + ns.Members.Add(new FunctionDeclaration("area") { ReturnType = "double" }); + + string code = Generator.Generate(ns); + + StringAssert.StartsWith(code, "// namespace geo_shapes", StringComparison.Ordinal); + StringAssert.Contains(code, $"{NewLine}double area(void)", StringComparison.Ordinal); + } + + /// + /// Tests that a header says so once and writes its includes, keeping the delimiters the caller + /// chose and quoting a path that carries none. + /// + [TestMethod] + public void Header_WritesPragmaOnceAndItsIncludes() + { + SourceFile file = new("shape") { IsHeader = true }; + file.Imports.Add(""); + file.Imports.Add("geometry/vector.h"); + file.Members.Add(new FunctionDeclaration("area") { ReturnType = "double" }); + + string code = Generator.Generate(file); + + StringAssert.StartsWith(code, "#pragma once", StringComparison.Ordinal); + StringAssert.Contains(code, "#include ", StringComparison.Ordinal); + StringAssert.Contains(code, "#include \"geometry/vector.h\"", StringComparison.Ordinal); + } + + /// + /// Tests that a source file that is not a header writes no include guard, since nothing includes + /// it. + /// + [TestMethod] + public void NonHeader_WritesNoPragmaOnce() + { + SourceFile file = new("main"); + file.Members.Add(new EntryPoint()); + + Assert.IsFalse(Generator.Generate(file).Contains("#pragma once", StringComparison.Ordinal)); + } + + /// + /// Tests that a local declaration keeps the type it was given and that a constant one is + /// const — with no linkage to collide with, a local needs nothing more. + /// + [TestMethod] + public void LocalDeclaration_WritesItsTypeAndConst() + { + FunctionDeclaration function = new("run"); + function.Body.Add(new VariableDeclaration("limit", "int", new LiteralExpression(4)) { IsConstant = true }); + function.Body.Add(new VariableDeclaration("name", "str")); + + string code = Generator.Generate(function); + + StringAssert.Contains(code, " const int limit = 4;", StringComparison.Ordinal); + StringAssert.Contains(code, " const char* name;", StringComparison.Ordinal); + } +} diff --git a/Coder.Test/Languages/GeneratedLineEndingTests.cs b/Coder.Test/Languages/GeneratedLineEndingTests.cs index 268cc62..43506e0 100644 --- a/Coder.Test/Languages/GeneratedLineEndingTests.cs +++ b/Coder.Test/Languages/GeneratedLineEndingTests.cs @@ -45,6 +45,7 @@ private static FunctionDeclaration MultiLineFunction() [DataRow("python")] [DataRow("csharp")] [DataRow("cpp")] + [DataRow("c")] [DataRow("javascript")] public void Generators_EmitLineFeedsOnly(string languageId) { @@ -53,6 +54,7 @@ public void Generators_EmitLineFeedsOnly(string languageId) "python" => new PythonGenerator(), "csharp" => new CSharpGenerator(), "cpp" => new CppGenerator(), + "c" => new CGenerator(), "javascript" => new JavaScriptGenerator(), _ => throw new ArgumentOutOfRangeException(nameof(languageId)) }; diff --git a/Coder.Test/ServiceCollectionExtensionsTests.cs b/Coder.Test/ServiceCollectionExtensionsTests.cs index 2b4568d..3b7e610 100644 --- a/Coder.Test/ServiceCollectionExtensionsTests.cs +++ b/Coder.Test/ServiceCollectionExtensionsTests.cs @@ -58,8 +58,9 @@ public void AddLanguageGenerators_ShouldRegisterEveryImplementedLanguage() Assert.IsInstanceOfType(generators.FirstOrDefault(g => g.LanguageId == "csharp")); Assert.IsInstanceOfType(generators.FirstOrDefault(g => g.LanguageId == "javascript")); Assert.IsInstanceOfType(generators.FirstOrDefault(g => g.LanguageId == "cpp")); + Assert.IsInstanceOfType(generators.FirstOrDefault(g => g.LanguageId == "c")); - Assert.AreEqual(4, generators.Count, "A new generator needs a registration and an entry here"); + Assert.AreEqual(5, generators.Count, "A new generator needs a registration and an entry here"); } /// diff --git a/Coder/Ast/CompileTimeAssertion.cs b/Coder/Ast/CompileTimeAssertion.cs index 775a032..2f63ff6 100644 --- a/Coder/Ast/CompileTimeAssertion.cs +++ b/Coder/Ast/CompileTimeAssertion.cs @@ -18,8 +18,10 @@ namespace ktsu.Coder.Ast; /// written for the language the file is for. /// /// -/// Only C++ has this. Every other target here writes a comment saying what was asserted, because a -/// generated file that silently drops a guarantee looks like one that still makes it. +/// Only C++ and C have this — static_assert and _Static_assert, which differ in +/// spelling and in whether the message may be left out. Every other target here writes a comment +/// saying what was asserted, because a generated file that silently drops a guarantee looks like one +/// that still makes it. /// /// public class CompileTimeAssertion : AstNode diff --git a/Coder/Ast/UnaryExpression.cs b/Coder/Ast/UnaryExpression.cs index 56a117a..426281b 100644 --- a/Coder/Ast/UnaryExpression.cs +++ b/Coder/Ast/UnaryExpression.cs @@ -73,7 +73,7 @@ public override AstNode Clone() /// Defines the types of unary operators supported. /// /// -/// Every operator here exists in all four target languages, so no AST using them is untranslatable. +/// Every operator here exists in all five target languages, so no AST using them is untranslatable. /// Only the spelling of differs, in Python. /// public enum UnaryOperator diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs new file mode 100644 index 0000000..b924db9 --- /dev/null +++ b/Coder/Languages/CGenerator.cs @@ -0,0 +1,1189 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Languages; + +using System; +using System.Collections.Generic; +using System.Linq; +using ktsu.Coder.Ast; +using ktsu.CodeBlocker; + +/// +/// Generates C code from AST nodes. +/// +/// +/// C is the one target here with no classes, no namespaces, no overloading and no generics, so this +/// generator spells the AST in the conventions C uses in their place rather than pretending it has +/// them: a type is a typedef struct, a member function is a free function whose first +/// parameter is the instance, an interface is a struct of function pointers, and a namespace is a +/// comment. Each of those is what a C programmer writes by hand for the same declaration. +/// +/// The dialect is C99 with C11's _Static_assert — designated initialisers, compound literals +/// and // comments are C99, and _Static_assert is the only thing asked of C11. Nothing +/// needs C23, which is why [[nodiscard]] is not written for a pure function and a fixed +/// underlying type is not written on an enumeration: both would restrict the output to a dialect +/// most C is still not compiled as, and neither changes what the program means. +/// +/// +/// The AST's type names are the same language-neutral set the other generators consume (str, +/// int, bool, …), so they are mapped to C spellings; anything unrecognised is emitted +/// verbatim on the assumption the caller meant a C type. bool maps to bool rather than +/// to _Bool, so a file using one declares <stdbool.h> among its imports — which +/// is what is for, being the one part of the AST that is chosen per +/// language anyway. +/// +/// +public class CGenerator : StandardLanguageGenerator +{ + /// + /// What a declaration that never said what type it is gets. + /// + /// + /// A type is optional on every node that carries one, because a half-built AST is a thing the + /// editor has to be able to hold. C's most general type is an untyped pointer, which keeps the + /// output compiling while making it obvious which declaration was never finished. + /// + private const string UnknownTypeName = "object"; + + /// + /// The name a member function's first parameter is given. + /// + /// + /// C has no implicit receiver, so the instance is an ordinary parameter and needs an ordinary + /// name. self rather than this, which is a keyword in the languages a caller is + /// most likely to paste the generated header into. + /// + private const string SelfParameterName = "self"; + + /// + /// The name given to the member a derived struct holds its base in. + /// + /// + /// One name rather than the base type's, so the member a caller reaches the base through is the + /// same word whatever it is derived from — and so that renaming the base does not rename the + /// member. + /// + private const string BaseMemberName = "base"; + + /// + /// How many type declarations enclose what is being written. + /// + /// + /// A depth rather than a flag, so a type declared inside a type leaves the count right when it + /// closes. It decides what a field may say about itself: at file scope a constant is + /// static const, and inside a struct there is no such thing — C has neither static data + /// members nor default member initialisers. + /// + private int insideType; + + private static readonly Dictionary TypeMappings = new(StringComparer.OrdinalIgnoreCase) + { + { "str", "const char*" }, + { "string", "const char*" }, + { "int", "int" }, + { "long", "long long" }, + { "float", "float" }, + { "double", "double" }, + { "bool", "bool" }, + { "void", "void" }, + { "object", "void*" }, + { "dict", "void*" } + }; + + /// + /// The word each operator is named by, where C has to spell an operator as a function. + /// + /// + /// C has no operator overloading, so an operator declaration becomes an ordinary function and + /// needs an ordinary name. The words are the ones the standard library and most C APIs already + /// use for these operations, so Vector_add reads as what it is; a symbol with no word here + /// keeps the symbol, spelled out of the way of the identifier grammar by + /// . + /// + private static readonly Dictionary OperatorNames = new(StringComparer.Ordinal) + { + { "+", "add" }, + { "-", "subtract" }, + { "*", "multiply" }, + { "/", "divide" }, + { "%", "modulo" }, + { "==", "equals" }, + { "!=", "not_equals" }, + { "<", "less" }, + { "<=", "less_equal" }, + { ">", "greater" }, + { ">=", "greater_equal" }, + { "&&", "and" }, + { "||", "or" }, + { "!", "not" }, + { "&", "bit_and" }, + { "|", "bit_or" }, + { "^", "bit_xor" }, + { "~", "bit_not" }, + { "<<", "shift_left" }, + { ">>", "shift_right" }, + { "[]", "at" }, + { "()", "call" } + }; + + /// + /// Gets the unique identifier for this language generator. + /// + public override string LanguageId => "c"; + + /// + /// Gets the display name for this language generator. + /// + public override string DisplayName => "C"; + + /// + /// Gets the file extension (without the dot) used for this language. + /// + public override string FileExtension => "c"; + + /// + protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl, CodeBlocker code) => + GenerateFunction(funcDecl, code, null); + + /// + /// Emits a function, which may have been declared as a member of a type. + /// + /// The declaration to emit. + /// The writer to emit into. + /// The name of the type it belongs to, when it belongs to one. + /// + /// C has no member functions, so a member becomes a free function named for the type it belongs + /// to and taking the instance as its first parameter — Point_translate(Point* self, …). + /// That is what C code written by hand does, and it is why the type's name has to be passed in + /// rather than read off the declaration: the declaration does not know it. + /// + /// Nothing is written for , + /// , + /// , + /// , or + /// . The first three have no spelling before C23, and + /// the last three describe things C does not have — exceptions, converting constructors, and an + /// access control to be excepted from. + /// + /// + /// Nothing is written for either, and that one is not + /// a gap: C dispatches dynamically through a struct of function pointers, which is what an + /// interface becomes here, so a virtual member is either already reached through one or is an + /// ordinary function that happens to be overridable in a language that is not C. + /// + /// + private void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType) + { + Ensure.NotNull(funcDecl); + Ensure.NotNull(code); + + // A deleted declaration exists to make a call illegal, and C has no way to say that. Writing + // the prototype would do the opposite of what it asks for, so what is written is the reason + // the function is missing — which is what someone looking for it needs. + if (funcDecl.Definition == FunctionDefinition.Deleted) + { + GenerateDocumentation(funcDecl, code); + WriteInexpressible(code, $"{SpellFunctionName(funcDecl, enclosingType)} is deleted: C cannot refuse a call"); + return; + } + + GenerateDocumentation(funcDecl, code); + + // Internal linkage is the only privacy C has, and it is what a private function wants: the + // declaration is visible to this translation unit and to nothing else. + if (funcDecl.Visibility == Visibility.Private) + { + code.Write("static "); + } + + // A constructor is the only member function that returns the type rather than acting on an + // instance of it; a destructor returns nothing. Neither has a return type of its own to read. + code.Write($"{SpellReturnType(funcDecl, enclosingType)} "); + code.Write(SpellFunctionName(funcDecl, enclosingType)); + WriteParameterList(ParametersOf(funcDecl, enclosingType), code); + + // A declaration with no definition is a prototype, which is all C has to offer for either of + // them: an abstract declaration is one an implementation must supply, and a defaulted one is + // one the language would have supplied had it been C++. + if (funcDecl.IsAbstract || funcDecl.Definition == FunctionDefinition.Defaulted) + { + code.WriteLine(";"); + return; + } + + // The line is ended before the scope opens, so C's brace lands on its own line. + code.WriteLine(); + + using Scope body = new(code); + + if (funcDecl.Kind == FunctionKind.Constructor) + { + GenerateConstructorBody(funcDecl, code, enclosingType); + return; + } + + foreach (AstNode statement in funcDecl.Body) + { + GenerateInternal(statement, code); + } + } + + /// + /// Writes a constructor's body: build the value, run what the constructor asked for, hand it back. + /// + /// The declaration being emitted. + /// The writer to emit into. + /// The name of the type being constructed. + /// + /// C constructs nothing on its own, so a constructor is a function that returns a value it built. + /// The initialiser list becomes the designated initialiser it already is — which is the one place + /// C is the better fit, since C's designators need not be in declaration order and C++20's do — + /// and a constructor with no initialisers starts from {0}, so every member of the returned + /// value has a value whether or not anyone named it. + /// + private void GenerateConstructorBody(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType) + { + string typeName = enclosingType ?? funcDecl.Name ?? "UnnamedType"; + + code.Write($"{typeName} {SelfParameterName} = "); + + if (funcDecl.Initialisers.Count == 0) + { + // Not `{}`, which C only allows from C23. `{0}` is the spelling that zeroes a whole + // object in every dialect, whatever the first member's type is. + code.WriteLine("{0};"); + } + else + { + ConstructionExpression initialiser = new(type: null); + foreach (MemberInitialiser member in funcDecl.Initialisers) + { + initialiser.Arguments.Add(member); + } + + WriteInitialiser(initialiser, code); + code.WriteLine(";"); + } + + foreach (AstNode statement in funcDecl.Body) + { + GenerateInternal(statement, code); + } + + code.WriteLine($"return {SelfParameterName};"); + } + + /// + /// Spells what a function hands back. + /// + /// The declaration being emitted. + /// The name of the type it belongs to, when it belongs to one. + /// The return type as C writes it. + private static string SpellReturnType(FunctionDeclaration funcDecl, string? enclosingType) => funcDecl.Kind switch + { + FunctionKind.Constructor => enclosingType ?? funcDecl.Name ?? "void", + FunctionKind.Destructor => "void", + _ => MapToCType(funcDecl.ReturnType ?? new TypeReference("void")), + }; + + /// + /// Spells the name a declaration is written under. + /// + /// The declaration being emitted. + /// The name of the type it belongs to, when it belongs to one. + /// The name as C writes it. + /// + /// C has one namespace for functions and no overloading, so a member's name has to carry the + /// type's: two types with a reset each would otherwise be one function declared twice. + /// + private static string SpellFunctionName(FunctionDeclaration funcDecl, string? enclosingType) + { + string bare = funcDecl.Kind switch + { + FunctionKind.Constructor => "create", + FunctionKind.Destructor => "destroy", + FunctionKind.Operator => SpellOperatorName(funcDecl.Name), + FunctionKind.ConversionOperator => + $"to_{Identifier(MapToCType(funcDecl.ReturnType ?? new TypeReference("void")))}", + _ => funcDecl.Name ?? "unnamedFunction", + }; + + return enclosingType is null ? bare : $"{enclosingType}_{bare}"; + } + + /// + /// Names an operator, which C can only declare as a function. + /// + /// The operator's symbol, as the declaration carries it. + /// The function's name, without the type it belongs to. + private static string SpellOperatorName(string? symbol) + { + if (symbol is null) + { + return "operator"; + } + + return OperatorNames.TryGetValue(symbol, out string? word) + ? word + : $"operator_{Identifier(symbol)}"; + } + + /// + /// Turns text into something that can be part of a C identifier. + /// + /// The text to fold. + /// The text with everything C would not accept replaced by an underscore. + private static string Identifier(string text) + { + char[] folded = [.. text.Select(character => char.IsLetterOrDigit(character) ? character : '_')]; + return new string(folded).Trim('_'); + } + + /// + /// The parameters a function is written with, including the instance a member acts on. + /// + /// The declaration being emitted. + /// The name of the type it belongs to, when it belongs to one. + /// The full parameter list. + /// + /// A static member acts on no instance and a constructor has none to act on yet, so neither takes + /// one. — a member function that does not modify + /// what it is called on — becomes a pointer to const, which is exactly what it says and is + /// the only place C has to say it. + /// + private static List ParametersOf(FunctionDeclaration funcDecl, string? enclosingType) + { + List parameters = []; + + if (enclosingType is not null && !funcDecl.IsStatic && funcDecl.Kind != FunctionKind.Constructor) + { + parameters.Add(SelfParameter(enclosingType, funcDecl.IsReadOnly)); + } + + parameters.AddRange(funcDecl.Parameters); + return parameters; + } + + /// + /// Builds the parameter a member function receives its instance through. + /// + /// The type the function belongs to, or null for an interface's own. + /// Whether the function promises not to modify the instance. + /// The parameter. + private static Parameter SelfParameter(string? typeName, bool isReadOnly) => + new(SelfParameterName) + { + // An interface does not know what implements it, so its receiver is an untyped pointer. + Type = new TypeReference(typeName ?? "void") + { + Indirection = TypeIndirection.Pointer, + IsReadOnly = isReadOnly, + }, + }; + + /// + /// Writes a parenthesised parameter list, saying void where there are none. + /// + /// The parameters to write. + /// The writer to emit into. + /// + /// An empty list in C declares a function whose parameters are unspecified rather than one that + /// takes none, so a call passing three arguments to it is legal and unchecked. (void) is + /// the spelling that means what every other language here means by writing nothing. + /// + private void WriteParameterList(List parameters, CodeBlocker code) + { + code.Write("("); + + if (parameters.Count == 0) + { + code.Write("void"); + } + else + { + GenerateParameterList(parameters, code); + } + + code.Write(")"); + } + + /// + /// + /// #pragma once rather than an include guard, for the reason a guard cannot answer: the + /// macro it needs must be unique across the whole program, which the file cannot know it has and + /// which a generator picking one would eventually collide on. Every C compiler this targets + /// supports the pragma. + /// + protected override bool WriteFileDirectives(SourceFile file, CodeBlocker code) + { + Ensure.NotNull(file); + Ensure.NotNull(code); + + if (!file.IsHeader) + { + return false; + } + + code.WriteLine("#pragma once"); + return true; + } + + /// + /// + /// An import that already carries its own delimiters is written as it stands, because the choice + /// between <> and "" says where the compiler should look and only whoever + /// wrote the file knows that. One that carries neither is quoted, which is right for a path + /// within the project being generated. + /// + protected override string? SpellImport(string import) + { + Ensure.NotNull(import); + + bool delimited = (import.StartsWith('<') && import.EndsWith('>')) + || (import.StartsWith('"') && import.EndsWith('"')); + + return delimited ? $"#include {import}" : $"#include \"{import}\""; + } + + /// + /// + /// C has no namespaces and no way to add one, so the members are written flat and the name is + /// written above them as a comment. The alternative — folding the name into every declaration's + /// name, which is what a C library does by hand — would rename the declarations without renaming + /// the references to them elsewhere in the AST, and a generator that silently breaks the code it + /// writes is worse than one that says what the language cannot do. + /// + protected override void GenerateNamespaceDeclaration(NamespaceDeclaration namespaceDecl, CodeBlocker code) + { + Ensure.NotNull(namespaceDecl); + Ensure.NotNull(code); + + GenerateDocumentation(namespaceDecl, code); + + string name = string.Join("_", NamespaceDeclaration.Split(namespaceDecl.Name)); + WriteInexpressible(code, $"namespace {name}: C has none, so these declarations are not nested in one"); + code.NewLine(); + + bool first = true; + foreach (AstNode member in namespaceDecl.Members) + { + if (!first) + { + code.NewLine(); + } + + first = false; + GenerateInternal(member, code); + } + } + + /// + /// + /// Every kind of type declaration becomes a typedef struct, so a caller writes + /// Point rather than struct Point — the tag is kept as well as the alias, because a + /// struct that names itself is the only way to declare one that points at its own kind. + /// + /// A C struct holds data and nothing else, so the members are written in three groups rather than + /// in the order they were declared: the types a member might be declared with first, then the + /// struct, then the functions. Anything else would reference a type the compiler has not seen. + /// + /// + /// An interface is the exception, and is where C is more interesting than it looks: a set of + /// members an implementation supplies is a struct of function pointers, each taking the instance + /// as an untyped pointer. That is what every C library that dispatches dynamically does, and it + /// is a real translation of the declaration rather than a comment apologising for one. + /// + /// + /// becomes a first member holding the base. A struct + /// whose first member is another struct is layout-compatible with it, so a pointer to the derived + /// one may be used as a pointer to the base — which is what C has instead of inheritance, and why + /// the member has to come first rather than merely be present. + /// + /// + /// A data member's visibility is not written at all: C has no access control within a struct, and + /// its one privacy — internal linkage — is a property of a declaration rather than of a member of + /// one. A member function is a declaration once it is lifted out of the struct, so a private one + /// does take internal linkage; that is 's business rather than this + /// method's. + /// + /// + protected override void GenerateClassDeclaration(ClassDeclaration classDecl, CodeBlocker code) + { + Ensure.NotNull(classDecl); + Ensure.NotNull(code); + + string name = classDecl.Name ?? "UnnamedStruct"; + bool isInterface = classDecl.Kind == TypeDeclarationKind.Interface; + + List nestedTypes = [.. classDecl.Members.Where(IsTypeDeclaration)]; + List functions = isInterface + ? [] + : [.. classDecl.Members.OfType()]; + List fields = + [ + .. classDecl.Members.Where(member => + !IsTypeDeclaration(member) && (isInterface || member is not FunctionDeclaration)), + ]; + + foreach (AstNode nested in nestedTypes) + { + GenerateInternal(nested, code); + code.NewLine(); + } + + GenerateDocumentation(classDecl, code); + + code.WriteLine($"typedef struct {name}"); + code.WriteLine("{"); + code.Indent(); + + insideType++; + + if (classDecl.BaseType is TypeReference baseType) + { + // First, and said so: the position is what makes the two layout-compatible, and a + // reader moving it would have no way to know that from the declaration alone. + WriteInexpressible(code, "the base, first so that a pointer to this is a pointer to it"); + code.WriteLine($"{SpellDeclarator(baseType, BaseMemberName)};"); + + if (fields.Count > 0) + { + code.NewLine(); + } + } + + AstNode? previous = null; + foreach (AstNode member in fields) + { + if (previous is not null && NeedsSeparation(previous, member)) + { + code.NewLine(); + } + + previous = member; + + switch (member) + { + case FunctionDeclaration method: + GenerateFunctionPointer(method, code); + break; + + case VariableDeclaration field: + GenerateStructMember(field.Name, field.Type, field.InitialValue, field.IsConstant, code); + break; + + default: + GenerateInternal(member, code); + break; + } + } + + insideType--; + + code.Outdent(); + code.WriteLine($"}} {name};"); + + foreach (FunctionDeclaration function in functions) + { + code.NewLine(); + GenerateFunction(function, code, name); + } + } + + /// + /// Reports whether a member declares a type rather than data or behaviour. + /// + /// The member to test. + /// True when it declares a type. + private static bool IsTypeDeclaration(AstNode member) => + member is ClassDeclaration or EnumDeclaration or UsingAlias; + + /// + /// Emits one member of a struct. + /// + /// The member's name. + /// The member's type. + /// What the declaration said it starts at, if anything. + /// Whether the declaration said its value never changes. + /// The writer to emit into. + /// + /// C has no default member initialisers and no static data members, so neither an initial value + /// nor a constant survives as syntax — but both are written as a comment rather than dropped, + /// because whoever writes the initialiser for this struct is the one who needs to know them. + /// A const member is not written either: it would make the whole struct unassignable, + /// which is a much larger claim than the declaration made. + /// + private void GenerateStructMember(string? name, TypeReference? type, Expression? initialValue, bool isConstant, CodeBlocker code) + { + code.Write(SpellDeclarator(type ?? new TypeReference(UnknownTypeName), name ?? string.Empty)); + code.Write(";"); + + if (initialValue is not null || isConstant) + { + code.Write($" {CommentPrefix} {DescribeMemberIntent(initialValue, isConstant)}"); + } + + code.WriteLine(); + } + + /// + /// Says what a struct member asked for that C cannot write. + /// + /// What the declaration said it starts at, if anything. + /// Whether the declaration said its value never changes. + /// The note to write after the member. + private string DescribeMemberIntent(Expression? initialValue, bool isConstant) + { + string? value = initialValue is null ? null : GenerateExpression(initialValue); + + return (value, isConstant) switch + { + (not null, true) => $"constant, {value}", + (not null, false) => $"defaults to {value}", + _ => "constant", + }; + } + + /// + /// Emits an interface's member: a pointer to the function an implementation supplies. + /// + /// The declaration to emit. + /// The writer to emit into. + /// + /// The receiver is void* rather than the interface's own type, because what implements an + /// interface is not the interface: a pointer to the implementation is what the caller has, and a + /// struct of function pointers is what it is reached through. + /// + private void GenerateFunctionPointer(FunctionDeclaration funcDecl, CodeBlocker code) + { + GenerateDocumentation(funcDecl, code); + + List parameters = []; + if (!funcDecl.IsStatic) + { + parameters.Add(SelfParameter(null, funcDecl.IsReadOnly)); + } + + parameters.AddRange(funcDecl.Parameters); + + code.Write($"{MapToCType(funcDecl.ReturnType ?? new TypeReference("void"))} "); + code.Write($"(*{funcDecl.Name ?? "unnamedFunction"})"); + WriteParameterList(parameters, code); + code.WriteLine(";"); + } + + /// + /// + /// _Static_assert rather than static_assert: the underscored spelling is C11's own + /// and needs no header, while the other is a macro in <assert.h> that the file would + /// have to have asked for. C11 also requires a message, so an assertion with none is given its + /// own condition — which is what a reader wants anyway when nobody wrote a better one. + /// + protected override void GenerateCompileTimeAssertion(CompileTimeAssertion assertion, CodeBlocker code) + { + Ensure.NotNull(assertion); + Ensure.NotNull(code); + + string condition = assertion.Condition ?? "0"; + + code.Write($"_Static_assert({condition},"); + code.WriteLine(); + code.Indent(); + code.Write($"\"{EscapeString(assertion.Message ?? condition)}\""); + code.Outdent(); + code.Write(")"); + EndStatement(code); + } + + /// + /// + /// A typedef, which is what C has where another language has an alias — and it is written through + /// rather than by naming the type and then the name, because a + /// typedef declares a name the same way a variable declaration does and an alias for an array + /// puts its brackets after the name. + /// + protected override void GenerateUsingAlias(UsingAlias usingAlias, CodeBlocker code) + { + Ensure.NotNull(usingAlias); + Ensure.NotNull(code); + + GenerateDocumentation(usingAlias, code); + code.Write($"typedef {SpellDeclarator(usingAlias.AliasedType ?? new TypeReference(UnknownTypeName), usingAlias.Name ?? string.Empty)}"); + EndStatement(code); + } + + /// + /// + /// A compound literal — (Point){ .x = 1 } — which is C's way of writing a value of a named + /// type where one is wanted, and the reason a constructor can return the thing it built in one + /// statement. With no type it is the braced list alone, which is what initialises a declaration + /// that has already said its type. + /// + /// A among the arguments is a designated initialiser, which C + /// invented and does not require to be in declaration order. A list whose own elements are lists + /// is a table and is written one row per line; a list of plain values stays on one. + /// + /// + protected override void GenerateConstructionExpression(ConstructionExpression construction, CodeBlocker code) => + WriteBracedList(construction, code, asExpression: true); + + /// + /// Writes what a declaration starts at. + /// + /// The value the declaration was given. + /// The writer to emit into. + /// + /// A braced list, not a compound literal: C distinguishes the two by where they appear, and only + /// the braced form is a constant expression, which is what an object with static storage duration + /// has to be initialised by. Naming the type again would be redundant in the best case and + /// rejected at file scope in the ordinary one. + /// + private void WriteInitialiser(Expression initialValue, CodeBlocker code) + { + if (initialValue is ConstructionExpression construction) + { + WriteBracedList(construction, code, asExpression: false); + return; + } + + GenerateInternal(initialValue, code); + } + + /// + /// Writes a braced list, as either an initialiser or a value in its own right. + /// + /// The expression to write. + /// The writer to emit into. + /// + /// Whether the list stands where a value is wanted, and so needs its type in front of it. + /// + private void WriteBracedList(ConstructionExpression construction, CodeBlocker code, bool asExpression) + { + Ensure.NotNull(construction); + Ensure.NotNull(code); + + if (asExpression && construction.Type is not null) + { + code.Write($"({MapToCType(construction.Type)})"); + } + + if (construction.Arguments.Count == 0) + { + // Not `{}`, which C only allows from C23. + code.Write("{0}"); + return; + } + + if (SpansLines(construction)) + { + WriteStacked(construction, code); + return; + } + + code.Write("{ "); + for (int index = 0; index < construction.Arguments.Count; index++) + { + if (index > 0) + { + code.Write(", "); + } + + WriteArgument(construction.Arguments[index], code); + } + + code.Write(" }"); + } + + /// + /// Writes a braced list one element per line. + /// + /// The expression whose arguments to write. + /// The writer to emit into. + /// + /// A trailing comma after the last element, which C allows in a braced list and which keeps + /// adding a row to a generated table from touching the row above it in the diff. + /// + private void WriteStacked(ConstructionExpression construction, CodeBlocker code) + { + code.WriteLine("{"); + code.Indent(); + + foreach (AstNode argument in construction.Arguments) + { + WriteArgument(argument, code); + code.WriteLine(","); + } + + code.Outdent(); + code.Write("}"); + } + + /// + /// Writes one element of a braced list, which may name the member it is for. + /// + /// The element to write. + /// The writer to emit into. + /// + /// An element that is itself a braced list is written without its type. The enclosing list has + /// already said what each element is, so a row of a table is { .x = 1 } rather than + /// (Point){ .x = 1 } — and at file scope the second is not a constant expression. + /// + private void WriteArgument(AstNode argument, CodeBlocker code) + { + if (argument is MemberInitialiser designated) + { + code.Write($".{designated.Name} = "); + WriteInitialiser(designated.Value ?? new VariableReference(string.Empty), code); + return; + } + + if (argument is Expression element) + { + WriteInitialiser(element, code); + return; + } + + GenerateInternal(argument, code); + } + + /// + /// Reports whether a braced list is worth breaking across lines. + /// + /// The expression to judge. + /// when it should be written one element per line. + /// + /// A list of values is a value and belongs on one line; a list whose elements are themselves + /// lists is a table, and a table written on one line is a row of a diff nobody can read. + /// + private static bool SpansLines(ConstructionExpression construction) => + construction.Arguments.Any(argument => + argument is ConstructionExpression or MemberInitialiser { Value: ConstructionExpression }); + + /// + /// + /// A C enumeration is unscoped: its members are names in the scope around it, so two enumerations + /// with a None each would be one name declared twice. Prefixing each member with the + /// enumeration's name is what C code does instead, and it is the same thing C++'s enum + /// class does by requiring the name at the use site — the difference is only that C has to + /// spell it into the declaration. + /// + /// A fixed underlying type is written as a comment rather than as syntax. C says only that the + /// type is one capable of holding every member, and the spelling that pins it is C23's; a file + /// that needs the guarantee can assert it with a , which is a + /// thing the AST can already say. + /// + /// + protected override void GenerateEnumDeclaration(EnumDeclaration enumDecl, CodeBlocker code) + { + Ensure.NotNull(enumDecl); + Ensure.NotNull(code); + + string name = enumDecl.Name ?? "UnnamedEnum"; + + // Above the documentation rather than below it, so the comment block in front of the + // declaration stays one block rather than the note splitting it in two. + if (enumDecl.UnderlyingType is TypeReference underlying) + { + WriteInexpressible(code, $"underlying type {MapToCType(underlying)}: C chooses one that fits the members"); + } + + GenerateDocumentation(enumDecl, code); + + code.WriteLine($"typedef enum {name}"); + code.WriteLine("{"); + code.Indent(); + + foreach (EnumMember member in enumDecl.Members) + { + code.Write(SpellEnumMember(name, member.Name)); + + if (member.Value is not null) + { + code.Write($" = {member.Value}"); + } + + // A trailing comma on the last member too, so adding one after it is a one-line diff. + code.WriteLine(","); + } + + code.Outdent(); + code.WriteLine($"}} {name};"); + } + + /// + /// Spells an enumeration member's name, qualified by the enumeration it belongs to. + /// + /// The enumeration's name. + /// The member's name. + /// The name as C declares it. + /// + /// A member already named for its enumeration is left alone: Color_Red in a Color + /// is a caller who has already done this, and Color_Color_Red would be the generator + /// doing it twice. + /// + private static string SpellEnumMember(string enumName, string? memberName) + { + string bare = memberName ?? "Unnamed"; + return bare.StartsWith($"{enumName}_", StringComparison.Ordinal) ? bare : $"{enumName}_{bare}"; + } + + /// + /// + /// A constant is static const, not const: a file-scope const in C has + /// external linkage, so a header declaring one and included twice is the same object defined + /// twice and does not link. The static spelling gives each translation unit its own, + /// which is what a constant in a header means and what C has in place of C++'s inline. + /// + /// Not #define, although that is what much C does: a macro has no type and no scope, it + /// is not visible to a debugger, and it would substitute itself into every later use of the same + /// word anywhere in the translation unit — including ones that are not this constant at all. + /// + /// + /// A field with no initialiser is left bare rather than zeroed. An object with static storage + /// duration is zero-initialised by the standard, so writing it would say nothing the language + /// does not already promise. + /// + /// + protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlocker code) + { + Ensure.NotNull(field); + Ensure.NotNull(code); + + if (insideType > 0) + { + GenerateDocumentation(field, code); + GenerateStructMember(field.Name, field.Type, field.InitialValue, field.IsConstant, code); + return; + } + + GenerateDocumentation(field, code); + + TypeReference type = field.Type ?? new TypeReference(UnknownTypeName); + + if (field.IsConstant) + { + code.Write("static const "); + + // The keyword has just been written, so a type that also says const would say it twice + // — and `const const T` is not a type. + if (type.IsReadOnly) + { + type = type.Clone(); + type.IsReadOnly = false; + } + } + else if (field.IsStatic || field.Visibility == Visibility.Private) + { + code.Write("static "); + } + + code.Write(SpellDeclarator(type, field.Name ?? string.Empty)); + + if (field.InitialValue is not null) + { + code.Write(" = "); + WriteInitialiser(field.InitialValue, code); + } + + EndStatement(code); + } + + /// + /// + /// Two of a kind that say nothing about themselves stay together, which is what keeps a run of + /// typedefs, of struct members, or of assertions about one type reading as one block. + /// + protected override bool NeedsSeparation(AstNode previous, AstNode member) + { + Ensure.NotNull(previous); + Ensure.NotNull(member); + + return previous.GetType() != member.GetType() + || IsDocumented(previous) + || IsDocumented(member); + } + + /// + /// Reports whether a member carries documentation. + /// + /// The member to test. + /// True when it does. + private static bool IsDocumented(AstNode member) => + member is IHasDocumentation documented && documented.Documentation.Count > 0; + + /// + /// + /// C's main returns int whether or not the program means to hand back an exit code, + /// so changes nothing in the signature — a program that + /// returns nothing exits with zero, which C99 supplies by falling off the end. + /// + /// main(void), not main(). An empty parameter list in C declares a function whose + /// parameters are unspecified rather than one that takes none, which is the opposite of what the + /// entry point of a program with no arguments means. + /// + /// + protected override void GenerateEntryPoint(EntryPoint entryPoint, CodeBlocker code) + { + Ensure.NotNull(entryPoint); + Ensure.NotNull(code); + + code.Write("int main("); + code.Write(entryPoint.AcceptsArguments ? "int argc, char* argv[]" : "void"); + + // The line is ended before the scope opens, so C's brace lands on its own line. + code.WriteLine(")"); + + using Scope body = new(code); + foreach (AstNode statement in entryPoint.Body) + { + GenerateInternal(statement, code); + } + } + + /// + /// + /// A parameter's default value is written as a comment beside it. C has no default arguments, so + /// the caller has to pass one — and the value the declaration chose is exactly what they need to + /// know in order to pass the same thing. + /// + protected override void GenerateParameter(Parameter parameter, CodeBlocker code, int position) + { + Ensure.NotNull(parameter); + Ensure.NotNull(code); + + TypeReference type = parameter.Type ?? new TypeReference(UnknownTypeName); + + // An empty name means deliberately unnamed, which C allows in a prototype. A null name means + // nobody said, so one is invented. + string name = parameter.Name is "" ? string.Empty : parameter.Name ?? $"param{position}"; + + code.Write(name.Length == 0 ? MapToCType(type) : SpellDeclarator(type, name)); + + if (parameter.IsOptional && !string.IsNullOrEmpty(parameter.DefaultValue)) + { + code.Write($" /* = {parameter.DefaultValue} */"); + } + } + + /// + /// + /// A constant local is const, which is all it needs to be: unlike a file-scope one it has + /// no linkage to collide with. There is no auto to fall back on — C's is a storage class, + /// not a deduced type — so an inferred declaration is written with the type it was given, or an + /// untyped pointer when it was given none. + /// + protected override void GenerateVariableDeclaration(VariableDeclaration varDecl, CodeBlocker code) + { + Ensure.NotNull(varDecl); + Ensure.NotNull(code); + + if (varDecl.IsConstant) + { + code.Write("const "); + } + + code.Write(SpellDeclarator(varDecl.Type ?? new TypeReference(UnknownTypeName), varDecl.Name)); + + if (varDecl.InitialValue is not null) + { + code.Write(" = "); + WriteInitialiser(varDecl.InitialValue, code); + } + + EndStatement(code); + } + + /// + /// Writes an expression to a string, for the places a comment has to quote one. + /// + /// The expression to write. + /// Its C source. + private string GenerateExpression(Expression expression) + { + using CodeBlocker inline = CodeBlocker.Create(IndentString); + GenerateInternal(expression, inline); + return inline.ToString().TrimEnd('\r', '\n'); + } + + /// + /// Spells a type in C. + /// + /// The type to spell. + /// The C source for it. + /// + /// A reference and a pointer are both *. C has no references, and the one thing the + /// distinction carries — that a reference is never null — is not something C can say about a + /// parameter either way. + /// + /// C has no generic types, so a type's arguments are folded into its name: + /// Vector<int> is Vector_int, which is what the macro that generated such a + /// type would have called it. A list is the one the AST names without C having it, and it + /// becomes a pointer to its element — the count travels separately, because in C it always does. + /// + /// + private static string MapToCType(TypeReference type) + { + string spelled = SpellTypeName(type); + + string indirection = type.Indirection is TypeIndirection.Reference or TypeIndirection.Pointer + ? "*" + : string.Empty; + + // A mapped spelling that is already const — `str` is `const char*` — is left alone rather + // than written `const const char*`, which is not a type. + string qualifier = type.IsReadOnly && !spelled.StartsWith("const ", StringComparison.Ordinal) + ? "const " + : string.Empty; + + return $"{qualifier}{spelled}{indirection}{(type.IsArray ? "[]" : string.Empty)}"; + } + + /// + /// Spells a type's name, arguments and all. + /// + /// The type whose name to spell. + /// The name as C writes it. + private static string SpellTypeName(TypeReference type) + { + // A list is a pointer to its elements, which is what C has: there is no container type, and + // the length is a second thing the program carries beside it. + if (string.Equals(type.Name, "list", StringComparison.OrdinalIgnoreCase)) + { + return type.TypeArguments.Count == 1 + ? $"{SpellTypeName(type.TypeArguments[0])}*" + : "void*"; + } + + if (TypeMappings.TryGetValue(type.Name, out string? mapped)) + { + return mapped; + } + + return type.TypeArguments.Count == 0 + ? type.Name + : $"{type.Name}_{string.Join("_", type.TypeArguments.Select(argument => Identifier(SpellTypeName(argument))))}"; + } + + /// + /// Spells a declaration of with that type. + /// + /// The declared type. + /// The name being declared. + /// The declaration, without an initialiser or a terminator. + /// + /// C puts an array's brackets on the declarator rather than on the type — T name[], never + /// T[] name — so a declaration cannot be built by writing the type and the name in that + /// order, which is what a language with a whole type on the left does. This is the one place that + /// difference lives. + /// + private static string SpellDeclarator(TypeReference type, string name) + { + TypeReference element = type.IsArray ? type.Clone() : type; + if (type.IsArray) + { + element.IsArray = false; + } + + return $"{MapToCType(element)} {name}{(type.IsArray ? "[]" : string.Empty)}"; + } +} diff --git a/Coder/ServiceCollectionExtensions.cs b/Coder/ServiceCollectionExtensions.cs index f2a1e3a..0bc6551 100644 --- a/Coder/ServiceCollectionExtensions.cs +++ b/Coder/ServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ public static IServiceCollection AddLanguageGenerators(this IServiceCollection s services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/README.md b/README.md index f5a7330..6490c26 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This makes it ideal for code generation tools, transpilers, and any application - **LiteralExpression**: Typed literals (string, int, bool, double) - **AstLeafNode**: Generic leaf nodes for literals (strings, numbers, booleans) -Every operator in `UnaryOperator` and `BinaryOperator` exists in all four target languages, so no +Every operator in `UnaryOperator` and `BinaryOperator` exists in all five target languages, so no AST built from them is untranslatable. Only the spelling varies — Python's `not`/`and`/`or`, JavaScript's strict `===`/`!==` — and each generator overrides just the operators it spells differently. @@ -65,20 +65,24 @@ rather than the modifier's text because no two languages spell visibility the sa |---|---| | C# | The keyword, in front of the declaration; a class or function with none is `public` | | C++ | An access label (`public:`, `protected:`, `private:`) the members are grouped under; `Internal` becomes `public:` | +| C | Nothing inside a struct, which has no access control; a `Private` declaration at file scope is `static`, which is the internal linkage C has instead | | JavaScript | A private class member takes the `#` prefix, which is JavaScript's own private syntax; nothing for the rest | | Python | Nothing — Python has no access modifiers, and its leading-underscore convention renames the declaration rather than modifying it | ### Constants and entry points A `VariableDeclaration` marked `IsConstant` with a literal `InitialValue` is a constant. C# writes -`const`, C++ writes `const` for a local and `static constexpr` for a class member, JavaScript writes -`const` for a local and `static` for a class member, and Python writes a plain assignment, having no -constant declaration to spell. +`const`, C++ writes `const` for a local and `static constexpr` for a class member, C writes `const` +for a local and `static const` at file scope — a file-scope `const` in C has external linkage, so a +header declaring one and included twice would not link — JavaScript writes `const` for a local and +`static` for a class member, and Python writes a plain assignment, having no constant declaration to +spell. An `EntryPoint` holds the statements a program runs. Each generator writes the spelling its language -looks for: C#'s `static Main`, C++'s free `int main`, Python's `main` with the `__main__` guard that -calls it (and the `import sys` its arguments and exit code need), and JavaScript's `main` with the -call that runs it. +looks for: C#'s `static Main`, C++'s free `int main`, C's `int main(void)` — an empty parameter list +in C declares a function whose parameters are unspecified rather than one that takes none — Python's +`main` with the `__main__` guard that calls it (and the `import sys` its arguments and exit code +need), and JavaScript's `main` with the call that runs it. ### Visual graph editor @@ -170,6 +174,7 @@ directly. | `csharp` | `CSharpGenerator` | `cs` | Mapped type names, `var` for inferred declarations, visibility keywords, `const`, `static Main` | | `javascript` | `JavaScriptGenerator` | `js` | Untyped; `const`/`let`; strict `===` and `!==`; method, `static` and `#private` syntax inside a class | | `cpp` | `CppGenerator` | `cpp` | Mapped type spellings (`str` → `std::string`); `auto` for inferred declarations; access labels, `static constexpr` members and a terminating `;` on a class | +| `c` | `CGenerator` | `c` | `typedef struct` for every kind of type; a member function is a free `Type_name(Type* self, …)`; an interface is a struct of function pointers; a base type is the first member; enumeration members are qualified by their enumeration; `_Static_assert`, `main(void)`, and `static const` for a constant | ## Installation diff --git a/docs/design.md b/docs/design.md index d60cae7..0cf6a13 100644 --- a/docs/design.md +++ b/docs/design.md @@ -62,7 +62,7 @@ functionDeclaration: * Base implementation (`LanguageGeneratorBase`) provides common functionality. * Four generators, each with proper indentation and language-appropriate type handling: Python (type hints), C#, JavaScript (untyped, strict equality), and C++ (mapped type spellings). - * Dependency injection configuration (`ServiceCollectionExtensions`) registers all four. + * Dependency injection configuration (`ServiceCollectionExtensions`) registers all five. 4. **Applications** ✅ **IMPLEMENTED**: @@ -228,7 +228,7 @@ string pythonCode = pythonGenerator.Generate(astLoaded); 3. **Expression system** — function calls (binary operators are implemented) ### Medium Priority -4. **Further language generators** beyond the four that exist +4. **Further language generators** beyond the five that exist 5. **Error handling and validation** improvements 6. **Performance optimization** and benchmarking @@ -239,6 +239,6 @@ string pythonCode = pythonGenerator.Generate(astLoaded); ## Conclusion -This .NET-based design has successfully implemented a robust, flexible foundation for AST-based code generation. The core infrastructure adheres to SOLID principles and provides working serialization and code generation for four target languages. The CLI and TUI applications demonstrate practical usage. +This .NET-based design has successfully implemented a robust, flexible foundation for AST-based code generation. The core infrastructure adheres to SOLID principles and provides working serialization and code generation for five target languages. The CLI and TUI applications demonstrate practical usage. The next development phase should focus on completing the dependency injection infrastructure and expanding the AST node types to support more complex code structures. From c3a9871c727b7fc1c62c37b1ae0a7f81d0208d39 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 22:40:52 +0000 Subject: [PATCH 2/3] refactor: single-source what the C and C++ generators share SonarCloud's quality gate failed the PR on duplication: 199 of the new lines in CGenerator were duplicates of CppGenerator, in three blocks. CFamilyGenerator now sits between the two and StandardLanguageGenerator and owns what they share, all of which is about C rather than about the AST: `#pragma once` and `#include`, the braced list and its designated initialisers, the declarator that puts an array's brackets after the name, the member-grouping rule, and the dispatch from a bare function declaration into the emitter that knows which type it belongs to. A generator supplies `SpellType` and, where it differs, how a value inside a list is written - which is the one thing C needs, since only there may it leave the type out. The type mappings stay with each generator: `str` is a `std::string` in one language and a `const char*` in the other, and the whole of what a mapping is is the spelling. C's operator names are now derived from the AST's own operator vocabulary rather than listed beside it, so an operator added to `BinaryOperator` is named without anybody remembering to, and the names cannot drift from the symbols `OperatorSymbols` spells. No output changes: all 534 tests pass, including the ones that pin C++'s generated source exactly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7 --- CLAUDE.md | 6 + Coder/Languages/CFamilyGenerator.cs | 268 ++++++++++++++++++++++++ Coder/Languages/CGenerator.cs | 310 ++++++++-------------------- Coder/Languages/CppGenerator.cs | 195 ++--------------- 4 files changed, 371 insertions(+), 408 deletions(-) create mode 100644 Coder/Languages/CFamilyGenerator.cs diff --git a/CLAUDE.md b/CLAUDE.md index 170f3cd..1a838ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,6 +92,12 @@ source in five target languages. The solution uses: - `Coder/Languages/StandardLanguageGenerator.cs` — owns the node dispatch, so a derived generator supplies only the syntax its language does not share. `CSharpGenerator` deliberately does not derive from it. +- `Coder/Languages/CFamilyGenerator.cs` — what C and C++ share beyond what every generator shares, + and all of it is about C: the preprocessor (`#pragma once`, `#include`), the braced list with its + designated initialisers, the declarator that puts an array's brackets after the name, and the rule + that two undocumented members of a kind stay in one block. The type mappings deliberately stay + with each generator, because `str` is a `std::string` in one language and a `const char*` in the + other and the whole of what a mapping is is the spelling. - `Coder/Languages/CGenerator.cs` — the target with the least to map onto, and so the one whose decisions are worth reading. C has no classes, namespaces, overloading or generics, so a type is a `typedef struct`, a member function is a free function taking the instance, an interface is a diff --git a/Coder/Languages/CFamilyGenerator.cs b/Coder/Languages/CFamilyGenerator.cs new file mode 100644 index 0000000..965c4f0 --- /dev/null +++ b/Coder/Languages/CFamilyGenerator.cs @@ -0,0 +1,268 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Languages; + +using System.Linq; +using ktsu.Coder.Ast; +using ktsu.CodeBlocker; + +/// +/// A for the two targets that are the same language +/// underneath: what C and C++ share beyond what every generator shares. +/// +/// +/// owns what is common to every target, and most of it is +/// about the shape of the AST rather than about any language. This class owns what is common to +/// these two in particular, and all of it is about C: the preprocessor, the braced list, and the +/// declarator syntax that puts an array's brackets after the name rather than after the type. +/// +/// Python and JavaScript are not C-family in any of those ways — neither has a preprocessor, an +/// array declarator or a designated initialiser — so this sits between them and their common base +/// rather than in it. CSharpGenerator is C-family in its syntax and is not here either, +/// because it does not derive from at all. +/// +/// +/// Two things stay with each generator that might look shareable and are not. The type mappings are +/// one, because str is a std::string in one language and a const char* in the +/// other, and the whole of what a mapping is is the spelling. Documentation comments are the other: +/// both write ///, which they inherit rather than agree on. +/// +/// +public abstract class CFamilyGenerator : StandardLanguageGenerator +{ + /// + /// + /// A function declared on its own belongs to no type, which is what the null says. A member is + /// reached through directly, by whichever emitter knows the name + /// of the type it belongs to — the declaration does not. + /// + protected sealed override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl, CodeBlocker code) => + GenerateFunction(funcDecl, code, null); + + /// + /// Emits a function, which may have been declared as a member of a type. + /// + /// The declaration to emit. + /// The writer to emit into. + /// The name of the type it belongs to, when it belongs to one. + /// + /// The type's name is a parameter rather than something read off the declaration because the + /// declaration does not carry it, and because both languages need it for a name they cannot + /// otherwise spell — C++'s constructor and destructor are named after the type, and C's every + /// member function is. + /// + protected abstract void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType); + + /// + /// Spells a type in the target language. + /// + /// The type to spell. + /// The source for it. + /// + /// The one thing the two languages disagree about everywhere, and the hook everything here that + /// needs a type name goes through. + /// + protected abstract string SpellType(TypeReference type); + + /// + /// + /// #pragma once rather than an include guard, for the reason a guard cannot answer: the + /// macro it needs must be unique across the whole program, which the file cannot know it has and + /// which a generator picking one would eventually collide on. Every compiler either language + /// targets supports the pragma. + /// + protected override bool WriteFileDirectives(SourceFile file, CodeBlocker code) + { + Ensure.NotNull(file); + Ensure.NotNull(code); + + if (!file.IsHeader) + { + return false; + } + + code.WriteLine("#pragma once"); + return true; + } + + /// + /// + /// An import that already carries its own delimiters is written as it stands, because the choice + /// between <> and "" says where the compiler should look and only whoever + /// wrote the file knows that. One that carries neither is quoted, which is right for a path + /// within the project being generated. + /// + protected override string? SpellImport(string import) + { + Ensure.NotNull(import); + + bool delimited = (import.StartsWith('<') && import.EndsWith('>')) + || (import.StartsWith('"') && import.EndsWith('"')); + + return delimited ? $"#include {import}" : $"#include \"{import}\""; + } + + /// + /// + /// Two of a kind that say nothing about themselves stay together, which is what keeps a run of + /// aliases, of defaulted declarations, or of assertions about one type reading as one block + /// rather than as four paragraphs. A documented member needs air above it or its first comment + /// line butts against the member before it and reads as belonging to that one. + /// + protected override bool NeedsSeparation(AstNode previous, AstNode member) + { + Ensure.NotNull(previous); + Ensure.NotNull(member); + + return previous.GetType() != member.GetType() + || IsDocumented(previous) + || IsDocumented(member); + } + + /// + /// Reports whether a member carries documentation. + /// + /// The member to test. + /// True when it does. + protected static bool IsDocumented(AstNode member) => + member is IHasDocumentation documented && documented.Documentation.Count > 0; + + /// + /// Spells a declaration of with that type. + /// + /// The declared type. + /// The name being declared. + /// The declaration, without an initialiser or a terminator. + /// + /// Both languages put an array's brackets on the declarator rather than on the type — + /// T name[], never T[] name — so a declaration cannot be built by writing the type + /// and the name in that order, which is what every other language here does. This is the one + /// place that difference lives. + /// + protected string SpellDeclarator(TypeReference type, string name) + { + Ensure.NotNull(type); + + TypeReference element = type.IsArray ? type.Clone() : type; + if (type.IsArray) + { + element.IsArray = false; + } + + return $"{SpellType(element)} {name}{(type.IsArray ? "[]" : string.Empty)}"; + } + + /// + /// Writes a braced list, without whatever the language writes in front of it. + /// + /// The expression whose arguments to write. + /// The writer to emit into. + /// What to write when there are no arguments at all. + /// + /// The empty list is the caller's because it is the one part the two languages spell + /// differently: {} is C++'s, and C only allows it from C23. + /// + /// A list of values is a value and belongs on one line; a list whose elements are themselves + /// lists is a table, and a table written on one line is a row of a diff nobody can read. The test + /// is the shape of the data rather than a column count, because a generated file has no idea how + /// wide anyone's editor is and a rule about that would have to be guessed. + /// + /// + protected void WriteBracedList(ConstructionExpression construction, CodeBlocker code, string emptyList) + { + Ensure.NotNull(construction); + Ensure.NotNull(code); + + if (construction.Arguments.Count == 0) + { + code.Write(emptyList); + return; + } + + if (SpansLines(construction)) + { + WriteStackedList(construction, code); + return; + } + + code.Write("{ "); + for (int index = 0; index < construction.Arguments.Count; index++) + { + if (index > 0) + { + code.Write(", "); + } + + WriteListElement(construction.Arguments[index], code); + } + + code.Write(" }"); + } + + /// + /// Writes a braced list one element per line. + /// + /// The expression whose arguments to write. + /// The writer to emit into. + /// + /// A trailing comma after the last element, which both languages allow in a braced list and which + /// keeps adding a row to a generated table from touching the row above it in the diff. + /// + private void WriteStackedList(ConstructionExpression construction, CodeBlocker code) + { + code.WriteLine("{"); + code.Indent(); + + foreach (AstNode argument in construction.Arguments) + { + WriteListElement(argument, code); + code.WriteLine(","); + } + + code.Outdent(); + code.Write("}"); + } + + /// + /// Writes one element of a braced list, which may name the member it is for. + /// + /// The element to write. + /// The writer to emit into. + /// + /// A is a designated initialiser, spelled the same in both + /// languages — C invented it and C++20 adopted it, with the one difference that C++ requires the + /// designators to appear in declaration order. That is the caller's business: the generator + /// writes the order it is given. + /// + private void WriteListElement(AstNode argument, CodeBlocker code) + { + if (argument is MemberInitialiser designated) + { + code.Write($".{designated.Name} = "); + WriteListValue(designated.Value ?? new VariableReference(string.Empty), code); + return; + } + + WriteListValue(argument, code); + } + + /// + /// Writes what one element of a braced list is. + /// + /// The value to write. + /// The writer to emit into. + /// + /// Ordinary generation, unless a language has something to say about a value that stands inside + /// a list rather than on its own — which C does, since only there may it leave the type out. + /// + protected virtual void WriteListValue(AstNode value, CodeBlocker code) => GenerateInternal(value, code); + + /// + /// Reports whether a braced list is worth breaking across lines. + /// + /// The expression to judge. + /// when it should be written one element per line. + private static bool SpansLines(ConstructionExpression construction) => + construction.Arguments.Any(argument => + argument is ConstructionExpression or MemberInitialiser { Value: ConstructionExpression }); +} diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs index b924db9..f10ff75 100644 --- a/Coder/Languages/CGenerator.cs +++ b/Coder/Languages/CGenerator.cs @@ -33,7 +33,7 @@ namespace ktsu.Coder.Languages; /// language anyway. /// /// -public class CGenerator : StandardLanguageGenerator +public class CGenerator : CFamilyGenerator { /// /// What a declaration that never said what type it is gets. @@ -65,17 +65,6 @@ public class CGenerator : StandardLanguageGenerator /// private const string BaseMemberName = "base"; - /// - /// How many type declarations enclose what is being written. - /// - /// - /// A depth rather than a flag, so a type declared inside a type leaves the count right when it - /// closes. It decides what a field may say about itself: at file scope a constant is - /// static const, and inside a struct there is no such thing — C has neither static data - /// members nor default member initialisers. - /// - private int insideType; - private static readonly Dictionary TypeMappings = new(StringComparer.OrdinalIgnoreCase) { { "str", "const char*" }, @@ -91,40 +80,73 @@ public class CGenerator : StandardLanguageGenerator }; /// - /// The word each operator is named by, where C has to spell an operator as a function. + /// How many type declarations enclose what is being written. + /// + /// + /// A depth rather than a flag, so a type declared inside a type leaves the count right when it + /// closes. It decides what a field may say about itself: at file scope a constant is + /// static const, and inside a struct there is no such thing — C has neither static data + /// members nor default member initialisers. + /// + private int insideType; + + /// + /// The word each operator symbol is named by, where C has to spell an operator as a function. /// /// /// C has no operator overloading, so an operator declaration becomes an ordinary function and - /// needs an ordinary name. The words are the ones the standard library and most C APIs already - /// use for these operations, so Vector_add reads as what it is; a symbol with no word here - /// keeps the symbol, spelled out of the way of the identifier grammar by - /// . + /// needs an ordinary name. The names are the AST's own — is + /// add and is less_than_or_equal — + /// derived from the vocabulary rather than listed beside it, so an operator added to the AST is + /// named here without anybody remembering to. A symbol the AST does not have keeps the symbol, + /// spelled out of the way of the identifier grammar by . + /// + /// A symbol that is both a binary and a unary operator — - is subtraction and negation — + /// is named for the binary one, which is what a declaration taking an operand beside the instance + /// means. + /// /// - private static readonly Dictionary OperatorNames = new(StringComparer.Ordinal) + private static readonly Dictionary OperatorNames = BuildOperatorNames(); + + /// + /// Builds the operator names from the AST's own operator vocabulary. + /// + /// The name for each symbol the AST can spell. + private static Dictionary BuildOperatorNames() { - { "+", "add" }, - { "-", "subtract" }, - { "*", "multiply" }, - { "/", "divide" }, - { "%", "modulo" }, - { "==", "equals" }, - { "!=", "not_equals" }, - { "<", "less" }, - { "<=", "less_equal" }, - { ">", "greater" }, - { ">=", "greater_equal" }, - { "&&", "and" }, - { "||", "or" }, - { "!", "not" }, - { "&", "bit_and" }, - { "|", "bit_or" }, - { "^", "bit_xor" }, - { "~", "bit_not" }, - { "<<", "shift_left" }, - { ">>", "shift_right" }, - { "[]", "at" }, - { "()", "call" } - }; + Dictionary names = new(StringComparer.Ordinal); + + foreach (BinaryOperator op in Enum.GetValues()) + { + if (OperatorSymbols.TryGetSymbol(op, out string? symbol) && symbol is not null) + { + names[symbol] = SnakeCase(op.ToString()); + } + } + + // Added second, and without replacing: a symbol both kinds of operator share is named for + // the binary one. + foreach (UnaryOperator op in Enum.GetValues()) + { + if (OperatorSymbols.TryGetSymbol(op, out string? symbol) && symbol is not null) + { + names.TryAdd(symbol, SnakeCase(op.ToString())); + } + } + + return names; + } + + /// + /// Writes a name the way C names things. + /// + /// The name, as the AST spells it. + /// The same name in lower case, with an underscore where each word begins. + private static string SnakeCase(string name) => + string.Concat(name.Select((character, index) => + char.IsUpper(character) && index > 0 + ? $"_{char.ToLowerInvariant(character)}" + : char.ToLowerInvariant(character).ToString())); /// /// Gets the unique identifier for this language generator. @@ -142,20 +164,10 @@ public class CGenerator : StandardLanguageGenerator public override string FileExtension => "c"; /// - protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl, CodeBlocker code) => - GenerateFunction(funcDecl, code, null); - - /// - /// Emits a function, which may have been declared as a member of a type. - /// - /// The declaration to emit. - /// The writer to emit into. - /// The name of the type it belongs to, when it belongs to one. /// /// C has no member functions, so a member becomes a free function named for the type it belongs /// to and taking the instance as its first parameter — Point_translate(Point* self, …). - /// That is what C code written by hand does, and it is why the type's name has to be passed in - /// rather than read off the declaration: the declaration does not know it. + /// That is what C code written by hand does. /// /// Nothing is written for , /// , @@ -172,7 +184,7 @@ protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl /// ordinary function that happens to be overridable in a language that is not C. /// /// - private void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType) + protected override void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType) { Ensure.NotNull(funcDecl); Ensure.NotNull(code); @@ -407,44 +419,6 @@ private void WriteParameterList(List parameters, CodeBlocker code) code.Write(")"); } - /// - /// - /// #pragma once rather than an include guard, for the reason a guard cannot answer: the - /// macro it needs must be unique across the whole program, which the file cannot know it has and - /// which a generator picking one would eventually collide on. Every C compiler this targets - /// supports the pragma. - /// - protected override bool WriteFileDirectives(SourceFile file, CodeBlocker code) - { - Ensure.NotNull(file); - Ensure.NotNull(code); - - if (!file.IsHeader) - { - return false; - } - - code.WriteLine("#pragma once"); - return true; - } - - /// - /// - /// An import that already carries its own delimiters is written as it stands, because the choice - /// between <> and "" says where the compiler should look and only whoever - /// wrote the file knows that. One that carries neither is quoted, which is right for a path - /// within the project being generated. - /// - protected override string? SpellImport(string import) - { - Ensure.NotNull(import); - - bool delimited = (import.StartsWith('<') && import.EndsWith('>')) - || (import.StartsWith('"') && import.EndsWith('"')); - - return delimited ? $"#include {import}" : $"#include \"{import}\""; - } - /// /// /// C has no namespaces and no way to add one, so the members are written flat and the name is @@ -698,7 +672,7 @@ protected override void GenerateCompileTimeAssertion(CompileTimeAssertion assert /// /// /// A typedef, which is what C has where another language has an alias — and it is written through - /// rather than by naming the type and then the name, because a + /// rather than by naming the type and then the name, because a /// typedef declares a name the same way a variable declaration does and an alias for an array /// puts its brackets after the name. /// @@ -725,7 +699,7 @@ protected override void GenerateUsingAlias(UsingAlias usingAlias, CodeBlocker co /// /// protected override void GenerateConstructionExpression(ConstructionExpression construction, CodeBlocker code) => - WriteBracedList(construction, code, asExpression: true); + WriteList(construction, code, asExpression: true); /// /// Writes what a declaration starts at. @@ -738,16 +712,8 @@ protected override void GenerateConstructionExpression(ConstructionExpression co /// has to be initialised by. Naming the type again would be redundant in the best case and /// rejected at file scope in the ordinary one. /// - private void WriteInitialiser(Expression initialValue, CodeBlocker code) - { - if (initialValue is ConstructionExpression construction) - { - WriteBracedList(construction, code, asExpression: false); - return; - } - - GenerateInternal(initialValue, code); - } + private void WriteInitialiser(Expression initialValue, CodeBlocker code) => + WriteListValue(initialValue, code); /// /// Writes a braced list, as either an initialiser or a value in its own right. @@ -757,7 +723,7 @@ private void WriteInitialiser(Expression initialValue, CodeBlocker code) /// /// Whether the list stands where a value is wanted, and so needs its type in front of it. /// - private void WriteBracedList(ConstructionExpression construction, CodeBlocker code, bool asExpression) + private void WriteList(ConstructionExpression construction, CodeBlocker code, bool asExpression) { Ensure.NotNull(construction); Ensure.NotNull(code); @@ -767,98 +733,29 @@ private void WriteBracedList(ConstructionExpression construction, CodeBlocker co code.Write($"({MapToCType(construction.Type)})"); } - if (construction.Arguments.Count == 0) - { - // Not `{}`, which C only allows from C23. - code.Write("{0}"); - return; - } - - if (SpansLines(construction)) - { - WriteStacked(construction, code); - return; - } - - code.Write("{ "); - for (int index = 0; index < construction.Arguments.Count; index++) - { - if (index > 0) - { - code.Write(", "); - } - - WriteArgument(construction.Arguments[index], code); - } - - code.Write(" }"); - } - - /// - /// Writes a braced list one element per line. - /// - /// The expression whose arguments to write. - /// The writer to emit into. - /// - /// A trailing comma after the last element, which C allows in a braced list and which keeps - /// adding a row to a generated table from touching the row above it in the diff. - /// - private void WriteStacked(ConstructionExpression construction, CodeBlocker code) - { - code.WriteLine("{"); - code.Indent(); - - foreach (AstNode argument in construction.Arguments) - { - WriteArgument(argument, code); - code.WriteLine(","); - } - - code.Outdent(); - code.Write("}"); + // `{0}` rather than `{}`, which C only allows from C23, and which zeroes a whole object + // whatever the type of its first member is. + WriteBracedList(construction, code, "{0}"); } - /// - /// Writes one element of a braced list, which may name the member it is for. - /// - /// The element to write. - /// The writer to emit into. + /// /// /// An element that is itself a braced list is written without its type. The enclosing list has /// already said what each element is, so a row of a table is { .x = 1 } rather than - /// (Point){ .x = 1 } — and at file scope the second is not a constant expression. + /// (Point){ .x = 1 } — and at file scope the second is not a constant expression, which + /// is what a constant table has to be initialised by. /// - private void WriteArgument(AstNode argument, CodeBlocker code) + protected override void WriteListValue(AstNode value, CodeBlocker code) { - if (argument is MemberInitialiser designated) + if (value is ConstructionExpression nested) { - code.Write($".{designated.Name} = "); - WriteInitialiser(designated.Value ?? new VariableReference(string.Empty), code); + WriteList(nested, code, asExpression: false); return; } - if (argument is Expression element) - { - WriteInitialiser(element, code); - return; - } - - GenerateInternal(argument, code); + GenerateInternal(value, code); } - /// - /// Reports whether a braced list is worth breaking across lines. - /// - /// The expression to judge. - /// when it should be written one element per line. - /// - /// A list of values is a value and belongs on one line; a list whose elements are themselves - /// lists is a table, and a table written on one line is a row of a diff nobody can read. - /// - private static bool SpansLines(ConstructionExpression construction) => - construction.Arguments.Any(argument => - argument is ConstructionExpression or MemberInitialiser { Value: ConstructionExpression }); - /// /// /// A C enumeration is unscoped: its members are names in the scope around it, so two enumerations @@ -988,29 +885,6 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo EndStatement(code); } - /// - /// - /// Two of a kind that say nothing about themselves stay together, which is what keeps a run of - /// typedefs, of struct members, or of assertions about one type reading as one block. - /// - protected override bool NeedsSeparation(AstNode previous, AstNode member) - { - Ensure.NotNull(previous); - Ensure.NotNull(member); - - return previous.GetType() != member.GetType() - || IsDocumented(previous) - || IsDocumented(member); - } - - /// - /// Reports whether a member carries documentation. - /// - /// The member to test. - /// True when it does. - private static bool IsDocumented(AstNode member) => - member is IHasDocumentation documented && documented.Documentation.Count > 0; - /// /// /// C's main returns int whether or not the program means to hand back an exit code, @@ -1164,26 +1038,10 @@ private static string SpellTypeName(TypeReference type) : $"{type.Name}_{string.Join("_", type.TypeArguments.Select(argument => Identifier(SpellTypeName(argument))))}"; } - /// - /// Spells a declaration of with that type. - /// - /// The declared type. - /// The name being declared. - /// The declaration, without an initialiser or a terminator. - /// - /// C puts an array's brackets on the declarator rather than on the type — T name[], never - /// T[] name — so a declaration cannot be built by writing the type and the name in that - /// order, which is what a language with a whole type on the left does. This is the one place that - /// difference lives. - /// - private static string SpellDeclarator(TypeReference type, string name) + /// + protected override string SpellType(TypeReference type) { - TypeReference element = type.IsArray ? type.Clone() : type; - if (type.IsArray) - { - element.IsArray = false; - } - - return $"{MapToCType(element)} {name}{(type.IsArray ? "[]" : string.Empty)}"; + Ensure.NotNull(type); + return MapToCType(type); } } diff --git a/Coder/Languages/CppGenerator.cs b/Coder/Languages/CppGenerator.cs index 5c34db7..a785cce 100644 --- a/Coder/Languages/CppGenerator.cs +++ b/Coder/Languages/CppGenerator.cs @@ -17,7 +17,7 @@ namespace ktsu.Coder.Languages; /// verbatim on the assumption the caller meant a C++ type. A declaration with no type, or one marked /// type-inferred, becomes auto. /// -public class CppGenerator : StandardLanguageGenerator +public class CppGenerator : CFamilyGenerator { /// /// Maps the AST's language-neutral type names onto C++ spellings. @@ -74,27 +74,18 @@ public class CppGenerator : StandardLanguageGenerator /// /// - /// A pure function is written [[nodiscard]]: discarding the result of a call that does - /// nothing else is always a mistake, and that is the whole of what the standard can say. The - /// compiler-specific __attribute__((pure)) asserts to the optimiser that the call may be - /// elided or duplicated, which is a stronger promise than the AST is in a position to make. - /// - protected override void GenerateFunctionDeclaration(FunctionDeclaration funcDecl, CodeBlocker code) => - GenerateFunction(funcDecl, code, null); - - /// - /// Emits a function, which may be a member of a type. - /// - /// The declaration to emit. - /// The writer to emit into. - /// The name of the type it belongs to, when it belongs to one. - /// /// A constructor and a destructor are named after the type rather than after themselves, so the /// name comes from the class emitter rather than from the declaration. That is what stops the two /// desynchronising when the type is renamed — the alternative is holding the type's name twice /// and hoping. + /// + /// A pure function is written [[nodiscard]]: discarding the result of a call that does + /// nothing else is always a mistake, and that is the whole of what the standard can say. The + /// compiler-specific __attribute__((pure)) asserts to the optimiser that the call may be + /// elided or duplicated, which is a stronger promise than the AST is in a position to make. + /// /// - private void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType) + protected override void GenerateFunction(FunctionDeclaration funcDecl, CodeBlocker code, string? enclosingType) { Ensure.NotNull(funcDecl); Ensure.NotNull(code); @@ -254,43 +245,6 @@ private static string SpellFunctionName(FunctionDeclaration funcDecl, string? en }; } - /// - /// - /// #pragma once rather than an include guard. Every compiler this targets supports it, and - /// a guard needs a macro name unique across the whole program — which the file cannot know it - /// has, and which a generator picking one would eventually collide on. - /// - protected override bool WriteFileDirectives(SourceFile file, CodeBlocker code) - { - Ensure.NotNull(file); - Ensure.NotNull(code); - - if (!file.IsHeader) - { - return false; - } - - code.WriteLine("#pragma once"); - return true; - } - - /// - /// - /// An import that already carries its own delimiters is written as it stands, because the choice - /// between <> and "" says where the compiler should look and only whoever - /// wrote the file knows that. One that carries neither is quoted, which is right for a path - /// within the project being generated. - /// - protected override string? SpellImport(string import) - { - Ensure.NotNull(import); - - bool delimited = (import.StartsWith('<') && import.EndsWith('>')) - || (import.StartsWith('"') && import.EndsWith('"')); - - return delimited ? $"#include {import}" : $"#include \"{import}\""; - } - /// /// /// The members are not indented. A namespace usually wraps a whole file, so indenting for it @@ -484,88 +438,9 @@ protected override void GenerateConstructionExpression(ConstructionExpression co code.Write(MapToCppType(construction.Type)); } - if (construction.Arguments.Count == 0) - { - code.Write("{}"); - return; - } - - if (SpansLines(construction)) - { - WriteStacked(construction, code); - return; - } - - code.Write("{ "); - for (int index = 0; index < construction.Arguments.Count; index++) - { - if (index > 0) - { - code.Write(", "); - } - - WriteArgument(construction.Arguments[index], code); - } - - code.Write(" }"); - } - - /// - /// Writes a braced list one element per line. - /// - /// The expression whose arguments to write. - /// The writer to emit into. - /// - /// A trailing comma after the last element, which C++ allows in a braced list and which keeps - /// adding a row to a generated table from touching the row above it in the diff. - /// - private void WriteStacked(ConstructionExpression construction, CodeBlocker code) - { - code.WriteLine("{"); - code.Indent(); - - foreach (AstNode argument in construction.Arguments) - { - WriteArgument(argument, code); - code.WriteLine(","); - } - - code.Outdent(); - code.Write("}"); + WriteBracedList(construction, code, "{}"); } - /// - /// Writes one element of a braced list, which may name the member it is for. - /// - /// The element to write. - /// The writer to emit into. - private void WriteArgument(AstNode argument, CodeBlocker code) - { - if (argument is MemberInitialiser designated) - { - code.Write($".{designated.Name} = "); - GenerateInternal(designated.Value ?? new VariableReference(string.Empty), code); - return; - } - - GenerateInternal(argument, code); - } - - /// - /// Reports whether a braced list is worth breaking across lines. - /// - /// The expression to judge. - /// when it should be written one element per line. - /// - /// A list of values is a value and belongs on one line; a list whose elements are themselves - /// lists is a table, and a table written on one line is a row of a diff nobody can read. The - /// test is the shape of the data rather than a column count, because a generated file has no - /// idea how wide anyone's editor is and a rule about that would have to be guessed. - /// - private static bool SpansLines(ConstructionExpression construction) => - construction.Arguments.Any(argument => - argument is ConstructionExpression or MemberInitialiser { Value: ConstructionExpression }); - /// /// /// Always enum class, never the unscoped form: an unscoped enumeration leaks its members @@ -645,34 +520,6 @@ protected override void GenerateFieldDeclaration(FieldDeclaration field, CodeBlo _ => Visibility.Public, }; - /// - /// Reports whether two adjacent members want a blank line between them. - /// - /// The member already written. - /// The member about to be written. - /// True when a blank line belongs between them. - /// - /// Two of a kind that say nothing about themselves stay together, which is what keeps a run of - /// aliases, of defaulted declarations, or of assertions about one type reading as one block. - /// - protected override bool NeedsSeparation(AstNode previous, AstNode member) - { - Ensure.NotNull(previous); - Ensure.NotNull(member); - - return previous.GetType() != member.GetType() - || IsDocumented(previous) - || IsDocumented(member); - } - - /// - /// Reports whether a member carries documentation. - /// - /// The member to test. - /// True when it does. - private static bool IsDocumented(AstNode member) => - member is IHasDocumentation documented && documented.Documentation.Count > 0; - /// /// Emits a variable declaration as a class member. /// @@ -836,27 +683,11 @@ private static string MapToCppType(TypeReference type) return $"{(type.IsReadOnly ? "const " : string.Empty)}{name}{arguments}{array}{indirection}"; } - /// - /// Spells a declaration of with that type. - /// - /// The declared type. - /// The name being declared. - /// The declaration, without an initialiser or a terminator. - /// - /// C++ puts an array's brackets on the declarator rather than on the type — T name[], - /// never T[] name — so a declaration cannot be built by writing the type and the name in - /// that order, which is what every other language here does. This is the one place that - /// difference lives. - /// - private static string SpellDeclarator(TypeReference type, string name) + /// + protected override string SpellType(TypeReference type) { - TypeReference element = type.IsArray ? type.Clone() : type; - if (type.IsArray) - { - element.IsArray = false; - } - - return $"{MapToCppType(element)} {name}{(type.IsArray ? "[]" : string.Empty)}"; + Ensure.NotNull(type); + return MapToCppType(type); } /// From 7b0ea6b046905429878cca42d1495458e2bae4ba Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 22:45:45 +0000 Subject: [PATCH 3/3] refactor: filter the operator vocabulary explicitly The review bot's finding: both loops in BuildOperatorNames filtered inside the body, so the sequence being iterated was not the sequence being used. `HasSymbol` is what the filter now reads as, and it is also what makes `GetSymbol` safe to call on what survives it - an operator the AST has no spelling for is left out rather than throwing before anything has run. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D7Exu6cR3QLkHY5F8uSks7 --- Coder/Languages/CGenerator.cs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs index f10ff75..6955136 100644 --- a/Coder/Languages/CGenerator.cs +++ b/Coder/Languages/CGenerator.cs @@ -116,27 +116,39 @@ private static Dictionary BuildOperatorNames() { Dictionary names = new(StringComparer.Ordinal); - foreach (BinaryOperator op in Enum.GetValues()) + foreach (BinaryOperator op in Enum.GetValues().Where(HasSymbol)) { - if (OperatorSymbols.TryGetSymbol(op, out string? symbol) && symbol is not null) - { - names[symbol] = SnakeCase(op.ToString()); - } + names[OperatorSymbols.GetSymbol(op)] = SnakeCase(op.ToString()); } // Added second, and without replacing: a symbol both kinds of operator share is named for // the binary one. - foreach (UnaryOperator op in Enum.GetValues()) + foreach (UnaryOperator op in Enum.GetValues().Where(HasSymbol)) { - if (OperatorSymbols.TryGetSymbol(op, out string? symbol) && symbol is not null) - { - names.TryAdd(symbol, SnakeCase(op.ToString())); - } + names.TryAdd(OperatorSymbols.GetSymbol(op), SnakeCase(op.ToString())); } return names; } + /// + /// Reports whether the AST can spell an operator at all. + /// + /// The operator to test. + /// True when it has a symbol. + /// + /// An operator with no spelling is left out rather than named, which is what keeps + /// safe to call on what survives this — + /// and what stops one added to the AST without a symbol from throwing before anything has run. + /// + private static bool HasSymbol(BinaryOperator op) => + OperatorSymbols.TryGetSymbol(op, out string? symbol) && symbol is not null; + + /// + /// The operator to test. + private static bool HasSymbol(UnaryOperator op) => + OperatorSymbols.TryGetSymbol(op, out string? symbol) && symbol is not null; + /// /// Writes a name the way C names things. ///