use std::collections::{BTreeMap, BTreeSet};
use crate::frontend::{LiteralKind, Token, TokenKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LiteralNorm {
Preserve,
Category,
#[default]
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NormAtom<'a> {
Renamed(u32),
Text(&'a str),
Literal(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NormToken<'a> {
pub tag: u8,
pub atom: NormAtom<'a>,
}
const fn literal_class(kind: LiteralKind, mode: LiteralNorm) -> u8 {
match mode {
LiteralNorm::Preserve | LiteralNorm::Full => 0,
LiteralNorm::Category => match kind {
LiteralKind::Integer => 1,
LiteralKind::Float => 2,
LiteralKind::String => 3,
LiteralKind::Char => 4,
LiteralKind::Bool => 5,
},
}
}
fn punct_text(token: &Token) -> Option<&str> {
(token.kind == TokenKind::Punctuation).then_some(token.text.as_str())
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Resolution {
external: BTreeSet<usize>,
local: BTreeSet<usize>,
}
impl Resolution {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, start_byte: usize, external: bool) {
if external {
self.local.remove(&start_byte);
self.external.insert(start_byte);
} else {
self.external.remove(&start_byte);
self.local.insert(start_byte);
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.external.is_empty() && self.local.is_empty()
}
fn verdict(&self, start_byte: usize) -> Option<bool> {
if self.external.contains(&start_byte) {
Some(true)
} else if self.local.contains(&start_byte) {
Some(false)
} else {
None
}
}
}
fn is_preserved(tokens: &[Token], i: usize, resolved: Option<&Resolution>) -> bool {
if let Some(verdict) = resolved.and_then(|r| r.verdict(tokens[i].span.start_byte)) {
return verdict;
}
if tokens[i]
.text
.chars()
.next()
.is_some_and(char::is_uppercase)
{
return true;
}
let prev = i
.checked_sub(1)
.and_then(|p| tokens.get(p))
.and_then(punct_text);
let next = tokens.get(i + 1).and_then(punct_text);
matches!(prev, Some("::" | "." | "->")) || matches!(next, Some("::" | "!"))
}
#[must_use]
pub fn normalize(tokens: &[Token], literals: LiteralNorm) -> Vec<NormToken<'_>> {
let mut out = Vec::new();
normalize_into(tokens, literals, &mut out);
out
}
pub fn normalize_into<'a>(
tokens: &'a [Token],
literals: LiteralNorm,
out: &mut Vec<NormToken<'a>>,
) {
normalize_resolved_into(tokens, literals, None, out);
}
pub fn normalize_resolved_into<'a>(
tokens: &'a [Token],
literals: LiteralNorm,
resolved: Option<&Resolution>,
out: &mut Vec<NormToken<'a>>,
) {
out.clear();
let mut names: BTreeMap<&str, u32> = BTreeMap::new();
out.extend(tokens.iter().enumerate().map(|(i, token)| {
let atom = match token.kind {
TokenKind::Identifier if is_preserved(tokens, i, resolved) => {
NormAtom::Text(&token.text)
}
TokenKind::Identifier | TokenKind::Lifetime => {
let next = u32::try_from(names.len()).unwrap_or(u32::MAX);
let n = *names.entry(token.text.as_str()).or_insert(next);
NormAtom::Renamed(n)
}
TokenKind::Literal(kind) => match literals {
LiteralNorm::Preserve => NormAtom::Text(&token.text),
mode => NormAtom::Literal(literal_class(kind, mode)),
},
_ => NormAtom::Text(&token.text),
};
NormToken {
tag: token.kind.tag(),
atom,
}
}));
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::frontend::SourceSpan;
fn toks(spec: &[(TokenKind, &str)]) -> Vec<Token> {
spec.iter()
.map(|(kind, text)| Token {
kind: *kind,
text: (*text).into(),
span: SourceSpan {
start_byte: 0,
end_byte: 0,
start_line: 1,
start_column: 1,
},
})
.collect()
}
use TokenKind::{Identifier as Id, Keyword as Kw, Punctuation as Pu};
const INT: TokenKind = TokenKind::Literal(LiteralKind::Integer);
const FLT: TokenKind = TokenKind::Literal(LiteralKind::Float);
#[test]
fn consistent_renames_normalize_equal() {
let a = toks(&[
(Kw, "let"),
(Id, "total"),
(Pu, "="),
(Id, "a"),
(Pu, "+"),
(Id, "b"),
(Pu, ";"),
(Id, "total"),
]);
let b = toks(&[
(Kw, "let"),
(Id, "sum"),
(Pu, "="),
(Id, "x"),
(Pu, "+"),
(Id, "y"),
(Pu, ";"),
(Id, "sum"),
]);
assert_eq!(
normalize(&a, LiteralNorm::Full),
normalize(&b, LiteralNorm::Full)
);
}
#[test]
fn inconsistent_renames_do_not_normalize_equal() {
let a = toks(&[(Id, "a"), (Pu, "+"), (Id, "a"), (Pu, "+"), (Id, "b")]);
let b = toks(&[(Id, "x"), (Pu, "+"), (Id, "y"), (Pu, "+"), (Id, "y")]);
assert_ne!(
normalize(&a, LiteralNorm::Full),
normalize(&b, LiteralNorm::Full)
);
}
#[test]
fn literal_modes_control_literal_equality() {
let a = toks(&[(Id, "x"), (Pu, "+"), (INT, "1")]);
let b = toks(&[(Id, "y"), (Pu, "+"), (INT, "2")]);
let c = toks(&[(Id, "z"), (Pu, "+"), (FLT, "2.0")]);
assert_eq!(
normalize(&a, LiteralNorm::Full),
normalize(&b, LiteralNorm::Full)
);
assert_eq!(
normalize(&a, LiteralNorm::Full),
normalize(&c, LiteralNorm::Full)
);
assert_eq!(
normalize(&a, LiteralNorm::Category),
normalize(&b, LiteralNorm::Category)
);
assert_ne!(
normalize(&a, LiteralNorm::Category),
normalize(&c, LiteralNorm::Category)
);
assert_ne!(
normalize(&a, LiteralNorm::Preserve),
normalize(&b, LiteralNorm::Preserve)
);
}
#[test]
fn method_and_path_names_are_preserved() {
let len_a = toks(&[(Id, "foo"), (Pu, "."), (Id, "len"), (Pu, "("), (Pu, ")")]);
let len_b = toks(&[(Id, "bar"), (Pu, "."), (Id, "len"), (Pu, "("), (Pu, ")")]);
assert_eq!(
normalize(&len_a, LiteralNorm::Full),
normalize(&len_b, LiteralNorm::Full)
);
let count = toks(&[(Id, "foo"), (Pu, "."), (Id, "count"), (Pu, "("), (Pu, ")")]);
assert_ne!(
normalize(&len_a, LiteralNorm::Full),
normalize(&count, LiteralNorm::Full)
);
let swap = toks(&[
(Id, "std"),
(Pu, "::"),
(Id, "mem"),
(Pu, "::"),
(Id, "swap"),
]);
let take = toks(&[
(Id, "std"),
(Pu, "::"),
(Id, "mem"),
(Pu, "::"),
(Id, "take"),
]);
assert_ne!(
normalize(&swap, LiteralNorm::Full),
normalize(&take, LiteralNorm::Full)
);
}
#[test]
fn arrow_member_names_are_preserved() {
let a = toks(&[(Id, "p"), (Pu, "->"), (Id, "next")]);
let b = toks(&[(Id, "q"), (Pu, "->"), (Id, "next")]);
assert_eq!(
normalize(&a, LiteralNorm::Full),
normalize(&b, LiteralNorm::Full)
);
let c = toks(&[(Id, "p"), (Pu, "->"), (Id, "prev")]);
assert_ne!(
normalize(&a, LiteralNorm::Full),
normalize(&c, LiteralNorm::Full)
);
}
#[test]
fn macro_names_and_uppercase_names_are_preserved() {
let a = toks(&[(Id, "println"), (Pu, "!"), (Pu, "("), (Pu, ")")]);
let b = toks(&[(Id, "eprintln"), (Pu, "!"), (Pu, "("), (Pu, ")")]);
assert_ne!(
normalize(&a, LiteralNorm::Full),
normalize(&b, LiteralNorm::Full)
);
let c = toks(&[(Id, "Some"), (Pu, "("), (Id, "x"), (Pu, ")")]);
let d = toks(&[(Id, "Ok"), (Pu, "("), (Id, "x"), (Pu, ")")]);
assert_ne!(
normalize(&c, LiteralNorm::Full),
normalize(&d, LiteralNorm::Full)
);
}
#[test]
fn normal_form_is_context_independent() {
let host_a = toks(&[
(Id, "extra"),
(Pu, ";"),
(Kw, "let"),
(Id, "v"),
(Pu, "="),
(INT, "1"),
(Pu, ";"),
]);
let host_b = toks(&[
(Id, "p"),
(Pu, "+"),
(Id, "q"),
(Pu, ";"),
(Kw, "let"),
(Id, "v"),
(Pu, "="),
(INT, "1"),
(Pu, ";"),
]);
let a = normalize(&host_a[2..], LiteralNorm::Full);
let b = normalize(&host_b[4..], LiteralNorm::Full);
assert_eq!(a, b);
}
#[test]
fn keywords_and_punctuation_pass_through() {
let a = toks(&[(Kw, "return"), (Pu, ";")]);
let n = normalize(&a, LiteralNorm::Full);
assert_eq!(n[0].atom, NormAtom::Text("return"));
assert_eq!(n[1].atom, NormAtom::Text(";"));
}
fn placed(spec: &[(TokenKind, &str)]) -> Vec<Token> {
let mut at = 0;
spec.iter()
.map(|(kind, text)| {
let start = at;
at += text.len() + 1;
Token {
kind: *kind,
text: (*text).into(),
span: SourceSpan {
start_byte: start,
end_byte: start + text.len(),
start_line: 1,
start_column: u32::try_from(start).unwrap() + 1,
},
}
})
.collect()
}
fn normalized<'a>(tokens: &'a [Token], resolved: Option<&Resolution>) -> Vec<NormToken<'a>> {
let mut out = Vec::new();
normalize_resolved_into(tokens, LiteralNorm::Full, resolved, &mut out);
out
}
#[test]
fn a_name_the_rules_would_rename_is_preserved_when_it_is_resolved_external() {
let tokens = placed(&[(Id, "encode"), (Pu, "("), (Id, "value"), (Pu, ")")]);
assert_eq!(normalized(&tokens, None)[0].atom, NormAtom::Renamed(0));
let mut resolution = Resolution::new();
resolution.insert(tokens[0].span.start_byte, true);
let resolved = normalized(&tokens, Some(&resolution));
assert_eq!(resolved[0].atom, NormAtom::Text("encode"));
assert_eq!(resolved[2].atom, NormAtom::Renamed(0));
}
#[test]
fn a_name_the_rules_would_preserve_is_renamed_when_it_is_resolved_local() {
let tokens = placed(&[(Id, "Buffer"), (Pu, "."), (Id, "len")]);
let guessed = normalized(&tokens, None);
assert_eq!(guessed[0].atom, NormAtom::Text("Buffer"));
assert_eq!(guessed[2].atom, NormAtom::Text("len"));
let mut resolution = Resolution::new();
resolution.insert(tokens[0].span.start_byte, false);
let resolved = normalized(&tokens, Some(&resolution));
assert_eq!(resolved[0].atom, NormAtom::Renamed(0));
assert_eq!(resolved[2].atom, NormAtom::Text("len"));
}
#[test]
fn two_fragments_resolved_alike_normalize_alike() {
let a = placed(&[(Id, "node"), (Pu, "."), (Id, "next")]);
let b = placed(&[(Id, "node"), (Pu, "."), (Id, "prev")]);
assert_ne!(normalized(&a, None), normalized(&b, None));
let mut ra = Resolution::new();
ra.insert(a[2].span.start_byte, false);
let mut rb = Resolution::new();
rb.insert(b[2].span.start_byte, false);
assert_eq!(normalized(&a, Some(&ra)), normalized(&b, Some(&rb)));
}
#[test]
fn an_empty_resolution_normalizes_exactly_as_no_resolution_does() {
let tokens = placed(&[(Id, "Value"), (Pu, "::"), (Id, "from"), (Id, "x")]);
let empty = Resolution::new();
assert!(empty.is_empty());
assert_eq!(normalized(&tokens, Some(&empty)), normalized(&tokens, None));
}
#[test]
fn resolving_one_name_twice_keeps_the_later_answer() {
let tokens = placed(&[(Id, "encode")]);
let mut resolution = Resolution::new();
resolution.insert(tokens[0].span.start_byte, true);
resolution.insert(tokens[0].span.start_byte, false);
assert_eq!(
normalized(&tokens, Some(&resolution))[0].atom,
NormAtom::Renamed(0)
);
}
}