ast_ast 0.0.0 → 0.1.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.
- data/LICENSE +1 -1
- data/README.md +103 -33
- data/Rakefile +5 -48
- data/lib/ast_ast.rb +6 -6
- data/lib/ast_ast/ast.rb +162 -1
- data/lib/ast_ast/bnf.rb +187 -0
- data/lib/ast_ast/token.rb +51 -8
- data/lib/ast_ast/tokeniser.rb +35 -13
- data/lib/ast_ast/tokens.rb +308 -6
- data/lib/ast_ast/tree.rb +74 -2
- data/lib/ast_ast/version.rb +3 -0
- data/spec/ast_ast/token_spec.rb +62 -0
- data/spec/ast_ast/tokeniser_spec.rb +101 -0
- data/spec/ast_ast/tokens_spec.rb +329 -0
- data/spec/spec_helper.rb +11 -0
- metadata +27 -45
- data/.document +0 -5
- data/.gitignore +0 -24
- data/VERSION +0 -1
- data/lib/ast_tokens.rb +0 -8
- data/test/helper.rb +0 -10
- data/test/test_ast_ast.rb +0 -5
- data/test/test_tokeniser.rb +0 -13
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,50 +1,120 @@
|
|
1
1
|
# AstAst
|
2
2
|
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
AaaaaaaAaaaaAAt TsssssTs
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
4
|
+
sSSSSs
|
5
|
+
saaAAA Tttttts
|
6
|
+
sa tT t TT tt
|
7
|
+
saaaaaaA t tT t TT Ts
|
8
|
+
sa tt T tT t TT Ts
|
9
|
+
- - AaaaaaaAaaaaAAt TsssssTs
|
10
|
+
tT t tSTSsssSTt tt
|
11
|
+
t tt t tt
|
12
|
+
st tt st tt
|
13
|
+
S t tt S t tt
|
14
|
+
st tt st tt
|
15
|
+
t tt t tt
|
16
|
+
t tts t tts
|
17
|
+
S tS s S tS ss
|
18
|
+
tsssstss tsssstSSS
|
19
|
+
|
19
20
|
|
20
21
|
|
22
|
+
## How To
|
23
|
+
### String -> Ast::Tokens
|
21
24
|
|
25
|
+
So you have a string, eg:
|
22
26
|
|
23
|
-
|
27
|
+
an example String, lorem!
|
28
|
+
|
29
|
+
And you want to turn it into a set of tokens, for some reason, but can't be bothered messing around with `strscan` so instead use `Ast::Tokeniser`
|
30
|
+
|
31
|
+
string = "an example String, lorem!"
|
32
|
+
|
33
|
+
class StringTokens < Ast::Tokeniser
|
34
|
+
|
35
|
+
# A rule uses a regular expression to match against the string given
|
36
|
+
# if it matches a token is created with the name given, eg. +:article+
|
37
|
+
rule :article, /an|a|the/
|
38
|
+
rule :word, /[a-z]+/
|
39
|
+
rule :punct, /,|\.|!/
|
40
|
+
|
41
|
+
# A rule can be passed a block that then modifies the match and returns
|
42
|
+
# something new in it's place, here we are removing the capital.
|
43
|
+
rule :pronoun, /[A-Z][a-z]+/ do |i|
|
44
|
+
i.downcase
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
StringTokens.tokenise(string)
|
49
|
+
#=> #< [0] <:article, "an">, <:word, "example">, <:pronoun, "string">, <:punct, ",">, <:word, "lorem">, <:punct, "!"> >
|
50
|
+
|
51
|
+
|
52
|
+
### Ast::Tokens -> Ast::Tree
|
53
|
+
|
54
|
+
Later.
|
24
55
|
|
25
56
|
## Goals/Ideas
|
26
57
|
|
27
|
-
|
58
|
+
Now that it is possible to take a string and turn it into a set of tokens, I want to be able to take the tokens and turn them into a tree structure. This should be easy to write using a similar DSL to Tokeniser. See below for an idea on how this might be done, though of course when I start writing it, it will change _a lot_.
|
59
|
+
|
60
|
+
### Ast::Ast
|
61
|
+
|
62
|
+
Imagine we have a string:
|
28
63
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
rule :word, /[a-zA-Z0-9]+/
|
64
|
+
string = <<EOS
|
65
|
+
def method
|
66
|
+
print 'hi'
|
33
67
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
68
|
+
EOS
|
69
|
+
|
70
|
+
Which becomes these tokens:
|
71
|
+
|
72
|
+
tokens #=> [:defn], [:id, 'method'], [:id, 'print'], [:string, 'Hi'], [:end]
|
73
|
+
|
74
|
+
We're looking for a tree like this:
|
75
|
+
|
76
|
+
tree #=> [:defn, 'method', [
|
77
|
+
[:id, 'print', [
|
78
|
+
[:string, 'Hi']
|
79
|
+
]]
|
80
|
+
]]
|
81
|
+
|
82
|
+
Then the class could look something like (something being the keyword):
|
83
|
+
|
84
|
+
class MyAst < Ast::Ast
|
37
85
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
86
|
+
# create a defn token
|
87
|
+
token :defn do
|
88
|
+
[
|
89
|
+
# start with :defn
|
90
|
+
:defn,
|
91
|
+
|
92
|
+
# get the name of method by reading next :id
|
93
|
+
read_next(:id), # if not :id throw error
|
94
|
+
|
95
|
+
# read rest of block, until the matching :end
|
96
|
+
[read_until(:end)]
|
97
|
+
]
|
98
|
+
end
|
99
|
+
|
100
|
+
# allows you to use the name given in place of a list of token names
|
101
|
+
group :literal, [:string, :integer, :float]
|
102
|
+
# really just creates an array which responds to name given
|
103
|
+
group :defined, [:print, :puts, :putc, :gets, :getc]
|
104
|
+
|
105
|
+
token :id do |t, v|
|
106
|
+
case v
|
107
|
+
when 'method'
|
108
|
+
v
|
109
|
+
when :defined
|
110
|
+
[:call, v, [read_next(:literal)]]
|
111
|
+
else
|
112
|
+
[t, v]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
token(:string) {|i| i } # not really necessary
|
43
117
|
end
|
44
|
-
input = "--along -sh aword"
|
45
|
-
MyTokeniser.tokenise(input)
|
46
|
-
#=> #<Ast::Tokens [[:long, "along"], [:short, "s"], [:short, "h"], [:word, "aword"]]>
|
47
|
-
|
48
118
|
|
49
119
|
## Copyright
|
50
120
|
|
data/Rakefile
CHANGED
@@ -1,53 +1,10 @@
|
|
1
|
-
require 'rubygems'
|
2
1
|
require 'rake'
|
3
2
|
|
4
|
-
|
5
|
-
require 'jeweler'
|
6
|
-
Jeweler::Tasks.new do |gem|
|
7
|
-
gem.name = "ast_ast"
|
8
|
-
gem.summary = %Q{String -> Tokens -> AST}
|
9
|
-
gem.description = %Q{Easily convert strings to Tokens and then on to an Abstract Syntax Tree easily. (Very far from finished!)}
|
10
|
-
gem.email = "m@hawx.me"
|
11
|
-
gem.homepage = "http://github.com/hawx/ast_ast"
|
12
|
-
gem.authors = ["Joshua Hawxwell"]
|
13
|
-
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
-
gem.add_development_dependency "yard", ">= 0"
|
15
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
16
|
-
end
|
17
|
-
Jeweler::GemcutterTasks.new
|
18
|
-
rescue LoadError
|
19
|
-
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
-
end
|
21
|
-
|
22
|
-
require 'rake/testtask'
|
23
|
-
Rake::TestTask.new(:test) do |test|
|
24
|
-
test.libs << 'lib' << 'test'
|
25
|
-
test.pattern = 'test/**/test_*.rb'
|
26
|
-
test.verbose = true
|
27
|
-
end
|
28
|
-
|
29
|
-
begin
|
30
|
-
require 'rcov/rcovtask'
|
31
|
-
Rcov::RcovTask.new do |test|
|
32
|
-
test.libs << 'test'
|
33
|
-
test.pattern = 'test/**/test_*.rb'
|
34
|
-
test.verbose = true
|
35
|
-
end
|
36
|
-
rescue LoadError
|
37
|
-
task :rcov do
|
38
|
-
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
task :test => :check_dependencies
|
43
|
-
|
44
|
-
task :default => :test
|
3
|
+
require File.expand_path('../lib/ast_ast/version', __FILE__)
|
45
4
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
task :yardoc do
|
51
|
-
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
5
|
+
namespace :release do
|
6
|
+
task :tag do
|
7
|
+
system("git tag v#{Ast::VERSION}")
|
8
|
+
system('git push origin --tags')
|
52
9
|
end
|
53
10
|
end
|
data/lib/ast_ast.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
|
2
|
-
$:.unshift File.dirname(__FILE__)
|
1
|
+
$: << File.dirname(__FILE__)
|
3
2
|
|
4
3
|
require 'strscan'
|
5
4
|
|
6
|
-
require 'ast_ast/ast'
|
7
|
-
require 'ast_ast/tree'
|
5
|
+
# require 'ast_ast/ast'
|
6
|
+
# require 'ast_ast/tree'
|
7
|
+
|
8
8
|
require 'ast_ast/tokeniser'
|
9
|
-
require 'ast_ast/
|
10
|
-
require 'ast_ast/
|
9
|
+
require 'ast_ast/token'
|
10
|
+
require 'ast_ast/tokens'
|
data/lib/ast_ast/ast.rb
CHANGED
@@ -1,5 +1,166 @@
|
|
1
1
|
module Ast
|
2
2
|
class Ast
|
3
|
+
attr_accessor :tokens, :token_descs, :block_descs, :groups
|
4
|
+
|
5
|
+
# @see Ast::Ast#token
|
6
|
+
class TokenDesc
|
7
|
+
attr_accessor :name, :block
|
8
|
+
|
9
|
+
def initialize(name, &block)
|
10
|
+
@name = name
|
11
|
+
@block = block || Proc.new {|t| t}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @see Ast::Ast#group
|
16
|
+
class Group
|
17
|
+
attr_accessor :name, :items
|
18
|
+
|
19
|
+
def initialize(name, items)
|
20
|
+
@name = name
|
21
|
+
@items = items
|
22
|
+
end
|
23
|
+
|
24
|
+
# @see Array#include?
|
25
|
+
def include?(arg)
|
26
|
+
@items.include?(arg)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @see Ast::Ast#block
|
31
|
+
class BlockDesc
|
32
|
+
attr_accessor :open, :close, :block
|
33
|
+
|
34
|
+
def initialize(open, close, &block)
|
35
|
+
@open, @close = open, close
|
36
|
+
@block = block || Proc.new {|b| b}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Creates a new token within the subclass. The block is executed
|
41
|
+
# when the token is found during the execution of #astify.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
#
|
45
|
+
# class TestAst < Ast::Ast
|
46
|
+
# token :test do
|
47
|
+
# p 'test'
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
def self.token(name, &block)
|
52
|
+
@token_descs ||= []
|
53
|
+
@token_descs << TokenDesc.new(name, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Creates a new group of token types, this allows you to refer
|
57
|
+
# to multiple tokens easily.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
#
|
61
|
+
# group :names, [:john, :dave, :josh]
|
62
|
+
#
|
63
|
+
def self.group(name, items)
|
64
|
+
@groups ||= []
|
65
|
+
@groups << Group.new(name, items)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates a block which begins with a certain token and ends with
|
69
|
+
# different token.
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
#
|
73
|
+
# block :begin => :end do |r|
|
74
|
+
# ...
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
def self.block(t, &block)
|
78
|
+
@block_descs ||= []
|
79
|
+
@block_descs << BlockDesc.new(t.keys[0], t.values[0], &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
def self.astify(tokens)
|
84
|
+
@tokens = tokens
|
85
|
+
t = find_block
|
86
|
+
t = run_tokens(t, @token_descs)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.run_tokens(tok, descs)
|
90
|
+
r = []
|
91
|
+
@curr_tree = tok
|
92
|
+
|
93
|
+
until tok.eot?
|
94
|
+
i = tok.scan
|
95
|
+
case i
|
96
|
+
when Token
|
97
|
+
# run the token
|
98
|
+
_desc = descs.find_all{|j| j.name == i.type}[0]
|
99
|
+
if _desc
|
100
|
+
r << _desc.block.call(i)
|
101
|
+
else
|
102
|
+
r << i
|
103
|
+
end
|
104
|
+
when Tree
|
105
|
+
# run the whole branch
|
106
|
+
r << run_tokens(i, descs)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
r
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.find_block(curr_desc=nil)
|
114
|
+
body = Tree.new
|
115
|
+
|
116
|
+
until @tokens.eot?
|
117
|
+
# Check if closes current search
|
118
|
+
if curr_desc && curr_desc.close == @tokens.curr_item.type
|
119
|
+
@tokens.inc
|
120
|
+
return body
|
121
|
+
|
122
|
+
# Check if close token in wrong place
|
123
|
+
elsif @block_descs.map(&:close).include?(@tokens.curr_item.type)
|
124
|
+
raise "close found before open: #{@tokens.curr_item}"
|
125
|
+
|
126
|
+
# Check if open token
|
127
|
+
elsif @block_descs.map(&:open).include?(@tokens.curr_item.type)
|
128
|
+
_desc = @block_descs.find_all {|i| i.open == @tokens.curr_item.type }[0]
|
129
|
+
@tokens.inc
|
130
|
+
found = find_block(_desc)
|
131
|
+
body << Tree.new(_desc.block.call(found))
|
132
|
+
|
133
|
+
# Otherwise add to body, and start with next token
|
134
|
+
else
|
135
|
+
body << @tokens.curr_item
|
136
|
+
@tokens.inc
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
body
|
141
|
+
end
|
142
|
+
|
143
|
+
# @group For #token block
|
144
|
+
|
145
|
+
# @see Tokens#scan
|
146
|
+
def self.scan(type=nil)
|
147
|
+
@curr_tree.scan(type)
|
148
|
+
end
|
149
|
+
|
150
|
+
# @see Tokens#check
|
151
|
+
def self.check(type=nil)
|
152
|
+
@curr_tree.check(type)
|
153
|
+
end
|
154
|
+
|
155
|
+
# @see Tokens#check
|
156
|
+
def self.scan_until(type)
|
157
|
+
@curr_tree.scan_until(type)
|
158
|
+
end
|
159
|
+
|
160
|
+
# @return [Ast::Tree] current tree being read
|
161
|
+
def self.curr_tree
|
162
|
+
@curr_tree
|
163
|
+
end
|
3
164
|
|
4
165
|
end
|
5
|
-
end
|
166
|
+
end
|
data/lib/ast_ast/bnf.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
$: << File.dirname(__FILE__)
|
2
|
+
require 'token'
|
3
|
+
|
4
|
+
module Ast
|
5
|
+
|
6
|
+
# Allows you to describe the tree using BNF style syntax.
|
7
|
+
#
|
8
|
+
# In normal BNF you would write something like:
|
9
|
+
#
|
10
|
+
# <LETTER> ::= a|b|c|d|...|X|Y|Z
|
11
|
+
# <WORD> ::= <WORD><LETTER>|<LETTER>
|
12
|
+
# <QUOTE> ::= '
|
13
|
+
# <STRING> ::= <QUOTE><WORD><QUOTE>
|
14
|
+
#
|
15
|
+
# With Ast::BNF, assuming you have the correct tokens, it would
|
16
|
+
# become:
|
17
|
+
#
|
18
|
+
# define "Word", ["Word", :letter], :letter
|
19
|
+
# define "String", [:quote, "Word", :quote]
|
20
|
+
#
|
21
|
+
class BNF
|
22
|
+
attr_accessor :tokens, :defs
|
23
|
+
|
24
|
+
class Definition
|
25
|
+
attr_accessor :name, :rules
|
26
|
+
|
27
|
+
def initialize(name, rules, klass)
|
28
|
+
@name = name
|
29
|
+
@rules = rules.map {|i| i.is_a?(Array) ? i : [i] }
|
30
|
+
@klass = klass
|
31
|
+
end
|
32
|
+
|
33
|
+
# Gets the order of the Definition, this does require
|
34
|
+
# access to the other definitions. Here's why:
|
35
|
+
#
|
36
|
+
# The order of a definition is basically how many (max)
|
37
|
+
# times would you have to loop thorugh to get to a
|
38
|
+
# terminal rule. So from the example below,
|
39
|
+
#
|
40
|
+
# <LETTER> ::= a|b|c|d|...|X|Y|Z #=> terminal
|
41
|
+
# <WORD> ::= <WORD><LETTER>|<LETTER> #=> 1st order
|
42
|
+
# <STRING> ::= '<WORD>' #=> 2nd order
|
43
|
+
#
|
44
|
+
# Here it is easy to see that <LETTER> is terminal, no
|
45
|
+
# other rule will have to be looked at to determine if
|
46
|
+
# something is a <LETTER>. For a <WORD> you have to look
|
47
|
+
# at the <LETTER> definition, so this is 1st order. And
|
48
|
+
# for <STRING>, you need to look at <WORD> which in turn
|
49
|
+
# looks at <LETTER>, so you are going back 2 steps.
|
50
|
+
#
|
51
|
+
# @return [Integer] order of definition
|
52
|
+
#
|
53
|
+
def order
|
54
|
+
if terminal?
|
55
|
+
0
|
56
|
+
elsif self_referential?
|
57
|
+
1
|
58
|
+
else
|
59
|
+
r = 0
|
60
|
+
@rules.each do |rule|
|
61
|
+
# Only interested in rule with recursion
|
62
|
+
if rule.size > 1
|
63
|
+
rule.each do |elem|
|
64
|
+
# Only interested in references
|
65
|
+
if elem.is_a? String
|
66
|
+
b = @klass.defs.find_all {|i| i.name == elem}[0].order + 1
|
67
|
+
r = b if b > r # swap if higher
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
r
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# A terminal defintion does not reference any other
|
77
|
+
# definitions. This is largely irrelevent as Ast::Tokeniser
|
78
|
+
# should take care of this but it may be useful in some
|
79
|
+
# cases.
|
80
|
+
#
|
81
|
+
# @return [Boolean] whether contains just terminal elements
|
82
|
+
#
|
83
|
+
def terminal?
|
84
|
+
@rules.each do |r|
|
85
|
+
if r.is_a? Array
|
86
|
+
r.each do |i|
|
87
|
+
return false if i.is_a? String
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
true
|
92
|
+
end
|
93
|
+
|
94
|
+
# A Definition is self referential if the only refernce to
|
95
|
+
# another rule is to itself or if the other references are
|
96
|
+
# to terminal rule.
|
97
|
+
#
|
98
|
+
# This is not a perfect definition of what "self referential"
|
99
|
+
# really means but it does help when finding the order!
|
100
|
+
#
|
101
|
+
# @return [Boolean] whether the definition is self referential
|
102
|
+
#
|
103
|
+
def self_referential?
|
104
|
+
r = false
|
105
|
+
@rules.each do |rule|
|
106
|
+
rule.each do |elem|
|
107
|
+
if elem == @name
|
108
|
+
r = true
|
109
|
+
else
|
110
|
+
k = @klass.defs.find_all{|i| i.name == elem}[0]
|
111
|
+
if k && k.terminal?
|
112
|
+
r = true
|
113
|
+
else
|
114
|
+
return false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
r
|
120
|
+
end
|
121
|
+
|
122
|
+
def inspect; "#<Ast::BNF::Definition #{@name}>"; end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
def initialize(name, &block)
|
127
|
+
@block = block
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_tree(tokens)
|
131
|
+
self.instance_eval(&@block)
|
132
|
+
|
133
|
+
# get matrix of defs in order by order
|
134
|
+
defs_orders = @defs.collect {|i| [i.order, i]}
|
135
|
+
ordered_defs = []
|
136
|
+
defs_orders.each do |i|
|
137
|
+
ordered_defs[i[0]] ||= []
|
138
|
+
ordered_defs[i[0]] << i[1]
|
139
|
+
end
|
140
|
+
|
141
|
+
result = []
|
142
|
+
ordered_defs.each do |order|
|
143
|
+
|
144
|
+
order.each do |definition|
|
145
|
+
c = tokens.scan
|
146
|
+
|
147
|
+
definition.rules.each do |rule|
|
148
|
+
list = tokens.peek(rule.size)
|
149
|
+
|
150
|
+
res = []
|
151
|
+
rule.zip(list) do |(a, b)|
|
152
|
+
next if b.nil?
|
153
|
+
if a == b.type
|
154
|
+
res << b.value
|
155
|
+
end
|
156
|
+
end
|
157
|
+
next if res.size != rule.size
|
158
|
+
p [definition.name, res.join('')]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
tokens
|
164
|
+
end
|
165
|
+
|
166
|
+
def define(name, *args)
|
167
|
+
@defs ||= []
|
168
|
+
@defs << Definition.new(name, args, self)
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
# This is here for testing only! Better name is required
|
176
|
+
def bnf_definition(name, &block)
|
177
|
+
Ast::BNF.new(name, &block)
|
178
|
+
end
|
179
|
+
|
180
|
+
test = bnf_definition('hello') do
|
181
|
+
define "Digit", :number
|
182
|
+
define "Letter", :letter
|
183
|
+
define "Number", ["Number", "Digit"], "Digit"
|
184
|
+
define "Word", ["Word", "Letter"], "Letter"
|
185
|
+
define "String", [:quote, "Word", :quote]
|
186
|
+
end
|
187
|
+
p test.to_tree Ast::Tokens.new([[:letter, 'a'], [:letter, 'b'], [:number, '5'], [:number, '9']])
|