keisan 0.8.2 → 0.8.7
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/.github/workflows/ruby.yml +28 -0
- data/README.md +16 -0
- data/lib/keisan/ast/block.rb +4 -0
- data/lib/keisan/ast/cell.rb +4 -0
- data/lib/keisan/ast/function_assignment.rb +16 -6
- data/lib/keisan/ast/hash.rb +4 -0
- data/lib/keisan/ast/node.rb +9 -0
- data/lib/keisan/ast/parent.rb +4 -0
- data/lib/keisan/calculator.rb +17 -3
- data/lib/keisan/context.rb +26 -5
- data/lib/keisan/evaluator.rb +16 -4
- data/lib/keisan/functions/enumerable_function.rb +7 -4
- data/lib/keisan/string_and_group_parser.rb +49 -7
- data/lib/keisan/tokenizer.rb +5 -16
- data/lib/keisan/version.rb +1 -1
- metadata +4 -4
- data/.travis.yml +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f7bc1aae204b2b2e112ca0c8f2567023684ddbec83cd1c5c4c1a354cf27eb75b
|
4
|
+
data.tar.gz: 6353f1b48be09dbbef2cf30167b9fe2d0ac9400fb728def8c75a2241294196e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fa93fed5b5fd0e55c0330e017e1628b3a3c4dc2b9fcdb2dbbf521b6e7149c9f2ee44544cb1f3cdbc84d09d171fb794f4d6a3a22997b5d8728607ea07542d3ee
|
7
|
+
data.tar.gz: 471b9f1378dab5e0dae5556ed693e00bd73c2e841f463486b5f7e013867dd1f05b52e675c381607b5f564d84ee0cfd1a4c69a49805f338d7823798bed7f94bff
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
strategy:
|
15
|
+
matrix:
|
16
|
+
ruby-version: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0]
|
17
|
+
|
18
|
+
steps:
|
19
|
+
- uses: actions/checkout@v2
|
20
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
21
|
+
uses: ruby/setup-ruby@v1
|
22
|
+
with:
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
24
|
+
bundler-cache: true
|
25
|
+
- name: Install dependencies
|
26
|
+
run: bundle install
|
27
|
+
- name: Run tests
|
28
|
+
run: bundle exec rspec
|
data/README.md
CHANGED
@@ -178,6 +178,22 @@ calculator.evaluate("x = 11; {let x = 12}; x")
|
|
178
178
|
#=> 11
|
179
179
|
```
|
180
180
|
|
181
|
+
##### Comments
|
182
|
+
|
183
|
+
When working with multi-line blocks of code, sometimes comments are useful to include.
|
184
|
+
Comments are parts of a string from the `#` character to the end of a line (indicated by a newline character `"\n"`).
|
185
|
+
|
186
|
+
```
|
187
|
+
calculator = Keisan::Calculator.new
|
188
|
+
calculator.evaluate("""
|
189
|
+
# This is a comment
|
190
|
+
x = 'foo'
|
191
|
+
x += '#bar' # Notice that `#` inside strings is not part of the comment
|
192
|
+
x # Should print 'foo#bar'
|
193
|
+
""")
|
194
|
+
#=> "foo#bar"
|
195
|
+
```
|
196
|
+
|
181
197
|
##### Lists
|
182
198
|
|
183
199
|
Just like in Ruby, lists can be defined using square brackets, and indexed using square brackets
|
data/lib/keisan/ast/block.rb
CHANGED
data/lib/keisan/ast/cell.rb
CHANGED
@@ -29,8 +29,7 @@ module Keisan
|
|
29
29
|
|
30
30
|
def evaluate
|
31
31
|
# Blocks might have local variable/function definitions, so skip check
|
32
|
-
verify_rhs_of_function_assignment_is_valid!
|
33
|
-
|
32
|
+
verify_rhs_of_function_assignment_is_valid!
|
34
33
|
context.register_function!(lhs.name, expression_function, local: local)
|
35
34
|
rhs
|
36
35
|
end
|
@@ -38,15 +37,26 @@ module Keisan
|
|
38
37
|
private
|
39
38
|
|
40
39
|
def verify_rhs_of_function_assignment_is_valid!
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
verify_unbound_functions!
|
41
|
+
verify_unbound_variables!
|
42
|
+
end
|
43
|
+
|
44
|
+
def verify_unbound_functions!
|
45
45
|
# Cannot have undefined functions unless allowed by context
|
46
46
|
unless context.allow_recursive || rhs.unbound_functions(context).empty?
|
47
47
|
raise Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
|
48
48
|
end
|
49
49
|
end
|
50
|
+
|
51
|
+
def verify_unbound_variables!
|
52
|
+
# We allow unbound variables inside block statements, as they could be temporary
|
53
|
+
# variables assigned locally
|
54
|
+
return if rhs.is_a?(Block)
|
55
|
+
# Only variables that can appear are those that are arguments to the function
|
56
|
+
unless rhs.unbound_variables(context) <= Set.new(argument_names)
|
57
|
+
raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
|
58
|
+
end
|
59
|
+
end
|
50
60
|
end
|
51
61
|
end
|
52
62
|
end
|
data/lib/keisan/ast/hash.rb
CHANGED
data/lib/keisan/ast/node.rb
CHANGED
data/lib/keisan/ast/parent.rb
CHANGED
data/lib/keisan/calculator.rb
CHANGED
@@ -2,8 +2,14 @@ module Keisan
|
|
2
2
|
class Calculator
|
3
3
|
attr_reader :context
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# Note, allow_recursive would be more appropriately named:
|
6
|
+
# allow_unbound_functions_in_function_definitions, but it is too late for that.
|
7
|
+
def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true)
|
8
|
+
@context = context || Context.new(
|
9
|
+
allow_recursive: allow_recursive,
|
10
|
+
allow_blocks: allow_blocks,
|
11
|
+
allow_multiline: allow_multiline
|
12
|
+
)
|
7
13
|
end
|
8
14
|
|
9
15
|
def allow_recursive
|
@@ -14,6 +20,14 @@ module Keisan
|
|
14
20
|
context.allow_recursive!
|
15
21
|
end
|
16
22
|
|
23
|
+
def allow_blocks
|
24
|
+
context.allow_blocks
|
25
|
+
end
|
26
|
+
|
27
|
+
def allow_multiline
|
28
|
+
context.allow_multiline
|
29
|
+
end
|
30
|
+
|
17
31
|
def evaluate(expression, definitions = {})
|
18
32
|
Evaluator.new(self).evaluate(expression, definitions)
|
19
33
|
end
|
@@ -23,7 +37,7 @@ module Keisan
|
|
23
37
|
end
|
24
38
|
|
25
39
|
def ast(expression)
|
26
|
-
Evaluator.new(self).
|
40
|
+
Evaluator.new(self).parse_ast(expression)
|
27
41
|
end
|
28
42
|
|
29
43
|
def define_variable!(name, value)
|
data/lib/keisan/context.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Context
|
3
|
-
attr_reader :function_registry,
|
4
|
-
|
5
|
-
|
3
|
+
attr_reader :function_registry,
|
4
|
+
:variable_registry,
|
5
|
+
:allow_recursive,
|
6
|
+
:allow_multiline,
|
7
|
+
:allow_blocks
|
8
|
+
|
9
|
+
def initialize(parent: nil,
|
10
|
+
random: nil,
|
11
|
+
allow_recursive: false,
|
12
|
+
allow_multiline: true,
|
13
|
+
allow_blocks: true,
|
14
|
+
shadowed: [])
|
6
15
|
@parent = parent
|
7
16
|
@function_registry = Functions::Registry.new(parent: @parent&.function_registry)
|
8
17
|
@variable_registry = Variables::Registry.new(parent: @parent&.variable_registry, shadowed: shadowed)
|
9
18
|
@random = random
|
10
19
|
@allow_recursive = allow_recursive
|
20
|
+
@allow_multiline = allow_multiline
|
21
|
+
@allow_blocks = allow_blocks
|
11
22
|
end
|
12
23
|
|
13
24
|
def allow_recursive!
|
@@ -100,7 +111,11 @@ module Keisan
|
|
100
111
|
end
|
101
112
|
|
102
113
|
def random
|
103
|
-
@random
|
114
|
+
@random ||= @parent&.random || Random.new
|
115
|
+
end
|
116
|
+
|
117
|
+
def set_random(random)
|
118
|
+
@random = random
|
104
119
|
end
|
105
120
|
|
106
121
|
protected
|
@@ -110,7 +125,13 @@ module Keisan
|
|
110
125
|
end
|
111
126
|
|
112
127
|
def pure_child(shadowed: [])
|
113
|
-
self.class.new(
|
128
|
+
self.class.new(
|
129
|
+
parent: self,
|
130
|
+
shadowed: shadowed,
|
131
|
+
allow_recursive: allow_recursive,
|
132
|
+
allow_multiline: allow_multiline,
|
133
|
+
allow_blocks: allow_blocks
|
134
|
+
)
|
114
135
|
end
|
115
136
|
end
|
116
137
|
end
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -8,7 +8,7 @@ module Keisan
|
|
8
8
|
|
9
9
|
def evaluate(expression, definitions = {})
|
10
10
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
11
|
-
ast =
|
11
|
+
ast = parse_ast(expression)
|
12
12
|
last_line = last_line(ast)
|
13
13
|
|
14
14
|
evaluation = ast.evaluated(context)
|
@@ -24,16 +24,28 @@ module Keisan
|
|
24
24
|
|
25
25
|
def simplify(expression, definitions = {})
|
26
26
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
27
|
-
ast =
|
27
|
+
ast = parse_ast(expression)
|
28
28
|
ast.simplify(context)
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
AST.parse(expression)
|
31
|
+
def parse_ast(expression)
|
32
|
+
AST.parse(expression).tap do |ast|
|
33
|
+
disallowed = disallowed_nodes
|
34
|
+
if !disallowed.empty? && ast.contains_a?(disallowed)
|
35
|
+
raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
|
36
|
+
end
|
37
|
+
end
|
33
38
|
end
|
34
39
|
|
35
40
|
private
|
36
41
|
|
42
|
+
def disallowed_nodes
|
43
|
+
disallowed = []
|
44
|
+
disallowed << Keisan::AST::Block unless calculator.allow_blocks
|
45
|
+
disallowed << Keisan::AST::MultiLine unless calculator.allow_multiline
|
46
|
+
disallowed
|
47
|
+
end
|
48
|
+
|
37
49
|
def last_line(ast)
|
38
50
|
ast.is_a?(AST::MultiLine) ? ast.children.last : ast
|
39
51
|
end
|
@@ -21,14 +21,17 @@ module Keisan
|
|
21
21
|
context ||= Context.new
|
22
22
|
|
23
23
|
operand, arguments, expression = operand_arguments_expression_for(ast_function, context)
|
24
|
+
|
25
|
+
# Extract underlying operand for cells
|
26
|
+
real_operand = operand.is_a?(AST::Cell) ? operand.node : operand
|
24
27
|
|
25
|
-
case
|
28
|
+
case real_operand
|
26
29
|
when AST::List
|
27
|
-
evaluate_list(
|
30
|
+
evaluate_list(real_operand, arguments, expression, context).evaluate(context)
|
28
31
|
when AST::Hash
|
29
|
-
evaluate_hash(
|
32
|
+
evaluate_hash(real_operand, arguments, expression, context).evaluate(context)
|
30
33
|
else
|
31
|
-
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{
|
34
|
+
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{real_operand}")
|
32
35
|
end
|
33
36
|
end
|
34
37
|
|
@@ -9,24 +9,26 @@ module Keisan
|
|
9
9
|
end
|
10
10
|
|
11
11
|
class StringPortion < Portion
|
12
|
-
attr_reader :string
|
12
|
+
attr_reader :string, :escaped_string
|
13
13
|
|
14
14
|
def initialize(expression, start_index)
|
15
15
|
super(start_index)
|
16
16
|
|
17
17
|
@string = expression[start_index]
|
18
|
+
@escaped_string = expression[start_index]
|
18
19
|
@end_index = start_index + 1
|
19
20
|
|
20
21
|
while @end_index < expression.size
|
21
22
|
if expression[@end_index] == quote_type
|
22
23
|
@string << quote_type
|
24
|
+
@escaped_string << quote_type
|
23
25
|
@end_index += 1
|
24
26
|
# Successfully parsed the string
|
25
27
|
return
|
26
28
|
end
|
27
29
|
|
28
|
-
n, c =
|
29
|
-
@
|
30
|
+
n, c = get_potentially_escaped_next_character(expression, @end_index)
|
31
|
+
@escaped_string << c
|
30
32
|
@end_index += n
|
31
33
|
end
|
32
34
|
|
@@ -44,9 +46,13 @@ module Keisan
|
|
44
46
|
private
|
45
47
|
|
46
48
|
# Returns number of processed input characters, and the output character
|
47
|
-
|
48
|
-
|
49
|
-
|
49
|
+
# If a sequence like '\"' is encountered, the first backslash escapes the
|
50
|
+
# second double-quote, and the two characters will act as a one double-quote
|
51
|
+
# character.
|
52
|
+
def get_potentially_escaped_next_character(expression, index)
|
53
|
+
@string << expression[index]
|
54
|
+
if expression[index] == "\\" && index + 1 < expression.size
|
55
|
+
@string << expression[index + 1]
|
50
56
|
return [2, escaped_character(expression[index + 1])]
|
51
57
|
else
|
52
58
|
return [1, expression[index]]
|
@@ -129,7 +135,7 @@ module Keisan
|
|
129
135
|
|
130
136
|
while index < expression.size
|
131
137
|
case expression[index]
|
132
|
-
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX
|
138
|
+
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX, COMMENT_CHARACTER_REGEX
|
133
139
|
break
|
134
140
|
else
|
135
141
|
index += 1
|
@@ -149,9 +155,40 @@ module Keisan
|
|
149
155
|
end
|
150
156
|
end
|
151
157
|
|
158
|
+
class CommentPortion < Portion
|
159
|
+
attr_reader :string
|
160
|
+
|
161
|
+
def initialize(expression, start_index)
|
162
|
+
super(start_index)
|
163
|
+
|
164
|
+
if expression[start_index] != '#'
|
165
|
+
raise Keisan::Exceptions::TokenizingError.new("Comment should start with '#'")
|
166
|
+
else
|
167
|
+
index = start_index + 1
|
168
|
+
end
|
169
|
+
|
170
|
+
while index < expression.size
|
171
|
+
break if expression[index] == "\n"
|
172
|
+
index += 1
|
173
|
+
end
|
174
|
+
|
175
|
+
@end_index = index
|
176
|
+
@string = expression[start_index...end_index]
|
177
|
+
end
|
178
|
+
|
179
|
+
def size
|
180
|
+
string.size
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_s
|
184
|
+
string
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
152
188
|
# An ordered array of "portions", which
|
153
189
|
attr_reader :portions, :size
|
154
190
|
|
191
|
+
COMMENT_CHARACTER_REGEX = /[#]/
|
155
192
|
STRING_CHARACTER_REGEX = /["']/
|
156
193
|
OPEN_GROUP_REGEX = /[\(\{\[]/
|
157
194
|
CLOSED_GROUP_REGEX = /[\)\}\]]/
|
@@ -176,6 +213,11 @@ module Keisan
|
|
176
213
|
when CLOSED_GROUP_REGEX
|
177
214
|
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unexpected closing brace #{expression[start_index]}")
|
178
215
|
|
216
|
+
when COMMENT_CHARACTER_REGEX
|
217
|
+
portion = CommentPortion.new(expression, index)
|
218
|
+
index += portion.size
|
219
|
+
@portions << portion
|
220
|
+
|
179
221
|
else
|
180
222
|
portion = OtherPortion.new(expression, index)
|
181
223
|
index += portion.size
|
data/lib/keisan/tokenizer.rb
CHANGED
@@ -24,14 +24,16 @@ module Keisan
|
|
24
24
|
attr_reader :expression, :tokens
|
25
25
|
|
26
26
|
def initialize(expression)
|
27
|
-
@expression =
|
27
|
+
@expression = expression
|
28
28
|
|
29
|
-
portions = StringAndGroupParser.new(
|
29
|
+
portions = StringAndGroupParser.new(expression).portions.reject do |portion|
|
30
|
+
portion.is_a? StringAndGroupParser::CommentPortion
|
31
|
+
end
|
30
32
|
|
31
33
|
@tokens = portions.inject([]) do |tokens, portion|
|
32
34
|
case portion
|
33
35
|
when StringAndGroupParser::StringPortion
|
34
|
-
tokens << Tokens::String.new(portion.
|
36
|
+
tokens << Tokens::String.new(portion.escaped_string)
|
35
37
|
when StringAndGroupParser::GroupPortion
|
36
38
|
tokens << Tokens::Group.new(portion.to_s)
|
37
39
|
when StringAndGroupParser::OtherPortion
|
@@ -43,21 +45,8 @@ module Keisan
|
|
43
45
|
end
|
44
46
|
end
|
45
47
|
|
46
|
-
def self.normalize_expression(expression)
|
47
|
-
expression = normalize_line_delimiters(expression)
|
48
|
-
expression = remove_comments(expression)
|
49
|
-
end
|
50
|
-
|
51
48
|
private
|
52
49
|
|
53
|
-
def self.normalize_line_delimiters(expression)
|
54
|
-
expression.gsub(/\n/, ";")
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.remove_comments(expression)
|
58
|
-
expression.gsub(/#[^;]*/, "")
|
59
|
-
end
|
60
|
-
|
61
50
|
def tokenize!(scan)
|
62
51
|
scan.map do |scan_result|
|
63
52
|
i = scan_result.find_index {|token| !token.nil?}
|
data/lib/keisan/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keisan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Locke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmath
|
@@ -115,9 +115,9 @@ executables: []
|
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
|
+
- ".github/workflows/ruby.yml"
|
118
119
|
- ".gitignore"
|
119
120
|
- ".rspec"
|
120
|
-
- ".travis.yml"
|
121
121
|
- Gemfile
|
122
122
|
- MIT-LICENSE
|
123
123
|
- README.md
|
@@ -328,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
328
328
|
- !ruby/object:Gem::Version
|
329
329
|
version: '0'
|
330
330
|
requirements: []
|
331
|
-
rubygems_version: 3.
|
331
|
+
rubygems_version: 3.2.15
|
332
332
|
signing_key:
|
333
333
|
specification_version: 4
|
334
334
|
summary: An equation parser and evaluator
|