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
65 changes: 65 additions & 0 deletions Coder.Test/Serialization/NonFiniteDoubleSerializationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Coder.Test.Serialization;

using ktsu.Coder.Ast;
using ktsu.Coder.Serialization;

/// <summary>
/// Tests that infinite and NaN double literals survive a YAML round trip.
///
/// YamlDotNet writes them with the core schema's spellings (<c>.inf</c>, <c>-.inf</c>, <c>.nan</c>),
/// which <see cref="double.TryParse(string?, System.Globalization.NumberStyles, IFormatProvider?, out double)"/>
/// cannot read. The deserializer used to drop the literal, so a declaration initialised to infinity
/// came back with no initialiser (ktsu-dev/Coder#78).
/// </summary>
[TestClass]
public class NonFiniteDoubleSerializationTests
{
[TestMethod]
[DataRow(double.PositiveInfinity)]
[DataRow(double.NegativeInfinity)]
[DataRow(double.NaN)]
public void RoundTrip_NonFiniteLiteral_KeepsValue(double value)
{
string yaml = new YamlSerializer().Serialize(Literal.DecimalValue(value));

AstNode? deserialized = new YamlDeserializer().Deserialize(yaml);

LiteralExpression<double> literal = Assert.IsInstanceOfType<LiteralExpression<double>>(deserialized);
Assert.AreEqual(value, literal.Value);
}

[TestMethod]
public void RoundTrip_VariableInitialisedToInfinity_KeepsInitialiser()
{
VariableDeclaration declaration = new("inf", "double", Literal.DecimalValue(double.PositiveInfinity));
string yaml = new YamlSerializer().Serialize(declaration);

AstNode? deserialized = new YamlDeserializer().Deserialize(yaml);

VariableDeclaration roundTripped = Assert.IsInstanceOfType<VariableDeclaration>(deserialized);
LiteralExpression<double> initialiser = Assert.IsInstanceOfType<LiteralExpression<double>>(roundTripped.InitialValue);
Assert.AreEqual(double.PositiveInfinity, initialiser.Value);
}

[TestMethod]
[DataRow(".inf", double.PositiveInfinity)]
[DataRow(".Inf", double.PositiveInfinity)]
[DataRow(".INF", double.PositiveInfinity)]
[DataRow("+.inf", double.PositiveInfinity)]
[DataRow("-.inf", double.NegativeInfinity)]
[DataRow("-.INF", double.NegativeInfinity)]
[DataRow(".nan", double.NaN)]
[DataRow(".NaN", double.NaN)]
[DataRow(".NAN", double.NaN)]
public void Deserialize_CoreSchemaNonFiniteSpelling_ReadsValue(string spelling, double expected)
{
string yaml = new YamlSerializer().Serialize(Literal.DecimalValue(1.5)).Replace("1.5", spelling, StringComparison.Ordinal);

AstNode? deserialized = new YamlDeserializer().Deserialize(yaml);

LiteralExpression<double> literal = Assert.IsInstanceOfType<LiteralExpression<double>>(deserialized);
Assert.AreEqual(expected, literal.Value);
}
}
32 changes: 31 additions & 1 deletion Coder/Serialization/YamlDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@
}
}

if (dict.TryGetValue("expectedType", out object? typeObj))

Check warning on line 543 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'expectedType' 6 times.

Check warning on line 543 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'expectedType' 6 times.

Check warning on line 543 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'expectedType' 6 times.

Check warning on line 543 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Define a constant instead of using this literal 'expectedType' 6 times.
{
callExpr.ExpectedType = typeObj?.ToString();
}
Expand Down Expand Up @@ -766,6 +766,36 @@
/// <summary>The key a node's single value is written under.</summary>
private const string ValueKey = "value";

/// <summary>
/// Parses a YAML float scalar, including the core schema's spellings of the non-finite values.
/// </summary>
/// <param name="text">The scalar's text.</param>
/// <param name="value">The parsed value.</param>
/// <returns><see langword="true"/> if <paramref name="text"/> is a float.</returns>
/// <remarks>
/// The serializer writes infinity and NaN as <c>.inf</c>, <c>-.inf</c> and <c>.nan</c>, which
/// <see cref="double.TryParse(string?, NumberStyles, IFormatProvider?, out double)"/> does not
/// read. Failing here drops the literal, so a declaration initialised to infinity would come back
/// with no initialiser at all (ktsu-dev/Coder#78).
/// </remarks>
private static bool TryParseDouble(string? text, out double value)
{
switch (text)
{
case ".inf" or ".Inf" or ".INF" or "+.inf" or "+.Inf" or "+.INF":
value = double.PositiveInfinity;
return true;
case "-.inf" or "-.Inf" or "-.INF":
value = double.NegativeInfinity;
return true;
case ".nan" or ".NaN" or ".NAN":
value = double.NaN;
return true;
default:
return double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
}

/// <summary>The key a declaration's documentation is written under.</summary>
private const string DocumentationKey = "documentation";

Expand Down Expand Up @@ -1147,7 +1177,7 @@
return returnStmt;
}

private BinaryExpression DeserializeBinaryExpression(object? nodeData)

Check warning on line 1180 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 1180 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.
{
BinaryExpression binaryExpr = new();
if (nodeData is Dictionary<object, object> dict)
Expand Down Expand Up @@ -1261,7 +1291,7 @@
"String" => new LiteralExpression<string>(value?.ToString() ?? string.Empty),
"Int32" when int.TryParse(value?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue) => new LiteralExpression<int>(intValue),
"Boolean" when bool.TryParse(value?.ToString(), out bool boolValue) => new LiteralExpression<bool>(boolValue),
"Double" when double.TryParse(value?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double doubleValue) => new LiteralExpression<double>(doubleValue),
"Double" when TryParseDouble(value?.ToString(), out double doubleValue) => new LiteralExpression<double>(doubleValue),
_ => null,
};

Expand Down Expand Up @@ -1294,7 +1324,7 @@
return varRef;
}

private VariableDeclaration DeserializeVariableDeclaration(object? nodeData)

Check warning on line 1327 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 1327 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.
{
VariableDeclaration varDecl = new();
if (nodeData is Dictionary<object, object> dict)
Expand Down Expand Up @@ -1340,7 +1370,7 @@
return varDecl;
}

private AssignmentStatement DeserializeAssignmentStatement(object? nodeData)

Check warning on line 1373 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 1373 in Coder/Serialization/YamlDeserializer.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.
{
AssignmentStatement assignment = new();
if (nodeData is Dictionary<object, object> dict)
Expand Down
Loading