Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(many) Add support for float data type #353

Merged
merged 1 commit into from
Dec 1, 2022
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
5 changes: 5 additions & 0 deletions release-notes/v0.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@
- Upgrade `System.CommandLine` to 2.0.0-beta4.22272.1 [[#350][350]]
- Upgrade to .NET 7 RTM [[#351][351]]

#### Data types
- Add support for `float` data type [[#353][353]]

### Changed
* Quote types names in error message emitted when attempting to reassign a variable to an incoercible type. [[#343][343]]

### Fixed

### Tests
- Continued adding more tests for functionality being added to the system. The number of tests increased from 1586 tests in the previous release to 1911 tests in version 0.2.0. This is partly caused by [[#353][353]], which added support for the `float` data type. Each primitive data type supported requires a significant number of tests to be added for the "meta-tests" to complete; these ensure that no data types get added to the system without properly defined semantics for all operator/primitive type combinations.

[343]: https://github.com/perlang-org/perlang/pull/343
[344]: https://github.com/perlang-org/perlang/pull/344
[347]: https://github.com/perlang-org/perlang/pull/347
[350]: https://github.com/perlang-org/perlang/pull/350
[351]: https://github.com/perlang-org/perlang/pull/351
[353]: https://github.com/perlang-org/perlang/pull/353
1 change: 1 addition & 0 deletions src/Perlang.Common/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static string ToTypeKeyword(this Type type)
{ } when type == typeof(BigInteger) => "bigint",
{ } when type == typeof(UInt32) => "uint",
{ } when type == typeof(UInt64) => "ulong",
{ } when type == typeof(Single) => "float",
{ } when type == typeof(Double) => "double",
{ } when type == typeof(NullObject) => "null",
{ } when type == typeof(String) => "string",
Expand Down
5 changes: 4 additions & 1 deletion src/Perlang.Common/NumericToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ namespace Perlang;
public class NumericToken : Token
{
public bool IsFractional { get; }
public char? Suffix { get; }
public Base NumberBase { get; }
public NumberStyles NumberStyles { get; }
public bool HasSuffix => Suffix != null;

public NumericToken(string lexeme, int line, string numberCharacters, bool isFractional, Base numberBase, NumberStyles numberStyles)
public NumericToken(string lexeme, int line, string numberCharacters, char? suffix, bool isFractional, Base numberBase, NumberStyles numberStyles)
: base(TokenType.NUMBER, lexeme, numberCharacters, line)
{
IsFractional = isFractional;
Suffix = suffix;
NumberBase = numberBase;
NumberStyles = numberStyles;
}
Expand Down
258 changes: 230 additions & 28 deletions src/Perlang.Interpreter/PerlangInterpreter.cs

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions src/Perlang.Interpreter/Typing/TypeCoercer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ public static class TypeCoercer

internal static ImmutableDictionary<Type, int?> FloatIntegerLengthByType => new Dictionary<Type, int?>
{
// Double-precision values are 64-bit but can store numbers up to 1.7E +/- 308 (with data loss, i.e. numbers
// larger than +/- 2^53 can not be exactly represented. We presume people working with numbers this large to
// be (or make themselves aware of) this limitation.
// Single-precision values are 32-bit but can store numbers between 1.4E-45 and ~3.40E38 (with data loss,
// i.e. numbers larger or equal than +/- 2^24 can not be exactly represented. We presume people working with
// numbers this large to be (or make themselves aware of) this limitation.)
{ typeof(Single), 32 },

// Double-precision values are 64-bit but can store numbers between 4.9E-324 and ~1.80E308 (with data loss,
// i.e. numbers larger or equal than +/- 2^53 can not be exactly represented. We presume people working with
// numbers this large to be (or make themselves aware of) this limitation.)
{ typeof(Double), 64 }
}.ToImmutableDictionary();

Expand Down
34 changes: 30 additions & 4 deletions src/Perlang.Interpreter/Typing/TypeResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,16 @@ public override VoidObject VisitBinaryExpr(Expr.Binary expr)
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType) &&
(leftTypeReference.ClrType == typeof(double) || rightTypeReference.ClrType == typeof(double)))
{
// Order is important. This branch must come _before_ the float branch, since `float +
// double` and `double + float` is expected to produce a `double`.
expr.TypeReference.ClrType = typeof(double);
}
else if (new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float), typeof(double) }.Contains(leftTypeReference.ClrType) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType) &&
(leftTypeReference.ClrType == typeof(float) || rightTypeReference.ClrType == typeof(float)))
{
expr.TypeReference.ClrType = typeof(float);
}
else if (new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(leftTypeReference.ClrType) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(rightTypeReference.ClrType) &&
(leftTypeReference.ClrType == typeof(BigInteger) || rightTypeReference.ClrType == typeof(BigInteger)))
Expand Down Expand Up @@ -176,9 +184,17 @@ public override VoidObject VisitBinaryExpr(Expr.Binary expr)
{
expr.TypeReference.ClrType = typeof(long);
}
else if (new[] { typeof(float), typeof(double) }.Contains(leftTypeReference.ClrType) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType) &&
(leftTypeReference.ClrType == typeof(double) || rightTypeReference.ClrType == typeof(double)))
else if (new[] { typeof(float) }.Contains(leftTypeReference.ClrType) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float) }.Contains(rightTypeReference.ClrType))
{
// Here it gets interesting: `float` += `double` is legal in Java but *NOT* supported in C#
// (Cannot implicitly convert type 'double' to 'float'). We go with the C# semantics for now
// since it seems like the safer approach. If/when we need to support this, some form of
// explicit casting mechanism would be more suitable.
expr.TypeReference.ClrType = typeof(float);
}
else if (new[] { typeof(double) }.Contains(leftTypeReference.ClrType) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType))
{
expr.TypeReference.ClrType = typeof(double);
}
Expand Down Expand Up @@ -279,8 +295,12 @@ public override VoidObject VisitBinaryExpr(Expr.Binary expr)
}
else if (new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float), typeof(double) }.Contains(leftTypeReference.ClrType) &&
new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType) &&
(leftTypeReference.ClrType == typeof(double) || rightTypeReference.ClrType == typeof(double)))
(new[] { typeof(float), typeof(double) }.Contains(leftTypeReference.ClrType) || new[] { typeof(float), typeof(double) }.Contains(rightTypeReference.ClrType)))
{
// The above check to ensure that either of the operands is `float` or `double` is important
// here, since e.g. `ulong` and `int` cannot be compared to each other (because of
// not-so-hard-to-understand limitations in the C# language; I'm not even sure this would be
// possible to implement with the existing x64 math instructions)
expr.TypeReference.ClrType = typeof(bool);
}
else if (new[] { typeof(int), typeof(long), typeof(uint), typeof(ulong), typeof(BigInteger) }.Contains(leftTypeReference.ClrType) &&
Expand Down Expand Up @@ -728,6 +748,12 @@ private static void ResolveExplicitTypes(ITypeReference typeReference)
typeReference.ClrType = typeof(ulong);
break;

// "Float" is called "Single" in C#/.NET, but Java uses `float` and `Float`. In this case, I think it
// makes little sense to make them inconsistent.
case "float" or "Float":
typeReference.ClrType = typeof(float);
break;

case "double" or "Double":
typeReference.ClrType = typeof(double);
break;
Expand Down
2 changes: 2 additions & 0 deletions src/Perlang.Parser/FloatingPointLiteral.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ public FloatingPointLiteral(T value)

BitsUsed = value switch
{
float floatValue => (int)Math.Ceiling(Math.Log2(floatValue)),
double doubleValue => (int)Math.Ceiling(Math.Log2(doubleValue)),
_ => throw new ArgumentException($"Unsupported numeric type encountered: {value.GetType().Name}")
};

IsPositive = value switch
{
float floatValue => floatValue >= 0,
double doubleValue => doubleValue >= 0,
_ => throw new ArgumentException($"Unsupported numeric type encountered: {value.GetType().Name}")
};
Expand Down
41 changes: 34 additions & 7 deletions src/Perlang.Parser/NumberParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,36 @@ public static INumericLiteral Parse(NumericToken numericToken)

if (numericToken.IsFractional)
{
// TODO: This is a mess. We currently treat all floating point values as _double_, which is insane. We
// TODO: should probably have a "use smallest possible type" logic as below for integers, for floating point
// TODO: values as well. We could also consider supporting `decimal` while we're at it.
if (numericToken.HasSuffix)
{
switch (numericToken.Suffix)
{
case 'f':
{
// The explicit IFormatProvider is required to ensure we use 123.45 format, regardless of host OS
// language/region settings. See #263 for more details.
float value = Single.Parse(numberCharacters, CultureInfo.InvariantCulture);
return new FloatingPointLiteral<float>(value);
}

case 'd':
{
// The explicit IFormatProvider is required to ensure we use 123.45 format, regardless of host OS
// language/region settings. See #263 for more details.
double value = Double.Parse(numberCharacters, CultureInfo.InvariantCulture);
return new FloatingPointLiteral<double>(value);
}

// The explicit IFormatProvider is required to ensure we use 123.45 format, regardless of host OS
// language/region settings. See #263 for more details.
return new FloatingPointLiteral<double>(Double.Parse(numberCharacters, CultureInfo.InvariantCulture));
default:
throw new InvalidOperationException($"Numeric literal suffix {numericToken.Suffix} is not supported");
}
}
else
{
// No suffix provided => use `double` precision by default, just like C#
double value = Double.Parse(numberCharacters, CultureInfo.InvariantCulture);
return new FloatingPointLiteral<double>(value);
}
}
else
{
Expand Down Expand Up @@ -85,7 +108,11 @@ public static object MakeNegative(object value)
{
if (value is INumericLiteral numericLiteral)
{
if (numericLiteral.Value is double doubleValue)
if (numericLiteral.Value is float floatValue)
{
return new FloatingPointLiteral<float>(-floatValue);
}
else if (numericLiteral.Value is double doubleValue)
{
return new FloatingPointLiteral<double>(-doubleValue);
}
Expand Down
14 changes: 12 additions & 2 deletions src/Perlang.Parser/Scanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ public class Scanner
{ "sbyte", RESERVED_WORD },
{ "short", RESERVED_WORD },
{ "ushort", RESERVED_WORD },
{ "float", RESERVED_WORD },
{ "decimal", RESERVED_WORD },
{ "char", RESERVED_WORD },

Expand Down Expand Up @@ -120,6 +119,7 @@ public class Scanner
"uint",
"ulong",
"bigint",
"float",
"double",
"string"
}.ToImmutableHashSet();
Expand Down Expand Up @@ -455,12 +455,18 @@ private void Number()
}

string numberCharacters = RemoveUnderscores(source[(start + startOffset)..current]);
char? suffix = null;

if (IsAlpha(Peek()))
{
suffix = Advance();
}

// Note that numbers are not parsed at this stage. We deliberately postpone it to the parsing stage, to be
// able to conjoin MINUS and NUMBER tokens together for negative numbers. The previous approach (inherited
// from Lox) worked poorly with our idea of "narrowing down" constants to smallest possible integer. See
// #302 for some more details.
AddToken(new NumericToken(source[start..current], line, numberCharacters, isFractional, numberBase, numberStyles));
AddToken(new NumericToken(source[start..current], line, numberCharacters, suffix, isFractional, numberBase, numberStyles));
}

private static string RemoveUnderscores(string s)
Expand Down Expand Up @@ -578,6 +584,10 @@ private static bool IsDigit(char c, NumericToken.Base @base) =>
private bool IsAtEnd() =>
current >= source.Length;

/// <summary>
/// Moves the cursor one step forward and returns the element which was previously current.
/// </summary>
/// <returns>The current element, before advancing the cursor.</returns>
private char Advance()
{
current++;
Expand Down
101 changes: 93 additions & 8 deletions src/Perlang.Tests.Integration/Number/NumberTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Perlang.Tests.Integration.Typing;
using Xunit;
using static Perlang.Tests.Integration.EvalHelper;
Expand Down Expand Up @@ -133,12 +134,29 @@ public async Task literal_float(CultureInfo cultureInfo)
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123.456
123.456f
";

object result = Eval(source);

result.Should()
.Be(123.456f);
}

[Theory]
[ClassData(typeof(TestCultures))]
public async Task literal_float_has_expected_type(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123.456f
";

object result = Eval(source);

Assert.Equal(123.456, result);
result.Should()
.BeOfType<float>();
}

[Theory]
Expand All @@ -148,12 +166,13 @@ public async Task literal_negative_float(CultureInfo cultureInfo)
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
-0.001
-0.001f
";

object result = Eval(source);

Assert.Equal(-0.001, result);
result.Should()
.Be(-0.001f);
}

[Theory]
Expand All @@ -163,12 +182,13 @@ public async Task literal_float_with_underscore_in_integer_part(CultureInfo cult
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123_45.678
123_45.678f
";

object result = Eval(source);

Assert.Equal(12345.678, result);
result.Should()
.Be(12345.678f);
}

[Theory]
Expand All @@ -178,12 +198,77 @@ public async Task literal_float_with_underscore_in_fractional_part(CultureInfo c
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123.45_678
123.45_678f
";

object result = Eval(source);

result.Should()
.Be(123.45678f);
}

[Theory]
[ClassData(typeof(TestCultures))]
public async Task literal_double_with_suffix(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123.456d
";

object result = Eval(source);

result.Should()
.Be(123.456d);
}

[Theory]
[ClassData(typeof(TestCultures))]
public async Task literal_double_with_suffix_has_expected_type(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123.456d
";

object result = Eval(source);

result.Should()
.BeOfType<double>();
}

[Theory]
[ClassData(typeof(TestCultures))]
public async Task literal_double_with_implicit_suffix(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123.456
";

object result = Eval(source);

result.Should()
.Be(123.456d);
}

[Theory]
[ClassData(typeof(TestCultures))]
public async Task literal_double_with_implicit_suffix_has_expected_type(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;

string source = @"
123.456
";

object result = Eval(source);

Assert.Equal(123.45678, result);
result.Should()
.BeOfType<double>();
}

[Fact]
Expand Down
Loading