plotnik_core/grammar/
json.rs

1//! JSON deserialization for grammar.json files.
2//!
3//! Tree-sitter's grammar.json uses externally-tagged enums with `type` field.
4
5use indexmap::IndexMap;
6use serde::Deserialize;
7
8use super::types::{Grammar, Precedence, PrecedenceEntry, Rule};
9
10/// Error during grammar parsing.
11#[derive(Debug)]
12pub enum GrammarError {
13    Json(serde_json::Error),
14    Binary(postcard::Error),
15}
16
17impl std::fmt::Display for GrammarError {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            Self::Json(e) => write!(f, "JSON parse error: {e}"),
21            Self::Binary(e) => write!(f, "binary decode error: {e}"),
22        }
23    }
24}
25
26impl std::error::Error for GrammarError {
27    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
28        match self {
29            Self::Json(e) => Some(e),
30            Self::Binary(e) => Some(e),
31        }
32    }
33}
34
35impl Grammar {
36    /// Parse grammar from JSON string.
37    pub fn from_json(json: &str) -> Result<Self, GrammarError> {
38        let raw: RawGrammar = serde_json::from_str(json).map_err(GrammarError::Json)?;
39        Ok(raw.into())
40    }
41}
42
43/// Raw grammar structure matching tree-sitter's JSON format.
44#[derive(Debug, Deserialize)]
45struct RawGrammar {
46    name: String,
47    rules: IndexMap<String, RawRule>,
48    #[serde(default)]
49    extras: Vec<RawRule>,
50    #[serde(default)]
51    precedences: Vec<Vec<RawPrecedenceEntry>>,
52    #[serde(default)]
53    conflicts: Vec<Vec<String>>,
54    #[serde(default)]
55    externals: Vec<RawRule>,
56    #[serde(default, rename = "inline")]
57    inline_rules: Vec<String>,
58    #[serde(default)]
59    supertypes: Vec<String>,
60    #[serde(default)]
61    word: Option<String>,
62    #[serde(default)]
63    reserved: IndexMap<String, Vec<RawRule>>,
64    #[serde(default)]
65    inherits: Option<String>,
66}
67
68impl From<RawGrammar> for Grammar {
69    fn from(raw: RawGrammar) -> Self {
70        // IndexMap preserves insertion order, which matches tree-sitter's definition order.
71        // The entry rule is always first.
72        Self {
73            name: raw.name,
74            rules: raw.rules.into_iter().map(|(k, v)| (k, v.into())).collect(),
75            extras: raw.extras.into_iter().map(Into::into).collect(),
76            precedences: raw
77                .precedences
78                .into_iter()
79                .map(|v| v.into_iter().map(Into::into).collect())
80                .collect(),
81            conflicts: raw.conflicts,
82            externals: raw.externals.into_iter().map(Into::into).collect(),
83            inline: raw.inline_rules,
84            supertypes: raw.supertypes,
85            word: raw.word,
86            reserved: raw
87                .reserved
88                .into_iter()
89                .map(|(k, v)| (k, v.into_iter().map(Into::into).collect()))
90                .collect(),
91            inherits: raw.inherits,
92        }
93    }
94}
95
96/// Raw rule matching tree-sitter's JSON format.
97#[derive(Debug, Deserialize)]
98#[serde(tag = "type")]
99#[allow(clippy::upper_case_acronyms, non_camel_case_types)]
100enum RawRule {
101    BLANK,
102    STRING {
103        value: String,
104    },
105    PATTERN {
106        value: String,
107        #[serde(default)]
108        flags: Option<String>,
109    },
110    SYMBOL {
111        name: String,
112    },
113    SEQ {
114        members: Vec<RawRule>,
115    },
116    CHOICE {
117        members: Vec<RawRule>,
118    },
119    REPEAT {
120        content: Box<RawRule>,
121    },
122    REPEAT1 {
123        content: Box<RawRule>,
124    },
125    FIELD {
126        name: String,
127        content: Box<RawRule>,
128    },
129    ALIAS {
130        content: Box<RawRule>,
131        value: String,
132        named: bool,
133    },
134    TOKEN {
135        content: Box<RawRule>,
136    },
137    IMMEDIATE_TOKEN {
138        content: Box<RawRule>,
139    },
140    PREC {
141        value: RawPrecedence,
142        content: Box<RawRule>,
143    },
144    PREC_LEFT {
145        value: RawPrecedence,
146        content: Box<RawRule>,
147    },
148    PREC_RIGHT {
149        value: RawPrecedence,
150        content: Box<RawRule>,
151    },
152    PREC_DYNAMIC {
153        value: i32,
154        content: Box<RawRule>,
155    },
156    RESERVED {
157        context_name: String,
158        content: Box<RawRule>,
159    },
160}
161
162impl From<RawRule> for Rule {
163    fn from(raw: RawRule) -> Self {
164        #[allow(clippy::boxed_local)] // Fields are Box<RawRule>, output needs Box<Rule>
165        fn conv(content: Box<RawRule>) -> Box<Rule> {
166            Box::new(Rule::from(*content))
167        }
168
169        match raw {
170            RawRule::BLANK => Rule::Blank,
171            RawRule::STRING { value } => Rule::String(value),
172            RawRule::PATTERN { value, flags } => Rule::Pattern { value, flags },
173            RawRule::SYMBOL { name } => Rule::Symbol(name),
174            RawRule::SEQ { members } => Rule::Seq(members.into_iter().map(Into::into).collect()),
175            RawRule::CHOICE { members } => {
176                Rule::Choice(members.into_iter().map(Into::into).collect())
177            }
178            RawRule::REPEAT { content } => Rule::Repeat(conv(content)),
179            RawRule::REPEAT1 { content } => Rule::Repeat1(conv(content)),
180            RawRule::FIELD { name, content } => Rule::Field {
181                name,
182                content: conv(content),
183            },
184            RawRule::ALIAS {
185                content,
186                value,
187                named,
188            } => Rule::Alias {
189                content: conv(content),
190                value,
191                named,
192            },
193            RawRule::TOKEN { content } => Rule::Token(conv(content)),
194            RawRule::IMMEDIATE_TOKEN { content } => Rule::ImmediateToken(conv(content)),
195            RawRule::PREC { value, content } => Rule::Prec {
196                value: value.into(),
197                content: conv(content),
198            },
199            RawRule::PREC_LEFT { value, content } => Rule::PrecLeft {
200                value: value.into(),
201                content: conv(content),
202            },
203            RawRule::PREC_RIGHT { value, content } => Rule::PrecRight {
204                value: value.into(),
205                content: conv(content),
206            },
207            RawRule::PREC_DYNAMIC { value, content } => Rule::PrecDynamic {
208                value,
209                content: conv(content),
210            },
211            RawRule::RESERVED {
212                context_name,
213                content,
214            } => Rule::Reserved {
215                context_name,
216                content: conv(content),
217            },
218        }
219    }
220}
221
222/// Raw precedence value (can be integer or string).
223#[derive(Debug, Deserialize)]
224#[serde(untagged)]
225enum RawPrecedence {
226    Integer(i32),
227    Name(String),
228}
229
230impl From<RawPrecedence> for Precedence {
231    fn from(raw: RawPrecedence) -> Self {
232        match raw {
233            RawPrecedence::Integer(n) => Precedence::Integer(n),
234            RawPrecedence::Name(s) => Precedence::Name(s),
235        }
236    }
237}
238
239/// Raw precedence entry (STRING or SYMBOL).
240#[derive(Debug, Deserialize)]
241#[serde(tag = "type")]
242#[allow(clippy::upper_case_acronyms)]
243enum RawPrecedenceEntry {
244    STRING { value: String },
245    SYMBOL { name: String },
246}
247
248impl From<RawPrecedenceEntry> for PrecedenceEntry {
249    fn from(raw: RawPrecedenceEntry) -> Self {
250        match raw {
251            RawPrecedenceEntry::STRING { value } => PrecedenceEntry::Name(value),
252            RawPrecedenceEntry::SYMBOL { name } => PrecedenceEntry::Symbol(name),
253        }
254    }
255}