keisan 0.3.0 → 0.4.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 +57 -9
- data/lib/keisan.rb +21 -12
- data/lib/keisan/ast.rb +50 -0
- data/lib/keisan/ast/arithmetic_operator.rb +0 -3
- data/lib/keisan/ast/assignment.rb +72 -0
- data/lib/keisan/ast/bitwise_and.rb +4 -4
- data/lib/keisan/ast/bitwise_operator.rb +0 -3
- data/lib/keisan/ast/bitwise_or.rb +4 -4
- data/lib/keisan/ast/bitwise_xor.rb +4 -4
- data/lib/keisan/ast/boolean.rb +25 -1
- data/lib/keisan/ast/builder.rb +98 -63
- data/lib/keisan/ast/constant_literal.rb +13 -0
- data/lib/keisan/ast/exponent.rb +62 -8
- data/lib/keisan/ast/function.rb +37 -26
- data/lib/keisan/ast/functions/diff.rb +57 -0
- data/lib/keisan/ast/functions/if.rb +47 -0
- data/lib/keisan/ast/indexing.rb +44 -4
- data/lib/keisan/ast/list.rb +4 -0
- data/lib/keisan/ast/literal.rb +8 -0
- data/lib/keisan/ast/logical_and.rb +9 -4
- data/lib/keisan/ast/logical_equal.rb +4 -4
- 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 +4 -4
- data/lib/keisan/ast/logical_operator.rb +0 -3
- data/lib/keisan/ast/logical_or.rb +10 -5
- data/lib/keisan/ast/modulo.rb +4 -4
- data/lib/keisan/ast/node.rb +132 -20
- data/lib/keisan/ast/null.rb +1 -1
- data/lib/keisan/ast/number.rb +172 -1
- data/lib/keisan/ast/operator.rb +66 -8
- data/lib/keisan/ast/parent.rb +50 -0
- data/lib/keisan/ast/plus.rb +38 -4
- data/lib/keisan/ast/string.rb +10 -1
- data/lib/keisan/ast/times.rb +47 -4
- data/lib/keisan/ast/unary_bitwise_not.rb +9 -1
- data/lib/keisan/ast/unary_identity.rb +18 -1
- data/lib/keisan/ast/unary_inverse.rb +35 -1
- data/lib/keisan/ast/unary_logical_not.rb +5 -1
- data/lib/keisan/ast/unary_minus.rb +31 -1
- data/lib/keisan/ast/unary_operator.rb +29 -1
- data/lib/keisan/ast/unary_plus.rb +14 -1
- data/lib/keisan/ast/variable.rb +44 -0
- data/lib/keisan/calculator.rb +2 -2
- data/lib/keisan/context.rb +32 -16
- data/lib/keisan/evaluator.rb +10 -65
- data/lib/keisan/exceptions.rb +2 -0
- data/lib/keisan/function.rb +11 -5
- data/lib/keisan/function_definition_context.rb +34 -0
- data/lib/keisan/functions/default_registry.rb +13 -5
- data/lib/keisan/functions/diff.rb +82 -0
- data/lib/keisan/functions/expression_function.rb +63 -0
- data/lib/keisan/functions/if.rb +62 -0
- data/lib/keisan/functions/proc_function.rb +52 -0
- data/lib/keisan/functions/rand.rb +1 -1
- data/lib/keisan/functions/registry.rb +20 -10
- data/lib/keisan/functions/replace.rb +49 -0
- data/lib/keisan/functions/sample.rb +1 -1
- data/lib/keisan/parser.rb +13 -1
- data/lib/keisan/parsing/assignment.rb +9 -0
- data/lib/keisan/parsing/operator.rb +8 -0
- data/lib/keisan/parsing/unary_operator.rb +1 -1
- data/lib/keisan/tokenizer.rb +1 -0
- data/lib/keisan/tokens/assignment.rb +15 -0
- data/lib/keisan/variables/default_registry.rb +4 -4
- data/lib/keisan/variables/registry.rb +17 -8
- data/lib/keisan/version.rb +1 -1
- metadata +15 -3
- data/lib/keisan/ast/priorities.rb +0 -27
@@ -2,7 +2,15 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryBitwiseNot < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return ~
|
5
|
+
return ~child.value(context)
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(context = nil)
|
9
|
+
~child.evaluate(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.symbol
|
13
|
+
:"~"
|
6
14
|
end
|
7
15
|
end
|
8
16
|
end
|
@@ -2,7 +2,24 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryIdentity < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return
|
5
|
+
return child.value(context)
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(context = nil)
|
9
|
+
child.evaluate(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.symbol
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def simplify(context = nil)
|
17
|
+
child.simplify(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def differentiate(variable, context = nil)
|
21
|
+
context ||= Context.new
|
22
|
+
child.differentiate(variable, context).simplify(context)
|
6
23
|
end
|
7
24
|
end
|
8
25
|
end
|
@@ -2,7 +2,41 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryInverse < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return Rational(1,
|
5
|
+
return Rational(1, child.value(context))
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
"(#{child.to_s})**(-1)"
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(context = nil)
|
13
|
+
1.to_node / child.evaluate(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def simplify(context = nil)
|
17
|
+
context ||= Context.new
|
18
|
+
|
19
|
+
@children = [child.simplify(context)]
|
20
|
+
case child
|
21
|
+
when AST::Number
|
22
|
+
AST::Number.new(Rational(1,child.value(context))).simplify(context)
|
23
|
+
else
|
24
|
+
(child ** -1).simplify(context)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def differentiate(variable, context = nil)
|
29
|
+
context ||= Context.new
|
30
|
+
AST::Times.new(
|
31
|
+
[
|
32
|
+
AST::UnaryMinus.new(child.differentiate(variable, context)),
|
33
|
+
AST::UnaryInverse.new(
|
34
|
+
AST::Exponent.new([
|
35
|
+
child.deep_dup, AST::Number.new(2)
|
36
|
+
])
|
37
|
+
)
|
38
|
+
]
|
39
|
+
).simplify(context)
|
6
40
|
end
|
7
41
|
end
|
8
42
|
end
|
@@ -2,7 +2,37 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryMinus < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return -1 *
|
5
|
+
return -1 * child.value(context)
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(context = nil)
|
9
|
+
-child.evaluate(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.symbol
|
13
|
+
:"-"
|
14
|
+
end
|
15
|
+
|
16
|
+
def simplify(context = nil)
|
17
|
+
context ||= Context.new
|
18
|
+
|
19
|
+
case child
|
20
|
+
when AST::Number
|
21
|
+
AST::Number.new(-child.value(context)).simplify(context)
|
22
|
+
else
|
23
|
+
AST::Times.new([
|
24
|
+
AST::Number.new(-1),
|
25
|
+
child
|
26
|
+
]).simplify(context)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def differentiate(variable, context = nil)
|
31
|
+
context ||= Context.new
|
32
|
+
AST::Times.new([
|
33
|
+
-1.to_node,
|
34
|
+
child.differentiate(variable, context)
|
35
|
+
]).simplify(context)
|
6
36
|
end
|
7
37
|
end
|
8
38
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
|
-
class UnaryOperator <
|
3
|
+
class UnaryOperator < Operator
|
4
4
|
def initialize(children = [])
|
5
5
|
children = Array.wrap(children)
|
6
6
|
super
|
@@ -8,6 +8,34 @@ module Keisan
|
|
8
8
|
raise Keisan::Exceptions::ASTError.new("Unary operator takes has a single child")
|
9
9
|
end
|
10
10
|
end
|
11
|
+
|
12
|
+
def child
|
13
|
+
children.first
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.arity
|
17
|
+
ARITIES[:"u#{symbol}"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.priority
|
21
|
+
PRIORITIES[:"u#{symbol}"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.associativity
|
25
|
+
ASSOCIATIVITIES[:"u#{symbol}"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
if child.is_a?(AST::Operator)
|
30
|
+
"#{symbol.to_s}(#{child.to_s})"
|
31
|
+
else
|
32
|
+
"#{symbol.to_s}#{child.to_s}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def simplify(context = nil)
|
37
|
+
self
|
38
|
+
end
|
11
39
|
end
|
12
40
|
end
|
13
41
|
end
|
@@ -1,9 +1,22 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
|
-
class UnaryPlus <
|
3
|
+
class UnaryPlus < UnaryIdentity
|
4
4
|
def value(context = nil)
|
5
5
|
return children.first.value(context)
|
6
6
|
end
|
7
|
+
|
8
|
+
def self.symbol
|
9
|
+
:"+"
|
10
|
+
end
|
11
|
+
|
12
|
+
def simplify(context = nil)
|
13
|
+
case child
|
14
|
+
when AST::Number
|
15
|
+
AST::Number.new(child.value(context)).simplify(context)
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
7
20
|
end
|
8
21
|
end
|
9
22
|
end
|
data/lib/keisan/ast/variable.rb
CHANGED
@@ -16,6 +16,50 @@ module Keisan
|
|
16
16
|
context ||= Keisan::Context.new
|
17
17
|
context.has_variable?(name) ? Set.new : Set.new([name])
|
18
18
|
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
name == other.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
name.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def evaluate(context = nil)
|
29
|
+
context ||= Keisan::Context.new
|
30
|
+
if context.has_variable?(name)
|
31
|
+
context.variable(name).to_node.evaluate(context)
|
32
|
+
else
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def simplify(context = nil)
|
38
|
+
context ||= Keisan::Context.new
|
39
|
+
if context.has_variable?(name)
|
40
|
+
context.variable(name).to_node.simplify(context)
|
41
|
+
else
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def replace(variable, replacement)
|
47
|
+
if name == variable.name
|
48
|
+
replacement
|
49
|
+
else
|
50
|
+
self
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def differentiate(variable, context = nil)
|
55
|
+
context ||= Keisan::Context.new
|
56
|
+
|
57
|
+
if name == variable.name && !context.has_variable?(name)
|
58
|
+
1.to_node
|
59
|
+
else
|
60
|
+
0.to_node
|
61
|
+
end
|
62
|
+
end
|
19
63
|
end
|
20
64
|
end
|
21
65
|
end
|
data/lib/keisan/calculator.rb
CHANGED
@@ -2,8 +2,8 @@ module Keisan
|
|
2
2
|
class Calculator
|
3
3
|
attr_reader :context
|
4
4
|
|
5
|
-
def initialize(context
|
6
|
-
@context = context || Context.new
|
5
|
+
def initialize(context: nil, allow_recursive: false)
|
6
|
+
@context = context || Context.new(allow_recursive: allow_recursive)
|
7
7
|
end
|
8
8
|
|
9
9
|
def evaluate(expression, definitions = {})
|
data/lib/keisan/context.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Context
|
3
|
-
attr_reader :function_registry, :variable_registry
|
3
|
+
attr_reader :function_registry, :variable_registry, :allow_recursive
|
4
4
|
|
5
|
-
def initialize(parent: nil, random: nil)
|
5
|
+
def initialize(parent: nil, random: nil, allow_recursive: false)
|
6
6
|
@parent = parent
|
7
7
|
@function_registry = Functions::Registry.new(parent: @parent.try(:function_registry))
|
8
8
|
@variable_registry = Variables::Registry.new(parent: @parent.try(:variable_registry))
|
9
9
|
@random = random
|
10
|
+
@allow_recursive = allow_recursive
|
10
11
|
end
|
11
12
|
|
12
|
-
def spawn_child(definitions
|
13
|
-
child =
|
13
|
+
def spawn_child(definitions: {}, transient: false)
|
14
|
+
child = Context.new(parent: self, allow_recursive: allow_recursive)
|
14
15
|
|
15
16
|
definitions.each do |name, value|
|
16
17
|
case value
|
@@ -21,35 +22,50 @@ module Keisan
|
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
25
|
+
child.set_transient! if transient
|
24
26
|
child
|
25
27
|
end
|
26
28
|
|
27
|
-
def
|
28
|
-
@
|
29
|
+
def variable(name)
|
30
|
+
@variable_registry[name.to_s]
|
29
31
|
end
|
30
32
|
|
31
|
-
def
|
32
|
-
@
|
33
|
+
def has_variable?(name)
|
34
|
+
@variable_registry.has?(name)
|
33
35
|
end
|
34
36
|
|
35
|
-
def
|
36
|
-
@
|
37
|
+
def register_variable!(name, value)
|
38
|
+
if @transient
|
39
|
+
@parent.register_variable!(name, value)
|
40
|
+
else
|
41
|
+
@variable_registry.register!(name.to_s, value)
|
42
|
+
end
|
37
43
|
end
|
38
44
|
|
39
|
-
def
|
40
|
-
@
|
45
|
+
def function(name)
|
46
|
+
@function_registry[name.to_s]
|
41
47
|
end
|
42
48
|
|
43
|
-
def
|
44
|
-
@
|
49
|
+
def has_function?(name)
|
50
|
+
@function_registry.has?(name)
|
45
51
|
end
|
46
52
|
|
47
|
-
def
|
48
|
-
@
|
53
|
+
def register_function!(name, function)
|
54
|
+
if @transient
|
55
|
+
@parent.register_function!(name, function)
|
56
|
+
else
|
57
|
+
@function_registry.register!(name.to_s, function)
|
58
|
+
end
|
49
59
|
end
|
50
60
|
|
51
61
|
def random
|
52
62
|
@random || @parent.try(:random) || Random.new
|
53
63
|
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def set_transient!
|
68
|
+
@transient = true
|
69
|
+
end
|
54
70
|
end
|
55
71
|
end
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
module Keisan
|
2
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
3
|
attr_reader :calculator
|
8
4
|
|
9
5
|
def initialize(calculator)
|
@@ -11,69 +7,18 @@ module Keisan
|
|
11
7
|
end
|
12
8
|
|
13
9
|
def evaluate(expression, definitions = {})
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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])
|
10
|
+
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
11
|
+
ast = Keisan::AST.parse(expression)
|
12
|
+
evaluation = ast.evaluate(context)
|
13
|
+
|
14
|
+
case ast
|
15
|
+
when Keisan::AST::Assignment
|
16
|
+
if ast.children.first.is_a?(Keisan::AST::Variable)
|
17
|
+
context.variable(ast.children.first.name)
|
64
18
|
end
|
65
|
-
|
66
|
-
|
19
|
+
else
|
20
|
+
evaluation.value(context)
|
67
21
|
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
22
|
end
|
78
23
|
end
|
79
24
|
end
|
data/lib/keisan/exceptions.rb
CHANGED
@@ -16,5 +16,7 @@ module Keisan
|
|
16
16
|
class UndefinedVariableError < StandardError; end
|
17
17
|
class UnmodifiableError < StandardError; end
|
18
18
|
class InvalidExpression < StandardError; end
|
19
|
+
class TypeError < StandardError; end
|
20
|
+
class NonDifferentiableError < StandardError; end
|
19
21
|
end
|
20
22
|
end
|