diff --git a/Coder.Test/Serialization/NonFiniteDoubleSerializationTests.cs b/Coder.Test/Serialization/NonFiniteDoubleSerializationTests.cs new file mode 100644 index 0000000..b4b4674 --- /dev/null +++ b/Coder.Test/Serialization/NonFiniteDoubleSerializationTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Coder.Test.Serialization; + +using ktsu.Coder.Ast; +using ktsu.Coder.Serialization; + +/// +/// Tests that infinite and NaN double literals survive a YAML round trip. +/// +/// YamlDotNet writes them with the core schema's spellings (.inf, -.inf, .nan), +/// which +/// cannot read. The deserializer used to drop the literal, so a declaration initialised to infinity +/// came back with no initialiser (ktsu-dev/Coder#78). +/// +[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 literal = Assert.IsInstanceOfType>(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(deserialized); + LiteralExpression initialiser = Assert.IsInstanceOfType>(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 literal = Assert.IsInstanceOfType>(deserialized); + Assert.AreEqual(expected, literal.Value); + } +} diff --git a/Coder/Serialization/YamlDeserializer.cs b/Coder/Serialization/YamlDeserializer.cs index bf22676..0a84ec1 100644 --- a/Coder/Serialization/YamlDeserializer.cs +++ b/Coder/Serialization/YamlDeserializer.cs @@ -766,6 +766,36 @@ private FieldDeclaration DeserializeFieldDeclaration(object? nodeData) /// The key a node's single value is written under. private const string ValueKey = "value"; + /// + /// Parses a YAML float scalar, including the core schema's spellings of the non-finite values. + /// + /// The scalar's text. + /// The parsed value. + /// if is a float. + /// + /// The serializer writes infinity and NaN as .inf, -.inf and .nan, which + /// 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). + /// + 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); + } + } + /// The key a declaration's documentation is written under. private const string DocumentationKey = "documentation"; @@ -1261,7 +1291,7 @@ private UnaryExpression DeserializeUnaryExpression(object? nodeData) "String" => new LiteralExpression(value?.ToString() ?? string.Empty), "Int32" when int.TryParse(value?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue) => new LiteralExpression(intValue), "Boolean" when bool.TryParse(value?.ToString(), out bool boolValue) => new LiteralExpression(boolValue), - "Double" when double.TryParse(value?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double doubleValue) => new LiteralExpression(doubleValue), + "Double" when TryParseDouble(value?.ToString(), out double doubleValue) => new LiteralExpression(doubleValue), _ => null, };