Add project files.

This commit is contained in:
2024-01-14 15:12:42 +01:00
parent 5c92898bc8
commit cf116e919b
12 changed files with 505 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>frick-cli</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Frick.NET\Frick.NET.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Commands\" />
</ItemGroup>
</Project>

View File

@@ -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 <FILENAME> 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 <FILENAME> 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;
}

View File

@@ -0,0 +1,21 @@
namespace Frick.NET
{
/// <summary>
/// Represents how the interpreter should behave when encountering a cell pointer overflow.
/// </summary>
public enum CellOverflowBehaviour
{
/// <summary>
/// The instruction causing the overflow should be ignored.
/// </summary>
Ignore = 1,
/// <summary>
/// The pointer should wrap around in the other direction.
/// </summary>
WrapAround,
/// <summary>
/// The interpreter should throw an exception.
/// </summary>
ThrowException
}
}

View File

@@ -0,0 +1,21 @@
namespace Frick.NET
{
/// <summary>
/// Represents how the interpreter should behave when encountering a cell value overflow.
/// </summary>
public enum CellValueOverflowBehaviour
{
/// <summary>
/// The instruction causing the overflow should be ignored.
/// </summary>
Ignore = 1,
/// <summary>
/// The value should wrap around in the other direction.
/// </summary>
WrapAround,
/// <summary>
/// The interpreter should throw an exception.
/// </summary>
ThrowException
}
}

View File

@@ -0,0 +1,12 @@
namespace Frick.NET.Exceptions
{
/// <summary>
/// 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.
/// </summary>
/// <param name="requestedCell"></param>
[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}")
{
}
}

View File

@@ -0,0 +1,12 @@
namespace Frick.NET.Exceptions
{
/// <summary>
/// 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.
/// </summary>
[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}")
{
}
}

View File

@@ -0,0 +1,12 @@
namespace Frick.NET.Exceptions
{
/// <summary>
/// Exception thrown when the interpreter encounters an unbalanced (i.e. not closed) bracket.
/// </summary>
/// <param name="bracketPos"></param>
[Serializable]
public class FrickUnbalancedBracketException(int bracketPos)
: Exception($"Unexpected EOF; Unbalanced loop instruction found at position {bracketPos}")
{
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,159 @@
using Frick.NET.Exceptions;
using Frick.NET.Internals;
namespace Frick.NET
{
/// <summary>
/// The brainfuck language interpreter
/// </summary>
public class FrickInterpreter
{
// what opcodes are recognized
private static readonly HashSet<char> _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;
/// <summary>
/// Constructs a brainfuck language interpreter with the given cell size and overflow behaviours
/// </summary>
/// <param name="cellSize">How many cells are available for programs. Defaults to 2^15. Cannot be smaller than 1.</param>
/// <param name="cellOverflowBehaviour">What should happen when a program tries to overflow the available cells.</param>
/// <param name="cellValueOverflowBehaviour">What should happen when a program tries to overflow the value of a cell.</param>
/// <exception cref="ArgumentException"></exception>
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);
}
/// <summary>
/// Runs a brainfuck program given in the <paramref name="source"/> parameter.
/// </summary>
/// <param name="source">The program to run.</param>
/// <param name="resetState">Flag to reset the internal state of the interpreter when running the source.</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="Exceptions.FrickCellOverflowException"></exception>
/// <exception cref="Exceptions.FrickCellValueOverflowException"></exception>
/// <exception cref="Exceptions.FrickUnbalancedBracketException"></exception>
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<int> 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;
}
}
}
}
/// <summary>
/// Finds a matching bracket "]" starting from the position specified
/// </summary>
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;
}
/// <summary>
/// Checks if the specified character is a valid operation in brainfuck
/// </summary>
private static bool IsValidInstruction(char c) =>
_validInstructions.Contains(c);
}
}

View File

@@ -0,0 +1,137 @@
using Frick.NET.Exceptions;
namespace Frick.NET.Internals
{
/// <summary>
/// Represents the internal state of the brainfuck interpreter. Internal use only.
/// </summary>
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;
/// <summary>
/// Move the currently pointed to cell one to the left.
/// </summary>
public void MovePointerLeft() =>
MovePointer(CurrentPointer - 1);
/// <summary>
/// Move the currently pointed to cell one to the right.
/// </summary>
public void MovePointerRight() =>
MovePointer(CurrentPointer + 1);
/// <summary>
/// Increment the currently pointed to cell's value by 1.
/// </summary>
public void IncrementCell() =>
ChangeCellValue(GetCurrentValue() + 1);
/// <summary>
/// Decrement the currently pointed to cell's value by 1.
/// </summary>
public void DecrementCell() =>
ChangeCellValue(GetCurrentValue() - 1);
/// <summary>
/// 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 <see cref="CellValueOverflowBehaviour"/> set on the interpreter.
/// </summary>
public void SetCell(int b) =>
ChangeCellValue(b);
/// <summary>
/// Returns the currently pointed to cell's value.
/// </summary>
/// <returns></returns>
public byte GetCurrentValue() =>
Cells[CurrentPointer];
/// <summary>
/// Moves the pointer to an arbitrary new place. The actual behaviour depends on the <see cref="CellOverflowBehaviour"/> set on the interpreter.
/// </summary>
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;
}
/// <summary>
/// Set the currently pointed to cell's value to an arbitrary value. The actual behaviour depends on the <see cref="CellOverflowBehaviour"/> set on the interpreter.
/// </summary>
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);
}
/// <summary>
/// Resets the state of the interpreter. Sets all cell values to 0 and moves the pointer to 0.
/// </summary>
public void Reset()
{
Array.Clear(Cells);
CurrentPointer = 0;
}
}
}

View File

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