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

Use a union struct for an optimal PythonConstant #235

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
57 changes: 25 additions & 32 deletions src/CSnakes.SourceGeneration/Parser/PythonParser.Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,59 +44,59 @@ from chars in Character.ExceptIn('\'').Many()
from close in Character.EqualTo('\'')
select Unit.Value;

public static TokenListParser<PythonToken, PythonConstant.String> DoubleQuotedStringConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> DoubleQuotedStringConstantTokenizer { get; } =
Token.EqualTo(PythonToken.DoubleQuotedString)
.Apply(ConstantParsers.DoubleQuotedString)
.Select(s => new PythonConstant.String(s))
.Select(s => new PythonConstant(s))
.Named("Double Quoted String Constant");

public static TokenListParser<PythonToken, PythonConstant.String> SingleQuotedStringConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> SingleQuotedStringConstantTokenizer { get; } =
Token.EqualTo(PythonToken.SingleQuotedString)
.Apply(ConstantParsers.SingleQuotedString)
.Select(s => new PythonConstant.String(s))
.Select(s => new PythonConstant(s))
.Named("Single Quoted String Constant");

public static TokenListParser<PythonToken, PythonConstant.Float> DecimalConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> DecimalConstantTokenizer { get; } =
Token.EqualTo(PythonToken.Decimal)
.Select(token => new PythonConstant.Float(double.Parse(token.ToStringValue().Replace("_", ""), NumberStyles.Float, CultureInfo.InvariantCulture)))
.Select(token => new PythonConstant(double.Parse(token.ToStringValue().Replace("_", ""), NumberStyles.Float, CultureInfo.InvariantCulture)))
.Named("Decimal Constant");

public static TokenListParser<PythonToken, PythonConstant.Integer> IntegerConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> IntegerConstantTokenizer { get; } =
Token.EqualTo(PythonToken.Integer)
.Select(d => new PythonConstant.Integer(long.Parse(d.ToStringValue().Replace("_", ""), NumberStyles.Integer)))
.Select(d => new PythonConstant(long.Parse(d.ToStringValue().Replace("_", ""), NumberStyles.Integer)))
.Named("Integer Constant");

public static TokenListParser<PythonToken, PythonConstant.HexidecimalInteger> HexidecimalIntegerConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> HexidecimalIntegerConstantTokenizer { get; } =
Token.EqualTo(PythonToken.HexidecimalInteger)
.Select(d => new PythonConstant.HexidecimalInteger(long.Parse(d.ToStringValue().Substring(2).Replace("_", ""), NumberStyles.HexNumber)))
.Select(d => PythonConstant.HexadecimalInteger(long.Parse(d.ToStringValue().Substring(2).Replace("_", ""), NumberStyles.HexNumber)))
.Named("Hexidecimal Integer Constant");

public static TokenListParser<PythonToken, PythonConstant.BinaryInteger> BinaryIntegerConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> BinaryIntegerConstantTokenizer { get; } =
Token.EqualTo(PythonToken.BinaryInteger)
// TODO: Consider Binary Format specifier introduced in .NET 8 https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings#binary-format-specifier-b
.Select(d => new PythonConstant.BinaryInteger((long)Convert.ToUInt64(d.ToStringValue().Substring(2).Replace("_", ""), 2)))
.Select(d => PythonConstant.BinaryInteger((long)Convert.ToUInt64(d.ToStringValue().Substring(2).Replace("_", ""), 2)))
.Named("Binary Integer Constant");

public static TokenListParser<PythonToken, PythonConstant.Bool> BoolConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> BoolConstantTokenizer { get; } =
Token.EqualTo(PythonToken.True).Or(Token.EqualTo(PythonToken.False))
.Select(d => (d.Kind == PythonToken.True ? PythonConstant.Bool.True : PythonConstant.Bool.False))
.Select(d => new PythonConstant(d.Kind == PythonToken.True))
.Named("Bool Constant");

public static TokenListParser<PythonToken, PythonConstant.None> NoneConstantTokenizer { get; } =
public static TokenListParser<PythonToken, PythonConstant> NoneConstantTokenizer { get; } =
Token.EqualTo(PythonToken.None)
.Select(d => PythonConstant.None.Value)
.Select(d => PythonConstant.None)
.Named("None Constant");

// Any constant value
public static TokenListParser<PythonToken, PythonConstant?> ConstantValueTokenizer { get; } =
DecimalConstantTokenizer.AsBase().AsNullable()
.Or(IntegerConstantTokenizer.AsBase().AsNullable())
.Or(HexidecimalIntegerConstantTokenizer.AsBase().AsNullable())
.Or(BinaryIntegerConstantTokenizer.AsBase().AsNullable())
.Or(BoolConstantTokenizer.AsBase().AsNullable())
.Or(NoneConstantTokenizer.AsBase().AsNullable())
.Or(DoubleQuotedStringConstantTokenizer.AsBase().AsNullable())
.Or(SingleQuotedStringConstantTokenizer.AsBase().AsNullable())
public static TokenListParser<PythonToken, PythonConstant> ConstantValueTokenizer { get; } =
DecimalConstantTokenizer
.Or(IntegerConstantTokenizer)
.Or(HexidecimalIntegerConstantTokenizer)
.Or(BinaryIntegerConstantTokenizer)
.Or(BoolConstantTokenizer)
.Or(NoneConstantTokenizer)
.Or(DoubleQuotedStringConstantTokenizer)
.Or(SingleQuotedStringConstantTokenizer)
.Named("Constant");

static class ConstantParsers
Expand Down Expand Up @@ -126,10 +126,3 @@ from close in Character.EqualTo('\'')
select new string(chars);
}
}

file static class Extensions
{
public static TokenListParser<TKind, PythonConstant> AsBase<TKind, T>(this TokenListParser<TKind, T> parser)
where T : PythonConstant =>
parser.Cast<TKind, T, PythonConstant>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ from type in Token.EqualTo(PythonToken.Colon).Optional().Then(
_ => PythonTypeDefinitionTokenizer.AssumeNotNull().OptionalOrDefault()
)
from defaultValue in Token.EqualTo(PythonToken.Equal).Optional().Then(
_ => ConstantValueTokenizer.AssumeNotNull().OptionalOrDefault()
_ => ConstantValueTokenizer.Optional()
)
select new PythonFunctionParameter(arg.Name, type, defaultValue, arg.ParameterType))
.Named("Parameter");
Expand Down
100 changes: 64 additions & 36 deletions src/CSnakes.SourceGeneration/Parser/Types/PythonConstant.cs
Original file line number Diff line number Diff line change
@@ -1,57 +1,85 @@
using System.Globalization;
using System.Runtime.InteropServices;

namespace CSnakes.Parser.Types;

public abstract class PythonConstant
public readonly struct PythonConstant
{
public abstract override string ToString();

public class Integer(long value) : PythonConstant
public enum ConstantType
{
public long Value { get; } = value;
public override string ToString() => Value.ToString();
None,
Integer,
HexidecimalInteger,
BinaryInteger,
Float,
String,
Bool,
}

public sealed class HexidecimalInteger(long value) : Integer(value)
{
public override string ToString() => $"0x{Value:X}";
}
readonly Union union; // union storage for value primitives (bool, long & double)
readonly string? str; // storage for string reference

public sealed class BinaryInteger(long value) : Integer(value)
{
public override string ToString() => $"0b{Value:X}";
}
private PythonConstant(ConstantType type, string str) :
this(type, default, str) { }

private PythonConstant(ConstantType type, Union union, string? str = null) =>
(Type, this.union, this.str) = (type, union, str);

public sealed class Float(double value) : PythonConstant
{
public double Value { get; } = value;
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
}

public sealed class String(string value) : PythonConstant
/// <summary>
/// Represents a union of all the primitive types supported by <see cref="PythonConstant"/>.
/// This structure occupies the memory of the largest field as opposed to the sum of all fields
/// since they are all physically stored at the beginning of the structure and permit to
/// optimise space.
/// </summary>

[StructLayout(LayoutKind.Explicit)]
private readonly struct Union
{
public string Value { get; } = value;
public override string ToString() => Value;
[FieldOffset(0)] public readonly bool Boolean;
[FieldOffset(0)] public readonly long Int64;
[FieldOffset(0)] public readonly double Double;

public Union(bool value) => this.Boolean = value;
public Union(long value) => this.Int64 = value;
public Union(double value) => this.Double = value;
}

public sealed class Bool : PythonConstant
{
public static readonly Bool True = new(true);
public static readonly Bool False = new(false);
public PythonConstant() : this(ConstantType.None, default(Union)) { }
public PythonConstant(long value) : this(ConstantType.Integer, new Union(value)) { }
public PythonConstant(double value) : this(ConstantType.Float, new Union(value)) { }
public PythonConstant(string value) : this(ConstantType.String, value) { }
public PythonConstant(bool value) : this(ConstantType.Bool, new Union(value)) { }

private Bool(bool value) => Value = value;
public static readonly PythonConstant None = new();

public bool Value { get; }
public static PythonConstant HexadecimalInteger(long value) =>
new(ConstantType.HexidecimalInteger, new Union(value));

public override string ToString() => Value.ToString();
}
public static PythonConstant BinaryInteger(long value) =>
new(ConstantType.BinaryInteger, new Union(value));

public sealed class None : PythonConstant
{
public static readonly None Value = new();
public ConstantType Type { get; }

private None() { }
public long? BinaryIntegerValue => Type is ConstantType.BinaryInteger ? this.union.Int64 : null;
public long? HexidecimalIntegerValue => Type is ConstantType.HexidecimalInteger ? this.union.Int64 : null;
public long? IntegerValue => IsInteger ? this.union.Int64 : null;
public string? StringValue => Type is ConstantType.String ? this.str : null;
public double? FloatValue => Type is ConstantType.Float ? this.union.Double : null;
public bool? BoolValue => Type is ConstantType.Bool ? this.union.Boolean : null;

public override string ToString() => "None";
}
public bool IsNone => Type is ConstantType.None;
public bool IsInteger => Type is ConstantType.Integer or ConstantType.BinaryInteger or ConstantType.HexidecimalInteger;

public override string ToString() => Type switch
{
ConstantType.Integer => this.union.Int64.ToString(),
ConstantType.HexidecimalInteger => $"0x{this.union.Int64:X}",
ConstantType.BinaryInteger => $"0b{this.union.Int64:X}",
ConstantType.Float => this.union.Double.ToString(CultureInfo.InvariantCulture),
ConstantType.Bool => this.union.Boolean.ToString(),
ConstantType.String => this.str ?? string.Empty,
ConstantType.None => "None",
_ => "unknown"
};
}
20 changes: 10 additions & 10 deletions src/CSnakes.SourceGeneration/Reflection/ArgumentReflection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class ArgumentReflection
parameter.DefaultValue is null)

{
parameter.DefaultValue = PythonConstant.None.Value;
parameter.DefaultValue = PythonConstant.None;
}

bool isNullableType = false;
Expand All @@ -45,44 +45,44 @@ public class ArgumentReflection
case null:
literalExpressionSyntax = null;
break;
case PythonConstant.HexidecimalInteger { Value: var v }:
case { HexidecimalIntegerValue: { } v }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(
SyntaxKind.NumericLiteralExpression,
SyntaxFactory.Literal($"0x{v:X}", v));
break;
case PythonConstant.BinaryInteger { Value: var v }:
case { BinaryIntegerValue: { } v }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(
SyntaxKind.NumericLiteralExpression,
SyntaxFactory.Literal($"0b{Convert.ToString(v, 2)}", v));
break;
case PythonConstant.Integer { Value: var v and >= int.MinValue and <= int.MaxValue }:
case { IntegerValue: { } v and >= int.MinValue and <= int.MaxValue }:
// Downcast long to int if the value is small as the code is more readable without the L suffix
literalExpressionSyntax = SyntaxFactory.LiteralExpression(
SyntaxKind.NumericLiteralExpression,
SyntaxFactory.Literal((int)v));
break;
case PythonConstant.Integer { Value: var v }:
case { IntegerValue: { } v }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(
SyntaxKind.NumericLiteralExpression,
SyntaxFactory.Literal(v));
break;
case PythonConstant.String { Value: var v }:
case { StringValue: { } v }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(v));
break;
case PythonConstant.Float { Value: var v }:
case { FloatValue: { } v }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(
SyntaxKind.NumericLiteralExpression,
SyntaxFactory.Literal(v));
break;
case PythonConstant.Bool { Value: true }:
case { BoolValue: true }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression);
break;
case PythonConstant.Bool { Value: false }:
case { BoolValue: false }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression);
break;
case PythonConstant.None:
case { IsNone: true }:
literalExpressionSyntax = SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression);
isNullableType = true;
break;
Expand Down
2 changes: 1 addition & 1 deletion src/CSnakes.Tests/PythonConstantTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void ToString_Is_Not_Culture_Specific()

try
{
var n = new PythonConstant.Float(1_737.4);
var n = new PythonConstant(1_737.4);
Assert.Equal("1737.4", n.ToString());
}
finally
Expand Down
11 changes: 3 additions & 8 deletions src/CSnakes.Tests/TokenizerTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using CSnakes.Parser;
using CSnakes.Parser.Types;
using Superpower;

namespace CSnakes.Tests;
Expand Down Expand Up @@ -490,10 +489,8 @@ public void TestIntegerTokenization(string code, long expectedValue)
var result = PythonParser.ConstantValueTokenizer.TryParse(tokens);

Assert.True(result.HasValue);
Assert.NotNull(result.Value);
var value = result.Value as PythonConstant.Integer;
Assert.NotNull(value);
Assert.Equal(expectedValue, value.Value);
Assert.True(result.Value.IsInteger);
Assert.Equal(expectedValue, result.Value.IntegerValue);
}

[Theory]
Expand All @@ -519,9 +516,7 @@ public void TestDoubleTokenization(string code, double expectedValue)
var result = PythonParser.DecimalConstantTokenizer.TryParse(tokens);

Assert.True(result.HasValue);
var value = result.Value as PythonConstant.Float;
Assert.NotNull(value);
Assert.Equal(expectedValue, value.Value);
Assert.Equal(expectedValue, result.Value.FloatValue);
}
}

Loading