pub(super) const WINDOW: usize = 256;
pub(super) fn named(before: &super::input::Before<'_>, after: &[u8]) -> bool {
(prefix_mutation_possible(&before.bytes) || postfix_mutation_follows(after))
&& named_in_full(before, after)
}
fn prefix_mutation_possible(before: &[u8]) -> bool {
let mut lines = before.split(|byte| matches!(byte, b'\n' | b'\r')).rev();
let mut own = true;
for line in lines.by_ref() {
if !own && (line.ends_with(b"\\") || line.iter().any(|b| matches!(b, b'/' | b'"' | b'\'')))
{
return true;
}
own = false;
if let Some(last) = line
.iter()
.rev()
.find(|byte| !matches!(byte, b' ' | b'\t' | b'\x0c' | b'('))
{
return matches!(last, b'+' | b'-' | b'/');
}
}
true
}
fn postfix_mutation_follows(after: &[u8]) -> bool {
let mut suffix = Tokens::new(after);
loop {
match kind(suffix.next().ok().flatten()) {
Some(Kind::Close) => {}
Some(Kind::Mutation) => return true,
_ => return false,
}
}
}
fn named_in_full(before: &super::input::Before<'_>, after: &[u8]) -> bool {
let mut source = Tokens::new(&before.bytes);
let mut prefix = Vec::new();
loop {
match source.next() {
Ok(Some(token)) => prefix.push(token),
Ok(None) => break,
Err(()) => return false,
}
}
let mut start = prefix.len();
while start > 0 && prefix[start - 1].kind == Kind::Open {
start -= 1;
}
let groups = prefix.len() - start;
if start > 0
&& prefix[start - 1].kind == Kind::Mutation
&& !(before.bytes.len() == WINDOW && prefix[start - 1].start == 0)
{
let mut suffix = Tokens::new(after);
if (0..groups).all(|_| {
suffix
.next()
.is_ok_and(|next| kind(next) == Some(Kind::Close))
}) && suffix.next().is_ok_and(|next| match kind(next) {
Some(Kind::Member | Kind::Subscript | Kind::Open) => false,
None => after.len() < WINDOW,
_ => true,
}) {
return true;
}
}
let mut suffix = Tokens::new(after);
let mut closes = 0;
loop {
match suffix.next() {
Ok(Some(Token {
kind: Kind::Close, ..
})) => closes += 1,
Ok(Some(Token {
kind: Kind::Mutation,
..
})) => break,
_ => return false,
}
}
if closes > groups {
return false;
}
if before.bytes.len() == WINDOW
&& (prefix.is_empty() || (closes > 0 && prefix[prefix.len() - closes].start == 0))
{
return false;
}
if before.clipped && prefix.len() == closes {
return false;
}
let preceding = prefix
.len()
.checked_sub(closes + 1)
.and_then(|at| prefix.get(at));
!preceding.is_some_and(|token| {
token.kind == Kind::Member || (closes > 0 && token.kind == Kind::CallWord)
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Mutation,
Open,
Close,
Member,
Subscript,
CallWord,
Other,
}
struct Token {
kind: Kind,
start: usize,
}
fn kind(token: Option<Token>) -> Option<Kind> {
token.map(|token| token.kind)
}
struct Tokens<'s> {
bytes: &'s [u8],
at: usize,
}
impl<'s> Tokens<'s> {
fn new(bytes: &'s [u8]) -> Self {
Self { bytes, at: 0 }
}
fn next(&mut self) -> Result<Option<Token>, ()> {
use super::assignments::{gap, logical_byte, quoted};
if !gap(self.bytes, &mut self.at) {
return Err(());
}
let start = self.at;
let Some((first, end)) = logical_byte(self.bytes, start) else {
return Ok(None);
};
self.at = end;
let next = logical_byte(self.bytes, end);
let kind = match (first, next) {
(b'+' | b'-', Some((second, end))) if first == second => {
self.at = end;
Kind::Mutation
}
(b'-', Some((b'>', end))) => {
self.at = end;
Kind::Member
}
(b'*', Some((b'/', _))) => return Err(()),
(b'.', _) => Kind::Member,
(b'(', _) => Kind::Open,
(b')', _) => Kind::Close,
(b'[', _) => Kind::Subscript,
(b'\'' | b'"', _) => {
self.at = start;
if !quoted(self.bytes, &mut self.at) {
return Err(());
}
Kind::Other
}
(byte, _) if super::shapes::is_word(byte) => {
while let Some((byte, end)) = logical_byte(self.bytes, self.at) {
if !super::shapes::is_word(byte) {
break;
}
self.at = end;
}
if matches!(&self.bytes[start..self.at], b"return" | b"else" | b"do") {
Kind::Other
} else {
Kind::CallWord
}
}
_ => Kind::Other,
};
Ok(Some(Token { kind, start }))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scanner::input::Before;
use std::borrow::Cow;
#[test]
fn the_quick_check_agrees_with_reading_in_full() {
const PIECES: &[&[u8]] = &[
b"++", b"--", b"+", b"-", b"(", b")", b"[", b"]", b".", b"->", b"=", b" ", b"\t",
b"\x0b", b"\x0c", b"\n", b"\r\n", b"\\\n", b"\\", b"/* c */", b"/*", b"*/", b"// c\n",
b"//", b"/", b"*", b"\"s\"", b"\"", b"'c'", b"'", b"x", b"a1", b"return", b";",
];
let mut state = 0x9e37_79b9_7f4a_7c15_u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let window = |next: &mut dyn FnMut() -> u64| -> Vec<u8> {
let pieces = (next() % 7) as usize;
(0..pieces)
.flat_map(|_| {
PIECES[(next() % PIECES.len() as u64) as usize]
.iter()
.copied()
})
.collect()
};
let mut mutations = 0;
for _ in 0..200_000 {
let before = window(&mut next);
let after = window(&mut next);
let before = Before {
bytes: Cow::Owned(before),
clipped: next() % 4 == 0,
};
let full = named_in_full(&before, &after);
assert_eq!(
named(&before, &after),
full,
"{:?} name {:?}",
String::from_utf8_lossy(&before.bytes),
String::from_utf8_lossy(&after)
);
mutations += usize::from(full);
}
assert!(mutations > 1_000, "{mutations}");
}
}