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
@@ -2,8 +2,20 @@
2
2
 
3
3
  module Flexr
4
4
  module Generated
5
+ ARTIFACT_SCHEMA_VERSION = 1
6
+ RUNTIME_ABI_VERSION = 1
7
+
5
8
  module_function
6
9
 
10
+ def artifact_metadata
11
+ {
12
+ schema_version: ARTIFACT_SCHEMA_VERSION,
13
+ compiler_version: VERSION,
14
+ runtime_abi_version: RUNTIME_ABI_VERSION,
15
+ unicode_version: Unicode::VERSION
16
+ }
17
+ end
18
+
7
19
  def install!(klass, payload)
8
20
  rules = payload.fetch(:rules)
9
21
  config = payload
@@ -34,20 +46,21 @@ module Flexr
34
46
  end
35
47
 
36
48
  def install_compiled!(klass, payload)
49
+ validate_artifact!(payload[:artifact])
37
50
  install!(klass, payload)
38
51
  machines = payload.fetch(:compiled).fetch(:machines).transform_values do |machine|
39
52
  dfa_data = machine.fetch(:dfa)
40
53
  packed = decode_packed(dfa_data[:packed])
41
54
  dfa = Automaton::DFA.new(
42
- transitions: dfa_data[:transitions] || inflate_packed(packed, dfa_data.fetch(:state_count),
43
- dfa_data.fetch(:class_count)),
55
+ transitions: dfa_data[:transitions],
44
56
  accepts: dfa_data.fetch(:accepts),
45
57
  ec: dfa_data.fetch(:ec),
46
58
  class_count: dfa_data.fetch(:class_count),
47
59
  start: dfa_data.fetch(:start),
48
60
  rule_ids: dfa_data.fetch(:rule_ids),
49
61
  packed: packed,
50
- direct: dfa_data[:direct]
62
+ direct: dfa_data[:direct],
63
+ state_count: dfa_data.fetch(:state_count)
51
64
  )
52
65
  Automaton::Machine.new(dfa: dfa, state_name: machine.fetch(:state_name).to_sym)
53
66
  end
@@ -65,6 +78,35 @@ module Flexr
65
78
  klass
66
79
  end
67
80
 
81
+ def validate_artifact!(artifact)
82
+ return incompatible_artifact!("artifact metadata is missing") unless artifact.is_a?(Hash)
83
+
84
+ expected = {
85
+ schema_version: ARTIFACT_SCHEMA_VERSION,
86
+ runtime_abi_version: RUNTIME_ABI_VERSION,
87
+ unicode_version: Unicode::VERSION
88
+ }
89
+ mismatch = expected.find { |name, value| artifact[name] != value }
90
+ compiler_version = artifact[:compiler_version]
91
+ return if mismatch.nil? && compiler_version.is_a?(String) && !compiler_version.empty?
92
+
93
+ detail = if mismatch
94
+ name, value = mismatch
95
+ "#{name}=#{artifact[name].inspect} (expected #{value.inspect})"
96
+ else
97
+ "compiler_version=#{compiler_version.inspect}"
98
+ end
99
+ incompatible_artifact!(detail)
100
+ end
101
+
102
+ def incompatible_artifact!(detail)
103
+ diagnostic = Diagnostics.error(
104
+ "FLEXR-E020", "generated artifact is incompatible with this runtime",
105
+ help: "regenerate the lexer with the installed flexr version", note: detail
106
+ )
107
+ raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
108
+ end
109
+
68
110
  def decode_packed(packed)
69
111
  return packed unless packed.is_a?(Hash) && packed[:encoding]&.to_sym == :base64
70
112
 
@@ -84,42 +126,26 @@ module Flexr
84
126
  values.map { |value| value == nil_value ? nil : value }
85
127
  end
86
128
 
87
- def inflate_packed(packed, state_count, class_count)
88
- return nil unless packed
89
-
90
- Array.new(state_count) do |state|
91
- Array.new(class_count) do |class_id|
92
- cursor = state
93
- loop do
94
- index = packed.fetch(:base).fetch(cursor) + class_id
95
- break packed.fetch(:next).fetch(index) if packed.fetch(:check)[index] == cursor
96
-
97
- fallback = packed[:fallback]&.fetch(cursor)
98
- break packed.fetch(:default).fetch(cursor) unless fallback
99
-
100
- cursor = fallback
101
- end
102
- end
103
- end
104
- end
105
129
  end
106
130
 
107
131
  module DSL
108
132
  def __flexr_add_generated_rule(definition)
109
- action = definition.fetch(:action)
110
- @__flexr_rules << IR::Rule.new(
111
- index: definition.fetch(:index), patterns: Array(definition.fetch(:patterns)),
112
- trailing: normalize_trailing(definition[:trailing]), action: action,
113
- states: Array(definition.fetch(:states)).map(&:to_sym),
114
- bol_only: definition.fetch(:bol_only, false), end_anchor: definition[:end_anchor],
115
- location: definition[:span] || definition[:location],
116
- pattern_conditions: Array(definition[:pattern_conditions]).map do |condition|
117
- next unless condition
118
-
119
- Automaton::Acceptance.new(rule_index: condition[0], pattern_index: condition[1],
120
- bol_only: condition[2], end_anchor: condition[3])
121
- end
122
- )
133
+ __flexr_mutate! do
134
+ action = definition.fetch(:action)
135
+ @__flexr_rules << IR::Rule.new(
136
+ index: definition.fetch(:index), patterns: Array(definition.fetch(:patterns)),
137
+ trailing: normalize_trailing(definition[:trailing]), action: action,
138
+ states: Array(definition.fetch(:states)).map(&:to_sym),
139
+ bol_only: definition.fetch(:bol_only, false), end_anchor: definition[:end_anchor],
140
+ location: definition[:span] || definition[:location],
141
+ pattern_conditions: Array(definition[:pattern_conditions]).map do |condition|
142
+ next unless condition
143
+
144
+ Automaton::Acceptance.new(rule_index: condition[0], pattern_index: condition[1],
145
+ bol_only: condition[2], end_anchor: condition[3])
146
+ end
147
+ )
148
+ end
123
149
  end
124
150
  end
125
151
  end
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+ require_relative "runtime" unless defined?(Flexr::Lexer)
5
+ require_relative "artifact_writer"
6
+ require_relative "codegen"
7
+ require_relative "source"
8
+
3
9
  module Flexr
4
10
  class Generator
5
11
  attr_reader :diagnostics
@@ -8,15 +14,18 @@ module Flexr
8
14
  INLINE_EMIT_ARGUMENTS = /\A(?::[A-Za-z_]\w*|true|false|nil|-?\d+(?:\.\d+)?|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|text(?:\.(?:to_f|to_i|bytesize|byteslice\([^()\n]+\)))?|lineno|line)(?:\s*,\s*(?::[A-Za-z_]\w*|true|false|nil|-?\d+(?:\.\d+)?|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|text(?:\.(?:to_f|to_i|bytesize|byteslice\([^()\n]+\)))?|lineno|line))*\z/
9
15
 
10
16
  RUNTIME_SOURCES = %w[
11
- version.rb errors.rb diagnostics.rb ir.rb
12
- regexp/ast.rb regexp/parser.rb regexp/normalizer.rb regexp/unsupported.rb regexp/char_class.rb
13
- unicode/utf8_splitter.rb unicode/data/properties.rb unicode/data/case_folding.rb unicode/property.rb unicode/reference_regexp.rb unicode/case_fold.rb
14
- automaton/byte_class_set.rb automaton/nfa.rb automaton/dfa.rb automaton/compiler.rb
15
- automaton/analysis.rb automaton/minimizer.rb automaton/accel.rb
17
+ version.rb errors.rb diagnostics.rb action_resolver.rb configuration.rb ir.rb
18
+ unicode/version.rb automaton/types.rb automaton/dfa.rb automaton/analysis.rb automaton/accel.rb
16
19
  runtime/location.rb runtime/token.rb runtime/buffer.rb runtime/errors.rb
17
20
  runtime/interpreter.rb runtime/core.rb dsl.rb lexer.rb generated.rb
18
21
  ].freeze
19
22
 
23
+ MATCHER_RUNTIME_SOURCES = %w[
24
+ regexp/ast.rb regexp/parser.rb regexp/unsupported.rb
25
+ unicode/utf8_splitter.rb unicode/data/properties.rb unicode/data/case_folding.rb
26
+ unicode/property.rb unicode/reference_regexp.rb unicode/case_fold.rb
27
+ ].freeze
28
+
20
29
  def initialize(path, output: nil, eval_mode: false, options: {})
21
30
  @path = path
22
31
  @output = output
@@ -26,6 +35,7 @@ module Flexr
26
35
  end
27
36
 
28
37
  def generate
38
+ ArtifactWriter.ensure_distinct!(@path, @output) if @output
29
39
  # Keep generated source stable when a checkout presents the spec with
30
40
  # Windows line endings. All offsets consumed by the parser and
31
41
  # passthrough writer are relative to this normalized source.
@@ -36,7 +46,7 @@ module Flexr
36
46
  parsed = Source::PrismReader.new(source, path: @path).read
37
47
  generate_static(parsed)
38
48
  end
39
- File.binwrite(@output, result) if @output
49
+ ArtifactWriter.write!(@output, result, source_path: @path) if @output
40
50
  result
41
51
  end
42
52
 
@@ -51,24 +61,26 @@ module Flexr
51
61
  install = "Flexr::Generated.install_compiled!(self, #{payload})\n"
52
62
  install = "#{install}#{Codegen::Table.new(compiled).source(indent: indent)}"
53
63
  install = "#{install}#{generated_action_source(parsed, indent)}"
54
- install = "#{install}#{Codegen::Direct.new(compiled).source(indent: indent)}#{indent}" if
55
- effective_backend(parsed) == :direct
56
- result = Source::Passthrough.remove_spans(parsed.source, parsed.dsl_spans, insertion: parsed.first_dsl_offset, payload: install)
57
- result = result.gsub(/^[ \t]+(?=\n)/, "")
58
- result = result.gsub(/^\s*require ["']flexr["']\s*\n/, "") if standalone?(parsed)
59
- digest = Digest::SHA256.hexdigest(payload)
64
+ edits = parsed.dsl_edits.dup
65
+ edits.concat(parsed.flexr_require_spans.map { |span| [*span, ""] }) if standalone?(parsed)
66
+ result = Source::Passthrough.rewrite(
67
+ parsed.source, edits, insertion: parsed.first_dsl_offset, payload: install
68
+ )
69
+ digest = Digest::SHA256.hexdigest(result)
60
70
  header = [
61
71
  "# Generated by flexr. DO NOT EDIT.",
62
72
  "# source: #{parsed.path}",
63
73
  "# spec-digest: sha256:#{digest}",
74
+ "# artifact-schema: #{Generated::ARTIFACT_SCHEMA_VERSION}",
75
+ "# runtime-abi: #{Generated::RUNTIME_ABI_VERSION}",
64
76
  "# unicode: #{Unicode::VERSION}",
65
77
  "# backend: #{effective_backend(parsed)}",
66
78
  "# compiled: true",
67
79
  "# eval: #{@eval_mode}",
68
80
  "# standalone: #{standalone?(parsed)}"
69
81
  ].join("\n")
70
- prefix = standalone?(parsed) ? "require \"json\"\n#{embedded_runtime}\n" : ""
71
- result.sub(/\A(# frozen_string_literal: true\n)/) { "#{::Regexp.last_match(1)}#{header}\n#{prefix}" }
82
+ prefix = standalone?(parsed) ? "require \"json\"\nrequire \"monitor\"\n#{embedded_runtime(parsed)}\n" : ""
83
+ Source::Passthrough.insert_after_preamble(result, "#{header}\n#{prefix}")
72
84
  end
73
85
 
74
86
  def generated_payload(parsed, compiled)
@@ -80,7 +92,7 @@ module Flexr
80
92
  "action: #{action_expression(rule.action)}, states: #{rule.states.inspect}, " \
81
93
  "bol_only: #{ruby_literal(rule.bol_only)}, end_anchor: #{ruby_literal(rule.end_anchor)} }"
82
94
  end
83
- "{ rules: [#{definitions.join(', ')}], " \
95
+ "{ artifact: #{ruby_literal(artifact_metadata)}, rules: [#{definitions.join(', ')}], " \
84
96
  "backend: #{ruby_literal(effective_backend(parsed))}, " \
85
97
  "token_kind: #{ruby_literal(@options.fetch(:token_kind, parsed.config[:token_kind]))}, " \
86
98
  "encoding: #{encoding_expression(parsed.config[:encoding])}, " \
@@ -93,6 +105,10 @@ module Flexr
93
105
  backend: effective_backend(parsed))} }"
94
106
  end
95
107
 
108
+ def artifact_metadata
109
+ Generated.artifact_metadata
110
+ end
111
+
96
112
  def compile_parsed(parsed)
97
113
  states = parsed.states.each_with_index.to_h do |(name, value), index|
98
114
  [name.to_sym, IR::State.new(name: name.to_sym, inclusive: value[:inclusive], id: index)]
@@ -111,7 +127,6 @@ module Flexr
111
127
  eof_rules: parsed.config[:eof_rules] || {}, verbatim: parsed.source
112
128
  )
113
129
  compiled = Automaton::Compiler.new(spec).compile
114
- validate_firstmatch_equivalence!(spec, compiled) if spec.backend == :firstmatch
115
130
  @resolved_backend = resolve_backend(spec.backend, compiled)
116
131
  parsed.rules.each do |rule|
117
132
  compiled_rule = spec.rules.fetch(rule.index)
@@ -128,8 +143,7 @@ module Flexr
128
143
  return requested unless requested == :auto
129
144
  return :table unless compiled
130
145
 
131
- cells = compiled.stats.values.map { |stats| stats[:states] * stats[:classes] }.max.to_i
132
- cells > DSL::AUTO_DIRECT_CELL_THRESHOLD ? :direct : :table
146
+ Automaton::BackendCostModel.choose(compiled)
133
147
  end
134
148
 
135
149
  def normalize_trailing(value)
@@ -144,66 +158,6 @@ module Flexr
144
158
  end
145
159
  end
146
160
 
147
- def validate_firstmatch_equivalence!(spec, compiled)
148
- return if spec.rules.any? do |rule|
149
- rule.trailing || rule.patterns.any? { |pattern| reference_pattern?(pattern, unicode: spec.options[:unicode] == true) }
150
- end
151
-
152
- machine = compiled.machines.fetch(:initial).dfa
153
- random = Random.new(17)
154
- inputs = ["", "a", "aa", "aaa", "ba", "aab"]
155
- inputs.concat(Array.new(9_994) do
156
- Array.new(random.rand(10)) { random.rand(32..126) }.pack("C*")
157
- end)
158
- inputs.each do |input|
159
- table = table_match(machine, input)
160
- firstmatch = firstmatch_match(spec.rules, input)
161
- next if table == firstmatch
162
-
163
- raise CompileError, "firstmatch differs from table for #{input.inspect}: #{firstmatch.inspect} vs #{table.inspect}"
164
- end
165
- end
166
-
167
- def reference_pattern?(pattern, unicode: false)
168
- return false unless pattern.is_a?(::Regexp)
169
- return true if pattern.source.match?(/\\[pP]\{/) || pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
170
-
171
- unicode && pattern.encoding != Encoding::BINARY && pattern.source.match?(/\\[dDwWsS]/)
172
- end
173
-
174
- def table_match(dfa, input)
175
- state = dfa.start
176
- best = nil
177
- input.each_byte.with_index do |byte, index|
178
- state = dfa.transition(state, byte)
179
- break unless state
180
-
181
- acceptance = dfa.accepts[state].select do |candidate|
182
- !candidate.end_anchor || index + 1 == input.bytesize || input.getbyte(index + 1) == 0x0a
183
- end.min_by(&:rule_index)
184
- next unless acceptance
185
-
186
- candidate = [acceptance.rule_index, index + 1]
187
- best = candidate if best.nil? || candidate[1] > best[1] ||
188
- (candidate[1] == best[1] && candidate[0] < best[0])
189
- end
190
- best
191
- end
192
-
193
- def firstmatch_match(rules, input)
194
- rules.sort_by(&:index).each do |rule|
195
- lengths = rule.patterns.filter_map do |pattern|
196
- regexp = pattern.is_a?(::Regexp) ? pattern : ::Regexp.new(::Regexp.escape(pattern.to_s))
197
- match = regexp.match(input, 0)
198
- match&.begin(0)&.zero? ? match[0].bytesize : nil
199
- rescue ArgumentError, RegexpError
200
- nil
201
- end
202
- return [rule.index, lengths.max] unless lengths.empty?
203
- end
204
- nil
205
- end
206
-
207
161
  def compiled_expression(compiled, compression: :none, backend: :table)
208
162
  machines = compiled.machines.map do |name, machine|
209
163
  dfa = machine.dfa
@@ -216,7 +170,9 @@ module Flexr
216
170
  rule_ids: dfa.rule_ids
217
171
  }
218
172
  pack_tables = %i[rows full].include?(compression) || @options.fetch(:table_format, :literal).to_sym == :packed
219
- if pack_tables
173
+ if backend == :direct
174
+ data[:direct] = Codegen::Direct.new(compiled).generate(state: name)
175
+ elsif pack_tables
220
176
  packed = Codegen::TablePacker.pack(dfa.transitions, compression: compression)
221
177
  data[:packed] = if @options.fetch(:table_format, :literal).to_sym == :packed
222
178
  Codegen::TablePacker.encode(packed)
@@ -226,7 +182,6 @@ module Flexr
226
182
  else
227
183
  data[:transitions] = dfa.transitions
228
184
  end
229
- data[:direct] = Codegen::Direct.new(compiled).generate(state: name) if backend == :direct
230
185
  "#{ruby_literal(name)} => { state_name: #{ruby_literal(machine.state_name)}, dfa: #{ruby_literal(data)} }"
231
186
  end
232
187
  # W016 is derived from wall-clock compilation time and must not make
@@ -253,12 +208,18 @@ module Flexr
253
208
  @options.fetch(:table_compression, parsed.config[:options]&.fetch(:table_compression, :rows) || :rows).to_sym
254
209
  end
255
210
 
256
- def embedded_runtime
257
- RUNTIME_SOURCES.map do |relative|
211
+ def embedded_runtime(parsed)
212
+ sources = RUNTIME_SOURCES.dup
213
+ sources.insert(6, *MATCHER_RUNTIME_SOURCES) if matcher_runtime?(parsed)
214
+ sources.map do |relative|
258
215
  File.binread(File.expand_path(relative, __dir__))
259
216
  end.join("\n")
260
217
  end
261
218
 
219
+ def matcher_runtime?(parsed)
220
+ effective_backend(parsed) == :firstmatch || parsed.rules.any?(&:trailing)
221
+ end
222
+
262
223
  def encoding_expression(encoding)
263
224
  encoding == Encoding::BINARY ? "Encoding::BINARY" : "Encoding::UTF_8"
264
225
  end
@@ -279,6 +240,10 @@ module Flexr
279
240
  lines = ["#{indent}def __flexr_generated_execute(rule)", "#{indent} case rule.index"]
280
241
  parsed.rules.each do |rule|
281
242
  lines << "#{indent} when #{rule.index}"
243
+ if rule.bind_action
244
+ lines << "#{indent} instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))"
245
+ next
246
+ end
282
247
  body = inline_action_body(rule)
283
248
  if body.nil?
284
249
  lines << "#{indent} instance_exec(&rule.action)"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "runtime" unless defined?(Flexr::Lexer)
4
+
3
5
  module Flexr
4
6
  class Importer
5
7
  Result = Struct.new(:source, :warnings, :complete?, keyword_init: true)
@@ -387,46 +389,8 @@ module Flexr
387
389
 
388
390
  def rexical_dfa_counterexample(regexps, automata)
389
391
  first, second = automata
390
- queue = [[first.start, second.start, false, +"".b]]
391
- visited = { [first.start, second.start, false] => true }
392
- bytes = rexical_byte_representatives(first, second)
393
-
394
- until queue.empty?
395
- first_state, second_state, first_seen, input = queue.shift
396
- bytes.each do |byte|
397
- next_first = first_state && first.transition(first_state, byte)
398
- next_second = second.transition(second_state, byte)
399
- next unless next_second
400
-
401
- next_input = input + byte.chr(Encoding::BINARY)
402
- next_first_seen = first_seen || rexical_accepting?(first, next_first)
403
- if next_first_seen && rexical_accepting?(second, next_second) &&
404
- !rexical_accepting?(first, next_first)
405
- lengths = rexical_match_lengths(regexps, next_input)
406
- return lengths if lengths
407
- end
408
-
409
- key = [next_first, next_second, next_first_seen]
410
- next if visited[key]
411
-
412
- visited[key] = true
413
- queue << [next_first, next_second, next_first_seen, next_input]
414
- end
415
- end
416
- nil
417
- end
418
-
419
- def rexical_byte_representatives(first, second)
420
- representatives = {}
421
- 256.times do |byte|
422
- key = [first.ec[byte], second.ec[byte]]
423
- representatives[key] ||= byte
424
- end
425
- representatives.values.sort_by { |byte| [byte.between?(32, 126) ? 0 : 1, byte] }
426
- end
427
-
428
- def rexical_accepting?(dfa, state)
429
- state && !dfa.accepts.fetch(state).empty?
392
+ input = Automaton::Analysis.firstmatch_counterexample(first, second)
393
+ input && rexical_match_lengths(regexps, input)
430
394
  end
431
395
 
432
396
  def rexical_match_lengths(regexps, input)
data/lib/flexr/options.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "runtime" unless defined?(Flexr::Lexer)
4
+
3
5
  module Flexr
4
6
  Options = Struct.new(
5
7
  :backend, :token_kind, :accel, :standalone, :eval_mode, :table_compression,
@@ -20,9 +22,9 @@ module Flexr
20
22
  end
21
23
 
22
24
  def validate!
23
- validate_value!(backend, %i[table direct firstmatch auto], :backend)
24
- validate_value!(token_kind, %i[array struct yield], :token_kind)
25
- validate_value!(accel, %i[auto strscan regexp none], :accel)
25
+ validate_value!(backend, Configuration::BACKENDS, :backend)
26
+ validate_value!(token_kind, Configuration::TOKEN_KINDS, :token_kind)
27
+ validate_value!(accel, Configuration::ACCELERATORS, :accel)
26
28
  validate_value!(table_compression, %i[none rows full], :table_compression)
27
29
  validate_value!(table_format, %i[literal packed], :table_format)
28
30
  validate_value!(warn_level, %i[all default none], :warn_level)
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "runtime" unless defined?(Flexr::Lexer)
4
+ require_relative "artifact_writer"
5
+ require_relative "generator"
6
+
3
7
  module Flexr
4
8
  class RakeTask
5
9
  attr_accessor :spec, :output, :warn_as_error
@@ -17,7 +21,8 @@ module Flexr
17
21
  require "rake"
18
22
  raise ArgumentError, "spec is required" unless @spec
19
23
 
20
- output = @output || @spec.sub(/\.flexr\.rb\z/, ".rb")
24
+ output = @output || ArtifactWriter.default_generated_path(@spec)
25
+ ArtifactWriter.ensure_distinct!(@spec, output)
21
26
  Rake::FileTask.define_task(output => @spec) do
22
27
  Generator.new(@spec, output: output, options: { warn_as_error: @warn_as_error }).generate
23
28
  end
@@ -4,11 +4,13 @@ module Flexr
4
4
  module Regexp
5
5
  module AST
6
6
  Empty = Struct.new(:loc, keyword_init: true)
7
+ Fail = Struct.new(:loc, keyword_init: true)
7
8
  ByteRange = Struct.new(:lo, :hi, :loc, keyword_init: true)
8
9
  CodepointRange = Struct.new(:lo, :hi, :loc, keyword_init: true)
9
10
  Seq = Struct.new(:children, :loc, keyword_init: true)
10
11
  Alt = Struct.new(:children, :loc, keyword_init: true)
11
12
  Star = Struct.new(:child, :loc, keyword_init: true)
13
+ Repeat = Struct.new(:child, :minimum, :maximum, :loc, keyword_init: true)
12
14
  Anchor = Struct.new(:kind, :loc, keyword_init: true)
13
15
  TrailMark = Struct.new(:rule_id, :loc, keyword_init: true)
14
16
  CharClass = Struct.new(:ranges, :negated, :loc, keyword_init: true)
@@ -18,18 +20,19 @@ module Flexr
18
20
  def to_s
19
21
  name = self.class.name.split("::").last
20
22
  case name
21
- when "Empty" then "Empty"
23
+ when "Empty", "Fail" then name
22
24
  when "ByteRange" then "ByteRange(#{lo}..#{hi})"
23
25
  when "CodepointRange" then "CodepointRange(#{lo}..#{hi})"
24
26
  when "Anchor" then "Anchor(#{kind})"
25
27
  when "Star" then "Star(#{child})"
28
+ when "Repeat" then "Repeat(#{child}, #{minimum}, #{maximum || '∞'})"
26
29
  when "Seq", "Alt" then "#{name}(#{children.join(', ')})"
27
30
  else super
28
31
  end
29
32
  end
30
33
  end
31
34
 
32
- [Empty, ByteRange, CodepointRange, Seq, Alt, Star, Anchor, TrailMark, CharClass].each do |node_class|
35
+ [Empty, Fail, ByteRange, CodepointRange, Seq, Alt, Star, Repeat, Anchor, TrailMark, CharClass].each do |node_class|
33
36
  node_class.include(Formatting)
34
37
  end
35
38
 
@@ -18,7 +18,7 @@ module Flexr
18
18
 
19
19
  def normalize_node(node)
20
20
  case node
21
- when AST::Empty, AST::ByteRange, AST::CodepointRange, AST::Anchor
21
+ when AST::Empty, AST::Fail, AST::ByteRange, AST::CodepointRange, AST::Anchor
22
22
  normalize_leaf(node)
23
23
  when AST::CharClass
24
24
  char_class(node)
@@ -27,7 +27,10 @@ module Flexr
27
27
  when AST::Alt
28
28
  alternatives(node.children.map { |child| normalize_node(child) })
29
29
  when AST::Star
30
- AST::Star.new(child: normalize_node(node.child), loc: node.loc)
30
+ child = normalize_node(node.child)
31
+ child.is_a?(AST::Fail) ? AST::Empty.new(loc: node.loc) : AST::Star.new(child: child, loc: node.loc)
32
+ when AST::Repeat
33
+ repeat(node)
31
34
  else
32
35
  raise CompileError, "unknown regexp AST node: #{node.class}"
33
36
  end
@@ -36,7 +39,7 @@ module Flexr
36
39
  def normalize_leaf(node)
37
40
  case node
38
41
  when AST::CodepointRange
39
- byte_sequences_for_ranges(casefold_ranges(node.lo, node.hi))
42
+ byte_sequences_for_ranges(casefold_ranges(node.lo, node.hi), loc: node.loc)
40
43
  else
41
44
  node
42
45
  end
@@ -45,44 +48,76 @@ module Flexr
45
48
  def char_class(node)
46
49
  ranges = node.ranges.flat_map do |range|
47
50
  if range.first == AST::Property
48
- Unicode::Property.ranges(range.last, negate: range[1])
51
+ property_ranges = Unicode::Property.ranges(range.last)
52
+ property_ranges = property_ranges.flat_map { |lo, hi| casefold_ranges(lo, hi) }
53
+ range[1] ? complement(property_ranges) : property_ranges
49
54
  else
50
55
  casefold_ranges(*range)
51
56
  end
52
57
  end
53
58
  ranges = complement(ranges) if node.negated
54
- byte_sequences_for_ranges(ranges)
59
+ byte_sequences_for_ranges(ranges, loc: node.loc)
55
60
  end
56
61
 
57
- def byte_sequences_for_ranges(ranges)
62
+ def byte_sequences_for_ranges(ranges, loc:)
58
63
  if @byte_mode
59
64
  byte_ranges = ranges.filter_map do |lo, hi|
60
65
  next if lo > 255
61
66
 
62
67
  [lo, [hi, 255].min]
63
68
  end
64
- return AST::Empty.new(loc: nil) if byte_ranges.empty?
69
+ return AST::Fail.new(loc: loc) if byte_ranges.empty?
65
70
 
66
- return alternatives(byte_ranges.map { |lo, hi| AST::ByteRange.new(lo: lo, hi: hi, loc: nil) })
71
+ return alternatives(byte_ranges.map { |lo, hi| AST::ByteRange.new(lo: lo, hi: hi, loc: loc) })
67
72
  end
68
73
 
69
74
  sequences = ranges.flat_map { |lo, hi| Unicode::Utf8Splitter.split(lo, hi) }
75
+ @trie_cache = {}
70
76
  trie(sequences, 0)
71
77
  end
72
78
 
73
79
  def trie(sequences, index)
74
- return AST::Empty.new(loc: nil) if sequences.empty?
75
- return AST::Empty.new(loc: nil) if sequences.first.length == index
76
-
77
- groups = sequences.group_by { |sequence| sequence[index] }
78
- alternatives(groups.map do |(lo, hi), group|
79
- child = trie(group, index + 1)
80
- AST::Seq.new(children: [AST::ByteRange.new(lo: lo, hi: hi, loc: nil), child], loc: nil)
80
+ return AST::Fail.new(loc: nil) if sequences.empty?
81
+
82
+ finished, continuing = sequences.partition { |sequence| sequence.length == index }
83
+ nodes = []
84
+ nodes << AST::Empty.new(loc: nil) unless finished.empty?
85
+ return nodes.first if continuing.empty?
86
+
87
+ key = [index, continuing]
88
+ return alternatives(nodes + [@trie_cache[key]]) if @trie_cache.key?(key)
89
+
90
+ endpoints = continuing.flat_map do |sequence|
91
+ lo, hi = sequence.fetch(index)
92
+ [lo, hi + 1]
93
+ end.uniq.sort
94
+ partitions = endpoints.each_cons(2).filter_map do |lo, after_hi|
95
+ covered = continuing.select do |sequence|
96
+ range = sequence.fetch(index)
97
+ range.first <= lo && range.last >= after_hi - 1
98
+ end
99
+ [lo, after_hi - 1, covered] unless covered.empty?
100
+ end
101
+ branches = partitions.map { |lo, hi, covered| [lo, hi, trie(covered, index + 1)] }
102
+ merged = branches.each_with_object([]) do |(lo, hi, child), result|
103
+ if !result.empty? && result.last[1] + 1 == lo && result.last[2] == child
104
+ result.last[1] = hi
105
+ else
106
+ result << [lo, hi, child]
107
+ end
108
+ end
109
+ tree = alternatives(merged.map do |lo, hi, child|
110
+ sequence([AST::ByteRange.new(lo: lo, hi: hi, loc: nil), child])
81
111
  end)
112
+ @trie_cache[key] = tree
113
+ alternatives(nodes + [tree])
82
114
  end
83
115
 
84
116
  def sequence(children)
85
117
  flattened = children.flat_map { |child| child.is_a?(AST::Seq) ? child.children : [child] }
118
+ return AST::Fail.new(loc: nil) if flattened.any?(AST::Fail)
119
+
120
+ flattened.reject! { |child| child.is_a?(AST::Empty) }
86
121
  return AST::Empty.new(loc: nil) if flattened.empty?
87
122
  return flattened.first if flattened.length == 1
88
123
 
@@ -91,12 +126,23 @@ module Flexr
91
126
 
92
127
  def alternatives(children)
93
128
  flattened = children.flat_map { |child| child.is_a?(AST::Alt) ? child.children : [child] }
94
- return AST::Empty.new(loc: nil) if flattened.empty?
129
+ flattened.reject! { |child| child.is_a?(AST::Fail) }
130
+ flattened.uniq!
131
+ return AST::Fail.new(loc: nil) if flattened.empty?
95
132
  return flattened.first if flattened.length == 1
96
133
 
97
134
  AST::Alt.new(children: flattened, loc: nil)
98
135
  end
99
136
 
137
+ def repeat(node)
138
+ child = normalize_node(node.child)
139
+ return AST::Empty.new(loc: node.loc) if node.maximum&.zero? || child.is_a?(AST::Empty)
140
+ return node.minimum.zero? ? AST::Empty.new(loc: node.loc) : AST::Fail.new(loc: node.loc) if
141
+ child.is_a?(AST::Fail)
142
+
143
+ AST::Repeat.new(child: child, minimum: node.minimum, maximum: node.maximum, loc: node.loc)
144
+ end
145
+
100
146
  def complement(ranges)
101
147
  result = []
102
148
  cursor = 0