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/functions/map.rb
CHANGED
@@ -1,27 +1,22 @@
|
|
1
|
+
require "keisan/functions/enumerable_function"
|
2
|
+
|
1
3
|
module Keisan
|
2
4
|
module Functions
|
3
|
-
class Map <
|
4
|
-
# Maps
|
5
|
-
#
|
6
|
-
#
|
5
|
+
class Map < EnumerableFunction
|
6
|
+
# Maps
|
7
|
+
# (list, variable, expression)
|
8
|
+
# (hash, key, value, expression)
|
7
9
|
def initialize
|
8
|
-
super("map"
|
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
|
-
|
21
|
-
validate_arguments!(ast_function.children.count)
|
13
|
+
private
|
22
14
|
|
23
|
-
|
24
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
-
|
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 <
|
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"
|
10
|
+
super("reduce")
|
9
11
|
end
|
10
12
|
|
11
|
-
|
12
|
-
evaluate(ast_function, context)
|
13
|
-
end
|
13
|
+
protected
|
14
14
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
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:
|
10
|
-
# Double argument:
|
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
|
data/lib/keisan/parser.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
217
|
+
@components << Parsing::CurlyGroup.new(token.sub_tokens)
|
208
218
|
end
|
209
219
|
else
|
210
|
-
raise Exceptions::ParseError.new("Unhandled
|
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
|
-
|
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
|
-
|
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
|