yabfi 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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'
|