From da653b3ce520f64b52e758da81c5028e4912f00c Mon Sep 17 00:00:00 2001 From: Qrakhen Date: Thu, 6 Nov 2025 14:36:38 +0100 Subject: [PATCH] initial commit --- Qrakhen.Qamp.CLI/Program.cs | 308 +++++++++ Qrakhen.Qamp.CLI/Qrakhen.Qamp.CLI.csproj | 14 + Qrakhen.Qamp.Core/Assert.cs | 55 ++ Qrakhen.Qamp.Core/Benchmark.cs | 89 +++ Qrakhen.Qamp.Core/Collections/Expander.cs | 61 ++ Qrakhen.Qamp.Core/Collections/FixedArray.cs | 36 + Qrakhen.Qamp.Core/Collections/PopStack.cs | 36 + Qrakhen.Qamp.Core/Collections/Register.cs | 25 + Qrakhen.Qamp.Core/Collections/StackLike.cs | 42 ++ .../Collections/StackRegister.cs | 49 ++ .../Compilation/Builders/Builder.cs | 13 + .../Compilation/Builders/ClassBuilder.cs | 7 + .../Compilation/Builders/FunctionBuilder.cs | 16 + .../Compilation/Builders/SegmentBuilder.cs | 39 ++ Qrakhen.Qamp.Core/Compilation/Digester.cs | 635 ++++++++++++++++++ .../Compilation/Digesters/IDigester.cs | 12 + .../Compilation/ExpressionParser.cs | 283 ++++++++ Qrakhen.Qamp.Core/Compilation/Rule.cs | 10 + Qrakhen.Qamp.Core/Compilation/Weight.cs | 16 + Qrakhen.Qamp.Core/Console.cs | 12 + Qrakhen.Qamp.Core/Exceptions.cs | 36 + Qrakhen.Qamp.Core/Execution/Instruction.cs | 16 + Qrakhen.Qamp.Core/Execution/Instructions.cs | 34 + Qrakhen.Qamp.Core/Execution/OpCode.cs | 63 ++ Qrakhen.Qamp.Core/Execution/Runner.cs | 387 +++++++++++ Qrakhen.Qamp.Core/Execution/Segment.cs | 73 ++ Qrakhen.Qamp.Core/Extensions.cs | 67 ++ Qrakhen.Qamp.Core/IAdd.cs | 8 + Qrakhen.Qamp.Core/ICapturableBuffer.cs | 8 + Qrakhen.Qamp.Core/IConsumable.cs | 6 + Qrakhen.Qamp.Core/IDebug.cs | 29 + Qrakhen.Qamp.Core/IGet.cs | 6 + Qrakhen.Qamp.Core/IGetSet.cs | 3 + Qrakhen.Qamp.Core/IPeekable.cs | 6 + Qrakhen.Qamp.Core/IReadable.cs | 7 + Qrakhen.Qamp.Core/IReader.cs | 10 + Qrakhen.Qamp.Core/ISeekable.cs | 7 + Qrakhen.Qamp.Core/ISerialize.cs | 7 + Qrakhen.Qamp.Core/ISet.cs | 8 + Qrakhen.Qamp.Core/IStack.cs | 13 + Qrakhen.Qamp.Core/ISteppable.cs | 7 + Qrakhen.Qamp.Core/IToArray.cs | 6 + Qrakhen.Qamp.Core/Logging/ILogger.cs | 160 +++++ Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj | 9 + .../Qrakhen.Qamp.Core.csproj.Backup.tmp | 18 + Qrakhen.Qamp.Core/Tokenization/Dialect.cs | 118 ++++ Qrakhen.Qamp.Core/Tokenization/Reader.cs | 333 +++++++++ Qrakhen.Qamp.Core/Tokenization/Token.cs | 42 ++ Qrakhen.Qamp.Core/Tokenization/TokenType.cs | 126 ++++ Qrakhen.Qamp.Core/Tokenization/Tokens.cs | 20 + Qrakhen.Qamp.Core/Values/Address.cs | 12 + Qrakhen.Qamp.Core/Values/Objects/Array.cs | 8 + Qrakhen.Qamp.Core/Values/Objects/Closure.cs | 12 + Qrakhen.Qamp.Core/Values/Objects/Function.cs | 13 + Qrakhen.Qamp.Core/Values/Objects/Obj.cs | 15 + Qrakhen.Qamp.Core/Values/Objects/String.cs | 13 + .../Values/Objects/TypeWrapper.cs | 6 + Qrakhen.Qamp.Core/Values/Ptr.cs | 35 + Qrakhen.Qamp.Core/Values/Ref.cs | 11 + Qrakhen.Qamp.Core/Values/Value.cs | 124 ++++ Qrakhen.Qamp.Core/Values/ValueExtensions.cs | 11 + Qrakhen.Qamp.Core/Values/ValueOperation.cs | 121 ++++ Qrakhen.Qamp.Core/Values/ValueType.cs | 35 + Qrakhen.Qamp.sln | 31 + Qrakhen.Qamp.sln.DotSettings | 35 + 65 files changed, 3873 insertions(+) create mode 100644 Qrakhen.Qamp.CLI/Program.cs create mode 100644 Qrakhen.Qamp.CLI/Qrakhen.Qamp.CLI.csproj create mode 100644 Qrakhen.Qamp.Core/Assert.cs create mode 100644 Qrakhen.Qamp.Core/Benchmark.cs create mode 100644 Qrakhen.Qamp.Core/Collections/Expander.cs create mode 100644 Qrakhen.Qamp.Core/Collections/FixedArray.cs create mode 100644 Qrakhen.Qamp.Core/Collections/PopStack.cs create mode 100644 Qrakhen.Qamp.Core/Collections/Register.cs create mode 100644 Qrakhen.Qamp.Core/Collections/StackLike.cs create mode 100644 Qrakhen.Qamp.Core/Collections/StackRegister.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Builders/Builder.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Builders/ClassBuilder.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Builders/FunctionBuilder.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Builders/SegmentBuilder.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Digester.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Digesters/IDigester.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/ExpressionParser.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Rule.cs create mode 100644 Qrakhen.Qamp.Core/Compilation/Weight.cs create mode 100644 Qrakhen.Qamp.Core/Console.cs create mode 100644 Qrakhen.Qamp.Core/Exceptions.cs create mode 100644 Qrakhen.Qamp.Core/Execution/Instruction.cs create mode 100644 Qrakhen.Qamp.Core/Execution/Instructions.cs create mode 100644 Qrakhen.Qamp.Core/Execution/OpCode.cs create mode 100644 Qrakhen.Qamp.Core/Execution/Runner.cs create mode 100644 Qrakhen.Qamp.Core/Execution/Segment.cs create mode 100644 Qrakhen.Qamp.Core/Extensions.cs create mode 100644 Qrakhen.Qamp.Core/IAdd.cs create mode 100644 Qrakhen.Qamp.Core/ICapturableBuffer.cs create mode 100644 Qrakhen.Qamp.Core/IConsumable.cs create mode 100644 Qrakhen.Qamp.Core/IDebug.cs create mode 100644 Qrakhen.Qamp.Core/IGet.cs create mode 100644 Qrakhen.Qamp.Core/IGetSet.cs create mode 100644 Qrakhen.Qamp.Core/IPeekable.cs create mode 100644 Qrakhen.Qamp.Core/IReadable.cs create mode 100644 Qrakhen.Qamp.Core/IReader.cs create mode 100644 Qrakhen.Qamp.Core/ISeekable.cs create mode 100644 Qrakhen.Qamp.Core/ISerialize.cs create mode 100644 Qrakhen.Qamp.Core/ISet.cs create mode 100644 Qrakhen.Qamp.Core/IStack.cs create mode 100644 Qrakhen.Qamp.Core/ISteppable.cs create mode 100644 Qrakhen.Qamp.Core/IToArray.cs create mode 100644 Qrakhen.Qamp.Core/Logging/ILogger.cs create mode 100644 Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj create mode 100644 Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj.Backup.tmp create mode 100644 Qrakhen.Qamp.Core/Tokenization/Dialect.cs create mode 100644 Qrakhen.Qamp.Core/Tokenization/Reader.cs create mode 100644 Qrakhen.Qamp.Core/Tokenization/Token.cs create mode 100644 Qrakhen.Qamp.Core/Tokenization/TokenType.cs create mode 100644 Qrakhen.Qamp.Core/Tokenization/Tokens.cs create mode 100644 Qrakhen.Qamp.Core/Values/Address.cs create mode 100644 Qrakhen.Qamp.Core/Values/Objects/Array.cs create mode 100644 Qrakhen.Qamp.Core/Values/Objects/Closure.cs create mode 100644 Qrakhen.Qamp.Core/Values/Objects/Function.cs create mode 100644 Qrakhen.Qamp.Core/Values/Objects/Obj.cs create mode 100644 Qrakhen.Qamp.Core/Values/Objects/String.cs create mode 100644 Qrakhen.Qamp.Core/Values/Objects/TypeWrapper.cs create mode 100644 Qrakhen.Qamp.Core/Values/Ptr.cs create mode 100644 Qrakhen.Qamp.Core/Values/Ref.cs create mode 100644 Qrakhen.Qamp.Core/Values/Value.cs create mode 100644 Qrakhen.Qamp.Core/Values/ValueExtensions.cs create mode 100644 Qrakhen.Qamp.Core/Values/ValueOperation.cs create mode 100644 Qrakhen.Qamp.Core/Values/ValueType.cs create mode 100644 Qrakhen.Qamp.sln create mode 100644 Qrakhen.Qamp.sln.DotSettings diff --git a/Qrakhen.Qamp.CLI/Program.cs b/Qrakhen.Qamp.CLI/Program.cs new file mode 100644 index 0000000..12a7b63 --- /dev/null +++ b/Qrakhen.Qamp.CLI/Program.cs @@ -0,0 +1,308 @@ + +using Qrakhen.Qamp.Core; +using Qrakhen.Qamp.Core.Execution; +using Qrakhen.Qamp.Core.Tokenization; +using System.Text; + +char[] Ignored = [ '\0', '\b' ]; +Stack History = []; +ConsoleKeyInfo input; +bool useSyntaxHighlighting = false; +(int x, int y) cursor = (0, 0); +ConsoleCode code = ConsoleCode.Error; +Runner runner = new Runner(new Options()); +ConsoleInterface cli = new ConsoleInterface(Encoding.Default); +do { + /*Console.ForegroundColor = ConsoleColor.White; + MemoryStream stream = new MemoryStream(); + Console.Write(" <: "); + cursor = Console.GetCursorPosition(); + do { + if (useSyntaxHighlighting) + new ConsoleRenderer(stream).Render(cursor); + input = Console.ReadKey(true); + + if (input.Modifiers == ConsoleModifiers.Control) { + if (input.Key == ConsoleKey.H) { + useSyntaxHighlighting = !useSyntaxHighlighting; + Console.Write($"\n #: {nameof(useSyntaxHighlighting)} = {useSyntaxHighlighting}\n <: "); + cursor = Console.GetCursorPosition(); + } + continue; + } + + if (input.Key == ConsoleKey.LeftArrow) { + if (stream.Position > 0) { + stream.Position--; + Console.CursorLeft--; + } + continue; + } + + if (input.Key == ConsoleKey.RightArrow) { + if (stream.Position < stream.Length) { + stream.Position++; + Console.CursorLeft++; + } + continue; + } + + if (input.Key == ConsoleKey.Backspace && stream.Position > 0) { + Console.CursorLeft -= 1; + Console.Write(' '); + if (!useSyntaxHighlighting) + Console.CursorLeft -= 1; + //stream.Position--; + //stream.SetLength(stream.Position); + stream.Remove(stream.Position - 1, 1); + } else if (input.Key == ConsoleKey.Enter && input.Modifiers == ConsoleModifiers.Shift) { + if (!useSyntaxHighlighting) + Console.Write("\n : "); + stream.Insert(Encoding.Default.GetBytes(Environment.NewLine)); + } else if (Ignored.IndexOf(input.KeyChar) < 0) { + if (!useSyntaxHighlighting) + Console.Write(input.KeyChar); + stream.Insert(Encoding.Default.GetBytes([input.KeyChar], 0, 1)); + } + } while (input.Key != ConsoleKey.Enter || input.Modifiers == ConsoleModifiers.Shift);*/ + try { + code = cli.Read(out Stream? stream); + if (code == ConsoleCode.OK) { + if (stream == null) + throw new QampException($"Stream from CLI was null {code}"); + Console.WriteLine(); + runner.Run(stream); + } + if (code == ConsoleCode.Error) + throw new QampException($"CLI returned {code}"); + } + catch (QampException e) { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($" !> {e.Message}"); + if (e.StackTrace != null) { + Console.Write($" ! "); + Console.WriteLine(string.Join("\n ! ", e.StackTrace.Split('\n'))); + } + } + catch (Exception e) { + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine($" !? {e.Message}"); + if (e.StackTrace != null) { + Console.Write($" ! "); + Console.WriteLine(string.Join("\n ! ", e.StackTrace.Split('\n'))); + } + } +} while(code != ConsoleCode.Exit); + +class ConsoleInterface +{ + private const string SYMBOL_INPUT = " <: "; + private const string SYMBOL_INPUT_CONTINUE = " : "; + + private readonly List _buffer = []; + private readonly Stack _history = []; + private readonly Encoding _encoding; + + private int _position; + private (int x, int y) _origin; + private (int x, int y) _cursorPosition => Console.GetCursorPosition(); + + private int _cursorOffset => SYMBOL_INPUT.Length; + private int _cursorLeft { get => Console.CursorLeft; set => Console.CursorLeft = value; } + private int _cursorTop { get => Console.CursorTop; set => Console.CursorTop = value; } + + public bool UseSyntaxHighlighting { get; set; } = false; + + public ConsoleInterface(Encoding encoding) + { + _encoding = encoding; + } + + private void Write(char c) => Console.Write(c); + private void Write(string str) => Console.Write(str); + private void SetCursor((int x, int y) position) => Console.SetCursorPosition(position.x, position.y); + + private void SetColor(ConsoleColor color, ConsoleColor background = ConsoleColor.Black) + { + Console.ForegroundColor = color; + Console.BackgroundColor = background; + } + + private void Move(int x, int y = 0) + { + _position = Math.Min(_buffer.Count, Math.Max(0, _position + x)); + _cursorLeft -= 1; //???? + } + + private void ClearLine() + { + int left = _cursorLeft; + while (_cursorLeft < Console.BufferWidth - 1) + Write(' '); + _cursorLeft = left; + } + + private void Print((int x, int y) origin, (int x, int y)? remain = null) + { + SetColor(ConsoleColor.White); + SetCursor(origin); + Write(" <: "); + foreach (var c in _buffer) { + if (c == '\n') { + ClearLine(); + Write("\n : "); + } else { + Write(c); + } + } + ClearLine(); + if (remain.HasValue) + SetCursor(remain.Value); + } + + private Stream Dispatch() + { + var stream = new MemoryStream(); + var data = _buffer.ToArray(); + stream.Write(_encoding.GetBytes(data)); + _history.Push(data); + _buffer.Clear(); + return stream; + } + + public ConsoleCode Read(out Stream? stream) + { + stream = null; + ConsoleKeyInfo input; + _origin = _cursorPosition; + _position = 0; + Print(_origin); + do { + if (UseSyntaxHighlighting) + new ConsoleRenderer(new MemoryStream(_encoding.GetBytes(_buffer.ToArray()))).Render(_origin); // ~ so efficient ~ XD + Print(_origin, _cursorPosition); + input = Console.ReadKey(true); + + if (input.Modifiers == ConsoleModifiers.Control) { + if (input.Key == ConsoleKey.C) + return ConsoleCode.Exit; + + if (input.Key == ConsoleKey.H) { + UseSyntaxHighlighting = !UseSyntaxHighlighting; + Write($"\n #: {nameof(UseSyntaxHighlighting)} = {UseSyntaxHighlighting}\n <: "); + _origin = _cursorPosition; + } + if (input.Key == ConsoleKey.LeftArrow) { + + } + continue; + } + + if (input.Key == ConsoleKey.LeftArrow) { + if (_position > 0) { + _position--; + _cursorLeft--; + } + continue; + } + + if (input.Key == ConsoleKey.RightArrow) { + if (_position < _buffer.Count) { + _position++; + _cursorLeft++; + } + continue; + } + + if (input.Key == ConsoleKey.Backspace) { + if (_position > 0) { + _cursorLeft--; + _buffer.RemoveRange(--_position, 1); + } + continue; + } else if (input.Check(ConsoleKey.Enter, ConsoleModifiers.Shift)) { + _buffer.Insert(_position++, '\n'); + _cursorLeft = _cursorOffset; + _cursorTop++; + continue; + } else { + _buffer.Insert(_position++, input.KeyChar); + _cursorLeft++; + } + } while (!input.Check(ConsoleKey.Enter, ConsoleModifiers.None, true)); + stream = Dispatch(); + return ConsoleCode.OK; + } +} + +enum ConsoleCode +{ + Error = -1, + OK = 0, + Exit = 1 +} + +class ConsoleRenderer +{ + private readonly IReader _reader; + private readonly Stream _stream; + + public ConsoleRenderer(Stream stream) + { + _stream = new MemoryStream(); + stream.Position = 0; + stream.CopyTo(_stream); + _reader = new Reader(_stream); + } + + public void Render((int x, int y) position) + { + Console.CursorVisible = false; + Console.SetCursorPosition(position.x, position.y); + List tokens = []; + while (!_reader.Done) { + tokens.Add(_reader.NextToken(true)); + } + _stream.Position = 0; + foreach (var token in tokens) { + ConsoleColor color = ConsoleColor.Gray; + if (token.Type.IsBoolean()) + color = ConsoleColor.Blue; + if (token.Type.IsString()) + color = ConsoleColor.Red; + if (token.Type.IsNumber()) + color = ConsoleColor.DarkCyan; + if (token.Type.IsOperator()) + color = ConsoleColor.DarkGreen; + if (token.Type.IsBracket()) + color = ConsoleColor.Yellow; + if (token.Type.IsIdentifier()) + color = ConsoleColor.White; + if (token.Type.IsControl()) + color = ConsoleColor.DarkYellow; + if (token.Type == TokenType.Error) + color = ConsoleColor.DarkRed; + Console.ForegroundColor = color; + byte[] buffer = new byte[token.Span.Length]; + _stream.ReadExactly(buffer, 0, (int)token.Span.Length); + string str = Encoding.Default.GetString(buffer); + Console.Write(str); + Console.ForegroundColor = ConsoleColor.White; + if (token.Type == TokenType.NewLine) + Console.Write(" : "); + } + Console.ForegroundColor = ConsoleColor.White; + Console.CursorVisible = true; + } +} + +public static class ConsoleExtensions +{ + public static bool Check(this ConsoleKeyInfo keyInfo, ConsoleKey key, ConsoleModifiers modifier = default, bool exactModifier = false) + { + return ((exactModifier ? + keyInfo.Modifiers == modifier : + (keyInfo.Modifiers & modifier) == modifier) + && keyInfo.Key == key); + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.CLI/Qrakhen.Qamp.CLI.csproj b/Qrakhen.Qamp.CLI/Qrakhen.Qamp.CLI.csproj new file mode 100644 index 0000000..48b6eb8 --- /dev/null +++ b/Qrakhen.Qamp.CLI/Qrakhen.Qamp.CLI.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/Qrakhen.Qamp.Core/Assert.cs b/Qrakhen.Qamp.Core/Assert.cs new file mode 100644 index 0000000..16bbe72 --- /dev/null +++ b/Qrakhen.Qamp.Core/Assert.cs @@ -0,0 +1,55 @@ +using Qrakhen.Qamp.Core.Values; +using System.Runtime.CompilerServices; + +using V = Qrakhen.Qamp.Core.Values.ValueType; + +namespace Qrakhen.Qamp.Core; + +public static class Assert +{ + public static void IsPrimitive(params Value[] values) + { + AssertAndThrow(() => values.All(value => value.Is(V.Primitive, false)), values); + } + + public static void NotZero(params Value[] values) + { + AssertAndThrow(() => values.All(value => value.Signed != 0), values); + } + + public static void IsInteger(params Value[] values) + { + AssertAndThrow(() => values.All(value => value.Is(V.Integer, false)), values); + } + + public static void NotVoid(params Value[] values) + { + AssertAndThrow(() => values.All(value => value.Type != V.Void), values); + } + + private static void AssertAndThrow(Func assertion, object? parameter, [CallerMemberName] string? method = "") => + AssertAndThrow(assertion, [parameter], method); + + private static void AssertAndThrow( + Func assertion, + object?[] parameters, + [CallerMemberName] string? method = "") + { + if (!assertion.Invoke()) + throw new AssertionException(method ?? "", parameters); + } + + public class AssertionException : Exception + { + public readonly string Assertion; + public readonly object?[] Parameters; + + public AssertionException(string assertion, params object?[] parameters) + : base($"Assertion {assertion} failed for parameters:{Environment.NewLine}" + + $"{string.Join(Environment.NewLine, parameters.Select(p => $" - <{p?.GetType().Name ?? "null"}>{p?.ToString() ?? "null"}"))}") + { + Assertion = assertion; + Parameters = parameters; + } + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Benchmark.cs b/Qrakhen.Qamp.Core/Benchmark.cs new file mode 100644 index 0000000..e37f057 --- /dev/null +++ b/Qrakhen.Qamp.Core/Benchmark.cs @@ -0,0 +1,89 @@ +using Qrakhen.Qamp.Core.Logging; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Qrakhen.Qamp.Core; + +public static class Benchmark +{ + private static readonly Dictionary _clocks = new(); + + public static bool Active { get; set; } + + private static readonly ILogger _logger = LoggerService.Get(nameof(Benchmark)); + + private static string MakeKey(string? id, string? member, string? file) + { + if (member == null) + return "global"; + return $"{file?.Split('\\').Last() ?? "unknown"}{(id == null ? "" : $"<{id}>")}::{member}"; + } + + public static void Start( + string? message = null, + string? id = null, + [CallerMemberName] string? member = null, + [CallerFilePath] string? file = null, + [CallerLineNumber] int? line = null) + { + if (!Active) + return; + + string key = MakeKey(id, member, file); + + if (!_clocks.TryGetValue(key, out Stopwatch? sw)) + { + sw = _clocks[key] = new Stopwatch(); + Console.WriteLine($" ::: Registered new benchmark clock '{key}'"); + } + + if (!string.IsNullOrEmpty(message)) + Console.WriteLine($" ::: {key}:{line} > {message}"); + + if (sw.IsRunning) + Console.WriteLine($" ::: {key}:{line} > already running with an elapsed time of {sw.Elapsed}. clock was reset."); + + sw.Reset(); + sw.Start(); + } + + public static long Report( + string? message = null, + string? id = null, + bool keep = true, + [CallerMemberName] string? member = null, + [CallerFilePath] string? file = null, + [CallerLineNumber] int? line = null) + { + if (!Active) + return 0; + + string key = MakeKey(id, member, file); + + if (!_clocks.TryGetValue(key, out Stopwatch sw)) + { + _logger.Debug($"No clock found for '{key}', start one first using Benchmark.Start()"); + return 0; + } + + sw.Stop(); + _logger.Debug($" ::: {key}:{line} > {message ?? "Elapsed"}: {sw.Elapsed}"); + if (keep) + sw.Start(); + + return sw.ElapsedMilliseconds; + } + + public static long End( + string? message = null, + string? id = null, + [CallerMemberName] string? member = null, + [CallerFilePath] string? file = null, + [CallerLineNumber] int? line = null) + { + if (!Active) + return 0; + + return Report(message, id, false, member, file, line); + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Collections/Expander.cs b/Qrakhen.Qamp.Core/Collections/Expander.cs new file mode 100644 index 0000000..5519184 --- /dev/null +++ b/Qrakhen.Qamp.Core/Collections/Expander.cs @@ -0,0 +1,61 @@ +using System.Collections; + +namespace Qrakhen.Qamp.Core.Collections; + +public abstract class Expander : IEnumerable, IGetSet, IAdd +{ + protected T[] Data; + + public long Count { get; protected set; }= 0; + public long Capacity => Data.Length; + + protected Expander(int capacity = 0x10) + { + Data = new T[capacity]; + } + + protected Expander(IEnumerable data) + { + Data = data.ToArray(); + Count = Data.Length; + } + + public T this[long index] { + get => Get(index); + set => Set(index, value); + } + + public T Get(long index) => Data[index]; + + public void Set(long index, T value) + { + Prepare(index); + Data[index] = value; + } + + public long Add(T value) + { + Prepare(Count); + Data[Count] = value; + return Count++; + } + + protected void Prepare(long position) + { + while (position >= Data.Length) { + T[] grown = new T[Data.Length * 2]; + Array.Copy(Data, grown, Count); + Data = grown; + } + } + + public IEnumerator GetEnumerator() + { + foreach (T? item in Data) { + yield return item; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/Qrakhen.Qamp.Core/Collections/FixedArray.cs b/Qrakhen.Qamp.Core/Collections/FixedArray.cs new file mode 100644 index 0000000..501cb31 --- /dev/null +++ b/Qrakhen.Qamp.Core/Collections/FixedArray.cs @@ -0,0 +1,36 @@ +using System.Collections; + +namespace Qrakhen.Qamp.Core.Collections; + +public class FixedArray : IEnumerable, IGetSet +{ + protected T[] Data; + + public long Length => Data.LongLength; + + public T this[long index] { + get => Get(index); + set => Set(index, value); + } + + public FixedArray(IEnumerable data) + { + Data = data.ToArray(); + } + + public T Get(long index) => Data[index]; + + public void Set(long index, T value) + { + Data[index] = value; + } + + public IEnumerator GetEnumerator() + { + foreach (var item in Data) + yield return item; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/Qrakhen.Qamp.Core/Collections/PopStack.cs b/Qrakhen.Qamp.Core/Collections/PopStack.cs new file mode 100644 index 0000000..fc22283 --- /dev/null +++ b/Qrakhen.Qamp.Core/Collections/PopStack.cs @@ -0,0 +1,36 @@ +using System.Collections; + +namespace Qrakhen.Qamp.Core.Collections; + +public class PopStack : IEnumerable, IPop, IPeekable +{ + private readonly T[] _data; + + private int _position; + + public int Position => _position; + public int Length => _data.Length; + public bool Done => _position >= _data.Length; + + public PopStack(IEnumerable data) + { + _data = data.ToArray(); + _position = 0; + } + + public T Pop() => _data[_position++]; + + public T Peek(int delta = 0) => _data[_position + delta]; + + public bool CanPeek(int delta = 0) + => _position + delta > -1 && _position + delta < _data.Length; + + public IEnumerator GetEnumerator() + { + foreach (var item in _data) + yield return item; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} diff --git a/Qrakhen.Qamp.Core/Collections/Register.cs b/Qrakhen.Qamp.Core/Collections/Register.cs new file mode 100644 index 0000000..d070eaa --- /dev/null +++ b/Qrakhen.Qamp.Core/Collections/Register.cs @@ -0,0 +1,25 @@ +namespace Qrakhen.Qamp.Core.Collections; + +public class Register : + IGetSet + where TKey : notnull +{ + private readonly Dictionary _data = new(); + + public int Length => _data.Count; + + public TValue this[TKey key] { + get => Get(key); + set => Set(key, value); + } + + public TKey Add(TKey key, TValue value) + { + _data.Add(key, value); + return key; + } + + public TValue Get(TKey key) => _data[key]; + public void Set(TKey key, TValue value) => _data[key] = value; + public bool Has(TKey key) => _data.ContainsKey(key); +} diff --git a/Qrakhen.Qamp.Core/Collections/StackLike.cs b/Qrakhen.Qamp.Core/Collections/StackLike.cs new file mode 100644 index 0000000..986afe9 --- /dev/null +++ b/Qrakhen.Qamp.Core/Collections/StackLike.cs @@ -0,0 +1,42 @@ +namespace Qrakhen.Qamp.Core.Collections; + +public class StackLike : Expander, + IPush, + IPop, + ISeekable, + IPeekable +{ + public long Position => Count; + + public T this[int position] { + get => Data[position]; + set => Data[position] = value; + } + + public StackLike(int capacity = 0x10) + { + Data = new T[capacity]; + Count = 0; + } + + public StackLike(IEnumerable data) + { + Data = data.ToArray(); + Count = 0; + } + + public long Push(T value) => Add(value); + + public T Pop() => Data[--Count]; + + public T Seek(long position) + { + Count = position; + return this[Count]; + } + + public T Peek(int delta = -1) => Data[Count + delta]; + + public bool CanPeek(int delta = 0) + => Count + delta > -1 && Count + delta < Count; +} diff --git a/Qrakhen.Qamp.Core/Collections/StackRegister.cs b/Qrakhen.Qamp.Core/Collections/StackRegister.cs new file mode 100644 index 0000000..427bced --- /dev/null +++ b/Qrakhen.Qamp.Core/Collections/StackRegister.cs @@ -0,0 +1,49 @@ +namespace Qrakhen.Qamp.Core.Collections; + +public class StackRegister : Expander, IToArray +{ + public StackRegister(int initialCapacity = 0x10) + { + Data = new T[initialCapacity]; + } + + public T? Find(Func callback) + { + for (int i = 0; i < Count; i++) + if (callback(Data[i])) + return Data[i]; + return default; + } + + public void Add(T[] array) + { + foreach (T value in array) { + Add(value); + } + } + + public void Add(IEnumerable array) + { + foreach (T value in array) { + Add(value); + } + } + + public bool TryGet(long index, out T value) + { + value = default; + if (index < 0 || index >= Count) + return false; + value = Data[index]; + return true; + } + + public T[] ToArray() => Data.Subset(0, Count); + + public T[] Subset(long from, int length) + { + T[] result = new T[length]; + Array.Copy(Data, from, result, 0, length); + return result; + } +} diff --git a/Qrakhen.Qamp.Core/Compilation/Builders/Builder.cs b/Qrakhen.Qamp.Core/Compilation/Builders/Builder.cs new file mode 100644 index 0000000..8c73496 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Builders/Builder.cs @@ -0,0 +1,13 @@ +using Qrakhen.Qamp.Core.Collections; + +namespace Qrakhen.Qamp.Core.Compilation.Builders; + +public class Builder +{ + public Builder? Outer; + public FunctionBuilder Function = new(); + + public StackLike Locals = []; + public StackLike Outers = []; + public int ScopeDepth = 0; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Compilation/Builders/ClassBuilder.cs b/Qrakhen.Qamp.Core/Compilation/Builders/ClassBuilder.cs new file mode 100644 index 0000000..d568956 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Builders/ClassBuilder.cs @@ -0,0 +1,7 @@ +namespace Qrakhen.Qamp.Core.Compilation.Builders; + +public class ClassBuilder +{ + public ClassBuilder? Outer; + public bool IsDerived; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Compilation/Builders/FunctionBuilder.cs b/Qrakhen.Qamp.Core/Compilation/Builders/FunctionBuilder.cs new file mode 100644 index 0000000..0502598 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Builders/FunctionBuilder.cs @@ -0,0 +1,16 @@ +using Qrakhen.Qamp.Core.Values.Objects; + +namespace Qrakhen.Qamp.Core.Compilation.Builders; + +public class FunctionBuilder +{ + public SegmentBuilder Segment = new(); + public string Name; + public int OuterCount; + public int ArgumentCount; + + public Function Build() + { + return new Function(Name, Segment.Build(), OuterCount, ArgumentCount); + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Compilation/Builders/SegmentBuilder.cs b/Qrakhen.Qamp.Core/Compilation/Builders/SegmentBuilder.cs new file mode 100644 index 0000000..d2c71b3 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Builders/SegmentBuilder.cs @@ -0,0 +1,39 @@ +using Qrakhen.Qamp.Core.Collections; +using Qrakhen.Qamp.Core.Execution; +using Qrakhen.Qamp.Core.Values; + +namespace Qrakhen.Qamp.Core.Compilation.Builders; + +public readonly struct SegmentBuilder() : IDebug +{ + public readonly StackRegister Instructions = new(); + public readonly StackRegister Constants = new(); + + public Segment Build() + { + return new Segment(Instructions.ToArray(), Constants.ToArray()); + } + + public string Debug(DebugLevel level = DebugLevel.None) + { + string str = Debugger.GetContextString(this); + for (int i = 0; i < Instructions.Count; i++) + { + var instruction = Instructions[i]; + if ((instruction.Code & 0x80) == 1) { + // dynamic + int length = instruction.Code & (0x80 - 1); + byte[] index = Instructions.Subset(i + 1, length).Select(n => n.Code).ToArray(); + str += $"\n [{i:x4}]: Dynamic ({string.Join(' ', index)})"; + i += length; + } else + str += $"\n [{i:x4}]: {instruction}".PadRight(32); + /*if (instruction.OpCode == OpCode.Constant) { + i++; // i know, i know + str += $" : {Constants[Instructions.Subset(i, 8).Select(n => n.Code).ToArray().ToInt64()]}"; + i += 8; + }*/ + } + return str; + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Compilation/Digester.cs b/Qrakhen.Qamp.Core/Compilation/Digester.cs new file mode 100644 index 0000000..7417539 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Digester.cs @@ -0,0 +1,635 @@ +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(); + + private readonly PopStack _stack; + + internal Builder Builder { get; private set; } +} + +public class DigesterProvider +{ + public object ExpressionDigester; // etc. +} + +public class Digester : ISteppable +{ + private readonly Compiler _compiler; + + public event Action? Error; + + private readonly ILogger _logger = LoggerService.Get(); + private readonly IReader _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 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 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; + } +} diff --git a/Qrakhen.Qamp.Core/Compilation/Digesters/IDigester.cs b/Qrakhen.Qamp.Core/Compilation/Digesters/IDigester.cs new file mode 100644 index 0000000..58df209 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Digesters/IDigester.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Qrakhen.Qamp.Core.Compilation.Digesters; + +public interface IDigester +{ + void Digest(Compiler compiler); +} diff --git a/Qrakhen.Qamp.Core/Compilation/ExpressionParser.cs b/Qrakhen.Qamp.Core/Compilation/ExpressionParser.cs new file mode 100644 index 0000000..b0c5599 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/ExpressionParser.cs @@ -0,0 +1,283 @@ +using System.Globalization; +using Qrakhen.Qamp.Core.Execution; +using Qrakhen.Qamp.Core.Tokenization; +using Qrakhen.Qamp.Core.Values; + +namespace Qrakhen.Qamp.Core.Compilation; + +public static class ExpressionParser +{ + private static readonly Dictionary _rules = new(); + + public static Rule Get(TokenType type) => _rules[type]; + + public static void And(Digester digester, bool canAssign) + { + long endJump = digester.EmitJump(OpCode.JumpIfFalse); + + digester.Emit(OpCode.Pop); + digester.WeightedDigest(Weight.And); + + digester.PatchJump(endJump); + } + + static void Binary(Digester digester, bool canAssign) + { + TokenType operatorType = digester.Previous.Type; + Rule rule = Get(operatorType); + digester.WeightedDigest(rule.Weight + 1); + + switch (operatorType) { + case TokenType.BangEqual: digester.Emit(OpCode.Equal, OpCode.Not); break; + case TokenType.EqualEqual: digester.Emit(OpCode.Equal); break; + case TokenType.Greater: digester.Emit(OpCode.Greater); break; + case TokenType.GreaterEqual: digester.Emit(OpCode.Less, OpCode.Not); break; + case TokenType.Less: digester.Emit(OpCode.Less); break; + case TokenType.LessEqual: digester.Emit(OpCode.Greater, OpCode.Not); break; + case TokenType.Plus: digester.Emit(OpCode.Add); break; + case TokenType.Minus: digester.Emit(OpCode.Subtract); break; + case TokenType.Star: digester.Emit(OpCode.Multiply); break; + case TokenType.Slash: digester.Emit(OpCode.Divide); break; + case TokenType.BitwiseAnd: digester.Emit(OpCode.BitwiseAnd); break; + case TokenType.BitwiseOr: digester.Emit(OpCode.BitwiseOr); break; + case TokenType.BitwiseXor: digester.Emit(OpCode.BitwiseXor); break; + case TokenType.BitwiseNot: digester.Emit(OpCode.BitwiseNot); break; + case TokenType.BitwiseLeft: digester.Emit(OpCode.BitwiseLeft); break; + case TokenType.BitwiseRight: digester.Emit(OpCode.BitwiseRight); break; + case TokenType.Increment: digester.Emit(OpCode.Increment); break; + case TokenType.Decrement: digester.Emit(OpCode.Decrement); break; + default: return; + } + } + + static void Call(Digester digester, bool canAssign) + { + byte count = digester.ArgumentList(); + digester.Emit(OpCode.Call, count); + } + + static void Dot(Digester digester, bool canAssign) + { + digester.Consume(TokenType.Identifier, "Expected property name after '.'."); + long name = digester.IdentifierConstant(digester.Previous.Value); + + if (canAssign && digester.Match(TokenType.Equal)) { + digester.Expression(); + digester.Emit(OpCode.SetProperty, name.GetBytes()); + } else if (digester.Match(TokenType.GroupOpen)) { + Byte argCount = 0; //argumentList(); + digester.Emit(OpCode.Invoke, name.GetBytes()); + digester.Emit(argCount); + } else { + digester.Emit(OpCode.GetProperty, name.GetBytes()); + } + } + + static void Literal(Digester digester, bool canAssign) + { + switch (digester.Previous.Type) { + case TokenType.False: + digester.Emit(OpCode.False); + break; + case TokenType.Null: + digester.Emit(OpCode.Null); + break; + case TokenType.True: + digester.Emit(OpCode.True); + break; + default: + break; + } + } + + static void Group(Digester digester, bool canAssign) + { + digester.Expression(); + digester.Consume(TokenType.GroupClose, "Expected ')' after expression."); + } + + static void Array(Digester digester, bool canAssign) + { + int length = 0; + while (!digester.Check(TokenType.ArrayClose)) { + digester.Expression(); + length++; + if (digester.Check(TokenType.Comma)) + digester.Next(); + else + break; + } + digester.Consume(TokenType.ArrayClose, "Expected ']' after array definition"); + digester.Emit( + OpCode.Array, + digester.MakeConstant(new Value((long)length)).GetBytes()); + } + + static void Add(Digester digester, bool canAssign) + { + digester.Expression(); + digester.Emit(OpCode.ArrayAdd); + } + + static void Index(Digester digester, bool canAssign) + { + digester.Expression(); + digester.Consume(TokenType.ArrayClose, "expected ] after array accessor"); + if (canAssign && digester.Match(TokenType.ArrayAdd)) { + digester.Expression(); + digester.Emit(OpCode.ArrayAdd); + } else if (canAssign && digester.Match(TokenType.Equal)) { + digester.Expression(); + digester.Emit(OpCode.ArraySet); + } else { + digester.Emit(OpCode.ArrayGet); + } + } + + static void Number(Digester digester, bool canAssign) + { + Token token = digester.Previous; + string number = token.Value ?? ""; + TokenType type = token.Type; + Value value = default; + if (type == TokenType.Hexadecimal) + value = new Value(long.Parse(number, NumberStyles.HexNumber)); + else if (type == TokenType.Decimal) + value = new Value(double.Parse(number, NumberStyles.Float, CultureInfo.InvariantCulture)); + else if (type == TokenType.Integer) + value = new Value(long.Parse(number, NumberStyles.Integer)); + else + digester.ErrorAt(token, $"Could not parse number {number}"); + digester.EmitConstant(value); + } + + static void Or(Digester digester, bool canAssign) + { + long elseJump = digester.EmitJump(OpCode.JumpIfFalse); + long endJump = digester.EmitJump(OpCode.Jump); + + digester.PatchJump(elseJump); + digester.Emit(OpCode.Pop); + + digester.WeightedDigest(Weight.Or); + digester.PatchJump(endJump); + } + + static void String(Digester digester, bool canAssign) + { + digester.EmitConstant(Values.Objects.String.Make(digester.Previous.Value)); + } + + static void Variable(Digester digester, bool canAssign) + { + OpCode Get, Set; + string name = digester.Previous.Value!; + long variable = digester.ResolveLocal(digester.Builder, name); + if (variable > -1) { + Get = OpCode.GetLocal; + Set = OpCode.SetLocal; + } else if ((variable = digester.ResolveOuter(digester.Builder, name)) > -1) { + Get = OpCode.GetOuter; + Set = OpCode.SetOuter; + } else { + variable = digester.IdentifierConstant(name); + Get = OpCode.GetGlobal; + Set = OpCode.SetGlobal; + } + + if (canAssign && digester.Match(TokenType.Equal)) { + digester.Expression(); + digester.Emit(Set); + digester.Emit(variable); + } else { + digester.Emit(Get); + digester.Emit(variable); + } + } + + static void Base(Digester digester, bool canAssign) + { + + } + + static void This(Digester digester, bool canAssign) + { + if (digester.Class == null) + digester.ErrorAtPrevious("We're currently not in a class. 'this' only refers to the current instance of one."); + else + Variable(digester, false); + } + + static void Modifier(Digester digester, bool canAssign) + { + Token token = digester.Previous; + digester.WeightedDigest(Weight.Modifier); + switch (token.Type) { + case TokenType.Bang: digester.Emit(OpCode.Not); break; + case TokenType.Minus: digester.Emit(OpCode.Negate); break; + case TokenType.BitwiseNot: digester.Emit(OpCode.BitwiseNot); break; + } + } + + static ExpressionParser() + { + _rules[TokenType.GroupOpen] = new Rule(Group, Call, Weight.Call); + _rules[TokenType.GroupClose] = new Rule(null, null, Weight.None); + _rules[TokenType.ContextOpen] = new Rule(null, null, Weight.None); + _rules[TokenType.ContextClose] = new Rule(null, null, Weight.None); + _rules[TokenType.ArrayOpen] = new Rule(Array, Index, Weight.Call); + _rules[TokenType.ArrayClose] = new Rule(null, null, Weight.None); + _rules[TokenType.ArrayAdd] = new Rule(null, null, Weight.None); + _rules[TokenType.ArrayRemove] = new Rule(null, null, Weight.None); + _rules[TokenType.Colon] = new Rule(null, Dot, Weight.Call); + _rules[TokenType.Comma] = new Rule(null, null, Weight.None); + _rules[TokenType.Dot] = new Rule(null, Dot, Weight.Call); + _rules[TokenType.Minus] = new Rule(Modifier, Binary, Weight.Term); + _rules[TokenType.Plus] = new Rule(null, Binary, Weight.Term); + _rules[TokenType.Increment] = new Rule(Binary, Binary, Weight.Call); + _rules[TokenType.Decrement] = new Rule(Binary, Binary, Weight.Call); + _rules[TokenType.BitwiseAnd] = new Rule(Modifier, Binary, Weight.Term); + _rules[TokenType.BitwiseOr] = new Rule(null, Binary, Weight.Term); + _rules[TokenType.BitwiseXor] = new Rule(null, Binary, Weight.Term); + _rules[TokenType.BitwiseLeft] = new Rule(null, Binary, Weight.Term); + _rules[TokenType.BitwiseRight] = new Rule(null, Binary, Weight.Term); + _rules[TokenType.BitwiseNot] = new Rule(Modifier, null, Weight.Term); + _rules[TokenType.Semicolon] = new Rule(null, null, Weight.None); + _rules[TokenType.Slash] = new Rule(null, Binary, Weight.Factor); + _rules[TokenType.Star] = new Rule(null, Binary, Weight.Factor); + _rules[TokenType.Bang] = new Rule(Modifier, null, Weight.None); + _rules[TokenType.BangEqual] = new Rule(null, Binary, Weight.Equal); + _rules[TokenType.Equal] = new Rule(null, null, Weight.None); + _rules[TokenType.EqualEqual] = new Rule(null, Binary, Weight.Equal); + _rules[TokenType.Greater] = new Rule(null, Binary, Weight.Compare); + _rules[TokenType.GreaterEqual] = new Rule(null, Binary, Weight.Compare); + _rules[TokenType.Less] = new Rule(null, Binary, Weight.Compare); + _rules[TokenType.LessEqual] = new Rule(null, Binary, Weight.Compare); + _rules[TokenType.Identifier] = new Rule(Variable, null, Weight.None); + _rules[TokenType.String] = new Rule(String, null, Weight.None); + _rules[TokenType.Integer] = new Rule(Number, null, Weight.None); + _rules[TokenType.Hexadecimal] = new Rule(Number, null, Weight.None); + _rules[TokenType.Decimal] = new Rule(Number, null, Weight.None); + _rules[TokenType.And] = new Rule(null, And, Weight.And); + _rules[TokenType.Class] = new Rule(null, null, Weight.None); + _rules[TokenType.Else] = new Rule(null, null, Weight.None); + _rules[TokenType.False] = new Rule(Literal, null, Weight.None); + _rules[TokenType.For] = new Rule(null, null, Weight.None); + _rules[TokenType.Function] = new Rule(null, null, Weight.None); + _rules[TokenType.If] = new Rule(null, null, Weight.None); + _rules[TokenType.Null] = new Rule(Literal, null, Weight.None); + _rules[TokenType.Or] = new Rule(null, Or, Weight.Or); + _rules[TokenType.Print] = new Rule(null, null, Weight.None); + _rules[TokenType.TypeOf] = new Rule(null, null, Weight.None); + _rules[TokenType.Export] = new Rule(null, null, Weight.None); + _rules[TokenType.Import] = new Rule(null, null, Weight.None); + _rules[TokenType.Return] = new Rule(null, null, Weight.None); + _rules[TokenType.Super] = new Rule(Base, null, Weight.None); + _rules[TokenType.This] = new Rule(This, null, Weight.None); + _rules[TokenType.True] = new Rule(Literal, null, Weight.None); + _rules[TokenType.Var] = new Rule(null, null, Weight.None); + _rules[TokenType.While] = new Rule(null, null, Weight.None); + _rules[TokenType.Error] = new Rule(null, null, Weight.None); + _rules[TokenType.Eof] = new Rule(null, null, Weight.None); + } +} diff --git a/Qrakhen.Qamp.Core/Compilation/Rule.cs b/Qrakhen.Qamp.Core/Compilation/Rule.cs new file mode 100644 index 0000000..3220b93 --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Rule.cs @@ -0,0 +1,10 @@ +namespace Qrakhen.Qamp.Core.Compilation; + +public delegate void RuleDelegate(Digester digester, bool canAssign); + +public readonly struct Rule(RuleDelegate? prefix = null, RuleDelegate? infix = null, Weight weight = Weight.None) +{ + public readonly RuleDelegate? Prefix = prefix; + public readonly RuleDelegate? Infix = infix; + public readonly Weight Weight = weight; +} diff --git a/Qrakhen.Qamp.Core/Compilation/Weight.cs b/Qrakhen.Qamp.Core/Compilation/Weight.cs new file mode 100644 index 0000000..9df715e --- /dev/null +++ b/Qrakhen.Qamp.Core/Compilation/Weight.cs @@ -0,0 +1,16 @@ +namespace Qrakhen.Qamp.Core.Compilation; + +public enum Weight +{ + None = 0, + Assign = 1, + Or = 2, + And = 3, + Equal = 4, + Compare = 5, + Term = 6, + Factor = 7, + Modifier = 8, + Call = 9, + Primary = 10 +} diff --git a/Qrakhen.Qamp.Core/Console.cs b/Qrakhen.Qamp.Core/Console.cs new file mode 100644 index 0000000..f6dfc12 --- /dev/null +++ b/Qrakhen.Qamp.Core/Console.cs @@ -0,0 +1,12 @@ +namespace Qrakhen.Qamp.Core.IO; + +public static class Console +{ + public static void Write(object? any) + { + string[] split = $"{any ?? "null"}".Split('\n'); + for (int i = 0; i < split.Length; i++) { + System.Console.Write((i == 0 ? " :> " : " ") + $"{split[i]}\n"); + } + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Exceptions.cs b/Qrakhen.Qamp.Core/Exceptions.cs new file mode 100644 index 0000000..57bf192 --- /dev/null +++ b/Qrakhen.Qamp.Core/Exceptions.cs @@ -0,0 +1,36 @@ +using Qrakhen.Qamp.Core.Tokenization; +using Qrakhen.Qamp.Core.Values; + +using V = Qrakhen.Qamp.Core.Values.ValueType; + +namespace Qrakhen.Qamp.Core; + +public class QampException(string message, TokenPosition position = default) + : Exception(message) +{ + public readonly TokenPosition Position = position; +} + +public class ReaderException(string message, IReader reader) + : QampException(message, reader.CurrentPosition) +{ + public readonly IReader Reader = reader; +} + +public class TokenException(string message, IReader reader, Token token) + : ReaderException($"TokenError {token} at {token.Position}: {message}", reader) +{ + public readonly Token Token = token; +} + +public class ExecutionException(string message, TokenPosition position = default) + : QampException(message, position); + +public class DivisionByZeroException(string? message, TokenPosition position = default) + : ExecutionException(message ?? "Can not divide by zero", position); + +public class ConversionException(string message, Value value, TokenPosition position = default) + : ExecutionException(message, position) +{ + public readonly Value Value = value; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Execution/Instruction.cs b/Qrakhen.Qamp.Core/Execution/Instruction.cs new file mode 100644 index 0000000..d88ffaa --- /dev/null +++ b/Qrakhen.Qamp.Core/Execution/Instruction.cs @@ -0,0 +1,16 @@ +namespace Qrakhen.Qamp.Core.Execution; + +public readonly record struct Instruction(byte Code) +{ + public OpCode OpCode => (OpCode)Code; + + public static implicit operator byte(Instruction instruction) => instruction.Code; + public static implicit operator Instruction(byte code) => new(code); + public static implicit operator OpCode(Instruction instruction) => (OpCode)instruction.Code; + public static implicit operator Instruction(OpCode code) => new((byte)code); + + public override string ToString() + { + return $"<{OpCode} ({Code})>"; + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Execution/Instructions.cs b/Qrakhen.Qamp.Core/Execution/Instructions.cs new file mode 100644 index 0000000..ae2eb22 --- /dev/null +++ b/Qrakhen.Qamp.Core/Execution/Instructions.cs @@ -0,0 +1,34 @@ +namespace Qrakhen.Qamp.Core.Execution; + +public class Instructions(IEnumerable instructions) : + ISteppable, + IPeekable, + ISeekable +{ + private readonly T[] _instructions = instructions.ToArray(); + + public long Position { get; private set; } = 0; + + public long Length => _instructions.LongLength; + public bool Done => Position >= _instructions.Length; + + public T this[long offset] => offset < Length ? + _instructions[offset] : + throw new ArgumentOutOfRangeException($"Tried to access offset beyond instruction length {offset}:{Length}"); + + public T Seek(long position) + { + Position = position; + return _instructions[Position]; + } + + public T Next() + { + return _instructions[Position++]; + } + + public T Peek(int delta = 0) + { + return _instructions[Position + delta]; + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Execution/OpCode.cs b/Qrakhen.Qamp.Core/Execution/OpCode.cs new file mode 100644 index 0000000..f3bdb2a --- /dev/null +++ b/Qrakhen.Qamp.Core/Execution/OpCode.cs @@ -0,0 +1,63 @@ +namespace Qrakhen.Qamp.Core.Execution; + +public enum OpCode +{ + None = 0, + Constant, + Null, + True, + False, + Pop, + Export, + Import, + GetLocal, + SetLocal, + GetGlobal, + DefineGlobal, + SetGlobal, + GetOuter, + SetOuter, + GetProperty, + SetProperty, + Property, + GetSuper, + Equal, + Greater, + Less, + Add, + Subtract, + Multiply, + Divide, + Modulo, + BitwiseAnd, + BitwiseOr, + BitwiseXor, + BitwiseNot, + BitwiseLeft, + BitwiseRight, + Increment, + Decrement, + Not, + Negate, + Ref, + Array, + ArrayGet, + ArraySet, + ArrayAdd, + ArrayRemove, + Print, + PrintExpr, + Typeof, + Jump, + JumpIfFalse, + Loop, + Call, + Invoke, + SuperInvoke, + Closure, + CloseUpvalue, + Return, + Class, + Inherit, + Method +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Execution/Runner.cs b/Qrakhen.Qamp.Core/Execution/Runner.cs new file mode 100644 index 0000000..95ba685 --- /dev/null +++ b/Qrakhen.Qamp.Core/Execution/Runner.cs @@ -0,0 +1,387 @@ +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; + +public class GetPtr +{ + public IGetSet Target; + public long Pointer; + + public GetPtr(IGetSet target, long pointer = 0) + { + Target = target; + Pointer = pointer; + } + + public T Next() => Target.Get(Pointer++); + + public void Set(long position, T value) + { + Target.Set(position, value); + } + + public T Get(long position) + { + return Target.Get(position); + } +} + +// this is all a bit cheesy imho +public class InstructionPtr : GetPtr +{ + public Segment Segment; + + public InstructionPtr(Segment segment, int position = 0) : base(segment.Instructions, position) + { + Segment = segment; + } + + public long NextLong() + { + long value = Segment.ReadLong(Pointer); + Pointer += sizeof(long); + return value; + } + + public Value NextConstant() + { + return Segment.Constants[NextLong()]; + } + + public String? GetString(long offset) + { + return Segment.Constants[offset].Ptr.Value as String; + } + + public Instruction Instruction => Segment.Instructions[Pointer]; + + public static Instruction operator +(InstructionPtr ptr, int value) => ptr.Segment.Instructions[ptr.Pointer + value]; + + public static Instruction operator -(InstructionPtr ptr, int value) => ptr.Segment.Instructions[ptr.Pointer - value]; + + public static InstructionPtr operator ++(InstructionPtr ptr) + { + ptr.Pointer++; + return ptr; + } + + public static InstructionPtr operator --(InstructionPtr ptr) + { + ptr.Pointer--; + return ptr; + } +} + +public class Call +{ + public Closure Closure; + public InstructionPtr Ptr; + public GetPtr StackPtr; + + public Call(Closure closure, StackLike stack, long stackOffset) + { + Closure = closure; + Ptr = new InstructionPtr(Closure.Function.Segment); + StackPtr = new GetPtr(stack, stackOffset); + } +} + +public unsafe class OuterWrapper +{ + public Value* Target; + public Value Stored; +} + +public class Runner : IDisposable +{ + private readonly ILogger _logger = LoggerService.Get(); + + public readonly Options Options; + + public StackLike Calls; + + // todo: maybe have a pointer everywhere to stacks or arrays and use those rather than built-in pointers or cursors... ? + public StackLike Stack; + public long Cursor; + + public Register Globals = new Register(); + public Register Imports = new Register(); + public Register Exports = new Register(); + + public Runner(Options options) + { + Options = options; + Calls = new StackLike(options.MaxCalls); + Stack = new StackLike(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.Ptr.Next(); + _logger.Verbose($"OpCode: {opCode}"); + switch (opCode) { + case Op.Constant: Push(call.Ptr.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.Ptr.NextLong(); + _logger.Verbose($"getting slot {slot} which is {call.StackPtr.Get(slot)}"); + Push(call.StackPtr.Get(slot)); + break; + } + + case Op.SetLocal: { + long slot = call.Ptr.NextLong(); + _logger.Verbose($"setting stackptr {slot} to {Peek()}"); + call.StackPtr.Set(slot, Peek()); + break; + } + + case Op.DefineGlobal: { + string? name = call.Ptr.GetString(call.Ptr.NextLong())?.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.Ptr.GetString(call.Ptr.NextLong())?.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; + } + + // todo: add closure + + case Op.GetGlobal: { + string? name = call.Ptr.GetString(call.Ptr.NextLong())?.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.Pointer; // well find a better way... + Push(result); + call = Calls.Peek(); + break; + } + + } while (call.Ptr.Segment.Instructions.Length > call.Ptr.Pointer); + + 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) { } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Execution/Segment.cs b/Qrakhen.Qamp.Core/Execution/Segment.cs new file mode 100644 index 0000000..b122efd --- /dev/null +++ b/Qrakhen.Qamp.Core/Execution/Segment.cs @@ -0,0 +1,73 @@ +using Qrakhen.Qamp.Core.Collections; +using Qrakhen.Qamp.Core.Values; + +namespace Qrakhen.Qamp.Core.Execution; + +using Unsigned = ulong; +using Signed = long; +using Char = char; +using Byte = byte; +using Bool = bool; +using Decimal = double; + +public class Segment( + IEnumerable instructions, + IEnumerable constants) : ISerialize +{ + public readonly FixedArray Instructions = new(instructions); + public readonly FixedArray Constants = new(constants); + + public byte Read(long offset) => Instructions[offset]; + + public byte[] Read(long offset, int length) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(offset + length, Instructions.Length); + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) + bytes[i] = Instructions[offset + i]; + return bytes; + } + + /// + /// Although this always returns a long, the actually stored data in the instruction set has a dynamic width (anything between 1 and 8 bytes) + /// + /// + /// + public long ReadDynamic(long offset, out byte length) + { + length = Read(offset, 1)[0]; + byte[] bytes = new byte[8]; + for (int i = 0; i < length; i++) { + bytes[i] = Instructions[offset + 1 + i]; + } + return bytes.ToInt64(); + } + + public short ReadShort(long offset) => BitConverter.ToInt16(Read(offset, 2)); + public int ReadInt(long offset) => BitConverter.ToInt32(Read(offset, 4)); + public long ReadLong(long offset) => BitConverter.ToInt64(Read(offset, 8)); + + public byte[] Serialize() + { + var opCodes = Instructions.Select(i => (byte)i).ToArray(); + var constants = Constants.Select(c => c.Unsigned.GetBytes()).ToArray(); + byte[] bytes = new byte[sizeof(long) * 2 + opCodes.Length + constants.Length * 8]; + Array.Copy(opCodes.LongLength.GetBytes(), 0, bytes, 0, sizeof(long)); + Array.Copy((constants.LongLength * sizeof(long)).GetBytes(), 0, bytes, sizeof(long), sizeof(long)); + int offset = sizeof(long) * 2; + for (long i = 0; i < opCodes.LongLength; i++) { + bytes[offset++] = opCodes[i]; + } + for (long i = 0; i < constants.LongLength; i++) { + for (int j = 0; j < sizeof(long); j++) { + bytes[offset++] = constants[i][j]; + } + } + return bytes; + } + + public static Segment Deserialize(byte[] data) + { + return null; + } +} diff --git a/Qrakhen.Qamp.Core/Extensions.cs b/Qrakhen.Qamp.Core/Extensions.cs new file mode 100644 index 0000000..15683d4 --- /dev/null +++ b/Qrakhen.Qamp.Core/Extensions.cs @@ -0,0 +1,67 @@ +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Qrakhen.Qamp.Core; + +public static class Extensions +{ + public static bool TryMatch(this Regex regex, string str, out Match match) + { + match = regex.Match(str); + return match.Success; + } + + public static T[] Subset(this T[] array, long from, long length) + { + var result = new T[length]; + Array.Copy(array, from, result, 0, length); + return result; + } + + /// + /// Expensive. + /// + public static void Insert(this Stream stream, byte[] data, long position = -1) + { + if (position < 0) + position = stream.Position; + + byte[] buffer = new byte[stream.Length - position]; + if (buffer.Length > 0) { + stream.Position = position; + stream.ReadExactly(buffer); + stream.Write(buffer); + } + stream.Position = position; + stream.Write(data); + } + + public static void Remove(this Stream stream, long position, int amount) + { + byte[] buffer = new byte[stream.Length - (position + amount)]; + if (buffer.Length > 0) { + stream.Position = position + amount; + stream.ReadExactly(buffer); + stream.Position = position; + stream.Write(buffer); + } + stream.Position = position; + stream.SetLength(stream.Length - amount); + } + + public static byte[] GetBytes(this short value) => BitConverter.GetBytes(value); + public static byte[] GetBytes(this ushort value) => BitConverter.GetBytes(value); + public static byte[] GetBytes(this int value) => BitConverter.GetBytes(value); + public static byte[] GetBytes(this uint value) => BitConverter.GetBytes(value); + public static byte[] GetBytes(this long value) => BitConverter.GetBytes(value); + public static byte[] GetBytes(this ulong value) => BitConverter.GetBytes(value); + public static byte[] GetBytes(this double value) => BitConverter.GetBytes(value); + + public static short ToInt16(this byte[] bytes) => BitConverter.ToInt16(bytes); + public static ushort ToUInt16(this byte[] bytes) => BitConverter.ToUInt16(bytes); + public static int ToInt32(this byte[] bytes) => BitConverter.ToInt32(bytes); + public static uint ToUInt32(this byte[] bytes) => BitConverter.ToUInt32(bytes); + public static long ToInt64(this byte[] bytes) => BitConverter.ToInt64(bytes); + public static ulong ToUInt64(this byte[] bytes) => BitConverter.ToUInt64(bytes); + public static double ToDouble(this byte[] bytes) => BitConverter.ToDouble(bytes); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/IAdd.cs b/Qrakhen.Qamp.Core/IAdd.cs new file mode 100644 index 0000000..2e69c38 --- /dev/null +++ b/Qrakhen.Qamp.Core/IAdd.cs @@ -0,0 +1,8 @@ +using System.Numerics; + +namespace Qrakhen.Qamp.Core; + +public interface IAdd +{ + TKey Add(TValue value); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/ICapturableBuffer.cs b/Qrakhen.Qamp.Core/ICapturableBuffer.cs new file mode 100644 index 0000000..101a92b --- /dev/null +++ b/Qrakhen.Qamp.Core/ICapturableBuffer.cs @@ -0,0 +1,8 @@ +namespace Qrakhen.Qamp.Core; + +public interface ICaptureBuffer +{ + THandle BeginCapture(); + TData[] EndCapture(THandle handle); + int Captured(THandle handle); +} diff --git a/Qrakhen.Qamp.Core/IConsumable.cs b/Qrakhen.Qamp.Core/IConsumable.cs new file mode 100644 index 0000000..a4bfdf6 --- /dev/null +++ b/Qrakhen.Qamp.Core/IConsumable.cs @@ -0,0 +1,6 @@ +namespace Qrakhen.Qamp.Core; + +public interface IConsumable +{ + void Consume(T expected, string? message = null); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/IDebug.cs b/Qrakhen.Qamp.Core/IDebug.cs new file mode 100644 index 0000000..836f395 --- /dev/null +++ b/Qrakhen.Qamp.Core/IDebug.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; + +namespace Qrakhen.Qamp.Core; + +public interface IDebug +{ + T Debug(DebugLevel level = DebugLevel.None); +} + +public interface IDebug +{ + TOut Debug(TContext? context, DebugLevel level = DebugLevel.None); +} + +public enum DebugLevel +{ + None = 0, + Mild = 1, + Strong = 2, + Verbose = 3 +} + +public static class Debugger +{ + public static string GetContextString(object? origin, [CallerMemberName] string? caller = null) + { + return $"{{Debugger::Builder}}: [{origin?.ToString() ?? "null"}] <{origin?.GetType().Name ?? "null"}> ({caller ?? "null"})"; + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/IGet.cs b/Qrakhen.Qamp.Core/IGet.cs new file mode 100644 index 0000000..3e1ca56 --- /dev/null +++ b/Qrakhen.Qamp.Core/IGet.cs @@ -0,0 +1,6 @@ +namespace Qrakhen.Qamp.Core; + +public interface IGet +{ + TValue Get(TKey index); +} diff --git a/Qrakhen.Qamp.Core/IGetSet.cs b/Qrakhen.Qamp.Core/IGetSet.cs new file mode 100644 index 0000000..04fbaf6 --- /dev/null +++ b/Qrakhen.Qamp.Core/IGetSet.cs @@ -0,0 +1,3 @@ +namespace Qrakhen.Qamp.Core; + +public interface IGetSet : IGet, ISet; diff --git a/Qrakhen.Qamp.Core/IPeekable.cs b/Qrakhen.Qamp.Core/IPeekable.cs new file mode 100644 index 0000000..4a71660 --- /dev/null +++ b/Qrakhen.Qamp.Core/IPeekable.cs @@ -0,0 +1,6 @@ +namespace Qrakhen.Qamp.Core; + +public interface IPeekable +{ + T Peek(int delta = -1); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/IReadable.cs b/Qrakhen.Qamp.Core/IReadable.cs new file mode 100644 index 0000000..90aee0f --- /dev/null +++ b/Qrakhen.Qamp.Core/IReadable.cs @@ -0,0 +1,7 @@ +namespace Qrakhen.Qamp.Core; + +public interface IReadable +{ + T Read(int position); + T[] Read(int position, int length); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/IReader.cs b/Qrakhen.Qamp.Core/IReader.cs new file mode 100644 index 0000000..c7b0d36 --- /dev/null +++ b/Qrakhen.Qamp.Core/IReader.cs @@ -0,0 +1,10 @@ +using Qrakhen.Qamp.Core.Tokenization; + +namespace Qrakhen.Qamp.Core; + +public interface IReader +{ + TokenPosition CurrentPosition { get; } + bool Done { get; } + T NextToken(bool includeCompilationIrrelevant = false); +} diff --git a/Qrakhen.Qamp.Core/ISeekable.cs b/Qrakhen.Qamp.Core/ISeekable.cs new file mode 100644 index 0000000..7d2b681 --- /dev/null +++ b/Qrakhen.Qamp.Core/ISeekable.cs @@ -0,0 +1,7 @@ +namespace Qrakhen.Qamp.Core; + +public interface ISeekable +{ + TKey Position { get; } + TValue Seek(TKey position); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/ISerialize.cs b/Qrakhen.Qamp.Core/ISerialize.cs new file mode 100644 index 0000000..27dfcad --- /dev/null +++ b/Qrakhen.Qamp.Core/ISerialize.cs @@ -0,0 +1,7 @@ +namespace Qrakhen.Qamp.Core; + +public interface ISerialize +{ + byte[] Serialize(); + static abstract TSelf Deserialize(byte[] data); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/ISet.cs b/Qrakhen.Qamp.Core/ISet.cs new file mode 100644 index 0000000..94c7978 --- /dev/null +++ b/Qrakhen.Qamp.Core/ISet.cs @@ -0,0 +1,8 @@ +namespace Qrakhen.Qamp.Core; + +public interface ISet +{ + TValue this[TKey index] { set; } + + void Set(TKey index, TValue value); +} diff --git a/Qrakhen.Qamp.Core/IStack.cs b/Qrakhen.Qamp.Core/IStack.cs new file mode 100644 index 0000000..cd37036 --- /dev/null +++ b/Qrakhen.Qamp.Core/IStack.cs @@ -0,0 +1,13 @@ +namespace Qrakhen.Qamp.Core; + +public interface IStack : IPush, IPop; + +public interface IPush +{ + TKey Push(T value); +} + +public interface IPop +{ + T Pop(); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/ISteppable.cs b/Qrakhen.Qamp.Core/ISteppable.cs new file mode 100644 index 0000000..d4016cb --- /dev/null +++ b/Qrakhen.Qamp.Core/ISteppable.cs @@ -0,0 +1,7 @@ +namespace Qrakhen.Qamp.Core; + +public interface ISteppable +{ + bool Done { get; } + T Next(); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/IToArray.cs b/Qrakhen.Qamp.Core/IToArray.cs new file mode 100644 index 0000000..b458aee --- /dev/null +++ b/Qrakhen.Qamp.Core/IToArray.cs @@ -0,0 +1,6 @@ +namespace Qrakhen.Qamp.Core; + +public interface IToArray +{ + T[] ToArray(); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Logging/ILogger.cs b/Qrakhen.Qamp.Core/Logging/ILogger.cs new file mode 100644 index 0000000..cc4cdc3 --- /dev/null +++ b/Qrakhen.Qamp.Core/Logging/ILogger.cs @@ -0,0 +1,160 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Xml.XPath; + +namespace Qrakhen.Qamp.Core.Logging; + +public interface ILogger +{ + LogLevel Level { get; set; } + + void Log(LogLevel level, params object?[] values); + + void Critical(params object?[] values); + void Error(params object?[] values); + void Warn(params object?[] values); + void Info(params object?[] values); + void Debug(params object?[] values); + void Trace(params object?[] values); + void Verbose(params object?[] values); + void Method(object? value = null, string? caller = null); +} + +[Flags] +public enum LogLevel +{ + Mute = 0x00, + Critical = 0x01, + Error = 0x02, + Warn = 0x04, + Info = 0x08, + Debug = 0x10, + Trace = 0x20, + Verbose = 0x40, + All = Critical | Error | Warn | Info | Debug | Trace | Verbose +} + +public interface ILogFormatter +{ + string[] Format(object? value, int maxWidth = 80); +} + +public interface ILoggerService +{ + ILogger Get(string? name = null, LogLevel? level = null); + ILogger Get(string name, LogLevel? level = null); +} + +public class LoggerService +{ + private static readonly Dictionary _loggers = new Dictionary(); + + public static LogLevel Default = LogLevel.All; + + public static ILogger Get(string? name = null, LogLevel? level = null) + { + level ??= Default; + string _name = nameof(T); + if (!string.IsNullOrEmpty(name)) + _name = $"{_name}:{name}"; + return Get(_name, level); + } + + public static ILogger Get(string name, LogLevel? level = null) + { + level ??= Default; + ILogger? logger; + if (_loggers.TryGetValue(name, out logger)) + return logger; + logger = new Logger(name, level.Value); + _loggers.Add(name, logger); + return logger; + } +} + +public class Logger : ILogger, ILogFormatter +{ + public string Name { get; private set; } + public LogLevel Level { get; set; } + + private static Dictionary headers = new() { + { LogLevel.Critical, (" !? ", " !? ") }, + { LogLevel.Error, (" !! ", " ! ") }, + { LogLevel.Warn, (" !> ", " ! ") }, + { LogLevel.Info, (" :> ", " : ") }, + { LogLevel.Debug, (" > ", " ") }, + { LogLevel.Trace, (" > ", " ") }, + { LogLevel.Verbose, (" > ", " ") }, + }; + + private static Dictionary colors = new() + { + { LogLevel.Critical, ConsoleColor.DarkRed }, + { LogLevel.Error, ConsoleColor.Red }, + { LogLevel.Warn, ConsoleColor.Yellow }, + { LogLevel.Info, ConsoleColor.White }, + { LogLevel.Debug, ConsoleColor.Gray }, + { LogLevel.Trace, ConsoleColor.DarkGray }, + { LogLevel.Verbose, ConsoleColor.DarkGray }, + }; + + public Logger(string name, LogLevel level) + { + Name = name; + Level = level; + } + + public string[] Format(object? value, int maxWidth = 80) + { + List result = []; + if (value == null) + result.Add("null"); + else if (value is string str) + result.AddRange(str.Split('\n')); + else if (value is Exception e) + { + result.Add(e.Message); + if (!string.IsNullOrEmpty(e.StackTrace)) + result.AddRange(e.StackTrace.Split('\n')); + } else + result.Add(value?.ToString() ?? "null"); + + return result.ToArray(); + } + + public void Log(LogLevel level, params object?[] values) + { + if ((level & Level) != level) + return; + + var header = headers[level]; + Console.ForegroundColor = colors[level]; + foreach (var value in values) + { + var lines = Format(value); + for (int i = 0; i < lines.Length; i++) + Console.WriteLine($"{(i == 0 ? header.First : header.Extra)}{lines[i]}"); + } + Console.ForegroundColor = ConsoleColor.White; + } + + public void Critical(params object?[] values) => Log(LogLevel.Critical, values); + public void Error(params object?[] values) => Log(LogLevel.Error, values); + public void Warn(params object?[] values) => Log(LogLevel.Warn, values); + public void Info(params object?[] values) => Log(LogLevel.Info, values); + public void Debug(params object?[] values) => Log(LogLevel.Debug, values); + public void Trace(params object?[] values) => Log(LogLevel.Trace, values); + public void Verbose(params object?[] values) => Log(LogLevel.Verbose, values); + + public void Method(object? value = null, [CallerMemberName] string? caller = null) + { + if (caller == null) + { + var st = new StackTrace(); + var previous = st.GetFrame(1)?.GetMethod(); + if (previous != null) + caller = $"{previous.DeclaringType?.Name}.{previous.Name}"; + } + Log(LogLevel.Trace, [ $"[{caller}] {value}" ]); + } +} diff --git a/Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj b/Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj new file mode 100644 index 0000000..cdfe8fc --- /dev/null +++ b/Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + true + + diff --git a/Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj.Backup.tmp b/Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj.Backup.tmp new file mode 100644 index 0000000..2e6a1eb --- /dev/null +++ b/Qrakhen.Qamp.Core/Qrakhen.Qamp.Core.csproj.Backup.tmp @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + diff --git a/Qrakhen.Qamp.Core/Tokenization/Dialect.cs b/Qrakhen.Qamp.Core/Tokenization/Dialect.cs new file mode 100644 index 0000000..59b7652 --- /dev/null +++ b/Qrakhen.Qamp.Core/Tokenization/Dialect.cs @@ -0,0 +1,118 @@ +namespace Qrakhen.Qamp.Core.Tokenization; + +using static TokenType; + +public class Dialect +{ + private char[][] _sequences; + + public Dialect() + { + _sequences = new char[0x80][]; + } + + public void Define(TokenType type, string sequence) + => Define(type, sequence.ToCharArray()); + + public void Define(TokenType type, char[] sequence) + { + _sequences[(int)type] = sequence; + } + + public char[] Get(TokenType type) + => _sequences[(int)type]; +} + +public class DefaultDialect : Dialect +{ + public DefaultDialect() + { + Define(Null, "null"); + Define(GroupOpen, "("); + Define(GroupClose, ")"); + Define(ContextOpen, "{"); + Define(ContextClose, "}"); + Define(ArrayOpen, "["); + Define(ArrayClose, "]"); + Define(ArrayAdd, "<+"); + Define(ArrayRemove, "->"); + Define(Comma, ","); + Define(Dot, "."); + Define(Colon, ":"); + Define(Semicolon, ";"); + Define(Minus, "-"); + Define(Plus, "+"); + Define(MinusEqual, "-="); + Define(PlusEqual, "+="); + Define(Decrement, "++"); + Define(Increment, "--"); + Define(Slash, "/"); + Define(SlashEqual, "/="); + Define(Star, "*"); + Define(StarEqual, "*="); + Define(Modulo, "%"); + Define(ModuloEqual, "%="); + Define(BitwiseAnd, "&"); + Define(BitwiseOr, "|"); + Define(BitwiseXor, "^"); + Define(BitwiseNot, "~"); + Define(BitwiseLeft, "<<"); + Define(BitwiseRight, ">>"); + Define(Bang, "!"); + Define(BangEqual, "!="); + Define(Equal, "="); + Define(EqualEqual, "=="); + Define(Greater, ">"); + Define(GreaterEqual, ">="); + Define(Less, "<"); + Define(LessEqual, "<="); + Define(Question, "?"); + Define(DoubleQuestion, "??"); + Define(Identifier, "([_a-zA-Z][_a-zA-Z0-9]+?)"); + Define(String, "(:?('.+')|\".+\")"); + Define(Integer, "(\\d+)"); + Define(Decimal, ""); + Define(Hexadecimal, "0x([a-fA-F0-9]+)"); + Define(And, "&&"); + Define(Else, "else"); + Define(False, "false"); + Define(For, "for"); + Define(If, "if"); + Define(Or, "||"); + Define(This, "this"); + Define(True, "true"); + Define(Var, "var"); + Define(While, "while"); + Define(Do, "do"); + Define(Return, "return"); + Define(Ref, "ref"); + Define(Function, "function"); + Define(Class, "class"); + Define(Super, "base"); + Define(TypeOf, "typeof"); + Define(Print, "print"); + Define(Import, "import"); + Define(Export, "export"); + } +} + +public class ClassicDialect : DefaultDialect +{ + public ClassicDialect() + { + Define(Null, "_"); + Define(ArrayAdd, "<+"); + Define(ArrayRemove, "->"); + Define(Equal, "<~"); + Define(This, ".~"); + Define(Var, "*~"); + Define(Return, "<:"); + Define(Ref, "*&"); + Define(Function, "fq"); + Define(Super, "^~"); + Define(TypeOf, "?:"); + Define(Print, "::"); + Define(Import, ""); + } +} diff --git a/Qrakhen.Qamp.Core/Tokenization/Reader.cs b/Qrakhen.Qamp.Core/Tokenization/Reader.cs new file mode 100644 index 0000000..0f37922 --- /dev/null +++ b/Qrakhen.Qamp.Core/Tokenization/Reader.cs @@ -0,0 +1,333 @@ +using Qrakhen.Qamp.Core.Logging; +using System.Text; +using System.Text.RegularExpressions; +using static Qrakhen.Qamp.Core.Tokenization.TokenType; + +namespace Qrakhen.Qamp.Core.Tokenization; + +internal static partial class ReaderPatterns +{ + [GeneratedRegex(@"((?:\d+)?\.(?:\d+))")] + public static partial Regex IsDecimal(); + + [GeneratedRegex(@"(\d+)")] + public static partial Regex IsInteger(); + + [GeneratedRegex(@"0x([a-fA-F0-9]+)")] + public static partial Regex IsHexadecimal(); + + public static readonly Dictionary Keywords = new(); + + static ReaderPatterns() + { + Keywords["false"] = False; + Keywords["true"] = True; + Keywords["null"] = Null; + Keywords["and"] = And; + Keywords["else"] = Else; + Keywords["for"] = For; + Keywords["if"] = If; + Keywords["or"] = Or; + Keywords["this"] = This; + Keywords["var"] = Var; + Keywords["while"] = While; + Keywords["do"] = Do; + Keywords["typeof"] = TypeOf; + Keywords["ref"] = Ref; + Keywords["function"] = Function; + Keywords["return"] = Return; + Keywords["class"] = Class; + Keywords["super"] = Super; + Keywords["print"] = Print; + Keywords["import"] = Import; + Keywords["export"] = Export; + } +} + +public class Reader : IReader, IDisposable +{ + private const char EofByte = (char)0xFF; + private const int ChunkSize = 0x20; + + private readonly Stream _stream; + private readonly Tokens _tokens; + + private readonly List _buffer; + + private readonly ILogger _logger = LoggerService.Get(); + + private int _line = 0; + private int _column = 0; + private int _current = 0; + private long _startIndex = 0; + private TokenPosition _startPosition = default; + + public bool Done => _stream.Position >= _stream.Length && + _current >= _buffer.Count; + + public TokenPosition CurrentPosition => new TokenPosition(_line, _column); + + public Reader(Stream stream) + { + _stream = stream; + _stream.Position = 0; + _tokens = new Tokens(); + _buffer = []; + } + + public void Dispose() + { + _stream.Dispose(); + } + + public Token NextToken(bool includeCompilationIrrelevant = false) + { + if (Done) + return Token.Eof(); + + Token token = ReadToken(); + + while (!includeCompilationIrrelevant && token.IsCompilationIrrelevant && !Done) + token = ReadToken(); + + if (!includeCompilationIrrelevant && token.IsCompilationIrrelevant && Done) + return Token.Eof(); + + return token; + } + + private char Next() + { + if (!Done && _current >= _buffer.Count) + Continue(); + if (Done) + return EofByte; + _column++; + return _buffer[_current++]; + } + + private char Peek(int delta = 0) + { + if (_current + delta >= _buffer.Count) + Continue(); + if (_current + delta >= _buffer.Count) + return EofByte; + return _buffer[_current + delta]; + } + + private Token ReadToken() + { + _startIndex = _current; + _startPosition = CurrentPosition; + string buffer = string.Empty; + char c = Next(); + buffer += c; + + if (c == '\n' || c == '\r') { + var position = CurrentPosition; + if (c == '\r') + buffer += Next(); + _line++; + _column = 0; + return MakeToken(NewLine, buffer); + } + + if (c == '#') { + var position = CurrentPosition; + do { + c = Next(); + buffer += c; + } while (c != '\n' && c != EofByte); + return MakeToken(Comment, buffer); + } + + if (c == ' ') { + var position = CurrentPosition; + while (Match(' ')) + buffer += ' '; + return MakeToken(Whitespace, buffer); + } + + if (c is '\'' or '"') { + string str = ReadUntil(buffer, c); + if (str[^1] != c || str.Length < 2) + return MakeToken(Error, str); + return MakeToken(TokenType.String, str.Substring(1, str.Length - 2)); + } + + if (IsDigit(c) || (c == '.' && IsDigit(Peek()))) { + do { + c = Peek(0); + if (IsDigit(c) || IsHex(c) || c == 'x' || c == '.') + buffer += Next(); + else + break; + } while (c != EofByte); + if (ReaderPatterns.IsDecimal().TryMatch(buffer, out Match match)) + return MakeToken(TokenType.Decimal, match.Groups[1].Value); + if (ReaderPatterns.IsHexadecimal().TryMatch(buffer, out match)) + return MakeToken(Hexadecimal, match.Groups[1].Value); + if (ReaderPatterns.IsInteger().TryMatch(buffer, out match)) + return MakeToken(Integer, match.Groups[1].Value); + throw new ReaderException($"Unrecognizable number detected <{buffer}>", this); + } + + if (IsLetter(c)) { + do { + c = Peek(); + if (IsLetter(c)) + buffer += Next(); + else + break; + } while (c != EofByte); + return MakeIdentifier(buffer); + } + + return MakeOperator(buffer); + } + + private string ReadUntil(string buffer, char until) + { + char c; + do { + c = Next(); + if (c == EofByte) + return buffer; + /*throw new ReaderException( + $"Unexpected end of feed while trying to read string <{buffer}>", + this);*/ + buffer += c; + } while (c != until || Peek(-2) == '\\'); + return buffer; + } + + private Token MakeIdentifier(string buffer) + { + if (ReaderPatterns.Keywords.TryGetValue(buffer, out TokenType type)) + return MakeToken(type, buffer); + return MakeToken(Identifier, buffer); + } + + private bool Check(char c) => Peek(0) == c; + + private bool Match(char c) + { + if (!Check(c)) + return false; + + Next(); + return true; + } + + private bool MatchSequence(char[] sequence, string buffer, TokenType type, out Token token) + { + token = Token.Void; + string _buffer = buffer; + for (int i = 0; i < sequence.Length; i++) { + char c = buffer.Length > i ? buffer[i] : Peek(i - buffer.Length); + if (sequence[i] != c) + return false; + _buffer += c; + } + + token = MakeToken(type, _buffer); + return true; + } + + private bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or '_'; + + private bool IsDigit(char c) => c is >= '0' and <= '9'; + + private bool IsHex(char c) => c is >= 'a' and <= 'f' or >= 'A' and <= 'F' || IsDigit(c); + + private Token MakeOperator(string buffer) + { + return buffer[0] switch { + '[' => MakeToken(ArrayOpen, buffer), + ']' => MakeToken(ArrayClose, buffer), + '{' => MakeToken(ContextOpen, buffer), + '}' => MakeToken(ContextClose, buffer), + '(' => MakeToken(GroupOpen, buffer), + ')' => MakeToken(GroupClose, buffer), + '.' => MakeToken(Dot, buffer), + ',' => MakeToken(Comma, buffer), + ';' => MakeToken(Semicolon, buffer), + ':' => MakeToken(Colon, buffer), + '&' => Check('&') ? + MakeToken(And, buffer + Next()) : + MakeToken(BitwiseAnd, buffer), + '^' => MakeToken(BitwiseXor, buffer), + '%' => Check('=') ? + MakeToken(ModuloEqual, buffer + Next()) : + MakeToken(Modulo, buffer), + '|' => Check('|') ? + MakeToken(Or, buffer + Next()) : + MakeToken(BitwiseOr, buffer), + '!' => Check('=') ? + MakeToken(BangEqual, buffer + Next()) : + MakeToken(Bang, buffer), + '+' => Check('+') ? + MakeToken(Increment, buffer + Next()) : + Check('=') ? + MakeToken(PlusEqual, buffer + Next()) : + MakeToken(Plus, buffer), + '-' => Check('-') ? + MakeToken(Decrement, buffer + Next()) : + Check('=') ? + MakeToken(MinusEqual, buffer + Next()) : + MakeToken(Minus, buffer), + '/' => Check('=') ? + MakeToken(SlashEqual, buffer + Next()) : + MakeToken(Slash, buffer), + '*' => Check('=') ? + MakeToken(StarEqual, buffer + Next()) : + MakeToken(Star, buffer), + '=' => Check('=') ? + MakeToken(EqualEqual, buffer + Next()) : + MakeToken(Equal, buffer), + '<' => Check('<') ? + MakeToken(BitwiseLeft, buffer + Next()) : + Check('=') ? + MakeToken(LessEqual, buffer + Next()) : + MakeToken(Less, buffer), + '>' => Check('>') ? + MakeToken(BitwiseRight, buffer + Next()) : + Check('=') ? + MakeToken(GreaterEqual, buffer + Next()) : + MakeToken(Greater, buffer), + '~' => MakeToken(BitwiseNot, buffer), + '?' => Check('?') ? + MakeToken(DoubleQuestion, buffer + Next()) : + MakeToken(Question, buffer), + _ => MakeToken(Error, buffer) //throw new ReaderException($"Could not identify operator <{buffer}>", this) + }; + } + + private Token MakeToken(TokenType type, string buffer) + { + return new Token( + type, + buffer, + _startPosition, + new StreamSpan(_startIndex, _current - _startIndex)); + } + + private void Continue() + { + if (Done) + return; + long size = _stream.Length - _stream.Position; + if (size > ChunkSize) + size = ChunkSize; + byte[] buffer = new byte[size]; + int read = _stream.Read(buffer, 0, (int)size); + if (read < 0) + return; + _buffer.AddRange(Encoding.UTF8.GetChars(buffer)); + } + + private readonly struct CaptureHandle(long start) + { + public readonly long Start = start; + } +} diff --git a/Qrakhen.Qamp.Core/Tokenization/Token.cs b/Qrakhen.Qamp.Core/Tokenization/Token.cs new file mode 100644 index 0000000..2bf8d9f --- /dev/null +++ b/Qrakhen.Qamp.Core/Tokenization/Token.cs @@ -0,0 +1,42 @@ +namespace Qrakhen.Qamp.Core.Tokenization; + +public readonly record struct Token( + TokenType Type, + string? Value = "", + TokenPosition Position = default, + StreamSpan Span = default +) +{ + public static Token Void = new(TokenType.Void); + public static Token Error(string? message = null) => new Token(TokenType.Error, message); + public static Token Eof(string? message = null) => new Token(TokenType.Eof, message); + + public override string ToString() + { + return $"[@{Position}] <{Type}> ({Value ?? "?"})"; + } + + public bool IsCompilationIrrelevant => Type == TokenType.NewLine || Type == TokenType.Comment || Type == TokenType.Whitespace; +} + +public readonly record struct StreamSpan(long Start = -1, long Length = 0); + +public readonly record struct TokenPosition(int Line = -1, int Column = -1) +{ + public static readonly TokenPosition None = new(-1, -1); + + public bool IsNone => this == None; + + public override string ToString() + { + return $"{Line}:{Column}"; + } + + public TokenPosition Previous => new(Line, Column - 1); + + public static TokenPosition operator +(TokenPosition a, TokenPosition b) => + new(a.Line + b.Line, a.Column + b.Column); + + public static TokenPosition operator -(TokenPosition a, TokenPosition b) => + new(a.Line - b.Line, a.Column - b.Column); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Tokenization/TokenType.cs b/Qrakhen.Qamp.Core/Tokenization/TokenType.cs new file mode 100644 index 0000000..5793740 --- /dev/null +++ b/Qrakhen.Qamp.Core/Tokenization/TokenType.cs @@ -0,0 +1,126 @@ +namespace Qrakhen.Qamp.Core.Tokenization; + +using static TokenType; + +public enum TokenType +{ + Error, + Void, + Null, + + Eof, + NewLine, + Whitespace, + Comment, + + Bracket = 1 << 8, + GroupOpen = Bracket | 1, + GroupClose = Bracket | 2, + + ContextOpen = Bracket | 3, + ContextClose = Bracket | 4, + + ArrayOpen = Bracket | 5, + ArrayClose = Bracket | 6, + + ArrayAdd, + ArrayRemove, + + Comma, + Dot, + Colon, + Semicolon, + + Minus, + Plus, + MinusEqual, + PlusEqual, + Decrement, + Increment, + + Slash, + SlashEqual, + Star, + StarEqual, + Modulo, + ModuloEqual, + + BitwiseAnd, + BitwiseOr, + BitwiseXor, + BitwiseNot, + + BitwiseLeft, + BitwiseRight, + + Bang, + BangEqual, + Equal, + EqualEqual, + Greater, + GreaterEqual, + Less, + LessEqual, + + Question, + DoubleQuestion, + + Identifier, + String, + Integer, + Decimal, + Hexadecimal, + + And, + Else, + False, + For, + If, + Or, + This, + True, + Var, + While, + Do, + Return, + + Ref, + Function, + Class, + Super, + + TypeOf, + Print, + + Import, + Export +} + +public static class TokenTypeExtensions +{ + public static bool IsBracket(this TokenType type) + => type is GroupClose or GroupOpen or ContextClose or ContextOpen or ArrayClose or ArrayOpen; + + public static bool IsString(this TokenType type) + => type is TokenType.String; + + public static bool IsNumber(this TokenType type) + => type is Integer or Decimal; + + public static bool IsIdentifier(this TokenType type) + => type is Identifier; + + public static bool IsControl(this TokenType type) + => type is If or Else or For or While or Do or Return or And or Or; + + public static bool IsBoolean(this TokenType type) + => type is True or False; + + public static bool IsOperator(this TokenType type) + => !type.IsBracket() && + !type.IsString() && + !type.IsNumber() && + !type.IsIdentifier() && + !type.IsControl() && + !type.IsBoolean(); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Tokenization/Tokens.cs b/Qrakhen.Qamp.Core/Tokenization/Tokens.cs new file mode 100644 index 0000000..f57f372 --- /dev/null +++ b/Qrakhen.Qamp.Core/Tokenization/Tokens.cs @@ -0,0 +1,20 @@ +namespace Qrakhen.Qamp.Core.Tokenization; + +public class Tokens : IDebug +{ + private readonly List _tokens = new(); + + public Token[] GetTokens() => _tokens.ToArray(); + + public void Feed(Token token) => _tokens.Add(token); + + public string Debug(DebugLevel level = DebugLevel.None) + { + string str = Debugger.GetContextString(this); + foreach (var token in _tokens) + { + str += $"\n {token}"; + } + return str; + } +} diff --git a/Qrakhen.Qamp.Core/Values/Address.cs b/Qrakhen.Qamp.Core/Values/Address.cs new file mode 100644 index 0000000..ce667ff --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Address.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Qrakhen.Qamp.Core.Values; + +[StructLayout(LayoutKind.Sequential, Size = sizeof(long))] +public readonly record struct Address(long Value) +{ + public override string ToString() => $"0x{Value:x8}"; + + public static implicit operator long(Address address) => address.Value; + public static implicit operator Address(long value) => new(value); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Objects/Array.cs b/Qrakhen.Qamp.Core/Values/Objects/Array.cs new file mode 100644 index 0000000..312f80f --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Objects/Array.cs @@ -0,0 +1,8 @@ +namespace Qrakhen.Qamp.Core.Values.Objects; + +public class Array(IEnumerable data) : Obj(ValueType.Array) +{ + public List Data = [..data]; + + public override string ToString() => $"Array<{Data.Count}>"; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Objects/Closure.cs b/Qrakhen.Qamp.Core/Values/Objects/Closure.cs new file mode 100644 index 0000000..ded090f --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Objects/Closure.cs @@ -0,0 +1,12 @@ +using Qrakhen.Qamp.Core.Collections; +using Qrakhen.Qamp.Core.Execution; + +namespace Qrakhen.Qamp.Core.Values.Objects; + +public class Closure(Function function) : Obj(ValueType.Context) +{ + public Function Function = function; + public StackRegister PreValues = new(); + + public override string ToString() => "{}"; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Objects/Function.cs b/Qrakhen.Qamp.Core/Values/Objects/Function.cs new file mode 100644 index 0000000..3b117de --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Objects/Function.cs @@ -0,0 +1,13 @@ +using Qrakhen.Qamp.Core.Execution; + +namespace Qrakhen.Qamp.Core.Values.Objects; + +public class Function(string name, Segment segment, int outerCount, int argumentCount) : Obj(ValueType.Function) +{ + public readonly string Name = name; + public Segment Segment = segment; + public readonly int OuterCount = outerCount; + public readonly int ArgumentCount = argumentCount; + + public override string ToString() => $"{name}()"; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Objects/Obj.cs b/Qrakhen.Qamp.Core/Values/Objects/Obj.cs new file mode 100644 index 0000000..9f92401 --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Objects/Obj.cs @@ -0,0 +1,15 @@ +namespace Qrakhen.Qamp.Core.Values.Objects; + +public class Obj(ValueType type) : IValue +{ + public readonly ValueType Type = type; + + public ValueType ValueType => Type; + + public static Value Create(Obj obj) + { + return new Value(new Ptr(obj)); + } + + public override string ToString() => "Obj"; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Objects/String.cs b/Qrakhen.Qamp.Core/Values/Objects/String.cs new file mode 100644 index 0000000..f7b04a6 --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Objects/String.cs @@ -0,0 +1,13 @@ +namespace Qrakhen.Qamp.Core.Values.Objects; + +public class String(string? value) : Obj(ValueType.String) +{ + public string? Value = value; + + public static Value Make(string? value) + { + return Create(new String(value)); + } + + public override string ToString() => Value ?? "null"; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Objects/TypeWrapper.cs b/Qrakhen.Qamp.Core/Values/Objects/TypeWrapper.cs new file mode 100644 index 0000000..2274d81 --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Objects/TypeWrapper.cs @@ -0,0 +1,6 @@ +namespace Qrakhen.Qamp.Core.Values.Objects; + +public class TypeWrapper() : Obj(ValueType.Undefined) +{ + +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Ptr.cs b/Qrakhen.Qamp.Core/Values/Ptr.cs new file mode 100644 index 0000000..ffb4e30 --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Ptr.cs @@ -0,0 +1,35 @@ +using Qrakhen.Qamp.Core.Values.Objects; +using System.Runtime.InteropServices; +using Qrakhen.Qamp.Core.Collections; + +namespace Qrakhen.Qamp.Core.Values; + +[StructLayout(LayoutKind.Sequential, Size = sizeof(ulong))] +public readonly struct Ptr +{ + private static readonly StackRegister _register = new(0x10); + + [Serialized] public readonly Address Address; + + //private readonly IntPtr _pointer; + //public IntPtr Pointer => _pointer; + + public Obj? Value { + get { + if (_register.TryGet(Address, out Obj obj)) + return obj; + return null; + //if (_pointer == IntPtr.Zero) + // throw new ObjectDisposedException(nameof(Ptr)); + //return (Obj)GCHandle.FromIntPtr(_pointer).Target!; + } + } + + public Ptr(Obj obj) + { + //_pointer = GCHandle.ToIntPtr(GCHandle.Alloc(obj, GCHandleType.Normal)); + Address = _register.Add(obj); + } + + public override string ToString() => $"0x{Value}"; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Ref.cs b/Qrakhen.Qamp.Core/Values/Ref.cs new file mode 100644 index 0000000..85d05a5 --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Ref.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Qrakhen.Qamp.Core.Values; + +[StructLayout(LayoutKind.Sequential, Size = sizeof(ulong))] +public readonly unsafe struct Ref(Value* value) +{ + [Serialized] public readonly Value* Value = value; + + public override string ToString() => $"0x{*Value}"; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/Value.cs b/Qrakhen.Qamp.Core/Values/Value.cs new file mode 100644 index 0000000..f0c1edb --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/Value.cs @@ -0,0 +1,124 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Qrakhen.Qamp.Core.Values.Objects; +using String = Qrakhen.Qamp.Core.Values.Objects.String; +using T = Qrakhen.Qamp.Core.Values.ValueType; + +namespace Qrakhen.Qamp.Core.Values; + +public interface IValue +{ + ValueType ValueType { get; } +} + +[StructLayout(LayoutKind.Explicit, Size = 0x10)] +public readonly struct Value : IValue, IDebug +{ + public static readonly Value Void = new Value(); + + [FieldOffset(0x00)] [Serialized] public readonly ulong Unsigned; + [FieldOffset(0x00)] [Serialized] public readonly long Signed; + [FieldOffset(0x00)] [Serialized] public readonly bool Bool; + [FieldOffset(0x00)] [Serialized] public readonly char Char; + [FieldOffset(0x00)] [Serialized] public readonly double Decimal; + [FieldOffset(0x00)] [Serialized] public readonly Address Address; + [FieldOffset(0x00)] [Serialized] public readonly Ptr Ptr; + [FieldOffset(0x00)] [Serialized] public readonly Ref Ref; + + [FieldOffset(0x08)] [Serialized] public readonly T Type; + + [FieldOffset(0x0c)] [Serialized] public readonly uint Meta; // like lengths of arrays or lists + + public T ValueType => Type; + + private Value(T type) => Type = type; + + public Value() : this(T.Void) { } + public Value(ulong unsigned) : this(T.Unsigned) => Unsigned = unsigned; + public Value(long signed) : this(T.Signed) => Signed = signed; + public Value(char character) : this(T.Char) => Char = character; + public Value(bool boolean) : this(T.Bool) => Bool = boolean; + public Value(double @decimal) : this(T.Decimal) => Decimal = @decimal; + public Value(Address address) : this(T.Address) => Address = address; + public Value(Ptr ptr) : this(ptr.Value?.Type ?? T.Pointer) => Ptr = ptr; + public Value(Ref @ref) : this(T.Reference) => Ref = @ref; + + public dynamic? Dynamic => AsDynamic(); + + public bool Is(T type, bool exact = true) + { + return exact ? (Type & type) == type : (Type & type) > 0; + } + + private unsafe dynamic? AsDynamic(bool throwWhenNull = true) + { + if (Type.HasFlag(T.Object)) + return Ptr.Value; + + return Type switch { + T.Void => throwWhenNull ? throw new NullReferenceException() : null, + T.Unsigned => Unsigned, + T.Signed => Signed, + T.Decimal => Decimal, + T.Bool => Bool, + T.Address => Address, + T.Reference => (Value)(*Ref.Value), + _ => throwWhenNull ? throw new NullReferenceException() : null, + }; + } + + public static Value FromAny(object? any = null) + { + if (any == null) + return new Value(); + + Type type = any.GetType(); + + return new Value(); + } + + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj switch { + Value value when Type == value.Type => Unsigned == value.Unsigned, + Value value => Dynamic?.Equals(value.Dynamic), + _ => false + }; + } + + public override int GetHashCode() => Unsigned.GetHashCode(); + + public override unsafe string ToString() + { + string type = Type.ToString(); + if (Type.HasFlag(T.Pointer)) + type = Ptr.Value?.Type.ToString() ?? "NullPtr"; + + return $"[{type}, {AsDynamic(false) ?? "null"}]"; + } + + public string Debug(DebugLevel level = DebugLevel.None) + { + if (level < DebugLevel.Strong) + return Debugger.GetContextString(this); + + string str = $"{Debugger.GetContextString(this)}\n"; + + string[] lines = + [ + $"{nameof(Meta)}: {Meta}", + $"{nameof(Unsigned)}: {Unsigned}", + $"{nameof(Signed)}: {Signed}", + $"{nameof(Bool)}: {Bool}", + $"{nameof(Char)}: {Char}", + $"{nameof(Decimal)}: {Decimal}", + $"{nameof(Address)}: {Address}", + $"{nameof(Ref)}: {Ref}", + $"{nameof(Ptr)}: {Ptr}" + ]; + + return str + string.Join("\n - ", lines); + } +} + +public class SerializedAttribute : Attribute; \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/ValueExtensions.cs b/Qrakhen.Qamp.Core/Values/ValueExtensions.cs new file mode 100644 index 0000000..d0c4a43 --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/ValueExtensions.cs @@ -0,0 +1,11 @@ +using Qrakhen.Qamp.Core.Execution; + +namespace Qrakhen.Qamp.Core.Values; + +internal static class ValueExtensions +{ + public static Value ExecuteOperation(this Value value, Value other, OpCode operation) + { + return new Value(); + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/ValueOperation.cs b/Qrakhen.Qamp.Core/Values/ValueOperation.cs new file mode 100644 index 0000000..1bd3ef9 --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/ValueOperation.cs @@ -0,0 +1,121 @@ +using Qrakhen.Qamp.Core.Logging; + +namespace Qrakhen.Qamp.Core.Values; + +public static class ValueOperation +{ + private static ILogger _logger = LoggerService.Get(nameof(ValueOperation)); + + public static Value Not(Value v) + { + _logger.Method($"{v}"); + Assert.NotVoid(v); + ulong inner = v.Unsigned; + return new Value(inner == 0); + } + + public static Value Add(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + bool convert = a.Type != b.Type; // meh, lets make c# do that for us for now + return new Value(a.Dynamic + b.Dynamic); + } + + public static Value Subtract(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + return new Value(a.Dynamic - b.Dynamic); + } + + public static Value Divide(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + Assert.NotZero(b); + return new Value(a.Dynamic / b.Dynamic); + } + + public static Value Modulo(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + Assert.NotZero(b); + return new Value(a.Dynamic % b.Dynamic); + } + + public static Value Multiply(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + return new Value(a.Dynamic * b.Dynamic); + } + + public static Value Negate(Value a) + { + _logger.Method($"{a}"); + Assert.NotVoid(a); + Assert.IsPrimitive(a); + return new Value(-a.Dynamic); + } + + public static Value BitwiseOr(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + Assert.IsInteger(a, b); + return new Value(a.Dynamic | b.Dynamic); + } + + public static Value BitwiseAnd(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + Assert.IsInteger(a, b); + return new Value(a.Dynamic & b.Dynamic); + } + + public static Value BitwiseXor(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + Assert.IsInteger(a, b); + return new Value(a.Dynamic ^ b.Dynamic); + } + + public static Value BitwiseLeft(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + Assert.IsInteger(a, b); + return new Value(a.Dynamic << (int)(b.Dynamic ?? 0)); + } + + public static Value BitwiseRight(Value a, Value b) + { + _logger.Method($"{a}, {b}"); + Assert.NotVoid(a, b); + Assert.IsPrimitive(a, b); + Assert.IsInteger(a, b); + return new Value(a.Dynamic >> (int)(b.Dynamic ?? 0)); + } + + public static Value BitwiseInvert(Value a) + { + _logger.Method($"{a}"); + Assert.NotVoid(a); + Assert.IsPrimitive(a); + Assert.IsInteger(a); + return new Value(~a.Dynamic); + } +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Core/Values/ValueType.cs b/Qrakhen.Qamp.Core/Values/ValueType.cs new file mode 100644 index 0000000..97090ba --- /dev/null +++ b/Qrakhen.Qamp.Core/Values/ValueType.cs @@ -0,0 +1,35 @@ +namespace Qrakhen.Qamp.Core.Values; + +[Flags] +public enum ValueType +{ + Undefined = -1, + Void = 0x0000, + Unsigned = 0x0001, + Signed = 0x0002, + Char = 0x0004, + Decimal = 0x0008, + Bool = 0x0010, + + Integer = Unsigned | Signed | Char, + Primitive = Integer | Decimal | Bool, + + Address = 0x0020, // coded with : symbol + + // classes etc. here? + + Reference = 0x1000, + Pointer = 0x2000, + Object = Pointer, // coded with & symbol + Native = Object | 0x0001, + String = Object | 0x0002, + Array = Object | 0x0004, + Function = Object | 0x0008, + Context = Object | 0x0010, + Instance = Object | 0x0020, + Class = Object | 0x0100, + Method = Class | Function, + PreValue = Object | 0x0200, + + Module = 0xFFFF +} \ No newline at end of file diff --git a/Qrakhen.Qamp.sln b/Qrakhen.Qamp.sln new file mode 100644 index 0000000..b5d19c9 --- /dev/null +++ b/Qrakhen.Qamp.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11123.170 d18.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qrakhen.Qamp.Core", "Qrakhen.Qamp.Core\Qrakhen.Qamp.Core.csproj", "{CA6F4D4E-AF35-4CA6-BC53-C83277EEE46C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qrakhen.Qamp.CLI", "Qrakhen.Qamp.CLI\Qrakhen.Qamp.CLI.csproj", "{1D355F76-9D35-4CB2-BF8C-944B0842BC30}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CA6F4D4E-AF35-4CA6-BC53-C83277EEE46C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA6F4D4E-AF35-4CA6-BC53-C83277EEE46C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA6F4D4E-AF35-4CA6-BC53-C83277EEE46C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA6F4D4E-AF35-4CA6-BC53-C83277EEE46C}.Release|Any CPU.Build.0 = Release|Any CPU + {1D355F76-9D35-4CB2-BF8C-944B0842BC30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D355F76-9D35-4CB2-BF8C-944B0842BC30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D355F76-9D35-4CB2-BF8C-944B0842BC30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D355F76-9D35-4CB2-BF8C-944B0842BC30}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {87910094-C957-4430-AF2E-4586EC943D0A} + EndGlobalSection +EndGlobal diff --git a/Qrakhen.Qamp.sln.DotSettings b/Qrakhen.Qamp.sln.DotSettings new file mode 100644 index 0000000..f1c3d6f --- /dev/null +++ b/Qrakhen.Qamp.sln.DotSettings @@ -0,0 +1,35 @@ + + False + True + True + True + True + 0 + 1 + 0 + 1 + 2 + 0 + 1 + 2 + 1 + 0 + 1 + 0 + 1 + 3 + 0 + 2 + 0 + 2 + 1 + 1 + 0 + 1 + 0 + 0 + 1 + 0 + 1 + 0 + True \ No newline at end of file