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.
- checksums.yaml +7 -0
- data/.rubocop.yml +33 -0
- data/CONTRIBUTING.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +468 -0
- data/benchmark/baselines/json.json +34 -0
- data/benchmark/baselines/json_handwritten.rb +43 -0
- data/benchmark/baselines/json_rexical.rex +25 -0
- data/benchmark/corpora/README.md +11 -0
- data/benchmark/corpora/generate_json.rb +26 -0
- data/benchmark/golden/calculator_lexer.sha256 +1 -0
- data/benchmark/golden/json_lexer.sha256 +1 -0
- data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
- data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
- data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
- data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
- data/benchmark/golden/with_racc_lexer.sha256 +1 -0
- data/benchmark/run.rb +254 -0
- data/docs/README.md +64 -0
- data/docs/RELEASING.md +30 -0
- data/docs/adr/0001-byte-level-dfa.md +5 -0
- data/docs/adr/0003-leftmost-longest.md +4 -0
- data/docs/adr/0006-accel-not-scanner.md +4 -0
- data/docs/adr/0008-what-pure-ruby-means.md +5 -0
- data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
- data/docs/adr/0017-static-analysis-by-default.md +5 -0
- data/docs/adr/0018-prism-for-generator-only.md +4 -0
- data/docs/adr/0019-measured-performance-floor.md +26 -0
- data/docs/adr/0020-vendored-unicode-contract.md +21 -0
- data/docs/explanation/backends.md +33 -0
- data/docs/explanation/matching-semantics.md +20 -0
- data/docs/explanation/runtime-vs-generated.md +22 -0
- data/docs/explanation/security-model.md +18 -0
- data/docs/explanation/unicode-and-encoding.md +20 -0
- data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
- data/docs/how-to/generate-a-lexer.md +39 -0
- data/docs/how-to/handle-errors.md +32 -0
- data/docs/how-to/integrate-with-lrama.md +21 -0
- data/docs/how-to/integrate-with-racc.md +25 -0
- data/docs/how-to/migrate-from-flex.md +21 -0
- data/docs/how-to/migrate-from-rexical.md +23 -0
- data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
- data/docs/how-to/track-token-locations.md +27 -0
- data/docs/how-to/tune-performance.md +23 -0
- data/docs/how-to/use-states.md +36 -0
- data/docs/how-to/use-trailing-context.md +22 -0
- data/docs/internals/README.md +14 -0
- data/docs/perf-log.md +56 -0
- data/docs/reference/README.md +23 -0
- data/docs/reference/actions.md +47 -0
- data/docs/reference/cli.md +80 -0
- data/docs/reference/compatibility.md +38 -0
- data/docs/reference/diagnostics.md +41 -0
- data/docs/reference/dsl.md +81 -0
- data/docs/reference/errors.md +27 -0
- data/docs/reference/generated-artifacts.md +50 -0
- data/docs/reference/public-api.md +42 -0
- data/docs/reference/regexp.md +39 -0
- data/docs/reference/runtime.md +49 -0
- data/docs/reference/tokens-and-locations.md +33 -0
- data/docs/tutorial/build-a-calculator-lexer.md +96 -0
- data/examples/calculator/README.md +27 -0
- data/examples/calculator/lexer.flexr.rb +17 -0
- data/examples/json/README.md +30 -0
- data/examples/json/lexer.flexr.rb +24 -0
- data/examples/ruby_subset/README.md +17 -0
- data/examples/ruby_subset/lexer.flexr.rb +22 -0
- data/examples/toy_lang/README.md +17 -0
- data/examples/toy_lang/lexer.flexr.rb +18 -0
- data/examples/with_lrama/README.md +17 -0
- data/examples/with_lrama/lexer.flexr.rb +13 -0
- data/examples/with_racc/README.md +17 -0
- data/examples/with_racc/lexer.flexr.rb +13 -0
- data/exe/flexr +7 -0
- data/lib/flexr/automaton/accel.rb +39 -0
- data/lib/flexr/automaton/analysis.rb +38 -0
- data/lib/flexr/automaton/byte_class_set.rb +29 -0
- data/lib/flexr/automaton/compiler.rb +413 -0
- data/lib/flexr/automaton/dfa.rb +103 -0
- data/lib/flexr/automaton/minimizer.rb +70 -0
- data/lib/flexr/automaton/nfa.rb +92 -0
- data/lib/flexr/cli.rb +342 -0
- data/lib/flexr/codegen/base.rb +17 -0
- data/lib/flexr/codegen/direct.rb +52 -0
- data/lib/flexr/codegen/firstmatch.rb +17 -0
- data/lib/flexr/codegen/table.rb +158 -0
- data/lib/flexr/codegen/table_packer.rb +61 -0
- data/lib/flexr/diagnostics.rb +94 -0
- data/lib/flexr/dsl.rb +182 -0
- data/lib/flexr/errors.rb +28 -0
- data/lib/flexr/generated.rb +125 -0
- data/lib/flexr/generator.rb +400 -0
- data/lib/flexr/importer.rb +560 -0
- data/lib/flexr/ir.rb +36 -0
- data/lib/flexr/lexer.rb +10 -0
- data/lib/flexr/options.rb +47 -0
- data/lib/flexr/rake_task.rb +27 -0
- data/lib/flexr/regexp/ast.rb +45 -0
- data/lib/flexr/regexp/char_class.rb +7 -0
- data/lib/flexr/regexp/normalizer.rb +117 -0
- data/lib/flexr/regexp/parser.rb +517 -0
- data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
- data/lib/flexr/regexp/tokenizer.rb +168 -0
- data/lib/flexr/regexp/unsupported.rb +7 -0
- data/lib/flexr/runtime/buffer.rb +112 -0
- data/lib/flexr/runtime/core.rb +388 -0
- data/lib/flexr/runtime/errors.rb +22 -0
- data/lib/flexr/runtime/interpreter.rb +505 -0
- data/lib/flexr/runtime/location.rb +26 -0
- data/lib/flexr/runtime/token.rb +7 -0
- data/lib/flexr/source/passthrough.rb +31 -0
- data/lib/flexr/source/prism_reader.rb +283 -0
- data/lib/flexr/source/static_eval.rb +145 -0
- data/lib/flexr/unicode/case_fold.rb +45 -0
- data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
- data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
- data/lib/flexr/unicode/data/case_folding.rb +9 -0
- data/lib/flexr/unicode/data/properties.rb +10 -0
- data/lib/flexr/unicode/property.rb +107 -0
- data/lib/flexr/unicode/reference_regexp.rb +102 -0
- data/lib/flexr/unicode/utf8_splitter.rb +109 -0
- data/lib/flexr/version.rb +5 -0
- data/lib/flexr.rb +81 -0
- data/site/README.md +22 -0
- data/site/astro.config.mjs +57 -0
- data/site/package.json +19 -0
- data/site/pnpm-lock.yaml +5029 -0
- data/site/pnpm-workspace.yaml +6 -0
- data/site/public/playground.js +189 -0
- data/site/scripts/verify-site.mjs +42 -0
- data/site/src/content/docs/benchmarks.md +8 -0
- data/site/src/content/docs/concepts/matching-semantics.md +15 -0
- data/site/src/content/docs/concepts/regexp-model.md +18 -0
- data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
- data/site/src/content/docs/concepts/security-model.md +15 -0
- data/site/src/content/docs/examples.md +17 -0
- data/site/src/content/docs/learn/generation.md +29 -0
- data/site/src/content/docs/learn/getting-started.md +56 -0
- data/site/src/content/docs/learn/parser-integration.md +27 -0
- data/site/src/content/docs/learn/runtime-mode.md +32 -0
- data/site/src/content/docs/reference/action-context.md +20 -0
- data/site/src/content/docs/reference/cli.md +22 -0
- data/site/src/content/docs/reference/diagnostics.md +16 -0
- data/site/src/content/docs/reference/dsl.md +19 -0
- data/site/src/content/docs/reference/public-api.md +18 -0
- data/site/src/content/docs/reference/regexp.md +16 -0
- data/site/src/content/docs/reference/runtime.md +16 -0
- data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
- data/site/src/content.config.ts +12 -0
- data/site/src/env.d.ts +1 -0
- data/site/src/layouts/SiteLayout.astro +39 -0
- data/site/src/pages/index.astro +174 -0
- data/site/src/pages/playground.astro +64 -0
- data/site/src/styles/custom.css +711 -0
- data/site/tsconfig.json +5 -0
- data/tools/coverage.rb +32 -0
- data/tools/docs_verify.rb +116 -0
- data/tools/gen_unicode_tables.rb +202 -0
- data/tools/regexp_tokenizer_reference.rb +60 -0
- 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,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
|