636 lines
18 KiB
C#
636 lines
18 KiB
C#
using Qrakhen.Qamp.Core.Collections;
|
|
using Qrakhen.Qamp.Core.Execution;
|
|
using Qrakhen.Qamp.Core.Values;
|
|
using T = Qrakhen.Qamp.Core.Tokenization.TokenType;
|
|
using Op = Qrakhen.Qamp.Core.Execution.OpCode;
|
|
using String = Qrakhen.Qamp.Core.Values.Objects.String;
|
|
using Qrakhen.Qamp.Core.Compilation.Builders;
|
|
using Qrakhen.Qamp.Core.Values.Objects;
|
|
using Qrakhen.Qamp.Core.Tokenization;
|
|
using Qrakhen.Qamp.Core.Logging;
|
|
|
|
namespace Qrakhen.Qamp.Core.Compilation;
|
|
|
|
public record struct Local(string Name, int Depth = -1, bool IsCaptured = false);
|
|
public record struct Outer(long Index, bool IsLocal);
|
|
public enum FunctionType { Function, Init, Method, Script }
|
|
|
|
public class Compiler
|
|
{
|
|
// could contain state like tokens, function builder, etc. and then IDigesters use that compiler state. smart, innit?
|
|
private readonly ILogger _logger = LoggerService.Get<Compiler>();
|
|
|
|
private readonly PopStack<Token> _stack;
|
|
|
|
internal Builder Builder { get; private set; }
|
|
}
|
|
|
|
public class DigesterProvider
|
|
{
|
|
public object ExpressionDigester; // etc.
|
|
}
|
|
|
|
public class Digester : ISteppable<Token>
|
|
{
|
|
private readonly Compiler _compiler;
|
|
|
|
public event Action<Token, string>? Error;
|
|
|
|
private readonly ILogger _logger = LoggerService.Get<Digester>();
|
|
private readonly IReader<Token> _reader;
|
|
|
|
internal FunctionBuilder Function => Builder.Function;
|
|
|
|
internal Token Current;
|
|
internal Token Previous;
|
|
|
|
public ClassBuilder? Class { get; private set; }
|
|
public Builder Builder { get; private set; }
|
|
public bool ThrowOnError { get; set; } = true;
|
|
|
|
public bool HadError => !ErrorPosition.IsNone;
|
|
public bool Done => _reader.Done;
|
|
|
|
public long CurrentInstruction => Function.Segment.Instructions.Count;
|
|
public TokenPosition CurrentPosition => _reader.CurrentPosition;
|
|
public TokenPosition ErrorPosition { get; private set; } = TokenPosition.None;
|
|
|
|
public Digester(IReader<Token> reader)
|
|
{
|
|
Builder = CreateBuilder(FunctionType.Script);
|
|
_reader = reader;
|
|
}
|
|
|
|
public Function Digest()
|
|
{
|
|
_logger.Method();
|
|
TokenPosition previousPosition = _reader.CurrentPosition;
|
|
Next();
|
|
while (!Done && !Match(T.Eof)) {
|
|
Token token = Current;
|
|
_logger.Verbose($"Digesting {token}");
|
|
if (previousPosition == _reader.CurrentPosition) {
|
|
ErrorAtCurrent("Loop detected / could not digest further");
|
|
break;
|
|
}
|
|
Process();
|
|
}
|
|
|
|
_logger.Debug($"Digest done, instruction set:");
|
|
_logger.Verbose(Builder.Function.Segment.Debug());
|
|
|
|
return Function.Build();
|
|
}
|
|
|
|
internal Builder CreateBuilder(FunctionType type)
|
|
{
|
|
Builder builder = new() {
|
|
Outer = Builder
|
|
};
|
|
|
|
if (type != FunctionType.Script)
|
|
builder.Function.Name = Previous.Value!;
|
|
|
|
builder.Locals.Push(new Local(type == FunctionType.Function ? "" : "this", 0));
|
|
Builder = builder;
|
|
return Builder;
|
|
}
|
|
|
|
internal Function FinishBuilder()
|
|
{
|
|
Emit(Op.Return);
|
|
Function function = Builder.Function.Build();
|
|
Builder = Builder.Outer!;
|
|
return function;
|
|
}
|
|
|
|
internal void Process()
|
|
{
|
|
_logger.Method();
|
|
if (Match(T.Class)) DeclareClass();
|
|
else if (Match(T.Function)) DeclareFunction();
|
|
else if (Match(T.Var)) DeclareVariable();
|
|
else Statement();
|
|
}
|
|
|
|
internal void Statement()
|
|
{
|
|
_logger.Method();
|
|
if (Match(T.If)) If();
|
|
else if (Match(T.Else)) Else();
|
|
else if (Match(T.While)) While();
|
|
else if (Match(T.Do)) Do();
|
|
else if (Match(T.For)) For();
|
|
else if (Match(T.Return)) Return();
|
|
else if (Match(T.TypeOf)) TypeOf();
|
|
else if (Match(T.Print)) Print();
|
|
else if (Match(T.Export)) Export();
|
|
else if (Match(T.Import)) Import();
|
|
else if (Match(T.ContextOpen)) CreateContext();
|
|
else StatementExpression();
|
|
}
|
|
|
|
internal void StatementExpression()
|
|
{
|
|
Expression();
|
|
Consume(T.Semicolon, "Expected ';' after statement");
|
|
Emit(Op.Pop); // in an expression statement, we definitely do not want to remain the value on stack,
|
|
// as it would fuck over the entire stack for the rest of the execution.
|
|
// not pushing it, e.g. with a flag to the Expression() call, would be a good todo idea.
|
|
//
|
|
// a tad bit older me: it's a terrible idea as return a = b; wouldn't work anymore
|
|
}
|
|
|
|
internal void Expression() => WeightedDigest(Weight.Assign);
|
|
|
|
internal void CreateContext()
|
|
{
|
|
_logger.Method();
|
|
BeginScope();
|
|
Block();
|
|
EndScope();
|
|
}
|
|
|
|
internal void Block()
|
|
{
|
|
_logger.Method();
|
|
while (!Check(T.ContextClose) && !Check(T.Eof))
|
|
Process();
|
|
Consume(T.ContextClose, "Expected '}' after context");
|
|
}
|
|
|
|
internal void WeightedDigest(Weight precedence)
|
|
{
|
|
_logger.Method();
|
|
Next();
|
|
Rule rule = ExpressionParser.Get(Previous.Type);
|
|
if (rule.Prefix == null) {
|
|
ErrorAtCurrent("Expected expression");
|
|
return;
|
|
}
|
|
|
|
bool canAssign = precedence <= Weight.Assign;
|
|
rule.Prefix(this, canAssign);
|
|
|
|
while (precedence <= ExpressionParser.Get(Current.Type).Weight) {
|
|
Next();
|
|
ExpressionParser.Get(Previous.Type).Infix?.Invoke(this, canAssign);
|
|
}
|
|
|
|
if (canAssign && Match(T.Equal)) {
|
|
ErrorAtCurrent("Invalid assignment target");
|
|
}
|
|
}
|
|
|
|
internal void DeclareClass()
|
|
{
|
|
_logger.Method();
|
|
}
|
|
|
|
internal void DeclareFunction()
|
|
{
|
|
long global = ParseVariable();
|
|
InitializeLocal();
|
|
CreateFunction(FunctionType.Function);
|
|
DefineVariable(global);
|
|
}
|
|
|
|
internal void DeclareVariable()
|
|
{
|
|
_logger.Method();
|
|
long global = ParseVariable();
|
|
if (Match(T.Equal))
|
|
Expression();
|
|
else
|
|
Emit(Op.Null);
|
|
Consume(T.Semicolon, "missing ; after variable declaration");
|
|
DefineVariable(global);
|
|
}
|
|
|
|
internal void Export()
|
|
{
|
|
_logger.Method();
|
|
}
|
|
|
|
internal void Import()
|
|
{
|
|
_logger.Method();
|
|
}
|
|
|
|
internal void If()
|
|
{
|
|
_logger.Method();
|
|
Consume(T.GroupOpen, "Expected '(' after if");
|
|
Expression();
|
|
Consume(T.GroupClose, "Expected ')' after condition");
|
|
|
|
long then = EmitJump(Op.JumpIfFalse);
|
|
Emit(Op.Pop);
|
|
Statement();
|
|
|
|
long @else = EmitJump(Op.Jump);
|
|
|
|
PatchJump(then);
|
|
Emit(Op.Pop);
|
|
|
|
if (Match(T.Else))
|
|
Statement();
|
|
|
|
PatchJump(@else);
|
|
}
|
|
|
|
internal void Else()
|
|
{
|
|
_logger.Method();
|
|
ErrorAtPrevious("Unexpected solitary 'else'");
|
|
}
|
|
|
|
internal void While()
|
|
{
|
|
_logger.Method();
|
|
}
|
|
|
|
internal void Do()
|
|
{
|
|
_logger.Method();
|
|
}
|
|
|
|
internal void For()
|
|
{
|
|
_logger.Method();
|
|
BeginScope();
|
|
Consume(T.GroupOpen, "Expected '(' after for statement");
|
|
if (Match(T.Semicolon)) {
|
|
// nothing
|
|
} else if (Match(T.Var)) {
|
|
DeclareVariable();
|
|
} else {
|
|
StatementExpression();
|
|
}
|
|
|
|
long start = Function.Segment.Instructions.Count;
|
|
long exit = -1;
|
|
if (!Match(T.Semicolon)) {
|
|
Expression();
|
|
Consume(T.Semicolon, "Expected ';' after condition");
|
|
exit = EmitJump(Op.JumpIfFalse);
|
|
Emit(Op.Pop);
|
|
}
|
|
|
|
if (!Match(T.GroupClose)) {
|
|
long body = EmitJump(Op.Jump);
|
|
long increment = Function.Segment.Instructions.Count;
|
|
Expression();
|
|
Emit(Op.Pop);
|
|
Consume(T.GroupClose, "Expected ')' after for clause");
|
|
|
|
EmitLoop(start);
|
|
start = increment;
|
|
PatchJump(body);
|
|
}
|
|
|
|
Statement();
|
|
EmitLoop(start);
|
|
if (exit > -1) {
|
|
PatchJump(exit);
|
|
Emit(Op.Pop);
|
|
}
|
|
|
|
EndScope();
|
|
}
|
|
|
|
internal void Return()
|
|
{
|
|
_logger.Method();
|
|
Emit(Op.Return);
|
|
}
|
|
|
|
internal void Print()
|
|
{
|
|
_logger.Method();
|
|
Expression();
|
|
Consume(T.Semicolon, "Expected ';' after print call.");
|
|
Emit(Op.Print);
|
|
}
|
|
|
|
internal void TypeOf()
|
|
{
|
|
_logger.Method();
|
|
Expression();
|
|
Emit(Op.Typeof);
|
|
}
|
|
|
|
internal void BeginScope()
|
|
{
|
|
_logger.Method();
|
|
Builder.ScopeDepth++;
|
|
}
|
|
|
|
internal void EndScope()
|
|
{
|
|
_logger.Method();
|
|
Builder.ScopeDepth--;
|
|
while (Builder.Locals.Count > 0 &&
|
|
Builder.Locals.Peek().Depth > Builder.ScopeDepth) {
|
|
if (Builder.Locals.Peek().IsCaptured) {
|
|
Emit(OpCode.CloseUpvalue);
|
|
} else {
|
|
Emit(OpCode.Pop);
|
|
}
|
|
Builder.Locals.Pop();
|
|
}
|
|
}
|
|
|
|
internal void CreateFunction(FunctionType type)
|
|
{
|
|
_logger.Method();
|
|
Builder next = CreateBuilder(type);
|
|
BeginScope();
|
|
Consume(T.GroupOpen, "Expected '(' to start argument list.");
|
|
if (!Check(T.GroupClose)) {
|
|
do {
|
|
Builder.Function.ArgumentCount++;
|
|
if (Builder.Function.ArgumentCount > 0xFF)
|
|
ErrorAtCurrent($"In the name of the lord, how many arguments do you need?");
|
|
DefineVariable(ParseVariable());
|
|
} while (Match(T.Comma));
|
|
}
|
|
|
|
Consume(T.GroupClose, "Expected ')' after argument list.");
|
|
Consume(T.ContextOpen, "Expected function body");
|
|
Block();
|
|
Function function = FinishBuilder();
|
|
Emit(Op.Closure);
|
|
Emit(MakeConstant(Obj.Create(function)));
|
|
for (int i = 0; i < function.OuterCount; i++) {
|
|
Outer outer = next.Outers.Get(i);
|
|
Emit(outer.IsLocal ? 1 : 0);
|
|
Emit(outer.Index);
|
|
}
|
|
}
|
|
|
|
internal long ResolveLocal(Builder builder, string name)
|
|
{
|
|
for (int i = 0; i < builder.Locals.Count; i++) {
|
|
if (builder.Locals[i].Name == name)
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
internal void AddLocal(string? name)
|
|
{
|
|
_logger.Method(name);
|
|
Builder.Locals.Push(new Local(ThrowIfEmpty(name), -1));
|
|
}
|
|
|
|
internal void DeclareLocal(string? name)
|
|
{
|
|
if (Builder.ScopeDepth == 0)
|
|
return;
|
|
|
|
_logger.Method(name);
|
|
for (int i = 0; i < Builder.Locals.Count; i++) {
|
|
Local local = Builder.Locals[i];
|
|
// skip if the most recent local isn't even in our scope
|
|
if (local.Depth != -1 && local.Depth < Builder.ScopeDepth)
|
|
break;
|
|
|
|
if (ThrowIfEmpty(name).Equals(local.Name))
|
|
ErrorAtCurrent($"A variable by the name {name} already exists in the current scope.");
|
|
}
|
|
AddLocal(name);
|
|
}
|
|
|
|
internal long AddOuter(Builder builder, long index, bool isLocal)
|
|
{
|
|
for (int i = 0; i < builder.Function.OuterCount; i++) {
|
|
Outer outer = builder.Outers[i];
|
|
if (outer.Index == index && outer.IsLocal == isLocal)
|
|
return i;
|
|
}
|
|
|
|
builder.Outers.Push(new Outer(index, isLocal));
|
|
return builder.Outers.Count - 1;
|
|
}
|
|
|
|
internal long ResolveOuter(Builder builder, string name)
|
|
{
|
|
if (builder.Outer == null)
|
|
return -1;
|
|
|
|
long local = ResolveLocal(builder.Outer, name);
|
|
if (local > -1) {
|
|
Local element = builder.Outer.Locals.Get(local);
|
|
element.IsCaptured = true;
|
|
builder.Outer.Locals.Set(local, element);
|
|
return AddOuter(builder, local, true);
|
|
}
|
|
|
|
long outer = ResolveOuter(builder.Outer, name);
|
|
if (outer > -1)
|
|
return AddOuter(builder, outer, false);
|
|
|
|
return -1;
|
|
}
|
|
|
|
internal long IdentifierConstant(string? name)
|
|
{
|
|
return MakeConstant(String.Make(name ??
|
|
throw new TokenException("Empty string value for identifier detected", _reader, Current)));
|
|
}
|
|
|
|
internal long MakeConstant(Value value)
|
|
{
|
|
_logger.Method(value.ToString());
|
|
long constant = Function.Segment.Constants.Add(value);
|
|
_logger.Verbose($"Registered constant {value} at index {constant}");
|
|
return constant;
|
|
}
|
|
|
|
internal long ParseVariable()
|
|
{
|
|
_logger.Method();
|
|
string? name = Consume(T.Identifier, "Missing identifier for variable").Value;
|
|
if (Builder.ScopeDepth == 0)
|
|
return IdentifierConstant(name);
|
|
DeclareLocal(name);
|
|
return 0;
|
|
}
|
|
|
|
internal void InitializeLocal()
|
|
{
|
|
if (Builder.ScopeDepth == 0)
|
|
return;
|
|
|
|
_logger.Method();
|
|
var local = Builder.Locals.Peek();
|
|
local.Depth = Builder.ScopeDepth;
|
|
Builder.Locals.Set(Builder.Locals.Position - 1, local);
|
|
}
|
|
|
|
internal void DefineVariable(long index)
|
|
{
|
|
_logger.Method(index.ToString());
|
|
if (Builder.ScopeDepth > 0) {
|
|
InitializeLocal();
|
|
} else {
|
|
Emit(Op.DefineGlobal);
|
|
Emit(index);
|
|
}
|
|
}
|
|
|
|
internal byte ArgumentList()
|
|
{
|
|
byte count = 0;
|
|
if (!Check(T.GroupClose)) {
|
|
do {
|
|
Expression();
|
|
if (count++ >= 0xFF)
|
|
ErrorAtCurrent("How many arguments do you need?");
|
|
} while (Match(T.Comma));
|
|
}
|
|
|
|
Consume(T.GroupClose, "Expected ')' after argument list");
|
|
return count;
|
|
}
|
|
|
|
internal void EmitConstant(Value constant)
|
|
{
|
|
Emit(Op.Constant);
|
|
Emit(MakeConstant(constant));
|
|
}
|
|
|
|
internal long EmitJump(Instruction instruction)
|
|
{
|
|
Emit(instruction);
|
|
Emit(0L);
|
|
return CurrentInstruction - sizeof(long);
|
|
}
|
|
|
|
internal void PatchJump(long offset)
|
|
{
|
|
long target = CurrentInstruction - (offset - sizeof(long));
|
|
Patch(target.GetBytes(), offset);
|
|
}
|
|
|
|
internal void EmitLoop(long start)
|
|
{
|
|
Emit(Op.Loop);
|
|
long offset = Function.Segment.Instructions.Count - start + sizeof(long);
|
|
Emit(offset);
|
|
}
|
|
|
|
internal void EmitDynamic(byte[] data)
|
|
{
|
|
Emit(data.Length | 0x80);
|
|
Emit(data);
|
|
}
|
|
|
|
internal void Emit(params byte[] data)
|
|
{
|
|
if (data.Length == 1)
|
|
_logger.Verbose($"Emitting {new Instruction(data[0])}");
|
|
else if (data.Length > 1)
|
|
_logger.Verbose($"Emitting {string.Join(' ', data)}");
|
|
foreach (var value in data)
|
|
Function.Segment.Instructions.Add(value);
|
|
}
|
|
|
|
internal void Emit(Instruction instruction)
|
|
{
|
|
Emit(instruction.Code);
|
|
}
|
|
|
|
internal void Emit(Instruction instruction, byte[] data)
|
|
{
|
|
Emit(instruction);
|
|
Emit(data);
|
|
}
|
|
|
|
internal void Emit(IEnumerable<Instruction> instructions)
|
|
{
|
|
foreach (var i in instructions)
|
|
Emit(i);
|
|
}
|
|
|
|
internal void Patch(byte[] bytes, long position)
|
|
{
|
|
for (int i = 0; i < bytes.Length; i++) {
|
|
Function.Segment.Instructions[position + i] = bytes[i];
|
|
}
|
|
}
|
|
|
|
internal void Emit(params Instruction[] instructions)
|
|
=> Emit(instructions.AsEnumerable());
|
|
|
|
internal void Emit(short value)
|
|
=> Emit(BitConverter.GetBytes(value));
|
|
|
|
internal void Emit(int value)
|
|
=> Emit(BitConverter.GetBytes(value));
|
|
|
|
internal void Emit(long value)
|
|
=> Emit(BitConverter.GetBytes(value));
|
|
|
|
internal void Emit(ulong value)
|
|
=> Emit(BitConverter.GetBytes(value));
|
|
|
|
internal void Emit(double value)
|
|
=> Emit(BitConverter.GetBytes(value));
|
|
|
|
public Token Next()
|
|
{
|
|
Previous = Current;
|
|
return Current = _reader.NextToken();
|
|
}
|
|
|
|
internal Token Consume(T expected, string errorMessage, bool acceptEof = true)
|
|
{
|
|
if ((acceptEof && Current.Type == T.Eof) || Current.Type == expected) {
|
|
Next();
|
|
return Previous;
|
|
}
|
|
ErrorAtCurrent(errorMessage);
|
|
return Token.Error(errorMessage);
|
|
}
|
|
|
|
internal bool Match(T type)
|
|
{
|
|
if (!Check(type))
|
|
return false;
|
|
Next();
|
|
return true;
|
|
}
|
|
|
|
internal bool Check(T type)
|
|
=> Current.Type == type;
|
|
|
|
internal void ErrorAt(Token token, string errorMessage)
|
|
=> ReportError(token, errorMessage);
|
|
|
|
internal void ErrorAtCurrent(string errorMessage)
|
|
=> ReportError(Current, errorMessage);
|
|
|
|
internal void ErrorAtPrevious(string errorMessage)
|
|
=> ReportError(Previous, errorMessage);
|
|
|
|
private void ReportError(Token token, string errorMessage)
|
|
{
|
|
_logger.Method();
|
|
ErrorPosition = token.Position;
|
|
_logger.Error($"At token index {token.Position}: {errorMessage}");
|
|
if (ThrowOnError)
|
|
throw new TokenException($"At token index {token.Position}: {errorMessage}", _reader, token);
|
|
Error?.Invoke(token, errorMessage);
|
|
}
|
|
|
|
private string ThrowIfEmpty(string? value, string? message = $"Unexpected empty string detected")
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
throw new TokenException("Empty string value for identifier detected", _reader, Current);
|
|
return value;
|
|
}
|
|
}
|