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
data/tools/coverage.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "coverage"
4
+ require "rspec/core"
5
+
6
+ Coverage.start(lines: true)
7
+
8
+ status = RSpec::Core::Runner.run(["spec", "--format", "progress", "--no-color"], File::NULL, $stdout)
9
+ result = Coverage.result
10
+ files = Dir[File.expand_path("../lib/flexr/**/*.rb", __dir__)]
11
+ totals = files.map do |file|
12
+ lines = result.fetch(file, {}).fetch(:lines, [])
13
+ executable = lines.count { |count| !count.nil? }
14
+ covered = lines.count { |count| count&.positive? }
15
+ uncovered = lines.each_index.select { |index| lines[index]&.zero? }
16
+ [file, covered, executable, uncovered]
17
+ end
18
+ covered = totals.sum { |_, count, _, _| count }
19
+ executable = totals.sum { |_, _, count, _| count }
20
+ percentage = executable.zero? ? 100.0 : covered.to_f / executable * 100
21
+
22
+ puts format("coverage: %<percentage>.2f%% (%<covered>d/%<executable>d lines)",
23
+ percentage: percentage, covered: covered, executable: executable)
24
+ puts "coverage target: 95.00% (gate)"
25
+ totals.reject { |_, count, total, _| total.zero? || count == total }.each do |file, count, total, uncovered|
26
+ puts format(" %<file>s: %<count>d/%<total>d", file: file.delete_prefix("#{Dir.pwd}/"), count: count,
27
+ total: total)
28
+ puts " uncovered lines: #{uncovered.map { |line| line + 1 }.join(', ')}"
29
+ end
30
+
31
+ exit status unless status.zero?
32
+ exit 1 if percentage < 95.0
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "rbconfig"
6
+ require "stringio"
7
+ require "tmpdir"
8
+ require_relative "../lib/flexr"
9
+
10
+ module FlexrDocumentation
11
+ ROOT = File.expand_path("..", __dir__)
12
+ MARKER_START = "<!-- flexr-help:start -->"
13
+ MARKER_END = "<!-- flexr-help:end -->"
14
+ MARKDOWN_FILES = [
15
+ File.join(ROOT, "README.md"),
16
+ File.join(ROOT, "CONTRIBUTING.md"),
17
+ *Dir[File.join(ROOT, "docs/**/*.md")],
18
+ *Dir[File.join(ROOT, "examples/**/README.md")]
19
+ ].freeze
20
+ STABLE_DSL_METHODS = Flexr::DSL::DSL_METHODS - %i[compile! dfa]
21
+
22
+ module_function
23
+
24
+ def verify
25
+ verify_links
26
+ verify_english
27
+ verify_tutorial
28
+ verify_cli_snapshot
29
+ verify_dsl_reference
30
+ puts "docs: links, language, tutorial, CLI snapshot, and DSL coverage passed"
31
+ end
32
+
33
+ def verify_links
34
+ MARKDOWN_FILES.each do |path|
35
+ File.read(path).scan(/\[[^\]]+\]\(([^)\s]+)(?:\s+"[^"]*")?\)/).flatten.each do |target|
36
+ next if target.match?(%r{\A(?:https?://|mailto:)}) || target.start_with?("#")
37
+
38
+ local_target = target.split("#", 2).first
39
+ resolved = File.expand_path(local_target, File.dirname(path))
40
+ next if File.file?(resolved) || File.directory?(resolved)
41
+
42
+ raise "broken documentation link: #{path}: #{target}"
43
+ end
44
+ end
45
+ end
46
+
47
+ def verify_english
48
+ MARKDOWN_FILES.each do |path|
49
+ raise "documentation contains non-English characters: #{path}" if
50
+ File.read(path).match?(/[ぁ-んァ-ヶ一-龯]/)
51
+ end
52
+ end
53
+
54
+ def verify_tutorial
55
+ spec = File.join(ROOT, "examples/calculator/lexer.flexr.rb")
56
+ input = "if ifx == = 12 + 3"
57
+ expected = [["IF", "if"], ["IDENT", "ifx"], ["EQ", "=="], ["ASSIGN", "="],
58
+ ["INTEGER", 12], ["PLUS", "+"], ["INTEGER", 3]]
59
+ runtime = ruby_json(<<~RUBY, spec, input)
60
+ require "json"
61
+ load ARGV.fetch(0)
62
+ puts JSON.generate(CalculatorExample::Lexer.new(ARGV.fetch(1)).tokens)
63
+ RUBY
64
+ raise "tutorial runtime output mismatch: #{runtime.inspect}" unless JSON.parse(runtime) == expected
65
+
66
+ Dir.mktmpdir("flexr-docs") do |directory|
67
+ output = File.join(directory, "calculator.rb")
68
+ stdout, stderr, status = command("examples/calculator/lexer.flexr.rb", "-o", output)
69
+ raise "tutorial generation failed: #{stderr}#{stdout}" unless status.success?
70
+
71
+ generated = ruby_json(<<~RUBY, output, input)
72
+ require "json"
73
+ load ARGV.fetch(0)
74
+ puts JSON.generate(CalculatorExample::Lexer.new(ARGV.fetch(1)).tokens)
75
+ RUBY
76
+ raise "tutorial runtime/generated mismatch" unless generated == runtime
77
+ end
78
+
79
+ stdout, stderr, status = command("check", spec, "--format", "json")
80
+ raise "tutorial check failed: #{stderr}#{stdout}" unless status.success? && JSON.parse(stdout) == []
81
+ end
82
+
83
+ def verify_cli_snapshot
84
+ output = StringIO.new
85
+ error = StringIO.new
86
+ status = Flexr::CLI.run(["--help"], out: output, err: error)
87
+ raise "CLI help failed: #{error.string}" unless status.zero?
88
+
89
+ page = File.read(File.join(ROOT, "docs/reference/cli.md"))
90
+ snapshot = page.split(MARKER_START, 2).last&.split(MARKER_END, 2)&.first
91
+ raise "CLI help markers are missing" unless snapshot
92
+ raise "CLI help snapshot is stale" unless output.string == "#{snapshot.strip}\n"
93
+ end
94
+
95
+ def verify_dsl_reference
96
+ page = File.read(File.join(ROOT, "docs/reference/dsl.md"))
97
+ STABLE_DSL_METHODS.each do |method_name|
98
+ next if page.include?("## `#{method_name}(")
99
+
100
+ raise "stable DSL method is missing from the reference: #{method_name}"
101
+ end
102
+ end
103
+
104
+ def command(*arguments)
105
+ Open3.capture3(RbConfig.ruby, "-Ilib", "exe/flexr", *arguments, chdir: ROOT)
106
+ end
107
+
108
+ def ruby_json(script, path, input)
109
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", script, path, input, chdir: ROOT)
110
+ raise "Ruby example failed: #{stderr}" unless status.success?
111
+
112
+ stdout.strip
113
+ end
114
+ end
115
+
116
+ FlexrDocumentation.verify
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Generate the vendored Unicode snapshot from the Unicode Character Database.
5
+ # This tool intentionally never asks the host Ruby Regexp implementation for
6
+ # a property set: changing the Ruby version must not change generated output.
7
+
8
+ require "fileutils"
9
+
10
+ input = ARGV.fetch(0) { abort "usage: gen_unicode_tables.rb UCD_DIRECTORY OUTPUT_DIRECTORY" }
11
+ output = ARGV.fetch(1) { abort "usage: gen_unicode_tables.rb UCD_DIRECTORY OUTPUT_DIRECTORY" }
12
+ FileUtils.mkdir_p(output)
13
+
14
+ def parse_ranges(path, property: nil)
15
+ ranges = []
16
+ File.foreach(path, encoding: "UTF-8") do |line|
17
+ body, _comment = line.split("#", 2)
18
+ fields = body.strip.split(";").map(&:strip)
19
+ next if fields.empty? || fields.first.empty?
20
+ next if property && fields[1] != property
21
+
22
+ first, last = fields.first.split("..", 2).map { |value| Integer(value, 16) }
23
+ last ||= first
24
+ ranges << [first, last]
25
+ end
26
+ merge_ranges(ranges)
27
+ end
28
+
29
+ def merge_ranges(ranges)
30
+ ranges.sort_by(&:first).each_with_object([]) do |range, merged|
31
+ if merged.empty? || range.first > merged.last.last + 1
32
+ merged << range.dup
33
+ else
34
+ merged.last[1] = [merged.last.last, range.last].max
35
+ end
36
+ end
37
+ end
38
+
39
+ def unicode_version(input)
40
+ readme = File.join(input, "ReadMe.txt")
41
+ text = File.file?(readme) ? File.read(readme) : ""
42
+ text[/Version[- ]([0-9]+\.[0-9]+\.[0-9]+)/, 1] ||
43
+ text[/Unicode\s+([0-9]+\.[0-9]+\.[0-9]+)/i, 1] || "unknown"
44
+ end
45
+
46
+ def category_ranges(path, categories)
47
+ unicode_data_ranges(path, categories: Array(categories))
48
+ end
49
+
50
+ def unicode_data_ranges(path, categories: nil)
51
+ selected_categories = categories && Array(categories)
52
+ ranges = []
53
+ first = last = nil
54
+ current_category = nil
55
+ File.foreach(path, encoding: "UTF-8") do |line|
56
+ body = line.split("#", 2).first
57
+ fields = body.strip.split(";")
58
+ next if fields.length < 3
59
+
60
+ codepoint = Integer(fields[0], 16)
61
+ name = fields[1]
62
+ category = fields[2]
63
+ if name.end_with?(", First>")
64
+ first = codepoint
65
+ current_category = category
66
+ elsif name.end_with?(", Last>") && first
67
+ last = codepoint
68
+ ranges << [first, last] if selected_categories.nil? || selected_categories.include?(current_category)
69
+ first = last = current_category = nil
70
+ elsif selected_categories.nil? || selected_categories.include?(category)
71
+ ranges << [codepoint, codepoint]
72
+ end
73
+ end
74
+ merge_ranges(ranges)
75
+ end
76
+
77
+ def property_file_ranges(path, names)
78
+ names = Array(names)
79
+ selected = []
80
+ File.foreach(path, encoding: "UTF-8") do |line|
81
+ body = line.split("#", 2).first
82
+ fields = body.strip.split(";").map(&:strip)
83
+ next if fields.length < 2 || !names.include?(fields[1])
84
+
85
+ first, last = fields.first.split("..", 2).map { |value| Integer(value, 16) }
86
+ last ||= first
87
+ selected << [first, last]
88
+ end
89
+ merge_ranges(selected)
90
+ end
91
+
92
+ def property_names(path)
93
+ names = {}
94
+ File.foreach(path, encoding: "UTF-8") do |line|
95
+ body = line.split("#", 2).first
96
+ fields = body.strip.split(";").map(&:strip)
97
+ names[fields[1]] = true if fields.length >= 2 && !fields[1].empty?
98
+ end
99
+ names.keys.sort
100
+ end
101
+
102
+ def complement_ranges(ranges, maximum: 0x10ffff)
103
+ result = []
104
+ cursor = 0
105
+ ranges.each do |first, last|
106
+ result << [cursor, first - 1] if cursor < first
107
+ cursor = [cursor, last + 1].max
108
+ end
109
+ result << [cursor, maximum] if cursor <= maximum
110
+ result
111
+ end
112
+
113
+ def case_fold_table(path)
114
+ table = {}
115
+ File.foreach(path, encoding: "UTF-8") do |line|
116
+ body = line.split("#", 2).first
117
+ fields = body.strip.split(";").map(&:strip)
118
+ next unless fields.length >= 3 && %w[C S].include?(fields[1])
119
+
120
+ mapping = fields[2].split.map { |value| Integer(value, 16) }
121
+ table[Integer(fields[0], 16)] = mapping.first if mapping.length == 1
122
+ end
123
+ table
124
+ end
125
+
126
+ unicode_data = File.join(input, "UnicodeData.txt")
127
+ scripts = File.join(input, "Scripts.txt")
128
+ prop_list = File.join(input, "PropList.txt")
129
+ case_folding = File.join(input, "CaseFolding.txt")
130
+ [unicode_data, scripts, prop_list, case_folding].each do |path|
131
+ abort "missing UCD file: #{path}" unless File.file?(path)
132
+ end
133
+
134
+ properties = {
135
+ "L" => category_ranges(unicode_data, %w[Lu Ll Lt Lm Lo]),
136
+ "Letter" => category_ranges(unicode_data, %w[Lu Ll Lt Lm Lo]),
137
+ "N" => category_ranges(unicode_data, %w[Nd Nl No]),
138
+ "Number" => category_ranges(unicode_data, %w[Nd Nl No]),
139
+ "Nd" => category_ranges(unicode_data, ["Nd"]),
140
+ "ASCII" => [[0, 0x7f]],
141
+ "Alphabetic" => merge_ranges(category_ranges(unicode_data, %w[Lu Ll Lt Lm Lo Nl]) +
142
+ property_file_ranges(prop_list, ["Other_Alphabetic"])),
143
+ "Other_Alphabetic" => property_file_ranges(prop_list, ["Other_Alphabetic"]),
144
+ "Alnum" => merge_ranges(category_ranges(unicode_data, %w[Lu Ll Lt Lm Lo Nd Nl No]) +
145
+ property_file_ranges(prop_list, ["Other_Alphabetic"])),
146
+ "Word" => merge_ranges(category_ranges(unicode_data, %w[Lu Ll Lt Lm Lo Nd Nl No]) +
147
+ property_file_ranges(prop_list, ["Other_Alphabetic"]) + [[0x5f, 0x5f]]),
148
+ "Space" => property_file_ranges(prop_list, ["White_Space"]),
149
+ "XDigit" => [[0x30, 0x39], [0x41, 0x46], [0x61, 0x66]],
150
+ "Cntrl" => [[0, 0x1f], [0x7f, 0x9f]],
151
+ "Lowercase" => merge_ranges(category_ranges(unicode_data, ["Ll"]) +
152
+ property_file_ranges(prop_list, ["Other_Lowercase"])),
153
+ "Uppercase" => merge_ranges(category_ranges(unicode_data, ["Lu"]) +
154
+ property_file_ranges(prop_list, ["Other_Uppercase"]))
155
+ }
156
+ %w[Cc Cf Cn Co Cs Ll Lm Lo Lt Lu Mc Me Mn Nd Nl No Pc Pd Pe Pf Pi Po Ps Sc Sk Sm So Zl Zp Zs].each do |category|
157
+ properties[category] = category_ranges(unicode_data, [category])
158
+ end
159
+ properties["Cn"] = complement_ranges(unicode_data_ranges(unicode_data))
160
+ {
161
+ "C" => %w[Cc Cf Cn Co Cs], "M" => %w[Mc Me Mn], "P" => %w[Pc Pd Pe Pf Pi Po Ps],
162
+ "S" => %w[Sc Sk Sm So], "Z" => %w[Zl Zp Zs], "LC" => %w[Lt Lu Ll]
163
+ }.each do |name, categories|
164
+ properties[name] = category_ranges(unicode_data, categories)
165
+ end
166
+ properties["C"] = merge_ranges(properties.fetch("C") + properties.fetch("Cn"))
167
+
168
+ property_names(scripts).each do |script|
169
+ properties[script] = property_file_ranges(scripts, [script])
170
+ end
171
+ properties["Any"] = [[0, 0x10ffff]]
172
+ properties["Assigned"] = complement_ranges(properties.fetch("Cn"))
173
+
174
+ version = unicode_version(input)
175
+ File.write(File.join(output, "UNICODE_VERSION"), "#{version}\n")
176
+ source = <<~RUBY
177
+ # frozen_string_literal: true
178
+
179
+ module Flexr
180
+ module Unicode
181
+ module Data
182
+ VERSION = #{version.inspect}
183
+ PROPERTIES = #{properties.inspect}.freeze
184
+ end
185
+ end
186
+ end
187
+ RUBY
188
+ File.write(File.join(output, "properties.rb"), source)
189
+
190
+ fold_source = <<~RUBY
191
+ # frozen_string_literal: true
192
+
193
+ module Flexr
194
+ module Unicode
195
+ module Data
196
+ CASE_FOLD = #{case_fold_table(case_folding).inspect}.freeze
197
+ end
198
+ end
199
+ end
200
+ RUBY
201
+ File.write(File.join(output, "case_folding.rb"), fold_source)
202
+ puts "Unicode #{version}: generated #{properties.length} property tables"
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlexrVerification
4
+ module RegexpTokenizerReference
5
+ module_function
6
+
7
+ class UnmatchedInputError < StandardError
8
+ attr_reader :offset, :byte
9
+
10
+ def initialize(offset:, byte:)
11
+ @offset = offset
12
+ @byte = byte
13
+ super(format("reference tokenizer could not match byte at offset %<offset>d (0x%<byte>02x)",
14
+ offset: offset, byte: byte))
15
+ end
16
+ end
17
+
18
+ RULES = [
19
+ [/[ \t\r\n]+/, ->(_text) {}],
20
+ [/\\p\{[A-Za-z_][A-Za-z0-9_]*\}/, ->(text) { [:PROPERTY, text.byteslice(3...-1)] }],
21
+ [/\\./, ->(text) { [:ESCAPE, text] }],
22
+ [/\[[^\]\n]*\]/, ->(text) { [:CHAR_CLASS, text] }],
23
+ [/[?*+]|\{[0-9]+(?:,[0-9]*)?\}/, ->(text) { [:QUANTIFIER, text] }],
24
+ [/\|/, ->(text) { [:ALTERNATION, text] }],
25
+ [/\(/, ->(text) { [:GROUP_OPEN, text] }],
26
+ [/\)/, ->(text) { [:GROUP_CLOSE, text] }],
27
+ [/\^|\$/, ->(text) { [:ANCHOR, text] }],
28
+ [/\./, ->(text) { [:DOT, text] }],
29
+ [/[^\\\[\]().|?*+{}^$ \t\r\n]/, ->(text) { [:LITERAL, text] }]
30
+ ].freeze
31
+ BYTE_RULES = RULES.map do |pattern, action|
32
+ [Regexp.new(pattern.source, pattern.options | ::Regexp::NOENCODING), action]
33
+ end.freeze
34
+
35
+ def tokens(input)
36
+ source = input.b
37
+ position = 0
38
+ result = []
39
+ while position < source.bytesize
40
+ match = match_at(source, position)
41
+ raise UnmatchedInputError.new(offset: position, byte: source.getbyte(position)) unless match
42
+
43
+ matched_text, action = match
44
+ text = input.byteslice(position, matched_text.bytesize)
45
+ position += matched_text.bytesize
46
+ token = action.call(text)
47
+ result << token if token
48
+ end
49
+ result
50
+ end
51
+
52
+ def match_at(source, position)
53
+ BYTE_RULES.each do |pattern, action|
54
+ candidate = pattern.match(source, position)
55
+ return [candidate[0], action] if candidate&.begin(0) == position
56
+ end
57
+ nil
58
+ end
59
+ end
60
+ end
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flexr
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A Ruby DSL and source generator for deterministic byte-oriented lexers.
13
+ email:
14
+ - t.yudai92@gmail.com
15
+ executables:
16
+ - flexr
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rubocop.yml"
21
+ - CONTRIBUTING.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - benchmark/baselines/json.json
26
+ - benchmark/baselines/json_handwritten.rb
27
+ - benchmark/baselines/json_rexical.rex
28
+ - benchmark/corpora/README.md
29
+ - benchmark/corpora/generate_json.rb
30
+ - benchmark/golden/calculator_lexer.sha256
31
+ - benchmark/golden/json_lexer.sha256
32
+ - benchmark/golden/regexp_tokenizer.sha256
33
+ - benchmark/golden/ruby_subset_lexer.sha256
34
+ - benchmark/golden/toy_lang_lexer.sha256
35
+ - benchmark/golden/with_lrama_lexer.sha256
36
+ - benchmark/golden/with_racc_lexer.sha256
37
+ - benchmark/run.rb
38
+ - docs/README.md
39
+ - docs/RELEASING.md
40
+ - docs/adr/0001-byte-level-dfa.md
41
+ - docs/adr/0003-leftmost-longest.md
42
+ - docs/adr/0006-accel-not-scanner.md
43
+ - docs/adr/0008-what-pure-ruby-means.md
44
+ - docs/adr/0016-spec-is-plain-ruby.md
45
+ - docs/adr/0017-static-analysis-by-default.md
46
+ - docs/adr/0018-prism-for-generator-only.md
47
+ - docs/adr/0019-measured-performance-floor.md
48
+ - docs/adr/0020-vendored-unicode-contract.md
49
+ - docs/explanation/backends.md
50
+ - docs/explanation/matching-semantics.md
51
+ - docs/explanation/runtime-vs-generated.md
52
+ - docs/explanation/security-model.md
53
+ - docs/explanation/unicode-and-encoding.md
54
+ - docs/how-to/deploy-a-standalone-lexer.md
55
+ - docs/how-to/generate-a-lexer.md
56
+ - docs/how-to/handle-errors.md
57
+ - docs/how-to/integrate-with-lrama.md
58
+ - docs/how-to/integrate-with-racc.md
59
+ - docs/how-to/migrate-from-flex.md
60
+ - docs/how-to/migrate-from-rexical.md
61
+ - docs/how-to/run-a-lexer-at-runtime.md
62
+ - docs/how-to/track-token-locations.md
63
+ - docs/how-to/tune-performance.md
64
+ - docs/how-to/use-states.md
65
+ - docs/how-to/use-trailing-context.md
66
+ - docs/internals/README.md
67
+ - docs/perf-log.md
68
+ - docs/reference/README.md
69
+ - docs/reference/actions.md
70
+ - docs/reference/cli.md
71
+ - docs/reference/compatibility.md
72
+ - docs/reference/diagnostics.md
73
+ - docs/reference/dsl.md
74
+ - docs/reference/errors.md
75
+ - docs/reference/generated-artifacts.md
76
+ - docs/reference/public-api.md
77
+ - docs/reference/regexp.md
78
+ - docs/reference/runtime.md
79
+ - docs/reference/tokens-and-locations.md
80
+ - docs/tutorial/build-a-calculator-lexer.md
81
+ - examples/calculator/README.md
82
+ - examples/calculator/lexer.flexr.rb
83
+ - examples/json/README.md
84
+ - examples/json/lexer.flexr.rb
85
+ - examples/ruby_subset/README.md
86
+ - examples/ruby_subset/lexer.flexr.rb
87
+ - examples/toy_lang/README.md
88
+ - examples/toy_lang/lexer.flexr.rb
89
+ - examples/with_lrama/README.md
90
+ - examples/with_lrama/lexer.flexr.rb
91
+ - examples/with_racc/README.md
92
+ - examples/with_racc/lexer.flexr.rb
93
+ - exe/flexr
94
+ - lib/flexr.rb
95
+ - lib/flexr/automaton/accel.rb
96
+ - lib/flexr/automaton/analysis.rb
97
+ - lib/flexr/automaton/byte_class_set.rb
98
+ - lib/flexr/automaton/compiler.rb
99
+ - lib/flexr/automaton/dfa.rb
100
+ - lib/flexr/automaton/minimizer.rb
101
+ - lib/flexr/automaton/nfa.rb
102
+ - lib/flexr/cli.rb
103
+ - lib/flexr/codegen/base.rb
104
+ - lib/flexr/codegen/direct.rb
105
+ - lib/flexr/codegen/firstmatch.rb
106
+ - lib/flexr/codegen/table.rb
107
+ - lib/flexr/codegen/table_packer.rb
108
+ - lib/flexr/diagnostics.rb
109
+ - lib/flexr/dsl.rb
110
+ - lib/flexr/errors.rb
111
+ - lib/flexr/generated.rb
112
+ - lib/flexr/generator.rb
113
+ - lib/flexr/importer.rb
114
+ - lib/flexr/ir.rb
115
+ - lib/flexr/lexer.rb
116
+ - lib/flexr/options.rb
117
+ - lib/flexr/rake_task.rb
118
+ - lib/flexr/regexp/ast.rb
119
+ - lib/flexr/regexp/char_class.rb
120
+ - lib/flexr/regexp/normalizer.rb
121
+ - lib/flexr/regexp/parser.rb
122
+ - lib/flexr/regexp/tokenizer.flexr.rb
123
+ - lib/flexr/regexp/tokenizer.rb
124
+ - lib/flexr/regexp/unsupported.rb
125
+ - lib/flexr/runtime/buffer.rb
126
+ - lib/flexr/runtime/core.rb
127
+ - lib/flexr/runtime/errors.rb
128
+ - lib/flexr/runtime/interpreter.rb
129
+ - lib/flexr/runtime/location.rb
130
+ - lib/flexr/runtime/token.rb
131
+ - lib/flexr/source/passthrough.rb
132
+ - lib/flexr/source/prism_reader.rb
133
+ - lib/flexr/source/static_eval.rb
134
+ - lib/flexr/unicode/case_fold.rb
135
+ - lib/flexr/unicode/data/LICENSE-UNICODE.txt
136
+ - lib/flexr/unicode/data/UNICODE_VERSION
137
+ - lib/flexr/unicode/data/case_folding.rb
138
+ - lib/flexr/unicode/data/properties.rb
139
+ - lib/flexr/unicode/property.rb
140
+ - lib/flexr/unicode/reference_regexp.rb
141
+ - lib/flexr/unicode/utf8_splitter.rb
142
+ - lib/flexr/version.rb
143
+ - site/README.md
144
+ - site/astro.config.mjs
145
+ - site/package.json
146
+ - site/pnpm-lock.yaml
147
+ - site/pnpm-workspace.yaml
148
+ - site/public/playground.js
149
+ - site/scripts/verify-site.mjs
150
+ - site/src/content.config.ts
151
+ - site/src/content/docs/benchmarks.md
152
+ - site/src/content/docs/concepts/matching-semantics.md
153
+ - site/src/content/docs/concepts/regexp-model.md
154
+ - site/src/content/docs/concepts/runtime-vs-generated.md
155
+ - site/src/content/docs/concepts/security-model.md
156
+ - site/src/content/docs/examples.md
157
+ - site/src/content/docs/learn/generation.md
158
+ - site/src/content/docs/learn/getting-started.md
159
+ - site/src/content/docs/learn/parser-integration.md
160
+ - site/src/content/docs/learn/runtime-mode.md
161
+ - site/src/content/docs/reference/action-context.md
162
+ - site/src/content/docs/reference/cli.md
163
+ - site/src/content/docs/reference/diagnostics.md
164
+ - site/src/content/docs/reference/dsl.md
165
+ - site/src/content/docs/reference/public-api.md
166
+ - site/src/content/docs/reference/regexp.md
167
+ - site/src/content/docs/reference/runtime.md
168
+ - site/src/content/docs/reference/tokens-and-locations.md
169
+ - site/src/env.d.ts
170
+ - site/src/layouts/SiteLayout.astro
171
+ - site/src/pages/index.astro
172
+ - site/src/pages/playground.astro
173
+ - site/src/styles/custom.css
174
+ - site/tsconfig.json
175
+ - tools/coverage.rb
176
+ - tools/docs_verify.rb
177
+ - tools/gen_unicode_tables.rb
178
+ - tools/regexp_tokenizer_reference.rb
179
+ homepage: https://github.com/ydah/flexr
180
+ licenses:
181
+ - MIT
182
+ metadata:
183
+ rubygems_mfa_required: 'true'
184
+ generator_ruby_version: ">= 3.3 (Prism)"
185
+ homepage_uri: https://github.com/ydah/flexr
186
+ source_code_uri: https://github.com/ydah/flexr/tree/main
187
+ bug_tracker_uri: https://github.com/ydah/flexr/issues
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: 3.1.0
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubygems_version: 4.0.6
203
+ specification_version: 4
204
+ summary: Ruby-native lexer generator with runtime and generated modes
205
+ test_files: []