keisan 0.6.0 → 0.7.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/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
|