///|
/// An incremental scanner for ASCII dictionary workloads stored as bytes.
pub struct ByteScanner {
automaton : Automaton
mut state : Int
mut position : Int
} derive(Debug)
///|
pub fn ByteScanner::new(automaton : Automaton) -> ByteScanner {
{ automaton, state: 0, position: 0 }
}
///|
/// Scan one byte chunk and emit matches with byte offsets.
pub fn ByteScanner::push(
self : ByteScanner,
bytes : Bytes,
) -> Array[LexiconMatch] {
let output : Array[LexiconMatch] = []
for byte in bytes {
let ch = byte.to_char()
self.state = self.automaton.step(self.state, ch)
for id in self.automaton.states[self.state].outputs {
let item = self.automaton.entries[id]
if is_ascii_entry(item) {
output.push({
keyword: item.keyword,
label: item.label,
start: self.position + 1 - item.keyword.length(),
end: self.position + 1,
weight: item.weight,
})
}
}
self.position += 1
}
output
}
///|
/// Reset a byte scanner at an end-of-stream boundary.
pub fn ByteScanner::finish(self : ByteScanner) -> Array[LexiconMatch] {
self.state = 0
self.position = 0
[]
}
///|
/// Scan a complete ASCII byte payload in one pass.
pub fn Automaton::find_ascii_bytes(
self : Automaton,
bytes : Bytes,
) -> Array[LexiconMatch] {
let scanner = ByteScanner::new(self)
scanner.push(bytes)
}
///|
/// Alias suitable for code that supports both text and byte backends.
pub fn Automaton::find_bytes(
self : Automaton,
bytes : Bytes,
) -> Array[LexiconMatch] {
self.find_ascii_bytes(bytes)
}
///|
/// True when every byte is a seven-bit ASCII value.
pub fn bytes_are_ascii(bytes : Bytes) -> Bool {
for byte in bytes {
if byte.to_int() > 127 {
return false
}
}
true
}
///|
/// Count bytes outside the ASCII range.
pub fn non_ascii_byte_count(bytes : Bytes) -> Int {
let mut count = 0
for byte in bytes {
if byte.to_int() > 127 {
count += 1
}
}
count
}
///|
/// Return the number of ASCII-compatible entries in an automaton.
pub fn Automaton::ascii_entry_count(self : Automaton) -> Int {
let mut count = 0
for item in self.entries {
if is_ascii_entry(item) {
count += 1
}
}
count
}
///|
/// Summary of a byte scan.
pub(all) struct ByteScanStats {
byte_count : Int
ascii : Bool
non_ascii_count : Int
match_count : Int
distinct_surface_count : Int
covered_bytes : Int
} derive(Debug, Eq, ToJson)
///|
/// Scan bytes and calculate operational counters.
pub fn scan_byte_stats(automaton : Automaton, bytes : Bytes) -> ByteScanStats {
let matches = automaton.find_ascii_bytes(bytes)
{
byte_count: bytes.length(),
ascii: bytes_are_ascii(bytes),
non_ascii_count: non_ascii_byte_count(bytes),
match_count: matches.length(),
distinct_surface_count: match_stats(matches).distinct_surface_count,
covered_bytes: covered_byte_count(bytes.length(), matches),
}
}
///|
/// Return bytes in a source span, clamped to valid byte boundaries.
pub fn byte_slice(bytes : Bytes, start : Int, end : Int) -> Bytes {
let safe_start = byte_clamp(start, 0, bytes.length())
let safe_end = byte_clamp(end, safe_start, bytes.length())
match bytes.get_view(start=safe_start, end=safe_end) {
Some(view) => view.to_owned()
None => Bytes::new(0)
}
}
///|
/// Convert ASCII bytes to text, returning an empty string for non-ASCII input.
pub fn ascii_bytes_to_string(bytes : Bytes) -> String {
if !bytes_are_ascii(bytes) {
return ""
}
let output = StringBuilder()
for byte in bytes {
output.write_char(byte.to_char())
}
output.to_string()
}
///|
/// Count covered byte positions using merged match intervals.
pub fn covered_byte_count(
byte_length : Int,
matches : ArrayView[LexiconMatch],
) -> Int {
covered_characters(byte_length, matches)
}
///|
/// Build an ASCII byte payload from a string, if every character fits.
pub fn string_to_ascii_bytes(text : String) -> Bytes? {
let output : Array[Byte] = []
for ch in text {
if ch.to_int() > 127 {
return None
}
output.push(ch.to_int().to_byte())
}
Some(Bytes::from_array(output))
}
///|
/// Return a stable diagnostic for the byte backend.
pub fn byte_backend_description() -> String {
"ASCII byte scanner: incremental Aho-Corasick with byte offsets"
}
///|
/// Scan multiple byte chunks with one retained automaton state.
pub fn scan_byte_chunks(
automaton : Automaton,
chunks : ArrayView[Bytes],
) -> Array[LexiconMatch] {
let scanner = ByteScanner::new(automaton)
let output : Array[LexiconMatch] = []
for chunk in chunks {
for hit in scanner.push(chunk) {
output.push(hit)
}
}
output
}
///|
/// Filter byte matches by a label without recompiling the automaton.
pub fn scan_bytes_for_label(
automaton : Automaton,
bytes : Bytes,
label : String,
) -> Array[LexiconMatch] {
[
for hit in automaton.find_ascii_bytes(bytes) if hit.label == label => hit
]
}
///|
/// Return true when all dictionary keywords can be represented by the byte API.
pub fn Automaton::supports_ascii_backend(self : Automaton) -> Bool {
self.ascii_entry_count() == self.entries.length()
}
///|
/// Return the first byte position containing a match, if any.
pub fn first_byte_match(automaton : Automaton, bytes : Bytes) -> LexiconMatch? {
automaton.find_ascii_bytes(bytes).get(0)
}
///|
/// Return a hexadecimal preview for diagnostics.
pub fn bytes_hex_preview(bytes : Bytes, limit : Int) -> String {
let output = StringBuilder()
let safe_limit = if limit < 0 { 0 } else { limit }
for index, byte in bytes {
if index >= safe_limit {
break
}
if index > 0 {
output.write_char(' ')
}
output.write_string(byte.to_hex())
}
output.to_string()
}
///|
fn is_ascii_entry(item : Entry) -> Bool {
for ch in item.keyword {
if ch.to_int() > 127 {
return false
}
}
true
}
///|
fn byte_clamp(value : Int, minimum : Int, maximum : Int) -> Int {
if value < minimum {
minimum
} else if value > maximum {
maximum
} else {
value
}
}