quecto_calc 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20adbf781b1eb596029581cca4444cb87ee04d5c9e5b821027789486f6f38b65
4
- data.tar.gz: 1acc1509de74d7f7f5681bd86c24d90433dd8bfc43706600c9d60bfadff78290
3
+ metadata.gz: 44bae8aee9c0431cfd4ea0b75aefe914bdc55f3e0ea4565c889ad608c7f67b64
4
+ data.tar.gz: 8a2a5b6e5e59304c184e2aa0eea67b36fdb9a90ccfa23fd19f71065a241fb135
5
5
  SHA512:
6
- metadata.gz: f25a9e6c5a2630528ff65aa83f46043ab1d0076dfe8b1c617cea9dfe23af4f9ea966f50b276e08c13236e1ccc715b6d9e06eb973d6bc090167609b3e23b136a9
7
- data.tar.gz: bdaac5eb72f9ace46f008759c07037b396af76e29e6984e6c7d6e6310b24e6437b77d0e48e134ddab417a128a07fd078b78ddcdecfa61a3055d2f4c245f48151
6
+ metadata.gz: 5decd805cf29de3744f18b161ea4cd0557df6ee6886f6a7f68527ce791083640ce8a29f92e4f5a3e0985bf19c21cf2dd4a4297c58f06ce9da1d8bfecbe6d1cb1
7
+ data.tar.gz: 78025741efc21e5957ccbab21ca3fab3963ddb47eb50b6e08f9c116d289b8d20d995f7357e9113141af9829fed8bd9419d871b787c688eae3d2567dbe5e5e178
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ### 0.2.0 / 2023-02-20
2
+
3
+ * Lexer/parser separated in a it's own gem 'quecto_parser';
4
+ * Bug fixes.
5
+
1
6
  ### 0.1.1 / 2023-02-19
2
7
 
3
8
  * Added constants interface.
data/Gemfile CHANGED
@@ -10,3 +10,5 @@ gem "rake", "~> 13.0"
10
10
  gem "minitest", "~> 5.0"
11
11
 
12
12
  gem "rubocop", "~> 0.80"
13
+
14
+ gem "quecto_parser", "~> 0.1.0"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quecto_calc (0.1.0)
4
+ quecto_calc (0.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,14 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "quecto_error"
4
- require_relative "token_types"
3
+ require_relative "quecto_calc_error"
5
4
 
6
5
  #
7
6
  # Evaluates expression from the abstract syntax tree.
8
7
  #
9
8
  class Evaluator
10
- include TokenTypes
11
-
12
9
  #
13
10
  # Initialize an evaluator instance.
14
11
  #
@@ -20,10 +17,27 @@ class Evaluator
20
17
  end
21
18
 
22
19
  #
23
- # @param [BinOpNode, NumberNode] node
20
+ # @param [QuectoParser::BinOpNode, QuectoParser::NumberNode] node
24
21
  #
25
22
  def visit(node)
26
- send("_visit_#{node.class}", node)
23
+ method_name = "_visit_#{node.class}"
24
+ send(method_name, node)
25
+ end
26
+
27
+ #
28
+ # @param [String] _method_name
29
+ #
30
+ # @param [Object] node
31
+ #
32
+ # @raise [NoMethodError]
33
+ #
34
+ def method_missing(_method_name, node)
35
+ error_msg = "unsupported node type: #{node.class}. Plase check parser's output for correct node types."
36
+ raise NoMethodError, error_msg
37
+ end
38
+
39
+ def respond_to_missing?(*)
40
+ true
27
41
  end
28
42
 
29
43
  private
@@ -31,7 +45,7 @@ class Evaluator
31
45
  #
32
46
  # Retrieve result of a binary operation node.
33
47
  #
34
- # @param [BinOpNode] node
48
+ # @param [QuectoParser::BinOpNode] node
35
49
  #
36
50
  # @return [Numeric]
37
51
  # Result of the binary operation.
@@ -41,9 +55,9 @@ class Evaluator
41
55
  right = visit(node.right_node)
42
56
 
43
57
  case node.operator.type
44
- when TT_PLUS
58
+ when TokenTypes::TT_PLUS
45
59
  left + right
46
- when TT_MINUS
60
+ when TokenTypes::TT_MINUS
47
61
  left - right
48
62
  end
49
63
  end
@@ -51,15 +65,15 @@ class Evaluator
51
65
  #
52
66
  # Retrieve value of a number node.
53
67
  #
54
- # @param [NumberNode] node
68
+ # @param [QuectoParser::NumberNode] node
55
69
  #
56
70
  # @return [Numeric]
57
71
  #
58
72
  def _visit_NumberNode(node)
59
73
  case node.token.type
60
- when TT_INT
74
+ when TokenTypes::TT_INT
61
75
  node.token.value
62
- when TT_CONST
76
+ when TokenTypes::TT_CONST
63
77
  _init_constant(node)
64
78
  end
65
79
  end
@@ -67,12 +81,12 @@ class Evaluator
67
81
  #
68
82
  # Replace constant with an associated value.
69
83
  #
70
- # @param [NumberNode] node
84
+ # @param [QuectoParser::NumberNode] node
71
85
  # A numeric node with @token of TT_CONST type.
72
86
  #
73
87
  # @return [Numeric]
74
88
  #
75
- # @raise [CalcError]
89
+ # @raise [CalcError::RunTimeError]
76
90
  # Raises if constant is not found in the @consts.
77
91
  #
78
92
  def _init_constant(node)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Errors rised by the quecto_calc.
5
+ #
6
+ class QuectoCalcError < ::StandardError
7
+ attr_reader :message
8
+
9
+ def initialize(message)
10
+ @message = message
11
+ super()
12
+ end
13
+ end
14
+
15
+ #
16
+ # Raised by the evaluator on an illegal operation.
17
+ #
18
+ class CalcError < QuectoCalcError
19
+ attr_reader :message
20
+
21
+ def initialize(message = "")
22
+ super(message)
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class QuectoCalc
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/quecto_calc.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "quecto_parser"
4
+
3
5
  require_relative "quecto_calc/evaluator"
4
- require_relative "quecto_calc/const_interface"
5
- require_relative "quecto_calc/lexer"
6
- require_relative "quecto_calc/parser"
7
6
  require_relative "quecto_calc/version"
8
7
 
9
8
  #
@@ -21,47 +20,26 @@ require_relative "quecto_calc/version"
21
20
  # @ https://github.com/davidcallanan/py-myopl-code
22
21
  #
23
22
  class QuectoCalc
24
- include ConstInterface
25
-
26
23
  #
27
24
  # Evaluate an expression.
28
25
  #
29
26
  # @param [String] expr
30
- # Expression to parse and evaluate.
27
+ # Expression (in a text form) to parse and evaluate.
31
28
  #
32
29
  # @option [Hash{ String => Numeric }] consts
33
30
  # List on constants and their values to put in the expression.
34
31
  #
35
32
  def evaluate(expr, consts = {})
36
- tokens = build_tokens(expr)
37
- ast = build_ast(tokens)
38
- evaluate_ast(ast, consts)
39
- rescue QuectoError => e
40
- puts "#{e.error_name} #{e.message}"
41
- end
42
-
43
- #
44
- # Get a list of tokens from the string.
45
- #
46
- # @param [String] expr
47
- #
48
- def build_tokens(expr)
49
- Lexer.new(expr).build_tokens
50
- end
33
+ parser = QuectoParser.new
51
34
 
52
- #
53
- # Build an abstract syntax tree from a list of tokens.
54
- #
55
- # @param [Array<Token>] tokens
56
- #
57
- def build_ast(tokens)
58
- Parser.new(tokens).parse
35
+ ast = parser.parse_expr(expr)
36
+ evaluate_ast(ast, consts) if ast
59
37
  end
60
38
 
61
39
  #
62
40
  # Evaluate expression from the AST.
63
41
  #
64
- # @param [NumberNode, BinOpNode] ast
42
+ # @param [QuectoParser::NumberNode, QuectoParser::BinOpNode] ast
65
43
  # Expression (in a form of AST) to evaluate.
66
44
  #
67
45
  # @option [Hash{ String => Numeric }] consts
@@ -70,5 +48,7 @@ class QuectoCalc
70
48
  def evaluate_ast(ast, consts = {})
71
49
  evaluator = Evaluator.new(consts)
72
50
  evaluator.visit(ast)
51
+ rescue QuectoCalcError, NoMethodError => e
52
+ puts "[#{e.class}] #{e.message}"
73
53
  end
74
54
  end
data/quecto_calc.gemspec CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.require_paths = ["lib"]
31
31
 
32
32
  # Uncomment to register a new dependency of your gem
33
- # spec.add_dependency "example-gem", "~> 1.0"
33
+ spec.add_dependency "quecto_parser", "~> 0.1.0"
34
34
 
35
35
  # For more information and examples about making a new gem, checkout our
36
36
  # guide at: https://bundler.io/guides/creating_gem.html
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quecto_calc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mate
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-19 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2023-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: quecto_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.0
13
27
  description: Evaluates primitive arithmetic expressions represented in a text form.
14
28
  email:
15
29
  executables: []
@@ -27,16 +41,8 @@ files:
27
41
  - bin/console
28
42
  - bin/setup
29
43
  - lib/quecto_calc.rb
30
- - lib/quecto_calc/bin_op_node.rb
31
- - lib/quecto_calc/char_rules.rb
32
- - lib/quecto_calc/const_interface.rb
33
44
  - lib/quecto_calc/evaluator.rb
34
- - lib/quecto_calc/lexer.rb
35
- - lib/quecto_calc/number_node.rb
36
- - lib/quecto_calc/parser.rb
37
- - lib/quecto_calc/quecto_error.rb
38
- - lib/quecto_calc/token.rb
39
- - lib/quecto_calc/token_types.rb
45
+ - lib/quecto_calc/quecto_calc_error.rb
40
46
  - lib/quecto_calc/version.rb
41
47
  - quecto_calc.gemspec
42
48
  homepage: https://github.com/8bit-mate/quecto_calc.rb/
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Stores a node with a binary operation (between two numbers).
5
- #
6
- class BinOpNode
7
- attr_reader :left_node, :operator, :right_node
8
-
9
- def initialize(left_node, operator, right_node)
10
- @left_node = left_node
11
- @operator = operator
12
- @right_node = right_node
13
- end
14
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Provides character rules that are used by the lexer.
5
- #
6
- module CharRules
7
- NUM_CHAR = /[[:digit:]]/.freeze # characters that could form a number: digits only
8
- CONST_FIRST_CHAR = /[[:alpha:]]/.freeze # characters that could be used as a 1st char. in the constant name
9
- CONTS_CHAR = /[[:alpha:][:digit:]_]/.freeze # characters that could form a constant: letters, digits, underscore char.
10
-
11
- ADD_CHAR = "+" # character that marks an addition operator
12
- SUB_CHAR = "-" # character that marks an subtraction operator
13
-
14
- IGNOR_CHAR = /\s/.freeze # characters that are ignored: all whitespace
15
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "token_types"
4
-
5
- #
6
- # Provides an interface to retrieve token types constants to the outer world.
7
- #
8
- module ConstInterface
9
- include TokenTypes
10
-
11
- def tt_int
12
- TT_INT
13
- end
14
-
15
- def tt_const
16
- TT_CONST
17
- end
18
-
19
- def tt_plus
20
- TT_PLUS
21
- end
22
-
23
- def tt_minus
24
- TT_MINUS
25
- end
26
-
27
- def tt_eof
28
- TT_EOF
29
- end
30
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "char_rules"
4
- require_relative "quecto_error"
5
- require_relative "token"
6
- require_relative "token_types"
7
-
8
- #
9
- # Performs lexical analysis of a given string.
10
- #
11
- class Lexer
12
- include TokenTypes
13
- include CharRules
14
-
15
- #
16
- # Initialize a lexer instance.
17
- #
18
- # @param [String] str
19
- # String to parse.
20
- #
21
- def initialize(str)
22
- @str = str
23
- @pos = 0
24
- @cur_char = nil
25
-
26
- _next_char
27
- end
28
-
29
- #
30
- # Create a list of tokens from a string.
31
- #
32
- # @return [Array<Token>] tokens
33
- #
34
- # @raise [IllegalCharError]
35
- # Raises when @str has an unsupported character.
36
- #
37
- def build_tokens
38
- tokens = []
39
-
40
- until @cur_char.nil?
41
- if @cur_char == ADD_CHAR
42
- tokens.append(Token.new(type: TT_PLUS))
43
- elsif @cur_char == SUB_CHAR
44
- tokens.append(Token.new(type: TT_MINUS))
45
- elsif @cur_char.match?(NUM_CHAR)
46
- num = Token.new(type: TT_INT, value: _build_word(NUM_CHAR).to_i)
47
- tokens.append(num)
48
- elsif @cur_char.match?(CONST_FIRST_CHAR)
49
- num = Token.new(type: TT_CONST, value: _build_word(CONTS_CHAR))
50
- tokens.append(num)
51
- elsif @cur_char.match?(IGNOR_CHAR)
52
- # Ignore whitespace.
53
- else
54
- error_msg = "illegal character '#{@cur_char}' at the position: #{@pos - 1}"
55
- raise IllegalCharError, error_msg
56
- end
57
- _next_char
58
- end
59
-
60
- tokens.append(Token.new(type: TT_EOF))
61
-
62
- tokens
63
- end
64
-
65
- private
66
-
67
- #
68
- # Build a 'word' out of a sequence of characters.
69
- #
70
- # A 'word' is a sequence of characters followed one by another without break characters (e.g. a space or a supported
71
- # math operator: '+', '-', etc).
72
- #
73
- # A 'word' could be represented in a form of:
74
- #
75
- # 1. a sequence of alphabetical characters optionally mixed with digits and joined into one string using a connective
76
- # character '_'. Examples:
77
- # - 'ruby' (a sequence of alphabetical characters only);
78
- # - 'foobar9000' (a mix of alphabetical characters and digits);
79
- # - 'this_is_a_3rd_example' (long string joined with a connective character).
80
- #
81
- # 2. an integer number, e.g.: '31337' (a sequence of digits without break characters).
82
- #
83
- # @param [Regexp] regexp
84
- # Defines that kind of character sequence the method should be looking for: NUM_CHAR (search for a digit) or
85
- # CONTS_CHAR (search for a constant).
86
- #
87
- # @return [String] word
88
- #
89
- def _build_word(regexp)
90
- word = ""
91
-
92
- until @cur_char.nil?
93
- break unless @cur_char.match?(regexp)
94
-
95
- word += @cur_char
96
- _next_char
97
- end
98
- _next_char
99
-
100
- @pos -= 1
101
- word
102
- end
103
-
104
- #
105
- # Process next character in the string.
106
- #
107
- def _next_char
108
- @cur_char = @pos < @str.length ? @str[@pos] : nil
109
- @pos += 1
110
- end
111
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Stores a node with a numeric value.
5
- #
6
- class NumberNode
7
- attr_reader :token
8
-
9
- def initialize(token)
10
- @token = token
11
- end
12
- end
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "bin_op_node"
4
- require_relative "number_node"
5
- require_relative "quecto_error"
6
- require_relative "token_types"
7
-
8
- #
9
- # Parses list of tokens to build an abstract syntax tree.
10
- #
11
- class Parser
12
- include TokenTypes
13
-
14
- #
15
- # Initialize a parser instance.
16
- #
17
- # @param [Array<Token>] tokens
18
- # List of tokens to parse.
19
- #
20
- def initialize(tokens)
21
- @tokens = tokens
22
- @cur_token = @tokens[0]
23
- @idx = 0
24
- end
25
-
26
- #
27
- # Parse list of tokens.
28
- #
29
- def parse
30
- _expr
31
- end
32
-
33
- private
34
-
35
- #
36
- # Buid a node for the expression.
37
- #
38
- # @return [BinOpNode, NumberNode] left
39
- # Node for an expression. The left.class corresponds to the expression type: a single number or a binary operation
40
- # (an operation between two numbers).
41
- #
42
- def _expr
43
- # retrieve left part of the expression:
44
- left = _term
45
-
46
- while BIN_OPS.include?(@cur_token.type)
47
- # retrieve operator between two number nodes:
48
- op_tok = @cur_token
49
-
50
- # retrieve right part of the expression:
51
- _next_token
52
- right = _term
53
-
54
- left = BinOpNode.new(left, op_tok, right)
55
- end
56
-
57
- left
58
- end
59
-
60
- #
61
- # Search for a term in the expression.
62
- #
63
- # @return [NumberNode] token
64
- # Found term.
65
- #
66
- # @raise [InvalidSyntaxError]
67
- #
68
- def _term
69
- if TERMS.include?(@cur_token.type)
70
- token = NumberNode.new(@cur_token)
71
- else
72
- error_msg = "expected TT_INT or TT_LBL, but got #{@cur_token.type}"
73
- raise InvalidSyntaxError, error_msg
74
- end
75
-
76
- _next_token
77
- token
78
- end
79
-
80
- #
81
- # Process next token.
82
- #
83
- def _next_token
84
- @idx += 1
85
- @cur_token = @tokens[@idx] if @idx < @tokens.length
86
- end
87
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Errors rised by the quecto_calc.
5
- #
6
- class QuectoError < ::StandardError
7
- attr_reader :error_name, :message
8
-
9
- def initialize(error_name, message)
10
- @error_name = error_name
11
- @message = message
12
- end
13
- end
14
-
15
- #
16
- # Raised by the lexer if an illegal character is found.
17
- #
18
- class IllegalCharError < QuectoError
19
- attr_reader :message
20
-
21
- def initialize(message = "")
22
- super("Illegal Character:", message)
23
- end
24
- end
25
-
26
- #
27
- # Raised by the parser if an illegal syntax is found.
28
- #
29
- class InvalidSyntaxError < QuectoError
30
- attr_reader :message
31
-
32
- def initialize(message = "")
33
- super("Syntax Error:", message)
34
- end
35
- end
36
-
37
- #
38
- # Raised by the evaluator on an illegal operation.
39
- #
40
- class CalcError < QuectoError
41
- attr_reader :message
42
-
43
- def initialize(message = "")
44
- super("Runtime Error:", message)
45
- end
46
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Stores a token generated by the lexer.
5
- #
6
- class Token
7
- attr_reader :type, :value
8
-
9
- def initialize(type:, value: nil)
10
- @type = type
11
- @value = value
12
- end
13
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # Provides supported token types.
5
- #
6
- module TokenTypes
7
- TT_INT = :TT_INT # integer number
8
- TT_CONST = :TT_CONST # constant (placeholder for a numeric value)
9
- TT_PLUS = :TT_PLUS # addition operator
10
- TT_MINUS = :TT_MINUS # subtraction operator
11
- TT_EOF = :TT_EOF # end of input
12
-
13
- TERMS = [TT_INT, TT_CONST].freeze
14
-
15
- BIN_OPS = [TT_PLUS, TT_MINUS].freeze
16
- end