diff --git a/CLAUDE.md b/CLAUDE.md index 64076bf..903c382 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,6 +51,13 @@ source in seven target languages. The solution uses: as a string can only paste it, so the caller would have to spell the target language itself. `Parse` and `ToString` are inverses and text the grammar cannot read becomes a name holding it verbatim, so a property that used to hold a string still takes and gives one. +- `Coder/Ast/Parameter.cs`'s `DefaultValue` — an `Expression`, not source text, for the reason + `TypeReference` is structure: `False` is Python's spelling and fails to compile in C# and C++, and + no spelling of a flag or a null works in every target. Each generator writes it with its own + literal writer; C, Go and Rust, which have no default arguments, write it into a comment with any + `*/` broken apart. `ParseDefaultValue`/`FormatDefaultValue` are the text form the YAML and the + editor's field hold, so a document from when it was text still reads: `true`/`false` in any case, + numbers and double-quoted strings become literals, and anything else is a name, written verbatim. - `Coder/Ast/SourceFile.cs`, `NamespaceDeclaration.cs`, `EnumDeclaration.cs`, `FieldDeclaration.cs` — what a generated *header* is made of rather than a snippet. `FieldDeclaration` is deliberately not `VariableDeclaration`: a field with no initialiser is value-initialised, so a default-constructed diff --git a/Coder.Cli/ExpressionDemo.cs b/Coder.Cli/ExpressionDemo.cs index 6d71b89..e0990e8 100644 --- a/Coder.Cli/ExpressionDemo.cs +++ b/Coder.Cli/ExpressionDemo.cs @@ -177,8 +177,8 @@ private static FunctionDeclaration CreateComplexFunction() ReturnType = "double" }; function.Parameters.Add(new Parameter("baseScore", "double")); - function.Parameters.Add(new Parameter("multiplier", "double") { IsOptional = true, DefaultValue = "1.0" }); - function.Parameters.Add(new Parameter("bonus", "double") { IsOptional = true, DefaultValue = "0.0" }); + function.Parameters.Add(new Parameter("multiplier", "double") { IsOptional = true, DefaultValue = Literal.DecimalValue(1.0) }); + function.Parameters.Add(new Parameter("bonus", "double") { IsOptional = true, DefaultValue = Literal.DecimalValue(0.0) }); // Complex calculation: (baseScore * multiplier) + bonus BinaryExpression multiplication = new( diff --git a/Coder.Cli/ExtendedDemo.cs b/Coder.Cli/ExtendedDemo.cs index eeb0e50..e1418b6 100644 --- a/Coder.Cli/ExtendedDemo.cs +++ b/Coder.Cli/ExtendedDemo.cs @@ -95,7 +95,7 @@ private static void DemoComplexFunction() function.Parameters.Add(new Parameter("principal", "double")); function.Parameters.Add(new Parameter("rate", "double")); function.Parameters.Add(new Parameter("time", "int")); - function.Parameters.Add(new Parameter("compound", "int") { IsOptional = true, DefaultValue = "1" }); + function.Parameters.Add(new Parameter("compound", "int") { IsOptional = true, DefaultValue = Literal.Number(1) }); // Create complex expression: principal * (1 + rate/compound)^(compound*time) // For demo, we'll create a simpler version: principal * (1 + rate * time) diff --git a/Coder.Cli/SampleCLI.cs b/Coder.Cli/SampleCLI.cs index b7d99b9..8f3e055 100644 --- a/Coder.Cli/SampleCLI.cs +++ b/Coder.Cli/SampleCLI.cs @@ -109,7 +109,7 @@ private static FunctionDeclaration CreateSampleFunction() // Add parameters function.Parameters.Add(new Parameter("a", "int")); function.Parameters.Add(new Parameter("b", "int")); - function.Parameters.Add(new Parameter("include_debug", "bool") { IsOptional = true, DefaultValue = "False" }); + function.Parameters.Add(new Parameter("include_debug", "bool") { IsOptional = true, DefaultValue = Literal.Bool(false) }); // Add function body function.Body.Add(new ReturnStatement(new AstLeafNode("a + b"))); @@ -125,7 +125,7 @@ private static void ShowAstDetails(FunctionDeclaration function) foreach (Parameter param in function.Parameters) { - string optionalInfo = param.IsOptional ? $" (optional, default: {param.DefaultValue})" : ""; + string optionalInfo = param.IsOptional ? $" (optional, default: {Parameter.FormatDefaultValue(param.DefaultValue)})" : ""; Console.WriteLine($" - {param.Name}: {param.Type}{optionalInfo}"); } diff --git a/Coder.Graph/AstFields.cs b/Coder.Graph/AstFields.cs index bc51477..7f547f6 100644 --- a/Coder.Graph/AstFields.cs +++ b/Coder.Graph/AstFields.cs @@ -277,7 +277,7 @@ private static IReadOnlyList OfCallable(AstNode node) new("Name", AstFieldKind.Text, parameter.Name ?? string.Empty), new("Type", AstFieldKind.Text, parameter.Type?.ToString() ?? string.Empty), new("Optional", AstFieldKind.Flag, Spell(parameter.IsOptional)), - new("Default", AstFieldKind.Text, parameter.DefaultValue ?? string.Empty), + new("Default", AstFieldKind.Text, Parameter.FormatDefaultValue(parameter.DefaultValue) ?? string.Empty), ], VariableDeclaration varDecl => @@ -505,7 +505,7 @@ private static bool TryWriteFunctionShape(AstNode node, string fieldName, string (Parameter parameter, "Name") => Assign(() => parameter.Name = OrNull(value)), (Parameter parameter, "Type") => Assign(() => parameter.Type = OrNull(value)), (Parameter parameter, "Optional") => AssignFlag(value, optional => parameter.IsOptional = optional), - (Parameter parameter, "Default") => Assign(() => parameter.DefaultValue = OrNull(value)), + (Parameter parameter, "Default") => Assign(() => parameter.DefaultValue = Parameter.ParseDefaultValue(value)), (VariableDeclaration varDecl, "Name") => value.Length > 0 && Assign(() => varDecl.Name = value), (VariableDeclaration varDecl, "Type") => Assign(() => varDecl.Type = OrNull(value)), diff --git a/Coder.Test/Ast/ParameterDefaultValueTests.cs b/Coder.Test/Ast/ParameterDefaultValueTests.cs new file mode 100644 index 0000000..35cda8e --- /dev/null +++ b/Coder.Test/Ast/ParameterDefaultValueTests.cs @@ -0,0 +1,263 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Ast; + +using System; +using System.IO; +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using ktsu.Coder.Serialization; +using ktsu.Coder.Test.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for : that it is spelled by each language rather than +/// pasted, and that the text form older documents hold still reads (issue #182). +/// +/// +/// A default used to be target-language source text, so the README's own DefaultValue = "False" +/// compiled in Python and nowhere else, and no spelling of a flag worked in every target. +/// +[TestClass] +public class ParameterDefaultValueTests +{ + /// + /// Builds describe(include_debug: bool = false) -> bool, which returns its parameter. + /// + /// The function. + private static FunctionDeclaration Describe() + { + FunctionDeclaration function = new("describe") { ReturnType = "bool" }; + function.Parameters.Add(new Parameter("include_debug", "bool") { IsOptional = true, DefaultValue = Literal.Bool(false) }); + function.Body.Add(new ReturnStatement(new VariableReference("include_debug"))); + return function; + } + + /// + /// Tests that a false default is spelled each language's own way. + /// + /// The language, which names the generator. + /// What the parameter has to come out as. + [TestMethod] + [DataRow("csharp", "bool include_debug = false")] + [DataRow("cpp", "bool include_debug = false")] + [DataRow("javascript", "include_debug = false")] + [DataRow("python", "include_debug: bool = False")] + [DataRow("c", "/* = false */")] + [DataRow("go", "/* = false */")] + [DataRow("rust", "/* = false */")] + public void AFlagDefaultIsSpelledByEachLanguage(string language, string expected) + { + ILanguageGenerator generator = language switch + { + "csharp" => new CSharpGenerator(), + "cpp" => new CppGenerator(), + "javascript" => new JavaScriptGenerator(), + "python" => new PythonGenerator(), + "c" => new CGenerator(), + "go" => new GoGenerator(), + "rust" => new RustGenerator(), + _ => throw new ArgumentOutOfRangeException(nameof(language)), + }; + + string generated = generator.Generate(Describe()); + + Assert.Contains(expected, generated, StringComparison.Ordinal, generated); + Assert.DoesNotContain("False", language == "python" ? string.Empty : generated, StringComparison.Ordinal, generated); + } + + /// + /// Tests that the generated Python runs when called without the argument. + /// + [TestMethod] + public void TheFlagDefaultRunsInPython() + { + string? python = ToolchainHarness.FindOnPath("--version", "python3", "python"); + if (python is null) + { + Assert.Inconclusive("No Python on the path, so nothing was run."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText( + Path.Join(directory, "driver.py"), + new PythonGenerator().Generate(Describe()) + "\nassert describe() is False\nassert describe(True) is True\n"); + + (int exitCode, string output) = ToolchainHarness.Run(python, "driver.py", directory); + + Assert.AreEqual(0, exitCode, output); + }); + } + + /// + /// Tests that the generated JavaScript runs when called without the argument, rather than throwing + /// a ReferenceError for a name it has never heard of. + /// + [TestMethod] + public void TheFlagDefaultRunsInJavaScript() + { + string? node = ToolchainHarness.FindOnPath("--version", "node"); + if (node is null) + { + Assert.Inconclusive("No Node.js on the path, so nothing was run."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText( + Path.Join(directory, "driver.js"), + new JavaScriptGenerator().Generate(Describe()) + + "\nif (describe() !== false || describe(true) !== true) { process.exit(1); }\n"); + + (int exitCode, string output) = ToolchainHarness.Run(node, "driver.js", directory); + + Assert.AreEqual(0, exitCode, output); + }); + } + + /// + /// Tests that the generated C++ compiles when called without the argument. + /// + [TestMethod] + public void TheFlagDefaultCompilesInCpp() + { + string? compiler = ToolchainHarness.FindOnPath("--version", "g++", "clang++"); + if (compiler is null) + { + Assert.Inconclusive("No C++ compiler on the path, so nothing was compiled."); + return; + } + + ToolchainHarness.InTemporaryDirectory(directory => + { + File.WriteAllText( + Path.Join(directory, "driver.cpp"), + new CppGenerator().Generate(Describe()) + "\nint main()\n{\n\treturn describe() ? 1 : 0;\n}\n"); + + (int exitCode, string output) = ToolchainHarness.Run(compiler, "-std=c++20 -Wall -Wextra -pedantic -c driver.cpp -o driver.o", directory); + + Assert.AreEqual(0, exitCode, output); + }); + } + + /// + /// Tests that a default whose spelling holds */ does not end the comment a language with no + /// default arguments writes it into. + /// + /// The language, which names the generator. + [TestMethod] + [DataRow("c")] + [DataRow("go")] + [DataRow("rust")] + public void ACommentedDefaultCannotEndItsComment(string language) + { + ILanguageGenerator generator = language switch + { + "c" => new CGenerator(), + "go" => new GoGenerator(), + _ => new RustGenerator(), + }; + + FunctionDeclaration function = new("greet") { ReturnType = "int" }; + function.Parameters.Add(new Parameter("text", "string") { IsOptional = true, DefaultValue = Literal.Text("a */ b") }); + function.Body.Add(new ReturnStatement(Literal.Number(0))); + + string generated = generator.Generate(function); + + Assert.Contains("/* = \"a * / b\" */", generated, StringComparison.Ordinal, generated); + } + + /// + /// Tests that the text form reads into literals, and that anything else is a name. + /// + [TestMethod] + public void ParseDefaultValue_ReadsLiteralsAndNames() + { + Assert.IsFalse(((LiteralExpression)Parameter.ParseDefaultValue("False")!).Value); + Assert.IsTrue(((LiteralExpression)Parameter.ParseDefaultValue("true")!).Value); + Assert.AreEqual(-3, ((LiteralExpression)Parameter.ParseDefaultValue("-3")!).Value); + Assert.AreEqual(5_000_000_000L, ((LiteralExpression)Parameter.ParseDefaultValue("5000000000")!).Value); + Assert.AreEqual(1.5, ((LiteralExpression)Parameter.ParseDefaultValue("1.5")!).Value); + Assert.AreEqual("say \"hi\"", ((LiteralExpression)Parameter.ParseDefaultValue("\"say \\\"hi\\\"\"")!).Value); + Assert.AreEqual("Color.Red", ((VariableReference)Parameter.ParseDefaultValue("Color.Red")!).Name); + Assert.IsNull(Parameter.ParseDefaultValue(" ")); + Assert.IsNull(Parameter.ParseDefaultValue(null)); + } + + /// + /// Tests that every text form reads back as the value it was written from. + /// + /// The text form. + [TestMethod] + [DataRow("true")] + [DataRow("false")] + [DataRow("42")] + [DataRow("-7")] + [DataRow("5000000000")] + [DataRow("1.0")] + [DataRow("0.25")] + [DataRow("\"default\"")] + [DataRow("\"back\\\\slash \\\"quoted\\\"\"")] + [DataRow("Color.Red")] + public void FormatDefaultValue_RoundTrips(string text) => + Assert.AreEqual(text, Parameter.FormatDefaultValue(Parameter.ParseDefaultValue(text))); + + /// + /// Tests that a document written while defaults were text still reads, including the README's own + /// Python spelling of a flag. + /// + [TestMethod] + public void AnOldDocumentsTextDefaultReadsAsALiteral() + { + const string Yaml = """ + functionDeclaration: + name: describe + parameters: + - name: include_debug + type: bool + isOptional: true + defaultValue: False + """; + + FunctionDeclaration function = (FunctionDeclaration)new YamlDeserializer().Deserialize(Yaml)!; + + Assert.IsFalse(((LiteralExpression)function.Parameters[0].DefaultValue!).Value); + } + + /// + /// Tests that a default with no text form survives a round trip through YAML as the node it is. + /// + [TestMethod] + public void ADefaultWithNoTextFormRoundTripsThroughYaml() + { + FunctionDeclaration function = new("scale") { ReturnType = "int" }; + function.Parameters.Add(new Parameter("by", "int") + { + IsOptional = true, + DefaultValue = new BinaryExpression(Literal.Number(2), BinaryOperator.Multiply, Literal.Number(3)), + }); + + string yaml = new YamlSerializer().Serialize(function)!; + FunctionDeclaration read = (FunctionDeclaration)new YamlDeserializer().Deserialize(yaml)!; + + BinaryExpression value = (BinaryExpression)read.Parameters[0].DefaultValue!; + Assert.AreEqual(BinaryOperator.Multiply, value.Operator, yaml); + } + + /// + /// Tests that a clone holds its own copy of the default rather than sharing the original's node. + /// + [TestMethod] + public void Clone_CopiesTheDefault() + { + Parameter original = new("n", "int") { IsOptional = true, DefaultValue = Literal.Number(1) }; + + Parameter clone = (Parameter)original.Clone(); + + Assert.AreNotSame(original.DefaultValue, clone.DefaultValue); + Assert.AreEqual(1, ((LiteralExpression)clone.DefaultValue!).Value); + } +} diff --git a/Coder.Test/Languages/CGeneratorTests.cs b/Coder.Test/Languages/CGeneratorTests.cs index ece9da2..e0bf295 100644 --- a/Coder.Test/Languages/CGeneratorTests.cs +++ b/Coder.Test/Languages/CGeneratorTests.cs @@ -552,7 +552,7 @@ public void UsingAlias_IsATypedef() public void OptionalParameter_KeepsItsDefaultAsAComment() { FunctionDeclaration function = new("repeat"); - function.Parameters.Add(new Parameter("times", "int") { IsOptional = true, DefaultValue = "1" }); + function.Parameters.Add(new Parameter("times", "int") { IsOptional = true, DefaultValue = Literal.Number(1) }); StringAssert.Contains(Generator.Generate(function), "void repeat(int times /* = 1 */)", StringComparison.Ordinal); } diff --git a/Coder.Test/Languages/CompiledExemplar.cs b/Coder.Test/Languages/CompiledExemplar.cs index 68063cb..9f831af 100644 --- a/Coder.Test/Languages/CompiledExemplar.cs +++ b/Coder.Test/Languages/CompiledExemplar.cs @@ -58,7 +58,7 @@ public static ClassDeclaration Point() 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.Parameters.Add(new Parameter("y", "int") { IsOptional = true, DefaultValue = Literal.Number(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); diff --git a/Coder.Test/Languages/GoGeneratorTests.cs b/Coder.Test/Languages/GoGeneratorTests.cs index a945af4..efd9558 100644 --- a/Coder.Test/Languages/GoGeneratorTests.cs +++ b/Coder.Test/Languages/GoGeneratorTests.cs @@ -944,7 +944,7 @@ public void NestedType_IsWrittenBesideTheOneDeclaringIt() public void OptionalParameter_KeepsItsDefaultAsAComment() { FunctionDeclaration function = new("greet"); - function.Parameters.Add(new Parameter("times", "int") { IsOptional = true, DefaultValue = "1" }); + function.Parameters.Add(new Parameter("times", "int") { IsOptional = true, DefaultValue = Literal.Number(1) }); StringAssert.Contains(Generator.Generate(function), "(times int /* = 1 */)", StringComparison.Ordinal); } diff --git a/Coder.Test/Languages/JavaScriptGeneratorTests.cs b/Coder.Test/Languages/JavaScriptGeneratorTests.cs index d754ab8..49e30c2 100644 --- a/Coder.Test/Languages/JavaScriptGeneratorTests.cs +++ b/Coder.Test/Languages/JavaScriptGeneratorTests.cs @@ -72,7 +72,7 @@ public void Parameters_DropTypesAndKeepDefaults() { FunctionDeclaration function = new("greet"); function.Parameters.Add(new Parameter("name", "str")); - function.Parameters.Add(new Parameter("loud", "bool") { IsOptional = true, DefaultValue = "false" }); + function.Parameters.Add(new Parameter("loud", "bool") { IsOptional = true, DefaultValue = Literal.Bool(false) }); string code = Generator.Generate(function); diff --git a/Coder.Test/Serialization/YamlSerializationTests.cs b/Coder.Test/Serialization/YamlSerializationTests.cs index 743e2f6..0ef35fd 100644 --- a/Coder.Test/Serialization/YamlSerializationTests.cs +++ b/Coder.Test/Serialization/YamlSerializationTests.cs @@ -23,7 +23,7 @@ public void Serialize_FunctionDeclaration_GeneratesCorrectYaml() Parameters = [ new Parameter("param1", "int"), - new Parameter("param2", "string") { IsOptional = true, DefaultValue = "\"default\"" } + new Parameter("param2", "string") { IsOptional = true, DefaultValue = Literal.Text("default") } ] }; funcDecl.Body.Add(new ReturnStatement(42)); @@ -57,7 +57,7 @@ public void SerializeAndDeserialize_FunctionDeclaration_RoundTripsCorrectly() Parameters = [ new Parameter("param1", "int"), - new Parameter("param2", "string") { IsOptional = true, DefaultValue = "\"default\"" } + new Parameter("param2", "string") { IsOptional = true, DefaultValue = Literal.Text("default") } ] }; originalFuncDecl.Body.Add(new ReturnStatement(42)); @@ -80,7 +80,7 @@ public void SerializeAndDeserialize_FunctionDeclaration_RoundTripsCorrectly() Assert.AreEqual(originalFuncDecl.Parameters[0].Name, deserializedFunc.Parameters[0].Name); Assert.AreEqual(originalFuncDecl.Parameters[0].Type, deserializedFunc.Parameters[0].Type); Assert.AreEqual(originalFuncDecl.Parameters[1].IsOptional, deserializedFunc.Parameters[1].IsOptional); - Assert.AreEqual(originalFuncDecl.Parameters[1].DefaultValue, deserializedFunc.Parameters[1].DefaultValue); + Assert.AreEqual("default", ((LiteralExpression)deserializedFunc.Parameters[1].DefaultValue!).Value); Assert.AreEqual(originalFuncDecl.Body.Count, deserializedFunc.Body.Count); Assert.IsInstanceOfType(deserializedFunc.Body[0]); diff --git a/Coder/Ast/Parameter.cs b/Coder/Ast/Parameter.cs index 013354b..4558ddd 100644 --- a/Coder/Ast/Parameter.cs +++ b/Coder/Ast/Parameter.cs @@ -2,6 +2,10 @@ namespace ktsu.Coder.Ast; +using System; +using System.Globalization; +using System.Text; + /// /// Represents a parameter in a function declaration. /// @@ -49,7 +53,14 @@ public Parameter(string name, string type) /// /// Gets or sets the default value of the parameter if it is optional. /// - public string? DefaultValue { get; set; } + /// + /// An expression rather than source text, for the reason is + /// structure: False, false and None are each one language's spelling, and a + /// default written in one of them fails to compile, or throws when called, in the others. Each + /// generator spells the expression with its own literal writer. + /// turns the text form older documents hold into one. + /// + public Expression? DefaultValue { get; set; } /// /// Gets the type name of this node for serialization purposes. @@ -68,7 +79,7 @@ public override AstNode Clone() Name = Name, Type = Type?.Clone(), IsOptional = IsOptional, - DefaultValue = DefaultValue + DefaultValue = DefaultValue?.Clone() as Expression }; // Copy metadata @@ -79,4 +90,103 @@ public override AstNode Clone() return clone; } + + /// + /// Reads a default value from its text form, as a document or the editor holds it. + /// + /// The text, or null or empty for no default. + /// + /// A literal for true and false (in any case, so a document written for Python + /// still reads), a whole or fractional number, or a double-quoted string; a reference to the + /// name for anything else. Null when there is no text. + /// + /// + /// Anything that is not a recognisable literal stays a reference to a name, which is written + /// verbatim, so a default that worked as text in its one target language still does. + /// + public static Expression? ParseDefaultValue(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return null; + } + + string trimmed = text.Trim(); + + if (bool.TryParse(trimmed, out bool flag)) + { + return new LiteralExpression(flag); + } + + if (int.TryParse(trimmed, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out int number)) + { + return new LiteralExpression(number); + } + + if (long.TryParse(trimmed, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out long bigNumber)) + { + return new LiteralExpression(bigNumber); + } + + if (LooksNumeric(trimmed) + && double.TryParse(trimmed, NumberStyles.Float, CultureInfo.InvariantCulture, out double fraction) + && double.IsFinite(fraction)) + { + return new LiteralExpression(fraction); + } + + if (trimmed.Length >= 2 && trimmed[0] == '"' && trimmed[^1] == '"') + { + return new LiteralExpression(Unescape(trimmed[1..^1])); + } + + return new VariableReference(trimmed); + } + + /// + /// Writes a default value in the text form reads. + /// + /// The default value. + /// + /// Its text, which reads back as the same value; or null when there is no value, or when it is + /// an expression with no text form. + /// + public static string? FormatDefaultValue(Expression? value) => value switch + { + LiteralExpression flag => flag.Value ? "true" : "false", + LiteralExpression number => number.Value.ToString(CultureInfo.InvariantCulture), + LiteralExpression bigNumber => bigNumber.Value.ToString(CultureInfo.InvariantCulture), + LiteralExpression fraction when double.IsFinite(fraction.Value) => FormatFraction(fraction.Value.ToString("R", CultureInfo.InvariantCulture)), + LiteralExpression fraction when float.IsFinite(fraction.Value) => FormatFraction(fraction.Value.ToString("R", CultureInfo.InvariantCulture)), + LiteralExpression text => $"\"{Escape(text.Value ?? string.Empty)}\"", + VariableReference reference => reference.Name, + _ => null, + }; + + /// + /// Keeps a fractional number fractional in text, so 1.0 does not read back as a whole one. + /// + private static string FormatFraction(string text) => + text.AsSpan().IndexOfAny('.', 'E', 'e') >= 0 ? text : text + ".0"; + + private static bool LooksNumeric(string text) => + text.Length > 0 && (char.IsAsciiDigit(text[0]) || (text.Length > 1 && (text[0] is '-' or '+' or '.') && (char.IsAsciiDigit(text[1]) || text[1] == '.'))); + + private static string Escape(string text) => text.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal); + + private static string Unescape(string text) + { + StringBuilder result = new(text.Length); + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '\\' && i + 1 < text.Length && text[i + 1] is '\\' or '"') + { + i++; + } + + result.Append(text[i]); + } + + return result.ToString(); + } } diff --git a/Coder/Languages/CGenerator.cs b/Coder/Languages/CGenerator.cs index 98fae99..87dc2e6 100644 --- a/Coder/Languages/CGenerator.cs +++ b/Coder/Languages/CGenerator.cs @@ -1169,10 +1169,7 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, code.Write(name.Length == 0 ? MapToCType(type) : SpellDeclarator(type, name)); - if (parameter.IsOptional && !string.IsNullOrEmpty(parameter.DefaultValue)) - { - code.Write($" /* = {parameter.DefaultValue} */"); - } + AppendDefaultValueComment(parameter, code); } /// diff --git a/Coder/Languages/CSharpGenerator.cs b/Coder/Languages/CSharpGenerator.cs index a40841b..bf2de75 100644 --- a/Coder/Languages/CSharpGenerator.cs +++ b/Coder/Languages/CSharpGenerator.cs @@ -1423,13 +1423,14 @@ private static string SpellFunctionName(FunctionDeclaration function, string? en }; } - private static void GenerateParameter(Parameter parameter, CodeBlocker code) + private void GenerateParameter(Parameter parameter, CodeBlocker code) { code.Write($"{MapToCSType(parameter.Type ?? new TypeReference(UnknownTypeName))} {parameter.Name}"); - if (parameter.IsOptional && !string.IsNullOrEmpty(parameter.DefaultValue)) + if (parameter.IsOptional && parameter.DefaultValue is Expression value) { - code.Write($" = {parameter.DefaultValue}"); + code.Write(" = "); + GenerateInternal(value, code); } } diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs index 56f7f51..1b9842d 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -1339,10 +1339,7 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, code.Write($"{parameter.Name ?? $"param{position}"} "); code.Write(SpellType(parameter.Type ?? new TypeReference(UnknownTypeName))); - if (parameter.IsOptional && !string.IsNullOrEmpty(parameter.DefaultValue)) - { - code.Write($" /* = {parameter.DefaultValue} */"); - } + AppendDefaultValueComment(parameter, code); } /// diff --git a/Coder/Languages/RustGenerator.cs b/Coder/Languages/RustGenerator.cs index 73f5e9f..e6db1ed 100644 --- a/Coder/Languages/RustGenerator.cs +++ b/Coder/Languages/RustGenerator.cs @@ -1548,10 +1548,7 @@ protected override void GenerateParameter(Parameter parameter, CodeBlocker code, code.Write($"{parameter.Name ?? $"param{position}"}: "); code.Write(SpellType(parameter.Type ?? new TypeReference(UnknownTypeName))); - if (parameter.IsOptional && !string.IsNullOrEmpty(parameter.DefaultValue)) - { - code.Write($" /* = {parameter.DefaultValue} */"); - } + AppendDefaultValueComment(parameter, code); } /// diff --git a/Coder/Languages/StandardLanguageGenerator.cs b/Coder/Languages/StandardLanguageGenerator.cs index b3a22e2..5250dc9 100644 --- a/Coder/Languages/StandardLanguageGenerator.cs +++ b/Coder/Languages/StandardLanguageGenerator.cs @@ -1100,14 +1100,38 @@ protected void GenerateClassMembers(ClassDeclaration classDecl, CodeBlocker code /// A default value is how every target language expresses ; /// none of them has separate syntax for it. /// - protected static void AppendDefaultValue(Parameter parameter, CodeBlocker code) + protected void AppendDefaultValue(Parameter parameter, CodeBlocker code) { Ensure.NotNull(parameter); Ensure.NotNull(code); - if (parameter.IsOptional && !string.IsNullOrEmpty(parameter.DefaultValue)) + if (parameter.IsOptional && parameter.DefaultValue is Expression value) { - code.Write($" = {parameter.DefaultValue}"); + code.Write(" = "); + GenerateInternal(value, code); + } + } + + /// + /// Appends a parameter's default value as a comment, for a language with no default arguments. + /// + /// The parameter whose default to note. + /// The writer to emit into. + /// + /// A */ in the value would end the comment early and leave the rest of it as source, so + /// it is broken apart. + /// + protected void AppendDefaultValueComment(Parameter parameter, CodeBlocker code) + { + Ensure.NotNull(parameter); + Ensure.NotNull(code); + + if (parameter.IsOptional && parameter.DefaultValue is Expression value) + { + using CodeBlocker inline = CodeBlocker.Create(IndentString); + GenerateInternal(value, inline); + string spelled = inline.ToString().TrimEnd('\r', '\n').Replace("*/", "* /", StringComparison.Ordinal); + code.Write($" /* = {spelled} */"); } } diff --git a/Coder/Serialization/YamlDeserializer.cs b/Coder/Serialization/YamlDeserializer.cs index a4a7fb1..0f1c1f8 100644 --- a/Coder/Serialization/YamlDeserializer.cs +++ b/Coder/Serialization/YamlDeserializer.cs @@ -321,7 +321,7 @@ private EntryPoint DeserializeEntryPoint(object? nodeData) return entryPoint; } - private static void DeserializeFunctionParameters(FunctionDeclaration funcDecl, Dictionary dict) + private void DeserializeFunctionParameters(FunctionDeclaration funcDecl, Dictionary dict) { if (!dict.TryGetValue("parameters", out object? paramsObj) || paramsObj is not List paramsList) { @@ -1131,7 +1131,7 @@ private void DeserializeKeyedChildren(AstCompositeNode node, Dictionary dict) @@ -1143,7 +1143,11 @@ private static Parameter DeserializeParameter(object? nodeData) { param.IsOptional = isOptional; - param.DefaultValue = ReadString(dict, "defaultValue"); + param.DefaultValue = dict.TryGetValue("defaultValue", out object? defaultObj) + && defaultObj is Dictionary defaultDict + && defaultDict.Count > 0 + ? DeserializeNode(defaultDict.First().Key.ToString() ?? string.Empty, defaultDict.First().Value) as Expression + : Parameter.ParseDefaultValue(ReadString(dict, "defaultValue")); } // Parse metadata if present diff --git a/Coder/Serialization/YamlSerializer.cs b/Coder/Serialization/YamlSerializer.cs index e711843..6df5e40 100644 --- a/Coder/Serialization/YamlSerializer.cs +++ b/Coder/Serialization/YamlSerializer.cs @@ -808,9 +808,18 @@ private static void SerializeParameter(Parameter param, Dictionary defaultData = []; + SerializeNode(param.DefaultValue, defaultData); + nodeData["defaultValue"] = defaultData; } } } diff --git a/README.md b/README.md index 93e6937..3810b62 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,9 @@ function.Parameters.Add(new Parameter("b", "int")); function.Parameters.Add(new Parameter("debug", "bool") { IsOptional = true, - DefaultValue = "False" + // An expression, not source text: each generator spells it its own way + // (False in Python, false in C#, C++ and JavaScript). + DefaultValue = Literal.Bool(false) }); // Add function body @@ -283,7 +285,7 @@ functionDeclaration: - name: debug type: bool isOptional: true - defaultValue: False + defaultValue: false body: - returnStatement: expression: diff --git a/docs/design.md b/docs/design.md index f894106..139a296 100644 --- a/docs/design.md +++ b/docs/design.md @@ -34,7 +34,7 @@ functionDeclaration: - name: "include_debug" type: "bool" isOptional: true - defaultValue: "False" + defaultValue: false body: - returnStatement: leaf: