yabfi 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rbenv-version +1 -0
- data/.rspec +4 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +72 -0
- data/Rakefile +53 -0
- data/bin/yabfi +56 -0
- data/example/brainfuck-to-c.b +31 -0
- data/example/cat.b +1 -0
- data/example/mandelbrot.b +144 -0
- data/ext/yabfi/extconf.rb +7 -0
- data/ext/yabfi/vm.c +340 -0
- data/lib/yabfi/consumer.rb +131 -0
- data/lib/yabfi/encoder.rb +21 -0
- data/lib/yabfi/lexer.rb +73 -0
- data/lib/yabfi/parser.rb +54 -0
- data/lib/yabfi/unroll.rb +33 -0
- data/lib/yabfi/version.rb +5 -0
- data/lib/yabfi.rb +36 -0
- data/spec/lib/yabfi/consumer_spec.rb +223 -0
- data/spec/lib/yabfi/encoder_spec.rb +40 -0
- data/spec/lib/yabfi/lexer_spec.rb +65 -0
- data/spec/lib/yabfi/parser_spec.rb +13 -0
- data/spec/lib/yabfi/unroll_spec.rb +46 -0
- data/spec/lib/yabfi/vm_spec.rb +201 -0
- data/spec/lib/yabfi_spec.rb +62 -0
- data/spec/spec_helper.rb +21 -0
- data/yabfi.gemspec +34 -0
- metadata +219 -0
data/ext/yabfi/vm.c
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Initially, the 256 ints are allocated for the VM. Whenever the memory_cursor
|
5
|
+
* advances beyond that, the memory size is doubled until it reaches 32768, at
|
6
|
+
* which point it will only allocate chunks of that size.
|
7
|
+
*/
|
8
|
+
#define INITIAL_MEMORY_SIZE 256
|
9
|
+
#define MAX_REALLOCATION 32768
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Size of the temporary buffer used by PUT instructions.
|
13
|
+
*/
|
14
|
+
#define INITIAL_BUFFER_SIZE 32
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Constants that map human readable names to instruction codes.
|
18
|
+
*/
|
19
|
+
#define INSTRUCTION_CHANGE_VALUE 0
|
20
|
+
#define INSTRUCTION_CHANGE_POINTER 1
|
21
|
+
#define INSTRUCTION_GET 2
|
22
|
+
#define INSTRUCTION_PUT 3
|
23
|
+
#define INSTRUCTION_BRANCH_IF_ZERO 4
|
24
|
+
#define INSTRUCTION_BRANCH_NOT_ZERO 5
|
25
|
+
|
26
|
+
/**
|
27
|
+
* This struct represents a VM instruction.
|
28
|
+
*/
|
29
|
+
typedef struct {
|
30
|
+
int code;
|
31
|
+
int argument;
|
32
|
+
} instruction;
|
33
|
+
|
34
|
+
/**
|
35
|
+
* This struct contains the state of the VM.
|
36
|
+
*/
|
37
|
+
typedef struct {
|
38
|
+
VALUE input;
|
39
|
+
VALUE output;
|
40
|
+
int eof;
|
41
|
+
|
42
|
+
instruction *instructions;
|
43
|
+
size_t instructions_length;
|
44
|
+
size_t program_counter;
|
45
|
+
|
46
|
+
int *memory;
|
47
|
+
size_t memory_length;
|
48
|
+
size_t memory_cursor;
|
49
|
+
} vm;
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Ruby classes in C!
|
53
|
+
*/
|
54
|
+
|
55
|
+
static VALUE rb_cYABFI;
|
56
|
+
static VALUE rb_cBaseError;
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Document-class: YABFI::VM
|
60
|
+
*
|
61
|
+
* This class, which is implemented as a C extension, executes the
|
62
|
+
* instructions generated by the upstream ruby pipeline.
|
63
|
+
*/
|
64
|
+
static VALUE rb_cVM;
|
65
|
+
/**
|
66
|
+
* Document-class: YABFI::VM::InvalidCommand
|
67
|
+
*
|
68
|
+
* Raised when an InvalidCommand is received by the VM.
|
69
|
+
*/
|
70
|
+
static VALUE rb_cInvalidCommand;
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Document-class: YABFI::VM::MemoryOutOfBounds
|
74
|
+
*
|
75
|
+
* Raised when the memory cursor is moved below zero.
|
76
|
+
*/
|
77
|
+
static VALUE rb_cMemoryOutOfBounds;
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Free the allocated memory for the virtual machine.
|
81
|
+
*/
|
82
|
+
static void
|
83
|
+
vm_free(void *p) {
|
84
|
+
vm *ptr = p;
|
85
|
+
|
86
|
+
if (ptr->instructions_length > 0) {
|
87
|
+
free(ptr->instructions);
|
88
|
+
}
|
89
|
+
|
90
|
+
if (ptr->memory_length > 0) {
|
91
|
+
free(ptr->memory);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Allocate a new VM.
|
97
|
+
*/
|
98
|
+
static VALUE
|
99
|
+
vm_alloc(VALUE klass) {
|
100
|
+
VALUE instance;
|
101
|
+
vm *ptr;
|
102
|
+
|
103
|
+
instance = Data_Make_Struct(klass, vm, NULL, vm_free, ptr);
|
104
|
+
|
105
|
+
ptr->input = Qnil;
|
106
|
+
ptr->output = Qnil;
|
107
|
+
ptr->eof = 0;
|
108
|
+
|
109
|
+
ptr->instructions = NULL;
|
110
|
+
ptr->instructions_length = 0;
|
111
|
+
ptr->program_counter = 0;
|
112
|
+
|
113
|
+
ptr->memory = NULL;
|
114
|
+
ptr->memory_length = 0;
|
115
|
+
ptr->memory_cursor = 0;
|
116
|
+
|
117
|
+
return instance;
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* Initialize a new VM.
|
122
|
+
*
|
123
|
+
* @param input [IO] the input from which the VM reads.
|
124
|
+
* @param output [IO] the output to which the VM writes.
|
125
|
+
* @param eof [Fixnum] the value to return when EOF is reached.
|
126
|
+
*
|
127
|
+
* @!parse [ruby]
|
128
|
+
* class YABFI::VM
|
129
|
+
* def initialize(input, output, eof)
|
130
|
+
* end
|
131
|
+
* end
|
132
|
+
*/
|
133
|
+
static VALUE
|
134
|
+
vm_initialize(VALUE self, VALUE input, VALUE output, VALUE rb_eof) {
|
135
|
+
vm *ptr;
|
136
|
+
|
137
|
+
Check_Type(rb_eof, T_FIXNUM);
|
138
|
+
Data_Get_Struct(self, vm, ptr);
|
139
|
+
|
140
|
+
ptr->input = input;
|
141
|
+
ptr->output = output;
|
142
|
+
ptr->eof = NUM2INT(rb_eof);
|
143
|
+
|
144
|
+
return self;
|
145
|
+
};
|
146
|
+
|
147
|
+
/**
|
148
|
+
* Load the VM with new instructions.
|
149
|
+
*
|
150
|
+
* @param ary [Array<Object>] list of instructions to execute.
|
151
|
+
* @return [nil] unconditionally.
|
152
|
+
*
|
153
|
+
* @!parse [ruby]
|
154
|
+
* class YABFI::VM
|
155
|
+
* def load!(ary)
|
156
|
+
* end
|
157
|
+
* end
|
158
|
+
*/
|
159
|
+
static VALUE
|
160
|
+
vm_load(VALUE self, VALUE ary) {
|
161
|
+
int iter;
|
162
|
+
vm *ptr;
|
163
|
+
VALUE entry, code, arg;
|
164
|
+
|
165
|
+
Data_Get_Struct(self, vm, ptr);
|
166
|
+
|
167
|
+
Check_Type(ary, T_ARRAY);
|
168
|
+
|
169
|
+
ptr->memory_cursor = 0;
|
170
|
+
ptr->memory_length = INITIAL_MEMORY_SIZE;
|
171
|
+
ptr->memory = calloc(INITIAL_MEMORY_SIZE, sizeof(int));
|
172
|
+
|
173
|
+
ptr->program_counter = 0;
|
174
|
+
ptr->instructions_length = RARRAY_LEN(ary);
|
175
|
+
ptr->instructions = malloc(sizeof(instruction) * ptr->instructions_length);
|
176
|
+
|
177
|
+
for (iter = 0; iter < (int) ptr->instructions_length; iter++) {
|
178
|
+
entry = rb_ary_entry(ary, iter);
|
179
|
+
Check_Type(entry, T_ARRAY);
|
180
|
+
if (RARRAY_LEN(entry) != 2) {
|
181
|
+
rb_raise(rb_cInvalidCommand, "Commands must be tuples");
|
182
|
+
}
|
183
|
+
code = rb_ary_entry(entry, 0);
|
184
|
+
arg = rb_ary_entry(entry, 1);
|
185
|
+
Check_Type(code, T_FIXNUM);
|
186
|
+
Check_Type(arg, T_FIXNUM);
|
187
|
+
ptr->instructions[iter] = (instruction) { FIX2INT(code), FIX2INT(arg) };
|
188
|
+
}
|
189
|
+
|
190
|
+
return Qnil;
|
191
|
+
}
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Execute the instructions loaded into the VM.
|
195
|
+
*
|
196
|
+
* @raise [MemoryOutOfBounds] when the memory cursor goes below zero.
|
197
|
+
* @raise [InvalidCommand] when an invalid command is executed.
|
198
|
+
* @return [nil] unconditionally.
|
199
|
+
*
|
200
|
+
* @!parse [ruby]
|
201
|
+
* class YABFI::VM
|
202
|
+
* def execute!
|
203
|
+
* end
|
204
|
+
* end
|
205
|
+
*/
|
206
|
+
static VALUE
|
207
|
+
vm_execute(VALUE self) {
|
208
|
+
vm *ptr;
|
209
|
+
char *buffer;
|
210
|
+
int *tmp_memory;
|
211
|
+
int buffer_size;
|
212
|
+
int delta;
|
213
|
+
int iter;
|
214
|
+
instruction curr;
|
215
|
+
|
216
|
+
Data_Get_Struct(self, vm, ptr);
|
217
|
+
buffer_size = INITIAL_BUFFER_SIZE;
|
218
|
+
buffer = malloc(buffer_size * sizeof(char));
|
219
|
+
|
220
|
+
while (ptr->program_counter < ptr->instructions_length) {
|
221
|
+
curr = ptr->instructions[ptr->program_counter];
|
222
|
+
switch (curr.code) {
|
223
|
+
case INSTRUCTION_CHANGE_VALUE:
|
224
|
+
ptr->memory[ptr->memory_cursor] += curr.argument;
|
225
|
+
ptr->program_counter++;
|
226
|
+
break;
|
227
|
+
case INSTRUCTION_CHANGE_POINTER:
|
228
|
+
if (((int) ptr->memory_cursor + curr.argument) < 0) {
|
229
|
+
rb_raise(rb_cMemoryOutOfBounds, "The memory cursor went below zero");
|
230
|
+
}
|
231
|
+
ptr->memory_cursor += curr.argument;
|
232
|
+
while (ptr->memory_cursor >= ptr->memory_length) {
|
233
|
+
delta = ptr->memory_length;
|
234
|
+
if (delta > MAX_REALLOCATION) {
|
235
|
+
delta = MAX_REALLOCATION;
|
236
|
+
}
|
237
|
+
tmp_memory = ptr->memory;
|
238
|
+
ptr->memory = malloc((ptr->memory_length + delta) * sizeof(int));
|
239
|
+
memcpy(ptr->memory, tmp_memory, ptr->memory_length * sizeof(int));
|
240
|
+
memset(ptr->memory + ptr->memory_length, 0, delta * sizeof(int));
|
241
|
+
ptr->memory_length += delta;
|
242
|
+
free(tmp_memory);
|
243
|
+
}
|
244
|
+
ptr->program_counter++;
|
245
|
+
break;
|
246
|
+
case INSTRUCTION_BRANCH_IF_ZERO:
|
247
|
+
if (ptr->memory[ptr->memory_cursor] == 0) {
|
248
|
+
ptr->program_counter += curr.argument;
|
249
|
+
} else {
|
250
|
+
ptr->program_counter++;
|
251
|
+
}
|
252
|
+
break;
|
253
|
+
case INSTRUCTION_BRANCH_NOT_ZERO:
|
254
|
+
if (ptr->memory[ptr->memory_cursor] != 0) {
|
255
|
+
ptr->program_counter += curr.argument;
|
256
|
+
} else {
|
257
|
+
ptr->program_counter++;
|
258
|
+
}
|
259
|
+
break;
|
260
|
+
case INSTRUCTION_GET:
|
261
|
+
for (iter = 0; iter < curr.argument; iter++) {
|
262
|
+
if (rb_funcall(ptr->input, rb_intern("eof?"), 0)) {
|
263
|
+
ptr->memory[ptr->memory_cursor] = ptr->eof;
|
264
|
+
} else {
|
265
|
+
ptr->memory[ptr->memory_cursor] =
|
266
|
+
FIX2INT(rb_funcall(ptr->input, rb_intern("getbyte"), 0));
|
267
|
+
}
|
268
|
+
}
|
269
|
+
ptr->program_counter++;
|
270
|
+
break;
|
271
|
+
case INSTRUCTION_PUT:
|
272
|
+
if (buffer_size < curr.argument) {
|
273
|
+
free(buffer);
|
274
|
+
buffer_size = curr.argument;
|
275
|
+
buffer = malloc(buffer_size * sizeof(char));
|
276
|
+
}
|
277
|
+
memset(buffer, ptr->memory[ptr->memory_cursor],
|
278
|
+
curr.argument * sizeof(char));
|
279
|
+
rb_funcall(ptr->output, rb_intern("write"), 1,
|
280
|
+
rb_str_new(buffer, curr.argument));
|
281
|
+
ptr->program_counter++;
|
282
|
+
break;
|
283
|
+
default:
|
284
|
+
free(buffer);
|
285
|
+
rb_raise(rb_cInvalidCommand, "Invalid command code: %i", curr.code);
|
286
|
+
}
|
287
|
+
}
|
288
|
+
|
289
|
+
free(buffer);
|
290
|
+
|
291
|
+
return Qnil;
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Return the VM's internal state -- used in testing and debugging.
|
296
|
+
*/
|
297
|
+
static VALUE
|
298
|
+
vm_state(VALUE self) {
|
299
|
+
vm *ptr;
|
300
|
+
VALUE hash;
|
301
|
+
|
302
|
+
Data_Get_Struct(self, vm, ptr);
|
303
|
+
hash = rb_hash_new();
|
304
|
+
|
305
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("memory_cursor")),
|
306
|
+
INT2FIX(ptr->memory_cursor));
|
307
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("memory_length")),
|
308
|
+
INT2FIX(ptr->memory_length));
|
309
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("program_counter")),
|
310
|
+
INT2FIX(ptr->program_counter));
|
311
|
+
if (ptr->memory_cursor < ptr->memory_length) {
|
312
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("current_value")),
|
313
|
+
INT2FIX(ptr->memory[ptr->memory_cursor]));
|
314
|
+
} else {
|
315
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("current_value")), Qnil);
|
316
|
+
}
|
317
|
+
|
318
|
+
return hash;
|
319
|
+
}
|
320
|
+
|
321
|
+
/**
|
322
|
+
* Initialize the C extension by defining all of the classes and methods.
|
323
|
+
*/
|
324
|
+
void
|
325
|
+
Init_vm(void) {
|
326
|
+
rb_cYABFI = rb_const_get(rb_cObject, rb_intern("YABFI"));
|
327
|
+
rb_cBaseError = rb_const_get(rb_cYABFI, rb_intern("BaseError"));
|
328
|
+
|
329
|
+
rb_cVM = rb_define_class_under(rb_cYABFI, "VM", rb_cObject);
|
330
|
+
rb_cInvalidCommand =
|
331
|
+
rb_define_class_under(rb_cVM, "InvalidCommand", rb_cBaseError);
|
332
|
+
rb_cMemoryOutOfBounds =
|
333
|
+
rb_define_class_under(rb_cVM, "MemoryOutOfBounds", rb_cBaseError);
|
334
|
+
|
335
|
+
rb_define_alloc_func(rb_cVM, vm_alloc);
|
336
|
+
rb_define_method(rb_cVM, "initialize", vm_initialize, 3);
|
337
|
+
rb_define_method(rb_cVM, "load!", vm_load, 1);
|
338
|
+
rb_define_method(rb_cVM, "execute!", vm_execute, 0);
|
339
|
+
rb_define_method(rb_cVM, "state", vm_state, 0);
|
340
|
+
}
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module YABFI
|
2
|
+
# This class provides generic methods to declaratively consume an Array of
|
3
|
+
# input.
|
4
|
+
class Consumer
|
5
|
+
# Raised when the expected input does not match the given input.
|
6
|
+
Unsatisfied = Class.new(BaseError)
|
7
|
+
|
8
|
+
# Raised when the end of input is reached.
|
9
|
+
EndOfInput = Class.new(BaseError)
|
10
|
+
|
11
|
+
# @attr_reader [Array<Object>] tokens to consume.
|
12
|
+
attr_reader :tokens
|
13
|
+
|
14
|
+
# Create a new Consumer.
|
15
|
+
#
|
16
|
+
# @param tokens [Array<Object>] consumer input.
|
17
|
+
def initialize(tokens)
|
18
|
+
@tokens = tokens
|
19
|
+
end
|
20
|
+
|
21
|
+
# Lazily evaluated _conumer_idx instnace variable.
|
22
|
+
#
|
23
|
+
# @return [Integer] of the current index of the input consumption.
|
24
|
+
def consume_index
|
25
|
+
@consume_index ||= 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# Seek to the given posision.
|
29
|
+
#
|
30
|
+
# @param n [Integer] the integer to seek to.
|
31
|
+
def seek(n)
|
32
|
+
@consume_index = n
|
33
|
+
end
|
34
|
+
|
35
|
+
# Test if the parse has completed.
|
36
|
+
#
|
37
|
+
# @return [true, false] whether or not the input has been fully consumed.
|
38
|
+
def end_of_input?
|
39
|
+
consume_index >= tokens.length
|
40
|
+
end
|
41
|
+
|
42
|
+
# Look at the next character of input without advancing the consumption.
|
43
|
+
#
|
44
|
+
# @return [Object] the next token in the parse.
|
45
|
+
# @raise [EndOfInput] if the parse has completed.
|
46
|
+
def peek
|
47
|
+
fail EndOfInput, '#peek: end of input' if end_of_input?
|
48
|
+
tokens[consume_index]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Look at the next character of input and advance the parse by one element.
|
52
|
+
#
|
53
|
+
# @return [Object] the next token in the parse.
|
54
|
+
# @raise [EndOfInput] if the parse has completed.
|
55
|
+
def advance
|
56
|
+
peek.tap { seek(consume_index.succ) }
|
57
|
+
end
|
58
|
+
|
59
|
+
# Given an optional error message and predicate, test if the next token in
|
60
|
+
# the parse satisfies the predicate.
|
61
|
+
#
|
62
|
+
# @param message [String] error message to throw when the condition is not
|
63
|
+
# satisfied.
|
64
|
+
# @yieldparam token [Object] the token to test.
|
65
|
+
# @return [Object] the satisfied token.
|
66
|
+
# @raise [EndOfInput] if the parse has completed.
|
67
|
+
# @raise [Unsatisfied] if the condition is not met.
|
68
|
+
def satisfy(message = nil)
|
69
|
+
message ||= '#satisfy:'
|
70
|
+
tok = peek
|
71
|
+
fail Unsatisfied, "#{message} '#{tok}'" unless yield(tok)
|
72
|
+
seek(consume_index.succ)
|
73
|
+
tok
|
74
|
+
end
|
75
|
+
|
76
|
+
# Declare that the next token in the stream should be the given token.
|
77
|
+
#
|
78
|
+
# @param expected [Object] next expected object in the parse.
|
79
|
+
# @return [Object] the satisfied token.
|
80
|
+
# @raise [EndOfInput] if the parse has completed.
|
81
|
+
# @raise [Unsatisfied] if the token does not equal the argument.
|
82
|
+
def eq(expected)
|
83
|
+
satisfy("Expected #{expected}, got:") { |tok| tok == expected }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Declare that the next token in the stream should match the given token.
|
87
|
+
#
|
88
|
+
# @param toks [Array<Object>] list of objects that could match.
|
89
|
+
# @return [Object] the satisfied token.
|
90
|
+
# @raise [EndOfInput] if the parse has completed.
|
91
|
+
# @raise [Unsatisfied] if the token cannot me matched.
|
92
|
+
def one_of(*toks)
|
93
|
+
satisfy("Expected one of #{toks}, got:") { |tok| toks.include?(tok) }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Try a block of code, resetting the parse state on failure.
|
97
|
+
#
|
98
|
+
# @return [Object, nil] the result of the block, or nil if the block fails.
|
99
|
+
def attempt
|
100
|
+
idx = consume_index
|
101
|
+
yield
|
102
|
+
rescue
|
103
|
+
seek(idx)
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
|
107
|
+
# Consume 0 or more occurrences of the given block.
|
108
|
+
#
|
109
|
+
# @return [Object, nil] the result of the block, or nil if the block fails.
|
110
|
+
def many
|
111
|
+
idx = consume_index
|
112
|
+
results = []
|
113
|
+
loop do
|
114
|
+
idx = consume_index
|
115
|
+
results << yield
|
116
|
+
end
|
117
|
+
rescue
|
118
|
+
seek(idx)
|
119
|
+
results
|
120
|
+
end
|
121
|
+
|
122
|
+
# Consume 1 or more occurrences of the given block.
|
123
|
+
#
|
124
|
+
# @return [Object, nil] the result of the block, or nil if the block fails.
|
125
|
+
def many_one(&block)
|
126
|
+
many(&block).tap do |results|
|
127
|
+
fail Unsatisfied, '#many_one: got no results' if results.empty?
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module YABFI
|
2
|
+
# This module encodes the human-readable instruction names to integers.
|
3
|
+
module Encoder
|
4
|
+
# Mapping of human readable instruction names to their encoded integers.
|
5
|
+
INSTRUCTIONS = {
|
6
|
+
change_value: 0,
|
7
|
+
change_pointer: 1,
|
8
|
+
get: 2,
|
9
|
+
put: 3,
|
10
|
+
branch_if_zero: 4,
|
11
|
+
branch_not_zero: 5
|
12
|
+
}
|
13
|
+
|
14
|
+
module_function
|
15
|
+
|
16
|
+
# Encode a list of instructions into
|
17
|
+
def encode(ary)
|
18
|
+
ary.map { |(code, argument)| [INSTRUCTIONS[code], argument] }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/yabfi/lexer.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module YABFI
|
2
|
+
# This module consumes tokens produced by the Parser and produces an
|
3
|
+
# unoptimized syntax tree.
|
4
|
+
class Lexer < Consumer
|
5
|
+
# This Hash maps tokens to method names to optimize the performance of the
|
6
|
+
# lexer.
|
7
|
+
DISPATCH_TABLE = {
|
8
|
+
loop: :while_loop,
|
9
|
+
succ: :change_value,
|
10
|
+
pred: :change_value,
|
11
|
+
next: :change_pointer,
|
12
|
+
prev: :change_pointer,
|
13
|
+
get: :get,
|
14
|
+
put: :put
|
15
|
+
}
|
16
|
+
|
17
|
+
# Run the lexer on the given tokens.
|
18
|
+
#
|
19
|
+
# @param tokens [Array<Symbol>] the input tokens.
|
20
|
+
# @raise [Consumer::Error] when the lexing fails.
|
21
|
+
# @return [Array<Object>] the lexed syntax tree.
|
22
|
+
def self.run!(tokens)
|
23
|
+
new(tokens).send(:run!)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def run!
|
29
|
+
forest = commands
|
30
|
+
return forest if end_of_input?
|
31
|
+
fail Consumer::Unsatisfied, "Unexpected token #{peek}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def commands
|
35
|
+
many { command }
|
36
|
+
end
|
37
|
+
|
38
|
+
def command
|
39
|
+
method = DISPATCH_TABLE[peek]
|
40
|
+
fail Consumer::Unsatisfied, "Unexpected token #{peek}" unless method
|
41
|
+
send(method)
|
42
|
+
end
|
43
|
+
|
44
|
+
def while_loop
|
45
|
+
eq(:loop)
|
46
|
+
inner = commands
|
47
|
+
eq(:end)
|
48
|
+
[:loop, inner]
|
49
|
+
end
|
50
|
+
|
51
|
+
def get
|
52
|
+
count = many_one { eq(:get) }.count
|
53
|
+
[:get, count]
|
54
|
+
end
|
55
|
+
|
56
|
+
def put
|
57
|
+
count = many_one { eq(:put) }.count
|
58
|
+
[:put, count]
|
59
|
+
end
|
60
|
+
|
61
|
+
def change_value
|
62
|
+
toks = many_one { one_of(:succ, :pred) }
|
63
|
+
total = toks.reduce(0) { |a, e| e == :succ ? a.succ : a.pred }
|
64
|
+
[:change_value, total]
|
65
|
+
end
|
66
|
+
|
67
|
+
def change_pointer
|
68
|
+
toks = many_one { one_of(:next, :prev) }
|
69
|
+
total = toks.reduce(0) { |a, e| e == :next ? a.succ : a.pred }
|
70
|
+
[:change_pointer, total]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/yabfi/parser.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module YABFI
|
2
|
+
# This module contains a set of functions that lazily parse an IO object and
|
3
|
+
# yield a symbol for each non-comment character that is read in.
|
4
|
+
module Parser
|
5
|
+
# Maximum number of bytes to read in from the IO object at a time.
|
6
|
+
DEFAULT_BUFFER_SIZE = 1_024
|
7
|
+
|
8
|
+
# Maps characters to human-readable Symbol command names.
|
9
|
+
COMMAND_MAPPINGS = {
|
10
|
+
'+' => :succ,
|
11
|
+
'-' => :pred,
|
12
|
+
'>' => :next,
|
13
|
+
'<' => :prev,
|
14
|
+
',' => :get,
|
15
|
+
'.' => :put,
|
16
|
+
'[' => :loop,
|
17
|
+
']' => :end
|
18
|
+
}
|
19
|
+
|
20
|
+
module_function
|
21
|
+
|
22
|
+
# Lazily parse an IO object while it still has input.
|
23
|
+
#
|
24
|
+
# @param io [IO] the object from which the parser lazily reads.
|
25
|
+
# @param buffer_size [Integer] maximum size to request from the IO at once.
|
26
|
+
# @yield [command] Symbol that represents the parsed command.
|
27
|
+
# @return [Enumator<Symbol>] of commands when no block is given.
|
28
|
+
def parse(io, buffer_size = DEFAULT_BUFFER_SIZE)
|
29
|
+
return enum_for(:parse, io, buffer_size) unless block_given?
|
30
|
+
loop do
|
31
|
+
buffer = read(io, buffer_size)
|
32
|
+
break unless buffer
|
33
|
+
buffer.each_char do |char|
|
34
|
+
command = COMMAND_MAPPINGS[char]
|
35
|
+
yield command if command
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Block waiting for the next set of commands.
|
41
|
+
#
|
42
|
+
# @param io [IO] the object from which the parser lazily reads.
|
43
|
+
# @param size [Integer] the maximum number of bytes to read in.
|
44
|
+
# @return [String, nil] the buffer of bytes read in, or nil on EOF.
|
45
|
+
def read(io, size)
|
46
|
+
io.read_nonblock(size)
|
47
|
+
rescue IO::WaitReadable
|
48
|
+
IO.select([io])
|
49
|
+
retry
|
50
|
+
rescue EOFError
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/yabfi/unroll.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module YABFI
|
2
|
+
# This module is used to transforms unrolls loops into multiple
|
3
|
+
# branch_if_zero and branch_not_zero instructions.
|
4
|
+
module Unroll
|
5
|
+
module_function
|
6
|
+
|
7
|
+
# Unroll an entire syntax forest.
|
8
|
+
#
|
9
|
+
# @param forest [Array<Object>] the forest to unroll.
|
10
|
+
# @return [Array<Object>] the unrolled commands.
|
11
|
+
def unroll(forest)
|
12
|
+
forest.each_with_object([]) do |(command, arg), ary|
|
13
|
+
if command == :loop
|
14
|
+
ary.push(*unroll_loop(arg))
|
15
|
+
else
|
16
|
+
ary.push([command, arg])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Unroll a single loop of commands.
|
22
|
+
#
|
23
|
+
# @param commands [Array<Object>] the loop to unroll.
|
24
|
+
# @return [Array<Object>] the unrolled commands.
|
25
|
+
def unroll_loop(commands)
|
26
|
+
unroll(commands).tap do |unrolled|
|
27
|
+
offset = unrolled.length
|
28
|
+
unrolled.unshift([:branch_if_zero, offset + 2])
|
29
|
+
unrolled.push([:branch_not_zero, -1 * offset])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/yabfi.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
# YABFI (Yet Another BrainFuck Interpreter) is the top level module for the gem.
|
4
|
+
module YABFI
|
5
|
+
# This is the base error for the gem from which the rest of the errors
|
6
|
+
# subclass.
|
7
|
+
BaseError = Class.new(StandardError)
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# Evaluate an IO of commands
|
12
|
+
#
|
13
|
+
# @param commands [String, IO] the commands to execute.
|
14
|
+
# @param input [IO] the input from which the commands read.
|
15
|
+
# @param output [IO] the output to which the commands write.
|
16
|
+
# @param eof [Integer] the value to set when EOF is reached.
|
17
|
+
# @raise [BaseError] when there is a compiling or execution error.
|
18
|
+
def eval!(commands, input: $stdin, output: $stdout, eof: 0)
|
19
|
+
io = commands.is_a?(String) ? StringIO.new(commands) : commands
|
20
|
+
tokens = Parser.parse(io)
|
21
|
+
lexed = Lexer.run!(tokens.to_a)
|
22
|
+
commands = Unroll.unroll(lexed)
|
23
|
+
encoded = Encoder.encode(commands)
|
24
|
+
vm = VM.new(input, output, eof)
|
25
|
+
vm.load!(encoded)
|
26
|
+
vm.execute!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'yabfi/version'
|
31
|
+
require 'yabfi/consumer'
|
32
|
+
require 'yabfi/parser'
|
33
|
+
require 'yabfi/lexer'
|
34
|
+
require 'yabfi/unroll'
|
35
|
+
require 'yabfi/encoder'
|
36
|
+
require 'yabfi/vm'
|