Skip to main content

brink_format/inkt/
read.rs

1//! Pest-based reader for the `.inkt` textual format.
2
3use pest::Parser;
4use pest_derive::Parser;
5
6use crate::counting::CountingFlags;
7use crate::definition::{
8    AddressDef, AddressPath, ContainerDef, ExternalFnDef, GlobalVarDef, LineEntry, ListDef,
9    ListItemDef, ScopeLineTable, SlotInfo, SourceLocation,
10};
11use crate::id::{DefinitionId, NameId};
12use crate::line::{LineContent, LinePart, PluralCategory, SelectKey};
13use crate::opcode::{ChoiceFlags, Opcode, SequenceKind};
14use crate::story::StoryData;
15use crate::value::{ListValue, Value, ValueType};
16
17#[derive(Parser)]
18#[grammar = "inkt/inkt.pest"]
19struct InktParser;
20
21/// Error returned when parsing `.inkt` text fails.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct InktParseError {
24    pub message: String,
25    pub line: usize,
26    pub col: usize,
27}
28
29impl core::fmt::Display for InktParseError {
30    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
31        write!(f, "{}:{}: {}", self.line, self.col, self.message)
32    }
33}
34
35impl std::error::Error for InktParseError {}
36
37/// Parse `.inkt` text into a [`StoryData`].
38pub fn read_inkt(input: &str) -> Result<StoryData, InktParseError> {
39    let pairs = InktParser::parse(Rule::story, input).map_err(|e| {
40        let (line, col) = match e.line_col {
41            pest::error::LineColLocation::Pos(pos) => pos,
42            pest::error::LineColLocation::Span(start, _) => start,
43        };
44        InktParseError {
45            message: e.to_string(),
46            line,
47            col,
48        }
49    })?;
50
51    let story_pair = pairs.into_iter().next().ok_or_else(|| InktParseError {
52        message: "no story node".into(),
53        line: 1,
54        col: 1,
55    })?;
56
57    parse_story(story_pair)
58}
59
60type P<'a> = pest::iterators::Pair<'a, Rule>;
61
62fn err(pair: &P<'_>, msg: impl Into<String>) -> InktParseError {
63    let (line, col) = pair.line_col();
64    InktParseError {
65        message: msg.into(),
66        line,
67        col,
68    }
69}
70
71fn parse_story(pair: P<'_>) -> Result<StoryData, InktParseError> {
72    let mut name_table = Vec::new();
73    let mut variables = Vec::new();
74    let mut list_defs = Vec::new();
75    let mut list_items = Vec::new();
76    let mut externals = Vec::new();
77    let mut addresses = Vec::new();
78    let mut address_paths = Vec::new();
79    let mut containers = Vec::new();
80    let mut line_tables = Vec::new();
81    let mut list_literals = Vec::new();
82    let mut source_checksum = 0u32;
83
84    for inner in pair.into_inner() {
85        match inner.as_rule() {
86            Rule::story_checksum => {
87                if let Some(hex_pair) = inner.into_inner().next() {
88                    source_checksum = parse_hex_u32(hex_pair.as_str());
89                }
90            }
91            Rule::name_table => name_table = parse_name_table(inner)?,
92            Rule::globals => variables = parse_globals(inner)?,
93            Rule::lists => list_defs = parse_lists(inner)?,
94            Rule::list_items => list_items = parse_list_items(inner)?,
95            Rule::externals => externals = parse_externals(inner)?,
96            Rule::addresses => addresses = parse_addresses(inner)?,
97            Rule::address_paths => address_paths = parse_address_paths(inner)?,
98            Rule::list_literals => list_literals = parse_list_literals(inner)?,
99            Rule::container => {
100                let (container, lt) = parse_container(inner)?;
101                let is_scope_owner = container.scope_id == container.id;
102                containers.push(container);
103                // Only add line tables for scope-owning containers.
104                // Child containers (scope_id != id) have no lines in the text.
105                if is_scope_owner {
106                    line_tables.push(lt);
107                }
108            }
109            _ => {}
110        }
111    }
112
113    // Sort line tables by scope_id for deterministic ordering,
114    // matching the converter's output.
115    line_tables.sort_by_key(|lt| lt.scope_id.to_raw());
116
117    Ok(StoryData {
118        containers,
119        line_tables,
120        variables,
121        list_defs,
122        list_items,
123        externals,
124        addresses,
125        address_paths,
126        name_table,
127        list_literals,
128        source_checksum,
129    })
130}
131
132// ── Name table ──────────────────────────────────────────────────────────────
133
134fn parse_name_table(pair: P<'_>) -> Result<Vec<String>, InktParseError> {
135    let mut names = Vec::new();
136    for entry in pair.into_inner() {
137        if entry.as_rule() == Rule::name_entry {
138            let mut inner = entry.into_inner();
139            let _index = inner.next(); // integer index (implied by position)
140            let s = inner.next().ok_or_else(|| InktParseError {
141                message: "expected string in name_entry".into(),
142                line: 0,
143                col: 0,
144            })?;
145            names.push(unescape_string(s.as_str()));
146        }
147    }
148    Ok(names)
149}
150
151// ── Globals ─────────────────────────────────────────────────────────────────
152
153fn parse_globals(pair: P<'_>) -> Result<Vec<GlobalVarDef>, InktParseError> {
154    let mut vars = Vec::new();
155    for entry in pair.into_inner() {
156        if entry.as_rule() == Rule::global_entry {
157            vars.push(parse_global_entry(entry)?);
158        }
159    }
160    Ok(vars)
161}
162
163fn parse_global_entry(pair: P<'_>) -> Result<GlobalVarDef, InktParseError> {
164    let mut inner = pair.into_inner();
165    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
166        message: "expected def_id in global".into(),
167        line: 0,
168        col: 0,
169    })?)?;
170
171    let type_pair = inner.next().ok_or_else(|| InktParseError {
172        message: "expected type_name in global".into(),
173        line: 0,
174        col: 0,
175    })?;
176    let value_type = parse_value_type(type_pair)?;
177
178    let value_pair = inner.next().ok_or_else(|| InktParseError {
179        message: "expected value in global".into(),
180        line: 0,
181        col: 0,
182    })?;
183    let default_value = parse_value(value_pair, Some(value_type))?;
184
185    let mut mutable = false;
186    let mut name = NameId(0);
187
188    for remaining in inner {
189        match remaining.as_rule() {
190            Rule::mutable_flag => mutable = true,
191            Rule::integer => {
192                name = NameId(parse_u16(&remaining)?);
193            }
194            _ => {}
195        }
196    }
197
198    Ok(GlobalVarDef {
199        id,
200        name,
201        value_type,
202        default_value,
203        mutable,
204    })
205}
206
207#[expect(clippy::needless_pass_by_value)]
208fn parse_value_type(pair: P<'_>) -> Result<ValueType, InktParseError> {
209    let s = pair.as_str();
210    match s {
211        "int" => Ok(ValueType::Int),
212        "float" => Ok(ValueType::Float),
213        "bool" => Ok(ValueType::Bool),
214        "string" => Ok(ValueType::String),
215        "list" => Ok(ValueType::List),
216        "divert_target" => Ok(ValueType::DivertTarget),
217        "var_pointer" => Ok(ValueType::VariablePointer),
218        "temp_pointer" => Ok(ValueType::TempPointer),
219        "fragment_ref" => Ok(ValueType::FragmentRef),
220        "null" => Ok(ValueType::Null),
221        _ => Err(err(&pair, format!("unknown value type: {s}"))),
222    }
223}
224
225fn parse_value(pair: P<'_>, type_hint: Option<ValueType>) -> Result<Value, InktParseError> {
226    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
227        message: "empty value".into(),
228        line: 0,
229        col: 0,
230    })?;
231
232    match inner.as_rule() {
233        Rule::integer => {
234            // Use the type hint to disambiguate: an integer literal can represent
235            // Int, Float, or Bool depending on the declared value_type.
236            match type_hint {
237                Some(ValueType::Float) => {
238                    let n: f32 = inner
239                        .as_str()
240                        .parse()
241                        .map_err(|_| err(&inner, "invalid float"))?;
242                    Ok(Value::Float(n))
243                }
244                Some(ValueType::Bool) => {
245                    let n: i32 = inner
246                        .as_str()
247                        .parse()
248                        .map_err(|_| err(&inner, "invalid integer"))?;
249                    Ok(Value::Bool(n != 0))
250                }
251                _ => {
252                    let n: i32 = inner
253                        .as_str()
254                        .parse()
255                        .map_err(|_| err(&inner, "invalid integer"))?;
256                    Ok(Value::Int(n))
257                }
258            }
259        }
260        Rule::float => {
261            let n: f32 = inner
262                .as_str()
263                .parse()
264                .map_err(|_| err(&inner, "invalid float"))?;
265            Ok(Value::Float(n))
266        }
267        Rule::bool_value => Ok(Value::Bool(inner.as_str() == "true")),
268        Rule::string => Ok(Value::String(unescape_string(inner.as_str()).into())),
269        Rule::def_id => Ok(Value::DivertTarget(parse_def_id(inner)?)),
270        Rule::null_value => Ok(Value::Null),
271        Rule::list_value => parse_list_value(inner),
272        Rule::var_pointer_value => {
273            let id_pair = inner.into_inner().next().ok_or_else(|| InktParseError {
274                message: "expected def_id in var_pointer".into(),
275                line: 0,
276                col: 0,
277            })?;
278            Ok(Value::VariablePointer(parse_def_id(id_pair)?))
279        }
280        Rule::fragment_ref_value => {
281            let idx_pair = inner.into_inner().next().ok_or_else(|| InktParseError {
282                message: "expected integer in fragment_ref".into(),
283                line: 0,
284                col: 0,
285            })?;
286            let idx: u32 = idx_pair.as_str().parse().map_err(|_| InktParseError {
287                message: "invalid fragment_ref index".into(),
288                line: 0,
289                col: 0,
290            })?;
291            Ok(Value::FragmentRef(idx))
292        }
293        _ => Err(err(
294            &inner,
295            format!("unexpected value rule: {:?}", inner.as_rule()),
296        )),
297    }
298}
299
300fn parse_list_value(pair: P<'_>) -> Result<Value, InktParseError> {
301    let mut items = Vec::new();
302    let mut origins = Vec::new();
303
304    for child in pair.into_inner() {
305        match child.as_rule() {
306            Rule::list_value_items => {
307                for def_pair in child.into_inner() {
308                    if def_pair.as_rule() == Rule::def_id {
309                        items.push(parse_def_id(def_pair)?);
310                    }
311                }
312            }
313            Rule::list_value_origins => {
314                for def_pair in child.into_inner() {
315                    if def_pair.as_rule() == Rule::def_id {
316                        origins.push(parse_def_id(def_pair)?);
317                    }
318                }
319            }
320            _ => {}
321        }
322    }
323
324    Ok(Value::List(ListValue { items, origins }.into()))
325}
326
327// ── Lists ───────────────────────────────────────────────────────────────────
328
329fn parse_lists(pair: P<'_>) -> Result<Vec<ListDef>, InktParseError> {
330    let mut defs = Vec::new();
331    for entry in pair.into_inner() {
332        if entry.as_rule() == Rule::list_entry {
333            defs.push(parse_list_entry(entry)?);
334        }
335    }
336    Ok(defs)
337}
338
339fn parse_list_entry(pair: P<'_>) -> Result<ListDef, InktParseError> {
340    let mut inner = pair.into_inner();
341    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
342        message: "expected def_id in list".into(),
343        line: 0,
344        col: 0,
345    })?)?;
346
347    let name_int = inner.next().ok_or_else(|| InktParseError {
348        message: "expected name integer in list".into(),
349        line: 0,
350        col: 0,
351    })?;
352    let name = NameId(parse_u16(&name_int)?);
353
354    let mut items = Vec::new();
355    for remaining in inner {
356        if remaining.as_rule() == Rule::list_item_inline {
357            let mut li_inner = remaining.into_inner();
358            let item_name_id = parse_u16(&li_inner.next().ok_or_else(|| InktParseError {
359                message: "expected name in list item".into(),
360                line: 0,
361                col: 0,
362            })?)?;
363            let ordinal: i32 = li_inner
364                .next()
365                .ok_or_else(|| InktParseError {
366                    message: "expected ordinal in list item".into(),
367                    line: 0,
368                    col: 0,
369                })?
370                .as_str()
371                .parse()
372                .map_err(|_| InktParseError {
373                    message: "invalid ordinal".into(),
374                    line: 0,
375                    col: 0,
376                })?;
377            items.push((NameId(item_name_id), ordinal));
378        }
379    }
380
381    Ok(ListDef { id, name, items })
382}
383
384// ── List items ──────────────────────────────────────────────────────────────
385
386fn parse_list_items(pair: P<'_>) -> Result<Vec<ListItemDef>, InktParseError> {
387    let mut items = Vec::new();
388    for entry in pair.into_inner() {
389        if entry.as_rule() == Rule::list_item_entry {
390            items.push(parse_list_item_entry(entry)?);
391        }
392    }
393    Ok(items)
394}
395
396fn parse_list_item_entry(pair: P<'_>) -> Result<ListItemDef, InktParseError> {
397    let mut inner = pair.into_inner();
398    let id = parse_def_id(next_rule(&mut inner, Rule::def_id, "list_item id")?)?;
399    let origin = parse_def_id(next_rule(&mut inner, Rule::def_id, "list_item origin")?)?;
400    let ordinal: i32 = next_rule(&mut inner, Rule::integer, "list_item ordinal")?
401        .as_str()
402        .parse()
403        .map_err(|_| InktParseError {
404            message: "invalid ordinal".into(),
405            line: 0,
406            col: 0,
407        })?;
408    let name_val =
409        next_rule(&mut inner, Rule::integer, "list_item name").map_or(Ok(0), |p| parse_u16(&p))?;
410    Ok(ListItemDef {
411        id,
412        origin,
413        ordinal,
414        name: NameId(name_val),
415    })
416}
417
418// ── Externals ───────────────────────────────────────────────────────────────
419
420fn parse_externals(pair: P<'_>) -> Result<Vec<ExternalFnDef>, InktParseError> {
421    let mut exts = Vec::new();
422    for entry in pair.into_inner() {
423        if entry.as_rule() == Rule::extern_entry {
424            exts.push(parse_extern_entry(entry)?);
425        }
426    }
427    Ok(exts)
428}
429
430fn parse_extern_entry(pair: P<'_>) -> Result<ExternalFnDef, InktParseError> {
431    let mut inner = pair.into_inner();
432    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
433        message: "expected def_id in extern".into(),
434        line: 0,
435        col: 0,
436    })?)?;
437
438    let argc_pair = inner.next().ok_or_else(|| InktParseError {
439        message: "expected argc in extern".into(),
440        line: 0,
441        col: 0,
442    })?;
443    let arg_count: u8 = argc_pair
444        .as_str()
445        .parse()
446        .map_err(|_| err(&argc_pair, "invalid argc"))?;
447
448    let name_int = inner.next().ok_or_else(|| InktParseError {
449        message: "expected name in extern".into(),
450        line: 0,
451        col: 0,
452    })?;
453    let name = NameId(parse_u16(&name_int)?);
454
455    let mut fallback = None;
456    for remaining in inner {
457        if remaining.as_rule() == Rule::fallback {
458            let fb_inner = remaining
459                .into_inner()
460                .next()
461                .ok_or_else(|| InktParseError {
462                    message: "expected def_id in fallback".into(),
463                    line: 0,
464                    col: 0,
465                })?;
466            fallback = Some(parse_def_id(fb_inner)?);
467        }
468    }
469
470    Ok(ExternalFnDef {
471        id,
472        name,
473        arg_count,
474        fallback,
475    })
476}
477
478// ── Addresses ───────────────────────────────────────────────────────────────
479
480fn parse_addresses(pair: P<'_>) -> Result<Vec<AddressDef>, InktParseError> {
481    let mut addresses = Vec::new();
482    for entry in pair.into_inner() {
483        if entry.as_rule() == Rule::address_entry {
484            addresses.push(parse_address_entry(entry)?);
485        }
486    }
487    Ok(addresses)
488}
489
490fn parse_address_entry(pair: P<'_>) -> Result<AddressDef, InktParseError> {
491    let mut inner = pair.into_inner();
492    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
493        message: "expected def_id in address".into(),
494        line: 0,
495        col: 0,
496    })?)?;
497    let container_id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
498        message: "expected container_id in address".into(),
499        line: 0,
500        col: 0,
501    })?)?;
502    let offset_pair = inner.next().ok_or_else(|| InktParseError {
503        message: "expected byte_offset in address".into(),
504        line: 0,
505        col: 0,
506    })?;
507    let byte_offset: u32 = offset_pair
508        .as_str()
509        .parse()
510        .map_err(|_| err(&offset_pair, "invalid byte_offset"))?;
511    Ok(AddressDef {
512        id,
513        container_id,
514        byte_offset,
515    })
516}
517
518fn parse_address_paths(pair: P<'_>) -> Result<Vec<AddressPath>, InktParseError> {
519    let mut paths = Vec::new();
520    for entry in pair.into_inner() {
521        if entry.as_rule() == Rule::address_path_entry {
522            paths.push(parse_address_path_entry(entry)?);
523        }
524    }
525    Ok(paths)
526}
527
528fn parse_address_path_entry(pair: P<'_>) -> Result<AddressPath, InktParseError> {
529    let mut inner = pair.into_inner();
530    let path_int = inner.next().ok_or_else(|| InktParseError {
531        message: "expected path index in address_path".into(),
532        line: 0,
533        col: 0,
534    })?;
535    let path = NameId(parse_u16(&path_int)?);
536    let target = parse_def_id(inner.next().ok_or_else(|| InktParseError {
537        message: "expected target def_id in address_path".into(),
538        line: 0,
539        col: 0,
540    })?)?;
541    Ok(AddressPath { path, target })
542}
543
544// ── List literals ────────────────────────────────────────────────────────────
545
546fn parse_list_literals(pair: P<'_>) -> Result<Vec<ListValue>, InktParseError> {
547    let mut literals = Vec::new();
548    for entry in pair.into_inner() {
549        if entry.as_rule() == Rule::list_literal_entry {
550            literals.push(parse_list_literal_entry(entry)?);
551        }
552    }
553    Ok(literals)
554}
555
556fn parse_list_literal_entry(pair: P<'_>) -> Result<ListValue, InktParseError> {
557    let mut items = Vec::new();
558    let mut origins = Vec::new();
559
560    for child in pair.into_inner() {
561        match child.as_rule() {
562            Rule::list_value_items => {
563                for def_pair in child.into_inner() {
564                    if def_pair.as_rule() == Rule::def_id {
565                        items.push(parse_def_id(def_pair)?);
566                    }
567                }
568            }
569            Rule::list_value_origins => {
570                for def_pair in child.into_inner() {
571                    if def_pair.as_rule() == Rule::def_id {
572                        origins.push(parse_def_id(def_pair)?);
573                    }
574                }
575            }
576            _ => {}
577        }
578    }
579
580    Ok(ListValue { items, origins })
581}
582
583// ── Containers ──────────────────────────────────────────────────────────────
584
585fn parse_container(pair: P<'_>) -> Result<(ContainerDef, ScopeLineTable), InktParseError> {
586    let mut inner = pair.into_inner();
587    let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
588        message: "expected def_id in container".into(),
589        line: 0,
590        col: 0,
591    })?)?;
592
593    let mut counting_flags = CountingFlags::empty();
594    let mut path_hash = 0i32;
595    let mut lines = Vec::new();
596    let mut bytecode = Vec::new();
597    let mut name: Option<NameId> = None;
598
599    let mut scope_id = id;
600
601    for child in inner {
602        match child.as_rule() {
603            Rule::scope_field => {
604                let scope_pair = child.into_inner().next().ok_or_else(|| InktParseError {
605                    message: "expected def_id in scope".into(),
606                    line: 0,
607                    col: 0,
608                })?;
609                scope_id = parse_def_id(scope_pair)?;
610            }
611            Rule::container_name_field => {
612                let val = child.into_inner().next().ok_or_else(|| InktParseError {
613                    message: "expected integer in container name".into(),
614                    line: 0,
615                    col: 0,
616                })?;
617                name = Some(NameId(parse_u16(&val)?));
618            }
619            Rule::flags_field => {
620                for flag in child.into_inner() {
621                    if flag.as_rule() == Rule::flag_name {
622                        match flag.as_str() {
623                            "visits" => counting_flags |= CountingFlags::VISITS,
624                            "turns" => counting_flags |= CountingFlags::TURNS,
625                            "start_only" => counting_flags |= CountingFlags::COUNT_START_ONLY,
626                            _ => {}
627                        }
628                    }
629                }
630            }
631            Rule::path_hash_field => {
632                let val = child.into_inner().next().ok_or_else(|| InktParseError {
633                    message: "expected integer in path_hash".into(),
634                    line: 0,
635                    col: 0,
636                })?;
637                path_hash = val.as_str().parse().map_err(|_| InktParseError {
638                    message: "invalid path_hash integer".into(),
639                    line: 0,
640                    col: 0,
641                })?;
642            }
643            Rule::lines_field => {
644                lines = parse_lines_field(child)?;
645            }
646            Rule::code_field => {
647                bytecode = parse_code_field(child)?;
648            }
649            _ => {}
650        }
651    }
652
653    let container = ContainerDef {
654        id,
655        scope_id,
656        name,
657        bytecode,
658        counting_flags,
659        path_hash,
660    };
661    let line_table = ScopeLineTable { scope_id, lines };
662    Ok((container, line_table))
663}
664
665fn parse_lines_field(pair: P<'_>) -> Result<Vec<LineEntry>, InktParseError> {
666    let mut entries = Vec::new();
667    for entry in pair.into_inner() {
668        if entry.as_rule() == Rule::line_entry {
669            entries.push(parse_line_entry(entry)?);
670        }
671    }
672    Ok(entries)
673}
674
675fn parse_line_entry(pair: P<'_>) -> Result<LineEntry, InktParseError> {
676    let mut inner = pair.into_inner();
677    let _index = inner.next(); // integer index (implied by position)
678    let content_pair = inner.next().ok_or_else(|| InktParseError {
679        message: "expected line content".into(),
680        line: 0,
681        col: 0,
682    })?;
683    let content = parse_line_content(content_pair)?;
684    let hash_pair = inner.next().ok_or_else(|| InktParseError {
685        message: "expected source_hash".into(),
686        line: 0,
687        col: 0,
688    })?;
689    // source_hash is @HHHHHHHHHHHHHHHH
690    let hash_str = hash_pair.as_str();
691    let source_hash = parse_hex_u64(&format!("0x{}", &hash_str[1..]))?;
692
693    let mut audio_ref = None;
694    let mut slot_info = Vec::new();
695    let mut source_location = None;
696
697    for remaining in inner {
698        match remaining.as_rule() {
699            Rule::audio_field => {
700                let s = remaining
701                    .into_inner()
702                    .next()
703                    .ok_or_else(|| InktParseError {
704                        message: "expected audio string".into(),
705                        line: 0,
706                        col: 0,
707                    })?;
708                audio_ref = Some(unescape_string(s.as_str()));
709            }
710            Rule::slots_field => {
711                for slot_entry in remaining.into_inner() {
712                    if slot_entry.as_rule() == Rule::slot_entry {
713                        let mut parts = slot_entry.into_inner();
714                        let idx_str = parts.next().map_or("0", |p| p.as_str());
715                        let idx: u8 = idx_str.parse().unwrap_or(0);
716                        let name_str = parts
717                            .next()
718                            .map_or_else(String::new, |p| unescape_string(p.as_str()));
719                        slot_info.push(SlotInfo {
720                            index: idx,
721                            name: name_str,
722                        });
723                    }
724                }
725            }
726            Rule::source_field => {
727                let mut parts = remaining.into_inner();
728                let file = parts
729                    .next()
730                    .map_or_else(String::new, |p| unescape_string(p.as_str()));
731                let start: u32 = parts
732                    .next()
733                    .and_then(|p| p.as_str().parse().ok())
734                    .unwrap_or(0);
735                let end: u32 = parts
736                    .next()
737                    .and_then(|p| p.as_str().parse().ok())
738                    .unwrap_or(0);
739                source_location = Some(SourceLocation {
740                    file,
741                    range_start: start,
742                    range_end: end,
743                });
744            }
745            _ => {}
746        }
747    }
748
749    let flags = crate::LineFlags::from_content(&content);
750    Ok(LineEntry {
751        content,
752        flags,
753        source_hash,
754        audio_ref,
755        slot_info,
756        source_location,
757    })
758}
759
760fn parse_line_content(pair: P<'_>) -> Result<LineContent, InktParseError> {
761    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
762        message: "empty line content".into(),
763        line: 0,
764        col: 0,
765    })?;
766    match inner.as_rule() {
767        Rule::string => Ok(LineContent::Plain(unescape_string(inner.as_str()))),
768        Rule::template => parse_template(inner),
769        _ => Err(err(
770            &inner,
771            format!("unexpected line content: {:?}", inner.as_rule()),
772        )),
773    }
774}
775
776fn parse_template(pair: P<'_>) -> Result<LineContent, InktParseError> {
777    let mut parts = Vec::new();
778    for child in pair.into_inner() {
779        if child.as_rule() == Rule::template_part {
780            parts.push(parse_template_part(child)?);
781        }
782    }
783    Ok(LineContent::Template(parts))
784}
785
786fn parse_template_part(pair: P<'_>) -> Result<LinePart, InktParseError> {
787    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
788        message: "empty template part".into(),
789        line: 0,
790        col: 0,
791    })?;
792    match inner.as_rule() {
793        Rule::literal_part => {
794            let s = inner.into_inner().next().ok_or_else(|| InktParseError {
795                message: "expected string in literal".into(),
796                line: 0,
797                col: 0,
798            })?;
799            Ok(LinePart::Literal(unescape_string(s.as_str())))
800        }
801        Rule::slot_part => {
802            let idx = inner.into_inner().next().ok_or_else(|| InktParseError {
803                message: "expected integer in slot".into(),
804                line: 0,
805                col: 0,
806            })?;
807            let n: u8 = idx
808                .as_str()
809                .parse()
810                .map_err(|_| err(&idx, "invalid slot index"))?;
811            Ok(LinePart::Slot(n))
812        }
813        Rule::select_part => parse_select_part(inner),
814        _ => Err(err(
815            &inner,
816            format!("unexpected template part: {:?}", inner.as_rule()),
817        )),
818    }
819}
820
821fn parse_select_part(pair: P<'_>) -> Result<LinePart, InktParseError> {
822    let mut inner = pair.into_inner();
823    let slot_pair = inner.next().ok_or_else(|| InktParseError {
824        message: "expected slot in select".into(),
825        line: 0,
826        col: 0,
827    })?;
828    let slot: u8 = slot_pair
829        .as_str()
830        .parse()
831        .map_err(|_| err(&slot_pair, "invalid slot"))?;
832
833    let mut variants = Vec::new();
834    let mut default = String::new();
835
836    for child in inner {
837        match child.as_rule() {
838            Rule::select_variant => {
839                let mut vi = child.into_inner();
840                let key_pair = vi.next().ok_or_else(|| InktParseError {
841                    message: "expected key in variant".into(),
842                    line: 0,
843                    col: 0,
844                })?;
845                let key = parse_select_key(key_pair)?;
846                let text = vi.next().ok_or_else(|| InktParseError {
847                    message: "expected text in variant".into(),
848                    line: 0,
849                    col: 0,
850                })?;
851                variants.push((key, unescape_string(text.as_str())));
852            }
853            Rule::select_default => {
854                let s = child.into_inner().next().ok_or_else(|| InktParseError {
855                    message: "expected string in default".into(),
856                    line: 0,
857                    col: 0,
858                })?;
859                default = unescape_string(s.as_str());
860            }
861            _ => {}
862        }
863    }
864
865    Ok(LinePart::Select {
866        slot,
867        variants,
868        default,
869    })
870}
871
872fn parse_select_key(pair: P<'_>) -> Result<SelectKey, InktParseError> {
873    let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
874        message: "empty select key".into(),
875        line: 0,
876        col: 0,
877    })?;
878    match inner.as_rule() {
879        Rule::cardinal_key => {
880            let cat = inner.into_inner().next().ok_or_else(|| InktParseError {
881                message: "expected plural_cat".into(),
882                line: 0,
883                col: 0,
884            })?;
885            Ok(SelectKey::Cardinal(parse_plural_cat(cat)?))
886        }
887        Rule::ordinal_key => {
888            let cat = inner.into_inner().next().ok_or_else(|| InktParseError {
889                message: "expected plural_cat".into(),
890                line: 0,
891                col: 0,
892            })?;
893            Ok(SelectKey::Ordinal(parse_plural_cat(cat)?))
894        }
895        Rule::exact_key => {
896            let n = inner.into_inner().next().ok_or_else(|| InktParseError {
897                message: "expected integer".into(),
898                line: 0,
899                col: 0,
900            })?;
901            let v: i32 = n
902                .as_str()
903                .parse()
904                .map_err(|_| err(&n, "invalid exact key"))?;
905            Ok(SelectKey::Exact(v))
906        }
907        Rule::keyword_key => {
908            let ident = inner.into_inner().next().ok_or_else(|| InktParseError {
909                message: "expected ident".into(),
910                line: 0,
911                col: 0,
912            })?;
913            Ok(SelectKey::Keyword(ident.as_str().to_owned()))
914        }
915        _ => Err(err(
916            &inner,
917            format!("unexpected select key: {:?}", inner.as_rule()),
918        )),
919    }
920}
921
922#[expect(clippy::needless_pass_by_value)]
923fn parse_plural_cat(pair: P<'_>) -> Result<PluralCategory, InktParseError> {
924    match pair.as_str() {
925        "Zero" => Ok(PluralCategory::Zero),
926        "One" => Ok(PluralCategory::One),
927        "Two" => Ok(PluralCategory::Two),
928        "Few" => Ok(PluralCategory::Few),
929        "Many" => Ok(PluralCategory::Many),
930        "Other" => Ok(PluralCategory::Other),
931        _ => Err(err(
932            &pair,
933            format!("unknown plural category: {}", pair.as_str()),
934        )),
935    }
936}
937
938// ── Code field ──────────────────────────────────────────────────────────────
939
940fn parse_code_field(pair: P<'_>) -> Result<Vec<u8>, InktParseError> {
941    let mut bytecode = Vec::new();
942    for child in pair.into_inner() {
943        if child.as_rule() == Rule::instruction {
944            let op = parse_instruction(child)?;
945            op.encode(&mut bytecode);
946        }
947    }
948    Ok(bytecode)
949}
950
951#[expect(clippy::too_many_lines)]
952fn parse_instruction(pair: P<'_>) -> Result<Opcode, InktParseError> {
953    let mut inner = pair.into_inner();
954    let mnemonic_pair = inner.next().ok_or_else(|| InktParseError {
955        message: "expected opcode mnemonic".into(),
956        line: 0,
957        col: 0,
958    })?;
959    let mnemonic = mnemonic_pair.as_str();
960
961    let operands: Vec<P<'_>> = inner.collect();
962
963    match mnemonic {
964        // Stack & literals
965        "push_int" => Ok(Opcode::PushInt(parse_operand_i32(&operands, 0, mnemonic)?)),
966        "push_float" => Ok(Opcode::PushFloat(parse_operand_f32(
967            &operands, 0, mnemonic,
968        )?)),
969        "push_bool" => {
970            let s = operand_str(&operands, 0, mnemonic)?;
971            Ok(Opcode::PushBool(s == "true"))
972        }
973        "push_string" => Ok(Opcode::PushString(parse_operand_u16(
974            &operands, 0, mnemonic,
975        )?)),
976        "push_list" => Ok(Opcode::PushList(parse_operand_u16(&operands, 0, mnemonic)?)),
977        "push_divert_target" => Ok(Opcode::PushDivertTarget(parse_operand_def_id(
978            &operands, 0, mnemonic,
979        )?)),
980        "push_null" => Ok(Opcode::PushNull),
981        "pop" => Ok(Opcode::Pop),
982        "duplicate" => Ok(Opcode::Duplicate),
983
984        // Arithmetic
985        "add" => Ok(Opcode::Add),
986        "subtract" => Ok(Opcode::Subtract),
987        "multiply" => Ok(Opcode::Multiply),
988        "divide" => Ok(Opcode::Divide),
989        "modulo" => Ok(Opcode::Modulo),
990        "negate" => Ok(Opcode::Negate),
991
992        // Comparison
993        "equal" => Ok(Opcode::Equal),
994        "not_equal" => Ok(Opcode::NotEqual),
995        "greater" => Ok(Opcode::Greater),
996        "greater_or_equal" => Ok(Opcode::GreaterOrEqual),
997        "less" => Ok(Opcode::Less),
998        "less_or_equal" => Ok(Opcode::LessOrEqual),
999
1000        // Logic
1001        "not" => Ok(Opcode::Not),
1002        "and" => Ok(Opcode::And),
1003        "or" => Ok(Opcode::Or),
1004
1005        // Global vars
1006        "get_global" => Ok(Opcode::GetGlobal(parse_operand_def_id(
1007            &operands, 0, mnemonic,
1008        )?)),
1009        "set_global" => Ok(Opcode::SetGlobal(parse_operand_def_id(
1010            &operands, 0, mnemonic,
1011        )?)),
1012
1013        // Temp vars
1014        "declare_temp" => Ok(Opcode::DeclareTemp(parse_operand_u16(
1015            &operands, 0, mnemonic,
1016        )?)),
1017        "get_temp" => Ok(Opcode::GetTemp(parse_operand_u16(&operands, 0, mnemonic)?)),
1018        "set_temp" => Ok(Opcode::SetTemp(parse_operand_u16(&operands, 0, mnemonic)?)),
1019        "get_temp_raw" => Ok(Opcode::GetTempRaw(parse_operand_u16(
1020            &operands, 0, mnemonic,
1021        )?)),
1022
1023        // Variable pointers
1024        "push_var_pointer" => Ok(Opcode::PushVarPointer(parse_operand_def_id(
1025            &operands, 0, mnemonic,
1026        )?)),
1027        "push_temp_pointer" => Ok(Opcode::PushTempPointer(parse_operand_u16(
1028            &operands, 0, mnemonic,
1029        )?)),
1030
1031        // Control flow
1032        "jump" => Ok(Opcode::Jump(parse_operand_i32(&operands, 0, mnemonic)?)),
1033        "jump_if_false" => Ok(Opcode::JumpIfFalse(parse_operand_i32(
1034            &operands, 0, mnemonic,
1035        )?)),
1036        "goto" => Ok(Opcode::Goto(parse_operand_def_id(&operands, 0, mnemonic)?)),
1037        "goto_if" => Ok(Opcode::GotoIf(parse_operand_def_id(
1038            &operands, 0, mnemonic,
1039        )?)),
1040        "goto_variable" => Ok(Opcode::GotoVariable),
1041
1042        // Container flow
1043        "enter_container" => Ok(Opcode::EnterContainer(parse_operand_def_id(
1044            &operands, 0, mnemonic,
1045        )?)),
1046        "exit_container" => Ok(Opcode::ExitContainer),
1047
1048        // Functions / tunnels
1049        "call" => Ok(Opcode::Call(parse_operand_def_id(&operands, 0, mnemonic)?)),
1050        "return" => Ok(Opcode::Return),
1051        "tunnel_call" => Ok(Opcode::TunnelCall(parse_operand_def_id(
1052            &operands, 0, mnemonic,
1053        )?)),
1054        "tunnel_return" => Ok(Opcode::TunnelReturn),
1055        "tunnel_call_variable" => Ok(Opcode::TunnelCallVariable),
1056        "call_variable" => Ok(Opcode::CallVariable),
1057
1058        // Threads
1059        "thread_call" => Ok(Opcode::ThreadCall(parse_operand_def_id(
1060            &operands, 0, mnemonic,
1061        )?)),
1062        "thread_start" => Ok(Opcode::ThreadStart),
1063        "thread_done" => Ok(Opcode::ThreadDone),
1064
1065        // Output
1066        "emit_line" => {
1067            let idx = parse_operand_u16(&operands, 0, mnemonic)?;
1068            let slots = parse_operand_u8(&operands, 1, mnemonic)?;
1069            Ok(Opcode::EmitLine(idx, slots))
1070        }
1071        "emit_value" => Ok(Opcode::EmitValue),
1072        "emit_newline" => Ok(Opcode::EmitNewline),
1073        "spring" => Ok(Opcode::Spring),
1074        "glue" => Ok(Opcode::Glue),
1075        "begin_tag" => Ok(Opcode::BeginTag),
1076        "end_tag" => Ok(Opcode::EndTag),
1077        "eval_line" => {
1078            let idx = parse_operand_u16(&operands, 0, mnemonic)?;
1079            let slots = parse_operand_u8(&operands, 1, mnemonic)?;
1080            Ok(Opcode::EvalLine(idx, slots))
1081        }
1082
1083        // Choices
1084        "begin_choice" => {
1085            let flags = parse_choice_flags_operand(&operands, 0, mnemonic)?;
1086            let target = parse_operand_def_id(&operands, 1, mnemonic)?;
1087            Ok(Opcode::BeginChoice(flags, target))
1088        }
1089        "end_choice" => Ok(Opcode::EndChoice),
1090
1091        // Sequences
1092        "sequence" => {
1093            let kind_str = operand_str(&operands, 0, mnemonic)?;
1094            let kind = match kind_str {
1095                "cycle" => SequenceKind::Cycle,
1096                "stopping" => SequenceKind::Stopping,
1097                "once_only" => SequenceKind::OnceOnly,
1098                "shuffle" => SequenceKind::Shuffle,
1099                _ => {
1100                    return Err(InktParseError {
1101                        message: format!("unknown sequence kind: {kind_str}"),
1102                        line: 0,
1103                        col: 0,
1104                    });
1105                }
1106            };
1107            let count: u8 =
1108                operand_str(&operands, 1, mnemonic)?
1109                    .parse()
1110                    .map_err(|_| InktParseError {
1111                        message: "invalid sequence count".into(),
1112                        line: 0,
1113                        col: 0,
1114                    })?;
1115            Ok(Opcode::Sequence(kind, count))
1116        }
1117        "sequence_branch" => Ok(Opcode::SequenceBranch(parse_operand_i32(
1118            &operands, 0, mnemonic,
1119        )?)),
1120
1121        // Intrinsics
1122        "visit_count" => Ok(Opcode::VisitCount),
1123        "current_visit_count" => Ok(Opcode::CurrentVisitCount),
1124        "turns_since" => Ok(Opcode::TurnsSince),
1125        "turn_index" => Ok(Opcode::TurnIndex),
1126        "choice_count" => Ok(Opcode::ChoiceCount),
1127        "random" => Ok(Opcode::Random),
1128        "seed_random" => Ok(Opcode::SeedRandom),
1129
1130        // Casts / math
1131        "cast_to_int" => Ok(Opcode::CastToInt),
1132        "cast_to_float" => Ok(Opcode::CastToFloat),
1133        "floor" => Ok(Opcode::Floor),
1134        "ceiling" => Ok(Opcode::Ceiling),
1135        "pow" => Ok(Opcode::Pow),
1136        "min" => Ok(Opcode::Min),
1137        "max" => Ok(Opcode::Max),
1138
1139        // External fns
1140        "call_external" => {
1141            let id = parse_operand_def_id(&operands, 0, mnemonic)?;
1142            // "argc=N" is parsed as a kv_operand. Extract the value after "=".
1143            let kv_str = operand_str(&operands, 1, mnemonic)?;
1144            let argc_str = kv_str.strip_prefix("argc=").unwrap_or(kv_str);
1145            let argc: u8 = argc_str.parse().map_err(|_| InktParseError {
1146                message: format!("invalid argc in call_external: {kv_str}"),
1147                line: 0,
1148                col: 0,
1149            })?;
1150            Ok(Opcode::CallExternal(id, argc))
1151        }
1152
1153        // List ops
1154        "list_contains" => Ok(Opcode::ListContains),
1155        "list_not_contains" => Ok(Opcode::ListNotContains),
1156        "list_intersect" => Ok(Opcode::ListIntersect),
1157        "list_all" => Ok(Opcode::ListAll),
1158        "list_invert" => Ok(Opcode::ListInvert),
1159        "list_count" => Ok(Opcode::ListCount),
1160        "list_min" => Ok(Opcode::ListMin),
1161        "list_max" => Ok(Opcode::ListMax),
1162        "list_value" => Ok(Opcode::ListValue),
1163        "list_range" => Ok(Opcode::ListRange),
1164        "list_from_int" => Ok(Opcode::ListFromInt),
1165        "list_random" => Ok(Opcode::ListRandom),
1166
1167        // Lifecycle
1168        "done" => Ok(Opcode::Done),
1169        "yield" => Ok(Opcode::Yield),
1170        "end" => Ok(Opcode::End),
1171        "nop" => Ok(Opcode::Nop),
1172
1173        // String eval
1174        "begin_string_eval" => Ok(Opcode::BeginStringEval),
1175        "end_string_eval" => Ok(Opcode::EndStringEval),
1176
1177        // Fragment capture
1178        "begin_fragment" => Ok(Opcode::BeginFragment),
1179        "end_fragment" => Ok(Opcode::EndFragment),
1180
1181        // Debug
1182        "source_location" => {
1183            // Written as "source_location LINE:COL" — parsed as source_loc operand
1184            let s = operand_str(&operands, 0, mnemonic)?;
1185            let parts: Vec<&str> = s.split(':').collect();
1186            if parts.len() != 2 {
1187                return Err(InktParseError {
1188                    message: format!("invalid source_location: {s}"),
1189                    line: 0,
1190                    col: 0,
1191                });
1192            }
1193            let line: u32 = parts[0].parse().map_err(|_| InktParseError {
1194                message: "invalid line".into(),
1195                line: 0,
1196                col: 0,
1197            })?;
1198            let col: u32 = parts[1].parse().map_err(|_| InktParseError {
1199                message: "invalid col".into(),
1200                line: 0,
1201                col: 0,
1202            })?;
1203            Ok(Opcode::SourceLocation(line, col))
1204        }
1205
1206        _ => Err(InktParseError {
1207            message: format!("unknown opcode: {mnemonic}"),
1208            line: mnemonic_pair.line_col().0,
1209            col: mnemonic_pair.line_col().1,
1210        }),
1211    }
1212}
1213
1214fn parse_choice_flags_operand(
1215    operands: &[P<'_>],
1216    idx: usize,
1217    context: &str,
1218) -> Result<ChoiceFlags, InktParseError> {
1219    let s = operand_str(operands, idx, context)?;
1220    let mut flags = ChoiceFlags {
1221        has_condition: false,
1222        has_start_content: false,
1223        has_choice_only_content: false,
1224        once_only: false,
1225        is_invisible_default: false,
1226    };
1227    if s == "none" {
1228        return Ok(flags);
1229    }
1230    for part in s.split('+') {
1231        match part {
1232            "cond" => flags.has_condition = true,
1233            "start" => flags.has_start_content = true,
1234            "choice_only" => flags.has_choice_only_content = true,
1235            "once" => flags.once_only = true,
1236            "invis_default" => flags.is_invisible_default = true,
1237            _ => {
1238                return Err(InktParseError {
1239                    message: format!("unknown choice flag: {part}"),
1240                    line: 0,
1241                    col: 0,
1242                });
1243            }
1244        }
1245    }
1246    Ok(flags)
1247}
1248
1249// ── Operand helpers ─────────────────────────────────────────────────────────
1250
1251fn operand_str<'a>(
1252    operands: &'a [P<'_>],
1253    idx: usize,
1254    context: &str,
1255) -> Result<&'a str, InktParseError> {
1256    let op = operands.get(idx).ok_or_else(|| InktParseError {
1257        message: format!("missing operand {idx} for {context}"),
1258        line: 0,
1259        col: 0,
1260    })?;
1261    // The operand rule wraps the actual value. Get the inner pair.
1262    let inner = op.clone().into_inner().next();
1263    match inner {
1264        Some(p) => Ok(p.as_str()),
1265        None => Ok(op.as_str()),
1266    }
1267}
1268
1269fn parse_operand_i32(operands: &[P<'_>], idx: usize, context: &str) -> Result<i32, InktParseError> {
1270    let s = operand_str(operands, idx, context)?;
1271    s.parse().map_err(|_| InktParseError {
1272        message: format!("invalid i32 operand for {context}: {s}"),
1273        line: 0,
1274        col: 0,
1275    })
1276}
1277
1278fn parse_operand_f32(operands: &[P<'_>], idx: usize, context: &str) -> Result<f32, InktParseError> {
1279    let s = operand_str(operands, idx, context)?;
1280    s.parse().map_err(|_| InktParseError {
1281        message: format!("invalid f32 operand for {context}: {s}"),
1282        line: 0,
1283        col: 0,
1284    })
1285}
1286
1287fn parse_operand_u8(operands: &[P<'_>], idx: usize, context: &str) -> Result<u8, InktParseError> {
1288    let s = operand_str(operands, idx, context)?;
1289    s.parse().map_err(|_| InktParseError {
1290        message: format!("invalid u8 operand for {context}: {s}"),
1291        line: 0,
1292        col: 0,
1293    })
1294}
1295
1296fn parse_operand_u16(operands: &[P<'_>], idx: usize, context: &str) -> Result<u16, InktParseError> {
1297    let s = operand_str(operands, idx, context)?;
1298    s.parse().map_err(|_| InktParseError {
1299        message: format!("invalid u16 operand for {context}: {s}"),
1300        line: 0,
1301        col: 0,
1302    })
1303}
1304
1305fn parse_operand_def_id(
1306    operands: &[P<'_>],
1307    idx: usize,
1308    context: &str,
1309) -> Result<DefinitionId, InktParseError> {
1310    let op = operands.get(idx).ok_or_else(|| InktParseError {
1311        message: format!("missing operand {idx} for {context}"),
1312        line: 0,
1313        col: 0,
1314    })?;
1315    // Drill into the operand to get the def_id inner pair
1316    let inner = op.clone().into_inner().next().unwrap_or_else(|| op.clone());
1317    parse_def_id(inner)
1318}
1319
1320// ── Shared parse helpers ────────────────────────────────────────────────────
1321
1322#[expect(clippy::needless_pass_by_value)]
1323fn parse_def_id(pair: P<'_>) -> Result<DefinitionId, InktParseError> {
1324    let s = pair.as_str();
1325    // Format: $TT_HHHHHHHHHHHHHH
1326    if !s.starts_with('$') || s.len() < 4 {
1327        return Err(err(&pair, format!("invalid def_id: {s}")));
1328    }
1329    let tag_str = &s[1..3];
1330    let hash_str = &s[4..]; // skip $TT_
1331
1332    let tag_byte = u8::from_str_radix(tag_str, 16)
1333        .map_err(|_| err(&pair, format!("invalid tag: {tag_str}")))?;
1334    let hash = u64::from_str_radix(hash_str, 16)
1335        .map_err(|_| err(&pair, format!("invalid hash: {hash_str}")))?;
1336
1337    let tag = crate::id::DefinitionTag::from_u8(tag_byte)
1338        .ok_or_else(|| err(&pair, format!("unknown tag byte: {tag_byte:#04x}")))?;
1339
1340    Ok(DefinitionId::new(tag, hash))
1341}
1342
1343fn parse_hex_u32(s: &str) -> u32 {
1344    let hex = s.strip_prefix("0x").unwrap_or(s);
1345    u32::from_str_radix(hex, 16).unwrap_or(0)
1346}
1347
1348fn parse_hex_u64(s: &str) -> Result<u64, InktParseError> {
1349    let hex = s.strip_prefix("0x").unwrap_or(s);
1350    u64::from_str_radix(hex, 16).map_err(|_| InktParseError {
1351        message: format!("invalid hex: {s}"),
1352        line: 0,
1353        col: 0,
1354    })
1355}
1356
1357fn parse_u16(pair: &P<'_>) -> Result<u16, InktParseError> {
1358    pair.as_str().parse().map_err(|_| err(pair, "invalid u16"))
1359}
1360
1361fn unescape_string(s: &str) -> String {
1362    // Strip surrounding quotes
1363    let inner = &s[1..s.len() - 1];
1364    let mut out = String::with_capacity(inner.len());
1365    let mut chars = inner.chars();
1366    while let Some(c) = chars.next() {
1367        if c == '\\' {
1368            match chars.next() {
1369                Some('\\') | None => out.push('\\'),
1370                Some('"') => out.push('"'),
1371                Some('n') => out.push('\n'),
1372                Some('t') => out.push('\t'),
1373                Some('r') => out.push('\r'),
1374                Some(other) => {
1375                    out.push('\\');
1376                    out.push(other);
1377                }
1378            }
1379        } else {
1380            out.push(c);
1381        }
1382    }
1383    out
1384}
1385
1386fn next_rule<'a>(
1387    iter: &mut impl Iterator<Item = P<'a>>,
1388    expected: Rule,
1389    context: &str,
1390) -> Result<P<'a>, InktParseError> {
1391    for pair in iter.by_ref() {
1392        if pair.as_rule() == expected {
1393            return Ok(pair);
1394        }
1395    }
1396    Err(InktParseError {
1397        message: format!("expected {expected:?} in {context}"),
1398        line: 0,
1399        col: 0,
1400    })
1401}