Skip to main content

vihaco_parser_core/container/
sst.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4use std::{collections::BTreeSet, ops::Range};
5
6use chumsky::{error::Simple, extra, prelude::*};
7use eyre::Result;
8
9use super::{
10    section::{validate_local_section_name, SectionNode, SectionPath},
11    ParsedFile,
12};
13
14type ParseExtra<'src> = extra::Err<Simple<'src, char>>;
15const ROOT_SECTION_NAME: &str = "root";
16pub const VERSION: u16 = 1;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19enum LineKind {
20    Version(u16),
21    BeginContext,
22    EndContext,
23    BeginSection(String),
24    EndSection(String),
25    BeginHeader(String),
26    EndHeader(String),
27    BeginBytecode(String),
28    EndBytecode(String),
29    Body,
30    Blank,
31}
32
33#[derive(Debug, Clone)]
34struct SourceLine {
35    kind: LineKind,
36    full: Range<usize>,
37    number: usize,
38}
39
40pub fn parse_file(text: &str) -> Result<ParsedFile> {
41    let lines = lex_lines(text)?;
42    let mut cursor = LineCursor::new(&lines);
43
44    let Some(version) = cursor.next_significant() else {
45        return Err(eyre::eyre!("expected `sst v{}`", VERSION));
46    };
47    match &version.kind {
48        LineKind::Version(version) => verify_version(*version)?,
49        _ => return Err(line_error(version, format!("expected `sst v{}`", VERSION))),
50    }
51
52    let Some(context_or_section) = cursor.peek_significant() else {
53        return Err(eyre::eyre!("expected `.global:` or root section"));
54    };
55    let context = match context_or_section.kind {
56        LineKind::BeginContext => {
57            let context_begin = cursor
58                .next_significant()
59                .expect("peeked context line must exist");
60            let context_start = context_begin.full.end;
61            context_start..consume_context(&mut cursor)?
62        }
63        LineKind::BeginSection(_) => context_or_section.full.start..context_or_section.full.start,
64        _ => {
65            return Err(line_error(
66                context_or_section,
67                "expected `.global:` or root section",
68            ));
69        }
70    };
71
72    let Some(section_begin) = cursor.peek_significant() else {
73        return Err(eyre::eyre!("expected root section"));
74    };
75    let root = parse_section(
76        &mut cursor,
77        SstSectionParseInfo {
78            parent: None,
79            begin: section_begin,
80        },
81    )?;
82
83    if let Some(extra) = cursor.next_significant() {
84        return Err(line_error(extra, "unexpected content after root section"));
85    }
86
87    Ok(ParsedFile { context, root })
88}
89
90fn verify_version(version: u16) -> Result<()> {
91    if version != VERSION {
92        return Err(eyre::eyre!(
93            "unsupported sst version {} (expected {})",
94            version,
95            VERSION
96        ));
97    }
98    Ok(())
99}
100
101fn parse_line(line: &str) -> Result<LineKind> {
102    line_parser()
103        .parse(line)
104        .into_result()
105        .map_err(format_parse_errors)
106}
107
108fn line_parser<'src>() -> impl Parser<'src, &'src str, LineKind, ParseExtra<'src>> {
109    let name = any()
110        .filter(|c: &char| !c.is_whitespace() && !matches!(*c, ':' | '.' | '(' | ')'))
111        .repeated()
112        .at_least(1)
113        .collect::<String>();
114
115    let version = just("sst")
116        .then_ignore(one_of(" \t").repeated().at_least(1))
117        .then_ignore(just('v'))
118        .ignore_then(text::int(10).try_map(|version: &str, span| {
119            version.parse::<u16>().map_err(|_| Simple::new(None, span))
120        }))
121        .map(LineKind::Version);
122
123    let begin_context = just(".global:").to(LineKind::BeginContext);
124    let end_context = just(".global.").to(LineKind::EndContext);
125
126    let begin_section = just(".section(")
127        .ignore_then(name)
128        .then_ignore(just("):"))
129        .map(LineKind::BeginSection);
130    let end_section = just(".section(")
131        .ignore_then(name)
132        .then_ignore(just(")."))
133        .map(LineKind::EndSection);
134
135    let begin_header = just(".header(")
136        .ignore_then(name)
137        .then_ignore(just("):"))
138        .map(LineKind::BeginHeader);
139    let end_header = just(".header(")
140        .ignore_then(name)
141        .then_ignore(just(")."))
142        .map(LineKind::EndHeader);
143
144    let begin_bytecode = just(".text(")
145        .ignore_then(name)
146        .then_ignore(just("):"))
147        .map(LineKind::BeginBytecode);
148    let end_bytecode = just(".text(")
149        .ignore_then(name)
150        .then_ignore(just(")."))
151        .map(LineKind::EndBytecode);
152
153    let blank = one_of(" \t").repeated().to(LineKind::Blank);
154    let body = any().repeated().at_least(1).to(LineKind::Body);
155
156    just(' ')
157        .repeated()
158        .ignore_then(choice((
159            version,
160            begin_context,
161            end_context,
162            begin_section,
163            end_section,
164            begin_header,
165            end_header,
166            begin_bytecode,
167            end_bytecode,
168            body,
169            blank,
170        )))
171        .then_ignore(end())
172}
173
174fn format_parse_errors(errors: Vec<Simple<'_, char>>) -> eyre::Report {
175    let error = errors
176        .into_iter()
177        .next()
178        .map(|error| format!("{error:?}"))
179        .unwrap_or_else(|| "unknown parse error".to_string());
180    eyre::eyre!("{error}")
181}
182
183fn lex_lines(text: &str) -> Result<Vec<SourceLine>> {
184    let mut lines = Vec::new();
185    let mut start = 0;
186    for (index, line) in text.split_inclusive('\n').enumerate() {
187        let full_end = start + line.len();
188        let mut content_end = full_end;
189        if line.ends_with('\n') {
190            content_end -= 1;
191            if text.as_bytes().get(content_end.wrapping_sub(1)) == Some(&b'\r') {
192                content_end -= 1;
193            }
194        }
195
196        let leading_tabs = text[start..content_end]
197            .bytes()
198            .take_while(|byte| *byte == b'\t')
199            .count();
200        let line_start = start + leading_tabs;
201        lines.push(SourceLine {
202            kind: parse_line(&text[line_start..content_end])
203                .map_err(|err| eyre::eyre!("line {}: {err}", index + 1))?,
204            full: start..full_end,
205            number: index + 1,
206        });
207        start = full_end;
208    }
209
210    if start < text.len() {
211        let number = lines.len() + 1;
212        let leading_tabs = text[start..]
213            .bytes()
214            .take_while(|byte| *byte == b'\t')
215            .count();
216        let line_start = start + leading_tabs;
217        lines.push(SourceLine {
218            kind: parse_line(&text[line_start..])
219                .map_err(|err| eyre::eyre!("line {}: {err}", number))?,
220            full: start..text.len(),
221            number,
222        });
223    }
224
225    Ok(lines)
226}
227
228struct LineCursor<'a> {
229    lines: &'a [SourceLine],
230    next: usize,
231}
232
233impl<'a> LineCursor<'a> {
234    fn new(lines: &'a [SourceLine]) -> Self {
235        Self { lines, next: 0 }
236    }
237
238    fn peek_significant(&self) -> Option<&'a SourceLine> {
239        self.lines[self.next..]
240            .iter()
241            .find(|line| line.kind != LineKind::Blank)
242    }
243
244    fn next_significant(&mut self) -> Option<&'a SourceLine> {
245        while let Some(line) = self.lines.get(self.next) {
246            self.next += 1;
247            if line.kind != LineKind::Blank {
248                return Some(line);
249            }
250        }
251        None
252    }
253}
254
255fn consume_context(cursor: &mut LineCursor<'_>) -> Result<usize> {
256    while let Some(line) = cursor.next_significant() {
257        if line.kind == LineKind::EndContext {
258            return Ok(line.full.start);
259        }
260    }
261
262    Err(eyre::eyre!("unterminated context; expected `.global.`"))
263}
264
265struct SstSectionParseInfo<'a> {
266    parent: Option<ParentSection<'a>>,
267    begin: &'a SourceLine,
268}
269
270#[derive(Clone, Copy)]
271struct ParentSection<'a> {
272    path: &'a SectionPath,
273}
274
275fn parse_section(
276    cursor: &mut LineCursor<'_>,
277    info: SstSectionParseInfo<'_>,
278) -> Result<SectionNode> {
279    let SstSectionParseInfo { parent, begin } = info;
280    let section_name = match &begin.kind {
281        LineKind::BeginSection(name) => name.clone(),
282        _ => return Err(line_error(begin, "expected section")),
283    };
284
285    let consumed_begin = cursor
286        .next_significant()
287        .ok_or_else(|| eyre::eyre!("expected section"))?;
288    debug_assert_eq!(consumed_begin.full, begin.full);
289
290    if parent.is_none() && section_name != ROOT_SECTION_NAME {
291        return Err(line_error(
292            begin,
293            format!("root section must be named `{ROOT_SECTION_NAME}`"),
294        ));
295    }
296
297    let path = match parent {
298        Some(parent) => {
299            validate_local_section_name(parent.path, &section_name)?;
300            parent.path.child(section_name.clone())
301        }
302        None => SectionPath::root(),
303    };
304
305    let mut header = None;
306    let mut bytecode = None;
307    let mut children = Vec::new();
308    let mut child_names = BTreeSet::new();
309
310    loop {
311        let Some(line) = cursor.peek_significant() else {
312            return Err(line_error(begin, "unterminated section"));
313        };
314
315        match &line.kind {
316            LineKind::EndSection(name) => {
317                if name != &section_name {
318                    return Err(line_error(
319                        line,
320                        format!(
321                            "section `{}` ended with mismatched marker `{}`",
322                            section_name, name
323                        ),
324                    ));
325                }
326                let end = cursor.next_significant().expect("peeked line must exist");
327                let fallback = end.full.start..end.full.start;
328                return Ok(SectionNode {
329                    path,
330                    section: begin.full.start..end.full.end,
331                    header: header.unwrap_or_else(|| fallback.clone()),
332                    bytecode: bytecode.unwrap_or(fallback),
333                    children,
334                });
335            }
336            LineKind::BeginHeader(name) => {
337                ensure_block_marker_name(line, "header", name, &section_name)?;
338                if header.is_some() {
339                    return Err(line_error(
340                        line,
341                        format!("section `{}` declares duplicate header", path),
342                    ));
343                }
344                header = Some(consume_named_block(cursor, &section_name, "header")?);
345            }
346            LineKind::BeginBytecode(name) => {
347                ensure_block_marker_name(line, "bytecode", name, &section_name)?;
348                if bytecode.is_some() {
349                    return Err(line_error(
350                        line,
351                        format!("section `{}` declares duplicate bytecode", path),
352                    ));
353                }
354                bytecode = Some(consume_named_block(cursor, &section_name, "bytecode")?);
355            }
356            LineKind::BeginSection(child_name) => {
357                validate_local_section_name(&path, child_name)?;
358                if !child_names.insert(child_name.clone()) {
359                    return Err(line_error(
360                        line,
361                        format!(
362                            "section `{}` declares duplicate child `{}`",
363                            path, child_name
364                        ),
365                    ));
366                }
367                let child = parse_section(
368                    cursor,
369                    SstSectionParseInfo {
370                        parent: Some(ParentSection { path: &path }),
371                        begin: line,
372                    },
373                )?;
374                children.push(child);
375            }
376            LineKind::Blank => {
377                cursor.next_significant();
378            }
379            _ => {
380                return Err(line_error(
381                    line,
382                    format!("unexpected content in section `{}`", path),
383                ));
384            }
385        }
386    }
387}
388
389fn consume_named_block(
390    cursor: &mut LineCursor<'_>,
391    section_name: &str,
392    label: &str,
393) -> Result<Range<usize>> {
394    let begin = cursor
395        .next_significant()
396        .ok_or_else(|| eyre::eyre!("expected `{label}` block in section `{section_name}`"))?;
397    let start = begin.full.end;
398
399    while let Some(line) = cursor.next_significant() {
400        if let Some(marker_name) = block_end_marker_name(&line.kind, label) {
401            ensure_block_marker_name(line, label, marker_name, section_name)?;
402            return Ok(start..line.full.start);
403        }
404    }
405
406    Err(line_error(
407        begin,
408        format!(
409            "unterminated {label}; expected `{}`",
410            end_marker(label, section_name)
411        ),
412    ))
413}
414
415fn block_end_marker_name<'a>(kind: &'a LineKind, label: &str) -> Option<&'a str> {
416    match (label, kind) {
417        ("header", LineKind::EndHeader(name)) | ("bytecode", LineKind::EndBytecode(name)) => {
418            Some(name)
419        }
420        _ => None,
421    }
422}
423
424fn ensure_block_marker_name(
425    line: &SourceLine,
426    label: &str,
427    marker_name: &str,
428    section_name: &str,
429) -> Result<()> {
430    if marker_name != section_name {
431        return Err(line_error(
432            line,
433            format!(
434                "{label} marker for section `{section_name}` uses mismatched name `{marker_name}`"
435            ),
436        ));
437    }
438    Ok(())
439}
440
441fn end_marker(label: &str, section_name: &str) -> String {
442    match label {
443        "header" => format!(".header({section_name})."),
444        "bytecode" => format!(".text({section_name})."),
445        _ => "end marker".to_string(),
446    }
447}
448
449fn line_error(line: &SourceLine, message: impl std::fmt::Display) -> eyre::Report {
450    eyre::eyre!("line {}: {}", line.number, message)
451}