Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ public ulong Convert(DataType targetType) {
return Value & Mask(targetType.BitWidth);
}

private ulong Mask(BitWidth bitWidth) {
/// <summary>
/// Returns the bit mask covering the given bit width.
/// </summary>
public static ulong Mask(BitWidth bitWidth) {
return bitWidth switch {
BitWidth.NIBBLE_4 => 0x0F,
BitWidth.QUIBBLE_5 => 0b11111,
Expand All @@ -51,6 +54,19 @@ private ulong Mask(BitWidth bitWidth) {
};
}

/// <summary>
/// Converts the value the way an unchecked C# cast does: a signed source is sign-extended before
/// truncation to the target width, regardless of the target's signedness. Converting to bool yields 1 for
/// any non-zero value.
/// </summary>
public ulong ConvertAsCSharpCast(DataType targetType) {
if (targetType == DataType.BOOL) {
return Value == 0 ? 0UL : 1UL;
}
long widened = DataType.Signed ? SignedValue : unchecked((long)Value);
return unchecked((ulong)widened) & Mask(targetType.BitWidth);
}

public override T Accept<T>(IAstVisitor<T> astVisitor) {
return astVisitor.VisitConstantNode(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Spice86.Core.Emulator.ReverseEngineer.CfgCodeGeneration;
using Spice86.Core.Emulator.ReverseEngineer.FunctionPartitioning.Model;
using Spice86.Shared.Emulator.Memory;

using System.Globalization;
using System.Linq;
using System.Reflection;

Expand Down Expand Up @@ -184,13 +185,13 @@ public EmittedCode VisitInterruptCallNode(InterruptCallNode node) {
CallContinuation continuation = Context.ResolveCallContinuation(node.Instruction);
SegmentedAddress expectedReturn = continuation.ExpectedReturnAddress;
return EmittedCode.Concat(
EmittedCode.Line($"InterruptCall({Context.GetSegmentVariable(expectedReturn.Segment)}, 0x{expectedReturn.Offset:X4}, unchecked((byte)({Expr(node.VectorNumber)})));"),
EmittedCode.Line($"InterruptCall({Context.GetSegmentVariable(expectedReturn.Segment)}, 0x{expectedReturn.Offset:X4}, {Cast(DataType.UINT8, Expr(node.VectorNumber))});"),
Transfer.EmitPostCallContinuation(node.Instruction, continuation, CurrentMethod));
}

public EmittedCode VisitCallbackNode(CallbackNode node) =>
EmittedCode.Concat(
EmittedCode.Line($"Callback(unchecked((ushort)({Expr(node.CallbackNumber)})));"),
EmittedCode.Line($"Callback({Cast(DataType.UINT16, Expr(node.CallbackNumber))});"),
Transfer.EmitFallthroughIfNeeded(node.Instruction, CurrentMethod));

public EmittedCode VisitSelectorNode(SelectorNode node) => LowerSelector(node.CfgSelector);
Expand Down Expand Up @@ -549,20 +550,7 @@ public EmittedCode VisitSegmentedPointer(SegmentedPointerNode node) {
public EmittedCode VisitAbsolutePointerNode(AbsolutePointerNode node) =>
Atomic($"{ToMemoryIndexer(node.DataType)}[unchecked((uint)({Expr(node.AbsoluteAddress)}))]", node.DataType);

public EmittedCode VisitConstantNode(ConstantNode node) {
if (node.DataType == DataType.BOOL) {
return Atomic(node.Value == 0 ? "false" : "true", DataType.BOOL);
}

string literal = node.DataType.BitWidth switch {
BitWidth.BYTE_8 => node.DataType.Signed ? $"(sbyte){node.SignedValue}" : $"(byte)0x{node.Value:X2}",
BitWidth.WORD_16 => node.DataType.Signed ? $"(short){node.SignedValue}" : $"(ushort)0x{node.Value:X4}",
BitWidth.DWORD_32 or BitWidth.BOOL_1 => node.DataType.Signed ? $"{node.SignedValue}" : $"0x{node.Value:X8}u",
BitWidth.QWORD_64 => node.DataType.Signed ? $"{node.SignedValue}L" : $"0x{node.Value:X16}UL",
_ => throw Unsupported(node)
};
return Atomic(literal, node.DataType);
}
public EmittedCode VisitConstantNode(ConstantNode node) => ConstantLiteral(node);

public EmittedCode VisitNearAddressNode(NearAddressNode node) => VisitConstantNode(node);

Expand Down Expand Up @@ -613,7 +601,8 @@ public EmittedCode VisitUnaryOperationNode(UnaryOperationNode node) {
UnaryOperation.BITWISE_NOT => "~",
_ => throw Unsupported(node)
};
string operand = Parenthesize(Expr(node.Value), UnaryPrecedence);
int operandPrecedence = node.UnaryOperation == UnaryOperation.NEGATE ? UnaryPrecedence + 1 : UnaryPrecedence;
string operand = Parenthesize(Expr(node.Value), operandPrecedence);
// Logical NOT yields bool; arithmetic/bitwise unary ops promote to int, so their evaluated type is
// not the node's semantic DataType.
DataType? resultType = node.UnaryOperation == UnaryOperation.NOT ? DataType.BOOL : null;
Expand All @@ -622,6 +611,9 @@ public EmittedCode VisitUnaryOperationNode(UnaryOperationNode node) {

public EmittedCode VisitTypeConversionNode(TypeConversionNode node) {
CSharpFragment inner = Expr(node.Value);
if (inner.Constant is ConstantNode constant) {
return ConstantLiteral(new ConstantNode(node.DataType, constant.ConvertAsCSharpCast(node.DataType)));
}
if (inner.Type == node.DataType || node.DataType == DataType.BOOL && inner.Type == DataType.BOOL) {
// Converting to a type the value already has is a no-op; keep the inner fragment as-is.
return inner;
Expand Down Expand Up @@ -654,7 +646,7 @@ public EmittedCode VisitVariableDeclarationNode(VariableDeclarationNode node) =>
(CSharpFragment)$"{ToCSharpType(node.DataType)} {LocalVariableName(node.VariableName)} = {Cast(node.DataType, Expr(node.Initializer))}";
public EmittedCode VisitInstructionNode(InstructionNode node) => throw Unsupported(node);
public EmittedCode VisitSegmentedAddressNode(SegmentedAddressNode node) =>
(CSharpFragment)$"new SegmentedAddress(unchecked((ushort)({Expr(node.Segment)})), unchecked((ushort)({Expr(node.Offset)})))";
(CSharpFragment)$"new SegmentedAddress({Cast(DataType.UINT16, Expr(node.Segment))}, {Cast(DataType.UINT16, Expr(node.Offset))})";

private string NearRetExpression(ReturnNearNode node) {
string helperName = node.RetBitWidth == BitWidth.WORD_16 ? "NearRet" : "NearRet32";
Expand Down Expand Up @@ -725,6 +717,60 @@ private static string Cast(Type targetType, CSharpFragment expression) {

private static CSharpFragment Atomic(string text, DataType type) => new(text, type, CSharpFragment.AtomicPrecedence);

/// <summary>
/// Renders a constant as a C# literal: magnitudes 0-9 in decimal, larger ones in hex zero-padded to the
/// operand width, a leading minus for negative signed values, and a suffix where C# needs one.
/// </summary>
internal static CSharpFragment ConstantLiteral(ConstantNode node) {
if (node.DataType == DataType.BOOL) {
return new CSharpFragment(node.Value == 0 ? "false" : "true", DataType.BOOL, CSharpFragment.AtomicPrecedence) { Constant = node };
}
string? minValueName = MinValueName(node);
if (minValueName is not null) {
return new CSharpFragment(minValueName, node.DataType, CSharpFragment.AtomicPrecedence) { Constant = node };
}
bool negative = node.IsNegative;
ulong magnitude = negative ? (ulong)(-node.SignedValue) : node.Value;
string sign = negative ? "-" : "";
string text = sign + FormatMagnitude(magnitude, node.DataType.BitWidth) + LiteralSuffix(node.DataType, node.Value);
int precedence = negative ? UnaryPrecedence : CSharpFragment.AtomicPrecedence;
return new CSharpFragment(text, node.DataType, precedence) { Constant = node };
}

private static string? MinValueName(ConstantNode node) {
if (node.DataType == DataType.INT32 && node.SignedValue == int.MinValue) {
return "int.MinValue";
}
if (node.DataType == DataType.INT64 && node.SignedValue == long.MinValue) {
return "long.MinValue";
}
return null;
}

private static string FormatMagnitude(ulong value, BitWidth width) {
if (value <= 9) {
return value.ToString(CultureInfo.InvariantCulture);
}
int hexDigits = width switch {
BitWidth.BYTE_8 => 2,
BitWidth.WORD_16 => 4,
BitWidth.DWORD_32 or BitWidth.BOOL_1 => 8,
BitWidth.QWORD_64 => 16,
_ => throw new NotSupportedException($"Unsupported bit width {width}")
};
Comment on lines +754 to +760
return "0x" + value.ToString("X" + hexDigits, CultureInfo.InvariantCulture);
}

private static string LiteralSuffix(DataType dataType, ulong value) {
if (dataType.BitWidth == BitWidth.QWORD_64) {
return dataType.Signed ? "L" : "UL";
}
if (!dataType.Signed && dataType.BitWidth == BitWidth.DWORD_32 && value > int.MaxValue) {
return "u";
}
Comment on lines +768 to +770
return "";
}

/// <summary>
/// Renders a memory-indexer offset. A constant or single register/variable offset is emitted directly
/// (the indexer has a <c>ushort</c> overload); a compound arithmetic offset is wrapped in
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
namespace Spice86.Core.Emulator.ReverseEngineer.CfgCodeGeneration.Model;

using Spice86.Core.Emulator.CPU.CfgCpu.Ast;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value.Constant;

/// <summary>
/// A piece of generated C# emitted by <see cref="CSharpAstEmitter"/> for an execution-AST node. It carries
/// the rendered text plus the context needed to avoid redundant casts and parentheses:
/// <list type="bullet">
/// <item><see cref="Type"/>: the C# data type the expression evaluates to, or <c>null</c> when unknown
/// (e.g. the <c>int</c>-promoted result of binary arithmetic). A cast to a type the fragment already has is
/// skipped.</item>
/// skipped. For a constant literal this is the constant's semantic type; the literal itself may be an unsuffixed C#
/// <c>int</c>, which C# implicitly converts to any integral type that can hold the value.</item>
/// <item><see cref="Precedence"/>: the binding strength of the top-level operator, used to drop parentheses
/// that are not required by C# precedence.</item>
/// <item><see cref="Constant"/>: the source constant when the fragment is a literal, used to fold conversions of constants.</item>
/// </list>
/// Implicit conversions to/from <see cref="string"/> keep interpolation transparent; a raw string is treated
/// as an atomic expression of unknown type.
Expand All @@ -19,6 +22,9 @@ internal readonly record struct CSharpFragment(string Text, DataType? Type, int
/// <summary>Precedence of a primary expression (literal, identifier, call, indexer, cast, parenthesized).</summary>
public const int AtomicPrecedence = 13;

/// <summary>The constant this fragment renders, or <c>null</c> when the fragment is not a constant literal.</summary>
public ConstantNode? Constant { get; init; }

public CSharpFragment(string Text) : this(Text, null, AtomicPrecedence) {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
namespace Spice86.Tests.CfgCodeGeneration;

using FluentAssertions;

using Spice86.Core.Emulator.CPU.CfgCpu.Ast;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value.Constant;
using Spice86.Core.Emulator.ReverseEngineer.CfgCodeGeneration;
using Spice86.Core.Emulator.ReverseEngineer.CfgCodeGeneration.Model;
using Spice86.Shared.Emulator.Memory;

using Xunit;

/// <summary>
/// Unit tests for constant literal rendering in <see cref="CSharpAstEmitter"/>.
/// </summary>
public class CSharpAstEmitterConstantTest {
public static TheoryData<DataType, ulong, string, bool> ConstantLiteralCases => new() {
// BOOL
{ DataType.BOOL, 0UL, "false", true },
{ DataType.BOOL, 1UL, "true", true },

// UINT8
{ DataType.UINT8, 0UL, "0", true },
{ DataType.UINT8, 9UL, "9", true },
{ DataType.UINT8, 0x0AUL, "0x0A", true },
{ DataType.UINT8, 0xFFUL, "0xFF", true },

// UINT16
{ DataType.UINT16, 0UL, "0", true },
{ DataType.UINT16, 9UL, "9", true },
{ DataType.UINT16, 0x0AUL, "0x000A", true },
{ DataType.UINT16, 0xFFUL, "0x00FF", true },
{ DataType.UINT16, 0xFFFFUL, "0xFFFF", true },

// UINT32
{ DataType.UINT32, 0UL, "0", true },
{ DataType.UINT32, 0x0AUL, "0x0000000A", true },
{ DataType.UINT32, 0x7FFFFFFFUL, "0x7FFFFFFF", true },
{ DataType.UINT32, 0x80000000UL, "0x80000000u", true },
{ DataType.UINT32, 0xFFFFFFFFUL, "0xFFFFFFFFu", true },

// UINT64
{ DataType.UINT64, 1UL, "1UL", true },
{ DataType.UINT64, 0xFFFFFFFFFFFFFFFFUL, "0xFFFFFFFFFFFFFFFFUL", true },

// INT8
{ DataType.INT8, 0xFEUL, "-2", false },
{ DataType.INT8, 0xF7UL, "-9", false },
{ DataType.INT8, 0xF6UL, "-0x0A", false },
{ DataType.INT8, 0x80UL, "-0x80", false },
{ DataType.INT8, 0UL, "0", true },
{ DataType.INT8, 9UL, "9", true },
{ DataType.INT8, 0x0AUL, "0x0A", true },
{ DataType.INT8, 0x7FUL, "0x7F", true },

// INT16
{ DataType.INT16, 0xFFFEUL, "-2", false },
{ DataType.INT16, 0xFF80UL, "-0x0080", false },
{ DataType.INT16, 0x8000UL, "-0x8000", false },
{ DataType.INT16, 0x7FFFUL, "0x7FFF", true },

// INT32
{ DataType.INT32, 0xFFFFFFFEUL, "-2", false },
{ DataType.INT32, 0xFFFFFF80UL, "-0x00000080", false },
{ DataType.INT32, 0x80000000UL, "int.MinValue", true },
{ DataType.INT32, 0x7FFFFFFFUL, "0x7FFFFFFF", true },

// INT64
{ DataType.INT64, 0xFFFFFFFFFFFFFFFFUL, "-1L", false },
{ DataType.INT64, 0UL, "0L", true },
{ DataType.INT64, 0x10UL, "0x0000000000000010L", true },
{ DataType.INT64, 0x8000000000000000UL, "long.MinValue", true }
};

[Theory]
[MemberData(nameof(ConstantLiteralCases))]
public void ConstantLiteral_RendersCorrectLiteral(DataType dataType, ulong value, string expectedText, bool expectedAtomic) {
// Arrange
ConstantNode node = new(dataType, value);

// Act
CSharpFragment fragment = CSharpAstEmitter.ConstantLiteral(node);

// Assert
fragment.Text.Should().Be(expectedText);
fragment.Type.Should().Be(dataType);
fragment.Constant.Should().Be(node);
(fragment.Precedence == CSharpFragment.AtomicPrecedence).Should().Be(expectedAtomic);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ public void RegisterNodeRendersBareRegisterName() {
}

[Fact]
public void ConstantNodeRendersTypedLiteral() {
public void ConstantNodeRendersUnprefixedLiteral() {
ConstantNode constant = new(DataType.UINT16, 0x111C);

CSharpFragment fragment = constant.Accept(_emitter).AsExpression();

fragment.Text.Should().Be("(ushort)0x111C");
fragment.Text.Should().Be("0x111C");
}

[Fact]
Expand All @@ -64,7 +64,7 @@ public void SegmentedPointerNodeRendersMemoryIndexer() {
CSharpFragment fragment = pointer.Accept(_emitter).AsExpression();

// A constant offset is emitted directly (the indexer has a ushort overload), not wrapped in (uint).
fragment.Text.Should().Be("UInt16[DS, (ushort)0x0050]");
fragment.Text.Should().Be("UInt16[DS, 0x0050]");
}

[Fact]
Expand All @@ -76,7 +76,17 @@ public void BinaryOperationNodeRendersParenthesizedExpression() {
CSharpFragment fragment = addition.Accept(_emitter).AsExpression();

// Redundant outer parentheses are dropped; precedence-required ones are kept.
fragment.Text.Should().Be("AX + (ushort)0x0001");
fragment.Text.Should().Be("AX + 1");
}

[Fact]
public void NegateOfNegativeConstantIsParenthesized() {
ConstantNode inner = new(DataType.INT16, 0xFFFE); // -2
UnaryOperationNode negate = new(DataType.INT16, UnaryOperation.NEGATE, inner);

CSharpFragment fragment = negate.Accept(_emitter).AsExpression();

fragment.Text.Should().Be("-(-2)");
}

[Fact]
Expand Down
36 changes: 36 additions & 0 deletions tests/Spice86.Tests/CfgCpu/Ast/Value/Constant/ConstantNodeTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Spice86.Core.Emulator.CPU.CfgCpu.Ast;
using Spice86.Core.Emulator.CPU.CfgCpu.Ast.Value.Constant;
using Spice86.Shared.Emulator.Memory;
using Xunit;

namespace Spice86.Tests.CfgCpu.Ast.Value.Constant;
Expand Down Expand Up @@ -110,4 +111,39 @@ public void Convert_Nibble_To_Byte_Unsigned() {
ulong result = node.Convert(DataType.UINT8);
Assert.Equal(0x0Fu, result);
}

public static TheoryData<DataType, ulong, DataType, ulong> CSharpCastCases => new() {
{ DataType.INT8, 0xFF, DataType.UINT16, 0xFFFF },
{ DataType.INT16, 0xFFFF, DataType.UINT32, 0xFFFFFFFF },
{ DataType.UINT16, 0x8000, DataType.INT16, 0x8000 },
{ DataType.UINT8, 0xFF, DataType.INT8, 0xFF },
{ DataType.UINT16, 0x1234, DataType.UINT8, 0x34 },
{ DataType.UINT8, 0x00, DataType.BOOL, 0 },
{ DataType.UINT16, 0x0100, DataType.BOOL, 1 }
};

[Theory]
[MemberData(nameof(CSharpCastCases))]
public void ConvertAsCSharpCast_MatchesUncheckedCSharpCast(DataType sourceType, ulong sourceValue, DataType targetType, ulong expected) {
// Arrange
ConstantNode node = new(sourceType, sourceValue);

// Act
ulong result = node.ConvertAsCSharpCast(targetType);

// Assert
Assert.Equal(expected, result);
}

[Theory]
[InlineData(BitWidth.NIBBLE_4, 0x0FUL)]
[InlineData(BitWidth.QUIBBLE_5, 0x1FUL)]
[InlineData(BitWidth.BYTE_8, 0xFFUL)]
[InlineData(BitWidth.WORD_16, 0xFFFFUL)]
[InlineData(BitWidth.DWORD_32, 0xFFFFFFFFUL)]
[InlineData(BitWidth.BOOL_1, 0x1UL)]
[InlineData(BitWidth.QWORD_64, 0xFFFFFFFFFFFFFFFFUL)]
public void Mask_CoversBitWidth(BitWidth bitWidth, ulong expected) {
Assert.Equal(expected, ConstantNode.Mask(bitWidth));
}
}
Loading
Loading