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,42 +2,81 @@
2
2
 
3
3
  module Flexr
4
4
  module Automaton
5
+ class TransitionRows
6
+ include Enumerable
7
+
8
+ attr_reader :length
9
+
10
+ def initialize(length, &loader)
11
+ @length = length
12
+ @loader = loader
13
+ @rows = Array.new(length)
14
+ freeze
15
+ end
16
+
17
+ alias size length
18
+
19
+ def [](state)
20
+ return unless state&.between?(0, length - 1)
21
+
22
+ @rows[state] ||= @loader.call(state).freeze
23
+ end
24
+
25
+ def fetch(state)
26
+ return self[state] if state.between?(0, length - 1)
27
+
28
+ raise IndexError, "transition row #{state} is out of bounds"
29
+ end
30
+
31
+ def each
32
+ return enum_for(__method__) unless block_given?
33
+
34
+ length.times { |state| yield self[state] }
35
+ end
36
+
37
+ def each_index(&)
38
+ return enum_for(__method__) unless block_given?
39
+
40
+ length.times(&)
41
+ end
42
+
43
+ def materialized_count
44
+ @rows.count { |row| !row.nil? }
45
+ end
46
+ end
47
+
5
48
  class DFA
6
49
  attr_reader :transitions, :accepts, :ec, :class_count, :start, :states, :rule_ids, :packed, :direct
7
50
 
8
- def initialize(transitions:, accepts:, ec:, class_count:, start:, rule_ids:, packed: nil, direct: nil)
9
- @transitions = transitions.freeze
51
+ def initialize(accepts:, ec:, class_count:, start:, rule_ids:, transitions: nil, packed: nil, direct: nil,
52
+ state_count: nil)
53
+ @packed = freeze_representation(packed)
54
+ @direct = freeze_representation(direct)
55
+ @states = state_count || transitions&.length || direct_state_count
56
+ raise ArgumentError, "state_count is required without dense transitions" unless @states
57
+
58
+ @transitions = transition_rows(transitions)
10
59
  @accepts = accepts.map do |rules|
11
60
  rules.map do |acceptance|
12
- next acceptance if acceptance.is_a?(Acceptance)
61
+ next acceptance.freeze if acceptance.is_a?(Acceptance)
13
62
 
14
63
  Acceptance.new(rule_index: acceptance[0], pattern_index: acceptance[1],
15
- bol_only: acceptance[2], end_anchor: acceptance[3])
64
+ bol_only: acceptance[2], end_anchor: acceptance[3]).freeze
16
65
  end.freeze
17
66
  end.freeze
18
67
  @ec = ec.freeze
19
68
  @class_count = class_count
20
69
  @start = start
21
- @states = transitions.length
22
70
  @rule_ids = rule_ids.map { |id| id.respond_to?(:rule_index) ? id.rule_index : id }.uniq.sort.freeze
23
- @packed = packed
24
- @direct = direct
71
+ freeze
25
72
  end
26
73
 
27
74
  def transition(state, byte)
28
75
  class_id = @ec[byte]
76
+ return direct_transition(state, class_id) if @direct
29
77
  return @transitions[state][class_id] unless @packed
30
78
 
31
- cursor = state
32
- loop do
33
- index = @packed.fetch(:base).fetch(cursor) + class_id
34
- return @packed.fetch(:next).fetch(index) if @packed.fetch(:check)[index] == cursor
35
-
36
- fallback = @packed[:fallback]&.fetch(cursor)
37
- return @packed.fetch(:default).fetch(cursor) unless fallback
38
-
39
- cursor = fallback
40
- end
79
+ packed_transition(state, class_id)
41
80
  end
42
81
 
43
82
  # Generated direct lexers use a flattened dispatch representation. The
@@ -45,8 +84,7 @@ module Flexr
45
84
  def transition_direct(state, byte)
46
85
  if @direct
47
86
  class_id = @ec[byte]
48
- value = @direct.fetch(:nxt).fetch((state * @direct.fetch(:classes)) + class_id)
49
- return value >= 0 ? value : nil
87
+ return direct_transition(state, class_id)
50
88
  end
51
89
 
52
90
  @transitions[state][@ec[byte]]
@@ -65,6 +103,55 @@ module Flexr
65
103
  def stats
66
104
  { states: states, classes: class_count, accepting_states: accepts.count { |rules| !rules.empty? } }
67
105
  end
106
+
107
+ private
108
+
109
+ def transition_rows(transitions)
110
+ return transitions.map(&:freeze).freeze if transitions
111
+ if @packed
112
+ return TransitionRows.new(states) do |state|
113
+ Array.new(class_count) { |class_id| packed_transition(state, class_id) }
114
+ end
115
+ end
116
+ if @direct
117
+ return TransitionRows.new(states) do |state|
118
+ Array.new(class_count) { |class_id| direct_transition(state, class_id) }
119
+ end
120
+ end
121
+
122
+ raise ArgumentError, "a transition representation is required"
123
+ end
124
+
125
+ def packed_transition(state, class_id)
126
+ cursor = state
127
+ loop do
128
+ index = @packed.fetch(:base).fetch(cursor) + class_id
129
+ return @packed.fetch(:next).fetch(index) if @packed.fetch(:check)[index] == cursor
130
+
131
+ fallback = @packed[:fallback]&.fetch(cursor)
132
+ return @packed.fetch(:default).fetch(cursor) unless fallback
133
+
134
+ cursor = fallback
135
+ end
136
+ end
137
+
138
+ def direct_transition(state, class_id)
139
+ value = @direct.fetch(:nxt).fetch((state * @direct.fetch(:classes)) + class_id)
140
+ value >= 0 ? value : nil
141
+ end
142
+
143
+ def direct_state_count
144
+ return unless @direct
145
+
146
+ @direct.fetch(:nxt).length / @direct.fetch(:classes)
147
+ end
148
+
149
+ def freeze_representation(representation)
150
+ return unless representation
151
+
152
+ representation.each_value { |value| value.freeze if value.respond_to?(:freeze) }
153
+ representation.freeze
154
+ end
68
155
  end
69
156
 
70
157
  class ReferenceDFA
@@ -6,65 +6,113 @@ module Flexr
6
6
  module_function
7
7
 
8
8
  def minimize(dfa)
9
- partitions = initial_partitions(dfa)
10
- loop do
11
- groups = group_ids(partitions)
12
- refined = partitions.flat_map do |partition|
13
- partition.group_by do |state|
14
- [dfa.accepts[state], dfa.transitions[state].map { |destination| destination.nil? ? -1 : groups[destination] }]
15
- end.values
9
+ dead = dfa.states
10
+ rows = dfa.transitions.map { |row| row.map { |destination| destination.nil? ? dead : destination } }
11
+ rows << Array.new(dfa.class_count, dead)
12
+ partitions = initial_partitions(dfa, dead)
13
+ predecessors = predecessor_index(rows, dfa.class_count)
14
+ partitions = refine(partitions, predecessors, dfa.class_count)
15
+ rebuild(dfa, partitions, dead)
16
+ end
17
+
18
+ def initial_partitions(dfa, dead)
19
+ (0..dead).group_by { |state| state == dead ? [] : dfa.accepts[state] }.values
20
+ end
21
+
22
+ def predecessor_index(rows, class_count)
23
+ Array.new(class_count) { Array.new(rows.length) { [] } }.tap do |index|
24
+ rows.each_with_index do |row, state|
25
+ row.each_with_index { |destination, class_id| index[class_id][destination] << state }
16
26
  end
17
- break if refined == partitions
27
+ end
28
+ end
18
29
 
19
- partitions = refined
30
+ def refine(partitions, predecessors, class_count)
31
+ worklist = partitions.dup
32
+ pending = {}.compare_by_identity
33
+ partitions.each { |partition| pending[partition] = true }
34
+ cursor = 0
35
+ while cursor < worklist.length
36
+ splitter = worklist.fetch(cursor)
37
+ cursor += 1
38
+ next unless pending.delete(splitter)
39
+
40
+ class_count.times do |class_id|
41
+ marked = splitter.each_with_object({}) do |state, result|
42
+ predecessors[class_id][state].each { |predecessor| result[predecessor] = true }
43
+ end
44
+ next if marked.empty?
45
+
46
+ partitions = split_partitions(partitions, marked, worklist, pending)
47
+ end
20
48
  end
21
- rebuild(dfa, partitions)
49
+ partitions
22
50
  end
23
51
 
24
- def initial_partitions(dfa)
25
- (0...dfa.states).group_by { |state| dfa.accepts[state] }.values
52
+ def split_partitions(partitions, marked, worklist, pending)
53
+ partitions.flat_map do |partition|
54
+ inside, outside = partition.partition { |state| marked[state] }
55
+ next [partition] if inside.empty? || outside.empty?
56
+
57
+ if pending.delete(partition)
58
+ enqueue(inside, worklist, pending)
59
+ enqueue(outside, worklist, pending)
60
+ else
61
+ enqueue(inside.length <= outside.length ? inside : outside, worklist, pending)
62
+ end
63
+ [inside, outside]
64
+ end
26
65
  end
27
66
 
28
- def group_ids(partitions)
29
- ids = {}
30
- partitions.each_with_index { |partition, id| partition.each { |state| ids[state] = id } }
31
- ids
67
+ def enqueue(partition, worklist, pending)
68
+ worklist << partition
69
+ pending[partition] = true
32
70
  end
33
71
 
34
- def rebuild(dfa, partitions)
72
+ def rebuild(dfa, partitions, dead)
35
73
  groups = group_ids(partitions)
74
+ dead_group = groups.fetch(dead)
36
75
  start_group = groups.fetch(dfa.start)
37
- order = bfs_groups(dfa, groups, start_group)
76
+ representatives = partitions.map { |partition| partition.find { |state| state != dead } }
77
+ order = reachable_groups(dfa, groups, representatives, start_group, dead_group)
38
78
  index = order.each_with_index.to_h
39
79
  transitions = order.map do |group|
40
- representative = partitions[group].first
41
- dfa.transitions[representative].map { |destination| destination.nil? ? nil : index[groups[destination]] }
80
+ representative = representatives.fetch(group)
81
+ dfa.transitions.fetch(representative).map do |destination|
82
+ next nil if destination.nil? || groups.fetch(destination) == dead_group
83
+
84
+ index.fetch(groups.fetch(destination))
85
+ end
42
86
  end
43
- accepts = order.map { |group| dfa.accepts[partitions[group].first] }
87
+ accepts = order.map { |group| dfa.accepts.fetch(representatives.fetch(group)) }
44
88
  rule_ids = accepts.flatten.map(&:rule_index).uniq.sort
45
89
  DFA.new(transitions: transitions, accepts: accepts, ec: dfa.ec, class_count: dfa.class_count,
46
90
  start: 0, rule_ids: rule_ids)
47
91
  end
48
92
 
49
- def bfs_groups(dfa, groups, start_group)
93
+ def group_ids(partitions)
94
+ partitions.each_with_index.with_object({}) do |(partition, group), result|
95
+ partition.each { |state| result[state] = group }
96
+ end
97
+ end
98
+
99
+ def reachable_groups(dfa, groups, representatives, start_group, dead_group)
50
100
  result = []
51
101
  queue = [start_group]
52
102
  seen = {}
53
- until queue.empty?
54
- group = queue.shift
55
- next if seen[group]
103
+ cursor = 0
104
+ while cursor < queue.length
105
+ group = queue.fetch(cursor)
106
+ cursor += 1
107
+ next if seen[group] || (group == dead_group && group != start_group)
56
108
 
57
109
  seen[group] = true
58
110
  result << group
59
- representative = dfa.transitions[group_members(dfa, groups, group).first]
60
- representative.compact.each { |destination| queue << groups[destination] }
111
+ representative = representatives.fetch(group)
112
+ dfa.transitions.fetch(representative).compact.each { |destination| queue << groups.fetch(destination) }
61
113
  end
62
114
  result
63
115
  end
64
-
65
- def group_members(dfa, groups, group)
66
- (0...dfa.states).select { |state| groups[state] == group }
67
- end
68
116
  end
69
117
  end
70
118
  end
@@ -4,11 +4,6 @@ module Flexr
4
4
  module Automaton
5
5
  NFAState = Struct.new(:epsilon, :transitions, :accepts, keyword_init: true)
6
6
  NFATransition = Struct.new(:lo, :hi, :to, keyword_init: true)
7
- Acceptance = Struct.new(:rule_index, :pattern_index, :bol_only, :end_anchor, keyword_init: true) do
8
- def inspect
9
- [rule_index, pattern_index, bol_only, end_anchor].inspect
10
- end
11
- end
12
7
 
13
8
  class NFA
14
9
  attr_reader :states, :start, :byte_classes
@@ -56,6 +51,8 @@ module Flexr
56
51
  when Regexp::AST::Empty
57
52
  state = @nfa.new_state
58
53
  [state, state]
54
+ when Regexp::AST::Fail
55
+ [@nfa.new_state, @nfa.new_state]
59
56
  when Regexp::AST::ByteRange
60
57
  from = @nfa.new_state
61
58
  to = @nfa.new_state
@@ -75,18 +72,53 @@ module Flexr
75
72
  end
76
73
  [from, to]
77
74
  when Regexp::AST::Star
78
- from = @nfa.new_state
79
- to = @nfa.new_state
80
- child_start, child_end = fragment(node.child)
81
- @nfa.epsilon(from, to)
82
- @nfa.epsilon(from, child_start)
83
- @nfa.epsilon(child_end, child_start)
84
- @nfa.epsilon(child_end, to)
85
- [from, to]
75
+ repeat_fragment(node.child, 0, nil)
76
+ when Regexp::AST::Repeat
77
+ repeat_fragment(node.child, node.minimum, node.maximum)
86
78
  else
87
79
  raise CompileError, "cannot build NFA from #{node.class}"
88
80
  end
89
81
  end
82
+
83
+ def repeat_fragment(child, minimum, maximum)
84
+ return fragment(Regexp::AST::Empty.new(loc: nil)) if maximum&.zero?
85
+ return star_fragment(child) if minimum.zero? && maximum.nil?
86
+
87
+ from = @nfa.new_state
88
+ cursor = from
89
+ last_start = nil
90
+ minimum.times do
91
+ child_start, child_end = fragment(child)
92
+ @nfa.epsilon(cursor, child_start)
93
+ cursor = child_end
94
+ last_start = child_start
95
+ end
96
+ if maximum.nil?
97
+ @nfa.epsilon(cursor, last_start)
98
+ return [from, cursor]
99
+ end
100
+
101
+ (maximum - minimum).times do
102
+ child_start, child_end = fragment(child)
103
+ next_cursor = @nfa.new_state
104
+ @nfa.epsilon(cursor, next_cursor)
105
+ @nfa.epsilon(cursor, child_start)
106
+ @nfa.epsilon(child_end, next_cursor)
107
+ cursor = next_cursor
108
+ end
109
+ [from, cursor]
110
+ end
111
+
112
+ def star_fragment(child)
113
+ from = @nfa.new_state
114
+ to = @nfa.new_state
115
+ child_start, child_end = fragment(child)
116
+ @nfa.epsilon(from, to)
117
+ @nfa.epsilon(from, child_start)
118
+ @nfa.epsilon(child_end, child_start)
119
+ @nfa.epsilon(child_end, to)
120
+ [from, to]
121
+ end
90
122
  end
91
123
  end
92
124
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Automaton
5
+ Acceptance = Struct.new(:rule_index, :pattern_index, :bol_only, :end_anchor, keyword_init: true) do
6
+ def inspect
7
+ [rule_index, pattern_index, bol_only, end_anchor].inspect
8
+ end
9
+ end
10
+
11
+ CompiledSpec = Struct.new(:machines, :rules, :states, :stats, :diagnostics, keyword_init: true)
12
+ Machine = Struct.new(:dfa, :state_name, keyword_init: true)
13
+ end
14
+ end
data/lib/flexr/cli.rb CHANGED
@@ -1,5 +1,11 @@
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
+ require_relative "importer"
7
+ require_relative "options"
8
+
3
9
  module Flexr
4
10
  module CLI
5
11
  COMMANDS = %w[check stats tokens dot explain trace bench import].freeze
@@ -121,13 +127,13 @@ module Flexr
121
127
  return EXIT_FAILURE unless result.complete?
122
128
 
123
129
  if output
124
- File.binwrite(output, result.source)
130
+ ArtifactWriter.write!(output, result.source, source_path: spec)
125
131
  else
126
132
  out.write(result.source)
127
133
  end
128
134
  EXIT_OK
129
135
  when :generate
130
- target = output || spec.sub(/\.flexr\.rb\z/, ".rb")
136
+ target = output || ArtifactWriter.default_generated_path(spec)
131
137
  Generator.new(spec, output: target, eval_mode: options.eval_mode,
132
138
  options: options.generator_options).generate
133
139
  EXIT_OK
@@ -142,7 +148,7 @@ module Flexr
142
148
  else
143
149
  compile(read_spec(spec), overrides: options.overrides)
144
150
  end
145
- diagnostics = compiled.is_a?(Array) ? compiled : Array(compiled&.diagnostics)
151
+ diagnostics = (compiled.is_a?(Array) ? compiled : Array(compiled&.diagnostics)).dup
146
152
  diagnostics.select! do |diagnostic|
147
153
  options.warn_level == :all || (options.warn_level == :default && diagnostic.code != "FLEXR-W016")
148
154
  end
@@ -275,8 +281,8 @@ module Flexr
275
281
  klass.encoding(config.fetch(:encoding, Encoding::UTF_8))
276
282
  Array(config[:declared_tokens]).each { |token| klass.emits(token) }
277
283
  config_options = config.fetch(:options, {}).merge(overrides.slice(:experimental, :allow_empty_match))
278
- config_options.each do |name, value|
279
- value ? klass.option(name) : nil
284
+ config_options.slice(*Configuration::BOOLEAN_OPTIONS).each do |name, value|
285
+ klass.option(name) if value
280
286
  end
281
287
  klass.__flexr_config.options[:max_dfa_states] = overrides[:max_dfa_states] if overrides[:max_dfa_states]
282
288
  accel = overrides.fetch(:accel, config_options[:accel])
@@ -4,49 +4,13 @@ module Flexr
4
4
  module Codegen
5
5
  class Direct < Table
6
6
  def generate(state: :initial)
7
- table = super
8
- table.merge(dispatch: :case).freeze
7
+ dfa = compiled.machines.fetch(state).dfa
8
+ {
9
+ nxt: dfa.transitions.flatten.map { |value| value || -1 }.freeze,
10
+ classes: dfa.class_count
11
+ }.freeze
9
12
  end
10
13
 
11
- def source(indent: " ")
12
- lines = []
13
- lines << "#{indent}def self.__flexr_generated_direct_transition(state_name, state, byte)"
14
- lines << "#{indent} case state_name"
15
- compiled.machines.each do |state_name, machine|
16
- lines << "#{indent} when #{state_name.inspect}"
17
- lines << "#{indent} class_id = case byte"
18
- byte_classes(machine.dfa.ec).each do |first, last, class_id|
19
- selector = first == last ? first.to_s : "#{first}..#{last}"
20
- lines << "#{indent} when #{selector} then #{class_id}"
21
- end
22
- lines << "#{indent} end"
23
- lines << "#{indent} case state"
24
- machine.dfa.transitions.each_with_index do |row, state|
25
- lines << "#{indent} when #{state}"
26
- lines << "#{indent} case class_id"
27
- row.each_with_index do |destination, class_id|
28
- value = destination.nil? ? "nil" : destination
29
- lines << "#{indent} when #{class_id} then #{value}"
30
- end
31
- lines << "#{indent} else nil"
32
- lines << "#{indent} end"
33
- end
34
- lines << "#{indent} else nil"
35
- lines << "#{indent} end"
36
- end
37
- lines << "#{indent} else nil"
38
- lines << "#{indent} end"
39
- lines << "#{indent}end"
40
- "#{lines.join("\n")}\n"
41
- end
42
-
43
- private
44
-
45
- def byte_classes(ec)
46
- ec.each_with_index.chunk_while { |left, right| right[0] == left[0] }.map do |run|
47
- [run.first[1], run.last[1], run.first[0]]
48
- end
49
- end
50
14
  end
51
15
  end
52
16
  end