use pest::Parser;
use pest_derive::Parser;
use crate::counting::CountingFlags;
use crate::definition::{
AddressDef, AddressPath, ContainerDef, ExternalFnDef, GlobalVarDef, LineEntry, ListDef,
ListItemDef, ScopeLineTable, SlotInfo, SourceLocation,
};
use crate::id::{DefinitionId, NameId};
use crate::line::{LineContent, LinePart, PluralCategory, SelectKey};
use crate::opcode::{ChoiceFlags, Opcode, SequenceKind};
use crate::story::StoryData;
use crate::value::{ListValue, Value, ValueType};
#[derive(Parser)]
#[grammar = "inkt/inkt.pest"]
struct InktParser;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InktParseError {
pub message: String,
pub line: usize,
pub col: usize,
}
impl core::fmt::Display for InktParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}:{}: {}", self.line, self.col, self.message)
}
}
impl std::error::Error for InktParseError {}
pub fn read_inkt(input: &str) -> Result<StoryData, InktParseError> {
let pairs = InktParser::parse(Rule::story, input).map_err(|e| {
let (line, col) = match e.line_col {
pest::error::LineColLocation::Pos(pos) => pos,
pest::error::LineColLocation::Span(start, _) => start,
};
InktParseError {
message: e.to_string(),
line,
col,
}
})?;
let story_pair = pairs.into_iter().next().ok_or_else(|| InktParseError {
message: "no story node".into(),
line: 1,
col: 1,
})?;
parse_story(story_pair)
}
type P<'a> = pest::iterators::Pair<'a, Rule>;
fn err(pair: &P<'_>, msg: impl Into<String>) -> InktParseError {
let (line, col) = pair.line_col();
InktParseError {
message: msg.into(),
line,
col,
}
}
fn parse_story(pair: P<'_>) -> Result<StoryData, InktParseError> {
let mut name_table = Vec::new();
let mut variables = Vec::new();
let mut list_defs = Vec::new();
let mut list_items = Vec::new();
let mut externals = Vec::new();
let mut addresses = Vec::new();
let mut address_paths = Vec::new();
let mut containers = Vec::new();
let mut line_tables = Vec::new();
let mut list_literals = Vec::new();
let mut source_checksum = 0u32;
for inner in pair.into_inner() {
match inner.as_rule() {
Rule::story_checksum => {
if let Some(hex_pair) = inner.into_inner().next() {
source_checksum = parse_hex_u32(hex_pair.as_str());
}
}
Rule::name_table => name_table = parse_name_table(inner)?,
Rule::globals => variables = parse_globals(inner)?,
Rule::lists => list_defs = parse_lists(inner)?,
Rule::list_items => list_items = parse_list_items(inner)?,
Rule::externals => externals = parse_externals(inner)?,
Rule::addresses => addresses = parse_addresses(inner)?,
Rule::address_paths => address_paths = parse_address_paths(inner)?,
Rule::list_literals => list_literals = parse_list_literals(inner)?,
Rule::container => {
let (container, lt) = parse_container(inner)?;
let is_scope_owner = container.scope_id == container.id;
containers.push(container);
if is_scope_owner {
line_tables.push(lt);
}
}
_ => {}
}
}
line_tables.sort_by_key(|lt| lt.scope_id.to_raw());
Ok(StoryData {
containers,
line_tables,
variables,
list_defs,
list_items,
externals,
addresses,
address_paths,
name_table,
list_literals,
source_checksum,
})
}
fn parse_name_table(pair: P<'_>) -> Result<Vec<String>, InktParseError> {
let mut names = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::name_entry {
let mut inner = entry.into_inner();
let _index = inner.next(); let s = inner.next().ok_or_else(|| InktParseError {
message: "expected string in name_entry".into(),
line: 0,
col: 0,
})?;
names.push(unescape_string(s.as_str()));
}
}
Ok(names)
}
fn parse_globals(pair: P<'_>) -> Result<Vec<GlobalVarDef>, InktParseError> {
let mut vars = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::global_entry {
vars.push(parse_global_entry(entry)?);
}
}
Ok(vars)
}
fn parse_global_entry(pair: P<'_>) -> Result<GlobalVarDef, InktParseError> {
let mut inner = pair.into_inner();
let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
message: "expected def_id in global".into(),
line: 0,
col: 0,
})?)?;
let type_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected type_name in global".into(),
line: 0,
col: 0,
})?;
let value_type = parse_value_type(type_pair)?;
let value_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected value in global".into(),
line: 0,
col: 0,
})?;
let default_value = parse_value(value_pair, Some(value_type))?;
let mut mutable = false;
let mut name = NameId(0);
for remaining in inner {
match remaining.as_rule() {
Rule::mutable_flag => mutable = true,
Rule::integer => {
name = NameId(parse_u16(&remaining)?);
}
_ => {}
}
}
Ok(GlobalVarDef {
id,
name,
value_type,
default_value,
mutable,
})
}
#[expect(clippy::needless_pass_by_value)]
fn parse_value_type(pair: P<'_>) -> Result<ValueType, InktParseError> {
let s = pair.as_str();
match s {
"int" => Ok(ValueType::Int),
"float" => Ok(ValueType::Float),
"bool" => Ok(ValueType::Bool),
"string" => Ok(ValueType::String),
"list" => Ok(ValueType::List),
"divert_target" => Ok(ValueType::DivertTarget),
"var_pointer" => Ok(ValueType::VariablePointer),
"temp_pointer" => Ok(ValueType::TempPointer),
"fragment_ref" => Ok(ValueType::FragmentRef),
"null" => Ok(ValueType::Null),
_ => Err(err(&pair, format!("unknown value type: {s}"))),
}
}
fn parse_value(pair: P<'_>, type_hint: Option<ValueType>) -> Result<Value, InktParseError> {
let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
message: "empty value".into(),
line: 0,
col: 0,
})?;
match inner.as_rule() {
Rule::integer => {
match type_hint {
Some(ValueType::Float) => {
let n: f32 = inner
.as_str()
.parse()
.map_err(|_| err(&inner, "invalid float"))?;
Ok(Value::Float(n))
}
Some(ValueType::Bool) => {
let n: i32 = inner
.as_str()
.parse()
.map_err(|_| err(&inner, "invalid integer"))?;
Ok(Value::Bool(n != 0))
}
_ => {
let n: i32 = inner
.as_str()
.parse()
.map_err(|_| err(&inner, "invalid integer"))?;
Ok(Value::Int(n))
}
}
}
Rule::float => {
let n: f32 = inner
.as_str()
.parse()
.map_err(|_| err(&inner, "invalid float"))?;
Ok(Value::Float(n))
}
Rule::bool_value => Ok(Value::Bool(inner.as_str() == "true")),
Rule::string => Ok(Value::String(unescape_string(inner.as_str()).into())),
Rule::def_id => Ok(Value::DivertTarget(parse_def_id(inner)?)),
Rule::null_value => Ok(Value::Null),
Rule::list_value => parse_list_value(inner),
Rule::var_pointer_value => {
let id_pair = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected def_id in var_pointer".into(),
line: 0,
col: 0,
})?;
Ok(Value::VariablePointer(parse_def_id(id_pair)?))
}
Rule::fragment_ref_value => {
let idx_pair = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected integer in fragment_ref".into(),
line: 0,
col: 0,
})?;
let idx: u32 = idx_pair.as_str().parse().map_err(|_| InktParseError {
message: "invalid fragment_ref index".into(),
line: 0,
col: 0,
})?;
Ok(Value::FragmentRef(idx))
}
_ => Err(err(
&inner,
format!("unexpected value rule: {:?}", inner.as_rule()),
)),
}
}
fn parse_list_value(pair: P<'_>) -> Result<Value, InktParseError> {
let mut items = Vec::new();
let mut origins = Vec::new();
for child in pair.into_inner() {
match child.as_rule() {
Rule::list_value_items => {
for def_pair in child.into_inner() {
if def_pair.as_rule() == Rule::def_id {
items.push(parse_def_id(def_pair)?);
}
}
}
Rule::list_value_origins => {
for def_pair in child.into_inner() {
if def_pair.as_rule() == Rule::def_id {
origins.push(parse_def_id(def_pair)?);
}
}
}
_ => {}
}
}
Ok(Value::List(ListValue { items, origins }.into()))
}
fn parse_lists(pair: P<'_>) -> Result<Vec<ListDef>, InktParseError> {
let mut defs = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::list_entry {
defs.push(parse_list_entry(entry)?);
}
}
Ok(defs)
}
fn parse_list_entry(pair: P<'_>) -> Result<ListDef, InktParseError> {
let mut inner = pair.into_inner();
let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
message: "expected def_id in list".into(),
line: 0,
col: 0,
})?)?;
let name_int = inner.next().ok_or_else(|| InktParseError {
message: "expected name integer in list".into(),
line: 0,
col: 0,
})?;
let name = NameId(parse_u16(&name_int)?);
let mut items = Vec::new();
for remaining in inner {
if remaining.as_rule() == Rule::list_item_inline {
let mut li_inner = remaining.into_inner();
let item_name_id = parse_u16(&li_inner.next().ok_or_else(|| InktParseError {
message: "expected name in list item".into(),
line: 0,
col: 0,
})?)?;
let ordinal: i32 = li_inner
.next()
.ok_or_else(|| InktParseError {
message: "expected ordinal in list item".into(),
line: 0,
col: 0,
})?
.as_str()
.parse()
.map_err(|_| InktParseError {
message: "invalid ordinal".into(),
line: 0,
col: 0,
})?;
items.push((NameId(item_name_id), ordinal));
}
}
Ok(ListDef { id, name, items })
}
fn parse_list_items(pair: P<'_>) -> Result<Vec<ListItemDef>, InktParseError> {
let mut items = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::list_item_entry {
items.push(parse_list_item_entry(entry)?);
}
}
Ok(items)
}
fn parse_list_item_entry(pair: P<'_>) -> Result<ListItemDef, InktParseError> {
let mut inner = pair.into_inner();
let id = parse_def_id(next_rule(&mut inner, Rule::def_id, "list_item id")?)?;
let origin = parse_def_id(next_rule(&mut inner, Rule::def_id, "list_item origin")?)?;
let ordinal: i32 = next_rule(&mut inner, Rule::integer, "list_item ordinal")?
.as_str()
.parse()
.map_err(|_| InktParseError {
message: "invalid ordinal".into(),
line: 0,
col: 0,
})?;
let name_val =
next_rule(&mut inner, Rule::integer, "list_item name").map_or(Ok(0), |p| parse_u16(&p))?;
Ok(ListItemDef {
id,
origin,
ordinal,
name: NameId(name_val),
})
}
fn parse_externals(pair: P<'_>) -> Result<Vec<ExternalFnDef>, InktParseError> {
let mut exts = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::extern_entry {
exts.push(parse_extern_entry(entry)?);
}
}
Ok(exts)
}
fn parse_extern_entry(pair: P<'_>) -> Result<ExternalFnDef, InktParseError> {
let mut inner = pair.into_inner();
let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
message: "expected def_id in extern".into(),
line: 0,
col: 0,
})?)?;
let argc_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected argc in extern".into(),
line: 0,
col: 0,
})?;
let arg_count: u8 = argc_pair
.as_str()
.parse()
.map_err(|_| err(&argc_pair, "invalid argc"))?;
let name_int = inner.next().ok_or_else(|| InktParseError {
message: "expected name in extern".into(),
line: 0,
col: 0,
})?;
let name = NameId(parse_u16(&name_int)?);
let mut fallback = None;
for remaining in inner {
if remaining.as_rule() == Rule::fallback {
let fb_inner = remaining
.into_inner()
.next()
.ok_or_else(|| InktParseError {
message: "expected def_id in fallback".into(),
line: 0,
col: 0,
})?;
fallback = Some(parse_def_id(fb_inner)?);
}
}
Ok(ExternalFnDef {
id,
name,
arg_count,
fallback,
})
}
fn parse_addresses(pair: P<'_>) -> Result<Vec<AddressDef>, InktParseError> {
let mut addresses = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::address_entry {
addresses.push(parse_address_entry(entry)?);
}
}
Ok(addresses)
}
fn parse_address_entry(pair: P<'_>) -> Result<AddressDef, InktParseError> {
let mut inner = pair.into_inner();
let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
message: "expected def_id in address".into(),
line: 0,
col: 0,
})?)?;
let container_id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
message: "expected container_id in address".into(),
line: 0,
col: 0,
})?)?;
let offset_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected byte_offset in address".into(),
line: 0,
col: 0,
})?;
let byte_offset: u32 = offset_pair
.as_str()
.parse()
.map_err(|_| err(&offset_pair, "invalid byte_offset"))?;
Ok(AddressDef {
id,
container_id,
byte_offset,
})
}
fn parse_address_paths(pair: P<'_>) -> Result<Vec<AddressPath>, InktParseError> {
let mut paths = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::address_path_entry {
paths.push(parse_address_path_entry(entry)?);
}
}
Ok(paths)
}
fn parse_address_path_entry(pair: P<'_>) -> Result<AddressPath, InktParseError> {
let mut inner = pair.into_inner();
let path_int = inner.next().ok_or_else(|| InktParseError {
message: "expected path index in address_path".into(),
line: 0,
col: 0,
})?;
let path = NameId(parse_u16(&path_int)?);
let target = parse_def_id(inner.next().ok_or_else(|| InktParseError {
message: "expected target def_id in address_path".into(),
line: 0,
col: 0,
})?)?;
Ok(AddressPath { path, target })
}
fn parse_list_literals(pair: P<'_>) -> Result<Vec<ListValue>, InktParseError> {
let mut literals = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::list_literal_entry {
literals.push(parse_list_literal_entry(entry)?);
}
}
Ok(literals)
}
fn parse_list_literal_entry(pair: P<'_>) -> Result<ListValue, InktParseError> {
let mut items = Vec::new();
let mut origins = Vec::new();
for child in pair.into_inner() {
match child.as_rule() {
Rule::list_value_items => {
for def_pair in child.into_inner() {
if def_pair.as_rule() == Rule::def_id {
items.push(parse_def_id(def_pair)?);
}
}
}
Rule::list_value_origins => {
for def_pair in child.into_inner() {
if def_pair.as_rule() == Rule::def_id {
origins.push(parse_def_id(def_pair)?);
}
}
}
_ => {}
}
}
Ok(ListValue { items, origins })
}
fn parse_container(pair: P<'_>) -> Result<(ContainerDef, ScopeLineTable), InktParseError> {
let mut inner = pair.into_inner();
let id = parse_def_id(inner.next().ok_or_else(|| InktParseError {
message: "expected def_id in container".into(),
line: 0,
col: 0,
})?)?;
let mut counting_flags = CountingFlags::empty();
let mut path_hash = 0i32;
let mut param_count = 0u8;
let mut lines = Vec::new();
let mut bytecode = Vec::new();
let mut name: Option<NameId> = None;
let mut scope_id = id;
for child in inner {
match child.as_rule() {
Rule::scope_field => {
let scope_pair = child.into_inner().next().ok_or_else(|| InktParseError {
message: "expected def_id in scope".into(),
line: 0,
col: 0,
})?;
scope_id = parse_def_id(scope_pair)?;
}
Rule::container_name_field => {
let val = child.into_inner().next().ok_or_else(|| InktParseError {
message: "expected integer in container name".into(),
line: 0,
col: 0,
})?;
name = Some(NameId(parse_u16(&val)?));
}
Rule::flags_field => {
for flag in child.into_inner() {
if flag.as_rule() == Rule::flag_name {
match flag.as_str() {
"visits" => counting_flags |= CountingFlags::VISITS,
"turns" => counting_flags |= CountingFlags::TURNS,
"start_only" => counting_flags |= CountingFlags::COUNT_START_ONLY,
_ => {}
}
}
}
}
Rule::path_hash_field => {
let val = child.into_inner().next().ok_or_else(|| InktParseError {
message: "expected integer in path_hash".into(),
line: 0,
col: 0,
})?;
path_hash = val.as_str().parse().map_err(|_| InktParseError {
message: "invalid path_hash integer".into(),
line: 0,
col: 0,
})?;
}
Rule::params_field => {
let val = child.into_inner().next().ok_or_else(|| InktParseError {
message: "expected integer in params".into(),
line: 0,
col: 0,
})?;
param_count = val.as_str().parse().map_err(|_| InktParseError {
message: "invalid params integer".into(),
line: 0,
col: 0,
})?;
}
Rule::lines_field => {
lines = parse_lines_field(child)?;
}
Rule::code_field => {
bytecode = parse_code_field(child)?;
}
_ => {}
}
}
let container = ContainerDef {
id,
scope_id,
name,
bytecode,
counting_flags,
path_hash,
param_count,
};
let line_table = ScopeLineTable { scope_id, lines };
Ok((container, line_table))
}
fn parse_lines_field(pair: P<'_>) -> Result<Vec<LineEntry>, InktParseError> {
let mut entries = Vec::new();
for entry in pair.into_inner() {
if entry.as_rule() == Rule::line_entry {
entries.push(parse_line_entry(entry)?);
}
}
Ok(entries)
}
fn parse_line_entry(pair: P<'_>) -> Result<LineEntry, InktParseError> {
let mut inner = pair.into_inner();
let _index = inner.next(); let content_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected line content".into(),
line: 0,
col: 0,
})?;
let content = parse_line_content(content_pair)?;
let hash_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected source_hash".into(),
line: 0,
col: 0,
})?;
let hash_str = hash_pair.as_str();
let source_hash = parse_hex_u64(&format!("0x{}", &hash_str[1..]))?;
let mut audio_ref = None;
let mut slot_info = Vec::new();
let mut source_location = None;
for remaining in inner {
match remaining.as_rule() {
Rule::audio_field => {
let s = remaining
.into_inner()
.next()
.ok_or_else(|| InktParseError {
message: "expected audio string".into(),
line: 0,
col: 0,
})?;
audio_ref = Some(unescape_string(s.as_str()));
}
Rule::slots_field => {
for slot_entry in remaining.into_inner() {
if slot_entry.as_rule() == Rule::slot_entry {
let mut parts = slot_entry.into_inner();
let idx_str = parts.next().map_or("0", |p| p.as_str());
let idx: u8 = idx_str.parse().unwrap_or(0);
let name_str = parts
.next()
.map_or_else(String::new, |p| unescape_string(p.as_str()));
slot_info.push(SlotInfo {
index: idx,
name: name_str,
});
}
}
}
Rule::source_field => {
let mut parts = remaining.into_inner();
let file = parts
.next()
.map_or_else(String::new, |p| unescape_string(p.as_str()));
let start: u32 = parts
.next()
.and_then(|p| p.as_str().parse().ok())
.unwrap_or(0);
let end: u32 = parts
.next()
.and_then(|p| p.as_str().parse().ok())
.unwrap_or(0);
source_location = Some(SourceLocation {
file,
range_start: start,
range_end: end,
});
}
_ => {}
}
}
let flags = crate::LineFlags::from_content(&content);
Ok(LineEntry {
content,
flags,
source_hash,
audio_ref,
slot_info,
source_location,
})
}
fn parse_line_content(pair: P<'_>) -> Result<LineContent, InktParseError> {
let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
message: "empty line content".into(),
line: 0,
col: 0,
})?;
match inner.as_rule() {
Rule::string => Ok(LineContent::Plain(unescape_string(inner.as_str()))),
Rule::template => parse_template(inner),
_ => Err(err(
&inner,
format!("unexpected line content: {:?}", inner.as_rule()),
)),
}
}
fn parse_template(pair: P<'_>) -> Result<LineContent, InktParseError> {
let mut parts = Vec::new();
for child in pair.into_inner() {
if child.as_rule() == Rule::template_part {
parts.push(parse_template_part(child)?);
}
}
Ok(LineContent::Template(parts))
}
fn parse_template_part(pair: P<'_>) -> Result<LinePart, InktParseError> {
let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
message: "empty template part".into(),
line: 0,
col: 0,
})?;
match inner.as_rule() {
Rule::literal_part => {
let s = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected string in literal".into(),
line: 0,
col: 0,
})?;
Ok(LinePart::Literal(unescape_string(s.as_str())))
}
Rule::slot_part => {
let idx = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected integer in slot".into(),
line: 0,
col: 0,
})?;
let n: u8 = idx
.as_str()
.parse()
.map_err(|_| err(&idx, "invalid slot index"))?;
Ok(LinePart::Slot(n))
}
Rule::select_part => parse_select_part(inner),
_ => Err(err(
&inner,
format!("unexpected template part: {:?}", inner.as_rule()),
)),
}
}
fn parse_select_part(pair: P<'_>) -> Result<LinePart, InktParseError> {
let mut inner = pair.into_inner();
let slot_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected slot in select".into(),
line: 0,
col: 0,
})?;
let slot: u8 = slot_pair
.as_str()
.parse()
.map_err(|_| err(&slot_pair, "invalid slot"))?;
let mut variants = Vec::new();
let mut default = String::new();
for child in inner {
match child.as_rule() {
Rule::select_variant => {
let mut vi = child.into_inner();
let key_pair = vi.next().ok_or_else(|| InktParseError {
message: "expected key in variant".into(),
line: 0,
col: 0,
})?;
let key = parse_select_key(key_pair)?;
let text = vi.next().ok_or_else(|| InktParseError {
message: "expected text in variant".into(),
line: 0,
col: 0,
})?;
variants.push((key, unescape_string(text.as_str())));
}
Rule::select_default => {
let s = child.into_inner().next().ok_or_else(|| InktParseError {
message: "expected string in default".into(),
line: 0,
col: 0,
})?;
default = unescape_string(s.as_str());
}
_ => {}
}
}
Ok(LinePart::Select {
slot,
variants,
default,
})
}
fn parse_select_key(pair: P<'_>) -> Result<SelectKey, InktParseError> {
let inner = pair.into_inner().next().ok_or_else(|| InktParseError {
message: "empty select key".into(),
line: 0,
col: 0,
})?;
match inner.as_rule() {
Rule::cardinal_key => {
let cat = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected plural_cat".into(),
line: 0,
col: 0,
})?;
Ok(SelectKey::Cardinal(parse_plural_cat(cat)?))
}
Rule::ordinal_key => {
let cat = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected plural_cat".into(),
line: 0,
col: 0,
})?;
Ok(SelectKey::Ordinal(parse_plural_cat(cat)?))
}
Rule::exact_key => {
let n = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected integer".into(),
line: 0,
col: 0,
})?;
let v: i32 = n
.as_str()
.parse()
.map_err(|_| err(&n, "invalid exact key"))?;
Ok(SelectKey::Exact(v))
}
Rule::keyword_key => {
let ident = inner.into_inner().next().ok_or_else(|| InktParseError {
message: "expected ident".into(),
line: 0,
col: 0,
})?;
Ok(SelectKey::Keyword(ident.as_str().to_owned()))
}
_ => Err(err(
&inner,
format!("unexpected select key: {:?}", inner.as_rule()),
)),
}
}
#[expect(clippy::needless_pass_by_value)]
fn parse_plural_cat(pair: P<'_>) -> Result<PluralCategory, InktParseError> {
match pair.as_str() {
"Zero" => Ok(PluralCategory::Zero),
"One" => Ok(PluralCategory::One),
"Two" => Ok(PluralCategory::Two),
"Few" => Ok(PluralCategory::Few),
"Many" => Ok(PluralCategory::Many),
"Other" => Ok(PluralCategory::Other),
_ => Err(err(
&pair,
format!("unknown plural category: {}", pair.as_str()),
)),
}
}
fn parse_code_field(pair: P<'_>) -> Result<Vec<u8>, InktParseError> {
let mut bytecode = Vec::new();
for child in pair.into_inner() {
if child.as_rule() == Rule::instruction {
let op = parse_instruction(child)?;
op.encode(&mut bytecode);
}
}
Ok(bytecode)
}
#[expect(clippy::too_many_lines)]
fn parse_instruction(pair: P<'_>) -> Result<Opcode, InktParseError> {
let mut inner = pair.into_inner();
let mnemonic_pair = inner.next().ok_or_else(|| InktParseError {
message: "expected opcode mnemonic".into(),
line: 0,
col: 0,
})?;
let mnemonic = mnemonic_pair.as_str();
let operands: Vec<P<'_>> = inner.collect();
match mnemonic {
"push_int" => Ok(Opcode::PushInt(parse_operand_i32(&operands, 0, mnemonic)?)),
"push_float" => Ok(Opcode::PushFloat(parse_operand_f32(
&operands, 0, mnemonic,
)?)),
"push_bool" => {
let s = operand_str(&operands, 0, mnemonic)?;
Ok(Opcode::PushBool(s == "true"))
}
"push_string" => Ok(Opcode::PushString(parse_operand_u16(
&operands, 0, mnemonic,
)?)),
"push_list" => Ok(Opcode::PushList(parse_operand_u16(&operands, 0, mnemonic)?)),
"push_divert_target" => Ok(Opcode::PushDivertTarget(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"push_null" => Ok(Opcode::PushNull),
"pop" => Ok(Opcode::Pop),
"duplicate" => Ok(Opcode::Duplicate),
"add" => Ok(Opcode::Add),
"subtract" => Ok(Opcode::Subtract),
"multiply" => Ok(Opcode::Multiply),
"divide" => Ok(Opcode::Divide),
"modulo" => Ok(Opcode::Modulo),
"negate" => Ok(Opcode::Negate),
"equal" => Ok(Opcode::Equal),
"not_equal" => Ok(Opcode::NotEqual),
"greater" => Ok(Opcode::Greater),
"greater_or_equal" => Ok(Opcode::GreaterOrEqual),
"less" => Ok(Opcode::Less),
"less_or_equal" => Ok(Opcode::LessOrEqual),
"not" => Ok(Opcode::Not),
"and" => Ok(Opcode::And),
"or" => Ok(Opcode::Or),
"get_global" => Ok(Opcode::GetGlobal(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"set_global" => Ok(Opcode::SetGlobal(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"declare_temp" => Ok(Opcode::DeclareTemp(parse_operand_u16(
&operands, 0, mnemonic,
)?)),
"get_temp" => Ok(Opcode::GetTemp(parse_operand_u16(&operands, 0, mnemonic)?)),
"set_temp" => Ok(Opcode::SetTemp(parse_operand_u16(&operands, 0, mnemonic)?)),
"get_temp_raw" => Ok(Opcode::GetTempRaw(parse_operand_u16(
&operands, 0, mnemonic,
)?)),
"push_var_pointer" => Ok(Opcode::PushVarPointer(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"push_temp_pointer" => Ok(Opcode::PushTempPointer(parse_operand_u16(
&operands, 0, mnemonic,
)?)),
"jump" => Ok(Opcode::Jump(parse_operand_i32(&operands, 0, mnemonic)?)),
"jump_if_false" => Ok(Opcode::JumpIfFalse(parse_operand_i32(
&operands, 0, mnemonic,
)?)),
"goto" => Ok(Opcode::Goto(parse_operand_def_id(&operands, 0, mnemonic)?)),
"goto_if" => Ok(Opcode::GotoIf(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"goto_variable" => Ok(Opcode::GotoVariable),
"enter_container" => Ok(Opcode::EnterContainer(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"exit_container" => Ok(Opcode::ExitContainer),
"call" => Ok(Opcode::Call(parse_operand_def_id(&operands, 0, mnemonic)?)),
"return" => Ok(Opcode::Return),
"tunnel_call" => Ok(Opcode::TunnelCall(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"tunnel_return" => Ok(Opcode::TunnelReturn),
"tunnel_call_variable" => Ok(Opcode::TunnelCallVariable),
"call_variable" => Ok(Opcode::CallVariable),
"thread_call" => Ok(Opcode::ThreadCall(parse_operand_def_id(
&operands, 0, mnemonic,
)?)),
"thread_start" => Ok(Opcode::ThreadStart),
"thread_done" => Ok(Opcode::ThreadDone),
"emit_line" => {
let idx = parse_operand_u16(&operands, 0, mnemonic)?;
let slots = parse_operand_u8(&operands, 1, mnemonic)?;
Ok(Opcode::EmitLine(idx, slots))
}
"emit_value" => Ok(Opcode::EmitValue),
"emit_newline" => Ok(Opcode::EmitNewline),
"spring" => Ok(Opcode::Spring),
"glue" => Ok(Opcode::Glue),
"begin_tag" => Ok(Opcode::BeginTag),
"end_tag" => Ok(Opcode::EndTag),
"eval_line" => {
let idx = parse_operand_u16(&operands, 0, mnemonic)?;
let slots = parse_operand_u8(&operands, 1, mnemonic)?;
Ok(Opcode::EvalLine(idx, slots))
}
"begin_choice" => {
let flags = parse_choice_flags_operand(&operands, 0, mnemonic)?;
let target = parse_operand_def_id(&operands, 1, mnemonic)?;
Ok(Opcode::BeginChoice(flags, target))
}
"end_choice" => Ok(Opcode::EndChoice),
"sequence" => {
let kind_str = operand_str(&operands, 0, mnemonic)?;
let kind = match kind_str {
"cycle" => SequenceKind::Cycle,
"stopping" => SequenceKind::Stopping,
"once_only" => SequenceKind::OnceOnly,
"shuffle" => SequenceKind::Shuffle,
_ => {
return Err(InktParseError {
message: format!("unknown sequence kind: {kind_str}"),
line: 0,
col: 0,
});
}
};
let count: u8 =
operand_str(&operands, 1, mnemonic)?
.parse()
.map_err(|_| InktParseError {
message: "invalid sequence count".into(),
line: 0,
col: 0,
})?;
Ok(Opcode::Sequence(kind, count))
}
"sequence_branch" => Ok(Opcode::SequenceBranch(parse_operand_i32(
&operands, 0, mnemonic,
)?)),
"visit_count" => Ok(Opcode::VisitCount),
"current_visit_count" => Ok(Opcode::CurrentVisitCount),
"turns_since" => Ok(Opcode::TurnsSince),
"turn_index" => Ok(Opcode::TurnIndex),
"choice_count" => Ok(Opcode::ChoiceCount),
"random" => Ok(Opcode::Random),
"seed_random" => Ok(Opcode::SeedRandom),
"cast_to_int" => Ok(Opcode::CastToInt),
"cast_to_float" => Ok(Opcode::CastToFloat),
"floor" => Ok(Opcode::Floor),
"ceiling" => Ok(Opcode::Ceiling),
"pow" => Ok(Opcode::Pow),
"min" => Ok(Opcode::Min),
"max" => Ok(Opcode::Max),
"call_external" => {
let id = parse_operand_def_id(&operands, 0, mnemonic)?;
let kv_str = operand_str(&operands, 1, mnemonic)?;
let argc_str = kv_str.strip_prefix("argc=").unwrap_or(kv_str);
let argc: u8 = argc_str.parse().map_err(|_| InktParseError {
message: format!("invalid argc in call_external: {kv_str}"),
line: 0,
col: 0,
})?;
Ok(Opcode::CallExternal(id, argc))
}
"list_contains" => Ok(Opcode::ListContains),
"list_not_contains" => Ok(Opcode::ListNotContains),
"list_intersect" => Ok(Opcode::ListIntersect),
"list_all" => Ok(Opcode::ListAll),
"list_invert" => Ok(Opcode::ListInvert),
"list_count" => Ok(Opcode::ListCount),
"list_min" => Ok(Opcode::ListMin),
"list_max" => Ok(Opcode::ListMax),
"list_value" => Ok(Opcode::ListValue),
"list_range" => Ok(Opcode::ListRange),
"list_from_int" => Ok(Opcode::ListFromInt),
"list_random" => Ok(Opcode::ListRandom),
"done" => Ok(Opcode::Done),
"yield" => Ok(Opcode::Yield),
"end" => Ok(Opcode::End),
"nop" => Ok(Opcode::Nop),
"begin_string_eval" => Ok(Opcode::BeginStringEval),
"end_string_eval" => Ok(Opcode::EndStringEval),
"begin_fragment" => Ok(Opcode::BeginFragment),
"end_fragment" => Ok(Opcode::EndFragment),
"source_location" => {
let s = operand_str(&operands, 0, mnemonic)?;
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return Err(InktParseError {
message: format!("invalid source_location: {s}"),
line: 0,
col: 0,
});
}
let line: u32 = parts[0].parse().map_err(|_| InktParseError {
message: "invalid line".into(),
line: 0,
col: 0,
})?;
let col: u32 = parts[1].parse().map_err(|_| InktParseError {
message: "invalid col".into(),
line: 0,
col: 0,
})?;
Ok(Opcode::SourceLocation(line, col))
}
_ => Err(InktParseError {
message: format!("unknown opcode: {mnemonic}"),
line: mnemonic_pair.line_col().0,
col: mnemonic_pair.line_col().1,
}),
}
}
fn parse_choice_flags_operand(
operands: &[P<'_>],
idx: usize,
context: &str,
) -> Result<ChoiceFlags, InktParseError> {
let s = operand_str(operands, idx, context)?;
let mut flags = ChoiceFlags {
has_condition: false,
has_start_content: false,
has_choice_only_content: false,
once_only: false,
is_invisible_default: false,
};
if s == "none" {
return Ok(flags);
}
for part in s.split('+') {
match part {
"cond" => flags.has_condition = true,
"start" => flags.has_start_content = true,
"choice_only" => flags.has_choice_only_content = true,
"once" => flags.once_only = true,
"invis_default" => flags.is_invisible_default = true,
_ => {
return Err(InktParseError {
message: format!("unknown choice flag: {part}"),
line: 0,
col: 0,
});
}
}
}
Ok(flags)
}
fn operand_str<'a>(
operands: &'a [P<'_>],
idx: usize,
context: &str,
) -> Result<&'a str, InktParseError> {
let op = operands.get(idx).ok_or_else(|| InktParseError {
message: format!("missing operand {idx} for {context}"),
line: 0,
col: 0,
})?;
let inner = op.clone().into_inner().next();
match inner {
Some(p) => Ok(p.as_str()),
None => Ok(op.as_str()),
}
}
fn parse_operand_i32(operands: &[P<'_>], idx: usize, context: &str) -> Result<i32, InktParseError> {
let s = operand_str(operands, idx, context)?;
s.parse().map_err(|_| InktParseError {
message: format!("invalid i32 operand for {context}: {s}"),
line: 0,
col: 0,
})
}
fn parse_operand_f32(operands: &[P<'_>], idx: usize, context: &str) -> Result<f32, InktParseError> {
let s = operand_str(operands, idx, context)?;
s.parse().map_err(|_| InktParseError {
message: format!("invalid f32 operand for {context}: {s}"),
line: 0,
col: 0,
})
}
fn parse_operand_u8(operands: &[P<'_>], idx: usize, context: &str) -> Result<u8, InktParseError> {
let s = operand_str(operands, idx, context)?;
s.parse().map_err(|_| InktParseError {
message: format!("invalid u8 operand for {context}: {s}"),
line: 0,
col: 0,
})
}
fn parse_operand_u16(operands: &[P<'_>], idx: usize, context: &str) -> Result<u16, InktParseError> {
let s = operand_str(operands, idx, context)?;
s.parse().map_err(|_| InktParseError {
message: format!("invalid u16 operand for {context}: {s}"),
line: 0,
col: 0,
})
}
fn parse_operand_def_id(
operands: &[P<'_>],
idx: usize,
context: &str,
) -> Result<DefinitionId, InktParseError> {
let op = operands.get(idx).ok_or_else(|| InktParseError {
message: format!("missing operand {idx} for {context}"),
line: 0,
col: 0,
})?;
let inner = op.clone().into_inner().next().unwrap_or_else(|| op.clone());
parse_def_id(inner)
}
#[expect(clippy::needless_pass_by_value)]
fn parse_def_id(pair: P<'_>) -> Result<DefinitionId, InktParseError> {
let s = pair.as_str();
if !s.starts_with('$') || s.len() < 4 {
return Err(err(&pair, format!("invalid def_id: {s}")));
}
let tag_str = &s[1..3];
let hash_str = &s[4..];
let tag_byte = u8::from_str_radix(tag_str, 16)
.map_err(|_| err(&pair, format!("invalid tag: {tag_str}")))?;
let hash = u64::from_str_radix(hash_str, 16)
.map_err(|_| err(&pair, format!("invalid hash: {hash_str}")))?;
let tag = crate::id::DefinitionTag::from_u8(tag_byte)
.ok_or_else(|| err(&pair, format!("unknown tag byte: {tag_byte:#04x}")))?;
Ok(DefinitionId::new(tag, hash))
}
fn parse_hex_u32(s: &str) -> u32 {
let hex = s.strip_prefix("0x").unwrap_or(s);
u32::from_str_radix(hex, 16).unwrap_or(0)
}
fn parse_hex_u64(s: &str) -> Result<u64, InktParseError> {
let hex = s.strip_prefix("0x").unwrap_or(s);
u64::from_str_radix(hex, 16).map_err(|_| InktParseError {
message: format!("invalid hex: {s}"),
line: 0,
col: 0,
})
}
fn parse_u16(pair: &P<'_>) -> Result<u16, InktParseError> {
pair.as_str().parse().map_err(|_| err(pair, "invalid u16"))
}
fn unescape_string(s: &str) -> String {
let inner = &s[1..s.len() - 1];
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('\\') | None => out.push('\\'),
Some('"') => out.push('"'),
Some('n') => out.push('\n'),
Some('t') => out.push('\t'),
Some('r') => out.push('\r'),
Some(other) => {
out.push('\\');
out.push(other);
}
}
} else {
out.push(c);
}
}
out
}
fn next_rule<'a>(
iter: &mut impl Iterator<Item = P<'a>>,
expected: Rule,
context: &str,
) -> Result<P<'a>, InktParseError> {
for pair in iter.by_ref() {
if pair.as_rule() == expected {
return Ok(pair);
}
}
Err(InktParseError {
message: format!("expected {expected:?} in {context}"),
line: 0,
col: 0,
})
}