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