arpaka 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +302 -0
- data/Rakefile +39 -0
- data/assets/arpaka-logo.svg +209 -0
- data/examples/calculator.y +13 -0
- data/examples/calculator_ast.y +13 -0
- data/exe/arpaka +6 -0
- data/lib/arpaka/BSDL +22 -0
- data/lib/arpaka/COPYING +58 -0
- data/lib/arpaka/actions.rb +656 -0
- data/lib/arpaka/cli.rb +60 -0
- data/lib/arpaka/generator.rb +43 -0
- data/lib/arpaka/ruby_grammar.rb +170 -0
- data/lib/arpaka/runtime/ast.rb.erb +35 -0
- data/lib/arpaka/runtime/builder.rb.erb +247 -0
- data/lib/arpaka/runtime/frontend.rb.erb +37 -0
- data/lib/arpaka/runtime/lexer.rb.erb +1371 -0
- data/lib/arpaka/source.json +11 -0
- data/lib/arpaka/upstream_rules.json +10202 -0
- data/lib/arpaka.rb +20 -0
- data/lib/lrama/ruby/action_code.rb +55 -0
- data/lib/lrama/ruby/action_scanner.rb +353 -0
- data/lib/lrama/ruby/backend.rb +103 -0
- data/lib/lrama/ruby/generator.rb +38 -0
- data/lib/lrama/ruby/parser.rb.erb +127 -0
- data/lib/lrama/ruby/version.rb +7 -0
- data/lib/lrama/ruby.rb +28 -0
- data/sig/arpaka.rbs +15 -0
- data/sig/lrama/ruby.rbs +17 -0
- metadata +86 -0
data/lib/arpaka.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lrama/ruby"
|
|
4
|
+
|
|
5
|
+
module Arpaka
|
|
6
|
+
class Error < Lrama::Ruby::Error; end
|
|
7
|
+
|
|
8
|
+
def self.generate(ruby_source:, class_name: "RubyParser")
|
|
9
|
+
Generator.new.generate(ruby_source: ruby_source, class_name: class_name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.compile(ruby_source:, class_name: "RubyParser")
|
|
13
|
+
source = generate(ruby_source: ruby_source, class_name: class_name)
|
|
14
|
+
box = ::Ruby::Box.new
|
|
15
|
+
box.eval(source)
|
|
16
|
+
box.const_get(class_name, false)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require_relative "arpaka/generator"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "action_scanner"
|
|
4
|
+
|
|
5
|
+
module Lrama
|
|
6
|
+
module Ruby
|
|
7
|
+
# Loaded only in the generator's box. Ruby strings, comments, regular
|
|
8
|
+
# expressions and instance variables must not become grammar references.
|
|
9
|
+
module ActionReferences
|
|
10
|
+
def ruby_short_reference?(offset)
|
|
11
|
+
references
|
|
12
|
+
@ruby_short_references.include?(offset)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def _references
|
|
18
|
+
result = ActionScanner.new(s_value, filename: location.grammar_file.path,
|
|
19
|
+
line: location.first_line, column: location.first_column).scan
|
|
20
|
+
@ruby_short_references = result.references.select(&:short_interpolation).map(&:first_column)
|
|
21
|
+
result.references.map do |reference|
|
|
22
|
+
attributes = if reference.number
|
|
23
|
+
{ number: reference.number, index: reference.number }
|
|
24
|
+
else
|
|
25
|
+
{ name: reference.name }
|
|
26
|
+
end
|
|
27
|
+
Grammar::Reference.new(type: :dollar, **attributes,
|
|
28
|
+
first_column: reference.first_column, last_column: reference.last_column)
|
|
29
|
+
end
|
|
30
|
+
rescue ActionScanner::Error => error
|
|
31
|
+
raise Backend::Error, error.message
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module ActionLexer
|
|
36
|
+
def lex_c_code
|
|
37
|
+
return super unless @end_symbol == "}"
|
|
38
|
+
|
|
39
|
+
reset_first_position
|
|
40
|
+
result = ActionScanner.new(@scanner.rest, filename: @grammar_file.path,
|
|
41
|
+
line: line, column: column).scan(action: true)
|
|
42
|
+
code = @scanner.string.byteslice(@scanner.pos, result.end_offset)
|
|
43
|
+
@scanner.pos += code.bytesize
|
|
44
|
+
@line += code.count("\n")
|
|
45
|
+
@head = @scanner.pos - code.bytesize + code.b.rindex("\n") + 1 if code.include?("\n")
|
|
46
|
+
[:C_DECLARATION, Lexer::Token::UserCode.new(s_value: code, location: location)]
|
|
47
|
+
rescue ActionScanner::Error => error
|
|
48
|
+
raise Backend::Error, error.message
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Lrama::Lexer::Token::UserCode.prepend(Lrama::Ruby::ActionReferences)
|
|
55
|
+
Lrama::Lexer.prepend(Lrama::Ruby::ActionLexer)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module Lrama
|
|
6
|
+
module Ruby
|
|
7
|
+
# A Ruby source lexer used for action boundary detection and references.
|
|
8
|
+
# All positions are bytes in the original source; literals are never decoded.
|
|
9
|
+
class ActionScanner
|
|
10
|
+
Reference = Data.define(:name, :number, :first_column, :last_column, :short_interpolation)
|
|
11
|
+
Result = Data.define(:end_offset, :references)
|
|
12
|
+
Heredoc = Data.define(:delimiter, :indent, :interpolate, :offset)
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
IDENTIFIER = /[a-zA-Z_\x80-\xff][a-zA-Z_0-9\x80-\xff]*[!?]?/n
|
|
16
|
+
NUMBER = /(?:0[xX][0-9a-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|[0-9][0-9_]*(?:\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?)[ri]*/
|
|
17
|
+
BEGIN_WORDS = %w[if unless while until when return yield break next rescue else elsif then do begin and or not in case for].freeze
|
|
18
|
+
END_WORDS = %w[nil true false self end __FILE__ __LINE__ __ENCODING__].freeze
|
|
19
|
+
PAIRS = { "(" => ")", "[" => "]", "{" => "}" }.freeze
|
|
20
|
+
LITERAL_PAIRS = PAIRS.merge("<" => ">").freeze
|
|
21
|
+
# Bound recursive interpolation rather than allowing SystemStackError.
|
|
22
|
+
MAX_INTERPOLATION_DEPTH = 128
|
|
23
|
+
|
|
24
|
+
def initialize(source, filename: "(grammar)", line: 1, column: 0)
|
|
25
|
+
@source = source.b
|
|
26
|
+
@scanner = StringScanner.new(@source)
|
|
27
|
+
@filename, @line, @column = filename, line, column
|
|
28
|
+
@references = []
|
|
29
|
+
@interpolation_depth = 0
|
|
30
|
+
@strict_references = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def scan(action: false)
|
|
34
|
+
@strict_references = action
|
|
35
|
+
code(action ? "}" : nil)
|
|
36
|
+
Result.new(@scanner.pos, @references.sort_by(&:first_column).freeze)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def fail_at(message, offset = @scanner.pos)
|
|
42
|
+
prefix = @source.byteslice(0, offset)
|
|
43
|
+
lines = prefix.count("\n")
|
|
44
|
+
column = lines.zero? ? @column + offset : offset - prefix.rindex("\n") - 1
|
|
45
|
+
raise Error, "#{@filename}:#{@line + lines}:#{column}: #{message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def code(terminator)
|
|
49
|
+
brackets = []
|
|
50
|
+
heredocs = []
|
|
51
|
+
state = :begin
|
|
52
|
+
spaced = false
|
|
53
|
+
until @scanner.eos?
|
|
54
|
+
offset = @scanner.pos
|
|
55
|
+
char = @scanner.peek(1)
|
|
56
|
+
if @scanner.scan(/[ \t\r\f]+/)
|
|
57
|
+
spaced = true
|
|
58
|
+
next
|
|
59
|
+
elsif @scanner.scan(/\\\r?\n/)
|
|
60
|
+
spaced = true
|
|
61
|
+
next
|
|
62
|
+
elsif @scanner.scan(/\n/)
|
|
63
|
+
heredocs.each { |item| heredoc_body(item) }
|
|
64
|
+
heredocs.clear
|
|
65
|
+
state = :begin unless [:begin, :method].include?(state)
|
|
66
|
+
spaced = true
|
|
67
|
+
next
|
|
68
|
+
elsif char == "#"
|
|
69
|
+
@scanner.scan(/[^\n]*/)
|
|
70
|
+
next
|
|
71
|
+
elsif (offset.zero? || @source.getbyte(offset - 1) == 10) && @scanner.check(/=begin(?=\s|\z)/)
|
|
72
|
+
@scanner.scan(/[^\n]*(?:\n|\z)/)
|
|
73
|
+
found = false
|
|
74
|
+
until @scanner.eos?
|
|
75
|
+
line = @scanner.scan(/[^\n]*(?:\n|\z)/)
|
|
76
|
+
if /\A=end(?:\s|\z)/.match?(line)
|
|
77
|
+
found = true
|
|
78
|
+
break
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
fail_at("Unterminated block comment", offset) unless found
|
|
82
|
+
next
|
|
83
|
+
elsif terminator.nil? && (offset.zero? || @source.getbyte(offset - 1) == 10) &&
|
|
84
|
+
@scanner.check(/__END__(?:\s|\z)/)
|
|
85
|
+
@scanner.terminate
|
|
86
|
+
return
|
|
87
|
+
elsif char == terminator && brackets.empty?
|
|
88
|
+
fail_at("Close the action/interpolation after the heredoc body", offset) unless heredocs.empty?
|
|
89
|
+
return
|
|
90
|
+
elsif PAIRS.key?(char)
|
|
91
|
+
brackets << [PAIRS.fetch(char), offset]
|
|
92
|
+
@scanner.getch
|
|
93
|
+
state = :begin
|
|
94
|
+
elsif [")", "]", "}"].include?(char)
|
|
95
|
+
fail_at("Mismatched closing #{char}", offset) unless brackets.last&.first == char
|
|
96
|
+
brackets.pop
|
|
97
|
+
@scanner.getch
|
|
98
|
+
state = :end
|
|
99
|
+
elsif ["'", '"', "`"].include?(char)
|
|
100
|
+
@scanner.getch
|
|
101
|
+
literal(char, interpolate: char != "'", offset: offset)
|
|
102
|
+
state = :end
|
|
103
|
+
elsif char == "$"
|
|
104
|
+
reference
|
|
105
|
+
state = :end
|
|
106
|
+
elsif @scanner.scan(/@@?[a-zA-Z_\x80-\xff][a-zA-Z_0-9\x80-\xff]*/n)
|
|
107
|
+
state = :end
|
|
108
|
+
elsif @scanner.check(NUMBER)
|
|
109
|
+
@scanner.scan(NUMBER)
|
|
110
|
+
state = :end
|
|
111
|
+
elsif @scanner.check(IDENTIFIER)
|
|
112
|
+
word = @scanner.scan(IDENTIFIER)
|
|
113
|
+
fail_at("__END__ is not supported in Ruby actions", offset) if word == "__END__"
|
|
114
|
+
# Method names after dot/:: are not keywords.
|
|
115
|
+
state = if state == :method
|
|
116
|
+
:bare
|
|
117
|
+
elsif BEGIN_WORDS.include?(word)
|
|
118
|
+
:begin
|
|
119
|
+
elsif END_WORDS.include?(word)
|
|
120
|
+
:end
|
|
121
|
+
else
|
|
122
|
+
:bare
|
|
123
|
+
end
|
|
124
|
+
elsif char == "/" || char == "%" || @scanner.check(/<</)
|
|
125
|
+
if char == "%" && @scanner.check(/%[qQwWiIrsx]?[a-zA-Z0-9_]*[^a-zA-Z0-9_\\\s]/n) &&
|
|
126
|
+
(@scanner.peek(2).byteslice(1, 1).match?(/[qQwWiIrsx]/n) ||
|
|
127
|
+
!@scanner.peek(2).byteslice(1, 1).match?(/[a-zA-Z0-9_]/n))
|
|
128
|
+
percent_literal(offset)
|
|
129
|
+
state = :end
|
|
130
|
+
elsif (state == :begin || (state == :bare && spaced)) &&
|
|
131
|
+
@scanner.check(/<<[-~]?(?:['"`][^\r\n]+['"`]|[a-zA-Z_][a-zA-Z_0-9]*)(?=\s|[;,)\]}]|\z)/n)
|
|
132
|
+
heredocs << heredoc_start(offset)
|
|
133
|
+
state = :end
|
|
134
|
+
elsif state == :method
|
|
135
|
+
@scanner.scan(/(?:<<|\/|%)/)
|
|
136
|
+
state = :bare
|
|
137
|
+
elsif state == :begin
|
|
138
|
+
if char == "/"
|
|
139
|
+
@scanner.getch
|
|
140
|
+
literal("/", interpolate: true, regexp: true, offset: offset)
|
|
141
|
+
@scanner.scan(/[a-z]*/)
|
|
142
|
+
elsif char == "%"
|
|
143
|
+
percent_literal(offset)
|
|
144
|
+
elsif @scanner.check(/<<[-~]?(?:['"`][^\r\n]+['"`]|[a-zA-Z_][a-zA-Z_0-9]*)(?=\s|[;,)\]}]|\z)/n)
|
|
145
|
+
heredocs << heredoc_start(offset)
|
|
146
|
+
else
|
|
147
|
+
@scanner.scan(/<</)
|
|
148
|
+
end
|
|
149
|
+
state = :end
|
|
150
|
+
else
|
|
151
|
+
@scanner.scan(/(?:<<|\/|%)=?/)
|
|
152
|
+
state = :begin
|
|
153
|
+
end
|
|
154
|
+
elsif char == "?" && state == :begin
|
|
155
|
+
@scanner.getch
|
|
156
|
+
character(offset)
|
|
157
|
+
state = :end
|
|
158
|
+
elsif @scanner.scan(/&\.|::|\.(?!\.)/)
|
|
159
|
+
state = :method
|
|
160
|
+
elsif char == ":" && state == :begin && @scanner.check(/:\s/)
|
|
161
|
+
@scanner.getch
|
|
162
|
+
state = :begin
|
|
163
|
+
elsif char == ":" && state == :begin
|
|
164
|
+
@scanner.getch
|
|
165
|
+
if @scanner.check(/['"]/)
|
|
166
|
+
quote = @scanner.getch
|
|
167
|
+
literal(quote, interpolate: quote != "'", offset: offset)
|
|
168
|
+
elsif @scanner.scan(/#\{/)
|
|
169
|
+
@interpolation_depth += 1
|
|
170
|
+
code("}")
|
|
171
|
+
@scanner.getch
|
|
172
|
+
@interpolation_depth -= 1
|
|
173
|
+
elsif !@scanner.scan(/(?:@{1,2}|\$?)[a-zA-Z_\x80-\xff][a-zA-Z_0-9\x80-\xff]*[!?]?/n) &&
|
|
174
|
+
!@scanner.scan(/(?:\[\]=?|<=>|===|==|=~|!~|!=|<=|>=|<<|>>|\*\*|[+\-~]@?|[!*\/%&|^<>`])/)
|
|
175
|
+
fail_at("Unsupported symbol literal", offset)
|
|
176
|
+
end
|
|
177
|
+
state = :end
|
|
178
|
+
elsif char == "@" && @scanner.check(/@@?[a-zA-Z_\x80-\xff][a-zA-Z_0-9\x80-\xff]*/n)
|
|
179
|
+
@scanner.scan(/@@?[a-zA-Z_\x80-\xff][a-zA-Z_0-9\x80-\xff]*/n)
|
|
180
|
+
state = :end
|
|
181
|
+
elsif char == "@" && state == :begin
|
|
182
|
+
@scanner.getch
|
|
183
|
+
state = :end
|
|
184
|
+
elsif @scanner.scan(/(?:\.\.\.?|->|\*\*=|&&=?|\|\|=?|<=>|===|==|=>|!=|!~|=~|<=|>=|>>=?|\*\*|[+\-*|&^]=|[=+\-*!,;:<>?~|&^])/)
|
|
185
|
+
state = :begin
|
|
186
|
+
else
|
|
187
|
+
fail_at("Unsupported Ruby action token #{char.inspect}", offset)
|
|
188
|
+
end
|
|
189
|
+
spaced = false
|
|
190
|
+
end
|
|
191
|
+
fail_at("Unterminated heredoc", heredocs.first.offset) unless heredocs.empty?
|
|
192
|
+
fail_at("Unclosed #{brackets.last.first}", brackets.last.last) unless brackets.empty?
|
|
193
|
+
fail_at("Unterminated Ruby action/interpolation") if terminator
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def reference(short: false)
|
|
197
|
+
offset = @scanner.pos
|
|
198
|
+
@global_variable_start = offset
|
|
199
|
+
value = @scanner.scan(/\$(?:\$|[0-9]+|[a-zA-Z_][a-zA-Z0-9_]*)/)
|
|
200
|
+
unless value
|
|
201
|
+
return scan_global_variable unless @strict_references
|
|
202
|
+
fail_at("Unsupported semantic reference", offset)
|
|
203
|
+
end
|
|
204
|
+
if value.match?(/\A\$0/) || @scanner.check(/[a-zA-Z_0-9\x80-\xff]/n)
|
|
205
|
+
return scan_global_variable unless @strict_references
|
|
206
|
+
fail_at("Unsupported semantic reference", offset)
|
|
207
|
+
end
|
|
208
|
+
number = value.match?(/\A\$[0-9]+\z/) ? value.delete_prefix("$").to_i : nil
|
|
209
|
+
@references << Reference.new(number ? nil : value.delete_prefix("$"), number,
|
|
210
|
+
offset, @scanner.pos, short)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def scan_global_variable
|
|
214
|
+
@scanner.pos = @global_variable_start || @scanner.pos
|
|
215
|
+
@scanner.scan(/\$(?:`|')/) ||
|
|
216
|
+
@scanner.scan(/\$(?:[<>]?|[!@&+~?=\/\\;,.:$-][a-zA-Z]?|[<>][^>\n]*>|[0-9]+|[a-zA-Z_][a-zA-Z0-9_]*)/)
|
|
217
|
+
fail_at("Unsupported global variable", @scanner.pos) if @scanner.pos == (@global_variable_start || @scanner.pos)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def interpolation
|
|
221
|
+
return false unless @scanner.peek(1) == "#"
|
|
222
|
+
if @scanner.scan(/#\{/)
|
|
223
|
+
@interpolation_depth += 1
|
|
224
|
+
fail_at("Interpolation nesting exceeds #{MAX_INTERPOLATION_DEPTH}") if @interpolation_depth > MAX_INTERPOLATION_DEPTH
|
|
225
|
+
code("}")
|
|
226
|
+
@scanner.getch
|
|
227
|
+
@interpolation_depth -= 1
|
|
228
|
+
elsif @scanner.scan(/#(?=\$)/)
|
|
229
|
+
reference(short: true)
|
|
230
|
+
elsif @scanner.scan(/#@@?[a-zA-Z_\x80-\xff][a-zA-Z_0-9\x80-\xff]*/n)
|
|
231
|
+
# Instance/class variable interpolation is preserved verbatim.
|
|
232
|
+
else
|
|
233
|
+
return false
|
|
234
|
+
end
|
|
235
|
+
true
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def literal(close, interpolate:, offset:, open: nil, regexp: false)
|
|
239
|
+
depth = 0
|
|
240
|
+
until @scanner.eos?
|
|
241
|
+
char = @scanner.peek(1)
|
|
242
|
+
if char == "\\"
|
|
243
|
+
@scanner.getch
|
|
244
|
+
@scanner.getch || fail_at("Unterminated escape", offset)
|
|
245
|
+
elsif interpolate && interpolation
|
|
246
|
+
next
|
|
247
|
+
elsif regexp && char == "["
|
|
248
|
+
regexp_class(offset)
|
|
249
|
+
elsif char == close
|
|
250
|
+
@scanner.getch
|
|
251
|
+
return if depth.zero?
|
|
252
|
+
depth -= 1
|
|
253
|
+
elsif open && char == open
|
|
254
|
+
depth += 1
|
|
255
|
+
@scanner.getch
|
|
256
|
+
else
|
|
257
|
+
@scanner.getch
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
fail_at("Unterminated literal (expected #{close})", offset)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def regexp_class(offset)
|
|
264
|
+
@scanner.getch
|
|
265
|
+
@scanner.scan(/\^/)
|
|
266
|
+
@scanner.scan(/\]/) # A leading ] is literal.
|
|
267
|
+
until @scanner.eos?
|
|
268
|
+
if @scanner.scan(/\\[\s\S]/)
|
|
269
|
+
next
|
|
270
|
+
elsif interpolation
|
|
271
|
+
next
|
|
272
|
+
elsif @scanner.scan(/\[:[^\]\n]*:\]/)
|
|
273
|
+
next # POSIX character class inside a bracket expression.
|
|
274
|
+
elsif @scanner.scan(/\]/)
|
|
275
|
+
return
|
|
276
|
+
else
|
|
277
|
+
@scanner.getch
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
fail_at("Unterminated regexp character class", offset)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def percent_literal(offset)
|
|
284
|
+
@scanner.getch
|
|
285
|
+
kind = @scanner.scan(/[qQwWiIrsx]/) || "Q"
|
|
286
|
+
delimiter = @scanner.getch
|
|
287
|
+
unless delimiter && delimiter.bytesize == 1 &&
|
|
288
|
+
!delimiter.match?(/[a-zA-Z0-9\\\r\n]/n)
|
|
289
|
+
fail_at("Unsupported percent literal delimiter", offset)
|
|
290
|
+
end
|
|
291
|
+
literal(LITERAL_PAIRS.fetch(delimiter, delimiter), open: LITERAL_PAIRS.key?(delimiter) ? delimiter : nil,
|
|
292
|
+
interpolate: !%w[q w i s].include?(kind), regexp: kind == "r", offset: offset)
|
|
293
|
+
@scanner.scan(/[a-z]*/) if kind == "r"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def heredoc_start(offset)
|
|
297
|
+
@scanner.scan(/<</)
|
|
298
|
+
indent = !!@scanner.scan(/[-~]/)
|
|
299
|
+
quote = @scanner.scan(/['"`]/)
|
|
300
|
+
delimiter = if quote
|
|
301
|
+
start = @scanner.pos
|
|
302
|
+
@scanner.getch until @scanner.eos? || [quote, "\r", "\n"].include?(@scanner.peek(1))
|
|
303
|
+
text = @source.byteslice(start, @scanner.pos - start)
|
|
304
|
+
fail_at("Unterminated heredoc delimiter", offset) unless @scanner.getch == quote
|
|
305
|
+
text
|
|
306
|
+
else
|
|
307
|
+
@scanner.scan(/[a-zA-Z_][a-zA-Z_0-9]*/)
|
|
308
|
+
end
|
|
309
|
+
fail_at("Unsupported heredoc delimiter", offset) unless delimiter && !delimiter.empty?
|
|
310
|
+
Heredoc.new(delimiter, indent, quote != "'", offset)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def heredoc_body(item)
|
|
314
|
+
until @scanner.eos?
|
|
315
|
+
if @scanner.pos.zero? || @source.getbyte(@scanner.pos - 1) == 10
|
|
316
|
+
start = @scanner.pos
|
|
317
|
+
@scanner.scan(/[ \t]*/) if item.indent
|
|
318
|
+
if @scanner.peek(item.delimiter.bytesize) == item.delimiter
|
|
319
|
+
@scanner.pos += item.delimiter.bytesize
|
|
320
|
+
return if @scanner.eos? || @scanner.scan(/\r?\n/)
|
|
321
|
+
end
|
|
322
|
+
@scanner.pos = start
|
|
323
|
+
end
|
|
324
|
+
if item.interpolate && interpolation
|
|
325
|
+
next
|
|
326
|
+
elsif item.interpolate && @scanner.scan(/\\[\s\S]/)
|
|
327
|
+
next
|
|
328
|
+
else
|
|
329
|
+
@scanner.getch
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
fail_at("Unterminated heredoc #{item.delimiter}", item.offset)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def character(offset)
|
|
336
|
+
fail_at("Unterminated character literal", offset) if @scanner.eos? || @scanner.check(/\s/)
|
|
337
|
+
if @scanner.scan(/\\/)
|
|
338
|
+
if @scanner.scan(/u\{/)
|
|
339
|
+
fail_at("Unterminated character escape", offset) unless @scanner.scan(/[0-9a-fA-F \t]+\}/)
|
|
340
|
+
elsif @scanner.scan(/(?:[CM]-|c)/)
|
|
341
|
+
character(offset)
|
|
342
|
+
else
|
|
343
|
+
@scanner.getch || fail_at("Unterminated character escape", offset)
|
|
344
|
+
end
|
|
345
|
+
else
|
|
346
|
+
# Consume exactly one UTF-8 codepoint (or one ASCII byte).
|
|
347
|
+
@scanner.scan(/(?:[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf4][\x80-\xbf]{3}|[\x00-\x7f])/n) ||
|
|
348
|
+
fail_at("Unsupported character encoding", offset)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lrama"
|
|
4
|
+
require "erb"
|
|
5
|
+
|
|
6
|
+
module Lrama
|
|
7
|
+
module Ruby
|
|
8
|
+
class Backend
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
def generate(source, filename:, class_name:, mode:, allow_error_rules: false)
|
|
12
|
+
recognizer = mode == :recognizer
|
|
13
|
+
grammar = Lrama::Parser.new(source, filename).parse
|
|
14
|
+
unless grammar.no_stdlib
|
|
15
|
+
path = Lrama::Command::STDLIB_FILE_PATH
|
|
16
|
+
stdlib = Lrama::Parser.new(File.read(path), path).parse
|
|
17
|
+
grammar.prepend_parameterized_rules(stdlib.parameterized_rules)
|
|
18
|
+
end
|
|
19
|
+
grammar.prepare
|
|
20
|
+
grammar.validate!
|
|
21
|
+
validate_features!(grammar, allow_error_rules: allow_error_rules) unless recognizer
|
|
22
|
+
|
|
23
|
+
states = Lrama::States.new(grammar, Lrama::Tracer.new($stderr))
|
|
24
|
+
states.compute
|
|
25
|
+
states.compute_ielr if grammar.ielr_defined?
|
|
26
|
+
if states.rr_conflicts_count.positive? || states.sr_conflicts_count != (grammar.expect || 0)
|
|
27
|
+
raise Error, "Unresolved conflicts: #{states.sr_conflicts_count} shift/reduce, #{states.rr_conflicts_count} reduce/reduce"
|
|
28
|
+
end
|
|
29
|
+
context = Lrama::Context.new(states)
|
|
30
|
+
tokens = token_names(grammar)
|
|
31
|
+
actions = grammar.rules.filter_map do |rule|
|
|
32
|
+
next if recognizer || !rule.token_code
|
|
33
|
+
# Preserve whitespace inside multiline strings and heredocs.
|
|
34
|
+
" when #{rule.id + 1} # #{rule.as_comment.gsub(/[\r\n]/, ' ')}\n#{translate_action(rule)}\n"
|
|
35
|
+
end.join
|
|
36
|
+
template = File.read(File.expand_path("parser.rb.erb", __dir__))
|
|
37
|
+
result = ::ERB.new(template, trim_mode: "-").result_with_hash(
|
|
38
|
+
class_name: class_name, context: context, tokens: tokens, actions: actions,
|
|
39
|
+
recognizer: recognizer
|
|
40
|
+
)
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def translate_action(rule)
|
|
47
|
+
code = rule.token_code.s_value.dup
|
|
48
|
+
position = rule.position_in_original_rule_rhs || rule.rhs.length
|
|
49
|
+
rule.token_code.references.reverse_each do |reference|
|
|
50
|
+
replacement = if reference.name == "$"
|
|
51
|
+
"yyval"
|
|
52
|
+
else
|
|
53
|
+
"values[#{reference.index - position - 1}]"
|
|
54
|
+
end
|
|
55
|
+
if rule.token_code.ruby_short_reference?(reference.first_column)
|
|
56
|
+
replacement = "{#{replacement}}"
|
|
57
|
+
end
|
|
58
|
+
# Lrama and action scanner locations are byte offsets, including UTF-8.
|
|
59
|
+
code = code.byteslice(0, reference.first_column) + replacement +
|
|
60
|
+
code.byteslice(reference.last_column..)
|
|
61
|
+
end
|
|
62
|
+
code
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def token_names(grammar)
|
|
66
|
+
grammar.terms.each_with_object({}) do |term, names|
|
|
67
|
+
next if term.error_symbol? || term.undef_symbol?
|
|
68
|
+
names[term.id.s_value] = term.token_id
|
|
69
|
+
if term.id.is_a?(Lrama::Lexer::Token::Char)
|
|
70
|
+
names[term.token_id.chr(Encoding::UTF_8)] = term.token_id
|
|
71
|
+
end
|
|
72
|
+
if term.alias_name&.start_with?('"')
|
|
73
|
+
names[term.alias_name[1...-1]] = term.token_id
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_features!(grammar, allow_error_rules:)
|
|
79
|
+
unsupported = []
|
|
80
|
+
unsupported << "%locations" if grammar.locations
|
|
81
|
+
unsupported << "%union" if grammar.union
|
|
82
|
+
unsupported << "%code" unless grammar.percent_codes.empty?
|
|
83
|
+
unsupported << "prologue" unless grammar.aux.prologue.to_s.strip.empty?
|
|
84
|
+
unsupported << "epilogue" unless grammar.aux.epilogue.to_s.strip.empty?
|
|
85
|
+
{
|
|
86
|
+
"%parse-param" => grammar.parse_param, "%lex-param" => grammar.lex_param,
|
|
87
|
+
"%initial-action" => grammar.initial_action, "%after-shift" => grammar.after_shift,
|
|
88
|
+
"%before-reduce" => grammar.before_reduce, "%after-reduce" => grammar.after_reduce,
|
|
89
|
+
"%after-shift-error-token" => grammar.after_shift_error_token,
|
|
90
|
+
"%after-pop-stack" => grammar.after_pop_stack
|
|
91
|
+
}.each { |name, value| unsupported << name if value }
|
|
92
|
+
unsupported << "%printer" unless grammar.printers.empty?
|
|
93
|
+
unsupported << "%error-token" unless grammar.error_tokens.empty?
|
|
94
|
+
unsupported << "%destructor" if grammar.symbols.any?(&:destructor)
|
|
95
|
+
unsupported << "typed symbols" if grammar.symbols.any?(&:tag)
|
|
96
|
+
if !allow_error_rules && grammar.rules.any? { |rule| rule.rhs.include?(grammar.error_symbol) }
|
|
97
|
+
unsupported << "error recovery rules"
|
|
98
|
+
end
|
|
99
|
+
raise Error, "Not supported by the Ruby backend yet: #{unsupported.join(', ')}" unless unsupported.empty?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lrama"
|
|
4
|
+
|
|
5
|
+
module Lrama
|
|
6
|
+
module Ruby
|
|
7
|
+
class Generator
|
|
8
|
+
def initialize
|
|
9
|
+
unless ::Ruby::Box.enabled?
|
|
10
|
+
raise Error, "Ruby Box is required. Start Ruby with RUBY_BOX=1."
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate(source, filename: "(grammar)", class_name: "Parser", mode: :parser, allow_error_rules: false)
|
|
15
|
+
unless /\A[A-Z][a-zA-Z0-9_]*\z/.match?(class_name)
|
|
16
|
+
raise Error, "class_name must be a single Ruby constant name"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
unless [:parser, :recognizer].include?(mode)
|
|
20
|
+
raise Error, "mode must be :parser or :recognizer"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
box = ::Ruby::Box.new
|
|
24
|
+
# A new box does not inherit Bundler's activated load paths.
|
|
25
|
+
box.load_path.replace($LOAD_PATH)
|
|
26
|
+
box.require(File.expand_path("backend.rb", __dir__))
|
|
27
|
+
box.require(File.expand_path("action_code.rb", __dir__)) if mode == :parser
|
|
28
|
+
begin
|
|
29
|
+
box::Lrama::Ruby::Backend.new.generate(source,
|
|
30
|
+
filename: filename, class_name: class_name, mode: mode,
|
|
31
|
+
allow_error_rules: allow_error_rules)
|
|
32
|
+
rescue box::Lrama::Ruby::Backend::Error => e
|
|
33
|
+
raise Error, e.message
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|