diff --git a/src/ArcadeBasic.Runtime/BuiltinImpls.cs b/src/ArcadeBasic.Runtime/BuiltinImpls.cs index 884c178..321ba64 100644 --- a/src/ArcadeBasic.Runtime/BuiltinImpls.cs +++ b/src/ArcadeBasic.Runtime/BuiltinImpls.cs @@ -41,35 +41,40 @@ private static IReadOnlyDictionary Build() t["TRUNCATE"] = args => Num(BigDecimal.Truncate(N(args[0]))); t["CEIL"] = args => Num(BigDecimal.Ceiling(N(args[0]))); t["ROUND"] = args => Num(BigDecimal.Round(N(args[0]), 0, RoundingMode.MidpointToEven)); - t["SQR"] = args => Num(FromDouble(Math.Sqrt(ToDouble(args[0])))); - t["EXP"] = args => Num(FromDouble(Math.Exp(ToDouble(args[0])))); + t["SQR"] = args => + { + var x = ToDouble(args[0]); + if (x < 0) throw new BasicRuntimeException(2001, "SQR requires non-negative argument"); + return Num(FromDouble(Math.Sqrt(x), "SQR")); + }; + t["EXP"] = args => Num(FromDouble(Math.Exp(ToDouble(args[0])), "EXP")); t["LOG"] = args => { var x = ToDouble(args[0]); if (x <= 0) throw new BasicRuntimeException(2001, "LOG requires positive argument"); - return Num(FromDouble(Math.Log(x))); + return Num(FromDouble(Math.Log(x), "LOG")); }; t["LOG2"] = args => { var x = ToDouble(args[0]); if (x <= 0) throw new BasicRuntimeException(2001, "LOG2 requires positive argument"); - return Num(FromDouble(Math.Log(x, 2))); + return Num(FromDouble(Math.Log(x, 2), "LOG2")); }; t["LOG10"] = args => { var x = ToDouble(args[0]); if (x <= 0) throw new BasicRuntimeException(2001, "LOG10 requires positive argument"); - return Num(FromDouble(Math.Log10(x))); + return Num(FromDouble(Math.Log10(x), "LOG10")); }; - t["SIN"] = args => Num(FromDouble(Math.Sin(ToDouble(args[0])))); - t["COS"] = args => Num(FromDouble(Math.Cos(ToDouble(args[0])))); - t["TAN"] = args => Num(FromDouble(Math.Tan(ToDouble(args[0])))); - t["ATN"] = args => Num(FromDouble(Math.Atan(ToDouble(args[0])))); - t["ASIN"] = args => Num(FromDouble(Math.Asin(ToDouble(args[0])))); - t["ACOS"] = args => Num(FromDouble(Math.Acos(ToDouble(args[0])))); - t["SEC"] = args => Num(FromDouble(1.0 / Math.Cos(ToDouble(args[0])))); - t["CSC"] = args => Num(FromDouble(1.0 / Math.Sin(ToDouble(args[0])))); - t["COT"] = args => Num(FromDouble(1.0 / Math.Tan(ToDouble(args[0])))); + t["SIN"] = args => Num(FromDouble(Math.Sin(ToDouble(args[0])), "SIN")); + t["COS"] = args => Num(FromDouble(Math.Cos(ToDouble(args[0])), "COS")); + t["TAN"] = args => Num(FromDouble(Math.Tan(ToDouble(args[0])), "TAN")); + t["ATN"] = args => Num(FromDouble(Math.Atan(ToDouble(args[0])), "ATN")); + t["ASIN"] = args => Num(FromDouble(Math.Asin(ToDouble(args[0])), "ASIN")); + t["ACOS"] = args => Num(FromDouble(Math.Acos(ToDouble(args[0])), "ACOS")); + t["SEC"] = args => Num(FromDouble(1.0 / Math.Cos(ToDouble(args[0])), "SEC")); + t["CSC"] = args => Num(FromDouble(1.0 / Math.Sin(ToDouble(args[0])), "CSC")); + t["COT"] = args => Num(FromDouble(1.0 / Math.Tan(ToDouble(args[0])), "COT")); var rng = new Random(); t["RND"] = args => Num(BigDecimal.Parse( @@ -247,9 +252,16 @@ private static double ToDouble(Value v) return d; } - private static BigDecimal FromDouble(double d) => - BigDecimal.Parse(d.ToString("R", System.Globalization.CultureInfo.InvariantCulture), + // NaN and infinity have no BigDecimal form; report them as BASIC exceptions + // (domain error / overflow) rather than letting the parse fail with a .NET + // FormatException, which surfaced as "Input string was not in a correct format". + private static BigDecimal FromDouble(double d, string function) + { + if (double.IsNaN(d)) throw new BasicRuntimeException(2001, $"{function} argument out of range"); + if (double.IsInfinity(d)) throw new BasicRuntimeException(1001, $"{function} result overflows"); + return BigDecimal.Parse(d.ToString("R", System.Globalization.CultureInfo.InvariantCulture), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture); + } private static int CountRunes(string s) { diff --git a/test/ArcadeBasic.Interpreter.Tests/ExceptionTests.cs b/test/ArcadeBasic.Interpreter.Tests/ExceptionTests.cs index 485b53a..d02b09d 100644 --- a/test/ArcadeBasic.Interpreter.Tests/ExceptionTests.cs +++ b/test/ArcadeBasic.Interpreter.Tests/ExceptionTests.cs @@ -268,4 +268,27 @@ END WHEN output.Should().Contain("outer caught"); output.Should().Contain("1"); } + + // -- Math function domain errors ------------------------------------- + + [Theory] + [InlineData("SQR(-1)", 2001, "SQR requires non-negative argument")] + [InlineData("ASIN(2)", 2001, "ASIN argument out of range")] + [InlineData("ACOS(-2)", 2001, "ACOS argument out of range")] + [InlineData("EXP(1000)", 1001, "EXP result overflows")] + public void MathDomainErrorsRaiseBasicExceptions(string expr, int type, string text) + { + var src = $""" + WHEN EXCEPTION IN + LET X = {expr} + USE + PRINT EXTYPE + PRINT EXTEXT$ + END WHEN + """; + var (output, exit) = Run(src); + exit.Should().Be(0); + // PRINT pads numbers with a trailing space, so compare trimmed lines. + output.Split('\n').Select(l => l.Trim()).Should().ContainInOrder(type.ToString(), text); + } } diff --git a/test/ArcadeBasic.Vm.Tests/VmTests.cs b/test/ArcadeBasic.Vm.Tests/VmTests.cs index d83c108..d9eaf59 100644 --- a/test/ArcadeBasic.Vm.Tests/VmTests.cs +++ b/test/ArcadeBasic.Vm.Tests/VmTests.cs @@ -1585,4 +1585,27 @@ END DEF """; Run(src).Output.Trim().Should().Be("reached"); } + + // -- Math function domain errors ------------------------------------- + + [Theory] + [InlineData("SQR(-1)", 2001, "SQR requires non-negative argument")] + [InlineData("ASIN(2)", 2001, "ASIN argument out of range")] + [InlineData("ACOS(-2)", 2001, "ACOS argument out of range")] + [InlineData("EXP(1000)", 1001, "EXP result overflows")] + public void MathDomainErrorsRaiseBasicExceptions(string expr, int type, string text) + { + var src = $""" + WHEN EXCEPTION IN + LET X = {expr} + USE + PRINT EXTYPE + PRINT EXTEXT$ + END WHEN + """; + var (output, exit) = Run(src); + exit.Should().Be(0); + // PRINT pads numbers with a trailing space, so compare trimmed lines. + output.Split('\n').Select(l => l.Trim()).Should().ContainInOrder(type.ToString(), text); + } }