keisan 0.7.0 → 0.8.4
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 +5 -5
- data/.travis.yml +6 -3
- data/README.md +47 -3
- data/keisan.gemspec +5 -5
- data/lib/keisan.rb +9 -3
- data/lib/keisan/ast.rb +25 -0
- data/lib/keisan/ast/bitwise_left_shift.rb +17 -0
- data/lib/keisan/ast/bitwise_right_shift.rb +17 -0
- data/lib/keisan/ast/block.rb +4 -0
- data/lib/keisan/ast/boolean.rb +1 -1
- data/lib/keisan/ast/builder.rb +2 -2
- data/lib/keisan/ast/cell.rb +10 -0
- data/lib/keisan/ast/date.rb +23 -0
- data/lib/keisan/ast/date_time_methods.rb +75 -0
- data/lib/keisan/ast/function.rb +9 -0
- data/lib/keisan/ast/function_assignment.rb +16 -6
- data/lib/keisan/ast/hash.rb +4 -0
- data/lib/keisan/ast/logical_and.rb +20 -3
- data/lib/keisan/ast/logical_equal.rb +6 -5
- data/lib/keisan/ast/logical_greater_than.rb +6 -4
- data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +6 -4
- data/lib/keisan/ast/logical_less_than.rb +6 -4
- data/lib/keisan/ast/logical_less_than_or_equal_to.rb +6 -4
- data/lib/keisan/ast/logical_not_equal.rb +6 -5
- data/lib/keisan/ast/logical_operator.rb +24 -0
- data/lib/keisan/ast/logical_or.rb +18 -1
- data/lib/keisan/ast/node.rb +25 -0
- data/lib/keisan/ast/number.rb +24 -0
- data/lib/keisan/ast/operator.rb +3 -1
- data/lib/keisan/ast/parent.rb +5 -1
- data/lib/keisan/ast/plus.rb +10 -0
- data/lib/keisan/ast/time.rb +23 -0
- data/lib/keisan/ast/unary_inverse.rb +1 -1
- data/lib/keisan/ast/unary_operator.rb +1 -1
- data/lib/keisan/ast/variable.rb +10 -9
- data/lib/keisan/calculator.rb +17 -3
- data/lib/keisan/context.rb +27 -10
- data/lib/keisan/evaluator.rb +16 -4
- data/lib/keisan/exceptions.rb +3 -0
- data/lib/keisan/function.rb +6 -0
- data/lib/keisan/functions/break.rb +11 -0
- data/lib/keisan/functions/cmath_function.rb +3 -1
- data/lib/keisan/functions/continue.rb +11 -0
- data/lib/keisan/functions/default_registry.rb +39 -0
- data/lib/keisan/functions/enumerable_function.rb +10 -2
- data/lib/keisan/functions/expression_function.rb +16 -9
- data/lib/keisan/functions/filter.rb +6 -0
- data/lib/keisan/functions/loop_control_flow_function.rb +22 -0
- data/lib/keisan/functions/map.rb +6 -0
- data/lib/keisan/functions/proc_function.rb +2 -2
- data/lib/keisan/functions/reduce.rb +5 -0
- data/lib/keisan/functions/replace.rb +6 -6
- data/lib/keisan/functions/while.rb +7 -1
- data/lib/keisan/parser.rb +7 -5
- data/lib/keisan/parsing/bitwise_left_shift.rb +9 -0
- data/lib/keisan/parsing/bitwise_right_shift.rb +9 -0
- data/lib/keisan/parsing/function.rb +1 -1
- data/lib/keisan/parsing/hash.rb +2 -2
- data/lib/keisan/string_and_group_parser.rb +229 -0
- data/lib/keisan/token.rb +1 -1
- data/lib/keisan/tokenizer.rb +20 -18
- data/lib/keisan/tokens/assignment.rb +3 -1
- data/lib/keisan/tokens/bitwise_shift.rb +23 -0
- data/lib/keisan/tokens/group.rb +1 -7
- data/lib/keisan/tokens/string.rb +2 -4
- data/lib/keisan/util.rb +19 -0
- data/lib/keisan/variables/default_registry.rb +2 -1
- data/lib/keisan/version.rb +1 -1
- metadata +40 -28
data/lib/keisan/functions/map.rb
CHANGED
@@ -28,7 +28,7 @@ module Keisan
|
|
28
28
|
|
29
29
|
ast_function.instance_variable_set(
|
30
30
|
:@children,
|
31
|
-
ast_function.children.map {|child| child.
|
31
|
+
ast_function.children.map {|child| child.simplify(context).to_node}
|
32
32
|
)
|
33
33
|
|
34
34
|
if ast_function.children.all? {|child| child.well_defined?(context)}
|
@@ -44,7 +44,7 @@ module Keisan
|
|
44
44
|
|
45
45
|
ast_function.instance_variable_set(
|
46
46
|
:@children,
|
47
|
-
ast_function.children.map {|child| child.
|
47
|
+
ast_function.children.map {|child| child.simplify(context)}
|
48
48
|
)
|
49
49
|
|
50
50
|
if ast_function.children.all? {|child| child.is_a?(AST::ConstantLiteral)}
|
@@ -6,12 +6,17 @@ module Keisan
|
|
6
6
|
# Reduces (list, initial, accumulator, variable, expression)
|
7
7
|
# e.g. reduce([1,2,3,4], 0, total, x, total+x)
|
8
8
|
# should give 10
|
9
|
+
# When hash: (hash, initial, accumulator, key, value, expression)
|
9
10
|
def initialize
|
10
11
|
super("reduce")
|
11
12
|
end
|
12
13
|
|
13
14
|
protected
|
14
15
|
|
16
|
+
def shadowing_variable_names(children)
|
17
|
+
children.size == 5 ? children[2..3] : children[2..4]
|
18
|
+
end
|
19
|
+
|
15
20
|
def verify_arguments!(arguments)
|
16
21
|
unless arguments[1..-1].all? {|argument| argument.is_a?(AST::Variable)}
|
17
22
|
raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
|
@@ -10,19 +10,19 @@ module Keisan
|
|
10
10
|
evaluate(ast_function, context).value(context)
|
11
11
|
end
|
12
12
|
|
13
|
-
def
|
13
|
+
def simplify(ast_function, context = nil)
|
14
14
|
context ||= Context.new
|
15
15
|
expression, variable, replacement = expression_variable_replacement(ast_function)
|
16
16
|
|
17
|
-
expression = expression.
|
18
|
-
replacement = replacement.
|
17
|
+
expression = expression.simplify(context)
|
18
|
+
replacement = replacement.simplify(context)
|
19
19
|
|
20
|
-
expression.replace(variable, replacement).
|
20
|
+
expression.replace(variable, replacement).simplify(context)
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
23
|
+
def evaluate(ast_function, context = nil)
|
24
24
|
context ||= Context.new
|
25
|
-
|
25
|
+
simplify(ast_function, context).evaluate(context)
|
26
26
|
end
|
27
27
|
|
28
28
|
private
|
@@ -28,7 +28,13 @@ module Keisan
|
|
28
28
|
current = Keisan::AST::Null.new
|
29
29
|
|
30
30
|
while logical_node_evaluates_to_true(logical_node, context)
|
31
|
-
|
31
|
+
begin
|
32
|
+
current = body_node.evaluated(context)
|
33
|
+
rescue Exceptions::BreakError
|
34
|
+
break
|
35
|
+
rescue Exceptions::ContinueError
|
36
|
+
next
|
37
|
+
end
|
32
38
|
end
|
33
39
|
|
34
40
|
current
|
data/lib/keisan/parser.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Parser
|
3
|
-
KEYWORDS = %w(let puts).freeze
|
3
|
+
KEYWORDS = %w(let puts break continue).freeze
|
4
4
|
|
5
5
|
attr_reader :tokens, :components
|
6
6
|
|
@@ -39,7 +39,7 @@ module Keisan
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def parse_multi_line!
|
42
|
-
line_parsers = @tokens
|
42
|
+
line_parsers = Util.array_split(@tokens) {|token| token.is_a?(Tokens::LineSeparator)}.map {|tokens| self.class.new(tokens: tokens)}
|
43
43
|
@components = []
|
44
44
|
line_parsers.each.with_index do |line_parser, i|
|
45
45
|
@components += line_parser.components
|
@@ -52,7 +52,7 @@ module Keisan
|
|
52
52
|
def parse_keyword!
|
53
53
|
keyword = tokens.first.string
|
54
54
|
arguments = if tokens[1].is_a?(Tokens::Group)
|
55
|
-
tokens[1].sub_tokens
|
55
|
+
Util.array_split(tokens[1].sub_tokens) {|token| token.is_a?(Tokens::Comma)}.map {|argument_tokens|
|
56
56
|
Parsing::Argument.new(argument_tokens)
|
57
57
|
}
|
58
58
|
else
|
@@ -212,7 +212,7 @@ module Keisan
|
|
212
212
|
@components << Parsing::List.new(arguments_from_group(token))
|
213
213
|
when :curly
|
214
214
|
if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)}
|
215
|
-
@components << Parsing::Hash.new(token.sub_tokens
|
215
|
+
@components << Parsing::Hash.new(Util.array_split(token.sub_tokens) {|token| token.is_a?(Tokens::Comma)})
|
216
216
|
else
|
217
217
|
@components << Parsing::CurlyGroup.new(token.sub_tokens)
|
218
218
|
end
|
@@ -241,6 +241,8 @@ module Keisan
|
|
241
241
|
:"&" => Parsing::BitwiseAnd,
|
242
242
|
:"|" => Parsing::BitwiseOr,
|
243
243
|
:"^" => Parsing::BitwiseXor,
|
244
|
+
:<< => Parsing::BitwiseLeftShift,
|
245
|
+
:>> => Parsing::BitwiseRightShift,
|
244
246
|
:"==" => Parsing::LogicalEqual,
|
245
247
|
:"!=" => Parsing::LogicalNotEqual,
|
246
248
|
:"&&" => Parsing::LogicalAnd,
|
@@ -280,7 +282,7 @@ module Keisan
|
|
280
282
|
if token.sub_tokens.empty?
|
281
283
|
[]
|
282
284
|
else
|
283
|
-
token.sub_tokens
|
285
|
+
Util.array_split(token.sub_tokens) {|sub_token| sub_token.is_a?(Tokens::Comma)}.map do |sub_tokens|
|
284
286
|
Parsing::Argument.new(sub_tokens)
|
285
287
|
end
|
286
288
|
end
|
data/lib/keisan/parsing/hash.rb
CHANGED
@@ -4,7 +4,7 @@ module Keisan
|
|
4
4
|
attr_reader :key_value_pairs
|
5
5
|
|
6
6
|
def initialize(key_value_pairs)
|
7
|
-
@key_value_pairs = Array
|
7
|
+
@key_value_pairs = Array(key_value_pairs).map {|key_value_pair|
|
8
8
|
validate_and_extract_key_value_pair(key_value_pair)
|
9
9
|
}
|
10
10
|
end
|
@@ -12,7 +12,7 @@ module Keisan
|
|
12
12
|
private
|
13
13
|
|
14
14
|
def validate_and_extract_key_value_pair(key_value_pair)
|
15
|
-
key, value = key_value_pair
|
15
|
+
key, value = Util.array_split(key_value_pair) {|token| token.is_a?(Tokens::Colon)}
|
16
16
|
raise Exceptions::ParseError.new("Invalid hash") unless key.size == 1 && value.size >= 1
|
17
17
|
|
18
18
|
key = key.first
|
@@ -0,0 +1,229 @@
|
|
1
|
+
module Keisan
|
2
|
+
class StringAndGroupParser
|
3
|
+
class Portion
|
4
|
+
attr_reader :start_index, :end_index
|
5
|
+
|
6
|
+
def initialize(start_index)
|
7
|
+
@start_index = start_index
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class StringPortion < Portion
|
12
|
+
attr_reader :string
|
13
|
+
|
14
|
+
def initialize(expression, start_index)
|
15
|
+
super(start_index)
|
16
|
+
|
17
|
+
@string = expression[start_index]
|
18
|
+
@end_index = start_index + 1
|
19
|
+
|
20
|
+
while @end_index < expression.size
|
21
|
+
if expression[@end_index] == quote_type
|
22
|
+
@string << quote_type
|
23
|
+
@end_index += 1
|
24
|
+
# Successfully parsed the string
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
n, c = process_next_character(expression, @end_index)
|
29
|
+
@string << c
|
30
|
+
@end_index += n
|
31
|
+
end
|
32
|
+
|
33
|
+
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, no closing quote #{quote_type}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def size
|
37
|
+
string.size
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
string
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Returns number of processed input characters, and the output character
|
47
|
+
def process_next_character(expression, index)
|
48
|
+
# escape character
|
49
|
+
if expression[index] == "\\"
|
50
|
+
return [2, escaped_character(expression[index + 1])]
|
51
|
+
else
|
52
|
+
return [1, expression[index]]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def quote_type
|
57
|
+
@string[0]
|
58
|
+
end
|
59
|
+
|
60
|
+
def escaped_character(character)
|
61
|
+
case character
|
62
|
+
when "\\", '"', "'"
|
63
|
+
character
|
64
|
+
when "a"
|
65
|
+
"\a"
|
66
|
+
when "b"
|
67
|
+
"\b"
|
68
|
+
when "r"
|
69
|
+
"\r"
|
70
|
+
when "n"
|
71
|
+
"\n"
|
72
|
+
when "s"
|
73
|
+
"\s"
|
74
|
+
when "t"
|
75
|
+
"\t"
|
76
|
+
else
|
77
|
+
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unknown escape character: \"\\#{character}\"")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class GroupPortion < Portion
|
83
|
+
attr_reader :opening_brace, :closing_brace ,:portions, :size
|
84
|
+
|
85
|
+
OPENING_TO_CLOSING_BRACE = {
|
86
|
+
"(" => ")",
|
87
|
+
"{" => "}",
|
88
|
+
"[" => "]",
|
89
|
+
}
|
90
|
+
|
91
|
+
def initialize(expression, start_index)
|
92
|
+
super(start_index)
|
93
|
+
|
94
|
+
case expression[start_index]
|
95
|
+
when OPEN_GROUP_REGEX
|
96
|
+
@opening_brace = expression[start_index]
|
97
|
+
else
|
98
|
+
raise Keisan::Exceptions::TokenizingError.new("Internal error, GroupPortion did not start with brace")
|
99
|
+
end
|
100
|
+
|
101
|
+
@closing_brace = OPENING_TO_CLOSING_BRACE[opening_brace]
|
102
|
+
|
103
|
+
parser = StringAndGroupParser.new(expression, start_index: start_index + 1, ending_character: closing_brace)
|
104
|
+
@portions = parser.portions
|
105
|
+
@size = parser.size + 2
|
106
|
+
|
107
|
+
if start_index + size > expression.size || expression[start_index + size - 1] != closing_brace
|
108
|
+
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, group with opening brace #{opening_brace} did not have closing brace")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_s
|
113
|
+
opening_brace + portions.map(&:to_s).join + closing_brace
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class OtherPortion < Portion
|
118
|
+
attr_reader :string
|
119
|
+
|
120
|
+
def initialize(expression, start_index)
|
121
|
+
super(start_index)
|
122
|
+
|
123
|
+
case expression[start_index]
|
124
|
+
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX
|
125
|
+
raise Keisan::Exceptions::TokenizingError.new("Internal error, OtherPortion should not have string/braces at start")
|
126
|
+
else
|
127
|
+
index = start_index + 1
|
128
|
+
end
|
129
|
+
|
130
|
+
while index < expression.size
|
131
|
+
case expression[index]
|
132
|
+
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX, COMMENT_CHARACTER_REGEX
|
133
|
+
break
|
134
|
+
else
|
135
|
+
index += 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
@end_index = index
|
140
|
+
@string = expression[start_index...end_index]
|
141
|
+
end
|
142
|
+
|
143
|
+
def size
|
144
|
+
string.size
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_s
|
148
|
+
string
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class CommentPortion < Portion
|
153
|
+
attr_reader :string
|
154
|
+
|
155
|
+
def initialize(expression, start_index)
|
156
|
+
super(start_index)
|
157
|
+
|
158
|
+
if expression[start_index] != '#'
|
159
|
+
raise Keisan::Exceptions::TokenizingError.new("Comment should start with '#'")
|
160
|
+
else
|
161
|
+
index = start_index + 1
|
162
|
+
end
|
163
|
+
|
164
|
+
while index < expression.size
|
165
|
+
break if expression[index] == "\n"
|
166
|
+
index += 1
|
167
|
+
end
|
168
|
+
|
169
|
+
@end_index = index
|
170
|
+
@string = expression[start_index...end_index]
|
171
|
+
end
|
172
|
+
|
173
|
+
def size
|
174
|
+
string.size
|
175
|
+
end
|
176
|
+
|
177
|
+
def to_s
|
178
|
+
string
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# An ordered array of "portions", which
|
183
|
+
attr_reader :portions, :size
|
184
|
+
|
185
|
+
COMMENT_CHARACTER_REGEX = /[#]/
|
186
|
+
STRING_CHARACTER_REGEX = /["']/
|
187
|
+
OPEN_GROUP_REGEX = /[\(\{\[]/
|
188
|
+
CLOSED_GROUP_REGEX = /[\)\}\]]/
|
189
|
+
|
190
|
+
# Ending character is used as a second ending condition besides expression size
|
191
|
+
def initialize(expression, start_index: 0, ending_character: nil)
|
192
|
+
index = start_index
|
193
|
+
@portions = []
|
194
|
+
|
195
|
+
while index < expression.size && (ending_character.nil? || expression[index] != ending_character)
|
196
|
+
case expression[index]
|
197
|
+
when STRING_CHARACTER_REGEX
|
198
|
+
portion = StringPortion.new(expression, index)
|
199
|
+
index = portion.end_index
|
200
|
+
@portions << portion
|
201
|
+
|
202
|
+
when OPEN_GROUP_REGEX
|
203
|
+
portion = GroupPortion.new(expression, index)
|
204
|
+
index += portion.size
|
205
|
+
@portions << portion
|
206
|
+
|
207
|
+
when CLOSED_GROUP_REGEX
|
208
|
+
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unexpected closing brace #{expression[start_index]}")
|
209
|
+
|
210
|
+
when COMMENT_CHARACTER_REGEX
|
211
|
+
portion = CommentPortion.new(expression, index)
|
212
|
+
index += portion.size
|
213
|
+
@portions << portion
|
214
|
+
|
215
|
+
else
|
216
|
+
portion = OtherPortion.new(expression, index)
|
217
|
+
index += portion.size
|
218
|
+
@portions << portion
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
@size = index - start_index
|
223
|
+
end
|
224
|
+
|
225
|
+
def to_s
|
226
|
+
portions.map(&:to_s).join
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
data/lib/keisan/token.rb
CHANGED
data/lib/keisan/tokenizer.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Tokenizer
|
3
3
|
TOKEN_CLASSES = [
|
4
|
-
Tokens::Group,
|
5
|
-
Tokens::String,
|
6
4
|
Tokens::Null,
|
7
5
|
Tokens::Boolean,
|
8
6
|
Tokens::Word,
|
9
7
|
Tokens::Number,
|
10
8
|
Tokens::Assignment,
|
9
|
+
Tokens::BitwiseShift,
|
11
10
|
Tokens::LogicalOperator,
|
12
11
|
Tokens::ArithmeticOperator,
|
13
12
|
Tokens::BitwiseOperator,
|
@@ -25,28 +24,31 @@ module Keisan
|
|
25
24
|
attr_reader :expression, :tokens
|
26
25
|
|
27
26
|
def initialize(expression)
|
28
|
-
@expression =
|
29
|
-
@scan = @expression.scan(TOKEN_REGEX)
|
30
|
-
@tokens = tokenize!
|
31
|
-
end
|
27
|
+
@expression = expression
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
29
|
+
portions = StringAndGroupParser.new(expression).portions.reject do |portion|
|
30
|
+
portion.is_a? StringAndGroupParser::CommentPortion
|
31
|
+
end
|
37
32
|
|
38
|
-
|
33
|
+
@tokens = portions.inject([]) do |tokens, portion|
|
34
|
+
case portion
|
35
|
+
when StringAndGroupParser::StringPortion
|
36
|
+
tokens << Tokens::String.new(portion.to_s)
|
37
|
+
when StringAndGroupParser::GroupPortion
|
38
|
+
tokens << Tokens::Group.new(portion.to_s)
|
39
|
+
when StringAndGroupParser::OtherPortion
|
40
|
+
scan = portion.to_s.scan(TOKEN_REGEX)
|
41
|
+
tokens += tokenize!(scan)
|
42
|
+
end
|
39
43
|
|
40
|
-
|
41
|
-
|
44
|
+
tokens
|
45
|
+
end
|
42
46
|
end
|
43
47
|
|
44
|
-
|
45
|
-
expression.gsub(/#[^;]*/, "")
|
46
|
-
end
|
48
|
+
private
|
47
49
|
|
48
|
-
def tokenize!
|
49
|
-
|
50
|
+
def tokenize!(scan)
|
51
|
+
scan.map do |scan_result|
|
50
52
|
i = scan_result.find_index {|token| !token.nil?}
|
51
53
|
token_string = scan_result[i]
|
52
54
|
token = TOKEN_CLASSES[i].new(token_string)
|