Skip to main content

praxis_syntax/
language.rs

1//! The rowan `Language` glue that gives Praxis a strongly-typed lossless tree.
2//!
3//! Praxis owns [`SyntaxKind`](crate::SyntaxKind); everything else here is the
4//! thin adapter that lets `rowan`'s generic node/token/element types carry it
5//! (ADR-003). Downstream crates (`praxis-parser`, `praxis-ast`, the LSP) refer
6//! to the tree through the [`SyntaxNode`] / [`SyntaxToken`] / [`SyntaxElement`]
7//! aliases so they stay free of generic parameters.
8
9use rowan::{Language, SyntaxKind as RawSyntaxKind};
10
11use crate::SyntaxKind;
12
13/// The Praxis language tag carried by every node in the lossless tree.
14///
15/// It is a zero-sized marker; its only job is to bind [`SyntaxKind`] to rowan's
16/// raw `u16` storage so that `SyntaxNode` is `SyntaxNode<PraxisLanguage>`.
17#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
18pub struct PraxisLanguage;
19
20impl Language for PraxisLanguage {
21    type Kind = SyntaxKind;
22
23    #[inline]
24    fn kind_from_raw(raw: RawSyntaxKind) -> Self::Kind {
25        // This is a *safe* function, so it may be called with any `u16`
26        // whatever the provenance of the value. `from_raw_u16` is total: out of
27        // range yields `ERROR` rather than an invalid discriminant.
28        SyntaxKind::from_raw_u16(raw.0)
29    }
30
31    #[inline]
32    fn kind_to_raw(kind: Self::Kind) -> RawSyntaxKind {
33        RawSyntaxKind(kind as u16)
34    }
35}
36
37/// A Praxis syntax node in the lossless tree.
38pub type SyntaxNode = rowan::SyntaxNode<PraxisLanguage>;
39/// A Praxis syntax token (a leaf) in the lossless tree.
40pub type SyntaxToken = rowan::SyntaxToken<PraxisLanguage>;
41/// Either a node or a token, as walked out of the tree.
42pub type SyntaxElement = rowan::SyntaxElement<PraxisLanguage>;
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn kinds_round_trip_through_raw() {
50        // Every discriminant must survive a to_raw/from_raw cycle, since rowan
51        // stores kinds as raw u16 in the green tree.
52        for kind in [
53            SyntaxKind::Whitespace,
54            SyntaxKind::Ident,
55            SyntaxKind::KW_IF,
56            SyntaxKind::PLUS,
57            SyntaxKind::EOF,
58            SyntaxKind::ERROR,
59            SyntaxKind::SOURCE_FILE,
60            SyntaxKind::PARSE_ERROR,
61        ] {
62            let raw = PraxisLanguage::kind_to_raw(kind);
63            assert_eq!(PraxisLanguage::kind_from_raw(raw), kind);
64        }
65    }
66
67    /// Every raw value in the range walked here must map to the kind with that
68    /// discriminant. This is what makes the range check in `from_raw_u16`
69    /// sufficient: it proves the discriminants really are consecutive.
70    #[test]
71    fn every_raw_value_in_range_round_trips() {
72        for raw in 0..=SyntaxKind::PARSER_NAMED_ARG as u16 {
73            let kind = PraxisLanguage::kind_from_raw(RawSyntaxKind(raw));
74            assert_eq!(
75                PraxisLanguage::kind_to_raw(kind).0,
76                raw,
77                "raw {raw} did not round-trip"
78            );
79        }
80    }
81
82    #[test]
83    fn repr_is_u16() {
84        // rowan stores a u16; the repr contract must hold for all time.
85        assert_eq!(
86            std::mem::size_of::<SyntaxKind>(),
87            std::mem::size_of::<u16>()
88        );
89    }
90
91    /// The safe `Language` boundary is reachable with any `u16`, so it must be
92    /// total. This runs in the ordinary suite, not only under Miri: a checked
93    /// conversion is observable without needing UB detection.
94    #[test]
95    fn out_of_range_raw_kind_maps_to_a_safe_error_kind() {
96        assert_eq!(
97            PraxisLanguage::kind_from_raw(RawSyntaxKind(u16::MAX)),
98            SyntaxKind::ERROR,
99            "the safe rowan Language boundary must not construct an invalid enum discriminant"
100        );
101    }
102}