Last active
September 15, 2022 20:14
-
-
Save dekrain/c82c537ab486e36215b7c1f1fd0d204f to your computer and use it in GitHub Desktop.
CLI Custom Attribute Virtual Machine: The best worst idea I had in 3 months
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Released under GNU GPLv3 license | |
// https://www.gnu.org/licenses/gpl-3.0.en.html | |
// (©) Copyright DeKrain 2022 | |
// The example program is located near the end of this file | |
AttrVM.Executor.CompileAndRunMainProgram(@"MainProgram.Program"); | |
#region VM Implementation | |
namespace AttrVM { | |
using System; | |
using System.Linq; | |
using System.Collections.Generic; | |
using System.Reflection; | |
using System.Reflection.Emit; | |
using static Constants; | |
using static OperandStackValueTypeHelpers; | |
public class BadProgramException : ApplicationException { | |
public BadProgramException(string Message) | |
: base(Message) {} | |
} | |
#region Base Attributes | |
static class Constants { | |
public const AttributeTargets ActionTargets = AttributeTargets.Property | AttributeTargets.Class; | |
} | |
[AttributeUsage(AttributeTargets.Class)] | |
public abstract class ExecutionPointAttribute : Attribute {} | |
[AttributeUsage(ActionTargets)] | |
public abstract class SeqPointAttribute : Attribute {} | |
[AttributeUsage(ActionTargets)] | |
public abstract class ActionAttribute : Attribute {} | |
public abstract class ComputationAttribute : ActionAttribute {} | |
#endregion | |
#region Execution Point Attributes | |
public class EntryPointAttribute : ExecutionPointAttribute { | |
public string Symbol { get; set; } | |
public EntryPointAttribute(string Symbol) { | |
this.Symbol = Symbol; | |
} | |
} | |
[AttributeUsage(AttributeTargets.Class)] | |
public class SubroutineArgumentsAttribute : Attribute { | |
public string[] Args { get; set; } | |
public SubroutineArgumentsAttribute(string[] Args) { | |
this.Args = Args; | |
} | |
} | |
#endregion | |
#region Sequence Points | |
public class SeqFlowNextAttribute : SeqPointAttribute { | |
public string Symbol { get; set; } | |
public SeqFlowNextAttribute(string Symbol) { | |
this.Symbol = Symbol; | |
} | |
} | |
public class SeqFlowBranchAttribute : SeqPointAttribute { | |
public string OnTrue { get; set; } | |
public string OnFalse { get; set; } | |
} | |
public class SeqFlowHaltAttribute : SeqPointAttribute { | |
public int Status { get; set; } = 0; | |
} | |
public class SubroutineResultAttribute : SeqPointAttribute { | |
public string Variable { get; set; } | |
public SubroutineResultAttribute(string Variable = null) { | |
this.Variable = Variable; | |
} | |
} | |
#endregion | |
#region Regular Actions | |
public class OutputStringAttribute : ActionAttribute { | |
public string String { get; set; } | |
public OutputStringAttribute(string String) { | |
this.String = String; | |
} | |
} | |
public class OutputVariableAttribute : ActionAttribute { | |
public string Variable { get; set; } | |
public bool Newline = false; | |
public OutputVariableAttribute(string Variable) { | |
this.Variable = Variable; | |
} | |
} | |
public class InputValueAttribute : ActionAttribute { | |
public string Variable { get; set; } | |
public string Prompt { get; set; } | |
public InputValueAttribute(string Variable) { | |
this.Variable = Variable; | |
} | |
} | |
public class InvokeSubroutineAttribute : ActionAttribute { | |
public string TargetRoutine { get; set; } | |
public string[] Args { get; set; } | |
public string Result { get; set; } | |
public InvokeSubroutineAttribute(string TargetRoutine) { | |
this.TargetRoutine = TargetRoutine; | |
} | |
} | |
public class AssignVariable : ActionAttribute { | |
public string Variable { get; set; } | |
public object Value { get; set; } | |
public AssignVariable(string Variable, object Value) { | |
this.Variable = Variable; | |
this.Value = Value; | |
} | |
} | |
#endregion | |
#region Computations | |
[AttributeUsage(ActionTargets)] | |
public class ComputeFlowAttribute : Attribute {} | |
public class ComputeNextAttribute : ComputeFlowAttribute { | |
public string Symbol { get; set; } | |
public ComputeNextAttribute(string Symbol) { | |
this.Symbol = Symbol; | |
} | |
} | |
/// <summary>Signifies end of resultless computation</summary> | |
[AttributeUsage(AttributeTargets.Property)] | |
public class ComputeDoneAttribute : ComputeFlowAttribute {} | |
/// <summary>Signifies end of a single-result computation</summary> | |
[AttributeUsage(AttributeTargets.Property)] | |
public class ComputeResultAttribute : ComputeFlowAttribute {} | |
[Flags] | |
public enum StackBehaviorOptions { | |
/// <summary>Don't pop top value(s) from the stack</summary> | |
NoPop = 1, | |
} | |
[AttributeUsage(ActionTargets)] | |
public class StackBehaviorAttribute : Attribute { | |
public StackBehaviorOptions Options { get; set; } | |
public StackBehaviorAttribute(StackBehaviorOptions Options) { | |
this.Options = Options; | |
} | |
} | |
// ================================ | |
/// <summary>Push variable value on the stack</summary> | |
public class LoadVariableAttribute : ComputationAttribute { | |
public string Variable { get; set; } | |
public LoadVariableAttribute(string Variable) { | |
this.Variable = Variable; | |
} | |
} | |
/// <summary>Like <see cref="LoadVariableAttribute"/>, but push the address instead</summary> | |
public class LoadVariableAddressAttribute : ComputationAttribute { | |
public string Variable { get; set; } | |
public LoadVariableAddressAttribute(string Variable) { | |
this.Variable = Variable; | |
} | |
} | |
/// <summary>Push constant integer on the stack</summary> | |
public class LoadConstantAttribute : ComputationAttribute { | |
public ulong Value { get; set; } | |
private Type type_; | |
public Type Type { get => type_; set { | |
if (!value.IsPrimitive) | |
throw new InvalidCastException("A contant must have an integer type"); | |
type_ = value; | |
} } | |
public LoadConstantAttribute(ulong Value, Type Type) { | |
this.Value = Value; | |
this.Type = Type; | |
} | |
public LoadConstantAttribute(object value) { | |
this.Value = Convert.ToUInt64(value); | |
this.Type = value.GetType(); | |
} | |
} | |
/// <summary>Dereference a value from address on top of the stack</summary> | |
/// <remarks>Pops the address from the stack unless <see cref="StackBehaviorAttributes.NoPop"/> is specified.</remarks> | |
public class DereferenceValueAttribute : ComputationAttribute {} | |
/// <summary>Pop a value from the stack and store it in address on top of the stack</summary> | |
/// <remarks>Pops the value and address from the stack unless <see cref="StackBehaviorAttributes.NoPop"/> is specified.</remarks> | |
public class StoreDerefValueAttribute : ComputationAttribute {} | |
public enum Comparison { | |
Equals, | |
NotEquals, | |
LessThan, | |
GreaterThan, | |
LessOrEqualTo, | |
GreaterOrEqualTo, | |
} | |
/// <summary>Pop two values from the stack and compare then according to specified <see cref="Comparison"/>, then push the result on the stack</summary> | |
public class CompareValuesAttribute : ComputationAttribute { | |
public Comparison Comparison { get; set; } | |
public CompareValuesAttribute(Comparison Comparison) { | |
this.Comparison = Comparison; | |
} | |
} | |
/// <summary>Convert the integer on top of the stack to a boolean value</summary> | |
public class ConvertToBooleanAttribute : ComputationAttribute {} | |
/// <summary>Pop two values from the stack and add them, push result on the stack</summary> | |
/// <remarks>All values are interpreted left-to-right in order of pushing.</remarks> | |
public class AddValuesAttribute : ComputationAttribute {} | |
/// <summary>Pop two values from the stack and subtract them, push result on the stack</summary> | |
/// <remarks>All values are interpreted left-to-right in order of pushing.</remarks> | |
public class SubtractValuesAttribute : ComputationAttribute {} | |
/// <summary>Pop two values from the stack and multiply them, push result on the stack</summary> | |
/// <remarks>All values are interpreted left-to-right in order of pushing.</remarks> | |
public class MultiplyValuesAttribute : ComputationAttribute {} | |
/// <summary>Pop two values from the stack and divide them, push result on the stack</summary> | |
/// <remarks>All values are interpreted left-to-right in order of pushing.</remarks> | |
public class DivideValuesAttribute : ComputationAttribute {} | |
/// <summary>Pop two values from the stack and divide them and take the remainder, push result on the stack</summary> | |
/// <remarks>All values are interpreted left-to-right in order of pushing.</remarks> | |
public class DivideModuleValuesAttribute : ComputationAttribute {} | |
#endregion | |
//// Now for the actual fun stuff... | |
public static class Executor { | |
public static void CompileAndRunMainProgram(string TypeName) { | |
var resolver = new StaticResolver(); | |
var sub = resolver.ResolveName(TypeName); | |
if (sub.HasParameters) | |
throw new BadProgramException("Entry point must be a subroutine with no parameters"); | |
resolver.CompileProgram(); | |
sub.Run(); | |
} | |
} | |
/// <summary>Keeps track of global symbols like types (unimpl.) and subroutines</summary> | |
public class StaticResolver { | |
private Dictionary<string, Compiler> ResolvedNames = new(); | |
public Compiler ResolveName(string TypeName) { | |
var type = Type.GetType(TypeName); | |
if (type == null) | |
throw new TypeLoadException("Type not found"); | |
var entry = (ExecutionPointAttribute)Attribute.GetCustomAttribute(type, typeof(ExecutionPointAttribute)); | |
if (entry == null) | |
throw new BadProgramException("Type is not an execution unit"); | |
if (entry is EntryPointAttribute ep) { | |
var compiler = new Compiler(this, type); | |
ResolvedNames.Add(TypeName, compiler); | |
compiler.CompileEntryPoint(ep.Symbol); | |
return compiler; | |
} else { | |
throw new NotSupportedException("Execution type not supported"); | |
} | |
} | |
public void CompileProgram() { | |
foreach (var method in ResolvedNames.Values) { | |
method.GenerateBytecode(); | |
} | |
} | |
} | |
/// <summary>Handles compilation of one program unit (subroutine)</summary> | |
public class Compiler { | |
public readonly Type Target; | |
public readonly StaticResolver Root; | |
public Type[] ParameterSignature { get; } | |
public bool HasParameters => ParameterSignature.Length != 0; | |
public Type ResultType { get; private set; } | |
private FlowAction EntryPoint; | |
private HashSet<string> ResolvedNames = new(); | |
private Dictionary<string, Variable> ResolvedVariables = new(); | |
private Dictionary<string, FlowAction> ResolvedActions = new(); | |
public Compiler(StaticResolver root, Type type) { | |
Root = root; | |
Target = type; | |
var paramSig = (SubroutineArgumentsAttribute)Attribute.GetCustomAttribute(type, typeof(SubroutineArgumentsAttribute)); | |
if (paramSig?.Args != null) { | |
ParameterSignature = new Type[paramSig.Args.Length]; | |
int idx = 0; | |
foreach (string Name in paramSig.Args) { | |
var Var = ResolveVariable(Name); | |
ParameterSignature[idx] = Var.Type; | |
++idx; | |
Var.ParamIndex = idx; | |
} | |
} else { | |
ParameterSignature = Array.Empty<Type>(); | |
} | |
} | |
public void CompileEntryPoint(string Symbol) { | |
if (EntryPoint != null) | |
throw new InvalidOperationException("Entry point already declared"); | |
EntryPoint = ResolveAction(Symbol); | |
ResultType ??= typeof(void); | |
} | |
private MemberInfo ResolveMember(string Name) { | |
if (ResolvedNames.Contains(Name)) | |
throw new BadProgramException($"Name {Name} declared as a different entity"); | |
var memb = Target.GetMember(Name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); | |
if (memb.Length != 1) | |
throw new BadProgramException("Expected one member of declared name"); | |
if (!(MemberTypes.Property | MemberTypes.NestedType).HasFlag(memb[0].MemberType)) | |
throw new BadProgramException("Program entity must be a property or a class"); | |
ResolvedNames.Add(Name); | |
return memb[0]; | |
} | |
private Variable ResolveVariable(string Name) { | |
if (ResolvedVariables.TryGetValue(Name, out var Var)) | |
return Var; | |
var memb = ResolveMember(Name); | |
if (memb.MemberType != MemberTypes.Property) | |
throw new BadProgramException("A data member must be a property"); | |
if (Attribute.IsDefined(memb, typeof(SeqPointAttribute)) || Attribute.IsDefined(memb, typeof(ActionAttribute))) | |
throw new BadProgramException("A data member cannot have execution attributes"); | |
var prop = (PropertyInfo)memb; | |
Var = new Variable(Name, prop.PropertyType, prop, (prop.CanRead ? VariableFlags.Readable : 0) | (prop.CanWrite ? VariableFlags.Writable : 0)); | |
ResolvedVariables.Add(Name, Var); | |
return Var; | |
} | |
private FlowAction ResolveAction(string Name) { | |
if (ResolvedActions.TryGetValue(Name, out var Act)) | |
return Act; | |
var memb = ResolveMember(Name); | |
var seq = (SeqPointAttribute)Attribute.GetCustomAttribute(memb, typeof(SeqPointAttribute)); | |
if (seq == null) | |
throw new BadProgramException("An instruction must have a seq flow attribute"); | |
var action = (ActionAttribute)Attribute.GetCustomAttribute(memb, typeof(ActionAttribute)); | |
Act = new FlowAction(memb, seq, action); | |
ResolvedActions.Add(Name, Act); | |
switch (seq) { | |
case SeqFlowNextAttribute next: | |
Act.Next = ResolveAction(next.Symbol); | |
break; | |
case SeqFlowBranchAttribute branch: | |
Act.BranchTrue = ResolveAction(branch.OnTrue); | |
Act.BranchFalse = ResolveAction(branch.OnFalse); | |
break; | |
case SeqFlowHaltAttribute halt: break; | |
case SubroutineResultAttribute result: { | |
Act.ResultVar = result.Variable == null ? null : ResolveVariable(result.Variable); | |
var type = Act.ResultVar?.Type ?? typeof(void); | |
if (this.ResultType == null) | |
this.ResultType = type; | |
else if (!this.ResultType.Equals(type)) | |
throw new InvalidCastException("Subroutine result type mismatch"); | |
break; | |
} | |
default: | |
throw new NotSupportedException("Flow kind not supported"); | |
} | |
if (action is ComputationAttribute comp) { | |
if (memb.MemberType != MemberTypes.NestedType) | |
throw new BadProgramException("Computation node must be a class"); | |
Act.Computation = ResolveExpression(comp, (Type)memb); | |
} else if (action != null) { | |
Act.Action = ResolveStatement(action); | |
} | |
if (seq is SeqFlowBranchAttribute) { | |
if (Act.Computation?.ResultType == null || !Act.Computation.ResultType.Equals(typeof(bool))) | |
throw new BadProgramException("Branch node must have a computation of boolean type"); | |
} | |
return Act; | |
} | |
private StatementAction ResolveStatement(ActionAttribute action) { | |
var stmt = new StatementAction(action); | |
switch (action) { | |
case OutputStringAttribute outs: break; | |
case OutputVariableAttribute outv: | |
stmt.Variable = ResolveVariable(outv.Variable); | |
CheckCanRead(stmt.Variable); | |
break; | |
case InputValueAttribute inv: | |
stmt.Variable = ResolveVariable(inv.Variable); | |
CheckCanWrite(stmt.Variable); | |
break; | |
case InvokeSubroutineAttribute invoke: | |
stmt.Subroutine = Root.ResolveName(invoke.TargetRoutine); | |
if (invoke.Args == null || invoke.Args.Length == 0) { | |
stmt.Arguments = Array.Empty<Variable>(); | |
} else { | |
stmt.Arguments = new Variable[invoke.Args.Length]; | |
int idx = 0; | |
foreach (var arg in invoke.Args) { | |
CheckCanRead(stmt.Arguments[idx] = ResolveVariable(arg)); | |
++idx; | |
} | |
} | |
if (invoke.Result != null) | |
CheckCanWrite(stmt.ResultVariable = ResolveVariable(invoke.Result)); | |
// Signature match check | |
if (stmt.Subroutine.ParameterSignature.Length != stmt.Arguments.Length) | |
throw new BadProgramException("Subroutine call argument count mismatch"); | |
for (int idx = 0; idx != stmt.Arguments.Length; ++idx) { | |
if (!stmt.Arguments[idx].Type.IsAssignableTo(stmt.Subroutine.ParameterSignature[idx])) | |
throw new InvalidCastException("Cannot convert subroutine call argument to target type"); | |
} | |
// TODO: Result type check (first find it out) | |
// Right now, it will be handled in codegen | |
break; | |
case AssignVariable assign: | |
stmt.Variable = ResolveVariable(assign.Variable); | |
CheckCanWrite(stmt.Variable); | |
if (assign.Value == null) | |
if (!stmt.Variable.Type.IsClass) | |
throw new InvalidCastException("Cannot assign a null value to a value type"); | |
else if (!stmt.Variable.Type.Equals(assign.Value.GetType())) | |
throw new InvalidCastException($"Cannot assign a {assign.Value.GetType().FullName} value to a variable of type {stmt.Variable.GetType().FullName}"); | |
break; | |
default: | |
throw new NotSupportedException("Action is not supported"); | |
} | |
return stmt; | |
} | |
private ComputationScope ResolveExpression(ComputationAttribute comp, Type scope) { | |
var flow = (ComputeFlowAttribute)Attribute.GetCustomAttribute(scope, typeof(ComputeFlowAttribute)); | |
if (!(flow is ComputeNextAttribute)) | |
throw new BadProgramException("Computation must begin with a next label"); | |
var expr = new ComputationScope(scope); | |
expr.Entry = ResolveComputation(expr, comp, scope); | |
return expr; | |
} | |
private ComputeAction ResolveComputation(ComputationScope scope, ComputationAttribute comp, MemberInfo member) { | |
var expr = new ComputeAction(comp, member); | |
StackBehaviorOptions stackBeh = ((StackBehaviorAttribute)Attribute.GetCustomAttribute(member, typeof(StackBehaviorAttribute)))?.Options ?? default; | |
expr.StackBehavior = stackBeh; | |
var flow = (ComputeFlowAttribute)Attribute.GetCustomAttribute(member, typeof(ComputeFlowAttribute)); | |
if (flow == null) | |
throw new BadProgramException("Computation must have a flow attribute"); | |
switch (comp) { | |
case LoadVariableAttribute load: | |
expr.Variable = ResolveVariable(load.Variable); | |
CheckCanRead(expr.Variable); | |
scope.PushValue(expr.Variable.Type); | |
break; | |
case LoadVariableAddressAttribute loada: | |
expr.Variable = ResolveVariable(loada.Variable); | |
CheckCanRead(expr.Variable); | |
// TODO: Puttis in codegen | |
// if ((expr.Variable.Flags & VariableFlags.Writable) != 0) | |
// ilgen.Emit(OpCodes.Readonly); | |
scope.PushValue(expr.Variable.Type.MakeByRefType()); | |
break; | |
case LoadConstantAttribute constant: | |
scope.PushValue(constant.Type); | |
break; | |
case DereferenceValueAttribute deref: { | |
var type = scope.TopValue; | |
if (!type.IsByRef) | |
throw new InvalidCastException("Dereferenced value is not by-ref type"); | |
if (!stackBeh.HasFlag(StackBehaviorOptions.NoPop)) | |
scope.PopValue(); | |
scope.PushValue(type.GetElementType()); | |
break; | |
} | |
case StoreDerefValueAttribute store: { | |
var value = scope.PopValue(); | |
var type = scope.PopValue(); | |
if (!type.IsByRef) | |
throw new InvalidCastException("Store target is not by-ref type"); | |
if (!value.IsAssignableTo(type.GetElementType())) | |
throw new InvalidCastException("Store value and target types don't match"); | |
if (stackBeh.HasFlag(StackBehaviorOptions.NoPop)) { | |
scope.PushValue(type); | |
scope.PushValue(value); | |
} | |
break; | |
} | |
case CompareValuesAttribute: | |
case AddValuesAttribute: | |
case SubtractValuesAttribute: | |
case MultiplyValuesAttribute: | |
case DivideValuesAttribute: | |
case DivideModuleValuesAttribute: { | |
var rhs = scope.PopValue(); | |
var lhs = scope.PopValue(); | |
if (stackBeh.HasFlag(StackBehaviorOptions.NoPop)) { | |
scope.PushValue(lhs); | |
scope.PushValue(rhs); | |
} | |
if (!rhs.Equals(lhs)) | |
throw new InvalidCastException($"Arithmetic operands' types don't match ({lhs.FullName} and {rhs.FullName})"); | |
if (!rhs.IsPrimitive) | |
throw new InvalidCastException($"Arithmetic operands aren't primitive types ({lhs.FullName})"); | |
if (comp is CompareValuesAttribute) | |
scope.PushValue(typeof(bool)); | |
else | |
scope.PushValue(lhs); | |
break; | |
} | |
case ConvertToBooleanAttribute: { | |
var value = scope.PopValue(); | |
scope.PushValue(typeof(bool)); | |
break; | |
} | |
default: | |
throw new NotSupportedException("Computation is not supported"); | |
} | |
L_execFlow: | |
if (flow is ComputeNextAttribute next) { | |
var nextMemb = scope.LookupNextMember(next.Symbol); | |
var nextComp = (ComputationAttribute)Attribute.GetCustomAttribute(nextMemb, typeof(ComputationAttribute)); | |
if (nextComp == null) { | |
flow = (ComputeFlowAttribute)Attribute.GetCustomAttribute(nextMemb, typeof(ComputeFlowAttribute)); | |
if (flow == null) | |
throw new BadProgramException("Computation must have a flow attribute"); | |
goto L_execFlow; | |
//throw new BadProgramException("Entity is not a computation"); | |
} | |
expr.Next = ResolveComputation(scope, nextComp, nextMemb); | |
} else if (flow is ComputeDoneAttribute) { | |
if (scope.Stack.Count != 0) | |
throw new BadProgramException("Expected empty stack"); | |
} else if (flow is ComputeResultAttribute) { | |
if (scope.Stack.Count != 1) | |
throw new BadProgramException("Expected single value on stack"); | |
scope.ResultType = scope.TopValue; | |
} else { | |
throw new NotSupportedException("Invalid compute flow"); | |
} | |
return expr; | |
} | |
public void Run(object[] args=null) { | |
if (!BytecodeGenerated) | |
throw new InvalidOperationException("Method is not compiled yet"); | |
Bytecode.Invoke(null, args); | |
} | |
private DynamicMethod Bytecode; | |
private bool BytecodeGenerated; | |
private MethodInfo GetMethodInfoForCall() | |
=> Bytecode ??= new DynamicMethod(Target.FullName, ResultType, ParameterSignature, Target); | |
public void GenerateBytecode() { | |
if (BytecodeGenerated) | |
return; | |
if (EntryPoint == null) | |
throw new InvalidOperationException("Method has no entry point"); | |
Bytecode ??= new DynamicMethod(Target.FullName, ResultType, ParameterSignature, Target); | |
BytecodeGenerated = true; | |
var ilgen = Bytecode.GetILGenerator(); | |
var visited = new HashSet<FlowAction>(); | |
var stack = new Stack<FlowAction>(); | |
stack.Push(EntryPoint); | |
foreach (var variable in ResolvedVariables.Values) { | |
if (variable.ParamIndex == 0) | |
variable.LocalRef = ilgen.DeclareLocal(variable.Type); | |
else | |
Bytecode.DefineParameter(variable.ParamIndex, ParameterAttributes.None, variable.Name); | |
} | |
foreach (var action in ResolvedActions.Values) { | |
action.Label = ilgen.DefineLabel(); | |
} | |
while (stack.Count != 0) { | |
var act = stack.Pop(); | |
if (visited.Contains(act)) | |
continue; | |
visited.Add(act); | |
GenerateAction(act, ilgen, stack); | |
} | |
//Console.WriteLine("Done compiling method {0}", Bytecode); | |
//DumpMethodBody(Bytecode.GetMethodBody()); | |
} | |
private MethodInfo cachedWriteLineString_; | |
private MethodInfo cachedWriteLineString | |
=> cachedWriteLineString_ ??= typeof(Console).GetMethod("WriteLine", new[] {typeof(string)}); | |
private MethodInfo cachedWriteString_; | |
private MethodInfo cachedWriteString | |
=> cachedWriteString_ ??= typeof(Console).GetMethod("Write", new[] {typeof(string)}); | |
private MethodInfo cachedWriteLineInt_; | |
private MethodInfo cachedWriteLineInt | |
=> cachedWriteLineInt_ ??= typeof(Console).GetMethod("WriteLine", new[] {typeof(int)}); | |
private MethodInfo cachedWriteInt_; | |
private MethodInfo cachedWriteInt | |
=> cachedWriteInt_ ??= typeof(Console).GetMethod("Write", new[] {typeof(int)}); | |
private MethodInfo cachedReadLine_; | |
private MethodInfo cachedReadLine | |
=> cachedReadLine_ ??= typeof(Console).GetMethod("ReadLine", Array.Empty<Type>()); | |
private MethodInfo cachedRead_; | |
private MethodInfo cachedRead | |
=> cachedRead_ ??= typeof(Console).GetMethod("Read", Array.Empty<Type>()); | |
private MethodInfo cachedTrim_; | |
private MethodInfo cachedTrim | |
=> cachedTrim_ ??= typeof(string).GetMethod("Trim", Array.Empty<Type>()); | |
private Exception excNoPop => new NotSupportedException("No popping is currently not supported in this context"); | |
private void GenerateAction(FlowAction action, ILGenerator ilgen, Stack<FlowAction> stack) { | |
ilgen.MarkLabel(action.Label); | |
if (action.Computation is ComputationScope compScope) { | |
var opstack = new Stack<OperandStackValueType>(); | |
for (var comp = compScope.Entry; comp != null; comp = comp.Next) | |
switch (comp.Action) { | |
case LoadVariableAttribute: | |
EmitVariableAccess(ilgen, comp.Variable, VariableAccessKind.Load); | |
opstack.Push(TypeToOperandType(comp.Variable.Type)); | |
break; | |
case LoadVariableAddressAttribute: | |
{ | |
EmitVariableAccess(ilgen, comp.Variable, VariableAccessKind.Address); | |
/*int typ = (int)TypeToOperandType(comp.Action.Variable.Type); | |
if (typ >= (int)OperandStackValueType.I4 && typ <= (int)OperandStackValueType.U) | |
opstack.Push((OperandStackValueType)(typ + 8)); | |
else | |
opstack.Push(OperandStackValueType.RefOther);*/ | |
opstack.Push(TypeToRefOperandType(comp.Variable.Type)); | |
break; | |
} | |
case LoadConstantAttribute cons: | |
GenerateNumericValue(ilgen, Convert.ChangeType(cons.Value, cons.Type), cons.Type); | |
opstack.Push(TypeToOperandType(cons.Type)); | |
break; | |
case DereferenceValueAttribute: | |
{ | |
if ((comp.StackBehavior & StackBehaviorOptions.NoPop) != 0) | |
{ | |
ilgen.Emit(OpCodes.Dup); | |
opstack.Push(opstack.Peek()); | |
} | |
var typ = opstack.Pop(); | |
ilgen.Emit(typ switch { | |
OperandStackValueType.RefI1 => OpCodes.Ldind_I1, | |
OperandStackValueType.RefU1 => OpCodes.Ldind_U1, | |
OperandStackValueType.RefI2 => OpCodes.Ldind_I2, | |
OperandStackValueType.RefU2 => OpCodes.Ldind_U2, | |
OperandStackValueType.RefI4 => OpCodes.Ldind_I4, | |
OperandStackValueType.RefU4 => OpCodes.Ldind_U4, | |
OperandStackValueType.RefI8 => OpCodes.Ldind_I8, | |
OperandStackValueType.RefU8 => OpCodes.Ldind_I8, | |
OperandStackValueType.RefI => OpCodes.Ldind_I, | |
OperandStackValueType.RefU => OpCodes.Ldind_I, | |
OperandStackValueType.RefOther => throw new InvalidCastException("Unknown reference type for dereferencing"), | |
_ => throw new InvalidOperationException("Invalid dereference reached codegen"), | |
}); | |
opstack.Push(typ switch { | |
OperandStackValueType.RefI1 or OperandStackValueType.RefI2 or OperandStackValueType.RefI4 => OperandStackValueType.I4, | |
OperandStackValueType.RefU1 or OperandStackValueType.RefU2 or OperandStackValueType.RefU4 => OperandStackValueType.U4, | |
OperandStackValueType.RefI8 => OperandStackValueType.I8, | |
OperandStackValueType.RefU8 => OperandStackValueType.U8, | |
OperandStackValueType.RefI => OperandStackValueType.I, | |
OperandStackValueType.RefU => OperandStackValueType.U, | |
_ => default, | |
}); | |
break; | |
} | |
case StoreDerefValueAttribute: | |
if ((comp.StackBehavior & StackBehaviorOptions.NoPop) != 0) | |
throw excNoPop; | |
opstack.Pop(); | |
ilgen.Emit(opstack.Pop() switch { | |
OperandStackValueType.RefI1 => OpCodes.Stind_I1, | |
OperandStackValueType.RefU1 => OpCodes.Stind_I1, | |
OperandStackValueType.RefI2 => OpCodes.Stind_I2, | |
OperandStackValueType.RefU2 => OpCodes.Stind_I2, | |
OperandStackValueType.RefI4 => OpCodes.Stind_I4, | |
OperandStackValueType.RefU4 => OpCodes.Stind_I4, | |
OperandStackValueType.RefI8 => OpCodes.Stind_I8, | |
OperandStackValueType.RefU8 => OpCodes.Stind_I8, | |
OperandStackValueType.RefI => OpCodes.Stind_I, | |
OperandStackValueType.RefU => OpCodes.Stind_I, | |
OperandStackValueType.RefOther => throw new InvalidCastException("Unknown reference type for dereferencing"), | |
_ => throw new InvalidOperationException("Invalid dereference reached codegen"), | |
}); | |
break; | |
case CompareValuesAttribute cmp: { | |
var trhs = opstack.Pop(); | |
var tlhs = opstack.Pop(); | |
if (tlhs != trhs) | |
throw new InvalidOperationException("Mismatch between compared types"); | |
bool isUnsigned = cmp.Comparison == Comparison.Equals || cmp.Comparison == Comparison.NotEquals | |
|| tlhs switch { | |
OperandStackValueType.I4 or OperandStackValueType.I8 or OperandStackValueType.I | |
or OperandStackValueType.Float => false, | |
OperandStackValueType.U4 or OperandStackValueType.U8 or OperandStackValueType.U | |
=> true, | |
_ => throw new InvalidOperationException("Cannot compare those values"), | |
}; | |
opstack.Push(OperandStackValueType.I4); | |
if (comp.Next == null && action.AttrSeq is SeqFlowBranchAttribute) { | |
// Emit branch instruction | |
switch (cmp.Comparison) { | |
case Comparison.Equals: | |
ilgen.Emit(OpCodes.Beq_S, action.BranchTrue.Label); | |
ilgen.Emit(OpCodes.Br_S, action.BranchFalse.Label); | |
break; | |
case Comparison.NotEquals: | |
ilgen.Emit(OpCodes.Beq_S, action.BranchFalse.Label); | |
ilgen.Emit(OpCodes.Br_S, action.BranchTrue.Label); | |
break; | |
case Comparison.LessThan: | |
ilgen.Emit(isUnsigned ? OpCodes.Blt_Un_S : OpCodes.Blt_S, action.BranchTrue.Label); | |
ilgen.Emit(OpCodes.Br_S, action.BranchFalse.Label); | |
break; | |
case Comparison.GreaterThan: | |
ilgen.Emit(isUnsigned ? OpCodes.Bgt_Un_S : OpCodes.Bgt_S, action.BranchTrue.Label); | |
ilgen.Emit(OpCodes.Br_S, action.BranchFalse.Label); | |
break; | |
case Comparison.LessOrEqualTo: | |
ilgen.Emit(isUnsigned ? OpCodes.Ble_Un_S : OpCodes.Ble_S, action.BranchTrue.Label); | |
ilgen.Emit(OpCodes.Br_S, action.BranchFalse.Label); | |
break; | |
case Comparison.GreaterOrEqualTo: | |
ilgen.Emit(isUnsigned ? OpCodes.Bge_Un_S : OpCodes.Bge_S, action.BranchTrue.Label); | |
ilgen.Emit(OpCodes.Br_S, action.BranchFalse.Label); | |
break; | |
} | |
stack.Push(action.BranchFalse); | |
stack.Push(action.BranchTrue); | |
return; | |
} | |
switch (cmp.Comparison) { | |
case Comparison.Equals: | |
ilgen.Emit(OpCodes.Ceq); | |
break; | |
case Comparison.NotEquals: | |
ilgen.Emit(OpCodes.Ceq); | |
ilgen.Emit(OpCodes.Ldc_I4_0); | |
ilgen.Emit(OpCodes.Ceq); | |
break; | |
case Comparison.LessThan: | |
ilgen.Emit(isUnsigned ? OpCodes.Clt_Un : OpCodes.Clt); | |
break; | |
case Comparison.GreaterThan: | |
ilgen.Emit(isUnsigned ? OpCodes.Cgt_Un : OpCodes.Cgt); | |
break; | |
case Comparison.LessOrEqualTo: | |
ilgen.Emit(isUnsigned ? OpCodes.Cgt_Un : OpCodes.Cgt); | |
ilgen.Emit(OpCodes.Ldc_I4_0); | |
ilgen.Emit(OpCodes.Ceq); | |
break; | |
case Comparison.GreaterOrEqualTo: | |
ilgen.Emit(isUnsigned ? OpCodes.Clt_Un : OpCodes.Cgt); | |
ilgen.Emit(OpCodes.Ldc_I4_0); | |
ilgen.Emit(OpCodes.Ceq); | |
break; | |
}} | |
break; | |
case ConvertToBooleanAttribute: { | |
var typ = opstack.Pop(); | |
ilgen.Emit(OpCodes.Ldc_I4_0); | |
if (typ == OperandStackValueType.I8 || typ == OperandStackValueType.U8) | |
ilgen.Emit(OpCodes.Conv_I8); | |
else if (typ == OperandStackValueType.I || typ == OperandStackValueType.U) | |
ilgen.Emit(OpCodes.Conv_I); | |
ilgen.Emit(OpCodes.Cgt_Un); | |
opstack.Push(OperandStackValueType.I4); | |
break; | |
} | |
case AddValuesAttribute: | |
case SubtractValuesAttribute: | |
case MultiplyValuesAttribute: | |
case DivideValuesAttribute: | |
case DivideModuleValuesAttribute: { | |
var trhs = opstack.Pop(); | |
var tlhs = opstack.Pop(); | |
if (tlhs != trhs) | |
throw new InvalidOperationException("Mismatch between compared types"); | |
bool isUnsigned = tlhs switch { | |
OperandStackValueType.I4 or OperandStackValueType.I8 or OperandStackValueType.I | |
or OperandStackValueType.Float => false, | |
OperandStackValueType.U4 or OperandStackValueType.U8 or OperandStackValueType.U | |
=> true, | |
_ => throw new InvalidOperationException("Cannot do arithmetic on those types"), | |
}; | |
switch (comp.Action) { | |
case AddValuesAttribute: | |
ilgen.Emit(isUnsigned ? OpCodes.Add_Ovf_Un : OpCodes.Add_Ovf); | |
break; | |
case SubtractValuesAttribute: | |
ilgen.Emit(isUnsigned ? OpCodes.Sub_Ovf_Un : OpCodes.Sub_Ovf); | |
break; | |
case MultiplyValuesAttribute: | |
ilgen.Emit(isUnsigned ? OpCodes.Mul_Ovf_Un : OpCodes.Mul_Ovf); | |
break; | |
case DivideValuesAttribute: | |
ilgen.Emit(isUnsigned ? OpCodes.Div_Un : OpCodes.Div); | |
break; | |
case DivideModuleValuesAttribute: | |
ilgen.Emit(isUnsigned ? OpCodes.Rem_Un : OpCodes.Rem); | |
break; | |
} | |
opstack.Push(tlhs); | |
break; | |
} | |
} | |
/*if (opstack.Count != 0) | |
Console.WriteLine("Debug: items on the stack = {0}", opstack.Count);*/ | |
} | |
else if (action.Action is StatementAction stmt) | |
switch (stmt.Action) { | |
case OutputStringAttribute outs: | |
ilgen.Emit(OpCodes.Ldstr, outs.String); | |
ilgen.Emit(OpCodes.Call, cachedWriteString); | |
break; | |
case OutputVariableAttribute outv: { | |
var type = stmt.Variable.Type; | |
if (!new[] {typeof(string), typeof(char), typeof(bool), typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(sbyte), typeof(byte), typeof(long), typeof(ulong), typeof(nint), typeof(nuint)}.Contains(type)) | |
throw new InvalidCastException($"Type {type.FullName} cannot be printed"); | |
EmitVariableAccess(ilgen, stmt.Variable, VariableAccessKind.Load); | |
MethodInfo mi; | |
TypeCode tc = Type.GetTypeCode(type); | |
switch (tc) | |
{ | |
case TypeCode.Int16: | |
case TypeCode.SByte: | |
ilgen.Emit(OpCodes.Conv_I4); | |
type = typeof(int); | |
break; | |
case TypeCode.UInt16: | |
case TypeCode.Byte: | |
ilgen.Emit(OpCodes.Conv_U4); | |
type = typeof(uint); | |
break; | |
/*case TypeCode.NativeInt: | |
ilgen.Emit(OpCodes.Conv_I8); | |
type = typeof(long); | |
break; | |
case TypeCode.NativeUInt: | |
ilgen.Emit(OpCodes.Conv_U8); | |
type = typeof(ulong); | |
break;*/ | |
} | |
if (outv.Newline) { | |
if (type == typeof(int)) | |
mi = cachedWriteLineInt; | |
else if (type == typeof(string)) | |
mi = cachedWriteLineString; | |
else | |
mi = typeof(Console).GetMethod("WriteLine", new[] {type}); | |
} else { | |
if (type == typeof(int)) | |
mi = cachedWriteInt; | |
else if (type == typeof(string)) | |
mi = cachedWriteString; | |
else | |
mi = typeof(Console).GetMethod("Write", new[] {type}); | |
} | |
ilgen.Emit(OpCodes.Call, mi); | |
break; | |
} | |
case InputValueAttribute inv: { | |
var type = stmt.Variable.Type; | |
if (!new[] {typeof(string), typeof(char), typeof(bool), typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(sbyte), typeof(byte), typeof(long), typeof(ulong), typeof(nint), typeof(nuint)}.Contains(type)) | |
throw new InvalidCastException($"Type {type.FullName} cannot be printed"); | |
if (!string.IsNullOrEmpty(inv.Prompt)) { | |
ilgen.Emit(OpCodes.Ldstr, inv.Prompt); | |
ilgen.Emit(OpCodes.Call, cachedWriteString); | |
} | |
if (type == typeof(string)) { | |
ilgen.Emit(OpCodes.Call, cachedReadLine); | |
} else if (type == typeof(char)) { | |
ilgen.Emit(OpCodes.Call, cachedRead); | |
} else { | |
ilgen.Emit(OpCodes.Call, cachedReadLine); | |
ilgen.Emit(OpCodes.Call, cachedTrim); | |
ilgen.Emit(OpCodes.Call, type.GetMethod("Parse", new[] {typeof(string)})); | |
} | |
EmitVariableAccess(ilgen, stmt.Variable, VariableAccessKind.Store); | |
break; | |
} | |
case InvokeSubroutineAttribute invoke: { | |
// Crossref: check result type match here | |
if (stmt.ResultVariable != null && !stmt.ResultVariable.Type.IsAssignableFrom(stmt.Subroutine.ResultType)) | |
throw new InvalidCastException("Cannot convert subroutine call result to variable type"); | |
foreach (var arg in stmt.Arguments) { | |
EmitVariableAccess(ilgen, arg, VariableAccessKind.Load); | |
} | |
var sub = stmt.Subroutine.GetMethodInfoForCall(); | |
ilgen.Emit(OpCodes.Call, sub); | |
if (stmt.ResultVariable != null) { | |
EmitVariableAccess(ilgen, stmt.ResultVariable, VariableAccessKind.Store); | |
} else if (stmt.Subroutine.ResultType != typeof(void)) { | |
ilgen.Emit(OpCodes.Pop); | |
} | |
break; | |
} | |
case AssignVariable assign: | |
if (assign.Value == null) | |
ilgen.Emit(OpCodes.Ldnull); | |
else { | |
var type = stmt.Variable.Type; | |
if (type == typeof(string)) | |
ilgen.Emit(OpCodes.Ldstr, (string)assign.Value); | |
else if (type.IsPrimitive) | |
GenerateNumericValue(ilgen, assign.Value, type); | |
else | |
throw new InvalidProgramException("Only string and primitive types can be explicitly assigned"); | |
} | |
EmitVariableAccess(ilgen, stmt.Variable, VariableAccessKind.Store); | |
break; | |
} | |
if (action.Computation?.ResultType != null && action.Computation.ResultType != typeof(void) && !(action.AttrSeq is SeqFlowBranchAttribute)) | |
// Pop computed value | |
ilgen.Emit(OpCodes.Pop); | |
switch (action.AttrSeq) { | |
case SeqFlowNextAttribute: | |
ilgen.Emit(OpCodes.Br_S, action.Next.Label); | |
stack.Push(action.Next); | |
break; | |
case SeqFlowBranchAttribute: | |
ilgen.Emit(OpCodes.Brfalse_S, action.BranchFalse.Label); | |
ilgen.Emit(OpCodes.Br_S, action.BranchTrue.Label); | |
stack.Push(action.BranchFalse); | |
stack.Push(action.BranchTrue); | |
break; | |
case SeqFlowHaltAttribute halt: | |
GenerateNumericValue(ilgen, halt.Status, typeof(int)); | |
ilgen.Emit(OpCodes.Call, typeof(Environment).GetMethod("Exit", new[] {typeof(int)})); | |
ilgen.Emit(OpCodes.Ret); | |
break; | |
case SubroutineResultAttribute: | |
if (action.ResultVar != null) | |
EmitVariableAccess(ilgen, action.ResultVar, VariableAccessKind.Load); | |
ilgen.Emit(OpCodes.Ret); | |
break; | |
} | |
} | |
private enum VariableAccessKind { | |
Load, Store, Address, | |
} | |
private void EmitVariableAccess(ILGenerator ilgen, Variable variable, VariableAccessKind access) { | |
if (variable.LocalRef != null) { | |
switch (access) { | |
case VariableAccessKind.Load: | |
ilgen.Emit(OpCodes.Ldloc_S, variable.LocalRef); | |
break; | |
case VariableAccessKind.Store: | |
ilgen.Emit(OpCodes.Stloc_S, variable.LocalRef); | |
break; | |
case VariableAccessKind.Address: | |
ilgen.Emit(OpCodes.Ldloca_S, variable.LocalRef); | |
break; | |
} | |
} else { | |
switch (access) { | |
case VariableAccessKind.Load: | |
ilgen.Emit(OpCodes.Ldarg_S, variable.ParamIndex - 1); | |
break; | |
case VariableAccessKind.Store: | |
ilgen.Emit(OpCodes.Starg_S, variable.ParamIndex - 1); | |
break; | |
case VariableAccessKind.Address: | |
ilgen.Emit(OpCodes.Ldarga_S, variable.ParamIndex - 1); | |
break; | |
} | |
} | |
} | |
private void GenerateNumericValue(ILGenerator ilgen, object value, Type type) { | |
ulong uns; | |
long sig; | |
switch (Type.GetTypeCode(type)) { | |
case TypeCode.Boolean: | |
ilgen.Emit((bool)value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); | |
break; | |
case TypeCode.Char: | |
uns = (char)value; | |
goto emitU4; | |
case TypeCode.SByte: | |
sig = (sbyte)value; | |
goto emitI4; | |
case TypeCode.Byte: | |
uns = (byte)value; | |
goto emitU4; | |
case TypeCode.Int16: | |
sig = (short)value; | |
goto emitI4; | |
case TypeCode.UInt16: | |
uns = (ushort)value; | |
goto emitU4; | |
case TypeCode.Int32: | |
sig = (int)value; | |
goto emitI4; | |
case TypeCode.UInt32: | |
uns = (uint)value; | |
goto emitU4; | |
case TypeCode.Int64: | |
sig = (long)value; | |
goto emitI8; | |
case TypeCode.UInt64: | |
uns = (ulong)value; | |
goto emitU8; | |
case TypeCode.Single: | |
ilgen.Emit(OpCodes.Ldc_R4, (float)value); | |
break; | |
case TypeCode.Double: | |
ilgen.Emit(OpCodes.Ldc_R8, (double)value); | |
break; | |
default: | |
throw new InvalidOperationException("Only numeric types are supported"); | |
} | |
return; | |
emitI4: | |
if (sig >= -1 && sig <= 8) | |
ilgen.Emit(NumeralOpCode((int)sig)); | |
else if (sig >= -0x80 && sig <= 0x7F) | |
ilgen.Emit(OpCodes.Ldc_I4_S, (int)sig); | |
else | |
ilgen.Emit(OpCodes.Ldc_I4, sig); | |
return; | |
emitU4: | |
if (uns <= 8) | |
ilgen.Emit(NumeralOpCode((int)uns)); | |
else if (uns <= 0x7F) | |
ilgen.Emit(OpCodes.Ldc_I4_S, (uint)uns); | |
else | |
ilgen.Emit(OpCodes.Ldc_I4, uns); | |
return; | |
emitI8: | |
if (sig >= -1 && sig <= 8) | |
ilgen.Emit(NumeralOpCode((int)sig)); | |
else if (sig >= -0x80 && sig <= 0x7F) | |
ilgen.Emit(OpCodes.Ldc_I4_S, sig); | |
else if (sig >= -0x8000_0000L && sig <= 0x7FFF_FFFFL) | |
ilgen.Emit(OpCodes.Ldc_I4, sig); | |
else { | |
ilgen.Emit(OpCodes.Ldc_I8, sig); | |
return; | |
} | |
ilgen.Emit(OpCodes.Conv_I8); | |
return; | |
emitU8: | |
if (uns <= 8) | |
ilgen.Emit(NumeralOpCode((int)uns)); | |
else if (uns <= 0x7F) | |
ilgen.Emit(OpCodes.Ldc_I4_S, uns); | |
else if (uns <= 0x7FFF_FFFFL) | |
ilgen.Emit(OpCodes.Ldc_I4, uns); | |
else { | |
ilgen.Emit(OpCodes.Ldc_I8, uns); | |
return; | |
} | |
ilgen.Emit(OpCodes.Conv_U8); | |
return; | |
} | |
// Computes OpCodes.Ldc_I4_0 + num | |
private static OpCode NumeralOpCode(int num) => num switch { | |
-1 => OpCodes.Ldc_I4_M1, | |
0 => OpCodes.Ldc_I4_0, | |
1 => OpCodes.Ldc_I4_1, | |
2 => OpCodes.Ldc_I4_2, | |
3 => OpCodes.Ldc_I4_3, | |
4 => OpCodes.Ldc_I4_4, | |
5 => OpCodes.Ldc_I4_5, | |
6 => OpCodes.Ldc_I4_6, | |
7 => OpCodes.Ldc_I4_7, | |
8 => OpCodes.Ldc_I4_8, | |
_ => throw null, | |
}; | |
private static void CheckCanRead(Variable variable) { | |
if ((variable.Flags & VariableFlags.Readable) == 0) | |
throw new InvalidProgramException("Variable cannot be read"); | |
} | |
private static void CheckCanWrite(Variable variable) { | |
if ((variable.Flags & VariableFlags.Writable) == 0) | |
throw new InvalidProgramException("Variable cannot be written"); | |
} | |
private static void DumpMethodBody(MethodBody mb) { | |
if (mb.InitLocals) | |
Console.WriteLine("Locals are initialised"); | |
Console.WriteLine("Max stack = {0}", mb.MaxStackSize); | |
Console.WriteLine("Local variables:"); | |
foreach (var local in mb.LocalVariables) { | |
Console.WriteLine("\t{0}", local); | |
} | |
Console.WriteLine("Bytecode for the method: {0}", mb.GetILAsByteArray()); | |
} | |
} | |
internal enum OperandStackValueType { | |
Unknown, | |
ValueType, | |
I4, | |
U4, | |
I8, | |
U8, | |
I, | |
U, | |
Float, | |
ObjRef, | |
// Difference between Nx types and RefNx types is 8 | |
RefI1, | |
RefU1, | |
RefI2, | |
RefU2, | |
RefI4, | |
RefU4, | |
RefI8, | |
RefU8, | |
RefI, | |
RefU, | |
RefOther, | |
Ptr, | |
}; | |
internal static class OperandStackValueTypeHelpers | |
{ | |
public static OperandStackValueType TypeToOperandType(Type type) => TypeCodeToOperandType(Type.GetTypeCode(type)); | |
public static OperandStackValueType TypeCodeToOperandType(TypeCode tc) => tc switch { | |
TypeCode.Empty or TypeCode.DBNull or TypeCode.String => OperandStackValueType.ObjRef, | |
TypeCode.Boolean or TypeCode.Char or TypeCode.Byte or TypeCode.UInt16 or TypeCode.UInt32 => OperandStackValueType.U4, | |
TypeCode.SByte or TypeCode.Int16 or TypeCode.Int32 => OperandStackValueType.I4, | |
TypeCode.UInt64 => OperandStackValueType.U8, | |
TypeCode.Int64 => OperandStackValueType.I8, | |
TypeCode.Single or TypeCode.Double => OperandStackValueType.Float, | |
TypeCode.DateTime => OperandStackValueType.ValueType, | |
_ => OperandStackValueType.Unknown, | |
}; | |
public static OperandStackValueType TypeToRefOperandType(Type type) => TypeCodeToRefOperandType(Type.GetTypeCode(type)); | |
public static OperandStackValueType TypeCodeToRefOperandType(TypeCode tc) => tc switch { | |
TypeCode.SByte => OperandStackValueType.RefI1, | |
TypeCode.Boolean or TypeCode.Byte => OperandStackValueType.RefU1, | |
TypeCode.Int16 => OperandStackValueType.RefI2, | |
TypeCode.Char or TypeCode.UInt16 => OperandStackValueType.RefU2, | |
TypeCode.Int32 => OperandStackValueType.RefI4, | |
TypeCode.UInt32 => OperandStackValueType.RefU4, | |
TypeCode.Int64 => OperandStackValueType.RefI8, | |
TypeCode.UInt64 => OperandStackValueType.RefU8, | |
//typeof(nint) => OperandStackValueType.RefI, | |
//typeof(nuint) => OperandStackValueType.RefU, | |
_ => OperandStackValueType.RefOther, | |
}; | |
} | |
[Flags] | |
internal enum VariableFlags { | |
Readable = 1, | |
Writable = 2, | |
} | |
internal class Variable { | |
public string Name { get; } | |
public Type Type { get; } | |
public PropertyInfo Node { get; } | |
public VariableFlags Flags { get; } | |
public LocalBuilder LocalRef; | |
public int ParamIndex; // 1-based | |
public Variable(string Name, Type Type, PropertyInfo Node, VariableFlags Flags) { | |
this.Name = Name; | |
this.Type = Type; | |
this.Node = Node; | |
this.Flags = Flags; | |
} | |
} | |
internal class FlowAction { | |
public readonly MemberInfo Node; | |
public readonly SeqPointAttribute AttrSeq; | |
public readonly ActionAttribute AttrAction; | |
public FlowAction Next; | |
public FlowAction BranchTrue; | |
public FlowAction BranchFalse; | |
public Variable ResultVar; | |
public StatementAction Action; | |
public ComputationScope Computation; | |
public Label Label; | |
public FlowAction(MemberInfo Node, SeqPointAttribute SeqFlow, ActionAttribute Action) { | |
this.Node = Node; | |
this.AttrSeq = SeqFlow; | |
this.AttrAction = Action; | |
} | |
} | |
internal class StatementAction { | |
public readonly ActionAttribute Action; | |
public Variable Variable; | |
public Compiler Subroutine; | |
public Variable[] Arguments; | |
public Variable ResultVariable { get => Variable; set => Variable = value; } | |
public StatementAction(ActionAttribute Action) { | |
this.Action = Action; | |
} | |
} | |
internal class ComputationScope { | |
public readonly Type Scope; | |
public ComputeAction Entry; | |
private HashSet<string> VisitedMembers = new(); | |
public readonly Stack<Type> Stack = new(); | |
public Type ResultType; | |
public Type TopValue => Stack.Peek(); | |
public void PushValue(Type value) => Stack.Push(value); | |
public Type PopValue() => Stack.Pop(); | |
public ComputationScope(Type Scope) { | |
this.Scope = Scope; | |
} | |
public MemberInfo LookupNextMember(string Name) { | |
if (VisitedMembers.Contains(Name)) | |
throw new BadProgramException("Cannot loop in computations"); | |
VisitedMembers.Add(Name); | |
var memb = Scope.GetMember(Name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); | |
if (memb.Length != 1) | |
throw new BadProgramException("Expected one member of declared name"); | |
if (memb[0].MemberType != MemberTypes.Property) | |
throw new BadProgramException("Compute node must be a property"); | |
return memb[0]; | |
} | |
} | |
internal class ComputeAction { | |
public readonly ComputationAttribute Action; | |
public readonly MemberInfo Node; | |
public StackBehaviorOptions StackBehavior; | |
public Variable Variable; | |
public ComputeAction Next; | |
public ComputeAction(ComputationAttribute Action, MemberInfo Node) { | |
this.Action = Action; | |
this.Node = Node; | |
} | |
} | |
} | |
#endregion VM Implementation | |
// Here is an example program | |
#region Example program | |
namespace MainProgram { | |
using AttrVM; | |
[EntryPoint(@"Start")] | |
[SubroutineArguments(new[] { @"MaxCount" })] | |
public static class Subroutine { | |
[AssignVariable(nameof(Iterator), (uint)0)] | |
[SeqFlowNext(nameof(RangeCheck))] | |
public static object Start { get; } | |
[LoadVariable(nameof(Iterator))] | |
[ComputeNext(@"LoadCount")] | |
[SeqFlowBranch(OnTrue= nameof(DoIteration), OnFalse= nameof(Exit))] | |
public static class RangeCheck { | |
[LoadVariable(nameof(MaxCount))] | |
[ComputeNext(nameof(Compare))] | |
public static object LoadCount { get; } | |
[CompareValues(Comparison.LessThan)] | |
[ComputeNext(nameof(Result))] | |
public static object Compare { get; } | |
[ComputeResult] | |
public static object Result { get; } | |
} | |
[LoadVariableAddress(nameof(Iterator))] | |
[ComputeNext(@"Deref")] | |
[SeqFlowNext(nameof(Case3Check))] | |
public static class DoIteration { | |
[DereferenceValue] | |
[StackBehavior(StackBehaviorOptions.NoPop)] | |
[ComputeNext(nameof(LoadOne))] | |
public static object Deref { get; } | |
[LoadConstant((uint)1)] | |
[ComputeNext(nameof(Add))] | |
public static object LoadOne { get; } | |
[AddValues] | |
[ComputeNext(nameof(Store))] | |
public static object Add { get; } | |
[StoreDerefValue] | |
[ComputeNext(nameof(Done))] | |
public static object Store { get; } | |
[ComputeDone] | |
public static object Done { get; } | |
} | |
[LoadVariable(nameof(Iterator))] | |
[ComputeNext(@"LoadConst")] | |
[SeqFlowBranch(OnTrue= nameof(Case1Check), OnFalse= nameof(DoCase3))] | |
public static class Case3Check { | |
[LoadConstant((uint)0x0F)] | |
[ComputeNext(nameof(AdvCompare))] | |
public static object LoadConst { get; } | |
[DivideModuleValues] | |
[ComputeNext(nameof(MakeResult))] | |
public static object AdvCompare { get; } | |
[ConvertToBoolean] | |
[ComputeResult] | |
public static object MakeResult { get; } | |
} | |
[OutputString("SneakSnack\n")] | |
[SeqFlowNext(nameof(RangeCheck))] | |
public static object DoCase3 { get; } | |
[LoadVariable(nameof(Iterator))] | |
[ComputeNext(@"LoadConst")] | |
[SeqFlowBranch(OnTrue= nameof(Case2Check), OnFalse= nameof(DoCase1))] | |
public static class Case1Check { | |
[LoadConstant((uint)0x03)] | |
[ComputeNext(nameof(AdvCompare))] | |
public static object LoadConst { get; } | |
[DivideModuleValues] | |
[ComputeNext(nameof(MakeResult))] | |
public static object AdvCompare { get; } | |
[ConvertToBoolean] | |
[ComputeResult] | |
public static object MakeResult { get; } | |
} | |
[OutputString("Sneak\n")] | |
[SeqFlowNext(nameof(RangeCheck))] | |
public static object DoCase1 { get; } | |
[LoadVariable(nameof(Iterator))] | |
[ComputeNext(@"LoadConst")] | |
[SeqFlowBranch(OnTrue= nameof(DoCase0), OnFalse= nameof(DoCase2))] | |
public static class Case2Check { | |
[LoadConstant((uint)0x05)] | |
[ComputeNext(nameof(AdvCompare))] | |
public static object LoadConst { get; } | |
[DivideModuleValues] | |
[ComputeNext(nameof(MakeResult))] | |
public static object AdvCompare { get; } | |
[ConvertToBoolean] | |
[ComputeResult] | |
public static object MakeResult { get; } | |
} | |
[OutputString("Snack\n")] | |
[SeqFlowNext(nameof(RangeCheck))] | |
public static object DoCase2 { get; } | |
[OutputVariable(nameof(Iterator), Newline= true)] | |
[SeqFlowNext(nameof(RangeCheck))] | |
public static object DoCase0 { get; } | |
[SubroutineResult] | |
public static object Exit { get; } | |
public static uint MaxCount { get; set; } | |
public static uint Iterator { get; set; } | |
} | |
[EntryPoint(@"Start")] | |
public static class Program { | |
[InputValue(nameof(MaxCount), Prompt= "Input number of iterations: ")] | |
[SeqFlowNext(nameof(RunComputation))] | |
public static object Start { get; } | |
[InvokeSubroutine(@"MainProgram.Subroutine", Args= new[] { nameof(MaxCount) }, Result= null)] | |
[SeqFlowNext(nameof(Halt))] | |
public static object RunComputation { get; } | |
[SeqFlowHalt(Status= 0)] | |
public static object Halt { get; } | |
public static uint MaxCount { get; set; } | |
} | |
} | |
#endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment