calcula 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
- # toks: List[Tokens]
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
- # unexpectedTok: Token | Nil, msg: string
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: #{if msg != "" then msg else "<no message>" end}"
39
+ raise "Unexpected EOF: #{msg != '' ? msg : '<no message>'}"
20
40
  end
21
- raise "Unexpected token #{unexpectedTok.to_s}: #{if msg != "" then msg else "<no message>" end}"
41
+ raise "Unexpected token #{unexpectedTok}: #{msg != '' ? msg : '<no message>'}"
22
42
  end
23
43
 
24
- # tokId: symbol
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
- def lookahead()
38
- return nil if @i == @toks.length
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
- def parse()
50
- if lookahead() != nil then
51
- return basicValExpr()
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
- # expr = parenExpr
57
- def expr()
58
- parenExpr()
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
- # parenExpr = PAREN_O expr PAREN_C
62
- # | basicValExpr
63
- def parenExpr()
64
- tmp = lookahead()
65
- parseError(nil, "Expected PAREN_O or basicValExpr") if tmp == nil
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
- case tmp.id
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
- inner = expr()
122
+ params = paramsExpr()
71
123
  consume(:PAREN_C)
72
- return inner
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
- # basicValExpr = numExpr | identExpr
78
- def basicValExpr()
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 identExpr()
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
- # paramsExpr = ID (COMMA ID)+
94
- # | ID
95
- def paramsExpr()
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
- while true
357
+ loop do
98
358
  begin
99
- tmp = consume(:COMMA)
100
- rescue Exception
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
- # lambdaExpr = LAMBDA paramsExpr parenExpr
109
- def lambdaExpr()
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.mkLambda(params, parenExpr())
375
+ return FuncExpr.new(params, equation())
113
376
  end
114
377
 
115
- # numExpr = NUM
116
- def numExpr()
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
- return NumExpr.new(tok)
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
- # identExpr = ID
122
- def identExpr()
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
- def reset()
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
- # id: symbol, text: string
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
- def to_s()
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
- def dup()
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