flexr 1.0.0 → 1.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +74 -28
  3. data/benchmark/golden/calculator_lexer.sha256 +1 -1
  4. data/benchmark/golden/json_lexer.sha256 +1 -1
  5. data/benchmark/golden/regexp_tokenizer.sha256 +1 -1
  6. data/benchmark/golden/ruby_subset_lexer.sha256 +1 -1
  7. data/benchmark/golden/toy_lang_lexer.sha256 +1 -1
  8. data/benchmark/golden/with_lrama_lexer.sha256 +1 -1
  9. data/benchmark/golden/with_racc_lexer.sha256 +1 -1
  10. data/docs/README.md +2 -1
  11. data/docs/explanation/backends.md +13 -10
  12. data/docs/explanation/matching-semantics.md +4 -2
  13. data/docs/explanation/security-model.md +10 -3
  14. data/docs/explanation/unicode-and-encoding.md +3 -1
  15. data/docs/how-to/deploy-a-standalone-lexer.md +5 -3
  16. data/docs/how-to/generate-a-lexer.md +3 -3
  17. data/docs/how-to/handle-errors.md +4 -1
  18. data/docs/how-to/run-a-lexer-at-runtime.md +5 -0
  19. data/docs/how-to/track-token-locations.md +2 -1
  20. data/docs/how-to/tune-performance.md +3 -3
  21. data/docs/perf-log.md +16 -0
  22. data/docs/reference/actions.md +5 -2
  23. data/docs/reference/diagnostics.md +8 -0
  24. data/docs/reference/dsl.md +19 -5
  25. data/docs/reference/errors.md +6 -3
  26. data/docs/reference/generated-artifacts.md +29 -2
  27. data/docs/reference/public-api.md +7 -2
  28. data/docs/reference/regexp.md +8 -6
  29. data/docs/reference/runtime.md +20 -3
  30. data/docs/reference/tokens-and-locations.md +6 -4
  31. data/docs/releases/v1.1.0.md +44 -0
  32. data/lib/flexr/action_resolver.rb +15 -0
  33. data/lib/flexr/artifact_writer.rb +61 -0
  34. data/lib/flexr/automaton/accel.rb +9 -4
  35. data/lib/flexr/automaton/analysis.rb +47 -6
  36. data/lib/flexr/automaton/backend_cost_model.rb +40 -0
  37. data/lib/flexr/automaton/compiler.rb +50 -71
  38. data/lib/flexr/automaton/dfa.rb +106 -19
  39. data/lib/flexr/automaton/minimizer.rb +79 -31
  40. data/lib/flexr/automaton/nfa.rb +45 -13
  41. data/lib/flexr/automaton/types.rb +14 -0
  42. data/lib/flexr/cli.rb +11 -5
  43. data/lib/flexr/codegen/direct.rb +5 -41
  44. data/lib/flexr/codegen/table.rb +175 -69
  45. data/lib/flexr/codegen.rb +8 -0
  46. data/lib/flexr/configuration.rb +37 -0
  47. data/lib/flexr/dsl.rb +105 -39
  48. data/lib/flexr/errors.rb +1 -0
  49. data/lib/flexr/generated.rb +61 -35
  50. data/lib/flexr/generator.rb +47 -82
  51. data/lib/flexr/importer.rb +4 -40
  52. data/lib/flexr/options.rb +5 -3
  53. data/lib/flexr/rake_task.rb +6 -1
  54. data/lib/flexr/regexp/ast.rb +5 -2
  55. data/lib/flexr/regexp/normalizer.rb +62 -16
  56. data/lib/flexr/regexp/parser.rb +121 -46
  57. data/lib/flexr/regexp/tokenizer.rb +192 -67
  58. data/lib/flexr/runtime/buffer.rb +115 -9
  59. data/lib/flexr/runtime/core.rb +214 -47
  60. data/lib/flexr/runtime/errors.rb +64 -4
  61. data/lib/flexr/runtime/interpreter.rb +178 -71
  62. data/lib/flexr/runtime.rb +70 -0
  63. data/lib/flexr/source/passthrough.rb +62 -9
  64. data/lib/flexr/source/prism_reader.rb +175 -46
  65. data/lib/flexr/source/static_eval.rb +40 -4
  66. data/lib/flexr/source.rb +6 -0
  67. data/lib/flexr/unicode/data/properties.rb +1 -1
  68. data/lib/flexr/unicode/data.rb +11 -0
  69. data/lib/flexr/unicode/property.rb +3 -5
  70. data/lib/flexr/unicode/reference_regexp.rb +4 -0
  71. data/lib/flexr/unicode/version.rb +7 -0
  72. data/lib/flexr/version.rb +1 -1
  73. data/lib/flexr.rb +9 -76
  74. data/tools/coverage.rb +6 -1
  75. metadata +12 -1
@@ -28,56 +28,139 @@ module Flexr
28
28
  def source(indent: " ")
29
29
  lines = []
30
30
  lines << "#{indent}def scan_one"
31
- lines << "#{indent} return Flexr::Runtime::Interpreter.new(self).scan unless __flexr_generated_fast_path?"
32
- lines << "#{indent} machine = self.class.__flexr_compiled.machines.fetch(state)"
33
- lines << "#{indent} dfa = machine.dfa"
31
+ lines << "#{indent} if @__flexr_generated_scan_state != @state"
32
+ lines << "#{indent} machine = self.class.__flexr_compiled.machines.fetch(@state)"
33
+ lines << "#{indent} @__flexr_generated_scan_state = @state"
34
+ lines << "#{indent} @__flexr_generated_scan_dfa = machine.dfa"
35
+ lines << "#{indent} @__flexr_generated_scan_fast = __flexr_generated_fast_path?(@__flexr_generated_scan_dfa)"
36
+ lines << "#{indent} @__flexr_generated_scan_simple = __flexr_generated_simple_fast_path?(@__flexr_generated_scan_dfa)"
37
+ lines << "#{indent} end"
38
+ lines << "#{indent} dfa = @__flexr_generated_scan_dfa"
39
+ lines << "#{indent} return Flexr::Runtime::Interpreter.new(self).scan unless @__flexr_generated_scan_fast"
34
40
  lines << "#{indent} position = byte_pos"
35
41
  lines << "#{indent} return nil unless valid_utf8_at?(position)"
42
+ lines << "#{indent} return __flexr_generated_scan_simple(dfa, position) if @__flexr_generated_scan_simple"
36
43
  lines << "#{indent} cursor = position"
37
44
  lines << "#{indent} current = dfa.start"
38
45
  lines << "#{indent} direct = dfa.direct"
39
- lines << "#{indent} source = buffer.source"
40
- simple = if simple_fast_path?
41
- lines << "#{indent} best = nil"
42
- true
43
- else
44
- lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, position, nil)"
45
- false
46
- end
47
- lines << "#{indent} accelerate = self.class.__flexr_config.options.fetch(:accel, :auto) != :none && !utf8_input?"
48
- lines << "#{indent} while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)"
46
+ lines << "#{indent} direct_nxt = direct&.fetch(:nxt)"
47
+ lines << "#{indent} direct_classes = direct&.fetch(:classes)"
48
+ lines << "#{indent} ec = dfa.ec"
49
+ lines << "#{indent} source = buffer.stable_source"
50
+ lines << "#{indent} guarded_steps = scan_steps_guarded?"
51
+ lines << "#{indent} token_start = more_text_start || position"
52
+ lines << "#{indent} token_limit = max_token_size"
53
+ lines << "#{indent} check_token_limit = token_limit_required?"
54
+ lines << "#{indent} best = nil"
55
+ lines << "#{indent} accel_mode = @accel_mode"
56
+ lines << "#{indent} accelerate = accel_mode != :none && !(accel_mode == :auto && @__flexr_generated_auto_accel_off)"
57
+ lines << "#{indent} while cursor < (source ? source.bytesize : buffer.bytesize) || buffer.ensure_available?(cursor + 1)"
49
58
  lines << "#{indent} if accelerate"
50
59
  lines << "#{indent} region = __flexr_generated_acceleration_region(dfa, current)"
51
- lines << "#{indent} accelerated_end = __flexr_generated_accelerate(region, cursor) if region"
60
+ lines << "#{indent} acceptance = dfa.accepts[current].first"
61
+ lines << "#{indent} token_rule = acceptance&.rule_index"
62
+ lines << "#{indent} accelerated_end = __flexr_generated_accelerate(region, cursor, token_rule, position) if region"
52
63
  lines << "#{indent} if accelerated_end && accelerated_end > cursor"
53
64
  lines << "#{indent} cursor = accelerated_end"
54
65
  lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
55
66
  lines << "#{indent} next"
56
67
  lines << "#{indent} end"
57
68
  lines << "#{indent} end"
58
- lines << "#{indent} byte = source.getbyte(cursor)"
59
- lines << "#{indent} current = if direct"
60
- lines << "#{indent} class_id = dfa.ec[byte]"
61
- lines << "#{indent} value = direct[:nxt][(current * direct[:classes]) + class_id]"
69
+ lines << "#{indent} byte = source ? source.getbyte(cursor) : buffer.getbyte(cursor)"
70
+ lines << "#{indent} consume_step! if guarded_steps"
71
+ lines << "#{indent} current = if direct_nxt"
72
+ lines << "#{indent} class_id = ec[byte]"
73
+ lines << "#{indent} value = direct_nxt[(current * direct_classes) + class_id]"
62
74
  lines << "#{indent} value >= 0 ? value : nil"
63
75
  lines << "#{indent} else"
64
76
  lines << "#{indent} dfa.transition(current, byte)"
65
77
  lines << "#{indent} end"
66
78
  lines << "#{indent} break unless current"
67
79
  lines << "#{indent} cursor += 1"
68
- if simple
69
- lines << "#{indent} acceptance = dfa.accepts[current].first"
70
- lines << "#{indent} if acceptance"
71
- lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
72
- lines << "#{indent} best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)"
73
- lines << "#{indent} best.rule = rule"
74
- lines << "#{indent} best.start_pos = position"
75
- lines << "#{indent} best.end_pos = cursor"
76
- lines << "#{indent} best.total_end_pos = cursor"
77
- lines << "#{indent} end"
78
- else
79
- lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
80
- end
80
+ lines << "#{indent} resource_acceptance = dfa.accepts[current].first"
81
+ lines << "#{indent} resource_rule = resource_acceptance&.rule_index"
82
+ lines << "#{indent} if check_token_limit"
83
+ lines << "#{indent} size = cursor - token_start"
84
+ lines << "#{indent} defer_token_size_check!(size, rule: resource_rule) if size > token_limit"
85
+ lines << "#{indent} end"
86
+ lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
87
+ lines << "#{indent} end"
88
+ lines << "#{indent} best"
89
+ lines << "#{indent}end"
90
+ lines << "#{indent}def __flexr_generated_scan_simple(dfa, position)"
91
+ lines << "#{indent} current = dfa.start"
92
+ lines << "#{indent} direct = dfa.direct"
93
+ lines << "#{indent} direct_nxt = direct&.fetch(:nxt)"
94
+ lines << "#{indent} direct_classes = direct&.fetch(:classes)"
95
+ lines << "#{indent} ec = dfa.ec"
96
+ lines << "#{indent} accepts = dfa.accepts"
97
+ lines << "#{indent} rules = self.class.__flexr_rules"
98
+ lines << "#{indent} source = buffer.stable_source"
99
+ lines << "#{indent} guarded_steps = scan_steps_guarded?"
100
+ lines << "#{indent} token_start = more_text_start || position"
101
+ lines << "#{indent} token_limit = max_token_size"
102
+ lines << "#{indent} check_token_limit = token_limit_required?"
103
+ lines << "#{indent} cursor = position"
104
+ lines << "#{indent} best = nil"
105
+ lines << "#{indent} accel_mode = @accel_mode"
106
+ lines << "#{indent} accelerate = accel_mode != :none && !(accel_mode == :auto && @__flexr_generated_auto_accel_off)"
107
+ lines << "#{indent} if source && !guarded_steps && !check_token_limit && !accelerate"
108
+ lines << "#{indent} while cursor < source.bytesize"
109
+ lines << "#{indent} byte = source.getbyte(cursor)"
110
+ lines << "#{indent} current = if direct_nxt"
111
+ lines << "#{indent} class_id = ec[byte]"
112
+ lines << "#{indent} value = direct_nxt[(current * direct_classes) + class_id]"
113
+ lines << "#{indent} value >= 0 ? value : nil"
114
+ lines << "#{indent} else"
115
+ lines << "#{indent} dfa.transition(current, byte)"
116
+ lines << "#{indent} end"
117
+ lines << "#{indent} break unless current"
118
+ lines << "#{indent} cursor += 1"
119
+ lines << "#{indent} acceptance = accepts[current].first"
120
+ lines << "#{indent} next unless acceptance"
121
+ lines << "#{indent} rule = rules.fetch(acceptance.rule_index)"
122
+ lines << "#{indent} best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)"
123
+ lines << "#{indent} best.rule = rule"
124
+ lines << "#{indent} best.start_pos = position"
125
+ lines << "#{indent} best.end_pos = cursor"
126
+ lines << "#{indent} best.total_end_pos = cursor"
127
+ lines << "#{indent} end"
128
+ lines << "#{indent} return best"
129
+ lines << "#{indent} end"
130
+ lines << "#{indent} while cursor < (source ? source.bytesize : buffer.bytesize) || buffer.ensure_available?(cursor + 1)"
131
+ lines << "#{indent} if accelerate"
132
+ lines << "#{indent} region = __flexr_generated_acceleration_region(dfa, current)"
133
+ lines << "#{indent} acceptance = accepts[current].first"
134
+ lines << "#{indent} accelerated_end = __flexr_generated_accelerate(region, cursor, acceptance&.rule_index, position) if region"
135
+ lines << "#{indent} if accelerated_end && accelerated_end > cursor"
136
+ lines << "#{indent} cursor = accelerated_end"
137
+ lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
138
+ lines << "#{indent} next"
139
+ lines << "#{indent} end"
140
+ lines << "#{indent} end"
141
+ lines << "#{indent} byte = source ? source.getbyte(cursor) : buffer.getbyte(cursor)"
142
+ lines << "#{indent} consume_step! if guarded_steps"
143
+ lines << "#{indent} current = if direct_nxt"
144
+ lines << "#{indent} class_id = ec[byte]"
145
+ lines << "#{indent} value = direct_nxt[(current * direct_classes) + class_id]"
146
+ lines << "#{indent} value >= 0 ? value : nil"
147
+ lines << "#{indent} else"
148
+ lines << "#{indent} dfa.transition(current, byte)"
149
+ lines << "#{indent} end"
150
+ lines << "#{indent} break unless current"
151
+ lines << "#{indent} cursor += 1"
152
+ lines << "#{indent} acceptance = accepts[current].first"
153
+ lines << "#{indent} if check_token_limit"
154
+ lines << "#{indent} size = cursor - token_start"
155
+ lines << "#{indent} defer_token_size_check!(size, rule: acceptance&.rule_index) if size > token_limit"
156
+ lines << "#{indent} end"
157
+ lines << "#{indent} next unless acceptance"
158
+ lines << "#{indent} rule = rules.fetch(acceptance.rule_index)"
159
+ lines << "#{indent} best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)"
160
+ lines << "#{indent} best.rule = rule"
161
+ lines << "#{indent} best.start_pos = position"
162
+ lines << "#{indent} best.end_pos = cursor"
163
+ lines << "#{indent} best.total_end_pos = cursor"
81
164
  lines << "#{indent} end"
82
165
  lines << "#{indent} best"
83
166
  lines << "#{indent}end"
@@ -86,44 +169,75 @@ module Flexr
86
169
  lines << "#{indent} regions = (@__flexr_generated_accel_regions[dfa] ||= Flexr::Automaton::Accel.extract(dfa).to_h { |region| [region.state, region] })"
87
170
  lines << "#{indent} region = regions[state]"
88
171
  lines << "#{indent} return unless region"
172
+ lines << "#{indent} return if @__flexr_generated_disabled_auto_accel&.key?(region)"
173
+ lines << "#{indent} return if utf8_input? && region.bytes.any? { |byte| byte >= 128 }"
89
174
  lines << "#{indent} return if dfa.accepts[state].any? do |acceptance|"
90
175
  lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
91
176
  lines << "#{indent} acceptance.bol_only || acceptance.end_anchor || rule.trailing"
92
177
  lines << "#{indent} end"
93
178
  lines << "#{indent} region"
94
179
  lines << "#{indent}end"
95
- lines << "#{indent}def __flexr_generated_accelerate(region, position)"
96
- lines << "#{indent} binary = buffer.source.b"
97
- lines << "#{indent} mode = self.class.__flexr_config.options.fetch(:accel, :auto)"
98
- lines << "#{indent} match_end = if %i[strscan auto].include?(mode) && defined?(StringScanner)"
99
- lines << "#{indent} scanner = StringScanner.new(binary)"
100
- lines << "#{indent} scanner.pos = position"
101
- lines << "#{indent} length = scanner.skip(region.regexp)"
102
- lines << "#{indent} length && scanner.pos"
103
- lines << "#{indent} else"
104
- lines << "#{indent} match = region.regexp.match(binary, position)"
105
- lines << "#{indent} match && match.begin(0) == position ? match.end(0) : nil"
106
- lines << "#{indent} end"
107
- lines << "#{indent} return match_end if match_end && match_end < buffer.bytesize"
108
- lines << "#{indent} return match_end if match_end && buffer.eof_loaded?"
109
- lines << "#{indent} return unless buffer.ensure_available?(buffer.bytesize + 1)"
110
- lines << "#{indent} __flexr_generated_accelerate(region, position)"
111
- lines << "#{indent}rescue ArgumentError"
112
- lines << "#{indent} nil"
113
- lines << "#{indent}end"
114
- lines << "#{indent}def __flexr_generated_fast_path?"
115
- lines << "#{indent} return @__flexr_generated_fast_path if defined?(@__flexr_generated_fast_path)"
116
- lines << "#{indent} @__flexr_generated_fast_path = if self.class.__flexr_config.backend == :firstmatch ||"
117
- lines << "#{indent} self.class.__flexr_config.options[:allow_empty_match] == true"
118
- lines << "#{indent} false"
119
- lines << "#{indent} else"
120
- lines << "#{indent} self.class.__flexr_rules.none? do |rule|"
121
- lines << "#{indent} !rule.trailing.nil? || rule.patterns.any? do |pattern|"
122
- lines << "#{indent} next false unless pattern.is_a?(Regexp)"
123
- lines << "#{indent} next true if pattern.source.match?(/\\\\[pP]\\\\{/) || pattern.source.match?(/\\[:(?:\\^)?[a-z]+:\\]/)"
124
- lines << "#{indent} self.class.__flexr_config.options[:unicode] == true && utf8_input? && pattern.source.match?(/\\\\[dDwWsS]/)"
180
+ lines << "#{indent}def __flexr_generated_accelerate(region, position, token_rule, token_start)"
181
+ lines << "#{indent} mode = @accel_mode"
182
+ lines << "#{indent} cursor = position"
183
+ lines << "#{indent} matched = false"
184
+ lines << "#{indent} loop do"
185
+ lines << "#{indent} if cursor >= buffer.bytesize"
186
+ lines << "#{indent} return matched ? cursor : nil if buffer.eof_loaded?"
187
+ lines << "#{indent} return matched ? cursor : nil unless buffer.ensure_available?(cursor + 1)"
188
+ lines << "#{indent} end"
189
+ lines << "#{indent} segment = buffer.source"
190
+ lines << "#{indent} segment_position = cursor - buffer.base_offset"
191
+ lines << "#{indent} if segment.encoding == ::Encoding::UTF_8 && region.utf8_regexp"
192
+ lines << "#{indent} regexp = region.utf8_regexp"
193
+ lines << "#{indent} else"
194
+ lines << "#{indent} segment = segment.b unless segment.encoding == ::Encoding::BINARY"
195
+ lines << "#{indent} regexp = region.regexp"
196
+ lines << "#{indent} end"
197
+ lines << "#{indent} length = begin"
198
+ lines << "#{indent} if %i[strscan auto].include?(mode) && defined?(::StringScanner)"
199
+ lines << "#{indent} @__flexr_generated_accel_scanner ||= ::StringScanner.new(\"\".b)"
200
+ lines << "#{indent} @__flexr_generated_accel_scanner.string = segment"
201
+ lines << "#{indent} @__flexr_generated_accel_scanner.pos = segment_position"
202
+ lines << "#{indent} @__flexr_generated_accel_scanner.skip(regexp)"
203
+ lines << "#{indent} else"
204
+ lines << "#{indent} match = regexp.match(segment, segment_position)"
205
+ lines << "#{indent} match && match.begin(0) == segment_position ? match.end(0) - segment_position : nil"
125
206
  lines << "#{indent} end"
207
+ lines << "#{indent} rescue ArgumentError"
208
+ lines << "#{indent} nil"
126
209
  lines << "#{indent} end"
210
+ lines << "#{indent} __flexr_generated_record_auto_accel_miss(region) if mode == :auto && length.to_i < 8"
211
+ lines << "#{indent} return matched ? cursor : nil unless length&.positive?"
212
+ lines << "#{indent} matched = true"
213
+ lines << "#{indent} cursor += length"
214
+ lines << "#{indent} consume_step!(length)"
215
+ lines << "#{indent} defer_token_size_check!(cursor - (more_text_start || token_start), rule: token_rule)"
216
+ lines << "#{indent} return cursor if cursor < buffer.bytesize || buffer.eof_loaded?"
217
+ lines << "#{indent} end"
218
+ lines << "#{indent}end"
219
+ lines << "#{indent}def __flexr_generated_record_auto_accel_miss(region)"
220
+ lines << "#{indent} @__flexr_generated_auto_accel_misses ||= Hash.new(0)"
221
+ lines << "#{indent} @__flexr_generated_auto_accel_misses[region] += 1"
222
+ lines << "#{indent} @__flexr_generated_auto_accel_miss_total = @__flexr_generated_auto_accel_miss_total.to_i + 1"
223
+ lines << "#{indent} @__flexr_generated_auto_accel_off = true if @__flexr_generated_auto_accel_miss_total >= 9"
224
+ lines << "#{indent} return if @__flexr_generated_auto_accel_misses[region] < 3"
225
+ lines << "#{indent} @__flexr_generated_disabled_auto_accel ||= {}"
226
+ lines << "#{indent} @__flexr_generated_disabled_auto_accel[region] = true"
227
+ lines << "#{indent}end"
228
+ lines << "#{indent}def __flexr_generated_fast_path?(dfa)"
229
+ lines << "#{indent} @__flexr_generated_fast_paths ||= {}"
230
+ lines << "#{indent} return @__flexr_generated_fast_paths[dfa] if @__flexr_generated_fast_paths.key?(dfa)"
231
+ lines << "#{indent} rules = dfa.rule_ids.map { |rule_index| self.class.__flexr_rules.fetch(rule_index) }"
232
+ lines << "#{indent} @__flexr_generated_fast_paths[dfa] = self.class.__flexr_config.backend != :firstmatch &&"
233
+ lines << "#{indent} self.class.__flexr_config.options[:allow_empty_match] != true && rules.none?(&:trailing)"
234
+ lines << "#{indent}end"
235
+ lines << "#{indent}def __flexr_generated_simple_fast_path?(dfa)"
236
+ lines << "#{indent} @__flexr_generated_simple_fast_paths ||= {}"
237
+ lines << "#{indent} return @__flexr_generated_simple_fast_paths[dfa] if @__flexr_generated_simple_fast_paths.key?(dfa)"
238
+ lines << "#{indent} rules = dfa.rule_ids.map { |rule_index| self.class.__flexr_rules.fetch(rule_index) }"
239
+ lines << "#{indent} @__flexr_generated_simple_fast_paths[dfa] = rules.all? do |rule|"
240
+ lines << "#{indent} rule.pattern_conditions.all? { |condition| condition && !condition.bol_only && !condition.end_anchor }"
127
241
  lines << "#{indent} end"
128
242
  lines << "#{indent}end"
129
243
  lines << "#{indent}def __flexr_generated_acceptance(dfa, state, start_position, cursor, best)"
@@ -131,7 +245,6 @@ module Flexr
131
245
  lines << "#{indent} next if acceptance.bol_only && !beginning_of_line?"
132
246
  lines << "#{indent} next if acceptance.end_anchor && !(buffer.eof?(cursor) || buffer.getbyte(cursor) == 0x0a)"
133
247
  lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
134
- lines << "#{indent} defer_token_size_check!(cursor - start_position)"
135
248
  lines << "#{indent} next if best && cursor < best.total_end_pos"
136
249
  lines << "#{indent} next if best && cursor == best.total_end_pos &&"
137
250
  lines << "#{indent} rule.index > best.rule.index"
@@ -146,13 +259,6 @@ module Flexr
146
259
  "#{lines.join("\n")}\n"
147
260
  end
148
261
 
149
- def simple_fast_path?
150
- compiled.rules.all? do |rule|
151
- rule.trailing.nil? && rule.pattern_conditions.all? do |condition|
152
- condition && !condition.bol_only && !condition.end_anchor
153
- end
154
- end
155
- end
156
262
  end
157
263
  end
158
264
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "runtime" unless defined?(Flexr::Lexer)
4
+ require_relative "codegen/base"
5
+ require_relative "codegen/table"
6
+ require_relative "codegen/direct"
7
+ require_relative "codegen/firstmatch"
8
+ require_relative "codegen/table_packer"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Configuration
5
+ BACKENDS = %i[table direct firstmatch auto].freeze
6
+ TOKEN_KINDS = %i[array struct yield].freeze
7
+ ACCELERATORS = %i[auto strscan regexp none].freeze
8
+ BOOLEAN_OPTIONS = %i[unicode eager_columns allow_empty_match experimental standalone].freeze
9
+
10
+ module_function
11
+
12
+ def backend!(value)
13
+ validated_symbol!(value, BACKENDS, :backend)
14
+ end
15
+
16
+ def token_kind!(value)
17
+ validated_symbol!(value, TOKEN_KINDS, :token_kind)
18
+ end
19
+
20
+ def accelerator!(value)
21
+ validated_symbol!(value, ACCELERATORS, :accel)
22
+ end
23
+
24
+ def option!(value)
25
+ validated_symbol!(value, BOOLEAN_OPTIONS, :option)
26
+ end
27
+
28
+ def validated_symbol!(value, allowed, name)
29
+ symbol = value.to_sym
30
+ return symbol if allowed.include?(symbol)
31
+
32
+ raise ArgumentError, "unsupported #{name}: #{value.inspect}"
33
+ rescue NoMethodError
34
+ raise ArgumentError, "unsupported #{name}: #{value.inspect}"
35
+ end
36
+ end
37
+ end
data/lib/flexr/dsl.rb CHANGED
@@ -3,8 +3,6 @@
3
3
  module Flexr
4
4
  module DSL
5
5
  DSL_METHODS = %i[rule state all_states on_eof emits backend token_kind encoding option accel].freeze
6
- AUTO_DIRECT_CELL_THRESHOLD = 100_000
7
-
8
6
  def inherited(child)
9
7
  super
10
8
  child.__flexr_reset!
@@ -21,23 +19,23 @@ module Flexr
21
19
  )
22
20
  @__flexr_compiled = nil
23
21
  @__flexr_generated = false
24
- @__flexr_compile_mutex = Mutex.new
22
+ @__flexr_generated_actions = []
23
+ @__flexr_specification_frozen = false
24
+ @__flexr_compile_mutex = Monitor.new
25
25
  end
26
26
 
27
27
  def rule(pattern, skip: false, emit: nil, followed_by: nil, &action)
28
- patterns = normalize_patterns(pattern)
29
- rule_action = if skip
30
- :skip
31
- elsif emit
32
- [:emit, emit.to_sym]
33
- else
34
- action || proc { emit(nil, text) }
28
+ __flexr_mutate! do
29
+ patterns = normalize_patterns(pattern)
30
+ rule_action = ActionResolver.resolve(
31
+ skip: skip, emit: emit, block: action, default: proc { emit(nil, text) }
32
+ )
33
+ states = @__flexr_state_stack.empty? ? [:initial] : @__flexr_state_stack.dup
34
+ @__flexr_rules << IR::Rule.new(
35
+ index: @__flexr_rules.length, patterns: patterns, trailing: normalize_trailing(followed_by),
36
+ action: rule_action, states: states, bol_only: false, end_anchor: nil
37
+ )
35
38
  end
36
- states = @__flexr_state_stack.empty? ? [:initial] : @__flexr_state_stack.dup
37
- @__flexr_rules << IR::Rule.new(
38
- index: @__flexr_rules.length, patterns: patterns, trailing: normalize_trailing(followed_by),
39
- action: rule_action, states: states, bol_only: false, end_anchor: nil
40
- )
41
39
  nil
42
40
  end
43
41
 
@@ -45,38 +43,56 @@ module Flexr
45
43
  raise ArgumentError, "state requires a block" unless block
46
44
  raise ArgumentError, "state requires a name" if names.empty?
47
45
 
48
- names.each do |name|
49
- symbol = name.to_sym
50
- @__flexr_states[symbol] ||= IR::State.new(name: symbol, inclusive: inclusive, id: @__flexr_states.length)
46
+ __flexr_mutate! do
47
+ names.each do |name|
48
+ symbol = name.to_sym
49
+ existing = @__flexr_states[symbol]
50
+ raise ArgumentError, "state #{symbol.inspect} already declared with inclusive: #{existing.inclusive}" if
51
+ existing && existing.inclusive != inclusive
52
+ @__flexr_states[symbol] ||= IR::State.new(
53
+ name: symbol, inclusive: inclusive, id: @__flexr_states.length
54
+ )
55
+ end
56
+ @__flexr_state_stack.concat(names.map(&:to_sym))
57
+ begin
58
+ class_eval(&block)
59
+ ensure
60
+ names.length.times { @__flexr_state_stack.pop }
61
+ end
51
62
  end
52
- @__flexr_state_stack.concat(names.map(&:to_sym))
53
- class_eval(&block)
54
- ensure
55
- names&.length&.times { @__flexr_state_stack.pop }
56
63
  end
57
64
 
58
65
  def all_states(&)
59
- state(*@__flexr_states.keys, &)
66
+ raise ArgumentError, "all_states requires a block" unless block_given?
67
+
68
+ __flexr_mutate! do
69
+ names = @__flexr_states.keys
70
+ @__flexr_state_stack.concat(names)
71
+ begin
72
+ class_eval(&)
73
+ ensure
74
+ names.length.times { @__flexr_state_stack.pop }
75
+ end
76
+ end
60
77
  end
61
78
 
62
79
  def on_eof(&action)
63
- state_name = @__flexr_state_stack.last || :initial
64
- @__flexr_eof_rules[state_name] = action
80
+ __flexr_mutate! do
81
+ state_name = @__flexr_state_stack.last || :initial
82
+ @__flexr_eof_rules[state_name] = action
83
+ end
65
84
  end
66
85
 
67
86
  def emits(*tokens)
68
- @__flexr_config.declared_tokens.concat(tokens.flatten.map(&:to_sym)).uniq!
87
+ __flexr_mutate! { @__flexr_config.declared_tokens.concat(tokens.flatten.map(&:to_sym)).uniq! }
69
88
  end
70
89
 
71
90
  def backend(name)
72
- @__flexr_config.backend = name.to_sym
91
+ __flexr_mutate! { @__flexr_config.backend = Configuration.backend!(name) }
73
92
  end
74
93
 
75
94
  def token_kind(name)
76
- value = name.to_sym
77
- raise ArgumentError, "unsupported token_kind: #{name}" unless %i[array struct yield].include?(value)
78
-
79
- @__flexr_config.token_kind = value
95
+ __flexr_mutate! { @__flexr_config.token_kind = Configuration.token_kind!(name) }
80
96
  end
81
97
 
82
98
  def encoding(value)
@@ -86,15 +102,17 @@ module Flexr
86
102
  raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
87
103
  end
88
104
 
89
- @__flexr_config.encoding = encoding
105
+ __flexr_mutate! { @__flexr_config.encoding = encoding }
90
106
  end
91
107
 
92
108
  def option(*values)
93
- values.each { |value| @__flexr_config.options[value.to_sym] = true }
109
+ __flexr_mutate! do
110
+ values.each { |value| @__flexr_config.options[Configuration.option!(value)] = true }
111
+ end
94
112
  end
95
113
 
96
114
  def accel(value)
97
- @__flexr_config.options[:accel] = value.to_sym
115
+ __flexr_mutate! { @__flexr_config.options[:accel] = Configuration.accelerator!(value) }
98
116
  end
99
117
 
100
118
  def compile!
@@ -104,6 +122,8 @@ module Flexr
104
122
  @__flexr_compiled ||= begin
105
123
  compiled = Automaton::Compiler.new(__flexr_spec).compile
106
124
  @__flexr_config.backend = auto_direct?(compiled) ? :direct : :table if @__flexr_config.backend == :auto
125
+ freeze_specification!
126
+ freeze_compiled!(compiled)
107
127
  compiled
108
128
  end
109
129
  # rubocop:enable Naming/MemoizedInstanceVariableName
@@ -131,26 +151,73 @@ module Flexr
131
151
  )
132
152
  end
133
153
 
134
- attr_reader :__flexr_rules, :__flexr_states, :__flexr_config, :__flexr_compiled
154
+ attr_reader :__flexr_rules, :__flexr_states, :__flexr_config, :__flexr_compiled, :__flexr_generated_actions
135
155
 
136
156
  def __flexr_add_generated_eof(state, action)
137
- @__flexr_eof_rules[state.to_sym] = action
157
+ __flexr_mutate! { @__flexr_eof_rules[state.to_sym] = action }
138
158
  end
139
159
 
140
160
  def __flexr_set_compiled!(compiled)
141
- @__flexr_compiled = compiled
161
+ @__flexr_compile_mutex.synchronize do
162
+ @__flexr_compiled = freeze_compiled!(compiled)
163
+ freeze_specification!
164
+ end
142
165
  end
143
166
 
144
167
  def __flexr_mark_generated!
145
168
  @__flexr_generated = true
146
169
  end
147
170
 
171
+ def __flexr_bind_generated_action(index, action)
172
+ raise FrozenSpecificationError, "generated actions can only be bound on a generated lexer" unless @__flexr_generated
173
+
174
+ @__flexr_generated_actions[index] = action
175
+ end
176
+
148
177
  def __flexr_generated?
149
178
  @__flexr_generated == true
150
179
  end
151
180
 
152
181
  private
153
182
 
183
+ def __flexr_mutate!
184
+ @__flexr_compile_mutex.synchronize do
185
+ raise FrozenSpecificationError, "lexer specification is immutable after compilation" if
186
+ @__flexr_specification_frozen
187
+
188
+ yield
189
+ end
190
+ end
191
+
192
+ def freeze_specification!
193
+ @__flexr_rules.each do |rule|
194
+ rule.patterns.freeze
195
+ rule.states.freeze
196
+ rule.pattern_conditions&.each(&:freeze)
197
+ rule.pattern_conditions&.freeze
198
+ rule.freeze
199
+ end
200
+ @__flexr_states.each_value(&:freeze)
201
+ @__flexr_eof_rules.freeze
202
+ @__flexr_rules.freeze
203
+ @__flexr_states.freeze
204
+ @__flexr_config.options.freeze
205
+ @__flexr_config.declared_tokens.freeze
206
+ @__flexr_config.freeze
207
+ @__flexr_specification_frozen = true
208
+ end
209
+
210
+ def freeze_compiled!(compiled)
211
+ compiled.machines.each_value(&:freeze)
212
+ compiled.machines.freeze
213
+ compiled.states.freeze
214
+ compiled.stats.each_value(&:freeze)
215
+ compiled.stats.freeze
216
+ Array(compiled.diagnostics).each(&:freeze)
217
+ compiled.diagnostics.freeze
218
+ compiled.freeze
219
+ end
220
+
154
221
  def normalize_patterns(pattern)
155
222
  values = pattern.is_a?(Array) ? pattern : [pattern]
156
223
  values.each do |value|
@@ -175,8 +242,7 @@ module Flexr
175
242
  end
176
243
 
177
244
  def auto_direct?(compiled)
178
- cells = compiled.stats.values.map { |stats| stats[:states] * stats[:classes] }.max.to_i
179
- cells > AUTO_DIRECT_CELL_THRESHOLD
245
+ Automaton::BackendCostModel.choose(compiled) == :direct
180
246
  end
181
247
  end
182
248
  end
data/lib/flexr/errors.rb CHANGED
@@ -25,4 +25,5 @@ module Flexr
25
25
  class CompileError < Error; end
26
26
  class UnsupportedRegexpError < CompileError; end
27
27
  class StaticResolutionError < CompileError; end
28
+ class FrozenSpecificationError < Error; end
28
29
  end