keisan 0.2.1 → 0.3.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 +92 -0
- data/lib/keisan.rb +12 -0
- data/lib/keisan/ast/bitwise_and.rb +1 -5
- data/lib/keisan/ast/bitwise_or.rb +1 -5
- data/lib/keisan/ast/bitwise_xor.rb +1 -5
- data/lib/keisan/ast/builder.rb +57 -22
- data/lib/keisan/ast/exponent.rb +1 -5
- data/lib/keisan/ast/function.rb +50 -1
- data/lib/keisan/ast/logical_and.rb +1 -5
- data/lib/keisan/ast/logical_equal.rb +18 -0
- data/lib/keisan/ast/logical_greater_than.rb +4 -4
- data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +4 -4
- data/lib/keisan/ast/logical_less_than.rb +4 -4
- data/lib/keisan/ast/logical_less_than_or_equal_to.rb +4 -4
- data/lib/keisan/ast/logical_not_equal.rb +18 -0
- data/lib/keisan/ast/logical_or.rb +1 -5
- data/lib/keisan/ast/modulo.rb +18 -0
- data/lib/keisan/ast/node.rb +26 -0
- data/lib/keisan/ast/operator.rb +9 -2
- data/lib/keisan/ast/plus.rb +1 -5
- data/lib/keisan/ast/priorities.rb +27 -0
- data/lib/keisan/ast/times.rb +1 -5
- data/lib/keisan/ast/variable.rb +5 -0
- data/lib/keisan/calculator.rb +1 -12
- data/lib/keisan/context.rb +20 -5
- data/lib/keisan/evaluator.rb +79 -0
- data/lib/keisan/exceptions.rb +1 -0
- data/lib/keisan/functions/default_registry.rb +0 -5
- data/lib/keisan/functions/registry.rb +7 -0
- data/lib/keisan/parser.rb +42 -29
- data/lib/keisan/parsing/dot.rb +6 -0
- data/lib/keisan/parsing/dot_operator.rb +12 -0
- data/lib/keisan/parsing/dot_word.rb +14 -0
- data/lib/keisan/parsing/logical_equal.rb +9 -0
- data/lib/keisan/parsing/logical_not_equal.rb +9 -0
- data/lib/keisan/parsing/modulo.rb +9 -0
- data/lib/keisan/tokenizer.rb +2 -1
- data/lib/keisan/tokens/arithmetic_operator.rb +4 -1
- data/lib/keisan/tokens/dot.rb +11 -0
- data/lib/keisan/tokens/logical_operator.rb +7 -1
- data/lib/keisan/variables/registry.rb +7 -0
- data/lib/keisan/version.rb +1 -1
- metadata +14 -2
@@ -0,0 +1,18 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class LogicalEqual < LogicalOperator
|
4
|
+
def arity
|
5
|
+
2..2
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.symbol
|
9
|
+
:"=="
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context=nil)
|
13
|
+
context ||= Context.new
|
14
|
+
children[0].value(context) == children[1].value(context)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
3
|
class LogicalGreaterThan < LogicalOperator
|
4
|
-
def self.priority
|
5
|
-
82
|
6
|
-
end
|
7
|
-
|
8
4
|
def arity
|
9
5
|
2..2
|
10
6
|
end
|
11
7
|
|
8
|
+
def self.symbol
|
9
|
+
:">"
|
10
|
+
end
|
11
|
+
|
12
12
|
def value(context = nil)
|
13
13
|
children.first.value(context) > children.last.value(context)
|
14
14
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
3
|
class LogicalGreaterThanOrEqualTo < LogicalOperator
|
4
|
-
def self.priority
|
5
|
-
62
|
6
|
-
end
|
7
|
-
|
8
4
|
def arity
|
9
5
|
2..2
|
10
6
|
end
|
11
7
|
|
8
|
+
def self.symbol
|
9
|
+
:">="
|
10
|
+
end
|
11
|
+
|
12
12
|
def value(context = nil)
|
13
13
|
children.first.value(context) >= children.last.value(context)
|
14
14
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
3
|
class LogicalLessThan < LogicalOperator
|
4
|
-
def self.priority
|
5
|
-
72
|
6
|
-
end
|
7
|
-
|
8
4
|
def arity
|
9
5
|
2..2
|
10
6
|
end
|
11
7
|
|
8
|
+
def self.symbol
|
9
|
+
:"<"
|
10
|
+
end
|
11
|
+
|
12
12
|
def value(context = nil)
|
13
13
|
children.first.value(context) < children.last.value(context)
|
14
14
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
3
|
class LogicalLessThanOrEqualTo < LogicalOperator
|
4
|
-
def self.priority
|
5
|
-
52
|
6
|
-
end
|
7
|
-
|
8
4
|
def arity
|
9
5
|
2..2
|
10
6
|
end
|
11
7
|
|
8
|
+
def self.symbol
|
9
|
+
:"<="
|
10
|
+
end
|
11
|
+
|
12
12
|
def value(context = nil)
|
13
13
|
children.first.value(context) <= children.last.value(context)
|
14
14
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class LogicalNotEqual < LogicalOperator
|
4
|
+
def arity
|
5
|
+
2..2
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.symbol
|
9
|
+
:"!="
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context=nil)
|
13
|
+
context ||= Context.new
|
14
|
+
children[0].value(context) != children[1].value(context)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class Modulo < ArithmeticOperator
|
4
|
+
def arity
|
5
|
+
2..Float::INFINITY
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.symbol
|
9
|
+
:%
|
10
|
+
end
|
11
|
+
|
12
|
+
def value(context = nil)
|
13
|
+
children_values = children.map {|child| child.value(context)}
|
14
|
+
children_values[1..-1].inject(children_values[0], &:%)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/keisan/ast/node.rb
CHANGED
@@ -4,6 +4,32 @@ module Keisan
|
|
4
4
|
def value(context = nil)
|
5
5
|
raise Keisan::Exceptions::NotImplementedError.new
|
6
6
|
end
|
7
|
+
|
8
|
+
def unbound_variables(context = nil)
|
9
|
+
context ||= Keisan::Context.new
|
10
|
+
|
11
|
+
case self
|
12
|
+
when Parent
|
13
|
+
children.inject(Set.new) do |vars, child|
|
14
|
+
vars | child.unbound_variables(context)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
Set.new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def unbound_functions(context = nil)
|
22
|
+
context ||= Keisan::Context.new
|
23
|
+
|
24
|
+
case self
|
25
|
+
when Parent
|
26
|
+
children.inject(Set.new) do |fns, child|
|
27
|
+
fns | child.unbound_functions(context)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
Set.new
|
31
|
+
end
|
32
|
+
end
|
7
33
|
end
|
8
34
|
end
|
9
35
|
end
|
data/lib/keisan/ast/operator.rb
CHANGED
@@ -16,17 +16,24 @@ module Keisan
|
|
16
16
|
@parsing_operators = parsing_operators
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
19
|
def arity
|
21
20
|
raise Keisan::Exceptions::NotImplementedError.new
|
22
21
|
end
|
23
22
|
|
23
|
+
def priority
|
24
|
+
self.class.priority
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.priority
|
28
|
+
Keisan::AST::Priorities.priorities[symbol]
|
29
|
+
end
|
30
|
+
|
24
31
|
def associativity
|
25
32
|
raise Keisan::Exceptions::NotImplementedError.new
|
26
33
|
end
|
27
34
|
|
28
35
|
def symbol
|
29
|
-
|
36
|
+
self.class.symbol
|
30
37
|
end
|
31
38
|
|
32
39
|
def blank_value
|
data/lib/keisan/ast/plus.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class Priorities
|
4
|
+
PRIORITIES = {
|
5
|
+
"**": 100,
|
6
|
+
"*": 95,
|
7
|
+
"%": 90,
|
8
|
+
"+": 85,
|
9
|
+
"&": 80,
|
10
|
+
"|": 75,
|
11
|
+
"^": 70,
|
12
|
+
">": 65,
|
13
|
+
">=": 60,
|
14
|
+
"<": 55,
|
15
|
+
"<=": 50,
|
16
|
+
"==": 45,
|
17
|
+
"!=": 40,
|
18
|
+
"&&": 35,
|
19
|
+
"||": 30
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def self.priorities
|
23
|
+
PRIORITIES
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/keisan/ast/times.rb
CHANGED
data/lib/keisan/ast/variable.rb
CHANGED
@@ -11,6 +11,11 @@ module Keisan
|
|
11
11
|
context = Keisan::Context.new if context.nil?
|
12
12
|
context.variable(name)
|
13
13
|
end
|
14
|
+
|
15
|
+
def unbound_variables(context = nil)
|
16
|
+
context ||= Keisan::Context.new
|
17
|
+
context.has_variable?(name) ? Set.new : Set.new([name])
|
18
|
+
end
|
14
19
|
end
|
15
20
|
end
|
16
21
|
end
|
data/lib/keisan/calculator.rb
CHANGED
@@ -7,18 +7,7 @@ module Keisan
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def evaluate(expression, definitions = {})
|
10
|
-
|
11
|
-
definitions.each do |name, value|
|
12
|
-
case value
|
13
|
-
when Proc
|
14
|
-
local.register_function!(name, value)
|
15
|
-
else
|
16
|
-
local.register_variable!(name, value)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
Keisan::AST::Builder.new(string: expression).ast.value(local)
|
21
|
-
end
|
10
|
+
Evaluator.new(self).evaluate(expression, definitions)
|
22
11
|
end
|
23
12
|
|
24
13
|
def define_variable!(name, value)
|
data/lib/keisan/context.rb
CHANGED
@@ -9,18 +9,29 @@ module Keisan
|
|
9
9
|
@random = random
|
10
10
|
end
|
11
11
|
|
12
|
-
def spawn_child
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
def spawn_child(definitions = {})
|
13
|
+
child = self.class.new(parent: self)
|
14
|
+
|
15
|
+
definitions.each do |name, value|
|
16
|
+
case value
|
17
|
+
when Proc
|
18
|
+
child.register_function!(name, value)
|
19
|
+
else
|
20
|
+
child.register_variable!(name, value)
|
21
|
+
end
|
17
22
|
end
|
23
|
+
|
24
|
+
child
|
18
25
|
end
|
19
26
|
|
20
27
|
def function(name)
|
21
28
|
@function_registry[name.to_s]
|
22
29
|
end
|
23
30
|
|
31
|
+
def has_function?(name)
|
32
|
+
@function_registry.has?(name)
|
33
|
+
end
|
34
|
+
|
24
35
|
def register_function!(name, function)
|
25
36
|
@function_registry.register!(name.to_s, function)
|
26
37
|
end
|
@@ -29,6 +40,10 @@ module Keisan
|
|
29
40
|
@variable_registry[name.to_s]
|
30
41
|
end
|
31
42
|
|
43
|
+
def has_variable?(name)
|
44
|
+
@variable_registry.has?(name)
|
45
|
+
end
|
46
|
+
|
32
47
|
def register_variable!(name, value)
|
33
48
|
@variable_registry.register!(name.to_s, value)
|
34
49
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Keisan
|
2
|
+
class Evaluator
|
3
|
+
WORD = /[a-zA-Z_]\w*/
|
4
|
+
VARIABLE_DEFINITION = /\A\s*(#{WORD})\s*=\s*(.+)\z/
|
5
|
+
FUNCTION_DEFINITION = /\A\s*(#{WORD})\s*\(((?:\s*#{WORD}\s*)(?:,\s*#{WORD}\s*)*)\)\s*=\s*(.+)\z/
|
6
|
+
|
7
|
+
attr_reader :calculator
|
8
|
+
|
9
|
+
def initialize(calculator)
|
10
|
+
@calculator = calculator
|
11
|
+
end
|
12
|
+
|
13
|
+
def evaluate(expression, definitions = {})
|
14
|
+
case expression
|
15
|
+
when VARIABLE_DEFINITION
|
16
|
+
variable_evaluate(expression, definitions)
|
17
|
+
when FUNCTION_DEFINITION
|
18
|
+
function_evaluate(expression, definitions)
|
19
|
+
else
|
20
|
+
pure_evaluate(expression, definitions)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def variable_evaluate(expression, definitions = {})
|
27
|
+
match = expression.match VARIABLE_DEFINITION
|
28
|
+
name = match[1]
|
29
|
+
expression = match[2]
|
30
|
+
ast = Keisan::AST::Builder.new(string: expression).ast
|
31
|
+
|
32
|
+
context = calculator.context.spawn_child(definitions)
|
33
|
+
unbound_variables = ast.unbound_variables(context)
|
34
|
+
unless unbound_variables <= Set.new([name])
|
35
|
+
raise Keisan::Exceptions::InvalidExpression.new("Unbound variables found in variable definition")
|
36
|
+
end
|
37
|
+
|
38
|
+
calculator.context.register_variable!(name, ast.value(context))
|
39
|
+
end
|
40
|
+
|
41
|
+
def function_evaluate(expression, definitions = {})
|
42
|
+
match = expression.match FUNCTION_DEFINITION
|
43
|
+
name = match[1]
|
44
|
+
args = match[2].split(",").map(&:strip)
|
45
|
+
expression = match[3]
|
46
|
+
ast = Keisan::AST::Builder.new(string: expression).ast
|
47
|
+
|
48
|
+
context = calculator.context.spawn_child(definitions)
|
49
|
+
unbound_variables = ast.unbound_variables(context)
|
50
|
+
unbound_functions = ast.unbound_functions(context)
|
51
|
+
# Ensure the variables are contained within the arguments
|
52
|
+
unless unbound_variables <= Set.new(args) && unbound_functions <= Set.new([name])
|
53
|
+
raise Keisan::Exceptions::InvalidExpression.new("Unbound variables found in function definition")
|
54
|
+
end
|
55
|
+
|
56
|
+
function = lambda do |*received_args|
|
57
|
+
unless args.count == received_args.count
|
58
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Invalid number of arguments for #{name} function")
|
59
|
+
end
|
60
|
+
|
61
|
+
local = calculator.context.spawn_child(definitions)
|
62
|
+
args.each.with_index do |arg, i|
|
63
|
+
local.register_variable!(arg, received_args[i])
|
64
|
+
end
|
65
|
+
|
66
|
+
ast.value(local)
|
67
|
+
end
|
68
|
+
|
69
|
+
calculator.context.register_function!(
|
70
|
+
name,
|
71
|
+
Keisan::Function.new(name, function)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def pure_evaluate(expression, definitions = {})
|
76
|
+
Keisan::AST::Builder.new(string: expression).ast.value(calculator.context.spawn_child(definitions))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/keisan/exceptions.rb
CHANGED