flexr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +33 -0
  3. data/CONTRIBUTING.md +39 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +116 -0
  6. data/Rakefile +468 -0
  7. data/benchmark/baselines/json.json +34 -0
  8. data/benchmark/baselines/json_handwritten.rb +43 -0
  9. data/benchmark/baselines/json_rexical.rex +25 -0
  10. data/benchmark/corpora/README.md +11 -0
  11. data/benchmark/corpora/generate_json.rb +26 -0
  12. data/benchmark/golden/calculator_lexer.sha256 +1 -0
  13. data/benchmark/golden/json_lexer.sha256 +1 -0
  14. data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
  15. data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
  16. data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
  17. data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
  18. data/benchmark/golden/with_racc_lexer.sha256 +1 -0
  19. data/benchmark/run.rb +254 -0
  20. data/docs/README.md +64 -0
  21. data/docs/RELEASING.md +30 -0
  22. data/docs/adr/0001-byte-level-dfa.md +5 -0
  23. data/docs/adr/0003-leftmost-longest.md +4 -0
  24. data/docs/adr/0006-accel-not-scanner.md +4 -0
  25. data/docs/adr/0008-what-pure-ruby-means.md +5 -0
  26. data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
  27. data/docs/adr/0017-static-analysis-by-default.md +5 -0
  28. data/docs/adr/0018-prism-for-generator-only.md +4 -0
  29. data/docs/adr/0019-measured-performance-floor.md +26 -0
  30. data/docs/adr/0020-vendored-unicode-contract.md +21 -0
  31. data/docs/explanation/backends.md +33 -0
  32. data/docs/explanation/matching-semantics.md +20 -0
  33. data/docs/explanation/runtime-vs-generated.md +22 -0
  34. data/docs/explanation/security-model.md +18 -0
  35. data/docs/explanation/unicode-and-encoding.md +20 -0
  36. data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
  37. data/docs/how-to/generate-a-lexer.md +39 -0
  38. data/docs/how-to/handle-errors.md +32 -0
  39. data/docs/how-to/integrate-with-lrama.md +21 -0
  40. data/docs/how-to/integrate-with-racc.md +25 -0
  41. data/docs/how-to/migrate-from-flex.md +21 -0
  42. data/docs/how-to/migrate-from-rexical.md +23 -0
  43. data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
  44. data/docs/how-to/track-token-locations.md +27 -0
  45. data/docs/how-to/tune-performance.md +23 -0
  46. data/docs/how-to/use-states.md +36 -0
  47. data/docs/how-to/use-trailing-context.md +22 -0
  48. data/docs/internals/README.md +14 -0
  49. data/docs/perf-log.md +56 -0
  50. data/docs/reference/README.md +23 -0
  51. data/docs/reference/actions.md +47 -0
  52. data/docs/reference/cli.md +80 -0
  53. data/docs/reference/compatibility.md +38 -0
  54. data/docs/reference/diagnostics.md +41 -0
  55. data/docs/reference/dsl.md +81 -0
  56. data/docs/reference/errors.md +27 -0
  57. data/docs/reference/generated-artifacts.md +50 -0
  58. data/docs/reference/public-api.md +42 -0
  59. data/docs/reference/regexp.md +39 -0
  60. data/docs/reference/runtime.md +49 -0
  61. data/docs/reference/tokens-and-locations.md +33 -0
  62. data/docs/tutorial/build-a-calculator-lexer.md +96 -0
  63. data/examples/calculator/README.md +27 -0
  64. data/examples/calculator/lexer.flexr.rb +17 -0
  65. data/examples/json/README.md +30 -0
  66. data/examples/json/lexer.flexr.rb +24 -0
  67. data/examples/ruby_subset/README.md +17 -0
  68. data/examples/ruby_subset/lexer.flexr.rb +22 -0
  69. data/examples/toy_lang/README.md +17 -0
  70. data/examples/toy_lang/lexer.flexr.rb +18 -0
  71. data/examples/with_lrama/README.md +17 -0
  72. data/examples/with_lrama/lexer.flexr.rb +13 -0
  73. data/examples/with_racc/README.md +17 -0
  74. data/examples/with_racc/lexer.flexr.rb +13 -0
  75. data/exe/flexr +7 -0
  76. data/lib/flexr/automaton/accel.rb +39 -0
  77. data/lib/flexr/automaton/analysis.rb +38 -0
  78. data/lib/flexr/automaton/byte_class_set.rb +29 -0
  79. data/lib/flexr/automaton/compiler.rb +413 -0
  80. data/lib/flexr/automaton/dfa.rb +103 -0
  81. data/lib/flexr/automaton/minimizer.rb +70 -0
  82. data/lib/flexr/automaton/nfa.rb +92 -0
  83. data/lib/flexr/cli.rb +342 -0
  84. data/lib/flexr/codegen/base.rb +17 -0
  85. data/lib/flexr/codegen/direct.rb +52 -0
  86. data/lib/flexr/codegen/firstmatch.rb +17 -0
  87. data/lib/flexr/codegen/table.rb +158 -0
  88. data/lib/flexr/codegen/table_packer.rb +61 -0
  89. data/lib/flexr/diagnostics.rb +94 -0
  90. data/lib/flexr/dsl.rb +182 -0
  91. data/lib/flexr/errors.rb +28 -0
  92. data/lib/flexr/generated.rb +125 -0
  93. data/lib/flexr/generator.rb +400 -0
  94. data/lib/flexr/importer.rb +560 -0
  95. data/lib/flexr/ir.rb +36 -0
  96. data/lib/flexr/lexer.rb +10 -0
  97. data/lib/flexr/options.rb +47 -0
  98. data/lib/flexr/rake_task.rb +27 -0
  99. data/lib/flexr/regexp/ast.rb +45 -0
  100. data/lib/flexr/regexp/char_class.rb +7 -0
  101. data/lib/flexr/regexp/normalizer.rb +117 -0
  102. data/lib/flexr/regexp/parser.rb +517 -0
  103. data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
  104. data/lib/flexr/regexp/tokenizer.rb +168 -0
  105. data/lib/flexr/regexp/unsupported.rb +7 -0
  106. data/lib/flexr/runtime/buffer.rb +112 -0
  107. data/lib/flexr/runtime/core.rb +388 -0
  108. data/lib/flexr/runtime/errors.rb +22 -0
  109. data/lib/flexr/runtime/interpreter.rb +505 -0
  110. data/lib/flexr/runtime/location.rb +26 -0
  111. data/lib/flexr/runtime/token.rb +7 -0
  112. data/lib/flexr/source/passthrough.rb +31 -0
  113. data/lib/flexr/source/prism_reader.rb +283 -0
  114. data/lib/flexr/source/static_eval.rb +145 -0
  115. data/lib/flexr/unicode/case_fold.rb +45 -0
  116. data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
  117. data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
  118. data/lib/flexr/unicode/data/case_folding.rb +9 -0
  119. data/lib/flexr/unicode/data/properties.rb +10 -0
  120. data/lib/flexr/unicode/property.rb +107 -0
  121. data/lib/flexr/unicode/reference_regexp.rb +102 -0
  122. data/lib/flexr/unicode/utf8_splitter.rb +109 -0
  123. data/lib/flexr/version.rb +5 -0
  124. data/lib/flexr.rb +81 -0
  125. data/site/README.md +22 -0
  126. data/site/astro.config.mjs +57 -0
  127. data/site/package.json +19 -0
  128. data/site/pnpm-lock.yaml +5029 -0
  129. data/site/pnpm-workspace.yaml +6 -0
  130. data/site/public/playground.js +189 -0
  131. data/site/scripts/verify-site.mjs +42 -0
  132. data/site/src/content/docs/benchmarks.md +8 -0
  133. data/site/src/content/docs/concepts/matching-semantics.md +15 -0
  134. data/site/src/content/docs/concepts/regexp-model.md +18 -0
  135. data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
  136. data/site/src/content/docs/concepts/security-model.md +15 -0
  137. data/site/src/content/docs/examples.md +17 -0
  138. data/site/src/content/docs/learn/generation.md +29 -0
  139. data/site/src/content/docs/learn/getting-started.md +56 -0
  140. data/site/src/content/docs/learn/parser-integration.md +27 -0
  141. data/site/src/content/docs/learn/runtime-mode.md +32 -0
  142. data/site/src/content/docs/reference/action-context.md +20 -0
  143. data/site/src/content/docs/reference/cli.md +22 -0
  144. data/site/src/content/docs/reference/diagnostics.md +16 -0
  145. data/site/src/content/docs/reference/dsl.md +19 -0
  146. data/site/src/content/docs/reference/public-api.md +18 -0
  147. data/site/src/content/docs/reference/regexp.md +16 -0
  148. data/site/src/content/docs/reference/runtime.md +16 -0
  149. data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
  150. data/site/src/content.config.ts +12 -0
  151. data/site/src/env.d.ts +1 -0
  152. data/site/src/layouts/SiteLayout.astro +39 -0
  153. data/site/src/pages/index.astro +174 -0
  154. data/site/src/pages/playground.astro +64 -0
  155. data/site/src/styles/custom.css +711 -0
  156. data/site/tsconfig.json +5 -0
  157. data/tools/coverage.rb +32 -0
  158. data/tools/docs_verify.rb +116 -0
  159. data/tools/gen_unicode_tables.rb +202 -0
  160. data/tools/regexp_tokenizer_reference.rb +60 -0
  161. metadata +205 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "flexr"
4
+
5
+ module Flexr
6
+ module Regexp
7
+ # This specification is intentionally small: it is a dogfood target for
8
+ # the source lexer path, not the parser's implementation.
9
+ class SourceLexer < Flexr::Lexer
10
+ encoding Encoding::BINARY
11
+ emits :PROPERTY, :ESCAPE, :CHAR_CLASS, :QUANTIFIER, :ALTERNATION,
12
+ :GROUP_OPEN, :GROUP_CLOSE, :ANCHOR, :DOT, :LITERAL
13
+
14
+ rule(/[ \t\r\n]+/, skip: true)
15
+ rule(/\\p\{[A-Za-z_][A-Za-z0-9_]*\}/) { emit :PROPERTY, text.byteslice(3...-1) }
16
+ rule(/\\./) { emit :ESCAPE, text }
17
+ rule(/\[[^\]\n]*\]/) { emit :CHAR_CLASS, text }
18
+ rule(/[?*+]|\{[0-9]+(?:,[0-9]*)?\}/) { emit :QUANTIFIER, text }
19
+ rule(/\|/) { emit :ALTERNATION, text }
20
+ rule(/\(/) { emit :GROUP_OPEN, text }
21
+ rule(/\)/) { emit :GROUP_CLOSE, text }
22
+ rule(/\^|\$/) { emit :ANCHOR, text }
23
+ rule(/\./) { emit :DOT, text }
24
+ rule(/[^\\\[\]().|?*+{}^$ \t\r\n]/) { emit :LITERAL, text }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ # Generated by flexr. DO NOT EDIT.
3
+ # source: lib/flexr/regexp/tokenizer.flexr.rb
4
+ # spec-digest: sha256:3129b1c3e05f1c0680e3176ad96eb4ca7ad77fdd7168c47228351e47d79ad8f4
5
+ # unicode: 15.1.0
6
+ # backend: table
7
+ # compiled: true
8
+ # eval: false
9
+ # standalone: false
10
+
11
+ require "flexr"
12
+
13
+ module Flexr
14
+ module Regexp
15
+ # This specification is intentionally small: it is a dogfood target for
16
+ # the source lexer path, not the parser's implementation.
17
+ 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: [] } })
19
+ 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
23
+ position = byte_pos
24
+ return nil unless valid_utf8_at?(position)
25
+ cursor = position
26
+ current = dfa.start
27
+ direct = dfa.direct
28
+ source = buffer.source
29
+ 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)
32
+ if accelerate
33
+ region = __flexr_generated_acceleration_region(dfa, current)
34
+ accelerated_end = __flexr_generated_accelerate(region, cursor) if region
35
+ if accelerated_end && accelerated_end > cursor
36
+ cursor = accelerated_end
37
+ best = __flexr_generated_acceptance(dfa, current, position, cursor, best)
38
+ next
39
+ end
40
+ 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]
45
+ value >= 0 ? value : nil
46
+ else
47
+ dfa.transition(current, byte)
48
+ end
49
+ break unless current
50
+ cursor += 1
51
+ acceptance = dfa.accepts[current].first
52
+ if acceptance
53
+ rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
54
+ best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)
55
+ best.rule = rule
56
+ best.start_pos = position
57
+ best.end_pos = cursor
58
+ best.total_end_pos = cursor
59
+ end
60
+ end
61
+ best
62
+ end
63
+ def __flexr_generated_acceleration_region(dfa, state)
64
+ @__flexr_generated_accel_regions ||= {}
65
+ regions = (@__flexr_generated_accel_regions[dfa] ||= Flexr::Automaton::Accel.extract(dfa).to_h { |region| [region.state, region] })
66
+ region = regions[state]
67
+ return unless region
68
+ return if dfa.accepts[state].any? do |acceptance|
69
+ rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
70
+ acceptance.bol_only || acceptance.end_anchor || rule.trailing
71
+ end
72
+ region
73
+ 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]/)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ def __flexr_generated_acceptance(dfa, state, start_position, cursor, best)
109
+ dfa.accepts[state].each do |acceptance|
110
+ next if acceptance.bol_only && !beginning_of_line?
111
+ next if acceptance.end_anchor && !(buffer.eof?(cursor) || buffer.getbyte(cursor) == 0x0a)
112
+ rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
113
+ defer_token_size_check!(cursor - start_position)
114
+ next if best && cursor < best.total_end_pos
115
+ next if best && cursor == best.total_end_pos &&
116
+ rule.index > best.rule.index
117
+ best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)
118
+ best.rule = rule
119
+ best.start_pos = start_position
120
+ best.end_pos = cursor
121
+ best.total_end_pos = cursor
122
+ end
123
+ best
124
+ end
125
+ def __flexr_generated_execute(rule)
126
+ case rule.index
127
+ when 0
128
+ when 1
129
+ emit :PROPERTY, text.byteslice(3...-1)
130
+ when 2
131
+ emit :ESCAPE, text
132
+ when 3
133
+ emit :CHAR_CLASS, text
134
+ when 4
135
+ emit :QUANTIFIER, text
136
+ when 5
137
+ emit :ALTERNATION, text
138
+ when 6
139
+ emit :GROUP_OPEN, text
140
+ when 7
141
+ emit :GROUP_CLOSE, text
142
+ when 8
143
+ emit :ANCHOR, text
144
+ when 9
145
+ emit :DOT, text
146
+ when 10
147
+ emit :LITERAL, text
148
+ else
149
+ instance_exec(&rule.action)
150
+ end
151
+ end
152
+
153
+
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Regexp
5
+ Unsupported = Flexr::UnsupportedRegexpError
6
+ end
7
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Runtime
5
+ class Buffer
6
+ DEFAULT_CHUNK_SIZE = 64 * 1024
7
+
8
+ attr_reader :source
9
+
10
+ def initialize(input, chunk_size: DEFAULT_CHUNK_SIZE)
11
+ raise ArgumentError, "chunk_size must be positive" unless chunk_size.to_i.positive?
12
+
13
+ @chunk_size = chunk_size.to_i
14
+ @io = input.is_a?(String) ? nil : input
15
+ raise ArgumentError, "input must be a String or IO" if @io && !@io.respond_to?(:read)
16
+
17
+ @source = input.is_a?(String) ? input : String.new(encoding: Encoding::BINARY)
18
+ @eof = @io.nil?
19
+ end
20
+
21
+ def bytesize
22
+ source.bytesize
23
+ end
24
+
25
+ def getbyte(position)
26
+ ensure_available?(position + 1)
27
+ source.getbyte(position)
28
+ end
29
+
30
+ def byteslice(range, length = nil)
31
+ ensure_range(range, length)
32
+ length ? source.byteslice(range, length) : source.byteslice(range)
33
+ end
34
+
35
+ def ensure_available?(end_position)
36
+ return true if end_position <= bytesize
37
+ return false if @eof
38
+
39
+ while bytesize < end_position && !@eof
40
+ chunk = @io.read(@chunk_size)
41
+ if chunk.nil? || chunk.empty?
42
+ @eof = true
43
+ break
44
+ end
45
+
46
+ @source.force_encoding(chunk.encoding) if @source.empty?
47
+ @source << chunk
48
+ end
49
+ bytesize >= end_position
50
+ end
51
+
52
+ def read_to_end
53
+ ensure_available?(Float::INFINITY)
54
+ source
55
+ end
56
+
57
+ def eof_loaded?
58
+ @eof
59
+ end
60
+
61
+ def eof?(position)
62
+ !ensure_available?(position + 1)
63
+ end
64
+
65
+ def utf8_boundary?(position)
66
+ return false if position.negative?
67
+ return true if position.zero?
68
+ return true unless ensure_available?(position)
69
+
70
+ following = source.getbyte(position)
71
+ following.nil? || (following & 0xc0) != 0x80
72
+ end
73
+
74
+ def valid_utf8_at?(position)
75
+ first = getbyte(position)
76
+ return false unless first
77
+ return true if first <= 0x7f
78
+
79
+ length = if first.between?(0xc2, 0xdf)
80
+ 2
81
+ elsif first.between?(0xe0, 0xef)
82
+ 3
83
+ elsif first.between?(0xf0, 0xf4)
84
+ 4
85
+ end
86
+ return false unless length
87
+ return false unless ensure_available?(position + length)
88
+
89
+ byteslice(position, length).dup.force_encoding(Encoding::UTF_8).valid_encoding?
90
+ end
91
+
92
+ private
93
+
94
+ def ensure_range(range, length)
95
+ if length
96
+ ensure_available?(range + length)
97
+ return
98
+ end
99
+
100
+ unless range.is_a?(Range)
101
+ ensure_available?(range + 1)
102
+ return
103
+ end
104
+
105
+ return read_to_end if range.end.nil?
106
+
107
+ ending = range.end + (range.exclude_end? ? 0 : 1)
108
+ ensure_available?(ending)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,388 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Runtime
5
+ def initialize(input, filename: nil, error_mode: :raise, max_token_size: 16 * 1024 * 1024,
6
+ max_state_stack: 1024, chunk_size: Runtime::Buffer::DEFAULT_CHUNK_SIZE)
7
+ raise ArgumentError, "max_token_size must be non-negative" if max_token_size.to_i.negative?
8
+ raise ArgumentError, "max_state_stack must be non-negative" if max_state_stack.to_i.negative?
9
+
10
+ self.class.compile!
11
+ @buffer = Runtime::Buffer.new(input, chunk_size: chunk_size)
12
+ @input = input.is_a?(String) ? input : nil
13
+ @line_tracking_needed = !@input || input.include?("\n")
14
+ @filename = filename
15
+ @error_mode = error_mode
16
+ @max_token_size = max_token_size.to_i
17
+ @max_state_stack = max_state_stack.to_i
18
+ @position = 0
19
+ @line = 1
20
+ @state = :initial
21
+ @state_stack = []
22
+ @pending = nil
23
+ @matched = nil
24
+ @match_start = 0
25
+ @match_end = 0
26
+ @text_start = 0
27
+ @text_line = 1
28
+ @bol = true
29
+ @more_start = nil
30
+ @more_line = nil
31
+ @more_requested = false
32
+ @candidate_token_size = 0
33
+ @eof_fired_states = {}
34
+ @on_error = nil
35
+ @halted = false
36
+ @interpreter = nil
37
+ end
38
+
39
+ attr_reader :filename, :error_mode, :buffer, :max_token_size
40
+
41
+ attr_writer :on_error
42
+
43
+ def input
44
+ @buffer.source
45
+ end
46
+
47
+ def next_token
48
+ return nil if @halted
49
+
50
+ loop do
51
+ return nil if @halted
52
+
53
+ if eof? && @pending.nil?
54
+ eof_action = self.class.__flexr_spec.eof_rules[@state]
55
+ if eof_action && !@eof_fired_states[@state]
56
+ @eof_fired_states[@state] = true
57
+ @match_start = @position
58
+ @match_end = @position
59
+ @text_start = @position
60
+ @text_line = @line
61
+ @matched = nil
62
+ instance_exec(&eof_action)
63
+ token = @pending
64
+ @pending = nil
65
+ return token if token
66
+ next
67
+ end
68
+ return nil
69
+ end
70
+
71
+ match = if generated_runtime? && respond_to?(:scan_one, true)
72
+ scan_one
73
+ else
74
+ (@interpreter ||= Runtime::Interpreter.new(self)).scan
75
+ end
76
+ unless match
77
+ unless eof?
78
+ token = handle_unmatched_byte
79
+ next unless token
80
+
81
+ return token
82
+ end
83
+ return nil
84
+ end
85
+ @match_start = match.start_pos
86
+ @match_end = match.end_pos
87
+ if @more_start
88
+ @text_start = @more_start
89
+ @text_line = @more_line
90
+ else
91
+ @text_start = @match_start
92
+ @text_line = @line
93
+ end
94
+ @matched = nil
95
+ @more_requested = false
96
+ @position = match.end_pos
97
+ empty_match = match.end_pos == match.start_pos
98
+ execute(match.rule)
99
+ finalize_more
100
+ force_empty_match_progress! if empty_match && self.class.__flexr_config.options[:allow_empty_match]
101
+ ensure_token_size!
102
+ update_position
103
+ token = @pending
104
+ @pending = nil
105
+ return token if token
106
+ end
107
+ end
108
+
109
+ def each_token
110
+ return enum_for(__method__) unless block_given?
111
+
112
+ loop do
113
+ token = next_token
114
+ break unless token
115
+
116
+ if self.class.__flexr_config.token_kind == :yield
117
+ yield(*token)
118
+ else
119
+ yield token
120
+ end
121
+ end
122
+ self
123
+ end
124
+
125
+ def tokens
126
+ result = []
127
+ while (token = next_token)
128
+ result << token
129
+ end
130
+ result
131
+ end
132
+
133
+ def racc_next_token
134
+ token = next_token
135
+ token ? [token[0], token[1]] : [false, "$end"]
136
+ end
137
+
138
+ def text
139
+ return @matched if @matched
140
+
141
+ @matched = @buffer.byteslice(@text_start...@match_end)
142
+ end
143
+
144
+ def text_bytesize
145
+ @match_end - @text_start
146
+ end
147
+
148
+ def byte_pos
149
+ @position
150
+ end
151
+
152
+ def more_text_start
153
+ @more_start
154
+ end
155
+
156
+ def defer_token_size_check!(size)
157
+ @candidate_token_size = size if size > @candidate_token_size
158
+ end
159
+
160
+ def utf8_input?
161
+ self.class.__flexr_config.encoding == Encoding::UTF_8
162
+ end
163
+
164
+ def valid_utf8_at?(position)
165
+ !utf8_input? || @buffer.valid_utf8_at?(position)
166
+ end
167
+
168
+ def utf8_boundary?(position)
169
+ !utf8_input? || @buffer.utf8_boundary?(position)
170
+ end
171
+
172
+ def lineno
173
+ @line
174
+ end
175
+
176
+ alias line lineno
177
+
178
+ def state
179
+ @state
180
+ end
181
+
182
+ def beginning_of_line?
183
+ @bol
184
+ end
185
+
186
+ def binary_input
187
+ @buffer.source.b
188
+ end
189
+
190
+ def emit(type, value = text)
191
+ @pending = case self.class.__flexr_config.token_kind
192
+ when :struct
193
+ Runtime::Token.new(type: type, value: value, location: last_location)
194
+ else
195
+ [type, value]
196
+ end
197
+ end
198
+
199
+ def skip
200
+ @pending = nil
201
+ end
202
+
203
+ def error!(message)
204
+ error = LexError.new(message, filename: @filename, byte_pos: @match_start, line: @line, text: text)
205
+ if @on_error
206
+ action = @on_error.call(error)
207
+ return @pending = nil if action == :skip
208
+ raise error if action == :raise
209
+ if action == :halt
210
+ @halted = true
211
+ return @pending = nil
212
+ end
213
+ return emit(:error, text) if action == :token
214
+ end
215
+ case @error_mode
216
+ when :token
217
+ emit(:error, text)
218
+ when :panic
219
+ @pending = nil
220
+ else
221
+ raise error
222
+ end
223
+ end
224
+
225
+ def echo
226
+ emit(nil, text)
227
+ end
228
+
229
+ def reject
230
+ diagnostic = Diagnostics.error(
231
+ "FLEXR-E013", "reject is not supported by flexr",
232
+ help: "use a state transition and less(n) to express the fallback"
233
+ )
234
+ raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
235
+ end
236
+
237
+ def push(name)
238
+ ensure_state!(name)
239
+ if @state_stack.length >= @max_state_stack
240
+ raise Runtime::StateStackOverflowError.new(
241
+ "state stack exceeds max_state_stack (#{@max_state_stack})",
242
+ filename: @filename, byte_pos: @position, line: @line, text: text
243
+ )
244
+ end
245
+
246
+ @state_stack << @state
247
+ @state = name.to_sym
248
+ end
249
+
250
+ alias push_state push
251
+
252
+ def pop
253
+ @state = @state_stack.pop || :initial
254
+ end
255
+
256
+ alias pop_state pop
257
+
258
+ def begin_state(name)
259
+ ensure_state!(name)
260
+ @state = name.to_sym
261
+ end
262
+
263
+ alias state= begin_state
264
+
265
+ def less(count)
266
+ matched_bytes = @match_end - @match_start
267
+ raise ArgumentError, "less must not exceed matched bytes" if count.negative? || count > matched_bytes
268
+
269
+ @position = @match_start + count
270
+ @match_end = @position
271
+ @matched = nil
272
+ end
273
+
274
+ def more
275
+ @more_requested = true
276
+ end
277
+
278
+ def last_location
279
+ line_begin = @text_line || @line
280
+ Runtime::Location.new(
281
+ filename: @filename, byte_begin: @text_start, byte_end: @match_end,
282
+ line_begin: line_begin, line_end: line_begin + text.to_s.b.count("\n"),
283
+ column_values: [column_at(@text_start), column_at(@match_end)],
284
+ eager_columns: self.class.__flexr_config.options[:eager_columns] == true
285
+ )
286
+ end
287
+
288
+ private
289
+
290
+ def execute(rule)
291
+ return __flexr_generated_execute(rule) if generated_runtime? && respond_to?(:__flexr_generated_execute, true)
292
+
293
+ case rule.action
294
+ when :skip
295
+ nil
296
+ when Array
297
+ emit(rule.action[1], text)
298
+ else
299
+ instance_exec(&rule.action)
300
+ end
301
+ end
302
+
303
+ def finalize_more
304
+ if @more_requested
305
+ @more_start = @text_start
306
+ @more_line = @text_line
307
+ else
308
+ @more_start = nil
309
+ @more_line = nil
310
+ end
311
+ @more_requested = false
312
+ end
313
+
314
+ def generated_runtime?
315
+ self.class.respond_to?(:__flexr_generated?) && self.class.__flexr_generated?
316
+ end
317
+
318
+ def ensure_token_size!
319
+ actual_size = @match_end - @text_start
320
+ @candidate_token_size = 0
321
+ return if actual_size <= @max_token_size
322
+
323
+ raise Runtime::TokenTooLargeError
324
+ end
325
+
326
+ def force_empty_match_progress!
327
+ return if eof?
328
+
329
+ byte = @buffer.byteslice(@position, 1).to_s.b
330
+ @position += 1
331
+ @line += 1 if byte == "\n"
332
+ @bol = byte == "\n"
333
+ end
334
+
335
+ def update_position
336
+ if @match_end > @match_start
337
+ length = @match_end - @match_start
338
+ @line += if length == 1
339
+ @buffer.source.getbyte(@match_start) == 0x0a ? 1 : 0
340
+ elsif @line_tracking_needed && (newline = @buffer.source.index("\n", @match_start)) && newline < @match_end
341
+ @buffer.byteslice(@match_start, length).to_s.count("\n")
342
+ else
343
+ 0
344
+ end
345
+ end
346
+ @bol = @match_end.zero? || @buffer.source.getbyte(@match_end - 1) == 0x0a
347
+ end
348
+
349
+ def handle_unmatched_byte
350
+ bad = @buffer.byteslice(@position, 1)
351
+ @match_start = @position
352
+ @match_end = @position + 1
353
+ @text_start = @match_start
354
+ @text_line = @line
355
+ @position += 1
356
+ @line += 1 if bad.to_s.b == "\n"
357
+ @bol = bad.to_s.b == "\n"
358
+ error!("unexpected byte #{bad.inspect}")
359
+ token = @pending
360
+ @pending = nil
361
+ token
362
+ end
363
+
364
+ def eof?
365
+ return @position >= @buffer.bytesize if @input
366
+
367
+ @buffer.eof?(@position)
368
+ end
369
+
370
+ def ensure_state!(name)
371
+ return if self.class.__flexr_config.states.key?(name.to_sym)
372
+
373
+ diagnostic = Diagnostics.error("FLEXR-E003", "undefined state: #{name}")
374
+ raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
375
+ end
376
+
377
+ def column_at(position)
378
+ prefix = @buffer.byteslice(0...position).to_s
379
+ last_newline = prefix.rindex("\n")
380
+ line_prefix = prefix.byteslice((last_newline ? last_newline + 1 : 0)..).to_s
381
+ if utf8_input? && line_prefix.dup.force_encoding(Encoding::UTF_8).valid_encoding?
382
+ line_prefix.force_encoding(Encoding::UTF_8).length + 1
383
+ else
384
+ line_prefix.bytesize + 1
385
+ end
386
+ end
387
+ end
388
+ end