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,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Codegen
5
+ class Table < Base
6
+ def generate(state: :initial)
7
+ machine = compiled.machines.fetch(state)
8
+ dfa = machine.dfa
9
+ {
10
+ ec: dfa.ec,
11
+ nxt: dfa.transitions.flatten.map { |value| value || -1 },
12
+ acc: dfa.accepts.map { |rules| rules.first&.rule_index || -1 },
13
+ start: dfa.start,
14
+ states: dfa.states,
15
+ classes: dfa.class_count
16
+ }.freeze
17
+ end
18
+
19
+ def header(source: nil, backend: :table)
20
+ stat = stats.fetch(:initial, {})
21
+ [
22
+ "# Generated by flexr. DO NOT EDIT.",
23
+ "# source: #{source}",
24
+ "# backend: #{backend} / states: #{stat[:states]} / byte-classes: #{stat[:classes]}"
25
+ ].join("\n")
26
+ end
27
+
28
+ def source(indent: " ")
29
+ lines = []
30
+ lines << "#{indent}def scan_one"
31
+ lines << "#{indent} return Flexr::Runtime::Interpreter.new(self).scan unless __flexr_generated_fast_path?"
32
+ lines << "#{indent} machine = self.class.__flexr_compiled.machines.fetch(state)"
33
+ lines << "#{indent} dfa = machine.dfa"
34
+ lines << "#{indent} position = byte_pos"
35
+ lines << "#{indent} return nil unless valid_utf8_at?(position)"
36
+ lines << "#{indent} cursor = position"
37
+ lines << "#{indent} current = dfa.start"
38
+ lines << "#{indent} direct = dfa.direct"
39
+ lines << "#{indent} source = buffer.source"
40
+ simple = if simple_fast_path?
41
+ lines << "#{indent} best = nil"
42
+ true
43
+ else
44
+ lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, position, nil)"
45
+ false
46
+ end
47
+ lines << "#{indent} accelerate = self.class.__flexr_config.options.fetch(:accel, :auto) != :none && !utf8_input?"
48
+ lines << "#{indent} while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)"
49
+ lines << "#{indent} if accelerate"
50
+ lines << "#{indent} region = __flexr_generated_acceleration_region(dfa, current)"
51
+ lines << "#{indent} accelerated_end = __flexr_generated_accelerate(region, cursor) if region"
52
+ lines << "#{indent} if accelerated_end && accelerated_end > cursor"
53
+ lines << "#{indent} cursor = accelerated_end"
54
+ lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
55
+ lines << "#{indent} next"
56
+ lines << "#{indent} end"
57
+ lines << "#{indent} end"
58
+ lines << "#{indent} byte = source.getbyte(cursor)"
59
+ lines << "#{indent} current = if direct"
60
+ lines << "#{indent} class_id = dfa.ec[byte]"
61
+ lines << "#{indent} value = direct[:nxt][(current * direct[:classes]) + class_id]"
62
+ lines << "#{indent} value >= 0 ? value : nil"
63
+ lines << "#{indent} else"
64
+ lines << "#{indent} dfa.transition(current, byte)"
65
+ lines << "#{indent} end"
66
+ lines << "#{indent} break unless current"
67
+ lines << "#{indent} cursor += 1"
68
+ if simple
69
+ lines << "#{indent} acceptance = dfa.accepts[current].first"
70
+ lines << "#{indent} if acceptance"
71
+ lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
72
+ lines << "#{indent} best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)"
73
+ lines << "#{indent} best.rule = rule"
74
+ lines << "#{indent} best.start_pos = position"
75
+ lines << "#{indent} best.end_pos = cursor"
76
+ lines << "#{indent} best.total_end_pos = cursor"
77
+ lines << "#{indent} end"
78
+ else
79
+ lines << "#{indent} best = __flexr_generated_acceptance(dfa, current, position, cursor, best)"
80
+ end
81
+ lines << "#{indent} end"
82
+ lines << "#{indent} best"
83
+ lines << "#{indent}end"
84
+ lines << "#{indent}def __flexr_generated_acceleration_region(dfa, state)"
85
+ lines << "#{indent} @__flexr_generated_accel_regions ||= {}"
86
+ lines << "#{indent} regions = (@__flexr_generated_accel_regions[dfa] ||= Flexr::Automaton::Accel.extract(dfa).to_h { |region| [region.state, region] })"
87
+ lines << "#{indent} region = regions[state]"
88
+ lines << "#{indent} return unless region"
89
+ lines << "#{indent} return if dfa.accepts[state].any? do |acceptance|"
90
+ lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
91
+ lines << "#{indent} acceptance.bol_only || acceptance.end_anchor || rule.trailing"
92
+ lines << "#{indent} end"
93
+ lines << "#{indent} region"
94
+ lines << "#{indent}end"
95
+ lines << "#{indent}def __flexr_generated_accelerate(region, position)"
96
+ lines << "#{indent} binary = buffer.source.b"
97
+ lines << "#{indent} mode = self.class.__flexr_config.options.fetch(:accel, :auto)"
98
+ lines << "#{indent} match_end = if %i[strscan auto].include?(mode) && defined?(StringScanner)"
99
+ lines << "#{indent} scanner = StringScanner.new(binary)"
100
+ lines << "#{indent} scanner.pos = position"
101
+ lines << "#{indent} length = scanner.skip(region.regexp)"
102
+ lines << "#{indent} length && scanner.pos"
103
+ lines << "#{indent} else"
104
+ lines << "#{indent} match = region.regexp.match(binary, position)"
105
+ lines << "#{indent} match && match.begin(0) == position ? match.end(0) : nil"
106
+ lines << "#{indent} end"
107
+ lines << "#{indent} return match_end if match_end && match_end < buffer.bytesize"
108
+ lines << "#{indent} return match_end if match_end && buffer.eof_loaded?"
109
+ lines << "#{indent} return unless buffer.ensure_available?(buffer.bytesize + 1)"
110
+ lines << "#{indent} __flexr_generated_accelerate(region, position)"
111
+ lines << "#{indent}rescue ArgumentError"
112
+ lines << "#{indent} nil"
113
+ lines << "#{indent}end"
114
+ lines << "#{indent}def __flexr_generated_fast_path?"
115
+ lines << "#{indent} return @__flexr_generated_fast_path if defined?(@__flexr_generated_fast_path)"
116
+ lines << "#{indent} @__flexr_generated_fast_path = if self.class.__flexr_config.backend == :firstmatch ||"
117
+ lines << "#{indent} self.class.__flexr_config.options[:allow_empty_match] == true"
118
+ lines << "#{indent} false"
119
+ lines << "#{indent} else"
120
+ lines << "#{indent} self.class.__flexr_rules.none? do |rule|"
121
+ lines << "#{indent} !rule.trailing.nil? || rule.patterns.any? do |pattern|"
122
+ lines << "#{indent} next false unless pattern.is_a?(Regexp)"
123
+ lines << "#{indent} next true if pattern.source.match?(/\\\\[pP]\\\\{/) || pattern.source.match?(/\\[:(?:\\^)?[a-z]+:\\]/)"
124
+ lines << "#{indent} self.class.__flexr_config.options[:unicode] == true && utf8_input? && pattern.source.match?(/\\\\[dDwWsS]/)"
125
+ lines << "#{indent} end"
126
+ lines << "#{indent} end"
127
+ lines << "#{indent} end"
128
+ lines << "#{indent}end"
129
+ lines << "#{indent}def __flexr_generated_acceptance(dfa, state, start_position, cursor, best)"
130
+ lines << "#{indent} dfa.accepts[state].each do |acceptance|"
131
+ lines << "#{indent} next if acceptance.bol_only && !beginning_of_line?"
132
+ lines << "#{indent} next if acceptance.end_anchor && !(buffer.eof?(cursor) || buffer.getbyte(cursor) == 0x0a)"
133
+ lines << "#{indent} rule = self.class.__flexr_rules.fetch(acceptance.rule_index)"
134
+ lines << "#{indent} defer_token_size_check!(cursor - start_position)"
135
+ lines << "#{indent} next if best && cursor < best.total_end_pos"
136
+ lines << "#{indent} next if best && cursor == best.total_end_pos &&"
137
+ lines << "#{indent} rule.index > best.rule.index"
138
+ lines << "#{indent} best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)"
139
+ lines << "#{indent} best.rule = rule"
140
+ lines << "#{indent} best.start_pos = start_position"
141
+ lines << "#{indent} best.end_pos = cursor"
142
+ lines << "#{indent} best.total_end_pos = cursor"
143
+ lines << "#{indent} end"
144
+ lines << "#{indent} best"
145
+ lines << "#{indent}end"
146
+ "#{lines.join("\n")}\n"
147
+ end
148
+
149
+ def simple_fast_path?
150
+ compiled.rules.all? do |rule|
151
+ rule.trailing.nil? && rule.pattern_conditions.all? do |condition|
152
+ condition && !condition.bol_only && !condition.end_anchor
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Codegen
5
+ module TablePacker
6
+ module_function
7
+
8
+ def pack(rows, compression: :rows)
9
+ full = compression.to_sym == :full
10
+ base = []
11
+ default = rows.map { |row| row.tally.max_by { |_value, count| count }&.first }
12
+ fallback = full ? Array.new(rows.length) : nil
13
+ next_table = []
14
+ check = []
15
+ occupied = {}
16
+ rows.each_with_index do |row, state|
17
+ if full
18
+ candidate, matches = rows.each_index.take(state).map do |other_state|
19
+ [other_state, row.each_index.count { |class_id| row[class_id] == rows[other_state][class_id] }]
20
+ end.max_by(&:last)
21
+ fallback[state] = candidate if candidate && matches > row.count { |value| value == default[state] }
22
+ end
23
+ inherited = fallback && fallback[state] ? rows.fetch(fallback.fetch(state)) : nil
24
+ entries = row.each_index.reject do |class_id|
25
+ row[class_id] == (inherited ? inherited[class_id] : default[state])
26
+ end
27
+ offset = 0
28
+ while entries.any? { |class_id| occupied.key?(offset + class_id) }
29
+ offset += 1
30
+ end
31
+ base[state] = offset
32
+ entries.each do |class_id|
33
+ index = offset + class_id
34
+ next_table[index] = row[class_id]
35
+ check[index] = state
36
+ occupied[index] = true
37
+ end
38
+ end
39
+ result = { base: base.freeze, default: default.freeze, next: next_table.freeze, check: check.freeze }
40
+ result[:fallback] = fallback.freeze if fallback
41
+ result.freeze
42
+ end
43
+
44
+ def encode(packed)
45
+ result = {
46
+ encoding: :base64,
47
+ base: encode_array(packed.fetch(:base)),
48
+ default: encode_array(packed.fetch(:default), nil_value: -1),
49
+ next: encode_array(packed.fetch(:next), nil_value: -1),
50
+ check: encode_array(packed.fetch(:check), nil_value: -1)
51
+ }
52
+ result[:fallback] = encode_array(packed.fetch(:fallback), nil_value: -1) if packed[:fallback]
53
+ result.freeze
54
+ end
55
+
56
+ def encode_array(values, nil_value: 0)
57
+ [values.map { |value| value.nil? ? nil_value : value }.pack("l<*")].pack("m0")
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ Diagnostic = Struct.new(:code, :severity, :message, :location, :help, :note, keyword_init: true) do
5
+ def error?
6
+ severity == :error
7
+ end
8
+
9
+ def warning?
10
+ severity == :warning
11
+ end
12
+
13
+ def to_h
14
+ {
15
+ code: code,
16
+ severity: severity,
17
+ message: message,
18
+ location: location,
19
+ help: help,
20
+ note: note
21
+ }.compact
22
+ end
23
+ end
24
+
25
+ class DiagnosticSet
26
+ include Enumerable
27
+
28
+ def initialize
29
+ @items = []
30
+ end
31
+
32
+ def each(&)
33
+ @items.each(&)
34
+ end
35
+
36
+ def <<(diagnostic)
37
+ @items << diagnostic
38
+ self
39
+ end
40
+
41
+ def any_error?
42
+ @items.any?(&:error?)
43
+ end
44
+
45
+ def empty?
46
+ @items.empty?
47
+ end
48
+
49
+ def to_a
50
+ @items.dup
51
+ end
52
+
53
+ def render(format: :human, color: :auto)
54
+ return JSON.generate(@items.map(&:to_h)) if format.to_sym == :json
55
+
56
+ @items.map { |item| render_one(item, color: color_enabled?(color)) }.join("\n")
57
+ end
58
+
59
+ private
60
+
61
+ def render_one(item, color:)
62
+ prefix = "#{item.severity}[#{item.code}]: #{item.message}"
63
+ prefix = "\e[31m#{prefix}\e[0m" if [true, :always].include?(color)
64
+ lines = [prefix]
65
+ lines << " help: #{item.help}" if item.help
66
+ lines << " note: #{item.note}" if item.note
67
+ lines.join("\n")
68
+ end
69
+
70
+ def color_enabled?(color)
71
+ return false if ENV.key?("NO_COLOR") || color == :never || color == false
72
+ return true if [:always, true].include?(color)
73
+
74
+ $stderr.tty?
75
+ end
76
+ end
77
+
78
+ module Diagnostics
79
+ module_function
80
+
81
+ def error(code, message, location: nil, help: nil, note: nil)
82
+ Diagnostic.new(code: code, severity: :error, message: message, location: location, help: help, note: note)
83
+ end
84
+
85
+ def warning(code, message, location: nil, help: nil, note: nil)
86
+ Diagnostic.new(code: code, severity: :warning, message: message, location: location, help: help, note: note)
87
+ end
88
+
89
+ def raise!(diagnostic)
90
+ klass = diagnostic.code == "FLEXR-E014" ? UnsupportedRegexpError : CompileError
91
+ raise klass.new(diagnostic.message, diagnostic: diagnostic)
92
+ end
93
+ end
94
+ end
data/lib/flexr/dsl.rb ADDED
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module DSL
5
+ DSL_METHODS = %i[rule state all_states on_eof emits backend token_kind encoding option accel].freeze
6
+ AUTO_DIRECT_CELL_THRESHOLD = 100_000
7
+
8
+ def inherited(child)
9
+ super
10
+ child.__flexr_reset!
11
+ end
12
+
13
+ def __flexr_reset!
14
+ @__flexr_rules = []
15
+ @__flexr_states = { initial: IR::State.new(name: :initial, inclusive: true, id: 0) }
16
+ @__flexr_state_stack = []
17
+ @__flexr_eof_rules = {}
18
+ @__flexr_config = IR::Config.new(
19
+ backend: :table, token_kind: :array, encoding: Encoding::UTF_8,
20
+ options: {}, declared_tokens: [], states: @__flexr_states
21
+ )
22
+ @__flexr_compiled = nil
23
+ @__flexr_generated = false
24
+ @__flexr_compile_mutex = Mutex.new
25
+ end
26
+
27
+ def rule(pattern, skip: false, emit: nil, followed_by: nil, &action)
28
+ patterns = normalize_patterns(pattern)
29
+ rule_action = if skip
30
+ :skip
31
+ elsif emit
32
+ [:emit, emit.to_sym]
33
+ else
34
+ action || proc { emit(nil, text) }
35
+ end
36
+ states = @__flexr_state_stack.empty? ? [:initial] : @__flexr_state_stack.dup
37
+ @__flexr_rules << IR::Rule.new(
38
+ index: @__flexr_rules.length, patterns: patterns, trailing: normalize_trailing(followed_by),
39
+ action: rule_action, states: states, bol_only: false, end_anchor: nil
40
+ )
41
+ nil
42
+ end
43
+
44
+ def state(*names, inclusive: false, &block)
45
+ raise ArgumentError, "state requires a block" unless block
46
+ raise ArgumentError, "state requires a name" if names.empty?
47
+
48
+ names.each do |name|
49
+ symbol = name.to_sym
50
+ @__flexr_states[symbol] ||= IR::State.new(name: symbol, inclusive: inclusive, id: @__flexr_states.length)
51
+ end
52
+ @__flexr_state_stack.concat(names.map(&:to_sym))
53
+ class_eval(&block)
54
+ ensure
55
+ names&.length&.times { @__flexr_state_stack.pop }
56
+ end
57
+
58
+ def all_states(&)
59
+ state(*@__flexr_states.keys, &)
60
+ end
61
+
62
+ def on_eof(&action)
63
+ state_name = @__flexr_state_stack.last || :initial
64
+ @__flexr_eof_rules[state_name] = action
65
+ end
66
+
67
+ def emits(*tokens)
68
+ @__flexr_config.declared_tokens.concat(tokens.flatten.map(&:to_sym)).uniq!
69
+ end
70
+
71
+ def backend(name)
72
+ @__flexr_config.backend = name.to_sym
73
+ end
74
+
75
+ def token_kind(name)
76
+ value = name.to_sym
77
+ raise ArgumentError, "unsupported token_kind: #{name}" unless %i[array struct yield].include?(value)
78
+
79
+ @__flexr_config.token_kind = value
80
+ end
81
+
82
+ def encoding(value)
83
+ encoding = value.is_a?(Encoding) ? value : Encoding.find(value.to_s)
84
+ unless [Encoding::UTF_8, Encoding::BINARY].include?(encoding)
85
+ diagnostic = Diagnostics.error("FLEXR-E011", "flexr supports UTF-8 and BINARY only")
86
+ raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
87
+ end
88
+
89
+ @__flexr_config.encoding = encoding
90
+ end
91
+
92
+ def option(*values)
93
+ values.each { |value| @__flexr_config.options[value.to_sym] = true }
94
+ end
95
+
96
+ def accel(value)
97
+ @__flexr_config.options[:accel] = value.to_sym
98
+ end
99
+
100
+ def compile!
101
+ @__flexr_compile_mutex.synchronize do
102
+ # The ivar is part of the generated/runtime class contract.
103
+ # rubocop:disable Naming/MemoizedInstanceVariableName
104
+ @__flexr_compiled ||= begin
105
+ compiled = Automaton::Compiler.new(__flexr_spec).compile
106
+ @__flexr_config.backend = auto_direct?(compiled) ? :direct : :table if @__flexr_config.backend == :auto
107
+ compiled
108
+ end
109
+ # rubocop:enable Naming/MemoizedInstanceVariableName
110
+ end
111
+ end
112
+
113
+ def dfa
114
+ compile!
115
+ __flexr_compiled
116
+ end
117
+
118
+ def __flexr_spec
119
+ IR::Spec.new(
120
+ class_name: name,
121
+ superclass: superclass&.name,
122
+ backend: @__flexr_config.backend,
123
+ token_kind: @__flexr_config.token_kind,
124
+ encoding: @__flexr_config.encoding,
125
+ options: @__flexr_config.options,
126
+ declared_tokens: @__flexr_config.declared_tokens,
127
+ states: @__flexr_states,
128
+ rules: @__flexr_rules,
129
+ eof_rules: @__flexr_eof_rules,
130
+ verbatim: nil
131
+ )
132
+ end
133
+
134
+ attr_reader :__flexr_rules, :__flexr_states, :__flexr_config, :__flexr_compiled
135
+
136
+ def __flexr_add_generated_eof(state, action)
137
+ @__flexr_eof_rules[state.to_sym] = action
138
+ end
139
+
140
+ def __flexr_set_compiled!(compiled)
141
+ @__flexr_compiled = compiled
142
+ end
143
+
144
+ def __flexr_mark_generated!
145
+ @__flexr_generated = true
146
+ end
147
+
148
+ def __flexr_generated?
149
+ @__flexr_generated == true
150
+ end
151
+
152
+ private
153
+
154
+ def normalize_patterns(pattern)
155
+ values = pattern.is_a?(Array) ? pattern : [pattern]
156
+ values.each do |value|
157
+ unless value.is_a?(::Regexp) || value.is_a?(String)
158
+ diagnostic = Diagnostics.error("FLEXR-E018", "rule pattern must be a Regexp, String, or Array")
159
+ raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
160
+ end
161
+ end
162
+ values
163
+ end
164
+
165
+ def normalize_trailing(value)
166
+ case value
167
+ when nil, ::Regexp
168
+ value
169
+ when String
170
+ ::Regexp.new(::Regexp.escape(value))
171
+ else
172
+ diagnostic = Diagnostics.error("FLEXR-E018", "followed_by must be a Regexp or String")
173
+ raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
174
+ end
175
+ end
176
+
177
+ def auto_direct?(compiled)
178
+ cells = compiled.stats.values.map { |stats| stats[:states] * stats[:classes] }.max.to_i
179
+ cells > AUTO_DIRECT_CELL_THRESHOLD
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ class Error < StandardError
5
+ attr_reader :diagnostic
6
+
7
+ def initialize(message = nil, diagnostic: nil)
8
+ @diagnostic = diagnostic
9
+ super(message || diagnostic&.message)
10
+ end
11
+ end
12
+
13
+ class LexError < Error
14
+ attr_reader :filename, :byte_pos, :line, :text
15
+
16
+ def initialize(message, filename: nil, byte_pos: nil, line: nil, text: nil, diagnostic: nil)
17
+ @filename = filename
18
+ @byte_pos = byte_pos
19
+ @line = line
20
+ @text = text
21
+ super(message, diagnostic: diagnostic)
22
+ end
23
+ end
24
+
25
+ class CompileError < Error; end
26
+ class UnsupportedRegexpError < CompileError; end
27
+ class StaticResolutionError < CompileError; end
28
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexr
4
+ module Generated
5
+ module_function
6
+
7
+ def install!(klass, payload)
8
+ rules = payload.fetch(:rules)
9
+ config = payload
10
+ klass.__flexr_reset!
11
+ klass.backend(config.fetch(:backend, :table))
12
+ klass.token_kind(config.fetch(:token_kind, :array))
13
+ klass.encoding(config.fetch(:encoding, Encoding::UTF_8))
14
+ Array(config[:declared_tokens]).each { |token| klass.emits(token) }
15
+ config.fetch(:options, {}).each do |option, value|
16
+ if option == :accel
17
+ klass.accel(value)
18
+ elsif value == true
19
+ klass.option(option)
20
+ else
21
+ klass.__flexr_config.options[option] = value
22
+ end
23
+ end
24
+ Array(config[:states]).each do |state|
25
+ klass.state(state, inclusive: config.fetch(:inclusive_states, {}).fetch(state.to_sym, false)) { nil }
26
+ end
27
+ rules.each do |definition|
28
+ klass.__flexr_add_generated_rule(definition)
29
+ end
30
+ config.fetch(:eof_rules, {}).each do |state, action|
31
+ klass.__flexr_add_generated_eof(state, action)
32
+ end
33
+ klass
34
+ end
35
+
36
+ def install_compiled!(klass, payload)
37
+ install!(klass, payload)
38
+ machines = payload.fetch(:compiled).fetch(:machines).transform_values do |machine|
39
+ dfa_data = machine.fetch(:dfa)
40
+ packed = decode_packed(dfa_data[:packed])
41
+ dfa = Automaton::DFA.new(
42
+ transitions: dfa_data[:transitions] || inflate_packed(packed, dfa_data.fetch(:state_count),
43
+ dfa_data.fetch(:class_count)),
44
+ accepts: dfa_data.fetch(:accepts),
45
+ ec: dfa_data.fetch(:ec),
46
+ class_count: dfa_data.fetch(:class_count),
47
+ start: dfa_data.fetch(:start),
48
+ rule_ids: dfa_data.fetch(:rule_ids),
49
+ packed: packed,
50
+ direct: dfa_data[:direct]
51
+ )
52
+ Automaton::Machine.new(dfa: dfa, state_name: machine.fetch(:state_name).to_sym)
53
+ end
54
+ compiled = Automaton::CompiledSpec.new(
55
+ machines: machines,
56
+ rules: klass.__flexr_rules,
57
+ states: payload.fetch(:compiled).fetch(:states).map(&:to_sym),
58
+ stats: payload.fetch(:compiled).fetch(:stats),
59
+ diagnostics: payload.fetch(:compiled).fetch(:diagnostics, []).map do |diagnostic|
60
+ Diagnostic.new(**diagnostic.transform_keys(&:to_sym))
61
+ end
62
+ )
63
+ klass.__flexr_set_compiled!(compiled)
64
+ klass.__flexr_mark_generated!
65
+ klass
66
+ end
67
+
68
+ def decode_packed(packed)
69
+ return packed unless packed.is_a?(Hash) && packed[:encoding]&.to_sym == :base64
70
+
71
+ {
72
+ base: decode_array(packed.fetch(:base)),
73
+ default: decode_array(packed.fetch(:default), nil_value: -1),
74
+ next: decode_array(packed.fetch(:next), nil_value: -1),
75
+ check: decode_array(packed.fetch(:check), nil_value: -1),
76
+ fallback: packed.key?(:fallback) ? decode_array(packed.fetch(:fallback), nil_value: -1) : nil
77
+ }
78
+ end
79
+
80
+ def decode_array(encoded, nil_value: nil)
81
+ values = encoded.unpack1("m0").unpack("l<*")
82
+ return values unless nil_value
83
+
84
+ values.map { |value| value == nil_value ? nil : value }
85
+ end
86
+
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
+ end
106
+
107
+ module DSL
108
+ 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
+ )
123
+ end
124
+ end
125
+ end