flexr 1.0.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.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +33 -0
  3. data/CONTRIBUTING.md +39 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +116 -0
  6. data/Rakefile +468 -0
  7. data/benchmark/baselines/json.json +34 -0
  8. data/benchmark/baselines/json_handwritten.rb +43 -0
  9. data/benchmark/baselines/json_rexical.rex +25 -0
  10. data/benchmark/corpora/README.md +11 -0
  11. data/benchmark/corpora/generate_json.rb +26 -0
  12. data/benchmark/golden/calculator_lexer.sha256 +1 -0
  13. data/benchmark/golden/json_lexer.sha256 +1 -0
  14. data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
  15. data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
  16. data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
  17. data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
  18. data/benchmark/golden/with_racc_lexer.sha256 +1 -0
  19. data/benchmark/run.rb +254 -0
  20. data/docs/README.md +64 -0
  21. data/docs/RELEASING.md +30 -0
  22. data/docs/adr/0001-byte-level-dfa.md +5 -0
  23. data/docs/adr/0003-leftmost-longest.md +4 -0
  24. data/docs/adr/0006-accel-not-scanner.md +4 -0
  25. data/docs/adr/0008-what-pure-ruby-means.md +5 -0
  26. data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
  27. data/docs/adr/0017-static-analysis-by-default.md +5 -0
  28. data/docs/adr/0018-prism-for-generator-only.md +4 -0
  29. data/docs/adr/0019-measured-performance-floor.md +26 -0
  30. data/docs/adr/0020-vendored-unicode-contract.md +21 -0
  31. data/docs/explanation/backends.md +33 -0
  32. data/docs/explanation/matching-semantics.md +20 -0
  33. data/docs/explanation/runtime-vs-generated.md +22 -0
  34. data/docs/explanation/security-model.md +18 -0
  35. data/docs/explanation/unicode-and-encoding.md +20 -0
  36. data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
  37. data/docs/how-to/generate-a-lexer.md +39 -0
  38. data/docs/how-to/handle-errors.md +32 -0
  39. data/docs/how-to/integrate-with-lrama.md +21 -0
  40. data/docs/how-to/integrate-with-racc.md +25 -0
  41. data/docs/how-to/migrate-from-flex.md +21 -0
  42. data/docs/how-to/migrate-from-rexical.md +23 -0
  43. data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
  44. data/docs/how-to/track-token-locations.md +27 -0
  45. data/docs/how-to/tune-performance.md +23 -0
  46. data/docs/how-to/use-states.md +36 -0
  47. data/docs/how-to/use-trailing-context.md +22 -0
  48. data/docs/internals/README.md +14 -0
  49. data/docs/perf-log.md +56 -0
  50. data/docs/reference/README.md +23 -0
  51. data/docs/reference/actions.md +47 -0
  52. data/docs/reference/cli.md +80 -0
  53. data/docs/reference/compatibility.md +38 -0
  54. data/docs/reference/diagnostics.md +41 -0
  55. data/docs/reference/dsl.md +81 -0
  56. data/docs/reference/errors.md +27 -0
  57. data/docs/reference/generated-artifacts.md +50 -0
  58. data/docs/reference/public-api.md +42 -0
  59. data/docs/reference/regexp.md +39 -0
  60. data/docs/reference/runtime.md +49 -0
  61. data/docs/reference/tokens-and-locations.md +33 -0
  62. data/docs/tutorial/build-a-calculator-lexer.md +96 -0
  63. data/examples/calculator/README.md +27 -0
  64. data/examples/calculator/lexer.flexr.rb +17 -0
  65. data/examples/json/README.md +30 -0
  66. data/examples/json/lexer.flexr.rb +24 -0
  67. data/examples/ruby_subset/README.md +17 -0
  68. data/examples/ruby_subset/lexer.flexr.rb +22 -0
  69. data/examples/toy_lang/README.md +17 -0
  70. data/examples/toy_lang/lexer.flexr.rb +18 -0
  71. data/examples/with_lrama/README.md +17 -0
  72. data/examples/with_lrama/lexer.flexr.rb +13 -0
  73. data/examples/with_racc/README.md +17 -0
  74. data/examples/with_racc/lexer.flexr.rb +13 -0
  75. data/exe/flexr +7 -0
  76. data/lib/flexr/automaton/accel.rb +39 -0
  77. data/lib/flexr/automaton/analysis.rb +38 -0
  78. data/lib/flexr/automaton/byte_class_set.rb +29 -0
  79. data/lib/flexr/automaton/compiler.rb +413 -0
  80. data/lib/flexr/automaton/dfa.rb +103 -0
  81. data/lib/flexr/automaton/minimizer.rb +70 -0
  82. data/lib/flexr/automaton/nfa.rb +92 -0
  83. data/lib/flexr/cli.rb +342 -0
  84. data/lib/flexr/codegen/base.rb +17 -0
  85. data/lib/flexr/codegen/direct.rb +52 -0
  86. data/lib/flexr/codegen/firstmatch.rb +17 -0
  87. data/lib/flexr/codegen/table.rb +158 -0
  88. data/lib/flexr/codegen/table_packer.rb +61 -0
  89. data/lib/flexr/diagnostics.rb +94 -0
  90. data/lib/flexr/dsl.rb +182 -0
  91. data/lib/flexr/errors.rb +28 -0
  92. data/lib/flexr/generated.rb +125 -0
  93. data/lib/flexr/generator.rb +400 -0
  94. data/lib/flexr/importer.rb +560 -0
  95. data/lib/flexr/ir.rb +36 -0
  96. data/lib/flexr/lexer.rb +10 -0
  97. data/lib/flexr/options.rb +47 -0
  98. data/lib/flexr/rake_task.rb +27 -0
  99. data/lib/flexr/regexp/ast.rb +45 -0
  100. data/lib/flexr/regexp/char_class.rb +7 -0
  101. data/lib/flexr/regexp/normalizer.rb +117 -0
  102. data/lib/flexr/regexp/parser.rb +517 -0
  103. data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
  104. data/lib/flexr/regexp/tokenizer.rb +168 -0
  105. data/lib/flexr/regexp/unsupported.rb +7 -0
  106. data/lib/flexr/runtime/buffer.rb +112 -0
  107. data/lib/flexr/runtime/core.rb +388 -0
  108. data/lib/flexr/runtime/errors.rb +22 -0
  109. data/lib/flexr/runtime/interpreter.rb +505 -0
  110. data/lib/flexr/runtime/location.rb +26 -0
  111. data/lib/flexr/runtime/token.rb +7 -0
  112. data/lib/flexr/source/passthrough.rb +31 -0
  113. data/lib/flexr/source/prism_reader.rb +283 -0
  114. data/lib/flexr/source/static_eval.rb +145 -0
  115. data/lib/flexr/unicode/case_fold.rb +45 -0
  116. data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
  117. data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
  118. data/lib/flexr/unicode/data/case_folding.rb +9 -0
  119. data/lib/flexr/unicode/data/properties.rb +10 -0
  120. data/lib/flexr/unicode/property.rb +107 -0
  121. data/lib/flexr/unicode/reference_regexp.rb +102 -0
  122. data/lib/flexr/unicode/utf8_splitter.rb +109 -0
  123. data/lib/flexr/version.rb +5 -0
  124. data/lib/flexr.rb +81 -0
  125. data/site/README.md +22 -0
  126. data/site/astro.config.mjs +57 -0
  127. data/site/package.json +19 -0
  128. data/site/pnpm-lock.yaml +5029 -0
  129. data/site/pnpm-workspace.yaml +6 -0
  130. data/site/public/playground.js +189 -0
  131. data/site/scripts/verify-site.mjs +42 -0
  132. data/site/src/content/docs/benchmarks.md +8 -0
  133. data/site/src/content/docs/concepts/matching-semantics.md +15 -0
  134. data/site/src/content/docs/concepts/regexp-model.md +18 -0
  135. data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
  136. data/site/src/content/docs/concepts/security-model.md +15 -0
  137. data/site/src/content/docs/examples.md +17 -0
  138. data/site/src/content/docs/learn/generation.md +29 -0
  139. data/site/src/content/docs/learn/getting-started.md +56 -0
  140. data/site/src/content/docs/learn/parser-integration.md +27 -0
  141. data/site/src/content/docs/learn/runtime-mode.md +32 -0
  142. data/site/src/content/docs/reference/action-context.md +20 -0
  143. data/site/src/content/docs/reference/cli.md +22 -0
  144. data/site/src/content/docs/reference/diagnostics.md +16 -0
  145. data/site/src/content/docs/reference/dsl.md +19 -0
  146. data/site/src/content/docs/reference/public-api.md +18 -0
  147. data/site/src/content/docs/reference/regexp.md +16 -0
  148. data/site/src/content/docs/reference/runtime.md +16 -0
  149. data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
  150. data/site/src/content.config.ts +12 -0
  151. data/site/src/env.d.ts +1 -0
  152. data/site/src/layouts/SiteLayout.astro +39 -0
  153. data/site/src/pages/index.astro +174 -0
  154. data/site/src/pages/playground.astro +64 -0
  155. data/site/src/styles/custom.css +711 -0
  156. data/site/tsconfig.json +5 -0
  157. data/tools/coverage.rb +32 -0
  158. data/tools/docs_verify.rb +116 -0
  159. data/tools/gen_unicode_tables.rb +202 -0
  160. data/tools/regexp_tokenizer_reference.rb +60 -0
  161. metadata +205 -0
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Runtime
5
+ class TokenTooLargeError < LexError
6
+ CODE = "FLEXR-E012"
7
+
8
+ attr_reader :code
9
+
10
+ def initialize(message = "token exceeds max_token_size", filename: nil, byte_pos: nil, line: nil, text: nil)
11
+ @code = CODE
12
+ diagnostic = Diagnostics.error(
13
+ CODE,
14
+ message,
15
+ help: "increase max_token_size or split the input token"
16
+ )
17
+ super(message, filename: filename, byte_pos: byte_pos, line: line, text: text, diagnostic: diagnostic)
18
+ end
19
+ end
20
+ class StateStackOverflowError < LexError; end
21
+ end
22
+ end
@@ -0,0 +1,505 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "strscan"
5
+ rescue LoadError
6
+ # StringScanner is an optional accelerator; the regexp path remains valid.
7
+ end
8
+
9
+ module Flexr
10
+ module Runtime
11
+ Match = Struct.new(:rule, :start_pos, :end_pos, :total_end_pos, keyword_init: true)
12
+
13
+ class Interpreter
14
+ def initialize(lexer)
15
+ @lexer = lexer
16
+ end
17
+
18
+ def scan
19
+ machine = @lexer.class.__flexr_compiled.machines.fetch(@lexer.state)
20
+ position = @lexer.byte_pos
21
+ return nil unless @lexer.valid_utf8_at?(position)
22
+ return scan_firstmatch(position) if @lexer.class.__flexr_config.backend == :firstmatch
23
+ return scan_fast(machine, position) if fast_path?
24
+
25
+ buffer = @lexer.buffer
26
+ state = machine.dfa.start
27
+ best = reference_match(position, buffer)
28
+ cursor = position
29
+ best = consider_acceptances(machine, state, cursor, position, buffer, best) if @lexer.class.__flexr_config.options[:allow_empty_match]
30
+ acceleration_regions = acceleration_enabled? ? acceleration_regions_for(machine) : nil
31
+
32
+ while buffer.ensure_available?(cursor + 1)
33
+ if acceleration_regions && (region = acceleration_regions[state])
34
+ accelerated_end = accelerate(region, buffer, cursor)
35
+ if accelerated_end && accelerated_end > cursor
36
+ cursor = accelerated_end
37
+ best = consider_acceptances(machine, state, cursor, position, buffer, best)
38
+ next
39
+ end
40
+ end
41
+
42
+ byte = buffer.getbyte(cursor)
43
+ state = transition(machine.dfa, state, byte)
44
+ break unless state
45
+
46
+ cursor += 1
47
+ best = consider_acceptances(machine, state, cursor, position, buffer, best)
48
+ end
49
+ best
50
+ end
51
+
52
+ def scan_fast(machine, position)
53
+ buffer = @lexer.buffer
54
+ source = buffer.source
55
+ dfa = machine.dfa
56
+ accepts = dfa.accepts
57
+ rules = @lexer.class.__flexr_rules
58
+ transitions = dfa.transitions
59
+ ec = dfa.ec
60
+ direct = dfa.direct
61
+ state = dfa.start
62
+ cursor = position
63
+ best = nil
64
+
65
+ while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)
66
+ byte = source.getbyte(cursor)
67
+ state = if direct
68
+ class_id = ec[byte]
69
+ value = direct[:nxt][(state * direct[:classes]) + class_id]
70
+ value >= 0 ? value : nil
71
+ else
72
+ transitions[state][ec[byte]]
73
+ end
74
+ break unless state
75
+
76
+ cursor += 1
77
+ acceptance = accepts[state].first
78
+ next unless acceptance
79
+
80
+ rule = rules.fetch(acceptance.rule_index)
81
+ next if best && cursor == best.total_end_pos && rule.index > best.rule.index
82
+
83
+ best ||= (@match ||= Match.new)
84
+ best.rule = rule
85
+ best.start_pos = position
86
+ best.end_pos = cursor
87
+ best.total_end_pos = cursor
88
+ end
89
+ best
90
+ end
91
+
92
+ private
93
+
94
+ def reference_match(position, buffer)
95
+ return unless reference_rules?
96
+
97
+ candidates = @lexer.class.__flexr_rules.filter_map do |rule|
98
+ next unless rule_active?(rule)
99
+ next unless rule.patterns.any? { |pattern| reference_pattern?(pattern) }
100
+
101
+ matches = rule.patterns.each_with_index.filter_map do |pattern, pattern_index|
102
+ condition = rule.pattern_conditions.fetch(pattern_index)
103
+ next if condition.bol_only && !@lexer.beginning_of_line?
104
+
105
+ match = streamed_match(pattern, buffer, position, reference: reference_pattern?(pattern))
106
+ next unless match&.begin(0)&.zero?
107
+
108
+ end_position = position + match[0].bytesize
109
+ next unless @lexer.utf8_boundary?(end_position)
110
+ next if condition.end_anchor && !end_anchor_match?(buffer, end_position)
111
+
112
+ [match, condition]
113
+ end
114
+ match, _condition = matches.max_by { |item| item[0][0].bytesize }
115
+ next unless match&.begin(0)&.zero?
116
+
117
+ end_position = position + match[0].bytesize
118
+ next unless @lexer.utf8_boundary?(end_position)
119
+
120
+ trailing = trailing_length(rule, buffer, end_position)
121
+ next if rule.trailing && trailing.nil?
122
+
123
+ ensure_token_size!(end_position, position)
124
+ Match.new(rule: rule, start_pos: position, end_pos: end_position,
125
+ total_end_pos: end_position + (trailing || 0))
126
+ end
127
+ candidates.max_by { |candidate| [candidate.total_end_pos, -candidate.rule.index] }
128
+ end
129
+
130
+ def reference_rules?
131
+ return @reference_rules unless @reference_rules.nil?
132
+
133
+ @reference_rules = @lexer.class.__flexr_rules.any? do |rule|
134
+ rule.patterns.any? { |pattern| reference_pattern?(pattern) }
135
+ end
136
+ end
137
+
138
+ def fast_path?
139
+ return @fast_path unless @fast_path.nil?
140
+
141
+ rules = @lexer.class.__flexr_rules
142
+ @fast_path = !@lexer.class.__flexr_config.options[:allow_empty_match] &&
143
+ !reference_rules? && rules.none?(&:trailing) &&
144
+ rules.none? { |rule| rule.pattern_conditions.any? { |condition| condition&.bol_only || condition&.end_anchor } }
145
+ end
146
+
147
+ def reference_pattern?(pattern)
148
+ return false unless pattern.is_a?(::Regexp)
149
+ return true if pattern.source.match?(/\\[pP]\{/) || pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
150
+
151
+ @lexer.class.__flexr_config.options[:unicode] == true && @lexer.utf8_input? &&
152
+ pattern.source.match?(/\\[dDwWsS]/)
153
+ end
154
+
155
+ def scan_firstmatch(position)
156
+ buffer = @lexer.buffer
157
+ @lexer.class.__flexr_rules.sort_by(&:index).each do |rule|
158
+ next unless rule_active?(rule)
159
+
160
+ matches = rule.patterns.each_with_index.filter_map do |pattern, pattern_index|
161
+ condition = rule.pattern_conditions.fetch(pattern_index)
162
+ next if condition.bol_only && !@lexer.beginning_of_line?
163
+
164
+ regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(::Regexp.escape(pattern.to_s))
165
+ streamed_match(regexp, buffer, position, reference: reference_pattern?(regexp))
166
+ &.then { |match| [match, condition] }
167
+ rescue ArgumentError, RegexpError
168
+ nil
169
+ end
170
+ match, condition = matches.select { |candidate| candidate[0].begin(0).zero? }
171
+ .max_by { |candidate| candidate[0][0].bytesize }
172
+ next unless match
173
+
174
+ end_position = position + match[0].bytesize
175
+ next unless @lexer.utf8_boundary?(end_position)
176
+ next if condition.end_anchor && !end_anchor_match?(buffer, end_position)
177
+ trailing = trailing_length(rule, buffer, end_position)
178
+ next if rule.trailing && trailing.nil?
179
+
180
+ ensure_token_size!(end_position, position)
181
+ return reusable_match(rule, position, end_position, end_position + (trailing || 0))
182
+ end
183
+ nil
184
+ end
185
+
186
+ def trailing_length(rule, buffer, position)
187
+ return 0 unless rule.trailing
188
+
189
+ regexp = rule.trailing
190
+ match = streamed_match(regexp, buffer, position, reference: reference_pattern?(regexp))
191
+ return nil unless match&.begin(0)&.zero?
192
+
193
+ match[0].bytesize
194
+ rescue ArgumentError
195
+ nil
196
+ end
197
+
198
+ def streamed_match(pattern, buffer, position, reference: false)
199
+ pattern = regexp_pattern(pattern)
200
+ minimum = minimum_match_bytes(pattern)
201
+ loop do
202
+ subject, tail = stream_subject(buffer, position)
203
+ match = if reference
204
+ Unicode::ReferenceRegexp.match(
205
+ pattern, subject, encoding: @lexer.class.__flexr_config.encoding,
206
+ options: pattern.options, unicode: @lexer.class.__flexr_config.options[:unicode] == true
207
+ )
208
+ else
209
+ pattern.match(subject, 0)
210
+ end
211
+ return match if match && match[0].bytesize < subject.bytesize
212
+ return match if match && %i[eof invalid].include?(tail)
213
+ if !match && subject.bytesize >= minimum && tail != :incomplete
214
+ first_byte = subject.getbyte(0)
215
+ return nil unless first_byte && possible_first_byte?(pattern, first_byte)
216
+ return nil if %i[eof invalid].include?(tail)
217
+ end
218
+ return match unless can_refill_match?(buffer, position, subject.bytesize, tail)
219
+ rescue ArgumentError, RegexpError
220
+ return nil
221
+ end
222
+ end
223
+
224
+ def stream_subject(buffer, position)
225
+ return [buffer.byteslice(position...buffer.bytesize).to_s.b, buffer.eof_loaded? ? :eof : :end] unless @lexer.utf8_input?
226
+
227
+ cursor = position
228
+ while cursor < buffer.bytesize
229
+ status, length = utf8_status(buffer, cursor)
230
+ break unless status == :complete
231
+
232
+ cursor += length
233
+ end
234
+ tail = if cursor < buffer.bytesize
235
+ utf8_status(buffer, cursor).first
236
+ elsif buffer.eof_loaded?
237
+ :eof
238
+ else
239
+ :end
240
+ end
241
+ subject = buffer.byteslice(position, cursor - position).to_s.dup.force_encoding(Encoding::UTF_8)
242
+ [subject, tail]
243
+ end
244
+
245
+ def utf8_status(buffer, position)
246
+ return [:eof, 0] if position >= buffer.bytesize
247
+
248
+ first = buffer.source.getbyte(position)
249
+ return [:complete, 1] if first <= 0x7f
250
+ return [:invalid, 1] unless first.between?(0xc2, 0xf4)
251
+
252
+ length = if first <= 0xdf
253
+ 2
254
+ elsif first <= 0xef
255
+ 3
256
+ else
257
+ 4
258
+ end
259
+ return [:incomplete, length] unless buffer.ensure_available?(position + length)
260
+ return [:invalid, length] unless buffer.valid_utf8_at?(position)
261
+
262
+ [:complete, length]
263
+ end
264
+
265
+ def can_refill_match?(buffer, position, subject_size, tail)
266
+ target = if tail == :incomplete
267
+ position + subject_size + 1
268
+ else
269
+ buffer.bytesize + 1
270
+ end
271
+ buffer.ensure_available?(target)
272
+ end
273
+
274
+ def minimum_match_bytes(pattern)
275
+ return 1 if posix_pattern?(pattern)
276
+
277
+ ast = Regexp::Parser.new(pattern.source, options: pattern.options,
278
+ encoding: pattern.encoding, unicode: true).parse
279
+ minimum_ast_bytes(ast, ignorecase: pattern.options.anybits?(::Regexp::IGNORECASE))
280
+ rescue CompileError, RegexpError
281
+ 1
282
+ end
283
+
284
+ def regexp_pattern(pattern)
285
+ return pattern if pattern.is_a?(::Regexp)
286
+
287
+ ::Regexp.new(::Regexp.escape(pattern.to_s))
288
+ end
289
+
290
+ def minimum_ast_bytes(node, ignorecase: false)
291
+ case node
292
+ when Regexp::AST::Empty, Regexp::AST::Anchor then 0
293
+ when Regexp::AST::ByteRange then 1
294
+ when Regexp::AST::CodepointRange then utf8_length(node.lo)
295
+ when Regexp::AST::CharClass
296
+ ranges = node.ranges.flat_map do |range|
297
+ range.first == Regexp::AST::Property ? property_ranges(range, ignorecase: ignorecase) : [range]
298
+ end
299
+ ranges = complement_codepoint_ranges(ranges) if node.negated
300
+ ranges.map { |lo, _hi| utf8_length(lo) }.min || 1
301
+ when Regexp::AST::Seq then node.children.sum { |child| minimum_ast_bytes(child, ignorecase: ignorecase) }
302
+ when Regexp::AST::Alt then node.children.map { |child| minimum_ast_bytes(child, ignorecase: ignorecase) }.min || 0
303
+ else minimum_unknown_bytes(node)
304
+ end
305
+ end
306
+
307
+ def minimum_unknown_bytes(node)
308
+ return 0 if node.is_a?(Regexp::AST::Star)
309
+
310
+ 1
311
+ end
312
+
313
+ def possible_first_byte?(pattern, byte)
314
+ return true if posix_pattern?(pattern)
315
+
316
+ ast = Regexp::Parser.new(pattern.source, options: pattern.options,
317
+ encoding: pattern.encoding, unicode: true).parse
318
+ binary = !@lexer.utf8_input?
319
+ first_byte_ranges(ast, pattern.options, binary: binary).any? { |lo, hi| byte.between?(lo, hi) }
320
+ rescue CompileError, RegexpError
321
+ true
322
+ end
323
+
324
+ def first_byte_ranges(node, options, binary: false)
325
+ case node
326
+ when Regexp::AST::Empty, Regexp::AST::Anchor then []
327
+ when Regexp::AST::ByteRange then [[node.lo, node.hi]]
328
+ when Regexp::AST::CodepointRange
329
+ ranges = options.anybits?(::Regexp::IGNORECASE) ? Unicode::CaseFold.ranges(node.lo, node.hi) : [[node.lo, node.hi]]
330
+ return ranges if binary
331
+
332
+ ranges.flat_map { |lo, hi| Unicode::Utf8Splitter.split(lo, hi).map(&:first).map(&:first) }
333
+ .map { |value| [value, value] }
334
+ when Regexp::AST::CharClass
335
+ ranges = node.ranges.flat_map do |range|
336
+ range.first == Regexp::AST::Property ? property_ranges(range, ignorecase: options.anybits?(::Regexp::IGNORECASE)) : [range]
337
+ end
338
+ ranges = complement_codepoint_ranges(ranges) if node.negated
339
+ return ranges if binary
340
+
341
+ ranges.flat_map { |lo, hi| Unicode::Utf8Splitter.split(lo, hi).map(&:first).map(&:first) }
342
+ .map { |value| [value, value] }
343
+ when Regexp::AST::Seq
344
+ ranges = []
345
+ node.children.each do |child|
346
+ ranges.concat(first_byte_ranges(child, options, binary: binary))
347
+ break unless nullable?(child)
348
+ end
349
+ ranges
350
+ when Regexp::AST::Alt
351
+ node.children.flat_map { |child| first_byte_ranges(child, options, binary: binary) }
352
+ when Regexp::AST::Star
353
+ first_byte_ranges(node.child, options, binary: binary)
354
+ else
355
+ first_byte_fallback
356
+ end
357
+ end
358
+
359
+ def first_byte_fallback
360
+ []
361
+ end
362
+
363
+ def nullable?(node)
364
+ case node
365
+ when Regexp::AST::Empty, Regexp::AST::Anchor, Regexp::AST::Star then true
366
+ when Regexp::AST::Seq then node.children.all? { |child| nullable?(child) }
367
+ when Regexp::AST::Alt then node.children.any? { |child| nullable?(child) }
368
+ else false
369
+ end
370
+ end
371
+
372
+ def utf8_length(codepoint)
373
+ return 1 if codepoint <= 0x7f
374
+ return 2 if codepoint <= 0x7ff
375
+ return 3 if codepoint <= 0xffff
376
+
377
+ 4
378
+ end
379
+
380
+ def complement_codepoint_ranges(ranges)
381
+ result = []
382
+ cursor = 0
383
+ ranges.sort_by(&:first).each do |lo, hi|
384
+ result << [cursor, lo - 1] if cursor < lo
385
+ cursor = [cursor, hi + 1].max
386
+ end
387
+ result << [cursor, 0x10ffff] if cursor <= 0x10ffff
388
+ result
389
+ end
390
+
391
+ def property_ranges(range, ignorecase:)
392
+ ranges = Unicode::Property.ranges(range[2])
393
+ ranges = Unicode::CaseFold.merge(ranges.flat_map { |lo, hi| Unicode::CaseFold.ranges(lo, hi) }) if ignorecase
394
+ range[1] ? complement_codepoint_ranges(ranges) : ranges
395
+ end
396
+
397
+ def posix_pattern?(pattern)
398
+ pattern.is_a?(::Regexp) && pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
399
+ end
400
+
401
+ def consider_acceptances(machine, state, cursor, position, buffer, best)
402
+ machine.dfa.accepts[state].each do |acceptance|
403
+ candidate = @lexer.class.__flexr_rules.fetch(acceptance.rule_index)
404
+ next if acceptance.bol_only && !@lexer.beginning_of_line?
405
+ next unless @lexer.utf8_boundary?(cursor)
406
+ next unless !acceptance.end_anchor || end_anchor_match?(buffer, cursor)
407
+
408
+ trailing_size = trailing_length(candidate, buffer, cursor)
409
+ next if candidate.trailing && trailing_size.nil?
410
+
411
+ total_end = cursor + (trailing_size || 0)
412
+ next if best && total_end < best.total_end_pos
413
+ next if best && total_end == best.total_end_pos && acceptance.rule_index > best.rule.index
414
+
415
+ ensure_token_size!(cursor, position)
416
+ best = @match ||= Match.new
417
+ best.rule = candidate
418
+ best.start_pos = position
419
+ best.end_pos = cursor
420
+ best.total_end_pos = total_end
421
+ end
422
+ best
423
+ end
424
+
425
+ def acceleration_enabled?
426
+ @lexer.class.__flexr_config.options.fetch(:accel, :auto) != :none
427
+ end
428
+
429
+ def reusable_match(rule, start_pos, end_pos, total_end_pos)
430
+ @match ||= Match.new
431
+ @match.rule = rule
432
+ @match.start_pos = start_pos
433
+ @match.end_pos = end_pos
434
+ @match.total_end_pos = total_end_pos
435
+ @match
436
+ end
437
+
438
+ def accelerate(region, buffer, position)
439
+ mode = @lexer.class.__flexr_config.options.fetch(:accel, :auto)
440
+ binary = buffer.source.b
441
+ match_end = if %i[strscan auto].include?(mode) && defined?(StringScanner)
442
+ scanner = StringScanner.new(binary)
443
+ scanner.pos = position
444
+ length = scanner.skip(region.regexp)
445
+ length && scanner.pos
446
+ else
447
+ region.regexp.match(binary, position)&.then { |match| match.begin(0) == position ? match.end(0) : nil }
448
+ end
449
+ return match_end if match_end && match_end < buffer.bytesize
450
+ return match_end if match_end && buffer.eof_loaded?
451
+ return unless buffer.ensure_available?(buffer.bytesize + 1)
452
+
453
+ accelerate(region, buffer, position)
454
+ rescue ArgumentError
455
+ nil
456
+ end
457
+
458
+ def transition(dfa, state, byte)
459
+ if @lexer.class.__flexr_config.backend == :direct &&
460
+ @lexer.class.respond_to?(:__flexr_generated_direct_transition)
461
+ return @lexer.class.__flexr_generated_direct_transition(@lexer.state, state, byte)
462
+ end
463
+ return dfa.transition_direct(state, byte) if @lexer.class.__flexr_config.backend == :direct
464
+
465
+ dfa.transition(state, byte)
466
+ end
467
+
468
+ def acceleration_regions_for(machine)
469
+ @acceleration_regions ||= {}
470
+ return @acceleration_regions[machine.dfa] if @acceleration_regions.key?(machine.dfa)
471
+
472
+ @acceleration_regions[machine.dfa] = Automaton::Accel.extract(machine.dfa).filter_map do |region|
473
+ next if region.bytes.any? { |byte| byte >= 128 }
474
+
475
+ accepting = machine.dfa.accepts[region.state]
476
+ next if accepting.any? do |acceptance|
477
+ rule = @lexer.class.__flexr_rules.fetch(acceptance.rule_index)
478
+ acceptance.bol_only || acceptance.end_anchor || rule.trailing
479
+ end
480
+
481
+ [region.state, region]
482
+ end.to_h
483
+ end
484
+
485
+ def end_anchor_match?(buffer, position)
486
+ buffer.eof?(position) || buffer.getbyte(position) == 0x0a
487
+ end
488
+
489
+ def ensure_token_size!(end_position, position)
490
+ text_start = @lexer.more_text_start || position
491
+ @lexer.defer_token_size_check!(end_position - text_start)
492
+ end
493
+
494
+ def rule_active?(rule)
495
+ return true if rule.states.include?(:initial) && @lexer.state == :initial
496
+
497
+ state = @lexer.class.__flexr_config.states.fetch(@lexer.state)
498
+ return true if state.inclusive && rule.states.include?(:initial)
499
+
500
+ rule.states.include?(@lexer.state)
501
+ end
502
+
503
+ end
504
+ end
505
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Runtime
5
+ Location = Struct.new(
6
+ :filename, :byte_begin, :byte_end, :line_begin, :column_begin, :line_end, :column_end,
7
+ keyword_init: true
8
+ ) do
9
+ def initialize(**values)
10
+ @column_values = values.delete(:column_values)
11
+ eager_columns = values.delete(:eager_columns)
12
+ super(**values) # rubocop:disable Style/SuperArguments
13
+ self.column_begin = @column_values.first if eager_columns && @column_values
14
+ self.column_end = @column_values.last if eager_columns && @column_values
15
+ end
16
+
17
+ def column_begin
18
+ self[:column_begin] ||= @column_values&.first
19
+ end
20
+
21
+ def column_end
22
+ self[:column_end] ||= @column_values&.last
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Runtime
5
+ Token = Struct.new(:type, :value, :location, keyword_init: true)
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Source
5
+ module Passthrough
6
+ module_function
7
+
8
+ def remove_spans(source, spans, insertion:, payload:)
9
+ result = source.dup
10
+ top_level = spans.reject do |span|
11
+ spans.any? do |outer|
12
+ next false if outer.equal?(span)
13
+
14
+ outer.first <= span.first && span.last <= outer.last &&
15
+ (outer.first < span.first || span.last < outer.last)
16
+ end
17
+ end
18
+ top_level.sort_by(&:first).reverse_each do |start_offset, end_offset|
19
+ result.slice!(start_offset...end_offset)
20
+ end
21
+ result.insert(insertion, payload)
22
+ result
23
+ end
24
+
25
+ def indentation(source, offset)
26
+ line_start = source.rindex("\n", offset - 1)
27
+ source[(line_start ? line_start + 1 : 0)...offset].to_s[/\A[ \t]*/].to_s
28
+ end
29
+ end
30
+ end
31
+ end