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
@@ -1,27 +1,22 @@
1
+ require "keisan/functions/enumerable_function"
2
+
1
3
  module Keisan
2
4
  module Functions
3
- class Map < Function
4
- # Maps (list, variable, expression)
5
- # e.g. map([1,2,3], x, 2*x)
6
- # should give [2,4,6]
5
+ class Map < EnumerableFunction
6
+ # Maps
7
+ # (list, variable, expression)
8
+ # (hash, key, value, expression)
7
9
  def initialize
8
- super("map", 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("map")
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("Map 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
 
@@ -33,22 +28,21 @@ module Keisan
33
28
  )
34
29
  end
35
30
 
36
- private
37
-
38
- def list_variable_expression_for(ast_function, context)
39
- list = ast_function.children[0].simplify(context)
40
- variable = ast_function.children[1]
41
- expression = ast_function.children[2]
42
-
43
- unless list.is_a?(AST::List)
44
- raise Exceptions::InvalidFunctionError.new("First argument to map must be a list")
31
+ def evaluate_hash(hash, arguments, expression, context)
32
+ unless arguments.count == 2
33
+ raise Exceptions::InvalidFunctionError.new("Map on hash must take 4 arguments")
45
34
  end
35
+ key, value = arguments[0..1]
46
36
 
47
- unless variable.is_a?(AST::Variable)
48
- raise Exceptions::InvalidFunctionError.new("Second argument to map must be a variable")
49
- end
37
+ local = context.spawn_child(transient: false, shadowed: [key.name, value.name])
50
38
 
51
- [list, variable, expression]
39
+ AST::List.new(
40
+ hash.map do |cur_key, cur_value|
41
+ local.register_variable!(key, cur_key)
42
+ local.register_variable!(value, cur_value)
43
+ expression.simplified(local)
44
+ end
45
+ )
52
46
  end
53
47
  end
54
48
  end
@@ -0,0 +1,23 @@
1
+ module Keisan
2
+ module Functions
3
+ class Puts < Function
4
+ def initialize
5
+ super("puts", 1)
6
+ end
7
+
8
+ def value(ast_function, context = nil)
9
+ evaluate(ast_function, context)
10
+ end
11
+
12
+ def evaluate(ast_function, context = nil)
13
+ validate_arguments!(ast_function.children.count)
14
+ puts ast_function.children.first.evaluate(context).to_s
15
+ Keisan::AST::Null.new
16
+ end
17
+
18
+ def simplify(ast_function, context = nil)
19
+ evaluate(ast_function, context)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,27 +1,31 @@
1
+ require "keisan/functions/enumerable_function"
2
+
1
3
  module Keisan
2
4
  module Functions
3
- class Reduce < Function
5
+ class Reduce < EnumerableFunction
4
6
  # Reduces (list, initial, accumulator, variable, expression)
5
7
  # e.g. reduce([1,2,3,4], 0, total, x, total+x)
6
8
  # should give 10
7
9
  def initialize
8
- super("reduce", 5)
10
+ super("reduce")
9
11
  end
10
12
 
11
- def value(ast_function, context = nil)
12
- evaluate(ast_function, context)
13
- end
13
+ protected
14
14
 
15
- def evaluate(ast_function, context = nil)
16
- context ||= Context.new
17
- simplify(ast_function, context).evaluate(context)
15
+ def verify_arguments!(arguments)
16
+ unless arguments[1..-1].all? {|argument| argument.is_a?(AST::Variable)}
17
+ raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
18
+ end
18
19
  end
19
20
 
20
- def simplify(ast_function, context = nil)
21
- validate_arguments!(ast_function.children.count)
21
+ private
22
+
23
+ def evaluate_list(list, arguments, expression, context)
24
+ unless arguments.count == 3
25
+ raise Exceptions::InvalidFunctionError.new("Reduce on list must take 3 arguments")
26
+ end
22
27
 
23
- context ||= Context.new
24
- list, initial, accumulator, variable, expression = list_initial_accumulator_variable_expression_for(ast_function, context)
28
+ initial, accumulator, variable = arguments[0...3]
25
29
 
26
30
  local = context.spawn_child(transient: false, shadowed: [accumulator.name, variable.name])
27
31
  local.register_variable!(accumulator, initial.simplify(context))
@@ -35,28 +39,24 @@ module Keisan
35
39
  local.variable(accumulator.name)
36
40
  end
37
41
 
38
- private
39
-
40
- def list_initial_accumulator_variable_expression_for(ast_function, context)
41
- list = ast_function.children[0].simplify(context)
42
- initial = ast_function.children[1]
43
- accumulator = ast_function.children[2]
44
- variable = ast_function.children[3]
45
- expression = ast_function.children[4]
46
-
47
- unless list.is_a?(AST::List)
48
- raise Exceptions::InvalidFunctionError.new("First argument to reduce must be a list")
42
+ def evaluate_hash(hash, arguments, expression, context)
43
+ unless arguments.count == 4
44
+ raise Exceptions::InvalidFunctionError.new("Reduce on list must take 3 arguments")
49
45
  end
50
46
 
51
- unless accumulator.is_a?(AST::Variable)
52
- raise Exceptions::InvalidFunctionError.new("Third argument to reduce is accumulator and must be a variable")
53
- end
47
+ initial, accumulator, key, value = arguments[0...4]
48
+
49
+ local = context.spawn_child(transient: false, shadowed: [accumulator.name, key.name, value.name])
50
+ local.register_variable!(accumulator, initial.simplify(context))
54
51
 
55
- unless variable.is_a?(AST::Variable)
56
- raise Exceptions::InvalidFunctionError.new("Fourth argument to reduce is variable and must be a variable")
52
+ hash.each do |cur_key, cur_value|
53
+ local.register_variable!(key, cur_key)
54
+ local.register_variable!(value, cur_value)
55
+ result = expression.simplified(local)
56
+ local.register_variable!(accumulator, result)
57
57
  end
58
58
 
59
- [list, initial, accumulator, variable, expression]
59
+ local.variable(accumulator.name)
60
60
  end
61
61
  end
62
62
  end
@@ -32,14 +32,14 @@ module Keisan
32
32
  false
33
33
  end
34
34
 
35
+ def modifiable?(name)
36
+ !frozen? && has?(name)
37
+ end
38
+
35
39
  def register!(name, function, force: false)
36
40
  raise Exceptions::UnmodifiableError.new("Cannot modify frozen functions registry") if frozen?
37
41
  name = name.to_s
38
42
 
39
- if !force && @use_defaults && default_registry.has_name?(name)
40
- raise Exceptions::UnmodifiableError.new("Cannot overwrite default function")
41
- end
42
-
43
43
  case function
44
44
  when Proc
45
45
  self[name] = ProcFunction.new(name, function)
@@ -3,15 +3,17 @@ module Keisan
3
3
  class Sample < ProcFunction
4
4
  def initialize
5
5
  @name = "sample"
6
- @arity = 1
6
+ @arity = ::Range.new(1, 2)
7
7
  end
8
8
 
9
- # Single argument: integer in range [0, max)
10
- # Double argument: integer in range [min, max)
9
+ # Single argument: list to sample element from
10
+ # Double argument: list and number of elements to sample
11
11
  def call(context, *args)
12
12
  case args.size
13
13
  when 1
14
14
  args.first.sample(random: context.random)
15
+ when 2
16
+ args[0].sample(args[1], random: context.random)
15
17
  else
16
18
  raise Exceptions::InvalidFunctionError.new
17
19
  end
@@ -0,0 +1,34 @@
1
+ module Keisan
2
+ module Functions
3
+ class ToH < Function
4
+ def initialize
5
+ super("to_h", 1)
6
+ end
7
+
8
+ def value(ast_function, context = nil)
9
+ validate_arguments!(ast_function.children.count)
10
+ evaluate(ast_function, context).value(context)
11
+ end
12
+
13
+ def evaluate(ast_function, context = nil)
14
+ validate_arguments!(ast_function.children.count)
15
+ context ||= Context.new
16
+
17
+ child = ast_function.children[0].simplify(context)
18
+
19
+ case child
20
+ when AST::List
21
+ AST::Hash.new(child.children)
22
+ when AST::Hash
23
+ child
24
+ else
25
+ raise Exceptions::InvalidFunctionError.new("Cannot call to_h on a #{child.class}")
26
+ end
27
+ end
28
+
29
+ def simplify(ast_function, context = nil)
30
+ evaluate(ast_function, context)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ require "keisan/repl"
2
+
3
+ module Keisan
4
+ class Interpreter
5
+ attr_reader :calculator
6
+
7
+ def initialize(allow_recursive: false)
8
+ @calculator = Calculator.new(allow_recursive: allow_recursive)
9
+ end
10
+
11
+ def run(file_name)
12
+ if file_name.nil?
13
+ run_from_stdin
14
+ else
15
+ run_from_file(file_name)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def run_from_stdin
22
+ run_on_content STDIN.tty? ? "" : STDIN.read
23
+ end
24
+
25
+ def run_from_file(file_name)
26
+ run_on_content(
27
+ File.exists?(file_name) ? File.open(file_name) do |file|
28
+ file.read
29
+ end : ""
30
+ )
31
+ end
32
+
33
+ def run_on_content(content)
34
+ content = content.strip
35
+ if content.nil? || content.empty?
36
+ Repl.new.start
37
+ else
38
+ calculator.evaluate(content)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,6 +1,6 @@
1
1
  module Keisan
2
2
  class Parser
3
- KEYWORDS = %w(let).freeze
3
+ KEYWORDS = %w(let puts).freeze
4
4
 
5
5
  attr_reader :tokens, :components
6
6
 
@@ -119,7 +119,9 @@ module Keisan
119
119
  elsif token.type == :operator
120
120
  add_operator_to_components!(token)
121
121
  else
122
- raise Exceptions::ParseError.new("Expected an operator, received #{token.string}")
122
+ # Concatenation is multiplication
123
+ @components << Parsing::Times.new
124
+ add_token_to_components!(token)
123
125
  end
124
126
 
125
127
  elsif @components[-1].is_a?(Parsing::Dot)
@@ -196,18 +198,26 @@ module Keisan
196
198
  when Tokens::Boolean
197
199
  @components << Parsing::Boolean.new(token.value)
198
200
  when Tokens::Group
199
- case token.group_type
200
- when :round
201
- @components << Parsing::RoundGroup.new(token.sub_tokens)
202
- when :square
203
- @components << Parsing::List.new(arguments_from_group(token))
204
- when :curly
205
- @components << Parsing::CurlyGroup.new(token.sub_tokens)
201
+ add_group_element_components!(token)
202
+ else
203
+ raise Exceptions::ParseError.new("Unhandled operator type #{token.operator_type}")
204
+ end
205
+ end
206
+
207
+ def add_group_element_components!(token)
208
+ case token.group_type
209
+ when :round
210
+ @components << Parsing::RoundGroup.new(token.sub_tokens)
211
+ when :square
212
+ @components << Parsing::List.new(arguments_from_group(token))
213
+ when :curly
214
+ if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)}
215
+ @components << Parsing::Hash.new(token.sub_tokens.split {|token| token.is_a?(Tokens::Comma)})
206
216
  else
207
- raise Exceptions::ParseError.new("Unhandled group type #{token.group_type}")
217
+ @components << Parsing::CurlyGroup.new(token.sub_tokens)
208
218
  end
209
219
  else
210
- raise Exceptions::ParseError.new("Unhandled operator type #{token.operator_type}")
220
+ raise Exceptions::ParseError.new("Unhandled group type #{token.group_type}")
211
221
  end
212
222
  end
213
223
 
@@ -215,46 +225,45 @@ module Keisan
215
225
  case token.operator_type
216
226
  # Assignment
217
227
  when :"="
218
- @components << Parsing::Assignment.new
219
- # Arithmetic
220
- when :+
221
- @components << Parsing::Plus.new
222
- when :-
223
- @components << Parsing::Minus.new
224
- when :*
225
- @components << Parsing::Times.new
226
- when :/
227
- @components << Parsing::Divide.new
228
- when :**
229
- @components << Parsing::Exponent.new
230
- when :%
231
- @components << Parsing::Modulo.new
232
- # Bitwise
233
- when :"&"
234
- @components << Parsing::BitwiseAnd.new
235
- when :"|"
236
- @components << Parsing::BitwiseOr.new
237
- when :"^"
238
- @components << Parsing::BitwiseXor.new
239
- # Logical
240
- when :"=="
241
- @components << Parsing::LogicalEqual.new
242
- when :"!="
243
- @components << Parsing::LogicalNotEqual.new
244
- when :"&&"
245
- @components << Parsing::LogicalAnd.new
246
- when :"||"
247
- @components << Parsing::LogicalOr.new
248
- when :">"
249
- @components << Parsing::LogicalGreaterThan.new
250
- when :"<"
251
- @components << Parsing::LogicalLessThan.new
252
- when :">="
253
- @components << Parsing::LogicalGreaterThanOrEqualTo.new
254
- when :"<="
255
- @components << Parsing::LogicalLessThanOrEqualTo.new
228
+ add_assignment_to_components!(token)
256
229
  else
257
- raise Exceptions::ParseError.new("Unhandled operator type #{token.operator_type}")
230
+ @components << operator_to_component(token.operator_type)
231
+ end
232
+ end
233
+
234
+ OPERATOR_TO_PARSING_CLASS = {
235
+ :+ => Parsing::Plus,
236
+ :- => Parsing::Minus,
237
+ :* => Parsing::Times,
238
+ :/ => Parsing::Divide,
239
+ :** => Parsing::Exponent,
240
+ :% => Parsing::Modulo,
241
+ :"&" => Parsing::BitwiseAnd,
242
+ :"|" => Parsing::BitwiseOr,
243
+ :"^" => Parsing::BitwiseXor,
244
+ :"==" => Parsing::LogicalEqual,
245
+ :"!=" => Parsing::LogicalNotEqual,
246
+ :"&&" => Parsing::LogicalAnd,
247
+ :"||" => Parsing::LogicalOr,
248
+ :">" => Parsing::LogicalGreaterThan,
249
+ :"<" => Parsing::LogicalLessThan,
250
+ :">=" => Parsing::LogicalGreaterThanOrEqualTo,
251
+ :"<=" => Parsing::LogicalLessThanOrEqualTo
252
+ }.freeze
253
+
254
+ def operator_to_component(operator)
255
+ if klass = OPERATOR_TO_PARSING_CLASS[operator]
256
+ klass.new
257
+ else
258
+ raise Exceptions::ParseError.new("Unhandled operator type #{operator}")
259
+ end
260
+ end
261
+
262
+ def add_assignment_to_components!(token)
263
+ if compound_operator = token.compound_operator
264
+ @components << Parsing::CompoundAssignment.new(compound_operator)
265
+ else
266
+ @components << Parsing::Assignment.new
258
267
  end
259
268
  end
260
269
 
@@ -0,0 +1,15 @@
1
+ module Keisan
2
+ module Parsing
3
+ class CompoundAssignment < Operator
4
+ attr_reader :compound_operator
5
+
6
+ def initialize(compound_operator)
7
+ @compound_operator = compound_operator
8
+ end
9
+
10
+ def node_class
11
+ AST::Assignment
12
+ end
13
+ end
14
+ end
15
+ end