Skip to main content

codehelion_core/
substitution.rs

1//! What normalization erased: which name became which.
2//!
3//! Type-2 detection works by not looking at identifiers, so two occurrences
4//! that differ only in their names come out equal. That is the right thing for
5//! finding the duplication and the wrong thing for judging it, because the
6//! commonest false positive in a typed language is a set of routines the
7//! language forced apart — one per integer width, one per float width — and
8//! the only thing that says so is the very substitution normalization threw
9//! away.
10//!
11//! This keeps it. A [`Witness`] is the list of name changes that turn one
12//! occurrence into the other, with the two questions worth asking of them
13//! already answered: whether every change is the same width being swapped for
14//! another, and whether any of them changed a literal rather than a name.
15//!
16//! # How the two occurrences are lined up
17//!
18//! By the very thing normalization kept. Two tokens may stand in the same place
19//! when they are the same kind — [`TokenKind::tag`] decides that, and it is the
20//! detector's own answer to what counts as the same token once the spelling is
21//! gone. So the alignment is computed over the normalized run and the
22//! substitutions are read off the raw one: exactly the two halves of what a
23//! Type-2 match is.
24//!
25//! Where an occurrence has tokens the other does not, those are edits rather
26//! than substitutions, and [`Witness::edits`] counts them. Nothing here decides
27//! what an edit means; a rule reading a witness does.
28//!
29//! # What it does not do
30//!
31//! It gives up on a pair too large to align, because the alignment is quadratic
32//! and a bound that is never hit is a bound nobody has tested. Being unable to
33//! say is recorded as [`None`], never as an empty witness.
34
35use crate::frontend::{LiteralKind, Token, TokenKind};
36
37/// Version of the witness rules, for recording alongside the other detector
38/// versions when something acts on one.
39pub const SUBSTITUTION_VERSION: &str = "substitution-v1";
40
41/// Largest product of the two token counts an alignment is computed for.
42///
43/// The table is one `u32` per cell, so this is four megabytes at the top end,
44/// spent on a pair of four-hundred-line bodies. Above it the answer is that
45/// nobody looked, which is what [`None`] says.
46const ALIGNMENT_LIMIT: usize = 1 << 20;
47
48/// The integer widths a type is spelled with.
49///
50/// Written as digits rather than as type names on purpose. A list of type
51/// spellings is a list per language, out of date the moment a project defines
52/// `U32`; the widths are the same three-or-so tokens everywhere, and they are
53/// what the spellings are built out of — `u32`, `int32_t`, `XXH32_hash_t`.
54const WIDTHS: [&str; 5] = ["8", "16", "32", "64", "128"];
55
56/// One name replaced by another.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct Change {
59    /// The text on the left-hand occurrence.
60    pub from: String,
61    /// The text on the right-hand occurrence.
62    pub to: String,
63    /// Whether the token that changed was a literal rather than a name.
64    pub literal: bool,
65}
66
67/// The substitutions that turn one occurrence into another.
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
69pub struct Witness {
70    /// Each distinct change, in the order first seen.
71    pub changes: Vec<Change>,
72    /// Tokens on either side that had no counterpart in the other.
73    ///
74    /// A substitution is one token standing where another stood. These are the
75    /// rest: the statement one occurrence has and the other does not, the cast
76    /// added on one side only. Counted rather than described, because a rule
77    /// over a witness cares whether the two bodies do the same work, and that
78    /// question is answered by there being none of these.
79    pub edits: usize,
80}
81
82impl Witness {
83    /// The one width swap that explains every change, if there is one.
84    ///
85    /// `Some(("32", "64"))` says every token that differs does so by having
86    /// that width in place of the other, everywhere it occurs: `u32`/`u64`,
87    /// `read32`/`read64`, `XXH32_hash_t`/`XXH64_hash_t`. That is what a set of
88    /// routines written once per width looks like from the outside, and it is
89    /// not what a copied function looks like — a copy renamed by hand changes
90    /// names that have nothing to do with each other.
91    ///
92    /// `None` when the changes are not all one swap, and for a witness with no
93    /// changes at all: two occurrences that differ in nothing were not written
94    /// one per width, they are the same text.
95    #[must_use]
96    pub fn one_width_apart(&self) -> Option<(&'static str, &'static str)> {
97        if self.changes.is_empty() {
98            return None;
99        }
100        WIDTHS
101            .into_iter()
102            .flat_map(|from| WIDTHS.into_iter().map(move |to| (from, to)))
103            .find(|&(from, to)| {
104                from != to
105                    && self.changes.iter().all(|change| {
106                        change.from.contains(from) && change.from.replace(from, to) == change.to
107                    })
108            })
109    }
110
111    /// Whether these two occurrences read as one routine written once per
112    /// width.
113    ///
114    /// The two questions above, asked together, which is the only way either is
115    /// worth asking. Kept here rather than at each call site so the rule the
116    /// engine acts on and the rule the corpus is measured against cannot drift
117    /// apart.
118    ///
119    /// Nothing is asked about [`Self::edits`]: a routine written for the wider
120    /// type does work the narrower one has no need of, and bounding that would
121    /// mean choosing a number no measurement over real code has supported.
122    #[must_use]
123    pub fn written_once_per_width(&self) -> bool {
124        self.one_width_apart().is_some() && !self.touches_a_literal()
125    }
126
127    /// Whether any change replaced a literal.
128    ///
129    /// A changed constant is a changed answer. Two bodies alike but for the
130    /// number they compare against are two decisions, however alike they read,
131    /// so no rule that sets duplication aside should reach them.
132    #[must_use]
133    pub fn touches_a_literal(&self) -> bool {
134        self.changes.iter().any(|change| change.literal)
135    }
136}
137
138/// The substitutions turning `left` into `right`, or `None` when the pair is
139/// too large to align.
140///
141/// The alignment is the one that pairs off the most tokens, counting an
142/// identical token for twice what a merely same-kind one is worth, so a name
143/// that did not change is never passed over in favour of one that did. Tokens
144/// left unpaired are counted in [`Witness::edits`]; paired tokens whose text
145/// differs are the substitutions.
146#[must_use]
147pub fn witness(left: &[Token], right: &[Token]) -> Option<Witness> {
148    let (rows, columns) = (left.len(), right.len());
149    if rows.checked_mul(columns)? > ALIGNMENT_LIMIT {
150        return None;
151    }
152    let stride = columns + 1;
153
154    // score[i][j] is the best total over the suffixes starting at i and j.
155    // Filled backwards so the traceback can read it forwards.
156    let mut score = vec![0u32; (rows + 1) * stride];
157    for i in (0..rows).rev() {
158        for j in (0..columns).rev() {
159            let paired = pairing(&left[i], &right[j]);
160            let best = score[(i + 1) * stride + j].max(score[i * stride + j + 1]);
161            score[i * stride + j] = if paired > 0 {
162                best.max(score[(i + 1) * stride + j + 1] + paired)
163            } else {
164                best
165            };
166        }
167    }
168
169    let mut changes: Vec<Change> = Vec::new();
170    let mut edits = 0usize;
171    let (mut i, mut j) = (0usize, 0usize);
172    while i < rows && j < columns {
173        let paired = pairing(&left[i], &right[j]);
174        let here = score[i * stride + j];
175        if paired > 0 && here == score[(i + 1) * stride + j + 1] + paired {
176            note(&mut changes, &left[i], &right[j]);
177            i += 1;
178            j += 1;
179        } else if here == score[(i + 1) * stride + j] {
180            i += 1;
181            edits += 1;
182        } else {
183            j += 1;
184            edits += 1;
185        }
186    }
187    // Whatever either run has left over was paired with nothing.
188    edits += (rows - i) + (columns - j);
189    Some(Witness { changes, edits })
190}
191
192/// What pairing these two tokens is worth: nothing unless they are the same
193/// kind, and one more when they are also the same text.
194///
195/// The two numbers are three and two rather than two and one so that the count
196/// of pairings decides first and the identical text only breaks a tie: two
197/// same-kind pairings are worth four and one identical pairing three, so
198/// nothing is ever left unpaired to keep a name intact. That order is the
199/// careful one. Reading a differing name as a substitution puts one more
200/// change in front of a rule that has to explain every one of them, where
201/// reading it as an insertion beside a deletion puts none.
202fn pairing(left: &Token, right: &Token) -> u32 {
203    if left.kind.tag() != right.kind.tag() {
204        0
205    } else if left.text == right.text {
206        3
207    } else {
208        2
209    }
210}
211
212/// Record the change between two paired tokens, unless they are the same text
213/// or the change is already known.
214fn note(changes: &mut Vec<Change>, from: &Token, to: &Token) {
215    if from.text == to.text {
216        return;
217    }
218    let change = Change {
219        from: from.text.to_string(),
220        to: to.text.to_string(),
221        literal: is_literal(from.kind) || is_literal(to.kind),
222    };
223    if !changes.contains(&change) {
224        changes.push(change);
225    }
226}
227
228const fn is_literal(kind: TokenKind) -> bool {
229    matches!(
230        kind,
231        TokenKind::Literal(
232            LiteralKind::Integer
233                | LiteralKind::Float
234                | LiteralKind::String
235                | LiteralKind::Char
236                | LiteralKind::Bool
237        )
238    )
239}
240
241#[cfg(test)]
242#[allow(clippy::unwrap_used, clippy::expect_used)]
243mod tests {
244    use super::*;
245    use crate::frontend::SourceSpan;
246
247    fn tokens(spec: &[(TokenKind, &str)]) -> Vec<Token> {
248        spec.iter()
249            .map(|&(kind, text)| Token {
250                kind,
251                text: text.into(),
252                span: SourceSpan {
253                    start_byte: 0,
254                    end_byte: text.len(),
255                    start_line: 1,
256                    start_column: 1,
257                },
258            })
259            .collect()
260    }
261
262    fn name(text: &str) -> (TokenKind, &str) {
263        (TokenKind::Identifier, text)
264    }
265
266    fn number(text: &str) -> (TokenKind, &str) {
267        (TokenKind::Literal(LiteralKind::Integer), text)
268    }
269
270    #[test]
271    fn a_token_with_no_counterpart_is_an_edit_and_not_a_change() {
272        let left = tokens(&[name("a")]);
273        let right = tokens(&[name("a"), name("b")]);
274        let witness = witness(&left, &right).unwrap();
275        assert!(witness.changes.is_empty(), "nothing was substituted");
276        assert_eq!(witness.edits, 1);
277    }
278
279    #[test]
280    fn a_pair_too_large_to_align_has_no_witness() {
281        let long = tokens(&vec![name("a"); 1100]);
282        assert_eq!(witness(&long, &long), None);
283    }
284
285    #[test]
286    fn identical_runs_witness_no_change() {
287        let left = tokens(&[name("a"), name("b")]);
288        let witness = witness(&left, &left).unwrap();
289        assert!(witness.changes.is_empty());
290        assert_eq!(witness.edits, 0);
291        // No change is not one width apart: they are the same text, which is
292        // what a verbatim copy is.
293        assert_eq!(witness.one_width_apart(), None);
294    }
295
296    #[test]
297    fn two_runs_of_one_shape_are_read_as_substitutions_throughout() {
298        // Keeping the `b` would mean leaving a token unpaired on each side. The
299        // alignment does not: same shape, both names changed, which is the
300        // reading a rule over the changes has to answer for.
301        let left = tokens(&[name("b"), name("x")]);
302        let right = tokens(&[name("y"), name("b")]);
303        let witness = witness(&left, &right).unwrap();
304        assert_eq!(witness.edits, 0);
305        assert_eq!(witness.changes.len(), 2);
306    }
307
308    #[test]
309    fn an_identical_name_settles_which_of_two_alignments_is_read() {
310        // Either `b` or `c` can be dropped to line these up. Dropping `b`
311        // leaves `c` against `c`; dropping `c` leaves `b` against `c` and
312        // invents a substitution nobody wrote.
313        let left = tokens(&[name("a"), name("b"), name("c")]);
314        let right = tokens(&[name("a"), name("c")]);
315        let witness = witness(&left, &right).unwrap();
316        assert!(witness.changes.is_empty());
317        assert_eq!(witness.edits, 1);
318    }
319
320    #[test]
321    fn a_width_swap_survives_an_edit_beside_it() {
322        // The 64-bit routine has a step the 32-bit one does not. The names are
323        // still one width apart; the extra work is an edit, and what that is
324        // worth is not this module's question.
325        let left = tokens(&[name("U32"), name("read32")]);
326        let right = tokens(&[name("U64"), name("read64"), name("finalize")]);
327        let witness = witness(&left, &right).unwrap();
328        assert_eq!(witness.one_width_apart(), Some(("32", "64")));
329        assert_eq!(witness.edits, 1);
330    }
331
332    #[test]
333    fn a_kind_that_differs_is_not_paired_off() {
334        let left = tokens(&[name("value")]);
335        let right = tokens(&[number("7")]);
336        let witness = witness(&left, &right).unwrap();
337        assert!(witness.changes.is_empty(), "a name did not become a number");
338        assert_eq!(witness.edits, 2);
339    }
340
341    #[test]
342    fn a_change_is_recorded_once_however_often_it_recurs() {
343        let left = tokens(&[name("u32"), name("u32"), name("u32")]);
344        let right = tokens(&[name("u64"), name("u64"), name("u64")]);
345        let witness = witness(&left, &right).unwrap();
346        assert_eq!(witness.changes.len(), 1);
347    }
348
349    #[test]
350    fn one_width_everywhere_is_recognised() {
351        // `U32 read32(...)` against `U64 read64(...)`: the whole difference is
352        // the width, in the type and in the names built from it.
353        let left = tokens(&[name("U32"), name("XXH_read32"), name("XXH_swap32")]);
354        let right = tokens(&[name("U64"), name("XXH_read64"), name("XXH_swap64")]);
355        assert_eq!(
356            witness(&left, &right).unwrap().one_width_apart(),
357            Some(("32", "64"))
358        );
359    }
360
361    #[test]
362    fn a_rename_that_is_not_a_width_is_not_one() {
363        // A function copied between two amalgamated libraries: one systematic
364        // rename, and nothing to do with a type.
365        let left = tokens(&[name("LZ4_isLittleEndian")]);
366        let right = tokens(&[name("XXH_isLittleEndian")]);
367        assert_eq!(witness(&left, &right).unwrap().one_width_apart(), None);
368    }
369
370    #[test]
371    fn a_width_beside_a_rename_that_is_not_one_is_not_one_either() {
372        // `long`/`short` alongside `m8Index`/`m4Index`: digits change, but not
373        // every change is that digit, so the pair was not written per width.
374        let left = tokens(&[name("long"), name("m8Index")]);
375        let right = tokens(&[name("short"), name("m4Index")]);
376        assert_eq!(witness(&left, &right).unwrap().one_width_apart(), None);
377    }
378
379    #[test]
380    fn a_changed_constant_is_visible_as_one() {
381        let left = tokens(&[name("wait"), number("24")]);
382        let right = tokens(&[name("wait"), number("1")]);
383        assert!(witness(&left, &right).unwrap().touches_a_literal());
384    }
385
386    #[test]
387    fn a_constant_that_reads_like_a_width_is_still_a_constant() {
388        // The digits alone do not say a type was involved. Two bodies that
389        // differ in a number are two answers, and a rule that sets width
390        // families aside has to be able to tell that apart from a type name.
391        let left = tokens(&[number("32")]);
392        let right = tokens(&[number("64")]);
393        let witness = witness(&left, &right).unwrap();
394        assert_eq!(witness.one_width_apart(), Some(("32", "64")));
395        assert!(witness.touches_a_literal());
396    }
397}