calcula 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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