diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0a52879..55274b2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,12 +26,13 @@
+
-
+
diff --git a/KtsuTools.CodeGen/CodeGenService.cs b/KtsuTools.CodeGen/CodeGenService.cs
index 9fae40e..c30584f 100644
--- a/KtsuTools.CodeGen/CodeGenService.cs
+++ b/KtsuTools.CodeGen/CodeGenService.cs
@@ -3,292 +3,64 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ktsu.KtsuTools.Test")]
-// Copyright (c) ktsu.dev
-// All rights reserved.
-// Licensed under the MIT license.
namespace KtsuTools.CodeGen;
using System.Collections.ObjectModel;
-using System.Globalization;
-using System.Text;
-using ktsu.Semantics.Paths;
-using Spectre.Console;
-using YamlDotNet.Serialization;
-using YamlDotNet.Serialization.NamingConventions;
-
-#pragma warning disable CA1002 // Do not expose generic lists - needed for AST node model
-
-///
-/// Interface for all AST nodes.
-///
-public interface IAstNode
-{
- /// Gets the node type name.
- public string NodeType { get; }
-}
-
-///
-/// A function declaration AST node.
-///
-public class FunctionDeclaration : IAstNode
-{
- ///
- public string NodeType => "functionDeclaration";
-
- /// Gets or sets the function name.
- public string Name { get; set; } = string.Empty;
-
- /// Gets or sets the return type.
- public string ReturnType { get; set; } = "void";
-
- /// Gets or sets the parameters.
- public Collection Parameters { get; init; } = [];
-
- /// Gets or sets the body statements.
- public Collection Body { get; init; } = [];
-}
-
-///
-/// A parameter AST node.
-///
-public class ParameterNode : IAstNode
-{
- ///
- public string NodeType => "parameter";
-
- /// Gets or sets the parameter name.
- public string Name { get; set; } = string.Empty;
-
- /// Gets or sets the type.
- public string Type { get; set; } = string.Empty;
-
- /// Gets or sets a value indicating whether this parameter is optional.
- public bool IsOptional { get; set; }
-
- /// Gets or sets the default value.
- public string? DefaultValue { get; set; }
-}
-
-///
-/// A variable declaration AST node.
-///
-public class VariableDeclaration : IAstNode
-{
- ///
- public string NodeType => "variableDeclaration";
-
- /// Gets or sets the variable name.
- public string Name { get; set; } = string.Empty;
-
- /// Gets or sets the type.
- public string? Type { get; set; }
-
- /// Gets or sets the initial value expression.
- public string? InitialValue { get; set; }
-
- /// Gets or sets a value indicating whether this is a constant.
- public bool IsConstant { get; set; }
-}
-
-///
-/// A return statement AST node.
-///
-public class ReturnStatement : IAstNode
-{
- ///
- public string NodeType => "returnStatement";
- /// Gets or sets the return expression.
- public string? Expression { get; set; }
-}
-
-///
-/// Interface for language code generators.
-///
-public interface ILanguageGenerator
-{
- /// Gets the language identifier.
- public string LanguageId { get; }
-
- /// Gets the display name.
- public string DisplayName { get; }
-
- /// Gets the file extension.
- public string FileExtension { get; }
-
- /// Generates code from an AST.
- public string Generate(FunctionDeclaration declaration);
-}
-
-///
-/// C# code generator.
-///
-public class CSharpGenerator : ILanguageGenerator
-{
- ///
- public string LanguageId => "csharp";
-
- ///
- public string DisplayName => "C#";
-
- ///
- public string FileExtension => "cs";
-
- ///
- public string Generate(FunctionDeclaration declaration)
- {
- Ensure.NotNull(declaration);
-
- StringBuilder sb = new();
- string returnType = MapType(declaration.ReturnType);
- string parameters = string.Join(", ", declaration.Parameters.Select(FormatParameter));
-
- sb.AppendLine(CultureInfo.InvariantCulture, $"public {returnType} {declaration.Name}({parameters})");
- sb.AppendLine("{");
-
- foreach (IAstNode statement in declaration.Body)
- {
- AppendStatement(sb, statement);
- }
-
- sb.AppendLine("}");
- return sb.ToString();
- }
-
- private static string FormatParameter(ParameterNode p)
- {
- string paramType = MapType(p.Type);
- string defaultVal = p.IsOptional && p.DefaultValue is not null
- ? $" = {p.DefaultValue}"
- : string.Empty;
- return $"{paramType} {p.Name}{defaultVal}";
- }
-
- private static void AppendStatement(StringBuilder sb, IAstNode statement)
- {
- if (statement is ReturnStatement ret)
- {
- sb.AppendLine(CultureInfo.InvariantCulture, $" return {ret.Expression};");
- }
- else if (statement is VariableDeclaration varDecl)
- {
- string varType = varDecl.Type is not null ? MapType(varDecl.Type) : "var";
- string init = varDecl.InitialValue is not null ? $" = {varDecl.InitialValue}" : string.Empty;
- string keyword = varDecl.IsConstant ? "const " : string.Empty;
- sb.AppendLine(CultureInfo.InvariantCulture, $" {keyword}{varType} {varDecl.Name}{init};");
- }
- }
+using ktsu.Coder.Ast;
+using ktsu.Coder.Languages;
+using ktsu.Coder.Serialization;
+using ktsu.Semantics.Paths;
- private static string MapType(string type) => type switch
- {
- "str" or "string" => "string",
- "int" => "int",
- "float" => "float",
- "double" => "double",
- "bool" => "bool",
- "void" => "void",
- _ => type,
- };
-}
+using Spectre.Console;
///
-/// Python code generator.
+/// Generates source from a YAML description of an abstract syntax tree.
///
-public class PythonGenerator : ILanguageGenerator
+///
+/// The AST, the YAML on both sides of it and every generator come from
+/// ktsu.Coder. This module used to carry its own
+/// — an IAstNode, five node types, a hand-written YAML reader and a C# and a Python emitter,
+/// all of which that package already had along with C++, JavaScript, a round trip back to YAML, and
+/// a test suite. What is left here is what a command-line front end is actually for: finding the
+/// file, choosing the generator, and putting the result somewhere.
+///
+public class CodeGenService
{
- ///
- public string LanguageId => "python";
-
- ///
- public string DisplayName => "Python";
-
- ///
- public string FileExtension => "py";
-
- ///
- public string Generate(FunctionDeclaration declaration)
- {
- Ensure.NotNull(declaration);
-
- StringBuilder sb = new();
- string parameters = string.Join(", ", declaration.Parameters.Select(FormatParameter));
- string returnHint = declaration.ReturnType != "void"
- ? $" -> {MapType(declaration.ReturnType)}"
- : string.Empty;
-
- sb.AppendLine(CultureInfo.InvariantCulture, $"def {declaration.Name}({parameters}){returnHint}:");
-
- if (declaration.Body.Count == 0)
- {
- sb.AppendLine(" pass");
- }
- else
- {
- foreach (IAstNode statement in declaration.Body)
- {
- AppendStatement(sb, statement);
- }
- }
-
- return sb.ToString();
- }
-
- private static string FormatParameter(ParameterNode p)
- {
- string typeHint = MapType(p.Type);
- string defaultVal = p.IsOptional && p.DefaultValue is not null
- ? $" = {p.DefaultValue}"
- : string.Empty;
- return $"{p.Name}: {typeHint}{defaultVal}";
- }
-
- private static void AppendStatement(StringBuilder sb, IAstNode statement)
- {
- if (statement is ReturnStatement ret)
- {
- sb.AppendLine(CultureInfo.InvariantCulture, $" return {ret.Expression}");
- }
- else if (statement is VariableDeclaration varDecl)
+ ///
+ /// The generators, by the name a caller asks for them under.
+ ///
+ private static readonly ReadOnlyDictionary Generators =
+ new(new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- string init = varDecl.InitialValue ?? "None";
- sb.AppendLine(CultureInfo.InvariantCulture, $" {varDecl.Name} = {init}");
- }
- }
+ ["csharp"] = new CSharpGenerator(),
+ ["cpp"] = new CppGenerator(),
+ ["javascript"] = new JavaScriptGenerator(),
+ ["python"] = new PythonGenerator(),
+ });
- private static string MapType(string type) => type switch
- {
- "int" => "int",
- "string" or "str" => "str",
- "bool" => "bool",
- "float" or "double" => "float",
- "void" => "None",
- _ => type,
- };
-}
-
-///
-/// Service for generating code from YAML AST definitions.
-///
-public class CodeGenService
-{
- private static readonly Dictionary Generators = new(StringComparer.OrdinalIgnoreCase)
- {
- ["csharp"] = new CSharpGenerator(),
- ["python"] = new PythonGenerator(),
- };
+ ///
+ /// Gets the languages this command can generate.
+ ///
+ public static IEnumerable Languages => Generators.Keys;
///
/// Generates code from a YAML AST definition file.
///
/// Absolute path to the YAML AST input file.
- /// Target language identifier (e.g. "csharp", "python").
+ /// Target language identifier (e.g. "csharp", "cpp").
/// Optional absolute path to write generated code to. If null, writes to console.
/// Cancellation token.
/// Exit code (0 for success).
-#pragma warning disable CA1822 // Mark members as static - instance method required for DI injection
+ ///
+ /// CA1822 and S2325 are the same complaint from two analyzers, and the answer to both is that
+ /// this service is a singleton injected into CodeGenCommand's constructor: a static
+ /// method here would leave the command holding a dependency it takes and never uses.
+ ///
+#pragma warning disable CA1822, S2325 // Mark members as static
public async Task GenerateAsync(AbsoluteFilePath inputFile, string language, AbsoluteFilePath? outputFile = null, CancellationToken ct = default)
-#pragma warning restore CA1822
+#pragma warning restore CA1822, S2325
{
Ensure.NotNull(inputFile);
Ensure.NotNull(language);
@@ -310,147 +82,66 @@ public async Task GenerateAsync(AbsoluteFilePath inputFile, string language
AnsiConsole.MarkupLine($"[bold]Code Generation[/] - {generator.DisplayName}");
- // Read and parse YAML
- string yamlContent = await File.ReadAllTextAsync(fullPath, ct).ConfigureAwait(false);
+ string yaml = await File.ReadAllTextAsync(fullPath, ct).ConfigureAwait(false);
- FunctionDeclaration? function = ParseYaml(yamlContent);
+ if (!TryRead(yaml, out AstNode? node))
+ {
+ return 1;
+ }
- if (function is null)
+ // A generator says whether it can write a node rather than throwing partway through one, so
+ // a file naming something the target has no form for is reported here instead of arriving
+ // as half a file.
+ if (!generator.CanGenerate(node!))
{
- AnsiConsole.MarkupLine("[red]Error: Could not parse YAML as a function declaration.[/]");
+ AnsiConsole.MarkupLine(
+ $"[red]Error: {generator.DisplayName.EscapeMarkup()} cannot generate a {node!.GetNodeTypeName().EscapeMarkup()}.[/]");
return 1;
}
- // Generate code
- string generatedCode = generator.Generate(function);
+ string generated = generator.Generate(node!);
- // Output
if (outputFile is not null)
{
string outputPath = outputFile.ToString();
- await File.WriteAllTextAsync(outputPath, generatedCode, ct).ConfigureAwait(false);
+ await File.WriteAllTextAsync(outputPath, generated, ct).ConfigureAwait(false);
AnsiConsole.MarkupLine($"[green]Generated code written to: {outputPath.EscapeMarkup()}[/]");
+ return 0;
}
- else
- {
- AnsiConsole.Write(new Panel(generatedCode.EscapeMarkup())
- .Header($"[blue]{generator.DisplayName} Output[/]")
- .Border(BoxBorder.Rounded));
- }
-
- return 0;
- }
-
- private static FunctionDeclaration? ParseYaml(string yamlContent)
- {
- try
- {
- IDeserializer deserializer = new DeserializerBuilder()
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
- .IgnoreUnmatchedProperties()
- .Build();
-
- Dictionary root = deserializer.Deserialize>(yamlContent);
-
- if (root.TryGetValue("functionDeclaration", out object? funcObj) && funcObj is Dictionary