using System; using System.Collections.Generic; using System.IO; namespace BrRun.Interpreters; public class StandardBrInterpreter : BrInterpreterBase { // Can technically be changed, but it should remain the same to match convention. public static readonly short TapeMinValue = 0, TapeMaxValue = 255; public static readonly int TapeLength = 30_000; protected FileStream? reader; protected readonly short[] tape; protected int dataPointer; protected Stack stack; protected Stack<(int, int)> debugStack; private int lineNumber; private int charNumber; private char curChar; private int awakeTop = 0; public StandardBrInterpreter(string filePath, BrInterpretContext context) : base(filePath, context) { reader = null; tape = new short[TapeLength]; dataPointer = 0; stack = []; debugStack = []; lineNumber = 1; charNumber = 0; } public override void Interpret() { awakeTop = Console.CursorTop; if (Context.stepFlag) { for (int i = 0; i < 12; i++) Console.WriteLine(); awakeTop = Console.CursorTop - 12; } reader = new(FilePath, FileMode.Open); IntentionKind intent; while ((intent = StepProgram()) != IntentionKind.EndOfFile) { if (Context.stepFlag) ShowDebugScreen(intent); HandleIntent(intent); } reader.Close(); Console.CursorVisible = true; } protected void HandleIntent(IntentionKind intent) { switch (intent) { case IntentionKind.IncrementPointer: dataPointer++; if (dataPointer >= TapeLength) { Console.WriteLine($"warn: data pointer has overflowed! (length {TapeLength})"); dataPointer = 0; } break; case IntentionKind.DecrementPointer: dataPointer--; if (dataPointer < 0) { Console.WriteLine($"warn: data pointer has underflowed! (length {TapeLength})"); dataPointer = TapeLength - 1; } break; case IntentionKind.IncrementValue: if (tape[dataPointer] == TapeMaxValue) tape[dataPointer] = TapeMinValue; else tape[dataPointer]++; break; case IntentionKind.DecrementValue: if (tape[dataPointer] == TapeMinValue) tape[dataPointer] = TapeMaxValue; else tape[dataPointer]--; break; case IntentionKind.OutputValue: Console.Write((char)tape[dataPointer]); break; case IntentionKind.InputValue: Console.CursorVisible = true; tape[dataPointer] = (byte)Console.ReadKey().KeyChar; break; case IntentionKind.BeginGroup: if (reader is null) Console.WriteLine("error: file hasn't been opened yet! how did this happen?"); else { stack.Push(reader.Position); debugStack.Push((lineNumber, charNumber)); } break; case IntentionKind.EndGroup: if (stack.Count == 0) { Console.WriteLine("error: no opening bracket to match closing bracket."); return; } if (tape[dataPointer] == 0) { // Exit loop. stack.Pop(); debugStack.Pop(); } else { // Restart. if (reader is null) Console.WriteLine("error: file hasn't been opened yet! how did this happen?"); else reader.Seek(stack.Peek(), SeekOrigin.Begin); (int newL, int newC) = debugStack.Peek(); lineNumber = newL; charNumber = newC; } break; default: Console.WriteLine("warn: unknown intent! how did this happen?"); break; } } private int remainingSkips = 0; protected void ShowDebugScreen(IntentionKind currentIntent) { const int numSpace = 3; Console.CursorVisible = false; int initialTop = Console.CursorTop, initialLeft = Console.CursorLeft; int totalCanFit = (Console.WindowWidth - 1) / (numSpace + 3); int startIndex = int.Max(0, (int)(dataPointer - totalCanFit * 0.5)), endIndex = int.Min(startIndex + totalCanFit - 1, TapeLength - 1); int consolePos = 0; for (int i = startIndex; i <= endIndex; i++) { Console.SetCursorPosition(consolePos, awakeTop + 1); Console.Write($" {i,numSpace + 2}"); Console.SetCursorPosition(consolePos, awakeTop + 2); if (i == startIndex) Console.Write("╔═════"); else Console.Write("╦═════"); Console.SetCursorPosition(consolePos, awakeTop + 3); Console.Write($"║ {tape[i],numSpace} "); Console.SetCursorPosition(consolePos, awakeTop + 4); if (i == startIndex) Console.Write("╚═════"); else Console.Write("╩═════"); Console.SetCursorPosition(consolePos, awakeTop + 5); if (i == dataPointer) Console.Write(" ^ "); else Console.Write(" "); Console.SetCursorPosition(consolePos, awakeTop + 9); Console.Write($"──────"); consolePos += numSpace + 3; } Console.SetCursorPosition(consolePos, awakeTop + 2); Console.Write('╗'); Console.SetCursorPosition(consolePos, awakeTop + 3); Console.Write('║'); Console.SetCursorPosition(consolePos, awakeTop + 4); Console.Write('╝'); Console.SetCursorPosition(consolePos, awakeTop + 9); Console.Write('─'); Console.SetCursorPosition(0, awakeTop + numSpace + 3); string message = $"L{lineNumber} C{charNumber}: {curChar} {GetDebugDescriptionOfOperator(currentIntent)} "; Console.Write(message + new string(' ', int.Max(Console.WindowWidth - message.Length - 1, 0))); if (remainingSkips < int.MaxValue - 1) remainingSkips--; if (remainingSkips == int.MaxValue - 1 && currentIntent == IntentionKind.EndGroup && tape[dataPointer] == 0) remainingSkips = 0; Console.SetCursorPosition(0, awakeTop + 7); if (currentIntent == IntentionKind.InputValue) { Console.Write("Enter one character to step the program." + new string(' ', 70)); } else { if (remainingSkips > 0) { string message2; if (remainingSkips == int.MaxValue) message2 = $"Continuing program to completion..."; else if (remainingSkips == int.MaxValue - 1) message2 = $"Waiting for loop exit..."; else message2 = $"Skipping {remainingSkips} steps..."; Console.Write(message2 + new string(' ', 110 - message2.Length)); } else { Console.Write("Press space to step the system. D = +5 steps, F = +25 steps, G = +100 steps, J = until loop ends, K = continuous"); _readKey: ConsoleKeyInfo stepKey = Console.ReadKey(true); switch (stepKey.Key) { case ConsoleKey.Spacebar: remainingSkips++; break; case ConsoleKey.D: remainingSkips += 5; break; case ConsoleKey.F: remainingSkips += 25; break; case ConsoleKey.G: remainingSkips += 100; break; case ConsoleKey.J: remainingSkips = int.MaxValue - 1; break; case ConsoleKey.K: remainingSkips = int.MaxValue; break; default: goto _readKey; } } } Console.SetCursorPosition(initialLeft, initialTop); } private string GetDebugDescriptionOfOperator(IntentionKind intent) => intent switch { IntentionKind.IncrementPointer => $"Move pointer to right ({dataPointer} -> {dataPointer + 1})", IntentionKind.DecrementPointer => $"Move pointer to left ({dataPointer} -> {dataPointer - 1})", IntentionKind.IncrementValue => $"Increase value at position {dataPointer} ({tape[dataPointer]} -> {tape[dataPointer] + 1})", IntentionKind.DecrementValue => $"Decrease value at position {dataPointer} ({tape[dataPointer]} -> {tape[dataPointer] - 1})", IntentionKind.OutputValue => $"Print out current value as character (value {tape[dataPointer]})", IntentionKind.InputValue => $"Input next character input into position {dataPointer}", IntentionKind.BeginGroup => "Beginning a loop", IntentionKind.EndGroup => tape[dataPointer] == 0 ? $"Breaking out of group" : $"Moving execution back to L{debugStack.Peek().Item1} C{debugStack.Peek().Item2} until value at position {dataPointer} is zero (currently {tape[dataPointer]})", _ => "?? unknown intent ??" }; protected IntentionKind StepProgram() { if (reader is null) { if (reader is null) Console.WriteLine("error: file hasn't been opened yet! how did this happen?"); return IntentionKind.EndOfFile; } int cI = reader.ReadByte(); if (cI == -1) return IntentionKind.EndOfFile; char c = (char)cI; if (c == '\n') { lineNumber++; charNumber = 0; } else if (c != '\r') charNumber++; curChar = c; switch (c) { case '\r' or '\n' or ' ' or '\t': return StepProgram(); // Skip newlines. case '#': Console.WriteLine("error: comments are not supported in standard brainfuck."); return StepProgram(); case '>': return IntentionKind.IncrementPointer; case '<': return IntentionKind.DecrementPointer; case '+': return IntentionKind.IncrementValue; case '-': return IntentionKind.DecrementValue; case '.': return IntentionKind.OutputValue; case ',': return IntentionKind.InputValue; case '[': return IntentionKind.BeginGroup; case ']': return IntentionKind.EndGroup; default: Console.WriteLine($"error: unsupported operator {c}"); return StepProgram(); } } protected enum IntentionKind { EndOfFile, IncrementPointer, DecrementPointer, IncrementValue, DecrementValue, OutputValue, InputValue, BeginGroup, EndGroup } }