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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -132
  3. data/bin/keisan +13 -2
  4. data/lib/keisan.rb +5 -0
  5. data/lib/keisan/ast.rb +11 -0
  6. data/lib/keisan/ast/assignment.rb +20 -55
  7. data/lib/keisan/ast/boolean.rb +16 -12
  8. data/lib/keisan/ast/cell.rb +17 -0
  9. data/lib/keisan/ast/cell_assignment.rb +70 -0
  10. data/lib/keisan/ast/function_assignment.rb +52 -0
  11. data/lib/keisan/ast/hash.rb +82 -0
  12. data/lib/keisan/ast/indexing.rb +26 -15
  13. data/lib/keisan/ast/line_builder.rb +22 -4
  14. data/lib/keisan/ast/list.rb +14 -7
  15. data/lib/keisan/ast/node.rb +13 -0
  16. data/lib/keisan/ast/null.rb +14 -0
  17. data/lib/keisan/ast/parent.rb +8 -3
  18. data/lib/keisan/ast/string.rb +10 -0
  19. data/lib/keisan/ast/variable.rb +4 -0
  20. data/lib/keisan/ast/variable_assignment.rb +62 -0
  21. data/lib/keisan/context.rb +16 -2
  22. data/lib/keisan/functions/default_registry.rb +4 -0
  23. data/lib/keisan/functions/enumerable_function.rb +56 -0
  24. data/lib/keisan/functions/filter.rb +34 -32
  25. data/lib/keisan/functions/map.rb +25 -31
  26. data/lib/keisan/functions/puts.rb +23 -0
  27. data/lib/keisan/functions/reduce.rb +29 -29
  28. data/lib/keisan/functions/registry.rb +4 -4
  29. data/lib/keisan/functions/sample.rb +5 -3
  30. data/lib/keisan/functions/to_h.rb +34 -0
  31. data/lib/keisan/interpreter.rb +42 -0
  32. data/lib/keisan/parser.rb +59 -50
  33. data/lib/keisan/parsing/compound_assignment.rb +15 -0
  34. data/lib/keisan/parsing/hash.rb +36 -0
  35. data/lib/keisan/repl.rb +1 -1
  36. data/lib/keisan/token.rb +1 -0
  37. data/lib/keisan/tokenizer.rb +23 -19
  38. data/lib/keisan/tokens/assignment.rb +21 -1
  39. data/lib/keisan/tokens/colon.rb +11 -0
  40. data/lib/keisan/tokens/unknown.rb +11 -0
  41. data/lib/keisan/variables/registry.rb +11 -6
  42. data/lib/keisan/version.rb +1 -1
  43. 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
- replacement_node = operator.node_class.new(
217
- children = [@nodes[index-1],@nodes[index+1]],
218
- parsing_operators = [operator]
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)
@@ -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
- super(context)
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 ||= Context.new
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!
@@ -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]
@@ -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
@@ -4,10 +4,10 @@ module Keisan
4
4
  attr_reader :children
5
5
 
6
6
  def initialize(children = [])
7
- children = Array.wrap(children).map(&:to_node)
8
- unless children.is_a?(Array) && children.all? {|children| children.is_a?(Node)}
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
 
@@ -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
@@ -7,6 +7,10 @@ module Keisan
7
7
  @name = name
8
8
  end
9
9
 
10
+ def variable_truthy?(context)
11
+ context.has_variable?(name) && context.variable(name).true?
12
+ end
13
+
10
14
  def value(context = nil)
11
15
  context ||= Context.new
12
16
  variable_node_from_context(context).value(context)
@@ -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
@@ -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&.has_variable?(name))
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&.has_function?(name)
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 < Function
4
- # Filters (list, variable, expression)
5
- # e.g. filter([1,2,3,4], x, x % 2 == 0)
6
- # should give [2,4]
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", 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
- context ||= Context.new
17
- simplify(ast_function, context).evaluate(context)
10
+ super("filter")
18
11
  end
19
12
 
20
- def simplify(ast_function, context = nil)
21
- validate_arguments!(ast_function.children.count)
13
+ private
22
14
 
23
- context ||= Context.new
24
- list, variable, expression = list_variable_expression_for(ast_function, context)
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.evaluate(local)
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
- private
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
- def list_variable_expression_for(ast_function, context)
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
- unless list.is_a?(AST::List)
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
- unless variable.is_a?(AST::Variable)
55
- raise Exceptions::InvalidFunctionError.new("Second argument to filter must be a variable")
56
- end
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
- [list, variable, expression]
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