Skip to main content

syn_canon/
form.rs

1//! Framed token identity and size metrics.
2
3use alloc::collections::BTreeMap;
4use alloc::string::{String, ToString};
5use alloc::vec::Vec;
6use core::fmt::Write;
7use proc_macro2::{Group, Ident, Spacing, TokenStream, TokenTree};
8use quote::ToTokens;
9use syn::visit::{self, Visit};
10use syn::visit_mut::{self, VisitMut};
11
12use crate::reference::{ReferenceFacts, ReferenceKind, Resolution};
13use crate::scope::PrimTy;
14
15/// The label prefix of an owner parameter in a method view, followed by its role or number.
16pub(crate) const OWNER_PARAMETER: &str = "__dejadoc_owner_param_";
17
18/// An opaque comparison key with canonical leaf and body sizes.
19pub struct CanonicalForm {
20    key: String,
21    /// The key with owner parameters labelled by role, when the view uses an owner parameter.
22    owner_roles: Option<String>,
23    leaf_tokens: usize,
24    body_units: usize,
25}
26
27impl CanonicalForm {
28    pub(crate) fn from_tokens(tokens: TokenStream, body_units: usize) -> Self {
29        let mut key = String::from("K1 S ");
30        let mut leaf_tokens = 0;
31        let mut scratch = String::new();
32        for tree in tokens {
33            framed_tree(&mut key, tree, &mut leaf_tokens, &mut scratch);
34        }
35        key.push('E');
36        Self {
37            key,
38            owner_roles: None,
39            leaf_tokens,
40            body_units,
41        }
42    }
43
44    /// The opaque comparison key.
45    #[must_use]
46    pub fn key(&self) -> &str {
47        &self.key
48    }
49
50    /// The key of a method view with its owner's parameters labelled by their role in the owner
51    /// type, when the view uses one. `key` numbers them by first use instead.
52    #[must_use]
53    pub fn owner_role_key(&self) -> Option<&str> {
54        self.owner_roles.as_deref()
55    }
56
57    /// The owned comparison key.
58    #[must_use]
59    pub fn into_key(self) -> String {
60        self.key
61    }
62
63    /// Canonical leaf tokens, excluding framing.
64    #[must_use]
65    pub fn leaf_tokens(&self) -> usize {
66        self.leaf_tokens
67    }
68
69    /// Canonical body units, excluding framing.
70    #[must_use]
71    pub fn body_units(&self) -> usize {
72        self.body_units
73    }
74}
75
76pub(crate) trait CanonicalNode: ToTokens {
77    fn visit<'ast>(&'ast self, visitor: &mut impl Visit<'ast>);
78
79    fn canonical_body_units(&self, tokens: &TokenStream) -> usize {
80        body_units(tokens.clone())
81    }
82}
83
84impl CanonicalNode for syn::File {
85    fn visit<'ast>(&'ast self, visitor: &mut impl Visit<'ast>) {
86        visitor.visit_file(self);
87    }
88
89    fn canonical_body_units(&self, tokens: &TokenStream) -> usize {
90        if !matches!(self.items.as_slice(), [syn::Item::Fn(_)]) {
91            return 0;
92        }
93        match tokens.clone().into_iter().last() {
94            Some(TokenTree::Group(body)) => body_units(body.stream()),
95            _ => 0,
96        }
97    }
98}
99
100impl CanonicalNode for syn::Block {
101    fn visit<'ast>(&'ast self, visitor: &mut impl Visit<'ast>) {
102        visitor.visit_block(self);
103    }
104}
105
106impl CanonicalNode for syn::Expr {
107    fn visit<'ast>(&'ast self, visitor: &mut impl Visit<'ast>) {
108        visitor.visit_expr(self);
109    }
110}
111
112impl CanonicalNode for syn::Arm {
113    fn visit<'ast>(&'ast self, visitor: &mut impl Visit<'ast>) {
114        visitor.visit_arm(self);
115    }
116}
117
118pub(crate) fn canonical_file_with_options(
119    file: syn::File,
120    compiles: bool,
121    options: crate::alpha::Options<'_>,
122) -> CanonicalForm {
123    canonical_file(file, compiles, options, true).0
124}
125
126/// The original proof backing of one item's comparison projection.
127pub(crate) struct PayloadProof<'a> {
128    pub(crate) table: Option<&'a crate::helpers::HelperTable>,
129    /// The scopes around the item leave every std formatting macro name bound to the std macro.
130    pub(crate) std_names: bool,
131    pub(crate) frame: &'a crate::scope::Frame,
132    pub(crate) syntax: crate::helpers::HelperSyntax,
133    pub(crate) edges: MarkerEdges,
134}
135
136/// The per-occurrence group references of one item's marker file, each
137/// substitution keeping the origin it replaced.
138pub(crate) struct MarkerEdges {
139    idents: crate::reference::MarkerOrigins<proc_macro2::Ident>,
140    macros: crate::reference::MarkerMacroOrigins,
141}
142
143impl MarkerEdges {
144    pub(crate) fn new() -> Self {
145        Self {
146            idents: alloc::collections::BTreeMap::new(),
147            macros: alloc::collections::BTreeMap::new(),
148        }
149    }
150
151    pub(crate) fn record_ident(
152        &mut self,
153        ident: &proc_macro2::Ident,
154        ns: crate::scope::Ns,
155        slot: usize,
156    ) {
157        self.idents.insert(core::ptr::from_ref(ident), (ns, slot));
158    }
159
160    pub(crate) fn record_macro(
161        &mut self,
162        mac: &syn::Macro,
163        occurrences: Vec<Option<(crate::scope::Ns, usize)>>,
164    ) {
165        let Some(first) = mac.path.segments.first() else {
166            return;
167        };
168        self.macros.insert(core::ptr::from_ref(first), occurrences);
169    }
170
171    pub(crate) fn into_parts(
172        self,
173    ) -> (
174        crate::reference::MarkerOrigins<proc_macro2::Ident>,
175        crate::reference::MarkerMacroOrigins,
176    ) {
177        (self.idents, self.macros)
178    }
179}
180
181struct Walker<'a> {
182    facts: &'a ReferenceFacts,
183    out: &'a mut Vec<(crate::scope::Ns, usize)>,
184}
185
186impl<'ast> Visit<'ast> for Walker<'_> {
187    fn visit_path(&mut self, path: &'ast syn::Path) {
188        if let Some(last) = path.segments.last()
189            && let Some((ns, slot)) = self.facts.marker_paths.get(&core::ptr::from_ref(last))
190        {
191            self.out.push((*ns, *slot));
192        }
193        visit::visit_path(self, path);
194    }
195
196    fn visit_macro(&mut self, mac: &'ast syn::Macro) {
197        if let Some(first) = mac.path.segments.first()
198            && let Some(records) = self.facts.marker_macros.get(&core::ptr::from_ref(first))
199        {
200            for record in records.iter().flatten() {
201                self.out.push(*record);
202            }
203        }
204        visit::visit_macro(self, mac);
205    }
206}
207
208pub(crate) fn item_payload(
209    file: syn::File,
210    proof: PayloadProof<'_>,
211) -> (CanonicalForm, Vec<(crate::scope::Ns, usize)>) {
212    let mut syntax = proof.syntax;
213    let (idents, macros) = proof.edges.into_parts();
214    syntax.ingest_marker_edges(&file, idents, macros);
215    let (form, normalized, facts) = canonical_file(
216        file,
217        true,
218        crate::alpha::Options {
219            seed: Some(proof.frame),
220            generics: None,
221            self_owner: None,
222            prim_fallback: true,
223            observed: false,
224            enclosing_observed: false,
225            std_names: proof.std_names,
226            prelude: false,
227            owned_methods: false,
228            clone_free: false,
229            collection_free: false,
230            helper_table: proof.table,
231            helper_module: &[],
232            helper_blocked: false,
233            helper_syntax: Some(syntax),
234        },
235        false,
236    );
237    let mut edges = Vec::new();
238    let mut walker = Walker {
239        facts: &facts,
240        out: &mut edges,
241    };
242    visit::visit_file(&mut walker, &normalized);
243    (form, edges)
244}
245
246fn canonical_file(
247    mut file: syn::File,
248    compiles: bool,
249    mut options: crate::alpha::Options<'_>,
250    items: bool,
251) -> (CanonicalForm, syn::File, ReferenceFacts) {
252    let owner = options.generics.is_some();
253    if owner {
254        ReservedLabels.visit_file_mut(&mut file);
255    }
256    let helpers = (compiles && items && options.helper_table.is_none())
257        .then(|| crate::helpers::module_table(&crate::helpers::Scopes::from_file(&file)));
258    let helper_table = options.helper_table.or(helpers.as_ref());
259    let mut helper_syntax = options.helper_syntax.take().unwrap_or_else(|| {
260        helper_table.map_or_else(crate::helpers::HelperSyntax::default, |table| {
261            crate::helpers::HelperSyntax::from_file(table, &mut file)
262        })
263    });
264    if compiles && items {
265        crate::items::normalize(
266            &mut file,
267            &options,
268            &mut helper_syntax,
269            helper_table.expect("items normalization has a helper table"),
270        );
271    }
272    crate::drift::normalize_file(
273        &mut file,
274        compiles,
275        &mut helper_syntax,
276        options.enclosing_observed,
277        options.std_names,
278    );
279    let options = crate::alpha::Options {
280        helper_table,
281        helper_syntax: Some(helper_syntax),
282        ..options
283    };
284    let facts = crate::alpha::normalize_file(&mut file, compiles, options);
285    (from_node_with(&file, &facts, owner), file, facts)
286}
287
288pub(crate) fn from_node(node: &impl CanonicalNode, facts: &ReferenceFacts) -> CanonicalForm {
289    from_node_with(node, facts, false)
290}
291
292/// The comparison form of `node`. With `owner`, the key numbers owner parameters by first
293/// appearance, and the role-labelled tokens give the owner role key.
294fn from_node_with(node: &impl CanonicalNode, facts: &ReferenceFacts, owner: bool) -> CanonicalForm {
295    let tokens: TokenStream =
296        crate::fold_tokens(node.to_token_stream(), facts.observed, facts.observed)
297            .into_iter()
298            .collect();
299    let units = node.canonical_body_units(&tokens);
300    let mut tail = String::from("R ");
301    node.visit(&mut ReferenceEncoding {
302        out: &mut tail,
303        facts,
304    });
305    tail.push('E');
306    let finish = |tokens: TokenStream| {
307        let mut form = CanonicalForm::from_tokens(tokens, units);
308        form.key.pop();
309        form.key.push_str(&tail);
310        form
311    };
312    if !owner {
313        return finish(tokens);
314    }
315    let mut ranks = BTreeMap::new();
316    let numbered = number_owner_parameters(tokens.clone(), &mut ranks);
317    if ranks.is_empty() {
318        return finish(tokens);
319    }
320    let roles = finish(tokens);
321    let mut form = finish(numbered);
322    form.owner_roles = Some(roles.key);
323    form
324}
325
326/// Rename every owner parameter label in `tokens` to its rank among the labels seen so far.
327fn number_owner_parameters(
328    tokens: TokenStream,
329    ranks: &mut BTreeMap<String, usize>,
330) -> TokenStream {
331    tokens
332        .into_iter()
333        .map(|tree| match tree {
334            TokenTree::Ident(ident) if ident.to_string().starts_with(OWNER_PARAMETER) => {
335                let next = ranks.len();
336                let rank = *ranks.entry(ident.to_string()).or_insert(next);
337                TokenTree::Ident(Ident::new(
338                    &alloc::format!("{OWNER_PARAMETER}{rank}"),
339                    ident.span(),
340                ))
341            }
342            TokenTree::Group(group) => {
343                let mut renamed = Group::new(
344                    group.delimiter(),
345                    number_owner_parameters(group.stream(), ranks),
346                );
347                renamed.set_span(group.span());
348                TokenTree::Group(renamed)
349            }
350            other => other,
351        })
352        .collect()
353}
354
355/// The prefix that keeps a source identifier apart from every generated owner parameter label.
356const ESCAPED: &str = "__dejadoc_escaped_";
357
358/// Respells source identifiers that start with [`OWNER_PARAMETER`] or [`ESCAPED`] under
359/// [`ESCAPED`], macro and attribute tokens included, so every owner parameter label left after
360/// renaming comes from an owner parameter. Raw identifiers keep their `r#` and never read as
361/// labels.
362struct ReservedLabels;
363
364impl ReservedLabels {
365    fn ident(ident: &Ident) -> Option<Ident> {
366        let name = ident.to_string();
367        (name.starts_with(OWNER_PARAMETER) || name.starts_with(ESCAPED))
368            .then(|| Ident::new(&alloc::format!("{ESCAPED}{name}"), ident.span()))
369    }
370
371    fn tokens(tokens: TokenStream) -> TokenStream {
372        tokens
373            .into_iter()
374            .map(|tree| match tree {
375                TokenTree::Ident(ident) => TokenTree::Ident(Self::ident(&ident).unwrap_or(ident)),
376                TokenTree::Group(group) => {
377                    let mut escaped = Group::new(group.delimiter(), Self::tokens(group.stream()));
378                    escaped.set_span(group.span());
379                    TokenTree::Group(escaped)
380                }
381                other => other,
382            })
383            .collect()
384    }
385}
386
387impl VisitMut for ReservedLabels {
388    fn visit_ident_mut(&mut self, ident: &mut Ident) {
389        if let Some(escaped) = Self::ident(ident) {
390            *ident = escaped;
391        }
392    }
393
394    fn visit_macro_mut(&mut self, mac: &mut syn::Macro) {
395        visit_mut::visit_macro_mut(self, mac);
396        mac.tokens = Self::tokens(core::mem::take(&mut mac.tokens));
397    }
398
399    fn visit_meta_list_mut(&mut self, list: &mut syn::MetaList) {
400        visit_mut::visit_meta_list_mut(self, list);
401        list.tokens = Self::tokens(core::mem::take(&mut list.tokens));
402    }
403
404    fn visit_expr_mut(&mut self, expr: &mut syn::Expr) {
405        match expr {
406            syn::Expr::Verbatim(tokens) => *tokens = Self::tokens(core::mem::take(tokens)),
407            other => visit_mut::visit_expr_mut(self, other),
408        }
409    }
410
411    fn visit_type_mut(&mut self, ty: &mut syn::Type) {
412        match ty {
413            syn::Type::Verbatim(tokens) => *tokens = Self::tokens(core::mem::take(tokens)),
414            other => visit_mut::visit_type_mut(self, other),
415        }
416    }
417
418    fn visit_pat_mut(&mut self, pat: &mut syn::Pat) {
419        match pat {
420            syn::Pat::Verbatim(tokens) => *tokens = Self::tokens(core::mem::take(tokens)),
421            other => visit_mut::visit_pat_mut(self, other),
422        }
423    }
424
425    fn visit_item_mut(&mut self, item: &mut syn::Item) {
426        match item {
427            syn::Item::Verbatim(tokens) => *tokens = Self::tokens(core::mem::take(tokens)),
428            other => visit_mut::visit_item_mut(self, other),
429        }
430    }
431
432    fn visit_impl_item_mut(&mut self, item: &mut syn::ImplItem) {
433        match item {
434            syn::ImplItem::Verbatim(tokens) => *tokens = Self::tokens(core::mem::take(tokens)),
435            other => visit_mut::visit_impl_item_mut(self, other),
436        }
437    }
438
439    fn visit_trait_item_mut(&mut self, item: &mut syn::TraitItem) {
440        match item {
441            syn::TraitItem::Verbatim(tokens) => *tokens = Self::tokens(core::mem::take(tokens)),
442            other => visit_mut::visit_trait_item_mut(self, other),
443        }
444    }
445}
446
447struct ReferenceEncoding<'a> {
448    out: &'a mut String,
449    facts: &'a ReferenceFacts,
450}
451
452impl ReferenceEncoding<'_> {
453    fn resolution(&mut self, resolution: Resolution) {
454        self.out.push('r');
455        self.out.push(match resolution.kind {
456            ReferenceKind::Unresolved => '0',
457            ReferenceKind::Owned => '1',
458            ReferenceKind::Inherited => '2',
459        });
460        self.out.push(' ');
461        self.primitive(resolution.prim);
462    }
463
464    fn primitive(&mut self, primitive: Option<PrimTy>) {
465        self.out.push('t');
466        self.out.push_str(primitive.map_or("_", PrimTy::suffix));
467        self.out.push(' ');
468    }
469}
470
471impl<'ast> Visit<'ast> for ReferenceEncoding<'_> {
472    fn visit_block(&mut self, block: &'ast syn::Block) {
473        if let Some(proof) = self.facts.blocks.get(&core::ptr::from_ref(block)) {
474            write!(self.out, "b {} ", proof.statements.len())
475                .expect("writing to a string succeeds");
476            for (&role, &primitive) in proof.statements.iter().zip(&proof.types) {
477                self.out.push(char::from(role));
478                self.primitive(primitive);
479            }
480            write!(self.out, "o {} ", proof.tail_types.len())
481                .expect("writing to a string succeeds");
482            for &primitive in &proof.tail_types {
483                self.primitive(primitive);
484            }
485        }
486        visit::visit_block(self, block);
487    }
488
489    fn visit_path(&mut self, path: &'ast syn::Path) {
490        self.resolution(
491            self.facts
492                .paths
493                .get(&core::ptr::from_ref(path))
494                .copied()
495                .unwrap_or_default(),
496        );
497        visit::visit_path(self, path);
498    }
499
500    fn visit_macro(&mut self, mac: &'ast syn::Macro) {
501        let resolutions = self
502            .facts
503            .macros
504            .get(&core::ptr::from_ref(mac))
505            .map_or(&[][..], alloc::vec::Vec::as_slice);
506        write!(self.out, "m {} ", resolutions.len()).expect("writing to a string succeeds");
507        for &resolution in resolutions {
508            self.resolution(resolution);
509        }
510        visit::visit_macro(self, mac);
511    }
512}
513
514impl core::fmt::Display for CanonicalForm {
515    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
516        f.write_str(&self.key)
517    }
518}
519
520impl core::fmt::Debug for CanonicalForm {
521    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
522        f.debug_tuple("CanonicalForm").field(&self.key).finish()
523    }
524}
525
526impl PartialEq for CanonicalForm {
527    fn eq(&self, other: &Self) -> bool {
528        self.key == other.key
529    }
530}
531
532impl Eq for CanonicalForm {}
533
534impl core::hash::Hash for CanonicalForm {
535    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
536        core::hash::Hash::hash(&self.key, state);
537    }
538}
539
540fn framed_tree(out: &mut String, tree: TokenTree, leaves: &mut usize, scratch: &mut String) {
541    match tree {
542        TokenTree::Ident(ident) => {
543            scratch.clear();
544            write!(scratch, "{ident}").expect("writing to a string succeeds");
545            framed_payload(out, 'i', scratch);
546            *leaves += 1;
547        }
548        TokenTree::Literal(literal) => {
549            scratch.clear();
550            write!(scratch, "{literal}").expect("writing to a string succeeds");
551            framed_payload(out, 'l', scratch);
552            *leaves += 1;
553        }
554        TokenTree::Punct(punct) => {
555            out.push('p');
556            out.push(if punct.spacing() == Spacing::Joint {
557                'j'
558            } else {
559                'a'
560            });
561            out.push(punct.as_char());
562            out.push(' ');
563            *leaves += 1;
564        }
565        TokenTree::Group(group) => {
566            out.push('g');
567            out.push(match group.delimiter() {
568                proc_macro2::Delimiter::None => 'n',
569                proc_macro2::Delimiter::Parenthesis => '(',
570                proc_macro2::Delimiter::Bracket => '[',
571                proc_macro2::Delimiter::Brace => '{',
572            });
573            out.push(' ');
574            for tree in group.stream() {
575                framed_tree(out, tree, leaves, scratch);
576            }
577            out.push_str("e ");
578        }
579    }
580}
581
582fn framed_payload(out: &mut String, kind: char, payload: &str) {
583    write!(out, "{kind} {} {payload} ", payload.len()).expect("writing to a string succeeds");
584}
585
586pub(crate) fn body_units(stream: TokenStream) -> usize {
587    let mut trees = stream.into_iter().peekable();
588    let mut count = 0;
589    let (mut ident, mut joined) = (false, false);
590    while let Some(tree) = trees.next() {
591        match tree {
592            TokenTree::Group(group) => {
593                count += body_units(group.stream());
594                (ident, joined) = (false, false);
595            }
596            TokenTree::Ident(_) => {
597                count += usize::from(!joined);
598                (ident, joined) = (true, false);
599            }
600            TokenTree::Literal(_) => {
601                count += 1;
602                (ident, joined) = (false, false);
603            }
604            TokenTree::Punct(first) => {
605                if first.as_char() == '\'' && matches!(trees.peek(), Some(TokenTree::Ident(_))) {
606                    trees.next();
607                    count += 1;
608                    (ident, joined) = (false, false);
609                    continue;
610                }
611                let c = first.as_char();
612                let second = match trees.peek() {
613                    Some(TokenTree::Punct(second)) if first.spacing() == Spacing::Joint => {
614                        Some((second.as_char(), second.spacing()))
615                    }
616                    _ => None,
617                };
618                if second.is_some_and(|(second, _)| c == ':' && second == ':') {
619                    trees.next();
620                    joined = ident;
621                    continue;
622                }
623                if let Some((second, spacing)) = second
624                    && OPERATORS.iter().any(|op| {
625                        let bytes = op.as_bytes();
626                        char::from(bytes[0]) == c && char::from(bytes[1]) == second
627                    })
628                {
629                    trees.next();
630                    if spacing == Spacing::Joint
631                        && let Some(TokenTree::Punct(third)) = trees.peek()
632                        && OPERATORS.iter().any(|op| {
633                            let bytes = op.as_bytes();
634                            bytes.len() == 3
635                                && char::from(bytes[0]) == c
636                                && char::from(bytes[1]) == second
637                                && char::from(bytes[2]) == third.as_char()
638                        })
639                    {
640                        trees.next();
641                    }
642                }
643                count += usize::from(c != ',');
644                (ident, joined) = (false, false);
645            }
646        }
647    }
648    count
649}
650
651const OPERATORS: [&str; 23] = [
652    "<<=", ">>=", "...", "..=", "=>", "==", "!=", "<=", ">=", "&&", "||", "+=", "-=", "*=", "/=",
653    "%=", "^=", "&=", "|=", "<<", ">>", "..", "->",
654];