// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace UnrealBuildTool { namespace VerseVMBytecode { internal enum CppType { LabelOffset, } internal enum Role { Use, Immediate, // This means that the operand will be embedded in the bytecode itself. UnifyDef, ClobberDef, } internal enum Arity { Fixed, Variadic, } static class Extensions { public static string ToCpp(this Role TheRole) { switch (TheRole) { case Role.Use: return "EOperandRole::Use"; case Role.Immediate: return "EOperandRole::Immediate"; case Role.UnifyDef: return "EOperandRole::UnifyDef"; case Role.ClobberDef: return "EOperandRole::ClobberDef"; default: break; } return "#error \"Unknown role.\""; } public static string DefCppType(this Argument TheArg) { switch (TheArg.Role) { case Role.Use: return "FValueOperand"; case Role.Immediate: return $"{TheArg.CppTypeName}"; case Role.UnifyDef: // fallthrough case Role.ClobberDef: return "FRegisterIndex"; default: throw new ArgumentException("Unknown role."); } } public static string ToCpp(this CppType Type) { switch (Type) { case CppType.LabelOffset: return "FLabelOffset"; } return "#error"; } public static string ToCpp(this bool Bool) { return Bool ? "true" : "false"; } } internal class Argument { public string Name; /// /// If this is an immediate operand, the value will be embedded in the opcode itself. /// This should be set to the the string name of the underlying operand native type. /// public string CppTypeName; public Role Role; public Arity Arity; public Argument(in string InName, in Role InRole, in Arity InArity, in string InCppTypeName) { Name = InName; Role = InRole; Arity = InArity; CppTypeName = InCppTypeName; } public Argument(in string InName, in Role InRole, in Arity InArity) : this(InName, InRole, InArity, "") {} public Argument(in string InName, in Role InRole) : this(InName, InRole, Arity.Fixed) {} } internal class Constant { public string Name; public CppType Type; public Constant(string _Name, CppType _Type) { Name = _Name; Type = _Type; } public bool IsJump() { return Type == CppType.LabelOffset; } } internal class Instruction { public string Name; public List Args = new List(); public List Consts = new List(); public bool _CapturesEffectToken = false; public bool _CreatesNewReturnEffectToken = false; public bool _Suspends = false; public bool _Jumps = false; public Instruction(string _Name) { Name = _Name; } public string CppName => $"FOp{Name}"; public string CppCapturesName => $"F{Name}SuspensionCaptures"; public Instruction Arg(in string InName, in Role InRole, in Arity InArity, in string InCppTypeName) { Args.Add(new Argument(InName, InRole, InArity, InCppTypeName)); return this; } public Instruction Arg(in string InName, in Role InRole, in Arity InArity) { return Arg(InName, InRole, InArity, ""); } public Instruction Arg(in string InName, in Role InRole) { return Arg(InName, InRole, Arity.Fixed); } public Instruction Const(string Name, CppType Type) { Consts.Add(new Constant(Name, Type)); return this; } public Instruction Jump(string Name) { _Jumps = true; return Const(Name, CppType.LabelOffset); } public Instruction CapturesEffectToken() { _CapturesEffectToken = true; return this; } public Instruction CreatesNewReturnEffectToken() { _CreatesNewReturnEffectToken = true; return this; } public Instruction Suspends() { _Suspends = true; return this; } } } } namespace UnrealBuildTool { using VerseVMBytecode; /// /// Generates bytecode and bytecode helpers for the VerseVM. /// public class VerseVMBytecodeGenerator { readonly List Instructions = new List(); Instruction Inst(string Name) { Instruction I = new Instruction(Name); Instructions.Add(I); return I; } static string Preamble() { StringBuilder S = new StringBuilder(); S.Append("// Copyright Epic Games, Inc. All Rights Reserved.\n\n"); S.Append("// WARNING: This code is autogenerated by VerseVMBytecodeGenerator.cs. Do not edit directly\n\n"); S.Append("#pragma once\n\n"); return S.ToString(); } string EmitBytecodeMacroList() { StringBuilder S = new StringBuilder(); S.Append(Preamble()); S.Append("// IWYU pragma: private, include \"VVMBytecodeOps.h\"\n\n"); S.Append("#define VERSE_ENUM_OPS(v) \\\n"); foreach (Instruction Inst in Instructions) { S.Append($" v({Inst.Name}) \\\n"); } S.Append("\n"); return S.ToString(); } string EmitReflectionMethods(in Instruction Inst, in bool bIsSuspensionCapture) { StringBuilder S = new StringBuilder(); void EmitForEachOperand(in StringBuilder S, in Instruction Inst, in bool bIsSuspensionCapture, in bool bIsConst) { string ConstString = bIsConst ? " const" : ""; S.Append(" template \n"); S.Append($" void ForEachOperand(FunctionType Function){ConstString}\n"); S.Append(" {\n"); foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { string OperandString = bIsSuspensionCapture ? "Operand.Get()" : "Operand"; S.Append($" for (const auto& Operand : {Arg.Name})\n"); S.Append(" {\n"); S.Append($" Function({Arg.Role.ToCpp()}, {OperandString});\n"); S.Append(" }\n"); } else { // If the argument is immediate, `ForEachOperand`/`ForEachOperandWithName` will operate on // the `TWriteBarrier` itself instead of `T`. This is to allow for better control over marking the // values encapsulated within the write barriers. string ArgString = (bIsSuspensionCapture && Arg.Role != Role.Immediate) ? $"{Arg.Name}.Get()" : $"{Arg.Name}"; S.Append($" Function({Arg.Role.ToCpp()}, {ArgString});\n"); } } S.Append(" }\n\n"); S.Append(" template \n"); S.Append($" void ForEachOperandWithName(FunctionType Function){ConstString}\n"); S.Append(" {\n"); foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { string OperandString = bIsSuspensionCapture ? "Operand.Get()" : "Operand"; S.Append($" for (int32 Index = 0; Index < {Arg.Name}.Num(); ++Index)\n"); S.Append(" {\n"); S.Append($" auto& Operand = {Arg.Name}[Index];\n"); S.Append($" const FString ArgName = FString::Format(TEXT(\"{{0}}{{1}}\"), {{\"{Arg.Name}\", Index}});\n"); S.Append($" Function({Arg.Role.ToCpp()}, {OperandString}, TCHAR_TO_ANSI(*ArgName));\n"); S.Append(" }\n"); } else { string ArgString = (bIsSuspensionCapture && Arg.Role != Role.Immediate) ? $"{Arg.Name}.Get()" : $"{Arg.Name}"; S.Append($" Function({Arg.Role.ToCpp()}, {ArgString}, \"{Arg.Name}\");\n"); } } S.Append(" }\n\n"); } // Generate const and non-const versions since it is useful for being able to mutate operands (i.e. marking). EmitForEachOperand(S, Inst, bIsSuspensionCapture, true); EmitForEachOperand(S, Inst, bIsSuspensionCapture, false); return S.ToString(); } String EmitBytecodeAndCaptureDefs() { StringBuilder S = new StringBuilder(); S.Append(Preamble()); S.Append("// IWYU pragma: private, include \"VVMBytecodesAndCaptures.h\"\n\n"); S.Append("namespace Verse {\n"); // Emit bytecode structs. foreach (Instruction Inst in Instructions) { S.Append($"struct {Inst.CppName} : public FOp"); S.Append("\n{\n"); int[] ImmediateArgsIndices = new int[Inst.Args.Count]; int[] VariadicArgsIndices = new int[Inst.Args.Count]; int NumImmediateArgs = 0; int NumVariadicArgs = 0; int Index = 0; // Define fields foreach (Argument Arg in Inst.Args) { if (Arg.Role == Role.Immediate) { ImmediateArgsIndices[NumImmediateArgs++] = Index; } if (Arg.Arity == Arity.Variadic) { VariadicArgsIndices[NumVariadicArgs++] = Index; } else if (Arg.Arity == Arity.Fixed && Arg.Role != Role.Immediate) { S.Append($" {Arg.DefCppType()} {Arg.Name};\n"); } ++Index; } foreach (Constant Const in Inst.Consts) { S.Append($" {Const.Type.ToCpp()} {Const.Name};\n"); } if (NumVariadicArgs > 0) { S.Append(" // Variadic arguments.\n"); } for (int CurrentIndex = 0; CurrentIndex < NumVariadicArgs; ++CurrentIndex) { Argument Arg = Inst.Args[VariadicArgsIndices[CurrentIndex]]; // NOTE: (yiliang.siew) This could be raised to a location such as in `VProgram` when that // exists and each opcode store only the index + size to index into that array, so that we don't // need to store a separate `TArray` of operand values per-opcode struct. S.Append($" TArray<{Arg.DefCppType()}> {Arg.Name};\n"); } // We wrap these in a `TWriteBarrier` so that we can have an easy way to mark these values for GC purposes. if (NumImmediateArgs > 0) { S.Append(" // Immediate arguments.\n"); } for (int CurrentIndex = 0; CurrentIndex < NumImmediateArgs; ++CurrentIndex) { Argument Arg = Inst.Args[ImmediateArgsIndices[CurrentIndex]]; if (Arg.Arity == Arity.Variadic) { throw new ArgumentException("Variadic immediate arguments are not currently supported!"); } S.Append($" TWriteBarrier<{Arg.DefCppType()}> {Arg.Name};\n"); } S.Append("\n"); // For readability. S.Append($" static constexpr EOpcode StaticOpcode = EOpcode::{Inst.Name};\n"); S.Append($" static constexpr bool bHasJumps = {Inst._Jumps.ToCpp()};\n\n"); // Constructor S.Append($" {Inst.CppName}("); string ArgumentsList = string.Join(", ", Inst.Args.Select(Arg => { // Prefix with `In` to avoid any shadowing issues. if (Arg.Role == Role.Immediate) { // TODO: Support variadic immediate arguments. return $"TWriteBarrier<{Arg.DefCppType()}>&& In{Arg.Name}"; } else if (Arg.Arity == Arity.Variadic) { return $"TArray<{Arg.DefCppType()}>&& In{Arg.Name}"; } else { return $"const {Arg.DefCppType()} In{Arg.Name}"; } })); S.Append(ArgumentsList); string ConstantsList = string.Join(", ", Inst.Consts.Select(Const => $"{Const.Type.ToCpp()} {Const.Name}")); S.Append(ConstantsList); S.Append(")\n"); S.Append(" : FOp(StaticOpcode)\n"); foreach (Argument Arg in Inst.Args) { // The order here has to match because C++ initializer order must match the order of the fields. if (Arg.Arity == Arity.Variadic || Arg.Role == Role.Immediate) { continue; } S.Append($" , {Arg.Name}(In{Arg.Name})\n"); } foreach (Constant Const in Inst.Consts) { S.Append($" , {Const.Name}({Const.Name})\n"); } for (int CurrentIndex = 0; CurrentIndex < NumVariadicArgs; ++CurrentIndex) { Argument Arg = Inst.Args[VariadicArgsIndices[CurrentIndex]]; S.Append($" , {Arg.Name}(In{Arg.Name})\n"); } for (int CurrentIndex = 0; CurrentIndex < NumImmediateArgs; ++CurrentIndex) { Argument Arg = Inst.Args[ImmediateArgsIndices[CurrentIndex]]; S.Append($" , {Arg.Name}(In{Arg.Name})\n"); } S.Append(" {}\n\n"); // Reflection methods S.Append(EmitReflectionMethods(Inst, false)); S.Append(" template \n"); S.Append(" FORCEINLINE void ForEachJump(FunctionType Function) const\n"); S.Append(" {\n"); foreach (Constant Const in Inst.Consts.Where(C => C.IsJump())) { S.Append($" Function({Const.Name});\n"); } S.Append(" }\n"); S.Append(" template \n"); S.Append(" FORCEINLINE void ForEachJumpWithName(FunctionType Function) const\n"); S.Append(" {\n"); foreach (Constant Const in Inst.Consts.Where(C => C.IsJump())) { S.Append($" Function({Const.Name}, \"{Const.Name}\");\n"); } S.Append(" }\n"); S.Append("\n};\n"); S.Append($"static_assert(alignof({Inst.CppName}) >= 8);\n\n"); } // Emit captures structs. foreach (Instruction Inst in Instructions.Where(I => I._Suspends)) { string Name = Inst.CppCapturesName; S.Append($"struct {Name}"); S.Append("\n{\n"); // Generate the fields. foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { S.Append($" TArray> {Arg.Name}; // Captured variadic arguments.\n"); } else if (Arg.Role == Role.Immediate) { S.Append($" TWriteBarrier<{Arg.DefCppType()}> {Arg.Name}; \n"); } else { S.Append($" TWriteBarrier {Arg.Name}; \n"); } } if (Inst._CapturesEffectToken) { S.Append($" TWriteBarrier EffectToken;\n"); } if (Inst._CreatesNewReturnEffectToken) { S.Append($" TWriteBarrier ReturnEffectToken;\n"); } S.Append("\n"); // Generate the constructor. { S.Append($" {Name}(FAccessContext Context"); foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { S.Append($", TArray>&& In{Arg.Name}"); } else if (Arg.Role == Role.Immediate) { S.Append($", const TWriteBarrier<{Arg.DefCppType()}>& In{Arg.Name}"); } else { S.Append($", VValue In{Arg.Name}"); } } if (Inst._CapturesEffectToken) { S.Append(", VValue EffectToken"); } if (Inst._CreatesNewReturnEffectToken) { S.Append(", VValue ReturnEffectToken"); } S.Append(")\n"); string Prefix = ":"; foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { S.Append($" {Prefix} {Arg.Name}(MoveTemp(In{Arg.Name}))\n"); } else { if (Arg.Role == Role.Immediate) { S.Append($" {Prefix} {Arg.Name}(In{Arg.Name})\n"); } else { S.Append($" {Prefix} {Arg.Name}(Context, In{Arg.Name})\n"); } } Prefix = ","; } if (Inst._CapturesEffectToken) { S.Append($" {Prefix} EffectToken(Context, EffectToken)\n"); Prefix = ","; } if (Inst._CreatesNewReturnEffectToken) { S.Append($" {Prefix} ReturnEffectToken(Context, ReturnEffectToken)\n"); Prefix = ","; } S.Append(" {}\n"); } S.Append("\n"); // Generate the copy constructor. { S.Append($" {Name}(FAccessContext Context, const {Name}& Other)\n"); string Prefix = ":"; foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { S.Append($" {Prefix} {Arg.Name}(Other.{Arg.Name})\n"); } else { if (Arg.Role == Role.Immediate) { S.Append($" {Prefix} {Arg.Name}(Other.{Arg.Name})\n"); } else { S.Append($" {Prefix} {Arg.Name}(Context, Other.{Arg.Name}.Get())\n"); } } Prefix = ","; } if (Inst._CapturesEffectToken) { S.Append($" {Prefix} EffectToken(Context, Other.EffectToken.Get())\n"); Prefix = ", "; } if (Inst._CreatesNewReturnEffectToken) { S.Append($" {Prefix} ReturnEffectToken(Context, Other.ReturnEffectToken.Get())\n"); Prefix = ", "; } S.Append(" {\n }\n"); } S.Append("\n"); S.Append(EmitReflectionMethods(Inst, true)); S.Append("};\n\n"); } S.Append("} // namespace Verse\n"); return S.ToString(); } string EmitMakeCapturesFunctions() { StringBuilder S = new StringBuilder(); S.Append(Preamble()); foreach (Instruction Inst in Instructions.Where(I => I._Suspends)) { S.Append($"FORCEINLINE {Inst.CppCapturesName} MakeCaptures(const {Inst.CppName}& Op)\n{{\n"); if (Inst._CapturesEffectToken) { S.Append(" const VValue IncomingEffectToken = EffectToken.Get(Context);\n"); } if (Inst._CreatesNewReturnEffectToken) { S.Append(" const VValue ReturnEffectToken = VValue::Placeholder(VPlaceholder::New(Context, 0));\n"); S.Append(" EffectToken.Set(Context, ReturnEffectToken);\n"); } foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { S.Append($" TArray> Array{Arg.Name};\n"); S.Append($" for (auto& CurrentValue : Op.{Arg.Name})\n"); S.Append(" {\n"); S.Append($" Array{Arg.Name}.Add({{Context, GetOperand(CurrentValue)}});\n"); S.Append(" }\n"); } } S.Append($" return {Inst.CppCapturesName}(Context"); foreach (Argument Arg in Inst.Args) { if (Arg.Arity == Arity.Variadic) { S.Append($", MoveTemp(Array{Arg.Name})"); } else if (Arg.Role == Role.Immediate) { S.Append($", Op.{Arg.Name}"); } else { S.Append($", GetOperand(Op.{Arg.Name})"); } } if (Inst._CapturesEffectToken) { S.Append(", IncomingEffectToken"); } if (Inst._CreatesNewReturnEffectToken) { S.Append(", ReturnEffectToken"); } S.Append(");\n"); S.Append("}\n\n"); } return S.ToString(); } string EmitCaptureSwitch() { StringBuilder S = new StringBuilder(); S.Append(Preamble()); S.Append("// IWYU pragma: private, include \"VVMCaptureSwitch.h\"\n\n"); S.Append("namespace Verse {\n"); S.Append("template \n"); S.Append("void VBytecodeSuspension::CaptureSwitch(const TFunc& Func)\n"); S.Append("{\n"); S.Append(" switch (PC->Opcode)\n"); S.Append(" {\n"); foreach (Instruction Inst in Instructions.Where(I => I._Suspends)) { S.Append($" case EOpcode::{Inst.Name}:\n"); S.Append(" {\n"); S.Append($" Func(GetCaptures<{Inst.CppCapturesName}>());\n"); S.Append(" break;\n"); S.Append(" }\n"); } S.Append(" default:\n"); S.Append(" {\n"); S.Append(" V_DIE(\"Opcode doesn't have a captures\");\n"); S.Append(" break;\n"); S.Append(" }\n"); S.Append(" }\n"); S.Append("}\n"); S.Append("} // namespace Verse\n"); return S.ToString(); } VerseVMBytecodeGenerator(ILogger Logger, DirectoryReference GenDirectory) { DefineOps(); Action> GenFile = (FileName, Method) => { FileReference File = FileReference.Combine(GenDirectory, FileName); bool bWritten = FileReference.WriteAllTextIfDifferent(File, Method()); Logger.LogDebug($"\tWriting out generated header file. Changed:{bWritten} Path:'{File}'"); }; GenFile("VVMBytecodeOps.gen.h", EmitBytecodeMacroList); GenFile("VVMBytecodesAndCaptures.gen.h", EmitBytecodeAndCaptureDefs); GenFile("VVMMakeCapturesFuncs.gen.h", EmitMakeCapturesFunctions); GenFile("VVMCaptureSwitch.gen.h", EmitCaptureSwitch); } /// /// Entrypoint to generate the bytecode. Generated code will go in Directory. /// public static void Generate(ILogger Logger, DirectoryReference Directory) { Logger.LogDebug($"VerseVMBytecodeGenerator.Generate, generating CPP headers in: '{Directory}'"); new VerseVMBytecodeGenerator(Logger, Directory); } void DefineOps() { string[] BinOps = { "Add", "Sub", "Mul", "Div", "Mod" }; foreach (string Op in BinOps) { Inst(Op) .Arg("Dest", Role.UnifyDef) .Arg("LeftSource", Role.Use) .Arg("RightSource", Role.Use) .Suspends(); } Inst("MutableAdd") .Arg("Dest", Role.UnifyDef) .Arg("LeftSource", Role.Use) .Arg("RightSource", Role.Use) .Suspends(); string[] UnaryOps = { "Neg", "Query" }; foreach (string Op in UnaryOps) { Inst(Op) .Arg("Dest", Role.UnifyDef) .Arg("Source", Role.Use) .Suspends(); } Inst("Err"); Inst("Move") .Arg("Dest", Role.UnifyDef) .Arg("Source", Role.Use); Inst("Reset") .Arg("Dest", Role.ClobberDef); Inst("Jump") .Jump("JumpOffset"); // These labels are needed for lenient execution. // On success, EndFailureContext falls through. // On failure, it jumps to OnFailure. // When there are still unresolved suspensions in this failure context, // we jump to "Done" to continue lenient execution. Inst("BeginFailureContext") .Jump("OnFailure"); Inst("EndFailureContext") .Jump("Done"); Inst("Call") .Arg("Dest", Role.UnifyDef) .Arg("Callee", Role.Use) .Arg("Arguments", Role.Use, Arity.Variadic) .CapturesEffectToken() .CreatesNewReturnEffectToken() .Suspends(); Inst("Return") .Arg("Value", Role.Use); Inst("NewVar") .Arg("Dest", Role.UnifyDef); Inst("VarGet") .Arg("Dest", Role.UnifyDef) .Arg("Var", Role.Use) .CapturesEffectToken() .Suspends(); Inst("VarSet") .Arg("Var", Role.Use) .Arg("Value", Role.Use) .CapturesEffectToken() .Suspends(); Inst("Length") .Arg("Dest", Role.UnifyDef) .Arg("Container", Role.Use) .Suspends(); Inst("IndexSet") .Arg("Container", Role.Use) .Arg("Index", Role.Use) .Arg("ValueToSet", Role.Use) .CapturesEffectToken() .Suspends(); Inst("NewArray") .Arg("Dest", Role.UnifyDef) .Arg("Values", Role.Use, Arity.Variadic) .Suspends(); Inst("NewMutableArray") .Arg("Dest", Role.UnifyDef) .Arg("Values", Role.Use, Arity.Variadic) .Suspends(); Inst("NewMutableArrayWithCapacity") .Arg("Dest", Role.UnifyDef) .Arg("Size", Role.Use) .Suspends(); Inst("ArrayAdd") .Arg("Container", Role.Use) .Arg("ValueToAdd", Role.Use) .Suspends(); // This in place converts a VMutableArray into a VArray. // This can get away with being non-transactional because we // call it on data structures before they become observable // to user code. Inst("InPlaceMakeImmutable") .Arg("Container", Role.Use) .Suspends(); Inst("NewOption") .Arg("Dest", Role.UnifyDef) .Arg("Value", Role.Use) .Suspends(); Inst("NewMap") .Arg("Dest", Role.UnifyDef) .Arg("Keys", Role.Use, Arity.Variadic) .Arg("Values", Role.Use, Arity.Variadic) .Suspends(); Inst("NewMutableMap") .Arg("Dest", Role.UnifyDef) .Arg("Keys", Role.Use, Arity.Variadic) .Arg("Values", Role.Use, Arity.Variadic) .Suspends(); Inst("NewMutableMapWithCapacity") .Arg("Dest", Role.UnifyDef) .Arg("Size", Role.Use) .Suspends(); Inst("MapKey") .Arg("Dest", Role.UnifyDef) .Arg("Map", Role.Use) .Arg("Index", Role.Use) .Suspends(); Inst("MapValue") .Arg("Dest", Role.UnifyDef) .Arg("Map", Role.Use) .Arg("Index", Role.Use) .Suspends(); Inst("NewClass") .Arg("Dest", Role.UnifyDef) .Arg("Constructor", Role.Immediate, Arity.Fixed, "VConstructor") .Arg("Inherited", Role.Use, Arity.Variadic) .Suspends(); Inst("NewObject") .Arg("Dest", Role.UnifyDef) .Arg("Class", Role.Use) .Arg("Fields", Role.Immediate, Arity.Fixed, "VUniqueStringSet") .Arg("Values", Role.Use, Arity.Variadic) .Suspends(); Inst("LoadField") .Arg("Dest", Role.UnifyDef) .Arg("Object", Role.Use) .Arg("Name", Role.Immediate, Arity.Fixed, "VUniqueString") .Suspends(); Inst("UnifyField") .Arg("Object", Role.Use) .Arg("Name", Role.Immediate, Arity.Fixed, "VUniqueString") .Arg("Value", Role.Use) .Suspends(); string[] ComparisonOps = { "Neq", "Lt", "Lte", "Gt", "Gte" }; foreach (string Op in ComparisonOps) { Inst(Op) .Arg("Dest", Role.UnifyDef) .Arg("LeftSource", Role.Use) .Arg("RightSource", Role.Use) .Suspends(); } } } }