Skip to main content

brink_format/inkt/
read.rs

1//! Pest-based reader for the `.inkt` textual format.
2//!
3//! The grammar-rule clusters live in sibling modules (issue #685, pure `mod`
4//! extraction — no logic changes): [`values`] (scalars and composite value
5//! forms), [`defs`] (top-level definition tables), [`lines`] (container
6//! metadata and the per-scope line table), [`instructions`] (the bytecode
7//! `code` field), and [`primitives`] (shared token parsers). This file keeps
8//! only the parser entry point (`read_inkt`/`parse_story`), the pest-derived
9//! `Rule` enum, and the public [`InktParseError`] type.
10
11use pest::Parser;
12use pest_derive::Parser;
13
14use crate::story::StoryData;
15
16mod defs;
17mod instructions;
18mod lines;
19mod primitives;
20mod values;
21
22#[derive(Parser)]
23#[grammar = "inkt/inkt.pest"]
24struct InktParser;
25
26/// Error returned when parsing `.inkt` text fails.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct InktParseError {
29    pub message: String,
30    pub line: usize,
31    pub col: usize,
32}
33
34impl core::fmt::Display for InktParseError {
35    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36        write!(f, "{}:{}: {}", self.line, self.col, self.message)
37    }
38}
39
40impl std::error::Error for InktParseError {}
41
42/// Parse `.inkt` text into a [`StoryData`].
43pub fn read_inkt(input: &str) -> Result<StoryData, InktParseError> {
44    let pairs = InktParser::parse(Rule::story, input).map_err(|e| {
45        let (line, col) = match e.line_col {
46            pest::error::LineColLocation::Pos(pos) => pos,
47            pest::error::LineColLocation::Span(start, _) => start,
48        };
49        InktParseError {
50            message: e.to_string(),
51            line,
52            col,
53        }
54    })?;
55
56    let story_pair = pairs.into_iter().next().ok_or_else(|| InktParseError {
57        message: "no story node".into(),
58        line: 1,
59        col: 1,
60    })?;
61
62    parse_story(story_pair)
63}
64
65type P<'a> = pest::iterators::Pair<'a, Rule>;
66
67fn parse_story(pair: P<'_>) -> Result<StoryData, InktParseError> {
68    let mut name_table = Vec::new();
69    // Fuzz-found (#1102): a `.inkt` document declaring the same container
70    // address twice is malformed input and must be rejected at read time.
71    // Accepting it poisons the roundtrip downstream: `write_inkt` collapses
72    // line tables through a `scope_id`-keyed `HashMap`, so the later
73    // duplicate's lines silently replace the earlier one's on the next write
74    // (same admission-check posture as the duplicate map key rejection, #985).
75    let mut seen_container_ids = std::collections::HashSet::new();
76    let mut variables = Vec::new();
77    let mut list_defs = Vec::new();
78    let mut list_items = Vec::new();
79    let mut externals = Vec::new();
80    let mut addresses = Vec::new();
81    let mut address_paths = Vec::new();
82    let mut containers = Vec::new();
83    let mut line_tables = Vec::new();
84    let mut list_literals = Vec::new();
85    let mut literal_pool = Vec::new();
86    let mut private_defs = Vec::new();
87    let mut alias_table = Vec::new();
88    let mut effect_rows = Vec::new();
89    let mut frame_shapes = Vec::new();
90    let mut struct_shapes = Vec::new();
91    let mut debug_info = None;
92    let mut line_variant_groups = Vec::new();
93    let mut source_checksum = 0u32;
94
95    for inner in pair.into_inner() {
96        match inner.as_rule() {
97            Rule::story_checksum => {
98                if let Some(hex_pair) = inner.into_inner().next() {
99                    source_checksum = primitives::parse_hex_u32(hex_pair.as_str());
100                }
101            }
102            Rule::name_table => name_table = defs::parse_name_table(inner)?,
103            Rule::globals => variables = defs::parse_globals(inner)?,
104            Rule::lists => list_defs = defs::parse_lists(inner)?,
105            Rule::list_items => list_items = defs::parse_list_items(inner)?,
106            Rule::externals => externals = defs::parse_externals(inner)?,
107            Rule::addresses => addresses = defs::parse_addresses(inner)?,
108            Rule::address_paths => address_paths = defs::parse_address_paths(inner)?,
109            Rule::list_literals => list_literals = defs::parse_list_literals(inner)?,
110            Rule::literal_pool => literal_pool = values::parse_literal_pool(inner)?,
111            Rule::struct_shapes => struct_shapes = defs::parse_struct_shapes(inner)?,
112            Rule::visibility => private_defs = defs::parse_visibility(inner)?,
113            Rule::alias_table => alias_table = defs::parse_alias_table(inner)?,
114            Rule::effect_rows => effect_rows = defs::parse_effect_rows(inner)?,
115            Rule::frame_shapes => frame_shapes = defs::parse_frame_shapes(inner)?,
116            Rule::debug_info => debug_info = Some(defs::parse_debug_info(inner)?),
117            Rule::line_variant_groups => {
118                line_variant_groups = defs::parse_line_variant_groups(inner)?;
119            }
120            Rule::container => {
121                let (line, col) = inner.line_col();
122                let (container, lt) = lines::parse_container(inner)?;
123                if !seen_container_ids.insert(container.id) {
124                    return Err(InktParseError {
125                        message: format!("duplicate container address: {}", container.id),
126                        line,
127                        col,
128                    });
129                }
130                let is_scope_owner = container.scope_id == container.id;
131                containers.push(container);
132                // Only add line tables for scope-owning containers.
133                // Child containers (scope_id != id) have no lines in the text.
134                if is_scope_owner {
135                    line_tables.push(lt);
136                }
137            }
138            _ => {}
139        }
140    }
141
142    // Sort line tables by scope_id for deterministic ordering,
143    // matching the converter's output.
144    line_tables.sort_by_key(|lt| lt.scope_id.to_raw());
145
146    Ok(StoryData {
147        containers,
148        line_tables,
149        variables,
150        list_defs,
151        list_items,
152        externals,
153        addresses,
154        address_paths,
155        name_table,
156        list_literals,
157        literal_pool,
158        struct_shapes,
159        private_defs,
160        alias_table,
161        effect_rows,
162        frame_shapes,
163        debug_info,
164        line_variant_groups,
165        source_checksum,
166    })
167}