vihaco_parser_core/container/
sst.rs1use 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, §ion_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 != §ion_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, §ion_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, §ion_name, "header")?);
345 }
346 LineKind::BeginBytecode(name) => {
347 ensure_block_marker_name(line, "bytecode", name, §ion_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, §ion_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}