diff --git a/Coder.Test/Languages/CSharpConstantLocalTests.cs b/Coder.Test/Languages/CSharpConstantLocalTests.cs new file mode 100644 index 0000000..ad7191b --- /dev/null +++ b/Coder.Test/Languages/CSharpConstantLocalTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Languages; + +using ktsu.Coder.Ast; +using ktsu.Coder.Languages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that a constant local is written as a C# const only when C# can hold it as one. +/// +/// +/// A C# const local must be a compile-time constant, so const int doubled = (n * 2); is +/// error CS0133. C# has no readonly local, so a constant computed at run time is an ordinary local, +/// the same as an inferred one already was. +/// +[TestClass] +public class CSharpConstantLocalTests +{ + /// + /// Tests that a constant computed from a parameter or a call is not written as const. + /// + [TestMethod] + public void NonLiteralConstant_IsAPlainLocal() + { + FunctionDeclaration twice = new("Twice") { ReturnType = "int", IsStatic = true }; + twice.Parameters.Add(new Parameter("n", "int")); + BinaryExpression product = new(new VariableReference("n"), BinaryOperator.Multiply, Literal.Number(2)); + twice.Body.Add(new VariableDeclaration("doubled", "int", product) { IsConstant = true }); + CallExpression call = new("Math.Abs"); + call.Arguments.Add(new VariableReference("doubled")); + twice.Body.Add(new VariableDeclaration("size", "int", call) { IsConstant = true }); + twice.Body.Add(new ReturnStatement(new VariableReference("size"))); + + string code = new CSharpGenerator().Generate(twice); + + Assert.Contains("int doubled = (n * 2);", code, StringComparison.Ordinal, code); + Assert.Contains("int size = Math.Abs(doubled);", code, StringComparison.Ordinal, code); + Assert.DoesNotContain("const", code, StringComparison.Ordinal, code); + } + + /// + /// Tests that a constant holding a literal is still written as const. + /// + [TestMethod] + public void LiteralConstant_IsConst() + { + FunctionDeclaration limit = new("Limit") { ReturnType = "int", IsStatic = true }; + limit.Body.Add(new VariableDeclaration("max", "int", Literal.Number(7)) { IsConstant = true }); + limit.Body.Add(new ReturnStatement(new VariableReference("max"))); + + string code = new CSharpGenerator().Generate(limit); + + Assert.Contains("const int max = 7;", code, StringComparison.Ordinal, code); + } +} diff --git a/Coder/Languages/CSharpGenerator.cs b/Coder/Languages/CSharpGenerator.cs index 772f358..5d4b552 100644 --- a/Coder/Languages/CSharpGenerator.cs +++ b/Coder/Languages/CSharpGenerator.cs @@ -900,9 +900,12 @@ private void GenerateVariableDeclaration(VariableDeclaration varDecl, CodeBlocke code.Write($"{modifier} "); } - // A C# constant must name its type, so an inferred one stays a plain declaration rather than - // becoming source that does not compile. - if (varDecl.IsConstant && !string.Equals(type, "var", StringComparison.Ordinal)) + // A C# constant must name its type and hold a compile-time value, so an inferred one, or one + // computed from a parameter or a call, stays a plain declaration rather than becoming source + // that does not compile. C# has no readonly local to say the rest with. + if (varDecl.IsConstant + && !string.Equals(type, "var", StringComparison.Ordinal) + && IsCompileTimeValue(varDecl.InitialValue)) { code.Write("const "); } diff --git a/Coder/Languages/GoGenerator.cs b/Coder/Languages/GoGenerator.cs index 01353d7..c5cfcf9 100644 --- a/Coder/Languages/GoGenerator.cs +++ b/Coder/Languages/GoGenerator.cs @@ -1331,26 +1331,6 @@ private void WriteStorageKeyword(string? name, bool wanted, AstNode? value, Code /// True when Go will hold it as a constant. private static bool IsConstant(bool wanted, AstNode? value) => wanted && IsCompileTimeValue(value); - /// - /// Reports whether a value is one Go will let a const hold. - /// - /// The value to test. - /// True when it is. - /// - /// A literal, and nothing else. Go's constants are the untyped ones the compiler evaluates, which - /// rules out every value with a field or an element in it however fixed its contents are. - /// - private static bool IsCompileTimeValue(AstNode? value) => - value is LiteralExpression - or LiteralExpression - or LiteralExpression - or LiteralExpression - or LiteralExpression - or LiteralExpression - or AstLeafNode - or AstLeafNode - or AstLeafNode; - /// /// Writes what a declaration starts at, giving a bare list the declaration's own type. /// diff --git a/Coder/Languages/LanguageGeneratorBase.cs b/Coder/Languages/LanguageGeneratorBase.cs index 5a72e9d..0e2c06f 100644 --- a/Coder/Languages/LanguageGeneratorBase.cs +++ b/Coder/Languages/LanguageGeneratorBase.cs @@ -448,6 +448,28 @@ protected static IEnumerable SelfAndDescendants(AstNode root) protected static bool ContainsNonFiniteDouble(SourceFile file) => SelfAndDescendants(file).Any(node => node is LiteralExpression literal && !double.IsFinite(literal.Value)); + /// + /// Reports whether a value is one the compiler evaluates, and so one a constant can hold. + /// + /// The value to test. + /// True when it is. + /// + /// A literal, and nothing else. Go's constants are the untyped ones the compiler evaluates, and a + /// C# const local must be a compile-time constant, which rules out a parameter, a call, an + /// expression over either, and every value with a field or an element in it however fixed its + /// contents are. Sharing the rule keeps the two generators from drifting apart on it. + /// + protected static bool IsCompileTimeValue(AstNode? value) => + value is LiteralExpression + or LiteralExpression + or LiteralExpression + or LiteralExpression + or LiteralExpression + or LiteralExpression + or AstLeafNode + or AstLeafNode + or AstLeafNode; + /// /// Writes down the types a declaration is written over, for a target that has no generics. ///