bferl
is a standard OTP application, with very simple supervision hierarchy:
Each service (interpreter without REPL, compiler and virtual machine) are gathered into one logical group called tools. Basically, each one is isolated from the others and crashing one of them do not affect others, so they can restart individually without any problems. Each facility in this logical group is an implementation of gen_server
.
There is one additional facility for Virtual Machine subsystem called bferl_vm_threads_sup
- if you are interested in details, you can read about it here.
Tools and I/O subsystem have a different supervisors in order differentiate restart strategies.
Besides subsystems and OTP specific processes, we have available also other modules with different responsibilities:
bferl_types
which contains predefined, internal type specifications.bferl_tokenizer
which is responsible for splitting Brainfuck / Brainfork code into single tokens.bferl_programming_language_logic
which is responsible for interpreting and representing language logic.
Last module from the previous section introduces a new data structure which is an internal state of an interpreter. In our case it is defined as a record with name interpreter
(it is defined in include/interpreter_definitions.hrl
). Let's look at the definition:
-record(
interpreter,
{ io = {undefined, undefined, undefined} :: bferl_types:io_callbacks(),
stack = [] :: list(pos_integer()),
instructions_counter = 0 :: non_neg_integer(),
instructions = undefined :: undefined | bferl_types:instructions(),
instructions_pointer = 1 :: pos_integer(),
memory_pointer = 0 :: non_neg_integer(),
memory = ?EMPTY_MEMORY :: array:array(integer())
}).
Inside this record we have:
- registered I/O subsystem references (inside
io
field), - a stack, a reference for looping constructs (inside
stack
field), - an IC (inside
instruction_counter
field), - program content (inside
instructions
field), - an IP (inside
instruction_pointer
field), - actual position in memory (inside
memory_pointer
field), - and memory representation (inside
memory
field).
Types attached to the record will help you deduce which values are valid for each field. By default interpreter
memory is initialized with zeros and it has maximum size of 30000 memory cells. Each memory cell is an Erlang number.
Below you can see pretty printed record representation:
1> application:ensure_all_started(bferl).
...
2> bferl_app:attach_console().
...
3> bferl_app:run_code(",>++++++[<-------->-],[<+>-]<.").
> 2
> 3
5
{interpreter,{#Fun<bferl_io.get_character_from_console.0>,
#Fun<bferl_io.put_character_to_console.1>,
#Fun<bferl_io.new_line_on_console.0>},
[],397,
[",",">","+","+","+","+","+","+","[","<","-","-","-","-",
"-","-","-","-",">","-","]",",","[",
[...]|...],
31,0,
{array,30000,0,0,
{{{{{53,0,0,0,0,0,0,0,0,0},10,10,10,10,10,10,10,10,10,10},
100,100,100,100,100,100,100,100,100,100},
1000,1000,1000,1000,1000,1000,1000,1000,1000,1000},
10000,10000,10000,10000,10000,10000,10000,10000,10000,
10000}}}
Trained eye will spot an Erlang array
representation in the last field.
As it is stated above, whole application is split into multiple subsystems. We will describe them one by one in this section. If you are interested in internals of Brainfork, you can find them here.
First tool delivered with the bferl_app
is the interpreter, together with a REPL functionality.
You can evaluate code delivered as a file or string, with these commands:
bferl_app:run_file("./hello_world.bf").
orbferl_app:run_code("[-]").
.- Interpreter does not support Brainfork code.
Also, you can start a REPL by invoking the following command:
bferl_app:repl().
- After that your prompt will change and you can start playing with REPL abilities.
- REPL has built-in help functionality, which is available by invoking
?help
in it. - It does not support Brainfork code.
Details about implementation and usage documentation is hosted here.
One of the tools delivered with the application is a compiler. It compiles Brainfuck / Brainfork code to the Core Erlang representation. If you are interested, you can find more details here.
How to compile a program? You can do it by invoking these commands in the shell:
bferl_app:compile_file("./hello_world.bf").
orbferl_app:compile_code("[-]").
.- If you want to compile your code in
debug
mode, you can add aforementioned atom as a second argument of both functions. - If you would like to compile a Brainfork code, you can do it only by delivering a file with an extension
.bfo
.
- If you want to compile your code in
After invoking those commands (with a valid program) it will produce a BEAM module which will be automatically loaded after successful compilation into the current shell session. Prepared module has two exported functions which are the starting points:
MODULE_NAME:start/0
- which starts program with console attached as an I/O subsystem.- As a result it will return amount of instructions executed during the program run.
MODULE_NAME:start/1
- which starts program with I/O subsystem represented as a tape, but you are providing only an input tape as an argument.- Output will be printed out on the console and result of the program execution is the same as in the example above.
Now you can invoke your program with one of the starting points.
This module is a very simple implementation of a VM which does very crude JIT optimizations and has different internal representation than the interpreter or compiled programs. If you are interested in details, you can find them here.
You can run code on it, with these commands:
bferl_app:run_file_on_vm("./hello_world.bf").
orbferl_app:run_code_on_vm("[-]").
- Both methods have ability to run programs in debug mode.
- Add
debug
atom to run it with special annotations, e.g.:bferl_app:run_code_on_vm("[-]", debug).
bferl_app:run_file_on_vm("./hello_world.bf", debug).
- Add
- Both methods have ability to run programs with attached tape.
- Add string with tape content as a second argument, e.g.:
bferl_app:run_code_on_vm(",+.", "A").
bferl_app:run_file_on_vm("./hello_world.bf", "A").
- You can combine that invocations with passing as a third argument
debug
atom.
- Add string with tape content as a second argument, e.g.:
- If you would like to run a Brainfork code, you can do it only by delivering a file with an extension
.bfo
.
- Both methods have ability to run programs in debug mode.
Interpreter and virtual machine are relying additionally on the I/O subsystem. In described application it is represented by bferl_io
.
It is a gen_event
behavior with ability to attach different mechanisms - a standard console or a tape-like mechanism. Tape in that context has a predefined input (delivered as a list of characters) and output (result of printing statements inside the executed program).