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

869 lines
27 KiB
C#

using Qrakhen.Qamp.Core.Collections;
using Qrakhen.Qamp.Core.Logging;
using Qrakhen.Qamp.Core.Tokenization;
using Qrakhen.Qamp.Core.Values;
using Qrakhen.Qamp.Core.Values.Objects;
using System.Xml.Linq;
using String = Qrakhen.Qamp.Core.Values.Objects.String;
namespace Qrakhen.Qamp.Core.Execution;
using Op = OpCode;
using T = Values.ValueType;
public class Call
{
public Context Context;
public InstructionPtr Instruction;
public Pointer<Value> StackPtr;
public Call(Context context, StackLike<Value> stack, long stackOffset)
{
Context = context;
Instruction = new InstructionPtr(Context.Function.Segment);
StackPtr = new Pointer<Value>(stack, stackOffset);
}
}
public interface IOperationResolver
{
bool CanResolve(OpCode code);
OperationResult Resolve(Runner runner, OpCode code);
}
public struct OperationResult
{
public bool Success;
public Value Value;
public RuntimeException? Error;
public static OperationResult MakeSuccess(Value value)
{
return new OperationResult { Success = true, Value = value };
}
public static OperationResult MakeError(RuntimeException exception)
{
return new OperationResult { Success = false, Value = Value.Void, Error = exception };
}
}
public class OperationRouter
{
private readonly List<IOperationResolver> _handlers = [];
private readonly Dictionary<OpCode, IOperationResolver> _cache = [];
public void Register(IOperationResolver resolver)
{
_handlers.Add(resolver);
}
public IOperationResolver? Route(Runner runner, OpCode opCode)
{
if (!_cache.TryGetValue(opCode, out IOperationResolver? resolver)) {
resolver = _handlers.FirstOrDefault(h => h.CanResolve(opCode));
if (resolver == null)
return null;
_cache[opCode] = resolver;
}
return _cache[opCode];
}
}
public class Runner : IDisposable
{
private readonly ILogger _logger = LoggerService.Get<Runner>();
private readonly OperationRouter _router = new OperationRouter();
private Value _runnerReturned = Value.Void;
public readonly Options Options;
private List<IOperationResolver> _resolvers = [];
private StackLike<Call> _calls;
// todo: maybe have a pointer everywhere to stacks or arrays and use those rather than built-in pointers or cursors... ?
private StackLike<Value> _stack;
private long _cursor => _stack.Position;
private List<Outer> Outers = [];
private Register<string, Value> Globals = new();
private Register<string, Value> Imports = new();
private Register<string, Value> Exports = new();
public Runner(Options options)
{
Options = options;
_calls = new StackLike<Call>(options.MaxCalls);
_stack = new StackLike<Value>(options.InitialStackSize);
IO.Console.StdOut ??= options.StdOut;
IO.Console.StdIn ??= options.StdIn;
Globals["time"] = Obj.Create(new NativeFunction("time",
v => new Value(DateTime.Now.Ticks / 100)));
Globals["ftime"] = Obj.Create(new NativeFunction("ftime",
v =>
String.Make(
new DateTime(v[0].Signed * 10)
.ToString(
v[1].GetString() ??
Timestamp.DefaultFormat)),
"time",
"format"));
Globals["sin"] = Obj.Create(new NativeFunction("sin",
v => new Value(Math.Sin(v[0].GetDecimal())), "v"));
Globals["cos"] = Obj.Create(new NativeFunction("cos",
v => new Value(Math.Cos(v[0].GetDecimal())), "v"));
Globals["exit"] = Obj.Create(new NativeFunction("exit", v => {
Environment.Exit((int)(v.FirstOrDefault().Signed));
return Value.Void;
},
"code?"));
Globals["read"] = Obj.Create(new NativeFunction("read", v =>
String.Make(IO.Console.StdIn())));
Globals["write"] = Obj.Create(new NativeFunction("write", v => {
try {
if (v[0].GetString() is string content) {
IO.Console.StdOut(content);
}
} catch { }
return Value.Void;
},
"content"));
Globals["fread"] = Obj.Create(new NativeFunction("fread", v =>
(v[0].GetString() is string path && File.Exists(Path.GetFullPath(path))) ?
String.Make(
File.ReadAllText(
Path.GetFullPath(path))) :
Value.Void,
"path"));
Globals["fwrite"] = Obj.Create(new NativeFunction("fwrite", v => {
try {
if (v[0].GetString() is string path) {
File.WriteAllText(
path,
v[1].GetString());
} else {
return Value.False;
}
} catch {
return Value.False;
}
return Value.True;
},
"path",
"content?"));
_router.Register(new ArithmeticResolver());
}
public ExecutionResult Run(Stream stream, params string[] parameters)
{
_logger.Method();
if (_stack.Position != 0) {
_logger.Warn($"Something went terribly wrong, stack cursor is at {_stack.Position} before execution.\n --- Resetting Stack ---");
_stack = new StackLike<Value>(Options.InitialStackSize);
}
DateTime start = DateTime.Now;
Benchmark.Start($"Compile Start");
// cached value, used to directly return resulting values to the calling process
_runnerReturned = Value.Void;
using Reader reader = new Reader(stream);
Compilation.Digester digester = new(reader);
Function function = digester.Digest();
Benchmark.End($"Compile End");
// compilation result taste test
if (stream.Length > 64)
File.WriteAllBytes($"./func.{DateTime.Now:yyyyMMdd_HHmmss}.sqi", function.Serialize(true));
// run the code in a new context.
// global namespace bleeds into here anyway, executing directly in global namespace is a 'safety risk'.
Context closure = new Context(function);
Push(Obj.Create(closure));
Invoke(closure, 0);
Benchmark.Start($"Execution Start");
RunnerResult result = Execute();
Benchmark.End($"Execution End");
DateTime end = DateTime.Now;
return new ExecutionResult(result,
_runnerReturned,
start,
end,
parameters);
}
private bool TestHandler(Op code, Op handler) => (code & Op.Mask_Handler) == handler;
private RunnerResult Execute()
{
#if LOG
_logger.Method();
#endif
Call call = _calls.Peek(-1);
do {
Op opCode = call.Instruction.Next();
#if LOG
_logger.Verbose($"OpCode: {opCode}");
#endif
//IO.Console.Write($"{opCode}: \n - {string.Join("\n - ", Stack.ToArray().Subset(0, Stack.Count))}");
IOperationResolver? resolver = _router.Route(this, opCode);
if (resolver != null)
{
OperationResult result = resolver.Resolve(this, opCode);
if (!result.Success)
{
_logger.Error(result.Error?.Message);
}
continue;
}
switch (opCode) {
case Op.Null:
Push(Value.Void);
break;
case Op.Constant:
Push(call.Instruction.NextConstant());
break;
case Op.Pop:
Pop();
break;
case Op.True:
Push(Value.True);
break;
case Op.False:
Push(Value.False);
break;
case Op.Addr: {
Value value = Pop();
if (!value.IsObj)
return Error($"can not get address of a value on stack");
Push(new Value(value.Ptr.Address));
break;
}
case Op.GetGlobal: {
string? name = call.Instruction.GetStringConstant(call.Instruction.NextDynamic())?.Value;
if (string.IsNullOrEmpty(name))
return Error($"tried to get global variable with empty name");
if (!Globals.Has(name))
Push(Value.Void);
else
Push(Globals[name]);
#if LOG
_logger.Verbose($"got global {name} ({Peek()})");
#endif
break;
}
case Op.SetGlobal: {
string? name = call.Instruction.GetStringConstant(call.Instruction.NextDynamic())?.Value;
if (string.IsNullOrEmpty(name))
return Error($"tried to set global variable with empty name");
if (!Globals.Has(name))
return Error($"tried to set a value of non-existing global variable");
Globals[name] = Peek();
#if LOG
_logger.Verbose($"set global {name} = {Globals[name]}");
#endif
break;
}
case Op.DefineGlobal: {
string? name = call.Instruction.GetStringConstant(call.Instruction.NextDynamic())?.Value;
if (string.IsNullOrEmpty(name))
return Error($"tried to define global variable with empty name");
Globals[name] = Pop();
#if LOG
_logger.Verbose($"defined global {name} as {Globals[name]}");
#endif
break;
}
case Op.GetLocal: {
long slot = call.Instruction.NextDynamic();
#if LOG
_logger.Verbose($"getting slot {slot} which is {call.StackPtr.Get(slot)}");
#endif
Push(call.StackPtr.Get(slot));
break;
}
case Op.SetLocal: {
long slot = call.Instruction.NextDynamic();
#if LOG
_logger.Verbose($"setting stackptr {slot} to {Peek()}");
#endif
call.StackPtr.Set(slot, Peek());
break;
}
case Op.GetOuter: {
long slot = call.Instruction.NextDynamic();
Push(call.Context.Outers[slot].Value);
break;
}
case Op.SetOuter: {
long slot = call.Instruction.NextDynamic();
call.Context.Outers[slot].Value = Peek();
break;
}
case Op.GetMember: {
Value value = Peek();
if (!value.Is(T.Instance))
return Error("Only instances have members.");
Instance instance = value.Ptr.As<Instance>()!;
string name = call.Instruction.NextString().Value!;
if (instance.Values.TryGet(name, out Value member)) {
Pop(); // remove instance from stack
Push(member);
} else if (!BindMethod(instance.Class, name)) {
return Error($"Could not bind member {name} to instance {instance} of class {instance.Class.Name}!");
}
break;
}
case Op.SetMember: {
Value value = Peek(-2);
if (!value.Is(T.Instance))
return Error("Only instances have members.");
Instance instance = value.Ptr.As<Instance>()!;
string name = call.Instruction.NextString().Value!;
instance.Values.Set(name, Peek());
Value move = Pop();
Pop(); // remove instance from stack
Push(move);
break;
}
case Op.Base: {
string name = call.Instruction.NextString().Value!;
Class @class = Pop().Ptr.As<Class>()!;
if (!BindMethod(@class, name))
return Error($"Could not bind member {name} to base class {@class.Name}");
break;
}
case Op.Array: {
int length = (int)call.Instruction.NextDynamic();
Value[] values = new Value[length];
for (int i = length - 1; i >= 0; i--) {
values[i] = Pop();
}
var array = new Values.Objects.Array(values);
Value value = Obj.Create(array);
Push(value);
break;
}
case Op.GetItem: {
Value index = Pop();
Value items = Pop();
if (!items.Is(T.ItemProvider, false))
return Error($"Can not access {index} of {items} - it is not an ItemProvider! (Array, List, etc.)");
if (items.Is(T.Array))
Push(items.Ptr.As<Values.Objects.Array>()!.Get(index));
else if (items.Is(T.List))
Push(items.Ptr.As<List>()!.Get(index));
else if (items.Is(T.Structure))
Push(items.Ptr.As<Structure>()!.Get(index));
else if (items.Is(T.String))
Push(items.Ptr.As<String>()!.SubString(index, new Value(1L)));
else
return Error($"Unsupported native accessor for type {items.Type}!");
break;
}
case Op.SetItem: {
Value value = Pop();
Value index = Pop();
Value items = Pop();
if (!items.Is(T.ItemProvider, false))
return Error($"Can not access {index} of {items} - it is not an ItemProvider! (Array, List, etc.)");
if (items.Is(T.Array))
items.Ptr.As<Values.Objects.Array>()!.Set(index, value);
else if (items.Is(T.List))
items.Ptr.As<List>()!.Set(index, value);
else if (items.Is(T.Structure))
items.Ptr.As<Structure>()!.Set(index, value);
else
return Error($"Unsupported native accessor for type {items.Type}!");
break;
}
case Op.AddItem: {
Value value = Pop();
Value items = Pop();
if (!items.Is(T.List))
return Error($"Can not add {value} to {items} - only lists :[] support the native add <+ operator.");
items.Ptr.As<List>()!.Add(value);
break;
}
case Op.Jump: {
long delta = call.Instruction.NextLong();
call.Instruction.Cursor += delta;
break;
}
case Op.JumpIfFalse: {
long delta = call.Instruction.NextLong();
if (Peek().IsFalsy)
call.Instruction.Cursor += delta;
break;
}
case Op.Loop: {
long delta = call.Instruction.NextLong();
call.Instruction.Cursor -= delta;
break;
}
case Op.Invoke: {
byte count = call.Instruction.Next();
if (!Invoke(Peek(-(count + 1)), count))
return Error($"Could not invoke function.");
call = _calls.Peek();
break;
}
case Op.InvokeMember: {
string name = call.Instruction.NextString().Value!;
byte argumentCount = call.Instruction.Next();
if (!Invoke(name, argumentCount))
return Error($"Could not invoke member {name}.");
call = _calls.Peek();
break;
}
case Op.InvokeBase: {
string name = call.Instruction.NextString().Value!;
byte argumentCount = call.Instruction.Next();
Class @class = Pop().Ptr.As<Class>()!;
if (!Invoke(@class, name, argumentCount))
return Error($"Could not invoke member {name} of base class {@class.Name}!");
call = _calls.Peek();
break;
}
case Op.Context: {
Function function = call.Instruction.NextConstant().Ptr.As<Function>()!;
Context context = new Context(function);
Push(Obj.Create(context));
for (int i = 0; i < context.Function.OuterCount; i++) {
Outer outer = context.Outers[i];
bool isLocal = call.Instruction.Next() == 1;
long index = call.Instruction.NextDynamic();
if (isLocal)
context.Outers.Add(CaptureOuter(call.StackPtr.Branch(index)));
else
context.Outers.Add(call.Context.Outers[index]);
}
break;
}
case Op.CloseOuter: {
CloseOuters(new Pointer<Value>(_stack, _cursor));
Pop();
break;
}
case Op.Return: {
Value result = Pop();
CloseOuters(call.StackPtr.Branch());
_calls.Pop();
if (_calls.Count == 0) {
if (_stack.Count > 0) {
// todo: very bad fix for our stack problem
Pop();
} else {
_logger.Warn($"Critical issue at end of execution detected - stack was empty at return.");
}
return RunnerResult.OK;
}
_stack.Decimate(call.StackPtr.Cursor);
Push(result);
call = _calls.Peek();
break;
}
case Op.Class: {
Push(Obj.Create(new Class(call.Instruction.NextString().Value!)));
break;
}
case Op.Inherit: {
Value @class = Peek(-2);
if (!@class.Is(T.Class))
return Error($"Can not inherit from {@class}, value must be of type Class.");
Class baseClass = @class.Ptr.As<Class>()!;
Class inheritingClass = Peek().Ptr.As<Class>()!;
foreach (var member in baseClass.Members) {
if (!inheritingClass.Members.Has(member.Key))
inheritingClass.Members.Add(member.Key, member.Value);
}
Pop();
break;
}
case Op.Method:
DefineMember(call.Instruction.NextString().Value!);
break;
case Op.Typeof: {
Value value = Pop();
string type;
if (value.Is(T.Class))
type = value.Ptr.As<Class>()!.Name;
else if (value.Is(T.Instance))
type = $"instance of {value.Ptr.As<Instance>()!.Class.Name}";
else if (value.Is(T.Function))
type = $"function {value.Ptr.As<Function>()!.Name}";
else if (value.Is(T.Method))
type = $"method {value.Ptr.As<Method>()!.Function.Name}";
else
type = value.Type.ToString();
Push(String.Make(type));
break;
}
case Op.Print: {
Value value = Pop();
IO.Console.Write(value);
break;
}
case Op.PrintStack: {
for (int i = 0; i < _cursor; i++) {
IO.Console.Write($"{_stack[i].ToString(true)}");
}
break;
}
case Op.PrintGlobals: {
foreach (var value in Globals) {
IO.Console.Write($"{value.Key}: {value.Value.ToString(true)}");
}
break;
}
default:
return Error($"Unexpected or not yet supported OpCode {opCode}!");
}
} while (call.Instruction.Segment.Instructions.Length > call.Instruction.Cursor);
return RunnerResult.OK;
}
private Outer CaptureOuter(Pointer<Value> target)
{
Outer? outer = null;
for (int i = Outers.Count; i-- > 0;) {
outer = Outers[i];
if (outer.Target.Cursor <= target.Cursor)
break;
}
if (outer != null && outer.Target.Cursor == target.Cursor)
return outer;
return new Outer(target);
}
private void CloseOuters(Pointer<Value> last)
{
for (int i = Outers.Count; i-- > 0;) {
Outer outer = Outers[i];
if (outer.IsClosed)
continue;
if (outer.Target.Cursor < last.Cursor)
return;
outer.Close();
}
}
private bool InvokeExtension(Value target, ExtensionMethod extension, int argumentCount)
{
#if LOG
_logger.Method(extension);
#endif
if (argumentCount != extension.Parameters.Length) {
Error($"{extension} expects {extension.Parameters.Length} arguments, received {argumentCount}");
return false;
}
var values = new Value[argumentCount];
for (int i = 0; i < argumentCount; i++) {
values[i] = Peek(-i - 1);
}
_stack.Decimate(_stack.Position - argumentCount - 1);
Value result = extension.Call(target, values);
Push(result);
return true;
}
private bool InvokeNative(NativeFunction native, int argumentCount)
{
#if LOG
_logger.Method(native);
#endif
if (argumentCount != native.Arguments.Length) {
Error($"{native} expects {native.Arguments.Length} arguments, received {argumentCount}");
return false;
}
var values = new Value[argumentCount];
for (int i = 0; i < argumentCount; i++) {
values[i] = Peek(-i - 1);
}
_stack.Decimate(_stack.Position - argumentCount - 1);
Value result = native.Target.Invoke(values);
Push(result);
return true;
}
private bool Invoke(Context closure, int argumentCount)
{
#if LOG
_logger.Method();
#endif
if (argumentCount != closure.Function.ArgumentCount) {
Error($"Expected {closure.Function.ArgumentCount} arguments but got {argumentCount}");
return false;
}
if (_calls.Count > Options.MaxCalls) {
Error($"Stack overflow {_calls.Count}");
return false;
}
Call call = new(closure, _stack, _cursor - argumentCount - 1);
_calls.Push(call);
return true;
}
private bool Invoke(string methodName, int argumentCount)
{
Value value = Peek(-argumentCount - 1);
TypeInfo typeInfo = value.TypeInfo;
if (!value.Is(T.Instance)) {
ExtensionMethod? extension = ExtensionMethod.Get(typeInfo, methodName, argumentCount);
if (extension == null) {
Error($"Could not invoke {methodName} on {value}, no method or extension by that name found for {typeInfo}.");
return false;
}
return InvokeExtension(value, extension, argumentCount);
}
Instance instance = value.Ptr.As<Instance>()!;
if (instance.Values.TryGet(methodName, out Value method))
return Invoke(method, argumentCount);
return Invoke(instance.Class, methodName, argumentCount);
}
private bool Invoke(Class @class, string methodName, int argumentCount)
{
if (@class.Members.TryGet(methodName, out Value method))
return Invoke(method, argumentCount);
Error($"Could not call method {methodName} on class {@class.Name}: Member does not exist");
return false;
}
private bool Invoke(Value value, int argumentCount)
{
if (!value.IsObj) {
Error($"Can only invoke non-primitive values! Tried to invoke {value.ToString(true)}", value);
return false;
}
if (value.Is(T.Native)) {
return InvokeNative(value.Ptr.As<NativeFunction>(), argumentCount);
} else if (value.Is(T.Method)) {
Method method = value.Ptr.As<Method>()!;
_stack.Set(_cursor - method.Function.ArgumentCount - 1, method.Receiver);
return Invoke(method, method.Function.ArgumentCount);
} else if (value.Is(T.Context)) {
Context? context = value.Ptr.As<Context>()!;
return Invoke(context, context.Function.ArgumentCount);
} else if (value.Is(T.Class)) {
Class? @class = value.Ptr.As<Class>()!;
_stack.Set(_cursor - argumentCount - 1, Instantiate(@class));
if (@class.Members.TryGet(@class.Name, out Value ctor)) {
Context? context = ctor.Ptr.As<Context>()!;
return Invoke(context, argumentCount);
} else if (argumentCount != 0) {
Error($"{@class.Name}() expects 0 arguments but received {argumentCount}");
return false;
}
} else {
return false;
}
return true;
}
private bool BindMethod(Class @class, string name)
{
if (!@class.Members.TryGet(name, out Value value)) {
Error($"Undefined member '{name}' on class {@class.Name}");
return false;
}
Method method = new Method(value.Ptr.As<Context>()!.Function, Peek());
Pop(); // remove instance from stack
Push(Obj.Create(method));
return true;
}
private Value Instantiate(Class @class)
{
Instance instance = new Instance(@class);
// todo: init fields etc here too
return Obj.Create(instance);
}
private void DefineMember(string name)
{
Value method = Peek();
Class @class = Peek(-2).Ptr.As<Class>()!;
@class.Members.Set(name, method);
Pop();
}
public Value Peek(int delta = -1)
{
#if LOG
_logger.Method();
#endif
return _stack.Peek(delta);
}
public Value Pop()
{
#if LOG
_logger.Method();
#endif
return _stack.Pop();
}
public void Push(Value value)
{
#if LOG
_logger.Method($"{value}");
#endif
_stack.Push(value);
}
private RunnerResult Error(string message, object? context = null, bool @throw = true)
{
#if LOG
_logger.Method(message);
#endif
#if LOG
_logger.Error(message, context);
#endif
Panic();
if (@throw)
throw new QampException(message, context);
return RunnerResult.Execution;
}
private void Panic()
{
#if LOG
_logger.Method();
#endif
_stack.Decimate(0);
}
public void Dispose()
{
#if LOG
_logger.Method();
#endif
}
}
public class GarbageCollector
{
}
public enum RunnerResult
{
OK = 0x0000,
Error = 0x1000,
Compilation = 0x0001,
Execution = 0x0002,
Unknown = -1
}
public readonly struct ExecutionResult(
RunnerResult code = RunnerResult.Unknown,
Value? returned = null,
DateTime? started = null,
DateTime? finished = null,
string[] parameters = null)
{
public RunnerResult RunnerResult { get; } = code;
public Value? Returned { get; } = returned;
public DateTime Started { get; } = started ?? DateTime.Now;
public DateTime Finished { get; } = finished ?? DateTime.Now;
public string[] Parameters { get; } = parameters ?? [];
public bool Success => RunnerResult == RunnerResult.OK;
public bool HasValue => Returned?.Type != T.Void;
public TimeSpan Elapsed => Finished - Started;
}
public readonly struct Options(
IO.OutputStreamMessageHandler stdOut = null,
IO.InputStreamListenHandler stdIn = null,
int maxCalls = 0x100,
int stackSize = 0x200,
int initialStackSize = 0x80)
{
public readonly IO.OutputStreamMessageHandler StdOut = stdOut;
public readonly IO.InputStreamListenHandler StdIn = stdIn;
public readonly int MaxCalls = maxCalls;
public readonly int StackSize = stackSize;
public readonly int InitialStackSize = initialStackSize;
public Options() : this(null, null, 0x100, 0x200, 0x80) { }
}