omnium 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8dab1a91f7d1e12c0bb73a14952497bf6abad7ce6c09b1fa6d1122fb6a65f1d
4
- data.tar.gz: e793ae1711b80f8e3c0825188ba945d7a9ff99839459129d91914209dc3db54e
3
+ metadata.gz: ea2b969a43198c92ed63117aea179c8f9b538f2bfc61f5217fc2211dd71a7190
4
+ data.tar.gz: 488b260b9b96f9168e1289f9c38a1d4316a1900ae866f0b14b43c2468601f2fd
5
5
  SHA512:
6
- metadata.gz: 0776f6230d75cf5225a8aec85d88a5eec5896eb87f3859320cd42b075cfee0ff703e31cb9103c5ee37001cb8d6fb04643e08ea8cd23b3fb1a564412b624b9601
7
- data.tar.gz: 4f46cc5e2d05fec10a94877de924679c0370310879bc99bf79602298c3228536661df4d10f8c2a3cfee6f1d296628e595fcbe4e1801f3244940e8abea0f9c555
6
+ metadata.gz: a7a227a21c5397bc15641dc0af6e749a40f8f1430b779063c1be79d3c0dd02884ad45abb11faf93a25545e1185846284841d614be5c114e2db4e64985531a0cb
7
+ data.tar.gz: d380605399e2bd7f311031d4d79cd7490623d74585af52c0c2ff39a4283f0608c593d1aaa80cfd9d35062c5148c4398a8d9dbf9ee1ed832a989ae6df9a2f182a
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2020 Rónán Duddy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # Omnium
2
+
3
+ > "Your talk," I said, "is surely the handiwork of wisdom because not one word of it do I understand."
4
+
5
+ Omnium is
6
+ 1. an ongoing educational project for learning how to write a programming language
7
+ 2. an interpreter written in Ruby
8
+ 3. a gem
9
+ 4. a bicycle
10
+ 5. quite the pancake
11
+
12
+ [![Build Status](https://travis-ci.org/ronanduddy/omnium.svg?branch=master)](https://travis-ci.org/ronanduddy/omnium)
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'omnium'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```Shell
25
+ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```Shell
31
+ gem install omnium
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```Shell
37
+ bundle exec omnium hello.om
38
+ {:a=>2, :b=>25, :y=>5.997142857142857}
39
+ ```
40
+
41
+ ## Development
42
+
43
+ After checking out the repo, run `make run filename=hello.om` to run the example program. Then, run `make test` to run all the tests or `make guard` to use guard for testing. You can also run `make irb` for an interactive prompt that will allow you to experiment.
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ronanduddy/omnium. Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details on our code of conduct.
48
+
49
+ ## License
50
+
51
+ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
52
+
53
+ ## Acknowledgments
54
+
55
+ * Brian O'Nolan
56
+ * [Ruslan Spivak](https://ruslanspivak.com)
57
+ * [Jonathan Blow](https://www.youtube.com/user/jblow888/)
File without changes
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'omnium/version'
4
+ require 'omnium/common'
5
+ require 'omnium/lexer'
6
+ require 'omnium/parser'
7
+ require 'omnium/interpreter'
8
+ require 'omnium/cli'
9
+
10
+ module Omnium
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module CLI
5
+ require 'omnium/cli/core'
6
+
7
+ def self.new(args)
8
+ CLI::Core.new(args)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module CLI
5
+ class Core
6
+ class CLIError < StandardError; end
7
+
8
+ def initialize(args)
9
+ @filename = args&.first
10
+ end
11
+
12
+ def run
13
+ program = IO.readlines(@filename).join
14
+
15
+ interpret(program)
16
+ rescue TypeError => e
17
+ raise(CLIError, '@filename is blank.')
18
+ rescue Errno::ENOENT => e
19
+ raise(CLIError, "@filename '#{@filename}' does not exist.")
20
+ end
21
+
22
+ private
23
+
24
+ def interpret(input)
25
+ lexer = Lexer.new(input)
26
+ parser = Parser.new(lexer)
27
+ interpreter = Interpreter.new(parser)
28
+ interpreter.interpret
29
+
30
+ interpreter.symbol_table
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Common
5
+ VALUE_BASED_TOKENS = [
6
+ { type: :plus, value: '+' },
7
+ { type: :minus, value: '-' },
8
+ { type: :multiply, value: '*' },
9
+ { type: :divide, value: '/' },
10
+ { type: :left_parenthesis, value: '(' },
11
+ { type: :right_parenthesis, value: ')' },
12
+ { type: :semicolon, value: ';' },
13
+ { type: :dot, value: '.' },
14
+ { type: :assignment, value: ':=' },
15
+ { type: :colon, value: ':' },
16
+ { type: :comma, value: ',' }
17
+ ].freeze
18
+
19
+ PARAMETERISED_TOKENS = [
20
+ { type: :identifier },
21
+ { type: :integer }, # int
22
+ { type: :real } # float
23
+ ].freeze
24
+
25
+ NIL_VALUE_TOKENS = [
26
+ { type: :eof, value: nil }
27
+ ].freeze
28
+
29
+ TOKENS = {}
30
+ .merge(value_based: VALUE_BASED_TOKENS)
31
+ .merge(parameterised: PARAMETERISED_TOKENS)
32
+ .merge(nil_value: NIL_VALUE_TOKENS)
33
+
34
+ RESERVED_KEYWORDS = {
35
+ program: 'program',
36
+ var: 'var',
37
+ int: 'int', # integer
38
+ float: 'float', # real
39
+ begin: 'begin',
40
+ end: 'end'
41
+ }.freeze
42
+
43
+ def token_entity
44
+ if instance_variable_defined?(:@character)
45
+ @character # Lexer#character
46
+ elsif instance_variable_defined?(:@token)
47
+ @token&.type # Parser::Core#token
48
+ elsif instance_variable_defined?(:@type)
49
+ @type # Interpreter#type
50
+ end
51
+ end
52
+
53
+ def define_token_predicate_method(type, value = nil)
54
+ define_method("#{type}?") do
55
+ token_entity == type || (!value.nil? && token_entity == value)
56
+ end
57
+ end
58
+
59
+ def define_token_type_method(type)
60
+ define_method("#{type}_token") { type }
61
+ end
62
+
63
+ module_function :token_entity, :define_token_predicate_method, :define_token_type_method
64
+
65
+ VALUE_BASED_TOKENS.each do |token|
66
+ define_token_predicate_method(token[:type], token[:value])
67
+
68
+ define_method("#{token[:type]}_token") do |symbol = :type|
69
+ return token[:type] if symbol == :type
70
+ return token[:value] if symbol == :value
71
+
72
+ raise(ArgumentError, "Invalid argument :#{symbol}")
73
+ end
74
+ end
75
+
76
+ PARAMETERISED_TOKENS.each do |token|
77
+ define_token_predicate_method(token[:type])
78
+ define_token_type_method(token[:type])
79
+ end
80
+
81
+ NIL_VALUE_TOKENS.each do |token|
82
+ define_token_predicate_method(token[:type])
83
+ define_token_type_method(token[:type])
84
+ end
85
+
86
+ RESERVED_KEYWORDS.each_pair do |key, _value|
87
+ define_token_predicate_method(key)
88
+ define_token_type_method(key)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Interpreter
5
+ require 'omnium/interpreter/node_visitor'
6
+ require 'omnium/interpreter/core'
7
+
8
+ def self.new(parser)
9
+ Interpreter::Core.new(parser)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Interpreter
5
+ # The visitor pattern is used to traverse the AST. This class may be thought of
6
+ # as a 'tree visitor'.
7
+ class Core < NodeVisitor
8
+ include Common
9
+
10
+ class InterpreterError < StandardError; end
11
+
12
+ attr_reader :symbol_table, :name
13
+
14
+ def initialize(parser)
15
+ @parser = parser
16
+ @symbol_table = {}
17
+ @name = nil
18
+ end
19
+
20
+ def interpret
21
+ tree = @parser.parse
22
+ visit(tree)
23
+ end
24
+
25
+ # concrete visitor operations below
26
+
27
+ def visit_Program(node)
28
+ @name = node.name
29
+ visit(node.block)
30
+ end
31
+
32
+ def visit_Block(node)
33
+ node.variable_declarations.each do |variable_declaration|
34
+ visit(variable_declaration)
35
+ end
36
+
37
+ visit(node.compound_statement)
38
+ end
39
+
40
+ def visit_Compound(node)
41
+ node.children.each { |child| visit(child) }
42
+ end
43
+
44
+ def visit_VariableDeclaration(node)
45
+ # noop
46
+ end
47
+
48
+ def visit_Assignment(node)
49
+ symbol_table[node.left.name.intern] = visit(node.right)
50
+ end
51
+
52
+ def visit_Identifier(node)
53
+ symbol_table.fetch(node.name.intern)
54
+ rescue KeyError
55
+ raise(InterpreterError, "Variable '#{node.name}' not found")
56
+ end
57
+
58
+ def visit_DataType(node)
59
+ # noop
60
+ end
61
+
62
+ def visit_NoOperation(node)
63
+ # noop
64
+ end
65
+
66
+ def visit_BinaryOperator(node)
67
+ @type = node.operator.type
68
+
69
+ if plus?
70
+ visit(node.left) + visit(node.right)
71
+ elsif minus?
72
+ visit(node.left) - visit(node.right)
73
+ elsif multiply?
74
+ visit(node.left) * visit(node.right)
75
+ elsif divide?
76
+ visit(node.left) / visit(node.right)
77
+ end
78
+ end
79
+
80
+ def visit_UnaryOperator(node)
81
+ @type = node.operator.type
82
+
83
+ if plus?
84
+ +visit(node.operand)
85
+ elsif minus?
86
+ -visit(node.operand)
87
+ end
88
+ end
89
+
90
+ def visit_Number(node)
91
+ node.value
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Interpreter
5
+ # This is to facilite the visitor pattern regards double dispatching.
6
+ class NodeVisitor
7
+ def visit(node)
8
+ method_name = "visit_#{class_name(node)}"
9
+ send(method_name, node)
10
+ rescue NameError
11
+ raise NotImplementedError, "Subclass does not implement #{method_name}"
12
+ end
13
+
14
+ private
15
+
16
+ def class_name(node)
17
+ node.class.name.split('::').last
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Lexer
5
+ require 'omnium/lexer/token'
6
+ require 'omnium/lexer/token_helper'
7
+ require 'omnium/lexer/core'
8
+
9
+ def self.new(input)
10
+ Lexer::Core.new(input)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ # frozen_text_literal: true
4
+
5
+ module Omnium
6
+ module Lexer
7
+ # The lexer returns tokens for a given text.
8
+ class Core
9
+ include Common
10
+ include TokenHelper
11
+
12
+ WHITESPACE = ' '
13
+ COMMENT = '#'
14
+ NEWLINE = "\n"
15
+ DECIMAL_POINT = '.'
16
+
17
+ INTEGER = /[0-9]/.freeze
18
+ ALPHA = /[a-zA-Z]/.freeze
19
+ IDENTIFIER = /[a-zA-Z0-9_]/.freeze
20
+
21
+ class LexerError < StandardError; end
22
+
23
+ def initialize(text)
24
+ @text = text
25
+ @pointer = 0
26
+ end
27
+
28
+ def next_token
29
+ ignore
30
+ return new_eof_token if eos?
31
+
32
+ (@pointer...@text.length).each do
33
+ return tokenise
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def ignore
40
+ advance while whitespace? || newline?
41
+
42
+ if comment?
43
+ advance until newline? # comment text
44
+ ignore if whitespace? || newline? # skip any other junk
45
+ end
46
+ end
47
+
48
+ def eos?
49
+ @pointer > @text.length - 1
50
+ end
51
+
52
+ def advance(n = 1)
53
+ @pointer += n
54
+ end
55
+
56
+ def character
57
+ @character = @text[@pointer]
58
+ end
59
+
60
+ def peek
61
+ return nil if eos?
62
+
63
+ @text[@pointer + 1]
64
+ end
65
+
66
+ def whitespace?
67
+ character == WHITESPACE
68
+ end
69
+
70
+ def comment?
71
+ character == COMMENT
72
+ end
73
+
74
+ def newline?
75
+ character == NEWLINE
76
+ end
77
+
78
+ def decimal?
79
+ character == DECIMAL_POINT
80
+ end
81
+
82
+ def number
83
+ result = ''
84
+
85
+ while character =~ INTEGER
86
+ result += character
87
+ advance
88
+ end
89
+
90
+ if decimal?
91
+ result += character
92
+ advance
93
+
94
+ while character =~ INTEGER
95
+ result += character
96
+ advance
97
+ end
98
+
99
+ return result.to_f
100
+ end
101
+
102
+ result.to_i
103
+ end
104
+
105
+ def reserved_keyword
106
+ result = ''
107
+
108
+ while character =~ IDENTIFIER
109
+ result += character
110
+ advance
111
+ end
112
+
113
+ result
114
+ end
115
+
116
+ def assignment?
117
+ colon = assignment_token(:value)[0]
118
+ equals = assignment_token(:value)[1]
119
+
120
+ character == colon && peek == equals
121
+ end
122
+
123
+ def word_token(word)
124
+ if RESERVED_KEYWORDS.include?(word.intern)
125
+ send("new_#{word}_token")
126
+ else
127
+ new_identifier_token(word)
128
+ end
129
+ end
130
+
131
+ def number_token(num)
132
+ return new_integer_token(num) if num.is_a? Integer
133
+
134
+ new_real_token(num)
135
+ end
136
+
137
+ def tokenise
138
+ if character =~ ALPHA
139
+ word_token(reserved_keyword)
140
+ elsif character =~ INTEGER
141
+ number_token(number)
142
+ elsif plus?
143
+ advance
144
+ new_plus_token
145
+ elsif minus?
146
+ advance
147
+ new_minus_token
148
+ elsif multiply?
149
+ advance
150
+ new_multiply_token
151
+ elsif divide?
152
+ advance
153
+ new_divide_token
154
+ elsif left_parenthesis?
155
+ advance
156
+ new_left_parenthesis_token
157
+ elsif right_parenthesis?
158
+ advance
159
+ new_right_parenthesis_token
160
+ elsif assignment?
161
+ advance(2)
162
+ new_assignment_token
163
+ elsif semicolon?
164
+ advance
165
+ new_semicolon_token
166
+ elsif colon?
167
+ advance
168
+ new_colon_token
169
+ elsif comma?
170
+ advance
171
+ new_comma_token
172
+ elsif dot?
173
+ advance
174
+ new_dot_token
175
+ else
176
+ raise(LexerError, "Error tokenising '#{character}' at position #{@pointer}")
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Lexer
5
+ # A token is a string with an assigned and identified meaning.
6
+ class Token
7
+ attr_reader :type, :value
8
+
9
+ def initialize(type, value)
10
+ @type = type
11
+ @value = value
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Lexer
5
+ module TokenHelper
6
+ include Common
7
+
8
+ def define_new_token_method(type, value, arity = 0)
9
+ method_name = "new_#{type}_token"
10
+
11
+ if arity == 0
12
+ define_method(method_name) { Token.new(type, value) }
13
+ else
14
+ define_method(method_name) { |argument| Token.new(type, argument) }
15
+ end
16
+ end
17
+
18
+ module_function :define_new_token_method
19
+
20
+ VALUE_BASED_TOKENS.each do |token|
21
+ define_new_token_method(token[:type], token[:value])
22
+ end
23
+
24
+ PARAMETERISED_TOKENS.each do |token|
25
+ define_new_token_method(token[:type], nil, 1)
26
+ end
27
+
28
+ NIL_VALUE_TOKENS.each do |token|
29
+ define_new_token_method(token[:type], nil)
30
+ end
31
+
32
+ RESERVED_KEYWORDS.each_pair do |key, value|
33
+ define_new_token_method(key, value)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ require 'omnium/parser/ast/base'
7
+ require 'omnium/parser/ast/assignment'
8
+ require 'omnium/parser/ast/binary_operator'
9
+ require 'omnium/parser/ast/block'
10
+ require 'omnium/parser/ast/compound'
11
+ require 'omnium/parser/ast/data_type'
12
+ require 'omnium/parser/ast/identifier'
13
+ require 'omnium/parser/ast/no_operation'
14
+ require 'omnium/parser/ast/number'
15
+ require 'omnium/parser/ast/program'
16
+ require 'omnium/parser/ast/unary_operator'
17
+ require 'omnium/parser/ast/variable_declaration'
18
+ end
19
+
20
+ require 'omnium/parser/parse_error_handler'
21
+ require 'omnium/parser/core'
22
+
23
+ def self.new(lexer)
24
+ Parser::Core.new(lexer)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # This is an assignment statement, for example 'x := 5' or 'y := x'. @left
7
+ # is a reference to the variable node and @right is a reference to the node
8
+ # returned by Parser::Core#expr
9
+ class Assignment < Base
10
+ attr_reader :left, :operator, :right
11
+
12
+ def initialize(left, operator, right)
13
+ @left = left
14
+ @operator = operator
15
+ @right = right
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ # Base class for the abstract syntax tree
5
+ module Parser
6
+ module AST
7
+ class Base
8
+ # noop...
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # Binary operator represents the four binary operations: add, subtract, multiply
7
+ # and divide. @left and @right are the nodes representing operands (i.e. Number).
8
+ # @operator represents the operator as a token.
9
+ class BinaryOperator < Base
10
+ attr_reader :left, :operator, :right
11
+
12
+ def initialize(left, operator, right)
13
+ @left = left
14
+ @operator = operator
15
+ @right = right
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # A block represents declarations and compound statements
7
+ class Block < Base
8
+ attr_reader :variable_declarations, :compound_statement
9
+
10
+ def initialize(variable_declarations, compound_statement)
11
+ @variable_declarations = variable_declarations
12
+ @compound_statement = compound_statement
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # This compound statement is for a 'begin ... end' block and therefore
7
+ # represents a list of statement nodes as @children.
8
+ class Compound < Base
9
+ attr_reader :children
10
+
11
+ def initialize
12
+ @children = []
13
+ end
14
+
15
+ def append(nodes)
16
+ nodes.each { |node| @children << node }
17
+ self
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # Int or float for example
7
+ class DataType < Base
8
+ attr_reader :value
9
+
10
+ def initialize(token)
11
+ @value = token.value
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # This node represents a variable or an identifier, accepting an ID token.
7
+ class Identifier < Base
8
+ attr_reader :name
9
+
10
+ def initialize(token)
11
+ @token = token # for convenience, though no getter for this
12
+ @name = token.value
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # This class represents no operation, nothing, a noop, an empty statement
7
+ # such as 'begin end'.
8
+ class NoOperation < Base
9
+ # noop
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # An integer node, which would accept an integer/number token.
7
+ class Number < Base
8
+ attr_reader :value
9
+
10
+ def initialize(token)
11
+ @token = token # for convenience, though no getter for this
12
+ @value = token.value
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # Program represents the root node.
7
+ class Program < Base
8
+ attr_reader :name, :block
9
+
10
+ def initialize(name, block)
11
+ @name = name
12
+ @block = block
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # Unary Operator represents a plus or minus on a number e.g. +1 or -5. That is
7
+ # an operator operating on one operand. @operator represents the unary operator
8
+ # token (plus or minus) and @operand represents another node (number or expression).
9
+ class UnaryOperator < Base
10
+ attr_reader :operator, :operand
11
+
12
+ def initialize(operator, operand)
13
+ @operator = operator
14
+ @operand = operand
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ module AST
6
+ # Representative of the declaration of a variable, containing a reference to
7
+ # the variable identifier_node and it's data type.
8
+ class VariableDeclaration < Base
9
+ attr_reader :identifier, :data_type
10
+
11
+ def initialize(identifier, data_type)
12
+ @identifier = identifier
13
+ @data_type = data_type
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ # The parser will verify the format of a list of tokens (syntax analysis) by
6
+ # 'recursive-descent'.
7
+ class Core
8
+ include Common
9
+ include ParseErrorHandler
10
+ include AST
11
+
12
+ def initialize(lexer)
13
+ @lexer = lexer
14
+ @token = nil
15
+ @consumed_token = nil
16
+ end
17
+
18
+ def parse
19
+ @token = @lexer.next_token
20
+ node = program
21
+
22
+ error(expected_type: eof_token, actual_type: @token.type) unless eof?
23
+
24
+ node
25
+ end
26
+
27
+ private
28
+
29
+ def program
30
+ # program : PROGRAM variable SEMI block DOT
31
+ consume(program_token)
32
+ name = identifier.name
33
+ consume(semicolon_token)
34
+ node = Program.new(name, block)
35
+ consume(dot_token)
36
+
37
+ node
38
+ end
39
+
40
+ def block
41
+ # block : variable_declarations compound_statement
42
+ Block.new(variable_declarations, compound_statement)
43
+ end
44
+
45
+ def variable_declarations
46
+ # variable_declarations : VAR (variable_declaration SEMI)+
47
+ # | empty
48
+ declarations = []
49
+
50
+ if var?
51
+ consume(var_token)
52
+
53
+ while identifier?
54
+ declarations << variable_declaration
55
+ consume(semicolon_token)
56
+ end
57
+ end
58
+
59
+ declarations.flatten
60
+ end
61
+
62
+ def variable_declaration
63
+ # variable_declaration : ID (COMMA ID)* COLON variable_data_type
64
+ identifiers = [identifier]
65
+
66
+ while comma?
67
+ consume(comma_token)
68
+ identifiers << identifier
69
+ end
70
+
71
+ consume(colon_token)
72
+
73
+ data_type = variable_data_type
74
+ identifiers.map { |identifier| VariableDeclaration.new(identifier, data_type) }
75
+ end
76
+
77
+ def variable_data_type
78
+ # variable_data_type : INTEGER
79
+ # | FLOAT
80
+ if int?
81
+ consume(int_token)
82
+ elsif float?
83
+ consume(float_token)
84
+ end
85
+
86
+ DataType.new(@consumed_token)
87
+ end
88
+
89
+ def compound_statement
90
+ # compound_statement : BEGIN statement_list END
91
+ consume(begin_token)
92
+ nodes = statement_list
93
+ consume(end_token)
94
+
95
+ Compound.new.append(nodes)
96
+ end
97
+
98
+ def statement_list
99
+ # statement_list : statement
100
+ # | statement SEMI statement_list
101
+ nodes = [statement]
102
+
103
+ while semicolon?
104
+ consume(semicolon_token)
105
+ nodes << statement
106
+ end
107
+
108
+ error("Invalid identifier found: #{@token.inspect}") if identifier?
109
+
110
+ nodes
111
+ end
112
+
113
+ def statement
114
+ # statement : compound_statement
115
+ # | assignment_statement
116
+ # | empty
117
+ if begin?
118
+ return compound_statement
119
+ elsif identifier?
120
+ return assignment_statement
121
+ end
122
+
123
+ empty
124
+ end
125
+
126
+ def assignment_statement
127
+ # assignment_statement : variable ASSIGN expr
128
+ left = identifier
129
+ consume(assignment_token)
130
+
131
+ Assignment.new(left, @consumed_token, expr)
132
+ end
133
+
134
+ def identifier
135
+ # variable : ID
136
+ node = Identifier.new(@token)
137
+ consume(identifier_token)
138
+
139
+ node
140
+ end
141
+
142
+ def empty
143
+ # no operation rule
144
+ NoOperation.new
145
+ end
146
+
147
+ def expr
148
+ # expr : term ((PLUS | MINUS) term)*
149
+ node = term
150
+
151
+ while plus? || minus?
152
+ if plus?
153
+ consume(plus_token)
154
+ elsif minus?
155
+ consume(minus_token)
156
+ end
157
+
158
+ node = BinaryOperator.new(node, @consumed_token, term)
159
+ end
160
+
161
+ node
162
+ end
163
+
164
+ def term
165
+ # term : factor ((MULTIPLY | DIVIDE) factor)*
166
+ node = factor
167
+
168
+ while multiply? || divide?
169
+ if multiply?
170
+ consume(multiply_token)
171
+ elsif divide?
172
+ consume(divide_token)
173
+ end
174
+
175
+ node = BinaryOperator.new(node, @consumed_token, factor)
176
+ end
177
+
178
+ node
179
+ end
180
+
181
+ def factor
182
+ # factor : PLUS factor
183
+ # | MINUS factor
184
+ # | INTEGER
185
+ # | LPAREN expr RPAREN
186
+ # | identifier
187
+ if plus?
188
+ consume(plus_token)
189
+ return UnaryOperator.new(@consumed_token, factor)
190
+ elsif minus?
191
+ consume(minus_token)
192
+ return UnaryOperator.new(@consumed_token, factor)
193
+ elsif integer?
194
+ consume(integer_token)
195
+ return Number.new(@consumed_token)
196
+ elsif real?
197
+ consume(real_token)
198
+ return Number.new(@consumed_token)
199
+ elsif left_parenthesis?
200
+ consume(left_parenthesis_token)
201
+ node = expr
202
+ consume(right_parenthesis_token)
203
+ return node
204
+ elsif identifier?
205
+ return identifier
206
+ end
207
+
208
+ expected_types = [
209
+ plus_token,
210
+ minus_token,
211
+ integer_token,
212
+ real_token,
213
+ left_parenthesis_token,
214
+ right_parenthesis_token,
215
+ identifier_token
216
+ ]
217
+
218
+ error(expected_type: expected_types, actual_type: @token.type)
219
+ end
220
+
221
+ def consume(type)
222
+ # verify the type of @token and advance @token to next_token
223
+ unless type == @token.type
224
+ error(expected_type: type, actual_type: @token.type)
225
+ end
226
+
227
+ @consumed_token = @token
228
+ @token = @lexer.next_token
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ module Parser
5
+ # The module responsible for generating ParseErrors
6
+ module ParseErrorHandler
7
+ def error(message = nil, expected_type: nil, actual_type: nil)
8
+ # a dirty sort of arg list...
9
+ raise ParseError.new(
10
+ actual_type: actual_type,
11
+ expected_type: expected_type,
12
+ message: message
13
+ )
14
+ end
15
+
16
+ # could possibly break this out if desired...
17
+ class ParseError < StandardError
18
+ attr_reader :actual_type, :expected_type
19
+
20
+ def initialize(**args)
21
+ @actual_type = args[:actual_type]
22
+ @expected_type = args[:expected_type]
23
+
24
+ super(args[:message] || default_message)
25
+ end
26
+
27
+ private
28
+
29
+ def default_message
30
+ template = "Expecting token type(s) '%s', got '%s'."
31
+ format(template, sanitised_expected_type, @actual_type)
32
+ end
33
+
34
+ def sanitised_expected_type
35
+ return @expected_type.to_s if @expected_type.is_a? Symbol
36
+
37
+ @expected_type.join(', ')
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omnium
4
+ VERSION = '0.2.0'
5
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnium
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
  - Rónán Duddy
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-02 00:00:00.000000000 Z
11
+ date: 2020-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: guard
@@ -116,13 +116,44 @@ dependencies:
116
116
  version: 0.81.0
117
117
  description: That is about the size of it
118
118
  email:
119
- - ronanduddy@live.ie
119
+ - dev@ronanduddy.xyz
120
120
  executables:
121
121
  - omnium
122
122
  extensions: []
123
- extra_rdoc_files: []
123
+ extra_rdoc_files:
124
+ - README.md
125
+ - LICENSE.md
124
126
  files:
125
- - bin/omnium
127
+ - LICENSE.md
128
+ - README.md
129
+ - exe/omnium
130
+ - lib/omnium.rb
131
+ - lib/omnium/cli.rb
132
+ - lib/omnium/cli/core.rb
133
+ - lib/omnium/common.rb
134
+ - lib/omnium/interpreter.rb
135
+ - lib/omnium/interpreter/core.rb
136
+ - lib/omnium/interpreter/node_visitor.rb
137
+ - lib/omnium/lexer.rb
138
+ - lib/omnium/lexer/core.rb
139
+ - lib/omnium/lexer/token.rb
140
+ - lib/omnium/lexer/token_helper.rb
141
+ - lib/omnium/parser.rb
142
+ - lib/omnium/parser/ast/assignment.rb
143
+ - lib/omnium/parser/ast/base.rb
144
+ - lib/omnium/parser/ast/binary_operator.rb
145
+ - lib/omnium/parser/ast/block.rb
146
+ - lib/omnium/parser/ast/compound.rb
147
+ - lib/omnium/parser/ast/data_type.rb
148
+ - lib/omnium/parser/ast/identifier.rb
149
+ - lib/omnium/parser/ast/no_operation.rb
150
+ - lib/omnium/parser/ast/number.rb
151
+ - lib/omnium/parser/ast/program.rb
152
+ - lib/omnium/parser/ast/unary_operator.rb
153
+ - lib/omnium/parser/ast/variable_declaration.rb
154
+ - lib/omnium/parser/core.rb
155
+ - lib/omnium/parser/parse_error_handler.rb
156
+ - lib/omnium/version.rb
126
157
  homepage: https://github.com/ronanduddy/omnium
127
158
  licenses:
128
159
  - MIT
@@ -135,14 +166,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
135
166
  requirements:
136
167
  - - ">="
137
168
  - !ruby/object:Gem::Version
138
- version: 2.7.0
169
+ version: 2.7.2
139
170
  required_rubygems_version: !ruby/object:Gem::Requirement
140
171
  requirements:
141
172
  - - ">="
142
173
  - !ruby/object:Gem::Version
143
174
  version: '0'
144
175
  requirements: []
145
- rubygems_version: 3.1.2
176
+ rubygems_version: 3.1.4
146
177
  signing_key:
147
178
  specification_version: 4
148
179
  summary: Omnium language.