loxxy 0.0.3
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +379 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +60 -0
- data/Rakefile +8 -0
- data/lib/loxxy.rb +8 -0
- data/lib/loxxy/datatype/all_datatypes.rb +7 -0
- data/lib/loxxy/datatype/boolean.rb +13 -0
- data/lib/loxxy/datatype/builtin_datatype.rb +25 -0
- data/lib/loxxy/datatype/false.rb +20 -0
- data/lib/loxxy/datatype/lx_string.rb +27 -0
- data/lib/loxxy/datatype/nil.rb +20 -0
- data/lib/loxxy/datatype/number.rb +35 -0
- data/lib/loxxy/datatype/true.rb +20 -0
- data/lib/loxxy/front_end/grammar.rb +152 -0
- data/lib/loxxy/front_end/literal.rb +25 -0
- data/lib/loxxy/front_end/parser.rb +40 -0
- data/lib/loxxy/front_end/scanner.rb +225 -0
- data/lib/loxxy/version.rb +5 -0
- data/loxxy.gemspec +63 -0
- data/spec/front_end/parser_spec.rb +56 -0
- data/spec/front_end/scanner_spec.rb +229 -0
- data/spec/loxxy_spec.rb +9 -0
- data/spec/spec_helper.rb +15 -0
- metadata +131 -0
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Loxxy
|
2
|
+
|
3
|
+
A Ruby implementation of the [Lox programming language](https://craftinginterpreters.com/the-lox-language.html),
|
4
|
+
a simple language used in Bob Nystrom's online book [Crafting Interpreters](https://craftinginterpreters.com/).
|
5
|
+
|
6
|
+
## Purpose of this project:
|
7
|
+
- To deliver an open source example of a programming language fully implemented in Ruby
|
8
|
+
(from the scanner, parser, code generation).
|
9
|
+
- The implementation should be mature enough to run (LoxLox)[https://github.com/benhoyt/loxlox],
|
10
|
+
a Lox interpreter written in Lox.
|
11
|
+
|
12
|
+
## Roadmap
|
13
|
+
- [DONE] Scanner (tokenizer)
|
14
|
+
- [STARTED] Recognizer
|
15
|
+
- [TODO] Parser
|
16
|
+
- [TODO] Interpreter or transpiler
|
17
|
+
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'loxxy'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
$ bundle install
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
$ gem install loxxy
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
TODO: Write usage instructions here
|
38
|
+
|
39
|
+
## Other Lox implementations in Ruby
|
40
|
+
An impressive list of Lox implementations can be found [here](https://github.com/munificent/craftinginterpreters/wiki/Lox-implementations
|
41
|
+
|
42
|
+
For Ruby, ther is the [lox](https://github.com/rdodson41/ruby-lox) gem.
|
43
|
+
There are other Ruby-based projects as well:
|
44
|
+
- [SlowLox](https://github.com/ArminKleinert/SlowLox), described as a "1-to-1 conversion of JLox to Ruby"
|
45
|
+
- [rulox](https://github.com/LevitatingBusinessMan/rulox)
|
46
|
+
|
47
|
+
## Development
|
48
|
+
|
49
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
50
|
+
|
51
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
|
55
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/famished_tiger/loxxy.
|
56
|
+
|
57
|
+
|
58
|
+
## License
|
59
|
+
|
60
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/loxxy.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'builtin_datatype'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Datatype
|
7
|
+
# Abstract class that generalizes a Lox boolean value.
|
8
|
+
# An instance acts merely as a wrapper around a Ruby representation
|
9
|
+
# of the value.
|
10
|
+
class Boolean < BuiltinDatatype
|
11
|
+
end # class
|
12
|
+
end # module
|
13
|
+
end # module
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Loxxy
|
4
|
+
module Datatype
|
5
|
+
# Abstract class that generalizes a value from a Lox built-in data types.
|
6
|
+
# An instance acts merely as a wrapper around a Ruby representation
|
7
|
+
# of the value.
|
8
|
+
class BuiltinDatatype
|
9
|
+
# @return [Object] The Ruby representation
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
# Constructor. Initialize a Lox value from one of its built-in data type.
|
13
|
+
def initialize(aValue)
|
14
|
+
@value = validated_value(aValue)
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def validated_value(aValue)
|
21
|
+
aValue
|
22
|
+
end
|
23
|
+
end # class
|
24
|
+
end # module
|
25
|
+
end # module
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton' # Use the Singleton design pattern
|
4
|
+
require_relative 'boolean'
|
5
|
+
|
6
|
+
module Loxxy
|
7
|
+
module Datatype
|
8
|
+
# Class for representing a Lox false value.
|
9
|
+
class False < Boolean
|
10
|
+
include Singleton # Make a singleton class
|
11
|
+
|
12
|
+
# Build the sole instance
|
13
|
+
def initialize
|
14
|
+
super(false)
|
15
|
+
end
|
16
|
+
end # class
|
17
|
+
|
18
|
+
False.instance.freeze # Make the sole instance immutable
|
19
|
+
end # module
|
20
|
+
end # module
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'builtin_datatype'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Datatype
|
7
|
+
# Class for representing a Lox string of characters value.
|
8
|
+
class LXString < BuiltinDatatype
|
9
|
+
|
10
|
+
# Build the sole instance
|
11
|
+
def initialize(aValue)
|
12
|
+
super(aValue)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def validated_value(aValue)
|
18
|
+
unless aValue.is_a?(String)
|
19
|
+
raise StandardError, "Invalid number value #{aValue}"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Remove double quotes delimiter
|
23
|
+
aValue.gsub(/(^")|("$)/, '')
|
24
|
+
end
|
25
|
+
end # class
|
26
|
+
end # module
|
27
|
+
end # module
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton' # Use the Singleton design pattern
|
4
|
+
require_relative 'builtin_datatype'
|
5
|
+
|
6
|
+
module Loxxy
|
7
|
+
module Datatype
|
8
|
+
# Class for representing a Lox nil "value".
|
9
|
+
class Nil < BuiltinDatatype
|
10
|
+
include Singleton # Make a singleton class
|
11
|
+
|
12
|
+
# Build the sole instance
|
13
|
+
def initialize
|
14
|
+
super(nil)
|
15
|
+
end
|
16
|
+
end # class
|
17
|
+
|
18
|
+
Nil.instance.freeze # Make the sole instance immutable
|
19
|
+
end # module
|
20
|
+
end # module
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'builtin_datatype'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Datatype
|
7
|
+
# Class for representing a Lox numeric value.
|
8
|
+
class Number < BuiltinDatatype
|
9
|
+
|
10
|
+
# Build the sole instance
|
11
|
+
def initialize(aValue)
|
12
|
+
super(aValue)
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def validated_value(aValue)
|
18
|
+
case aValue
|
19
|
+
when Integer
|
20
|
+
result = aValue
|
21
|
+
when Numeric
|
22
|
+
result = aValue
|
23
|
+
when /^-?\d+$/
|
24
|
+
result = aValue.to_i
|
25
|
+
when /^-?\d+\.\d+$/
|
26
|
+
result = aValue.to_f
|
27
|
+
else
|
28
|
+
raise StandardError, "Invalid number value #{aValue}"
|
29
|
+
end
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
end # class
|
34
|
+
end # module
|
35
|
+
end # module
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton' # Use the Singleton design pattern
|
4
|
+
require_relative 'boolean'
|
5
|
+
|
6
|
+
module Loxxy
|
7
|
+
module Datatype
|
8
|
+
# Class for representing a Lox true value.
|
9
|
+
class True < Boolean
|
10
|
+
include Singleton # Make a singleton class
|
11
|
+
|
12
|
+
# Build the sole instance
|
13
|
+
def initialize
|
14
|
+
super(true)
|
15
|
+
end
|
16
|
+
end # class
|
17
|
+
|
18
|
+
True.instance.freeze # Make the sole instance immutable
|
19
|
+
end # module
|
20
|
+
end # module
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
require 'rley' # Load the gem
|
5
|
+
|
6
|
+
module Loxxy
|
7
|
+
module FrontEnd
|
8
|
+
########################################
|
9
|
+
# Grammar for Lox language
|
10
|
+
# Authoritave grammar at:
|
11
|
+
# https://craftinginterpreters.com/appendix-i.html
|
12
|
+
builder = Rley::Syntax::GrammarBuilder.new do
|
13
|
+
# Punctuators, separators...
|
14
|
+
add_terminals('LEFT_PAREN', 'RIGHT_PAREN', 'LEFT_BRACE', 'RIGHT_BRACE')
|
15
|
+
add_terminals('COMMA', 'DOT', 'MINUS', 'PLUS')
|
16
|
+
add_terminals('SEMICOLON', 'SLASH', 'STAR')
|
17
|
+
add_terminals('BANG', 'BANG_EQUAL', 'EQUAL', 'EQUAL_EQUAL')
|
18
|
+
add_terminals('GREATER', 'GREATER_EQUAL', 'LESS', 'LESS_EQUAL')
|
19
|
+
|
20
|
+
# Keywords and literals
|
21
|
+
add_terminals('AND', 'CLASS', 'ELSE', 'FALSE')
|
22
|
+
add_terminals('FUN', 'FOR', 'IDENTIFIER', 'IF')
|
23
|
+
add_terminals('NIL', 'NUMBER', 'OR', 'PRINT')
|
24
|
+
add_terminals('RETURN', 'STRING', 'SUPER', 'THIS')
|
25
|
+
add_terminals('TRUE', 'VAR', 'WHILE')
|
26
|
+
add_terminals('EOF')
|
27
|
+
|
28
|
+
# Top-level rule that matches an entire Lox program
|
29
|
+
rule('program' => 'declaration_star EOF')
|
30
|
+
|
31
|
+
# Declarations: bind an identifier to something
|
32
|
+
rule('declaration_star' => 'declaration_star declaration')
|
33
|
+
rule('declaration_star' => [])
|
34
|
+
rule('declaration' => 'classDecl')
|
35
|
+
rule('declaration' => 'funDecl')
|
36
|
+
rule('declaration' => 'varDecl')
|
37
|
+
rule('declaration' => 'statement')
|
38
|
+
|
39
|
+
rule('classDecl' => 'CLASS classNaming class_body')
|
40
|
+
rule('classNaming' => 'IDENTIFIER LESS IDENTIFIER')
|
41
|
+
rule('classNaming' => 'IDENTIFIER')
|
42
|
+
rule('class_body' => 'LEFT_BRACE function_star RIGHT_BRACE')
|
43
|
+
rule('function_star' => 'function_star function')
|
44
|
+
rule('function_star' => [])
|
45
|
+
|
46
|
+
rule('funDecl' => 'FUN function')
|
47
|
+
|
48
|
+
rule('varDecl' => 'VAR IDENTIFIER SEMICOLON')
|
49
|
+
rule('varDecl' => 'VAR IDENTIFIER EQUAL expression SEMICOLON')
|
50
|
+
|
51
|
+
# Statements: produce side effects, but don't introduce bindings
|
52
|
+
rule('statement' => 'exprStmt')
|
53
|
+
rule('statement' => 'forStmt')
|
54
|
+
rule('statement' => 'ifStmt')
|
55
|
+
rule('statement' => 'printStmt')
|
56
|
+
rule('statement' => 'returnStmt')
|
57
|
+
rule('statement' => 'whileStmt')
|
58
|
+
rule('statement' => 'block')
|
59
|
+
|
60
|
+
rule('exprStmt' => 'expression SEMICOLON')
|
61
|
+
|
62
|
+
rule('forStmt' => 'FOR LEFT_PAREN forControl RIGHT_PAREN statement')
|
63
|
+
rule('forControl' => 'forInitialization forTest forUpdate')
|
64
|
+
rule('forInitialization' => 'varDecl')
|
65
|
+
rule('forInitialization' => 'exprStmt')
|
66
|
+
rule('forInitialization' => 'SEMICOLON')
|
67
|
+
rule('forTest' => 'expression_opt SEMICOLON')
|
68
|
+
rule('forUpdate' => 'expression_opt')
|
69
|
+
|
70
|
+
rule('ifStmt' => 'IF ifCondition statement elsePart_opt')
|
71
|
+
rule('ifCondition' => 'LEFT_PAREN expression RIGHT_PAREN')
|
72
|
+
rule('elsePart_opt' => 'ELSE statement')
|
73
|
+
rule('elsePart_opt' => [])
|
74
|
+
|
75
|
+
rule('printStmt' => 'PRINT expression SEMICOLON')
|
76
|
+
rule('returnStmt' => 'RETURN expression_opt SEMICOLON')
|
77
|
+
rule('whileStmt' => 'WHILE LEFT_PAREN expression RIGHT_PAREN statement')
|
78
|
+
rule('block' => 'LEFT_BRACE declaration_star RIGHT_BRACE')
|
79
|
+
|
80
|
+
# Expressions: produce values
|
81
|
+
rule('expression_opt' => 'expression')
|
82
|
+
rule('expression_opt' => [])
|
83
|
+
rule('expression' => 'assignment')
|
84
|
+
rule('assignment' => 'owner_opt IDENTIFIER EQUAL assignment')
|
85
|
+
rule('assignment' => 'logic_or')
|
86
|
+
rule('owner_opt' => 'call DOT')
|
87
|
+
rule('owner_opt' => [])
|
88
|
+
rule('logic_or' => 'logic_and disjunct_star')
|
89
|
+
rule('disjunct_star' => 'disjunct_star OR logic_and')
|
90
|
+
rule('disjunct_star' => [])
|
91
|
+
rule('logic_and' => 'equality conjunct_star')
|
92
|
+
rule('conjunct_star' => 'conjunct_star AND equality')
|
93
|
+
rule('conjunct_star' => [])
|
94
|
+
rule('equality' => 'comparison equalityTest_star')
|
95
|
+
rule('equalityTest_star' => 'equalityTest_star equalityTest comparison')
|
96
|
+
rule('equalityTest_star' => [])
|
97
|
+
rule('equalityTest' => 'BANG_EQUAL')
|
98
|
+
rule('equalityTest' => 'EQUAL_EQUAL')
|
99
|
+
rule('comparison' => 'term comparisonTest_star')
|
100
|
+
rule('comparisonTest_star' => 'comparisonTest_star comparisonTest term')
|
101
|
+
rule('comparisonTest_star' => [])
|
102
|
+
rule('comparisonTest' => 'GREATER')
|
103
|
+
rule('comparisonTest' => 'GREATER_EQUAL')
|
104
|
+
rule('comparisonTest' => 'LESS')
|
105
|
+
rule('comparisonTest' => 'LESS_EQUAL')
|
106
|
+
rule('term' => 'factor additive_star')
|
107
|
+
rule('additive_star' => 'additive_star additionOp factor')
|
108
|
+
rule('additive_star' => [])
|
109
|
+
rule('additionOp' => 'MINUS')
|
110
|
+
rule('additionOp' => 'PLUS')
|
111
|
+
rule('factor' => 'unary multiplicative_star')
|
112
|
+
rule('multiplicative_star' => 'multiplicative_star multOp unary')
|
113
|
+
rule('multiplicative_star' => [])
|
114
|
+
rule('multOp' => 'SLASH')
|
115
|
+
rule('multOp' => 'STAR')
|
116
|
+
rule('unary' => 'unaryOp unary')
|
117
|
+
rule('unary' => 'call')
|
118
|
+
rule('unaryOp' => 'BANG')
|
119
|
+
rule('unaryOp' => 'MINUS')
|
120
|
+
rule('call' => 'primary refinement_star')
|
121
|
+
rule('refinement_star' => 'refinement_star refinement')
|
122
|
+
rule('refinement_star' => [])
|
123
|
+
rule('refinement' => 'LEFT_PAREN arguments_opt RIGHT_PAREN')
|
124
|
+
rule('refinement' => 'DOT IDENTIFIER')
|
125
|
+
rule('primary' => 'TRUE')
|
126
|
+
rule('primary' => 'FALSE')
|
127
|
+
rule('primary' => 'NIL')
|
128
|
+
rule('primary' => 'THIS')
|
129
|
+
rule('primary' => 'NUMBER')
|
130
|
+
rule('primary' => 'STRING')
|
131
|
+
rule('primary' => 'IDENTIFIER')
|
132
|
+
rule('primary' => 'LEFT_PAREN expression RIGHT_PAREN')
|
133
|
+
rule('primary' => 'SUPER DOT IDENTIFIER')
|
134
|
+
|
135
|
+
# Utility rules
|
136
|
+
rule('function' => 'IDENTIFIER LEFT_PAREN params_opt RIGHT_PAREN block')
|
137
|
+
rule('params_opt' => 'parameters')
|
138
|
+
rule('params_opt' => [])
|
139
|
+
rule('parameters' => 'parameters COMMA IDENTIFIER')
|
140
|
+
rule('parameters' => 'IDENTIFIER')
|
141
|
+
rule('arguments_opt' => 'arguments')
|
142
|
+
rule('arguments_opt' => [])
|
143
|
+
rule('arguments' => 'arguments COMMA expression')
|
144
|
+
rule('arguments' => 'expression')
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
# And now build the grammar and make it accessible via a constant
|
149
|
+
# @return [Rley::Syntax::Grammar]
|
150
|
+
Grammar = builder.grammar
|
151
|
+
end # module
|
152
|
+
end # module
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rley'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module FrontEnd
|
7
|
+
# The superclass for all tokens that have a data value.
|
8
|
+
class Literal < Rley::Lexical::Token
|
9
|
+
# @return [Datatype] The value expressed in one of the Lox datatype.
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
# Constructor.
|
13
|
+
# @param aValue [Datatype::BuiltinDatatype] the Lox data value
|
14
|
+
# @param theLexeme [String] the lexeme (= piece of text from input)
|
15
|
+
# @param aTerminal [Rley::Syntax::Terminal, String]
|
16
|
+
# The terminal symbol corresponding to the lexeme.
|
17
|
+
# @param aPosition [Rley::Lexical::Position] The position of lexeme
|
18
|
+
# in input text.
|
19
|
+
def initialize(aValue, aLexeme, aTerminal, aPosition)
|
20
|
+
super(aLexeme, aTerminal, aPosition)
|
21
|
+
@value = aValue
|
22
|
+
end
|
23
|
+
end # class
|
24
|
+
end # module
|
25
|
+
end # module
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'scanner'
|
4
|
+
require_relative 'grammar'
|
5
|
+
|
6
|
+
module Loxxy
|
7
|
+
module FrontEnd
|
8
|
+
class Parser
|
9
|
+
attr_reader(:engine)
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
# Create a Rley facade object
|
13
|
+
@engine = Rley::Engine.new do |cfg|
|
14
|
+
cfg.diagnose = true
|
15
|
+
# cfg.repr_builder = SkmBuilder
|
16
|
+
end
|
17
|
+
|
18
|
+
# Step 1. Load Lox grammar
|
19
|
+
@engine.use_grammar(Loxxy::FrontEnd::Grammar)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse the given Lox program into a parse tree.
|
23
|
+
# @param source [String] Lox program to parse
|
24
|
+
# @return [Rley::ParseTree] A parse tree equivalent to the Lox input.
|
25
|
+
def parse(source)
|
26
|
+
lexer = Scanner.new(source)
|
27
|
+
result = engine.parse(lexer.tokens)
|
28
|
+
|
29
|
+
unless result.success?
|
30
|
+
# Stop if the parse failed...
|
31
|
+
line1 = "Parsing failed\n"
|
32
|
+
line2 = "Reason: #{result.failure_reason.message}"
|
33
|
+
raise StandardError, line1 + line2
|
34
|
+
end
|
35
|
+
|
36
|
+
return engine.convert(result) # engine.to_ptree(result)
|
37
|
+
end
|
38
|
+
end # class
|
39
|
+
end # module
|
40
|
+
end # module
|