qamp/Qrakhen.Qamp.Core/Execution/Runner.cs

430 lines
13 KiB
C#

using Qrakhen.Qamp.Core.Collections;
using Qrakhen.Qamp.Core.Compilation;
using Qrakhen.Qamp.Core.Logging;
using Qrakhen.Qamp.Core.Tokenization;
using Qrakhen.Qamp.Core.Values;
using Qrakhen.Qamp.Core.Values.Objects;
using String = Qrakhen.Qamp.Core.Values.Objects.String;
namespace Qrakhen.Qamp.Core.Execution;
using Op = OpCode;
/*
* Todo:
* - Make Digester OOP-ish
* - inherit from a base digester that has the continouus stack / reader references as protected information
* - one digester for every job area (Loops, Functions, Variables, etc.)
* - time is not as essential here as I want to have the executables pre-compiled to OpCodes anyway
* - Think of a cleaner way to handle Obj (Pointers specifically)
* - Finish runner
* - add interpretation for all remaining op codes
* - make variable operations faster with optimized inline lambda expressions
* - better debugging for the instruction set
* - Add classic sqript dialect
* - Fix console "IDE"
* - Split Runner, Digester and Core into separate projects
*/
/// <summary>
/// Dynamic Pointer able to point to any object implementing the <see cref="IGetSet{long, TValue}"/> interface.
/// </summary>
public class Pointer<T>
{
protected IGetSet<long, T> Target;
public long Ptr;
public Pointer(IGetSet<long, T> target, long pointer = 0)
{
Target = target;
Ptr = pointer;
}
/// <summary>
/// Returns the next item from <see cref="Target"/> and increases the pointer by 1.
/// </summary>
public T Next() => Target.Get(Ptr++);
/// <summary>
/// Sets the value of <see cref="Target"/> at <paramref name="position"/>.
/// </summary>
public void Set(long position, T value) => Target.Set(position, value);
/// <summary>
/// Sets the value of <see cref="Target"/> at the current <see cref="Ptr"/> location.
/// </summary>
public void Set(T value) => Set(Ptr, value);
/// <summary>
/// Gets the value of <see cref="Target"/> at <paramref name="position"/>.
/// </summary>
public T Get(long position) => Target.Get(position);
/// <summary>
/// Gets the value of <see cref="Target"/> at the current <see cref="Ptr"/> location.
/// </summary>
public T Get() => Get(Ptr);
}
// this is all a bit cheesy imho
public class InstructionPtr : Pointer<Instruction>
{
public Segment Segment;
public InstructionPtr(Segment segment, int position = 0) : base(segment.Instructions, position)
{
Segment = segment;
}
public long NextLong()
{
long value = Segment.ReadLong(Ptr);
Ptr += sizeof(long);
return value;
}
public Value NextConstant()
{
return Segment.Constants[NextDynamic()];
}
public long NextDynamic()
{
int length = Segment.Read(Ptr) ^ 0x80;
Ptr++;
byte[] data = new byte[8];
for (int i = 0; i < length; i++)
data[i] = Segment.Read(Ptr++);
return data.ToInt64();
}
public String? GetStringConstant(long offset)
{
return Segment.Constants[offset].Ptr.Value as String;
}
public Instruction Instruction => Segment.Instructions[Ptr];
public static Instruction operator +(InstructionPtr ptr, int value)
=> ptr.Segment.Instructions[ptr.Ptr + value];
public static Instruction operator -(InstructionPtr ptr, int value)
=> ptr.Segment.Instructions[ptr.Ptr - value];
public static InstructionPtr operator ++(InstructionPtr ptr)
{
ptr.Ptr++;
return ptr;
}
public static InstructionPtr operator --(InstructionPtr ptr)
{
ptr.Ptr--;
return ptr;
}
}
public class Call
{
public Closure Closure;
public InstructionPtr Instruction;
public Pointer<Value> StackPtr;
public Call(Closure closure, StackLike<Value> stack, long stackOffset)
{
Closure = closure;
Instruction = new InstructionPtr(Closure.Function.Segment);
StackPtr = new Pointer<Value>(stack, stackOffset);
}
}
public unsafe class OuterWrapper
{
public Value* Target;
public Value Stored;
}
public class Runner : IDisposable
{
private readonly ILogger _logger = LoggerService.Get<Runner>();
public readonly Options Options;
public StackLike<Call> Calls;
// todo: maybe have a pointer everywhere to stacks or arrays and use those rather than built-in pointers or cursors... ?
public StackLike<Value> Stack;
public long Cursor;
public Register<string, Value> Globals = new Register<string, Value>();
public Register<string, Value> Imports = new Register<string, Value>();
public Register<string, Value> Exports = new Register<string, Value>();
public Runner(Options options)
{
Options = options;
Calls = new StackLike<Call>(options.MaxCalls);
Stack = new StackLike<Value>(options.InitialStackSize);
}
public ExecutionResult Run(Stream stream)
{
_logger.Method();
using Reader reader = new Reader(stream);
Digester digester = new Digester(reader);
Function function = digester.Digest();
File.WriteAllBytes("func.sqi", function.Segment.Serialize());
Closure closure = new Closure(function);
Push(Obj.Create(closure));
Call(closure, 0);
return Interpret();
}
private ExecutionResult Interpret()
{
_logger.Method();
Call call = Calls.Peek(-1);
do {
Op opCode = call.Instruction.Next();
_logger.Verbose($"OpCode: {opCode}");
switch (opCode) {
case Op.Constant: Push(call.Instruction.NextConstant()); break;
case Op.Pop: Pop(); break;
case Op.Not: OpNot(); break;
case Op.Add: OpAdd(); break;
case Op.Subtract: OpSubtract(); break;
case Op.Divide: OpDivide(); break;
case Op.Modulo: OpModulo(); break;
case Op.Multiply: OpMultiply(); break;
case Op.Negate: OpNegate(); break;
case Op.BitwiseOr: OpBitwiseOr(); break;
case Op.BitwiseAnd: OpBitwiseAnd(); break;
case Op.BitwiseXor: OpBitwiseXor(); break;
case Op.BitwiseNot: OpBitwiseNot(); break;
case Op.BitwiseLeft: OpBitwiseLeft(); break;
case Op.BitwiseRight: OpBitwiseRight(); break;
case Op.GetLocal: {
long slot = call.Instruction.NextDynamic();
_logger.Verbose($"getting slot {slot} which is {call.StackPtr.Get(slot)}");
Push(call.StackPtr.Get(slot));
break;
}
case Op.SetLocal: {
long slot = call.Instruction.NextDynamic();
_logger.Verbose($"setting stackptr {slot} to {Peek()}");
call.StackPtr.Set(slot, Peek());
break;
}
case Op.DefineGlobal: {
string? name = call.Instruction.GetStringConstant(call.Instruction.NextDynamic())?.Value;
if (string.IsNullOrEmpty(name))
throw new ExecutionException($"tried to define global variable with empty name");
Globals[name] = Pop();
_logger.Verbose($"defined global {name} as {Globals[name]}");
break;
}
case Op.SetGlobal: {
string? name = call.Instruction.GetStringConstant(call.Instruction.NextDynamic())?.Value;
if (string.IsNullOrEmpty(name))
throw new ExecutionException($"tried to set global variable with empty name");
if (!Globals.Has(name))
throw new ExecutionException($"tried to set a value of non-existing global variable");
Globals[name] = Pop();
_logger.Verbose($"set global {name} = {Globals[name]}");
break;
}
case Op.GetGlobal: {
string? name = call.Instruction.GetStringConstant(call.Instruction.NextDynamic())?.Value;
if (string.IsNullOrEmpty(name))
throw new ExecutionException($"tried to set global variable with empty name");
if (!Globals.Has(name))
Push(Value.Void);
else
Push(Globals[name]);
_logger.Verbose($"got global {name} ({Peek()})");
break;
}
case Op.Typeof: {
Value value = Pop();
Push(String.Make(value.Type.ToString()));
break;
}
case Op.Print: {
Value value = Pop();
IO.Console.Write(value);
break;
}
case Op.Return: // todo: not done
Value result = Pop();
Calls.Pop();
if (Calls.Count == 0) {
Pop();
return ExecutionResult.OK;
}
Cursor = call.StackPtr.Ptr; // well find a better way...
Push(result);
call = Calls.Peek();
break;
}
} while (call.Instruction.Segment.Instructions.Length > call.Instruction.Ptr);
return ExecutionResult.OK;
}
private bool Call(Closure closure, int argumentCount)
{
_logger.Method();
if (argumentCount != closure.Function.ArgumentCount)
throw new Exception($"Expected {closure.Function.ArgumentCount} arguments but got {argumentCount}");
if (Calls.Count > Options.MaxCalls)
throw new StackOverflowException($"Stack overflow {Calls.Count}");
Call call = new(closure, Stack, Stack.Position - argumentCount);
Calls.Push(call);
return true;
}
private Value Peek(int delta = -1)
{
_logger.Method();
return Stack.Peek(delta);
}
private Value Pop()
{
_logger.Method();
return Stack.Pop();
}
private void Push(Value value)
{
_logger.Method($"{value}");
Stack.Push(value);
}
private void OpNot()
{
Value value = Pop();
Push(ValueOperation.Not(value));
}
private void OpAdd()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.Add(left, right));
}
private void OpSubtract()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.Subtract(left, right));
}
private void OpDivide()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.Divide(left, right));
}
private void OpModulo()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.Modulo(left, right));
}
private void OpMultiply()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.Multiply(left, right));
}
private void OpNegate()
{
Value value = Pop();
Push(ValueOperation.Negate(value));
}
private void OpBitwiseOr()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.BitwiseOr(left, right));
}
private void OpBitwiseAnd()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.BitwiseAnd(left, right));
}
private void OpBitwiseXor()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.BitwiseXor(left, right));
}
private void OpBitwiseLeft()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.BitwiseLeft(left, right));
}
private void OpBitwiseRight()
{
Value right = Pop();
Value left = Pop();
Push(ValueOperation.BitwiseRight(left, right));
}
private void OpBitwiseNot()
{
Value value = Pop();
Push(ValueOperation.BitwiseInvert(value));
}
public void Dispose()
{
_logger.Method();
}
}
public enum ExecutionResult
{
OK = 0x0000,
Error = 0x1000,
Compilation = Error | 0x0001,
Execution = Error | 0x0002
}
public readonly struct Options(
int maxCalls = 0x100,
int stackSize = 0x200,
int initialStackSize = 0x80)
{
public readonly int MaxCalls = maxCalls;
public readonly int StackSize = stackSize;
public readonly int InitialStackSize = initialStackSize;
public Options() : this(0x100, 0x200, 0x80) { }
}