calcula 0.1.0 → 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.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +5 -8
- data/lib/Expr.rb +32 -12
- data/lib/Exprs/AssignExpr.rb +39 -0
- data/lib/Exprs/BinopExpr.rb +53 -0
- data/lib/Exprs/BracedExpr.rb +48 -0
- data/lib/Exprs/FuncExpr.rb +31 -31
- data/lib/Exprs/IdentExpr.rb +25 -17
- data/lib/Exprs/NumExpr.rb +25 -17
- data/lib/Exprs/ParamsExpr.rb +24 -18
- data/lib/Exprs/RatExpr.rb +37 -0
- data/lib/Exprs/UnaryExpr.rb +84 -0
- data/lib/Lexer.rb +158 -119
- data/lib/Parser.rb +317 -40
- data/lib/Token.rb +23 -4
- data/lib/calcula.rb +79 -4
- data/lib/calcula/version.rb +1 -1
- metadata +7 -2
data/lib/Parser.rb
CHANGED
@@ -2,26 +2,52 @@ require_relative "./Exprs/IdentExpr"
|
|
2
2
|
require_relative "./Exprs/NumExpr"
|
3
3
|
require_relative "./Exprs/ParamsExpr"
|
4
4
|
require_relative "./Exprs/FuncExpr"
|
5
|
+
require_relative "./Exprs/RatExpr"
|
6
|
+
require_relative "./Exprs/BinopExpr"
|
7
|
+
require_relative "./Exprs/BracedExpr"
|
8
|
+
require_relative "./Exprs/UnaryExpr"
|
9
|
+
require_relative "./Exprs/AssignExpr"
|
5
10
|
|
11
|
+
# The parser for the Calcula language written in Ruby. Please, if adding a
|
12
|
+
# grammar rule, document it.
|
13
|
+
#
|
14
|
+
# @author Paul T.
|
6
15
|
class Calcula::Parser
|
7
16
|
|
8
17
|
include Calcula::Exprs
|
9
18
|
|
10
|
-
|
19
|
+
EQL_LIKE_OPS = [:OP_EQ, :OP_NE, :OP_LT, :OP_GT, :OP_LE, :OP_GE]
|
20
|
+
MUL_LIKE_OPS = [:OP_REM, :OP_MUL, :OP_DIV, :PAREN_O]
|
21
|
+
ADD_OR_SUB = [:OP_ADD, :OP_SUB]
|
22
|
+
STMT_TERMINATOR = [:DISP, :ASSERT]
|
23
|
+
|
24
|
+
# Constructs a new parser with the specified Tokens
|
25
|
+
#
|
26
|
+
# @param toks [Array<Calcula::Token>] The tokens most likely generated from the lexer
|
11
27
|
def initialize(toks)
|
12
28
|
@toks = toks
|
13
29
|
@i = 0
|
14
30
|
end
|
15
31
|
|
16
|
-
#
|
32
|
+
# Raises an error with seemingly useful message
|
33
|
+
#
|
34
|
+
# @param unexpectedTok [Calcula::Token, nil] The token that was found instead, nil if End of file was found.
|
35
|
+
# @param msg [String] The additional message or the reason of the error
|
36
|
+
# @raise [RuntimeError] Calling this method raises this error
|
17
37
|
def parseError(unexpectedTok, msg)
|
18
38
|
if unexpectedTok == nil then
|
19
|
-
raise "Unexpected EOF: #{
|
39
|
+
raise "Unexpected EOF: #{msg != '' ? msg : '<no message>'}"
|
20
40
|
end
|
21
|
-
raise "Unexpected token #{unexpectedTok
|
41
|
+
raise "Unexpected token #{unexpectedTok}: #{msg != '' ? msg : '<no message>'}"
|
22
42
|
end
|
23
43
|
|
24
|
-
#
|
44
|
+
# Performs a lookahead and checks to see if the next token is the one specified.
|
45
|
+
# If the specified token is not found, then the method `parseError` is called.
|
46
|
+
#
|
47
|
+
# (see #lookahead)
|
48
|
+
# (see #parseError)
|
49
|
+
# @param tokId [Symbol] The type of token being expected
|
50
|
+
# @return [Calcula::Token] The actual token found
|
25
51
|
def consume(tokId)
|
26
52
|
cons = lookahead()
|
27
53
|
if cons == nil then
|
@@ -34,10 +60,14 @@ class Calcula::Parser
|
|
34
60
|
end
|
35
61
|
end
|
36
62
|
|
37
|
-
|
38
|
-
|
63
|
+
# Finds the next non-ignored token and returns it
|
64
|
+
#
|
65
|
+
# @return [Calcula::Token, nil] nil if the next non-ignored token nil. This could mean end of script.
|
66
|
+
def lookahead
|
67
|
+
return nil if @toks[@i] == nil
|
39
68
|
while @toks[@i].id == :WS || @toks[@i].id == :COMMENT do
|
40
69
|
@i += 1
|
70
|
+
return nil if @toks[@i] == nil
|
41
71
|
end
|
42
72
|
if @i < @toks.length then
|
43
73
|
return @toks[@i]
|
@@ -46,36 +76,253 @@ class Calcula::Parser
|
|
46
76
|
end
|
47
77
|
end
|
48
78
|
|
49
|
-
|
50
|
-
|
51
|
-
|
79
|
+
# Begins to parse the list of tokens and tries to convert it into an expression tree
|
80
|
+
#
|
81
|
+
# @return [Calcula::Expr, nil] If an expression tree can be constructed, nil otherwise
|
82
|
+
def parse
|
83
|
+
tmp = lookahead
|
84
|
+
if tmp == nil then
|
85
|
+
nil
|
86
|
+
else
|
87
|
+
case tmp.id
|
88
|
+
when :LET then
|
89
|
+
defExpr
|
90
|
+
when :COMMA then
|
91
|
+
consume(:COMMA) # Comma is like semicolons in ruby. Optional!
|
92
|
+
parse
|
93
|
+
else
|
94
|
+
printExpr
|
95
|
+
end
|
52
96
|
end
|
53
|
-
return nil
|
54
97
|
end
|
55
98
|
|
56
|
-
#
|
57
|
-
|
58
|
-
|
99
|
+
# Parses the syntax rule of expr
|
100
|
+
# equation = logicOrExpr
|
101
|
+
#
|
102
|
+
# @return [Calcula::Expr]
|
103
|
+
def equation
|
104
|
+
logicOrExpr()
|
59
105
|
end
|
60
106
|
|
61
|
-
#
|
62
|
-
#
|
63
|
-
|
64
|
-
|
65
|
-
|
107
|
+
# Parses the syntax rule of defExpr
|
108
|
+
# defExpr = LET identExpr PAREN_O paramsExpr PAREN_C OP_EQ equation
|
109
|
+
# | LET identExpr OP_EQ equation
|
110
|
+
# @return [Calcula::Expr]
|
111
|
+
def defExpr
|
112
|
+
consume(:LET)
|
113
|
+
name = identExpr()
|
114
|
+
val = nil
|
66
115
|
|
67
|
-
|
116
|
+
maybeParenO = lookahead
|
117
|
+
parseError(nil, "Expected a complete function declaration or '='") if maybeParenO == nil
|
118
|
+
|
119
|
+
case maybeParenO.id
|
68
120
|
when :PAREN_O then
|
69
121
|
consume(:PAREN_O)
|
70
|
-
|
122
|
+
params = paramsExpr()
|
71
123
|
consume(:PAREN_C)
|
72
|
-
|
124
|
+
consume(:OP_EQ)
|
125
|
+
act = equation()
|
126
|
+
val = FuncExpr.new(params, act)
|
127
|
+
when :OP_EQ then
|
128
|
+
consume(:OP_EQ)
|
129
|
+
val = equation()
|
130
|
+
else
|
131
|
+
parseError(maybeParenO, "Expected complete function declaration or '='")
|
132
|
+
end
|
133
|
+
AssignExpr.new(name, val)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Parses the syntax ruke for printExpr
|
137
|
+
# printExpr = equation (DISP | ASSERT)?
|
138
|
+
#
|
139
|
+
def printExpr
|
140
|
+
base = equation()
|
141
|
+
maybeDisp = lookahead()
|
142
|
+
if maybeDisp != nil && STMT_TERMINATOR.index(maybeDisp.id) != nil then
|
143
|
+
UnaryExpr.mkPostfix(consume(maybeDisp.id), base)
|
144
|
+
else
|
145
|
+
base
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Parses the syntax rule of logicNotExpr
|
150
|
+
# logicNotExpr = NOT logicOrExpr
|
151
|
+
#
|
152
|
+
# @return [Calcula::Expr]
|
153
|
+
def logicNotExpr
|
154
|
+
tmp = lookahead
|
155
|
+
parseError(nil, "Expected an equation") if tmp == nil
|
156
|
+
if tmp.id == :NOT then
|
157
|
+
tmp = consume(tmp.id)
|
158
|
+
end
|
159
|
+
rst = logicOrExpr
|
160
|
+
if tmp.id == :NOT then
|
161
|
+
rst = UnaryExpr.mkPrefix(tmp, rst)
|
162
|
+
end
|
163
|
+
return rst
|
164
|
+
end
|
165
|
+
|
166
|
+
# Performs lookahead and constructs a parse tree. This could only be used on
|
167
|
+
# parse trees with the grammar of
|
168
|
+
# self = $1:child ($2:SOMETHING $3:child)*
|
169
|
+
#
|
170
|
+
# The tree being constructed must have the initialization sequence of
|
171
|
+
# SubclassOfExpr.new($2, $1, $3)
|
172
|
+
#
|
173
|
+
# @param childrenMethod [Symbol] The name of `child` in the example grammar
|
174
|
+
# @param exprClass [Constant (class name)] The `SubclassOfExpr` in the example
|
175
|
+
# @yield [id, text] Predicate for whether or not lookahead should continue
|
176
|
+
# @yieldparam id [Symbol] The current lookahead token's id
|
177
|
+
# @yieldparam text [String] The current lookahead token's text
|
178
|
+
# @yieldreturn [true, false] true if consumption should continue, false otherwise
|
179
|
+
# @return [Calcula::Expr] The tree being constructed
|
180
|
+
def consumeWhen(childrenMethod, exprClass) # => Take a block (predicate)
|
181
|
+
rst = self.send(childrenMethod)
|
182
|
+
tmp = lookahead
|
183
|
+
while tmp != nil && yield(tmp.id, tmp.text) do
|
184
|
+
rst = exprClass.new(consume(tmp.id), rst, self.send(childrenMethod))
|
185
|
+
tmp = lookahead
|
186
|
+
end
|
187
|
+
return rst
|
188
|
+
end
|
189
|
+
|
190
|
+
# Parses the syntax rule of logicOrExpr
|
191
|
+
# logicOrExpr = logicAndExpr (AND logicAndExpr)*
|
192
|
+
#
|
193
|
+
# @return [Calcula::Expr]
|
194
|
+
def logicOrExpr
|
195
|
+
return consumeWhen(:logicAndExpr, BinopExpr) do |id, _txt|
|
196
|
+
id == :AND
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Parses the syntax rule of logicAndExpr
|
201
|
+
# logicAndExpr = equalLikeExpr (OR equalLikeExpr)*
|
202
|
+
#
|
203
|
+
# @return [Calcula::Expr]
|
204
|
+
def logicAndExpr
|
205
|
+
return consumeWhen(:equalLikeExpr, BinopExpr) do |id, _txt|
|
206
|
+
id == :OR
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Parses the syntax rule of equalLikeExpr
|
211
|
+
# equalLikeExpr = addLikeExpr ((OP_EQ | OP_NE | OP_LT | OP_GT | OP_LE | OP_GE) addLikeExpr)?
|
212
|
+
#
|
213
|
+
# @return [Calcula::Expr]
|
214
|
+
def equalLikeExpr
|
215
|
+
return consumeWhen(:addLikeExpr, BinopExpr) do |id, _txt|
|
216
|
+
EQL_LIKE_OPS.index(id) != nil
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Parses the syntax rule of addLikeExpr
|
221
|
+
# addLikeExpr = mulLikeExpr ((OP_ADD | OP_SUB) mulLikeExpr)*
|
222
|
+
#
|
223
|
+
# @return [Calcula::Expr]
|
224
|
+
def addLikeExpr
|
225
|
+
return consumeWhen(:mulLikeExpr, BinopExpr) do |id, _txt|
|
226
|
+
ADD_OR_SUB.index(id) != nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Parses the syntax rule of mulLikeExpr
|
231
|
+
# mulLikeExpr = prefixExpr ((OP_REM | OP_MUL | OP_DIV) mulLikeExpr)?
|
232
|
+
# | prefixExpr (PAREN_O equation mulLikeExpr)?
|
233
|
+
#
|
234
|
+
# @return [Calcula::Expr]
|
235
|
+
def mulLikeExpr
|
236
|
+
rst = prefixExpr
|
237
|
+
tmp = lookahead
|
238
|
+
while tmp != nil && MUL_LIKE_OPS.index(tmp.id) != nil do
|
239
|
+
if tmp.id == :PAREN_O then
|
240
|
+
paren = consume(tmp.id)
|
241
|
+
appliedOn = equation
|
242
|
+
consume(:PAREN_C)
|
243
|
+
rst = BinopExpr.new(paren, rst, appliedOn)
|
244
|
+
else
|
245
|
+
rst = BinopExpr.new(consume(tmp.id), rst, prefixExpr)
|
246
|
+
end
|
247
|
+
tmp = lookahead
|
248
|
+
end
|
249
|
+
return rst
|
250
|
+
end
|
251
|
+
|
252
|
+
# Parses the syntax rule for prefixExpr
|
253
|
+
# prefixExpr = (OP_SUB | OP_ADD)? postfixExpr
|
254
|
+
#
|
255
|
+
# @return [Calcula::Expr]
|
256
|
+
def prefixExpr
|
257
|
+
tmp = lookahead
|
258
|
+
parseError(nil, "Expected a prefix operator or equation") if tmp == nil
|
259
|
+
|
260
|
+
case tmp.id
|
261
|
+
when :OP_SUB, :OP_ADD then
|
262
|
+
op = consume(tmp.id)
|
263
|
+
return UnaryExpr.mkPrefix(op, postfixExpr)
|
264
|
+
else
|
265
|
+
return postfixExpr
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Parses the syntax rule for postfixExpr
|
270
|
+
# postfixExpr = exponentExpr ROUND_DOLLAR?
|
271
|
+
#
|
272
|
+
# @return [Calcula::Expr]
|
273
|
+
def postfixExpr
|
274
|
+
base = exponentExpr
|
275
|
+
maybePostfix = lookahead
|
276
|
+
if maybePostfix != nil then
|
277
|
+
case maybePostfix.id
|
278
|
+
when :ROUND_DOLLAR then
|
279
|
+
return UnaryExpr.mkPostfix(consume(:ROUND_DOLLAR), base)
|
280
|
+
end
|
281
|
+
# Default case fallsthough and base itself gets returned
|
282
|
+
end
|
283
|
+
return base
|
284
|
+
end
|
285
|
+
|
286
|
+
# Parses the syntax rule for exponentExpr
|
287
|
+
# exponentExpr = parenExpr (OP_POW parenExpr)*
|
288
|
+
#
|
289
|
+
# @return [Calcula::Expr]
|
290
|
+
def exponentExpr
|
291
|
+
return consumeWhen(:parenExpr, BinopExpr) do |id, _txt|
|
292
|
+
id == :OP_POW
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Parses the syntax rule of parenExpr
|
297
|
+
# parenExpr = PAREN_O equation PAREN_C
|
298
|
+
# | SQUARE_O equation SQUARE_C
|
299
|
+
# | basicValExpr
|
300
|
+
#
|
301
|
+
# @return [Calcula::Expr]
|
302
|
+
def parenExpr
|
303
|
+
tmp = lookahead()
|
304
|
+
parseError(nil, "Expected PAREN_O or an equation") if tmp == nil
|
305
|
+
|
306
|
+
case tmp.id
|
307
|
+
when :PAREN_O then
|
308
|
+
head = consume(:PAREN_O)
|
309
|
+
inner = equation()
|
310
|
+
tail = consume(:PAREN_C)
|
311
|
+
return BracedExpr.new(head, tail, inner)
|
312
|
+
when :SQUARE_O then
|
313
|
+
head = consume(:SQUARE_O)
|
314
|
+
inner = equation()
|
315
|
+
tail = consume(:SQUARE_C)
|
316
|
+
return BracedExpr.new(head, tail, inner)
|
73
317
|
else return basicValExpr()
|
74
318
|
end
|
75
319
|
end
|
76
320
|
|
77
|
-
#
|
78
|
-
|
321
|
+
# Parses the syntax rule for basicValExpr
|
322
|
+
# basicValExpr = numExpr | composeExpr | lambdaExpr
|
323
|
+
#
|
324
|
+
# @return [Calcula::Expr]
|
325
|
+
def basicValExpr
|
79
326
|
tmp = lookahead()
|
80
327
|
parseError(nil, "Expected numExpr or identExpr") if tmp == nil
|
81
328
|
|
@@ -83,21 +330,34 @@ class Calcula::Parser
|
|
83
330
|
when :NUM then
|
84
331
|
return numExpr()
|
85
332
|
when :ID then
|
86
|
-
return
|
333
|
+
return composeExpr()
|
87
334
|
when :LAMBDA then
|
88
335
|
return lambdaExpr()
|
89
336
|
else parseError(tmp, "Expected numExpr or identExpr")
|
90
337
|
end
|
91
338
|
end
|
92
339
|
|
93
|
-
#
|
94
|
-
#
|
95
|
-
def
|
340
|
+
# Parses the syntax rule for composeExpr
|
341
|
+
# composeExpr = identExpr (COMPOSE identExpr)*
|
342
|
+
def composeExpr
|
343
|
+
return consumeWhen(:identExpr, BinopExpr) do |id, _txt|
|
344
|
+
id == :COMPOSE
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Parses the syntax rule for paramsExpr
|
349
|
+
# paramsExpr = ID (COMMA ID)+
|
350
|
+
# | ID
|
351
|
+
#
|
352
|
+
# @return [Calcula::Expr]
|
353
|
+
def paramsExpr
|
354
|
+
# NOTE: Do not change this part to using the `consumeWhen` method.
|
355
|
+
# it does not work like that.
|
96
356
|
params = [consume(:ID)]
|
97
|
-
|
357
|
+
loop do
|
98
358
|
begin
|
99
|
-
|
100
|
-
rescue
|
359
|
+
consume(:COMMA)
|
360
|
+
rescue RuntimeError
|
101
361
|
break
|
102
362
|
end
|
103
363
|
params << consume(:ID)
|
@@ -105,26 +365,43 @@ class Calcula::Parser
|
|
105
365
|
return ParamsExpr.new(params)
|
106
366
|
end
|
107
367
|
|
108
|
-
#
|
109
|
-
|
368
|
+
# Parses the syntax rule for lambdaExpr
|
369
|
+
# lambdaExpr = LAMBDA paramsExpr equation
|
370
|
+
#
|
371
|
+
# @return [Calcula::Expr]
|
372
|
+
def lambdaExpr
|
110
373
|
consume(:LAMBDA)
|
111
374
|
params = paramsExpr()
|
112
|
-
return FuncExpr.
|
375
|
+
return FuncExpr.new(params, equation())
|
113
376
|
end
|
114
377
|
|
115
|
-
#
|
116
|
-
|
378
|
+
# Parses the syntax rule for numExpr
|
379
|
+
# numExpr = NUM (RAT NUM)?
|
380
|
+
#
|
381
|
+
# @return [Calcula::Expr]
|
382
|
+
def numExpr
|
117
383
|
tok = consume(:NUM)
|
118
|
-
|
384
|
+
maybeRat = lookahead()
|
385
|
+
if maybeRat != nil and maybeRat.id == :RAT then
|
386
|
+
consume(:RAT)
|
387
|
+
bottom = consume(:NUM)
|
388
|
+
return RatExpr.new(tok, bottom)
|
389
|
+
else
|
390
|
+
return NumExpr.new(tok)
|
391
|
+
end
|
119
392
|
end
|
120
393
|
|
121
|
-
#
|
122
|
-
|
394
|
+
# Parses the syntax rule for identExpr
|
395
|
+
# identExpr = ID
|
396
|
+
#
|
397
|
+
# @return [Calcula::Expr]
|
398
|
+
def identExpr
|
123
399
|
tok = consume(:ID)
|
124
400
|
return IdentExpr.new(tok)
|
125
401
|
end
|
126
402
|
|
127
|
-
|
403
|
+
# Resets the state of the parser
|
404
|
+
def reset
|
128
405
|
@i = 0
|
129
406
|
end
|
130
407
|
end
|
data/lib/Token.rb
CHANGED
@@ -1,15 +1,34 @@
|
|
1
|
+
# A token that is created by the lexer and can be recognized by the parser.
|
2
|
+
#
|
3
|
+
# @author Paul T.
|
1
4
|
class Calcula::Token
|
2
|
-
attr_reader :id, :text
|
3
5
|
|
4
|
-
#
|
6
|
+
# @return [Symbol] Returns the type of the token
|
7
|
+
attr_reader :id
|
8
|
+
|
9
|
+
# @return [String] Returns the string segment from the source
|
10
|
+
attr_reader :text
|
11
|
+
|
12
|
+
# Constructs a new Token that can be used with `Calcula::Parser`
|
13
|
+
#
|
14
|
+
# @param id [symbol] The type of the Token
|
15
|
+
# @param text [string] The actual text
|
5
16
|
def initialize(id, text)
|
6
17
|
@id = id
|
7
18
|
@text = text
|
8
19
|
end
|
9
|
-
|
20
|
+
|
21
|
+
# Converts the token into a format of `[:id => :text]`
|
22
|
+
#
|
23
|
+
# @return [String] The string representation of the Token in above format
|
24
|
+
def to_s
|
10
25
|
return "[#{@id}=>#{text}]"
|
11
26
|
end
|
12
|
-
|
27
|
+
|
28
|
+
# Duplicates a token
|
29
|
+
#
|
30
|
+
# @return [Calcula::Token] A duplicate of the token
|
31
|
+
def dup
|
13
32
|
Token.new(@id, @text)
|
14
33
|
end
|
15
34
|
end
|