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.
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