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
data/lib/keisan/ast.rb
CHANGED
@@ -66,9 +66,20 @@ module KeisanArray
|
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
+
module KeisanHash
|
70
|
+
def to_node
|
71
|
+
Keisan::AST::Hash.new(map {|k,v| [k.to_node, v.to_node]})
|
72
|
+
end
|
73
|
+
|
74
|
+
def value(context = nil)
|
75
|
+
self
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
69
79
|
class Numeric; prepend KeisanNumeric; end
|
70
80
|
class String; prepend KeisanString; end
|
71
81
|
class TrueClass; prepend KeisanTrueClass; end
|
72
82
|
class FalseClass; prepend KeisanFalseClass; end
|
73
83
|
class NilClass; prepend KeisanNilClass; end
|
74
84
|
class Array; prepend KeisanArray; end
|
85
|
+
class Hash; prepend KeisanHash; end
|
@@ -1,17 +1,26 @@
|
|
1
|
+
require_relative "variable_assignment"
|
2
|
+
require_relative "function_assignment"
|
3
|
+
require_relative "cell_assignment"
|
4
|
+
|
1
5
|
module Keisan
|
2
6
|
module AST
|
3
7
|
class Assignment < Operator
|
4
|
-
attr_reader :local
|
8
|
+
attr_reader :local, :compound_operator
|
5
9
|
|
6
|
-
def initialize(children = [], parsing_operators = [], local: false)
|
10
|
+
def initialize(children = [], parsing_operators = [], local: false, compound_operator: nil)
|
7
11
|
super(children, parsing_operators)
|
8
12
|
@local = local
|
13
|
+
@compound_operator = compound_operator
|
9
14
|
end
|
10
15
|
|
11
16
|
def self.symbol
|
12
17
|
:"="
|
13
18
|
end
|
14
19
|
|
20
|
+
def symbol
|
21
|
+
:"#{compound_operator}="
|
22
|
+
end
|
23
|
+
|
15
24
|
def evaluate(context = nil)
|
16
25
|
context ||= Context.new
|
17
26
|
|
@@ -19,9 +28,9 @@ module Keisan
|
|
19
28
|
rhs = children.last
|
20
29
|
|
21
30
|
if is_variable_definition?
|
22
|
-
|
31
|
+
evaluate_variable_assignment(context, lhs, rhs)
|
23
32
|
elsif is_function_definition?
|
24
|
-
|
33
|
+
evaluate_function_assignment(context, lhs, rhs)
|
25
34
|
else
|
26
35
|
# Try cell assignment
|
27
36
|
evaluate_cell_assignment(context, lhs, rhs)
|
@@ -64,61 +73,17 @@ module Keisan
|
|
64
73
|
|
65
74
|
private
|
66
75
|
|
67
|
-
def
|
68
|
-
|
69
|
-
unless lhs.is_a?(Cell)
|
70
|
-
raise Exceptions::InvalidExpression.new("Unhandled left hand side #{lhs} in assignment")
|
71
|
-
end
|
72
|
-
|
73
|
-
rhs = rhs.evaluate(context)
|
74
|
-
|
75
|
-
lhs.node = rhs
|
76
|
-
rhs
|
76
|
+
def evaluate_variable_assignment(context, lhs, rhs)
|
77
|
+
VariableAssignment.new(self, context, lhs, rhs).evaluate
|
77
78
|
end
|
78
79
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
82
|
-
unless rhs.well_defined?
|
83
|
-
raise Exceptions::InvalidExpression.new("Right hand side of assignment to variable must be well defined")
|
84
|
-
end
|
85
|
-
|
86
|
-
rhs_value = rhs.value(context)
|
87
|
-
context.register_variable!(lhs.name, rhs_value, local: local)
|
88
|
-
# Return the variable assigned value
|
89
|
-
rhs
|
80
|
+
def evaluate_function_assignment(context, lhs, rhs)
|
81
|
+
raise Exceptions::InvalidExpression.new("Cannot do compound assignment on functions") if compound_operator
|
82
|
+
FunctionAssignment.new(context, lhs, rhs, local).evaluate
|
90
83
|
end
|
91
84
|
|
92
|
-
def
|
93
|
-
|
94
|
-
raise Exceptions::InvalidExpression.new("Left hand side function must have variables as arguments")
|
95
|
-
end
|
96
|
-
|
97
|
-
argument_names = lhs.children.map(&:name)
|
98
|
-
function_definition_context = context.spawn_child(shadowed: argument_names, transient: true)
|
99
|
-
|
100
|
-
# Blocks might have local variable/function definitions
|
101
|
-
if !rhs.is_a?(Block)
|
102
|
-
unless rhs.unbound_variables(context) <= Set.new(argument_names)
|
103
|
-
raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
|
104
|
-
end
|
105
|
-
unless context.allow_recursive || rhs.unbound_functions(context).empty?
|
106
|
-
raise Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
context.register_function!(
|
111
|
-
lhs.name,
|
112
|
-
Functions::ExpressionFunction.new(
|
113
|
-
lhs.name,
|
114
|
-
argument_names,
|
115
|
-
rhs.evaluate_assignments(function_definition_context),
|
116
|
-
context.transient_definitions
|
117
|
-
),
|
118
|
-
local: local
|
119
|
-
)
|
120
|
-
|
121
|
-
rhs
|
85
|
+
def evaluate_cell_assignment(context, lhs, rhs)
|
86
|
+
CellAssignment.new(self, context, lhs, rhs).evaluate
|
122
87
|
end
|
123
88
|
end
|
124
89
|
end
|
data/lib/keisan/ast/boolean.rb
CHANGED
@@ -11,28 +11,32 @@ module Keisan
|
|
11
11
|
bool
|
12
12
|
end
|
13
13
|
|
14
|
+
def true?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
14
18
|
def !
|
15
19
|
Boolean.new(!bool)
|
16
20
|
end
|
17
21
|
|
18
22
|
def and(other)
|
19
23
|
other = other.to_node
|
20
|
-
|
21
|
-
when Boolean
|
22
|
-
Boolean.new(bool && other.bool)
|
23
|
-
else
|
24
|
-
super
|
25
|
-
end
|
24
|
+
other.is_a?(Boolean) ? Boolean.new(bool && other.bool) : super
|
26
25
|
end
|
27
26
|
|
28
27
|
def or(other)
|
29
28
|
other = other.to_node
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
other.is_a?(Boolean) ? Boolean.new(bool || other.bool) : super
|
30
|
+
end
|
31
|
+
|
32
|
+
def equal(other)
|
33
|
+
other = other.to_node
|
34
|
+
other.is_a?(Boolean) ? Boolean.new(value == other.value) : super
|
35
|
+
end
|
36
|
+
|
37
|
+
def not_equal(other)
|
38
|
+
other = other.to_node
|
39
|
+
other.is_a?(Boolean) ? Boolean.new(value != other.value) : super
|
36
40
|
end
|
37
41
|
end
|
38
42
|
end
|
data/lib/keisan/ast/cell.rb
CHANGED
@@ -24,10 +24,23 @@ module Keisan
|
|
24
24
|
dupped
|
25
25
|
end
|
26
26
|
|
27
|
+
def freeze
|
28
|
+
node.freeze
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
27
32
|
def value(context = nil)
|
28
33
|
node.value(context)
|
29
34
|
end
|
30
35
|
|
36
|
+
def true?
|
37
|
+
node.true?
|
38
|
+
end
|
39
|
+
|
40
|
+
def false?
|
41
|
+
node.false?
|
42
|
+
end
|
43
|
+
|
31
44
|
def evaluate(context = nil)
|
32
45
|
node.evaluate(context)
|
33
46
|
end
|
@@ -48,6 +61,10 @@ module Keisan
|
|
48
61
|
node.replace(variable, replacement)
|
49
62
|
end
|
50
63
|
|
64
|
+
def to_cell
|
65
|
+
self.class.new(node.to_cell)
|
66
|
+
end
|
67
|
+
|
51
68
|
def to_s
|
52
69
|
node.to_s
|
53
70
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class CellAssignment
|
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
|
+
lhs = lhs_evaluate_and_check_modifiable
|
15
|
+
|
16
|
+
unless lhs.is_a?(Cell)
|
17
|
+
raise Exceptions::InvalidExpression.new("Unhandled left hand side #{lhs} in assignment")
|
18
|
+
end
|
19
|
+
|
20
|
+
case assignment.compound_operator
|
21
|
+
when :"||"
|
22
|
+
evaluate_cell_or_assignment(context, lhs, rhs)
|
23
|
+
when :"&&"
|
24
|
+
evaluate_cell_and_assignment(context, lhs, rhs)
|
25
|
+
else
|
26
|
+
evaluate_cell_non_logical_assignment(context, lhs, rhs)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def lhs_evaluate_and_check_modifiable
|
33
|
+
lhs.evaluate(context)
|
34
|
+
rescue RuntimeError => e
|
35
|
+
raise Exceptions::UnmodifiableError.new("Cannot modify frozen variables") if e.message =~ /can't modify frozen/
|
36
|
+
raise
|
37
|
+
end
|
38
|
+
|
39
|
+
def evaluate_cell_or_assignment(context, lhs, rhs)
|
40
|
+
if lhs.false?
|
41
|
+
rhs = rhs.evaluate(context)
|
42
|
+
lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
|
43
|
+
rhs
|
44
|
+
else
|
45
|
+
lhs
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def evaluate_cell_and_assignment(context, lhs, rhs)
|
50
|
+
if lhs.true?
|
51
|
+
rhs = rhs.evaluate(context)
|
52
|
+
lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
|
53
|
+
rhs
|
54
|
+
else
|
55
|
+
lhs
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def evaluate_cell_non_logical_assignment(context, lhs, rhs)
|
60
|
+
rhs = rhs.evaluate(context)
|
61
|
+
if assignment.compound_operator
|
62
|
+
rhs = rhs.send(assignment.compound_operator, lhs.node).evaluate(context)
|
63
|
+
end
|
64
|
+
|
65
|
+
lhs.node = rhs.is_a?(Cell) ? rhs.node.deep_dup : rhs
|
66
|
+
rhs
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class FunctionAssignment
|
4
|
+
attr_reader :context, :lhs, :rhs, :local
|
5
|
+
|
6
|
+
def initialize(context, lhs, rhs, local)
|
7
|
+
@context = context
|
8
|
+
@lhs = lhs
|
9
|
+
@rhs = rhs
|
10
|
+
@local = local
|
11
|
+
|
12
|
+
unless lhs.children.all? {|arg| arg.is_a?(Variable)}
|
13
|
+
raise Exceptions::InvalidExpression.new("Left hand side function must have variables as arguments")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def argument_names
|
18
|
+
@argument_names ||= lhs.children.map(&:name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def expression_function
|
22
|
+
Functions::ExpressionFunction.new(
|
23
|
+
lhs.name,
|
24
|
+
argument_names,
|
25
|
+
rhs.evaluate_assignments(context.spawn_child(shadowed: argument_names, transient: true)),
|
26
|
+
context.transient_definitions
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def evaluate
|
31
|
+
# Blocks might have local variable/function definitions, so skip check
|
32
|
+
verify_rhs_of_function_assignment_is_valid! unless rhs.is_a?(Block)
|
33
|
+
|
34
|
+
context.register_function!(lhs.name, expression_function, local: local)
|
35
|
+
rhs
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def verify_rhs_of_function_assignment_is_valid!
|
41
|
+
# Only variables that can appear are those that are arguments to the function
|
42
|
+
unless rhs.unbound_variables(context) <= Set.new(argument_names)
|
43
|
+
raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
|
44
|
+
end
|
45
|
+
# Cannot have undefined functions unless allowed by context
|
46
|
+
unless context.allow_recursive || rhs.unbound_functions(context).empty?
|
47
|
+
raise Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
class Hash < Node
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(key_value_pairs)
|
7
|
+
@hash = ::Hash[key_value_pairs.map(&:to_a).map {|k,v| [k.value, v.to_node]}]
|
8
|
+
end
|
9
|
+
|
10
|
+
def freeze
|
11
|
+
values.each(&:freeze)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
key = key.to_node
|
17
|
+
return nil unless key.is_a?(AST::ConstantLiteral)
|
18
|
+
|
19
|
+
@hash[key.value] || Cell.new(Null.new).tap do |cell|
|
20
|
+
@hash[key.value] = cell
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def evaluate(context = nil)
|
25
|
+
context ||= Context.new
|
26
|
+
|
27
|
+
@hash = ::Hash[
|
28
|
+
@hash.map do |key, val|
|
29
|
+
if val.is_a?(Cell)
|
30
|
+
[key, val]
|
31
|
+
else
|
32
|
+
[key, val.evaluate(context)]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
]
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def simplify(context = nil)
|
41
|
+
evaluate(context)
|
42
|
+
end
|
43
|
+
|
44
|
+
def each(&block)
|
45
|
+
@hash.each(&block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def keys
|
49
|
+
@hash.keys
|
50
|
+
end
|
51
|
+
|
52
|
+
def values
|
53
|
+
@hash.values
|
54
|
+
end
|
55
|
+
|
56
|
+
def value(context = nil)
|
57
|
+
context ||= Context.new
|
58
|
+
evaluate(context)
|
59
|
+
|
60
|
+
::Hash[
|
61
|
+
@hash.map {|key, val|
|
62
|
+
[key, val.value(context)]
|
63
|
+
}
|
64
|
+
]
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
"{#{@hash.map {|k,v| "#{k.is_a?(::String) ? "'#{k}'" : k}: #{v}"}.join(', ')}}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_cell
|
72
|
+
h = self.class.new([])
|
73
|
+
h.instance_variable_set(:@hash, ::Hash[
|
74
|
+
@hash.map do |key, value|
|
75
|
+
[key, value.to_cell]
|
76
|
+
end
|
77
|
+
])
|
78
|
+
AST::Cell.new(h)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/keisan/ast/indexing.rb
CHANGED
@@ -25,24 +25,11 @@ module Keisan
|
|
25
25
|
@children = children.map {|child| child.evaluate(context)}
|
26
26
|
@indexes = indexes.map {|index| index.evaluate(context)}
|
27
27
|
|
28
|
-
|
29
|
-
list.children[@indexes.first.value(context)]
|
30
|
-
else
|
31
|
-
self
|
32
|
-
end
|
28
|
+
evaluate_list(context) || evaluate_hash(context) || self
|
33
29
|
end
|
34
30
|
|
35
31
|
def simplify(context = nil)
|
36
|
-
context
|
37
|
-
|
38
|
-
@indexes = indexes.map {|index| index.simplify(context)}
|
39
|
-
@children = [child.simplify(context)]
|
40
|
-
|
41
|
-
if list = extract_list
|
42
|
-
Cell.new(list.children[@indexes.first.value(context)].simplify(context))
|
43
|
-
else
|
44
|
-
self
|
45
|
-
end
|
32
|
+
evaluate(context)
|
46
33
|
end
|
47
34
|
|
48
35
|
def replace(variable, replacement)
|
@@ -52,6 +39,20 @@ module Keisan
|
|
52
39
|
|
53
40
|
private
|
54
41
|
|
42
|
+
def evaluate_list(context)
|
43
|
+
if list = extract_list
|
44
|
+
element = list.children[@indexes.first.value(context)]
|
45
|
+
element.nil? ? AST::Null.new : element
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def evaluate_hash(context)
|
50
|
+
if hash = extract_hash
|
51
|
+
element = hash[@indexes.first.value(context)]
|
52
|
+
element.nil? ? AST::Null.new : element
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
55
56
|
def extract_list
|
56
57
|
if child.is_a?(List)
|
57
58
|
child
|
@@ -61,6 +62,16 @@ module Keisan
|
|
61
62
|
nil
|
62
63
|
end
|
63
64
|
end
|
65
|
+
|
66
|
+
def extract_hash
|
67
|
+
if child.is_a?(AST::Hash)
|
68
|
+
child
|
69
|
+
elsif child.is_a?(Cell) && child.node.is_a?(AST::Hash)
|
70
|
+
child.node
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
64
75
|
end
|
65
76
|
end
|
66
77
|
end
|