So I've completely changed how BrainfuckSharp does things, and added some features. This is obviously a follow up to: Interpreting Brainfuck code to C#, then compiling to a .exe
It now features an OptimizationLevel
(currently hardcoded, will fix for the future) that allows you to indicate how far you want the interpreted code optimized. The OptimizationLevel.Level1
level will smash subsequent calls to the increment/decrement and move left/right tokens together into one call. (Instead of >>
turning into index++; index++;
it now turns into index += 2;
.)
Just as well, it's now much more like a "true" compiler in that it treats everything like a token.
Let's start with the TokenSymbol
enum:
public enum TokenSymbol
{
None,
MoveLeft,
MoveRight,
LoopBegin,
LoopEnd,
Increment,
Decrement,
OutputValue,
InputCharacter
}
This represents every possible token in Brainfuck, and a default "None" token.
Next, OptimizationLevel
:
/// <summary>
/// Determines how the <see cref="Interpreter"/> will optimize Brainfuck code.
/// </summary>
public enum OptimizationLevel
{
/// <summary>
/// Do not apply any optimizations.
/// </summary>
None,
/// <summary>
/// Apply first level optimizations (combining sequences of increment/decrement characters into one increment/decrement per sequence).
/// </summary>
Level1,
}
This is self-explanatory. It represents the (currently two) possible optimization levels of BrainfuckSharp. Specifying None
will not optimize the code, specifying Level1
will apply basic optimizations.
On to the Interpreter
:
public class Interpreter
{
public OptimizationLevel OptimizationLevel { get; set; } = OptimizationLevel.None;
public List<string> Lines { get; } = new List<string>();
private List<TokenSymbol> _symbols = new List<TokenSymbol>();
private List<char> _validTokens = new List<char> { '<', '>', ',', '.', '+', '-', '[', ']' };
private Dictionary<char, TokenSymbol> _tokenMap = new Dictionary<char, TokenSymbol>
{
{ '<', TokenSymbol.MoveLeft },
{ '>', TokenSymbol.MoveRight },
{ ',', TokenSymbol.InputCharacter },
{ '.', TokenSymbol.OutputValue },
{ '[', TokenSymbol.LoopBegin },
{ ']', TokenSymbol.LoopEnd },
{ '+', TokenSymbol.Increment },
{ '-', TokenSymbol.Decrement }
};
private void Tokenize(string brainfuck)
{
foreach (char c in brainfuck)
{
if (_validTokens.Contains(c))
{
_symbols.Add(_tokenMap[c]);
}
}
}
public void WriteToFile(string filename)
{
using (var sw = new StreamWriter(filename))
{
foreach (string line in Lines)
{
sw.WriteLine(line);
}
}
}
public void Interpret(string brainfuck)
{
Tokenize(brainfuck);
var lastSymbol = TokenSymbol.None;
var symbolRepeated = 0;
var indents = 3;
foreach (var symbol in _symbols)
{
switch (symbol)
{
case TokenSymbol.LoopBegin:
if ((OptimizationLevel & OptimizationLevel.Level1) >= OptimizationLevel.Level1)
{
if (lastSymbol != symbol && lastSymbol != TokenSymbol.None && (lastSymbol == TokenSymbol.Decrement || lastSymbol == TokenSymbol.Increment || lastSymbol == TokenSymbol.MoveLeft || lastSymbol == TokenSymbol.MoveRight))
{
Lines.AddRange(SymbolToLines(lastSymbol, symbolRepeated, indents));
symbolRepeated = 0;
}
else
{
lastSymbol = symbol;
symbolRepeated++;
}
}
symbolRepeated = 0;
Lines.AddRange(SymbolToLines(symbol, symbolRepeated, indents));
indents += 1;
break;
case TokenSymbol.LoopEnd:
if ((OptimizationLevel & OptimizationLevel.Level1) >= OptimizationLevel.Level1)
{
if (lastSymbol != symbol && lastSymbol != TokenSymbol.None && (lastSymbol == TokenSymbol.Decrement || lastSymbol == TokenSymbol.Increment || lastSymbol == TokenSymbol.MoveLeft || lastSymbol == TokenSymbol.MoveRight))
{
Lines.AddRange(SymbolToLines(lastSymbol, symbolRepeated, indents));
symbolRepeated = 0;
}
else
{
lastSymbol = symbol;
symbolRepeated++;
}
}
indents -= 1;
symbolRepeated = 0;
Lines.AddRange(SymbolToLines(symbol, symbolRepeated, indents));
break;
case TokenSymbol.InputCharacter:
if ((OptimizationLevel & OptimizationLevel.Level1) >= OptimizationLevel.Level1)
{
if (lastSymbol != symbol && lastSymbol != TokenSymbol.None && (lastSymbol == TokenSymbol.Decrement || lastSymbol == TokenSymbol.Increment || lastSymbol == TokenSymbol.MoveLeft || lastSymbol == TokenSymbol.MoveRight))
{
Lines.AddRange(SymbolToLines(lastSymbol, symbolRepeated, indents));
symbolRepeated = 0;
}
else
{
lastSymbol = symbol;
symbolRepeated++;
}
}
symbolRepeated = 0;
Lines.AddRange(SymbolToLines(symbol, symbolRepeated, indents));
break;
case TokenSymbol.OutputValue:
if ((OptimizationLevel & OptimizationLevel.Level1) >= OptimizationLevel.Level1)
{
if (lastSymbol != symbol && lastSymbol != TokenSymbol.None && (lastSymbol == TokenSymbol.Decrement || lastSymbol == TokenSymbol.Increment || lastSymbol == TokenSymbol.MoveLeft || lastSymbol == TokenSymbol.MoveRight))
{
Lines.AddRange(SymbolToLines(lastSymbol, symbolRepeated, indents));
symbolRepeated = 0;
}
else
{
lastSymbol = symbol;
symbolRepeated++;
}
}
symbolRepeated = 0;
Lines.AddRange(SymbolToLines(symbol, symbolRepeated, indents));
break;
case TokenSymbol.Decrement:
case TokenSymbol.Increment:
case TokenSymbol.MoveLeft:
case TokenSymbol.MoveRight:
if ((OptimizationLevel & OptimizationLevel.Level1) >= OptimizationLevel.Level1)
{
if (lastSymbol != symbol && lastSymbol != TokenSymbol.None && (lastSymbol == TokenSymbol.Decrement || lastSymbol == TokenSymbol.Increment || lastSymbol == TokenSymbol.MoveLeft || lastSymbol == TokenSymbol.MoveRight))
{
Lines.AddRange(SymbolToLines(lastSymbol, symbolRepeated, indents));
symbolRepeated = 1;
}
else
{
lastSymbol = symbol;
symbolRepeated++;
}
}
else
{
symbolRepeated = 1;
Lines.AddRange(SymbolToLines(symbol, symbolRepeated, indents));
}
break;
}
lastSymbol = symbol;
}
}
private string[] SymbolToLines(TokenSymbol symbol, int repeated, int indentLevels)
{
switch (symbol)
{
case TokenSymbol.Decrement:
if (repeated == 1)
{
return new[] { $"{GetIndents(indentLevels)}buffer[index]--;" };
}
else
{
return new[] { $"{GetIndents(indentLevels)}buffer[index] -= {repeated};" };
}
case TokenSymbol.Increment:
if (repeated == 1)
{
return new[] { $"{GetIndents(indentLevels)}buffer[index]++;" };
}
else
{
return new[] { $"{GetIndents(indentLevels)}buffer[index] += {repeated};" };
}
case TokenSymbol.InputCharacter:
return new[] { $"{GetIndents(indentLevels)}buffer[index] = ReadChar();" };
case TokenSymbol.LoopBegin:
return new[] { "", $"{GetIndents(indentLevels)}while (buffer[index] != 0)", $"{GetIndents(indentLevels)}{{" };
case TokenSymbol.LoopEnd:
return new[] { $"{GetIndents(indentLevels)}}}", "" };
case TokenSymbol.MoveLeft:
if (repeated == 1)
{
return new[] { $"{GetIndents(indentLevels)}index--;" };
}
else
{
return new[] { $"{GetIndents(indentLevels)}index -= {repeated};" };
}
case TokenSymbol.MoveRight:
if (repeated == 1)
{
return new[] { $"{GetIndents(indentLevels)}index++;" };
}
else
{
return new[] { $"{GetIndents(indentLevels)}index += {repeated};" };
}
case TokenSymbol.OutputValue:
return new[] { $"{GetIndents(indentLevels)}Console.Write((char)buffer[index]);" };
}
return null;
}
public static string GetIndents(int levels) => new string(' ', levels * 4);
}
This interprets the Brainfuck to it's appropriate lines.
The Transpiler
:
public class Transpiler
{
public ILogger Logger { get; }
public string SourceFile { get; set; }
public string ProgrammeName { get; set; }
public string DestinationFile { get; set; }
public string ExecutableFile { get; set; }
public int BufferSize { get; set; }
public string DotNetVersion { get; set; }
public OptimizationLevel OptimizationLevel { get; set; }
private static Dictionary<string, string> _dotNetFolders = new Dictionary<string, string> { { "2.0", "v2.0.50727" }, { "3.5", "v3.5" }, { "4.0", "v4.0.30319" } };
public Transpiler(ILogger logger)
{
Logger = logger;
}
public void AcquireInput(IPrompt prompt)
{
SourceFile = AddExtension(prompt.Prompt<string>("Enter the source filename (.bf)", PromptOptions.Required, validationMethod: x => !string.IsNullOrWhiteSpace(x)), ".bf");
ProgrammeName = SourceFile.Substring(0, SourceFile.LastIndexOf('.'));
DestinationFile = AddExtension(prompt.Prompt("Enter the destination filename (.cs)", PromptOptions.Optional, ProgrammeName + ".cs", validationMethod: x => !string.IsNullOrWhiteSpace(x)), ".cs");
ExecutableFile = AddExtension(prompt.Prompt("Enter the destination executable (.exe)", PromptOptions.Optional, ProgrammeName + ".exe", validationMethod: x => !string.IsNullOrWhiteSpace(x)), ".exe");
BufferSize = prompt.Prompt("Enter buffer size", PromptOptions.Optional, 2048);
DotNetVersion = prompt.Prompt("Enter .NET version to compile with", PromptOptions.Optional, "4.0", "Version can be any of: 2.0, 3.5, 4.0", x => _dotNetFolders.ContainsKey(x));
}
private static string AddExtension(string data, string extension)
{
if (data.IndexOf('.') == -1)
{
data += extension;
}
return data;
}
public string LoadBrainfuckCode(string filename) => File.ReadAllText(filename);
public void Transpile()
{
if (string.IsNullOrWhiteSpace(SourceFile)
|| string.IsNullOrWhiteSpace(ProgrammeName)
|| string.IsNullOrWhiteSpace(DestinationFile)
|| string.IsNullOrWhiteSpace(ExecutableFile)
|| string.IsNullOrWhiteSpace(DotNetVersion)
|| BufferSize <= 0)
{
throw new ArgumentException($"The values for {nameof(SourceFile)}, {nameof(ProgrammeName)}, {nameof(DestinationFile)}, {nameof(ExecutableFile)}, {nameof(DotNetVersion)} cannot be null or whitespace, and {nameof(BufferSize)} cannot be zero or negative. Did you forget to call {nameof(AcquireInput)}?");
}
// Now that that's over.
var interpreter = new Interpreter();
interpreter.OptimizationLevel = OptimizationLevel;
var brainfuck = LoadBrainfuckCode(SourceFile);
interpreter.Interpret(brainfuck);
var template = AcquireTemplate("CompiledTemplate.cs");
var sb = new StringBuilder();
foreach (var line in interpreter.Lines)
{
sb.AppendLine(line);
}
var result = template;
result = result.Replace("BufferSize", BufferSize.ToString());
result = result.Replace("BrainfuckCode;", sb.ToString().TrimStart());
WriteToCsFile(DestinationFile, result);
CompileCsFile(_dotNetFolders[DotNetVersion], DestinationFile, ExecutableFile);
}
public void WriteToCsFile(string filename, string data)
{
using (StreamWriter sw = new StreamWriter(filename))
{
sw.WriteLine(data);
}
}
public string AcquireTemplate(string filename) => File.ReadAllText(filename);
public void CompileCsFile(string dotNetPath, string sourceFile, string executableFile)
{
Logger.LogImportant($"C:\\Windows\\Microsoft.NET\\Framework\\{dotNetPath}\\csc.exe /t:exe /out:\"{Directory.GetCurrentDirectory()}\\{executableFile}\" \"{Directory.GetCurrentDirectory()}\\{sourceFile}\"");
ProcessStartInfo info = new ProcessStartInfo($@"C:\Windows\Microsoft.NET\Framework\{dotNetPath}\csc.exe", $"/t:exe /out:\"{Directory.GetCurrentDirectory()}\\{executableFile}\" \"{Directory.GetCurrentDirectory()}\\{sourceFile}\"");
var process = Process.Start(info);
process.WaitForExit();
}
}
This does the heavy-lifting of the compiler to call the appropriate methods to convert the Brainfuck to C#.
Lastly, the new Program
:
class Program
{
static void Main(string[] args)
{
var logger = new ConsoleLogger(LoggingType.All);
var transpiler = new Transpiler.Transpiler(logger);
transpiler.OptimizationLevel = Transpiler.OptimizationLevel.Level1;
var prompter = new ConsolePrompt(logger);
transpiler.AcquireInput(prompter);
transpiler.Transpile();
Process.Start(transpiler.ExecutableFile);
Console.WriteLine("Press enter key to exit...");
Console.ReadLine();
}
}
Pretty self-explanatory.
Major notes: The project is no longer one huge Program.cs
, but features a somewhat object-oriented approach to the Brainfuck interpretation and compilation.
Example HelloWorld.bf
:
[ This program prints "Hello World!" and a newline to the screen, its length is 106 active command characters. [It is not the shortest.] This loop is a "comment loop", a simple way of adding a comment to a BF program such that you don't have to worry about any command characters. Any ".", ",", "+", "-", "<" and ">" characters are simply ignored, the "[" and "]" characters just have to be balanced. This loop and the commands it contains are ignored because the current cell defaults to a value of 0; the 0 value causes this loop to be skipped. ] +++++ +++ Set Cell #0 to 8 [ >++++ Add 4 to Cell #1; this will always set Cell #1 to 4 [ as the cell will be cleared by the loop >++ Add 2 to Cell #2 >+++ Add 3 to Cell #3 >+++ Add 3 to Cell #4 >+ Add 1 to Cell #5 <<<<- Decrement the loop counter in Cell #1 ] Loop till Cell #1 is zero; number of iterations is 4 >+ Add 1 to Cell #2 >+ Add 1 to Cell #3 >- Subtract 1 from Cell #4 >>+ Add 1 to Cell #6 [<] Move back to the first zero cell you find; this will be Cell #1 which was cleared by the previous loop <- Decrement the loop Counter in Cell #0 ] Loop till Cell #0 is zero; number of iterations is 8 The result of this is: Cell No : 0 1 2 3 4 5 6 Contents: 0 0 72 104 88 32 8 Pointer : ^ >>. Cell #2 has value 72 which is 'H' >---. Subtract 3 from Cell #3 to get 101 which is 'e' +++++++..+++. Likewise for 'llo' from Cell #3 >>. Cell #5 is 32 for the space <-. Subtract 1 from Cell #4 for 87 to give a 'W' <. Cell #3 was set to 'o' from the end of 'Hello' +++.------.--------. Cell #3 for 'rl' and 'd' >>+. Add 1 to Cell #5 gives us an exclamation point >++. And finally a newline from Cell #6
Converts to:
using System; namespace Brainfuck_Interpreter { class CompiledTemplate { static byte ReadChar() { return (byte)Console.ReadKey().KeyChar; } static void Main(string[] args) { byte[] buffer = new byte[2048]; int index = 0; while (buffer[index] != 0) { buffer[index] = ReadChar(); Console.Write((char)buffer[index]); while (buffer[index] != 0) { Console.Write((char)buffer[index]); } buffer[index] = ReadChar(); Console.Write((char)buffer[index]); Console.Write((char)buffer[index]); buffer[index] = ReadChar(); buffer[index] = ReadChar(); buffer[index] = ReadChar(); buffer[index]++; buffer[index] = ReadChar(); buffer[index]--; buffer[index] = ReadChar(); index--; index++; buffer[index] = ReadChar(); while (buffer[index] != 0) { } Console.Write((char)buffer[index]); Console.Write((char)buffer[index]); } buffer[index] += 8; while (buffer[index] != 0) { index++; buffer[index] += 4; while (buffer[index] != 0) { index++; buffer[index] += 2; index++; buffer[index] += 3; index++; buffer[index] += 3; index++; buffer[index]++; index -= 4; buffer[index]--; } index++; buffer[index]++; index++; buffer[index]++; index++; buffer[index]--; index += 2; buffer[index]++; while (buffer[index] != 0) { index--; } index--; buffer[index]--; } index += 2; Console.Write((char)buffer[index]); index++; buffer[index] -= 3; Console.Write((char)buffer[index]); buffer[index] += 7; Console.Write((char)buffer[index]); Console.Write((char)buffer[index]); buffer[index] += 3; Console.Write((char)buffer[index]); index += 2; Console.Write((char)buffer[index]); index--; buffer[index]--; Console.Write((char)buffer[index]); index--; Console.Write((char)buffer[index]); buffer[index] += 3; Console.Write((char)buffer[index]); buffer[index] -= 6; Console.Write((char)buffer[index]); buffer[index] -= 8; Console.Write((char)buffer[index]); index += 2; buffer[index]++; Console.Write((char)buffer[index]); index++; buffer[index] += 2; Console.Write((char)buffer[index]); Console.WriteLine("Program terminated successfully..."); Console.ReadLine(); } } }