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}