Created
March 30, 2021 21:24
-
-
Save SingleAccretion/82849e05f0719ec5433786c16d1d3484 to your computer and use it in GitHub Desktop.
Generating tests for value numering checked ops
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using Microsoft.Toolkit.HighPerformance.Extensions; | |
| using System; | |
| using System.Collections.Generic; | |
| using System.ComponentModel; | |
| using System.Globalization; | |
| using System.Linq; | |
| using System.Numerics; | |
| #pragma warning disable | |
| namespace ThrowawayTesting | |
| { | |
| public unsafe class Program | |
| { | |
| public static void Main() | |
| { | |
| static string Esc(string str) => str.Replace("\"", "\\\""); | |
| const string Indent = " "; | |
| string indent = ""; | |
| void OpenScope(string header) | |
| { | |
| Out(header); | |
| Out("{"); | |
| indent += Indent; | |
| } | |
| void CloseScope() | |
| { | |
| indent = indent[..^Indent.Length]; | |
| Out("}"); | |
| } | |
| void Out(string line = "") | |
| { | |
| line = string.IsNullOrWhiteSpace(line) ? "" : indent + line; | |
| Console.WriteLine(line); | |
| } | |
| void BreakFlow() | |
| { | |
| Out(); | |
| Out($"if (BreakUpFlow())"); | |
| Out(Indent + "return;"); | |
| Out(); | |
| } | |
| void CallFunc(string name, Action body, string retType = "void", string mods = "static", params (string Decl, string Val)[] args) | |
| { | |
| Out($"{name}({string.Join(", ", args.Select(x => x.Val))});"); | |
| Out("[MethodImpl(MethodImplOptions.NoInlining)]"); | |
| OpenScope($"{(mods is "" ? "" : $"{mods} ")}{retType} {name}({string.Join(", ", args.Select(x => x.Decl))})"); | |
| body(); | |
| CloseScope(); | |
| } | |
| void ConfirmException(string exc, string failMsg, Action tryDo) | |
| { | |
| Out("_counter++;"); | |
| OpenScope("try"); | |
| tryDo(); | |
| CloseScope(); | |
| Out($"catch ({exc}) {{ _counter--; }}"); | |
| Out("if (_counter != 100)"); | |
| Echo(failMsg, Indent); | |
| } | |
| void ConfirmEqual(string expr, string val, string expl = null, string msgOvr = null) | |
| { | |
| OpenScope($"if ({expr} != {val})"); | |
| expl = expl is null ? "" : $"{expl} "; | |
| var msg = $"{expl}'{expr}' was evaluted to '{{{expr}}}'. Expected: '{{{val}}}'."; | |
| if (msgOvr is not null) | |
| { | |
| msg = msgOvr; | |
| } | |
| Out($"Console.WriteLine($\"{msg}\");"); | |
| Out("_counter++;"); | |
| CloseScope(); | |
| } | |
| void Echo(string msg, string ind = "") | |
| { | |
| Out(ind + $"Console.WriteLine(\"{Esc(msg)}\");"); | |
| } | |
| void DeclVar(string type, string name, string value) => Out($"{type} {name} = {value};"); | |
| Out("// Licensed to the .NET Foundation under one or more agreements."); | |
| Out("// The .NET Foundation licenses this file to you under the MIT license."); | |
| Out(); | |
| Out("using System;"); | |
| Out("using System.Runtime.CompilerServices;"); | |
| Out(); | |
| #if false | |
| static string Up(string var) => var[0..1].ToUpper() + var[1..]; | |
| var types = new[] { "int", "uint", "long", "ulong" }; | |
| string RtType(string type) => type switch | |
| { | |
| "int" => "Int32", | |
| "uint" => "UInt32", | |
| "long" => "Int64", | |
| "ulong" => "UInt64", | |
| _ => throw new Exception($"Unknown type: '{type}'") | |
| }; | |
| var ops = new (string Value, string Name, string Verb)[] | |
| { | |
| ("+", "addition", "Plus"), | |
| ("-", "subtraction", "Minus"), | |
| ("*", "multiplication", "MultipliedBy"), | |
| }; | |
| (string Name, string SourceValue)[] Vals(string type) => | |
| new[] | |
| { | |
| ("min", "type.MinValue"), | |
| ("minusHalf", "type.MinValue / 2"), | |
| ("minusOne", "-1"), | |
| ("zero", "0"), | |
| ("one", "1"), | |
| ("half", "type.MaxValue / 2"), | |
| ("max", "type.MaxValue") | |
| }[(type.Contains('u') ? 3 : 0)..]; | |
| static BigInteger RawVal(string var, string type) => (var, type) switch | |
| { | |
| ("min", "long") => long.MinValue, | |
| ("min", "int") => int.MinValue, | |
| ("min", "uint" or "ulong") => 0, | |
| ("minusHalf", _) => RawVal("min", type) / 2, | |
| ("minusOne", _) => -1, | |
| ("zero", _) => 0, | |
| ("one", _) => 1, | |
| ("half", _) => RawVal("max", type) / 2, | |
| ("max", "int") => int.MaxValue, | |
| ("max", "uint") => uint.MaxValue, | |
| ("max", "long") => long.MaxValue, | |
| ("max", "ulong") => ulong.MaxValue, | |
| _ => throw new Exception($"Unknown var for type: {(var, type)}") | |
| }; | |
| static bool Ovfls(BigInteger rawVal1, BigInteger rawVal2, string op, string type) | |
| { | |
| var l = RawVal("min", type); | |
| var h = RawVal("max", type); | |
| bool InRange(BigInteger val) => l <= val && val <= h; | |
| return op switch | |
| { | |
| "+" => !InRange(rawVal1 + rawVal2), | |
| "-" => !InRange(rawVal1 - rawVal2), | |
| "*" => !InRange(rawVal1 * rawVal2), | |
| _ => throw new Exception($"Unknown op: '{op}'") | |
| }; | |
| } | |
| OpenScope("public class ValueNumberingCheckedIntegerArithemticWithConstants"); | |
| Out($"private static int _global = 0;"); | |
| Out("private static int _counter = 100;"); | |
| Out(); | |
| OpenScope("public static int Main()"); | |
| Out("RuntimeHelpers.RunClassConstructor(typeof(ValueNumberingCheckedIntegerArithemticWithConstants).TypeHandle);"); | |
| foreach (var type in types) | |
| { | |
| Out($"Test{RtType(type)}();"); | |
| } | |
| Out(); | |
| Out("return _counter;"); | |
| CloseScope(); | |
| Out(); | |
| Out("[MethodImpl(MethodImplOptions.NoInlining)]"); | |
| Out("private static int IncrementGlobal() => ++_global;"); | |
| Out(); | |
| Out("[MethodImpl(MethodImplOptions.NoInlining)]"); | |
| Out("private static bool BreakUpFlow() => false;"); | |
| Out(); | |
| Out("[MethodImpl(MethodImplOptions.NoInlining)]"); | |
| Out("private static void EvalArg<T>(T arg) { }"); | |
| Out(); | |
| foreach (var type in types) | |
| { | |
| OpenScope($"private static void Test{RtType(type)}()"); | |
| var vals = Vals(type); | |
| CallFunc($"ConfirmAdditionIdentities", args: ($"{type} value", "42"), body: () => | |
| { | |
| var expl = $"Addition identity for {type}"; | |
| DeclVar(type, "zero", "0"); | |
| BreakFlow(); | |
| ConfirmEqual("checked(value + zero)", "value", expl); | |
| Out(); | |
| ConfirmEqual("checked(zero + value)", "value", expl); | |
| }); | |
| CallFunc($"ConfirmSubtractionIdentities", args: ($"{type} value", "42"), body: () => | |
| { | |
| var expl = $"Subtraction identity for {type}"; | |
| DeclVar(type, "zero", "0"); | |
| BreakFlow(); | |
| ConfirmEqual("checked(value - zero)", "value", expl); | |
| Out(); | |
| ConfirmEqual("checked(value - value)", "0", expl); | |
| }); | |
| CallFunc($"ConfirmMultiplicationIdentities", args: ($"{type} value", "42"), body: () => | |
| { | |
| var expl = $"Multiplication identity for {type}"; | |
| DeclVar(type, "zero", "0"); | |
| DeclVar(type, "one", "1"); | |
| BreakFlow(); | |
| ConfirmEqual("checked(value * zero)", "0", expl); | |
| Out(); | |
| ConfirmEqual("checked(zero * value)", "0", expl); | |
| Out(); | |
| ConfirmEqual("checked(value * one)", "value", expl); | |
| Out(); | |
| ConfirmEqual("checked(one * value)", "value", expl); | |
| }); | |
| foreach (var (op, opName, verb) in ops) | |
| { | |
| foreach (var (var1, val1) in vals) | |
| { | |
| foreach (var (var2, val2) in vals) | |
| { | |
| var rawVal1 = RawVal(var1, type); | |
| var rawVal2 = RawVal(var2, type); | |
| var ovfls = Ovfls(rawVal1, rawVal2, op, type); | |
| var needsParens = op is "*" && val2.Contains(' '); | |
| var outVal1 = val1.Replace("type", type); | |
| var outVal2 = val2.Replace("type", type); | |
| if (needsParens) | |
| { | |
| outVal2 = $"({outVal2})"; | |
| } | |
| void DeclVars() | |
| { | |
| DeclVar(type, var1, outVal1); | |
| if (var2 != var1) | |
| { | |
| DeclVar(type, var2, outVal2); | |
| } | |
| } | |
| var expr = $"checked({var1} {op} {var2})"; | |
| var val = $"{outVal1} {op} {outVal2}"; | |
| if (!ovfls) | |
| { | |
| var fldCase = $"{Up(var1)}{verb}{Up(var2)}"; | |
| CallFunc($"Confirm{fldCase}IsFoldedCorrectly", () => | |
| { | |
| DeclVars(); | |
| BreakFlow(); | |
| OpenScope($"if ({expr} != {val})"); | |
| Out($"Console.WriteLine($\"'{expr}' was evaluted to '{{{expr}}}'. Expected: '{{{val}}}'.\");"); | |
| Out("_counter++;"); | |
| CloseScope(); | |
| }); | |
| #region Confirming side effects are not optimized out | |
| /* | |
| CallFunc($"ConfirmFolding{fldCase}DoesNotOptimizeOutNullChecks", args: ("string obj", "null"), body: () => | |
| { | |
| DeclVars(); | |
| Out(); | |
| var cast = type is "ulong" ? "(ulong)" : ""; | |
| var expr = $"checked({var1} {op} (unchecked({var2} + {cast}(obj.Length * \"\".Length))))"; | |
| ConfirmException("NullReferenceException", $"'{expr}' did not throw NullReferenceException on 'null' 'obj'", () => | |
| { | |
| Out($"_ = {expr};"); | |
| }); | |
| }); | |
| CallFunc($"ConfirmFolding{fldCase}DoesNotOptimizeOutPossibleDivisionByZero", args: ($"{type} zeroValue", "0"), body: () => | |
| { | |
| DeclVars(); | |
| Out(); | |
| var cast = type is "ulong" ? "(ulong)" : ""; | |
| var expr = $"checked({var1} * unchecked(({var2} / {cast}zeroValue - {var2} / {cast}zeroValue) | {var2}))"; | |
| ConfirmException("DivideByZeroException", $"'{expr}' did not throw DivideByZeroException on '0' 'zeroValue'", () => | |
| { | |
| Out($"_ = {expr};"); | |
| }); | |
| Out(); | |
| expr = expr.Replace("/", "%"); | |
| ConfirmException("DivideByZeroException", $"'{expr}' did not throw DivideByZeroException on '0' 'zeroValue'", () => | |
| { | |
| Out($"_ = {expr};"); | |
| }); | |
| }); | |
| CallFunc($"ConfirmFolding{fldCase}DoesNotOptimizaOutGlobalAssignements", () => | |
| { | |
| DeclVars(); | |
| BreakFlow(); | |
| var cast = type is "ulong" ? "(ulong)" : ""; | |
| var expr = $"checked({var1} {op} unchecked({cast}(Interlocked.Increment(ref _global) & \"\".Length) | {var2}))"; | |
| Out($"EvalArg({expr});"); | |
| Out(); | |
| OpenScope("if (_global != 1)"); | |
| Echo($"Optimization of '{expr}' lead to the assignment being removed"); | |
| Out("_counter++;"); | |
| CloseScope(); | |
| Out(); | |
| Out("_global = 0;"); | |
| }); | |
| CallFunc($"ConfirmFolding{fldCase}DoesNotOptimizeOutCalls", () => | |
| { | |
| DeclVars(); | |
| BreakFlow(); | |
| var cast = type is "ulong" ? "(ulong)" : ""; | |
| var expr = $"checked({var1} {op} unchecked(({var1} + ({cast}(IncrementGlobal() & \"\".Length)) - {var1}) | {var2}))"; | |
| Out($"EvalArg({expr});"); | |
| Out(); | |
| OpenScope("if (_global != 1)"); | |
| Echo($"Optimization of '{expr}' lead to the assignment being removed"); | |
| Out("_counter++;"); | |
| CloseScope(); | |
| Out(); | |
| Out("_global = 0;"); | |
| }); | |
| */ | |
| #endregion | |
| } | |
| else | |
| { | |
| CallFunc($"Confirm{Up(var1)}{verb}{Up(var2)}Overflows", () => | |
| { | |
| DeclVars(); | |
| Out(); | |
| ConfirmException("OverflowException", $"'{expr}' did not throw OverflowException.", () => | |
| { | |
| Out($"_ = {expr};"); | |
| }); | |
| }); | |
| } | |
| } | |
| } | |
| if (opName != ops[^1].Name) | |
| { | |
| Out(); | |
| } | |
| } | |
| CloseScope(); | |
| if (type != types[^1]) | |
| { | |
| Out(); | |
| } | |
| } | |
| CloseScope(); | |
| #else | |
| static PreciseValue Cast(PreciseValue value, string csType) => csType switch | |
| { | |
| "float" => value.CastToSingle(), | |
| "double" => value.CastToDouble(), | |
| _ => value.CastToInteger() | |
| }; | |
| static string ToCamelCase(string name) => name[0..1].ToLower() + name[1..]; | |
| static bool InRange(PreciseValue min, PreciseValue max, PreciseValue value) | |
| { | |
| if (value.IsNaN()) | |
| { | |
| return false; | |
| } | |
| if (value.IsFloatingPoint && min.IsInteger && max.IsInteger) | |
| { | |
| if (!value.IsFinite()) | |
| { | |
| return false; | |
| } | |
| value = value.CastToInteger(); | |
| } | |
| return min <= value && value <= max; | |
| } | |
| static bool IsFloatingPoint(string csType) => csType is "float" or "double"; | |
| var anotherSingleNaN = BitConverter.Int32BitsToSingle(BitConverter.SingleToInt32Bits(float.NaN) + 1); | |
| var anotherDoubleNaN = BitConverter.Int64BitsToDouble(BitConverter.DoubleToInt64Bits(double.NaN) + 1); | |
| var types = new (string CsName, string RtName, PreciseValue Min, PreciseValue Max)[] | |
| { | |
| ("float", "Single", float.MinValue, float.MaxValue), | |
| ("double", "Double", double.MinValue, double.MaxValue), | |
| ("sbyte", "SByte", sbyte.MinValue, sbyte.MaxValue), | |
| ("byte", "Byte", byte.MinValue, byte.MaxValue), | |
| ("short", "Int16", short.MinValue, short.MaxValue), | |
| ("ushort", "UInt16", ushort.MinValue, ushort.MaxValue), | |
| ("int", "Int32", int.MinValue, int.MaxValue), | |
| ("uint", "UInt32", uint.MinValue, uint.MaxValue), | |
| ("long", "Int64", long.MinValue, long.MaxValue), | |
| ("ulong", "UInt64", ulong.MinValue, ulong.MaxValue) | |
| }; | |
| var values = new List<NamedValue>() | |
| { | |
| (0, "IntegerZero"), | |
| (-0.0f, "FloatMinusZero"), | |
| (0.0f, "FloatZero"), | |
| (float.NegativeInfinity, "FloatNegativeInfinity"), | |
| (float.PositiveInfinity, "FloatPositiveInfinity"), | |
| (float.NaN, "FloatNaN"), | |
| (anotherSingleNaN, "FloatAnotherNaN"), | |
| (float.MaxValue / 2, "FloatHalfOfMaxValue"), | |
| (float.MinValue / 2, "FloatHalfOfMinValue"), | |
| (-0.0d, "DoubleMinusZero"), | |
| (0.0d, "DoubleZero"), | |
| (double.NegativeInfinity, "DoubleNegativeInfinity"), | |
| (double.PositiveInfinity, "DoublePositiveInfinity"), | |
| (double.NaN, "DoubleNaN"), | |
| (anotherDoubleNaN, "DoubleAnotherNaN"), | |
| }; | |
| foreach (var (csType, rtType, min, max) in types) | |
| { | |
| void Add(NamedValue value) | |
| { | |
| if (!values.Where(x => !x.Value.IsNaN()).Any(x => | |
| x.Value.IsInteger == value.Value.IsInteger && | |
| x.Value.IsSingle == value.Value.IsSingle && | |
| x.Value.IsDouble == value.Value.IsDouble && | |
| x.Value == value.Value)) | |
| { | |
| values.Add(value); | |
| } | |
| } | |
| Add(new(min, $"{rtType}MinValue")); | |
| Add(new(max, $"{rtType}MaxValue")); | |
| if (!IsFloatingPoint(csType)) | |
| { | |
| foreach (var (toCsType, toRtType, _, _) in types) | |
| { | |
| foreach (var (value, name) in new[] { (min, "MinValue"), (max, "MaxValue") }) | |
| { | |
| var typeName = IsFloatingPoint(toCsType) ? toRtType : "Integer"; | |
| PreciseValue toValue = Cast(value, toCsType); | |
| if (value != long.MinValue) | |
| { | |
| while (!(toValue < Cast(value, toCsType))) | |
| { | |
| toValue = toValue.Decrement(); | |
| } | |
| Add(new(toValue, $"{typeName}OneDecrementUnder{rtType}{name}")); | |
| toValue = Cast(value, toCsType); | |
| while (!(toValue.CastToInteger() < value.CastToInteger())) | |
| { | |
| var intDec = Cast(toValue.CastToInteger().Decrement(), toCsType); | |
| if (intDec < toValue) | |
| { | |
| toValue = intDec; | |
| } | |
| else | |
| { | |
| toValue = toValue.Decrement(); | |
| } | |
| } | |
| Add(new(toValue, $"{typeName}OneFullDecrementUnder{rtType}{name}")); | |
| } | |
| if (value != ulong.MaxValue) | |
| { | |
| toValue = Cast(value, toCsType); | |
| while (!(toValue > Cast(value, toCsType))) | |
| { | |
| toValue = toValue.Increment(); | |
| } | |
| Add(new(toValue, $"{typeName}OneIncrementAbove{rtType}{name}")); | |
| toValue = Cast(value, toCsType); | |
| while (!(toValue.CastToInteger() > value.CastToInteger())) | |
| { | |
| var intInc = Cast(toValue.CastToInteger().Increment(), toCsType); | |
| if (intInc > toValue) | |
| { | |
| toValue = intInc; | |
| } | |
| else | |
| { | |
| toValue = toValue.Increment(); | |
| } | |
| } | |
| Add(new(toValue, $"{typeName}OneFullIncrementAbove{rtType}{name}")); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| OpenScope("public class ValueNumberingCheckedCastsOfConstants"); | |
| Out($"private static int _global = 0;"); | |
| Out("private static int _counter = 100;"); | |
| Out(); | |
| OpenScope("public static int Main()"); | |
| foreach (var (fromCsType, fromRtType, _, _) in types) | |
| { | |
| foreach (var (toCsType, toRtType, _, _) in types) | |
| { | |
| if (IsFloatingPoint(fromCsType) && IsFloatingPoint(toCsType)) | |
| { | |
| continue; | |
| } | |
| Out($"TestCasting{fromRtType}To{toRtType}();"); | |
| } | |
| } | |
| Out(); | |
| Out("return _counter;"); | |
| CloseScope(); | |
| Out(); | |
| Out("[MethodImpl(MethodImplOptions.NoInlining)]"); | |
| Out("private static bool BreakUpFlow() => false;"); | |
| Out(); | |
| foreach (var (fromCsType, fromRtType, fromMin, fromMax) in types) | |
| { | |
| bool FromCanRepresent(PreciseValue value) | |
| { | |
| if (value.IsSingle && fromCsType is not "float" || | |
| value.IsDouble && fromCsType is not "double") | |
| { | |
| return false; | |
| } | |
| return InRange(fromMin, fromMax, value); | |
| }; | |
| foreach (var (toCsType, toRtType, toMin, toMax) in types) | |
| { | |
| if (IsFloatingPoint(fromCsType) && IsFloatingPoint(toCsType)) | |
| { | |
| continue; | |
| } | |
| OpenScope($"private static void TestCasting{fromRtType}To{toRtType}()"); | |
| foreach (var (value, valueName) in values) | |
| { | |
| if (!FromCanRepresent(value)) | |
| { | |
| continue; | |
| } | |
| if (!InRange(toMin, toMax, Cast(value, fromCsType))) | |
| { | |
| ConfirmNoFolding(valueName, value); | |
| } | |
| else | |
| { | |
| ConfirmCorrectFolding(valueName, value, Cast(Cast(value, fromCsType), toCsType)); | |
| } | |
| } | |
| CloseScope(); | |
| if ((fromCsType, toCsType) != (types[^1].CsName, types[^1].CsName)) | |
| { | |
| Out(); | |
| } | |
| void ConfirmCorrectFolding(string fromName, PreciseValue fromValue, PreciseValue toValue) | |
| { | |
| var fromVar = ToCamelCase(fromName); | |
| CallFunc($"Confirm{fromName}CastTo{toRtType}IsFoldedCorrectly", body: () => | |
| { | |
| DeclVar(fromCsType, fromVar, $"{fromValue}"); | |
| BreakFlow(); | |
| var clarityCast = ""; | |
| if (fromValue.IsFloatingPoint != IsFloatingPoint(fromCsType)) | |
| { | |
| clarityCast = $"({fromCsType})"; | |
| } | |
| ConfirmEqual($"checked(({toCsType}){fromVar})", $"{toValue}", msgOvr: $"'({toCsType}){clarityCast}{fromValue}' was evaluted to '{{({toCsType}){fromVar}}}'. Expected: '{toValue}'."); | |
| }); | |
| } | |
| void ConfirmNoFolding(string fromName, PreciseValue fromValue) | |
| { | |
| if (!IsFloatingPoint(fromCsType)) | |
| { | |
| fromName = fromName.Replace("Integer", fromRtType); | |
| } | |
| CallFunc($"Confirm{fromName}CastTo{toRtType}Overflows", body: () => | |
| { | |
| DeclVar(fromCsType, "from", $"{fromValue}"); | |
| var clarityCast = ""; | |
| if (fromValue.IsFloatingPoint != IsFloatingPoint(fromCsType)) | |
| { | |
| clarityCast = $"({fromCsType})"; | |
| } | |
| ConfirmException("OverflowException", $"'checked(({toCsType}){clarityCast}{fromValue})' did not throw OverflowException.", () => | |
| { | |
| Out($"_ = checked(({toCsType})from);"); | |
| }); | |
| }); | |
| } | |
| } | |
| } | |
| CloseScope(); | |
| #endif | |
| } | |
| struct PreciseValue : IEquatable<PreciseValue> | |
| { | |
| private readonly BigInteger _integer; | |
| private readonly float _single; | |
| private readonly double _double; | |
| private readonly Mode _mode; | |
| public PreciseValue(float sgl) : this() => (_single, _mode) = (sgl, Mode.Single); | |
| public PreciseValue(double dbl) : this() => (_double, _mode) = (dbl, Mode.Double); | |
| public PreciseValue(BigInteger integer) : this() => (_integer, _mode) = (integer, Mode.Integer); | |
| public double Double => _mode is Mode.Double ? _double : throw new Exception($"Unexpected mode: '{_mode}', expected: {Mode.Double}"); | |
| public float Single => _mode is Mode.Single ? _single : throw new Exception($"Unexpected mode: '{_mode}', expected: {Mode.Single}"); | |
| public BigInteger Integer => _mode is Mode.Integer ? _integer : throw new Exception($"Unexpected mode: '{_mode}', expected: {Mode.Integer}"); | |
| public bool IsFloatingPoint => _mode is Mode.Single or Mode.Double; | |
| public bool IsInteger => _mode is Mode.Integer; | |
| public bool IsSingle => _mode is Mode.Single; | |
| public bool IsDouble => _mode is Mode.Double; | |
| public PreciseValue CastToSingle() => _mode switch | |
| { | |
| Mode.Integer => Integer switch | |
| { | |
| _ when long.MinValue <= Integer && Integer <= long.MaxValue => new PreciseValue((float)(long)Integer), | |
| _ when 0 <= Integer && Integer <= ulong.MaxValue => new PreciseValue((float)(ulong)Integer), | |
| _ => (float)Integer | |
| }, | |
| Mode.Single => Single, | |
| Mode.Double => (float)Double, | |
| _ => throw new Exception($"Unexpected mode: '{_mode}'") | |
| }; | |
| public PreciseValue CastToDouble() => _mode switch | |
| { | |
| // Working around https://github.com/dotnet/runtime/issues/49611 | |
| Mode.Integer => Integer switch | |
| { | |
| _ when long.MinValue <= Integer && Integer <= long.MaxValue => new PreciseValue((double)(long)Integer), | |
| _ when 0 <= Integer && Integer <= ulong.MaxValue => new PreciseValue((double)(ulong)Integer), | |
| _ => (double)Integer | |
| }, | |
| Mode.Single => new PreciseValue((double)Single), | |
| Mode.Double => Double, | |
| _ => throw new Exception($"Unexpected mode: '{_mode}'") | |
| }; | |
| public PreciseValue CastToInteger() => _mode switch | |
| { | |
| Mode.Integer => Integer, | |
| Mode.Single when !float.IsFinite(Single) => throw new InvalidOperationException($"Cannot cast a non-finite Single '{Single}' to an integer"), | |
| Mode.Single => (BigInteger)Single, | |
| Mode.Double when !double.IsFinite(Double) => throw new InvalidOperationException($"Cannot cast a non-finite Double '{Double}' to an integer"), | |
| Mode.Double => (BigInteger)Double, | |
| _ => throw new Exception($"Unexpected mode: '{_mode}'") | |
| }; | |
| public PreciseValue Increment() => _mode switch | |
| { | |
| Mode.Integer => Integer + 1, | |
| Mode.Single => MathF.BitIncrement(Single), | |
| Mode.Double => Math.BitIncrement(Double), | |
| _ => throw new Exception($"Unexpected mode: '{_mode}'") | |
| }; | |
| public PreciseValue Decrement() => _mode switch | |
| { | |
| Mode.Integer => Integer - 1, | |
| Mode.Single => MathF.BitDecrement(Single), | |
| Mode.Double => Math.BitDecrement(Double), | |
| _ => throw new Exception($"Unexpected mode: '{_mode}'") | |
| }; | |
| public bool IsNaN() => _mode is Mode.Single && _single is float.NaN || _mode is Mode.Double && _double is double.NaN; | |
| public bool IsFinite() => _mode is Mode.Single && float.IsFinite(Single) || _mode is Mode.Double && double.IsFinite(Double) || _mode is Mode.Integer; | |
| public bool Equals(PreciseValue other) | |
| { | |
| ThrowIfNaN(); | |
| other.ThrowIfNaN(); | |
| static string GetPreciseValue(PreciseValue value) => value._mode switch | |
| { | |
| Mode.Integer => value.Integer.ToString(CultureInfo.InvariantCulture), | |
| Mode.Single => value.Single is 0 ? "0" : value.Single.ToString(CultureInfo.InvariantCulture), | |
| Mode.Double => value.Double is 0 ? "0" : value.Double.ToString(CultureInfo.InvariantCulture), | |
| _ => throw new Exception($"Unexpected mode: '{value._mode}'") | |
| }; | |
| return GetPreciseValue(this) == GetPreciseValue(other); | |
| } | |
| public override bool Equals(object obj) => obj is PreciseValue value && Equals(value); | |
| public override int GetHashCode() => base.GetHashCode(); | |
| public override string ToString() => _mode switch | |
| { | |
| Mode.Integer => Integer.ToString(), | |
| Mode.Single => Single switch | |
| { | |
| float.PositiveInfinity => "float.PositiveInfinity", | |
| float.NegativeInfinity => "float.NegativeInfinity", | |
| float.NaN when BitConverter.SingleToInt32Bits(Single) == BitConverter.SingleToInt32Bits(float.NaN) => "float.NaN", | |
| float.NaN => $"BitConverter.Int32BitsToSingle(unchecked((int)0x{Convert.ToString(BitConverter.SingleToInt32Bits(Single), 16)}))", | |
| _ => $"{Single}f" | |
| }, | |
| Mode.Double => Double switch | |
| { | |
| double.PositiveInfinity => "double.PositiveInfinity", | |
| double.NegativeInfinity => "double.NegativeInfinity", | |
| double.NaN when BitConverter.DoubleToInt64Bits(Double) == BitConverter.DoubleToInt64Bits(double.NaN) => "double.NaN", | |
| double.NaN => $"BitConverter.Int64BitsToDouble(unchecked((long)0x{Convert.ToString(BitConverter.DoubleToInt64Bits(Double), 16)}))", | |
| _ => $"{Double}d" | |
| }, | |
| _ => throw new Exception($"Unexpected mode: '{_mode}'") | |
| }; | |
| public static bool operator <(PreciseValue l, PreciseValue r) | |
| { | |
| static bool IsLess(IFormattable lv, IFormattable rv) | |
| { | |
| var ld = lv is BigInteger lInt ? lInt.ToString() : lv.ToString("F800", CultureInfo.InvariantCulture); | |
| var rd = rv is BigInteger rInt ? rInt.ToString() : rv.ToString("F800", CultureInfo.InvariantCulture); | |
| static BigInteger Integer(string d) | |
| { | |
| var s = d.AsSpan(); | |
| if (s.IndexOf('.') is > 0 and var index) | |
| { | |
| s = s[..index]; | |
| } | |
| return BigInteger.Parse(s); | |
| } | |
| static BigInteger Fraction(string d) | |
| { | |
| var s = d.AsSpan(); | |
| if (s.IndexOf('.') is > 0 and var index) | |
| { | |
| var mod = BigInteger.Parse(s[(index + 1)..]); | |
| return s[0] is '-' ? -mod : mod; | |
| } | |
| return BigInteger.Zero; | |
| } | |
| var li = Integer(ld); | |
| var ri = Integer(rd); | |
| if (li < ri) | |
| { | |
| return true; | |
| } | |
| else if (li > ri) | |
| { | |
| return false; | |
| } | |
| else | |
| { | |
| static string Normalize(string d) | |
| { | |
| if (!d.Contains('.')) | |
| { | |
| d += ".0"; | |
| } | |
| return d; | |
| } | |
| ld = Normalize(ld); | |
| rd = Normalize(rd); | |
| var length = Math.Max(ld.Length, rd.Length); | |
| ld = ld.PadRight(length, '0'); | |
| rd = rd.PadRight(length, '0'); | |
| return Fraction(ld) < Fraction(rd); | |
| } | |
| } | |
| l.ThrowIfNaN(); | |
| r.ThrowIfNaN(); | |
| switch (l._mode) | |
| { | |
| case Mode.Integer: | |
| switch (r._mode) | |
| { | |
| case Mode.Integer: | |
| return l.Integer < r.Integer; | |
| case Mode.Single: | |
| if (r.Single is float.PositiveInfinity) | |
| return true; | |
| if (r.Single == float.NegativeInfinity) | |
| return false; | |
| if (r.Single is 0) | |
| return l.Integer < 0; | |
| return IsLess(l.Integer, r.Single); | |
| case Mode.Double: | |
| if (r.Double is double.PositiveInfinity) | |
| return true; | |
| if (r.Double == double.NegativeInfinity) | |
| return false; | |
| if (r.Double is 0) | |
| return l.Integer < 0; | |
| return IsLess(l.Integer, r.Double); | |
| default: throw new InvalidEnumArgumentException(); | |
| } | |
| case Mode.Single: | |
| return r._mode switch | |
| { | |
| Mode.Integer => !(r < l) && l != r, | |
| Mode.Single => l.Single < r.Single, | |
| Mode.Double => l.Single < r.Double, | |
| _ => throw new InvalidEnumArgumentException(), | |
| }; | |
| case Mode.Double: | |
| return r._mode switch | |
| { | |
| Mode.Integer => !(r < l) && l != r, | |
| Mode.Single => l.Double < r.Single, | |
| Mode.Double => l.Double < r.Double, | |
| _ => throw new InvalidEnumArgumentException(), | |
| }; | |
| default: throw new InvalidEnumArgumentException(); | |
| } | |
| } | |
| public static bool operator >(PreciseValue l, PreciseValue r) => !(l < r) && l != r; | |
| public static bool operator <=(PreciseValue l, PreciseValue r) => l < r || l == r; | |
| public static bool operator >=(PreciseValue l, PreciseValue r) => l > r || l == r; | |
| public static bool operator ==(PreciseValue l, PreciseValue r) => l.Equals(r); | |
| public static bool operator !=(PreciseValue l, PreciseValue r) => !l.Equals(r); | |
| public static implicit operator PreciseValue(float sgl) => new(sgl); | |
| public static implicit operator PreciseValue(double dbl) => new(dbl); | |
| public static implicit operator PreciseValue(sbyte integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(byte integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(short integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(ushort integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(int integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(uint integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(long integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(ulong integer) => new(new BigInteger(integer)); | |
| public static implicit operator PreciseValue(BigInteger integer) => new(integer); | |
| private void ThrowIfNaN() | |
| { | |
| if (IsNaN()) | |
| { | |
| throw new Exception("Comparisons with NaNs are meaningless!"); | |
| } | |
| } | |
| private enum Mode | |
| { | |
| Integer, | |
| Single, | |
| Double | |
| } | |
| } | |
| struct NamedValue | |
| { | |
| public NamedValue(PreciseValue value, string name) | |
| { | |
| Value = value; | |
| Name = name; | |
| } | |
| public PreciseValue Value { get; } | |
| public string Name { get; } | |
| public void Deconstruct(out PreciseValue value, out string name) => (value, name) = (Value, Name); | |
| public override string ToString() => $"{Name}: {Value}"; | |
| public static implicit operator NamedValue((PreciseValue Value, string Name) tuple) => new(tuple.Value, tuple.Name); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment