keisan 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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