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
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  # Generated by flexr. DO NOT EDIT.
3
3
  # source: lib/flexr/regexp/tokenizer.flexr.rb
4
- # spec-digest: sha256:3129b1c3e05f1c0680e3176ad96eb4ca7ad77fdd7168c47228351e47d79ad8f4
4
+ # spec-digest: sha256:ef176b30fa1aec18f8474cdc44dfc9004802e14e4d7ed89e9a20890930811c63
5
+ # artifact-schema: 1
6
+ # runtime-abi: 1
5
7
  # unicode: 15.1.0
6
8
  # backend: table
7
9
  # compiled: true
@@ -15,48 +17,141 @@ module Flexr
15
17
  # This specification is intentionally small: it is a dogfood target for
16
18
  # the source lexer path, not the parser's implementation.
17
19
  class SourceLexer < Flexr::Lexer
18
- Flexr::Generated.install_compiled!(self, { rules: [{ index: 0, patterns: [/[ \t\r\n]+/], pattern_conditions: [[0, 0, false, false]], trailing: nil, action: :skip, states: [:initial], bol_only: false, end_anchor: nil }, { index: 1, patterns: [/\\p\{[A-Za-z_][A-Za-z0-9_]*\}/], pattern_conditions: [[1, 0, false, false]], trailing: nil, action: proc{ emit :PROPERTY, text.byteslice(3...-1) }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 2, patterns: [/\\./], pattern_conditions: [[2, 0, false, false]], trailing: nil, action: proc{ emit :ESCAPE, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 3, patterns: [/\[[^\]\n]*\]/], pattern_conditions: [[3, 0, false, false]], trailing: nil, action: proc{ emit :CHAR_CLASS, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 4, patterns: [/[?*+]|\{[0-9]+(?:,[0-9]*)?\}/], pattern_conditions: [[4, 0, false, false]], trailing: nil, action: proc{ emit :QUANTIFIER, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 5, patterns: [/\|/], pattern_conditions: [[5, 0, false, false]], trailing: nil, action: proc{ emit :ALTERNATION, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 6, patterns: [/\(/], pattern_conditions: [[6, 0, false, false]], trailing: nil, action: proc{ emit :GROUP_OPEN, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 7, patterns: [/\)/], pattern_conditions: [[7, 0, false, false]], trailing: nil, action: proc{ emit :GROUP_CLOSE, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 8, patterns: [/\^|\$/], pattern_conditions: [[8, 0, false, false]], trailing: nil, action: proc{ emit :ANCHOR, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 9, patterns: [/\./], pattern_conditions: [[9, 0, false, false]], trailing: nil, action: proc{ emit :DOT, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 10, patterns: [/[^\\\[\]().|?*+{}^$ \t\r\n]/], pattern_conditions: [[10, 0, false, false]], trailing: nil, action: proc{ emit :LITERAL, text }, states: [:initial], bol_only: false, end_anchor: nil }], backend: :table, token_kind: :array, encoding: Encoding::BINARY, declared_tokens: [:PROPERTY, :ESCAPE, :CHAR_CLASS, :QUANTIFIER, :ALTERNATION, :GROUP_OPEN, :GROUP_CLOSE, :ANCHOR, :DOT, :LITERAL], options: {}, eof_rules: { }, states: [], inclusive_states: {initial: true}, compiled: { machines: { :initial => { state_name: :initial, dfa: {accepts: [[], [[10, 0, false, false]], [[0, 0, false, false]], [[8, 0, false, false]], [[6, 0, false, false]], [[7, 0, false, false]], [[4, 0, false, false]], [[9, 0, false, false]], [], [], [], [[5, 0, false, false]], [[3, 0, false, false]], [[2, 0, false, false]], [[2, 0, false, false]], [], [], [], [], [[1, 0, false, false]]], ec: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 7, 7, 8, 9, 9, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 23, 24, 25, 26, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 32, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], class_count: 35, state_count: 20, start: 0, rule_ids: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], packed: {base: [0, 0, 12, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 3, 22, 15, 4, 21, 0], default: [1, nil, nil, nil, nil, nil, nil, nil, 8, 13, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], next: [nil, 2, 2, nil, 2, nil, 2, nil, 3, nil, 4, 5, 6, 2, 2, 7, 2, 15, 2, 6, nil, 17, 8, 9, nil, 3, nil, 12, nil, nil, 14, 10, 11, nil, 16, 17, 18, 6, 18, 15, nil, 18, 18, 18, 18, 18, nil, 18, nil, 18, 18, 18, nil, nil, 19, 6], check: [nil, 0, 0, 9, 0, 8, 0, nil, 0, nil, 0, 0, 0, 2, 2, 0, 2, 10, 2, 0, nil, 17, 0, 0, 0, 0, nil, 8, nil, nil, 9, 0, 0, 0, 14, 15, 16, 17, 18, 15, nil, 16, 18, 16, 16, 16, nil, 18, nil, 18, 18, 18, nil, nil, 18, 15]}} } }, states: [:initial], stats: {initial: {states: 20, classes: 35, accepting_states: 12}}, diagnostics: [] } })
20
+ Flexr::Generated.install_compiled!(self, { artifact: {schema_version: 1, compiler_version: "1.1.0", runtime_abi_version: 1, unicode_version: "15.1.0"}, rules: [{ index: 0, patterns: [/[ \t\r\n]+/], pattern_conditions: [[0, 0, false, false]], trailing: nil, action: :skip, states: [:initial], bol_only: false, end_anchor: nil }, { index: 1, patterns: [/\\p\{[A-Za-z_][A-Za-z0-9_]*\}/], pattern_conditions: [[1, 0, false, false]], trailing: nil, action: proc{ emit :PROPERTY, text.byteslice(3...-1) }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 2, patterns: [/\\./], pattern_conditions: [[2, 0, false, false]], trailing: nil, action: proc{ emit :ESCAPE, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 3, patterns: [/\[[^\]\n]*\]/], pattern_conditions: [[3, 0, false, false]], trailing: nil, action: proc{ emit :CHAR_CLASS, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 4, patterns: [/[?*+]|\{[0-9]+(?:,[0-9]*)?\}/], pattern_conditions: [[4, 0, false, false]], trailing: nil, action: proc{ emit :QUANTIFIER, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 5, patterns: [/\|/], pattern_conditions: [[5, 0, false, false]], trailing: nil, action: proc{ emit :ALTERNATION, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 6, patterns: [/\(/], pattern_conditions: [[6, 0, false, false]], trailing: nil, action: proc{ emit :GROUP_OPEN, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 7, patterns: [/\)/], pattern_conditions: [[7, 0, false, false]], trailing: nil, action: proc{ emit :GROUP_CLOSE, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 8, patterns: [/\^|\$/], pattern_conditions: [[8, 0, false, false]], trailing: nil, action: proc{ emit :ANCHOR, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 9, patterns: [/\./], pattern_conditions: [[9, 0, false, false]], trailing: nil, action: proc{ emit :DOT, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 10, patterns: [/[^\\\[\]().|?*+{}^$ \t\r\n]/], pattern_conditions: [[10, 0, false, false]], trailing: nil, action: proc{ emit :LITERAL, text }, states: [:initial], bol_only: false, end_anchor: nil }], backend: :table, token_kind: :array, encoding: Encoding::BINARY, declared_tokens: [:PROPERTY, :ESCAPE, :CHAR_CLASS, :QUANTIFIER, :ALTERNATION, :GROUP_OPEN, :GROUP_CLOSE, :ANCHOR, :DOT, :LITERAL], options: {}, eof_rules: { }, states: [], inclusive_states: {initial: true}, compiled: { machines: { :initial => { state_name: :initial, dfa: {accepts: [[], [[10, 0, false, false]], [[0, 0, false, false]], [[8, 0, false, false]], [[6, 0, false, false]], [[7, 0, false, false]], [[4, 0, false, false]], [[9, 0, false, false]], [], [], [], [[5, 0, false, false]], [[3, 0, false, false]], [[2, 0, false, false]], [[2, 0, false, false]], [], [], [], [], [[1, 0, false, false]]], ec: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 7, 7, 8, 9, 9, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 23, 24, 25, 26, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 32, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], class_count: 35, state_count: 20, start: 0, rule_ids: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], packed: {base: [0, 0, 12, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 3, 22, 15, 4, 21, 0], default: [1, nil, nil, nil, nil, nil, nil, nil, 8, 13, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], next: [nil, 2, 2, nil, 2, nil, 2, nil, 3, nil, 4, 5, 6, 2, 2, 7, 2, 15, 2, 6, nil, 17, 8, 9, nil, 3, nil, 12, nil, nil, 14, 10, 11, nil, 16, 17, 18, 6, 18, 15, nil, 18, 18, 18, 18, 18, nil, 18, nil, 18, 18, 18, nil, nil, 19, 6], check: [nil, 0, 0, 9, 0, 8, 0, nil, 0, nil, 0, 0, 0, 2, 2, 0, 2, 10, 2, 0, nil, 17, 0, 0, 0, 0, nil, 8, nil, nil, 9, 0, 0, 0, 14, 15, 16, 17, 18, 15, nil, 16, 18, 16, 16, 16, nil, 18, nil, 18, 18, 18, nil, nil, 18, 15]}} } }, states: [:initial], stats: {initial: {states: 20, classes: 35, accepting_states: 12}}, diagnostics: [] } })
19
21
  def scan_one
20
- return Flexr::Runtime::Interpreter.new(self).scan unless __flexr_generated_fast_path?
21
- machine = self.class.__flexr_compiled.machines.fetch(state)
22
- dfa = machine.dfa
22
+ if @__flexr_generated_scan_state != @state
23
+ machine = self.class.__flexr_compiled.machines.fetch(@state)
24
+ @__flexr_generated_scan_state = @state
25
+ @__flexr_generated_scan_dfa = machine.dfa
26
+ @__flexr_generated_scan_fast = __flexr_generated_fast_path?(@__flexr_generated_scan_dfa)
27
+ @__flexr_generated_scan_simple = __flexr_generated_simple_fast_path?(@__flexr_generated_scan_dfa)
28
+ end
29
+ dfa = @__flexr_generated_scan_dfa
30
+ return Flexr::Runtime::Interpreter.new(self).scan unless @__flexr_generated_scan_fast
23
31
  position = byte_pos
24
32
  return nil unless valid_utf8_at?(position)
33
+ return __flexr_generated_scan_simple(dfa, position) if @__flexr_generated_scan_simple
25
34
  cursor = position
26
35
  current = dfa.start
27
36
  direct = dfa.direct
28
- source = buffer.source
37
+ direct_nxt = direct&.fetch(:nxt)
38
+ direct_classes = direct&.fetch(:classes)
39
+ ec = dfa.ec
40
+ source = buffer.stable_source
41
+ guarded_steps = scan_steps_guarded?
42
+ token_start = more_text_start || position
43
+ token_limit = max_token_size
44
+ check_token_limit = token_limit_required?
29
45
  best = nil
30
- accelerate = self.class.__flexr_config.options.fetch(:accel, :auto) != :none && !utf8_input?
31
- while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)
46
+ accel_mode = @accel_mode
47
+ accelerate = accel_mode != :none && !(accel_mode == :auto && @__flexr_generated_auto_accel_off)
48
+ while cursor < (source ? source.bytesize : buffer.bytesize) || buffer.ensure_available?(cursor + 1)
32
49
  if accelerate
33
50
  region = __flexr_generated_acceleration_region(dfa, current)
34
- accelerated_end = __flexr_generated_accelerate(region, cursor) if region
51
+ acceptance = dfa.accepts[current].first
52
+ token_rule = acceptance&.rule_index
53
+ accelerated_end = __flexr_generated_accelerate(region, cursor, token_rule, position) if region
35
54
  if accelerated_end && accelerated_end > cursor
36
55
  cursor = accelerated_end
37
56
  best = __flexr_generated_acceptance(dfa, current, position, cursor, best)
38
57
  next
39
58
  end
40
59
  end
41
- byte = source.getbyte(cursor)
42
- current = if direct
43
- class_id = dfa.ec[byte]
44
- value = direct[:nxt][(current * direct[:classes]) + class_id]
60
+ byte = source ? source.getbyte(cursor) : buffer.getbyte(cursor)
61
+ consume_step! if guarded_steps
62
+ current = if direct_nxt
63
+ class_id = ec[byte]
64
+ value = direct_nxt[(current * direct_classes) + class_id]
45
65
  value >= 0 ? value : nil
46
66
  else
47
67
  dfa.transition(current, byte)
48
68
  end
49
69
  break unless current
50
70
  cursor += 1
51
- acceptance = dfa.accepts[current].first
52
- if acceptance
53
- rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
71
+ resource_acceptance = dfa.accepts[current].first
72
+ resource_rule = resource_acceptance&.rule_index
73
+ if check_token_limit
74
+ size = cursor - token_start
75
+ defer_token_size_check!(size, rule: resource_rule) if size > token_limit
76
+ end
77
+ best = __flexr_generated_acceptance(dfa, current, position, cursor, best)
78
+ end
79
+ best
80
+ end
81
+ def __flexr_generated_scan_simple(dfa, position)
82
+ current = dfa.start
83
+ direct = dfa.direct
84
+ direct_nxt = direct&.fetch(:nxt)
85
+ direct_classes = direct&.fetch(:classes)
86
+ ec = dfa.ec
87
+ accepts = dfa.accepts
88
+ rules = self.class.__flexr_rules
89
+ source = buffer.stable_source
90
+ guarded_steps = scan_steps_guarded?
91
+ token_start = more_text_start || position
92
+ token_limit = max_token_size
93
+ check_token_limit = token_limit_required?
94
+ cursor = position
95
+ best = nil
96
+ accel_mode = @accel_mode
97
+ accelerate = accel_mode != :none && !(accel_mode == :auto && @__flexr_generated_auto_accel_off)
98
+ if source && !guarded_steps && !check_token_limit && !accelerate
99
+ while cursor < source.bytesize
100
+ byte = source.getbyte(cursor)
101
+ current = if direct_nxt
102
+ class_id = ec[byte]
103
+ value = direct_nxt[(current * direct_classes) + class_id]
104
+ value >= 0 ? value : nil
105
+ else
106
+ dfa.transition(current, byte)
107
+ end
108
+ break unless current
109
+ cursor += 1
110
+ acceptance = accepts[current].first
111
+ next unless acceptance
112
+ rule = rules.fetch(acceptance.rule_index)
54
113
  best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)
55
114
  best.rule = rule
56
115
  best.start_pos = position
57
116
  best.end_pos = cursor
58
117
  best.total_end_pos = cursor
59
118
  end
119
+ return best
120
+ end
121
+ while cursor < (source ? source.bytesize : buffer.bytesize) || buffer.ensure_available?(cursor + 1)
122
+ if accelerate
123
+ region = __flexr_generated_acceleration_region(dfa, current)
124
+ acceptance = accepts[current].first
125
+ accelerated_end = __flexr_generated_accelerate(region, cursor, acceptance&.rule_index, position) if region
126
+ if accelerated_end && accelerated_end > cursor
127
+ cursor = accelerated_end
128
+ best = __flexr_generated_acceptance(dfa, current, position, cursor, best)
129
+ next
130
+ end
131
+ end
132
+ byte = source ? source.getbyte(cursor) : buffer.getbyte(cursor)
133
+ consume_step! if guarded_steps
134
+ current = if direct_nxt
135
+ class_id = ec[byte]
136
+ value = direct_nxt[(current * direct_classes) + class_id]
137
+ value >= 0 ? value : nil
138
+ else
139
+ dfa.transition(current, byte)
140
+ end
141
+ break unless current
142
+ cursor += 1
143
+ acceptance = accepts[current].first
144
+ if check_token_limit
145
+ size = cursor - token_start
146
+ defer_token_size_check!(size, rule: acceptance&.rule_index) if size > token_limit
147
+ end
148
+ next unless acceptance
149
+ rule = rules.fetch(acceptance.rule_index)
150
+ best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)
151
+ best.rule = rule
152
+ best.start_pos = position
153
+ best.end_pos = cursor
154
+ best.total_end_pos = cursor
60
155
  end
61
156
  best
62
157
  end
@@ -65,44 +160,75 @@ module Flexr
65
160
  regions = (@__flexr_generated_accel_regions[dfa] ||= Flexr::Automaton::Accel.extract(dfa).to_h { |region| [region.state, region] })
66
161
  region = regions[state]
67
162
  return unless region
163
+ return if @__flexr_generated_disabled_auto_accel&.key?(region)
164
+ return if utf8_input? && region.bytes.any? { |byte| byte >= 128 }
68
165
  return if dfa.accepts[state].any? do |acceptance|
69
166
  rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
70
167
  acceptance.bol_only || acceptance.end_anchor || rule.trailing
71
168
  end
72
169
  region
73
170
  end
74
- def __flexr_generated_accelerate(region, position)
75
- binary = buffer.source.b
76
- mode = self.class.__flexr_config.options.fetch(:accel, :auto)
77
- match_end = if %i[strscan auto].include?(mode) && defined?(StringScanner)
78
- scanner = StringScanner.new(binary)
79
- scanner.pos = position
80
- length = scanner.skip(region.regexp)
81
- length && scanner.pos
82
- else
83
- match = region.regexp.match(binary, position)
84
- match && match.begin(0) == position ? match.end(0) : nil
85
- end
86
- return match_end if match_end && match_end < buffer.bytesize
87
- return match_end if match_end && buffer.eof_loaded?
88
- return unless buffer.ensure_available?(buffer.bytesize + 1)
89
- __flexr_generated_accelerate(region, position)
90
- rescue ArgumentError
91
- nil
92
- end
93
- def __flexr_generated_fast_path?
94
- return @__flexr_generated_fast_path if defined?(@__flexr_generated_fast_path)
95
- @__flexr_generated_fast_path = if self.class.__flexr_config.backend == :firstmatch ||
96
- self.class.__flexr_config.options[:allow_empty_match] == true
97
- false
98
- else
99
- self.class.__flexr_rules.none? do |rule|
100
- !rule.trailing.nil? || rule.patterns.any? do |pattern|
101
- next false unless pattern.is_a?(Regexp)
102
- next true if pattern.source.match?(/\\[pP]\\{/) || pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
103
- self.class.__flexr_config.options[:unicode] == true && utf8_input? && pattern.source.match?(/\\[dDwWsS]/)
171
+ def __flexr_generated_accelerate(region, position, token_rule, token_start)
172
+ mode = @accel_mode
173
+ cursor = position
174
+ matched = false
175
+ loop do
176
+ if cursor >= buffer.bytesize
177
+ return matched ? cursor : nil if buffer.eof_loaded?
178
+ return matched ? cursor : nil unless buffer.ensure_available?(cursor + 1)
179
+ end
180
+ segment = buffer.source
181
+ segment_position = cursor - buffer.base_offset
182
+ if segment.encoding == ::Encoding::UTF_8 && region.utf8_regexp
183
+ regexp = region.utf8_regexp
184
+ else
185
+ segment = segment.b unless segment.encoding == ::Encoding::BINARY
186
+ regexp = region.regexp
187
+ end
188
+ length = begin
189
+ if %i[strscan auto].include?(mode) && defined?(::StringScanner)
190
+ @__flexr_generated_accel_scanner ||= ::StringScanner.new("".b)
191
+ @__flexr_generated_accel_scanner.string = segment
192
+ @__flexr_generated_accel_scanner.pos = segment_position
193
+ @__flexr_generated_accel_scanner.skip(regexp)
194
+ else
195
+ match = regexp.match(segment, segment_position)
196
+ match && match.begin(0) == segment_position ? match.end(0) - segment_position : nil
104
197
  end
198
+ rescue ArgumentError
199
+ nil
105
200
  end
201
+ __flexr_generated_record_auto_accel_miss(region) if mode == :auto && length.to_i < 8
202
+ return matched ? cursor : nil unless length&.positive?
203
+ matched = true
204
+ cursor += length
205
+ consume_step!(length)
206
+ defer_token_size_check!(cursor - (more_text_start || token_start), rule: token_rule)
207
+ return cursor if cursor < buffer.bytesize || buffer.eof_loaded?
208
+ end
209
+ end
210
+ def __flexr_generated_record_auto_accel_miss(region)
211
+ @__flexr_generated_auto_accel_misses ||= Hash.new(0)
212
+ @__flexr_generated_auto_accel_misses[region] += 1
213
+ @__flexr_generated_auto_accel_miss_total = @__flexr_generated_auto_accel_miss_total.to_i + 1
214
+ @__flexr_generated_auto_accel_off = true if @__flexr_generated_auto_accel_miss_total >= 9
215
+ return if @__flexr_generated_auto_accel_misses[region] < 3
216
+ @__flexr_generated_disabled_auto_accel ||= {}
217
+ @__flexr_generated_disabled_auto_accel[region] = true
218
+ end
219
+ def __flexr_generated_fast_path?(dfa)
220
+ @__flexr_generated_fast_paths ||= {}
221
+ return @__flexr_generated_fast_paths[dfa] if @__flexr_generated_fast_paths.key?(dfa)
222
+ rules = dfa.rule_ids.map { |rule_index| self.class.__flexr_rules.fetch(rule_index) }
223
+ @__flexr_generated_fast_paths[dfa] = self.class.__flexr_config.backend != :firstmatch &&
224
+ self.class.__flexr_config.options[:allow_empty_match] != true && rules.none?(&:trailing)
225
+ end
226
+ def __flexr_generated_simple_fast_path?(dfa)
227
+ @__flexr_generated_simple_fast_paths ||= {}
228
+ return @__flexr_generated_simple_fast_paths[dfa] if @__flexr_generated_simple_fast_paths.key?(dfa)
229
+ rules = dfa.rule_ids.map { |rule_index| self.class.__flexr_rules.fetch(rule_index) }
230
+ @__flexr_generated_simple_fast_paths[dfa] = rules.all? do |rule|
231
+ rule.pattern_conditions.all? { |condition| condition && !condition.bol_only && !condition.end_anchor }
106
232
  end
107
233
  end
108
234
  def __flexr_generated_acceptance(dfa, state, start_position, cursor, best)
@@ -110,7 +236,6 @@ module Flexr
110
236
  next if acceptance.bol_only && !beginning_of_line?
111
237
  next if acceptance.end_anchor && !(buffer.eof?(cursor) || buffer.getbyte(cursor) == 0x0a)
112
238
  rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
113
- defer_token_size_check!(cursor - start_position)
114
239
  next if best && cursor < best.total_end_pos
115
240
  next if best && cursor == best.total_end_pos &&
116
241
  rule.index > best.rule.index
@@ -126,25 +251,25 @@ module Flexr
126
251
  case rule.index
127
252
  when 0
128
253
  when 1
129
- emit :PROPERTY, text.byteslice(3...-1)
254
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
130
255
  when 2
131
- emit :ESCAPE, text
256
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
132
257
  when 3
133
- emit :CHAR_CLASS, text
258
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
134
259
  when 4
135
- emit :QUANTIFIER, text
260
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
136
261
  when 5
137
- emit :ALTERNATION, text
262
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
138
263
  when 6
139
- emit :GROUP_OPEN, text
264
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
140
265
  when 7
141
- emit :GROUP_CLOSE, text
266
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
142
267
  when 8
143
- emit :ANCHOR, text
268
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
144
269
  when 9
145
- emit :DOT, text
270
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
146
271
  when 10
147
- emit :LITERAL, text
272
+ instance_exec(&self.class.__flexr_generated_actions.fetch(rule.index))
148
273
  else
149
274
  instance_exec(&rule.action)
150
275
  end
@@ -153,16 +278,16 @@ module Flexr
153
278
 
154
279
 
155
280
 
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
281
+ __flexr_bind_generated_action(1, proc{ emit :PROPERTY, text.byteslice(3...-1) })
282
+ __flexr_bind_generated_action(2, proc{ emit :ESCAPE, text })
283
+ __flexr_bind_generated_action(3, proc{ emit :CHAR_CLASS, text })
284
+ __flexr_bind_generated_action(4, proc{ emit :QUANTIFIER, text })
285
+ __flexr_bind_generated_action(5, proc{ emit :ALTERNATION, text })
286
+ __flexr_bind_generated_action(6, proc{ emit :GROUP_OPEN, text })
287
+ __flexr_bind_generated_action(7, proc{ emit :GROUP_CLOSE, text })
288
+ __flexr_bind_generated_action(8, proc{ emit :ANCHOR, text })
289
+ __flexr_bind_generated_action(9, proc{ emit :DOT, text })
290
+ __flexr_bind_generated_action(10, proc{ emit :LITERAL, text })
166
291
  end
167
292
  end
168
293
  end
@@ -5,31 +5,65 @@ module Flexr
5
5
  class Buffer
6
6
  DEFAULT_CHUNK_SIZE = 64 * 1024
7
7
 
8
- attr_reader :source
8
+ attr_reader :base_offset, :max_buffer_size
9
9
 
10
- def initialize(input, chunk_size: DEFAULT_CHUNK_SIZE)
10
+ def initialize(input, chunk_size: DEFAULT_CHUNK_SIZE, max_buffer_size: 64 * 1024 * 1024,
11
+ retain_input: true, filename: nil)
11
12
  raise ArgumentError, "chunk_size must be positive" unless chunk_size.to_i.positive?
13
+ raise ArgumentError, "max_buffer_size must be non-negative" if max_buffer_size.to_i.negative?
12
14
 
13
15
  @chunk_size = chunk_size.to_i
16
+ @max_buffer_size = max_buffer_size.to_i
17
+ @retain_input = retain_input
18
+ @filename = filename
19
+ @base_offset = 0
20
+ @window_start = 0
14
21
  @io = input.is_a?(String) ? nil : input
15
22
  raise ArgumentError, "input must be a String or IO" if @io && !@io.respond_to?(:read)
16
23
 
17
24
  @source = input.is_a?(String) ? input : String.new(encoding: Encoding::BINARY)
18
25
  @eof = @io.nil?
26
+ ensure_buffer_size!(@source.bytesize)
19
27
  end
20
28
 
21
29
  def bytesize
22
- source.bytesize
30
+ return @source.bytesize if @window_start.zero? && base_offset.zero?
31
+
32
+ base_offset + retained_bytesize
33
+ end
34
+
35
+ def retained_bytesize
36
+ @source.bytesize - @window_start
37
+ end
38
+
39
+ def source
40
+ return @source if @window_start.zero?
41
+
42
+ @source.byteslice(@window_start..).to_s
43
+ end
44
+
45
+ def stable_source
46
+ @source if @retain_input && @window_start.zero? && base_offset.zero?
23
47
  end
24
48
 
25
49
  def getbyte(position)
50
+ return @source.getbyte(position) if @window_start.zero? && base_offset.zero? && position < @source.bytesize
51
+
26
52
  ensure_available?(position + 1)
27
- source.getbyte(position)
53
+ @source.getbyte(storage_position(position))
28
54
  end
29
55
 
30
56
  def byteslice(range, length = nil)
31
57
  ensure_range(range, length)
32
- length ? source.byteslice(range, length) : source.byteslice(range)
58
+ if @window_start.zero? && base_offset.zero?
59
+ return length ? @source.byteslice(range, length) : @source.byteslice(range)
60
+ end
61
+
62
+ if length
63
+ @source.byteslice(storage_position(range), length)
64
+ else
65
+ @source.byteslice(storage_range(range))
66
+ end
33
67
  end
34
68
 
35
69
  def ensure_available?(end_position)
@@ -37,7 +71,7 @@ module Flexr
37
71
  return false if @eof
38
72
 
39
73
  while bytesize < end_position && !@eof
40
- chunk = @io.read(@chunk_size)
74
+ chunk = read_chunk
41
75
  if chunk.nil? || chunk.empty?
42
76
  @eof = true
43
77
  break
@@ -63,11 +97,11 @@ module Flexr
63
97
  end
64
98
 
65
99
  def utf8_boundary?(position)
66
- return false if position.negative?
67
- return true if position.zero?
100
+ return false if position < base_offset
101
+ return true if position == base_offset
68
102
  return true unless ensure_available?(position)
69
103
 
70
- following = source.getbyte(position)
104
+ following = getbyte(position)
71
105
  following.nil? || (following & 0xc0) != 0x80
72
106
  end
73
107
 
@@ -89,6 +123,31 @@ module Flexr
89
123
  byteslice(position, length).dup.force_encoding(Encoding::UTF_8).valid_encoding?
90
124
  end
91
125
 
126
+ def utf8_character_length(position)
127
+ first = getbyte(position)
128
+ return 0 unless first
129
+ return 1 if first <= 0x7f
130
+
131
+ length = if first <= 0xdf
132
+ 2
133
+ elsif first <= 0xef
134
+ 3
135
+ else
136
+ 4
137
+ end
138
+ valid_utf8_at?(position) ? length : 1
139
+ end
140
+
141
+ def discard_before(position)
142
+ return if @retain_input || position <= base_offset
143
+
144
+ ending = [position, bytesize].min
145
+ removed = ending - base_offset
146
+ @window_start += removed
147
+ @base_offset = ending
148
+ compact_storage!
149
+ end
150
+
92
151
  private
93
152
 
94
153
  def ensure_range(range, length)
@@ -107,6 +166,53 @@ module Flexr
107
166
  ending = range.end + (range.exclude_end? ? 0 : 1)
108
167
  ensure_available?(ending)
109
168
  end
169
+
170
+ def storage_position(position)
171
+ local = position - base_offset
172
+ return @window_start + local unless local.negative?
173
+
174
+ raise RangeError, "byte #{position} has been discarded from the streaming buffer"
175
+ end
176
+
177
+ def storage_range(range)
178
+ return storage_position(range) unless range.is_a?(Range)
179
+
180
+ ending = range.end
181
+ Range.new(storage_position(range.begin), ending.nil? ? nil : storage_position(ending), range.exclude_end?)
182
+ end
183
+
184
+ def ensure_buffer_size!(size)
185
+ return if size <= max_buffer_size
186
+
187
+ raise BufferTooLargeError.new(filename: @filename, byte_pos: bytesize)
188
+ end
189
+
190
+ def read_chunk
191
+ capacity = max_buffer_size - retained_bytesize
192
+ if capacity.zero?
193
+ probe = @io.read(1)
194
+ if probe.nil? || probe.empty?
195
+ @eof = true
196
+ return nil
197
+ end
198
+
199
+ raise BufferTooLargeError.new(filename: @filename, byte_pos: bytesize)
200
+ end
201
+
202
+ @io.read([@chunk_size, capacity].min)
203
+ end
204
+
205
+ def compact_storage!
206
+ return if @window_start.zero?
207
+
208
+ if @window_start == @source.bytesize
209
+ @source = String.new(encoding: @source.encoding)
210
+ @window_start = 0
211
+ elsif @window_start >= @chunk_size || @window_start * 2 >= @source.bytesize
212
+ @source = @source.byteslice(@window_start..)
213
+ @window_start = 0
214
+ end
215
+ end
110
216
  end
111
217
  end
112
218
  end