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 = ']';
+ }
+}