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 +4 -4
- data/LICENSE.md +9 -0
- data/README.md +57 -0
- data/{bin → exe}/omnium +0 -0
- data/lib/omnium.rb +11 -0
- data/lib/omnium/cli.rb +11 -0
- data/lib/omnium/cli/core.rb +34 -0
- data/lib/omnium/common.rb +91 -0
- data/lib/omnium/interpreter.rb +12 -0
- data/lib/omnium/interpreter/core.rb +95 -0
- data/lib/omnium/interpreter/node_visitor.rb +21 -0
- data/lib/omnium/lexer.rb +13 -0
- data/lib/omnium/lexer/core.rb +181 -0
- data/lib/omnium/lexer/token.rb +15 -0
- data/lib/omnium/lexer/token_helper.rb +37 -0
- data/lib/omnium/parser.rb +27 -0
- data/lib/omnium/parser/ast/assignment.rb +20 -0
- data/lib/omnium/parser/ast/base.rb +12 -0
- data/lib/omnium/parser/ast/binary_operator.rb +20 -0
- data/lib/omnium/parser/ast/block.rb +17 -0
- data/lib/omnium/parser/ast/compound.rb +22 -0
- data/lib/omnium/parser/ast/data_type.rb +16 -0
- data/lib/omnium/parser/ast/identifier.rb +17 -0
- data/lib/omnium/parser/ast/no_operation.rb +13 -0
- data/lib/omnium/parser/ast/number.rb +17 -0
- data/lib/omnium/parser/ast/program.rb +17 -0
- data/lib/omnium/parser/ast/unary_operator.rb +19 -0
- data/lib/omnium/parser/ast/variable_declaration.rb +18 -0
- data/lib/omnium/parser/core.rb +232 -0
- data/lib/omnium/parser/parse_error_handler.rb +42 -0
- data/lib/omnium/version.rb +5 -0
- metadata +39 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea2b969a43198c92ed63117aea179c8f9b538f2bfc61f5217fc2211dd71a7190
|
4
|
+
data.tar.gz: 488b260b9b96f9168e1289f9c38a1d4316a1900ae866f0b14b43c2468601f2fd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7a227a21c5397bc15641dc0af6e749a40f8f1430b779063c1be79d3c0dd02884ad45abb11faf93a25545e1185846284841d614be5c114e2db4e64985531a0cb
|
7
|
+
data.tar.gz: d380605399e2bd7f311031d4d79cd7490623d74585af52c0c2ff39a4283f0608c593d1aaa80cfd9d35062c5148c4398a8d9dbf9ee1ed832a989ae6df9a2f182a
|
data/LICENSE.md
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
[](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/)
|
data/{bin → exe}/omnium
RENAMED
File without changes
|
data/lib/omnium.rb
ADDED
data/lib/omnium/cli.rb
ADDED
@@ -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,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
|
data/lib/omnium/lexer.rb
ADDED
@@ -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,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,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,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
|
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.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rónán Duddy
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
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
|
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
|
-
-
|
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.
|
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.
|
176
|
+
rubygems_version: 3.1.4
|
146
177
|
signing_key:
|
147
178
|
specification_version: 4
|
148
179
|
summary: Omnium language.
|