From cf116e919bf89f8a37d4a06fdfe3f53846b065cc Mon Sep 17 00:00:00 2001 From: Mate Farkas Date: Sun, 14 Jan 2024 15:12:42 +0100 Subject: [PATCH] Add project files. --- Frick.NET.sln | 34 ++++ src/Frick.NET.Cli/Frick.NET.Cli.csproj | 19 +++ src/Frick.NET.Cli/Program.cs | 55 ++++++ src/Frick.NET/CellOverflowBehaviour.cs | 21 +++ src/Frick.NET/CellValueOverflowBehaviour.cs | 21 +++ .../Exceptions/FrickCellOverflowException.cs | 12 ++ .../FrickCellValueOverflowException.cs | 12 ++ .../FrickUnbalancedBracketException.cs | 12 ++ src/Frick.NET/Frick.NET.csproj | 9 + src/Frick.NET/FrickInterpreter.cs | 159 ++++++++++++++++++ .../Internals/FrickInterperterState.cs | 137 +++++++++++++++ src/Frick.NET/Internals/Instructions.cs | 14 ++ 12 files changed, 505 insertions(+) create mode 100644 Frick.NET.sln create mode 100644 src/Frick.NET.Cli/Frick.NET.Cli.csproj create mode 100644 src/Frick.NET.Cli/Program.cs create mode 100644 src/Frick.NET/CellOverflowBehaviour.cs create mode 100644 src/Frick.NET/CellValueOverflowBehaviour.cs create mode 100644 src/Frick.NET/Exceptions/FrickCellOverflowException.cs create mode 100644 src/Frick.NET/Exceptions/FrickCellValueOverflowException.cs create mode 100644 src/Frick.NET/Exceptions/FrickUnbalancedBracketException.cs create mode 100644 src/Frick.NET/Frick.NET.csproj create mode 100644 src/Frick.NET/FrickInterpreter.cs create mode 100644 src/Frick.NET/Internals/FrickInterperterState.cs create mode 100644 src/Frick.NET/Internals/Instructions.cs diff --git a/Frick.NET.sln b/Frick.NET.sln new file mode 100644 index 0000000..033a27a --- /dev/null +++ b/Frick.NET.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0D330CE8-BC91-41CE-BDBA-9E2D704C4271}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frick.NET", "src\Frick.NET\Frick.NET.csproj", "{B36C9587-76FE-4D08-B2C6-556FCDB0F6A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frick.NET.Cli", "src\Frick.NET.Cli\Frick.NET.Cli.csproj", "{64922087-5D0E-4A35-8EFE-CC53955A8517}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B36C9587-76FE-4D08-B2C6-556FCDB0F6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B36C9587-76FE-4D08-B2C6-556FCDB0F6A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B36C9587-76FE-4D08-B2C6-556FCDB0F6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B36C9587-76FE-4D08-B2C6-556FCDB0F6A6}.Release|Any CPU.Build.0 = Release|Any CPU + {64922087-5D0E-4A35-8EFE-CC53955A8517}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64922087-5D0E-4A35-8EFE-CC53955A8517}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64922087-5D0E-4A35-8EFE-CC53955A8517}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64922087-5D0E-4A35-8EFE-CC53955A8517}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B36C9587-76FE-4D08-B2C6-556FCDB0F6A6} = {0D330CE8-BC91-41CE-BDBA-9E2D704C4271} + {64922087-5D0E-4A35-8EFE-CC53955A8517} = {0D330CE8-BC91-41CE-BDBA-9E2D704C4271} + EndGlobalSection +EndGlobal diff --git a/src/Frick.NET.Cli/Frick.NET.Cli.csproj b/src/Frick.NET.Cli/Frick.NET.Cli.csproj new file mode 100644 index 0000000..fed07d6 --- /dev/null +++ b/src/Frick.NET.Cli/Frick.NET.Cli.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + enable + enable + frick-cli + + + + + + + + + + + diff --git a/src/Frick.NET.Cli/Program.cs b/src/Frick.NET.Cli/Program.cs new file mode 100644 index 0000000..4643ad9 --- /dev/null +++ b/src/Frick.NET.Cli/Program.cs @@ -0,0 +1,55 @@ +using Frick.NET; + +try +{ + string? bfSource = null; + + if (args.Length > 0) + { + string argFlag = args[0].Trim(); + if (argFlag == "-i") + { + if (args.Length < 2) + { + Console.Error.WriteLine("The -i flag requires a file to be specified. Please pass a file name."); + return -1; + } + bfSource = File.ReadAllText(args[1]); + } + else + { + Console.Error.WriteLine("Unknown argument. Please use -i if you want to run a Brainfuck program from a file, or pass the source code using STDIN."); + return -1; + } + } + else + { + if (Console.IsInputRedirected) + { + using var inputStream = Console.OpenStandardInput(); + using var streamReader = new StreamReader(inputStream); + bfSource = streamReader.ReadToEnd(); + } + else + { + bfSource = Console.ReadLine(); + } + } + + if (string.IsNullOrWhiteSpace(bfSource)) + { + Console.Error.WriteLine("Brainfuck source code was empty. Please provide the source either using -i or using STDIN"); + return -1; + } + + FrickInterpreter interpreter = new(); + + interpreter.Run(bfSource); + + return 0; +} +catch (Exception ex) +{ + Console.Error.WriteLine($"Error while running the CLI: {ex.Message}"); + return -1; +} \ No newline at end of file diff --git a/src/Frick.NET/CellOverflowBehaviour.cs b/src/Frick.NET/CellOverflowBehaviour.cs new file mode 100644 index 0000000..c10e9f8 --- /dev/null +++ b/src/Frick.NET/CellOverflowBehaviour.cs @@ -0,0 +1,21 @@ +namespace Frick.NET +{ + /// + /// Represents how the interpreter should behave when encountering a cell pointer overflow. + /// + public enum CellOverflowBehaviour + { + /// + /// The instruction causing the overflow should be ignored. + /// + Ignore = 1, + /// + /// The pointer should wrap around in the other direction. + /// + WrapAround, + /// + /// The interpreter should throw an exception. + /// + ThrowException + } +} diff --git a/src/Frick.NET/CellValueOverflowBehaviour.cs b/src/Frick.NET/CellValueOverflowBehaviour.cs new file mode 100644 index 0000000..f82a9b8 --- /dev/null +++ b/src/Frick.NET/CellValueOverflowBehaviour.cs @@ -0,0 +1,21 @@ +namespace Frick.NET +{ + /// + /// Represents how the interpreter should behave when encountering a cell value overflow. + /// + public enum CellValueOverflowBehaviour + { + /// + /// The instruction causing the overflow should be ignored. + /// + Ignore = 1, + /// + /// The value should wrap around in the other direction. + /// + WrapAround, + /// + /// The interpreter should throw an exception. + /// + ThrowException + } +} diff --git a/src/Frick.NET/Exceptions/FrickCellOverflowException.cs b/src/Frick.NET/Exceptions/FrickCellOverflowException.cs new file mode 100644 index 0000000..a1d19c2 --- /dev/null +++ b/src/Frick.NET/Exceptions/FrickCellOverflowException.cs @@ -0,0 +1,12 @@ +namespace Frick.NET.Exceptions +{ + /// + /// Exception thrown when the cell pointer is moved outside of the bounds of the cell memory, and the interpreter is configured to produce an error. + /// + /// + [Serializable] + public class FrickCellOverflowException(int requestedCell) + : Exception($"Cell overflow error. The interpreter is configured to throw exceptions on cell overflow. The requested cell value: {requestedCell}") + { + } +} diff --git a/src/Frick.NET/Exceptions/FrickCellValueOverflowException.cs b/src/Frick.NET/Exceptions/FrickCellValueOverflowException.cs new file mode 100644 index 0000000..a277e13 --- /dev/null +++ b/src/Frick.NET/Exceptions/FrickCellValueOverflowException.cs @@ -0,0 +1,12 @@ +namespace Frick.NET.Exceptions +{ + + /// + /// Exception thrown when the cell value is set to a value not inside the range of an unsigned 8-bit number, and the interpreter is configured to produce an error. + /// + [Serializable] + public class FrickCellValueOverflowException(int requestedValue) + : Exception($"Cell calue overflow error. The interpreter is configured to throw exceptions on cell value overflow. The requested cell index: {requestedValue}") + { + } +} diff --git a/src/Frick.NET/Exceptions/FrickUnbalancedBracketException.cs b/src/Frick.NET/Exceptions/FrickUnbalancedBracketException.cs new file mode 100644 index 0000000..a597416 --- /dev/null +++ b/src/Frick.NET/Exceptions/FrickUnbalancedBracketException.cs @@ -0,0 +1,12 @@ +namespace Frick.NET.Exceptions +{ + /// + /// Exception thrown when the interpreter encounters an unbalanced (i.e. not closed) bracket. + /// + /// + [Serializable] + public class FrickUnbalancedBracketException(int bracketPos) + : Exception($"Unexpected EOF; Unbalanced loop instruction found at position {bracketPos}") + { + } +} diff --git a/src/Frick.NET/Frick.NET.csproj b/src/Frick.NET/Frick.NET.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/src/Frick.NET/Frick.NET.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/Frick.NET/FrickInterpreter.cs b/src/Frick.NET/FrickInterpreter.cs new file mode 100644 index 0000000..2115f20 --- /dev/null +++ b/src/Frick.NET/FrickInterpreter.cs @@ -0,0 +1,159 @@ +using Frick.NET.Exceptions; +using Frick.NET.Internals; + +namespace Frick.NET +{ + /// + /// The brainfuck language interpreter + /// + public class FrickInterpreter + { + // what opcodes are recognized + private static readonly HashSet _validInstructions = [ + Instructions.MOVE_POINTER_LEFT, + Instructions.MOVE_POINTER_RIGHT, + Instructions.INCREMENT_CELL, + Instructions.DECREMENT_CELL, + Instructions.OUTPUT_CELL, + Instructions.GET_INPUT, + Instructions.LOOP_START, + Instructions.LOOP_END, + ]; + + // the internal state of the brainfuck interpreters + // contains cells and pointer values + private readonly FrickInterperterState _state; + + /// + /// Constructs a brainfuck language interpreter with the given cell size and overflow behaviours + /// + /// How many cells are available for programs. Defaults to 2^15. Cannot be smaller than 1. + /// What should happen when a program tries to overflow the available cells. + /// What should happen when a program tries to overflow the value of a cell. + /// + public FrickInterpreter(int cellSize = 1 << 15, + CellOverflowBehaviour cellOverflowBehaviour = CellOverflowBehaviour.Ignore, + CellValueOverflowBehaviour cellValueOverflowBehaviour = CellValueOverflowBehaviour.WrapAround) + { + if (cellSize == 0) { throw new ArgumentException("Cell size cannot be smaller than 1.", nameof(cellSize)); } + + _state = new(cellSize, cellOverflowBehaviour, cellValueOverflowBehaviour); + } + + /// + /// Runs a brainfuck program given in the parameter. + /// + /// The program to run. + /// Flag to reset the internal state of the interpreter when running the source. + /// + /// + /// + /// + public void Run(string source, bool resetState = true) + { + if (string.IsNullOrWhiteSpace(source)) + { + throw new ArgumentNullException(nameof(source), "Source code cannot be null or empty."); + } + + if (resetState) { _state.Reset(); } + + // this is for handling starting indexes of loops in the source + Stack loopStack = new(); + + for (int idx = 0; idx < source.Length; idx++) + { + char c = source[idx]; + + // ignore non-valid tokens + if (!IsValidInstruction(c)) { continue; } + + switch (c) + { + case Instructions.MOVE_POINTER_RIGHT: + _state.MovePointerRight(); + break; + case Instructions.MOVE_POINTER_LEFT: + _state.MovePointerLeft(); + break; + case Instructions.INCREMENT_CELL: + _state.IncrementCell(); + break; + case Instructions.DECREMENT_CELL: + _state.DecrementCell(); + break; + case Instructions.OUTPUT_CELL: + { + byte currentCellValue = _state.GetCurrentValue(); + // TODO: generalize output + Console.Write((char)currentCellValue); + break; + } + case Instructions.GET_INPUT: + { + // TODO: generalize input + int readValue = Console.Read(); + _state.SetCell(readValue); + break; + } + case Instructions.LOOP_START: + { + // if the currently pointed to value is not 0, the loop will run, so push + // the starting index for it onto the stack + if (_state.GetCurrentValue() != 0) + { + loopStack.Push(idx); + } + // if the currently pointed to value is 0, the loop is skipped + // in this case, we should iterate forwards and find the matching bracket + // if no matching bracket is found, throw an error + else + { + int loopEnd = FindMatchingBracket(source, idx); + if (loopEnd < 0) { throw new FrickUnbalancedBracketException(idx); } + idx = loopEnd; + } + break; + } + case Instructions.LOOP_END: + { + // pop the last pushed index off the stack + // if the currently pointed to value is not 0, set the text iteration + // back to the beginning of this loop, at which point, the position + // will be pushed onto the stack again; this repeats until the value pointed to becomes zero + int poppedIdx = loopStack.Pop(); + if (_state.GetCurrentValue() != 0) + { + idx = poppedIdx - 1; + } + break; + } + } + } + } + + /// + /// Finds a matching bracket "]" starting from the position specified + /// + private static int FindMatchingBracket(string text, int startPos) + { + int endPos = startPos; + int openBracketCount = 1; + while (openBracketCount > 0 && endPos < text.Length) + { + char c = text[++endPos]; + if (c == Instructions.LOOP_START) { openBracketCount++; } + else if (c == Instructions.LOOP_END) { openBracketCount--; } + } + return openBracketCount == 0 + ? endPos + : -1; + } + + /// + /// Checks if the specified character is a valid operation in brainfuck + /// + private static bool IsValidInstruction(char c) => + _validInstructions.Contains(c); + } +} diff --git a/src/Frick.NET/Internals/FrickInterperterState.cs b/src/Frick.NET/Internals/FrickInterperterState.cs new file mode 100644 index 0000000..4575187 --- /dev/null +++ b/src/Frick.NET/Internals/FrickInterperterState.cs @@ -0,0 +1,137 @@ +using Frick.NET.Exceptions; + +namespace Frick.NET.Internals +{ + /// + /// Represents the internal state of the brainfuck interpreter. Internal use only. + /// + internal class FrickInterperterState(int cellSize, + CellOverflowBehaviour cellOverflowBehaviour, + CellValueOverflowBehaviour cellValueOverflowBehaviour) + { + // what the interpreter does when it encounters a cell pointer overflow + private readonly CellOverflowBehaviour _cellOverflowBehaviour = cellOverflowBehaviour; + // what the interpreter does when it encounters a cell value overflow + private readonly CellValueOverflowBehaviour _cellValueOverflowBehaviour = cellValueOverflowBehaviour; + // The "memory" model of the brainfuck interpreter + public byte[] Cells { get; } = new byte[cellSize]; + // Which cell is currently being pointed to + public int CurrentPointer { get; private set; } = 0; + + /// + /// Move the currently pointed to cell one to the left. + /// + public void MovePointerLeft() => + MovePointer(CurrentPointer - 1); + + /// + /// Move the currently pointed to cell one to the right. + /// + public void MovePointerRight() => + MovePointer(CurrentPointer + 1); + + /// + /// Increment the currently pointed to cell's value by 1. + /// + public void IncrementCell() => + ChangeCellValue(GetCurrentValue() + 1); + + /// + /// Decrement the currently pointed to cell's value by 1. + /// + public void DecrementCell() => + ChangeCellValue(GetCurrentValue() - 1); + + /// + /// Set the currently pointed to cell's value to an arbitrary value. Generally should be constrained to a byte's size + /// but the actual behaviour depends on the set on the interpreter. + /// + public void SetCell(int b) => + ChangeCellValue(b); + + /// + /// Returns the currently pointed to cell's value. + /// + /// + public byte GetCurrentValue() => + Cells[CurrentPointer]; + + /// + /// Moves the pointer to an arbitrary new place. The actual behaviour depends on the set on the interpreter. + /// + private void MovePointer(int newValue) + { + if (newValue < 0) + { + switch (_cellOverflowBehaviour) + { + case CellOverflowBehaviour.Ignore: + return; + case CellOverflowBehaviour.WrapAround: + newValue = Cells.Length - 1; + break; + case CellOverflowBehaviour.ThrowException: + throw new FrickCellOverflowException(newValue); + } + } + else if (newValue >= Cells.Length) + { + switch (_cellOverflowBehaviour) + { + case CellOverflowBehaviour.Ignore: + return; + case CellOverflowBehaviour.WrapAround: + newValue = 0; + break; + case CellOverflowBehaviour.ThrowException: + throw new FrickCellOverflowException(newValue); + } + } + + CurrentPointer = newValue; + } + + /// + /// Set the currently pointed to cell's value to an arbitrary value. The actual behaviour depends on the set on the interpreter. + /// + private void ChangeCellValue(int newValue) + { + if (newValue > byte.MaxValue) + { + switch (_cellValueOverflowBehaviour) + { + case CellValueOverflowBehaviour.Ignore: + return; + case CellValueOverflowBehaviour.WrapAround: + newValue = byte.MinValue; + break; + case CellValueOverflowBehaviour.ThrowException: + throw new FrickCellValueOverflowException(newValue); + } + } + else if (newValue < byte.MinValue) + { + switch (_cellValueOverflowBehaviour) + { + case CellValueOverflowBehaviour.Ignore: + return; + case CellValueOverflowBehaviour.WrapAround: + newValue = byte.MaxValue; + break; + case CellValueOverflowBehaviour.ThrowException: + throw new FrickCellValueOverflowException(newValue); + } + } + Cells[CurrentPointer] = Convert.ToByte(newValue); + } + + /// + /// Resets the state of the interpreter. Sets all cell values to 0 and moves the pointer to 0. + /// + public void Reset() + { + Array.Clear(Cells); + CurrentPointer = 0; + } + } +} diff --git a/src/Frick.NET/Internals/Instructions.cs b/src/Frick.NET/Internals/Instructions.cs new file mode 100644 index 0000000..7b4b0ee --- /dev/null +++ b/src/Frick.NET/Internals/Instructions.cs @@ -0,0 +1,14 @@ +namespace Frick.NET.Internals +{ + internal static class Instructions + { + public const char MOVE_POINTER_RIGHT = '>'; + public const char MOVE_POINTER_LEFT = '<'; + public const char INCREMENT_CELL = '+'; + public const char DECREMENT_CELL = '-'; + public const char OUTPUT_CELL = '.'; + public const char GET_INPUT = ','; + public const char LOOP_START = '['; + public const char LOOP_END = ']'; + } +}