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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "flexr"
|
|
4
|
+
|
|
5
|
+
module Flexr
|
|
6
|
+
module Regexp
|
|
7
|
+
# This specification is intentionally small: it is a dogfood target for
|
|
8
|
+
# the source lexer path, not the parser's implementation.
|
|
9
|
+
class SourceLexer < Flexr::Lexer
|
|
10
|
+
encoding Encoding::BINARY
|
|
11
|
+
emits :PROPERTY, :ESCAPE, :CHAR_CLASS, :QUANTIFIER, :ALTERNATION,
|
|
12
|
+
:GROUP_OPEN, :GROUP_CLOSE, :ANCHOR, :DOT, :LITERAL
|
|
13
|
+
|
|
14
|
+
rule(/[ \t\r\n]+/, skip: true)
|
|
15
|
+
rule(/\\p\{[A-Za-z_][A-Za-z0-9_]*\}/) { emit :PROPERTY, text.byteslice(3...-1) }
|
|
16
|
+
rule(/\\./) { emit :ESCAPE, text }
|
|
17
|
+
rule(/\[[^\]\n]*\]/) { emit :CHAR_CLASS, text }
|
|
18
|
+
rule(/[?*+]|\{[0-9]+(?:,[0-9]*)?\}/) { emit :QUANTIFIER, text }
|
|
19
|
+
rule(/\|/) { emit :ALTERNATION, text }
|
|
20
|
+
rule(/\(/) { emit :GROUP_OPEN, text }
|
|
21
|
+
rule(/\)/) { emit :GROUP_CLOSE, text }
|
|
22
|
+
rule(/\^|\$/) { emit :ANCHOR, text }
|
|
23
|
+
rule(/\./) { emit :DOT, text }
|
|
24
|
+
rule(/[^\\\[\]().|?*+{}^$ \t\r\n]/) { emit :LITERAL, text }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by flexr. DO NOT EDIT.
|
|
3
|
+
# source: lib/flexr/regexp/tokenizer.flexr.rb
|
|
4
|
+
# spec-digest: sha256:3129b1c3e05f1c0680e3176ad96eb4ca7ad77fdd7168c47228351e47d79ad8f4
|
|
5
|
+
# unicode: 15.1.0
|
|
6
|
+
# backend: table
|
|
7
|
+
# compiled: true
|
|
8
|
+
# eval: false
|
|
9
|
+
# standalone: false
|
|
10
|
+
|
|
11
|
+
require "flexr"
|
|
12
|
+
|
|
13
|
+
module Flexr
|
|
14
|
+
module Regexp
|
|
15
|
+
# This specification is intentionally small: it is a dogfood target for
|
|
16
|
+
# the source lexer path, not the parser's implementation.
|
|
17
|
+
class SourceLexer < Flexr::Lexer
|
|
18
|
+
Flexr::Generated.install_compiled!(self, { rules: [{ index: 0, patterns: [/[ \t\r\n]+/], pattern_conditions: [[0, 0, false, false]], trailing: nil, action: :skip, states: [:initial], bol_only: false, end_anchor: nil }, { index: 1, patterns: [/\\p\{[A-Za-z_][A-Za-z0-9_]*\}/], pattern_conditions: [[1, 0, false, false]], trailing: nil, action: proc{ emit :PROPERTY, text.byteslice(3...-1) }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 2, patterns: [/\\./], pattern_conditions: [[2, 0, false, false]], trailing: nil, action: proc{ emit :ESCAPE, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 3, patterns: [/\[[^\]\n]*\]/], pattern_conditions: [[3, 0, false, false]], trailing: nil, action: proc{ emit :CHAR_CLASS, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 4, patterns: [/[?*+]|\{[0-9]+(?:,[0-9]*)?\}/], pattern_conditions: [[4, 0, false, false]], trailing: nil, action: proc{ emit :QUANTIFIER, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 5, patterns: [/\|/], pattern_conditions: [[5, 0, false, false]], trailing: nil, action: proc{ emit :ALTERNATION, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 6, patterns: [/\(/], pattern_conditions: [[6, 0, false, false]], trailing: nil, action: proc{ emit :GROUP_OPEN, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 7, patterns: [/\)/], pattern_conditions: [[7, 0, false, false]], trailing: nil, action: proc{ emit :GROUP_CLOSE, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 8, patterns: [/\^|\$/], pattern_conditions: [[8, 0, false, false]], trailing: nil, action: proc{ emit :ANCHOR, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 9, patterns: [/\./], pattern_conditions: [[9, 0, false, false]], trailing: nil, action: proc{ emit :DOT, text }, states: [:initial], bol_only: false, end_anchor: nil }, { index: 10, patterns: [/[^\\\[\]().|?*+{}^$ \t\r\n]/], pattern_conditions: [[10, 0, false, false]], trailing: nil, action: proc{ emit :LITERAL, text }, states: [:initial], bol_only: false, end_anchor: nil }], backend: :table, token_kind: :array, encoding: Encoding::BINARY, declared_tokens: [:PROPERTY, :ESCAPE, :CHAR_CLASS, :QUANTIFIER, :ALTERNATION, :GROUP_OPEN, :GROUP_CLOSE, :ANCHOR, :DOT, :LITERAL], options: {}, eof_rules: { }, states: [], inclusive_states: {initial: true}, compiled: { machines: { :initial => { state_name: :initial, dfa: {accepts: [[], [[10, 0, false, false]], [[0, 0, false, false]], [[8, 0, false, false]], [[6, 0, false, false]], [[7, 0, false, false]], [[4, 0, false, false]], [[9, 0, false, false]], [], [], [], [[5, 0, false, false]], [[3, 0, false, false]], [[2, 0, false, false]], [[2, 0, false, false]], [], [], [], [], [[1, 0, false, false]]], ec: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 7, 7, 8, 9, 9, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 19, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 23, 24, 25, 26, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 31, 32, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34], class_count: 35, state_count: 20, start: 0, rule_ids: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], packed: {base: [0, 0, 12, 0, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 3, 22, 15, 4, 21, 0], default: [1, nil, nil, nil, nil, nil, nil, nil, 8, 13, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil], next: [nil, 2, 2, nil, 2, nil, 2, nil, 3, nil, 4, 5, 6, 2, 2, 7, 2, 15, 2, 6, nil, 17, 8, 9, nil, 3, nil, 12, nil, nil, 14, 10, 11, nil, 16, 17, 18, 6, 18, 15, nil, 18, 18, 18, 18, 18, nil, 18, nil, 18, 18, 18, nil, nil, 19, 6], check: [nil, 0, 0, 9, 0, 8, 0, nil, 0, nil, 0, 0, 0, 2, 2, 0, 2, 10, 2, 0, nil, 17, 0, 0, 0, 0, nil, 8, nil, nil, 9, 0, 0, 0, 14, 15, 16, 17, 18, 15, nil, 16, 18, 16, 16, 16, nil, 18, nil, 18, 18, 18, nil, nil, 18, 15]}} } }, states: [:initial], stats: {initial: {states: 20, classes: 35, accepting_states: 12}}, diagnostics: [] } })
|
|
19
|
+
def scan_one
|
|
20
|
+
return Flexr::Runtime::Interpreter.new(self).scan unless __flexr_generated_fast_path?
|
|
21
|
+
machine = self.class.__flexr_compiled.machines.fetch(state)
|
|
22
|
+
dfa = machine.dfa
|
|
23
|
+
position = byte_pos
|
|
24
|
+
return nil unless valid_utf8_at?(position)
|
|
25
|
+
cursor = position
|
|
26
|
+
current = dfa.start
|
|
27
|
+
direct = dfa.direct
|
|
28
|
+
source = buffer.source
|
|
29
|
+
best = nil
|
|
30
|
+
accelerate = self.class.__flexr_config.options.fetch(:accel, :auto) != :none && !utf8_input?
|
|
31
|
+
while cursor < source.bytesize || buffer.ensure_available?(cursor + 1)
|
|
32
|
+
if accelerate
|
|
33
|
+
region = __flexr_generated_acceleration_region(dfa, current)
|
|
34
|
+
accelerated_end = __flexr_generated_accelerate(region, cursor) if region
|
|
35
|
+
if accelerated_end && accelerated_end > cursor
|
|
36
|
+
cursor = accelerated_end
|
|
37
|
+
best = __flexr_generated_acceptance(dfa, current, position, cursor, best)
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
byte = source.getbyte(cursor)
|
|
42
|
+
current = if direct
|
|
43
|
+
class_id = dfa.ec[byte]
|
|
44
|
+
value = direct[:nxt][(current * direct[:classes]) + class_id]
|
|
45
|
+
value >= 0 ? value : nil
|
|
46
|
+
else
|
|
47
|
+
dfa.transition(current, byte)
|
|
48
|
+
end
|
|
49
|
+
break unless current
|
|
50
|
+
cursor += 1
|
|
51
|
+
acceptance = dfa.accepts[current].first
|
|
52
|
+
if acceptance
|
|
53
|
+
rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
|
|
54
|
+
best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)
|
|
55
|
+
best.rule = rule
|
|
56
|
+
best.start_pos = position
|
|
57
|
+
best.end_pos = cursor
|
|
58
|
+
best.total_end_pos = cursor
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
best
|
|
62
|
+
end
|
|
63
|
+
def __flexr_generated_acceleration_region(dfa, state)
|
|
64
|
+
@__flexr_generated_accel_regions ||= {}
|
|
65
|
+
regions = (@__flexr_generated_accel_regions[dfa] ||= Flexr::Automaton::Accel.extract(dfa).to_h { |region| [region.state, region] })
|
|
66
|
+
region = regions[state]
|
|
67
|
+
return unless region
|
|
68
|
+
return if dfa.accepts[state].any? do |acceptance|
|
|
69
|
+
rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
|
|
70
|
+
acceptance.bol_only || acceptance.end_anchor || rule.trailing
|
|
71
|
+
end
|
|
72
|
+
region
|
|
73
|
+
end
|
|
74
|
+
def __flexr_generated_accelerate(region, position)
|
|
75
|
+
binary = buffer.source.b
|
|
76
|
+
mode = self.class.__flexr_config.options.fetch(:accel, :auto)
|
|
77
|
+
match_end = if %i[strscan auto].include?(mode) && defined?(StringScanner)
|
|
78
|
+
scanner = StringScanner.new(binary)
|
|
79
|
+
scanner.pos = position
|
|
80
|
+
length = scanner.skip(region.regexp)
|
|
81
|
+
length && scanner.pos
|
|
82
|
+
else
|
|
83
|
+
match = region.regexp.match(binary, position)
|
|
84
|
+
match && match.begin(0) == position ? match.end(0) : nil
|
|
85
|
+
end
|
|
86
|
+
return match_end if match_end && match_end < buffer.bytesize
|
|
87
|
+
return match_end if match_end && buffer.eof_loaded?
|
|
88
|
+
return unless buffer.ensure_available?(buffer.bytesize + 1)
|
|
89
|
+
__flexr_generated_accelerate(region, position)
|
|
90
|
+
rescue ArgumentError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
def __flexr_generated_fast_path?
|
|
94
|
+
return @__flexr_generated_fast_path if defined?(@__flexr_generated_fast_path)
|
|
95
|
+
@__flexr_generated_fast_path = if self.class.__flexr_config.backend == :firstmatch ||
|
|
96
|
+
self.class.__flexr_config.options[:allow_empty_match] == true
|
|
97
|
+
false
|
|
98
|
+
else
|
|
99
|
+
self.class.__flexr_rules.none? do |rule|
|
|
100
|
+
!rule.trailing.nil? || rule.patterns.any? do |pattern|
|
|
101
|
+
next false unless pattern.is_a?(Regexp)
|
|
102
|
+
next true if pattern.source.match?(/\\[pP]\\{/) || pattern.source.match?(/\[:(?:\^)?[a-z]+:\]/)
|
|
103
|
+
self.class.__flexr_config.options[:unicode] == true && utf8_input? && pattern.source.match?(/\\[dDwWsS]/)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
def __flexr_generated_acceptance(dfa, state, start_position, cursor, best)
|
|
109
|
+
dfa.accepts[state].each do |acceptance|
|
|
110
|
+
next if acceptance.bol_only && !beginning_of_line?
|
|
111
|
+
next if acceptance.end_anchor && !(buffer.eof?(cursor) || buffer.getbyte(cursor) == 0x0a)
|
|
112
|
+
rule = self.class.__flexr_rules.fetch(acceptance.rule_index)
|
|
113
|
+
defer_token_size_check!(cursor - start_position)
|
|
114
|
+
next if best && cursor < best.total_end_pos
|
|
115
|
+
next if best && cursor == best.total_end_pos &&
|
|
116
|
+
rule.index > best.rule.index
|
|
117
|
+
best ||= (@__flexr_generated_match ||= Flexr::Runtime::Match.new)
|
|
118
|
+
best.rule = rule
|
|
119
|
+
best.start_pos = start_position
|
|
120
|
+
best.end_pos = cursor
|
|
121
|
+
best.total_end_pos = cursor
|
|
122
|
+
end
|
|
123
|
+
best
|
|
124
|
+
end
|
|
125
|
+
def __flexr_generated_execute(rule)
|
|
126
|
+
case rule.index
|
|
127
|
+
when 0
|
|
128
|
+
when 1
|
|
129
|
+
emit :PROPERTY, text.byteslice(3...-1)
|
|
130
|
+
when 2
|
|
131
|
+
emit :ESCAPE, text
|
|
132
|
+
when 3
|
|
133
|
+
emit :CHAR_CLASS, text
|
|
134
|
+
when 4
|
|
135
|
+
emit :QUANTIFIER, text
|
|
136
|
+
when 5
|
|
137
|
+
emit :ALTERNATION, text
|
|
138
|
+
when 6
|
|
139
|
+
emit :GROUP_OPEN, text
|
|
140
|
+
when 7
|
|
141
|
+
emit :GROUP_CLOSE, text
|
|
142
|
+
when 8
|
|
143
|
+
emit :ANCHOR, text
|
|
144
|
+
when 9
|
|
145
|
+
emit :DOT, text
|
|
146
|
+
when 10
|
|
147
|
+
emit :LITERAL, text
|
|
148
|
+
else
|
|
149
|
+
instance_exec(&rule.action)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Runtime
|
|
5
|
+
class Buffer
|
|
6
|
+
DEFAULT_CHUNK_SIZE = 64 * 1024
|
|
7
|
+
|
|
8
|
+
attr_reader :source
|
|
9
|
+
|
|
10
|
+
def initialize(input, chunk_size: DEFAULT_CHUNK_SIZE)
|
|
11
|
+
raise ArgumentError, "chunk_size must be positive" unless chunk_size.to_i.positive?
|
|
12
|
+
|
|
13
|
+
@chunk_size = chunk_size.to_i
|
|
14
|
+
@io = input.is_a?(String) ? nil : input
|
|
15
|
+
raise ArgumentError, "input must be a String or IO" if @io && !@io.respond_to?(:read)
|
|
16
|
+
|
|
17
|
+
@source = input.is_a?(String) ? input : String.new(encoding: Encoding::BINARY)
|
|
18
|
+
@eof = @io.nil?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def bytesize
|
|
22
|
+
source.bytesize
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def getbyte(position)
|
|
26
|
+
ensure_available?(position + 1)
|
|
27
|
+
source.getbyte(position)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def byteslice(range, length = nil)
|
|
31
|
+
ensure_range(range, length)
|
|
32
|
+
length ? source.byteslice(range, length) : source.byteslice(range)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ensure_available?(end_position)
|
|
36
|
+
return true if end_position <= bytesize
|
|
37
|
+
return false if @eof
|
|
38
|
+
|
|
39
|
+
while bytesize < end_position && !@eof
|
|
40
|
+
chunk = @io.read(@chunk_size)
|
|
41
|
+
if chunk.nil? || chunk.empty?
|
|
42
|
+
@eof = true
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@source.force_encoding(chunk.encoding) if @source.empty?
|
|
47
|
+
@source << chunk
|
|
48
|
+
end
|
|
49
|
+
bytesize >= end_position
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def read_to_end
|
|
53
|
+
ensure_available?(Float::INFINITY)
|
|
54
|
+
source
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def eof_loaded?
|
|
58
|
+
@eof
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def eof?(position)
|
|
62
|
+
!ensure_available?(position + 1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def utf8_boundary?(position)
|
|
66
|
+
return false if position.negative?
|
|
67
|
+
return true if position.zero?
|
|
68
|
+
return true unless ensure_available?(position)
|
|
69
|
+
|
|
70
|
+
following = source.getbyte(position)
|
|
71
|
+
following.nil? || (following & 0xc0) != 0x80
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def valid_utf8_at?(position)
|
|
75
|
+
first = getbyte(position)
|
|
76
|
+
return false unless first
|
|
77
|
+
return true if first <= 0x7f
|
|
78
|
+
|
|
79
|
+
length = if first.between?(0xc2, 0xdf)
|
|
80
|
+
2
|
|
81
|
+
elsif first.between?(0xe0, 0xef)
|
|
82
|
+
3
|
|
83
|
+
elsif first.between?(0xf0, 0xf4)
|
|
84
|
+
4
|
|
85
|
+
end
|
|
86
|
+
return false unless length
|
|
87
|
+
return false unless ensure_available?(position + length)
|
|
88
|
+
|
|
89
|
+
byteslice(position, length).dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def ensure_range(range, length)
|
|
95
|
+
if length
|
|
96
|
+
ensure_available?(range + length)
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
unless range.is_a?(Range)
|
|
101
|
+
ensure_available?(range + 1)
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
return read_to_end if range.end.nil?
|
|
106
|
+
|
|
107
|
+
ending = range.end + (range.exclude_end? ? 0 : 1)
|
|
108
|
+
ensure_available?(ending)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flexr
|
|
4
|
+
module Runtime
|
|
5
|
+
def initialize(input, filename: nil, error_mode: :raise, max_token_size: 16 * 1024 * 1024,
|
|
6
|
+
max_state_stack: 1024, chunk_size: Runtime::Buffer::DEFAULT_CHUNK_SIZE)
|
|
7
|
+
raise ArgumentError, "max_token_size must be non-negative" if max_token_size.to_i.negative?
|
|
8
|
+
raise ArgumentError, "max_state_stack must be non-negative" if max_state_stack.to_i.negative?
|
|
9
|
+
|
|
10
|
+
self.class.compile!
|
|
11
|
+
@buffer = Runtime::Buffer.new(input, chunk_size: chunk_size)
|
|
12
|
+
@input = input.is_a?(String) ? input : nil
|
|
13
|
+
@line_tracking_needed = !@input || input.include?("\n")
|
|
14
|
+
@filename = filename
|
|
15
|
+
@error_mode = error_mode
|
|
16
|
+
@max_token_size = max_token_size.to_i
|
|
17
|
+
@max_state_stack = max_state_stack.to_i
|
|
18
|
+
@position = 0
|
|
19
|
+
@line = 1
|
|
20
|
+
@state = :initial
|
|
21
|
+
@state_stack = []
|
|
22
|
+
@pending = nil
|
|
23
|
+
@matched = nil
|
|
24
|
+
@match_start = 0
|
|
25
|
+
@match_end = 0
|
|
26
|
+
@text_start = 0
|
|
27
|
+
@text_line = 1
|
|
28
|
+
@bol = true
|
|
29
|
+
@more_start = nil
|
|
30
|
+
@more_line = nil
|
|
31
|
+
@more_requested = false
|
|
32
|
+
@candidate_token_size = 0
|
|
33
|
+
@eof_fired_states = {}
|
|
34
|
+
@on_error = nil
|
|
35
|
+
@halted = false
|
|
36
|
+
@interpreter = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :filename, :error_mode, :buffer, :max_token_size
|
|
40
|
+
|
|
41
|
+
attr_writer :on_error
|
|
42
|
+
|
|
43
|
+
def input
|
|
44
|
+
@buffer.source
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def next_token
|
|
48
|
+
return nil if @halted
|
|
49
|
+
|
|
50
|
+
loop do
|
|
51
|
+
return nil if @halted
|
|
52
|
+
|
|
53
|
+
if eof? && @pending.nil?
|
|
54
|
+
eof_action = self.class.__flexr_spec.eof_rules[@state]
|
|
55
|
+
if eof_action && !@eof_fired_states[@state]
|
|
56
|
+
@eof_fired_states[@state] = true
|
|
57
|
+
@match_start = @position
|
|
58
|
+
@match_end = @position
|
|
59
|
+
@text_start = @position
|
|
60
|
+
@text_line = @line
|
|
61
|
+
@matched = nil
|
|
62
|
+
instance_exec(&eof_action)
|
|
63
|
+
token = @pending
|
|
64
|
+
@pending = nil
|
|
65
|
+
return token if token
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
return nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
match = if generated_runtime? && respond_to?(:scan_one, true)
|
|
72
|
+
scan_one
|
|
73
|
+
else
|
|
74
|
+
(@interpreter ||= Runtime::Interpreter.new(self)).scan
|
|
75
|
+
end
|
|
76
|
+
unless match
|
|
77
|
+
unless eof?
|
|
78
|
+
token = handle_unmatched_byte
|
|
79
|
+
next unless token
|
|
80
|
+
|
|
81
|
+
return token
|
|
82
|
+
end
|
|
83
|
+
return nil
|
|
84
|
+
end
|
|
85
|
+
@match_start = match.start_pos
|
|
86
|
+
@match_end = match.end_pos
|
|
87
|
+
if @more_start
|
|
88
|
+
@text_start = @more_start
|
|
89
|
+
@text_line = @more_line
|
|
90
|
+
else
|
|
91
|
+
@text_start = @match_start
|
|
92
|
+
@text_line = @line
|
|
93
|
+
end
|
|
94
|
+
@matched = nil
|
|
95
|
+
@more_requested = false
|
|
96
|
+
@position = match.end_pos
|
|
97
|
+
empty_match = match.end_pos == match.start_pos
|
|
98
|
+
execute(match.rule)
|
|
99
|
+
finalize_more
|
|
100
|
+
force_empty_match_progress! if empty_match && self.class.__flexr_config.options[:allow_empty_match]
|
|
101
|
+
ensure_token_size!
|
|
102
|
+
update_position
|
|
103
|
+
token = @pending
|
|
104
|
+
@pending = nil
|
|
105
|
+
return token if token
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def each_token
|
|
110
|
+
return enum_for(__method__) unless block_given?
|
|
111
|
+
|
|
112
|
+
loop do
|
|
113
|
+
token = next_token
|
|
114
|
+
break unless token
|
|
115
|
+
|
|
116
|
+
if self.class.__flexr_config.token_kind == :yield
|
|
117
|
+
yield(*token)
|
|
118
|
+
else
|
|
119
|
+
yield token
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def tokens
|
|
126
|
+
result = []
|
|
127
|
+
while (token = next_token)
|
|
128
|
+
result << token
|
|
129
|
+
end
|
|
130
|
+
result
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def racc_next_token
|
|
134
|
+
token = next_token
|
|
135
|
+
token ? [token[0], token[1]] : [false, "$end"]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def text
|
|
139
|
+
return @matched if @matched
|
|
140
|
+
|
|
141
|
+
@matched = @buffer.byteslice(@text_start...@match_end)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def text_bytesize
|
|
145
|
+
@match_end - @text_start
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def byte_pos
|
|
149
|
+
@position
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def more_text_start
|
|
153
|
+
@more_start
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def defer_token_size_check!(size)
|
|
157
|
+
@candidate_token_size = size if size > @candidate_token_size
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def utf8_input?
|
|
161
|
+
self.class.__flexr_config.encoding == Encoding::UTF_8
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def valid_utf8_at?(position)
|
|
165
|
+
!utf8_input? || @buffer.valid_utf8_at?(position)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def utf8_boundary?(position)
|
|
169
|
+
!utf8_input? || @buffer.utf8_boundary?(position)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def lineno
|
|
173
|
+
@line
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
alias line lineno
|
|
177
|
+
|
|
178
|
+
def state
|
|
179
|
+
@state
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def beginning_of_line?
|
|
183
|
+
@bol
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def binary_input
|
|
187
|
+
@buffer.source.b
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def emit(type, value = text)
|
|
191
|
+
@pending = case self.class.__flexr_config.token_kind
|
|
192
|
+
when :struct
|
|
193
|
+
Runtime::Token.new(type: type, value: value, location: last_location)
|
|
194
|
+
else
|
|
195
|
+
[type, value]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def skip
|
|
200
|
+
@pending = nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def error!(message)
|
|
204
|
+
error = LexError.new(message, filename: @filename, byte_pos: @match_start, line: @line, text: text)
|
|
205
|
+
if @on_error
|
|
206
|
+
action = @on_error.call(error)
|
|
207
|
+
return @pending = nil if action == :skip
|
|
208
|
+
raise error if action == :raise
|
|
209
|
+
if action == :halt
|
|
210
|
+
@halted = true
|
|
211
|
+
return @pending = nil
|
|
212
|
+
end
|
|
213
|
+
return emit(:error, text) if action == :token
|
|
214
|
+
end
|
|
215
|
+
case @error_mode
|
|
216
|
+
when :token
|
|
217
|
+
emit(:error, text)
|
|
218
|
+
when :panic
|
|
219
|
+
@pending = nil
|
|
220
|
+
else
|
|
221
|
+
raise error
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def echo
|
|
226
|
+
emit(nil, text)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def reject
|
|
230
|
+
diagnostic = Diagnostics.error(
|
|
231
|
+
"FLEXR-E013", "reject is not supported by flexr",
|
|
232
|
+
help: "use a state transition and less(n) to express the fallback"
|
|
233
|
+
)
|
|
234
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def push(name)
|
|
238
|
+
ensure_state!(name)
|
|
239
|
+
if @state_stack.length >= @max_state_stack
|
|
240
|
+
raise Runtime::StateStackOverflowError.new(
|
|
241
|
+
"state stack exceeds max_state_stack (#{@max_state_stack})",
|
|
242
|
+
filename: @filename, byte_pos: @position, line: @line, text: text
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
@state_stack << @state
|
|
247
|
+
@state = name.to_sym
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
alias push_state push
|
|
251
|
+
|
|
252
|
+
def pop
|
|
253
|
+
@state = @state_stack.pop || :initial
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
alias pop_state pop
|
|
257
|
+
|
|
258
|
+
def begin_state(name)
|
|
259
|
+
ensure_state!(name)
|
|
260
|
+
@state = name.to_sym
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
alias state= begin_state
|
|
264
|
+
|
|
265
|
+
def less(count)
|
|
266
|
+
matched_bytes = @match_end - @match_start
|
|
267
|
+
raise ArgumentError, "less must not exceed matched bytes" if count.negative? || count > matched_bytes
|
|
268
|
+
|
|
269
|
+
@position = @match_start + count
|
|
270
|
+
@match_end = @position
|
|
271
|
+
@matched = nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def more
|
|
275
|
+
@more_requested = true
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def last_location
|
|
279
|
+
line_begin = @text_line || @line
|
|
280
|
+
Runtime::Location.new(
|
|
281
|
+
filename: @filename, byte_begin: @text_start, byte_end: @match_end,
|
|
282
|
+
line_begin: line_begin, line_end: line_begin + text.to_s.b.count("\n"),
|
|
283
|
+
column_values: [column_at(@text_start), column_at(@match_end)],
|
|
284
|
+
eager_columns: self.class.__flexr_config.options[:eager_columns] == true
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
def execute(rule)
|
|
291
|
+
return __flexr_generated_execute(rule) if generated_runtime? && respond_to?(:__flexr_generated_execute, true)
|
|
292
|
+
|
|
293
|
+
case rule.action
|
|
294
|
+
when :skip
|
|
295
|
+
nil
|
|
296
|
+
when Array
|
|
297
|
+
emit(rule.action[1], text)
|
|
298
|
+
else
|
|
299
|
+
instance_exec(&rule.action)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def finalize_more
|
|
304
|
+
if @more_requested
|
|
305
|
+
@more_start = @text_start
|
|
306
|
+
@more_line = @text_line
|
|
307
|
+
else
|
|
308
|
+
@more_start = nil
|
|
309
|
+
@more_line = nil
|
|
310
|
+
end
|
|
311
|
+
@more_requested = false
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def generated_runtime?
|
|
315
|
+
self.class.respond_to?(:__flexr_generated?) && self.class.__flexr_generated?
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def ensure_token_size!
|
|
319
|
+
actual_size = @match_end - @text_start
|
|
320
|
+
@candidate_token_size = 0
|
|
321
|
+
return if actual_size <= @max_token_size
|
|
322
|
+
|
|
323
|
+
raise Runtime::TokenTooLargeError
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def force_empty_match_progress!
|
|
327
|
+
return if eof?
|
|
328
|
+
|
|
329
|
+
byte = @buffer.byteslice(@position, 1).to_s.b
|
|
330
|
+
@position += 1
|
|
331
|
+
@line += 1 if byte == "\n"
|
|
332
|
+
@bol = byte == "\n"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def update_position
|
|
336
|
+
if @match_end > @match_start
|
|
337
|
+
length = @match_end - @match_start
|
|
338
|
+
@line += if length == 1
|
|
339
|
+
@buffer.source.getbyte(@match_start) == 0x0a ? 1 : 0
|
|
340
|
+
elsif @line_tracking_needed && (newline = @buffer.source.index("\n", @match_start)) && newline < @match_end
|
|
341
|
+
@buffer.byteslice(@match_start, length).to_s.count("\n")
|
|
342
|
+
else
|
|
343
|
+
0
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
@bol = @match_end.zero? || @buffer.source.getbyte(@match_end - 1) == 0x0a
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def handle_unmatched_byte
|
|
350
|
+
bad = @buffer.byteslice(@position, 1)
|
|
351
|
+
@match_start = @position
|
|
352
|
+
@match_end = @position + 1
|
|
353
|
+
@text_start = @match_start
|
|
354
|
+
@text_line = @line
|
|
355
|
+
@position += 1
|
|
356
|
+
@line += 1 if bad.to_s.b == "\n"
|
|
357
|
+
@bol = bad.to_s.b == "\n"
|
|
358
|
+
error!("unexpected byte #{bad.inspect}")
|
|
359
|
+
token = @pending
|
|
360
|
+
@pending = nil
|
|
361
|
+
token
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def eof?
|
|
365
|
+
return @position >= @buffer.bytesize if @input
|
|
366
|
+
|
|
367
|
+
@buffer.eof?(@position)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def ensure_state!(name)
|
|
371
|
+
return if self.class.__flexr_config.states.key?(name.to_sym)
|
|
372
|
+
|
|
373
|
+
diagnostic = Diagnostics.error("FLEXR-E003", "undefined state: #{name}")
|
|
374
|
+
raise CompileError.new(diagnostic.message, diagnostic: diagnostic)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def column_at(position)
|
|
378
|
+
prefix = @buffer.byteslice(0...position).to_s
|
|
379
|
+
last_newline = prefix.rindex("\n")
|
|
380
|
+
line_prefix = prefix.byteslice((last_newline ? last_newline + 1 : 0)..).to_s
|
|
381
|
+
if utf8_input? && line_prefix.dup.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
382
|
+
line_prefix.force_encoding(Encoding::UTF_8).length + 1
|
|
383
|
+
else
|
|
384
|
+
line_prefix.bytesize + 1
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|