keisan 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +90 -132
- data/bin/keisan +13 -2
- data/lib/keisan.rb +5 -0
- data/lib/keisan/ast.rb +11 -0
- data/lib/keisan/ast/assignment.rb +20 -55
- data/lib/keisan/ast/boolean.rb +16 -12
- data/lib/keisan/ast/cell.rb +17 -0
- data/lib/keisan/ast/cell_assignment.rb +70 -0
- data/lib/keisan/ast/function_assignment.rb +52 -0
- data/lib/keisan/ast/hash.rb +82 -0
- data/lib/keisan/ast/indexing.rb +26 -15
- data/lib/keisan/ast/line_builder.rb +22 -4
- data/lib/keisan/ast/list.rb +14 -7
- data/lib/keisan/ast/node.rb +13 -0
- data/lib/keisan/ast/null.rb +14 -0
- data/lib/keisan/ast/parent.rb +8 -3
- data/lib/keisan/ast/string.rb +10 -0
- data/lib/keisan/ast/variable.rb +4 -0
- data/lib/keisan/ast/variable_assignment.rb +62 -0
- data/lib/keisan/context.rb +16 -2
- data/lib/keisan/functions/default_registry.rb +4 -0
- data/lib/keisan/functions/enumerable_function.rb +56 -0
- data/lib/keisan/functions/filter.rb +34 -32
- data/lib/keisan/functions/map.rb +25 -31
- data/lib/keisan/functions/puts.rb +23 -0
- data/lib/keisan/functions/reduce.rb +29 -29
- data/lib/keisan/functions/registry.rb +4 -4
- data/lib/keisan/functions/sample.rb +5 -3
- data/lib/keisan/functions/to_h.rb +34 -0
- data/lib/keisan/interpreter.rb +42 -0
- data/lib/keisan/parser.rb +59 -50
- data/lib/keisan/parsing/compound_assignment.rb +15 -0
- data/lib/keisan/parsing/hash.rb +36 -0
- data/lib/keisan/repl.rb +1 -1
- data/lib/keisan/token.rb +1 -0
- data/lib/keisan/tokenizer.rb +23 -19
- data/lib/keisan/tokens/assignment.rb +21 -1
- data/lib/keisan/tokens/colon.rb +11 -0
- data/lib/keisan/tokens/unknown.rb +11 -0
- data/lib/keisan/variables/registry.rb +11 -6
- data/lib/keisan/version.rb +1 -1
- metadata +14 -2
@@ -143,6 +143,15 @@ module Keisan
|
|
143
143
|
Builder.new(components: parsing_argument.components).node
|
144
144
|
}
|
145
145
|
)
|
146
|
+
when Parsing::Hash
|
147
|
+
AST::Hash.new(
|
148
|
+
component.key_value_pairs.map {|key_value_pair|
|
149
|
+
[
|
150
|
+
Builder.new(components: [key_value_pair[0]]).node,
|
151
|
+
Builder.new(components: key_value_pair[1].components).node
|
152
|
+
]
|
153
|
+
}
|
154
|
+
)
|
146
155
|
when Parsing::RoundGroup
|
147
156
|
Builder.new(components: component.components).node
|
148
157
|
when Parsing::CurlyGroup
|
@@ -213,10 +222,19 @@ module Keisan
|
|
213
222
|
@nodes.insert(index, replacement_node)
|
214
223
|
@priorities.insert(index, -1)
|
215
224
|
elsif operator.is_a?(Keisan::Parsing::Operator)
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
225
|
+
if operator.is_a?(Keisan::Parsing::CompoundAssignment)
|
226
|
+
replacement_node = operator.node_class.new(
|
227
|
+
children = [@nodes[index-1],@nodes[index+1]],
|
228
|
+
parsing_operators = [operator],
|
229
|
+
compound_operator: operator.compound_operator
|
230
|
+
)
|
231
|
+
else
|
232
|
+
replacement_node = operator.node_class.new(
|
233
|
+
children = [@nodes[index-1],@nodes[index+1]],
|
234
|
+
parsing_operators = [operator]
|
235
|
+
)
|
236
|
+
end
|
237
|
+
|
220
238
|
@nodes.delete_if.with_index {|node, i| i >= index-1 && i <= index+1}
|
221
239
|
@priorities.delete_if.with_index {|node, i| i >= index-1 && i <= index+1}
|
222
240
|
@nodes.insert(index-1, replacement_node)
|
data/lib/keisan/ast/list.rb
CHANGED
@@ -3,21 +3,16 @@ module Keisan
|
|
3
3
|
class List < Parent
|
4
4
|
def initialize(children = [])
|
5
5
|
super(children)
|
6
|
-
cellify!
|
7
6
|
end
|
8
7
|
|
9
8
|
def evaluate(context = nil)
|
10
9
|
context ||= Context.new
|
11
|
-
|
12
|
-
cellify!
|
10
|
+
@children = children.map {|child| child.is_a?(Cell) ? child : child.evaluate(context)}
|
13
11
|
self
|
14
12
|
end
|
15
13
|
|
16
14
|
def simplify(context = nil)
|
17
|
-
context
|
18
|
-
super(context)
|
19
|
-
cellify!
|
20
|
-
self
|
15
|
+
evaluate(context)
|
21
16
|
end
|
22
17
|
|
23
18
|
def value(context = nil)
|
@@ -29,6 +24,18 @@ module Keisan
|
|
29
24
|
"[#{children.map(&:to_s).join(',')}]"
|
30
25
|
end
|
31
26
|
|
27
|
+
def to_a
|
28
|
+
@children.map(&:value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_cell
|
32
|
+
AST::Cell.new(
|
33
|
+
self.class.new(
|
34
|
+
@children.map(&:to_cell)
|
35
|
+
)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
32
39
|
private
|
33
40
|
|
34
41
|
def cellify!
|
data/lib/keisan/ast/node.rb
CHANGED
@@ -57,6 +57,19 @@ module Keisan
|
|
57
57
|
self
|
58
58
|
end
|
59
59
|
|
60
|
+
def to_cell
|
61
|
+
AST::Cell.new(self)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Will only return False for AST::Boolean(false) and AST::Null
|
65
|
+
def true?
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def false?
|
70
|
+
!true?
|
71
|
+
end
|
72
|
+
|
60
73
|
def +(other)
|
61
74
|
Plus.new(
|
62
75
|
[self, other.to_node]
|
data/lib/keisan/ast/null.rb
CHANGED
@@ -7,6 +7,20 @@ module Keisan
|
|
7
7
|
def value(context = nil)
|
8
8
|
nil
|
9
9
|
end
|
10
|
+
|
11
|
+
def true?
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def equal(other)
|
16
|
+
other = other.to_node
|
17
|
+
other.is_a?(AST::Null) ? Boolean.new(value == other.value) : super
|
18
|
+
end
|
19
|
+
|
20
|
+
def not_equal(other)
|
21
|
+
other = other.to_node
|
22
|
+
other.is_a?(AST::Null) ? Boolean.new(value != other.value) : super
|
23
|
+
end
|
10
24
|
end
|
11
25
|
end
|
12
26
|
end
|
data/lib/keisan/ast/parent.rb
CHANGED
@@ -4,10 +4,10 @@ module Keisan
|
|
4
4
|
attr_reader :children
|
5
5
|
|
6
6
|
def initialize(children = [])
|
7
|
-
children = Array.wrap(children).map
|
8
|
-
|
9
|
-
raise Exceptions::InternalError.new
|
7
|
+
children = Array.wrap(children).map do |child|
|
8
|
+
child.is_a?(Cell) ? child : child.to_node
|
10
9
|
end
|
10
|
+
raise Exceptions::InternalError.new unless children.is_a?(Array)
|
11
11
|
@children = children
|
12
12
|
end
|
13
13
|
|
@@ -25,6 +25,11 @@ module Keisan
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
def freeze
|
29
|
+
children.each(&:freeze)
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
28
33
|
def ==(other)
|
29
34
|
return false unless self.class == other.class
|
30
35
|
|
data/lib/keisan/ast/string.rb
CHANGED
@@ -27,6 +27,16 @@ module Keisan
|
|
27
27
|
"\"#{value}\""
|
28
28
|
end
|
29
29
|
end
|
30
|
+
|
31
|
+
def equal(other)
|
32
|
+
other = other.to_node
|
33
|
+
other.is_a?(AST::String) ? Boolean.new(value == other.value) : super
|
34
|
+
end
|
35
|
+
|
36
|
+
def not_equal(other)
|
37
|
+
other = other.to_node
|
38
|
+
other.is_a?(AST::String) ? Boolean.new(value != other.value) : super
|
39
|
+
end
|
30
40
|
end
|
31
41
|
end
|
32
42
|
end
|
data/lib/keisan/ast/variable.rb
CHANGED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class VariableAssignment
|
4
|
+
attr_reader :assignment, :context, :lhs, :rhs
|
5
|
+
|
6
|
+
def initialize(assignment, context, lhs, rhs)
|
7
|
+
@assignment = assignment
|
8
|
+
@context = context
|
9
|
+
@lhs = lhs
|
10
|
+
@rhs = rhs
|
11
|
+
end
|
12
|
+
|
13
|
+
def evaluate
|
14
|
+
case assignment.compound_operator
|
15
|
+
when :"||"
|
16
|
+
evaluate_variable_or_assignment(context, lhs, rhs)
|
17
|
+
when :"&&"
|
18
|
+
evaluate_variable_and_assignment(context, lhs, rhs)
|
19
|
+
else
|
20
|
+
evaluate_variable_non_logical_assignment(context, lhs, rhs)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def evaluate_variable_or_assignment(context, lhs, rhs)
|
27
|
+
if lhs.variable_truthy?(context)
|
28
|
+
lhs
|
29
|
+
else
|
30
|
+
rhs = rhs.evaluate(context)
|
31
|
+
context.register_variable!(lhs.name, rhs.value(context))
|
32
|
+
rhs
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def evaluate_variable_and_assignment(context, lhs, rhs)
|
37
|
+
if lhs.variable_truthy?(context)
|
38
|
+
rhs = rhs.evaluate(context)
|
39
|
+
context.register_variable!(lhs.name, rhs.value(context))
|
40
|
+
rhs
|
41
|
+
else
|
42
|
+
context.register_variable!(lhs.name, nil) unless context.has_variable?(lhs.name)
|
43
|
+
lhs
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def evaluate_variable_non_logical_assignment(context, lhs, rhs)
|
48
|
+
rhs = rhs.evaluate(context)
|
49
|
+
rhs_value = rhs.value(context)
|
50
|
+
|
51
|
+
if assignment.compound_operator
|
52
|
+
raise Exceptions::InvalidExpression.new("Compound assignment requires variable #{lhs.name} to already exist") unless context.has_variable?(lhs.name)
|
53
|
+
rhs_value = context.variable(lhs.name).value.send(assignment.compound_operator, rhs_value)
|
54
|
+
end
|
55
|
+
|
56
|
+
context.register_variable!(lhs.name, rhs_value, local: assignment.local)
|
57
|
+
# Return the variable assigned value
|
58
|
+
rhs
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/keisan/context.rb
CHANGED
@@ -14,6 +14,12 @@ module Keisan
|
|
14
14
|
@allow_recursive = true
|
15
15
|
end
|
16
16
|
|
17
|
+
def freeze
|
18
|
+
super
|
19
|
+
@function_registry.freeze
|
20
|
+
@variable_registry.freeze
|
21
|
+
end
|
22
|
+
|
17
23
|
# A transient context does not persist variables and functions in this context, but
|
18
24
|
# rather store them one level higher in the parent context. When evaluating a string,
|
19
25
|
# the entire operation is done in a transient context that is unique from the calculators
|
@@ -61,8 +67,12 @@ module Keisan
|
|
61
67
|
@variable_registry.has?(name)
|
62
68
|
end
|
63
69
|
|
70
|
+
def variable_is_modifiable?(name)
|
71
|
+
@variable_registry.modifiable?(name)
|
72
|
+
end
|
73
|
+
|
64
74
|
def register_variable!(name, value, local: false)
|
65
|
-
if !@variable_registry.shadowed.member?(name) && (transient? || !local && @parent&.
|
75
|
+
if !@variable_registry.shadowed.member?(name) && (transient? || !local && @parent&.variable_is_modifiable?(name))
|
66
76
|
@parent.register_variable!(name, value)
|
67
77
|
else
|
68
78
|
@variable_registry.register!(name, value)
|
@@ -77,8 +87,12 @@ module Keisan
|
|
77
87
|
@function_registry.has?(name)
|
78
88
|
end
|
79
89
|
|
90
|
+
def function_is_modifiable?(name)
|
91
|
+
@function_registry.modifiable?(name)
|
92
|
+
end
|
93
|
+
|
80
94
|
def register_function!(name, function, local: false)
|
81
|
-
if transient? || !local && @parent&.
|
95
|
+
if transient? || !local && @parent&.function_is_modifiable?(name)
|
82
96
|
@parent.register_function!(name, function)
|
83
97
|
else
|
84
98
|
@function_registry.register!(name.to_s, function)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative "let"
|
2
|
+
require_relative "puts"
|
2
3
|
|
3
4
|
require_relative "if"
|
4
5
|
require_relative "while"
|
@@ -8,6 +9,7 @@ require_relative "range"
|
|
8
9
|
require_relative "map"
|
9
10
|
require_relative "filter"
|
10
11
|
require_relative "reduce"
|
12
|
+
require_relative "to_h"
|
11
13
|
require_relative "rand"
|
12
14
|
require_relative "sample"
|
13
15
|
require_relative "math_function"
|
@@ -46,6 +48,7 @@ module Keisan
|
|
46
48
|
|
47
49
|
def self.register_defaults!(registry)
|
48
50
|
registry.register!(:let, Let.new, force: true)
|
51
|
+
registry.register!(:puts, Puts.new, force: true)
|
49
52
|
|
50
53
|
registry.register!(:if, If.new, force: true)
|
51
54
|
registry.register!(:while, While.new, force: true)
|
@@ -57,6 +60,7 @@ module Keisan
|
|
57
60
|
registry.register!(:select, Filter.new, force: true)
|
58
61
|
registry.register!(:reduce, Reduce.new, force: true)
|
59
62
|
registry.register!(:inject, Reduce.new, force: true)
|
63
|
+
registry.register!(:to_h, ToH.new, force: true)
|
60
64
|
|
61
65
|
register_math!(registry)
|
62
66
|
register_array_methods!(registry)
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class EnumerableFunction < Function
|
4
|
+
# Filters lists/hashes:
|
5
|
+
# (list, variable, boolean_expression)
|
6
|
+
# (hash, key, value, boolean_expression)
|
7
|
+
def initialize(name)
|
8
|
+
super(name, -3)
|
9
|
+
end
|
10
|
+
|
11
|
+
def value(ast_function, context = nil)
|
12
|
+
evaluate(ast_function, context)
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate(ast_function, context = nil)
|
16
|
+
validate_arguments!(ast_function.children.count)
|
17
|
+
context ||= Context.new
|
18
|
+
|
19
|
+
operand, arguments, expression = operand_arguments_expression_for(ast_function, context)
|
20
|
+
|
21
|
+
case operand
|
22
|
+
when AST::List
|
23
|
+
evaluate_list(operand, arguments, expression, context)
|
24
|
+
when AST::Hash
|
25
|
+
evaluate_hash(operand, arguments, expression, context)
|
26
|
+
else
|
27
|
+
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{operand}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def simplify(ast_function, context = nil)
|
32
|
+
evaluate(ast_function, context)
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def verify_arguments!(arguments)
|
38
|
+
unless arguments.all? {|argument| argument.is_a?(AST::Variable)}
|
39
|
+
raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def operand_arguments_expression_for(ast_function, context)
|
46
|
+
operand = ast_function.children[0].simplify(context)
|
47
|
+
arguments = ast_function.children[1...-1]
|
48
|
+
expression = ast_function.children[-1]
|
49
|
+
|
50
|
+
verify_arguments!(arguments)
|
51
|
+
|
52
|
+
[operand, arguments, expression]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,61 +1,63 @@
|
|
1
|
+
require "keisan/functions/enumerable_function"
|
2
|
+
|
1
3
|
module Keisan
|
2
4
|
module Functions
|
3
|
-
class Filter <
|
4
|
-
# Filters
|
5
|
-
#
|
6
|
-
#
|
5
|
+
class Filter < EnumerableFunction
|
6
|
+
# Filters lists/hashes:
|
7
|
+
# (list, variable, boolean_expression)
|
8
|
+
# (hash, key, value, boolean_expression)
|
7
9
|
def initialize
|
8
|
-
super("filter"
|
9
|
-
end
|
10
|
-
|
11
|
-
def value(ast_function, context = nil)
|
12
|
-
evaluate(ast_function, context)
|
13
|
-
end
|
14
|
-
|
15
|
-
def evaluate(ast_function, context = nil)
|
16
|
-
context ||= Context.new
|
17
|
-
simplify(ast_function, context).evaluate(context)
|
10
|
+
super("filter")
|
18
11
|
end
|
19
12
|
|
20
|
-
|
21
|
-
validate_arguments!(ast_function.children.count)
|
13
|
+
private
|
22
14
|
|
23
|
-
|
24
|
-
|
15
|
+
def evaluate_list(list, arguments, expression, context)
|
16
|
+
unless arguments.count == 1
|
17
|
+
raise Exceptions::InvalidFunctionError.new("Filter on list must take 3 arguments")
|
18
|
+
end
|
19
|
+
variable = arguments.first
|
25
20
|
|
26
21
|
local = context.spawn_child(transient: false, shadowed: [variable.name])
|
27
22
|
|
28
23
|
AST::List.new(
|
29
24
|
list.children.select do |element|
|
30
25
|
local.register_variable!(variable, element)
|
31
|
-
result = expression.
|
26
|
+
result = expression.evaluated(local)
|
32
27
|
|
33
28
|
case result
|
34
29
|
when AST::Boolean
|
35
30
|
result.value
|
36
31
|
else
|
37
|
-
raise Exceptions::InvalidFunctionError.new("Filter requires expression to be a logical expression")
|
32
|
+
raise Exceptions::InvalidFunctionError.new("Filter requires expression to be a logical expression, received: #{result.to_s}")
|
38
33
|
end
|
39
34
|
end
|
40
35
|
)
|
41
36
|
end
|
42
37
|
|
43
|
-
|
38
|
+
def evaluate_hash(hash, arguments, expression, context)
|
39
|
+
unless arguments.count == 2
|
40
|
+
raise Exceptions::InvalidFunctionError.new("Filter on hash must take 4 arguments")
|
41
|
+
end
|
44
42
|
|
45
|
-
|
46
|
-
list = ast_function.children[0].simplify(context)
|
47
|
-
variable = ast_function.children[1]
|
48
|
-
expression = ast_function.children[2]
|
43
|
+
key, value = arguments[0..1]
|
49
44
|
|
50
|
-
|
51
|
-
raise Exceptions::InvalidFunctionError.new("First argument to filter must be a list")
|
52
|
-
end
|
45
|
+
local = context.spawn_child(transient: false, shadowed: [key.name, value.name])
|
53
46
|
|
54
|
-
|
55
|
-
|
56
|
-
|
47
|
+
AST::Hash.new(
|
48
|
+
hash.select do |cur_key, cur_value|
|
49
|
+
local.register_variable!(key, cur_key)
|
50
|
+
local.register_variable!(value, cur_value)
|
51
|
+
result = expression.evaluated(local)
|
57
52
|
|
58
|
-
|
53
|
+
case result
|
54
|
+
when AST::Boolean
|
55
|
+
result.value
|
56
|
+
else
|
57
|
+
raise Exceptions::InvalidFunctionError.new("Filter requires expression to be a logical expression, received: #{result.to_s}")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
)
|
59
61
|
end
|
60
62
|
end
|
61
63
|
end
|