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.
@@ -1,27 +1,33 @@
1
1
  require_relative "../Expr"
2
2
 
3
- module Calcula::Exprs
4
- class ParamsExpr < Calcula::Expr
3
+ # Expression for representing parameter lists (for functions)
4
+ #
5
+ # @author Paul T.
6
+ class Calcula::Exprs::ParamsExpr < Calcula::Expr
5
7
 
6
- # params: List[Token]
7
- def initialize(params)
8
- @params = params
9
- end
8
+ # @param params [Array<Calcula::Token>] The tokens should all have type of ID
9
+ def initialize(params)
10
+ @params = params
11
+ end
10
12
 
11
- def to_s()
13
+ # @see Calcula::Expr#to_s
14
+ # @param (see Calcula::Expr#to_s)
15
+ # @return (see Calcula::Expr#to_s)
16
+ def to_s(form: :src)
17
+ case form
18
+ when :src then
12
19
  @params.collect { |e| e.text }.join(',')
20
+ when :tree, :ruby then
21
+ "(#{self})"
22
+ else
23
+ nil
13
24
  end
25
+ end
14
26
 
15
- def to_tree()
16
- "(#{to_s()})"
17
- end
18
-
19
- def exec(binding)
20
- return to_s()
21
- end
22
-
23
- def children()
24
- []
25
- end
27
+ # @see Calcula::Expr#children
28
+ # @param (see Calcula::Expr#children)
29
+ # @return (see Calcula::Expr#children)
30
+ def children
31
+ []
26
32
  end
27
33
  end
@@ -0,0 +1,37 @@
1
+ require_relative "../Expr"
2
+
3
+ # Expression for representing fractions
4
+ #
5
+ # @author Paul T.
6
+ class Calcula::Exprs::RatExpr < Calcula::Expr
7
+
8
+ # @param top [Calcula::Token] The token should have the type of NUM
9
+ # @param bot [Calcula::Token] The token should have the type of NUM
10
+ def initialize(top, bot)
11
+ @top = top
12
+ @bot = bot
13
+ end
14
+
15
+ # @see Calcula::Expr#to_s
16
+ # @param (see Calcula::Expr#to_s)
17
+ # @return (see Calcula::Expr#to_s)
18
+ def to_s(form: :src)
19
+ case form
20
+ when :src then
21
+ "#{@top.text}//#{@bot.text}"
22
+ when :tree then
23
+ "(rat #{@top.text} #{@bot.text})"
24
+ when :ruby then
25
+ "Rational(#{@top.text}, #{@bot.text})"
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ # @see Calcula::Expr#children
32
+ # @param (see Calcula::Expr#children)
33
+ # @return (see Calcula::Expr#children)
34
+ def children
35
+ []
36
+ end
37
+ end
@@ -0,0 +1,84 @@
1
+ require_relative "../Expr"
2
+
3
+ # Expression for unary operators
4
+ #
5
+ # @author Paul T.
6
+ class Calcula::Exprs::UnaryExpr < Calcula::Expr
7
+
8
+ # @param op [Calcula::Token] Should have type prefixed with `OP_`
9
+ # @param base [Calcula::Expr]
10
+ # @param prefixed [true, false]
11
+ def initialize(op, base, prefixed:)
12
+ @op = op
13
+ @base = base
14
+ @prefixed = prefixed
15
+ end
16
+
17
+ # @see Calcula::Expr#to_s
18
+ # @param (see Calcula::Expr#to_s)
19
+ # @return (see Calcula::Expr#to_s)
20
+ def to_s(form: :src)
21
+ baseTxt = @base.to_s(form: form)
22
+ case form
23
+ when :src then
24
+ @prefixed ? "#{@op.text}#{baseTxt}" : "#{baseTxt}#{@op.text}"
25
+ when :tree then
26
+ "(#{@op.text} #{baseTxt})"
27
+ when :ruby then
28
+ if @prefixed then
29
+ case @op.id
30
+ when :OP_ADD then
31
+ "(#{baseTxt}).abs"
32
+ when :NOT then
33
+ "!(#{baseTxt})"
34
+ else
35
+ "#{@op.text}#{baseTxt}"
36
+ end
37
+ else
38
+ case @op.id
39
+ when :ROUND_DOLLAR then
40
+ "(#{baseTxt}).to_f.round(2)"
41
+ when :DISP then
42
+ "puts _ = #{baseTxt}\n_"
43
+ when :ASSERT then
44
+ "_ = #{baseTxt}\nif _ == false then\n raise 'Equation #{@base.to_s} failed'\nend"
45
+ else "#{baseTxt}#{@op.text}"
46
+ end
47
+ end
48
+ else
49
+ nil
50
+ end
51
+ end
52
+
53
+ # @see Calcula::Expr#children
54
+ # @param (see Calcula::Expr#children)
55
+ # @return (see Calcula::Expr#children)
56
+ def children
57
+ [@base]
58
+ end
59
+
60
+ # Creates a prefixed unary expression. Shorthand for
61
+ # `UnaryExpr.new(op, base, prefixed: true)`.
62
+ #
63
+ # @see Calcula::Exprs::UnaryExpr#initialize
64
+ # @see Calcula::Exprs::UnaryExpr#mkPostfix
65
+ # @param op [Calcula::Token] Should have type prefixed with `OP_`
66
+ # @param base [Calcula::Expr]
67
+ # @return [Calcula::Exprs::UnaryExpr] The newly created prefix unary expression
68
+ def self.mkPrefix(op, base)
69
+ new(op, base, prefixed: true)
70
+ end
71
+
72
+
73
+ # Creates a postfix unary expression. Shorthand for
74
+ # `UnaryExpr.new(op, base, prefixed: false)`.
75
+ #
76
+ # @see Calcula::Exprs::UnaryExpr#initialize
77
+ # @see Calcula::Exprs::UnaryExpr#mkPrefix
78
+ # @param op [Calcula::Token] Should have type prefixed with `OP_`
79
+ # @param base [Calcula::Expr]
80
+ # @return [Calcula::Exprs::UnaryExpr] The newly created postfix unary expression
81
+ def self.mkPostfix(op, base)
82
+ new(op, base, prefixed: false)
83
+ end
84
+ end
data/lib/Lexer.rb CHANGED
@@ -1,137 +1,176 @@
1
- class Array
1
+ # A lexer for the Calcula language written in Ruby.
2
+ #
3
+ # @author Paul T.
4
+ class Calcula::Lexer
2
5
 
3
- # el: [T]
4
- def ===(el)
5
- return find_index(el) != nil
6
+ ZERO_TO_NINE = "0".."9"
7
+ LOWER_A_TO_Z = "a".."z"
8
+ UPPER_A_TO_Z = "A".."Z"
9
+
10
+ SINGLE_CHAR_TOKEN = {
11
+ "(" => :PAREN_O, ")" => :PAREN_C,
12
+ "[" => :SQUARE_O, "]" => :SQUARE_C,
13
+ "+" => :OP_ADD, "-" => :OP_SUB,
14
+ "%" => :OP_REM, "$" => :ROUND_DOLLAR,
15
+ "@" => :COMPOSE, ";" => :DISP,
16
+ "," => :COMMA, "\\" => :LAMBDA,
17
+ "=" => :OP_EQ,
18
+
19
+ " " => :WS, "\t" => :WS, "\r" => :WS,
20
+ "\f" => :WS, "\n" => :WS
21
+ }
22
+
23
+ # Constructs a new Lexer instance
24
+ #
25
+ # @param src [String] The source code
26
+ def initialize(src)
27
+ @src = src
28
+ @i = 0
29
+ @lineNum = 0
30
+ @linePos = 0
6
31
  end
7
- end
8
32
 
9
- module Calcula
10
- class Lexer
11
- ZERO_TO_NINE = "0".."9"
12
- A_TO_Z = ("a".."z").to_a + ("A" .. "Z").to_a
33
+ # Raises a error with a seemingly useful message
34
+ #
35
+ # @raise [RuntimeError] Calling this method raises this error
36
+ # @param msg [String] The additional message or the reason of the error
37
+ def lexError(msg)
38
+ raise "Failed to lex past (L#{@lineNum + 1}:#{@linePos + 1}): #{if msg != "" then msg else "<no message>" end}"
39
+ end
13
40
 
14
- # src: string
15
- def initialize(src)
16
- @src = src
17
- @i = 0
18
- @lineNum = 0
41
+ # Advances the line number and the column position based on the character found
42
+ #
43
+ # @param c [Character] The character used to determine the advancing rule
44
+ def advancePos(c)
45
+ @i += 1
46
+ if c == '\n' then
47
+ @lineNum += 1
19
48
  @linePos = 0
49
+ else
50
+ @linePos += 1
20
51
  end
52
+ end
21
53
 
22
- # msg: string
23
- def lexError(msg)
24
- raise "Failed to lex past (L#{@lineNum + 1}:#{@linePos + 1}): #{if msg != "" then msg else "<no message>" end}"
25
- end
26
-
27
- # c: char
28
- def advancePos(c)
29
- @i += 1
30
- if c == '\n' then
31
- @lineNum += 1
32
- @linePos = 0
33
- else
34
- @linePos += 1
35
- end
54
+ # Consumes characters until the block yields false. The first character is
55
+ # consumed no matter what.
56
+ #
57
+ # @param pre_append [Optional, lambda] Executed before appending. If returns false, the process terminates and the consumed chars are returned
58
+ # @param post_append [Optional, lambda] Executed after appending. If returns false, the process terminates and the consumed chars are returned
59
+ # @yield [c] Predicate for whether or not character consumption should continue
60
+ # @yieldparam c [String (Char)] The current character
61
+ # @yieldreturn [true, false] true if consumption should continue, false otherwise
62
+ # @return [String] The characters being consumed
63
+ def consumeWhen(pre_append: ->(_c){ true }, post_append: ->(_c){ true })
64
+ advancePos(buf = @src[@i])
65
+ while @i < @src.length && yield(@src[@i]) do
66
+ break unless pre_append.call(@src[@i])
67
+ buf += @src[@i]
68
+ advancePos(@src[@i])
69
+ break unless post_append.call(@src[@i])
36
70
  end
71
+ buf
72
+ end
37
73
 
38
- def lex()
39
- len = @src.length
40
- rst = []
41
- while @i < len do
42
- case (@src[@i])
43
- when "(" then
44
- advancePos(@src[@i])
45
- rst << Token.new(:PAREN_O, "(")
46
- when ")" then
47
- advancePos(@src[@i])
48
- rst << Token.new(:PAREN_C, ")")
49
- when "[" then
50
- advancePos(@src[@i])
51
- rst << Token.new(:SQUARE_O, "[")
52
- when "]" then
53
- advancePos(@src[@i])
54
- rst << Token.new(:SQUARE_C, "]")
55
- when "+" then
56
- advancePos(@src[@i])
57
- rst << Token.new(:OP_ADD, "+")
58
- when "-" then
59
- advancePos(@src[@i])
60
- rst << Token.new(:OP_SUB, "-")
61
- when "*" then
62
- advancePos(@src[@i])
63
- if @src[@i] == "*" then
64
- advancePos(@src[@i])
65
- rst << Token.new(:OP_POW, "**")
66
- else
67
- rst << Token.new(:OP_MUL, "*")
68
- end
69
- when "/" then
70
- advancePos(@src[@i])
71
- rst << Token.new(:OP_DIV, "/")
72
- when "=" then
73
- advancePos(@src[@i])
74
- rst << Token.new(:ASSIGN, "=")
75
- when "%" then
76
- advancePos(@src[@i])
77
- rst << Token.new(:OP_REM, "%")
78
- when "$" then
79
- advancePos(@src[@i])
80
- rst << Token.new(:ROUND_DOLLAR, "$")
81
- when "@" then
82
- advancePos(@src[@i])
83
- rst << Token.new(:COMPOSE, "@")
84
- when ";" then
85
- advancePos(@src[@i])
86
- rst << Token.new(:DISP, ";")
87
- when "\\" then
88
- advancePos(@src[@i])
89
- rst << Token.new(:LAMBDA, "\\")
90
- when "," then
91
- advancePos(@src[@i])
92
- rst << Token.new(:COMMA, ",")
93
- when "#" then
94
- advancePos(buf = @src[@i])
95
- while @i < len && @src[@i] != "\n" do
96
- buf += @src[@i]
97
- advancePos(@src[@i])
98
- end
99
- rst << Token.new(:COMMENT, buf)
100
- when ZERO_TO_NINE then
101
- advancePos(buf = @src[@i])
102
- inDecimals = false
103
- while @i < len && (ZERO_TO_NINE === @src[@i] || "." == @src[@i]) do
104
- if @src[@i] == "." then
105
- if !inDecimals then
106
- inDecimals = true
107
- else
108
- break
109
- end
74
+ # Starts the lexing routine which converts the source code into a list of tokens
75
+ #
76
+ # @return [Array<Calcula::Token>] The tokens based on the source code
77
+ def lex
78
+ len = @src.length
79
+ rst = []
80
+ while @i < len do
81
+ case @src[@i]
82
+ when "*" then
83
+ rst << potentialDuplicateToken(:OP_MUL, :OP_POW)
84
+ when "/" then
85
+ rst << potentialDuplicateToken(:OP_DIV, :RAT)
86
+ when "#" then
87
+ rst << Calcula::Token.new(:COMMENT, consumeWhen { |c| c != "\n" })
88
+ when "!" then
89
+ rst << continuousConsume(:ASSERT, {"=" => :OP_NE})
90
+ when "<" then
91
+ rst << continuousConsume(:OP_LT, {"=" => :OP_LE})
92
+ when ">" then
93
+ rst << continuousConsume(:OP_GT, {"=" => :OP_GE})
94
+ when LOWER_A_TO_Z, UPPER_A_TO_Z then
95
+ txtSeq = consumeWhen { |c| isIdentChar? c }
96
+ case txtSeq
97
+ when "let", "and", "or", "not" then # The textual keywords
98
+ rst << Calcula::Token.new(txtSeq.upcase.to_sym, txtSeq)
99
+ else
100
+ rst << Calcula::Token.new(:ID, txtSeq)
101
+ end
102
+ when ZERO_TO_NINE then
103
+ inDecimals = false
104
+ txt = consumeWhen(pre_append: ->(c){
105
+ if c == "." then
106
+ if !inDecimals then
107
+ inDecimals = true
108
+ else
109
+ lexError("Numbers with two or more decimal points are illegal")
110
110
  end
111
- buf += @src[@i]
112
- advancePos(@src[@i])
113
- end
114
- rst << Token.new(:NUM, buf)
115
- when A_TO_Z then
116
- advancePos(buf = @src[@i])
117
- while @i < len && (A_TO_Z === @src[@i] || "'" == @src[@i]) do
118
- buf += @src[@i]
119
- advancePos(@src[@i])
120
111
  end
121
- rst << Token.new(:ID, buf)
122
- when [" ", "\t", "\r", "\f", "\n"] then
123
- advancePos(ws = @src[@i])
124
- rst << Token.new(:WS, ws)
125
- else lexError("Unrecognized token starting with '#{@src[@i]}'")
112
+ true
113
+ }) { |c| isNumericChar? c }
114
+ rst << Calcula::Token.new(:NUM, txt)
115
+ else
116
+ if (type = SINGLE_CHAR_TOKEN[chr = @src[@i]]) != nil then
117
+ advancePos(@src[@i])
118
+ rst << Calcula::Token.new(type, chr)
119
+ else
120
+ lexError("Unrecognized token starting with '#{@src[@i]}'")
126
121
  end
127
122
  end
128
- return rst
129
123
  end
124
+ return rst
125
+ end
130
126
 
131
- def reset()
132
- @i = 0
133
- @lineNum = 0
134
- @linePos = 0
127
+ # Resets the lexing position
128
+ def reset
129
+ @i = 0
130
+ @lineNum = 0
131
+ @linePos = 0
132
+ end
133
+
134
+ # Checks to see if the character is part of a valid identifier
135
+ #
136
+ # @param c [String (Char)] The character
137
+ # @return [true, false] If the character is valid
138
+ def isIdentChar?(c)
139
+ LOWER_A_TO_Z === c || UPPER_A_TO_Z === c || "'" == c
140
+ end
141
+
142
+ # Checks to see if the character is numeric (including the decimal point)
143
+ #
144
+ # @param c [String (Char)] The character
145
+ # @return [true, false] If the character is valid
146
+ def isNumericChar?(c)
147
+ ZERO_TO_NINE === c || "." == c
148
+ end
149
+
150
+ # Checks if the two characters in sequence are the same and form a token by
151
+ # itself. Short hand for `continuousConsume(idSingle, {current_char => idDouble})`.
152
+ #
153
+ # @param idSingle [Symbol] If the first character itself is an individual token
154
+ # @param idDouble [Symbol] If both characters are interpreted as a token
155
+ # @return [Calcula::Token] The token with either `idSingle` or `idDouble` as type
156
+ def potentialDuplicateToken(idSingle, idDouble)
157
+ previous = @src[@i]
158
+ continuousConsume(idSingle, {previous => idDouble})
159
+ end
160
+
161
+ # Checks if the consecutive character forms another token
162
+ #
163
+ # @param idSingle [Symbol] The token type for the first character alone
164
+ # @param continuousTok [Hash{String (Char) => Symbol}] The consecutive character
165
+ # @return [Calcula::Token] The token found
166
+ def continuousConsume(idSingle, continuousTok)
167
+ advancePos(buf = @src[@i])
168
+ if (maybeId = continuousTok[@src[@i]]) == nil then
169
+ maybeId = idSingle
170
+ else
171
+ buf += @src[@i]
172
+ advancePos(@src[@i])
135
173
  end
174
+ return Calcula::Token.new(maybeId, buf)
136
175
  end
137
176
  end