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
@@ -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