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.
Files changed (74) hide show
  1. data/LICENSE +26 -0
  2. data/README.mkd +103 -0
  3. data/Rakefile +21 -0
  4. data/UnicodeData.txt +23697 -0
  5. data/bin/layo +22 -0
  6. data/layo.gemspec +23 -0
  7. data/lib/layo.rb +11 -0
  8. data/lib/layo/ast.rb +5 -0
  9. data/lib/layo/ast/block.rb +13 -0
  10. data/lib/layo/ast/expression.rb +14 -0
  11. data/lib/layo/ast/node.rb +6 -0
  12. data/lib/layo/ast/program.rb +9 -0
  13. data/lib/layo/ast/statement.rb +10 -0
  14. data/lib/layo/interpreter.rb +360 -0
  15. data/lib/layo/lexer.rb +162 -0
  16. data/lib/layo/parser.rb +371 -0
  17. data/lib/layo/peekable.rb +31 -0
  18. data/lib/layo/runtime_error.rb +9 -0
  19. data/lib/layo/syntax_error.rb +14 -0
  20. data/lib/layo/tokenizer.rb +119 -0
  21. data/lib/layo/unexpected_token_error.rb +13 -0
  22. data/lib/layo/unicode.rb +23614 -0
  23. data/lib/layo/unknown_token_error.rb +7 -0
  24. data/spec/interpreter_spec.rb +52 -0
  25. data/spec/lexer_spec.rb +176 -0
  26. data/spec/parser_spec.rb +373 -0
  27. data/spec/source/basic/comments.lol +16 -0
  28. data/spec/source/basic/comments.out +2 -0
  29. data/spec/source/basic/line-continuation.lol +8 -0
  30. data/spec/source/basic/line-continuation.out +2 -0
  31. data/spec/source/basic/line-endings.lol +5 -0
  32. data/spec/source/basic/line-endings.out +3 -0
  33. data/spec/source/basic/minimal.lol +2 -0
  34. data/spec/source/casting/boolean.lol +8 -0
  35. data/spec/source/casting/boolean.out +5 -0
  36. data/spec/source/casting/float.lol +10 -0
  37. data/spec/source/casting/float.out +5 -0
  38. data/spec/source/casting/int.lol +9 -0
  39. data/spec/source/casting/int.out +4 -0
  40. data/spec/source/casting/nil.lol +9 -0
  41. data/spec/source/casting/nil.out +4 -0
  42. data/spec/source/casting/string.lol +5 -0
  43. data/spec/source/casting/string.out +2 -0
  44. data/spec/source/expressions/boolean.lol +30 -0
  45. data/spec/source/expressions/boolean.out +17 -0
  46. data/spec/source/expressions/cast.lol +28 -0
  47. data/spec/source/expressions/cast.out +20 -0
  48. data/spec/source/expressions/function.lol +24 -0
  49. data/spec/source/expressions/function.out +4 -0
  50. data/spec/source/expressions/math.lol +9 -0
  51. data/spec/source/expressions/math.out +7 -0
  52. data/spec/source/expressions/string.lol +20 -0
  53. data/spec/source/expressions/string.out +7 -0
  54. data/spec/source/statements/assignment.lol +8 -0
  55. data/spec/source/statements/assignment.out +3 -0
  56. data/spec/source/statements/cast.lol +11 -0
  57. data/spec/source/statements/cast.out +3 -0
  58. data/spec/source/statements/declaration.lol +9 -0
  59. data/spec/source/statements/declaration.out +2 -0
  60. data/spec/source/statements/expression.lol +10 -0
  61. data/spec/source/statements/expression.out +2 -0
  62. data/spec/source/statements/if_then_else.lol +42 -0
  63. data/spec/source/statements/if_then_else.out +3 -0
  64. data/spec/source/statements/input.in +1 -0
  65. data/spec/source/statements/input.lol +4 -0
  66. data/spec/source/statements/input.out +1 -0
  67. data/spec/source/statements/loop.lol +50 -0
  68. data/spec/source/statements/loop.out +20 -0
  69. data/spec/source/statements/print.lol +7 -0
  70. data/spec/source/statements/print.out +2 -0
  71. data/spec/source/statements/switch.lol +95 -0
  72. data/spec/source/statements/switch.out +12 -0
  73. data/spec/tokenizer_spec.rb +105 -0
  74. 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,5 @@
1
+ require_relative 'ast/node'
2
+ require_relative 'ast/program'
3
+ require_relative 'ast/block'
4
+ require_relative 'ast/statement'
5
+ require_relative 'ast/expression'
@@ -0,0 +1,13 @@
1
+ module Layo::Ast
2
+ class Block < Node
3
+ attr_reader :statement_list
4
+
5
+ def initialize(statement_list = [])
6
+ @statement_list = statement_list
7
+ end
8
+
9
+ def each
10
+ @statement_list.each { |stmt| yield(stmt) }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Layo::Ast
2
+ class Expression < Node
3
+ attr_accessor :type
4
+
5
+ def initialize(type, args = {})
6
+ @type = type
7
+ super(args)
8
+ end
9
+
10
+ def ==(other)
11
+ @type == other.type && super(other)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ require 'ostruct'
2
+
3
+ module Layo::Ast
4
+ # Base class for all AST nodes
5
+ class Node < OpenStruct; end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Layo::Ast
2
+ class Program < Node
3
+ attr_reader :version, :block
4
+
5
+ def initialize(version, block)
6
+ @version, @block = version, block
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Layo::Ast
2
+ class Statement < Node
3
+ attr_accessor :type
4
+
5
+ def initialize(type, args = {})
6
+ @type = type
7
+ super(args)
8
+ end
9
+ end
10
+ end
@@ -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