calcula 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/Exprs/ParamsExpr.rb
CHANGED
@@ -1,27 +1,33 @@
|
|
1
1
|
require_relative "../Expr"
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
# Expression for representing parameter lists (for functions)
|
4
|
+
#
|
5
|
+
# @author Paul T.
|
6
|
+
class Calcula::Exprs::ParamsExpr < Calcula::Expr
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
1
|
+
# A lexer for the Calcula language written in Ruby.
|
2
|
+
#
|
3
|
+
# @author Paul T.
|
4
|
+
class Calcula::Lexer
|
2
5
|
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
when "
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|