Add project files.
This commit is contained in:
34
Frick.NET.sln
Normal file
34
Frick.NET.sln
Normal file
@@ -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
|
||||
19
src/Frick.NET.Cli/Frick.NET.Cli.csproj
Normal file
19
src/Frick.NET.Cli/Frick.NET.Cli.csproj
Normal 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>
|
||||
55
src/Frick.NET.Cli/Program.cs
Normal file
55
src/Frick.NET.Cli/Program.cs
Normal 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;
|
||||
}
|
||||
21
src/Frick.NET/CellOverflowBehaviour.cs
Normal file
21
src/Frick.NET/CellOverflowBehaviour.cs
Normal 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
|
||||
}
|
||||
}
|
||||
21
src/Frick.NET/CellValueOverflowBehaviour.cs
Normal file
21
src/Frick.NET/CellValueOverflowBehaviour.cs
Normal 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
|
||||
}
|
||||
}
|
||||
12
src/Frick.NET/Exceptions/FrickCellOverflowException.cs
Normal file
12
src/Frick.NET/Exceptions/FrickCellOverflowException.cs
Normal 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}")
|
||||
{
|
||||
}
|
||||
}
|
||||
12
src/Frick.NET/Exceptions/FrickCellValueOverflowException.cs
Normal file
12
src/Frick.NET/Exceptions/FrickCellValueOverflowException.cs
Normal 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}")
|
||||
{
|
||||
}
|
||||
}
|
||||
12
src/Frick.NET/Exceptions/FrickUnbalancedBracketException.cs
Normal file
12
src/Frick.NET/Exceptions/FrickUnbalancedBracketException.cs
Normal 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}")
|
||||
{
|
||||
}
|
||||
}
|
||||
9
src/Frick.NET/Frick.NET.csproj
Normal file
9
src/Frick.NET/Frick.NET.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
159
src/Frick.NET/FrickInterpreter.cs
Normal file
159
src/Frick.NET/FrickInterpreter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
137
src/Frick.NET/Internals/FrickInterperterState.cs
Normal file
137
src/Frick.NET/Internals/FrickInterperterState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Frick.NET/Internals/Instructions.cs
Normal file
14
src/Frick.NET/Internals/Instructions.cs
Normal 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 = ']';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user