layo 1.0.0 → 1.1.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/README.mkd +30 -5
- data/bin/layo +17 -13
- data/examples/1.lol +27 -0
- data/examples/2.lol +37 -0
- data/examples/3.lol +30 -0
- data/layo.gemspec +2 -1
- data/lib/layo.rb +6 -0
- data/lib/layo/base_interpreter.rb +348 -0
- data/lib/layo/interactive_interpreter.rb +42 -0
- data/lib/layo/interpreter.rb +4 -339
- data/lib/layo/lexer.rb +6 -1
- data/lib/layo/parser.rb +5 -1
- data/lib/layo/tokenizer.rb +4 -2
- data/spec/interpreter_spec.rb +14 -0
- data/spec/source/statements/input.out +1 -0
- metadata +10 -5
data/README.mkd
CHANGED
@@ -5,7 +5,7 @@ Layo is a LOLCODE interpreter written in Ruby. It tries to conform to the
|
|
5
5
|
everything described there. I purposely did not use any parser generators
|
6
6
|
such as `Racc` for this project.
|
7
7
|
|
8
|
-
Latest version is 1.
|
8
|
+
Latest version is 1.1.0
|
9
9
|
|
10
10
|
# Usage
|
11
11
|
|
@@ -16,20 +16,43 @@ gem install layo
|
|
16
16
|
layo program.lol
|
17
17
|
```
|
18
18
|
|
19
|
-
If you don't want to install the gem, you can download sources,
|
20
|
-
and run from project's root:
|
19
|
+
If you don't want to install the gem, you can download sources, extract somewhere
|
20
|
+
and run from the project's root:
|
21
21
|
|
22
22
|
`bin/layo program.lol`
|
23
23
|
|
24
|
+
Layo treats source files as UTF-8 encoded, if your file contains non-ascii
|
25
|
+
characters, be sure to encode it as UTF-8.
|
26
|
+
|
27
|
+
Without arguments Layo works in interactive mode, i.e. it executes statements
|
28
|
+
read from a standard input. A sample session:
|
29
|
+
|
30
|
+
```
|
31
|
+
[galymzhan@g8host layo]$ bin/layo
|
32
|
+
Layo version 1.1.0
|
33
|
+
Press Control-C to exit
|
34
|
+
> VISIBLE "HAI WORLD!"
|
35
|
+
HAI WORLD!
|
36
|
+
> I HAS A animal ITZ "cat"
|
37
|
+
> BOTH SAEM animal AN "cat", O RLY?
|
38
|
+
YA RLY, VISIBLE "I HAV A CAT"
|
39
|
+
NO WAI, VISIBLE "J00 SUX"
|
40
|
+
OIC
|
41
|
+
I HAV A CAT
|
42
|
+
> ^CExiting
|
43
|
+
```
|
44
|
+
|
24
45
|
Oh, and there are tests too. In order to run them, you have to install `mocha`:
|
25
|
-
`gem install mocha`. Then go to project's root and execute `rake test`.
|
46
|
+
`gem install mocha`. Then go to the project's root and execute `rake test`.
|
26
47
|
|
27
48
|
# Requirements
|
28
49
|
|
29
50
|
* Ruby >= 1.9.2
|
30
51
|
* [Mocha](http://mocha.rubyforge.org/) for development
|
31
52
|
|
32
|
-
#
|
53
|
+
# LOLCODE examples
|
54
|
+
|
55
|
+
Classical hello world:
|
33
56
|
|
34
57
|
```
|
35
58
|
HAI 1.2
|
@@ -37,6 +60,8 @@ VISIBLE "Hello world!"
|
|
37
60
|
KTHXBYE
|
38
61
|
```
|
39
62
|
|
63
|
+
For more examples see `examples` and `spec/source` directories.
|
64
|
+
|
40
65
|
# Grammar
|
41
66
|
|
42
67
|
Layo uses the following context-free grammar in Extended Backus-Naur Form
|
data/bin/layo
CHANGED
@@ -5,18 +5,22 @@ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
|
5
5
|
require 'layo'
|
6
6
|
|
7
7
|
if ARGV.empty?
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
8
|
+
parser = Layo::Parser.new(Layo::Tokenizer.new(Layo::Lexer.new(STDIN)))
|
9
|
+
interpreter = Layo::InteractiveInterpreter.new(parser)
|
10
|
+
puts "Layo version #{Layo::VERSION}"
|
11
|
+
interpreter.interpret
|
12
|
+
else
|
13
|
+
raise "File #{ARGV[0]} does not exist" unless File.exists?(ARGV[0])
|
14
|
+
File.open(ARGV[0]) do |f|
|
15
|
+
f.set_encoding(Encoding::UTF_8)
|
16
|
+
parser = Layo::Parser.new(Layo::Tokenizer.new(Layo::Lexer.new(f)))
|
17
|
+
interpreter = Layo::Interpreter.new
|
18
|
+
begin
|
19
|
+
interpreter.interpret(parser.parse)
|
20
|
+
rescue Layo::SyntaxError => e
|
21
|
+
$stderr.puts "Syntax error: #{e}"
|
22
|
+
rescue Layo::RuntimeError => e
|
23
|
+
$stderr.puts "Runtime error: #{e}"
|
24
|
+
end
|
21
25
|
end
|
22
26
|
end
|
data/examples/1.lol
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
OBTW
|
2
|
+
|
3
|
+
This is solution for the Problem 1 from Project Euler: http://projecteuler.net/problem=1
|
4
|
+
|
5
|
+
Problem description:
|
6
|
+
If we list all the natural numbers below 10 that are multiples of 3 or 5,
|
7
|
+
we get 3, 5, 6 and 9. The sum of these multiples is 23.
|
8
|
+
Find the sum of all the multiples of 3 or 5 below 1000.
|
9
|
+
|
10
|
+
TLDR
|
11
|
+
|
12
|
+
HAI 1.2
|
13
|
+
|
14
|
+
I HAS A result ITZ 0
|
15
|
+
|
16
|
+
IM IN YR loop UPPIN YR i TIL BOTH SAEM i AN 1000
|
17
|
+
EITHER OF ...
|
18
|
+
BOTH SAEM MOD OF i AN 3 AN 0 ...
|
19
|
+
AN BOTH SAEM MOD OF i AN 5 AN 0
|
20
|
+
O RLY?
|
21
|
+
YA RLY
|
22
|
+
result R SUM OF result AN i
|
23
|
+
OIC
|
24
|
+
IM OUTTA YR loop
|
25
|
+
|
26
|
+
VISIBLE "Result: " result
|
27
|
+
KTHXBYE
|
data/examples/2.lol
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
OBTW
|
2
|
+
|
3
|
+
This is solution for the Problem 2 from Project Euler: http://projecteuler.net/problem=2
|
4
|
+
|
5
|
+
Problem description:
|
6
|
+
Each new term in the Fibonacci sequence is generated by adding the previous
|
7
|
+
two terms. By starting with 1 and 2, the first 10 terms will be:
|
8
|
+
|
9
|
+
1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
|
10
|
+
|
11
|
+
By considering the terms in the Fibonacci sequence whose values do not exceed
|
12
|
+
four million, find the sum of the even-valued terms.
|
13
|
+
|
14
|
+
TLDR
|
15
|
+
|
16
|
+
HAI 1.2
|
17
|
+
I HAS A a ITZ 1
|
18
|
+
I HAS A b ITZ 2
|
19
|
+
I HAS A result ITZ 2
|
20
|
+
|
21
|
+
IM IN YR loop
|
22
|
+
b R SUM OF a AN b
|
23
|
+
a R DIFF OF b AN a
|
24
|
+
BOTH SAEM 0 AN MOD OF b AN 2, O RLY?
|
25
|
+
YA RLY
|
26
|
+
result R SUM OF result AN b
|
27
|
+
OIC
|
28
|
+
|
29
|
+
DIFFRINT b AN SMALLR OF b AN 4000000, O RLY?
|
30
|
+
YA RLY
|
31
|
+
GTFO
|
32
|
+
OIC
|
33
|
+
IM OUTTA YR loop
|
34
|
+
|
35
|
+
VISIBLE "Result: " result
|
36
|
+
|
37
|
+
KTHXBYE
|
data/examples/3.lol
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
OBTW
|
2
|
+
This is solution for the '99 Bottles Of Beer' challenge from Code Golf:
|
3
|
+
http://codegolf.com/99-bottles-of-beer
|
4
|
+
TLDR
|
5
|
+
|
6
|
+
HAI 1.2
|
7
|
+
|
8
|
+
BTW recursive approach
|
9
|
+
HOW DUZ I bottle YR count
|
10
|
+
BOTH SAEM count AN 1, O RLY?
|
11
|
+
YA RLY
|
12
|
+
VISIBLE "1 bottle of beer on the wall, 1 bottle of beer.:)" ...
|
13
|
+
"Go to the store and buy some more, 99 bottles of beer on the wall."
|
14
|
+
NO WAI
|
15
|
+
I HAS A newCount ITZ DIFF OF count AN 1
|
16
|
+
BOTH SAEM newCount AN 1, O RLY?
|
17
|
+
YA RLY
|
18
|
+
I HAS A s ITZ "1 bottle"
|
19
|
+
NO WAI
|
20
|
+
I HAS A s ITZ ":{newCount} bottles"
|
21
|
+
OIC
|
22
|
+
VISIBLE ":{count} bottles of beer on the wall, :{count} bottles of beer.:)" ...
|
23
|
+
"Take one down and pass it around, :{s} of beer on the wall.:)"
|
24
|
+
bottle newCount
|
25
|
+
OIC
|
26
|
+
IF U SAY SO
|
27
|
+
|
28
|
+
bottle 99
|
29
|
+
|
30
|
+
KTHXBYE
|
data/layo.gemspec
CHANGED
@@ -2,11 +2,12 @@ require 'base64'
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'layo'
|
5
|
-
s.version = '1.
|
5
|
+
s.version = '1.1.0'
|
6
6
|
s.summary = 'LOLCODE interpreter written in plain Ruby'
|
7
7
|
s.description = <<-EOF
|
8
8
|
Layo is a LOLCODE interpreter written in plain Ruby. It tries to conform to
|
9
9
|
the LOLCODE 1.2 specification and supports everything described there.
|
10
|
+
Supports interactive mode.
|
10
11
|
EOF
|
11
12
|
s.required_ruby_version = '>= 1.9.2'
|
12
13
|
s.add_development_dependency 'mocha', '~> 0.10.0'
|
data/lib/layo.rb
CHANGED
@@ -6,6 +6,12 @@ require_relative 'layo/peekable'
|
|
6
6
|
require_relative 'layo/lexer'
|
7
7
|
require_relative 'layo/tokenizer'
|
8
8
|
require_relative 'layo/parser'
|
9
|
+
require_relative 'layo/base_interpreter'
|
9
10
|
require_relative 'layo/interpreter'
|
11
|
+
require_relative 'layo/interactive_interpreter'
|
10
12
|
require_relative 'layo/ast'
|
11
13
|
require_relative 'layo/unicode'
|
14
|
+
|
15
|
+
module Layo
|
16
|
+
VERSION = '1.1.0'
|
17
|
+
end
|
@@ -0,0 +1,348 @@
|
|
1
|
+
module Layo
|
2
|
+
class BaseInterpreter
|
3
|
+
attr_accessor :input, :output
|
4
|
+
|
5
|
+
def initialize(input = STDIN, output = STDOUT)
|
6
|
+
@input, @output = input, output
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_variable_table
|
10
|
+
table = Hash.new do |hash, key|
|
11
|
+
raise RuntimeError, "Variable '#{key}' is not declared"
|
12
|
+
end
|
13
|
+
table['IT'] = { type: :noob, value: nil}
|
14
|
+
table
|
15
|
+
end
|
16
|
+
|
17
|
+
# Initializes empty function and variable tables
|
18
|
+
def init_tables
|
19
|
+
@functions = {}
|
20
|
+
@vtable = create_variable_table
|
21
|
+
end
|
22
|
+
|
23
|
+
# Runs piece of code inside guard to catch :break and :return thrown by
|
24
|
+
# illegal statements. Also assigns line numbers to RuntimeError's
|
25
|
+
def with_guard(&block)
|
26
|
+
begin
|
27
|
+
illegal = true
|
28
|
+
catch(:break) do
|
29
|
+
catch(:return) do
|
30
|
+
block.call
|
31
|
+
illegal = false
|
32
|
+
end
|
33
|
+
raise RuntimeError, "Illegal return statement" if illegal
|
34
|
+
end
|
35
|
+
raise RuntimeError, "Illegal break statement" if illegal
|
36
|
+
rescue RuntimeError => e
|
37
|
+
e.line = @stmt_line
|
38
|
+
raise e
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def eval_block(block)
|
43
|
+
block.each do |stmt|
|
44
|
+
@stmt_line = stmt.line
|
45
|
+
send("eval_#{stmt.type}_stmt", stmt)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def eval_assignment_stmt(stmt)
|
50
|
+
# We should access by variable name first to ensure that it is defined
|
51
|
+
@vtable[stmt.identifier]
|
52
|
+
@vtable[stmt.identifier] = eval_expr(stmt.expression)
|
53
|
+
end
|
54
|
+
|
55
|
+
def eval_break_stmt(stmt)
|
56
|
+
throw :break
|
57
|
+
end
|
58
|
+
|
59
|
+
def eval_cast_stmt(stmt)
|
60
|
+
var = @vtable[stmt.identifier]
|
61
|
+
var[:value] = cast(var, stmt.to, false)
|
62
|
+
var[:type] = stmt.to
|
63
|
+
end
|
64
|
+
|
65
|
+
def eval_declaration_stmt(stmt)
|
66
|
+
if @vtable.has_key?(stmt.identifier)
|
67
|
+
raise RuntimeError, "Variable '#{stmt.identifier}' is already declared"
|
68
|
+
end
|
69
|
+
@vtable[stmt.identifier] = { type: :noob, value: nil }
|
70
|
+
unless stmt.initialization.nil?
|
71
|
+
@vtable[stmt.identifier] = eval_expr(stmt.initialization)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def eval_expression_stmt(stmt)
|
76
|
+
@vtable['IT'] = eval_expr(stmt.expression)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def eval_condition_stmt(stmt)
|
81
|
+
if cast(@vtable['IT'], :troof)
|
82
|
+
# if block
|
83
|
+
eval_block(stmt.then)
|
84
|
+
else
|
85
|
+
# else if blocks
|
86
|
+
condition_met = false
|
87
|
+
stmt.elseif.each do |elseif|
|
88
|
+
condition = eval_expr(elseif[:condition])
|
89
|
+
if condition_met = cast(condition, :troof)
|
90
|
+
eval_block(elseif[:block])
|
91
|
+
break
|
92
|
+
end
|
93
|
+
end
|
94
|
+
unless condition_met || stmt.else.nil?
|
95
|
+
# else block
|
96
|
+
eval_block(stmt.else)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def eval_input_stmt(stmt)
|
102
|
+
@vtable[stmt.identifier] = { type: :yarn, value: @input.gets }
|
103
|
+
end
|
104
|
+
|
105
|
+
def eval_loop_stmt(stmt)
|
106
|
+
unless stmt.op.nil?
|
107
|
+
# Backup any local variable if its name is the same as the counter
|
108
|
+
# variable's name
|
109
|
+
if @vtable.has_key?(stmt.counter)
|
110
|
+
var_backup = @vtable[stmt.counter]
|
111
|
+
end
|
112
|
+
@vtable[stmt.counter] = { type: :numbr, value: 0 }
|
113
|
+
update_op = if stmt.op == :uppin
|
114
|
+
lambda { @vtable[stmt.counter][:value] += 1 }
|
115
|
+
elsif stmt.op == :nerfin
|
116
|
+
lambda { @vtable[stmt.counter][:value] -= 1 }
|
117
|
+
else
|
118
|
+
lambda {
|
119
|
+
@vtable[stmt.counter] = call_func(stmt.op, [@vtable[stmt.counter]])
|
120
|
+
}
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
catch :break do
|
125
|
+
while true
|
126
|
+
unless stmt.guard.nil?
|
127
|
+
condition_met = cast(eval_expr(stmt.guard[:expression]), :troof)
|
128
|
+
if (stmt.guard[:type] == :wile && !condition_met) or
|
129
|
+
(stmt.guard[:type] == :til && condition_met)
|
130
|
+
throw :break
|
131
|
+
end
|
132
|
+
end
|
133
|
+
eval_block(stmt.block)
|
134
|
+
update_op.call if update_op
|
135
|
+
end
|
136
|
+
end
|
137
|
+
# Restore backed up variable
|
138
|
+
unless stmt.op.nil? || var_backup.nil?
|
139
|
+
@vtable[stmt.counter] = var_backup
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def eval_print_stmt(stmt)
|
144
|
+
text = ''
|
145
|
+
# todo rewrite using map or similar
|
146
|
+
stmt.expressions.each do |expr|
|
147
|
+
text << cast(eval_expr(expr), :yarn)
|
148
|
+
end
|
149
|
+
text << "\n" unless stmt.suppress
|
150
|
+
@output.print(text)
|
151
|
+
end
|
152
|
+
|
153
|
+
def eval_return_stmt(stmt)
|
154
|
+
throw :return, eval_expr(stmt.expression)
|
155
|
+
end
|
156
|
+
|
157
|
+
def eval_switch_stmt(stmt)
|
158
|
+
stmt.cases.combination(2) do |c|
|
159
|
+
raise RuntimeError, 'Literals must be unique' if c[0] == c[1]
|
160
|
+
end
|
161
|
+
case_found = false
|
162
|
+
it = @vtable['IT']
|
163
|
+
stmt.cases.each do |kase|
|
164
|
+
unless case_found
|
165
|
+
literal = eval_expr(kase[:expression])
|
166
|
+
if it == literal
|
167
|
+
case_found = true
|
168
|
+
end
|
169
|
+
end
|
170
|
+
if case_found
|
171
|
+
breaked = true
|
172
|
+
catch :break do
|
173
|
+
eval_block(kase[:block])
|
174
|
+
breaked = false
|
175
|
+
end
|
176
|
+
break if breaked
|
177
|
+
end
|
178
|
+
end
|
179
|
+
unless case_found || stmt.default.nil?
|
180
|
+
catch :break do
|
181
|
+
eval_block(stmt.default)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Casts given variable 'var' into type 'to'
|
187
|
+
# Returns only value part of the variable, type will be 'to' anyway
|
188
|
+
def cast(var, to, implicit = true)
|
189
|
+
return var[:value] if var[:type] == to
|
190
|
+
return nil if to == :noob
|
191
|
+
case var[:type]
|
192
|
+
when :noob
|
193
|
+
if implicit && to != :troof
|
194
|
+
raise RuntimeError, "NOOB cannot be implicitly cast into #{to.to_s.upcase}"
|
195
|
+
end
|
196
|
+
return false if to == :troof
|
197
|
+
return 0 if to == :numbr
|
198
|
+
return 0.0 if to == :numbar
|
199
|
+
return ''
|
200
|
+
when :troof
|
201
|
+
return (var[:value] ? 1 : 0) if to == :numbr
|
202
|
+
return (var[:value] ? 1.0 : 0.0) if to == :numbar
|
203
|
+
return (var[:value] ? 'WIN' : 'FAIL')
|
204
|
+
when :numbr
|
205
|
+
return (var[:value].zero? ? false : true) if to == :troof
|
206
|
+
return var[:value].to_f if to == :numbar
|
207
|
+
return var[:value].to_s
|
208
|
+
when :numbar
|
209
|
+
return (var[:value].zero? ? false : true) if to == :troof
|
210
|
+
return var[:value].to_int if to == :numbr
|
211
|
+
# Truncate to 2 digits after decimal point
|
212
|
+
return ((var[:value] * 100).floor / 100.0).to_s
|
213
|
+
else
|
214
|
+
return !var[:value].empty? if to == :troof
|
215
|
+
if to == :numbr
|
216
|
+
return var[:value].to_i if var[:value].lol_integer?
|
217
|
+
raise RuntimeError, "'#{var[:value]}' is not a valid integer"
|
218
|
+
end
|
219
|
+
return var[:value].to_f if var[:value].lol_float?
|
220
|
+
raise RuntimeError, "'#{var[:value]}' is not a valid float"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def eval_expr(expr)
|
225
|
+
send("eval_#{expr.type}_expr", expr)
|
226
|
+
end
|
227
|
+
|
228
|
+
def eval_binary_expr(expr)
|
229
|
+
l = eval_expr(expr.left)
|
230
|
+
r = eval_expr(expr.right)
|
231
|
+
methods = {
|
232
|
+
:sum_of => :+, :diff_of => :-, :produkt_of => :*, :quoshunt_of => :/,
|
233
|
+
:mod_of => :modulo, :both_of => :&, :either_of => :|, :won_of => :^,
|
234
|
+
:both_saem => :==, :diffrint => :!=
|
235
|
+
}
|
236
|
+
case expr.operator
|
237
|
+
when :sum_of, :diff_of, :produkt_of, :quoshunt_of, :mod_of, :biggr_of, :smallr_of
|
238
|
+
type = l[:type] == :numbar || r[:type] == :numbar ||
|
239
|
+
(l[:type] == :yarn && l[:value].lol_float?) ||
|
240
|
+
(r[:type] == :yarn && r[:value].lol_float?) ? :numbar : :numbr
|
241
|
+
l, r = cast(l, type), cast(r, type)
|
242
|
+
if expr.operator == :biggr_of
|
243
|
+
value = [l, r].max
|
244
|
+
elsif expr.operator == :smallr_of
|
245
|
+
value = [l, r].min
|
246
|
+
else
|
247
|
+
value = l.send(methods[expr.operator], r)
|
248
|
+
end
|
249
|
+
when :both_saem, :diffrint
|
250
|
+
type = :troof
|
251
|
+
if (l[:type] == :numbr && r[:type] == :numbar) ||
|
252
|
+
(l[:type] == :numbar && r[:type] == :numbr)
|
253
|
+
l, r = cast(l, :numbar), cast(r, :numbar)
|
254
|
+
elsif l[:type] != r[:type]
|
255
|
+
raise RuntimeError, 'Operands must have same type'
|
256
|
+
end
|
257
|
+
value = l.send(methods[expr.operator], r)
|
258
|
+
else
|
259
|
+
type = :troof
|
260
|
+
l, r = cast(l, :troof), cast(r, :troof)
|
261
|
+
value = l.send(methods[expr.operator], r)
|
262
|
+
end
|
263
|
+
{ type: type, value: value }
|
264
|
+
end
|
265
|
+
|
266
|
+
def eval_cast_expr(expr)
|
267
|
+
casted_expr = eval_expr(expr.being_casted)
|
268
|
+
{ type: expr.to, value: cast(casted_expr, expr.to, false) }
|
269
|
+
end
|
270
|
+
|
271
|
+
def eval_constant_expr(expr)
|
272
|
+
mapping = { boolean: :troof, string: :yarn, integer: :numbr, float: :numbar }
|
273
|
+
value = expr.vtype == :string ? interpolate_string(expr.value) : expr.value
|
274
|
+
{ type: mapping[expr.vtype], value: value }
|
275
|
+
end
|
276
|
+
|
277
|
+
def eval_function_expr(expr)
|
278
|
+
parameters = []
|
279
|
+
expr.parameters.each do |param|
|
280
|
+
parameters << eval_expr(param)
|
281
|
+
end
|
282
|
+
call_func(expr.name, parameters)
|
283
|
+
end
|
284
|
+
|
285
|
+
def call_func(name, arguments)
|
286
|
+
function = @functions[name]
|
287
|
+
# Replace variable table by 'clean' variable table inside functions
|
288
|
+
old_table = @vtable
|
289
|
+
@vtable = create_variable_table
|
290
|
+
function[:args].each_index do |index|
|
291
|
+
@vtable[function[:args][index]] = arguments[index]
|
292
|
+
end
|
293
|
+
retval = nil
|
294
|
+
retval = catch :return do
|
295
|
+
breaked = true
|
296
|
+
catch(:break) do
|
297
|
+
eval_block(function[:block])
|
298
|
+
breaked = false
|
299
|
+
end
|
300
|
+
retval = { type: :noob, value: nil } if breaked
|
301
|
+
end
|
302
|
+
retval = @vtable['IT'] if retval.nil?
|
303
|
+
@vtable = old_table
|
304
|
+
retval
|
305
|
+
end
|
306
|
+
|
307
|
+
def eval_nary_expr(expr)
|
308
|
+
case expr.operator
|
309
|
+
when :all_of
|
310
|
+
type, value = :troof, true
|
311
|
+
expr.expressions.each do |operand|
|
312
|
+
unless cast(eval_expr(operand), :troof)
|
313
|
+
value = false
|
314
|
+
break
|
315
|
+
end
|
316
|
+
end
|
317
|
+
when :any_of
|
318
|
+
type, value = :troof, false
|
319
|
+
expr.expressions.each do |operand|
|
320
|
+
if cast(eval_expr(operand), :troof)
|
321
|
+
value = true
|
322
|
+
break
|
323
|
+
end
|
324
|
+
end
|
325
|
+
when :smoosh
|
326
|
+
type, value = :yarn, ''
|
327
|
+
expr.expressions.each do |operand|
|
328
|
+
value << cast(eval_expr(operand), :yarn)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
{ type: type, value: value }
|
332
|
+
end
|
333
|
+
|
334
|
+
def eval_unary_expr(expr)
|
335
|
+
# the only unary op in LOLCODE is NOT
|
336
|
+
{ type: :troof, value: !cast(eval_expr(expr.expression), :troof) }
|
337
|
+
end
|
338
|
+
|
339
|
+
def eval_variable_expr(expr)
|
340
|
+
@vtable[expr.name]
|
341
|
+
end
|
342
|
+
|
343
|
+
# Interpolates values of variables in the string
|
344
|
+
def interpolate_string(str)
|
345
|
+
str.gsub(/:\{([a-zA-Z]\w*)\}/) { cast(@vtable[$1], :yarn, false) }
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Layo
|
2
|
+
class InteractiveInterpreter < BaseInterpreter
|
3
|
+
attr_accessor :parser
|
4
|
+
|
5
|
+
def initialize(parser)
|
6
|
+
@parser = parser
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def interpret
|
11
|
+
init_tables
|
12
|
+
@stmt_line = 1
|
13
|
+
@output.puts 'Press Control-C to exit'
|
14
|
+
while true
|
15
|
+
begin
|
16
|
+
@output.print ' > '
|
17
|
+
break if @parser.tokenizer.try(:eof)
|
18
|
+
begin
|
19
|
+
statement = @parser.parse_statement
|
20
|
+
with_guard { send("eval_#{statement.type}_stmt", statement) }
|
21
|
+
end until @parser.tokenizer.lexer.buffer_empty?
|
22
|
+
rescue Layo::SyntaxError => e
|
23
|
+
@parser.tokenizer.reset
|
24
|
+
@parser.tokenizer.lexer.reset
|
25
|
+
$stderr.puts "Syntax error: #{e}"
|
26
|
+
rescue Layo::RuntimeError => e
|
27
|
+
$stderr.puts "Runtime error: #{e}"
|
28
|
+
rescue Interrupt
|
29
|
+
@output.puts 'Exiting'
|
30
|
+
break
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def eval_function_stmt(stmt)
|
36
|
+
if @functions.has_key?(stmt.name)
|
37
|
+
raise RuntimeError, "Function '#{stmt.name}' is already declared"
|
38
|
+
end
|
39
|
+
@functions[stmt.name] = { args: stmt.args, block: stmt.block }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/layo/interpreter.rb
CHANGED
@@ -1,16 +1,10 @@
|
|
1
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
|
-
|
2
|
+
class Interpreter < BaseInterpreter
|
9
3
|
# Interprets program given as an AST node
|
10
4
|
def interpret(program)
|
11
5
|
# We should gather all function definitions along with their bodies
|
12
6
|
# beforehand so we could call them wherever a call appears
|
13
|
-
|
7
|
+
init_tables
|
14
8
|
program.block.each do |statement|
|
15
9
|
if statement.type == 'function'
|
16
10
|
@functions[statement.name] = {
|
@@ -21,340 +15,11 @@ module Layo
|
|
21
15
|
eval_program(program)
|
22
16
|
end
|
23
17
|
|
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
18
|
def eval_program(program)
|
33
|
-
|
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)
|
19
|
+
with_guard { eval_block(program.block) }
|
85
20
|
end
|
86
21
|
|
22
|
+
# Don't do anything because function table is already ready
|
87
23
|
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
24
|
end
|
360
25
|
end
|
data/lib/layo/lexer.rb
CHANGED
@@ -38,7 +38,7 @@ module Layo
|
|
38
38
|
def next_item
|
39
39
|
return @last_lexeme if @last_lexeme[0].nil?
|
40
40
|
while true
|
41
|
-
@line = next_line if
|
41
|
+
@line = next_line if buffer_empty?
|
42
42
|
if @line.nil?
|
43
43
|
lexeme = [nil, @line_no, 1]
|
44
44
|
break
|
@@ -158,5 +158,10 @@ module Layo
|
|
158
158
|
end
|
159
159
|
end
|
160
160
|
end
|
161
|
+
|
162
|
+
# Returns true if input buffer is fully processed
|
163
|
+
def buffer_empty?
|
164
|
+
@line_no.zero? || @pos > @line.length - 1
|
165
|
+
end
|
161
166
|
end
|
162
167
|
end
|
data/lib/layo/parser.rb
CHANGED
@@ -100,9 +100,13 @@ module Layo
|
|
100
100
|
nil
|
101
101
|
end
|
102
102
|
|
103
|
-
def parse_statement(name)
|
103
|
+
def parse_statement(name = nil)
|
104
104
|
token = @tokenizer.peek
|
105
105
|
@tokenizer.unpeek
|
106
|
+
name = next_statement unless name
|
107
|
+
unless name
|
108
|
+
raise SyntaxError.new(token[:line], token[:pos], 'Expected statement')
|
109
|
+
end
|
106
110
|
statement = send("parse_#{name}_statement".to_sym)
|
107
111
|
expect_token(:newline)
|
108
112
|
statement.line = token[:line]
|
data/lib/layo/tokenizer.rb
CHANGED
@@ -69,8 +69,6 @@ module Layo
|
|
69
69
|
end
|
70
70
|
root[lexemes.last] = { match: t.gsub(' ', '_').downcase.to_sym }
|
71
71
|
end
|
72
|
-
@token_table[:list]["\n"] = { match: :newline }
|
73
|
-
@token_table[:list]['!'] = { match: :exclamation }
|
74
72
|
end
|
75
73
|
|
76
74
|
def match_longest(lexeme, root)
|
@@ -98,6 +96,10 @@ module Layo
|
|
98
96
|
token = { type: :float, data: lexeme[0].to_f }
|
99
97
|
elsif lexeme[0].lol_boolean?
|
100
98
|
token = { type: :boolean, data: (lexeme[0] == 'WIN') }
|
99
|
+
elsif lexeme[0] == '!'
|
100
|
+
token = { type: :exclamation }
|
101
|
+
elsif lexeme[0] == "\n"
|
102
|
+
token = { type: :newline }
|
101
103
|
else
|
102
104
|
# Try to match keyword
|
103
105
|
token_type = match_longest(lexeme[0], @token_table)
|
data/spec/interpreter_spec.rb
CHANGED
@@ -13,6 +13,7 @@ describe Interpreter do
|
|
13
13
|
mask = File.join(File.dirname(__FILE__), 'source', '**', '*.lol')
|
14
14
|
Dir.glob(mask).each do |source_filename|
|
15
15
|
lexer = Lexer.new(File.new(source_filename))
|
16
|
+
lexer.input.set_encoding(Encoding::UTF_8)
|
16
17
|
parser = Parser.new(Tokenizer.new(lexer))
|
17
18
|
@interpreter.output.string = ''
|
18
19
|
|
@@ -49,4 +50,17 @@ describe Interpreter do
|
|
49
50
|
infile.close if infile.instance_of?(File)
|
50
51
|
end
|
51
52
|
end
|
53
|
+
|
54
|
+
describe "#eval_print_stmt" do
|
55
|
+
it "should print trailing newline" do
|
56
|
+
string = Ast::Expression.new('constant', { vtype: :string, value: "a\n" })
|
57
|
+
stmt = Ast::Statement.new('print', {
|
58
|
+
expressions: [string], suppress: false
|
59
|
+
})
|
60
|
+
|
61
|
+
@interpreter.eval_print_stmt(stmt)
|
62
|
+
|
63
|
+
@interpreter.output.string.must_equal "a\n\n"
|
64
|
+
end
|
65
|
+
end
|
52
66
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: layo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-01-
|
12
|
+
date: 2012-01-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mocha
|
16
|
-
requirement: &
|
16
|
+
requirement: &70607720 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.10.0
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70607720
|
25
25
|
description: ! " Layo is a LOLCODE interpreter written in plain Ruby. It tries
|
26
26
|
to conform to\n the LOLCODE 1.2 specification and supports everything described
|
27
|
-
there.\n"
|
27
|
+
there.\n Supports interactive mode.\n"
|
28
28
|
email: kozhayev@gmail.com
|
29
29
|
executables:
|
30
30
|
- layo
|
@@ -36,6 +36,9 @@ files:
|
|
36
36
|
- Rakefile
|
37
37
|
- UnicodeData.txt
|
38
38
|
- bin/layo
|
39
|
+
- examples/1.lol
|
40
|
+
- examples/2.lol
|
41
|
+
- examples/3.lol
|
39
42
|
- layo.gemspec
|
40
43
|
- lib/layo.rb
|
41
44
|
- lib/layo/ast.rb
|
@@ -44,6 +47,8 @@ files:
|
|
44
47
|
- lib/layo/ast/node.rb
|
45
48
|
- lib/layo/ast/program.rb
|
46
49
|
- lib/layo/ast/statement.rb
|
50
|
+
- lib/layo/base_interpreter.rb
|
51
|
+
- lib/layo/interactive_interpreter.rb
|
47
52
|
- lib/layo/interpreter.rb
|
48
53
|
- lib/layo/lexer.rb
|
49
54
|
- lib/layo/parser.rb
|