loxxy 0.0.5 → 0.0.6
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/CHANGELOG.md +14 -0
- data/README.md +51 -7
- data/lib/loxxy.rb +1 -0
- data/lib/loxxy/ast/ast_builder.rb +74 -0
- data/lib/loxxy/front_end/grammar.rb +5 -3
- data/lib/loxxy/front_end/parser.rb +56 -0
- data/lib/loxxy/version.rb +1 -1
- data/spec/front_end/parser_spec.rb +96 -0
- data/spec/spec_helper.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de9d3c24db15863c6ea99c2a9c8a2e413b2a243ddf79de28da5583d1944fc4e4
|
4
|
+
data.tar.gz: a5f6dacfd67b6833e3c051273765e1d94c95d1d1700a974a09a3caf0519a1570
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c4bd62b1a9cd960ee97d807a5f44a26cc360df4344975210ca967117c62585a2138fef382ffb5ea7d79f002b9a9a951ba77622cb8d799973c0ad8a24ffbae52
|
7
|
+
data.tar.gz: 5b14b5c4a6ae98f5e1d9a819160393393654cc3d817e36f1da68cd12fcfedfbce47097463dc6babcf36b5582e970ea41b4cef5ebceb768b962c8bd7b02bcb553
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## [0.0.6] - 2021-01-03
|
2
|
+
- First iteration of a parser with AST generation.
|
3
|
+
|
4
|
+
## Added
|
5
|
+
- Class `Parser` this one generates AST's (Abstract Syntax Tree)
|
6
|
+
- Class `AST::ASTBuilder` default code to generate an AST.
|
7
|
+
|
8
|
+
## Changed
|
9
|
+
- File `spec_helper.rb`: removed Bundler dependency
|
10
|
+
- File `README.md` Added example with AST visualization.
|
11
|
+
|
12
|
+
## Fixed
|
13
|
+
- File `grammar.rb` ensure that the constant `Grammar` is created once only.
|
14
|
+
|
1
15
|
## [0.0.5] - 2021-01-02
|
2
16
|
- Improved example in `README.md`, code re-styling to please `Rubocop` 1.7
|
3
17
|
|
data/README.md
CHANGED
@@ -12,16 +12,19 @@ a simple language used in Bob Nystrom's online book [Crafting Interpreters](http
|
|
12
12
|
## Roadmap
|
13
13
|
- [DONE] Scanner (tokenizer)
|
14
14
|
- [DONE] Raw parser. It parses Lox programs and generates a parse tree.
|
15
|
-
- [
|
16
|
-
- [
|
15
|
+
- [DONE] Tailored parser for generating AST (Abstract Syntax Tree)
|
16
|
+
- [STARTED] Custom AST builder class
|
17
17
|
- [TODO] Hierarchy classes for representing Lox expressions in AST
|
18
18
|
- [TODO] Interpreter or transpiler
|
19
19
|
|
20
|
-
|
21
20
|
## Example
|
22
|
-
|
21
|
+
The __loxxy__ hosts two distinct parsers classes (`RawParser` and `Parser`).
|
22
|
+
- A `RawParser` instance is able to recognize Lox input and to generate
|
23
23
|
a concrete parse tree from it.
|
24
|
+
- A `Parser` instance can also parse Lox source code but will generate an AST
|
25
|
+
(Abstract Syntax Tree) that will be used by the future tree-walking interpreter.
|
24
26
|
|
27
|
+
### Example using RawParser class
|
25
28
|
```ruby
|
26
29
|
require 'loxxy'
|
27
30
|
|
@@ -74,9 +77,50 @@ program
|
|
74
77
|
+-- EOF: ''
|
75
78
|
```
|
76
79
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
+
### Example using Parser class
|
81
|
+
Except for the `Parser` class name, this example is identical
|
82
|
+
to the previous one.
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
require 'loxxy'
|
86
|
+
|
87
|
+
|
88
|
+
lox_input = <<-LOX_END
|
89
|
+
// Your first Lox program!
|
90
|
+
print "Hello, world!";
|
91
|
+
LOX_END
|
92
|
+
|
93
|
+
# Show that the parser accepts the above program
|
94
|
+
parser = Loxxy::FrontEnd::Parser.new
|
95
|
+
|
96
|
+
# Now parse the input into an abstract parse tree...
|
97
|
+
ptree = parser.parse(lox_input)
|
98
|
+
|
99
|
+
# Display the parse tree thanks to Rley utility classes...
|
100
|
+
visitor = Rley::ParseTreeVisitor.new(ptree)
|
101
|
+
tree_formatter = Rley::Formatter::Asciitree.new($stdout)
|
102
|
+
tree_formatter.render(visitor)
|
103
|
+
```
|
104
|
+
|
105
|
+
However the output differs significantly:
|
106
|
+
```
|
107
|
+
program
|
108
|
+
+-- declaration_star
|
109
|
+
| +-- printStmt
|
110
|
+
| +-- PRINT: 'print'
|
111
|
+
| +-- logic_or
|
112
|
+
| | +-- logic_and
|
113
|
+
| | +-- equality
|
114
|
+
| | +-- comparison
|
115
|
+
| | +-- term
|
116
|
+
| | +-- factor
|
117
|
+
| | +-- call
|
118
|
+
| | +-- STRING: '"Hello, world!"'
|
119
|
+
| +-- SEMICOLON: ';'
|
120
|
+
+-- EOF: ''
|
121
|
+
```
|
122
|
+
|
123
|
+
|
80
124
|
|
81
125
|
## Installation
|
82
126
|
|
data/lib/loxxy.rb
CHANGED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../datatype/all_datatypes'
|
4
|
+
|
5
|
+
module Loxxy
|
6
|
+
module Ast
|
7
|
+
# The purpose of ASTBuilder is to build piece by piece an AST
|
8
|
+
# (Abstract Syntax Tree) from a sequence of input tokens and
|
9
|
+
# visit events produced by walking over a GFGParsing object.
|
10
|
+
class ASTBuilder < Rley::ParseRep::ASTBaseBuilder
|
11
|
+
# Terminal2NodeClass = {
|
12
|
+
# 'FALSE' => Datatype::False,
|
13
|
+
# 'NIL' => Datatype::Nil,
|
14
|
+
# 'NUMBER' => Datatype::Number,
|
15
|
+
# 'STRING' => Datatype::LXString,
|
16
|
+
# 'TRUE' => Datatype::True
|
17
|
+
# }.freeze
|
18
|
+
|
19
|
+
attr_reader :strict
|
20
|
+
|
21
|
+
# Create a new AST builder instance.
|
22
|
+
# @param theTokens [Array<Rley::Lexical::Token>] The sequence of input tokens.
|
23
|
+
def initialize(theTokens)
|
24
|
+
super(theTokens)
|
25
|
+
@strict = false
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def terminal2node
|
31
|
+
Terminal2NodeClass
|
32
|
+
end
|
33
|
+
|
34
|
+
# Method override
|
35
|
+
def new_leaf_node(_production, _terminal, aTokenPosition, aToken)
|
36
|
+
Rley::PTree::TerminalNode.new(aToken, aTokenPosition)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Factory method for creating a parent node object.
|
40
|
+
# @param aProduction [Production] Production rule
|
41
|
+
# @param aRange [Range] Range of tokens matched by the rule
|
42
|
+
# @param theTokens [Array] The input tokens
|
43
|
+
# @param theChildren [Array] Children nodes (one per rhs symbol)
|
44
|
+
def new_parent_node(aProduction, aRange, theTokens, theChildren)
|
45
|
+
mth_name = method_name(aProduction.name)
|
46
|
+
if respond_to?(mth_name, true)
|
47
|
+
node = send(mth_name, aProduction, aRange, theTokens, theChildren)
|
48
|
+
else
|
49
|
+
# Default action...
|
50
|
+
node = case aProduction.rhs.size
|
51
|
+
when 0
|
52
|
+
return_epsilon(aRange, theTokens, theChildren)
|
53
|
+
when 1
|
54
|
+
return_first_child(aRange, theTokens, theChildren)
|
55
|
+
else
|
56
|
+
if strict
|
57
|
+
msg = "Don't know production '#{aProduction.name}'"
|
58
|
+
raise StandardError, msg
|
59
|
+
else
|
60
|
+
node = Rley::PTree::NonTerminalNode.new(aProduction.lhs, aRange)
|
61
|
+
theChildren&.reverse_each do |child|
|
62
|
+
node.add_subnode(child) if child
|
63
|
+
end
|
64
|
+
|
65
|
+
node
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
node
|
71
|
+
end
|
72
|
+
end # class
|
73
|
+
end # module
|
74
|
+
end # module
|
@@ -144,8 +144,10 @@ module Loxxy
|
|
144
144
|
rule('arguments' => 'expression')
|
145
145
|
end
|
146
146
|
|
147
|
-
|
148
|
-
|
149
|
-
|
147
|
+
unless defined?(Grammar)
|
148
|
+
# And now build the grammar and make it accessible via a constant
|
149
|
+
# @return [Rley::Syntax::Grammar]
|
150
|
+
Grammar = builder.grammar
|
151
|
+
end
|
150
152
|
end # module
|
151
153
|
end # module
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'scanner'
|
4
|
+
require_relative 'grammar'
|
5
|
+
require_relative '../ast/ast_builder'
|
6
|
+
|
7
|
+
module Loxxy
|
8
|
+
module FrontEnd
|
9
|
+
# A Lox parser that produce concrete parse trees.
|
10
|
+
# Concrete parse trees are the default kind of parse tree
|
11
|
+
# generated by the Rley library.
|
12
|
+
# They consist of two node types only:
|
13
|
+
# - NonTerminalNode
|
14
|
+
# - TerminalNode
|
15
|
+
# A NonTerminalNode has zero or more child nodes (called subnodes)
|
16
|
+
# A TerminalNode is leaf node, that is, it has no child node.
|
17
|
+
# While concrete parse tree nodes can be generated out of the box,
|
18
|
+
# they have the following drawbacks:
|
19
|
+
# - Generic node classes that aren't always suited for the needs of
|
20
|
+
# the language being processing.
|
21
|
+
# - Concrete parse tree tend to be deeply nested, which may complicate
|
22
|
+
# further processing.
|
23
|
+
class Parser
|
24
|
+
# @return [Rley::Engine] A facade object for the Rley parsing library
|
25
|
+
attr_reader(:engine)
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
# Create a Rley facade object
|
29
|
+
@engine = Rley::Engine.new do |cfg|
|
30
|
+
cfg.diagnose = true
|
31
|
+
cfg.repr_builder = Ast::ASTBuilder
|
32
|
+
end
|
33
|
+
|
34
|
+
# Step 1. Load Lox grammar
|
35
|
+
@engine.use_grammar(Loxxy::FrontEnd::Grammar)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parse the given Lox program into a parse tree.
|
39
|
+
# @param source [String] Lox program to parse
|
40
|
+
# @return [Rley::ParseTree] A parse tree equivalent to the Lox input.
|
41
|
+
def parse(source)
|
42
|
+
lexer = Scanner.new(source)
|
43
|
+
result = engine.parse(lexer.tokens)
|
44
|
+
|
45
|
+
unless result.success?
|
46
|
+
# Stop if the parse failed...
|
47
|
+
line1 = "Parsing failed\n"
|
48
|
+
line2 = "Reason: #{result.failure_reason.message}"
|
49
|
+
raise StandardError, line1 + line2
|
50
|
+
end
|
51
|
+
|
52
|
+
return engine.convert(result) # engine.to_ptree(result)
|
53
|
+
end
|
54
|
+
end # class
|
55
|
+
end # module
|
56
|
+
end # module
|
data/lib/loxxy/version.rb
CHANGED
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../spec_helper' # Use the RSpec framework
|
4
|
+
|
5
|
+
# Load the class under test
|
6
|
+
require_relative '../../lib/loxxy/front_end/parser'
|
7
|
+
|
8
|
+
module Loxxy
|
9
|
+
module FrontEnd
|
10
|
+
describe Parser do
|
11
|
+
subject { Parser.new }
|
12
|
+
|
13
|
+
context 'Initialization:' do
|
14
|
+
it 'should be initialized without argument' do
|
15
|
+
expect { Parser.new }.not_to raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should have its parse engine initialized' do
|
19
|
+
expect(subject.engine).to be_kind_of(Rley::Engine)
|
20
|
+
end
|
21
|
+
end # context
|
22
|
+
|
23
|
+
context 'Parsing blank files:' do
|
24
|
+
def check_empty_input_result(aParseTree)
|
25
|
+
# Parse results MUST to comply to grammar rule:
|
26
|
+
# program => declaration_star EOF
|
27
|
+
# where the declaration_star MUST be empty
|
28
|
+
expect(aParseTree.root.symbol.name).to eq('program')
|
29
|
+
expect(aParseTree.root.subnodes.size).to eq(1)
|
30
|
+
eof = aParseTree.root.subnodes[0]
|
31
|
+
expect(eof).to be_kind_of(Rley::PTree::TerminalNode)
|
32
|
+
expect(eof.symbol.name).to eq('EOF')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should cope with an empty input' do
|
36
|
+
ptree = subject.parse('')
|
37
|
+
check_empty_input_result(ptree)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should cope with whitespaces only input' do
|
41
|
+
ptree = subject.parse(' ' * 80 + "\n" * 20)
|
42
|
+
check_empty_input_result(ptree)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should cope with comments only input' do
|
46
|
+
input = +''
|
47
|
+
%w[First Second Third].each do |ordinal|
|
48
|
+
input << "// #{ordinal} comment line\r\n"
|
49
|
+
end
|
50
|
+
ptree = subject.parse(input)
|
51
|
+
check_empty_input_result(ptree)
|
52
|
+
end
|
53
|
+
end # context
|
54
|
+
|
55
|
+
context 'Parsing expressions:' do
|
56
|
+
# Utility method to walk towards deeply nested node
|
57
|
+
# @param aNTNode [Rley::PTree::NonTerminalNode]
|
58
|
+
# @param subnodePath[Array<Integer>] An Array of subnode indices
|
59
|
+
def walk_subnodes(aNTNode, subnodePath)
|
60
|
+
curr_node = aNTNode
|
61
|
+
subnodePath.each do |index|
|
62
|
+
curr_node = curr_node.subnodes[index]
|
63
|
+
end
|
64
|
+
|
65
|
+
curr_node
|
66
|
+
end
|
67
|
+
it 'should parse a hello world program' do
|
68
|
+
program = <<-LOX_END
|
69
|
+
// Your first Lox program!
|
70
|
+
print "Hello, world!";
|
71
|
+
LOX_END
|
72
|
+
ptree = subject.parse(program)
|
73
|
+
root = ptree.root
|
74
|
+
expect(root.symbol.name).to eq('program')
|
75
|
+
(decls, eof) = root.subnodes
|
76
|
+
expect(decls).to be_kind_of(Rley::PTree::NonTerminalNode)
|
77
|
+
expect(decls.symbol.name).to eq('declaration_star')
|
78
|
+
prnt_stmt = decls.subnodes[0]
|
79
|
+
expect(prnt_stmt).to be_kind_of(Rley::PTree::NonTerminalNode)
|
80
|
+
expect(prnt_stmt.subnodes.size).to eq(3)
|
81
|
+
expect(prnt_stmt.subnodes[0]).to be_kind_of(Rley::PTree::TerminalNode)
|
82
|
+
expect(prnt_stmt.subnodes[0].symbol.name).to eq('PRINT')
|
83
|
+
expect(prnt_stmt.subnodes[1]).to be_kind_of(Rley::PTree::NonTerminalNode)
|
84
|
+
# leaf_node = walk_subnodes(prnt_stmt, [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
85
|
+
# expect(leaf_node).to be_kind_of(Rley::PTree::TerminalNode)
|
86
|
+
# expect(leaf_node.symbol.name).to eq('STRING')
|
87
|
+
# expect(leaf_node.token.value).to eq('Hello, world!')
|
88
|
+
expect(prnt_stmt.subnodes[2]).to be_kind_of(Rley::PTree::TerminalNode)
|
89
|
+
expect(prnt_stmt.subnodes[2].symbol.name).to eq('SEMICOLON')
|
90
|
+
expect(eof).to be_kind_of(Rley::PTree::TerminalNode)
|
91
|
+
expect(eof.symbol.name).to eq('EOF')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end # describe
|
95
|
+
end # module
|
96
|
+
end # module
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: loxxy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dimitri Geshef
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rley
|
@@ -84,6 +84,7 @@ files:
|
|
84
84
|
- README.md
|
85
85
|
- Rakefile
|
86
86
|
- lib/loxxy.rb
|
87
|
+
- lib/loxxy/ast/ast_builder.rb
|
87
88
|
- lib/loxxy/datatype/all_datatypes.rb
|
88
89
|
- lib/loxxy/datatype/boolean.rb
|
89
90
|
- lib/loxxy/datatype/builtin_datatype.rb
|
@@ -94,11 +95,13 @@ files:
|
|
94
95
|
- lib/loxxy/datatype/true.rb
|
95
96
|
- lib/loxxy/front_end/grammar.rb
|
96
97
|
- lib/loxxy/front_end/literal.rb
|
98
|
+
- lib/loxxy/front_end/parser.rb
|
97
99
|
- lib/loxxy/front_end/raw_parser.rb
|
98
100
|
- lib/loxxy/front_end/scanner.rb
|
99
101
|
- lib/loxxy/version.rb
|
100
102
|
- loxxy.gemspec
|
101
103
|
- spec/datatype/lx_string_spec.rb
|
104
|
+
- spec/front_end/parser_spec.rb
|
102
105
|
- spec/front_end/raw_parser_spec.rb
|
103
106
|
- spec/front_end/scanner_spec.rb
|
104
107
|
- spec/loxxy_spec.rb
|
@@ -129,6 +132,7 @@ specification_version: 4
|
|
129
132
|
summary: An implementation of the Lox programming language. WIP
|
130
133
|
test_files:
|
131
134
|
- spec/datatype/lx_string_spec.rb
|
135
|
+
- spec/front_end/parser_spec.rb
|
132
136
|
- spec/front_end/raw_parser_spec.rb
|
133
137
|
- spec/front_end/scanner_spec.rb
|
134
138
|
- spec/loxxy_spec.rb
|