// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using EpicGames.BuildGraph.Expressions; using EpicGames.Core; using Microsoft.Extensions.Logging; namespace EpicGames.BuildGraph { /// /// Exception thrown by the runtime /// public sealed class BgBytecodeException : Exception { /// /// Source file that the error was thrown from /// public string SourceFile { get; } /// /// Line number that threw the exception /// public int SourceLine { get; } /// /// Message to display /// public string Diagnostic { get; } /// /// Constructor /// /// /// /// public BgBytecodeException(string sourceFile, int sourceLine, string diagnostic) { SourceFile = sourceFile; SourceLine = sourceLine; Diagnostic = diagnostic; } /// public override string ToString() => $"{SourceFile}({SourceLine}): {Diagnostic}"; } /// /// Interprets compiled buildgraph bytecode /// public class BgInterpreter { enum BgArg { None, Value, ArgList, Fragment, Name, Thunk, StringLiteral, VarIntSigned, VarIntUnsigned, } readonly struct BgOpcodeInfo { readonly uint _data; public BgOpcodeInfo(BgArg arg1) : this(1, arg1, BgArg.None, BgArg.None) { } public BgOpcodeInfo(BgArg arg1, BgArg arg2) : this(2, arg1, arg2, BgArg.None) { } public BgOpcodeInfo(BgArg arg1, BgArg arg2, BgArg arg3) : this(3, arg1, arg2, arg3) { } private BgOpcodeInfo(int count, BgArg arg1, BgArg arg2, BgArg arg3) { _data = (uint)arg1 | ((uint)arg2 << 4) | ((uint)arg3 << 8) | ((uint)count << 12); } public int ArgCount => (int)((_data >> 12) & 0x7); public BgArg Arg0 => GetArg(0); public BgArg Arg1 => GetArg(1); public BgArg Arg2 => GetArg(2); public BgArg GetArg(int idx) => (BgArg)((_data >> (idx * 4)) & 0xf); } class Frame { public int Offset { get; set; } public IReadOnlyList Arguments { get; } public Dictionary Objects { get; set; } public Frame(Frame other) { Offset = other.Offset; Arguments = other.Arguments; Objects = other.Objects; } public Frame(int offset, IReadOnlyList arguments) { Offset = offset; Arguments = arguments; Objects = new Dictionary(); } } readonly byte[] _data; readonly BgThunkDef[] _thunks; readonly string[] _names; readonly IReadOnlyDictionary _options; readonly BgBytecodeVersion _version; readonly int[] _fragments; readonly List _optionDefs = new List(); /// /// The option definitions that were parsed during execution /// public IReadOnlyList OptionDefs => _optionDefs; static readonly BgOpcodeInfo[] s_opcodeTable = CreateOpcodeLookup(); static BgOpcodeInfo[] CreateOpcodeLookup() { BgOpcodeInfo[] opcodes = new BgOpcodeInfo[256]; opcodes[(int)BgOpcode.BoolFalse] = new BgOpcodeInfo(); opcodes[(int)BgOpcode.BoolTrue] = new BgOpcodeInfo(); opcodes[(int)BgOpcode.BoolNot] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.BoolAnd] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.BoolOr] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.BoolXor] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.BoolEq] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.BoolOption] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.IntLiteral] = new BgOpcodeInfo(BgArg.VarIntSigned); opcodes[(int)BgOpcode.IntEq] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.IntLt] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.IntGt] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.IntAdd] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.IntMultiply] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.IntDivide] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.IntModulo] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.IntNegate] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.IntOption] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.StrEmpty] = new BgOpcodeInfo(); opcodes[(int)BgOpcode.StrLiteral] = new BgOpcodeInfo(BgArg.StringLiteral); opcodes[(int)BgOpcode.StrCompare] = new BgOpcodeInfo(BgArg.Value, BgArg.Value, BgArg.VarIntUnsigned); opcodes[(int)BgOpcode.StrConcat] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.StrFormat] = new BgOpcodeInfo(BgArg.Value, BgArg.ArgList); opcodes[(int)BgOpcode.StrSplit] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.StrJoin] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.StrMatch] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.StrReplace] = new BgOpcodeInfo(BgArg.Value, BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.StrOption] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.EnumConstant] = new BgOpcodeInfo(BgArg.VarIntSigned); opcodes[(int)BgOpcode.EnumParse] = new BgOpcodeInfo(BgArg.Value, BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.EnumToString] = new BgOpcodeInfo(BgArg.Value, BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.ListEmpty] = new BgOpcodeInfo(); opcodes[(int)BgOpcode.ListPush] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.ListPushLazy] = new BgOpcodeInfo(BgArg.Value, BgArg.Fragment); opcodes[(int)BgOpcode.ListCount] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.ListElement] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.ListConcat] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.ListUnion] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.ListExcept] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.ListSelect] = new BgOpcodeInfo(BgArg.Value, BgArg.Fragment); opcodes[(int)BgOpcode.ListWhere] = new BgOpcodeInfo(BgArg.Value, BgArg.Fragment); opcodes[(int)BgOpcode.ListDistinct] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.ListContains] = new BgOpcodeInfo(BgArg.Value, BgArg.Value); opcodes[(int)BgOpcode.ListLazy] = new BgOpcodeInfo(BgArg.Fragment); opcodes[(int)BgOpcode.ListOption] = new BgOpcodeInfo(BgArg.Value); opcodes[(int)BgOpcode.ObjEmpty] = new BgOpcodeInfo(); opcodes[(int)BgOpcode.ObjGet] = new BgOpcodeInfo(BgArg.Value, BgArg.Name, BgArg.Value); opcodes[(int)BgOpcode.ObjSet] = new BgOpcodeInfo(BgArg.Value, BgArg.Name, BgArg.Value); opcodes[(int)BgOpcode.Call] = new BgOpcodeInfo(BgArg.Fragment, BgArg.ArgList); opcodes[(int)BgOpcode.Argument] = new BgOpcodeInfo(BgArg.VarIntUnsigned); opcodes[(int)BgOpcode.Jump] = new BgOpcodeInfo(BgArg.Fragment); opcodes[(int)BgOpcode.Choose] = new BgOpcodeInfo(BgArg.Value, BgArg.Fragment, BgArg.Fragment); opcodes[(int)BgOpcode.Throw] = new BgOpcodeInfo(BgArg.StringLiteral, BgArg.VarIntUnsigned, BgArg.Value); opcodes[(int)BgOpcode.Null] = new BgOpcodeInfo(); opcodes[(int)BgOpcode.Thunk] = new BgOpcodeInfo(BgArg.Thunk, BgArg.ArgList); return opcodes; } /// /// Constructor /// /// /// Thunks to native code /// Options for evaluating the graph public BgInterpreter(byte[] data, BgThunkDef[] thunks, IReadOnlyDictionary options) { _data = data; _thunks = thunks; _options = options; MemoryReader reader = new MemoryReader(data); _version = (BgBytecodeVersion)reader.ReadUnsignedVarInt(); _names = reader.ReadVariableLengthArray(() => reader.ReadString()); int[] fragmentLengths = reader.ReadVariableLengthArray(() => (int)reader.ReadUnsignedVarInt()); int offset = data.Length - reader.RemainingMemory.Length; _fragments = new int[fragmentLengths.Length]; for (int idx = 0; idx < _fragments.Length; idx++) { _fragments[idx] = offset; offset += fragmentLengths[idx]; } } /// /// Evaluates the graph /// public object Evaluate() { // Take the given parameters, evaluate the graph that was produced (creating a map of all named entites) // Filter the map to the required target // Evaluate it fully into a BgGraph return Evaluate(new Frame(_fragments[0], Array.Empty())); } object Evaluate(Frame frame) { BgOpcode opcode = ReadOpcode(frame); BgOpcodeInfo opcodeInfo = s_opcodeTable[(int)opcode]; object arg0 = ReadArgument(frame, opcodeInfo.Arg0); object arg1 = ReadArgument(frame, opcodeInfo.Arg1); object arg2 = ReadArgument(frame, opcodeInfo.Arg2); return Evaluate(frame, opcode, arg0, arg1, arg2); } object ReadArgument(Frame frame, BgArg type) { switch (type) { case BgArg.None: return null!; case BgArg.Value: return Evaluate(frame); case BgArg.Name: return ReadName(frame); case BgArg.Fragment: return ReadFragment(frame); case BgArg.VarIntSigned: return ReadSignedInteger(frame); case BgArg.VarIntUnsigned: return ReadUnsignedInteger(frame); case BgArg.StringLiteral: return ReadString(frame); case BgArg.ArgList: { int argCount = (int)ReadUnsignedInteger(frame); object[] argList = new object[argCount]; for (int argIdx = 0; argIdx < argCount; argIdx++) { argList[argIdx] = Evaluate(frame); } return argList; } case BgArg.Thunk: return _thunks[(int)ReadUnsignedInteger(frame)]; default: throw new InvalidDataException(); } } object Evaluate(Frame frame, BgOpcode opcode, object arg0, object arg1, object arg2) { switch (opcode) { #region Bool opcodes case BgOpcode.BoolFalse: return false; case BgOpcode.BoolTrue: return true; case BgOpcode.BoolNot: return !(bool)arg0; case BgOpcode.BoolAnd: { bool lhs = (bool)arg0; bool rhs = (bool)arg1; return lhs & rhs; } case BgOpcode.BoolOr: { bool lhs = (bool)arg0; bool rhs = (bool)arg1; return lhs | rhs; } case BgOpcode.BoolXor: { bool lhs = (bool)arg0; bool rhs = (bool)arg1; return lhs ^ rhs; } case BgOpcode.BoolEq: { bool lhs = (bool)arg0; bool rhs = (bool)arg1; return lhs == rhs; } case BgOpcode.BoolOption: { BgObjectDef obj = (BgObjectDef)arg0; BgBoolOptionDef option = obj.Deserialize(); _optionDefs.Add(option); bool value = option.DefaultValue; if (_options.TryGetValue(option.Name, out string? str)) { value = Boolean.Parse(str); } return value; } #endregion #region Integer opcodes case BgOpcode.IntLiteral: return arg0; case BgOpcode.IntEq: { int lhs = (int)arg0; int rhs = (int)arg1; return lhs == rhs; } case BgOpcode.IntLt: { int lhs = (int)arg0; int rhs = (int)arg1; return lhs < rhs; } case BgOpcode.IntGt: { int lhs = (int)arg0; int rhs = (int)arg1; return lhs > rhs; } case BgOpcode.IntAdd: { int lhs = (int)arg0; int rhs = (int)arg1; return lhs + rhs; } case BgOpcode.IntMultiply: { int lhs = (int)arg0; int rhs = (int)arg1; return lhs * rhs; } case BgOpcode.IntDivide: { int lhs = (int)arg0; int rhs = (int)arg1; return lhs / rhs; } case BgOpcode.IntModulo: { int lhs = (int)arg0; int rhs = (int)arg1; return lhs % rhs; } case BgOpcode.IntNegate: return -(int)arg0; case BgOpcode.IntOption: { BgObjectDef obj = (BgObjectDef)arg0; BgIntOptionDef option = obj.Deserialize(); _optionDefs.Add(option); int value = option.DefaultValue; if (_options.TryGetValue(option.Name, out string? str)) { value = Int32.Parse(str); } return value; } #endregion #region String opcodes case BgOpcode.StrEmpty: return String.Empty; case BgOpcode.StrLiteral: return arg0; case BgOpcode.StrCompare: { string lhs = (string)arg0; string rhs = (string)arg1; StringComparison comparison = (StringComparison)(ulong)arg2; return String.Compare(lhs, rhs, comparison); } case BgOpcode.StrConcat: { string lhs = (string)arg0; string rhs = (string)arg1; return lhs + rhs; } case BgOpcode.StrFormat: { string format = (string)arg0; object?[] arguments = (object?[])arg1; return String.Format(format, arguments); } case BgOpcode.StrSplit: { string source = (string)arg0; string separator = (string)arg1; return source.Split(separator, StringSplitOptions.RemoveEmptyEntries); } case BgOpcode.StrJoin: { string lhs = (string)arg0; IEnumerable rhs = (IEnumerable)arg1; return String.Join(lhs, rhs); } case BgOpcode.StrMatch: { string input = (string)arg0; string pattern = (string)arg1; return Regex.IsMatch(input, pattern); } case BgOpcode.StrReplace: { string input = (string)arg0; string pattern = (string)arg1; string replacement = (string)arg2; return Regex.Replace(input, pattern, replacement); } case BgOpcode.StrOption: { BgObjectDef obj = (BgObjectDef)arg0; BgStringOptionDef option = obj.Deserialize(); _optionDefs.Add(option); string value = option.DefaultValue; if (_options.TryGetValue(option.Name, out string? result)) { value = result; } return value; } #endregion #region Enum opcodes case BgOpcode.EnumConstant: return (int)arg0; case BgOpcode.EnumParse: { string name = (string)arg0; List names = GetListArg(arg1); List values = GetListArg(arg2); for (int idx = 0; idx < names.Count; idx++) { if (String.Equals(names[idx], name, StringComparison.OrdinalIgnoreCase)) { return values[idx]; } } throw new InvalidDataException($"Unable to parse enum '{name}'"); } case BgOpcode.EnumToString: { int value = (int)arg0; List names = GetListArg(arg1); List values = GetListArg(arg2); for (int idx = 0; idx < names.Count; idx++) { if (value == values[idx]) { return names[idx]; } } return $"{value}"; } #endregion #region List opcodes case BgOpcode.ListEmpty: return Enumerable.Empty(); case BgOpcode.ListPush: { IEnumerable list = (IEnumerable)arg0; object item = arg1; return list.Concat(new[] { item }); } case BgOpcode.ListPushLazy: { IEnumerable list = (IEnumerable)arg0; int fragment = (int)arg1; IEnumerable item = LazyEvaluateItem(new Frame(frame), fragment); return list.Concat(item); } case BgOpcode.ListCount: { IEnumerable list = (IEnumerable)arg0; return list.Count(); } case BgOpcode.ListElement: { IEnumerable list = (IEnumerable)arg0; int index = (int)arg1; return list.ElementAt(index); } case BgOpcode.ListConcat: { IEnumerable lhs = (IEnumerable)arg0; IEnumerable rhs = (IEnumerable)arg1; return Enumerable.Concat(lhs, rhs); } case BgOpcode.ListUnion: { IEnumerable lhs = (IEnumerable)arg0; IEnumerable rhs = (IEnumerable)arg1; return lhs.Union(rhs); } case BgOpcode.ListExcept: { IEnumerable lhs = (IEnumerable)arg0; IEnumerable rhs = (IEnumerable)arg1; return lhs.Except(rhs); } case BgOpcode.ListSelect: { IEnumerable source = (IEnumerable)arg0; int fragment = (int)arg1; return source.Select(x => Call(fragment, new object[] { x })); } case BgOpcode.ListWhere: { IEnumerable source = (IEnumerable)arg0; int fragment = (int)arg1; return source.Where(x => (bool)Call(fragment, new object[] { x })); } case BgOpcode.ListDistinct: { IEnumerable source = (IEnumerable)arg0; return source.Distinct(); } case BgOpcode.ListContains: { IEnumerable source = (IEnumerable)arg0; object item = arg1; return source.Contains(item); } case BgOpcode.ListLazy: { int fragment = (int)arg0; return LazyEvaluateList(new Frame(frame), fragment); } case BgOpcode.ListOption: { BgObjectDef obj = (BgObjectDef)arg0; BgListOptionDef option = obj.Deserialize(); _optionDefs.Add(option); string? value = option.DefaultValue ?? String.Empty; if (_options.TryGetValue(option.Name, out string? result)) { value = result; } return value!.Split(new[] { '+', ';' }, StringSplitOptions.RemoveEmptyEntries).Select(x => (object)x).ToList(); } #endregion #region Object opcodes case BgOpcode.ObjEmpty: return new BgObjectDef(); case BgOpcode.ObjGet: { BgObjectDef obj = (BgObjectDef)arg0; string name = (string)arg1; object defaultValue = arg2; object? value; if (!obj.Properties.TryGetValue(name, out value)) { value = defaultValue; } return value!; } case BgOpcode.ObjSet: { BgObjectDef obj = (BgObjectDef)arg0; string name = (string)arg1; object value = arg2; return obj.Set(name, value); } #endregion #region Function opcodes case BgOpcode.Call: { int function = (int)arg0; object[] arguments = (object[])arg1; return Evaluate(new Frame(_fragments[function], arguments)); } case BgOpcode.Argument: { int index = (int)(ulong)arg0; return frame.Arguments[index]; } case BgOpcode.Jump: { int fragment = (int)arg0; return Jump(frame, fragment); } #endregion #region Generic opcodes case BgOpcode.Choose: { bool condition = (bool)arg0; int fragmentIfTrue = (int)arg1; int fragmentIfFalse = (int)arg2; return Jump(frame, condition ? fragmentIfTrue : fragmentIfFalse); } case BgOpcode.Throw: { string sourceFile = (string)arg0; int sourceLine = (int)(ulong)arg1; string message = (string)arg2; throw new BgBytecodeException(sourceFile, sourceLine, message); } case BgOpcode.Null: return null!; case BgOpcode.Thunk: { BgThunkDef method = (BgThunkDef)arg0; object[] arguments = (object[])arg1; object[] thunkArgs = new object[arguments.Length]; ParameterInfo[] parameters = method.Method.GetParameters(); for (int idx = 0; idx < arguments.Length; idx++) { object value = arguments[idx]; if (typeof(BgExpr).IsAssignableFrom(parameters[idx].ParameterType)) { thunkArgs[idx] = BgType.Constant(parameters[idx].ParameterType, value); } else { thunkArgs[idx] = method.Arguments[idx]!; } } return new BgThunkDef(method.Method, thunkArgs); } #endregion default: throw new InvalidDataException($"Invalid opcode: {opcode}"); } } static List GetListArg(object value) { return ((IEnumerable)value).Select(x => (T)x).ToList(); } IEnumerable LazyEvaluateItem(Frame frame, int fragment) { yield return Jump(frame, fragment); } IEnumerable LazyEvaluateList(Frame frame, int fragment) { IEnumerable result = (IEnumerable)Jump(frame, fragment); foreach (object item in result) { yield return item; } } object Jump(Frame frame, int fragment) { object? result; if (!frame.Objects.TryGetValue(fragment, out result)) { int prevOffset = frame.Offset; frame.Offset = _fragments[fragment]; result = Evaluate(frame); frame.Objects.Add(fragment, result); frame.Offset = prevOffset; } return result; } object Call(int fragment, object[] arguments) { int offset = _fragments[fragment]; return Evaluate(new Frame(offset, arguments)); } int ReadFragment(Frame frame) { return (int)ReadUnsignedInteger(frame); } /// /// Reads an opcode from the input stream /// /// The opcode that was read BgOpcode ReadOpcode(Frame frame) { BgOpcode opcode = (BgOpcode)_data[frame.Offset++]; return opcode; } /// /// Reads a name from the input stream /// string ReadName(Frame frame) { int index = (int)ReadUnsignedInteger(frame); return _names[index]; } /// /// Reads a string from the input stream /// string ReadString(Frame frame) { ReadOnlySpan buffer = _data.AsSpan(frame.Offset); int length = (int)VarInt.ReadUnsigned(buffer, out int bytesRead); frame.Offset += bytesRead; string text = Encoding.UTF8.GetString(buffer.Slice(bytesRead, length)); frame.Offset += length; return text; } /// /// Writes a signed integer value to the output /// /// the value that was read int ReadSignedInteger(Frame frame) { ulong encoded = ReadUnsignedInteger(frame); return DecodeSignedInteger(encoded); } /// /// Read an unsigned integer value from the input /// /// The value that was read ulong ReadUnsignedInteger(Frame frame) { ulong value = VarInt.ReadUnsigned(_data.AsSpan(frame.Offset), out int bytesRead); frame.Offset += bytesRead; return value; } /// /// Decode a signed integer using the lower bit for the sign flag, allowing us to encode it more efficiently as a /// /// Value to be decoded /// The decoded value static int DecodeSignedInteger(ulong value) { if ((value & 1) != 0) { return -(int)(value >> 1); } else { return (int)(value >> 1); } } /// /// Disassemble the current script to a logger /// /// public void Disassemble(ILogger logger) { logger.LogInformation("Version: {Version}", _version); for (int idx = 0; idx < _fragments.Length; idx++) { logger.LogInformation(""); logger.LogInformation("Fragment {Idx}:", idx); Disassemble(new Frame(_fragments[idx], Array.Empty()), logger); } } void Disassemble(Frame frame, ILogger logger) { BgOpcode opcode = Trace(frame, null, ReadOpcode, logger); BgOpcodeInfo opcodeInfo = s_opcodeTable[(int)opcode]; for (int idx = 0; idx < opcodeInfo.ArgCount; idx++) { BgArg arg = opcodeInfo.GetArg(idx); switch (arg) { case BgArg.Value: Disassemble(frame, logger); break; case BgArg.Name: Trace(frame, ReadName, x => $"name: {x}", logger); break; case BgArg.Fragment: TraceFragment(frame, logger); break; case BgArg.VarIntSigned: Trace(frame, "int:", ReadSignedInteger, logger); break; case BgArg.VarIntUnsigned: Trace(frame, "uint:", ReadUnsignedInteger, logger); break; case BgArg.StringLiteral: Trace(frame, ReadString, x => $"\"{x}\"", logger); break; case BgArg.ArgList: { int argCount = (int)Trace(frame, "count", ReadUnsignedInteger, logger); for (int argIdx = 0; argIdx < argCount; argIdx++) { Disassemble(frame, logger); } break; } case BgArg.Thunk: Trace(frame, "thunk", ReadUnsignedInteger, logger); break; default: throw new InvalidDataException(); } } } int TraceFragment(Frame frame, ILogger logger) { return Trace(frame, "-> Fragment", ReadFragment, logger); } T Trace(Frame frame, string? type, Func readValue, ILogger logger) { int offset = frame.Offset; T value = readValue(frame); int length = frame.Offset - offset; string valueAndType = (type == null) ? $"{value}" : $"{type} {value}"; Trace(offset, length, valueAndType, logger); return value; } T Trace(Frame frame, Func readValue, Func formatValue, ILogger logger) { int offset = frame.Offset; T value = readValue(frame); int length = frame.Offset - offset; Trace(offset, length, formatValue(value), logger); return value; } void Trace(int offset, int length, object value, ILogger logger) { string bytes = String.Join(" ", _data.AsSpan(offset, length).ToArray().Select(x => $"{x:x2}")); logger.LogInformation("{Offset,6}: {Value,-20} {Bytes}", offset, value, bytes); } } }