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.
- checksums.yaml +7 -0
- data/.rubocop.yml +33 -0
- data/CONTRIBUTING.md +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +468 -0
- data/benchmark/baselines/json.json +34 -0
- data/benchmark/baselines/json_handwritten.rb +43 -0
- data/benchmark/baselines/json_rexical.rex +25 -0
- data/benchmark/corpora/README.md +11 -0
- data/benchmark/corpora/generate_json.rb +26 -0
- data/benchmark/golden/calculator_lexer.sha256 +1 -0
- data/benchmark/golden/json_lexer.sha256 +1 -0
- data/benchmark/golden/regexp_tokenizer.sha256 +1 -0
- data/benchmark/golden/ruby_subset_lexer.sha256 +1 -0
- data/benchmark/golden/toy_lang_lexer.sha256 +1 -0
- data/benchmark/golden/with_lrama_lexer.sha256 +1 -0
- data/benchmark/golden/with_racc_lexer.sha256 +1 -0
- data/benchmark/run.rb +254 -0
- data/docs/README.md +64 -0
- data/docs/RELEASING.md +30 -0
- data/docs/adr/0001-byte-level-dfa.md +5 -0
- data/docs/adr/0003-leftmost-longest.md +4 -0
- data/docs/adr/0006-accel-not-scanner.md +4 -0
- data/docs/adr/0008-what-pure-ruby-means.md +5 -0
- data/docs/adr/0016-spec-is-plain-ruby.md +4 -0
- data/docs/adr/0017-static-analysis-by-default.md +5 -0
- data/docs/adr/0018-prism-for-generator-only.md +4 -0
- data/docs/adr/0019-measured-performance-floor.md +26 -0
- data/docs/adr/0020-vendored-unicode-contract.md +21 -0
- data/docs/explanation/backends.md +33 -0
- data/docs/explanation/matching-semantics.md +20 -0
- data/docs/explanation/runtime-vs-generated.md +22 -0
- data/docs/explanation/security-model.md +18 -0
- data/docs/explanation/unicode-and-encoding.md +20 -0
- data/docs/how-to/deploy-a-standalone-lexer.md +23 -0
- data/docs/how-to/generate-a-lexer.md +39 -0
- data/docs/how-to/handle-errors.md +32 -0
- data/docs/how-to/integrate-with-lrama.md +21 -0
- data/docs/how-to/integrate-with-racc.md +25 -0
- data/docs/how-to/migrate-from-flex.md +21 -0
- data/docs/how-to/migrate-from-rexical.md +23 -0
- data/docs/how-to/run-a-lexer-at-runtime.md +29 -0
- data/docs/how-to/track-token-locations.md +27 -0
- data/docs/how-to/tune-performance.md +23 -0
- data/docs/how-to/use-states.md +36 -0
- data/docs/how-to/use-trailing-context.md +22 -0
- data/docs/internals/README.md +14 -0
- data/docs/perf-log.md +56 -0
- data/docs/reference/README.md +23 -0
- data/docs/reference/actions.md +47 -0
- data/docs/reference/cli.md +80 -0
- data/docs/reference/compatibility.md +38 -0
- data/docs/reference/diagnostics.md +41 -0
- data/docs/reference/dsl.md +81 -0
- data/docs/reference/errors.md +27 -0
- data/docs/reference/generated-artifacts.md +50 -0
- data/docs/reference/public-api.md +42 -0
- data/docs/reference/regexp.md +39 -0
- data/docs/reference/runtime.md +49 -0
- data/docs/reference/tokens-and-locations.md +33 -0
- data/docs/tutorial/build-a-calculator-lexer.md +96 -0
- data/examples/calculator/README.md +27 -0
- data/examples/calculator/lexer.flexr.rb +17 -0
- data/examples/json/README.md +30 -0
- data/examples/json/lexer.flexr.rb +24 -0
- data/examples/ruby_subset/README.md +17 -0
- data/examples/ruby_subset/lexer.flexr.rb +22 -0
- data/examples/toy_lang/README.md +17 -0
- data/examples/toy_lang/lexer.flexr.rb +18 -0
- data/examples/with_lrama/README.md +17 -0
- data/examples/with_lrama/lexer.flexr.rb +13 -0
- data/examples/with_racc/README.md +17 -0
- data/examples/with_racc/lexer.flexr.rb +13 -0
- data/exe/flexr +7 -0
- data/lib/flexr/automaton/accel.rb +39 -0
- data/lib/flexr/automaton/analysis.rb +38 -0
- data/lib/flexr/automaton/byte_class_set.rb +29 -0
- data/lib/flexr/automaton/compiler.rb +413 -0
- data/lib/flexr/automaton/dfa.rb +103 -0
- data/lib/flexr/automaton/minimizer.rb +70 -0
- data/lib/flexr/automaton/nfa.rb +92 -0
- data/lib/flexr/cli.rb +342 -0
- data/lib/flexr/codegen/base.rb +17 -0
- data/lib/flexr/codegen/direct.rb +52 -0
- data/lib/flexr/codegen/firstmatch.rb +17 -0
- data/lib/flexr/codegen/table.rb +158 -0
- data/lib/flexr/codegen/table_packer.rb +61 -0
- data/lib/flexr/diagnostics.rb +94 -0
- data/lib/flexr/dsl.rb +182 -0
- data/lib/flexr/errors.rb +28 -0
- data/lib/flexr/generated.rb +125 -0
- data/lib/flexr/generator.rb +400 -0
- data/lib/flexr/importer.rb +560 -0
- data/lib/flexr/ir.rb +36 -0
- data/lib/flexr/lexer.rb +10 -0
- data/lib/flexr/options.rb +47 -0
- data/lib/flexr/rake_task.rb +27 -0
- data/lib/flexr/regexp/ast.rb +45 -0
- data/lib/flexr/regexp/char_class.rb +7 -0
- data/lib/flexr/regexp/normalizer.rb +117 -0
- data/lib/flexr/regexp/parser.rb +517 -0
- data/lib/flexr/regexp/tokenizer.flexr.rb +27 -0
- data/lib/flexr/regexp/tokenizer.rb +168 -0
- data/lib/flexr/regexp/unsupported.rb +7 -0
- data/lib/flexr/runtime/buffer.rb +112 -0
- data/lib/flexr/runtime/core.rb +388 -0
- data/lib/flexr/runtime/errors.rb +22 -0
- data/lib/flexr/runtime/interpreter.rb +505 -0
- data/lib/flexr/runtime/location.rb +26 -0
- data/lib/flexr/runtime/token.rb +7 -0
- data/lib/flexr/source/passthrough.rb +31 -0
- data/lib/flexr/source/prism_reader.rb +283 -0
- data/lib/flexr/source/static_eval.rb +145 -0
- data/lib/flexr/unicode/case_fold.rb +45 -0
- data/lib/flexr/unicode/data/LICENSE-UNICODE.txt +5 -0
- data/lib/flexr/unicode/data/UNICODE_VERSION +1 -0
- data/lib/flexr/unicode/data/case_folding.rb +9 -0
- data/lib/flexr/unicode/data/properties.rb +10 -0
- data/lib/flexr/unicode/property.rb +107 -0
- data/lib/flexr/unicode/reference_regexp.rb +102 -0
- data/lib/flexr/unicode/utf8_splitter.rb +109 -0
- data/lib/flexr/version.rb +5 -0
- data/lib/flexr.rb +81 -0
- data/site/README.md +22 -0
- data/site/astro.config.mjs +57 -0
- data/site/package.json +19 -0
- data/site/pnpm-lock.yaml +5029 -0
- data/site/pnpm-workspace.yaml +6 -0
- data/site/public/playground.js +189 -0
- data/site/scripts/verify-site.mjs +42 -0
- data/site/src/content/docs/benchmarks.md +8 -0
- data/site/src/content/docs/concepts/matching-semantics.md +15 -0
- data/site/src/content/docs/concepts/regexp-model.md +18 -0
- data/site/src/content/docs/concepts/runtime-vs-generated.md +15 -0
- data/site/src/content/docs/concepts/security-model.md +15 -0
- data/site/src/content/docs/examples.md +17 -0
- data/site/src/content/docs/learn/generation.md +29 -0
- data/site/src/content/docs/learn/getting-started.md +56 -0
- data/site/src/content/docs/learn/parser-integration.md +27 -0
- data/site/src/content/docs/learn/runtime-mode.md +32 -0
- data/site/src/content/docs/reference/action-context.md +20 -0
- data/site/src/content/docs/reference/cli.md +22 -0
- data/site/src/content/docs/reference/diagnostics.md +16 -0
- data/site/src/content/docs/reference/dsl.md +19 -0
- data/site/src/content/docs/reference/public-api.md +18 -0
- data/site/src/content/docs/reference/regexp.md +16 -0
- data/site/src/content/docs/reference/runtime.md +16 -0
- data/site/src/content/docs/reference/tokens-and-locations.md +16 -0
- data/site/src/content.config.ts +12 -0
- data/site/src/env.d.ts +1 -0
- data/site/src/layouts/SiteLayout.astro +39 -0
- data/site/src/pages/index.astro +174 -0
- data/site/src/pages/playground.astro +64 -0
- data/site/src/styles/custom.css +711 -0
- data/site/tsconfig.json +5 -0
- data/tools/coverage.rb +32 -0
- data/tools/docs_verify.rb +116 -0
- data/tools/gen_unicode_tables.rb +202 -0
- data/tools/regexp_tokenizer_reference.rb +60 -0
- 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: []
|