Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Coder.Test/Languages/CSharpConstantLocalTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests that a constant local is written as a C# <c>const</c> only when C# can hold it as one.
/// </summary>
/// <remarks>
/// A C# <c>const</c> local must be a compile-time constant, so <c>const int doubled = (n * 2);</c> 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.
/// </remarks>
[TestClass]
public class CSharpConstantLocalTests
{
/// <summary>
/// Tests that a constant computed from a parameter or a call is not written as <c>const</c>.
/// </summary>
[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);
}

/// <summary>
/// Tests that a constant holding a literal is still written as <c>const</c>.
/// </summary>
[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);
}
}
9 changes: 6 additions & 3 deletions Coder/Languages/CSharpGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ");
}
Expand Down
20 changes: 0 additions & 20 deletions Coder/Languages/GoGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@
/// holds one.
/// </remarks>
protected override string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "math.NaN()" : value > 0 ? "math.Inf(1)" : "math.Inf(-1)";

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 272 in Coder/Languages/GoGenerator.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <inheritdoc/>
/// <remarks>
Expand Down Expand Up @@ -1331,26 +1331,6 @@
/// <returns>True when Go will hold it as a constant.</returns>
private static bool IsConstant(bool wanted, AstNode? value) => wanted && IsCompileTimeValue(value);

/// <summary>
/// Reports whether a value is one Go will let a <c>const</c> hold.
/// </summary>
/// <param name="value">The value to test.</param>
/// <returns>True when it is.</returns>
/// <remarks>
/// 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.
/// </remarks>
private static bool IsCompileTimeValue(AstNode? value) =>
value is LiteralExpression<string>
or LiteralExpression<int>
or LiteralExpression<bool>
or LiteralExpression<double>
or LiteralExpression<float>
or LiteralExpression<long>
or AstLeafNode<string>
or AstLeafNode<int>
or AstLeafNode<bool>;

/// <summary>
/// Writes what a declaration starts at, giving a bare list the declaration's own type.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions Coder/Languages/LanguageGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@
}
}

foreach (object item in expected)

Check warning on line 372 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 372 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!emittedPreambleItems!.Contains(item))
{
Expand All @@ -387,7 +387,7 @@
/// Read off the nodes' public properties rather than a list of node types, so a node added to the
/// AST is walked without anybody remembering to add it here.
/// </remarks>
protected static IEnumerable<AstNode> SelfAndDescendants(AstNode root)

Check warning on line 390 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

Check warning on line 390 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.
{
HashSet<AstNode> visited = new(ReferenceEqualityComparer.Instance);
Stack<AstNode> pending = new();
Expand Down Expand Up @@ -448,6 +448,28 @@
protected static bool ContainsNonFiniteDouble(SourceFile file) =>
SelfAndDescendants(file).Any(node => node is LiteralExpression<double> literal && !double.IsFinite(literal.Value));

/// <summary>
/// Reports whether a value is one the compiler evaluates, and so one a constant can hold.
/// </summary>
/// <param name="value">The value to test.</param>
/// <returns>True when it is.</returns>
/// <remarks>
/// A literal, and nothing else. Go's constants are the untyped ones the compiler evaluates, and a
/// C# <c>const</c> 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.
/// </remarks>
protected static bool IsCompileTimeValue(AstNode? value) =>
value is LiteralExpression<string>
or LiteralExpression<int>
or LiteralExpression<bool>
or LiteralExpression<double>
or LiteralExpression<float>
or LiteralExpression<long>
or AstLeafNode<string>
or AstLeafNode<int>
or AstLeafNode<bool>;

/// <summary>
/// Writes down the types a declaration is written over, for a target that has no generics.
/// </summary>
Expand Down Expand Up @@ -1128,7 +1150,7 @@
/// the generator adds through <see cref="RequiredImports"/>.
/// </remarks>
protected virtual string SpellNonFiniteDouble(double value) =>
double.IsNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity";

Check warning on line 1153 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

Check warning on line 1153 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Extract this nested ternary operation into an independent statement.

/// <summary>
/// Formats a single-precision value so that every target reads it as floating-point.
Expand Down Expand Up @@ -1193,7 +1215,7 @@
string? origin = null;
long offset = 0;

foreach (EnumMember member in enumDecl.Members)

Check warning on line 1218 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)

Check warning on line 1218 in Coder/Languages/LanguageGeneratorBase.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loop should be simplified by calling Select(member => member.Value)
{
if (member.Value is not null)
{
Expand Down
Loading