Skip to main content

macroonz_compiler/token/capture/item/
lens.rs

1//! A shallow structural lens into one complete caller-authored Rust item.
2//!
3//! The lens recognizes only the item envelope needed to preserve and augment authored material.
4//! It does not parse a second Rust AST, decide what an item means, or replace Rustc's grammar judgment.
5
6use super::{
7    AuthoredItem, AuthoredItemKind, AuthoredItemReadIssue, AuthoredItemReadRefusal,
8    CapturedDelimiter, CapturedFragment, CapturedInput, CapturedTokenTree, SpanHandle,
9};
10
11/// The established coordinates used to assemble one borrowed item lens.
12#[derive(Clone, Copy)]
13struct ItemCoordinates {
14    attributes_end: usize,
15    visibility_end: usize,
16    kind_index: usize,
17    kind: AuthoredItemKind,
18    name_index: Option<usize>,
19    terminator: usize,
20    body_index: Option<usize>,
21}
22
23impl CapturedInput {
24    /// Read this declared item boundary into a source-coupled structural lens for one supported item family.
25    ///
26    /// The complete token reading remains available through [`AuthoredItem::preserved`].
27    /// The lens identifies only the outer item envelope and leaves full Rust legality to Rustc.
28    ///
29    /// # Errors
30    ///
31    /// Returns a typed refusal where the boundary is empty, has no recognized item family or required name, or does not end as one complete item boundary.
32    pub fn authored_item(&self) -> Result<AuthoredItem<'_>, AuthoredItemReadRefusal> {
33        read_item(self.trees())
34    }
35}
36
37impl<'tokens> AuthoredItem<'tokens> {
38    /// The complete authored token reading this lens stands over.
39    #[must_use]
40    pub const fn preserved(self) -> CapturedFragment<'tokens> {
41        self.preserved
42    }
43
44    /// The leading outer attributes, in authored order.
45    #[must_use]
46    pub const fn attributes(self) -> CapturedFragment<'tokens> {
47        self.attributes
48    }
49
50    /// The explicit visibility tokens, or an empty fragment for inherited visibility.
51    #[must_use]
52    pub const fn visibility(self) -> CapturedFragment<'tokens> {
53        self.visibility
54    }
55
56    /// The qualifiers between visibility and the item-family keyword.
57    #[must_use]
58    pub const fn qualifiers(self) -> CapturedFragment<'tokens> {
59        self.qualifiers
60    }
61
62    /// The recognized structural item family.
63    #[must_use]
64    pub const fn kind(self) -> AuthoredItemKind {
65        self.kind
66    }
67
68    /// The exact token carrying the item-family keyword.
69    #[must_use]
70    pub const fn kind_token(self) -> &'tokens CapturedTokenTree {
71        self.kind_token
72    }
73
74    /// The optional item-name token and its ordinary or raw spelling.
75    #[must_use]
76    pub fn name(self) -> Option<(&'tokens CapturedTokenTree, &'tokens str)> {
77        self.name_token
78            .and_then(|token| identifier(token).map(|spelling| (token, spelling)))
79    }
80
81    /// The exact generic-parameter run, including its angle punctuation, where one is present.
82    #[must_use]
83    pub const fn generics(self) -> Option<CapturedFragment<'tokens>> {
84        self.generics
85    }
86
87    /// The exact where-clause run, beginning with `where`, where one is present.
88    #[must_use]
89    pub const fn where_clause(self) -> Option<CapturedFragment<'tokens>> {
90        self.where_clause
91    }
92
93    /// The item-signature run after outer attributes and before its first body group or terminator.
94    #[must_use]
95    pub const fn signature(self) -> CapturedFragment<'tokens> {
96        self.signature
97    }
98
99    /// The optional body group's delimiter and exact inner token fragment.
100    #[must_use]
101    pub fn body(self) -> Option<(CapturedDelimiter, CapturedFragment<'tokens>)> {
102        self.body_delimiter.zip(self.body)
103    }
104
105    /// The explicit `unsafe` qualifier token on this item, where the caller wrote one.
106    ///
107    /// This is a local syntactic boundary, not a global unsafe scan or a soundness claim.
108    #[must_use]
109    pub const fn unsafe_token(self) -> Option<&'tokens CapturedTokenTree> {
110        self.unsafe_token
111    }
112}
113
114impl AuthoredItemReadRefusal {
115    /// The structural item-envelope issue this read established.
116    pub const fn issue(self) -> AuthoredItemReadIssue {
117        self.issue
118    }
119
120    /// The exact producer span available at the refusal site.
121    #[must_use]
122    pub const fn token(self) -> Option<SpanHandle> {
123        self.at
124    }
125}
126
127/// Read one item lens after the caller or proc boundary declared the complete item run.
128fn read_item(tokens: &[CapturedTokenTree]) -> Result<AuthoredItem<'_>, AuthoredItemReadRefusal> {
129    if tokens.is_empty() {
130        return Err(refused(AuthoredItemReadIssue::ItemMissing, None));
131    }
132    let attributes_end = attributes_end(tokens);
133    let visibility_end = visibility_end(tokens, attributes_end);
134    let (kind_index, kind) = item_kind(tokens, visibility_end)?;
135    let name_index = item_name(tokens, kind_index, kind)?;
136    let terminator = terminator(tokens, kind)?;
137    let body_index = body_index(tokens, name_index, kind, terminator);
138    assemble(
139        tokens,
140        ItemCoordinates {
141            attributes_end,
142            visibility_end,
143            kind_index,
144            kind,
145            name_index,
146            terminator,
147            body_index,
148        },
149    )
150}
151
152/// Assemble the lens after every structural coordinate has been established.
153fn assemble(
154    tokens: &[CapturedTokenTree],
155    coordinates: ItemCoordinates,
156) -> Result<AuthoredItem<'_>, AuthoredItemReadRefusal> {
157    let signature_end = coordinates.body_index.unwrap_or(coordinates.terminator);
158    let generic_start = coordinates.name_index.map_or_else(
159        || coordinates.kind_index.checked_add(1),
160        |index| index.checked_add(1),
161    );
162    let generics = generic_start
163        .and_then(|start| generic_range(tokens, start, signature_end))
164        .map(|(start, end)| fragment(tokens, start, end, None))
165        .transpose()?;
166    let where_clause = word_index(
167        tokens,
168        "where",
169        coordinates.kind_index,
170        coordinates.terminator,
171    )
172    .map(|start| fragment(tokens, start, coordinates.terminator, None))
173    .transpose()?;
174    let body_token = coordinates.body_index.and_then(|index| tokens.get(index));
175    let (body_delimiter, body) = body_token.and_then(CapturedTokenTree::group).map_or(
176        (None, None),
177        |(delimiter, members)| {
178            (
179                Some(delimiter),
180                Some(CapturedFragment::over(
181                    members,
182                    body_token.map(CapturedTokenTree::span),
183                )),
184            )
185        },
186    );
187    Ok(AuthoredItem {
188        preserved: fragment(tokens, 0, tokens.len(), None)?,
189        attributes: fragment(tokens, 0, coordinates.attributes_end, None)?,
190        visibility: fragment(
191            tokens,
192            coordinates.attributes_end,
193            coordinates.visibility_end,
194            None,
195        )?,
196        qualifiers: fragment(
197            tokens,
198            coordinates.visibility_end,
199            coordinates.kind_index,
200            None,
201        )?,
202        signature: fragment(tokens, coordinates.attributes_end, signature_end, None)?,
203        generics,
204        where_clause,
205        body,
206        body_delimiter,
207        kind: coordinates.kind,
208        kind_token: token_at(tokens, coordinates.kind_index)?,
209        name_token: coordinates.name_index.and_then(|index| tokens.get(index)),
210        unsafe_token: word_token(
211            tokens,
212            "unsafe",
213            coordinates.visibility_end,
214            coordinates.kind_index,
215        ),
216    })
217}
218
219/// The end of the leading outer-attribute run.
220fn attributes_end(tokens: &[CapturedTokenTree]) -> usize {
221    let mut next = 0usize;
222    loop {
223        let Some(mark) = tokens.get(next) else {
224            return next;
225        };
226        let Some(group) = next.checked_add(1).and_then(|index| tokens.get(index)) else {
227            return next;
228        };
229        let is_outer = mark.punct() == Some('#')
230            && group
231                .group()
232                .is_some_and(|(delimiter, _)| delimiter == CapturedDelimiter::Bracket);
233        if !is_outer {
234            return next;
235        }
236        next = next.saturating_add(2);
237    }
238}
239
240/// The end of one optional `pub` visibility, including a restriction group.
241fn visibility_end(tokens: &[CapturedTokenTree], start: usize) -> usize {
242    let Some(token) = tokens.get(start) else {
243        return start;
244    };
245    if token.word() != Some("pub") {
246        return start;
247    }
248    let after_pub = start.saturating_add(1);
249    tokens.get(after_pub).map_or(after_pub, |next| {
250        if next
251            .group()
252            .is_some_and(|(delimiter, _)| delimiter == CapturedDelimiter::Parenthesis)
253        {
254            after_pub.saturating_add(1)
255        } else {
256            after_pub
257        }
258    })
259}
260
261/// Find the item-family keyword after the lawful qualifier vocabulary.
262fn item_kind(
263    tokens: &[CapturedTokenTree],
264    start: usize,
265) -> Result<(usize, AuthoredItemKind), AuthoredItemReadRefusal> {
266    let mut next = start;
267    while let Some(token) = tokens.get(next) {
268        let word = token.word();
269        let found = match word {
270            Some("mod") => Some(AuthoredItemKind::Module),
271            Some("struct") => Some(AuthoredItemKind::Structure),
272            Some("enum") => Some(AuthoredItemKind::Enumeration),
273            Some("union") => Some(AuthoredItemKind::Union),
274            Some("trait") => Some(AuthoredItemKind::Trait),
275            Some("fn") => Some(AuthoredItemKind::Function),
276            Some("impl") => Some(AuthoredItemKind::Implementation),
277            Some("type") => Some(AuthoredItemKind::TypeAlias),
278            Some("static") => Some(AuthoredItemKind::Static),
279            Some("use") => Some(AuthoredItemKind::Use),
280            Some("const") if !function_follows(tokens, next.saturating_add(1)) => {
281                Some(AuthoredItemKind::Constant)
282            }
283            Some("extern") if next_word(tokens, next) == Some("crate") => {
284                Some(AuthoredItemKind::ExternalCrate)
285            }
286            Some("unsafe" | "async" | "default" | "auto" | "extern" | "const") => None,
287            _ => {
288                return Err(refused(
289                    AuthoredItemReadIssue::ItemKindMissing,
290                    Some(token.span()),
291                ));
292            }
293        };
294        if let Some(kind) = found {
295            return Ok((next, kind));
296        }
297        next = next.saturating_add(1);
298        if word == Some("extern")
299            && tokens
300                .get(next)
301                .is_some_and(|candidate| candidate.text().is_some())
302        {
303            next = next.saturating_add(1);
304        }
305    }
306    Err(refused(
307        AuthoredItemReadIssue::ItemKindMissing,
308        tokens.last().map(CapturedTokenTree::span),
309    ))
310}
311
312/// Whether the qualifier run after `const` reaches a function item.
313fn function_follows(tokens: &[CapturedTokenTree], start: usize) -> bool {
314    let mut next = start;
315    while let Some(token) = tokens.get(next) {
316        match token.word() {
317            Some("fn") => return true,
318            Some("unsafe" | "async" | "extern") => {
319                next = next.saturating_add(1);
320                if token.word() == Some("extern")
321                    && tokens
322                        .get(next)
323                        .is_some_and(|candidate| candidate.text().is_some())
324                {
325                    next = next.saturating_add(1);
326                }
327            }
328            _ => return false,
329        }
330    }
331    false
332}
333
334/// Read the required name seat for item families that carry one.
335fn item_name(
336    tokens: &[CapturedTokenTree],
337    kind_index: usize,
338    kind: AuthoredItemKind,
339) -> Result<Option<usize>, AuthoredItemReadRefusal> {
340    if matches!(
341        kind,
342        AuthoredItemKind::Implementation | AuthoredItemKind::Use
343    ) {
344        return Ok(None);
345    }
346    let mut candidate = kind_index.saturating_add(1);
347    if kind == AuthoredItemKind::ExternalCrate {
348        candidate = candidate.saturating_add(1);
349    }
350    if kind == AuthoredItemKind::Static
351        && tokens.get(candidate).and_then(CapturedTokenTree::word) == Some("mut")
352    {
353        candidate = candidate.saturating_add(1);
354    }
355    let token = tokens.get(candidate).ok_or(refused(
356        AuthoredItemReadIssue::ItemNameMissing(kind),
357        tokens.get(kind_index).map(CapturedTokenTree::span),
358    ))?;
359    identifier(token).ok_or(refused(
360        AuthoredItemReadIssue::ItemNameMissing(kind),
361        Some(token.span()),
362    ))?;
363    Ok(Some(candidate))
364}
365
366/// Establish that the declared item boundary ends with a body or semicolon.
367fn terminator(
368    tokens: &[CapturedTokenTree],
369    kind: AuthoredItemKind,
370) -> Result<usize, AuthoredItemReadRefusal> {
371    let index = tokens
372        .len()
373        .checked_sub(1)
374        .ok_or(refused(AuthoredItemReadIssue::ItemMissing, None))?;
375    let token = token_at(tokens, index)?;
376    let finished = token.punct() == Some(';')
377        || token
378            .group()
379            .is_some_and(|(delimiter, _)| delimiter == CapturedDelimiter::Brace);
380    if finished {
381        Ok(index)
382    } else {
383        Err(refused(
384            AuthoredItemReadIssue::ItemBoundaryUnfinished(kind),
385            Some(token.span()),
386        ))
387    }
388}
389
390/// Select the body group without interpreting its contents.
391fn body_index(
392    tokens: &[CapturedTokenTree],
393    name: Option<usize>,
394    kind: AuthoredItemKind,
395    terminator: usize,
396) -> Option<usize> {
397    if tokens
398        .get(terminator)
399        .is_some_and(|token| token.group().is_some())
400    {
401        return Some(terminator);
402    }
403    if kind != AuthoredItemKind::Structure {
404        return None;
405    }
406    let start = name.and_then(|index| index.checked_add(1))?;
407    tokens
408        .iter()
409        .enumerate()
410        .skip(start)
411        .take(terminator.saturating_sub(start))
412        .find_map(|(index, token)| {
413            token.group().and_then(|(delimiter, _)| {
414                matches!(
415                    delimiter,
416                    CapturedDelimiter::Parenthesis | CapturedDelimiter::Brace
417                )
418                .then_some(index)
419            })
420        })
421}
422
423/// Find an immediately seated generic-parameter run and its matching close.
424fn generic_range(tokens: &[CapturedTokenTree], start: usize, end: usize) -> Option<(usize, usize)> {
425    if tokens.get(start).and_then(CapturedTokenTree::punct) != Some('<') {
426        return None;
427    }
428    let mut depth = 0usize;
429    for (index, token) in tokens
430        .iter()
431        .enumerate()
432        .skip(start)
433        .take(end.saturating_sub(start))
434    {
435        match token.punct() {
436            Some('<') => depth = depth.saturating_add(1),
437            Some('>') if !arrow_close(tokens, index) => {
438                depth = depth.checked_sub(1)?;
439                if depth == 0 {
440                    return index.checked_add(1).map(|after| (start, after));
441                }
442            }
443            Some(_) | None => {}
444        }
445    }
446    None
447}
448
449/// Whether this greater-than punctuation closes a thin arrow instead of a generic roster.
450fn arrow_close(tokens: &[CapturedTokenTree], index: usize) -> bool {
451    index
452        .checked_sub(1)
453        .and_then(|previous| tokens.get(previous))
454        .and_then(CapturedTokenTree::joint_punct)
455        == Some('-')
456}
457
458/// Borrow one established token range as a fragment.
459fn fragment(
460    tokens: &[CapturedTokenTree],
461    start: usize,
462    end: usize,
463    enclosing: Option<SpanHandle>,
464) -> Result<CapturedFragment<'_>, AuthoredItemReadRefusal> {
465    tokens.get(start..end).map_or_else(
466        || {
467            Err(refused(
468                AuthoredItemReadIssue::LensRangeContradiction,
469                tokens.get(start).map(CapturedTokenTree::span),
470            ))
471        },
472        |borrowed| Ok(CapturedFragment::over(borrowed, enclosing)),
473    )
474}
475
476/// Read one required token coordinate established by the lens.
477fn token_at(
478    tokens: &[CapturedTokenTree],
479    index: usize,
480) -> Result<&CapturedTokenTree, AuthoredItemReadRefusal> {
481    tokens.get(index).ok_or(refused(
482        AuthoredItemReadIssue::LensRangeContradiction,
483        tokens.last().map(CapturedTokenTree::span),
484    ))
485}
486
487/// One ordinary or raw identifier spelling.
488fn identifier(token: &CapturedTokenTree) -> Option<&str> {
489    token.word().or_else(|| token.raw_identifier())
490}
491
492/// The word after one token coordinate.
493fn next_word(tokens: &[CapturedTokenTree], index: usize) -> Option<&str> {
494    index
495        .checked_add(1)
496        .and_then(|next| tokens.get(next))
497        .and_then(CapturedTokenTree::word)
498}
499
500/// Find one exact word in an established half-open range.
501fn word_index(tokens: &[CapturedTokenTree], word: &str, start: usize, end: usize) -> Option<usize> {
502    tokens
503        .iter()
504        .enumerate()
505        .skip(start)
506        .take(end.saturating_sub(start))
507        .find_map(|(index, token)| (token.word() == Some(word)).then_some(index))
508}
509
510/// Find one exact word token in an established half-open range.
511fn word_token<'tokens>(
512    tokens: &'tokens [CapturedTokenTree],
513    word: &str,
514    start: usize,
515    end: usize,
516) -> Option<&'tokens CapturedTokenTree> {
517    word_index(tokens, word, start, end).and_then(|index| tokens.get(index))
518}
519
520/// Construct one typed authored-item refusal.
521const fn refused(issue: AuthoredItemReadIssue, at: Option<SpanHandle>) -> AuthoredItemReadRefusal {
522    AuthoredItemReadRefusal { issue, at }
523}