brainfuck 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile.lock +8 -23
- data/Rakefile +12 -10
- data/Readme.md +17 -11
- data/bin/brainfuck +1 -1
- data/brainfuck.gemspec +5 -6
- data/lib/brainfuck.rb +5 -18
- data/lib/brainfuck/ast.rb +126 -18
- data/lib/brainfuck/code_loader.rb +31 -0
- data/lib/brainfuck/compiler.rb +81 -0
- data/lib/brainfuck/lexer.rb +32 -0
- data/lib/brainfuck/main.rb +99 -0
- data/lib/brainfuck/parser.rb +9 -24
- data/lib/brainfuck/stages.rb +170 -0
- data/lib/brainfuck/version.rb +1 -1
- data/test/acceptance/acceptance_test.rb +75 -0
- data/test/brainfuck/ast_test.rb +7 -0
- data/test/brainfuck/lexer_test.rb +21 -0
- data/test/brainfuck/parser_test.rb +16 -0
- data/test/test_helper.rb +10 -0
- metadata +32 -53
- data/lib/brainfuck/interpreter.rb +0 -20
- data/lib/brainfuck/stack.rb +0 -51
- data/spec/acceptance/acceptance_spec.rb +0 -105
- data/spec/brainfuck/ast_spec.rb +0 -36
- data/spec/brainfuck/interpreter_spec.rb +0 -25
- data/spec/brainfuck/parser_spec.rb +0 -27
- data/spec/brainfuck/stack_spec.rb +0 -81
- data/spec/brainfuck_spec.rb +0 -23
- data/spec/spec_helper.rb +0 -18
@@ -0,0 +1,32 @@
|
|
1
|
+
module Brainfuck
|
2
|
+
class Lexer < Parslet::Parser
|
3
|
+
INSTRUCTIONS = %w{> < + - [ ] . ,}
|
4
|
+
|
5
|
+
alias_method :tokenize, :parse
|
6
|
+
class << self
|
7
|
+
def clean code
|
8
|
+
code.chars.select {|c| INSTRUCTIONS.include? c }.join
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
rule(:lparen) { str('[') >> space? }
|
13
|
+
rule(:rparen) { str(']') >> space? }
|
14
|
+
|
15
|
+
rule(:space) { match('\s').repeat(1) }
|
16
|
+
rule(:space?) { space.maybe }
|
17
|
+
|
18
|
+
rule(:fwd) { str('>') >> space? }
|
19
|
+
rule(:bwd) { str('<') >> space? }
|
20
|
+
|
21
|
+
rule(:inc) { str('+') >> space? }
|
22
|
+
rule(:dec) { str('-') >> space? }
|
23
|
+
|
24
|
+
rule(:puts) { str('.') >> space? }
|
25
|
+
rule(:gets) { str(',') >> space? }
|
26
|
+
|
27
|
+
rule(:iteration) { lparen >> expression >> rparen }
|
28
|
+
|
29
|
+
rule(:expression) { (iteration.as(:iteration) | fwd.as(:fwd) | bwd.as(:bwd) | inc.as(:inc) | dec.as(:dec) | puts.as(:puts) | gets.as(:gets)).repeat.as(:exp) }
|
30
|
+
root :expression
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'brainfuck/version'
|
2
|
+
|
3
|
+
module Brainfuck
|
4
|
+
|
5
|
+
# Command line interface to Brainfuck.
|
6
|
+
#
|
7
|
+
# Currently we only take a brainfuck source name to compile and
|
8
|
+
# run.
|
9
|
+
#
|
10
|
+
class Main
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@print = Compiler::Print.new
|
14
|
+
@compile_only = false
|
15
|
+
@evals = []
|
16
|
+
@rest = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def main(argv=ARGV)
|
20
|
+
options(argv)
|
21
|
+
evals unless @evals.empty?
|
22
|
+
script unless @rest.empty?
|
23
|
+
compile if @compile_only
|
24
|
+
end
|
25
|
+
|
26
|
+
# Batch compile all brainfuck files given as arguments.
|
27
|
+
def compile
|
28
|
+
@rest.each do |bf|
|
29
|
+
begin
|
30
|
+
Compiler.compile_file bf, nil, @print
|
31
|
+
rescue Compiler::Error => e
|
32
|
+
e.show
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Evaluate code given on command line
|
38
|
+
def evals
|
39
|
+
bnd = Object.new
|
40
|
+
def bnd.get; binding; end
|
41
|
+
bnd = bnd.get
|
42
|
+
mod = nil
|
43
|
+
@evals.each do |code|
|
44
|
+
CodeLoader.execute_code code, bnd, mod, @print
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Run the given script if any
|
49
|
+
def script
|
50
|
+
CodeLoader.execute_file @rest.first, nil, @print
|
51
|
+
end
|
52
|
+
|
53
|
+
# Parse command line options
|
54
|
+
def options(argv)
|
55
|
+
options = Rubinius::Options.new "Usage: brainfuck [options] [program]", 20
|
56
|
+
options.doc "Brainfuck is a Brainfuck implementation for the Rubinius VM."
|
57
|
+
options.doc ""
|
58
|
+
options.doc "OPTIONS:"
|
59
|
+
|
60
|
+
# options.on "-", "Read and evalute code from STDIN" do
|
61
|
+
# @evals << STDIN.read
|
62
|
+
# end
|
63
|
+
|
64
|
+
# options.on "--print-ast", "Print the Brainfuck AST" do
|
65
|
+
# @print.ast = true
|
66
|
+
# end
|
67
|
+
|
68
|
+
# options.on "--print-asm", "Print the Rubinius ASM" do
|
69
|
+
# @print.asm = true
|
70
|
+
# end
|
71
|
+
|
72
|
+
# options.on "--print-sexp", "Print the Brainfuck Sexp" do
|
73
|
+
# @print.sexp = true
|
74
|
+
# end
|
75
|
+
|
76
|
+
# options.on "--print-all", "Print Sexp, AST and Rubinius ASM" do
|
77
|
+
# @print.ast = @print.asm = @print.sexp = true
|
78
|
+
# end
|
79
|
+
|
80
|
+
options.on "-C", "--compile", "Just batch compile dont execute." do
|
81
|
+
@compile_only = true
|
82
|
+
end
|
83
|
+
#
|
84
|
+
# options.on "-e", "CODE", "Execute CODE" do |e|
|
85
|
+
# @evals << e
|
86
|
+
# end
|
87
|
+
|
88
|
+
options.on "-h", "--help", "Display this help" do
|
89
|
+
puts options
|
90
|
+
exit 0
|
91
|
+
end
|
92
|
+
|
93
|
+
options.doc ""
|
94
|
+
|
95
|
+
@rest = options.parse(argv)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
data/lib/brainfuck/parser.rb
CHANGED
@@ -1,31 +1,16 @@
|
|
1
1
|
module Brainfuck
|
2
|
-
class Parser < Parslet::
|
3
|
-
|
2
|
+
class Parser < Parslet::Transform
|
3
|
+
rule(:fwd => simple(:fwd)) { AST::FwdNode.new }
|
4
|
+
rule(:bwd => simple(:bwd)) { AST::BwdNode.new }
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
code.chars.select {|c| INSTRUCTIONS.include? c }.join
|
8
|
-
end
|
9
|
-
end
|
6
|
+
rule(:inc => simple(:inc)) { AST::IncNode.new }
|
7
|
+
rule(:dec => simple(:dec)) { AST::DecNode.new }
|
10
8
|
|
11
|
-
rule(:
|
12
|
-
rule(:
|
9
|
+
rule(:puts => simple(:puts)) { AST::PutsNode.new }
|
10
|
+
rule(:gets => simple(:gets)) { AST::GetsNode.new }
|
13
11
|
|
14
|
-
rule(:
|
15
|
-
rule(:space?) { space.maybe }
|
12
|
+
rule(:iteration => subtree(:iteration)) { AST::IterationNode.new(iteration) }
|
16
13
|
|
17
|
-
rule(:
|
18
|
-
rule(:bwd) { str('<') >> space? }
|
19
|
-
|
20
|
-
rule(:inc) { str('+') >> space? }
|
21
|
-
rule(:dec) { str('-') >> space? }
|
22
|
-
|
23
|
-
rule(:puts) { str('.') >> space? }
|
24
|
-
rule(:gets) { str(',') >> space? }
|
25
|
-
|
26
|
-
rule(:iteration) { lparen >> expression >> rparen }
|
27
|
-
|
28
|
-
rule(:expression) { (iteration.as(:iteration) | fwd.as(:fwd) | bwd.as(:bwd) | inc.as(:inc) | dec.as(:dec) | puts.as(:puts) | gets.as(:gets)).repeat.as(:exp) }
|
29
|
-
root :expression
|
14
|
+
rule(:exp => subtree(:exp)) { AST::Script.new(exp) }
|
30
15
|
end
|
31
16
|
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'pp'
|
2
|
+
module Brainfuck
|
3
|
+
class Stage
|
4
|
+
|
5
|
+
|
6
|
+
# This stage takes a tree of Brainfuck::AST nodes and
|
7
|
+
# simply calls the bytecode method on them.
|
8
|
+
class Generator < Rubinius::Compiler::Stage
|
9
|
+
next_stage Rubinius::Compiler::Encoder
|
10
|
+
attr_accessor :variable_scope, :root
|
11
|
+
|
12
|
+
def initialize(compiler, last)
|
13
|
+
super
|
14
|
+
@compiler = compiler
|
15
|
+
@variable_scope = nil
|
16
|
+
compiler.generator = self
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
@output = Rubinius::Generator.new
|
21
|
+
|
22
|
+
@output.set_line Integer(1)
|
23
|
+
|
24
|
+
# ENVIRONMENT INITIALIZATION
|
25
|
+
# --------------------------
|
26
|
+
|
27
|
+
# Initialize the heap as a one-cell array containing zero,
|
28
|
+
# and the pointer as a zero integer.
|
29
|
+
@output.meta_push_0
|
30
|
+
@output.set_local 1
|
31
|
+
@output.cast_array
|
32
|
+
@output.set_local 0
|
33
|
+
|
34
|
+
@input.bytecode @output
|
35
|
+
|
36
|
+
bottom = @output.new_label
|
37
|
+
|
38
|
+
@output.push_const :ENV
|
39
|
+
@output.push_literal "DEBUG"
|
40
|
+
@output.send :[], 1, false
|
41
|
+
|
42
|
+
@output.gif bottom
|
43
|
+
|
44
|
+
# Print the heap and the pointer if ENV['DEBUG']
|
45
|
+
@output.push_literal "Heap: "
|
46
|
+
|
47
|
+
@output.push_local 0
|
48
|
+
@output.send :inspect, 0
|
49
|
+
|
50
|
+
@output.push_literal "\nPointer: "
|
51
|
+
@output.push_local 1
|
52
|
+
@output.push_literal "\n"
|
53
|
+
@output.send :print, 5, true
|
54
|
+
# end Print
|
55
|
+
|
56
|
+
bottom.set!
|
57
|
+
|
58
|
+
@output.use_detected
|
59
|
+
# Return the heap
|
60
|
+
@output.push_local 0
|
61
|
+
@output.ret
|
62
|
+
@output.close
|
63
|
+
|
64
|
+
run_next
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
# AST trasnformation for evaling source string.
|
70
|
+
#
|
71
|
+
# This stage removes the ModuleNode from root of AST tree if
|
72
|
+
# the code being compiled is going to be used for eval. We remove
|
73
|
+
# the module, because in eval we must not return the module object
|
74
|
+
# Also if the last statement is a DiscardNode, we replace it with
|
75
|
+
# its inner expression, in order to return a value.
|
76
|
+
#
|
77
|
+
# If the source is not being compiled for eval, then output is
|
78
|
+
# the same AST given as input.
|
79
|
+
class EvalExpr < Rubinius::Compiler::Stage
|
80
|
+
next_stage Generator
|
81
|
+
|
82
|
+
def initialize(compiler, last)
|
83
|
+
@compiler = compiler
|
84
|
+
super
|
85
|
+
end
|
86
|
+
|
87
|
+
def run
|
88
|
+
@output = @input
|
89
|
+
run_next
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# This stage takes a ruby array as produced by the lexer
|
94
|
+
# and produces a tree of Brainfuck::AST nodes.
|
95
|
+
class BfAST < Rubinius::Compiler::Stage
|
96
|
+
next_stage EvalExpr
|
97
|
+
|
98
|
+
def initialize(compiler, last)
|
99
|
+
@compiler = compiler
|
100
|
+
super
|
101
|
+
end
|
102
|
+
|
103
|
+
def run
|
104
|
+
@output = Brainfuck::Parser.new.apply @input
|
105
|
+
pp(@output) if @compiler.parser.print.ast?
|
106
|
+
run_next
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# This stage takes python code and produces a ruby array
|
111
|
+
# containing representation of the python source.
|
112
|
+
# We are currently using python's own parser, so we just
|
113
|
+
# read the sexp as its printed by bin/astpretty.py
|
114
|
+
class BfCode < Rubinius::Compiler::Stage
|
115
|
+
|
116
|
+
stage :brainfuck_code
|
117
|
+
next_stage BfAST
|
118
|
+
attr_reader :filename, :line
|
119
|
+
attr_accessor :print
|
120
|
+
|
121
|
+
def initialize(compiler, last)
|
122
|
+
super
|
123
|
+
@print = Compiler::Print.new
|
124
|
+
compiler.parser = self
|
125
|
+
end
|
126
|
+
|
127
|
+
def input(code, filename = "eval", line = 1)
|
128
|
+
@code = code
|
129
|
+
@filename = filename
|
130
|
+
@line = line
|
131
|
+
end
|
132
|
+
|
133
|
+
def run
|
134
|
+
code = Lexer.clean(@code)
|
135
|
+
@output = Lexer.new.tokenize(code)
|
136
|
+
pp(@output) if @print.sexp?
|
137
|
+
run_next
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# This stage takes a brainfuck filename and produces a ruby array
|
142
|
+
# containing representation of the brainfuck source.
|
143
|
+
class BfFile < Rubinius::Compiler::Stage
|
144
|
+
|
145
|
+
stage :brainfuck_file
|
146
|
+
next_stage BfAST
|
147
|
+
attr_reader :filename, :line
|
148
|
+
attr_accessor :print
|
149
|
+
|
150
|
+
def initialize(compiler, last)
|
151
|
+
super
|
152
|
+
@print = Compiler::Print.new
|
153
|
+
compiler.parser = self
|
154
|
+
end
|
155
|
+
|
156
|
+
def input(filename, line = 1)
|
157
|
+
@filename = filename
|
158
|
+
@line = line
|
159
|
+
end
|
160
|
+
|
161
|
+
def run
|
162
|
+
code = Lexer.clean(File.read(@filename))
|
163
|
+
@output = Lexer.new.tokenize(code)
|
164
|
+
pp(@output) if @print.sexp?
|
165
|
+
run_next
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
data/lib/brainfuck/version.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BrainfuckAcceptanceTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_ok
|
6
|
+
assert_evaluates [2,1], "++++>++++---.<--."
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_without_loops_nor_user_input
|
10
|
+
STDOUT.expects(:putc).times(2)
|
11
|
+
assert_evaluates [2,1], "++++>++++---.<--."
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_with_user_input
|
15
|
+
STDIN.expects(:getc).returns 97
|
16
|
+
|
17
|
+
assert_evaluates [101], ",++++"
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_with_loops
|
21
|
+
assert_evaluates [1], "++++[-]+-+"
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_transfers_from_one_cell_to_another
|
25
|
+
assert_evaluates [0, 10], "++++++++++ [>+<-]"
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_transfers_from_one_cell_to_the_third
|
29
|
+
assert_evaluates [0, 0, 10], "++++++++++ [>+<-]>[>+<-]"
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_transfers_from_one_cell_to_the_third_and_back_to_the_second
|
33
|
+
assert_evaluates [0, 10, 0], "++++++++++ [>+<-]>[>+<-]>[<+>-]"
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_nested_loop_examples
|
37
|
+
assert_evaluates [0], "[++++++++++[-]+-+-]"
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_hello_world
|
41
|
+
assert_evaluates [0, 87, 100, 33, 10], <<-EOS
|
42
|
+
+++++ +++++
|
43
|
+
[
|
44
|
+
> +++++ ++
|
45
|
+
> +++++ +++++
|
46
|
+
> +++
|
47
|
+
> +
|
48
|
+
<<<< -
|
49
|
+
]
|
50
|
+
> ++ .
|
51
|
+
> + .
|
52
|
+
+++++ ++ .
|
53
|
+
.
|
54
|
+
+++ .
|
55
|
+
> ++ .
|
56
|
+
<< +++++ +++++ +++++ .
|
57
|
+
> .
|
58
|
+
+++ .
|
59
|
+
----- - .
|
60
|
+
----- --- .
|
61
|
+
> + .
|
62
|
+
> .
|
63
|
+
EOS
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def assert_evaluates(expected, code)
|
69
|
+
bnd = Object.new
|
70
|
+
def bnd.get; binding; end
|
71
|
+
bnd = bnd.get
|
72
|
+
mod = nil
|
73
|
+
assert_equal expected, Brainfuck::CodeLoader.execute_code(code, bnd, mod)
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Brainfuck
|
4
|
+
class LexerTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
def test_INSTRUCTIONS_returns_valid_symbols
|
7
|
+
assert_equal %w{> < + - [ ] . ,}, Lexer::INSTRUCTIONS
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_clean_cleans_all_invalid_symbols
|
11
|
+
assert_equal '><+-[-].,', Lexer.clean(">3< 223+fn - ()()[r23-] .bdn*& ,")
|
12
|
+
end
|
13
|
+
|
14
|
+
%w{lparen rparen space space? fwd bwd inc dec puts gets iteration expression}.each do |rule|
|
15
|
+
define_method("test_implements_a_#{rule}_rule") do
|
16
|
+
assert Lexer.new.respond_to?(rule), "Parser should implement a #{rule} rule"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|