Skip to main content

synapse_parser/ast/
builder.rs

1use pest::{Parser, error::Error, iterators::Pair};
2
3use crate::synapse::{Rule, SynapseParser};
4
5use super::*;
6
7// ── Entry point ───────────────────────────────────────────────────────────────
8
9pub fn parse(input: &str) -> Result<SynFile, Error<Rule>> {
10    let file_pair = SynapseParser::parse(Rule::file, input)?.next().unwrap();
11    Ok(build_file(file_pair))
12}
13
14// ── Builders ──────────────────────────────────────────────────────────────────
15
16fn build_file(pair: Pair<Rule>) -> SynFile {
17    let mut items = Vec::new();
18    for pair in pair.into_inner() {
19        match pair.as_rule() {
20            Rule::namespace_decl => items.push(Item::Namespace(build_namespace(pair))),
21            Rule::import_decl => items.push(Item::Import(build_import(pair))),
22            Rule::const_decl => items.push(Item::Const(build_const(pair))),
23            Rule::enum_def => items.push(Item::Enum(build_enum(pair))),
24            Rule::struct_def => items.push(Item::Struct(build_struct(pair))),
25            Rule::table_def => items.push(Item::Table(build_struct(pair))),
26            Rule::command_group_def => items.extend(build_command_group(pair)),
27            Rule::telemetry_def => {
28                items.push(Item::Telemetry(build_packet(
29                    pair,
30                    PacketKind::Telemetry,
31                    None,
32                )));
33            }
34            Rule::message_def => {
35                items.push(Item::Message(build_packet(pair, PacketKind::Message, None)));
36            }
37            Rule::EOI => {}
38            rule => unreachable!("unexpected rule: {rule:?}"),
39        }
40    }
41    SynFile { items }
42}
43
44fn build_namespace(pair: Pair<Rule>) -> NamespaceDecl {
45    let scoped = pair.into_inner().next().unwrap();
46    NamespaceDecl {
47        name: build_scoped_ident(scoped),
48    }
49}
50
51fn build_import(pair: Pair<Rule>) -> ImportDecl {
52    let s = pair.into_inner().next().unwrap().as_str();
53    ImportDecl {
54        path: s[1..s.len() - 1].to_string(),
55    }
56}
57
58fn build_const(pair: Pair<Rule>) -> ConstDecl {
59    let mut inner = pair.into_inner().peekable();
60    let doc = extract_doc(&mut inner);
61    let attrs = extract_attrs(&mut inner);
62    let name = inner.next().unwrap().as_str().to_string();
63    let ty = build_type_expr(inner.next().unwrap());
64    let value = build_literal(inner.next().unwrap());
65    ConstDecl {
66        name,
67        ty,
68        value,
69        doc,
70        attrs,
71    }
72}
73
74fn build_enum(pair: Pair<Rule>) -> EnumDef {
75    let mut inner = pair.into_inner().peekable();
76    let doc = extract_doc(&mut inner);
77    let attrs = extract_attrs(&mut inner);
78    let first = inner.next().unwrap();
79    let (repr, name) = if first.as_rule() == Rule::primitive_type {
80        let repr = build_primitive_type(first);
81        (Some(repr), inner.next().unwrap().as_str().to_string())
82    } else {
83        (None, first.as_str().to_string())
84    };
85    let variants = inner.map(build_enum_variant).collect();
86    EnumDef {
87        name,
88        repr,
89        variants,
90        doc,
91        attrs,
92    }
93}
94
95fn build_enum_variant(pair: Pair<Rule>) -> EnumVariant {
96    let mut inner = pair.into_inner().peekable();
97    let doc = extract_doc(&mut inner);
98    let name = inner.next().unwrap().as_str().to_string();
99    let value = inner.next().map(|p| p.as_str().parse::<i64>().unwrap());
100    EnumVariant { name, value, doc }
101}
102
103fn build_struct(pair: Pair<Rule>) -> StructDef {
104    let mut inner = pair.into_inner().peekable();
105    let doc = extract_doc(&mut inner);
106    let attrs = extract_attrs(&mut inner);
107    let name = inner.next().unwrap().as_str().to_string();
108    let fields = inner.map(build_field).collect();
109    StructDef {
110        name,
111        fields,
112        doc,
113        attrs,
114    }
115}
116
117fn build_command_group(pair: Pair<Rule>) -> Vec<Item> {
118    let mut inner = pair.into_inner().peekable();
119    let _doc = extract_doc(&mut inner);
120    let group = inner.next().unwrap().as_str().to_string();
121    inner
122        .map(|pair| Item::Command(build_packet(pair, PacketKind::Command, Some(group.clone()))))
123        .collect()
124}
125
126fn build_packet(pair: Pair<Rule>, kind: PacketKind, command_group: Option<String>) -> MessageDef {
127    let mut inner = pair.into_inner().peekable();
128    let doc = extract_doc(&mut inner);
129    let attrs = extract_attrs(&mut inner);
130    let name = inner.next().unwrap().as_str().to_string();
131    let fields = inner.map(build_field).collect();
132    MessageDef {
133        kind,
134        command_group,
135        name,
136        fields,
137        doc,
138        attrs,
139    }
140}
141
142fn build_field(pair: Pair<Rule>) -> FieldDef {
143    let mut inner = pair.into_inner().peekable();
144    let doc = extract_doc(&mut inner);
145    let name = inner.next().unwrap().as_str().to_string();
146
147    let next = inner.next().unwrap();
148    let (optional, type_pair) = if next.as_rule() == Rule::optional_marker {
149        (true, inner.next().unwrap())
150    } else {
151        (false, next)
152    };
153
154    let ty = build_type_expr(type_pair);
155    let default = inner.next().map(build_literal);
156
157    FieldDef {
158        name,
159        optional,
160        ty,
161        default,
162        doc,
163    }
164}
165
166/// Consume a leading `doc_block` (if present) and return the trimmed doc lines.
167fn extract_doc<'i>(
168    inner: &mut std::iter::Peekable<impl Iterator<Item = Pair<'i, Rule>>>,
169) -> Vec<String> {
170    if inner.peek().map(|p| p.as_rule()) == Some(Rule::doc_block) {
171        inner
172            .next()
173            .unwrap()
174            .into_inner()
175            .map(|p| {
176                p.as_str()
177                    .strip_prefix("///")
178                    .unwrap_or("")
179                    .trim()
180                    .to_string()
181            })
182            .collect()
183    } else {
184        vec![]
185    }
186}
187
188/// Consume zero or more leading `attribute` pairs and return them.
189fn extract_attrs<'i>(
190    inner: &mut std::iter::Peekable<impl Iterator<Item = Pair<'i, Rule>>>,
191) -> Vec<Attribute> {
192    let mut attrs = vec![];
193    while inner.peek().map(|p| p.as_rule()) == Some(Rule::attribute) {
194        let attr = inner.next().unwrap();
195        let mut ai = attr.into_inner();
196        let name = ai.next().unwrap().as_str().to_string();
197        let value = build_literal(ai.next().unwrap());
198        attrs.push(Attribute { name, value });
199    }
200    attrs
201}
202
203fn build_type_expr(pair: Pair<Rule>) -> TypeExpr {
204    let mut inner = pair.into_inner();
205    let base = build_base_type(inner.next().unwrap());
206    let array = inner.next().map(build_array_suffix);
207    TypeExpr { base, array }
208}
209
210fn build_base_type(pair: Pair<Rule>) -> BaseType {
211    let inner = pair.into_inner().next().unwrap();
212    match inner.as_rule() {
213        Rule::string_type => BaseType::String,
214        Rule::primitive_type => BaseType::Primitive(build_primitive_type(inner)),
215        Rule::type_ref => BaseType::Ref(build_scoped_ident(inner.into_inner().next().unwrap())),
216        r => unreachable!("unexpected base_type rule: {:?}", r),
217    }
218}
219
220fn build_primitive_type(pair: Pair<Rule>) -> PrimitiveType {
221    const PRIMITIVES: &[(&str, PrimitiveType)] = &[
222        ("f32", PrimitiveType::F32),
223        ("f64", PrimitiveType::F64),
224        ("i8", PrimitiveType::I8),
225        ("i16", PrimitiveType::I16),
226        ("i32", PrimitiveType::I32),
227        ("i64", PrimitiveType::I64),
228        ("u8", PrimitiveType::U8),
229        ("u16", PrimitiveType::U16),
230        ("u32", PrimitiveType::U32),
231        ("u64", PrimitiveType::U64),
232        ("bool", PrimitiveType::Bool),
233        ("bytes", PrimitiveType::Bytes),
234    ];
235
236    let primitive = pair.as_str();
237    PRIMITIVES
238        .iter()
239        .find_map(|(name, ty)| (*name == primitive).then_some(*ty))
240        .unwrap_or_else(|| unreachable!("unknown primitive: {}", primitive))
241}
242
243fn build_array_suffix(pair: Pair<Rule>) -> ArraySuffix {
244    let Some(size) = pair.into_inner().next() else {
245        return ArraySuffix::Dynamic;
246    };
247    build_sized_array_suffix(size)
248}
249
250fn build_sized_array_suffix(pair: Pair<Rule>) -> ArraySuffix {
251    let inner = pair.into_inner().next().unwrap();
252    match inner.as_rule() {
253        Rule::bounded_size => {
254            let n = inner
255                .into_inner()
256                .next()
257                .unwrap()
258                .as_str()
259                .parse::<u64>()
260                .unwrap();
261            ArraySuffix::Bounded(n)
262        }
263        Rule::pos_int => ArraySuffix::Fixed(inner.as_str().parse::<u64>().unwrap()),
264        r => unreachable!("unexpected array_size rule: {:?}", r),
265    }
266}
267
268fn build_literal(pair: Pair<Rule>) -> Literal {
269    let inner = pair.into_inner().next().unwrap();
270    if matches!(
271        inner.as_rule(),
272        Rule::float_lit | Rule::hex_lit | Rule::int_lit
273    ) {
274        return build_numeric_literal(inner);
275    }
276    match inner.as_rule() {
277        Rule::bool_lit => build_bool_literal(inner),
278        Rule::string_lit => build_string_literal(inner),
279        Rule::ident_lit => Literal::Ident(build_scoped_ident(inner.into_inner().next().unwrap())),
280        r => unreachable!("unexpected literal rule: {:?}", r),
281    }
282}
283
284fn build_numeric_literal(pair: Pair<Rule>) -> Literal {
285    match pair.as_rule() {
286        Rule::float_lit => Literal::Float(pair.as_str().parse::<f64>().unwrap()),
287        Rule::hex_lit => {
288            let s = pair.as_str();
289            let digits = &s[2..]; // strip 0x / 0X
290            Literal::Hex(u64::from_str_radix(digits, 16).unwrap())
291        }
292        Rule::int_lit => Literal::Int(pair.as_str().parse::<i64>().unwrap()),
293        r => unreachable!("unexpected numeric literal rule: {:?}", r),
294    }
295}
296
297fn build_bool_literal(pair: Pair<Rule>) -> Literal {
298    Literal::Bool(pair.as_str() == "true")
299}
300
301fn build_string_literal(pair: Pair<Rule>) -> Literal {
302    let s = pair.as_str();
303    Literal::Str(unescape(&s[1..s.len() - 1]))
304}
305
306fn build_scoped_ident(pair: Pair<Rule>) -> ScopedIdent {
307    pair.into_inner().map(|p| p.as_str().to_string()).collect()
308}
309
310fn unescape(s: &str) -> String {
311    let mut out = String::with_capacity(s.len());
312    let mut chars = s.chars();
313    while let Some(c) = chars.next() {
314        if c == '\\' {
315            push_escape(&mut out, chars.next());
316        } else {
317            out.push(c);
318        }
319    }
320    out
321}
322
323fn push_escape(out: &mut String, escaped: Option<char>) {
324    let Some(escaped) = escaped else {
325        out.push('\\');
326        return;
327    };
328
329    match escaped {
330        'n' => out.push('\n'),
331        't' => out.push('\t'),
332        'r' => out.push('\r'),
333        c => push_quoted_escape(out, c),
334    }
335}
336
337fn push_quoted_escape(out: &mut String, escaped: char) {
338    match escaped {
339        '\\' => out.push('\\'),
340        '"' => out.push('"'),
341        c => {
342            out.push('\\');
343            out.push(c);
344        }
345    }
346}