diff --git a/src/Draco.Compiler.Tests/Decompilation/CilFormatter.cs b/src/Draco.Compiler.Tests/Decompilation/CilFormatter.cs new file mode 100644 index 000000000..72a4eb0c8 --- /dev/null +++ b/src/Draco.Compiler.Tests/Decompilation/CilFormatter.cs @@ -0,0 +1,157 @@ +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Text; +using Draco.Compiler.Internal.Symbols; +using Draco.Compiler.Internal.Symbols.Metadata; +using Draco.Compiler.Tests.Utilities; + +namespace Draco.Compiler.Tests.Decompilation; + +internal static class CilFormatter +{ + public static string VisualizeIl(CompiledLibrary library, MetadataMethodSymbol func, PEReader peReader, MetadataReader reader) + { + var body = peReader.GetMethodBody(func.BodyRelativeVirtualAddress); + + var sb = new StringBuilder(); + var isb = new IndentedStringBuilder(sb); + isb.AppendLine("{"); + isb.PushIndent(); + + // TODO: branching & exception handling + WriteBodyProlog(library, func, reader, body, isb); + WriteInstructions(library, func, reader, body, isb); + + isb.PopIndent(); + isb.AppendLine("}"); + + return sb.ToString(); + } + + private static unsafe void WriteInstructions(CompiledLibrary library, MetadataMethodSymbol func, MetadataReader reader, MethodBodyBlock body, IndentedStringBuilder sb) + { + var blobReader = body.GetILReader(); + + var span = new ReadOnlySpan(blobReader.StartPointer, blobReader.Length); + + var instructions = new List(); + instructions.EnsureCapacity(10); + + HashSet? jumpTargets = null; + + while (!span.IsEmpty) + { + var instruction = InstructionDecoder.Read(span, blobReader.Length - span.Length, library.Codegen.GetSymbol, reader.GetUserString, out var advance); + span = span[advance..]; + + if (InstructionDecoder.IsBranch(instruction.OpCode)) + { + jumpTargets ??= new(); + jumpTargets.Add(((IConvertible)instruction.Operand!).ToInt32(null)); + } + + instructions.Add(instruction); + } + + foreach (var (opCode, offset, operand) in instructions) + { + foreach (var region in body.ExceptionRegions) + if (region.TryOffset == offset) + { + sb.AppendLine(".try {"); + sb.PushIndent(); + } + else if (region.HandlerOffset == offset) + switch (region.Kind) + { + case ExceptionRegionKind.Catch: + break; + case ExceptionRegionKind.Filter: + break; + case ExceptionRegionKind.Finally: + sb.AppendLine("finally {"); + sb.PushIndent(); + break; + case ExceptionRegionKind.Fault: + break; + } + + if (jumpTargets is { } && jumpTargets.Contains(offset)) + using (sb.WithDedent()) + { + sb.Append("IL_"); + sb.Append(offset.ToString("X4")); + sb.AppendLine(":"); + } + + sb.Append(InstructionDecoder.GetText(opCode)); + + switch (operand) + { + case Symbol symbol: + sb.Append(' '); + MethodBodyTokenFormatter.FormatTo(symbol, library.Compilation, sb); + break; + case string strOp: + sb.Append(' '); + sb.Append('"'); + sb.Append(strOp); + sb.Append('"'); + break; + case { } when InstructionDecoder.IsBranch(opCode): + sb.Append(" IL_"); + sb.AppendLine(((IFormattable)operand).ToString("X4", null)); + break; + case { }: + sb.Append(' '); + sb.Append(operand); + break; + case null: + break; + } + + sb.AppendLine(); + + var opCodeEndOffset = offset + InstructionDecoder.GetTotalOpCodeSize(opCode); + + foreach (var region in body.ExceptionRegions) + { + if (region.TryOffset + region.TryLength == opCodeEndOffset + || region.HandlerOffset + region.HandlerLength == opCodeEndOffset) + { + sb.PopIndent(); + sb.AppendLine("}"); + } + } + } + } + + private static void WriteBodyProlog(CompiledLibrary library, MetadataMethodSymbol func, MetadataReader reader, MethodBodyBlock body, IndentedStringBuilder sb) + { + if (!body.LocalSignature.IsNil) + { + sb.Append(".maxstack "); + sb.Append(body.MaxStack); + sb.AppendLine(); + + sb.Append(".locals "); + if (body.LocalVariablesInitialized) + sb.Append("init "); + + sb.Append('('); + + var locals = reader.GetStandaloneSignature(body.LocalSignature).DecodeLocalSignature(library.Compilation.TypeProvider, func); + for (var i = 0; i < locals.Length; i++) + { + if (i > 0) + sb.Append(", "); + + MethodBodyTokenFormatter.FormatTo(locals[i], library.Compilation, sb); + } + + sb.Append(')'); + sb.AppendLine(); + sb.AppendLine(); + } + } +} diff --git a/src/Draco.Compiler.Tests/Decompilation/CilInstruction.cs b/src/Draco.Compiler.Tests/Decompilation/CilInstruction.cs new file mode 100644 index 000000000..5c02f10a4 --- /dev/null +++ b/src/Draco.Compiler.Tests/Decompilation/CilInstruction.cs @@ -0,0 +1,5 @@ +using System.Reflection.Metadata; + +namespace Draco.Compiler.Tests.Decompilation; + +internal readonly record struct CilInstruction(ILOpCode OpCode, int Offset, object? Operand); diff --git a/src/Draco.Compiler.Tests/Decompilation/CilSpaceAgnosticStringComparer.cs b/src/Draco.Compiler.Tests/Decompilation/CilSpaceAgnosticStringComparer.cs new file mode 100644 index 000000000..e2b72d811 --- /dev/null +++ b/src/Draco.Compiler.Tests/Decompilation/CilSpaceAgnosticStringComparer.cs @@ -0,0 +1,118 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Draco.Compiler.Tests.Decompilation; + +internal sealed class CilSpaceAgnosticStringComparer : IEqualityComparer +{ + public static CilSpaceAgnosticStringComparer Ordinal { get; } = new(StringComparison.Ordinal); + + private readonly StringComparison _comparison; + + public CilSpaceAgnosticStringComparer(StringComparison comparison) + { + _comparison = comparison; + } + + public bool Equals(string? x, string? y) + { + if (x is null) + return y is null; + + if (y is null) + return false; + + var xIt = new Enumerator(x); + var yIt = new Enumerator(y); + + while (true) + if (xIt.MoveNext()) + { + if (!yIt.MoveNext()) + return false; + + if (!xIt.CurrentSpan.Equals(yIt.CurrentSpan, _comparison)) + return false; + } + else + // one of them ended earlier + return !yIt.MoveNext(); + } + + public int GetHashCode([DisallowNull] string obj) + { + var hash = new HashCode(); + + var span = obj.AsSpan(); + + foreach (var range in new Enumerable(span)) + hash.AddBytes(MemoryMarshal.AsBytes(span[range])); + + return hash.ToHashCode(); + } + + private readonly ref struct Enumerable + { + public ReadOnlySpan String { get; } + + public Enumerable(ReadOnlySpan @string) => String = @string; + + public Enumerator GetEnumerator() => new(String); + } + + private ref struct Enumerator + { + public ReadOnlySpan String { get; } + + private int _start; + private int _end; + + public Enumerator(ReadOnlySpan @string) + { + String = @string; + } + + public bool MoveNext() + { + var s = String; + + _start = _end; + + while (_start < s.Length && IsWhiteSpace(s[_start])) + _start++; + + _end = _start; + + if (_start == s.Length) + return false; + + if (s[_end] is '\'' or '\"') + { + var quote = s[_end]; + _end++; + + while (s[_end] != quote && _end < s.Length) + _end++; + + if (_end == s.Length) + throw new InvalidOperationException("Unclosed quoted string"); + + _end++; + } + else + while (_end < s.Length && !IsWhiteSpace(s[_end])) + _end++; + return true; + } + + private static bool IsWhiteSpace(char ch) + { + // don't use char.IsWhiteSpace as it checks additional chars, which won't appear in code + return ch is ' ' or '\n' or '\r'; + } + + public readonly ReadOnlySpan CurrentSpan => String[Current]; + + public readonly Range Current => new(_start, _end); + } +} diff --git a/src/Draco.Compiler.Tests/Decompilation/CompiledLibrary.cs b/src/Draco.Compiler.Tests/Decompilation/CompiledLibrary.cs new file mode 100644 index 000000000..2f8f8637d --- /dev/null +++ b/src/Draco.Compiler.Tests/Decompilation/CompiledLibrary.cs @@ -0,0 +1,130 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Reflection; +using System.Reflection.PortableExecutable; +using System.Runtime.Loader; +using Draco.Compiler.Api; +using Draco.Compiler.Api.Semantics; +using Draco.Compiler.Api.Syntax; +using Draco.Compiler.Internal.Codegen; +using Draco.Compiler.Internal.Symbols.Metadata; +using Draco.Compiler.Tests.Utilities; + +namespace Draco.Compiler.Tests.Decompilation; + +internal sealed class CompiledLibrary +{ + public CompiledLibrary(Compilation compilation, MetadataCodegen codeGen, ReadOnlyMemory assemblyBytes) + { + Compilation = compilation; + Codegen = codeGen; + AssemblyBytes = assemblyBytes; + } + + public Compilation Compilation { get; } + public MetadataCodegen Codegen { get; } + public ReadOnlyMemory AssemblyBytes { get; } + + private PEReader GetPeReader() => _reader ??= new PEReader(new ReadOnlyMemoryStream(AssemblyBytes)); + private PEReader? _reader; + + public Assembly GetLoadedAssembly() => _loadedAssembly ??= LoadAssembly(); + private Assembly? _loadedAssembly; + + private Assembly LoadAssembly() + { + Debug.Assert(_loadedAssembly is null); + + var loadContext = new AssemblyLoadContext("testLoadContext"); + + var asm = loadContext.LoadFromStream(new ReadOnlyMemoryStream(AssemblyBytes)); + return asm; + } + + public void AssertIL(string memberName, string il) + { + var peReader = GetPeReader(); + var compiledAssemblyReference = MetadataReference.FromPeReader(peReader); + var c = Compilation.Create( + ImmutableArray.Empty, + Compilation.MetadataReferences.Add(compiledAssemblyReference)); + + + // TODO: very clunky lookup which won't work if multiple method are defined + var member = + Compilation + .GetSemanticModel(Compilation.SyntaxTrees[0]) + .GetAllDefinedSymbols(Compilation.SyntaxTrees[0].Root) + .OfType() + .Single(s => s.Symbol is Draco.Compiler.Internal.Symbols.FunctionSymbol); + + var func = Assert.IsAssignableFrom(member); + + var actualIl = CilFormatter.VisualizeIl(this, func, peReader, compiledAssemblyReference.MetadataReader); + + Assert.True(CilSpaceAgnosticStringComparer.Ordinal.Equals(il, actualIl)); + } + + public TypeInfo GetTypeInfo(string type) + { + var asm = GetLoadedAssembly(); + + var typeInfo = asm.GetType(type); + + Assert.NotNull(typeInfo); + + return typeInfo.GetTypeInfo(); + } + + public MethodInfo GetMethodInfo(string member) + { + var memberPath = member.Split('.'); + + var type = GetTypeInfo(string.Join(".", memberPath.SkipLast(1))); + + var method = type.GetMethod(memberPath[^1]); + + Assert.NotNull(method); + + return method; + } + + public void Execute(string member, Action configureBuilder) + { + var method = GetMethodInfo(member); + + var builder = new MethodExecutionBuilder(); + + configureBuilder.Invoke(builder); + + Debug.Assert(builder.CheckStdOutActon is { } || builder.CheckReturnAction is { }); + + IDisposable disposable = Disposable.Empty; + + string? stdOut = null; + + if (builder.CheckStdOutActon is { }) + { + var writer = new StringWriter(); + var defaultStdOut = Console.Out; + + Console.SetOut(writer); + + disposable = Disposable.Create(() => + { + Console.SetOut(defaultStdOut); + stdOut = writer.ToString(); + }); + } + + object? result; + + using (disposable) + { + result = method.Invoke(builder.Instance, builder.Arguments); + } + + builder.CheckReturnAction?.Invoke(result); + builder.CheckStdOutActon?.Invoke(stdOut!); + } +} diff --git a/src/Draco.Compiler.Tests/Decompilation/InstructionDecoder.cs b/src/Draco.Compiler.Tests/Decompilation/InstructionDecoder.cs new file mode 100644 index 000000000..602d2bf0b --- /dev/null +++ b/src/Draco.Compiler.Tests/Decompilation/InstructionDecoder.cs @@ -0,0 +1,105 @@ +using System.Buffers.Binary; +using System.Reflection; +using System.Reflection.Emit; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Draco.Compiler.Tests.Decompilation; + +internal static class InstructionDecoder +{ + public static string GetText(ILOpCode code) => s_opCodes[(ushort)code].Name!; + + public static CilInstruction Read(ReadOnlySpan ilStream, int offset, Func resolveToken, Func resolveString, out int advance) + { + ushort id = ilStream[0]; + + var opCodeSize = 1; + + if (IsWideInstruction(id)) + { + id = BinaryPrimitives.ReadUInt16BigEndian(ilStream); + opCodeSize++; + } + + var operandType = GetOperandType((ILOpCode)id); + + advance = opCodeSize + GetOperandSize(operandType); + + var operand = ReadOperand(operandType, ilStream[opCodeSize..], offset + advance, resolveToken, resolveString); + + return new CilInstruction((ILOpCode)id, offset, operand); + } + + private static object? ReadOperand(OperandType type, ReadOnlySpan span, int offset, Func resolveToken, Func resolveString) + { + return type switch + { + OperandType.InlineBrTarget => BinaryPrimitives.ReadInt32LittleEndian(span) + offset, + OperandType.ShortInlineBrTarget => (byte)(span[0] + offset), + + OperandType.ShortInlineVar or + OperandType.ShortInlineI => span[0], + + OperandType.InlineVar => BinaryPrimitives.ReadInt16LittleEndian(span), + + OperandType.InlineI or + OperandType.InlineSwitch => BinaryPrimitives.ReadInt32LittleEndian(span), + OperandType.InlineI8 => BinaryPrimitives.ReadInt64LittleEndian(span), + + // standalone signature can describe locals or 'calli' instruction, but in this context it's only 'calli' + OperandType.InlineSig or + OperandType.InlineMethod or + OperandType.InlineTok or + OperandType.InlineType or + OperandType.InlineField => resolveToken(MetadataTokens.EntityHandle(BinaryPrimitives.ReadInt32LittleEndian(span))), + + OperandType.InlineString => resolveString(MetadataTokens.UserStringHandle(BinaryPrimitives.ReadInt32LittleEndian(span))), + + OperandType.InlineR => BinaryPrimitives.ReadDoubleLittleEndian(span), + OperandType.ShortInlineR => BinaryPrimitives.ReadSingleLittleEndian(span), + + OperandType.InlineNone => null, + + _ => throw new NotSupportedException(), + }; + } + + public static int GetTotalOpCodeSize(ILOpCode opCode) => 1 + Convert.ToInt32(IsWideInstruction((ushort)opCode)) + GetOperandSize(GetOperandType(opCode)); + + private static bool IsWideInstruction(ushort code) => s_opCodes[code].OpCodeType is OpCodeType.Nternal; + + private static OperandType GetOperandType(ILOpCode code) => s_opCodes[(ushort)code].OperandType; + + public static bool IsBranch(ILOpCode code) => s_opCodes[(ushort)code].OperandType is OperandType.InlineBrTarget or OperandType.ShortInlineBrTarget; + + private static int GetOperandSize(OperandType type) => type switch + { + OperandType.InlineBrTarget => 4, + OperandType.InlineField => 4, + OperandType.InlineI => 4, + OperandType.InlineI8 => 8, + OperandType.InlineMethod => 4, + OperandType.InlineNone => 0, + OperandType.InlineR => 8, + OperandType.InlineSig => 4, + OperandType.InlineString => 4, + OperandType.InlineSwitch => 4, + OperandType.InlineTok => 4, + OperandType.InlineType => 4, + OperandType.InlineVar => 2, + OperandType.ShortInlineBrTarget => 1, + OperandType.ShortInlineI => 1, + OperandType.ShortInlineR => 4, + OperandType.ShortInlineVar => 1, + //OperandType.InlinePhi // reserved in spec + _ => throw new NotSupportedException(), + }; + + private static readonly Dictionary s_opCodes = + typeof(OpCodes) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(f => (OpCode)f.GetValue(null)!) + .ToDictionary(o => (ushort)o.Value, o => o); + +} diff --git a/src/Draco.Compiler.Tests/Decompilation/MethodBodyTokenFormatter.cs b/src/Draco.Compiler.Tests/Decompilation/MethodBodyTokenFormatter.cs new file mode 100644 index 000000000..ffca4a002 --- /dev/null +++ b/src/Draco.Compiler.Tests/Decompilation/MethodBodyTokenFormatter.cs @@ -0,0 +1,144 @@ +using System.Diagnostics; +using Draco.Compiler.Api; +using Draco.Compiler.Internal.Symbols; +using Draco.Compiler.Tests.Utilities; + +namespace Draco.Compiler.Tests.Decompilation; + +internal sealed class MethodBodyTokenFormatter : SymbolVisitor +{ + private readonly IndentedStringBuilder _sb; + private readonly Compilation _compilation; + + // TODO: generics + private MethodBodyTokenFormatter(IndentedStringBuilder sb, Compilation compilation) + { + _sb = sb; + _compilation = compilation; + } + + public static void FormatTo(Symbol symbol, Compilation compilation, IndentedStringBuilder stringBuilder) + { + var formatter = new MethodBodyTokenFormatter(stringBuilder, compilation); + symbol.Accept(formatter); + } + + public override void VisitLocal(LocalSymbol localSymbol) + { + _sb.Append(localSymbol.Type); + + if (!string.IsNullOrEmpty(localSymbol.Name)) + { + _sb.Append(' '); + _sb.Append(EscapeName(localSymbol.Name)); + } + } + + public override void VisitTypeParameter(TypeParameterSymbol typeParameterSymbol) + { + throw new NotImplementedException(); + } + + public override void VisitLabel(LabelSymbol labelSymbol) + { + throw new NotSupportedException(); + } + + public override void VisitParameter(ParameterSymbol parameterSymbol) + { + throw new NotSupportedException(); + } + + public override void VisitField(FieldSymbol fieldSymbol) + { + throw new NotImplementedException(); + } + + public override void VisitProperty(PropertySymbol fieldSymbol) + { + // can be used with ldtoken + throw new NotImplementedException(); + } + + public override void VisitModule(ModuleSymbol namespaceSymbol) + { + throw new NotSupportedException(); + } + + public override void VisitType(TypeSymbol typeSymbol) + { + // when it's standalone type, it's fine to write short name + FormatTypeCore(typeSymbol, true); + } + + private void FormatTypeCore(TypeSymbol typeSymbol, bool allowPrimitives) + { + if (allowPrimitives) + { + if (typeSymbol == _compilation.WellKnownTypes.SystemInt32) + { + _sb.Append("int32"); + return; + } + else if (typeSymbol == _compilation.WellKnownTypes.SystemString) + { + _sb.Append("string"); + return; + } + } + + var ancestors = typeSymbol.AncestorChain.Skip(1).Reverse().Skip(2); // skip self then assembly and root namespace, which have empty name + foreach (var ancestor in ancestors) + { + _sb.Append(ancestor.Name); + if (ancestor is TypeSymbol) + _sb.Append('/'); + else + { + Debug.Assert(ancestor is ModuleSymbol); + _sb.Append('.'); + } + } + + _sb.Append(typeSymbol.Name); + } + + public override void VisitFunction(FunctionSymbol functionSymbol) + { + if (!functionSymbol.IsStatic) + _sb.Append("instance "); + + FormatTypeCore(functionSymbol.ReturnType, true); + + _sb.Append(' '); + + if (functionSymbol.ContainingSymbol is { } container) + { + // cannot use short name when referencing members of type + // e.g. 'string System.Int32::ToString()' is allowed + // but 'string int32::ToString()' is not + FormatTypeCore((TypeSymbol)container, false); + _sb.Append("::"); + } + + _sb.Append(EscapeName(functionSymbol.Name)); + + _sb.Append('('); + + for (var i = 0; i < functionSymbol.Parameters.Length; i++) + { + if (i > 0) + _sb.Append(", "); + + var parameter = functionSymbol.Parameters[i]; + FormatTypeCore(parameter.Type, true); + } + + _sb.Append(')'); + } + + private static string EscapeName(string name) + { + return name; + } +} diff --git a/src/Draco.Compiler.Tests/Decompilation/MethodExecutionBuilder.cs b/src/Draco.Compiler.Tests/Decompilation/MethodExecutionBuilder.cs new file mode 100644 index 000000000..b3ac3d9b6 --- /dev/null +++ b/src/Draco.Compiler.Tests/Decompilation/MethodExecutionBuilder.cs @@ -0,0 +1,36 @@ +namespace Draco.Compiler.Tests.Decompilation; + +internal sealed class MethodExecutionBuilder +{ + public object?[] Arguments { get; private set; } = Array.Empty(); + + public object? Instance { get; private set; } + + public Action? CheckReturnAction { get; private set; } + + public Action? CheckStdOutActon { get; private set; } + + public MethodExecutionBuilder Return(Action action) + { + CheckReturnAction = action; + return this; + } + + public MethodExecutionBuilder StdOut(Action action) + { + CheckStdOutActon = action; + return this; + } + + public MethodExecutionBuilder WithArguments(params object?[] arguments) + { + Arguments = arguments; + return this; + } + + public MethodExecutionBuilder WithInstance(object instance) + { + Instance = instance; + return this; + } +} diff --git a/src/Draco.Compiler.Tests/Draco.Compiler.Tests.csproj b/src/Draco.Compiler.Tests/Draco.Compiler.Tests.csproj index 6186d9c0d..b4ddfa5a8 100644 --- a/src/Draco.Compiler.Tests/Draco.Compiler.Tests.csproj +++ b/src/Draco.Compiler.Tests/Draco.Compiler.Tests.csproj @@ -6,6 +6,7 @@ enable 11 false + true diff --git a/src/Draco.Compiler.Tests/Utilities/Disposable.cs b/src/Draco.Compiler.Tests/Utilities/Disposable.cs new file mode 100644 index 000000000..953fb7d2b --- /dev/null +++ b/src/Draco.Compiler.Tests/Utilities/Disposable.cs @@ -0,0 +1,30 @@ +namespace Draco.Compiler.Tests.Utilities; + +internal static class Disposable +{ + public static IDisposable Empty { get; } = new EmptyDisposable(); + + public static IDisposable Create(Action action) => new ActionDisposable(action); + + private sealed class EmptyDisposable : IDisposable + { + public void Dispose() + { + } + } + + private class ActionDisposable : IDisposable + { + private Action? _action; + + public ActionDisposable(Action action) + { + _action = action; + } + + public void Dispose() + { + Interlocked.Exchange(ref _action, null)?.Invoke(); + } + } +} diff --git a/src/Draco.Compiler.Tests/Utilities/IndentedStringBuilder.cs b/src/Draco.Compiler.Tests/Utilities/IndentedStringBuilder.cs new file mode 100644 index 000000000..79fbc1274 --- /dev/null +++ b/src/Draco.Compiler.Tests/Utilities/IndentedStringBuilder.cs @@ -0,0 +1,103 @@ +using System.Text; + +namespace Draco.Compiler.Tests.Utilities; + +internal sealed class IndentedStringBuilder +{ + private int _indent; + private int _indentSize = 4; + private bool _doIndent = false; + private readonly StringBuilder _sb; + + public IndentedStringBuilder(StringBuilder sb) + { + _sb = sb; + } + + private void WriteIndent() + { + if (_doIndent) + { + Span span = stackalloc char[_indent * _indentSize]; + span.Fill(' '); + _sb.Append(span); + } + } + + public void Append(string text) + { + WriteIndent(); + _sb.Append(text); + _doIndent = false; + } + + public void Append(object o) + { + WriteIndent(); + _sb.Append(o); + _doIndent = false; + } + + public void Append(int i) + { + WriteIndent(); + _sb.Append(i); + _doIndent = false; + } + + public void Append(char c) + { + WriteIndent(); + _sb.Append(c); + _doIndent = false; + } + + public void AppendLine(string text) + { + WriteIndent(); + _sb.AppendLine(text); + _doIndent = true; + } + + public void AppendLine() + { + WriteIndent(); + _sb.AppendLine(); + _doIndent = true; + } + + public void PushIndent() => _indent++; + public void PopIndent() => _indent--; + + public Indenter WithIndent() + { + PushIndent(); + return new Indenter(this, true); + } + + public Indenter WithDedent() + { + PopIndent(); + return new Indenter(this, false); + } + + public readonly struct Indenter : IDisposable + { + private readonly IndentedStringBuilder _sb; + private readonly bool _doPop; + + public Indenter(IndentedStringBuilder sb, bool doPop) + { + _sb = sb; + _doPop = doPop; + } + + public void Dispose() + { + if (_doPop) + _sb.PopIndent(); + else + _sb.PushIndent(); + } + } +} diff --git a/src/Draco.Compiler.Tests/Utilities/ReadOnlyMemoryStream.cs b/src/Draco.Compiler.Tests/Utilities/ReadOnlyMemoryStream.cs new file mode 100644 index 000000000..6f33ec7c8 --- /dev/null +++ b/src/Draco.Compiler.Tests/Utilities/ReadOnlyMemoryStream.cs @@ -0,0 +1,95 @@ +using System.ComponentModel; + +namespace Draco.Compiler.Tests.Utilities; + +internal sealed class ReadOnlyMemoryStream : Stream +{ + private readonly ReadOnlyMemory _memory; + + public ReadOnlyMemoryStream(ReadOnlyMemory memory) + { + _memory = memory; + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => _memory.Length; + + private int _position; + + public override long Position + { + get => _position; + set + { + if (value < 0 || value > _memory.Length) + throw new ArgumentOutOfRangeException(nameof(value)); + + _position = (int)value; // conversion would succeed, no need for checked context + } + } + + + public override int Read(byte[] buffer, int offset, int count) + { + return Read(buffer.AsSpan(offset, count)); + } + + public override int Read(Span buffer) + { + if (Position == _memory.Length) + return 0; + else if (Position > _memory.Length) + throw new InvalidOperationException(); + else + { + var bytesRead = Math.Min(_memory.Length - _position, buffer.Length); + _memory.Span.Slice(_position, bytesRead).CopyTo(buffer); + _position += bytesRead; + return bytesRead; + } + } + + public override int ReadByte() + { + if (Position == _memory.Length) + return -1; + else if (Position > _memory.Length) + throw new InvalidOperationException(); + else + { + _position++; + return _memory.Span[_position - 1]; + } + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.FromResult(Read(buffer.AsSpan(offset, count))); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult(Read(buffer.Span)); + } + + + public override long Seek(long offset, SeekOrigin origin) + { + return origin switch + { + SeekOrigin.Begin => Position = offset, + SeekOrigin.Current => Position += offset, + SeekOrigin.End => Position = Length + offset, + _ => throw new InvalidEnumArgumentException(nameof(origin), (int)origin, typeof(SeekOrigin)), + }; + } + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override void Flush() => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); +} diff --git a/src/Draco.Compiler/Api/MetadataReference.cs b/src/Draco.Compiler/Api/MetadataReference.cs index 824ae2aba..29f0303aa 100644 --- a/src/Draco.Compiler/Api/MetadataReference.cs +++ b/src/Draco.Compiler/Api/MetadataReference.cs @@ -53,6 +53,17 @@ public static MetadataReference FromPeStream(Stream peStream) return new MetadataReaderReference(metadataReader); } + /// + /// Creates a metadata reference from the given PE reader. + /// + /// The PE reader to create the metadata reference from. + /// The reading up from . + public static MetadataReference FromPeReader(PEReader peReader) + { + var metadataReader = peReader.GetMetadataReader(); + return new MetadataReaderReference(metadataReader); + } + /// /// Adds xml documentation to this metadata reference. /// diff --git a/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs b/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs index a0e05277a..cb6c08d36 100644 --- a/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs +++ b/src/Draco.Compiler/Internal/Codegen/MetadataCodegen.cs @@ -7,6 +7,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; using Draco.Compiler.Api; using Draco.Compiler.Internal.OptimizingIr.Model; using Draco.Compiler.Internal.Symbols; @@ -139,8 +140,34 @@ public TypeReferenceHandle GetModuleReferenceHandle(IModule module) public MemberReferenceHandle GetIntrinsicReferenceHandle(Symbol symbol) => this.intrinsicReferenceHandles[symbol]; - // TODO: This can be cached by symbol to avoid double reference instertion + private readonly Dictionary _symbolToToken = new(ReferenceEqualityComparer.Instance); + public EntityHandle GetEntityHandle(Symbol symbol) + { + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(_symbolToToken, symbol, out bool exists); + + if (!exists) + { + value = GetEntityHandleCore(symbol); + } + + return value; + } + + public Symbol GetSymbol(EntityHandle handle) + { + foreach (var (k, v) in _symbolToToken) + { + if (v == handle) + { + return k; + } + } + + throw new KeyNotFoundException(); + } + + private EntityHandle GetEntityHandleCore(Symbol symbol) { switch (symbol) { diff --git a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs index ee6f6106a..bf7200d58 100644 --- a/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs +++ b/src/Draco.Compiler/Internal/Symbols/Metadata/MetadataMethodSymbol.cs @@ -16,6 +16,8 @@ namespace Draco.Compiler.Internal.Symbols.Metadata; /// internal class MetadataMethodSymbol : FunctionSymbol, IMetadataSymbol { + public int BodyRelativeVirtualAddress => this.methodDefinition.RelativeVirtualAddress; + public override ImmutableArray GenericParameters => InterlockedUtils.InitializeDefault(ref this.genericParameters, this.BuildGenericParameters); private ImmutableArray genericParameters;