layo 1.0.0
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.
- data/LICENSE +26 -0
- data/README.mkd +103 -0
- data/Rakefile +21 -0
- data/UnicodeData.txt +23697 -0
- data/bin/layo +22 -0
- data/layo.gemspec +23 -0
- data/lib/layo.rb +11 -0
- data/lib/layo/ast.rb +5 -0
- data/lib/layo/ast/block.rb +13 -0
- data/lib/layo/ast/expression.rb +14 -0
- data/lib/layo/ast/node.rb +6 -0
- data/lib/layo/ast/program.rb +9 -0
- data/lib/layo/ast/statement.rb +10 -0
- data/lib/layo/interpreter.rb +360 -0
- data/lib/layo/lexer.rb +162 -0
- data/lib/layo/parser.rb +371 -0
- data/lib/layo/peekable.rb +31 -0
- data/lib/layo/runtime_error.rb +9 -0
- data/lib/layo/syntax_error.rb +14 -0
- data/lib/layo/tokenizer.rb +119 -0
- data/lib/layo/unexpected_token_error.rb +13 -0
- data/lib/layo/unicode.rb +23614 -0
- data/lib/layo/unknown_token_error.rb +7 -0
- data/spec/interpreter_spec.rb +52 -0
- data/spec/lexer_spec.rb +176 -0
- data/spec/parser_spec.rb +373 -0
- data/spec/source/basic/comments.lol +16 -0
- data/spec/source/basic/comments.out +2 -0
- data/spec/source/basic/line-continuation.lol +8 -0
- data/spec/source/basic/line-continuation.out +2 -0
- data/spec/source/basic/line-endings.lol +5 -0
- data/spec/source/basic/line-endings.out +3 -0
- data/spec/source/basic/minimal.lol +2 -0
- data/spec/source/casting/boolean.lol +8 -0
- data/spec/source/casting/boolean.out +5 -0
- data/spec/source/casting/float.lol +10 -0
- data/spec/source/casting/float.out +5 -0
- data/spec/source/casting/int.lol +9 -0
- data/spec/source/casting/int.out +4 -0
- data/spec/source/casting/nil.lol +9 -0
- data/spec/source/casting/nil.out +4 -0
- data/spec/source/casting/string.lol +5 -0
- data/spec/source/casting/string.out +2 -0
- data/spec/source/expressions/boolean.lol +30 -0
- data/spec/source/expressions/boolean.out +17 -0
- data/spec/source/expressions/cast.lol +28 -0
- data/spec/source/expressions/cast.out +20 -0
- data/spec/source/expressions/function.lol +24 -0
- data/spec/source/expressions/function.out +4 -0
- data/spec/source/expressions/math.lol +9 -0
- data/spec/source/expressions/math.out +7 -0
- data/spec/source/expressions/string.lol +20 -0
- data/spec/source/expressions/string.out +7 -0
- data/spec/source/statements/assignment.lol +8 -0
- data/spec/source/statements/assignment.out +3 -0
- data/spec/source/statements/cast.lol +11 -0
- data/spec/source/statements/cast.out +3 -0
- data/spec/source/statements/declaration.lol +9 -0
- data/spec/source/statements/declaration.out +2 -0
- data/spec/source/statements/expression.lol +10 -0
- data/spec/source/statements/expression.out +2 -0
- data/spec/source/statements/if_then_else.lol +42 -0
- data/spec/source/statements/if_then_else.out +3 -0
- data/spec/source/statements/input.in +1 -0
- data/spec/source/statements/input.lol +4 -0
- data/spec/source/statements/input.out +1 -0
- data/spec/source/statements/loop.lol +50 -0
- data/spec/source/statements/loop.out +20 -0
- data/spec/source/statements/print.lol +7 -0
- data/spec/source/statements/print.out +2 -0
- data/spec/source/statements/switch.lol +95 -0
- data/spec/source/statements/switch.out +12 -0
- data/spec/tokenizer_spec.rb +105 -0
- metadata +135 -0
data/bin/layo
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
|
+
|
5
|
+
require 'layo'
|
6
|
+
|
7
|
+
if ARGV.empty?
|
8
|
+
puts 'Usage: layo [filename]'
|
9
|
+
exit
|
10
|
+
end
|
11
|
+
raise "File #{ARGV[0]} does not exist" unless File.exists?(ARGV[0])
|
12
|
+
File.open(ARGV[0]) do |f|
|
13
|
+
parser = Layo::Parser.new(Layo::Tokenizer.new(Layo::Lexer.new(f)))
|
14
|
+
interpreter = Layo::Interpreter.new
|
15
|
+
begin
|
16
|
+
interpreter.interpret(parser.parse)
|
17
|
+
rescue Layo::SyntaxError => e
|
18
|
+
$stderr.puts "Syntax error: #{e}"
|
19
|
+
rescue Layo::RuntimeError => e
|
20
|
+
$stderr.puts "Runtime error: #{e}"
|
21
|
+
end
|
22
|
+
end
|
data/layo.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'layo'
|
5
|
+
s.version = '1.0.0'
|
6
|
+
s.summary = 'LOLCODE interpreter written in plain Ruby'
|
7
|
+
s.description = <<-EOF
|
8
|
+
Layo is a LOLCODE interpreter written in plain Ruby. It tries to conform to
|
9
|
+
the LOLCODE 1.2 specification and supports everything described there.
|
10
|
+
EOF
|
11
|
+
s.required_ruby_version = '>= 1.9.2'
|
12
|
+
s.add_development_dependency 'mocha', '~> 0.10.0'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
|
16
|
+
s.executables << 'layo'
|
17
|
+
|
18
|
+
s.test_files = s.files.select { |path| path =~ /^spec\/.*_spec\.rb/ }
|
19
|
+
|
20
|
+
s.authors = ['Galymzhan Kozhayev']
|
21
|
+
s.email = Base64.decode64("a296aGF5ZXZAZ21haWwuY29t\n")
|
22
|
+
s.homepage = 'http://github.com/galymzhan/layo'
|
23
|
+
end
|
data/lib/layo.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'layo/syntax_error'
|
2
|
+
require_relative 'layo/runtime_error'
|
3
|
+
require_relative 'layo/unknown_token_error'
|
4
|
+
require_relative 'layo/unexpected_token_error'
|
5
|
+
require_relative 'layo/peekable'
|
6
|
+
require_relative 'layo/lexer'
|
7
|
+
require_relative 'layo/tokenizer'
|
8
|
+
require_relative 'layo/parser'
|
9
|
+
require_relative 'layo/interpreter'
|
10
|
+
require_relative 'layo/ast'
|
11
|
+
require_relative 'layo/unicode'
|
data/lib/layo/ast.rb
ADDED
@@ -0,0 +1,360 @@
|
|
1
|
+
module Layo
|
2
|
+
class Interpreter
|
3
|
+
attr_accessor :input, :output
|
4
|
+
|
5
|
+
def initialize(input = STDIN, output = STDOUT)
|
6
|
+
@input, @output = input, output
|
7
|
+
end
|
8
|
+
|
9
|
+
# Interprets program given as an AST node
|
10
|
+
def interpret(program)
|
11
|
+
# We should gather all function definitions along with their bodies
|
12
|
+
# beforehand so we could call them wherever a call appears
|
13
|
+
@functions = {}
|
14
|
+
program.block.each do |statement|
|
15
|
+
if statement.type == 'function'
|
16
|
+
@functions[statement.name] = {
|
17
|
+
args: statement.args, block: statement.block
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
eval_program(program)
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_variable_table
|
25
|
+
table = Hash.new do |hash, key|
|
26
|
+
raise RuntimeError, "Variable '#{key}' is not declared"
|
27
|
+
end
|
28
|
+
table['IT'] = { type: :noob, value: nil}
|
29
|
+
table
|
30
|
+
end
|
31
|
+
|
32
|
+
def eval_program(program)
|
33
|
+
@vtable = create_variable_table
|
34
|
+
begin
|
35
|
+
illegal = true
|
36
|
+
catch(:break) do
|
37
|
+
catch(:return) do
|
38
|
+
eval_block(program.block)
|
39
|
+
illegal = false
|
40
|
+
end
|
41
|
+
raise RuntimeError, "Illegal return statement" if illegal
|
42
|
+
end
|
43
|
+
raise RuntimeError, "Illegal break statement" if illegal
|
44
|
+
rescue RuntimeError => e
|
45
|
+
e.line = @stmt_line
|
46
|
+
raise e
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def eval_block(block)
|
51
|
+
block.each do |stmt|
|
52
|
+
@stmt_line = stmt.line
|
53
|
+
send("eval_#{stmt.type}_stmt", stmt)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def eval_assignment_stmt(stmt)
|
58
|
+
# We should access by variable name first to ensure that it is defined
|
59
|
+
@vtable[stmt.identifier]
|
60
|
+
@vtable[stmt.identifier] = eval_expr(stmt.expression)
|
61
|
+
end
|
62
|
+
|
63
|
+
def eval_break_stmt(stmt)
|
64
|
+
throw :break
|
65
|
+
end
|
66
|
+
|
67
|
+
def eval_cast_stmt(stmt)
|
68
|
+
var = @vtable[stmt.identifier]
|
69
|
+
var[:value] = cast(var, stmt.to, false)
|
70
|
+
var[:type] = stmt.to
|
71
|
+
end
|
72
|
+
|
73
|
+
def eval_declaration_stmt(stmt)
|
74
|
+
if @vtable.has_key?(stmt.identifier)
|
75
|
+
raise RuntimeError, "Variable '#{stmt.identifier}' is already declared"
|
76
|
+
end
|
77
|
+
@vtable[stmt.identifier] = { type: :noob, value: nil }
|
78
|
+
unless stmt.initialization.nil?
|
79
|
+
@vtable[stmt.identifier] = eval_expr(stmt.initialization)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def eval_expression_stmt(stmt)
|
84
|
+
@vtable['IT'] = eval_expr(stmt.expression)
|
85
|
+
end
|
86
|
+
|
87
|
+
def eval_function_stmt(stmt); end
|
88
|
+
|
89
|
+
def eval_condition_stmt(stmt)
|
90
|
+
if cast(@vtable['IT'], :troof)
|
91
|
+
# if block
|
92
|
+
eval_block(stmt.then)
|
93
|
+
else
|
94
|
+
# else if blocks
|
95
|
+
condition_met = false
|
96
|
+
stmt.elseif.each do |elseif|
|
97
|
+
condition = eval_expr(elseif[:condition])
|
98
|
+
if condition_met = cast(condition, :troof)
|
99
|
+
eval_block(elseif[:block])
|
100
|
+
break
|
101
|
+
end
|
102
|
+
end
|
103
|
+
unless condition_met || stmt.else.nil?
|
104
|
+
# else block
|
105
|
+
eval_block(stmt.else)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def eval_input_stmt(stmt)
|
111
|
+
@vtable[stmt.identifier] = { type: :yarn, value: @input.gets }
|
112
|
+
end
|
113
|
+
|
114
|
+
def eval_loop_stmt(stmt)
|
115
|
+
unless stmt.op.nil?
|
116
|
+
# Backup any local variable if its name is the same as the counter
|
117
|
+
# variable's name
|
118
|
+
if @vtable.has_key?(stmt.counter)
|
119
|
+
var_backup = @vtable[stmt.counter]
|
120
|
+
end
|
121
|
+
@vtable[stmt.counter] = { type: :numbr, value: 0 }
|
122
|
+
update_op = if stmt.op == :uppin
|
123
|
+
lambda { @vtable[stmt.counter][:value] += 1 }
|
124
|
+
elsif stmt.op == :nerfin
|
125
|
+
lambda { @vtable[stmt.counter][:value] -= 1 }
|
126
|
+
else
|
127
|
+
lambda {
|
128
|
+
@vtable[stmt.counter] = call_func(stmt.op, [@vtable[stmt.counter]])
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
catch :break do
|
134
|
+
while true
|
135
|
+
unless stmt.guard.nil?
|
136
|
+
condition_met = cast(eval_expr(stmt.guard[:expression]), :troof)
|
137
|
+
if (stmt.guard[:type] == :wile && !condition_met) or
|
138
|
+
(stmt.guard[:type] == :til && condition_met)
|
139
|
+
throw :break
|
140
|
+
end
|
141
|
+
end
|
142
|
+
eval_block(stmt.block)
|
143
|
+
update_op.call if update_op
|
144
|
+
end
|
145
|
+
end
|
146
|
+
# Restore backed up variable
|
147
|
+
unless stmt.op.nil? || var_backup.nil?
|
148
|
+
@vtable[stmt.counter] = var_backup
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def eval_print_stmt(stmt)
|
153
|
+
text = ''
|
154
|
+
# todo rewrite using map or similar
|
155
|
+
stmt.expressions.each do |expr|
|
156
|
+
text << cast(eval_expr(expr), :yarn)
|
157
|
+
end
|
158
|
+
if stmt.suppress
|
159
|
+
@output.print text
|
160
|
+
else
|
161
|
+
@output.puts text
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def eval_return_stmt(stmt)
|
166
|
+
throw :return, eval_expr(stmt.expression)
|
167
|
+
end
|
168
|
+
|
169
|
+
def eval_switch_stmt(stmt)
|
170
|
+
stmt.cases.combination(2) do |c|
|
171
|
+
raise RuntimeError, 'Literals must be unique' if c[0] == c[1]
|
172
|
+
end
|
173
|
+
case_found = false
|
174
|
+
it = @vtable['IT']
|
175
|
+
stmt.cases.each do |kase|
|
176
|
+
unless case_found
|
177
|
+
literal = eval_expr(kase[:expression])
|
178
|
+
if it == literal
|
179
|
+
case_found = true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
if case_found
|
183
|
+
breaked = true
|
184
|
+
catch :break do
|
185
|
+
eval_block(kase[:block])
|
186
|
+
breaked = false
|
187
|
+
end
|
188
|
+
break if breaked
|
189
|
+
end
|
190
|
+
end
|
191
|
+
unless case_found || stmt.default.nil?
|
192
|
+
catch :break do
|
193
|
+
eval_block(stmt.default)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Casts given variable 'var' into type 'to'
|
199
|
+
# Returns only value part of the variable, type will be 'to' anyway
|
200
|
+
def cast(var, to, implicit = true)
|
201
|
+
return var[:value] if var[:type] == to
|
202
|
+
return nil if to == :noob
|
203
|
+
case var[:type]
|
204
|
+
when :noob
|
205
|
+
if implicit && to != :troof
|
206
|
+
raise RuntimeError, "NOOB cannot be implicitly cast into #{to.to_s.upcase}"
|
207
|
+
end
|
208
|
+
return false if to == :troof
|
209
|
+
return 0 if to == :numbr
|
210
|
+
return 0.0 if to == :numbar
|
211
|
+
return ''
|
212
|
+
when :troof
|
213
|
+
return (var[:value] ? 1 : 0) if to == :numbr
|
214
|
+
return (var[:value] ? 1.0 : 0.0) if to == :numbar
|
215
|
+
return (var[:value] ? 'WIN' : 'FAIL')
|
216
|
+
when :numbr
|
217
|
+
return (var[:value].zero? ? false : true) if to == :troof
|
218
|
+
return var[:value].to_f if to == :numbar
|
219
|
+
return var[:value].to_s
|
220
|
+
when :numbar
|
221
|
+
return (var[:value].zero? ? false : true) if to == :troof
|
222
|
+
return var[:value].to_int if to == :numbr
|
223
|
+
# Truncate to 2 digits after decimal point
|
224
|
+
return ((var[:value] * 100).floor / 100.0).to_s
|
225
|
+
else
|
226
|
+
return !var[:value].empty? if to == :troof
|
227
|
+
if to == :numbr
|
228
|
+
return var[:value].to_i if var[:value].lol_integer?
|
229
|
+
raise RuntimeError, "'#{var[:value]}' is not a valid integer"
|
230
|
+
end
|
231
|
+
return var[:value].to_f if var[:value].lol_float?
|
232
|
+
raise RuntimeError, "'#{var[:value]}' is not a valid float"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def eval_expr(expr)
|
237
|
+
send("eval_#{expr.type}_expr", expr)
|
238
|
+
end
|
239
|
+
|
240
|
+
def eval_binary_expr(expr)
|
241
|
+
l = eval_expr(expr.left)
|
242
|
+
r = eval_expr(expr.right)
|
243
|
+
methods = {
|
244
|
+
:sum_of => :+, :diff_of => :-, :produkt_of => :*, :quoshunt_of => :/,
|
245
|
+
:mod_of => :modulo, :both_of => :&, :either_of => :|, :won_of => :^,
|
246
|
+
:both_saem => :==, :diffrint => :!=
|
247
|
+
}
|
248
|
+
case expr.operator
|
249
|
+
when :sum_of, :diff_of, :produkt_of, :quoshunt_of, :mod_of, :biggr_of, :smallr_of
|
250
|
+
type = l[:type] == :numbar || r[:type] == :numbar ||
|
251
|
+
(l[:type] == :yarn && l[:value].lol_float?) ||
|
252
|
+
(r[:type] == :yarn && r[:value].lol_float?) ? :numbar : :numbr
|
253
|
+
l, r = cast(l, type), cast(r, type)
|
254
|
+
if expr.operator == :biggr_of
|
255
|
+
value = [l, r].max
|
256
|
+
elsif expr.operator == :smallr_of
|
257
|
+
value = [l, r].min
|
258
|
+
else
|
259
|
+
value = l.send(methods[expr.operator], r)
|
260
|
+
end
|
261
|
+
when :both_saem, :diffrint
|
262
|
+
type = :troof
|
263
|
+
if (l[:type] == :numbr && r[:type] == :numbar) ||
|
264
|
+
(l[:type] == :numbar && r[:type] == :numbr)
|
265
|
+
l, r = cast(l, :numbar), cast(r, :numbar)
|
266
|
+
elsif l[:type] != r[:type]
|
267
|
+
raise RuntimeError, 'Operands must have same type'
|
268
|
+
end
|
269
|
+
value = l.send(methods[expr.operator], r)
|
270
|
+
else
|
271
|
+
type = :troof
|
272
|
+
l, r = cast(l, :troof), cast(r, :troof)
|
273
|
+
value = l.send(methods[expr.operator], r)
|
274
|
+
end
|
275
|
+
{ type: type, value: value }
|
276
|
+
end
|
277
|
+
|
278
|
+
def eval_cast_expr(expr)
|
279
|
+
casted_expr = eval_expr(expr.being_casted)
|
280
|
+
{ type: expr.to, value: cast(casted_expr, expr.to, false) }
|
281
|
+
end
|
282
|
+
|
283
|
+
def eval_constant_expr(expr)
|
284
|
+
mapping = { boolean: :troof, string: :yarn, integer: :numbr, float: :numbar }
|
285
|
+
value = expr.vtype == :string ? interpolate_string(expr.value) : expr.value
|
286
|
+
{ type: mapping[expr.vtype], value: value }
|
287
|
+
end
|
288
|
+
|
289
|
+
def eval_function_expr(expr)
|
290
|
+
parameters = []
|
291
|
+
expr.parameters.each do |param|
|
292
|
+
parameters << eval_expr(param)
|
293
|
+
end
|
294
|
+
call_func(expr.name, parameters)
|
295
|
+
end
|
296
|
+
|
297
|
+
def call_func(name, arguments)
|
298
|
+
function = @functions[name]
|
299
|
+
# Replace variable table by 'clean' variable table inside functions
|
300
|
+
old_table = @vtable
|
301
|
+
@vtable = create_variable_table
|
302
|
+
function[:args].each_index do |index|
|
303
|
+
@vtable[function[:args][index]] = arguments[index]
|
304
|
+
end
|
305
|
+
retval = nil
|
306
|
+
retval = catch :return do
|
307
|
+
breaked = true
|
308
|
+
catch(:break) do
|
309
|
+
eval_block(function[:block])
|
310
|
+
breaked = false
|
311
|
+
end
|
312
|
+
retval = { type: :noob, value: nil } if breaked
|
313
|
+
end
|
314
|
+
retval = @vtable['IT'] if retval.nil?
|
315
|
+
@vtable = old_table
|
316
|
+
retval
|
317
|
+
end
|
318
|
+
|
319
|
+
def eval_nary_expr(expr)
|
320
|
+
case expr.operator
|
321
|
+
when :all_of
|
322
|
+
type, value = :troof, true
|
323
|
+
expr.expressions.each do |operand|
|
324
|
+
unless cast(eval_expr(operand), :troof)
|
325
|
+
value = false
|
326
|
+
break
|
327
|
+
end
|
328
|
+
end
|
329
|
+
when :any_of
|
330
|
+
type, value = :troof, false
|
331
|
+
expr.expressions.each do |operand|
|
332
|
+
if cast(eval_expr(operand), :troof)
|
333
|
+
value = true
|
334
|
+
break
|
335
|
+
end
|
336
|
+
end
|
337
|
+
when :smoosh
|
338
|
+
type, value = :yarn, ''
|
339
|
+
expr.expressions.each do |operand|
|
340
|
+
value << cast(eval_expr(operand), :yarn)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
{ type: type, value: value }
|
344
|
+
end
|
345
|
+
|
346
|
+
def eval_unary_expr(expr)
|
347
|
+
# the only unary op in LOLCODE is NOT
|
348
|
+
{ type: :troof, value: !cast(eval_expr(expr.expression), :troof) }
|
349
|
+
end
|
350
|
+
|
351
|
+
def eval_variable_expr(expr)
|
352
|
+
@vtable[expr.name]
|
353
|
+
end
|
354
|
+
|
355
|
+
# Interpolates values of variables in the string
|
356
|
+
def interpolate_string(str)
|
357
|
+
str.gsub(/:\{([a-zA-Z]\w*)\}/) { cast(@vtable[$1], :yarn, false) }
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|