diff --git a/Cargo.toml b/Cargo.toml index b21fb4fb..dc3f0dd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,5 @@ indexmap = "2.7.0" approx = "0.5.1" insta = { version = "1.39.0", features = ["ron"] } camino = "1.2.1" + +pretty_assertions = "1.4.1" diff --git a/crates/ltk_meta/src/lib.rs b/crates/ltk_meta/src/lib.rs index a7a53403..173d0dac 100644 --- a/crates/ltk_meta/src/lib.rs +++ b/crates/ltk_meta/src/lib.rs @@ -27,13 +27,13 @@ for (path_hash, object) in &tree.objects { ``` use ltk_meta::{Bin, BinObject}; -use ltk_meta::property::values; +use ltk_meta::property::{values, NoMeta}; // Using the builder pattern let tree = Bin::builder() .dependency("common.bin") .object( - BinObject::builder(0x12345678, 0xABCDEF00) + BinObject::::builder(0x12345678, 0xABCDEF00) .property(0x1111, values::I32::new(42)) .property(0x2222, values::String::from("hello")) .build() @@ -70,7 +70,7 @@ tree.to_writer(&mut output)?; ``` */ pub mod property; -pub use property::{BinProperty, Kind as PropertyKind, PropertyValueEnum}; +pub use property::{Kind as PropertyKind, PropertyValueEnum}; mod tree; pub use tree::*; diff --git a/crates/ltk_meta/src/property.rs b/crates/ltk_meta/src/property.rs index b81d3afa..8c1dfaf1 100644 --- a/crates/ltk_meta/src/property.rs +++ b/crates/ltk_meta/src/property.rs @@ -6,50 +6,8 @@ pub use kind::*; mod r#enum; pub use r#enum::*; -use super::traits::{ReaderExt as _, WriterExt as _}; use super::Error; -use byteorder::{ReadBytesExt as _, WriteBytesExt as _, LE}; -use std::io; - -use crate::traits::PropertyExt; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NoMeta; - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, PartialEq, Debug)] -pub struct BinProperty { - pub name_hash: u32, - #[cfg_attr(feature = "serde", serde(flatten))] - pub value: PropertyValueEnum, -} - -impl BinProperty { - /// Read a BinProperty from a reader. This will read the name_hash, prop kind and then value, in that order. - pub fn from_reader( - reader: &mut R, - legacy: bool, - ) -> Result { - let name_hash = reader.read_u32::()?; - let kind = reader.read_property_kind(legacy)?; - - Ok(Self { - name_hash, - value: PropertyValueEnum::from_reader(reader, kind, legacy)?, - }) - } - pub fn to_writer( - &self, - writer: &mut W, - ) -> Result<(), io::Error> { - writer.write_u32::(self.name_hash)?; - writer.write_property_kind(self.value.kind())?; - - self.value.to_writer(writer)?; - Ok(()) - } - pub fn size(&self) -> usize { - 5 + self.value.size_no_header() - } -} diff --git a/crates/ltk_meta/src/property/enum.rs b/crates/ltk_meta/src/property/enum.rs index 726ff38b..3bbd36dc 100644 --- a/crates/ltk_meta/src/property/enum.rs +++ b/crates/ltk_meta/src/property/enum.rs @@ -1,8 +1,6 @@ -use enum_dispatch::enum_dispatch; - use crate::{ property::{Kind, NoMeta}, - traits::{ReadProperty as _, WriteProperty as _}, + traits::{PropertyExt, ReadProperty as _, WriteProperty as _}, Error, }; use std::io; @@ -50,10 +48,9 @@ macro_rules! create_enum { )] #[cfg_attr(feature = "serde", serde(tag = "kind", content = "value"))] #[derive(Clone, Debug, PartialEq)] - #[enum_dispatch(PropertyExt)] /// The value part of a [`super::BinProperty`]. Holds the type of the value, and the value itself. pub enum PropertyValueEnum { - $( $variant (pub self::$variant), )* + $( $variant (self::$variant), )* } @@ -81,13 +78,56 @@ macro_rules! create_enum { } } impl PropertyValueEnum { + #[inline(always)] #[must_use] pub fn kind(&self) -> Kind { match self { $(Self::$variant(_) => Kind::$variant,)* } } + + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> PropertyValueEnum { + match self { + $(Self::$variant(i) => PropertyValueEnum::$variant(i.no_meta()),)* + } + } + } + + impl PropertyExt for PropertyValueEnum { + type Meta = M; + fn meta(&self) -> &Self::Meta { + match self { + $(Self::$variant(i) => i.meta(),)* + } + } + fn meta_mut(&mut self) -> &mut Self::Meta { + match self { + $(Self::$variant(i) => i.meta_mut(),)* + } + } + + fn size(&self, include_header: bool) -> usize { + match self { + $(Self::$variant(i) => i.size(include_header),)* + } + } + fn size_no_header(&self) -> usize { + match self { + $(Self::$variant(i) => i.size_no_header(),)* + } + } + } + + $( + impl From> for PropertyValueEnum { + fn from(other: values::$variant) -> Self { + Self::$variant(other) + } + } + )* }; } diff --git a/crates/ltk_meta/src/property/values/container.rs b/crates/ltk_meta/src/property/values/container.rs index 2771f27b..12094f7b 100644 --- a/crates/ltk_meta/src/property/values/container.rs +++ b/crates/ltk_meta/src/property/values/container.rs @@ -42,6 +42,22 @@ macro_rules! define_container_enum { })* } } + + type Meta = M; + fn meta(&self) -> &Self::Meta { + match &self { + $(Self::$variant{meta,..} => { + meta + })* + } + } + fn meta_mut(&mut self) -> &mut Self::Meta { + match self { + $(Self::$variant{meta,..} => { + meta + })* + } + } } $( @@ -108,6 +124,48 @@ macro_rules! define_container_enum { } impl Container { + #[inline(always)] + pub fn empty(item_kind: Kind) -> Result + where + M: Default + { + match item_kind { + $(Kind::$variant => Ok(Self::$variant { + items: vec![], + meta: M::default(), + }),)* + kind => Err(Error::InvalidNesting(kind)), + + } + } + + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> Container { + match self { + $(Self::$variant{items,..} => { + Container::$variant { + items: items.into_iter().map(|i| i.no_meta()).collect(), + meta: NoMeta + } + })* + } + } + + pub fn push(&mut self, value: PropertyValueEnum) -> Result<(), Error>{ + let got = value.kind(); + let expected = self.item_kind(); + match (self, value) { + $((Self::$variant{items,..}, PropertyValueEnum::$variant(item)) => { + items.push(item); + Ok(()) + })* + _ => { + Err(Error::MismatchedContainerTypes { got, expected }) + } + } + } + /// Iterator that returns each item as a [`PropertyValueEnum`] for convenience. #[inline(always)] #[must_use] @@ -149,7 +207,7 @@ impl Container { Self::from(items) } - pub fn empty() -> Self + pub fn empty_const() -> Self where Self: From>, { diff --git a/crates/ltk_meta/src/property/values/container/iter.rs b/crates/ltk_meta/src/property/values/container/iter.rs index fbfbb692..d6b54bf9 100644 --- a/crates/ltk_meta/src/property/values/container/iter.rs +++ b/crates/ltk_meta/src/property/values/container/iter.rs @@ -6,7 +6,7 @@ use crate::{ pub struct ItemsDyn<'a, M>(ItemsDynInner<'a, M>); impl<'a, M> Iterator for ItemsDyn<'a, M> { - type Item = &'a dyn PropertyValueDyn; + type Item = &'a dyn PropertyValueDyn; fn next(&mut self) -> Option { self.0.next() @@ -39,7 +39,7 @@ macro_rules! define_dyn_iter { } impl<'a, M> Iterator for ItemsDynInner<'a, M> { - type Item = &'a dyn PropertyValueDyn; + type Item = &'a dyn PropertyValueDyn; fn next(&mut self) -> Option { match self { diff --git a/crates/ltk_meta/src/property/values/container/variants.rs b/crates/ltk_meta/src/property/values/container/variants.rs index f9924208..ac5fbaf9 100644 --- a/crates/ltk_meta/src/property/values/container/variants.rs +++ b/crates/ltk_meta/src/property/values/container/variants.rs @@ -23,42 +23,42 @@ macro_rules! container_variants { }; } -macro_rules! match_property { - ($value:expr, $on:ident, $inner:pat => $body:expr, $def:pat => $def_body: expr) => { - container_variants!(match_property_arms, ($value, $on, $inner => $body, $def => $def_body)) - }; - ($value:expr, $on:ident, $inner:pat => $body:expr) => { - container_variants!(match_property_arms, ($value, $on, $inner => $body)) - }; - - ($value:expr, $inner:pat => $body:expr, $def:pat => $def_body: expr) => { - container_variants!(match_property_arms, ($value, Self, $inner => $body, $def => $def_body)) - }; - ($value:expr, $inner:pat => $body:expr) => { - container_variants!(match_property_arms, ($value, Self, $inner => $body)) - }; -} - -macro_rules! match_property_arms { - (($value:expr, $on:ident, $inner:pat => $body:expr, $def:pat => $def_body: expr) - [$( $variant:ident, )*]) => { - match $value { - $( - $on::$variant($inner) => $body, - )* - $def => $def_body - } - }; - - (($value:expr, $on:ident, $inner:pat => $body:expr) - [$( $variant:ident, )*]) => { - match $value { - $( - $on::$variant($inner) => $body, - )* - } - }; -} +// macro_rules! match_property { +// ($value:expr, $on:ident, $inner:pat => $body:expr, $def:pat => $def_body: expr) => { +// container_variants!(match_property_arms, ($value, $on, $inner => $body, $def => $def_body)) +// }; +// ($value:expr, $on:ident, $inner:pat => $body:expr) => { +// container_variants!(match_property_arms, ($value, $on, $inner => $body)) +// }; +// +// ($value:expr, $inner:pat => $body:expr, $def:pat => $def_body: expr) => { +// container_variants!(match_property_arms, ($value, Self, $inner => $body, $def => $def_body)) +// }; +// ($value:expr, $inner:pat => $body:expr) => { +// container_variants!(match_property_arms, ($value, Self, $inner => $body)) +// }; +// } +// +// macro_rules! match_property_arms { +// (($value:expr, $on:ident, $inner:pat => $body:expr, $def:pat => $def_body: expr) +// [$( $variant:ident, )*]) => { +// match $value { +// $( +// $on::$variant($inner) => $body, +// )* +// $def => $def_body +// } +// }; +// +// (($value:expr, $on:ident, $inner:pat => $body:expr) +// [$( $variant:ident, )*]) => { +// match $value { +// $( +// $on::$variant($inner) => $body, +// )* +// } +// }; +// } // macro_rules! match_enum_inner { // (($value:expr, $on:ident, ||, $body:expr) diff --git a/crates/ltk_meta/src/property/values/embedded.rs b/crates/ltk_meta/src/property/values/embedded.rs index 958e40a6..2f8fce08 100644 --- a/crates/ltk_meta/src/property/values/embedded.rs +++ b/crates/ltk_meta/src/property/values/embedded.rs @@ -13,6 +13,14 @@ use super::Struct; #[derive(Clone, PartialEq, Debug, Default)] pub struct Embedded(pub Struct); +impl Embedded { + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> Embedded { + Embedded(self.0.no_meta()) + } +} + impl PropertyValueExt for Embedded { const KIND: Kind = Kind::Embedded; } @@ -21,6 +29,13 @@ impl PropertyExt for Embedded { fn size_no_header(&self) -> usize { self.0.size_no_header() } + type Meta = M; + fn meta(&self) -> &Self::Meta { + self.0.meta() + } + fn meta_mut(&mut self) -> &mut Self::Meta { + self.0.meta_mut() + } } impl ReadProperty for Embedded { @@ -31,7 +46,7 @@ impl ReadProperty for Embedded { Struct::::from_reader(reader, legacy).map(Self) } } -impl WriteProperty for Embedded { +impl WriteProperty for Embedded { fn to_writer( &self, writer: &mut R, diff --git a/crates/ltk_meta/src/property/values/map.rs b/crates/ltk_meta/src/property/values/map.rs index 03ef2d21..6d38a7e2 100644 --- a/crates/ltk_meta/src/property/values/map.rs +++ b/crates/ltk_meta/src/property/values/map.rs @@ -42,6 +42,21 @@ pub struct Map { } impl Map { + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> Map { + Map { + key_kind: self.key_kind, + value_kind: self.value_kind, + entries: self + .entries + .into_iter() + .map(|(k, v)| (k.no_meta(), v.no_meta())) + .collect(), + meta: NoMeta, + } + } + #[inline(always)] #[must_use] pub fn key_kind(&self) -> Kind { @@ -65,6 +80,28 @@ impl Map { pub fn into_entries(self) -> Vec<(PropertyValueEnum, PropertyValueEnum)> { self.entries } + + #[inline(always)] + pub fn push( + &mut self, + key: PropertyValueEnum, + value: PropertyValueEnum, + ) -> Result<(), Error> { + if self.key_kind != key.kind() { + return Err(Error::MismatchedContainerTypes { + expected: self.key_kind, + got: key.kind(), + }); + } + if self.value_kind != value.kind() { + return Err(Error::MismatchedContainerTypes { + expected: self.value_kind, + got: value.kind(), + }); + } + self.entries.push((key, value)); + Ok(()) + } } impl Map { @@ -119,6 +156,14 @@ impl PropertyExt for Map { .map(|(k, v)| k.size_no_header() + v.size_no_header()) .sum::() } + + type Meta = M; + fn meta(&self) -> &Self::Meta { + &self.meta + } + fn meta_mut(&mut self) -> &mut Self::Meta { + &mut self.meta + } } impl ReadProperty for Map { diff --git a/crates/ltk_meta/src/property/values/none.rs b/crates/ltk_meta/src/property/values/none.rs index 0c3a1ff1..3fc37e68 100644 --- a/crates/ltk_meta/src/property/values/none.rs +++ b/crates/ltk_meta/src/property/values/none.rs @@ -19,6 +19,28 @@ impl PropertyExt for None { fn size_no_header(&self) -> usize { 0 } + + type Meta = M; + fn meta(&self) -> &Self::Meta { + &self.meta + } + fn meta_mut(&mut self) -> &mut Self::Meta { + &mut self.meta + } +} + +impl None { + #[inline(always)] + #[must_use] + pub fn new(meta: M) -> Self { + Self { meta } + } + + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> None { + None { meta: NoMeta } + } } impl ReadProperty for None { diff --git a/crates/ltk_meta/src/property/values/optional.rs b/crates/ltk_meta/src/property/values/optional.rs index b3748b30..24ec6d2b 100644 --- a/crates/ltk_meta/src/property/values/optional.rs +++ b/crates/ltk_meta/src/property/values/optional.rs @@ -14,25 +14,49 @@ macro_rules! construct_enum { )] #[derive(Clone, PartialEq, Debug)] pub enum Optional { - $($variant(Option>),)* + $($variant{ + value: Option>, + meta: M + },)* } - impl Optional { + impl Optional { #[inline(always)] #[must_use] /// Helper function to create an empty [`Optional`], if the property kind can be stored in one. - pub fn empty(kind: Kind) -> Option { + pub fn empty(kind: Kind) -> Option where M: Default { Some(match kind { - $(Kind::$variant => Self::$variant(None),)* + $(Kind::$variant => Self::$variant{value: None, meta: M::default()},)* _ => return None }) } + + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> Optional { + match self { + $(Self::$variant{ value, meta: _ } => Optional::$variant{ value: value.map(|v| v.no_meta()), meta: NoMeta },)* + } + } } impl PropertyExt for Optional { fn size_no_header(&self) -> usize { 2 + match &self { - $(Self::$variant(inner) => inner.as_ref().map(|i| i.size_no_header()).unwrap_or_default(),)* + $(Self::$variant{value, ..} => value.as_ref().map(|i| i.size_no_header()).unwrap_or_default(),)* + } + } + type Meta = M; + fn meta(&self) -> &Self::Meta { + match &self { + $(Self::$variant{meta, ..} => meta,)* + } + } + fn meta_mut(&mut self) -> &mut Self::Meta { + match self { + $(Self::$variant{meta, ..} => { + meta + })* } } } @@ -40,7 +64,7 @@ macro_rules! construct_enum { $( impl From>> for Optional { fn from(other: Option>) -> Self { - Self::$variant(other) + Self::$variant{value: other, meta: M::default()} } } @@ -48,27 +72,50 @@ macro_rules! construct_enum { $( impl From> for Optional { fn from(other: values::$variant) -> Self { - Self::$variant(Some(other)) + Self::$variant{value: Some(other), meta: M::default()} } } )* - impl Optional { - pub fn new(item_kind: Kind, value: Option>) -> Result { + impl Optional { + #[inline(always)] + pub fn new(item_kind: Kind, value: Option>) -> Result where M: Default { + Self::new_with_meta(item_kind, value, M::default()) + } + #[inline(always)] + pub fn new_with_meta(item_kind: Kind, value: Option>, meta: M) -> Result { match item_kind { $(Kind::$variant => match value { - Some(PropertyValueEnum::$variant(inner)) => Ok(Self::$variant(Some(inner))), - None => Ok(Self::$variant(None)), + Some(PropertyValueEnum::$variant(inner)) => Ok(Self::$variant{value: Some(inner), meta}), + None => Ok(Self::$variant{value: None, meta}), Some(value) => Err(Error::MismatchedContainerTypes {expected: item_kind, got: value.kind()}), },)* kind => Err(Error::InvalidNesting(kind)), } } - pub fn into_inner(self) -> Option> { + #[inline(always)] + #[must_use] + pub fn into_parts(self) -> (Option>, M) { match self { - $(Optional::$variant(item) => item.map(PropertyValueEnum::$variant),)* + $(Optional::$variant{ value, meta } => (value.map(PropertyValueEnum::$variant), meta),)* + } + } + + #[inline(always)] + #[must_use] + pub fn is_some(&self) -> bool { + match self { + $(Self::$variant{value,..} => value.is_some(),)* + } + } + + #[inline(always)] + #[must_use] + pub fn is_none(&self) -> bool { + match self { + $(Self::$variant{value,..} => value.is_none(),)* } } } @@ -79,27 +126,32 @@ container_variants!(construct_enum); impl Default for Optional { fn default() -> Self { - Self::None(None) + Self::None { + value: None, + meta: M::default(), + } } } impl Optional { + pub fn empty_const() -> Self + where + Self: From>, + { + Self::from(None) + } + #[inline(always)] #[must_use] pub fn item_kind(&self) -> Kind { container_variants!(property_kinds, (self)) } - #[inline(always)] - #[must_use] - pub fn is_some(&self) -> bool { - match_property!(self, inner => inner.is_some()) + pub fn into_inner(self) -> Option> { + self.into_parts().0 } - - #[inline(always)] - #[must_use] - pub fn is_none(&self) -> bool { - match_property!(self, inner => inner.is_none()) + pub fn into_meta(self) -> M { + self.into_parts().1 } } @@ -125,8 +177,8 @@ impl ReadProperty for Optional { match $value { $( Kind::$variant => match is_some { - true => values::$variant::from_reader(reader, legacy).map(Some).map(Self::$variant), - false => Ok(Self::$variant(None)), + true => Ok(values::$variant::from_reader(reader, legacy)?.into()), + false => Ok(Self::empty_const::>()), }, )* kind => { return Err(Error::InvalidNesting(kind)) } @@ -137,7 +189,7 @@ impl ReadProperty for Optional { container_variants!(read_inner, (kind)) } } -impl WriteProperty for Optional { +impl WriteProperty for Optional { fn to_writer( &self, writer: &mut R, @@ -149,14 +201,19 @@ impl WriteProperty for Optional { writer.write_property_kind(self.item_kind())?; writer.write_bool(self.is_some())?; - match_property!( - self, - Some(inner) => { - inner.to_writer(writer, legacy)?; - }, - _ => {} - ); + macro_rules! write_inner { + (($value:expr) + [$( $variant:ident, )*]) => { + match $value { + $( + Self::$variant{value: Some(value),..} => value.to_writer(writer, legacy), + + )* + _ => { Ok(()) } + } + }; + } - Ok(()) + container_variants!(write_inner, (self)) } } diff --git a/crates/ltk_meta/src/property/values/primitives.rs b/crates/ltk_meta/src/property/values/primitives.rs index eb9c30ac..09077432 100644 --- a/crates/ltk_meta/src/property/values/primitives.rs +++ b/crates/ltk_meta/src/property/values/primitives.rs @@ -20,11 +20,29 @@ macro_rules! impl_prim { pub meta: M } - impl $name { + impl $name { #[inline(always)] #[must_use] - pub fn new(value: $rust) -> Self { - Self { value, meta: M::default() } + pub fn new(value: $rust) -> Self where M: Default { + Self::new_with_meta(value, M::default()) + } + + #[inline(always)] + #[must_use] + pub fn new_with_meta(value: $rust, meta: M) -> Self { + Self { value, meta } + } + + #[inline(always)] + #[must_use] + pub fn with_meta(self, meta: T) -> $name { + $name { value: self.value, meta } + } + + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> $name { + self.with_meta(NoMeta) } } @@ -32,6 +50,14 @@ macro_rules! impl_prim { fn size_no_header(&self) -> usize { core::mem::size_of::<$rust>() } + + type Meta = M; + fn meta(&self) -> &Self::Meta { + &self.meta + } + fn meta_mut(&mut self) -> &mut Self::Meta { + &mut self.meta + } } impl PropertyValueExt for $name { diff --git a/crates/ltk_meta/src/property/values/string.rs b/crates/ltk_meta/src/property/values/string.rs index 7a377919..0004111c 100644 --- a/crates/ltk_meta/src/property/values/string.rs +++ b/crates/ltk_meta/src/property/values/string.rs @@ -16,15 +16,36 @@ pub struct String { pub meta: M, } -impl String { +impl String { #[inline(always)] #[must_use] - pub fn new(value: std::string::String) -> Self { - Self { - value, - meta: M::default(), + pub fn new(value: std::string::String) -> Self + where + M: Default, + { + Self::new_with_meta(value, M::default()) + } + + #[inline(always)] + #[must_use] + pub fn new_with_meta(value: std::string::String, meta: M) -> Self { + Self { value, meta } + } + + #[inline(always)] + #[must_use] + pub fn with_meta(self, meta: T) -> String { + String { + value: self.value, + meta, } } + + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> String { + self.with_meta(NoMeta) + } } impl PropertyValueExt for String { @@ -35,6 +56,14 @@ impl PropertyExt for String { fn size_no_header(&self) -> usize { self.value.len() + 2 } + + type Meta = M; + fn meta(&self) -> &Self::Meta { + &self.meta + } + fn meta_mut(&mut self) -> &mut Self::Meta { + &mut self.meta + } } impl ReadProperty for String { diff --git a/crates/ltk_meta/src/property/values/struct.rs b/crates/ltk_meta/src/property/values/struct.rs index c58d243b..77c0b9b6 100644 --- a/crates/ltk_meta/src/property/values/struct.rs +++ b/crates/ltk_meta/src/property/values/struct.rs @@ -2,8 +2,10 @@ use std::io; use crate::{ property::{Kind, NoMeta}, - traits::{PropertyExt, PropertyValueExt, ReadProperty, WriteProperty}, - BinProperty, Error, + traits::{ + PropertyExt, PropertyValueExt, ReadProperty, ReaderExt, WriteProperty, WriterExt as _, + }, + Error, PropertyValueEnum, }; use byteorder::{ReadBytesExt as _, WriteBytesExt as _, LE}; use indexmap::IndexMap; @@ -17,10 +19,26 @@ use ltk_io_ext::{measure, window_at}; #[derive(Clone, PartialEq, Debug, Default)] pub struct Struct { pub class_hash: u32, - pub properties: IndexMap, + pub properties: IndexMap>, pub meta: M, } +impl Struct { + #[inline(always)] + #[must_use] + pub fn no_meta(self) -> Struct { + Struct { + class_hash: self.class_hash, + properties: self + .properties + .into_iter() + .map(|(k, v)| (k, v.no_meta())) + .collect(), + meta: NoMeta, + } + } +} + impl PropertyValueExt for Struct { const KIND: Kind = Kind::Struct; } @@ -29,9 +47,23 @@ impl PropertyExt for Struct { fn size_no_header(&self) -> usize { match self.class_hash { 0 => 4, - _ => 10 + self.properties.values().map(|p| p.size()).sum::(), + _ => { + 10 + self + .properties + .values() + .map(|p| 5 + p.size_no_header()) + .sum::() + } } } + + type Meta = M; + fn meta(&self) -> &Self::Meta { + &self.meta + } + fn meta_mut(&mut self) -> &mut Self::Meta { + &mut self.meta + } } impl ReadProperty for Struct { @@ -53,8 +85,8 @@ impl ReadProperty for Struct { let prop_count = reader.read_u16::()?; let mut properties = IndexMap::with_capacity(prop_count as _); for _ in 0..prop_count { - let prop = BinProperty::from_reader(reader, legacy)?; - properties.insert(prop.name_hash, prop); + let (name_hash, value) = reader.read_property::(legacy)?; + properties.insert(name_hash, value); } Ok::<_, Error>(Self { class_hash, @@ -70,7 +102,7 @@ impl ReadProperty for Struct { Ok(value) } } -impl WriteProperty for Struct { +impl WriteProperty for Struct { fn to_writer( &self, writer: &mut R, @@ -92,8 +124,10 @@ impl WriteProperty for Struct { let (size, _) = measure(writer, |writer| { writer.write_u16::(self.properties.len() as _)?; - for prop in self.properties.values() { - prop.to_writer(writer)?; + for (name_hash, value) in self.properties.iter() { + writer.write_u32::(*name_hash)?; + writer.write_property_kind(value.kind())?; + value.to_writer(writer)?; } Ok::<_, io::Error>(()) diff --git a/crates/ltk_meta/src/property/values/unordered_container.rs b/crates/ltk_meta/src/property/values/unordered_container.rs index 1766f6e5..decd40a1 100644 --- a/crates/ltk_meta/src/property/values/unordered_container.rs +++ b/crates/ltk_meta/src/property/values/unordered_container.rs @@ -1,5 +1,5 @@ use crate::{ - property::Kind, + property::{Kind, NoMeta}, traits::{PropertyExt, PropertyValueExt, ReadProperty, WriteProperty}, }; @@ -13,6 +13,12 @@ use super::Container; #[derive(Clone, PartialEq, Debug, Default)] pub struct UnorderedContainer(pub Container); +impl UnorderedContainer { + pub fn no_meta(self) -> UnorderedContainer { + UnorderedContainer(self.0.no_meta()) + } +} + impl PropertyValueExt for UnorderedContainer { const KIND: Kind = Kind::UnorderedContainer; } @@ -21,6 +27,14 @@ impl PropertyExt for UnorderedContainer { fn size_no_header(&self) -> usize { self.0.size_no_header() } + + type Meta = M; + fn meta(&self) -> &Self::Meta { + self.0.meta() + } + fn meta_mut(&mut self) -> &mut Self::Meta { + self.0.meta_mut() + } } impl ReadProperty for UnorderedContainer { diff --git a/crates/ltk_meta/src/traits.rs b/crates/ltk_meta/src/traits.rs index e718f92f..be1a85f5 100644 --- a/crates/ltk_meta/src/traits.rs +++ b/crates/ltk_meta/src/traits.rs @@ -1,13 +1,13 @@ use std::io; -use super::property::{values::*, Kind, PropertyValueEnum}; -use byteorder::{ReadBytesExt, WriteBytesExt}; -use enum_dispatch::enum_dispatch; +use crate::PropertyValueEnum; + +use super::property::Kind; +use byteorder::{ReadBytesExt, WriteBytesExt, LE}; const HEADER_SIZE: usize = 5; /// General methods for property values -#[enum_dispatch] pub trait PropertyExt { /// Get the size of the property value, including the kind header if specified fn size(&self, include_header: bool) -> usize { @@ -18,6 +18,10 @@ pub trait PropertyExt { } } fn size_no_header(&self) -> usize; + + type Meta; + fn meta(&self) -> &Self::Meta; + fn meta_mut(&mut self) -> &mut Self::Meta; } pub trait PropertyValueExt { @@ -48,6 +52,22 @@ pub trait ReaderExt: io::Read { fn read_property_kind(&mut self, legacy: bool) -> Result { Kind::unpack(self.read_u8()?, legacy) } + + fn read_property( + &mut self, + legacy: bool, + ) -> Result<(u32, PropertyValueEnum), crate::Error> + where + Self: io::Seek, + { + let name_hash = self.read_u32::()?; + let kind = self.read_property_kind(legacy)?; + + Ok(( + name_hash, + PropertyValueEnum::from_reader(self, kind, legacy)?, + )) + } } impl ReaderExt for R {} @@ -66,6 +86,21 @@ pub trait WriterExt: io::Write { fn write_property_kind(&mut self, kind: Kind) -> Result<(), io::Error> { self.write_u8(kind.into()) } + + fn write_property( + &mut self, + name_hash: u32, + value: &PropertyValueEnum, + ) -> Result<(), io::Error> + where + M: Clone, + Self: io::Seek, + { + self.write_u32::(name_hash)?; + self.write_property_kind(value.kind())?; + value.to_writer(self)?; + Ok(()) + } } impl WriterExt for R {} diff --git a/crates/ltk_meta/src/tree.rs b/crates/ltk_meta/src/tree.rs index 67c6b611..af0dd94e 100644 --- a/crates/ltk_meta/src/tree.rs +++ b/crates/ltk_meta/src/tree.rs @@ -12,6 +12,8 @@ mod tests; use indexmap::IndexMap; +use crate::property::NoMeta; + /// The complete contents of a League of Legends property bin file. /// /// It contains a collection of objects, each identified @@ -32,9 +34,13 @@ use indexmap::IndexMap; /// .dependency("base.bin") /// .build(); /// ``` -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound = "for <'dee> M: serde::Serialize + serde::Deserialize<'dee>") +)] #[derive(Debug, Clone, PartialEq)] -pub struct Bin { +pub struct Bin { /// Whether this is an override/patch bin file. pub is_override: bool, @@ -43,7 +49,7 @@ pub struct Bin { pub version: u32, /// The objects in this bin tree, keyed by their path hash. - pub objects: IndexMap, + pub objects: IndexMap>, /// List of other property bins this file depends on. /// @@ -67,7 +73,7 @@ impl Default for Bin { } } -impl Bin { +impl Bin { /// Creates a new `Bin` with the given objects and dependencies. /// /// The version is set to 3 and `is_override` is set to false. @@ -83,7 +89,7 @@ impl Bin { /// ); /// ``` pub fn new( - objects: impl IntoIterator, + objects: impl IntoIterator>, dependencies: impl IntoIterator>, ) -> Self { Self { @@ -91,7 +97,7 @@ impl Bin { is_override: false, objects: objects .into_iter() - .map(|o: BinObject| (o.path_hash, o)) + .map(|o: BinObject| (o.path_hash, o)) .collect(), dependencies: dependencies.into_iter().map(Into::into).collect(), data_overrides: Vec::new(), @@ -128,13 +134,13 @@ impl Bin { /// Returns a reference to the object with the given path hash, if it exists. #[inline] - pub fn get_object(&self, path_hash: u32) -> Option<&BinObject> { + pub fn get_object(&self, path_hash: u32) -> Option<&BinObject> { self.objects.get(&path_hash) } /// Returns a mutable reference to the object with the given path hash, if it exists. #[inline] - pub fn get_object_mut(&mut self, path_hash: u32) -> Option<&mut BinObject> { + pub fn get_object_mut(&mut self, path_hash: u32) -> Option<&mut BinObject> { self.objects.get_mut(&path_hash) } @@ -148,12 +154,12 @@ impl Bin { /// /// If an object with the same path hash already exists, it is replaced /// and the old object is returned. - pub fn add_object(&mut self, object: BinObject) -> Option { + pub fn add_object(&mut self, object: BinObject) -> Option> { self.objects.insert(object.path_hash, object) } /// Removes and returns the object with the given path hash, if it exists. - pub fn remove_object(&mut self, path_hash: u32) -> Option { + pub fn remove_object(&mut self, path_hash: u32) -> Option> { self.objects.shift_remove(&path_hash) } @@ -164,38 +170,38 @@ impl Bin { /// Returns an iterator over the objects in the tree. #[inline] - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator)> { self.objects.iter() } /// Returns a mutable iterator over the objects in the tree. #[inline] - pub fn iter_mut(&mut self) -> impl Iterator { + pub fn iter_mut(&mut self) -> impl Iterator)> { self.objects.iter_mut() } } -impl<'a> IntoIterator for &'a Bin { - type Item = (&'a u32, &'a BinObject); - type IntoIter = indexmap::map::Iter<'a, u32, BinObject>; +impl<'a, M> IntoIterator for &'a Bin { + type Item = (&'a u32, &'a BinObject); + type IntoIter = indexmap::map::Iter<'a, u32, BinObject>; fn into_iter(self) -> Self::IntoIter { self.objects.iter() } } -impl<'a> IntoIterator for &'a mut Bin { - type Item = (&'a u32, &'a mut BinObject); - type IntoIter = indexmap::map::IterMut<'a, u32, BinObject>; +impl<'a, M> IntoIterator for &'a mut Bin { + type Item = (&'a u32, &'a mut BinObject); + type IntoIter = indexmap::map::IterMut<'a, u32, BinObject>; fn into_iter(self) -> Self::IntoIter { self.objects.iter_mut() } } -impl IntoIterator for Bin { - type Item = (u32, BinObject); - type IntoIter = indexmap::map::IntoIter; +impl IntoIterator for Bin { + type Item = (u32, BinObject); + type IntoIter = indexmap::map::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.objects.into_iter() diff --git a/crates/ltk_meta/src/tree/object.rs b/crates/ltk_meta/src/tree/object.rs index a8273079..81162f92 100644 --- a/crates/ltk_meta/src/tree/object.rs +++ b/crates/ltk_meta/src/tree/object.rs @@ -8,7 +8,12 @@ use std::io; use indexmap::IndexMap; use ltk_io_ext::{measure, window_at}; -use super::super::{BinProperty, Error, PropertyValueEnum}; +use crate::{ + property::NoMeta, + traits::{ReaderExt, WriterExt}, +}; + +use super::super::{Error, PropertyValueEnum}; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; /// A node/object in the bin tree. @@ -23,20 +28,24 @@ use byteorder::{ReadBytesExt, WriteBytesExt, LE}; /// /// ``` /// use ltk_meta::BinObject; -/// use ltk_meta::property::values; +/// use ltk_meta::property::{values, NoMeta}; /// /// // Simple construction -/// let obj = BinObject::new(0x1234, 0x5678); +/// let obj = BinObject::::new(0x1234, 0x5678); /// /// // Builder pattern with properties -/// let obj = BinObject::builder(0x1234, 0x5678) +/// let obj = BinObject::::builder(0x1234, 0x5678) /// .property(0xAAAA, values::I32::new(42)) /// .property(0xBBBB, values::String::from("hello")) /// .build(); /// ``` -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct BinObject { +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(bound = "for <'dee> M: serde::Serialize + serde::Deserialize<'dee>") +)] +#[derive(Debug, Clone, PartialEq, Default)] +pub struct BinObject { /// The unique path hash identifying this object. pub path_hash: u32, @@ -44,10 +53,10 @@ pub struct BinObject { pub class_hash: u32, /// The properties of this object, keyed by their name hash. - pub properties: IndexMap, + pub properties: IndexMap>, } -impl BinObject { +impl BinObject { /// Creates a new `BinObject` with the given path and class hashes. /// /// The object starts with no properties. @@ -55,16 +64,16 @@ impl BinObject { /// # Examples /// /// ``` - /// use ltk_meta::BinObject; + /// use ltk_meta::{property::NoMeta, BinObject}; /// - /// let obj = BinObject::new(0x12345678, 0xABCDEF00); + /// let obj = BinObject::::new(0x12345678, 0xABCDEF00); /// assert!(obj.properties.is_empty()); /// ``` pub fn new(path_hash: u32, class_hash: u32) -> Self { Self { path_hash, class_hash, - properties: IndexMap::new(), + properties: IndexMap::default(), } } @@ -74,9 +83,9 @@ impl BinObject { /// /// ``` /// use ltk_meta::BinObject; - /// use ltk_meta::property::values; + /// use ltk_meta::property::{values, NoMeta}; /// - /// let obj = BinObject::builder(0x12345678, 0xABCDEF00) + /// let obj = BinObject::::builder(0x12345678, 0xABCDEF00) /// .property(0x1111, values::I32::new(42)) /// .property(0x2222, values::String::from("hello")) /// .property(0x3333, values::Bool::new(true)) @@ -99,7 +108,10 @@ impl BinObject { reader: &mut R, class_hash: u32, legacy: bool, - ) -> Result { + ) -> Result + where + M: Default, + { let size = reader.read_u32::()?; let (real_size, value) = measure(reader, |reader| { let path_hash = reader.read_u32::()?; @@ -107,8 +119,8 @@ impl BinObject { let prop_count = reader.read_u16::()? as usize; let mut properties = IndexMap::with_capacity(prop_count); for _ in 0..prop_count { - let prop = BinProperty::from_reader(reader, legacy)?; - properties.insert(prop.name_hash, prop); + let (name_hash, value) = reader.read_property::(legacy)?; + properties.insert(name_hash, value); } Ok::<_, Error>(Self { @@ -129,15 +141,18 @@ impl BinObject { /// # Arguments /// /// * `writer` - A writer that implements io::Write and io::Seek. - pub fn to_writer(&self, writer: &mut W) -> io::Result<()> { + pub fn to_writer(&self, writer: &mut W) -> io::Result<()> + where + M: Clone, + { let size_pos = writer.stream_position()?; writer.write_u32::(0)?; let (size, _) = measure(writer, |writer| { writer.write_u32::(self.path_hash)?; writer.write_u16::(self.properties.len() as _)?; - for prop in self.properties.values() { - prop.to_writer(writer)?; + for (name_hash, value) in self.properties.iter() { + writer.write_property(*name_hash, value)?; } Ok::<_, io::Error>(()) })?; @@ -147,43 +162,36 @@ impl BinObject { } /// Returns the number of properties in this object. - #[inline] + #[must_use] + #[inline(always)] pub fn len(&self) -> usize { self.properties.len() } /// Returns `true` if this object has no properties. - #[inline] + #[must_use] + #[inline(always)] pub fn is_empty(&self) -> bool { self.properties.is_empty() } /// Returns a reference to the property with the given name hash, if it exists. - #[inline] - pub fn get_property(&self, name_hash: u32) -> Option<&BinProperty> { + #[must_use] + #[inline(always)] + pub fn get_property(&self, name_hash: u32) -> Option<&PropertyValueEnum> { self.properties.get(&name_hash) } /// Returns a mutable reference to the property with the given name hash, if it exists. - #[inline] - pub fn get_property_mut(&mut self, name_hash: u32) -> Option<&mut BinProperty> { + #[must_use] + #[inline(always)] + pub fn get_property_mut(&mut self, name_hash: u32) -> Option<&mut PropertyValueEnum> { self.properties.get_mut(&name_hash) } - /// Returns a reference to the property value with the given name hash, if it exists. - #[inline] - pub fn get_value(&self, name_hash: u32) -> Option<&PropertyValueEnum> { - self.properties.get(&name_hash).map(|p| &p.value) - } - - /// Returns a mutable reference to the property value with the given name hash, if it exists. - #[inline] - pub fn get_value_mut(&mut self, name_hash: u32) -> Option<&mut PropertyValueEnum> { - self.properties.get_mut(&name_hash).map(|p| &mut p.value) - } - /// Returns `true` if this object has a property with the given name hash. - #[inline] + #[must_use] + #[inline(always)] pub fn contains_property(&self, name_hash: u32) -> bool { self.properties.contains_key(&name_hash) } @@ -192,70 +200,54 @@ impl BinObject { /// /// If a property with the same name hash already exists, it is replaced /// and the old property is returned. - pub fn set_property(&mut self, property: BinProperty) -> Option { - self.properties.insert(property.name_hash, property) - } - - /// Adds or replaces a property value. - /// - /// This is a convenience method that creates a [`BinProperty`] from the - /// name hash and value. - /// - /// If a property with the same name hash already exists, it is replaced - /// and the old property is returned. - pub fn set_value( + #[inline(always)] + pub fn insert( &mut self, name_hash: u32, - value: impl Into, - ) -> Option { - self.properties.insert( - name_hash, - BinProperty { - name_hash, - value: value.into(), - }, - ) + value: impl Into>, + ) -> Option> { + self.properties.insert(name_hash, value.into()) } /// Removes and returns the property with the given name hash, if it exists. - pub fn remove_property(&mut self, name_hash: u32) -> Option { + pub fn remove_property(&mut self, name_hash: u32) -> Option> { self.properties.shift_remove(&name_hash) } /// Returns an iterator over the properties in this object. #[inline] - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator)> { self.properties.iter() } /// Returns a mutable iterator over the properties in this object. #[inline] - pub fn iter_mut(&mut self) -> impl Iterator { + pub fn iter_mut(&mut self) -> impl Iterator)> { self.properties.iter_mut() } } -impl<'a> IntoIterator for &'a BinObject { - type Item = (&'a u32, &'a BinProperty); - type IntoIter = indexmap::map::Iter<'a, u32, BinProperty>; +impl<'a, M> IntoIterator for &'a BinObject { + type Item = (&'a u32, &'a PropertyValueEnum); + type IntoIter = indexmap::map::Iter<'a, u32, PropertyValueEnum>; fn into_iter(self) -> Self::IntoIter { self.properties.iter() } } -impl<'a> IntoIterator for &'a mut BinObject { - type Item = (&'a u32, &'a mut BinProperty); - type IntoIter = indexmap::map::IterMut<'a, u32, BinProperty>; +impl<'a, M> IntoIterator for &'a mut BinObject { + type Item = (&'a u32, &'a mut PropertyValueEnum); + type IntoIter = indexmap::map::IterMut<'a, u32, PropertyValueEnum>; fn into_iter(self) -> Self::IntoIter { self.properties.iter_mut() } } -impl IntoIterator for BinObject { - type Item = (u32, BinProperty); - type IntoIter = indexmap::map::IntoIter; +impl IntoIterator for BinObject { + type Item = (u32, PropertyValueEnum); + type IntoIter = indexmap::map::IntoIter>; fn into_iter(self) -> Self::IntoIter { self.properties.into_iter() diff --git a/crates/ltk_meta/src/tree/object/builder.rs b/crates/ltk_meta/src/tree/object/builder.rs index 732b3196..c38b3999 100644 --- a/crates/ltk_meta/src/tree/object/builder.rs +++ b/crates/ltk_meta/src/tree/object/builder.rs @@ -1,18 +1,18 @@ use indexmap::IndexMap; -use crate::{BinObject, BinProperty, PropertyValueEnum}; +use crate::{property::NoMeta, BinObject, PropertyValueEnum}; /// A builder for constructing [`BinObject`] instances. /// /// See: [`BinObject::builder`] #[derive(Debug, Clone)] -pub struct Builder { +pub struct Builder { path_hash: u32, class_hash: u32, - properties: IndexMap, + properties: IndexMap>, } -impl Builder { +impl Builder { /// See: [`BinObject::builder`] pub fn new(path_hash: u32, class_hash: u32) -> Self { Self { @@ -32,37 +32,23 @@ impl Builder { self } - /// Add a [`BinProperty`] - pub fn bin_property(mut self, prop: BinProperty) -> Self { - self.properties.insert(prop.name_hash, prop); - self - } - /// Adds a property with the given name hash and value. - /// - /// This is a convenience method that accepts any type that can be converted - /// into a [`PropertyValueEnum`]. - pub fn property(mut self, name_hash: u32, value: impl Into) -> Self { - self.properties.insert( - name_hash, - BinProperty { - name_hash, - value: value.into(), - }, - ); + pub fn property(mut self, name_hash: u32, value: impl Into>) -> Self { + self.properties.insert(name_hash, value.into()); self } - /// Adds multiple properties from [`BinProperty`] instances. - pub fn bin_properties(mut self, props: impl IntoIterator) -> Self { - for prop in props { - self.properties.insert(prop.name_hash, prop); - } + /// Adds multiple properties from an iterator of name hashes & [`PropertyValueEnum`]s. + pub fn properties( + mut self, + props: impl IntoIterator)>, + ) -> Self { + self.properties.extend(props); self } /// Builds the final [`BinObject`]. - pub fn build(self) -> BinObject { + pub fn build(self) -> BinObject { BinObject { path_hash: self.path_hash, class_hash: self.class_hash, diff --git a/crates/ltk_meta/src/tree/tests.rs b/crates/ltk_meta/src/tree/tests.rs index a4594773..6908efe4 100644 --- a/crates/ltk_meta/src/tree/tests.rs +++ b/crates/ltk_meta/src/tree/tests.rs @@ -6,18 +6,26 @@ use glam::{Mat4, Vec2, Vec3, Vec4}; use indexmap::IndexMap; use ltk_primitives::Color; -use crate::property::{values, NoMeta}; -use crate::property::{BinProperty, Kind, PropertyValueEnum}; +use crate::{ + property::{values, NoMeta}, + traits::WriterExt, +}; +use crate::{ + property::{Kind, PropertyValueEnum}, + traits::ReaderExt, +}; use crate::{Bin, BinObject as Object}; /// Helper to roundtrip a property value through write/read -fn roundtrip_property(prop: &BinProperty) -> BinProperty { +fn roundtrip_property(prop: &(u32, PropertyValueEnum)) -> (u32, PropertyValueEnum) { let mut buffer = Vec::new(); let mut cursor = Cursor::new(&mut buffer); - prop.to_writer(&mut cursor).expect("write failed"); + cursor + .write_property(prop.0, &prop.1) + .expect("write failed"); cursor.set_position(0); - BinProperty::from_reader(&mut cursor, false).expect("read failed") + cursor.read_property(false).expect("read failed") } /// Helper to roundtrip a Bin through write/read @@ -31,8 +39,8 @@ fn roundtrip_tree(tree: &Bin) -> Bin { } /// Helper to create a BinProperty with a given name hash and value -fn make_prop(name_hash: u32, value: PropertyValueEnum) -> BinProperty { - BinProperty { name_hash, value } +fn make_prop(name_hash: u32, value: PropertyValueEnum) -> (u32, PropertyValueEnum) { + (name_hash, value) } // ============================================================================= @@ -298,7 +306,7 @@ fn test_object_link_property_roundtrip() { fn test_container_empty_roundtrip() { let prop = make_prop( 0x1234, - PropertyValueEnum::Container(values::Container::empty::()), + PropertyValueEnum::Container(values::Container::empty_const::()), ); let result = roundtrip_property(&prop); assert_eq!(prop, result); @@ -334,13 +342,7 @@ fn test_container_with_strings_roundtrip() { #[test] fn test_container_with_structs_roundtrip() { let mut properties = IndexMap::new(); - properties.insert( - 0xAAAA, - BinProperty { - name_hash: 0xAAAA, - value: PropertyValueEnum::I32(values::I32::new(42)), - }, - ); + properties.insert(0xAAAA, PropertyValueEnum::I32(values::I32::new(42))); let prop = make_prop( 0x1234, @@ -405,13 +407,7 @@ fn test_optional_some_string_roundtrip() { #[test] fn test_optional_some_struct_roundtrip() { let mut properties = IndexMap::new(); - properties.insert( - 0xAAAA, - BinProperty { - name_hash: 0xAAAA, - value: PropertyValueEnum::Bool(values::Bool::from(true)), - }, - ); + properties.insert(0xAAAA, PropertyValueEnum::Bool(values::Bool::from(true))); let prop = make_prop( 0x1234, @@ -465,10 +461,7 @@ fn test_map_hash_to_struct_roundtrip() { let mut struct_props = IndexMap::new(); struct_props.insert( 0x1111, - BinProperty { - name_hash: 0x1111, - value: PropertyValueEnum::F32(values::F32::new(std::f32::consts::PI)), - }, + PropertyValueEnum::F32(values::F32::new(std::f32::consts::PI)), ); let entries = vec![( @@ -509,27 +502,12 @@ fn test_struct_empty_roundtrip() { #[test] fn test_struct_with_properties_roundtrip() { let mut properties = IndexMap::new(); - properties.insert( - 0x1111, - BinProperty { - name_hash: 0x1111, - value: PropertyValueEnum::I32(values::I32::new(42)), - }, - ); + properties.insert(0x1111, PropertyValueEnum::I32(values::I32::new(42))); properties.insert( 0x2222, - BinProperty { - name_hash: 0x2222, - value: PropertyValueEnum::String(values::String::from("test")), - }, - ); - properties.insert( - 0x3333, - BinProperty { - name_hash: 0x3333, - value: PropertyValueEnum::Bool(values::Bool::new(true)), - }, + PropertyValueEnum::String(values::String::from("test")), ); + properties.insert(0x3333, PropertyValueEnum::Bool(values::Bool::new(true))); let prop = make_prop( 0x1234, @@ -546,25 +524,16 @@ fn test_struct_with_properties_roundtrip() { #[test] fn test_struct_nested_roundtrip() { let mut inner_props = IndexMap::new(); - inner_props.insert( - 0xAAAA, - BinProperty { - name_hash: 0xAAAA, - value: PropertyValueEnum::F32(values::F32::new(1.5)), - }, - ); + inner_props.insert(0xAAAA, PropertyValueEnum::F32(values::F32::new(1.5))); let mut outer_props = IndexMap::new(); outer_props.insert( 0xBBBB, - BinProperty { - name_hash: 0xBBBB, - value: PropertyValueEnum::Struct(values::Struct { - class_hash: 0x1111, - properties: inner_props, - meta: NoMeta, - }), - }, + PropertyValueEnum::Struct(values::Struct { + class_hash: 0x1111, + properties: inner_props, + meta: NoMeta, + }), ); let prop = make_prop( @@ -588,10 +557,7 @@ fn test_embedded_roundtrip() { let mut properties = IndexMap::new(); properties.insert( 0x1111, - BinProperty { - name_hash: 0x1111, - value: PropertyValueEnum::Vector3(values::Vector3::new(Vec3::new(1.0, 2.0, 3.0))), - }, + PropertyValueEnum::Vector3(values::Vector3::new(Vec3::new(1.0, 2.0, 3.0))), ); let prop = make_prop( @@ -612,10 +578,10 @@ fn test_embedded_roundtrip() { #[test] fn test_bin_tree_object_empty_roundtrip() { - let obj = Object { + let obj = Object:: { path_hash: 0x1234, class_hash: 0x5678, - properties: IndexMap::new(), + properties: Default::default(), }; let mut buffer = Vec::new(); @@ -630,22 +596,13 @@ fn test_bin_tree_object_empty_roundtrip() { #[test] fn test_bin_tree_object_with_properties_roundtrip() { let mut properties = IndexMap::new(); - properties.insert( - 0xAAAA, - BinProperty { - name_hash: 0xAAAA, - value: PropertyValueEnum::I32(values::I32::new(100)), - }, - ); + properties.insert(0xAAAA, PropertyValueEnum::I32(values::I32::new(100))); properties.insert( 0xBBBB, - BinProperty { - name_hash: 0xBBBB, - value: PropertyValueEnum::String(values::String::from("object property")), - }, + PropertyValueEnum::String(values::String::from("object property")), ); - let obj = Object { + let obj = Object:: { path_hash: 0x1234, class_hash: 0x5678, properties, @@ -684,15 +641,9 @@ fn test_bin_tree_with_dependencies_roundtrip() { #[test] fn test_bin_tree_with_objects_roundtrip() { let mut properties = IndexMap::new(); - properties.insert( - 0xAAAA, - BinProperty { - name_hash: 0xAAAA, - value: PropertyValueEnum::I32(values::I32::new(42)), - }, - ); + properties.insert(0xAAAA, PropertyValueEnum::I32(values::I32::new(42))); - let obj = Object { + let obj = Object:: { path_hash: 0x1234, class_hash: 0x5678, properties, @@ -707,30 +658,18 @@ fn test_bin_tree_with_objects_roundtrip() { fn test_bin_tree_complex_roundtrip() { // Create a complex tree with multiple objects and various property types let mut obj1_props = IndexMap::new(); - obj1_props.insert( - 0x1111, - BinProperty { - name_hash: 0x1111, - value: PropertyValueEnum::Bool(values::Bool::new(true)), - }, - ); + obj1_props.insert(0x1111, PropertyValueEnum::Bool(values::Bool::new(true))); obj1_props.insert( 0x2222, - BinProperty { - name_hash: 0x2222, - value: PropertyValueEnum::String(values::String::from("test string")), - }, + PropertyValueEnum::String(values::String::from("test string")), ); obj1_props.insert( 0x3333, - BinProperty { - name_hash: 0x3333, - value: PropertyValueEnum::Container(values::Container::from(vec![ - values::I32::new(1), - values::I32::new(2), - values::I32::new(3), - ])), - }, + PropertyValueEnum::Container(values::Container::from(vec![ + values::I32::new(1), + values::I32::new(2), + values::I32::new(3), + ])), ); let obj1 = Object { @@ -742,17 +681,11 @@ fn test_bin_tree_complex_roundtrip() { let mut obj2_props = IndexMap::new(); obj2_props.insert( 0x4444, - BinProperty { - name_hash: 0x4444, - value: PropertyValueEnum::Vector3(values::Vector3::new(Vec3::new(1.0, 2.0, 3.0))), - }, + PropertyValueEnum::Vector3(values::Vector3::new(Vec3::new(1.0, 2.0, 3.0))), ); obj2_props.insert( 0x5555, - BinProperty { - name_hash: 0x5555, - value: PropertyValueEnum::Optional(values::F32::new(std::f32::consts::PI).into()), - }, + PropertyValueEnum::Optional(values::F32::new(std::f32::consts::PI).into()), ); let obj2 = Object { @@ -826,38 +759,26 @@ fn test_property_kind_roundtrip() { fn test_deeply_nested_struct() { // Create a deeply nested struct to test recursion handling let mut deepest_props = IndexMap::new(); - deepest_props.insert( - 0x1111, - BinProperty { - name_hash: 0x1111, - value: PropertyValueEnum::I32(values::I32::new(42)), - }, - ); + deepest_props.insert(0x1111, PropertyValueEnum::I32(values::I32::new(42))); let mut level2_props = IndexMap::new(); level2_props.insert( 0x2222, - BinProperty { - name_hash: 0x2222, - value: PropertyValueEnum::Struct(values::Struct { - class_hash: 0xAAAA, - properties: deepest_props, - meta: NoMeta, - }), - }, + PropertyValueEnum::Struct(values::Struct { + class_hash: 0xAAAA, + properties: deepest_props, + meta: NoMeta, + }), ); let mut level1_props = IndexMap::new(); level1_props.insert( 0x3333, - BinProperty { - name_hash: 0x3333, - value: PropertyValueEnum::Struct(values::Struct { - class_hash: 0xBBBB, - properties: level2_props, - meta: NoMeta, - }), - }, + PropertyValueEnum::Struct(values::Struct { + class_hash: 0xBBBB, + properties: level2_props, + meta: NoMeta, + }), ); let prop = make_prop( @@ -875,13 +796,7 @@ fn test_deeply_nested_struct() { #[test] fn test_container_with_embedded_roundtrip() { let mut embedded_props = IndexMap::new(); - embedded_props.insert( - 0x1111, - BinProperty { - name_hash: 0x1111, - value: PropertyValueEnum::U32(values::U32::new(999)), - }, - ); + embedded_props.insert(0x1111, PropertyValueEnum::U32(values::U32::new(999))); let prop = make_prop( 0x1234, diff --git a/crates/ltk_meta/tests/snapshots/meta__read.snap b/crates/ltk_meta/tests/snapshots/meta__read.snap index dd129ded..54b4e9d5 100644 --- a/crates/ltk_meta/tests/snapshots/meta__read.snap +++ b/crates/ltk_meta/tests/snapshots/meta__read.snap @@ -10,127 +10,119 @@ Bin( path_hash: 2154231397, class_hash: 1171098015, properties: { - 2257500010: { - "kind": Container, - "name_hash": 2257500010, - "value": Struct( + 2257500010: PropertyValueEnum( + kind: Container, + value: Struct( items: [ Struct( class_hash: 164488258, properties: { - 431879391: { - "kind": U8, - "name_hash": 431879391, - "value": U8( + 431879391: PropertyValueEnum( + kind: U8, + value: U8( value: 0, meta: NoMeta, ), - }, - 607245356: { - "kind": Optional, - "name_hash": 607245356, - "value": F32(Some(F32( - value: 10.0, + ), + 607245356: PropertyValueEnum( + kind: Optional, + value: F32( + value: Some(F32( + value: 10.0, + meta: NoMeta, + )), meta: NoMeta, - ))), - }, - 895082843: { - "kind": BitBool, - "name_hash": 895082843, - "value": BitBool( + ), + ), + 895082843: PropertyValueEnum( + kind: BitBool, + value: BitBool( value: true, meta: NoMeta, ), - }, - 1013213428: { - "kind": String, - "name_hash": 1013213428, - "value": String( + ), + 1013213428: PropertyValueEnum( + kind: String, + value: String( value: "ASSETS/Characters/Leona/Skins/Base/Particles/common_Black.dds", meta: NoMeta, ), - }, - 1025882318: { - "kind": String, - "name_hash": 1025882318, - "value": String( + ), + 1025882318: PropertyValueEnum( + kind: String, + value: String( value: "empty", meta: NoMeta, ), - }, - 1376955374: { - "kind": Optional, - "name_hash": 1376955374, - "value": F32(Some(F32( - value: 1.0, + ), + 1376955374: PropertyValueEnum( + kind: Optional, + value: F32( + value: Some(F32( + value: 1.0, + meta: NoMeta, + )), meta: NoMeta, - ))), - }, - 2927860839: { - "kind": Embedded, - "name_hash": 2927860839, - "value": Embedded(Struct( + ), + ), + 2927860839: PropertyValueEnum( + kind: Embedded, + value: Embedded(Struct( class_hash: 70254680, properties: { - 3031705514: { - "kind": F32, - "name_hash": 3031705514, - "value": F32( + 3031705514: PropertyValueEnum( + kind: F32, + value: F32( value: 1.0, meta: NoMeta, ), - }, + ), }, meta: NoMeta, )), - }, - 3043919889: { - "kind": String, - "name_hash": 3043919889, - "value": String( + ), + 3043919889: PropertyValueEnum( + kind: String, + value: String( value: "ASSETS/Characters/Leona/Skins/Base/Particles/common_Black.dds", meta: NoMeta, ), - }, + ), }, meta: NoMeta, ), ], meta: NoMeta, ), - }, - 2624027180: { - "kind": U16, - "name_hash": 2624027180, - "value": U16( + ), + 2624027180: PropertyValueEnum( + kind: U16, + value: U16( value: 132, meta: NoMeta, ), - }, - 3770906030: { - "kind": String, - "name_hash": 3770906030, - "value": String( + ), + 3770906030: PropertyValueEnum( + kind: String, + value: String( value: "Play_sfx_Leona_LeonaZenithBladeMissile_hit", meta: NoMeta, ), - }, - 3882058040: { - "kind": String, - "name_hash": 3882058040, - "value": String( + ), + 3882058040: PropertyValueEnum( + kind: String, + value: String( value: "Characters/Leona/Skins/Skin0/Particles/Leona_Base_E_ZB_sfx_01", meta: NoMeta, ), - }, - 3975268028: { - "kind": String, - "name_hash": 3975268028, - "value": String( + ), + 3975268028: PropertyValueEnum( + kind: String, + value: String( value: "Leona_Base_E_ZB_sfx_01", meta: NoMeta, ), - }, + ), }, ), }, diff --git a/crates/ltk_primitives/src/color.rs b/crates/ltk_primitives/src/color.rs index 3677cc82..824984f6 100644 --- a/crates/ltk_primitives/src/color.rs +++ b/crates/ltk_primitives/src/color.rs @@ -21,6 +21,13 @@ impl Color { pub const fn new(r: T, g: T, b: T, a: T) -> Self { Self { r, g, b, a } } + + pub const fn to_array(&self) -> [T; 4] + where + T: Copy, + { + [self.r, self.g, self.b, self.a] + } } impl Color { diff --git a/crates/ltk_ritobin/Cargo.toml b/crates/ltk_ritobin/Cargo.toml index 6d329e7b..ff614211 100644 --- a/crates/ltk_ritobin/Cargo.toml +++ b/crates/ltk_ritobin/Cargo.toml @@ -25,7 +25,11 @@ ltk_hash = { version = "0.2.5", path = "../ltk_hash" } ltk_primitives = { version = "0.3.2", path = "../ltk_primitives" } serde = { workspace = true, optional = true } +salsa = "0.22.0" [dev-dependencies] -insta = { workspace = true } +insta.workspace = true +pretty_assertions.workspace = true +serde.workspace = true +ltk_ritobin = { path = ".", features = ["serde"] } diff --git a/crates/ltk_ritobin/examples/bin_to_rito.rs b/crates/ltk_ritobin/examples/bin_to_rito.rs index f7ed795e..db0b73f7 100644 --- a/crates/ltk_ritobin/examples/bin_to_rito.rs +++ b/crates/ltk_ritobin/examples/bin_to_rito.rs @@ -1,4 +1,7 @@ -use ltk_ritobin::HashMapProvider; +use ltk_ritobin::{ + print::{Print, PrintConfig}, + HashMapProvider, +}; use std::{fs::File, io::BufReader, path::PathBuf, str::FromStr}; fn main() { @@ -8,7 +11,7 @@ fn main() { .and_then(|p| PathBuf::from_str(&p).ok()) .zip(args.next().and_then(|p| PathBuf::from_str(&p).ok())) else { - eprintln!("Usage: './from_bin [PATH_TO_BIN] [OUTPUT_PATH]'"); + eprintln!("Usage: './bin_to_rito [PATH_TO_BIN] [OUTPUT_PATH]'"); return; }; println!("Converting {input_path:?} to ritobin..."); @@ -25,6 +28,8 @@ fn main() { .and_then(|p| PathBuf::from_str(&p).ok()) .unwrap_or(std::env::current_dir().unwrap()), ); - let text = ltk_ritobin::write_with_hashes(&tree, &hashes).unwrap(); + let text = tree + .print_with_config(PrintConfig::default().with_hashes(hashes)) + .unwrap(); std::fs::write(output_path, text).unwrap(); } diff --git a/crates/ltk_ritobin/examples/rito_fmt.rs b/crates/ltk_ritobin/examples/rito_fmt.rs new file mode 100644 index 00000000..db8ddd42 --- /dev/null +++ b/crates/ltk_ritobin/examples/rito_fmt.rs @@ -0,0 +1,39 @@ +use std::{path::PathBuf, str::FromStr}; + +use ltk_ritobin::{ + cst::Cst, + print::{CstPrinter, PrintConfig, WrapConfig}, +}; + +fn main() { + let mut args = std::env::args().skip(1); + let Some(input_path) = args.next().and_then(|p| PathBuf::from_str(&p).ok()) else { + eprintln!("Usage: './from_bin [PATH_TO_RITOBIN]'"); + return; + }; + + let size = args + .next() + .and_then(|p| usize::from_str(&p).ok()) + .unwrap_or(80); + eprintln!("Formatting {input_path:?}... (size {size})"); + + let input = std::fs::read_to_string(input_path).unwrap(); + + let cst = Cst::parse(&input); + + // let mut str = String::new(); + // cst.print(&mut str, 0, &input); + // eprintln!("#### CST:\n{str}"); + + let mut str = String::new(); + CstPrinter::new( + &input, + &mut str, + PrintConfig::default().wrap(WrapConfig::default().line_width(size)), + ) + .print(&cst) + .unwrap(); + + println!("{str}"); +} diff --git a/crates/ltk_ritobin/examples/rito_to_bin.rs b/crates/ltk_ritobin/examples/rito_to_bin.rs new file mode 100644 index 00000000..61cc1379 --- /dev/null +++ b/crates/ltk_ritobin/examples/rito_to_bin.rs @@ -0,0 +1,37 @@ +use std::{fs::File, path::PathBuf, str::FromStr}; + +fn main() { + let mut args = std::env::args().skip(1); + let Some((input_path, output_path)) = args + .next() + .and_then(|p| PathBuf::from_str(&p).ok()) + .zip(args.next().and_then(|p| PathBuf::from_str(&p).ok())) + else { + eprintln!("Usage: './rito_to_bin [PATH_TO_RITOBIN] [OUTPUT_BIN_PATH]'"); + return; + }; + println!("Converting {input_path:?} to bin..."); + + let text = std::fs::read_to_string(input_path).unwrap(); + + let cst = ltk_ritobin::Cst::parse(&text); + if !cst.errors.is_empty() { + eprintln!("Errors while parsing:"); + for err in cst.errors { + eprintln!("- {err:#?}"); + } + return; + } + + let (bin, errors) = cst.build_bin(&text); + if !errors.is_empty() { + eprintln!("Errors while converting to bin:"); + for err in errors { + eprintln!("- {err:#?}"); + } + return; + } + + let mut file = File::create(output_path).unwrap(); + bin.to_writer(&mut file).unwrap(); +} diff --git a/crates/ltk_ritobin/src/cst.rs b/crates/ltk_ritobin/src/cst.rs new file mode 100644 index 00000000..2106adc5 --- /dev/null +++ b/crates/ltk_ritobin/src/cst.rs @@ -0,0 +1,11 @@ +mod tree; +pub use tree::Kind as TreeKind; +pub use tree::*; + +pub mod visitor; +pub use visitor::Visitor; + +pub mod builder; + +mod flat_errors; +pub use flat_errors::*; diff --git a/crates/ltk_ritobin/src/cst/builder.rs b/crates/ltk_ritobin/src/cst/builder.rs new file mode 100644 index 00000000..1f7830ba --- /dev/null +++ b/crates/ltk_ritobin/src/cst/builder.rs @@ -0,0 +1,612 @@ +use std::fmt::{LowerHex, Write}; + +use ltk_meta::{property::values, Bin, BinObject, PropertyKind, PropertyValueEnum}; + +use crate::{ + cst::{Child, Cst, Kind}, + parse::{Span, Token, TokenKind as Tok}, + typecheck::visitor::{PropertyValueExt, RitoType}, + HashProvider, RitobinName as _, +}; + +pub struct Builder { + buf: String, + hashes: H, +} + +pub fn tree(kind: Kind, children: Vec) -> Child { + Child::Tree(Cst { + span: Span::default(), + kind, + children, + errors: vec![], + }) +} +pub fn token(kind: Tok) -> Child { + Child::Token(Token { + kind, + span: Span::default(), + }) +} + +impl Default for Builder<()> { + fn default() -> Self { + Self::new(()) + } +} + +impl Builder { + pub fn new(hashes: H) -> Self { + Self { + buf: String::new(), + hashes, + } + } + + pub fn build(&mut self, bin: &Bin) -> Cst { + self.bin_to_cst(bin) + } + + /// Get a reference to the underlying text buffer all Cst's built by this builder reference in + /// their spans. + pub fn text_buffer(&self) -> &str { + &self.buf + } + /// Get the underlying text buffer all Cst's built by this builder reference in + /// their spans. + pub fn into_text_buffer(self) -> String { + self.buf + } +} + +fn hex_fmt(v: T) -> String { + format!("0x{v:x}") +} + +impl Builder { + fn number(&mut self, v: impl AsRef) -> Child { + tree(Kind::Literal, vec![self.spanned_token(Tok::Number, v)]) + } + + fn spanned_token(&mut self, kind: Tok, str: impl AsRef) -> Child { + let start = self.buf.len() as u32; + self.buf.write_str(str.as_ref()).unwrap(); + let end = self.buf.len() as u32; + Child::Token(Token { + kind, + span: Span::new(start, end), + }) + } + + fn string(&mut self, v: impl AsRef) -> Child { + self.spanned_token(Tok::String, v) + } + + fn hex_lit(&mut self, v: impl AsRef) -> Child { + self.spanned_token(Tok::HexLit, v) + } + + fn hash_hash_lit(&mut self, h: u32) -> Child { + match self.hashes.lookup_hash(h).map(|h| format!("\"{h}\"")) { + Some(h) => self.spanned_token(Tok::String, h), + None => self.spanned_token(Tok::HexLit, hex_fmt(h)), + } + } + fn hash_type_lit(&mut self, h: u32) -> Child { + match self.hashes.lookup_type(h).map(|h| h.to_string()) { + Some(h) => self.spanned_token(Tok::Name, h), + None => self.spanned_token(Tok::HexLit, hex_fmt(h)), + } + } + fn hash_field_lit(&mut self, h: u32) -> Child { + match self.hashes.lookup_field(h).map(|h| h.to_string()) { + Some(h) => self.spanned_token(Tok::Name, h), + None => self.spanned_token(Tok::HexLit, hex_fmt(h)), + } + } + fn hash_entry_lit(&mut self, h: u32) -> Child { + match self.hashes.lookup_entry(h).map(|h| format!("\"{h}\"")) { + Some(h) => self.spanned_token(Tok::String, h), + None => self.spanned_token(Tok::HexLit, hex_fmt(h)), + } + } + + fn block(&self, children: Vec) -> Child { + tree( + Kind::Block, + [vec![token(Tok::LCurly)], children, vec![token(Tok::RCurly)]].concat(), + ) + } + + fn bool(&self, v: bool) -> Child { + tree( + Kind::Literal, + vec![token(match v { + true => Tok::True, + false => Tok::False, + })], + ) + } + + fn value_to_cst(&mut self, value: &PropertyValueEnum) -> Child { + match value { + PropertyValueEnum::Bool(b) => self.bool(**b), + PropertyValueEnum::BitBool(b) => self.bool(**b), + + PropertyValueEnum::U8(n) => self.number(n.to_string()), + PropertyValueEnum::U16(n) => self.number(n.to_string()), + PropertyValueEnum::U32(n) => self.number(n.to_string()), + PropertyValueEnum::U64(n) => self.number(n.to_string()), + PropertyValueEnum::I8(n) => self.number(n.to_string()), + PropertyValueEnum::I16(n) => self.number(n.to_string()), + PropertyValueEnum::I32(n) => self.number(n.to_string()), + PropertyValueEnum::I64(n) => self.number(n.to_string()), + PropertyValueEnum::F32(n) => self.number(n.to_string()), + PropertyValueEnum::Vector2(v) => { + let items = v + .to_array() + .iter() + .map(|v| tree(Kind::ListItem, vec![self.number(v.to_string())])) + .collect(); + self.block(items) + } + PropertyValueEnum::Vector3(v) => { + let items = v + .to_array() + .iter() + .map(|v| tree(Kind::ListItem, vec![self.number(v.to_string())])) + .collect(); + self.block(items) + } + PropertyValueEnum::Vector4(v) => { + let items = v + .to_array() + .iter() + .map(|v| tree(Kind::ListItem, vec![self.number(v.to_string())])) + .collect(); + self.block(items) + } + PropertyValueEnum::Matrix44(v) => { + let items = v + .transpose() // ritobin text stores matrices row-major, glam::Mat4 is column-major. + .to_cols_array_2d() + .iter() + .flat_map(|v| { + [ + tree(Kind::ListItem, vec![self.number(v[0].to_string())]), + tree(Kind::ListItem, vec![self.number(v[1].to_string())]), + tree(Kind::ListItem, vec![self.number(v[2].to_string())]), + tree(Kind::ListItem, vec![self.number(v[3].to_string())]), + ] + }) + .collect(); + self.block(items) + } + PropertyValueEnum::Color(v) => { + let items = v + .to_array() + .iter() + .map(|v| tree(Kind::ListItem, vec![self.number(v.to_string())])) + .collect(); + self.block(items) + } + PropertyValueEnum::String(s) => tree( + Kind::Literal, + vec![token(Tok::Quote), self.string(&**s), token(Tok::Quote)], + ), + + // hash/hash-likes + PropertyValueEnum::Hash(h) => self.hash_hash_lit(**h), + PropertyValueEnum::WadChunkLink(h) => self.hex_lit(hex_fmt(**h)), + PropertyValueEnum::ObjectLink(h) => self.hash_hash_lit(**h), + + PropertyValueEnum::Container(container) + | PropertyValueEnum::UnorderedContainer(values::UnorderedContainer(container)) => { + let mut children = vec![token(Tok::LCurly)]; + + for item in container.clone().into_items() { + children.push(tree(Kind::ListItem, vec![self.value_to_cst(&item)])); + } + + children.push(token(Tok::RCurly)); + tree(Kind::TypeArgList, children) + } + PropertyValueEnum::Embedded(values::Embedded(s)) | PropertyValueEnum::Struct(s) => { + let k = self.hash_type_lit(s.class_hash); + let children = s + .properties + .iter() + .map(|(k, v)| self.property_to_cst(*k, v)) + .collect(); + tree(Kind::Class, vec![k, self.block(children)]) + } + + PropertyValueEnum::Optional(optional) => { + let children = match optional.clone().into_inner() { + Some(v) => vec![self.value_to_cst(&v)], + None => vec![], + }; + self.block(children) + } + PropertyValueEnum::None(_) => tree(Kind::Literal, vec![token(Tok::Null)]), + + PropertyValueEnum::Map(map) => { + let children = map + .entries() + .iter() + .map(|(k, v)| { + tree( + Kind::Entry, + vec![self.value_to_cst(k), token(Tok::Eq), self.value_to_cst(v)], + ) + }) + .collect(); + self.block(children) + } + } + } + + fn entry(&self, key: Child, kind: Option, value: Child) -> Child { + tree( + Kind::Entry, + match kind { + Some(kind) => vec![key, token(Tok::Colon), kind, token(Tok::Eq), value], + None => vec![key, token(Tok::Eq), value], + }, + ) + } + + fn rito_type(&mut self, rito_type: RitoType) -> Child { + let mut children = vec![self.spanned_token(Tok::Name, rito_type.base.to_rito_name())]; + + if let Some(sub) = rito_type.subtypes[0] { + let mut args = vec![ + token(Tok::LBrack), + tree( + Kind::TypeArg, + vec![self.spanned_token(Tok::Name, sub.to_rito_name())], + ), + ]; + if let Some(sub) = rito_type.subtypes[1] { + args.push(tree( + Kind::TypeArg, + vec![self.spanned_token(Tok::Name, sub.to_rito_name())], + )); + } + args.push(token(Tok::RBrack)); + children.push(tree(Kind::TypeArgList, args)); + } + + tree(Kind::TypeExpr, children) + } + fn property_to_cst(&mut self, name_hash: u32, value: &PropertyValueEnum) -> Child { + let k = self.hash_field_lit(name_hash); + let t = self.rito_type(value.rito_type()); + let v = self.value_to_cst(value); + self.entry(k, Some(t), v) + } + + fn class(&self, class_name: Child, items: Vec) -> Child { + tree(Kind::Class, vec![class_name, self.block(items)]) + } + + fn bin_object_to_cst(&mut self, obj: &BinObject) -> Child { + let k = self.hash_entry_lit(obj.path_hash); + + let class_hash = self.hash_type_lit(obj.class_hash); + let class_values = obj + .properties + .iter() + .map(|(k, v)| { + let k = tree(Kind::EntryKey, vec![self.hash_field_lit(*k)]); + let t = self.rito_type(v.rito_type()); + let v = self.value_to_cst(v); + self.entry(k, Some(t), v) + }) + .collect(); + + let value = self.class(class_hash, class_values); + self.entry(k, None, value) + } + + fn bin_to_cst(&mut self, bin: &Bin) -> Cst { + let mut entries = Vec::new(); + + for obj in bin.objects.values() { + entries.push(self.bin_object_to_cst(obj)); + } + + let entries_key = self.spanned_token(Tok::Name, "entries"); + let entries_type = self.rito_type(RitoType { + base: ltk_meta::PropertyKind::Map, + subtypes: [Some(PropertyKind::Hash), Some(PropertyKind::Embedded)], + }); + let entries = self.entry( + tree(Kind::EntryKey, vec![entries_key]), + Some(entries_type), + tree(Kind::EntryValue, vec![self.block(entries)]), + ); + + Cst { + kind: Kind::File, + span: Span::default(), + children: vec![entries], + errors: vec![], + } + } +} + +#[cfg(test)] +mod test { + use glam::Vec2; + use ltk_meta::{ + property::{values, NoMeta}, + Bin, BinObject, + }; + + use super::*; + use crate::print::CstPrinter; + + // bin -> cst -> txt -> cst -> bin + fn roundtrip(bin: Bin) { + println!("bin: {bin:#?}"); + + let mut builder = Builder::default(); + let cst = builder.build(&bin); + let buf = builder.text_buffer(); + + let mut str = String::new(); + cst.print(&mut str, 0, buf); + + println!("cst:\n{str}"); + + let mut str = String::new(); + + CstPrinter::new(buf, &mut str, Default::default()) + .print(&cst) + .unwrap(); + println!("RITOBIN:\n{str}"); + + let cst2 = Cst::parse(&str); + assert!( + cst2.errors.is_empty(), + "errors parsing ritobin - {:#?}", + cst2.errors + ); + let (bin2, errors) = cst2.build_bin(&str); + + assert!( + errors.is_empty(), + "errors building tree from reparsed ritobin - {errors:#?}" + ); + + pretty_assertions::assert_eq!(bin2, bin); + } + + #[test] + fn null() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property(0x1, values::None::default()) + .build(), + ) + .build(), + ); + } + + #[test] + fn numerics() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property(0x1, values::U64::new(12)) + .property(0x2, values::U32::new(23)) + .property(0x3, values::U16::new(34)) + .property(0x4, values::U8::new(45)) + .property(0x11, values::I64::new(-12)) + .property(0x22, values::I32::new(-23)) + .property(0x33, values::I16::new(-34)) + .property(0x44, values::I8::new(45)) + .property(0x66, values::Hash::new(123123)) + .property(0x99, values::F32::new(-45.45345)) + .property(0x98, values::F32::new(199999.)) + .build(), + ) + .build(), + ); + } + #[test] + fn vectors_colors_and_matrices() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property(0x1, values::Vector2::new(glam::Vec2::new(0.1, -65.0))) + .property(0x2, values::Vector3::new(glam::Vec3::new(1000., -0.0, 2.))) + .property( + 0x3, + values::Vector4::new(glam::Vec4::new(0.1, -65.0, 100.0, 481.)), + ) + .property( + 0x4, + values::Color::new(ltk_primitives::Color { + r: 123, + g: 255, + b: 2, + a: 5, + }), + ) + .property( + 0x5, + values::Matrix44::new(glam::Mat4::from_cols_array(&[ + 0.1, 0.5, 0.7, 0.9, 10.0, 11.2, 13.8, 15.3, 19.52, -0.123, -55.11, + -13.005, 23.0, 99.02, 101.1, 500.0, + ])), + ) + .build(), + ) + .build(), + ); + } + #[test] + fn bool_bitbool() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property(0x1, values::Bool::new(true)) + .property(0x2, values::Bool::new(false)) + .property(0x3, values::BitBool::new(true)) + .property(0x4, values::BitBool::new(false)) + .build(), + ) + .build(), + ); + } + #[test] + fn string() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property(0x44444444, values::String::from("hello")) + .build(), + ) + .build(), + ); + } + #[test] + fn hashes() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property(0x1, values::Hash::new(123123)) + .property(0x2, values::Hash::new(u32::MAX)) + .property(0x3, values::ObjectLink::new(123123)) + .property(0x4, values::ObjectLink::new(u32::MAX)) + .property(0x5, values::WadChunkLink::new(123123)) + .property(0x6, values::WadChunkLink::new(u64::MAX)) + .build(), + ) + .build(), + ); + } + + #[test] + fn struct_and_embedded() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property( + 0x91, + values::Struct { + class_hash: 0x123, + meta: Default::default(), + properties: [ + (0x1, values::U64::new(5).into()), + (0x2, values::U64::new(10).into()), + ] + .into_iter() + .collect(), + }, + ) + .property( + 0x92, + values::Embedded(values::Struct { + class_hash: 0x234, + meta: Default::default(), + properties: [ + (0x2, values::U64::new(5).into()), + (0x3, values::U64::new(10).into()), + ] + .into_iter() + .collect(), + }), + ) + .build(), + ) + .build(), + ); + } + + #[test] + fn lists() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property( + 0x9191919, + values::Container::new(vec![ + values::U64::new(5), + values::U64::new(6), + values::U64::new(7), + ]), + ) + .property( + 0x9191918, + values::UnorderedContainer(values::Container::new(vec![ + values::U32::new(5), + values::U32::new(6), + values::U32::new(7), + ])), + ) + .build(), + ) + .build(), + ); + } + + #[test] + fn map() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xfeeb1e, 0x111) + .property( + 0x1, + values::Map::new( + PropertyKind::String, + PropertyKind::U64, + vec![( + values::String::from("asdasd").into(), + values::U64::new(1).into(), + )], + ) + .unwrap(), + ) + .build(), + ) + .build(), + ); + } + + #[test] + fn optional() { + roundtrip( + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x12344321) + .property( + 0x1, + values::Optional::new(PropertyKind::Vector2, None).unwrap(), + ) + .property( + 0x2, + values::Optional::new( + PropertyKind::Vector2, + Some(values::Vector2::new(Vec2::new(0.1, -5.0)).into()), + ) + .unwrap(), + ) + .build(), + ) + .build(), + ); + } +} diff --git a/crates/ltk_ritobin/src/cst/flat_errors.rs b/crates/ltk_ritobin/src/cst/flat_errors.rs new file mode 100644 index 00000000..598d8633 --- /dev/null +++ b/crates/ltk_ritobin/src/cst/flat_errors.rs @@ -0,0 +1,34 @@ +use crate::{ + cst::{ + visitor::{Visit, Visitor}, + Cst, + }, + parse::Error, +}; + +#[derive(Default)] +pub struct FlatErrors { + errors: Vec, +} + +impl FlatErrors { + pub fn new() -> Self { + Self::default() + } + pub fn into_errors(self) -> Vec { + self.errors + } + + pub fn walk(tree: &Cst) -> Vec { + let mut errors = Self::new(); + tree.walk(&mut errors); + errors.into_errors() + } +} + +impl Visitor for FlatErrors { + fn exit_tree(&mut self, tree: &Cst) -> Visit { + self.errors.extend_from_slice(&tree.errors); + Visit::Continue + } +} diff --git a/crates/ltk_ritobin/src/cst/tree.rs b/crates/ltk_ritobin/src/cst/tree.rs new file mode 100644 index 00000000..feb90281 --- /dev/null +++ b/crates/ltk_ritobin/src/cst/tree.rs @@ -0,0 +1,151 @@ +use std::fmt::Display; + +use crate::{ + parse::{ + self, impls, + tokenizer::{self, Token}, + Parser, Span, + }, + typecheck::visitor::DiagnosticWithSpan, +}; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[rustfmt::skip] +pub enum Kind { + ErrorTree, + File, + TypeExpr, TypeArgList, TypeArg, + Block, BlockKey, Class, ListItem, ListItemBlock, + + Entry, EntryKey, EntryValue, EntryTerminator, + Literal, + + Comment, +} +impl Display for Kind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::ErrorTree => "error tree", + Self::File => "file", + Self::TypeExpr => "bin entry type", + Self::TypeArgList => "type argument list", + Self::TypeArg => "type argument", + Self::Block => "block", + Self::Entry => "bin entry", + Self::EntryKey => "key", + Self::EntryValue => "value", + Self::Literal => "literal", + Self::EntryTerminator => "bin entry terminator (new line or ';')", + Self::BlockKey => "key", + Self::Class => "bin class", + Self::ListItem => "list item", + Self::ListItemBlock => "list item (block)", + Self::Comment => "comment", + }) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub struct Cst { + pub span: Span, + pub kind: Kind, + pub children: Vec, + #[cfg_attr(feature = "serde", serde(skip_deserializing))] + pub errors: Vec, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug)] +pub enum Child { + Token(Token), + Tree(Cst), +} + +impl Child { + pub fn token(&self) -> Option<&Token> { + match self { + Child::Token(token) => Some(token), + Child::Tree(_) => None, + } + } + pub fn tree(&self) -> Option<&Cst> { + match self { + Child::Token(_) => None, + Child::Tree(cst) => Some(cst), + } + } + pub fn span(&self) -> Span { + match self { + Child::Token(token) => token.span, + Child::Tree(tree) => tree.span, + } + } +} + +#[macro_export] +macro_rules! format_to { + ($buf:expr) => (); + ($buf:expr, $lit:literal $($arg:tt)*) => { + { use ::std::fmt::Write as _; let _ = ::std::write!($buf, $lit $($arg)*); } + }; +} +impl Cst { + /// Parses a CST from ritobin source code. + /// + /// **NOTE:** Parsing errors will end up in [`Self::errors`] - make sure to check this if needed + /// (e.g before calling [`Self::build_bin`] later) + pub fn parse(text: &str) -> Self { + let tokens = tokenizer::lex(text); + let mut p = Parser::new(text, tokens); + impls::file(&mut p); + p.build_tree() + } + + pub fn build_bin(&self, text: &str) -> (ltk_meta::Bin, Vec) { + let mut checker = crate::typecheck::visitor::TypeChecker::new(text); + self.walk(&mut checker); + checker.collect_to_bin() + } + + pub fn print(&self, buf: &mut String, level: usize, source: &str) { + // let parent_indent = "│ ".repeat(level.saturating_sub(1)); + let parent_indent = " ".repeat(level.saturating_sub(1)); + let indent = match level > 0 { + true => " ", // "├ " + false => "", + }; + let safe_span = match self.span.end >= self.span.start { + true => &source[self.span], + false => "!!!!!!", + }; + format_to!( + buf, + "{parent_indent}{indent}{:?} - ({}..{}): {:?}\n", + self.kind, + self.span.start, + self.span.end, + safe_span + ); + for (i, child) in self.children.iter().enumerate() { + let bar = match i + 1 == self.children.len() { + true => ' ', // '└' + false => ' ', // '├' + }; + match child { + Child::Token(token) => { + format_to!( + buf, + // "{parent_indent}│ {bar} {:?} ({:?})\n", + "{parent_indent} {bar} {:?} ({:?})\n", + &source[token.span.start as _..token.span.end as _], + token.kind, + ) + } + Child::Tree(tree) => tree.print(buf, level + 1, source), + } + } + assert!(buf.ends_with('\n')); + } +} diff --git a/crates/ltk_ritobin/src/cst/visitor.rs b/crates/ltk_ritobin/src/cst/visitor.rs new file mode 100644 index 00000000..91deb50e --- /dev/null +++ b/crates/ltk_ritobin/src/cst/visitor.rs @@ -0,0 +1,72 @@ +use super::{tree::Child, Cst}; +use crate::parse::tokenizer::Token; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Visit { + Stop, + /// Skip the current tree + Skip, + Continue, +} +#[allow(unused_variables)] +pub trait Visitor { + #[must_use] + fn enter_tree(&mut self, tree: &Cst) -> Visit { + Visit::Continue + } + #[must_use] + fn exit_tree(&mut self, tree: &Cst) -> Visit { + Visit::Continue + } + #[must_use] + fn visit_token(&mut self, token: &Token, context: &Cst) -> Visit { + Visit::Continue + } +} + +pub trait VisitorExt: Sized + Visitor { + fn walk(mut self, tree: &Cst) -> Self { + tree.walk(&mut self); + self + } +} + +impl VisitorExt for T {} + +impl Cst { + pub fn walk(&self, visitor: &mut V) { + self.walk_inner(visitor); + } + + fn walk_inner(&self, visitor: &mut V) -> Visit { + if let Some(ret) = match visitor.enter_tree(self) { + Visit::Stop => Some(Visit::Stop), + Visit::Skip => Some(Visit::Continue), + _ => None, + } { + if visitor.exit_tree(self) == Visit::Stop { + return Visit::Stop; + } + return ret; + } + + for child in &self.children { + match child { + Child::Token(token) => match visitor.visit_token(token, self) { + Visit::Continue => {} + Visit::Skip => break, + Visit::Stop => return Visit::Stop, + }, + Child::Tree(child_tree) => match child_tree.walk_inner(visitor) { + Visit::Continue => {} + Visit::Skip => { + break; + } + Visit::Stop => return Visit::Stop, + }, + } + } + + visitor.exit_tree(self) + } +} diff --git a/crates/ltk_ritobin/src/error.rs b/crates/ltk_ritobin/src/error.rs deleted file mode 100644 index 350e35c0..00000000 --- a/crates/ltk_ritobin/src/error.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Error types for ritobin parsing and writing. - -// The miette Diagnostic derive macro generates code that triggers this warning -#![allow(unused_assignments)] - -use miette::{Diagnostic, SourceSpan}; -use thiserror::Error; - -/// A span in the source text (offset and length). -#[derive(Debug, Clone, Copy, Default)] -pub struct Span { - pub offset: usize, - pub len: usize, -} - -impl Span { - pub fn new(offset: usize, len: usize) -> Self { - Self { offset, len } - } -} - -impl From for SourceSpan { - fn from(span: Span) -> Self { - SourceSpan::new(span.offset.into(), span.len) - } -} - -/// Errors that can occur during ritobin parsing. -#[derive(Debug, Error, Diagnostic)] -pub enum ParseError { - #[error("unexpected end of input")] - #[diagnostic(code(ltk_ritobin::unexpected_eof))] - UnexpectedEof, - - #[error("invalid header: expected '#PROP_text'")] - #[diagnostic(code(ltk_ritobin::invalid_header))] - InvalidHeader { - #[source_code] - src: String, - #[label("expected '#PROP_text' here")] - span: SourceSpan, - }, - - #[error("unknown type name: '{type_name}'")] - #[diagnostic(code(ltk_ritobin::unknown_type), help("valid types: bool, i8, u8, i16, u16, i32, u32, i64, u64, f32, vec2, vec3, vec4, mtx44, rgba, string, hash, file, link, flag, list, list2, option, map, pointer, embed"))] - UnknownType { - type_name: String, - #[source_code] - src: String, - #[label("unknown type")] - span: SourceSpan, - }, - - #[error("invalid number: '{value}'")] - #[diagnostic(code(ltk_ritobin::invalid_number))] - InvalidNumber { - value: String, - #[source_code] - src: String, - #[label("could not parse as number")] - span: SourceSpan, - }, - - #[error("invalid hex value: '{value}'")] - #[diagnostic(code(ltk_ritobin::invalid_hex))] - InvalidHex { - value: String, - #[source_code] - src: String, - #[label("invalid hexadecimal")] - span: SourceSpan, - }, - - #[error("expected '{expected}'")] - #[diagnostic(code(ltk_ritobin::expected))] - Expected { - expected: String, - #[source_code] - src: String, - #[label("expected {expected}")] - span: SourceSpan, - }, - - #[error("missing type info for container type")] - #[diagnostic( - code(ltk_ritobin::missing_type_info), - help( - "container types require inner type specification, e.g. list[string], map[hash,embed]" - ) - )] - MissingTypeInfo { - #[source_code] - src: String, - #[label("container type needs type parameters")] - span: SourceSpan, - }, - - #[error("unexpected content after parsing")] - #[diagnostic(code(ltk_ritobin::trailing_content))] - TrailingContent { - #[source_code] - src: String, - #[label("unexpected content here")] - span: SourceSpan, - }, - - #[error("parse error: {message}")] - #[diagnostic(code(ltk_ritobin::parse_error))] - ParseErrorAt { - message: String, - #[source_code] - src: String, - #[label("{message}")] - span: SourceSpan, - }, - - #[error("invalid escape sequence")] - #[diagnostic(code(ltk_ritobin::invalid_escape))] - InvalidEscape { - #[source_code] - src: String, - #[label("invalid escape sequence")] - span: SourceSpan, - }, - - #[error("unclosed string")] - #[diagnostic(code(ltk_ritobin::unclosed_string))] - UnclosedString { - #[source_code] - src: String, - #[label("string starts here but is never closed")] - span: SourceSpan, - }, - - #[error("unclosed block")] - #[diagnostic(code(ltk_ritobin::unclosed_block))] - UnclosedBlock { - #[source_code] - src: String, - #[label("block starts here but is never closed with '}}'")] - span: SourceSpan, - }, -} - -impl ParseError { - /// Create an "expected" error with span information. - pub fn expected(expected: impl Into, src: &str, offset: usize, len: usize) -> Self { - Self::Expected { - expected: expected.into(), - src: src.to_string(), - span: SourceSpan::new(offset.into(), len), - } - } - - /// Create a parse error with span information. - pub fn at(message: impl Into, src: &str, offset: usize, len: usize) -> Self { - Self::ParseErrorAt { - message: message.into(), - src: src.to_string(), - span: SourceSpan::new(offset.into(), len), - } - } - - /// Create an unknown type error. - pub fn unknown_type( - type_name: impl Into, - src: &str, - offset: usize, - len: usize, - ) -> Self { - Self::UnknownType { - type_name: type_name.into(), - src: src.to_string(), - span: SourceSpan::new(offset.into(), len), - } - } -} - -/// Errors that can occur during ritobin writing. -#[derive(Debug, Error, Diagnostic)] -pub enum WriteError { - #[error("fmt error: {0}")] - #[diagnostic(code(ltk_ritobin::write::fmt))] - Fmt(#[from] std::fmt::Error), -} diff --git a/crates/ltk_ritobin/src/hashes.rs b/crates/ltk_ritobin/src/hashes.rs index 90a0af61..42ba87a8 100644 --- a/crates/ltk_ritobin/src/hashes.rs +++ b/crates/ltk_ritobin/src/hashes.rs @@ -28,13 +28,7 @@ pub trait HashProvider { fn lookup_type(&self, hash: u32) -> Option<&str>; } -/// A hash provider that always returns `None`, causing all hashes to be written as hex. -/// -/// This is the default provider and is useful when you don't have hash tables available. -#[derive(Debug, Clone, Copy, Default)] -pub struct HexHashProvider; - -impl HashProvider for HexHashProvider { +impl HashProvider for () { fn lookup_entry(&self, _hash: u32) -> Option<&str> { None } @@ -230,8 +224,8 @@ mod tests { use super::*; #[test] - fn test_hex_provider() { - let provider = HexHashProvider; + fn test_empty_provider() { + let provider = (); assert_eq!(provider.lookup_entry(0x12345678), None); assert_eq!(provider.lookup_field(0x12345678), None); assert_eq!(provider.lookup_hash(0x12345678), None); diff --git a/crates/ltk_ritobin/src/lib.rs b/crates/ltk_ritobin/src/lib.rs index 1ba3e2d9..d5535f7f 100644 --- a/crates/ltk_ritobin/src/lib.rs +++ b/crates/ltk_ritobin/src/lib.rs @@ -44,14 +44,14 @@ // Nom-style parsers use elided lifetimes extensively #![allow(mismatched_lifetime_syntaxes)] -pub mod error; +pub mod cst; pub mod hashes; -pub mod parser; +pub mod parse; +pub mod print; +pub mod typecheck; pub mod types; -pub mod writer; -pub use error::*; pub use hashes::*; -pub use parser::{parse, parse_to_bin_tree, RitobinFile}; pub use types::*; -pub use writer::*; + +pub use cst::Cst; diff --git a/crates/ltk_ritobin/src/parse.rs b/crates/ltk_ritobin/src/parse.rs new file mode 100644 index 00000000..7dec4b48 --- /dev/null +++ b/crates/ltk_ritobin/src/parse.rs @@ -0,0 +1,127 @@ +//! Parser for ritobin text format with CST output for better error reporting. + +mod error; +pub use error::*; + +mod parser; +pub use parser::*; + +pub mod tokenizer; +pub use tokenizer::{Token, TokenKind}; + +pub mod impls; + +mod span; +pub use span::Span; + +use crate::cst; + +#[cfg(test)] +mod test { + use crate::{cst::Cst, print::CstPrinter, typecheck::visitor::TypeChecker}; + + use super::*; + #[test] + fn smoke_test() { + let text = r#" +entries: map[hash, embed] = { + "foo" = Foo { + guy: u32 = "asdasd" + } +} +"#; + let cst = Cst::parse(text); + let errors = cst::FlatErrors::walk(&cst); + + let mut str = String::new(); + cst.print(&mut str, 0, text); + eprintln!("text len: {}", text.len()); + eprintln!("{str}\n====== errors: ======\n"); + for err in errors { + eprintln!("{:?}: {:#?}", &text[err.span], err.kind); + } + + let mut checker = TypeChecker::new(text); + cst.walk(&mut checker); + + // let (mut roots, errors) = checker.into_parts(); + let (tree, errors) = checker.collect_to_bin(); + + eprintln!("{str}\n====== type errors: ======\n"); + for err in errors { + eprintln!("{:?}: {:#?}", &text[err.span], err.diagnostic); + } + + eprintln!("==== FINAL TREE =====\n{tree:#?}"); + } + + #[test] + fn writer_test() { + let text = r#" +entries: map[hash,embed] = { + "myPath" = VfxEmitter { + a: string = "hello" + b: list[i8] = {3 6 1} + } + "cock" = VfxEmitterDefinitionData { + rate: embed = ValueFloat { + constantValue: f32 = 1 + } + particleLifetime: embed = ValueFloat { + constantValue: f32 = 1 + } + particleLinger: option[f32] = { + 2 + } + lifetime: option[f32] = { + 1 + } + emitterName: string = "JudgementCut" + bindWeight: embed = ValueFloat { + constantValue: f32 = 1 + } + primitive: pointer = VfxPrimitiveMesh { + mMesh: embed = VfxMeshDefinitionData { + mMeshName: string = "ASSETS/Characters/viego/Skins/base/judgementcut.skn" + mMeshSkeletonName: string = "ASSETS/Characters/viego/Skins/base/judgementcut.skl" + mAnimationName: string = "ASSETS/Characters/viego/Skins/base/judgementcut.anm" + } + } + birthScale0: embed = ValueVector3 { + constantValue: vec3 = { 15, 15, 15 } + } + blendMode: u8 = 1 + disableBackfaceCull: bool = true + miscRenderFlags: u8 = 1 + texture: string = "ASSETS/Characters/viego/Skins/base/slashes.dds" + particleUVScrollRate: embed = IntegratedValueVector2 { + constantValue: vec2 = { 1, 0 } + dynamics: pointer = VfxAnimatedVector2fVariableData { + times: list[f32] = { + 0 + } + values: list[vec2] = { + { 1, 0 } + } + } + } + } +} +"#; + let cst = Cst::parse(text); + + let mut str = String::new(); + cst.print(&mut str, 0, text); + + println!("============= CST ==========="); + println!("{str}"); + + let mut str = String::new(); + CstPrinter::new(text, &mut str, Default::default()) + .print(&cst) + .unwrap(); + + println!("{}", "*".repeat(80)); + println!("========\n{str}"); + } +} diff --git a/crates/ltk_ritobin/src/parse/error.rs b/crates/ltk_ritobin/src/parse/error.rs new file mode 100644 index 00000000..f6c68980 --- /dev/null +++ b/crates/ltk_ritobin/src/parse/error.rs @@ -0,0 +1,29 @@ +use crate::parse::{cst, tokenizer::TokenKind, Span}; + +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[derive(Debug, Clone, Copy)] +pub enum ErrorKind { + Expected { + expected: TokenKind, + got: TokenKind, + }, + ExpectedAny { + expected: &'static [TokenKind], + got: TokenKind, + }, + UnterminatedString, + Unexpected { + token: TokenKind, + }, + /// When the entire tree we're in is unexpected + UnexpectedTree, + Custom(&'static str), +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[derive(Debug, Clone, Copy)] +pub struct Error { + pub span: Span, + pub tree: cst::Kind, + pub kind: ErrorKind, +} diff --git a/crates/ltk_ritobin/src/parse/impls.rs b/crates/ltk_ritobin/src/parse/impls.rs new file mode 100644 index 00000000..71e73f62 --- /dev/null +++ b/crates/ltk_ritobin/src/parse/impls.rs @@ -0,0 +1,196 @@ +use crate::parse::{ + cst::Kind as TreeKind, + error::ErrorKind, + parser::{MarkClosed, MarkOpened, Parser}, + tokenizer::TokenKind, +}; + +use TokenKind::*; + +pub fn file(p: &mut Parser) { + let m = p.open(); + while !p.eof() { + if p.at(Comment) { + p.scope(TreeKind::Comment, |p| p.advance()); + } + stmt_or_list_item(p); + } + p.close(m, TreeKind::File); +} + +pub fn stmt_or_list_item(p: &mut Parser) -> (MarkClosed, TreeKind) { + let res; + match (p.nth(0), p.nth(1), p.nth(2)) { + (Name | HexLit, LCurly, _) => { + let m = p.open(); + p.advance(); + let block = block(p); + p.close(block, TreeKind::Block); + res = (p.close(m, TreeKind::Class), TreeKind::Class); + } + (Name | String | HexLit, Colon | Eq, _) => { + res = (stmt(p), TreeKind::Entry); + } + (LCurly, _, _) => { + let m = block(p); + res = (p.close(m, TreeKind::ListItemBlock), TreeKind::ListItemBlock); + p.eat(Comma); + } + (Name | HexLit | String | Number | True | False, _, _) => { + let m = p.open(); + p.scope(TreeKind::Literal, |p| p.advance()); + res = (p.close(m, TreeKind::ListItem), TreeKind::ListItem); + p.eat(Comma); + } + _ => { + res = (stmt(p), TreeKind::Entry); + } + } + + while p.eat(Newline) {} + + res +} + +pub fn stmt(p: &mut Parser) -> MarkClosed { + let m = p.open(); + + p.scope(TreeKind::EntryKey, |p| { + p.expect_any(&[Name, String, HexLit]) + }); + if p.eat_any(&[Colon, Eq, Newline]) == Some(Colon) { + type_expr(p); + p.expect(TokenKind::Eq); + } + + if !entry_value(p) { + return p.close(m, TreeKind::Entry); + } + + if p.at(TokenKind::RCurly) { + p.scope(TreeKind::EntryTerminator, |_| {}); + let g = p.close(m, TreeKind::Entry); + return g; + } + + p.scope(TreeKind::EntryTerminator, |p| { + let mut one = false; + if p.eof() { + return; + } + while p + .eat_any(&[TokenKind::SemiColon, TokenKind::Newline]) + .is_some() + { + one = true; + } + + if !one { + // if something was between us and our statement terminator, + // we eat it all and then try again + p.scope(TreeKind::ErrorTree, |p| { + while !matches!( + p.nth(0), + TokenKind::SemiColon | TokenKind::Newline | TokenKind::Eof + ) { + p.advance(); + } + p.report(ErrorKind::UnexpectedTree); + }); + while p + .eat_any(&[TokenKind::SemiColon, TokenKind::Newline]) + .is_some() + {} + } + }); + p.close(m, TreeKind::Entry) +} + +pub fn entry_value(p: &mut Parser) -> bool { + p.scope(TreeKind::EntryValue, |p| { + match (p.nth(0), p.nth(1)) { + (Name, _) | (HexLit, LCurly) => { + // p.scope(TreeKind::ListItem, |p| { + p.scope(TreeKind::Class, |p| { + p.advance(); + if p.at(LCurly) { + let block = block(p); + p.close(block, TreeKind::Block); + } + }); + // }); + } + (UnterminatedString, _) => { + p.scope(TreeKind::Literal, |p| { + p.advance_with_error(ErrorKind::UnterminatedString, None) + }); + return false; + } + (String | Number | HexLit | True | False | Null, _) => { + p.scope(TreeKind::Literal, |p| p.advance()); + } + (LCurly, _) => { + let block = block(p); + p.close(block, TreeKind::Block); + } + (Newline, _) => { + p.advance_with_error(ErrorKind::Unexpected { token: Newline }, None); + while p.eat(Newline) {} + return false; + } + (token @ TokenKind::Eof, _) => p.report(ErrorKind::Unexpected { token }), + (token, _) => p.advance_with_error(ErrorKind::Unexpected { token }, None), + } + true + }) + .0 +} + +pub fn type_expr(p: &mut Parser) { + p.scope(TreeKind::TypeExpr, |p| { + p.expect(TokenKind::Name); + if p.eat(TokenKind::LBrack) { + p.scope(TreeKind::TypeArgList, |p| { + while !p.at(TokenKind::RBrack) && !p.eof() { + if p.at(TokenKind::Name) { + expr_type_arg(p); + } else { + break; + } + } + }); + while !p.at(TokenKind::RBrack) { + p.advance(); + } + p.expect(TokenKind::RBrack); + } + }); +} + +pub fn expr_type_arg(p: &mut Parser) { + assert!(p.at(Name)); + let m = p.open(); + + p.expect(Name); + p.close(m, TreeKind::TypeArg); + + if !p.at(RBrack) { + p.expect(Comma); + } +} + +#[must_use] +pub fn block(p: &mut Parser) -> MarkOpened { + assert!(p.at(LCurly)); + let m = p.open(); + p.expect(LCurly); + while !p.at(RCurly) && !p.eof() { + let (mark, kind) = stmt_or_list_item(p); + if kind == TreeKind::Class { + let m = p.open_before(mark); + p.close(m, TreeKind::ListItem); + } + } + p.expect(RCurly); + m +} diff --git a/crates/ltk_ritobin/src/parse/parser.rs b/crates/ltk_ritobin/src/parse/parser.rs new file mode 100644 index 00000000..65c8d46a --- /dev/null +++ b/crates/ltk_ritobin/src/parse/parser.rs @@ -0,0 +1,265 @@ +use std::cell::Cell; + +use crate::parse::{ + cst::{Child, Cst, Kind as TreeKind}, + error::{Error, ErrorKind}, + tokenizer::{Token, TokenKind}, + Span, +}; + +pub enum Event { + Open { kind: TreeKind }, + Close, + Error { kind: ErrorKind, span: Option }, + Advance, +} + +pub struct MarkOpened { + index: usize, +} +pub struct MarkClosed { + index: usize, +} + +pub struct Parser<'a> { + pub text: &'a str, + pub tokens: Vec, + pos: usize, + fuel: Cell, + pub events: Vec, +} + +impl<'a> Parser<'a> { + pub fn new(text: &'a str, tokens: Vec) -> Parser { + Parser { + text, + tokens, + pos: 0, + fuel: Cell::new(256), + events: Vec::new(), + } + } + + pub fn build_tree(self) -> Cst { + let last_token = self.tokens.last().copied(); + + let mut tokens = self.tokens.into_iter().peekable(); + let mut events = self.events; + + assert!(matches!(events.pop(), Some(Event::Close))); + let mut stack = Vec::new(); + let mut last_span = Span::default(); + let mut just_opened = false; + for event in events { + match event { + Event::Open { kind } => { + just_opened = true; + stack.push(Cst { + span: Span::new(last_span.end, 0), + kind, + children: Vec::new(), + errors: Vec::new(), + }) + } + Event::Close => { + let mut tree = stack.pop().unwrap(); + let last = stack.last_mut().unwrap(); + if tree.span.end == 0 { + // empty trees + tree.span.end = tree.span.start; + } + last.span.end = tree.span.end.max(last.span.end); // update our parent tree's span + last.children.push(Child::Tree(tree)); + } + Event::Advance => { + let token = tokens.next().unwrap(); + let last = stack.last_mut().unwrap(); + + if just_opened { + // first token of the tree + last.span.start = token.span.start; + } + just_opened = false; + + last.span.end = token.span.end; + last_span = token.span; + last.children.push(Child::Token(token)); + } + Event::Error { kind, span } => { + let cur_tree = stack.last_mut().unwrap(); + let span = match cur_tree.kind { + TreeKind::ErrorTree => cur_tree.span, + _ => match kind { + // these errors are talking about what they wanted next + ErrorKind::Expected { .. } | ErrorKind::Unexpected { .. } => { + let mut span = tokens.peek().map(|t| t.span).unwrap_or(last_span); + // so we point at the character just after our token + span.end += 1; + span.start = span.end - 1; + span + } + // whole tree is the problem + ErrorKind::UnexpectedTree => cur_tree.span, + _ => span + .or(cur_tree.children.last().map(|c| c.span())) + // we can't use Tree.span.end because that's only known on Close + .unwrap_or(Span::new( + cur_tree.span.start, + last_token + .as_ref() + .map(|t| t.span.end) + .unwrap_or(cur_tree.span.start), + )), + }, + }; + cur_tree.errors.push(Error { + span, + tree: cur_tree.kind, + kind, + }); + } + } + } + + let tree = stack.pop().unwrap(); + assert!(stack.is_empty()); + assert!(tokens.next().is_none()); + tree + } + + pub(crate) fn open(&mut self) -> MarkOpened { + let mark = MarkOpened { + index: self.events.len(), + }; + self.events.push(Event::Open { + kind: TreeKind::ErrorTree, + }); + mark + } + + pub(crate) fn scope(&mut self, kind: TreeKind, mut f: F) -> (R, MarkClosed) + where + F: FnMut(&mut Self) -> R, + { + let m = MarkOpened { + index: self.events.len(), + }; + self.events.push(Event::Open { + kind: TreeKind::ErrorTree, + }); + let ret = f(self); + self.events[m.index] = Event::Open { kind }; + self.events.push(Event::Close); + (ret, MarkClosed { index: m.index }) + } + + pub(crate) fn open_before(&mut self, m: MarkClosed) -> MarkOpened { + let mark = MarkOpened { index: m.index }; + self.events.insert( + m.index, + Event::Open { + kind: TreeKind::ErrorTree, + }, + ); + mark + } + + pub(crate) fn close(&mut self, m: MarkOpened, kind: TreeKind) -> MarkClosed { + self.events[m.index] = Event::Open { kind }; + self.events.push(Event::Close); + MarkClosed { index: m.index } + } + + pub(crate) fn advance(&mut self) { + assert!(!self.eof()); + self.fuel.set(256); + self.events.push(Event::Advance); + self.pos += 1; + } + + pub(crate) fn report(&mut self, kind: ErrorKind) { + self.events.push(Event::Error { kind, span: None }); + } + + pub(crate) fn advance_with_error(&mut self, kind: ErrorKind, span: Option) { + let m = self.open(); + self.advance(); + self.events.push(Event::Error { kind, span }); + self.close(m, TreeKind::ErrorTree); + } + + pub(crate) fn eof(&self) -> bool { + self.pos == self.tokens.len() + } + + pub(crate) fn nth(&self, lookahead: usize) -> TokenKind { + if self.fuel.get() == 0 { + eprintln!("last 5 tokens behind self.pos:"); + for tok in &self.tokens[self.pos.saturating_sub(5)..self.pos] { + eprintln!(" - {:?}: {:?}", tok.kind, &self.text[tok.span]); + } + + panic!("parser is stuck") + } + self.fuel.set(self.fuel.get() - 1); + self.tokens + .get(self.pos + lookahead) + .map_or(TokenKind::Eof, |it| it.kind) + } + + pub(crate) fn at(&self, kind: TokenKind) -> bool { + self.nth(0) == kind + } + + pub(crate) fn at_any(&self, kinds: &[TokenKind]) -> Option { + kinds.contains(&self.nth(0)).then_some(self.nth(0)) + } + + pub(crate) fn eat_any(&mut self, kinds: &[TokenKind]) -> Option { + if let Some(kind) = self.at_any(kinds) { + self.advance(); + Some(kind) + } else { + None + } + } + + pub(crate) fn eat(&mut self, kind: TokenKind) -> bool { + if self.at(kind) { + self.advance(); + true + } else { + false + } + } + + pub(crate) fn expect_any(&mut self, kinds: &'static [TokenKind]) -> Option { + if let Some(kind) = self.eat_any(kinds) { + return Some(kind); + } + self.report(ErrorKind::ExpectedAny { + expected: kinds, + got: self.nth(0), + }); + None + } + + pub(crate) fn expect(&mut self, kind: TokenKind) -> bool { + if self.eat(kind) { + return true; + } + self.report(ErrorKind::Expected { + expected: kind, + got: self.nth(0), + }); + false + } + + #[allow(unused)] + pub(crate) fn expect_fallable(&mut self, kind: TokenKind) -> Result<(), ()> { + match self.expect(kind) { + true => Ok(()), + false => Err(()), + } + } +} diff --git a/crates/ltk_ritobin/src/parse/span.rs b/crates/ltk_ritobin/src/parse/span.rs new file mode 100644 index 00000000..7d008f5d --- /dev/null +++ b/crates/ltk_ritobin/src/parse/span.rs @@ -0,0 +1,56 @@ +/// A span in the source text (offset and length). +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Copy, Default)] +pub struct Span { + pub start: u32, + pub end: u32, +} + +impl Span { + #[must_use] + #[inline] + pub fn new(start: u32, end: u32) -> Self { + Self { start, end } + } + + #[must_use] + #[inline] + pub fn contains(&self, offset: u32) -> bool { + self.start <= offset && offset <= self.end + } + + #[must_use] + #[inline] + pub fn intersects(&self, other: &Span) -> bool { + self.start < other.end && other.start < self.end + } + + #[must_use] + #[inline] + pub fn len(&self) -> u32 { + self.end - self.start + } + + #[must_use] + #[inline] + pub fn is_empty(&self) -> bool { + self.end <= self.start + } +} + +impl std::ops::Index for str { + type Output = str; + + fn index(&self, index: Span) -> &Self::Output { + &self[&index] + } +} +impl std::ops::Index<&Span> for str { + type Output = str; + + fn index(&self, index: &Span) -> &Self::Output { + let start = index.start as usize; + let end = index.end as usize; + &self[start..end.min(self.len())] + } +} diff --git a/crates/ltk_ritobin/src/parse/tokenizer.rs b/crates/ltk_ritobin/src/parse/tokenizer.rs new file mode 100644 index 00000000..8fd8afdc --- /dev/null +++ b/crates/ltk_ritobin/src/parse/tokenizer.rs @@ -0,0 +1,286 @@ +use std::fmt::Display; + +use crate::parse::Span; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[rustfmt::skip] +pub enum TokenKind { + Unknown, Eof, Newline, + + LParen, RParen, LCurly, RCurly, + LBrack, RBrack, + Eq, Comma, Colon, SemiColon, + Star, Slash, + Quote, + + String, UnterminatedString, + + Comment, + + // keywords + True, False, Null, + + Name, Number, HexLit, +} + +impl TokenKind { + /// Whether we are a string/unterminated string + pub fn is_string(&self) -> bool { + matches!(self, Self::String | Self::UnterminatedString) + } + + pub fn print_value(&self) -> Option<&'static str> { + match self { + TokenKind::Unknown => None, + TokenKind::Eof => None, + TokenKind::Newline => None, + TokenKind::LParen => Some("("), + TokenKind::RParen => Some(")"), + TokenKind::LCurly => Some("{"), + TokenKind::RCurly => Some("}"), + TokenKind::LBrack => Some("["), + TokenKind::RBrack => Some("]"), + TokenKind::Eq => Some("="), + TokenKind::Comma => Some(","), + TokenKind::Colon => Some(":"), + TokenKind::SemiColon => Some(";"), + TokenKind::Star => Some("*"), + TokenKind::Slash => Some("/"), + TokenKind::Quote => Some("\""), + TokenKind::String => None, + TokenKind::UnterminatedString => None, + TokenKind::Comment => None, + TokenKind::True => Some("true"), + TokenKind::False => Some("false"), + TokenKind::Null => Some("null"), + TokenKind::Name => None, + TokenKind::Number => None, + TokenKind::HexLit => None, + } + } +} + +impl Display for TokenKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + TokenKind::Unknown => "unknown text", + TokenKind::Eof => "end of file", + TokenKind::Newline => "new line", + TokenKind::LParen => "'('", + TokenKind::RParen => "')'", + TokenKind::LCurly => "'{'", + TokenKind::RCurly => "'}'", + TokenKind::LBrack => "'['", + TokenKind::RBrack => "']'", + TokenKind::Eq => "'='", + TokenKind::Comma => "','", + TokenKind::Colon => "':'", + TokenKind::SemiColon => "';'", + TokenKind::Star => "'*'", + TokenKind::Slash => "'/'", + TokenKind::Quote => "'\"'", + TokenKind::String => "string literal", + TokenKind::UnterminatedString => "unterminated string literal", + TokenKind::True => "'true'", + TokenKind::False => "'false'", + TokenKind::Null => "'null'", + TokenKind::Name => "keyword", + TokenKind::Number => "number", + TokenKind::HexLit => "hexadecimal literal", + TokenKind::Comment => "comment", + }) + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug)] +pub struct Token { + pub kind: TokenKind, + pub span: Span, +} +pub fn lex(mut text: &str) -> Vec { + use TokenKind::*; + let punctuation = ( + "( ) { } [ ] = , : ; * /", + [ + LParen, RParen, LCurly, RCurly, LBrack, RBrack, Eq, Comma, Colon, SemiColon, Star, + Slash, + ], + ); + + let keywords = ("true false null", [True, False, Null]); + + let source = text; + + let mut result: Vec = Vec::new(); + while !text.is_empty() { + if let Some(rest) = trim(text, |it| it.is_ascii_whitespace()) { + let eaten = &source[source.len() - text.len()..source.len() - rest.len()]; + if let Some(last_token) = result.last() { + if matches!( + last_token.kind, + TokenKind::Name + | TokenKind::HexLit + | TokenKind::True + | TokenKind::False + | TokenKind::Number + | TokenKind::RCurly + | TokenKind::String + | TokenKind::Eq + ) && eaten.find(['\n', '\r']).is_some() + { + let start = source.len() - text.len(); + let end = source.len() - rest.len(); + let span = Span::new(start as _, end as _); + result.push(Token { + span, + kind: TokenKind::Newline, + }); + } + } + + text = rest; + continue; + } + let text_orig = text; + let mut kind = 'kind: { + for (i, symbol) in punctuation.0.split_ascii_whitespace().enumerate() { + if let Some(rest) = text.strip_prefix(symbol) { + text = rest; + break 'kind punctuation.1[i]; + } + } + + if let Some(rest) = text.strip_prefix('#') { + text = rest; + if let Some(rest) = trim(text, |t| !matches!(t, '\n' | '\r')) { + text = rest; + } + break 'kind Comment; + } + + if let Some(rest) = text.strip_prefix("0x") { + text = rest; + if let Some(rest) = trim(text, |it: char| it.is_ascii_hexdigit()) { + text = rest; + } + break 'kind HexLit; + } + + if let Some(rest) = scan_number(text) { + text = rest; + break 'kind Number; + } + + if let Some(rest) = text.strip_prefix(['\'', '"']) { + let eaten = &source[source.len() - text.len()..source.len() - rest.len()] + .chars() + .next() + .unwrap(); + text = rest; + let mut skip = false; + loop { + let Some(c) = text.chars().next() else { + break 'kind UnterminatedString; + }; + + text = &text[c.len_utf8()..]; + match c { + '\\' => { + skip = true; + } + c if c == *eaten => match skip { + true => { + skip = false; + } + false => { + break 'kind String; + } + }, + '\n' | '\r' => break 'kind UnterminatedString, + _ => { + skip = false; + } + } + } + } + if let Some(rest) = trim(text, name_char) { + text = rest; + break 'kind Name; + } + + let error_index = text + .find(|it: char| it.is_ascii_whitespace()) + .unwrap_or(text.len()); + text = &text[error_index..]; + Unknown + }; + assert!(text.len() < text_orig.len()); + let token_text = &text_orig[..text_orig.len() - text.len()]; + + let start = source.len() - text_orig.len(); + let end = source.len() - text.len(); + + let span = Span { + start: start as u32, + end: end as u32, + }; + if kind == Name { + for (i, symbol) in keywords.0.split_ascii_whitespace().enumerate() { + if token_text == symbol { + kind = keywords.1[i]; + break; + } + } + } + result.push(Token { kind, span }); + } + return result; + + fn name_char(c: char) -> bool { + matches!(c, '_' | 'a'..='z' | 'A'..='Z' | '0'..='9') + } + + fn trim(text: &str, predicate: impl std::ops::Fn(char) -> bool) -> Option<&str> { + let index = text.find(|it: char| !predicate(it)).unwrap_or(text.len()); + if index == 0 { + None + } else { + Some(&text[index..]) + } + } + + fn take_num_segment(s: &str) -> Option<(&str, &str)> { + let rest = trim(s, |c| matches!(c, '0'..='9' | '_'))?; + let seg = &s[..s.len() - rest.len()]; + + if seg.is_empty() || seg.starts_with('_') || seg.ends_with('_') || seg.contains("__") { + return None; + } + + Some((seg, rest)) + } + + fn scan_number(mut s: &str) -> Option<&str> { + if let Some(rest) = s.strip_prefix('-') { + s = rest; + } + + // ".5" + if let Some(after_dot) = s.strip_prefix('.') { + let (_frac, rest) = take_num_segment(after_dot)?; + return Some(rest); + } + + // "123" / "123.45" + let (_int, mut rest) = take_num_segment(s)?; + + if let Some(after_dot) = rest.strip_prefix('.') { + let (_frac, new_rest) = take_num_segment(after_dot)?; + rest = new_rest; + } + + Some(rest) + } +} diff --git a/crates/ltk_ritobin/src/parser.rs b/crates/ltk_ritobin/src/parser.rs deleted file mode 100644 index dbe88a58..00000000 --- a/crates/ltk_ritobin/src/parser.rs +++ /dev/null @@ -1,1073 +0,0 @@ -//! Nom parser for ritobin text format with span tracking for error reporting. - -// Nom-style parsers use elided lifetimes extensively -#![allow(clippy::type_complexity)] - -use glam::{Mat4, Vec2, Vec3, Vec4}; -use indexmap::IndexMap; -use ltk_hash::fnv1a::hash_lower; -use ltk_meta::{ - property::{values, NoMeta, PropertyValueEnum}, - Bin, BinObject, BinProperty, PropertyKind, -}; -use ltk_primitives::Color; -use nom::{ - branch::alt, - bytes::complete::{is_not, tag, take_until, take_while, take_while1}, - character::complete::{char, hex_digit1, multispace1, one_of}, - combinator::{map, map_res, opt, recognize, value}, - error::{ErrorKind, FromExternalError, ParseError as NomParseError}, - multi::many0, - sequence::{delimited, pair, preceded, tuple}, - Err as NomErr, IResult, -}; -use nom_locate::LocatedSpan; - -use crate::{ - error::ParseError, - types::{type_name_to_kind, RitobinType}, -}; - -// ============================================================================ -// Span Types and Custom Error -// ============================================================================ - -/// Input type that tracks position in the source. -pub type Span<'a> = LocatedSpan<&'a str>; - -/// Custom error type that preserves span information. -#[derive(Debug, Clone)] -pub struct SpannedError<'a> { - pub span: Span<'a>, - pub kind: SpannedErrorKind, -} - -#[derive(Debug, Clone)] -pub enum SpannedErrorKind { - Nom(ErrorKind), - Expected(&'static str), - UnknownType(String), - InvalidNumber(String), - InvalidHex(String), - UnclosedString, - UnclosedBlock, - Context(&'static str), -} - -impl<'a> NomParseError> for SpannedError<'a> { - fn from_error_kind(input: Span<'a>, kind: ErrorKind) -> Self { - SpannedError { - span: input, - kind: SpannedErrorKind::Nom(kind), - } - } - - fn append(_input: Span<'a>, _kind: ErrorKind, other: Self) -> Self { - other - } -} - -impl<'a, E> FromExternalError, E> for SpannedError<'a> { - fn from_external_error(input: Span<'a>, kind: ErrorKind, _e: E) -> Self { - SpannedError { - span: input, - kind: SpannedErrorKind::Nom(kind), - } - } -} - -impl<'a> SpannedError<'a> { - pub fn expected(span: Span<'a>, what: &'static str) -> Self { - SpannedError { - span, - kind: SpannedErrorKind::Expected(what), - } - } - - pub fn unknown_type(span: Span<'a>, type_name: String) -> Self { - SpannedError { - span, - kind: SpannedErrorKind::UnknownType(type_name), - } - } - - pub fn to_parse_error(&self, src: &str) -> ParseError { - let offset = self.span.location_offset(); - let len = self.span.fragment().len().max(1); - - match &self.kind { - SpannedErrorKind::Nom(kind) => ParseError::ParseErrorAt { - message: format!("{:?}", kind), - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - SpannedErrorKind::Expected(what) => ParseError::Expected { - expected: (*what).to_string(), - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - SpannedErrorKind::UnknownType(name) => ParseError::UnknownType { - type_name: name.clone(), - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - SpannedErrorKind::InvalidNumber(val) => ParseError::InvalidNumber { - value: val.clone(), - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - SpannedErrorKind::InvalidHex(val) => ParseError::InvalidHex { - value: val.clone(), - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - SpannedErrorKind::UnclosedString => ParseError::UnclosedString { - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - SpannedErrorKind::UnclosedBlock => ParseError::UnclosedBlock { - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - SpannedErrorKind::Context(ctx) => ParseError::ParseErrorAt { - message: (*ctx).to_string(), - src: src.to_string(), - span: miette::SourceSpan::new(offset.into(), len), - }, - } - } -} - -type ParseResult<'a, T> = IResult, T, SpannedError<'a>>; - -// ============================================================================ -// Basic Parsers -// ============================================================================ - -/// Parse whitespace and comments (lines starting with #, except at the very beginning). -fn ws(input: Span) -> ParseResult<()> { - value( - (), - many0(alt(( - value((), multispace1), - value( - (), - pair(char('#'), alt((take_until("\n"), take_while(|_| true)))), - ), - ))), - )(input) -} - -/// Parse an identifier (alphanumeric + underscore, starting with letter or underscore). -fn identifier(input: Span) -> ParseResult { - preceded(ws, take_while1(|c: char| c.is_alphanumeric() || c == '_'))(input) -} - -/// Parse a word that can include various characters found in paths/identifiers. -fn word(input: Span) -> ParseResult { - preceded( - ws, - take_while1(|c: char| { - c.is_alphanumeric() || c == '_' || c == '+' || c == '-' || c == '.' || c == '/' - }), - )(input) -} - -/// Parse a quoted string with escape sequences. -fn quoted_string(input: Span) -> ParseResult { - preceded( - ws, - alt(( - delimited( - char('"'), - map( - many0(alt(( - map(is_not("\\\""), |s: Span| s.fragment().to_string()), - map(preceded(char('\\'), one_of("nrt\\\"'")), |c| match c { - 'n' => "\n".to_string(), - 'r' => "\r".to_string(), - 't' => "\t".to_string(), - '\\' => "\\".to_string(), - '"' => "\"".to_string(), - '\'' => "'".to_string(), - _ => c.to_string(), - }), - ))), - |parts| parts.join(""), - ), - char('"'), - ), - delimited( - char('\''), - map( - many0(alt(( - map(is_not("\\'"), |s: Span| s.fragment().to_string()), - map(preceded(char('\\'), one_of("nrt\\\"'")), |c| match c { - 'n' => "\n".to_string(), - 'r' => "\r".to_string(), - 't' => "\t".to_string(), - '\\' => "\\".to_string(), - '"' => "\"".to_string(), - '\'' => "'".to_string(), - _ => c.to_string(), - }), - ))), - |parts| parts.join(""), - ), - char('\''), - ), - )), - )(input) -} - -/// Parse a hex u32 (0x12345678 or decimal). -fn hex_u32(input: Span) -> ParseResult { - preceded( - ws, - alt(( - map_res( - preceded(alt((tag("0x"), tag("0X"))), hex_digit1), - |s: Span| u32::from_str_radix(s.fragment(), 16), - ), - map_res( - recognize(pair( - opt(char('-')), - take_while1(|c: char| c.is_ascii_digit()), - )), - |s: Span| s.fragment().parse::(), - ), - )), - )(input) -} - -/// Parse a hex u64 (0x123456789abcdef0 or decimal). -fn hex_u64(input: Span) -> ParseResult { - preceded( - ws, - alt(( - map_res( - preceded(alt((tag("0x"), tag("0X"))), hex_digit1), - |s: Span| u64::from_str_radix(s.fragment(), 16), - ), - map_res(take_while1(|c: char| c.is_ascii_digit()), |s: Span| { - s.fragment().parse::() - }), - )), - )(input) -} - -/// Parse a boolean. -fn parse_bool(input: Span) -> ParseResult { - preceded( - ws, - alt((value(true, tag("true")), value(false, tag("false")))), - )(input) -} - -/// Parse an integer number. -fn parse_int(input: Span) -> ParseResult { - preceded( - ws, - map_res( - recognize(pair( - opt(char('-')), - take_while1(|c: char| c.is_ascii_digit()), - )), - |s: Span| s.fragment().parse::(), - ), - )(input) -} - -/// Parse a float number. -fn parse_float(input: Span) -> ParseResult { - preceded( - ws, - map_res( - recognize(tuple(( - opt(char('-')), - take_while1(|c: char| c.is_ascii_digit() || c == '.'), - opt(pair( - one_of("eE"), - pair(opt(one_of("+-")), take_while1(|c: char| c.is_ascii_digit())), - )), - ))), - |s: Span| s.fragment().parse::(), - ), - )(input) -} - -// ============================================================================ -// Type Parsers -// ============================================================================ - -/// Parse a type name and return the BinPropertyKind. -fn parse_type_name(input: Span) -> ParseResult { - let (input, type_span) = word(input)?; - match type_name_to_kind(type_span.fragment()) { - Some(kind) => Ok((input, kind)), - None => Err(NomErr::Failure(SpannedError::unknown_type( - type_span, - type_span.fragment().to_string(), - ))), - } -} - -/// Parse container type parameters: `\[type\]` or `\[key,value\]`. -fn parse_container_type_params(input: Span) -> ParseResult<(PropertyKind, Option)> { - preceded( - ws, - delimited( - char('['), - alt(( - // map[key,value] - map( - tuple(( - parse_type_name, - preceded(tuple((ws, char(','), ws)), parse_type_name), - )), - |(k, v)| (k, Some(v)), - ), - // list[type] or option[type] - map(parse_type_name, |t| (t, None)), - )), - preceded(ws, char(']')), - ), - )(input) -} - -/// Parse a full type specification including container parameters. -fn parse_type(input: Span) -> ParseResult { - let (input, kind) = parse_type_name(input)?; - - if kind.is_container() || kind == PropertyKind::Optional { - let (input, (inner, value_kind)) = parse_container_type_params(input)?; - if kind == PropertyKind::Map { - Ok(( - input, - RitobinType::map(inner, value_kind.unwrap_or(PropertyKind::None)), - )) - } else { - Ok((input, RitobinType::container(kind, inner))) - } - } else { - Ok((input, RitobinType::simple(kind))) - } -} - -// ============================================================================ -// Value Parsers -// ============================================================================ - -/// Parse a vec2: { x, y } -fn parse_vec2(input: Span) -> ParseResult { - delimited( - preceded(ws, char('{')), - map( - tuple(( - parse_float, - preceded(tuple((ws, char(','), ws)), parse_float), - )), - |(x, y)| Vec2::new(x, y), - ), - preceded(ws, char('}')), - )(input) -} - -/// Parse a vec3: { x, y, z } -fn parse_vec3(input: Span) -> ParseResult { - delimited( - preceded(ws, char('{')), - map( - tuple(( - parse_float, - preceded(tuple((ws, char(','), ws)), parse_float), - preceded(tuple((ws, char(','), ws)), parse_float), - )), - |(x, y, z)| Vec3::new(x, y, z), - ), - preceded(ws, char('}')), - )(input) -} - -/// Parse a vec4: { x, y, z, w } -fn parse_vec4(input: Span) -> ParseResult { - delimited( - preceded(ws, char('{')), - map( - tuple(( - parse_float, - preceded(tuple((ws, char(','), ws)), parse_float), - preceded(tuple((ws, char(','), ws)), parse_float), - preceded(tuple((ws, char(','), ws)), parse_float), - )), - |(x, y, z, w)| Vec4::new(x, y, z, w), - ), - preceded(ws, char('}')), - )(input) -} - -/// Parse a mtx44: { 16 floats } -fn parse_mtx44(input: Span) -> ParseResult { - let (input, _) = preceded(ws, char('{'))(input)?; - - let mut values = [0.0f32; 16]; - let mut remaining = input; - - for (i, val) in values.iter_mut().enumerate() { - let (r, _) = ws(remaining)?; - let (r, v) = parse_float(r)?; - *val = v; - - // Handle optional comma or whitespace between values - let (r, _) = ws(r)?; - let (r, _) = opt(char(','))(r)?; - remaining = r; - - if i < 15 { - let (r, _) = ws(remaining)?; - remaining = r; - } - } - - let (remaining, _) = preceded(ws, char('}'))(remaining)?; - - // text values are row-major but from_cols_array expects column-major, - // so transpose to get the correct internal layout. - Ok((remaining, Mat4::from_cols_array(&values).transpose())) -} - -/// Parse rgba: { r, g, b, a } -fn parse_rgba(input: Span) -> ParseResult> { - delimited( - preceded(ws, char('{')), - map( - tuple(( - parse_int::, - preceded(tuple((ws, char(','), ws)), parse_int::), - preceded(tuple((ws, char(','), ws)), parse_int::), - preceded(tuple((ws, char(','), ws)), parse_int::), - )), - |(r, g, b, a)| Color::new(r, g, b, a), - ), - preceded(ws, char('}')), - )(input) -} - -/// Parse a hash value (hex or quoted string that gets hashed). -fn parse_hash_value(input: Span) -> ParseResult { - preceded(ws, alt((map(quoted_string, |s| hash_lower(&s)), hex_u32)))(input) -} - -/// Parse a file hash (u64). -fn parse_file_hash(input: Span) -> ParseResult { - preceded( - ws, - alt(( - map(quoted_string, |s| { - xxhash_rust::xxh64::xxh64(s.to_lowercase().as_bytes(), 0) - }), - hex_u64, - )), - )(input) -} - -/// Parse a link value (hash or quoted string). -fn parse_link_value(input: Span) -> ParseResult { - preceded(ws, alt((map(quoted_string, |s| hash_lower(&s)), hex_u32)))(input) -} - -/// Parse items in a list/container. -fn parse_list_items(input: Span, item_kind: PropertyKind) -> ParseResult> { - let (input, _) = preceded(ws, char('{'))(input)?; - let (input, _) = ws(input)?; - - let mut items = Vec::new(); - let mut remaining = input; - - loop { - let (r, _) = ws(remaining)?; - - // Check for closing brace - if let Ok((r, _)) = char::('}')(r) { - return Ok((r, items)); - } - - // Parse an item - let (r, item) = parse_value_for_kind(r, item_kind)?; - items.push(item); - - let (r, _) = ws(r)?; - // Optional comma or newline separator - let (r, _) = opt(char(','))(r)?; - remaining = r; - } -} - -/// Parse map entries. -fn parse_map_entries( - input: Span, - key_kind: PropertyKind, - value_kind: PropertyKind, -) -> ParseResult> { - let (input, _) = preceded(ws, char('{'))(input)?; - let (input, _) = ws(input)?; - - let mut entries = Vec::new(); - let mut remaining = input; - - loop { - let (r, _) = ws(remaining)?; - - // Check for closing brace - if let Ok((r, _)) = char::('}')(r) { - return Ok((r, entries)); - } - - // Parse key = value - let (r, key) = parse_value_for_kind(r, key_kind)?; - let (r, _) = preceded(ws, char('='))(r)?; - let (r, value) = parse_value_for_kind(r, value_kind)?; - - entries.push((key, value)); - - let (r, _) = ws(r)?; - let (r, _) = opt(char(','))(r)?; - remaining = r; - } -} - -/// Parse optional value. -fn parse_optional_value(input: Span, inner_kind: PropertyKind) -> ParseResult { - let (input, _) = preceded(ws, char('{'))(input)?; - let (input, _) = ws(input)?; - - // Check for empty optional - if let Ok((input, _)) = char::('}')(input) { - return Ok(( - input, - values::Optional::empty(inner_kind).expect("invalid item type for optional"), - )); - } - - // Parse the inner value - let (input, value) = parse_value_for_kind(input, inner_kind)?; - let (input, _) = ws(input)?; - let (input, _) = char('}')(input)?; - - Ok(( - input, - values::Optional::new(inner_kind, Some(value)).expect("valid inner item for optional"), - )) -} - -/// Parse struct/embed fields. -fn parse_struct_fields(input: Span) -> ParseResult> { - let (input, _) = preceded(ws, char('{'))(input)?; - let (input, _) = ws(input)?; - - let mut properties = IndexMap::new(); - let mut remaining = input; - - loop { - let (r, _) = ws(remaining)?; - - // Check for closing brace - if let Ok((r, _)) = char::('}')(r) { - return Ok((r, properties)); - } - - // Parse field: name: type = value - let (r, field) = parse_field(r)?; - properties.insert(field.name_hash, field); - - let (r, _) = ws(r)?; - let (r, _) = opt(char(','))(r)?; - remaining = r; - } -} - -/// Parse a single field: name: type = value -fn parse_field(input: Span) -> ParseResult { - let (input, _) = ws(input)?; - let (input, name_span) = word(input)?; - let name_str = *name_span.fragment(); - - // Determine hash from name - let name_hash = if name_str.starts_with("0x") || name_str.starts_with("0X") { - u32::from_str_radix(&name_str[2..], 16).unwrap_or(0) - } else { - hash_lower(name_str) - }; - - let (input, _) = preceded(ws, char(':'))(input)?; - let (input, ty) = parse_type(input)?; - let (input, _) = preceded(ws, char('='))(input)?; - let (input, value) = parse_value_for_type(input, &ty)?; - - Ok((input, BinProperty { name_hash, value })) -} - -/// Parse a pointer value (null or name { fields }). -fn parse_pointer_value(input: Span) -> ParseResult { - let (input, _) = ws(input)?; - - // Check for null - if let Ok((input, _)) = tag::<&str, Span, SpannedError>("null")(input) { - return Ok(( - input, - values::Struct { - class_hash: 0, - properties: IndexMap::new(), - meta: NoMeta, - }, - )); - } - - // Parse class name - let (input, class_span) = word(input)?; - let class_str = *class_span.fragment(); - let class_hash = if class_str.starts_with("0x") || class_str.starts_with("0X") { - u32::from_str_radix(&class_str[2..], 16).unwrap_or(0) - } else { - hash_lower(class_str) - }; - - // Check for empty struct - let (input, _) = ws(input)?; - if let Ok((input, _)) = tag::<&str, Span, SpannedError>("{}")(input) { - return Ok(( - input, - values::Struct { - class_hash, - properties: IndexMap::new(), - meta: NoMeta, - }, - )); - } - - // Parse fields - let (input, properties) = parse_struct_fields(input)?; - - Ok(( - input, - values::Struct { - class_hash, - properties, - meta: NoMeta, - }, - )) -} - -/// Parse an embed value (name { fields }). -fn parse_embed_value(input: Span) -> ParseResult { - let (input, struct_val) = parse_pointer_value(input)?; - Ok((input, values::Embedded(struct_val))) -} - -/// Parse a value given a BinPropertyKind. -fn parse_value_for_kind(input: Span, kind: PropertyKind) -> ParseResult { - match kind { - PropertyKind::None => { - let (input, _) = preceded(ws, tag("null"))(input)?; - Ok((input, PropertyValueEnum::None(values::None::default()))) - } - PropertyKind::Bool => { - let (input, v) = parse_bool(input)?; - Ok((input, PropertyValueEnum::Bool(values::Bool::new(v)))) - } - PropertyKind::I8 => { - let (input, v) = parse_int::(input)?; - Ok((input, PropertyValueEnum::I8(values::I8::new(v)))) - } - PropertyKind::U8 => { - let (input, v) = parse_int::(input)?; - Ok((input, PropertyValueEnum::U8(values::U8::new(v)))) - } - PropertyKind::I16 => { - let (input, v) = parse_int::(input)?; - Ok((input, PropertyValueEnum::I16(values::I16::new(v)))) - } - PropertyKind::U16 => { - let (input, v) = parse_int::(input)?; - Ok((input, PropertyValueEnum::U16(values::U16::new(v)))) - } - PropertyKind::I32 => { - let (input, v) = parse_int::(input)?; - Ok((input, PropertyValueEnum::I32(values::I32::new(v)))) - } - PropertyKind::U32 => { - let (input, v) = hex_u32(input)?; - Ok((input, PropertyValueEnum::U32(values::U32::new(v)))) - } - PropertyKind::I64 => { - let (input, v) = parse_int::(input)?; - Ok((input, PropertyValueEnum::I64(values::I64::new(v)))) - } - PropertyKind::U64 => { - let (input, v) = hex_u64(input)?; - Ok((input, PropertyValueEnum::U64(values::U64::new(v)))) - } - PropertyKind::F32 => { - let (input, v) = parse_float(input)?; - Ok((input, PropertyValueEnum::F32(values::F32::new(v)))) - } - PropertyKind::Vector2 => { - let (input, v) = parse_vec2(input)?; - Ok((input, PropertyValueEnum::Vector2(values::Vector2::new(v)))) - } - PropertyKind::Vector3 => { - let (input, v) = parse_vec3(input)?; - Ok((input, PropertyValueEnum::Vector3(values::Vector3::new(v)))) - } - PropertyKind::Vector4 => { - let (input, v) = parse_vec4(input)?; - Ok((input, PropertyValueEnum::Vector4(values::Vector4::new(v)))) - } - PropertyKind::Matrix44 => { - let (input, v) = parse_mtx44(input)?; - Ok((input, PropertyValueEnum::Matrix44(values::Matrix44::new(v)))) - } - PropertyKind::Color => { - let (input, v) = parse_rgba(input)?; - Ok((input, PropertyValueEnum::Color(values::Color::new(v)))) - } - PropertyKind::String => { - let (input, v) = preceded(ws, quoted_string)(input)?; - Ok((input, PropertyValueEnum::String(values::String::new(v)))) - } - PropertyKind::Hash => { - let (input, v) = parse_hash_value(input)?; - Ok((input, PropertyValueEnum::Hash(values::Hash::new(v)))) - } - PropertyKind::WadChunkLink => { - let (input, v) = parse_file_hash(input)?; - Ok(( - input, - PropertyValueEnum::WadChunkLink(values::WadChunkLink::new(v)), - )) - } - PropertyKind::ObjectLink => { - let (input, v) = parse_link_value(input)?; - Ok(( - input, - PropertyValueEnum::ObjectLink(values::ObjectLink::new(v)), - )) - } - PropertyKind::BitBool => { - let (input, v) = parse_bool(input)?; - Ok((input, PropertyValueEnum::BitBool(values::BitBool::new(v)))) - } - PropertyKind::Struct => { - let (input, v) = parse_pointer_value(input)?; - Ok((input, PropertyValueEnum::Struct(v))) - } - PropertyKind::Embedded => { - let (input, v) = parse_embed_value(input)?; - Ok((input, PropertyValueEnum::Embedded(v))) - } - // Container types need additional type info, handled separately - PropertyKind::Container - | PropertyKind::UnorderedContainer - | PropertyKind::Optional - | PropertyKind::Map => Err(NomErr::Failure(SpannedError::expected( - input, - "non-container type", - ))), - } -} - -/// Parse a value given a full RitobinType. -fn parse_value_for_type<'a>( - input: Span<'a>, - ty: &RitobinType, -) -> ParseResult<'a, PropertyValueEnum> { - match ty.kind { - PropertyKind::Container => { - let inner_kind = ty.inner_kind.unwrap_or(PropertyKind::None); - let (input, items) = parse_list_items(input, inner_kind)?; - Ok(( - input, - PropertyValueEnum::Container( - values::Container::try_from(items).unwrap_or_default(), - ), // TODO: handle error here - )) - } - PropertyKind::UnorderedContainer => { - let inner_kind = ty.inner_kind.unwrap_or(PropertyKind::None); - let (input, items) = parse_list_items(input, inner_kind)?; - Ok(( - input, - PropertyValueEnum::UnorderedContainer(values::UnorderedContainer( - values::Container::try_from(items).unwrap_or_default(), // TODO: handle error here - )), - )) - } - PropertyKind::Optional => { - let inner_kind = ty.inner_kind.unwrap_or(PropertyKind::None); - let (input, opt_val) = parse_optional_value(input, inner_kind)?; - Ok((input, PropertyValueEnum::Optional(opt_val))) - } - PropertyKind::Map => { - let key_kind = ty.inner_kind.unwrap_or(PropertyKind::Hash); - let value_kind = ty.value_kind.unwrap_or(PropertyKind::None); - let (input, entries) = parse_map_entries(input, key_kind, value_kind)?; - Ok(( - input, - PropertyValueEnum::Map( - values::Map::new(key_kind, value_kind, entries).expect("valid items in map"), - ), - )) - } - _ => parse_value_for_kind(input, ty.kind), - } -} - -// ============================================================================ -// Top-Level Parsers -// ============================================================================ - -/// Parse a top-level entry: key: type = value -fn parse_entry(input: Span) -> ParseResult<(String, BinProperty)> { - let (input, _) = ws(input)?; - let (input, key) = identifier(input)?; - let (input, _) = preceded(ws, char(':'))(input)?; - let (input, ty) = parse_type(input)?; - let (input, _) = preceded(ws, char('='))(input)?; - let (input, value) = parse_value_for_type(input, &ty)?; - - let name_hash = hash_lower(key.fragment()); - - Ok(( - input, - (key.fragment().to_string(), BinProperty { name_hash, value }), - )) -} - -/// Parse the entire ritobin file. -fn parse_ritobin(input: Span) -> ParseResult { - let (input, _) = ws(input)?; - // The header comment is consumed by ws, but we should verify it's present - // For now, we're lenient about the header - - let (input, entries) = many0(parse_entry)(input)?; - let (input, _) = ws(input)?; - - let mut file = RitobinFile::new(); - for (key, prop) in entries { - file.entries.insert(key, prop); - } - - Ok((input, file)) -} - -// ============================================================================ -// Public Types and API -// ============================================================================ - -/// A ritobin file representation (intermediate format before conversion to BinTree). -#[derive(Debug, Clone, Default)] -pub struct RitobinFile { - pub entries: IndexMap, -} - -impl RitobinFile { - pub fn new() -> Self { - Self { - entries: IndexMap::new(), - } - } - - /// Get the "type" field value as a string. - pub fn file_type(&self) -> Option<&str> { - self.entries.get("type").and_then(|p| { - if let PropertyValueEnum::String(s) = &p.value { - Some(s.value.as_str()) - } else { - None - } - }) - } - - /// Get the "version" field as u32. - pub fn version(&self) -> Option { - self.entries.get("version").and_then(|p| { - if let PropertyValueEnum::U32(v) = &p.value { - Some(**v) - } else { - None - } - }) - } - - /// Get the "linked" dependencies list. - pub fn linked(&self) -> Vec { - self.entries - .get("linked") - .and_then(|p| { - if let PropertyValueEnum::Container(values::Container::String { items, .. }) = - &p.value - { - Some(items.iter().cloned().map(|i| i.value).collect()) - } else { - None - } - }) - .unwrap_or_default() - } - - /// Get the "entries" map as BinTreeObjects. - pub fn objects(&self) -> IndexMap { - self.entries - .get("entries") - .and_then(|p| { - if let PropertyValueEnum::Map(map) = &p.value { - Some( - map.entries() - .iter() - .filter_map(|(key, value)| { - let path_hash = match &key { - PropertyValueEnum::Hash(h) => **h, - _ => return None, - }; - - if let PropertyValueEnum::Embedded(values::Embedded(struct_val)) = - value - { - Some(( - path_hash, - BinObject { - path_hash, - class_hash: struct_val.class_hash, - properties: struct_val.properties.clone(), - }, - )) - } else { - None - } - }) - .collect(), - ) - } else { - None - } - }) - .unwrap_or_default() - } - - /// Convert to a BinTree. - pub fn to_bin_tree(&self) -> Bin { - Bin::new(self.objects().into_values(), self.linked()) - } -} - -/// Parse ritobin text format. -pub fn parse(input: &str) -> Result { - let span = Span::new(input); - match parse_ritobin(span) { - Ok((remaining, file)) => { - let trimmed = remaining.fragment().trim(); - if !trimmed.is_empty() { - Err(ParseError::TrailingContent { - src: input.to_string(), - span: miette::SourceSpan::new( - remaining.location_offset().into(), - trimmed.len().min(50), - ), - }) - } else { - Ok(file) - } - } - Err(NomErr::Error(e)) | Err(NomErr::Failure(e)) => Err(e.to_parse_error(input)), - Err(NomErr::Incomplete(_)) => Err(ParseError::UnexpectedEof), - } -} - -/// Parse ritobin text directly to BinTree. -pub fn parse_to_bin_tree(input: &str) -> Result { - parse(input).map(|f| f.to_bin_tree()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_simple_types() { - let input = r#" -#PROP_text -type: string = "PROP" -version: u32 = 3 -"#; - let file = parse(input).unwrap(); - assert_eq!(file.file_type(), Some("PROP")); - assert_eq!(file.version(), Some(3)); - } - - #[test] - fn test_parse_list() { - let input = r#" -linked: list[string] = { - "path/to/file1.bin" - "path/to/file2.bin" -} -"#; - let file = parse(input).unwrap(); - let linked = file.linked(); - assert_eq!(linked.len(), 2); - assert_eq!(linked[0], "path/to/file1.bin"); - } - - #[test] - fn test_parse_vec3() { - let input = r#" -pos: vec3 = { 1.0, 2.5, -3.0 } -"#; - let file = parse(input).unwrap(); - let prop = file.entries.get("pos").unwrap(); - if let PropertyValueEnum::Vector3(v) = &prop.value { - assert_eq!(v.x, 1.0); - assert_eq!(v.y, 2.5); - assert_eq!(v.z, -3.0); - } else { - panic!("Expected Vector3"); - } - } - - #[test] - fn test_parse_embed() { - let input = r#" -data: embed = TestClass { - name: string = "test" - value: u32 = 42 -} -"#; - let file = parse(input).unwrap(); - let prop = file.entries.get("data").unwrap(); - if let PropertyValueEnum::Embedded(values::Embedded(s)) = &prop.value { - assert_eq!(s.class_hash, hash_lower("TestClass")); - assert_eq!(s.properties.len(), 2); - } else { - panic!("Expected Embedded"); - } - } - - #[test] - fn test_error_reporting() { - let input = r#" -test: unknowntype = 42 -"#; - let err = parse(input).unwrap_err(); - // The error should be an UnknownType with span info - match err { - ParseError::UnknownType { - type_name, span, .. - } => { - assert_eq!(type_name, "unknowntype"); - // Span should point to "unknowntype" - assert!(span.offset() > 0); - } - _ => panic!("Expected UnknownType error, got: {:?}", err), - } - } -} diff --git a/crates/ltk_ritobin/src/print.rs b/crates/ltk_ritobin/src/print.rs new file mode 100644 index 00000000..774d0e4d --- /dev/null +++ b/crates/ltk_ritobin/src/print.rs @@ -0,0 +1,309 @@ +#[derive(Debug, thiserror::Error)] +pub enum PrintError { + #[error(transparent)] + FmtError(#[from] fmt::Error), +} + +use std::fmt::{self}; + +use ltk_meta::Bin; + +use crate::hashes::HashProvider; + +pub mod command; +pub mod visitor; + +mod config; +pub use config::*; + +mod printers; +pub use printers::*; + +pub trait Print { + /// Print as ritobin code to the given writer (using default config, which prints hashes as hex). + fn print_to_writer(&self, writer: &mut W) -> Result { + Self::print_to_writer_with_config::(self, writer, Default::default()) + } + /// Print as ritobin code to the given writer, using the given config. + fn print_to_writer_with_config( + &self, + writer: &mut W, + config: PrintConfig, + ) -> Result; + + /// Print as ritobin code to a string (using default config, which prints hashes as hex). + fn print(&self) -> Result { + let mut str = String::new(); + Self::print_to_writer(self, &mut str)?; + Ok(str) + } + /// Print as ritobin code to a string, using the given config. + fn print_with_config( + &self, + config: PrintConfig, + ) -> Result { + let mut str = String::new(); + Self::print_to_writer_with_config(self, &mut str, config)?; + Ok(str) + } +} + +impl Print for Bin { + fn print_to_writer_with_config( + &self, + writer: &mut W, + config: PrintConfig, + ) -> Result { + BinPrinter::new().with_config(config).print(self, writer) + } +} + +#[cfg(test)] +mod test { + use crate::{ + cst::Cst, + print::{config::PrintConfig, CstPrinter, WrapConfig}, + }; + + fn assert_pretty(input: &str, is: &str, config: PrintConfig<()>) { + let cst = Cst::parse(input); + let mut str = String::new(); + + cst.print(&mut str, 0, input); + eprintln!("#### CST:\n{str}"); + + let mut str = String::new(); + CstPrinter::new(input, &mut str, config) + .print(&cst) + .unwrap(); + + pretty_assertions::assert_eq!(str.trim(), is.trim()); + } + + fn assert_pretty_rt(input: &str, config: PrintConfig<()>) { + assert_pretty(input, input, config); + } + + #[test] + fn simple_list() { + assert_pretty( + r#" b : list [ i8, ] = { 3, 6 1 }"#, + r#"b: list[i8] = { 3, 6, 1 }"#, + PrintConfig::default(), + ); + } + + #[test] + fn vec2_list() { + assert_pretty( + r#" vec2List : list [ vec2, ] = { {3, 6} {1 10000} }"#, + r#"vec2List: list[vec2] = { + { 3, 6 } + { 1, 10000 } +}"#, + PrintConfig::default(), + ); + } + + #[test] + fn class_list() { + assert_pretty( + r#" classList : list2[ embed] = { MyClass {a: string = "hello"} + FooClass {b: string = "foo"}}"#, + r#"classList: list2[embed] = { + MyClass { + a: string = "hello" + } + FooClass { + b: string = "foo" + } +}"#, + PrintConfig::default(), + ); + } + + #[test] + fn simple_class_embed() { + assert_pretty( + r#"skinUpgradeData: embed = skinUpgradeData { + mGearSkinUpgrades: list[link] = { 0x3b9c7079, 0x17566805 } + }"#, + r#"skinUpgradeData: embed = skinUpgradeData { + mGearSkinUpgrades: list[link] = { 0x3b9c7079, 0x17566805 } +}"#, + PrintConfig::default(), + ); + } + + #[test] + fn long_string_list() { + assert_pretty( + r#" +linked: list[string] = { "DATA/Characters/Viego/Viego.bin" + "DATA/Viego_Skins_Skin0_Skins_Skin1_Skins_Skin10_Skins_Skin11_Skins_Skin12_Skins_Skin13_Skins_Skin14_Skins_Skin15_Skins_Skin16_Skins_Skin17_Skins_Skin18_Skins_Skin2_Skins_Skin3_Skins_Skin4_Skins_Skin43_Skins_Skin5_Skins_Skin6_Skins_Skin7_Skins_Skin8.bin" +} +"#, + r#"linked: list[string] = { + "DATA/Characters/Viego/Viego.bin" + "DATA/Viego_Skins_Skin0_Skins_Skin1_Skins_Skin10_Skins_Skin11_Skins_Skin12_Skins_Skin13_Skins_Skin14_Skins_Skin15_Skins_Skin16_Skins_Skin17_Skins_Skin18_Skins_Skin2_Skins_Skin3_Skins_Skin4_Skins_Skin43_Skins_Skin5_Skins_Skin6_Skins_Skin7_Skins_Skin8.bin" +}"#, + PrintConfig::default(), + ); + } + + #[test] + fn list_of_list_of_link() { + assert_pretty( + r#"BorderAugments: list2[embed] = { + 0x4a70b12c { + AugmentGroup: list2[link] = { 0x383e4602 } + } +}"#, + r#"BorderAugments: list2[embed] = { + 0x4a70b12c { + AugmentGroup: list2[link] = { 0x383e4602 } + } +}"#, + PrintConfig::default(), + ); + } + + #[test] + fn zaahen_01() { + assert_pretty_rt( + r#"bankUnits: list2[embed] = { + BankUnit { + events: list[string] = { + "PPlay_sfx_Zaahen_Dance3D_buffactivatePlay_sfx_Zaahen_Dance3D_buffactivatelay_sfx_Zaahen_Dance3D_buffactivate" + } + } + BankUnit { } +}"#, + PrintConfig::default(), + ); + } + + #[test] + fn broken_type_arg() { + assert_pretty( + r#"thing4: list[u3 2] = { 0, 0, 0, 0, 0, 0, 0 }"#, + r#"thing4: list[u32] = { 0, 0, 0, 0, 0, 0, 0 }"#, + PrintConfig::default(), + ); + } + + #[test] + fn inline_single_field_struct() { + assert_pretty_rt( + r#"loadscreen: embed = CensoredImage { image: string = "val" }"#, + PrintConfig::default().wrap(WrapConfig::default().inline_structs(true)), + ); + } + #[test] + fn inline_nested_single_field_struct() { + assert_pretty_rt( + r#"loadscreen: embed = CensoredImage { + image: embed = Image { src: string = "val" } +}"#, + PrintConfig::default().wrap(WrapConfig::default().inline_structs(true)), + ); + } + + #[test] + fn dont_inline_nested_single_field_struct() { + assert_pretty_rt( + r#"loadscreen: embed = CensoredImage { + image: embed = Image { + src: string = "val" + } +}"#, + PrintConfig::default().wrap(WrapConfig::default().inline_structs(false)), + ); + } + + #[test] + fn dont_inline_simple_list_or_struct() { + assert_pretty_rt( + r#"loadscreen: embed = CensoredImage { + b: list[i8] = { + 3 + 6 + 1 + } + image: embed = Image { + src: string = "val" + } +}"#, + PrintConfig::default().wrap( + WrapConfig::default() + .inline_lists(false) + .inline_structs(false), + ), + ); + } + #[test] + fn dont_inline_simple_list_and_inline_struct() { + assert_pretty_rt( + r#"loadscreen: embed = CensoredImage { + b: list[i8] = { + 3 + 6 + 1 + } + image: embed = Image { src: string = "val" } +}"#, + PrintConfig::default().wrap( + WrapConfig::default() + .inline_lists(false) + .inline_structs(true), + ), + ); + } + #[test] + fn inline_simple_list_and_dont_inline_struct() { + assert_pretty_rt( + r#"loadscreen: embed = CensoredImage { + b: list[i8] = { 3, 6, 1 } + image: embed = Image { + src: string = "val" + } +}"#, + PrintConfig::default().wrap( + WrapConfig::default() + .inline_lists(true) + .inline_structs(false), + ), + ); + } + + #[test] + fn unterminated_string() { + assert_pretty_rt( + r#"ConformToPathRigPoseModifierData { + mStartingJointName: hash = "L_Clavicle + mEndingJointName: hash = "l_hand" + mDefaultMaskName: hash = 0x7136e1bc + mMaxBoneAngle: f32 = 115 + mDampingValue: f32 = 8 + mVelMultiplier: f32 = 0 + mFrequency: f32 = 20 +}"#, + PrintConfig::default(), + ) + } + + #[ignore = "nice to have"] + #[test] + fn class_body_on_new_line() { + assert_pretty( + r#"ConformToPathRigPoseModifierData + { + mFrequency: f32 = 20 +}"#, + r#"ConformToPathRigPoseModifierData { + mFrequency: f32 = 20 +}"#, + PrintConfig::default(), + ) + } +} diff --git a/crates/ltk_ritobin/src/print/command.rs b/crates/ltk_ritobin/src/print/command.rs new file mode 100644 index 00000000..5fb32de8 --- /dev/null +++ b/crates/ltk_ritobin/src/print/command.rs @@ -0,0 +1,26 @@ +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum Mode { + Flat, + Break, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Cmd<'a> { + Text(&'a str), + TextIf(&'a str, Mode), + Line, + SoftLine, + Space, + Begin(Option), + End, + Indent(usize), + Dedent(usize), +} + +impl Cmd<'_> { + #[inline(always)] + #[must_use] + pub fn is_whitespace(&self) -> bool { + matches!(self, Self::Space | Self::Line | Self::SoftLine) + } +} diff --git a/crates/ltk_ritobin/src/print/config.rs b/crates/ltk_ritobin/src/print/config.rs new file mode 100644 index 00000000..de32d246 --- /dev/null +++ b/crates/ltk_ritobin/src/print/config.rs @@ -0,0 +1,83 @@ +use crate::HashProvider; + +#[derive(Debug, Clone, Copy)] +pub struct WrapConfig { + /// Maximum line width - will try to break blocks if the line exceeds this number. + pub line_width: usize, + + /// Whether to allow the printing of structs/classes in one line + /// (subject to [`Self::line_width`] wrapping) + pub inline_structs: bool, + + /// Whether to allow the printing of lists in one line + /// (subject to [`Self::line_width`] wrapping) + pub inline_lists: bool, +} + +impl WrapConfig { + pub fn line_width(mut self, line_width: usize) -> Self { + self.line_width = line_width; + self + } + + pub fn inline_structs(mut self, inline_structs: bool) -> Self { + self.inline_structs = inline_structs; + self + } + + pub fn inline_lists(mut self, inline_lists: bool) -> Self { + self.inline_lists = inline_lists; + self + } +} + +impl Default for WrapConfig { + fn default() -> Self { + Self { + line_width: 120, + inline_structs: false, + inline_lists: true, + } + } +} + +/// Configuration for the ritobin printer. +#[derive(Debug, Clone)] +pub struct PrintConfig { + /// Number of spaces per indent level. + pub indent_size: usize, + + /// Config relating to how/when to wrap + pub wrap: WrapConfig, + + pub hashes: Hashes, +} + +impl Default for PrintConfig<()> { + fn default() -> Self { + Self { + indent_size: 4, + wrap: Default::default(), + hashes: (), + } + } +} + +impl PrintConfig { + pub fn wrap(mut self, wrap: WrapConfig) -> Self { + self.wrap = wrap; + self + } + pub fn indent_size(mut self, indent_size: usize) -> Self { + self.indent_size = indent_size; + self + } + + pub fn with_hashes(self, hashes: H2) -> PrintConfig

{ + PrintConfig { + indent_size: self.indent_size, + wrap: self.wrap, + hashes, + } + } +} diff --git a/crates/ltk_ritobin/src/print/printers.rs b/crates/ltk_ritobin/src/print/printers.rs new file mode 100644 index 00000000..a7477409 --- /dev/null +++ b/crates/ltk_ritobin/src/print/printers.rs @@ -0,0 +1,85 @@ +use std::fmt::{self, Write}; + +use ltk_meta::Bin; + +use crate::{ + cst::{self, Cst}, + hashes::HashProvider, + print::{PrintConfig, PrintError}, +}; + +pub struct CstPrinter<'a, W: Write> { + visitor: super::visitor::CstVisitor<'a, W>, +} + +impl<'a, W: Write> CstPrinter<'a, W> { + pub fn new(src: &'a str, out: W, config: PrintConfig<()>) -> Self { + Self { + visitor: super::visitor::CstVisitor::new(src, out, config), + } + } + + pub fn print(mut self, cst: &Cst) -> Result { + cst.walk(&mut self.visitor); + self.visitor.flush()?; + if let Some(e) = self.visitor.error { + return Err(e); + } + eprintln!("max q size: {}", self.visitor.queue_size_max()); + Ok(self.visitor.printed_bytes()) + } +} + +/// Text writer for ritobin format with hash provider support. +pub struct BinPrinter { + buffer: String, + config: PrintConfig, +} + +impl BinPrinter<()> { + /// Create a new text writer without hash lookup (all hashes written as hex). + pub fn new() -> Self { + Self::default() + } +} + +impl BinPrinter { + /// Create a new printer with custom configuration and hash provider. + pub fn with_config(self, config: PrintConfig

) -> BinPrinter

{ + BinPrinter { + buffer: self.buffer, + config, + } + } + + pub fn print( + &mut self, + tree: &Bin, + writer: &mut W, + ) -> Result + where + H: Clone, + { + let mut builder = cst::builder::Builder::new(self.config.hashes.clone()); + let cst = builder.build(tree); + let buf = builder.into_text_buffer(); + CstPrinter::new(&buf, writer, Default::default()).print(&cst) + } + + pub fn print_to_string(&mut self, tree: &Bin) -> Result + where + H: Clone, + { + let mut str = String::new(); + self.print(tree, &mut str)?; + Ok(str) + } +} +impl Default for BinPrinter<()> { + fn default() -> Self { + Self { + buffer: String::new(), + config: PrintConfig::default(), + } + } +} diff --git a/crates/ltk_ritobin/src/print/visitor.rs b/crates/ltk_ritobin/src/print/visitor.rs new file mode 100644 index 00000000..4a6928b8 --- /dev/null +++ b/crates/ltk_ritobin/src/print/visitor.rs @@ -0,0 +1,629 @@ +use std::{ + collections::VecDeque, + fmt::{self, Write}, +}; + +use crate::{ + cst::{visitor::Visit, Cst, Kind, Visitor}, + parse::TokenKind, + print::{ + command::{Cmd, Mode}, + PrintConfig, PrintError, + }, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub struct GroupId(usize); + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +struct ListContext { + len: u32, + idx: u32, + grp: GroupId, +} + +const MAX_QUEUE: usize = 4096; + +pub struct CstVisitor<'a, W: Write> { + config: PrintConfig<()>, + + src: &'a str, + out: W, + col: usize, + indent: usize, + + printed_bytes: usize, + printed_commands: usize, + queue: VecDeque>, + queue_size_max: usize, + modes: Vec, + + /// group start indices + group_stack: Vec, + list_stack: Vec, + /// running inline size for each group + size_stack: Vec, + + pub error: Option, + + block_space: bool, + block_line: bool, +} + +impl<'a, W: Write> CstVisitor<'a, W> { + #[inline(always)] + #[must_use] + pub fn queue_size_max(&self) -> usize { + self.queue_size_max + } + + #[inline(always)] + #[must_use] + pub fn printed_bytes(&self) -> usize { + self.printed_bytes + } + + #[inline(always)] + #[must_use] + pub fn printed_commands(&self) -> usize { + self.printed_commands + } +} + +impl<'a, W: Write> CstVisitor<'a, W> { + pub fn new(src: &'a str, out: W, config: PrintConfig<()>) -> Self { + Self { + src, + out, + config, + col: 0, + indent: 0, + printed_commands: 0, + printed_bytes: 0, + queue: VecDeque::new(), + queue_size_max: 0, + modes: vec![Mode::Break], + + group_stack: Vec::new(), + list_stack: Vec::new(), + size_stack: Vec::new(), + + error: None, + + block_space: false, + block_line: false, + } + } + + fn push(&mut self, cmd: Cmd<'a>) { + // eprintln!("+ {cmd:?}"); + self.queue.push_back(cmd); + self.queue_size_max = self.queue_size_max.max(self.queue.len()); + if self.queue.len() > MAX_QUEUE { + eprintln!("[!!] hit hard queue limit - force breaking to save memory"); + self.break_first_group(); + } + } + + pub fn text(&mut self, txt: &'a str) { + for size in &mut self.size_stack { + *size += txt.len(); + } + self.check_running_size(); + self.push(Cmd::Text(txt)); + } + + pub fn text_if(&mut self, txt: &'a str, mode: Mode) { + self.push(Cmd::TextIf(txt, mode)); + } + + pub fn space(&mut self) { + if self.queue.back().is_some_and(|c| c.is_whitespace()) { + // eprintln!("# skipping Space! ({:?})", self.queue.back()); + return; + } + self.push(Cmd::Space); + } + + pub fn line(&mut self) { + if self.queue.is_empty() && self.printed_commands == 0 { + return; + } + if self.queue.back().is_some_and(|c| c.is_whitespace()) { + // eprintln!("# replacing ({:?}) w/ Line", self.queue.back()); + self.queue.pop_back(); + } + self.push(Cmd::Line); + } + pub fn softline(&mut self) { + if self.queue.is_empty() && self.printed_commands == 0 { + return; + } + if self.queue.back().is_some_and(|c| c.is_whitespace()) { + // eprintln!("# replacing ({:?}) w/ SoftLine", self.queue.back()); + self.queue.pop_back(); + } + self.push(Cmd::SoftLine); + } + + pub fn begin_group(&mut self, mode: Option) -> GroupId { + let idx = self.queue.len() + self.printed_commands; + + self.push(Cmd::Begin(mode)); + + self.group_stack.push(idx); + self.size_stack.push(0); + + GroupId(idx) + } + + pub fn end_group(&mut self) { + self.push(Cmd::End); + + self.group_stack.pop(); + self.size_stack.pop(); + + self.print_safe().unwrap(); + } + + pub fn indent(&mut self, n: usize) { + self.push(Cmd::Indent(n)); + } + + pub fn dedent(&mut self, n: usize) { + self.push(Cmd::Dedent(n)); + } + + /// break the first/oldest group if running size is too big (bottom of the stack) + pub fn break_first_group(&mut self) { + if let Some(&idx) = self.group_stack.first() { + // eprintln!("[printer] breaking first group"); + self.force_group(GroupId(idx), Mode::Break); + + self.group_stack.remove(0); + self.size_stack.remove(0); + } + self.print_safe().unwrap(); + } + + /// break the first/oldest group if running size is too big (bottom of the stack) + pub fn check_running_size(&mut self) { + if let Some(size) = self.size_stack.last() { + if self.col + size > self.config.wrap.line_width { + self.break_first_group(); + } + } + } + + fn fits(&self) -> bool { + let mut col = self.col; + let mut depth = 0; + + for (i, cmd) in self.queue.iter().enumerate() { + if i > 512 { + panic!("fits too long"); + } + match cmd { + Cmd::Text(s) | Cmd::TextIf(s, Mode::Flat) => { + col += s.len(); + if col > self.config.wrap.line_width { + return false; + } + } + Cmd::Line => return true, + + Cmd::Begin(_) => depth += 1, + Cmd::End => { + if depth == 0 { + break; + } + depth -= 1; + } + + _ => {} + } + } + + true + } + + pub fn force_group(&mut self, group: GroupId, mode: Mode) { + if group.0 < self.printed_commands { + // eprintln!("[!!] trying to mutate already printed group! {group:?}"); + return; + // panic!("trying to mutate already printed group!"); + } + let cmd = self.queue.get_mut(group.0 - self.printed_commands).unwrap(); + let Cmd::Begin(grp_mode) = cmd else { + unreachable!("grp pointing at non begin cmd {cmd:?}"); + }; + grp_mode.replace(mode); + } + + fn print(&mut self, cmd: Cmd) -> fmt::Result { + match cmd { + Cmd::Text(s) => { + self.out.write_str(s)?; + self.printed_bytes += s.len(); + self.col += s.len(); + self.block_space = false; + self.block_line = false; + } + Cmd::TextIf(s, mode) => { + if *self.modes.last().unwrap() == mode { + self.out.write_str(s)?; + self.printed_bytes += s.len(); + self.col += s.len(); + self.block_space = false; + self.block_line = false; + } + } + Cmd::Space => { + if !self.block_space { + self.out.write_char(' ')?; + self.printed_bytes += 1; + self.col += 1; + self.block_space = true; + self.block_line = false; + } + } + + Cmd::Line => { + if !self.block_line { + self.out.write_char('\n')?; + for _ in 0..self.indent { + self.out.write_char(' ')?; + } + self.printed_bytes += self.indent + 1; + self.col = self.indent; + self.propagate_break(); + self.block_space = true; + self.block_line = true; + } + } + + Cmd::SoftLine => match self.modes.last().unwrap() { + Mode::Flat => { + if !self.block_space { + // eprintln!(" - not skipping -> space!"); + self.out.write_char(' ')?; + self.printed_bytes += 1; + self.col += 1; + self.block_space = true; + self.block_line = false; + } + } + Mode::Break => { + if !self.block_line { + // eprintln!(" - not skipping -> line!"); + self.out.write_char('\n')?; + for _ in 0..self.indent { + self.out.write_char(' ')?; + } + self.printed_bytes += self.indent + 1; + self.col = self.indent; + self.propagate_break(); + self.block_space = true; + self.block_line = true; + } + } + }, + + Cmd::Begin(mode) => { + self.modes.push(match mode { + Some(mode) => mode, + None => match self.fits() { + true => Mode::Flat, + false => Mode::Break, + }, + }); + } + Cmd::End => { + self.modes.pop(); + } + + Cmd::Indent(n) => { + self.indent += n; + } + Cmd::Dedent(n) => { + self.indent = self.indent.saturating_sub(n); + } + } + Ok(()) + } + + pub fn flush(&mut self) -> fmt::Result { + // eprintln!("###### FLUSH ##"); + while let Some(cmd) = self.queue.pop_front() { + self.printed_commands += 1; + self.print(cmd)?; + // eprintln!("- {cmd:?}"); + } + + Ok(()) + } + + pub fn print_safe(&mut self) -> fmt::Result { + let limit = self + .group_stack + .first() + .copied() + .unwrap_or(self.queue.len() + self.printed_commands); + + // if self.printed_commands < limit { + // println!("## SAFE FLUSH"); + // } + + while self.printed_commands < limit { + let cmd = self.queue.pop_front().unwrap(); + self.printed_commands += 1; + // eprintln!("- {cmd:?}"); + self.print(cmd)?; + } + Ok(()) + } + + fn propagate_break(&mut self) { + for group in self.modes.iter_mut().rev() { + if *group == Mode::Flat { + *group = Mode::Break; + } else { + break; // stop once we hit an already broken group + } + } + } + + fn enter_tree_inner(&mut self, tree: &Cst) -> Result<(), PrintError> { + match tree.kind { + Kind::TypeArgList => { + let grp = self.begin_group(Some(Mode::Flat)); + // eprintln!("{:#?}", tree.children); + self.list_stack.push(ListContext { + len: tree + .children + .iter() + .filter(|n| n.tree().is_some_and(|t| t.kind == Kind::TypeArg)) + .count() + .try_into() + .unwrap(), + idx: 0, + grp, + }); + } + Kind::ListItemBlock => { + self.softline(); + let grp = self.begin_group(None); + + let len = tree + .children + .iter() + .filter(|n| n.tree().is_some_and(|t| t.kind == Kind::ListItem)) + .count(); + + if !self.config.wrap.inline_lists { + self.force_group(grp, Mode::Break); + } + + if len > 0 { + self.list_stack.push(ListContext { + len: len.try_into().unwrap(), + idx: 0, + grp, + }); + } + } + Kind::Block => { + // eprintln!("BLOCK: {:#?}", tree.children); + let grp = self.begin_group(None); + let list_len = tree + .children + .iter() + .filter(|n| { + n.tree() + .is_some_and(|t| matches!(t.kind, Kind::ListItem | Kind::ListItemBlock)) + }) + .count(); + let struct_len = tree + .children + .iter() + .filter(|n| n.tree().is_some_and(|t| matches!(t.kind, Kind::Entry))) + .count(); + + if self.config.wrap.inline_structs { + if let Some(last) = self.list_stack.last() { + if struct_len > 1 { + self.force_group(grp, Mode::Break); + } else { + self.force_group(last.grp, Mode::Break); + } + } + } + if !self.config.wrap.inline_lists && list_len > 0 { + self.force_group(grp, Mode::Break); + } + let len = struct_len + list_len; + + if len > 0 { + self.list_stack.push(ListContext { + len: len.try_into().unwrap(), + idx: 0, + grp, + }); + } + } + Kind::Class => {} + Kind::ListItem => { + if tree + .children + .first() + .is_some_and(|c| c.tree().is_some_and(|t| t.kind == Kind::Class)) + { + if let Some(list) = self.list_stack.last() { + self.force_group(list.grp, Mode::Break); + } + } + self.softline(); + } + Kind::Entry => { + match self.config.wrap.inline_structs { + true => self.softline(), + false => self.line(), + }; + // self.flush().unwrap(); + } + _ => {} + } + Ok(()) + } + + fn exit_tree_inner(&mut self, tree: &Cst) -> Result<(), PrintError> { + match tree.kind { + Kind::TypeArgList => { + self.list_stack.pop(); + self.end_group(); + } + Kind::ListItemBlock | Kind::Block => { + self.list_stack.pop(); + // eprintln!("exit {} | stack: {}", tree.kind, self.list_stack.len()); + if let Some(list) = self.list_stack.last() { + if list.len > 1 { + self.force_group(list.grp, Mode::Break); + // self.softline(); + } + } + self.end_group(); + } + Kind::Entry if self.config.wrap.inline_structs => { + if let Some(list) = self.list_stack.last() { + if list.len > 1 { + self.force_group(list.grp, Mode::Break); + self.text_if(";", Mode::Flat); + // self.softline(); + } + } else { + // self.text_if(";", Mode::Flat); + // self.softline(); + } + } + Kind::ListItem | Kind::TypeArg => { + if let Some(ctx) = self.list_stack.last() { + let last_item = ctx.idx + 1 == ctx.len; + + if !last_item { + self.text_if(",", Mode::Flat); + self.space(); + if tree.kind == Kind::ListItem { + self.softline(); + } + } + + self.list_stack.last_mut().unwrap(/* guaranteed by if let */).idx += 1; + } + } + _ => {} + } + Ok(()) + } + + fn visit_token_inner( + &mut self, + token: &crate::parse::Token, + _context: &Cst, + ) -> Result<(), PrintError> { + let txt = self.src[token.span].trim(); + let print_value = token.kind.print_value(); + + if txt.is_empty() && print_value.is_none() { + return Ok(()); + } + + // eprintln!("->{:?}", token.kind); + match token.kind { + TokenKind::LCurly => { + self.space(); + self.text("{"); + self.indent(4); + self.space(); + // self.softline(); + } + + TokenKind::RCurly => { + self.dedent(4); + self.softline(); + self.text("}"); + } + + TokenKind::Comma => { + // self.text_if(",", Mode::Flat); + // self.softline(); + } + TokenKind::Colon => { + self.text(":"); + self.space(); + } + + TokenKind::Eq => { + self.space(); + self.text("="); + self.space(); + } + + TokenKind::LBrack => { + self.text("["); + } + TokenKind::RBrack => { + self.text("]"); + } + TokenKind::Quote => { + self.text("\""); + } + TokenKind::False => { + self.text("false"); + } + TokenKind::True => { + self.text("true"); + } + + _ => { + if let Some(print) = print_value { + self.text(print); + } else { + self.text(txt); + } + } + } + self.print_safe()?; + // self.flush()?; + Ok(()) + } +} + +impl<'a, W: fmt::Write> Visitor for CstVisitor<'a, W> { + fn enter_tree(&mut self, tree: &Cst) -> Visit { + match self.enter_tree_inner(tree) { + Ok(_) => Visit::Continue, + Err(e) => { + self.error.replace(e); + Visit::Stop + } + } + } + fn exit_tree(&mut self, tree: &Cst) -> Visit { + match self.exit_tree_inner(tree) { + Ok(_) => Visit::Continue, + Err(e) => { + self.error.replace(e); + Visit::Stop + } + } + } + fn visit_token(&mut self, token: &crate::parse::Token, context: &crate::cst::Cst) -> Visit { + match self.visit_token_inner(token, context) { + Ok(_) => Visit::Continue, + Err(e) => { + self.error.replace(e); + Visit::Stop + } + } + } +} diff --git a/crates/ltk_ritobin/src/typecheck.rs b/crates/ltk_ritobin/src/typecheck.rs new file mode 100644 index 00000000..1a49dced --- /dev/null +++ b/crates/ltk_ritobin/src/typecheck.rs @@ -0,0 +1 @@ +pub mod visitor; diff --git a/crates/ltk_ritobin/src/typecheck/visitor.rs b/crates/ltk_ritobin/src/typecheck/visitor.rs new file mode 100644 index 00000000..1fa2df22 --- /dev/null +++ b/crates/ltk_ritobin/src/typecheck/visitor.rs @@ -0,0 +1,1292 @@ +use std::fmt::{Debug, Display}; + +use glam::Vec4; +use indexmap::IndexMap; +use ltk_hash::fnv1a; +use ltk_meta::{ + property::values, traits::PropertyExt, Bin, BinObject, PropertyKind, PropertyValueEnum, +}; +use xxhash_rust::xxh64::xxh64; + +use crate::{ + cst::{self, visitor::Visit, Child, Cst, Kind, Visitor}, + parse::{Span, Token, TokenKind}, + RitobinName, +}; + +#[derive(Debug, Clone)] +pub enum ClassKind { + Str(String), + Hash(u32), +} + +#[derive(Debug, Clone)] +pub struct IrEntry { + pub key: PropertyValueEnum, + pub value: PropertyValueEnum, +} + +#[derive(Debug, Clone)] +pub struct IrListItem(pub PropertyValueEnum); + +#[derive(Debug, Clone)] +pub enum IrItem { + Entry(IrEntry), + ListItem(IrListItem), +} + +impl IrItem { + pub fn is_entry(&self) -> bool { + matches!(self, Self::Entry { .. }) + } + + pub fn as_entry(&self) -> Option<&IrEntry> { + match self { + IrItem::Entry(i) => Some(i), + _ => None, + } + } + pub fn is_list_item(&self) -> bool { + matches!(self, Self::ListItem { .. }) + } + pub fn as_list_item(&self) -> Option<&IrListItem> { + match self { + IrItem::ListItem(i) => Some(i), + _ => None, + } + } + pub fn value(&self) -> &PropertyValueEnum { + match self { + IrItem::Entry(i) => &i.value, + IrItem::ListItem(i) => &i.0, + } + } + pub fn value_mut(&mut self) -> &mut PropertyValueEnum { + match self { + IrItem::Entry(i) => &mut i.value, + IrItem::ListItem(i) => &mut i.0, + } + } + pub fn into_value(self) -> PropertyValueEnum { + match self { + IrItem::Entry(i) => i.value, + IrItem::ListItem(i) => i.0, + } + } +} + +pub struct TypeChecker<'a> { + ctx: Ctx<'a>, + pub root: IndexMap>, + // current: Option<(PropertyValueEnum, PropertyValueEnum)>, + stack: Vec<(u32, IrItem)>, + list_queue: Vec, + depth: u32, +} + +impl<'a> TypeChecker<'a> { + pub fn new(text: &'a str) -> Self { + Self { + ctx: Ctx { + text, + diagnostics: Vec::new(), + }, + root: IndexMap::new(), + stack: Vec::new(), + list_queue: Vec::new(), + depth: 0, + } + } + pub fn into_parts( + self, + ) -> ( + IndexMap>, + Vec, + ) { + (self.root, self.ctx.diagnostics) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum RitoTypeOrVirtual { + RitoType(RitoType), + Numeric, + StructOrEmbedded, +} + +impl RitoTypeOrVirtual { + pub fn numeric() -> Self { + Self::Numeric + } +} + +impl From for RitoTypeOrVirtual { + fn from(value: RitoType) -> Self { + RitoTypeOrVirtual::RitoType(value) + } +} + +impl Display for RitoTypeOrVirtual { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::RitoType(rito_type) => Display::fmt(rito_type, f), + Self::Numeric => f.write_str("numeric type"), + Self::StructOrEmbedded => f.write_str("struct/embedded"), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ColorOrVec { + Color, + Vec2, + Vec3, + Vec4, + Mat44, +} + +#[derive(Debug, Clone, Copy)] +pub enum Diagnostic { + MissingTree(cst::Kind), + EmptyTree(cst::Kind), + MissingToken(TokenKind), + UnknownType(Span), + MissingType(Span), + + MissingEntriesMap, + InvalidEntriesMap { + span: Span, + got: RitoType, + }, + + TypeMismatch { + span: Span, + expected: RitoType, + expected_span: Option, + got: RitoTypeOrVirtual, + }, + + UnexpectedContainerItem { + span: Span, + expected: RitoType, + expected_span: Option, + }, + + ResolveLiteral, + AmbiguousNumeric(Span), + + NotEnoughItems { + span: Span, + got: u8, + expected: ColorOrVec, + }, + TooManyItems { + span: Span, + extra: u8, + expected: ColorOrVec, + }, + + RootNonEntry, + ShadowedEntry { + shadowee: Span, + shadower: Span, + }, + + InvalidHash(Span), + + SubtypeCountMismatch { + span: Span, + got: u8, + expected: u8, + }, + /// Subtypes found on a type that has no subtypes + UnexpectedSubtypes { + span: Span, + base_type: Span, + }, +} + +impl Diagnostic { + pub fn span(&self) -> Option<&Span> { + match self { + MissingTree(_) | EmptyTree(_) | MissingToken(_) | RootNonEntry | ResolveLiteral + | MissingEntriesMap => None, + UnknownType(span) + | SubtypeCountMismatch { span, .. } + | UnexpectedSubtypes { span, .. } + | UnexpectedContainerItem { span, .. } + | MissingType(span) + | TypeMismatch { span, .. } + | ShadowedEntry { shadower: span, .. } + | InvalidHash(span) + | AmbiguousNumeric(span) + | NotEnoughItems { span, .. } + | TooManyItems { span, .. } + | InvalidEntriesMap { span, .. } => Some(span), + } + } + + pub fn default_span(self, span: Span) -> DiagnosticWithSpan { + DiagnosticWithSpan { + span: self.span().copied().unwrap_or(span), + diagnostic: self, + } + } + + pub fn unwrap(self) -> DiagnosticWithSpan { + DiagnosticWithSpan { + span: self.span().copied().unwrap(), + diagnostic: self, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct DiagnosticWithSpan { + pub diagnostic: Diagnostic, + pub span: Span, +} + +#[derive(Debug, Clone, Copy)] +pub struct MaybeSpanDiag { + pub diagnostic: Diagnostic, + pub span: Option, +} + +impl MaybeSpanDiag { + pub fn fallback(self, span: Span) -> DiagnosticWithSpan { + DiagnosticWithSpan { + span: self.span.unwrap_or(span), + diagnostic: self.diagnostic, + } + } +} + +impl From for MaybeSpanDiag { + fn from(diagnostic: Diagnostic) -> Self { + Self { + span: diagnostic.span().copied(), + diagnostic, + } + } +} + +use Diagnostic::*; + +pub trait PropertyValueExt { + fn rito_type(&self) -> RitoType; +} +impl PropertyValueExt for PropertyValueEnum { + fn rito_type(&self) -> RitoType { + let base = self.kind(); + let subtypes = match self { + PropertyValueEnum::Map(map) => [Some(map.key_kind()), Some(map.value_kind())], + PropertyValueEnum::UnorderedContainer(values::UnorderedContainer(container)) + | PropertyValueEnum::Container(container) => [Some(container.item_kind()), None], + PropertyValueEnum::Optional(optional) => [Some(optional.item_kind()), None], + + _ => [None, None], + }; + RitoType { base, subtypes } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RitoType { + pub base: PropertyKind, + pub subtypes: [Option; 2], +} + +impl Display for RitoType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let base = self.base.to_rito_name(); + match self.subtypes { + [None, None] => f.write_str(base), + [Some(a), None] => write!(f, "{base}[{}]", a.to_rito_name()), + [Some(a), Some(b)] => { + write!(f, "{base}[{},{}]", a.to_rito_name(), b.to_rito_name()) + } + _ => write!(f, "{base}[!!]"), + } + } +} + +impl RitoType { + pub fn simple(kind: PropertyKind) -> Self { + Self { + base: kind, + subtypes: [None, None], + } + } + + fn subtype(&self, idx: usize) -> PropertyKind { + self.subtypes[idx].unwrap_or_default() + } + + fn value_subtype(&self) -> Option { + self.subtypes[1].or(self.subtypes[0]) + } + + pub fn make_default(&self, span: Span) -> PropertyValueEnum { + let mut value = match self.base { + PropertyKind::Map => { + PropertyValueEnum::Map(values::Map::empty(self.subtype(0), self.subtype(1))) + } + PropertyKind::UnorderedContainer => { + PropertyValueEnum::UnorderedContainer(values::UnorderedContainer( + values::Container::empty(self.subtype(0)).unwrap_or_default(), + )) + } + PropertyKind::Container => PropertyValueEnum::Container( + values::Container::empty(self.subtype(0)).unwrap_or_default(), + ), + PropertyKind::Optional => PropertyValueEnum::Optional( + values::Optional::empty(self.subtype(0)).unwrap_or_default(), + ), + + _ => self.base.default_value(), + }; + *value.meta_mut() = span; + value + } +} +pub enum Statement { + KeyValue { + key: Span, + value: Span, + kind: Option, + }, +} + +trait TreeIterExt<'a>: Iterator { + fn expect_tree(&mut self, kind: cst::Kind) -> Result<&'a Cst, Diagnostic>; + fn expect_token(&mut self, kind: TokenKind) -> Result<&'a Token, Diagnostic>; +} + +impl<'a, I> TreeIterExt<'a> for I +where + I: Iterator, +{ + fn expect_tree(&mut self, kind: cst::Kind) -> Result<&'a Cst, Diagnostic> { + self.find_map(|c| c.tree().filter(|t| t.kind == kind)) + .ok_or(MissingTree(kind)) + } + fn expect_token(&mut self, kind: TokenKind) -> Result<&'a Token, Diagnostic> { + self.find_map(|c| c.token().filter(|t| t.kind == kind)) + .ok_or(MissingToken(kind)) + } +} + +pub struct Ctx<'a> { + text: &'a str, + diagnostics: Vec, +} + +pub fn coerce_type( + value: PropertyValueEnum, + to: PropertyKind, +) -> Option> { + match to { + PropertyKind::Hash => Some(match value { + PropertyValueEnum::Hash(_) => return Some(value), + PropertyValueEnum::String(str) => { + values::Hash::new_with_meta(fnv1a::hash_lower(&str), str.meta).into() + } + other => { + eprintln!("\x1b[41mcannot coerce {other:?} to {to:?}\x1b[0m"); + return None; + } + }), + PropertyKind::ObjectLink => Some(match value { + PropertyValueEnum::Hash(hash) => { + values::ObjectLink::new_with_meta(*hash, hash.meta).into() + } + PropertyValueEnum::ObjectLink(_) => return Some(value), + PropertyValueEnum::String(str) => { + values::ObjectLink::new_with_meta(fnv1a::hash_lower(&str), str.meta).into() + } + other => { + eprintln!("\x1b[41mcannot coerce {other:?} to {to:?}\x1b[0m"); + return None; + } + }), + PropertyKind::WadChunkLink => Some(match value { + PropertyValueEnum::WadChunkLink(_) => return Some(value), + PropertyValueEnum::Hash(hash) => { + values::WadChunkLink::new_with_meta((*hash).into(), hash.meta).into() + } + PropertyValueEnum::String(str) => { + values::WadChunkLink::new_with_meta(xxh64(str.as_bytes(), 0), str.meta).into() + } + other => { + eprintln!("\x1b[41mcannot coerce {other:?} to {to:?}\x1b[0m"); + return None; + } + }), + PropertyKind::BitBool => Some(match value { + PropertyValueEnum::BitBool(_) => return Some(value), + PropertyValueEnum::Bool(bool) => { + values::BitBool::new_with_meta(*bool, bool.meta).into() + } + other => { + eprintln!("\x1b[41mcannot coerce {other:?} to {to:?}\x1b[0m"); + return None; + } + }), + PropertyKind::Bool => Some(match value { + PropertyValueEnum::Bool(_) => return Some(value), + PropertyValueEnum::BitBool(bool) => { + values::Bool::new_with_meta(*bool, bool.meta).into() + } + other => { + eprintln!("\x1b[41mcannot coerce {other:?} to {to:?}\x1b[0m"); + return None; + } + }), + to if to == value.kind() => Some(value), + _ => None, + } +} + +pub fn resolve_rito_type(ctx: &mut Ctx<'_>, tree: &Cst) -> Result { + let mut c = tree.children.iter(); + + let base = c.expect_token(TokenKind::Name)?; + let base_span = base.span; + + let base = PropertyKind::from_rito_name(&ctx.text[base.span]).ok_or(UnknownType(base.span))?; + + let subtypes = match c + .clone() + .find_map(|c| c.tree().filter(|t| t.kind == Kind::TypeArgList)) + { + Some(subtypes) => { + let subtypes_span = subtypes.span; + + let expected = base.subtype_count(); + + if expected == 0 { + return Err(UnexpectedSubtypes { + span: subtypes_span, + base_type: base_span, + }); + } + + let subtypes = subtypes + .children + .iter() + .filter_map(|c| c.tree().filter(|t| t.kind == Kind::TypeArg)) + .map(|t| { + let resolved = PropertyKind::from_rito_name(&ctx.text[t.span]); + if resolved.is_none() { + ctx.diagnostics.push(UnknownType(t.span).unwrap()); + } + (resolved, t.span) + }) + .collect::>(); + + if subtypes.len() > expected.into() { + return Err(SubtypeCountMismatch { + span: subtypes[expected as _..] + .iter() + .map(|s| s.1) + .reduce(|acc, s| Span::new(acc.start, s.end)) + .unwrap_or(subtypes_span), + got: subtypes.len() as u8, + expected, + }); + } + if subtypes.len() < expected.into() { + return Err(SubtypeCountMismatch { + span: subtypes.last().map(|s| s.1).unwrap_or(subtypes_span), + got: subtypes.len() as u8, + expected, + }); + } + + let mut subtypes = subtypes.iter(); + [ + subtypes.next().and_then(|s| s.0), + subtypes.next().and_then(|s| s.0), + ] + } + None => [None, None], + }; + + Ok(RitoType { base, subtypes }) +} + +fn resolve_hash(ctx: &Ctx, span: Span) -> Result, Diagnostic> { + // TODO: better errs here? + let src = ctx.text[span].strip_prefix("0x").ok_or(InvalidHash(span))?; + + Ok(match u32::from_str_radix(src, 16) { + Ok(hash) => PropertyValueEnum::Hash(values::Hash::new_with_meta(hash, span)), + Err(_) => match u64::from_str_radix(src, 16) { + Ok(hash) => { + PropertyValueEnum::WadChunkLink(values::WadChunkLink::new_with_meta(hash, span)) + } + Err(_) => return Err(InvalidHash(span)), + }, + }) +} + +pub fn resolve_value( + ctx: &mut Ctx, + tree: &Cst, + kind_hint: Option, +) -> Result>, Diagnostic> { + use PropertyKind as K; + use PropertyValueEnum as P; + + // dbg!(tree, kind_hint); + + let Some(child) = tree.children.first() else { + return Ok(None); + }; + Ok(Some(match child { + cst::Child::Tree(Cst { + kind: Kind::Class, + children, + span, + .. + }) => { + let Some(kind_hint) = kind_hint else { + return Ok(None); // TODO: err + }; + let Some(class) = children.first().and_then(|t| t.token()) else { + return Err(InvalidHash(*span)); + }; + + let class_hash = match class { + Token { + kind: TokenKind::Name, + span, + } => fnv1a::hash_lower(&ctx.text[span]), + Token { + kind: TokenKind::HexLit, + span, + } => match resolve_hash(ctx, *span)? { + PropertyValueEnum::Hash(hash) => *hash, + value => { + return Err(TypeMismatch { + span: *value.meta(), + expected: RitoType::simple(PropertyKind::Hash), + expected_span: None, + got: value.rito_type().into(), + }); + } + }, + _ => { + return Err(InvalidHash(class.span)); + } + }; + match kind_hint { + K::Struct => P::Struct(values::Struct { + class_hash, + meta: class.span, + properties: Default::default(), + }), + K::Embedded => P::Embedded(values::Embedded(values::Struct { + class_hash, + meta: class.span, + properties: Default::default(), + })), + other => { + eprintln!("can't create class value from kind {other:?}"); + return Err(TypeMismatch { + span: class.span, + expected: RitoType::simple(other), + expected_span: None, + got: RitoTypeOrVirtual::StructOrEmbedded, + }); + } + } + } + cst::Child::Tree(Cst { + kind: Kind::Literal, + children, + .. + }) => { + let Some(child) = children.first() else { + return Ok(None); + }; + match child { + cst::Child::Token(Token { + kind: TokenKind::String, + span, + }) => values::String::new_with_meta( + ctx.text[Span::new(span.start + 1, span.end - 1)].into(), + *span, + ) + .into(), + + cst::Child::Token(Token { + kind: TokenKind::True, + span, + }) => values::Bool::new_with_meta(true, *span).into(), + cst::Child::Token(Token { + kind: TokenKind::False, + span, + }) => values::Bool::new_with_meta(false, *span).into(), + + cst::Child::Token(Token { + kind: TokenKind::HexLit, + span, + }) => resolve_hash(ctx, *span)?, + cst::Child::Token(Token { + kind: TokenKind::Number, + span, + }) => { + let txt = &ctx.text[span]; + let Some(kind_hint) = kind_hint else { + return Err(AmbiguousNumeric(*span)); + }; + + let txt = txt.replace('_', ""); + + match kind_hint { + K::U8 => P::U8(values::U8::new_with_meta( + txt.parse::().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::U16 => P::U16(values::U16::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::U32 => P::U32(values::U32::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::U64 => P::U64(values::U64::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::I8 => P::I8(values::I8::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::I16 => P::I16(values::I16::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::I32 => P::I32(values::I32::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::I64 => P::I64(values::I64::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + K::F32 => P::F32(values::F32::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ResolveLiteral)?, + *span, + )), + _ => { + return Err(TypeMismatch { + span: *span, + expected: RitoType::simple(kind_hint), + expected_span: None, // TODO: would be nice here + got: RitoTypeOrVirtual::numeric(), + }); + } + } + } + _ => return Ok(None), + } + } + _ => return Ok(None), + })) +} + +pub fn resolve_entry( + ctx: &mut Ctx, + tree: &Cst, + parent_value_kind: Option, +) -> Result { + let mut c = tree.children.iter(); + + let key = c.expect_tree(Kind::EntryKey)?; + + let key = match key.children.first().ok_or(InvalidHash(key.span))? { + Child::Token(Token { + kind: TokenKind::Name, + span, + }) => PropertyValueEnum::from(values::String::new_with_meta(ctx.text[span].into(), *span)), + Child::Token(Token { + kind: TokenKind::String, + span, + }) => PropertyValueEnum::from(values::String::new_with_meta( + ctx.text[Span::new(span.start + 1, span.end - 1)].into(), + *span, + )), + Child::Token(Token { + kind: TokenKind::HexLit, + span, + }) => resolve_hash(ctx, *span)?, + _ => { + return Err(InvalidHash(key.span).into()); + } + }; + + let parent_value_kind = parent_value_kind + .and_then(|p| p.value_subtype()) + .map(RitoType::simple); + + let kind = c + .clone() + .find_map(|c| c.tree().filter(|t| t.kind == Kind::TypeExpr)); + let kind_span = kind.map(|k| k.span); + let kind = kind.map(|t| resolve_rito_type(ctx, t)).transpose()?; + + let value = c.expect_tree(Kind::EntryValue)?; + let value_span = value.span; + + // entries: map[string, u8] = { + // "bad": string = "string" + // ^ + // } + if let Some(parent) = parent_value_kind.as_ref() { + if let Some((kind, kind_span)) = kind.as_ref().zip(kind_span) { + if parent != kind { + ctx.diagnostics.push( + TypeMismatch { + span: kind_span, + expected: *parent, + expected_span: None, // TODO: would be nice here + got: (*kind).into(), + } + .unwrap(), + ); + return Ok(IrEntry { + key, + value: parent.make_default(value.span), + }); + } + } + } + + let kind = kind.or(parent_value_kind); + + let resolved_val = resolve_value(ctx, value, kind.map(|k| k.base))?.map(|value| match kind { + Some(kind) => coerce_type(value.clone(), kind.base).unwrap_or(value), + None => value, + }); + + let value = match (kind, resolved_val) { + (None, Some(value)) => value, + (None, None) => return Err(MissingType(*key.meta()).into()), + (Some(kind), Some(ivalue)) => match ivalue.kind() == kind.base { + true => ivalue, + false => { + return Err(TypeMismatch { + span: *ivalue.meta(), + expected: kind, + expected_span: kind_span, + got: ivalue.rito_type().into(), + } + .into()) + } + }, + (Some(kind), _) => kind.make_default(value_span), + }; + + Ok(IrEntry { key, value }) +} + +impl<'a> TypeChecker<'a> { + pub fn collect_to_bin(mut self) -> (Bin, Vec) { + let objects = self + .root + .swap_remove("entries") + .and_then(|v| { + let PropertyValueEnum::Map(map) = v else { + self.ctx.diagnostics.push( + InvalidEntriesMap { + span: *v.meta(), + got: RitoType::simple(v.kind()), + } + .unwrap(), + ); + return None; + }; + Some(map.into_entries().into_iter().filter_map(|(key, value)| { + let PropertyValueEnum::Hash(path_hash) = coerce_type(key, PropertyKind::Hash)? + else { + return None; + }; + + if let PropertyValueEnum::Embedded(values::Embedded(struct_val)) = value { + let struct_val = struct_val.no_meta(); + // eprintln!("struct_val: {struct_val:?}"); + Some(BinObject { + path_hash: *path_hash, + class_hash: struct_val.class_hash, + properties: struct_val.properties.clone(), + }) + } else { + None + } + })) + }) + .expect("no 'entries' entry"); + + let tree = Bin::new(objects, Vec::::new()); + + (tree, self.ctx.diagnostics) + } + + fn merge_ir(&mut self, mut parent: IrItem, child: IrItem) -> IrItem { + // eprintln!("\x1b[0;33mmerge {child:?}\n-----> {parent:?}\x1b[0m"); + match &mut parent.value_mut() { + PropertyValueEnum::Container(list) + | PropertyValueEnum::UnorderedContainer(values::UnorderedContainer(list)) => { + match child { + IrItem::ListItem(IrListItem(value)) => { + let value = coerce_type(value.clone(), list.item_kind()).unwrap_or(value); + let span = *value.meta(); + match list.push(value) { + Ok(_) => {} + Err(ltk_meta::Error::MismatchedContainerTypes { expected, got }) => { + self.ctx.diagnostics.push( + TypeMismatch { + span, + expected: RitoType::simple(expected), + expected_span: None, // TODO: would be nice here + got: RitoType::simple(got).into(), + } + .unwrap(), + ); + } + Err(_e) => { + todo!("handle unexpected error"); + } + } + } + IrItem::Entry(IrEntry { key: _, value: _ }) => { + eprintln!("\x1b[41mlist item must be list item\x1b[0m"); + return parent; + } + } + } + PropertyValueEnum::Struct(struct_val) + | PropertyValueEnum::Embedded(values::Embedded(struct_val)) => { + let IrItem::Entry(IrEntry { key, value }) = child else { + eprintln!("\x1b[41mstruct item must be entry\x1b[0m"); + return parent; + }; + + let Some(PropertyValueEnum::Hash(key)) = coerce_type(key, PropertyKind::Hash) + else { + // eprintln!("\x1b[41m{other:?} not valid hash\x1b[0m"); + return parent; + }; + + struct_val.properties.insert(*key, value); + } + PropertyValueEnum::ObjectLink(_object_link_value) => todo!(), + PropertyValueEnum::Map(map_value) => { + let IrItem::Entry(IrEntry { key, value }) = child else { + eprintln!("map item must be entry"); + return parent; + }; + let span = *value.meta(); + let Some(key) = coerce_type(key, map_value.key_kind()) else { + // eprintln!("\x1b[41m{other:?} not valid hash\x1b[0m"); + return parent; + }; + match map_value.push(key, value) { + Ok(()) => {} + Err(ltk_meta::Error::MismatchedContainerTypes { expected, got }) => { + self.ctx.diagnostics.push( + TypeMismatch { + span, + expected: RitoType::simple(expected), + expected_span: None, // TODO: would be nice here + got: RitoType::simple(got).into(), + } + .unwrap(), + ); + } + Err(_e) => { + todo!("handle unexpected err"); + } + } + } + PropertyValueEnum::Optional(option) => { + let IrItem::ListItem(IrListItem(child)) = child else { + eprintln!("\x1b[41moptional value must be list item\x1b[0m"); + return parent; + }; + if child.kind() != option.item_kind() { + self.ctx.diagnostics.push( + TypeMismatch { + span: *child.meta(), + expected: RitoType::simple(option.item_kind()), + expected_span: None, // TODO: would be nice here + got: child.rito_type().into(), + } + .unwrap(), + ); + return parent; + } + + *option = values::Optional::new_with_meta( + option.item_kind(), + Some(child), + *option.meta(), + ) + .unwrap(); + } + other => { + self.ctx.diagnostics.push( + UnexpectedContainerItem { + span: *other.meta(), + expected: other.rito_type(), + expected_span: None, + } + .unwrap(), + ); + + eprintln!("cant inject into {:?}", other.kind()) + } + } + parent + } +} + +fn populate_vec_or_color( + target: &mut IrItem, + items: &mut Vec, +) -> Result<(), MaybeSpanDiag> { + let resolve_f32 = |n: PropertyValueEnum| -> Result { + match n { + PropertyValueEnum::F32(values::F32 { value: n, .. }) => Ok(n), + _ => Err(TypeMismatch { + span: *n.meta(), + expected: RitoType::simple(PropertyKind::F32), + expected_span: None, // TODO: would be nice + got: RitoTypeOrVirtual::RitoType(RitoType::simple(n.kind())), + } + .into()), + } + }; + let resolve_u8 = |n: PropertyValueEnum| -> Result { + match n { + PropertyValueEnum::U8(values::U8 { value: n, .. }) => Ok(n), + _ => Err(TypeMismatch { + span: *n.meta(), + expected: RitoType::simple(PropertyKind::U8), + expected_span: None, // TODO: would be nice + got: RitoTypeOrVirtual::RitoType(RitoType::simple(n.kind())), + } + .into()), + } + }; + + let mut items = items.drain(..); + let mut get_next = |span: &mut Span| -> Result<_, Diagnostic> { + let item = items + .next() + .ok_or(NotEnoughItems { + span: *span, + got: 0, + expected: ColorOrVec::Vec2, + })? + .0; + *span = *item.meta(); + Ok(item) + }; + + use PropertyValueEnum as V; + let mut span = Span::new(0, 0); // FIXME: get a span in here stat + let expected; + match target.value_mut() { + V::Vector2(values::Vector2 { value: vec, .. }) => { + vec.x = resolve_f32(get_next(&mut span)?)?; + vec.y = resolve_f32(get_next(&mut span)?)?; + expected = ColorOrVec::Vec2; + } + V::Vector3(values::Vector3 { value: vec, .. }) => { + vec.x = resolve_f32(get_next(&mut span)?)?; + vec.y = resolve_f32(get_next(&mut span)?)?; + vec.z = resolve_f32(get_next(&mut span)?)?; + expected = ColorOrVec::Vec3; + } + V::Vector4(values::Vector4 { value: vec, .. }) => { + vec.x = resolve_f32(get_next(&mut span)?)?; + vec.y = resolve_f32(get_next(&mut span)?)?; + vec.z = resolve_f32(get_next(&mut span)?)?; + vec.w = resolve_f32(get_next(&mut span)?)?; + expected = ColorOrVec::Vec4; + } + V::Color(values::Color { value: color, .. }) => { + color.r = resolve_u8(get_next(&mut span)?)?; + color.g = resolve_u8(get_next(&mut span)?)?; + color.b = resolve_u8(get_next(&mut span)?)?; + color.a = resolve_u8(get_next(&mut span)?)?; + expected = ColorOrVec::Color; + } + V::Matrix44(values::Matrix44 { value: mat, .. }) => { + mat.x_axis = Vec4::new( + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + ); + mat.y_axis = Vec4::new( + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + ); + mat.z_axis = Vec4::new( + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + ); + mat.w_axis = Vec4::new( + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + resolve_f32(get_next(&mut span)?)?, + ); + *mat = mat.transpose(); + expected = ColorOrVec::Mat44; + } + _ => { + unreachable!("non-empty list queue with non color/vec type receiver?"); + } + } + + if let Some(extra) = items.next() { + let count = 1 + items.count(); + return Err(TooManyItems { + span: *extra.0.meta(), + extra: count as _, + expected, + } + .into()); + } + Ok(()) +} + +impl Visitor for TypeChecker<'_> { + fn enter_tree(&mut self, tree: &Cst) -> Visit { + self.depth += 1; + let depth = self.depth; + + let indent = " ".repeat(depth.saturating_sub(1) as _); + if std::env::var("RB_STACK").is_ok() { + eprintln!("{indent}> d:{} | {:?}", depth, tree.kind); + eprint!("{indent} stack: "); + if self.stack.is_empty() { + eprint!("empty") + } + eprintln!(); + for s in &self.stack { + eprintln!("{indent} - {}: {:?}", s.0, s.1); + } + } + + let parent = self.stack.last(); + + match tree.kind { + Kind::ErrorTree => return Visit::Skip, + + Kind::ListItemBlock => { + let Some((_, parent)) = parent else { + self.ctx + .diagnostics + .push(RootNonEntry.default_span(tree.span)); + return Visit::Skip; + }; + + let parent_type = parent.value().rito_type(); + + use PropertyKind as K; + match parent_type.base { + K::Container | K::UnorderedContainer | K::Optional => { + let value_type = parent_type + .value_subtype() + .expect("container must have value_subtype"); + self.stack.push(( + depth, + IrItem::ListItem(IrListItem({ + let mut v = value_type.default_value(); + *v.meta_mut() = tree.span; + v + })), + )); + } + parent_type => { + eprintln!( + "[warn] got {parent_type:?} in ListItemBlock - {:?}", + &self.ctx.text[tree.span] + ); + } + } + } + Kind::ListItem => { + let Some((_, parent)) = parent else { + self.ctx + .diagnostics + .push(RootNonEntry.default_span(tree.span)); + return Visit::Skip; + }; + + let parent_type = parent.value().rito_type(); + + use PropertyKind as K; + let color_vec_type = match parent_type.base { + K::Vector2 | K::Vector3 | K::Vector4 | K::Matrix44 => Some(K::F32), + K::Color => Some(K::U8), + _ => None, + }; + + // dbg!(color_vec_type, parent_type); + + let value_hint = color_vec_type.or(parent_type.value_subtype()); + + match resolve_value(&mut self.ctx, tree, value_hint) { + Ok(Some(item)) => { + // eprintln!("{indent} list item {item:?}"); + if color_vec_type.is_some() { + self.list_queue.push(IrListItem(item)); + } else { + self.stack.push((depth, IrItem::ListItem(IrListItem(item)))); + } + } + Ok(None) => { + // eprintln!("{indent} ERROR empty item"); + } + Err(e) => self.ctx.diagnostics.push(e.default_span(tree.span)), + } + } + + Kind::Entry => { + match resolve_entry(&mut self.ctx, tree, parent.map(|p| p.1.value().rito_type())) + .map_err(|e| e.fallback(tree.span)) + { + Ok(entry) => { + // eprintln!("{indent} push {entry:?}"); + self.stack.push((depth, IrItem::Entry(entry))); + } + Err(e) => self.ctx.diagnostics.push(e), + } + } + + _ => {} + } + + // match self.current.as_mut() { + // Some((depth, name, value)) => {} + // None => { + // match tree.kind { + // Kind::Entry => {} + // Kind::File => return Visit::Continue, + // kind => { + // if depth == 2 { + // self.ctx + // .diagnostics + // .push(RootNonEntry.default_span(tree.span)); + // } + // return Visit::Skip; + // } + // } + // + // } + // } + + Visit::Continue + } + + fn exit_tree(&mut self, tree: &cst::Cst) -> Visit { + let depth = self.depth; + self.depth -= 1; + let indent = " ".repeat(depth.saturating_sub(1) as _); + if std::env::var("RB_STACK").is_ok() { + eprintln!("{indent}< d:{} | {:?}", depth, tree.kind); + eprint!("{indent} stack: "); + if self.stack.is_empty() { + eprint!("empty") + } + eprintln!(); + for s in &self.stack { + eprintln!("{indent} - {}: {:?}", s.0, s.1); + } + } + if tree.kind == cst::Kind::ErrorTree { + return Visit::Continue; + } + + match self.stack.pop() { + Some(mut ir) => { + if std::env::var("RB_STACK").is_ok() { + eprintln!("{indent}< popped {}", ir.0); + } + if ir.0 != depth { + self.stack.push(ir); + return Visit::Continue; + } + + if !self.list_queue.is_empty() { + // let (d, mut ir) = &mut ir; + if let Err(e) = populate_vec_or_color(&mut ir.1, &mut self.list_queue) { + self.ctx.diagnostics.push(e.fallback(*ir.1.value().meta())); + } + // self.stack.push((d, ir)); + // return Visit::Continue; + } + + match self.stack.pop() { + Some((d, parent)) => { + let parent = self.merge_ir(parent, ir.1); + self.stack.push((d, parent)); + } + None => { + if depth != 2 { + // eprintln!("ERROR: depth not 2??? - {depth}"); + // eprintln!("{ir:?}"); + return Visit::Continue; + } + // assert_eq!(depth, 2); + let ( + _, + IrItem::Entry(IrEntry { + key: + PropertyValueEnum::String(values::String { + value: key, + meta: key_span, + }), + value, + }), + ) = ir.clone() + else { + self.ctx + .diagnostics + .push(RootNonEntry.default_span(tree.span)); + return Visit::Continue; + }; + if let Some(existing) = self.root.insert(key, value) { + self.ctx.diagnostics.push( + ShadowedEntry { + shadowee: *existing.meta(), + shadower: key_span, + } + .unwrap(), + ); + } + } + } + // TODO: warn when shadowed + } + _ => { + // eprintln!("exit tree with no current?"); + } + } + + Visit::Continue + } +} diff --git a/crates/ltk_ritobin/src/types.rs b/crates/ltk_ritobin/src/types.rs index ee758262..49e71c11 100644 --- a/crates/ltk_ritobin/src/types.rs +++ b/crates/ltk_ritobin/src/types.rs @@ -3,70 +3,82 @@ use ltk_meta::PropertyKind; use std::str::FromStr; -/// Maps a ritobin type name string to a [`ltk_meta::PropertyKind`]. -pub fn type_name_to_kind(name: &str) -> Option { - match name { - "none" => Some(PropertyKind::None), - "bool" => Some(PropertyKind::Bool), - "i8" => Some(PropertyKind::I8), - "u8" => Some(PropertyKind::U8), - "i16" => Some(PropertyKind::I16), - "u16" => Some(PropertyKind::U16), - "i32" => Some(PropertyKind::I32), - "u32" => Some(PropertyKind::U32), - "i64" => Some(PropertyKind::I64), - "u64" => Some(PropertyKind::U64), - "f32" => Some(PropertyKind::F32), - "vec2" => Some(PropertyKind::Vector2), - "vec3" => Some(PropertyKind::Vector3), - "vec4" => Some(PropertyKind::Vector4), - "mtx44" => Some(PropertyKind::Matrix44), - "rgba" => Some(PropertyKind::Color), - "string" => Some(PropertyKind::String), - "hash" => Some(PropertyKind::Hash), - "file" => Some(PropertyKind::WadChunkLink), - "list" => Some(PropertyKind::Container), - "list2" => Some(PropertyKind::UnorderedContainer), - "pointer" => Some(PropertyKind::Struct), - "embed" => Some(PropertyKind::Embedded), - "link" => Some(PropertyKind::ObjectLink), - "option" => Some(PropertyKind::Optional), - "map" => Some(PropertyKind::Map), - "flag" => Some(PropertyKind::BitBool), - _ => None, - } +/// Extension trait for mapping ritobin type names to/from [`PropertyKind`]'s +pub trait RitobinName { + /// Maps a ritobin type name string to a [`ltk_meta::PropertyKind`]. + /// **NOTE:** Case sensitive. + fn from_rito_name(name: &str) -> Option + where + Self: Sized; + + /// Maps a [`ltk_meta::PropertyKind`] to its ritobin type name string. + fn to_rito_name(&self) -> &'static str; } -/// Maps a [`ltk_meta::PropertyKind`] to its ritobin type name string. -pub fn kind_to_type_name(kind: PropertyKind) -> &'static str { - match kind { - PropertyKind::None => "none", - PropertyKind::Bool => "bool", - PropertyKind::I8 => "i8", - PropertyKind::U8 => "u8", - PropertyKind::I16 => "i16", - PropertyKind::U16 => "u16", - PropertyKind::I32 => "i32", - PropertyKind::U32 => "u32", - PropertyKind::I64 => "i64", - PropertyKind::U64 => "u64", - PropertyKind::F32 => "f32", - PropertyKind::Vector2 => "vec2", - PropertyKind::Vector3 => "vec3", - PropertyKind::Vector4 => "vec4", - PropertyKind::Matrix44 => "mtx44", - PropertyKind::Color => "rgba", - PropertyKind::String => "string", - PropertyKind::Hash => "hash", - PropertyKind::WadChunkLink => "file", - PropertyKind::Container => "list", - PropertyKind::UnorderedContainer => "list2", - PropertyKind::Struct => "pointer", - PropertyKind::Embedded => "embed", - PropertyKind::ObjectLink => "link", - PropertyKind::Optional => "option", - PropertyKind::Map => "map", - PropertyKind::BitBool => "flag", +impl RitobinName for PropertyKind { + fn from_rito_name(name: &str) -> Option { + match name { + "none" => Some(Self::None), + "bool" => Some(Self::Bool), + "i8" => Some(Self::I8), + "u8" => Some(Self::U8), + "i16" => Some(Self::I16), + "u16" => Some(Self::U16), + "i32" => Some(Self::I32), + "u32" => Some(Self::U32), + "i64" => Some(Self::I64), + "u64" => Some(Self::U64), + "f32" => Some(Self::F32), + "vec2" => Some(Self::Vector2), + "vec3" => Some(Self::Vector3), + "vec4" => Some(Self::Vector4), + "mtx44" => Some(Self::Matrix44), + "rgba" => Some(Self::Color), + "string" => Some(Self::String), + "hash" => Some(Self::Hash), + "file" => Some(Self::WadChunkLink), + "list" => Some(Self::Container), + "list2" => Some(Self::UnorderedContainer), + "pointer" => Some(Self::Struct), + "embed" => Some(Self::Embedded), + "link" => Some(Self::ObjectLink), + "option" => Some(Self::Optional), + "map" => Some(Self::Map), + "flag" => Some(Self::BitBool), + _ => None, + } + } + + fn to_rito_name(&self) -> &'static str { + match self { + Self::None => "none", + Self::Bool => "bool", + Self::I8 => "i8", + Self::U8 => "u8", + Self::I16 => "i16", + Self::U16 => "u16", + Self::I32 => "i32", + Self::U32 => "u32", + Self::I64 => "i64", + Self::U64 => "u64", + Self::F32 => "f32", + Self::Vector2 => "vec2", + Self::Vector3 => "vec3", + Self::Vector4 => "vec4", + Self::Matrix44 => "mtx44", + Self::Color => "rgba", + Self::String => "string", + Self::Hash => "hash", + Self::WadChunkLink => "file", + Self::Container => "list", + Self::UnorderedContainer => "list2", + Self::Struct => "pointer", + Self::Embedded => "embed", + Self::ObjectLink => "link", + Self::Optional => "option", + Self::Map => "map", + Self::BitBool => "flag", + } } } @@ -108,6 +120,8 @@ impl FromStr for RitobinType { type Err = (); fn from_str(s: &str) -> Result { - type_name_to_kind(s).map(RitobinType::simple).ok_or(()) + PropertyKind::from_rito_name(s) + .map(RitobinType::simple) + .ok_or(()) } } diff --git a/crates/ltk_ritobin/src/writer.rs b/crates/ltk_ritobin/src/writer.rs index 4f131943..8ebbd53e 100644 --- a/crates/ltk_ritobin/src/writer.rs +++ b/crates/ltk_ritobin/src/writer.rs @@ -1,553 +1,5 @@ //! Text writer for ritobin format. -use std::fmt::Write; - -use ltk_meta::{ - property::{ - values::{Embedded, Struct, UnorderedContainer}, - PropertyValueEnum, - }, - Bin, BinObject, BinProperty, -}; - -use crate::{ - error::WriteError, - hashes::{HashMapProvider, HashProvider, HexHashProvider}, - types::kind_to_type_name, -}; - -/// Configuration for the text writer. -#[derive(Debug, Clone)] -pub struct WriterConfig { - /// Number of spaces per indent level. - pub indent_size: usize, -} - -impl Default for WriterConfig { - fn default() -> Self { - Self { indent_size: 4 } - } -} - -/// Text writer for ritobin format with hash provider support. -pub struct TextWriter<'a, H: HashProvider = HexHashProvider> { - buffer: String, - indent_level: usize, - config: WriterConfig, - hashes: &'a H, -} - -impl<'a> TextWriter<'a, HexHashProvider> { - /// Create a new text writer without hash lookup (all hashes written as hex). - pub fn new() -> Self { - static HEX_PROVIDER: HexHashProvider = HexHashProvider; - Self { - buffer: String::new(), - indent_level: 0, - config: WriterConfig::default(), - hashes: &HEX_PROVIDER, - } - } -} - -impl<'a, H: HashProvider> TextWriter<'a, H> { - /// Create a new text writer with a hash provider for name lookup. - pub fn with_hashes(hashes: &'a H) -> Self { - Self { - buffer: String::new(), - indent_level: 0, - config: WriterConfig::default(), - hashes, - } - } - - /// Create a new text writer with custom configuration and hash provider. - pub fn with_config_and_hashes(config: WriterConfig, hashes: &'a H) -> Self { - Self { - buffer: String::new(), - indent_level: 0, - config, - hashes, - } - } - - /// Consume the writer and return the generated string. - pub fn into_string(self) -> String { - self.buffer - } - - /// Get a reference to the generated string. - pub fn as_str(&self) -> &str { - &self.buffer - } - - fn indent(&mut self) { - self.indent_level += self.config.indent_size; - } - - fn dedent(&mut self) { - self.indent_level = self.indent_level.saturating_sub(self.config.indent_size); - } - - fn pad(&mut self) { - for _ in 0..self.indent_level { - self.buffer.push(' '); - } - } - - fn write_raw(&mut self, s: &str) { - self.buffer.push_str(s); - } - - fn write_type(&mut self, value: &PropertyValueEnum) { - let type_name = kind_to_type_name(value.kind()); - self.write_raw(type_name); - - match value { - PropertyValueEnum::Container(container) - | PropertyValueEnum::UnorderedContainer(UnorderedContainer(container)) => { - self.write_raw("["); - self.write_raw(kind_to_type_name(container.item_kind())); - self.write_raw("]"); - } - PropertyValueEnum::Optional(optional) => { - self.write_raw("["); - self.write_raw(kind_to_type_name(optional.item_kind())); - self.write_raw("]"); - } - PropertyValueEnum::Map(map) => { - self.write_raw("["); - self.write_raw(kind_to_type_name(map.key_kind())); - self.write_raw(","); - self.write_raw(kind_to_type_name(map.value_kind())); - self.write_raw("]"); - } - _ => {} - } - } - - /// Write an entry/object path hash (looks up in entries table). - fn write_entry_hash(&mut self, hash: u32) -> Result<(), WriteError> { - if let Some(name) = self.hashes.lookup_entry(hash) { - write!(self.buffer, "{:?}", name)?; - } else { - write!(self.buffer, "{:#x}", hash)?; - } - Ok(()) - } - - /// Write a field/property name hash (looks up in fields table). - fn write_field_hash(&mut self, hash: u32) -> Result<(), WriteError> { - if let Some(name) = self.hashes.lookup_field(hash) { - self.write_raw(name); - } else { - write!(self.buffer, "{:#x}", hash)?; - } - Ok(()) - } - - /// Write a hash property value (looks up in hashes table). - fn write_hash_value(&mut self, hash: u32) -> Result<(), WriteError> { - if let Some(name) = self.hashes.lookup_hash(hash) { - write!(self.buffer, "{:?}", name)?; - } else { - write!(self.buffer, "{:#x}", hash)?; - } - Ok(()) - } - - /// Write a type/class hash (looks up in types table). - fn write_type_hash(&mut self, hash: u32) -> Result<(), WriteError> { - if let Some(name) = self.hashes.lookup_type(hash) { - self.write_raw(name); - } else { - write!(self.buffer, "{:#x}", hash)?; - } - Ok(()) - } - - /// Write a link hash (looks up in entries table, same as entry paths). - fn write_link_hash(&mut self, hash: u32) -> Result<(), WriteError> { - if let Some(name) = self.hashes.lookup_entry(hash) { - write!(self.buffer, "{:?}", name)?; - } else { - write!(self.buffer, "{:#x}", hash)?; - } - Ok(()) - } - - fn write_value(&mut self, value: &PropertyValueEnum) -> Result<(), WriteError> { - match value { - PropertyValueEnum::None(_) => self.write_raw("null"), - PropertyValueEnum::Bool(v) => self.write_raw(if **v { "true" } else { "false" }), - PropertyValueEnum::I8(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::U8(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::I16(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::U16(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::I32(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::U32(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::I64(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::U64(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::F32(v) => write!(self.buffer, "{}", v.value)?, - PropertyValueEnum::Vector2(v) => { - write!(self.buffer, "{{ {}, {} }}", v.value.x, v.value.y)?; - } - PropertyValueEnum::Vector3(v) => { - write!( - self.buffer, - "{{ {}, {}, {} }}", - v.value.x, v.value.y, v.value.z - )?; - } - PropertyValueEnum::Vector4(v) => { - write!( - self.buffer, - "{{ {}, {}, {}, {} }}", - v.value.x, v.value.y, v.value.z, v.value.w - )?; - } - PropertyValueEnum::Matrix44(v) => { - self.write_raw("{\n"); - self.indent(); - // ritobin text stores matrices row-major, glam::Mat4 is column-major. - // transpose so to_cols_array() yields values in row-major order. - let arr = v.value.transpose().to_cols_array(); - for (i, val) in arr.iter().enumerate() { - if i % 4 == 0 { - self.pad(); - } - write!(self.buffer, "{}", val)?; - if i % 4 == 3 { - self.write_raw("\n"); - if i == 15 { - self.dedent(); - } - } else { - self.write_raw(", "); - } - } - self.pad(); - self.write_raw("}"); - } - PropertyValueEnum::Color(v) => { - write!( - self.buffer, - "{{ {}, {}, {}, {} }}", - v.value.r, v.value.g, v.value.b, v.value.a - )?; - } - PropertyValueEnum::String(v) => { - write!(self.buffer, "{:?}", v.value)?; - } - PropertyValueEnum::Hash(v) => { - self.write_hash_value(v.value)?; - } - PropertyValueEnum::WadChunkLink(v) => { - // WAD chunk links are u64 xxhash, we don't have lookup for these yet - write!(self.buffer, "{:#x}", v.value)?; - } - PropertyValueEnum::ObjectLink(v) => { - self.write_link_hash(v.value)?; - } - PropertyValueEnum::BitBool(v) => self.write_raw(if v.value { "true" } else { "false" }), - - PropertyValueEnum::Container(container) - | PropertyValueEnum::UnorderedContainer(UnorderedContainer(container)) => { - let items = container.clone().into_items().collect::>(); - if items.is_empty() { - self.write_raw("{}"); - } else { - self.write_raw("{\n"); - self.indent(); - for item in items { - self.pad(); - self.write_value(&item)?; - self.write_raw("\n"); - } - self.dedent(); - self.pad(); - self.write_raw("}"); - } - } - PropertyValueEnum::Optional(value) => { - if let Some(inner) = value.clone().into_inner() { - self.write_raw("{\n"); - self.indent(); - self.pad(); - self.write_value(&inner)?; - self.write_raw("\n"); - self.dedent(); - self.pad(); - self.write_raw("}"); - } else { - self.write_raw("{}"); - } - } - PropertyValueEnum::Map(map) => { - let entries = map.entries(); - if entries.is_empty() { - self.write_raw("{}"); - } else { - self.write_raw("{\n"); - self.indent(); - for (key, value) in entries { - self.pad(); - self.write_value(key)?; - self.write_raw(" = "); - self.write_value(value)?; - self.write_raw("\n"); - } - self.dedent(); - self.pad(); - self.write_raw("}"); - } - } - PropertyValueEnum::Struct(v) => { - self.write_struct_value(v)?; - } - PropertyValueEnum::Embedded(Embedded(v)) => { - self.write_struct_value(v)?; - } - } - Ok(()) - } - - fn write_struct_value(&mut self, v: &Struct) -> Result<(), WriteError> { - if v.class_hash == 0 && v.properties.is_empty() { - self.write_raw("null"); - } else { - self.write_type_hash(v.class_hash)?; - self.write_raw(" "); - if v.properties.is_empty() { - self.write_raw("{}"); - } else { - self.write_raw("{\n"); - self.indent(); - for prop in v.properties.values() { - self.write_property(prop)?; - } - self.dedent(); - self.pad(); - self.write_raw("}"); - } - } - Ok(()) - } - - fn write_property(&mut self, prop: &BinProperty) -> Result<(), WriteError> { - self.pad(); - self.write_field_hash(prop.name_hash)?; - self.write_raw(": "); - self.write_type(&prop.value); - self.write_raw(" = "); - self.write_value(&prop.value)?; - self.write_raw("\n"); - Ok(()) - } - - /// Write a Bin to the buffer. - pub fn write_tree(&mut self, tree: &Bin) -> Result<(), WriteError> { - // Header - self.write_raw("#PROP_text\n"); - - // Type - self.write_raw("type: string = \"PROP\"\n"); - - // Version - writeln!(self.buffer, "version: u32 = {}", tree.version)?; - - // Dependencies (linked) - if !tree.dependencies.is_empty() { - self.write_raw("linked: list[string] = {\n"); - self.indent(); - for dep in &tree.dependencies { - self.pad(); - writeln!(self.buffer, "{:?}", dep)?; - } - self.dedent(); - self.write_raw("}\n"); - } - - // Entries (objects) - if !tree.objects.is_empty() { - self.write_raw("entries: map[hash,embed] = {\n"); - self.indent(); - for obj in tree.objects.values() { - self.write_object(obj)?; - } - self.dedent(); - self.write_raw("}\n"); - } - - Ok(()) - } - - /// Write a single [`BinObject`]. - fn write_object(&mut self, obj: &BinObject) -> Result<(), WriteError> { - self.pad(); - self.write_entry_hash(obj.path_hash)?; - self.write_raw(" = "); - self.write_type_hash(obj.class_hash)?; - self.write_raw(" "); - - if obj.properties.is_empty() { - self.write_raw("{}\n"); - } else { - self.write_raw("{\n"); - self.indent(); - for prop in obj.properties.values() { - self.write_property(prop)?; - } - self.dedent(); - self.pad(); - self.write_raw("}\n"); - } - - Ok(()) - } -} - -impl Default for TextWriter<'_, HexHashProvider> { - fn default() -> Self { - Self::new() - } -} - -// ============================================================================ -// Public API Functions -// ============================================================================ - -/// Write a [`Bin`] to ritobin text format (hashes as hex). -pub fn write(tree: &Bin) -> Result { - let mut writer = TextWriter::new(); - writer.write_tree(tree)?; - Ok(writer.into_string()) -} - -/// Write a [`Bin`] to ritobin text format with custom configuration. -pub fn write_with_config(tree: &Bin, config: WriterConfig) -> Result { - static HEX_PROVIDER: HexHashProvider = HexHashProvider; - let mut writer = TextWriter::with_config_and_hashes(config, &HEX_PROVIDER); - writer.write_tree(tree)?; - Ok(writer.into_string()) -} - -/// Write a [`Bin`] to ritobin text format with hash name lookup. -pub fn write_with_hashes(tree: &Bin, hashes: &H) -> Result { - let mut writer = TextWriter::with_hashes(hashes); - writer.write_tree(tree)?; - Ok(writer.into_string()) -} - -/// Write a [`Bin`] to ritobin text format with configuration and hash name lookup. -pub fn write_with_config_and_hashes( - tree: &Bin, - config: WriterConfig, - hashes: &H, -) -> Result { - let mut writer = TextWriter::with_config_and_hashes(config, hashes); - writer.write_tree(tree)?; - Ok(writer.into_string()) -} - -// ============================================================================ -// Builder -// ============================================================================ - -/// A builder for creating ritobin files programmatically and converting to text. -/// -/// This is a convenience wrapper around [`ltk_meta::Builder`] -/// that adds methods for direct text output. -/// -/// # Examples -/// -/// ``` -/// use ltk_ritobin::writer::RitobinBuilder; -/// use ltk_meta::BinObject; -/// -/// let text = RitobinBuilder::new() -/// .dependency("base.bin") -/// .object(BinObject::new(0x1234, 0x5678)) -/// .to_text() -/// .unwrap(); -/// ``` -#[derive(Debug, Default, Clone)] -pub struct RitobinBuilder { - is_override: bool, - dependencies: Vec, - objects: Vec, -} - -impl RitobinBuilder { - /// Creates a new [`RitobinBuilder`] with default values. - pub fn new() -> Self { - Self::default() - } - - /// Sets whether this is an override bin file. - /// - /// Default is `false`. - pub fn is_override(mut self, is_override: bool) -> Self { - self.is_override = is_override; - self - } - - /// Adds a single dependency. - pub fn dependency(mut self, dep: impl Into) -> Self { - self.dependencies.push(dep.into()); - self - } - - /// Adds multiple dependencies. - pub fn dependencies(mut self, deps: impl IntoIterator>) -> Self { - self.dependencies.extend(deps.into_iter().map(Into::into)); - self - } - - /// Adds a single object. - pub fn object(mut self, obj: BinObject) -> Self { - self.objects.push(obj); - self - } - - /// Adds multiple objects. - pub fn objects(mut self, objs: impl IntoIterator) -> Self { - self.objects.extend(objs); - self - } - - /// Builds the [`Bin`]. - /// - /// The resulting tree will have version 3, which is always used when writing. - pub fn build(self) -> Bin { - Bin::builder() - .is_override(self.is_override) - .dependencies(self.dependencies) - .objects(self.objects) - .build() - } - - /// Build and write to ritobin text format (hashes as hex). - pub fn to_text(self) -> Result { - write(&self.build()) - } - - /// Build and write to ritobin text format with hash name lookup. - pub fn to_text_with_hashes(self, hashes: &H) -> Result { - write_with_hashes(&self.build(), hashes) - } -} - -// ============================================================================ -// Convenience type aliases -// ============================================================================ - -/// A pre-configured writer that outputs all hashes as hex values. -pub type HexWriter<'a> = TextWriter<'a, HexHashProvider>; - -/// A pre-configured writer that looks up hashes from HashMaps. -pub type NamedWriter<'a> = TextWriter<'a, HashMapProvider>; - #[cfg(test)] mod tests { use super::*; @@ -556,7 +8,7 @@ mod tests { #[test] fn test_write_simple() { let tree = Bin::new([], std::iter::empty::<&str>()); - let text = write(&tree).unwrap(); + let text = print(&tree).unwrap(); assert!(text.contains("#PROP_text")); assert!(text.contains("type: string = \"PROP\"")); assert!(text.contains("version: u32 = 3")); @@ -571,20 +23,12 @@ mod tests { "path/to/dep2.bin".to_string(), ], ); - let text = write(&tree).unwrap(); + let text = print(&tree).unwrap(); assert!(text.contains("linked: list[string] = {")); assert!(text.contains("\"path/to/dep1.bin\"")); assert!(text.contains("\"path/to/dep2.bin\"")); } - #[test] - fn test_builder() { - let tree = RitobinBuilder::new().dependency("path/to/dep.bin").build(); - - assert_eq!(tree.dependencies.len(), 1); - assert_eq!(tree.version, 3); // Version is always 3 - } - #[test] fn test_write_with_hash_lookup() { use indexmap::IndexMap; @@ -593,13 +37,7 @@ mod tests { // Create a simple tree with a hash value let mut properties = IndexMap::new(); let name_hash = ltk_hash::fnv1a::hash_lower("testField"); - properties.insert( - name_hash, - BinProperty { - name_hash, - value: PropertyValueEnum::String(String::from("hello")), - }, - ); + properties.insert(name_hash, PropertyValueEnum::String(String::from("hello"))); let path_hash = ltk_hash::fnv1a::hash_lower("Test/Path"); let class_hash = ltk_hash::fnv1a::hash_lower("TestClass"); @@ -613,7 +51,7 @@ mod tests { let tree = Bin::new(std::iter::once(obj), std::iter::empty::<&str>()); // Without hash lookup - should have hex values - let text_hex = write(&tree).unwrap(); + let text_hex = print(&tree).unwrap(); assert!(text_hex.contains(&format!("{:#x}", path_hash))); // With hash lookup - should have named values @@ -622,7 +60,7 @@ mod tests { hashes.insert_field(name_hash, "testField"); hashes.insert_type(class_hash, "TestClass"); - let text_named = write_with_hashes(&tree, &hashes).unwrap(); + let text_named = print_with_hashes(&tree, &hashes).unwrap(); assert!(text_named.contains("\"Test/Path\"")); assert!(text_named.contains("testField:")); assert!(text_named.contains("TestClass {")); diff --git a/crates/ltk_ritobin/tests/parse_sample.rs b/crates/ltk_ritobin/tests/parse_sample.rs index 740c4ec8..af7aadd1 100644 --- a/crates/ltk_ritobin/tests/parse_sample.rs +++ b/crates/ltk_ritobin/tests/parse_sample.rs @@ -1,6 +1,6 @@ //! Integration test for parsing a sample ritobin file. -use ltk_ritobin::{parse, write, ParseError}; +use ltk_ritobin::{cst::Cst, print::Print as _}; const SAMPLE_RITOBIN: &str = r#"#PROP_text type: string = "PROP" @@ -43,51 +43,51 @@ entries: map[hash,embed] = { } "#; -#[test] -fn test_parse_sample() { - let file = parse(SAMPLE_RITOBIN).expect("Failed to parse sample"); - - // Verify basic fields - assert_eq!(file.file_type(), Some("PROP")); - assert_eq!(file.version(), Some(3)); - - // Verify dependencies - let linked = file.linked(); - assert_eq!(linked.len(), 2); - assert!(linked[0].contains("Animations")); +fn tree(input: &str) -> String { + let cst = Cst::parse(input); + assert!(cst.errors.is_empty()); - // Verify objects - let objects = file.objects(); - assert_eq!(objects.len(), 1); + let mut debug = String::new(); + cst.print(&mut debug, 0, input); - // Convert to BinTree - let tree = file.to_bin_tree(); - assert_eq!(tree.version, 3); - assert_eq!(tree.dependencies.len(), 2); - assert_eq!(tree.objects.len(), 1); + debug } #[test] fn test_roundtrip() { - let file = parse(SAMPLE_RITOBIN).expect("Failed to parse sample"); - let tree = file.to_bin_tree(); + let cst = Cst::parse(SAMPLE_RITOBIN); + let (tree, errors) = cst.build_bin(SAMPLE_RITOBIN); + assert!(errors.is_empty(), "errors = {errors:#?}"); // Write back to text - let output = write(&tree).expect("Failed to write"); + let output = tree.print().expect("Failed to write"); + + println!("output:\n{output}"); // Parse again - let file2 = parse(&output).expect("Failed to parse output"); - let tree2 = file2.to_bin_tree(); + let cst2 = Cst::parse(&output); + assert!( + cst2.errors.is_empty(), + "reparse errors = {:#?}", + cst2.errors + ); + + let mut str = String::new(); + cst2.print(&mut str, 0, &output); + println!("reparsed:\n{str}"); + + let (tree2, errors) = cst2.build_bin(&output); + assert!(errors.is_empty(), "build bin errors = {errors:#?}"); // Verify structure is preserved assert_eq!(tree.version, tree2.version); assert_eq!(tree.dependencies.len(), tree2.dependencies.len()); assert_eq!(tree.objects.len(), tree2.objects.len()); } - #[test] fn test_parse_primitives() { - let input = r#" + insta::assert_snapshot!(tree( + r#" test_bool: bool = true test_i8: i8 = -128 test_u8: u8 = 255 @@ -104,19 +104,14 @@ test_string: string = "Hello, World!" test_hash: hash = 0xdeadbeef test_link: link = "path/to/object" test_flag: flag = false -"#; - - let file = parse(input).expect("Failed to parse primitives"); - assert!(file.entries.contains_key("test_bool")); - assert!(file.entries.contains_key("test_f32")); - assert!(file.entries.contains_key("test_vec3")); - assert!(file.entries.contains_key("test_rgba")); - assert!(file.entries.contains_key("test_string")); +"#, + )); } #[test] fn test_parse_containers() { - let input = r#" + insta::assert_snapshot!(tree( + r#" test_list: list[string] = { "item1" "item2" @@ -134,20 +129,14 @@ test_option_none: option[string] = {} test_map: map[hash,string] = { 0x12345678 = "value1" 0xdeadbeef = "value2" -} -"#; - - let file = parse(input).expect("Failed to parse containers"); - assert!(file.entries.contains_key("test_list")); - assert!(file.entries.contains_key("test_list2")); - assert!(file.entries.contains_key("test_option_some")); - assert!(file.entries.contains_key("test_option_none")); - assert!(file.entries.contains_key("test_map")); +}"#, + )); } #[test] fn test_parse_nested_embeds() { - let input = r#" + insta::assert_snapshot!(tree( + r#" data: embed = OuterClass { name: string = "outer" inner: embed = InnerClass { @@ -157,156 +146,86 @@ data: embed = OuterClass { } } } -"#; - - let file = parse(input).expect("Failed to parse nested embeds"); - assert!(file.entries.contains_key("data")); +"#, + )); } -#[test] -fn test_parse_pointer_null() { - let input = r#" -null_ptr: pointer = null -"#; - - let file = parse(input).expect("Failed to parse null pointer"); - assert!(file.entries.contains_key("null_ptr")); -} +// #[test] +// fn test_parse_pointer_null() { +// let input = r#" +// null_ptr: pointer = null +// "#; +// +// let file = parse(input).expect("Failed to parse null pointer"); +// assert!(file.entries.contains_key("null_ptr")); +// } #[test] fn test_parse_hex_property_names() { - let input = r#" + insta::assert_snapshot!(tree( + r#" entries: map[hash,embed] = { "Test/Path" = TestClass { 0xcb13aff1: f32 = -40 normalName: string = "test" } } -"#; - - let file = parse(input).expect("Failed to parse hex property names"); - assert!(file.entries.contains_key("entries")); -} - -#[test] -fn test_error_span_unknown_type() { - let input = "test: badtype = 42"; - let err = parse(input).unwrap_err(); - - // Verify we get an UnknownType error with correct span - match err { - ParseError::UnknownType { - type_name, span, .. - } => { - assert_eq!(type_name, "badtype"); - // "badtype" starts at position 6 (after "test: ") - assert_eq!(span.offset(), 6); - assert_eq!(span.len(), 7); // "badtype" is 7 chars - } - _ => panic!("Expected UnknownType error, got: {:?}", err), - } -} - -#[test] -fn test_error_span_multiline() { - let input = r#" -valid: string = "hello" -broken: unknowntype = 123 -"#; - let err = parse(input).unwrap_err(); - - match err { - ParseError::UnknownType { - type_name, span, .. - } => { - assert_eq!(type_name, "unknowntype"); - // The span offset should point into the second line - assert!(span.offset() > 20); // After first line - } - _ => panic!("Expected UnknownType error, got: {:?}", err), - } -} - -/// Make sure mtx44 values don't get mangled during a text round-trip. -/// uh the text format is row-major but glam stores column-major so we -/// transpose on write and on parse, i guess this just makes sure that -/// doesn't break anything and the values come out right. -#[test] -fn test_matrix44_roundtrip_ordering() { - use glam::Mat4; - use ltk_meta::property::PropertyValueEnum; - - // non-symmetric so we'd notice if it got transposed wrong - // text layout (row-major): - // 1 2 3 4 - // 5 6 7 8 - // 9 10 11 12 - // 13 14 15 16 - // - // glam is column-major internally so: - let expected = Mat4::from_cols_array(&[ - 1.0, 5.0, 9.0, 13.0, // col0 - 2.0, 6.0, 10.0, 14.0, // col1 - 3.0, 7.0, 11.0, 15.0, // col2 - 4.0, 8.0, 12.0, 16.0, // col3 - ]); - - let input = r#"#PROP_text -type: string = "PROP" -version: u32 = 3 -entries: map[hash,embed] = { - "test/object" = TestClass { - transform: mtx44 = { - 1, 2, 3, 4 - 5, 6, 7, 8 - 9, 10, 11, 12 - 13, 14, 15, 16 - } - } -} -"#; - - // 1) Parse and verify the Mat4 matches expected column-major layout - let file = parse(input).expect("Failed to parse matrix input"); - let tree = file.to_bin_tree(); - let obj = tree.objects.values().next().expect("Expected one object"); - let parsed_mat = match &obj.properties.values().next().unwrap().value { - PropertyValueEnum::Matrix44(v) => v.value, - other => panic!("Expected Matrix44, got {:?}", other), - }; - assert_eq!( - parsed_mat, expected, - "Parsed Mat4 should match expected column-major layout" - ); - - // 2) Write back to text, parse again, verify values survive the round-trip - let output = write(&tree).expect("Failed to write tree"); - let file2 = parse(&output).expect("Failed to re-parse written output"); - let tree2 = file2.to_bin_tree(); - let obj2 = tree2 - .objects - .values() - .next() - .expect("Expected one object after round-trip"); - let roundtrip_mat = match &obj2.properties.values().next().unwrap().value { - PropertyValueEnum::Matrix44(v) => v.value, - other => panic!("Expected Matrix44 after round-trip, got {:?}", other), - }; - assert_eq!( - roundtrip_mat, expected, - "Matrix44 should survive text round-trip unchanged" - ); -} - +"#, + )); +} + +// #[test] +// fn test_error_span_unknown_type() { +// let input = "test: badtype = 42"; +// let err = parse(input).unwrap_err(); +// +// // Verify we get an UnknownType error with correct span +// match err { +// ParseError::UnknownType { +// type_name, span, .. +// } => { +// assert_eq!(type_name, "badtype"); +// // "badtype" starts at position 6 (after "test: ") +// assert_eq!(span.offset(), 6); +// assert_eq!(span.len(), 7); // "badtype" is 7 chars +// } +// _ => panic!("Expected UnknownType error, got: {:?}", err), +// } +// } + +// #[test] +// fn test_error_span_multiline() { +// let input = r#" +// valid: string = "hello" +// broken: unknowntype = 123 +// "#; +// let err = parse(input).unwrap_err(); +// +// match err { +// ParseError::UnknownType { +// type_name, span, .. +// } => { +// assert_eq!(type_name, "unknowntype"); +// // The span offset should point into the second line +// assert!(span.offset() > 20); // After first line +// } +// _ => panic!("Expected UnknownType error, got: {:?}", err), +// } +// } +// +// #[test] +// fn test_error_is_miette_diagnostic() { +// use miette::Diagnostic; +// +// let input = "test: badtype = 42"; +// let err = parse(input).unwrap_err(); +// +// // ParseError implements Diagnostic +// let _code = err.code(); +// let _labels = err.labels(); +// let _source = err.source_code(); +// } #[test] -fn test_error_is_miette_diagnostic() { - use miette::Diagnostic; - - let input = "test: badtype = 42"; - let err = parse(input).unwrap_err(); - - // ParseError implements Diagnostic - let _code = err.code(); - let _labels = err.labels(); - let _source = err.source_code(); +fn test_parse_sample() { + insta::assert_snapshot!(tree(SAMPLE_RITOBIN)); } diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_containers.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_containers.snap new file mode 100644 index 00000000..c84b6d40 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_containers.snap @@ -0,0 +1,143 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: "tree(r#\"\ntest_list: list[string] = {\n \"item1\"\n \"item2\"\n \"item3\"\n}\ntest_list2: list2[u32] = {\n 1\n 2\n 3\n}\ntest_option_some: option[string] = {\n \"value\"\n}\ntest_option_none: option[string] = {}\ntest_map: map[hash,string] = {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}\"#,)" +--- +File - (0..287): "\ntest_list: list[string] = {\n \"item1\"\n \"item2\"\n \"item3\"\n}\ntest_list2: list2[u32] = {\n 1\n 2\n 3\n}\ntest_option_some: option[string] = {\n \"value\"\n}\ntest_option_none: option[string] = {}\ntest_map: map[hash,string] = {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + Entry - (0..67): "\ntest_list: list[string] = {\n \"item1\"\n \"item2\"\n \"item3\"\n}\n" + EntryKey - (1..10): "test_list" + "test_list" (Name) + ":" (Colon) + TypeExpr - (12..24): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (17..23): "string" + TypeArg - (17..23): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (26..66): " {\n \"item1\"\n \"item2\"\n \"item3\"\n}" + Block - (27..66): "{\n \"item1\"\n \"item2\"\n \"item3\"\n}" + "{" (LCurly) + ListItem - (28..40): "\n \"item1\"" + Literal - (33..40): "\"item1\"" + "\"item1\"" (String) + "\n " (Newline) + ListItem - (45..52): "\"item2\"" + Literal - (45..52): "\"item2\"" + "\"item2\"" (String) + "\n " (Newline) + ListItem - (57..64): "\"item3\"" + Literal - (57..64): "\"item3\"" + "\"item3\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (66..67): "\n" + "\n" (Newline) + Entry - (67..114): "test_list2: list2[u32] = {\n 1\n 2\n 3\n}\n" + EntryKey - (67..77): "test_list2" + "test_list2" (Name) + ":" (Colon) + TypeExpr - (79..89): "list2[u32]" + "list2" (Name) + "[" (LBrack) + TypeArgList - (85..88): "u32" + TypeArg - (85..88): "u32" + "u32" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (91..113): " {\n 1\n 2\n 3\n}" + Block - (92..113): "{\n 1\n 2\n 3\n}" + "{" (LCurly) + ListItem - (93..99): "\n 1" + Literal - (98..99): "1" + "1" (Number) + "\n " (Newline) + ListItem - (104..105): "2" + Literal - (104..105): "2" + "2" (Number) + "\n " (Newline) + ListItem - (110..111): "3" + Literal - (110..111): "3" + "3" (Number) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (113..114): "\n" + "\n" (Newline) + Entry - (114..165): "test_option_some: option[string] = {\n \"value\"\n}\n" + EntryKey - (114..130): "test_option_some" + "test_option_some" (Name) + ":" (Colon) + TypeExpr - (132..146): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (139..145): "string" + TypeArg - (139..145): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (148..164): " {\n \"value\"\n}" + Block - (149..164): "{\n \"value\"\n}" + "{" (LCurly) + ListItem - (150..162): "\n \"value\"" + Literal - (155..162): "\"value\"" + "\"value\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (164..165): "\n" + "\n" (Newline) + Entry - (165..203): "test_option_none: option[string] = {}\n" + EntryKey - (165..181): "test_option_none" + "test_option_none" (Name) + ":" (Colon) + TypeExpr - (183..197): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (190..196): "string" + TypeArg - (190..196): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (199..202): " {}" + Block - (200..202): "{}" + "{" (LCurly) + "}" (RCurly) + EntryTerminator - (202..203): "\n" + "\n" (Newline) + Entry - (203..287): "test_map: map[hash,string] = {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + EntryKey - (203..211): "test_map" + "test_map" (Name) + ":" (Colon) + TypeExpr - (213..229): "map[hash,string]" + "map" (Name) + "[" (LBrack) + TypeArgList - (217..228): "hash,string" + TypeArg - (217..221): "hash" + "hash" (Name) + "," (Comma) + TypeArg - (222..228): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (231..287): " {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + Block - (232..287): "{\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + "{" (LCurly) + Entry - (233..264): "\n 0x12345678 = \"value1\"\n " + EntryKey - (238..248): "0x12345678" + "0x12345678" (HexLit) + "=" (Eq) + EntryValue - (250..259): " \"value1\"" + Literal - (251..259): "\"value1\"" + "\"value1\"" (String) + EntryTerminator - (259..264): "\n " + "\n " (Newline) + Entry - (264..286): "0xdeadbeef = \"value2\"\n" + EntryKey - (264..274): "0xdeadbeef" + "0xdeadbeef" (HexLit) + "=" (Eq) + EntryValue - (276..285): " \"value2\"" + Literal - (277..285): "\"value2\"" + "\"value2\"" (String) + EntryTerminator - (285..286): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (287..287): "" diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_hex_property_names.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_hex_property_names.snap new file mode 100644 index 00000000..a4ab0885 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_hex_property_names.snap @@ -0,0 +1,62 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: "tree(r#\"\nentries: map[hash,embed] = {\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}\n\"#,)" +--- +File - (0..134): "\nentries: map[hash,embed] = {\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}\n" + Entry - (0..134): "\nentries: map[hash,embed] = {\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}\n" + EntryKey - (1..8): "entries" + "entries" (Name) + ":" (Colon) + TypeExpr - (10..25): "map[hash,embed]" + "map" (Name) + "[" (LBrack) + TypeArgList - (14..24): "hash,embed" + TypeArg - (14..18): "hash" + "hash" (Name) + "," (Comma) + TypeArg - (19..24): "embed" + "embed" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (27..133): " {\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}" + Block - (28..133): "{\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}" + "{" (LCurly) + Entry - (29..132): "\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n" + EntryKey - (34..45): "\"Test/Path\"" + "\"Test/Path\"" (String) + "=" (Eq) + EntryValue - (47..131): " TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }" + Class - (48..131): "TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }" + "TestClass" (Name) + Block - (58..131): "{\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }" + "{" (LCurly) + Entry - (59..98): "\n 0xcb13aff1: f32 = -40\n " + EntryKey - (68..78): "0xcb13aff1" + "0xcb13aff1" (HexLit) + ":" (Colon) + TypeExpr - (80..83): "f32" + "f32" (Name) + "=" (Eq) + EntryValue - (85..89): " -40" + Literal - (86..89): "-40" + "-40" (Number) + EntryTerminator - (89..98): "\n " + "\n " (Newline) + Entry - (98..130): "normalName: string = \"test\"\n " + EntryKey - (98..108): "normalName" + "normalName" (Name) + ":" (Colon) + TypeExpr - (110..116): "string" + "string" (Name) + "=" (Eq) + EntryValue - (118..125): " \"test\"" + Literal - (119..125): "\"test\"" + "\"test\"" (String) + EntryTerminator - (125..130): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (131..132): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (133..134): "\n" + "\n" (Newline) diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_nested_embeds.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_nested_embeds.snap new file mode 100644 index 00000000..ca063771 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_nested_embeds.snap @@ -0,0 +1,86 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: "tree(r#\"\ndata: embed = OuterClass {\n name: string = \"outer\"\n inner: embed = InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }\n}\n\"#,)" +--- +File - (0..199): "\ndata: embed = OuterClass {\n name: string = \"outer\"\n inner: embed = InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }\n}\n" + Entry - (0..199): "\ndata: embed = OuterClass {\n name: string = \"outer\"\n inner: embed = InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }\n}\n" + EntryKey - (1..5): "data" + "data" (Name) + ":" (Colon) + TypeExpr - (7..12): "embed" + "embed" (Name) + "=" (Eq) + EntryValue - (14..198): " OuterClass {\n name: string = \"outer\"\n inner: embed = InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }\n}" + Class - (15..198): "OuterClass {\n name: string = \"outer\"\n inner: embed = InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }\n}" + "OuterClass" (Name) + Block - (26..198): "{\n name: string = \"outer\"\n inner: embed = InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }\n}" + "{" (LCurly) + Entry - (27..59): "\n name: string = \"outer\"\n " + EntryKey - (32..36): "name" + "name" (Name) + ":" (Colon) + TypeExpr - (38..44): "string" + "string" (Name) + "=" (Eq) + EntryValue - (46..54): " \"outer\"" + Literal - (47..54): "\"outer\"" + "\"outer\"" (String) + EntryTerminator - (54..59): "\n " + "\n " (Newline) + Entry - (59..197): "inner: embed = InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }\n" + EntryKey - (59..64): "inner" + "inner" (Name) + ":" (Colon) + TypeExpr - (66..71): "embed" + "embed" (Name) + "=" (Eq) + EntryValue - (73..196): " InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }" + Class - (74..196): "InnerClass {\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }" + "InnerClass" (Name) + Block - (85..196): "{\n value: u32 = 42\n nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n }" + "{" (LCurly) + Entry - (86..119): "\n value: u32 = 42\n " + EntryKey - (95..100): "value" + "value" (Name) + ":" (Colon) + TypeExpr - (102..105): "u32" + "u32" (Name) + "=" (Eq) + EntryValue - (107..110): " 42" + Literal - (108..110): "42" + "42" (Number) + EntryTerminator - (110..119): "\n " + "\n " (Newline) + Entry - (119..195): "nested: embed = DeepClass {\n deep_value: f32 = 1.5\n }\n " + EntryKey - (119..125): "nested" + "nested" (Name) + ":" (Colon) + TypeExpr - (127..132): "embed" + "embed" (Name) + "=" (Eq) + EntryValue - (134..190): " DeepClass {\n deep_value: f32 = 1.5\n }" + Class - (135..190): "DeepClass {\n deep_value: f32 = 1.5\n }" + "DeepClass" (Name) + Block - (145..190): "{\n deep_value: f32 = 1.5\n }" + "{" (LCurly) + Entry - (146..189): "\n deep_value: f32 = 1.5\n " + EntryKey - (159..169): "deep_value" + "deep_value" (Name) + ":" (Colon) + TypeExpr - (171..174): "f32" + "f32" (Name) + "=" (Eq) + EntryValue - (176..180): " 1.5" + Literal - (177..180): "1.5" + "1.5" (Number) + EntryTerminator - (180..189): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (190..195): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (196..197): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (198..199): "\n" + "\n" (Newline) diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_primitives.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_primitives.snap new file mode 100644 index 00000000..59932165 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_primitives.snap @@ -0,0 +1,249 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: debug +--- +File - (0..459): "\ntest_bool: bool = true\ntest_i8: i8 = -128\ntest_u8: u8 = 255\ntest_i16: i16 = -32768\ntest_u16: u16 = 65535\ntest_i32: i32 = -2147483648\ntest_u32: u32 = 4294967295\ntest_f32: f32 = 3.14159\ntest_vec2: vec2 = { 1.0, 2.0 }\ntest_vec3: vec3 = { 1.0, 2.0, 3.0 }\ntest_vec4: vec4 = { 1.0, 2.0, 3.0, 4.0 }\ntest_rgba: rgba = { 255, 128, 64, 255 }\ntest_string: string = \"Hello, World!\"\ntest_hash: hash = 0xdeadbeef\ntest_link: link = \"path/to/object\"\ntest_flag: flag = false\n" + Entry - (0..24): "\ntest_bool: bool = true\n" + EntryKey - (1..10): "test_bool" + "test_bool" (Name) + ":" (Colon) + TypeExpr - (12..16): "bool" + "bool" (Name) + "=" (Eq) + EntryValue - (18..23): " true" + Literal - (19..23): "true" + "true" (True) + EntryTerminator - (23..24): "\n" + "\n" (Newline) + Entry - (24..43): "test_i8: i8 = -128\n" + EntryKey - (24..31): "test_i8" + "test_i8" (Name) + ":" (Colon) + TypeExpr - (33..35): "i8" + "i8" (Name) + "=" (Eq) + EntryValue - (37..42): " -128" + Literal - (38..42): "-128" + "-128" (Number) + EntryTerminator - (42..43): "\n" + "\n" (Newline) + Entry - (43..61): "test_u8: u8 = 255\n" + EntryKey - (43..50): "test_u8" + "test_u8" (Name) + ":" (Colon) + TypeExpr - (52..54): "u8" + "u8" (Name) + "=" (Eq) + EntryValue - (56..60): " 255" + Literal - (57..60): "255" + "255" (Number) + EntryTerminator - (60..61): "\n" + "\n" (Newline) + Entry - (61..84): "test_i16: i16 = -32768\n" + EntryKey - (61..69): "test_i16" + "test_i16" (Name) + ":" (Colon) + TypeExpr - (71..74): "i16" + "i16" (Name) + "=" (Eq) + EntryValue - (76..83): " -32768" + Literal - (77..83): "-32768" + "-32768" (Number) + EntryTerminator - (83..84): "\n" + "\n" (Newline) + Entry - (84..106): "test_u16: u16 = 65535\n" + EntryKey - (84..92): "test_u16" + "test_u16" (Name) + ":" (Colon) + TypeExpr - (94..97): "u16" + "u16" (Name) + "=" (Eq) + EntryValue - (99..105): " 65535" + Literal - (100..105): "65535" + "65535" (Number) + EntryTerminator - (105..106): "\n" + "\n" (Newline) + Entry - (106..134): "test_i32: i32 = -2147483648\n" + EntryKey - (106..114): "test_i32" + "test_i32" (Name) + ":" (Colon) + TypeExpr - (116..119): "i32" + "i32" (Name) + "=" (Eq) + EntryValue - (121..133): " -2147483648" + Literal - (122..133): "-2147483648" + "-2147483648" (Number) + EntryTerminator - (133..134): "\n" + "\n" (Newline) + Entry - (134..161): "test_u32: u32 = 4294967295\n" + EntryKey - (134..142): "test_u32" + "test_u32" (Name) + ":" (Colon) + TypeExpr - (144..147): "u32" + "u32" (Name) + "=" (Eq) + EntryValue - (149..160): " 4294967295" + Literal - (150..160): "4294967295" + "4294967295" (Number) + EntryTerminator - (160..161): "\n" + "\n" (Newline) + Entry - (161..185): "test_f32: f32 = 3.14159\n" + EntryKey - (161..169): "test_f32" + "test_f32" (Name) + ":" (Colon) + TypeExpr - (171..174): "f32" + "f32" (Name) + "=" (Eq) + EntryValue - (176..184): " 3.14159" + Literal - (177..184): "3.14159" + "3.14159" (Number) + EntryTerminator - (184..185): "\n" + "\n" (Newline) + Entry - (185..216): "test_vec2: vec2 = { 1.0, 2.0 }\n" + EntryKey - (185..194): "test_vec2" + "test_vec2" (Name) + ":" (Colon) + TypeExpr - (196..200): "vec2" + "vec2" (Name) + "=" (Eq) + EntryValue - (202..215): " { 1.0, 2.0 }" + Block - (203..215): "{ 1.0, 2.0 }" + "{" (LCurly) + ListItem - (204..208): " 1.0" + Literal - (205..208): "1.0" + "1.0" (Number) + "," (Comma) + ListItem - (209..213): " 2.0" + Literal - (210..213): "2.0" + "2.0" (Number) + "}" (RCurly) + EntryTerminator - (215..216): "\n" + "\n" (Newline) + Entry - (216..252): "test_vec3: vec3 = { 1.0, 2.0, 3.0 }\n" + EntryKey - (216..225): "test_vec3" + "test_vec3" (Name) + ":" (Colon) + TypeExpr - (227..231): "vec3" + "vec3" (Name) + "=" (Eq) + EntryValue - (233..251): " { 1.0, 2.0, 3.0 }" + Block - (234..251): "{ 1.0, 2.0, 3.0 }" + "{" (LCurly) + ListItem - (235..239): " 1.0" + Literal - (236..239): "1.0" + "1.0" (Number) + "," (Comma) + ListItem - (240..244): " 2.0" + Literal - (241..244): "2.0" + "2.0" (Number) + "," (Comma) + ListItem - (245..249): " 3.0" + Literal - (246..249): "3.0" + "3.0" (Number) + "}" (RCurly) + EntryTerminator - (251..252): "\n" + "\n" (Newline) + Entry - (252..293): "test_vec4: vec4 = { 1.0, 2.0, 3.0, 4.0 }\n" + EntryKey - (252..261): "test_vec4" + "test_vec4" (Name) + ":" (Colon) + TypeExpr - (263..267): "vec4" + "vec4" (Name) + "=" (Eq) + EntryValue - (269..292): " { 1.0, 2.0, 3.0, 4.0 }" + Block - (270..292): "{ 1.0, 2.0, 3.0, 4.0 }" + "{" (LCurly) + ListItem - (271..275): " 1.0" + Literal - (272..275): "1.0" + "1.0" (Number) + "," (Comma) + ListItem - (276..280): " 2.0" + Literal - (277..280): "2.0" + "2.0" (Number) + "," (Comma) + ListItem - (281..285): " 3.0" + Literal - (282..285): "3.0" + "3.0" (Number) + "," (Comma) + ListItem - (286..290): " 4.0" + Literal - (287..290): "4.0" + "4.0" (Number) + "}" (RCurly) + EntryTerminator - (292..293): "\n" + "\n" (Newline) + Entry - (293..333): "test_rgba: rgba = { 255, 128, 64, 255 }\n" + EntryKey - (293..302): "test_rgba" + "test_rgba" (Name) + ":" (Colon) + TypeExpr - (304..308): "rgba" + "rgba" (Name) + "=" (Eq) + EntryValue - (310..332): " { 255, 128, 64, 255 }" + Block - (311..332): "{ 255, 128, 64, 255 }" + "{" (LCurly) + ListItem - (312..316): " 255" + Literal - (313..316): "255" + "255" (Number) + "," (Comma) + ListItem - (317..321): " 128" + Literal - (318..321): "128" + "128" (Number) + "," (Comma) + ListItem - (322..325): " 64" + Literal - (323..325): "64" + "64" (Number) + "," (Comma) + ListItem - (326..330): " 255" + Literal - (327..330): "255" + "255" (Number) + "}" (RCurly) + EntryTerminator - (332..333): "\n" + "\n" (Newline) + Entry - (333..371): "test_string: string = \"Hello, World!\"\n" + EntryKey - (333..344): "test_string" + "test_string" (Name) + ":" (Colon) + TypeExpr - (346..352): "string" + "string" (Name) + "=" (Eq) + EntryValue - (354..370): " \"Hello, World!\"" + Literal - (355..370): "\"Hello, World!\"" + "\"Hello, World!\"" (String) + EntryTerminator - (370..371): "\n" + "\n" (Newline) + Entry - (371..400): "test_hash: hash = 0xdeadbeef\n" + EntryKey - (371..380): "test_hash" + "test_hash" (Name) + ":" (Colon) + TypeExpr - (382..386): "hash" + "hash" (Name) + "=" (Eq) + EntryValue - (388..399): " 0xdeadbeef" + Literal - (389..399): "0xdeadbeef" + "0xdeadbeef" (HexLit) + EntryTerminator - (399..400): "\n" + "\n" (Newline) + Entry - (400..435): "test_link: link = \"path/to/object\"\n" + EntryKey - (400..409): "test_link" + "test_link" (Name) + ":" (Colon) + TypeExpr - (411..415): "link" + "link" (Name) + "=" (Eq) + EntryValue - (417..434): " \"path/to/object\"" + Literal - (418..434): "\"path/to/object\"" + "\"path/to/object\"" (String) + EntryTerminator - (434..435): "\n" + "\n" (Newline) + Entry - (435..459): "test_flag: flag = false\n" + EntryKey - (435..444): "test_flag" + "test_flag" (Name) + ":" (Colon) + TypeExpr - (446..450): "flag" + "flag" (Name) + "=" (Eq) + EntryValue - (452..458): " false" + Literal - (453..458): "false" + "false" (False) + EntryTerminator - (458..459): "\n" + "\n" (Newline) diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_sample.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_sample.snap new file mode 100644 index 00000000..cc4e896d --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__parse_sample.snap @@ -0,0 +1,319 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: tree(SAMPLE_RITOBIN) +--- +File - (0..1310): "#PROP_text\ntype: string = \"PROP\"\nversion: u32 = 3\nlinked: list[string] = {\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}\nentries: map[hash,embed] = {\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}\n" + Comment - (0..10): "#PROP_text" + "#PROP_text" (Comment) + Entry - (10..33): "\ntype: string = \"PROP\"\n" + EntryKey - (11..15): "type" + "type" (Name) + ":" (Colon) + TypeExpr - (17..23): "string" + "string" (Name) + "=" (Eq) + EntryValue - (25..32): " \"PROP\"" + Literal - (26..32): "\"PROP\"" + "\"PROP\"" (String) + EntryTerminator - (32..33): "\n" + "\n" (Newline) + Entry - (33..50): "version: u32 = 3\n" + EntryKey - (33..40): "version" + "version" (Name) + ":" (Colon) + TypeExpr - (42..45): "u32" + "u32" (Name) + "=" (Eq) + EntryValue - (47..49): " 3" + Literal - (48..49): "3" + "3" (Number) + EntryTerminator - (49..50): "\n" + "\n" (Newline) + Entry - (50..161): "linked: list[string] = {\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}\n" + EntryKey - (50..56): "linked" + "linked" (Name) + ":" (Colon) + TypeExpr - (58..70): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (63..69): "string" + TypeArg - (63..69): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (72..160): " {\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}" + Block - (73..160): "{\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}" + "{" (LCurly) + ListItem - (74..122): "\n \"DATA/Characters/Test/Animations/Skin0.bin\"" + Literal - (79..122): "\"DATA/Characters/Test/Animations/Skin0.bin\"" + "\"DATA/Characters/Test/Animations/Skin0.bin\"" (String) + "\n " (Newline) + ListItem - (127..158): "\"DATA/Characters/Test/Test.bin\"" + Literal - (127..158): "\"DATA/Characters/Test/Test.bin\"" + "\"DATA/Characters/Test/Test.bin\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (160..161): "\n" + "\n" (Newline) + Entry - (161..1310): "entries: map[hash,embed] = {\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}\n" + EntryKey - (161..168): "entries" + "entries" (Name) + ":" (Colon) + TypeExpr - (170..185): "map[hash,embed]" + "map" (Name) + "[" (LBrack) + TypeArgList - (174..184): "hash,embed" + TypeArg - (174..178): "hash" + "hash" (Name) + "," (Comma) + TypeArg - (179..184): "embed" + "embed" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (187..1309): " {\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}" + Block - (188..1309): "{\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}" + "{" (LCurly) + Entry - (189..1308): "\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n" + EntryKey - (194..223): "\"Characters/Test/Skins/Skin0\"" + "\"Characters/Test/Skins/Skin0\"" (String) + "=" (Eq) + EntryValue - (225..1307): " SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }" + Class - (226..1307): "SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }" + "SkinCharacterDataProperties" (Name) + Block - (254..1307): "{\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }" + "{" (LCurly) + Entry - (255..300): "\n skinClassification: u32 = 1\n " + EntryKey - (264..282): "skinClassification" + "skinClassification" (Name) + ":" (Colon) + TypeExpr - (284..287): "u32" + "u32" (Name) + "=" (Eq) + EntryValue - (289..291): " 1" + Literal - (290..291): "1" + "1" (Number) + EntryTerminator - (291..300): "\n " + "\n " (Newline) + Entry - (300..346): "championSkinName: string = \"TestBase\"\n " + EntryKey - (300..316): "championSkinName" + "championSkinName" (Name) + ":" (Colon) + TypeExpr - (318..324): "string" + "string" (Name) + "=" (Eq) + EntryValue - (326..337): " \"TestBase\"" + Literal - (327..337): "\"TestBase\"" + "\"TestBase\"" (String) + EntryTerminator - (337..346): "\n " + "\n " (Newline) + Entry - (346..391): "metaDataTags: string = \"gender:male\"\n " + EntryKey - (346..358): "metaDataTags" + "metaDataTags" (Name) + ":" (Colon) + TypeExpr - (360..366): "string" + "string" (Name) + "=" (Eq) + EntryValue - (368..382): " \"gender:male\"" + Literal - (369..382): "\"gender:male\"" + "\"gender:male\"" (String) + EntryTerminator - (382..391): "\n " + "\n " (Newline) + Entry - (391..528): "loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n " + EntryKey - (391..401): "loadscreen" + "loadscreen" (Name) + ":" (Colon) + TypeExpr - (403..408): "embed" + "embed" (Name) + "=" (Eq) + EntryValue - (410..519): " CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }" + Class - (411..519): "CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }" + "CensoredImage" (Name) + Block - (425..519): "{\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }" + "{" (LCurly) + Entry - (426..518): "\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n " + EntryKey - (439..444): "image" + "image" (Name) + ":" (Colon) + TypeExpr - (446..452): "string" + "string" (Name) + "=" (Eq) + EntryValue - (454..509): " \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"" + Literal - (455..509): "\"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"" + "\"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"" (String) + EntryTerminator - (509..518): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (519..528): "\n " + "\n " (Newline) + Entry - (528..1167): "skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n " + EntryKey - (528..547): "skinAudioProperties" + "skinAudioProperties" (Name) + ":" (Colon) + TypeExpr - (549..554): "embed" + "embed" (Name) + "=" (Eq) + EntryValue - (556..1158): " skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }" + Class - (557..1158): "skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }" + "skinAudioProperties" (Name) + Block - (577..1158): "{\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }" + "{" (LCurly) + Entry - (578..671): "\n tagEventList: list[string] = {\n \"Test\"\n }\n " + EntryKey - (591..603): "tagEventList" + "tagEventList" (Name) + ":" (Colon) + TypeExpr - (605..617): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (610..616): "string" + TypeArg - (610..616): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (619..658): " {\n \"Test\"\n }" + Block - (620..658): "{\n \"Test\"\n }" + "{" (LCurly) + ListItem - (621..644): "\n \"Test\"" + Literal - (638..644): "\"Test\"" + "\"Test\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (658..671): "\n " + "\n " (Newline) + Entry - (671..1157): "bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n " + EntryKey - (671..680): "bankUnits" + "bankUnits" (Name) + ":" (Colon) + TypeExpr - (682..694): "list2[embed]" + "list2" (Name) + "[" (LBrack) + TypeArgList - (688..693): "embed" + TypeArg - (688..693): "embed" + "embed" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (696..1148): " {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }" + Block - (697..1148): "{\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }" + "{" (LCurly) + ListItem - (698..1147): "\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n " + Class - (715..1134): "BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }" + "BankUnit" (Name) + Block - (724..1134): "{\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }" + "{" (LCurly) + Entry - (725..797): "\n name: string = \"Test_Base_SFX\"\n " + EntryKey - (746..750): "name" + "name" (Name) + ":" (Colon) + TypeExpr - (752..758): "string" + "string" (Name) + "=" (Eq) + EntryValue - (760..776): " \"Test_Base_SFX\"" + Literal - (761..776): "\"Test_Base_SFX\"" + "\"Test_Base_SFX\"" (String) + EntryTerminator - (776..797): "\n " + "\n " (Newline) + Entry - (797..977): "bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n " + EntryKey - (797..805): "bankPath" + "bankPath" (Name) + ":" (Colon) + TypeExpr - (807..819): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (812..818): "string" + TypeArg - (812..818): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (821..956): " {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }" + Block - (822..956): "{\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }" + "{" (LCurly) + ListItem - (823..878): "\n \"ASSETS/Sounds/Test/audio.bnk\"" + Literal - (848..878): "\"ASSETS/Sounds/Test/audio.bnk\"" + "\"ASSETS/Sounds/Test/audio.bnk\"" (String) + "\n " (Newline) + ListItem - (903..934): "\"ASSETS/Sounds/Test/events.bnk\"" + Literal - (903..934): "\"ASSETS/Sounds/Test/events.bnk\"" + "\"ASSETS/Sounds/Test/events.bnk\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (956..977): "\n " + "\n " (Newline) + Entry - (977..1133): "events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n " + EntryKey - (977..983): "events" + "events" (Name) + ":" (Colon) + TypeExpr - (985..997): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (990..996): "string" + TypeArg - (990..996): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (999..1116): " {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }" + Block - (1000..1116): "{\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }" + "{" (LCurly) + ListItem - (1001..1048): "\n \"Play_sfx_Test_Attack\"" + Literal - (1026..1048): "\"Play_sfx_Test_Attack\"" + "\"Play_sfx_Test_Attack\"" (String) + "\n " (Newline) + ListItem - (1073..1094): "\"Play_sfx_Test_Death\"" + Literal - (1073..1094): "\"Play_sfx_Test_Death\"" + "\"Play_sfx_Test_Death\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1116..1133): "\n " + "\n " (Newline) + "}" (RCurly) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1148..1157): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1158..1167): "\n " + "\n " (Newline) + Entry - (1167..1270): "iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n " + EntryKey - (1167..1177): "iconCircle" + "iconCircle" (Name) + ":" (Colon) + TypeExpr - (1179..1193): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (1186..1192): "string" + TypeArg - (1186..1192): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (1195..1261): " {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }" + Block - (1196..1261): "{\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }" + "{" (LCurly) + ListItem - (1197..1251): "\n \"ASSETS/Characters/Test/Icons/Circle.tex\"" + Literal - (1210..1251): "\"ASSETS/Characters/Test/Icons/Circle.tex\"" + "\"ASSETS/Characters/Test/Icons/Circle.tex\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1261..1270): "\n " + "\n " (Newline) + Entry - (1270..1306): "iconSquare: option[string] = {}\n " + EntryKey - (1270..1280): "iconSquare" + "iconSquare" (Name) + ":" (Colon) + TypeExpr - (1282..1296): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (1289..1295): "string" + TypeArg - (1289..1295): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (1298..1301): " {}" + Block - (1299..1301): "{}" + "{" (LCurly) + "}" (RCurly) + EntryTerminator - (1301..1306): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1307..1308): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (1309..1310): "\n" + "\n" (Newline) diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-2.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-2.snap new file mode 100644 index 00000000..2ed7a2f9 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-2.snap @@ -0,0 +1,143 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: debug +--- +File - (0..287): "\ntest_list: list[string] = {\n \"item1\"\n \"item2\"\n \"item3\"\n}\ntest_list2: list2[u32] = {\n 1\n 2\n 3\n}\ntest_option_some: option[string] = {\n \"value\"\n}\ntest_option_none: option[string] = {}\ntest_map: map[hash,string] = {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + Entry - (0..67): "\ntest_list: list[string] = {\n \"item1\"\n \"item2\"\n \"item3\"\n}\n" + EntryKey - (1..10): "test_list" + "test_list" (Name) + ":" (Colon) + TypeExpr - (12..24): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (17..23): "string" + TypeArg - (17..23): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (26..66): " {\n \"item1\"\n \"item2\"\n \"item3\"\n}" + Block - (27..66): "{\n \"item1\"\n \"item2\"\n \"item3\"\n}" + "{" (LCurly) + ListItem - (28..40): "\n \"item1\"" + Literal - (33..40): "\"item1\"" + "\"item1\"" (String) + "\n " (Newline) + ListItem - (45..52): "\"item2\"" + Literal - (45..52): "\"item2\"" + "\"item2\"" (String) + "\n " (Newline) + ListItem - (57..64): "\"item3\"" + Literal - (57..64): "\"item3\"" + "\"item3\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (66..67): "\n" + "\n" (Newline) + Entry - (67..114): "test_list2: list2[u32] = {\n 1\n 2\n 3\n}\n" + EntryKey - (67..77): "test_list2" + "test_list2" (Name) + ":" (Colon) + TypeExpr - (79..89): "list2[u32]" + "list2" (Name) + "[" (LBrack) + TypeArgList - (85..88): "u32" + TypeArg - (85..88): "u32" + "u32" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (91..113): " {\n 1\n 2\n 3\n}" + Block - (92..113): "{\n 1\n 2\n 3\n}" + "{" (LCurly) + ListItem - (93..99): "\n 1" + Literal - (98..99): "1" + "1" (Number) + "\n " (Newline) + ListItem - (104..105): "2" + Literal - (104..105): "2" + "2" (Number) + "\n " (Newline) + ListItem - (110..111): "3" + Literal - (110..111): "3" + "3" (Number) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (113..114): "\n" + "\n" (Newline) + Entry - (114..165): "test_option_some: option[string] = {\n \"value\"\n}\n" + EntryKey - (114..130): "test_option_some" + "test_option_some" (Name) + ":" (Colon) + TypeExpr - (132..146): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (139..145): "string" + TypeArg - (139..145): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (148..164): " {\n \"value\"\n}" + Block - (149..164): "{\n \"value\"\n}" + "{" (LCurly) + ListItem - (150..162): "\n \"value\"" + Literal - (155..162): "\"value\"" + "\"value\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (164..165): "\n" + "\n" (Newline) + Entry - (165..203): "test_option_none: option[string] = {}\n" + EntryKey - (165..181): "test_option_none" + "test_option_none" (Name) + ":" (Colon) + TypeExpr - (183..197): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (190..196): "string" + TypeArg - (190..196): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (199..202): " {}" + Block - (200..202): "{}" + "{" (LCurly) + "}" (RCurly) + EntryTerminator - (202..203): "\n" + "\n" (Newline) + Entry - (203..287): "test_map: map[hash,string] = {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + EntryKey - (203..211): "test_map" + "test_map" (Name) + ":" (Colon) + TypeExpr - (213..229): "map[hash,string]" + "map" (Name) + "[" (LBrack) + TypeArgList - (217..228): "hash,string" + TypeArg - (217..221): "hash" + "hash" (Name) + "," (Comma) + TypeArg - (222..228): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (231..287): " {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + Block - (232..287): "{\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + "{" (LCurly) + Entry - (233..264): "\n 0x12345678 = \"value1\"\n " + EntryKey - (238..248): "0x12345678" + "0x12345678" (HexLit) + "=" (Eq) + EntryValue - (250..259): " \"value1\"" + Literal - (251..259): "\"value1\"" + "\"value1\"" (String) + EntryTerminator - (259..264): "\n " + "\n " (Newline) + Entry - (264..286): "0xdeadbeef = \"value2\"\n" + EntryKey - (264..274): "0xdeadbeef" + "0xdeadbeef" (HexLit) + "=" (Eq) + EntryValue - (276..285): " \"value2\"" + Literal - (277..285): "\"value2\"" + "\"value2\"" (String) + EntryTerminator - (285..286): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (287..287): "" diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-3.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-3.snap new file mode 100644 index 00000000..61e77213 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-3.snap @@ -0,0 +1,319 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: debug +--- +File - (0..1310): "#PROP_text\ntype: string = \"PROP\"\nversion: u32 = 3\nlinked: list[string] = {\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}\nentries: map[hash,embed] = {\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}\n" + Comment - (0..10): "#PROP_text" + "#PROP_text" (Comment) + Entry - (10..33): "\ntype: string = \"PROP\"\n" + EntryKey - (11..15): "type" + "type" (Name) + ":" (Colon) + TypeExpr - (17..23): "string" + "string" (Name) + "=" (Eq) + EntryValue - (25..32): " \"PROP\"" + Literal - (26..32): "\"PROP\"" + "\"PROP\"" (String) + EntryTerminator - (32..33): "\n" + "\n" (Newline) + Entry - (33..50): "version: u32 = 3\n" + EntryKey - (33..40): "version" + "version" (Name) + ":" (Colon) + TypeExpr - (42..45): "u32" + "u32" (Name) + "=" (Eq) + EntryValue - (47..49): " 3" + Literal - (48..49): "3" + "3" (Number) + EntryTerminator - (49..50): "\n" + "\n" (Newline) + Entry - (50..161): "linked: list[string] = {\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}\n" + EntryKey - (50..56): "linked" + "linked" (Name) + ":" (Colon) + TypeExpr - (58..70): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (63..69): "string" + TypeArg - (63..69): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (72..160): " {\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}" + Block - (73..160): "{\n \"DATA/Characters/Test/Animations/Skin0.bin\"\n \"DATA/Characters/Test/Test.bin\"\n}" + "{" (LCurly) + ListItem - (74..122): "\n \"DATA/Characters/Test/Animations/Skin0.bin\"" + Literal - (79..122): "\"DATA/Characters/Test/Animations/Skin0.bin\"" + "\"DATA/Characters/Test/Animations/Skin0.bin\"" (String) + "\n " (Newline) + ListItem - (127..158): "\"DATA/Characters/Test/Test.bin\"" + Literal - (127..158): "\"DATA/Characters/Test/Test.bin\"" + "\"DATA/Characters/Test/Test.bin\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (160..161): "\n" + "\n" (Newline) + Entry - (161..1310): "entries: map[hash,embed] = {\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}\n" + EntryKey - (161..168): "entries" + "entries" (Name) + ":" (Colon) + TypeExpr - (170..185): "map[hash,embed]" + "map" (Name) + "[" (LBrack) + TypeArgList - (174..184): "hash,embed" + TypeArg - (174..178): "hash" + "hash" (Name) + "," (Comma) + TypeArg - (179..184): "embed" + "embed" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (187..1309): " {\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}" + Block - (188..1309): "{\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n}" + "{" (LCurly) + Entry - (189..1308): "\n \"Characters/Test/Skins/Skin0\" = SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }\n" + EntryKey - (194..223): "\"Characters/Test/Skins/Skin0\"" + "\"Characters/Test/Skins/Skin0\"" (String) + "=" (Eq) + EntryValue - (225..1307): " SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }" + Class - (226..1307): "SkinCharacterDataProperties {\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }" + "SkinCharacterDataProperties" (Name) + Block - (254..1307): "{\n skinClassification: u32 = 1\n championSkinName: string = \"TestBase\"\n metaDataTags: string = \"gender:male\"\n loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n iconSquare: option[string] = {}\n }" + "{" (LCurly) + Entry - (255..300): "\n skinClassification: u32 = 1\n " + EntryKey - (264..282): "skinClassification" + "skinClassification" (Name) + ":" (Colon) + TypeExpr - (284..287): "u32" + "u32" (Name) + "=" (Eq) + EntryValue - (289..291): " 1" + Literal - (290..291): "1" + "1" (Number) + EntryTerminator - (291..300): "\n " + "\n " (Newline) + Entry - (300..346): "championSkinName: string = \"TestBase\"\n " + EntryKey - (300..316): "championSkinName" + "championSkinName" (Name) + ":" (Colon) + TypeExpr - (318..324): "string" + "string" (Name) + "=" (Eq) + EntryValue - (326..337): " \"TestBase\"" + Literal - (327..337): "\"TestBase\"" + "\"TestBase\"" (String) + EntryTerminator - (337..346): "\n " + "\n " (Newline) + Entry - (346..391): "metaDataTags: string = \"gender:male\"\n " + EntryKey - (346..358): "metaDataTags" + "metaDataTags" (Name) + ":" (Colon) + TypeExpr - (360..366): "string" + "string" (Name) + "=" (Eq) + EntryValue - (368..382): " \"gender:male\"" + Literal - (369..382): "\"gender:male\"" + "\"gender:male\"" (String) + EntryTerminator - (382..391): "\n " + "\n " (Newline) + Entry - (391..528): "loadscreen: embed = CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }\n " + EntryKey - (391..401): "loadscreen" + "loadscreen" (Name) + ":" (Colon) + TypeExpr - (403..408): "embed" + "embed" (Name) + "=" (Eq) + EntryValue - (410..519): " CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }" + Class - (411..519): "CensoredImage {\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }" + "CensoredImage" (Name) + Block - (425..519): "{\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n }" + "{" (LCurly) + Entry - (426..518): "\n image: string = \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"\n " + EntryKey - (439..444): "image" + "image" (Name) + ":" (Colon) + TypeExpr - (446..452): "string" + "string" (Name) + "=" (Eq) + EntryValue - (454..509): " \"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"" + Literal - (455..509): "\"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"" + "\"ASSETS/Characters/Test/Skins/Base/TestLoadScreen.tex\"" (String) + EntryTerminator - (509..518): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (519..528): "\n " + "\n " (Newline) + Entry - (528..1167): "skinAudioProperties: embed = skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }\n " + EntryKey - (528..547): "skinAudioProperties" + "skinAudioProperties" (Name) + ":" (Colon) + TypeExpr - (549..554): "embed" + "embed" (Name) + "=" (Eq) + EntryValue - (556..1158): " skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }" + Class - (557..1158): "skinAudioProperties {\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }" + "skinAudioProperties" (Name) + Block - (577..1158): "{\n tagEventList: list[string] = {\n \"Test\"\n }\n bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n }" + "{" (LCurly) + Entry - (578..671): "\n tagEventList: list[string] = {\n \"Test\"\n }\n " + EntryKey - (591..603): "tagEventList" + "tagEventList" (Name) + ":" (Colon) + TypeExpr - (605..617): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (610..616): "string" + TypeArg - (610..616): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (619..658): " {\n \"Test\"\n }" + Block - (620..658): "{\n \"Test\"\n }" + "{" (LCurly) + ListItem - (621..644): "\n \"Test\"" + Literal - (638..644): "\"Test\"" + "\"Test\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (658..671): "\n " + "\n " (Newline) + Entry - (671..1157): "bankUnits: list2[embed] = {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }\n " + EntryKey - (671..680): "bankUnits" + "bankUnits" (Name) + ":" (Colon) + TypeExpr - (682..694): "list2[embed]" + "list2" (Name) + "[" (LBrack) + TypeArgList - (688..693): "embed" + TypeArg - (688..693): "embed" + "embed" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (696..1148): " {\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }" + Block - (697..1148): "{\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n }" + "{" (LCurly) + ListItem - (698..1147): "\n BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }\n " + Class - (715..1134): "BankUnit {\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }" + "BankUnit" (Name) + Block - (724..1134): "{\n name: string = \"Test_Base_SFX\"\n bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n }" + "{" (LCurly) + Entry - (725..797): "\n name: string = \"Test_Base_SFX\"\n " + EntryKey - (746..750): "name" + "name" (Name) + ":" (Colon) + TypeExpr - (752..758): "string" + "string" (Name) + "=" (Eq) + EntryValue - (760..776): " \"Test_Base_SFX\"" + Literal - (761..776): "\"Test_Base_SFX\"" + "\"Test_Base_SFX\"" (String) + EntryTerminator - (776..797): "\n " + "\n " (Newline) + Entry - (797..977): "bankPath: list[string] = {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }\n " + EntryKey - (797..805): "bankPath" + "bankPath" (Name) + ":" (Colon) + TypeExpr - (807..819): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (812..818): "string" + TypeArg - (812..818): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (821..956): " {\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }" + Block - (822..956): "{\n \"ASSETS/Sounds/Test/audio.bnk\"\n \"ASSETS/Sounds/Test/events.bnk\"\n }" + "{" (LCurly) + ListItem - (823..878): "\n \"ASSETS/Sounds/Test/audio.bnk\"" + Literal - (848..878): "\"ASSETS/Sounds/Test/audio.bnk\"" + "\"ASSETS/Sounds/Test/audio.bnk\"" (String) + "\n " (Newline) + ListItem - (903..934): "\"ASSETS/Sounds/Test/events.bnk\"" + Literal - (903..934): "\"ASSETS/Sounds/Test/events.bnk\"" + "\"ASSETS/Sounds/Test/events.bnk\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (956..977): "\n " + "\n " (Newline) + Entry - (977..1133): "events: list[string] = {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }\n " + EntryKey - (977..983): "events" + "events" (Name) + ":" (Colon) + TypeExpr - (985..997): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (990..996): "string" + TypeArg - (990..996): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (999..1116): " {\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }" + Block - (1000..1116): "{\n \"Play_sfx_Test_Attack\"\n \"Play_sfx_Test_Death\"\n }" + "{" (LCurly) + ListItem - (1001..1048): "\n \"Play_sfx_Test_Attack\"" + Literal - (1026..1048): "\"Play_sfx_Test_Attack\"" + "\"Play_sfx_Test_Attack\"" (String) + "\n " (Newline) + ListItem - (1073..1094): "\"Play_sfx_Test_Death\"" + Literal - (1073..1094): "\"Play_sfx_Test_Death\"" + "\"Play_sfx_Test_Death\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1116..1133): "\n " + "\n " (Newline) + "}" (RCurly) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1148..1157): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1158..1167): "\n " + "\n " (Newline) + Entry - (1167..1270): "iconCircle: option[string] = {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }\n " + EntryKey - (1167..1177): "iconCircle" + "iconCircle" (Name) + ":" (Colon) + TypeExpr - (1179..1193): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (1186..1192): "string" + TypeArg - (1186..1192): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (1195..1261): " {\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }" + Block - (1196..1261): "{\n \"ASSETS/Characters/Test/Icons/Circle.tex\"\n }" + "{" (LCurly) + ListItem - (1197..1251): "\n \"ASSETS/Characters/Test/Icons/Circle.tex\"" + Literal - (1210..1251): "\"ASSETS/Characters/Test/Icons/Circle.tex\"" + "\"ASSETS/Characters/Test/Icons/Circle.tex\"" (String) + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1261..1270): "\n " + "\n " (Newline) + Entry - (1270..1306): "iconSquare: option[string] = {}\n " + EntryKey - (1270..1280): "iconSquare" + "iconSquare" (Name) + ":" (Colon) + TypeExpr - (1282..1296): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (1289..1295): "string" + TypeArg - (1289..1295): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (1298..1301): " {}" + Block - (1299..1301): "{}" + "{" (LCurly) + "}" (RCurly) + EntryTerminator - (1301..1306): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (1307..1308): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (1309..1310): "\n" + "\n" (Newline) diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-4.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-4.snap new file mode 100644 index 00000000..2ed7a2f9 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-4.snap @@ -0,0 +1,143 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: debug +--- +File - (0..287): "\ntest_list: list[string] = {\n \"item1\"\n \"item2\"\n \"item3\"\n}\ntest_list2: list2[u32] = {\n 1\n 2\n 3\n}\ntest_option_some: option[string] = {\n \"value\"\n}\ntest_option_none: option[string] = {}\ntest_map: map[hash,string] = {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + Entry - (0..67): "\ntest_list: list[string] = {\n \"item1\"\n \"item2\"\n \"item3\"\n}\n" + EntryKey - (1..10): "test_list" + "test_list" (Name) + ":" (Colon) + TypeExpr - (12..24): "list[string]" + "list" (Name) + "[" (LBrack) + TypeArgList - (17..23): "string" + TypeArg - (17..23): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (26..66): " {\n \"item1\"\n \"item2\"\n \"item3\"\n}" + Block - (27..66): "{\n \"item1\"\n \"item2\"\n \"item3\"\n}" + "{" (LCurly) + ListItem - (28..40): "\n \"item1\"" + Literal - (33..40): "\"item1\"" + "\"item1\"" (String) + "\n " (Newline) + ListItem - (45..52): "\"item2\"" + Literal - (45..52): "\"item2\"" + "\"item2\"" (String) + "\n " (Newline) + ListItem - (57..64): "\"item3\"" + Literal - (57..64): "\"item3\"" + "\"item3\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (66..67): "\n" + "\n" (Newline) + Entry - (67..114): "test_list2: list2[u32] = {\n 1\n 2\n 3\n}\n" + EntryKey - (67..77): "test_list2" + "test_list2" (Name) + ":" (Colon) + TypeExpr - (79..89): "list2[u32]" + "list2" (Name) + "[" (LBrack) + TypeArgList - (85..88): "u32" + TypeArg - (85..88): "u32" + "u32" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (91..113): " {\n 1\n 2\n 3\n}" + Block - (92..113): "{\n 1\n 2\n 3\n}" + "{" (LCurly) + ListItem - (93..99): "\n 1" + Literal - (98..99): "1" + "1" (Number) + "\n " (Newline) + ListItem - (104..105): "2" + Literal - (104..105): "2" + "2" (Number) + "\n " (Newline) + ListItem - (110..111): "3" + Literal - (110..111): "3" + "3" (Number) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (113..114): "\n" + "\n" (Newline) + Entry - (114..165): "test_option_some: option[string] = {\n \"value\"\n}\n" + EntryKey - (114..130): "test_option_some" + "test_option_some" (Name) + ":" (Colon) + TypeExpr - (132..146): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (139..145): "string" + TypeArg - (139..145): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (148..164): " {\n \"value\"\n}" + Block - (149..164): "{\n \"value\"\n}" + "{" (LCurly) + ListItem - (150..162): "\n \"value\"" + Literal - (155..162): "\"value\"" + "\"value\"" (String) + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (164..165): "\n" + "\n" (Newline) + Entry - (165..203): "test_option_none: option[string] = {}\n" + EntryKey - (165..181): "test_option_none" + "test_option_none" (Name) + ":" (Colon) + TypeExpr - (183..197): "option[string]" + "option" (Name) + "[" (LBrack) + TypeArgList - (190..196): "string" + TypeArg - (190..196): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (199..202): " {}" + Block - (200..202): "{}" + "{" (LCurly) + "}" (RCurly) + EntryTerminator - (202..203): "\n" + "\n" (Newline) + Entry - (203..287): "test_map: map[hash,string] = {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + EntryKey - (203..211): "test_map" + "test_map" (Name) + ":" (Colon) + TypeExpr - (213..229): "map[hash,string]" + "map" (Name) + "[" (LBrack) + TypeArgList - (217..228): "hash,string" + TypeArg - (217..221): "hash" + "hash" (Name) + "," (Comma) + TypeArg - (222..228): "string" + "string" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (231..287): " {\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + Block - (232..287): "{\n 0x12345678 = \"value1\"\n 0xdeadbeef = \"value2\"\n}" + "{" (LCurly) + Entry - (233..264): "\n 0x12345678 = \"value1\"\n " + EntryKey - (238..248): "0x12345678" + "0x12345678" (HexLit) + "=" (Eq) + EntryValue - (250..259): " \"value1\"" + Literal - (251..259): "\"value1\"" + "\"value1\"" (String) + EntryTerminator - (259..264): "\n " + "\n " (Newline) + Entry - (264..286): "0xdeadbeef = \"value2\"\n" + EntryKey - (264..274): "0xdeadbeef" + "0xdeadbeef" (HexLit) + "=" (Eq) + EntryValue - (276..285): " \"value2\"" + Literal - (277..285): "\"value2\"" + "\"value2\"" (String) + EntryTerminator - (285..286): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (287..287): "" diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-5.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-5.snap new file mode 100644 index 00000000..59932165 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot-5.snap @@ -0,0 +1,249 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: debug +--- +File - (0..459): "\ntest_bool: bool = true\ntest_i8: i8 = -128\ntest_u8: u8 = 255\ntest_i16: i16 = -32768\ntest_u16: u16 = 65535\ntest_i32: i32 = -2147483648\ntest_u32: u32 = 4294967295\ntest_f32: f32 = 3.14159\ntest_vec2: vec2 = { 1.0, 2.0 }\ntest_vec3: vec3 = { 1.0, 2.0, 3.0 }\ntest_vec4: vec4 = { 1.0, 2.0, 3.0, 4.0 }\ntest_rgba: rgba = { 255, 128, 64, 255 }\ntest_string: string = \"Hello, World!\"\ntest_hash: hash = 0xdeadbeef\ntest_link: link = \"path/to/object\"\ntest_flag: flag = false\n" + Entry - (0..24): "\ntest_bool: bool = true\n" + EntryKey - (1..10): "test_bool" + "test_bool" (Name) + ":" (Colon) + TypeExpr - (12..16): "bool" + "bool" (Name) + "=" (Eq) + EntryValue - (18..23): " true" + Literal - (19..23): "true" + "true" (True) + EntryTerminator - (23..24): "\n" + "\n" (Newline) + Entry - (24..43): "test_i8: i8 = -128\n" + EntryKey - (24..31): "test_i8" + "test_i8" (Name) + ":" (Colon) + TypeExpr - (33..35): "i8" + "i8" (Name) + "=" (Eq) + EntryValue - (37..42): " -128" + Literal - (38..42): "-128" + "-128" (Number) + EntryTerminator - (42..43): "\n" + "\n" (Newline) + Entry - (43..61): "test_u8: u8 = 255\n" + EntryKey - (43..50): "test_u8" + "test_u8" (Name) + ":" (Colon) + TypeExpr - (52..54): "u8" + "u8" (Name) + "=" (Eq) + EntryValue - (56..60): " 255" + Literal - (57..60): "255" + "255" (Number) + EntryTerminator - (60..61): "\n" + "\n" (Newline) + Entry - (61..84): "test_i16: i16 = -32768\n" + EntryKey - (61..69): "test_i16" + "test_i16" (Name) + ":" (Colon) + TypeExpr - (71..74): "i16" + "i16" (Name) + "=" (Eq) + EntryValue - (76..83): " -32768" + Literal - (77..83): "-32768" + "-32768" (Number) + EntryTerminator - (83..84): "\n" + "\n" (Newline) + Entry - (84..106): "test_u16: u16 = 65535\n" + EntryKey - (84..92): "test_u16" + "test_u16" (Name) + ":" (Colon) + TypeExpr - (94..97): "u16" + "u16" (Name) + "=" (Eq) + EntryValue - (99..105): " 65535" + Literal - (100..105): "65535" + "65535" (Number) + EntryTerminator - (105..106): "\n" + "\n" (Newline) + Entry - (106..134): "test_i32: i32 = -2147483648\n" + EntryKey - (106..114): "test_i32" + "test_i32" (Name) + ":" (Colon) + TypeExpr - (116..119): "i32" + "i32" (Name) + "=" (Eq) + EntryValue - (121..133): " -2147483648" + Literal - (122..133): "-2147483648" + "-2147483648" (Number) + EntryTerminator - (133..134): "\n" + "\n" (Newline) + Entry - (134..161): "test_u32: u32 = 4294967295\n" + EntryKey - (134..142): "test_u32" + "test_u32" (Name) + ":" (Colon) + TypeExpr - (144..147): "u32" + "u32" (Name) + "=" (Eq) + EntryValue - (149..160): " 4294967295" + Literal - (150..160): "4294967295" + "4294967295" (Number) + EntryTerminator - (160..161): "\n" + "\n" (Newline) + Entry - (161..185): "test_f32: f32 = 3.14159\n" + EntryKey - (161..169): "test_f32" + "test_f32" (Name) + ":" (Colon) + TypeExpr - (171..174): "f32" + "f32" (Name) + "=" (Eq) + EntryValue - (176..184): " 3.14159" + Literal - (177..184): "3.14159" + "3.14159" (Number) + EntryTerminator - (184..185): "\n" + "\n" (Newline) + Entry - (185..216): "test_vec2: vec2 = { 1.0, 2.0 }\n" + EntryKey - (185..194): "test_vec2" + "test_vec2" (Name) + ":" (Colon) + TypeExpr - (196..200): "vec2" + "vec2" (Name) + "=" (Eq) + EntryValue - (202..215): " { 1.0, 2.0 }" + Block - (203..215): "{ 1.0, 2.0 }" + "{" (LCurly) + ListItem - (204..208): " 1.0" + Literal - (205..208): "1.0" + "1.0" (Number) + "," (Comma) + ListItem - (209..213): " 2.0" + Literal - (210..213): "2.0" + "2.0" (Number) + "}" (RCurly) + EntryTerminator - (215..216): "\n" + "\n" (Newline) + Entry - (216..252): "test_vec3: vec3 = { 1.0, 2.0, 3.0 }\n" + EntryKey - (216..225): "test_vec3" + "test_vec3" (Name) + ":" (Colon) + TypeExpr - (227..231): "vec3" + "vec3" (Name) + "=" (Eq) + EntryValue - (233..251): " { 1.0, 2.0, 3.0 }" + Block - (234..251): "{ 1.0, 2.0, 3.0 }" + "{" (LCurly) + ListItem - (235..239): " 1.0" + Literal - (236..239): "1.0" + "1.0" (Number) + "," (Comma) + ListItem - (240..244): " 2.0" + Literal - (241..244): "2.0" + "2.0" (Number) + "," (Comma) + ListItem - (245..249): " 3.0" + Literal - (246..249): "3.0" + "3.0" (Number) + "}" (RCurly) + EntryTerminator - (251..252): "\n" + "\n" (Newline) + Entry - (252..293): "test_vec4: vec4 = { 1.0, 2.0, 3.0, 4.0 }\n" + EntryKey - (252..261): "test_vec4" + "test_vec4" (Name) + ":" (Colon) + TypeExpr - (263..267): "vec4" + "vec4" (Name) + "=" (Eq) + EntryValue - (269..292): " { 1.0, 2.0, 3.0, 4.0 }" + Block - (270..292): "{ 1.0, 2.0, 3.0, 4.0 }" + "{" (LCurly) + ListItem - (271..275): " 1.0" + Literal - (272..275): "1.0" + "1.0" (Number) + "," (Comma) + ListItem - (276..280): " 2.0" + Literal - (277..280): "2.0" + "2.0" (Number) + "," (Comma) + ListItem - (281..285): " 3.0" + Literal - (282..285): "3.0" + "3.0" (Number) + "," (Comma) + ListItem - (286..290): " 4.0" + Literal - (287..290): "4.0" + "4.0" (Number) + "}" (RCurly) + EntryTerminator - (292..293): "\n" + "\n" (Newline) + Entry - (293..333): "test_rgba: rgba = { 255, 128, 64, 255 }\n" + EntryKey - (293..302): "test_rgba" + "test_rgba" (Name) + ":" (Colon) + TypeExpr - (304..308): "rgba" + "rgba" (Name) + "=" (Eq) + EntryValue - (310..332): " { 255, 128, 64, 255 }" + Block - (311..332): "{ 255, 128, 64, 255 }" + "{" (LCurly) + ListItem - (312..316): " 255" + Literal - (313..316): "255" + "255" (Number) + "," (Comma) + ListItem - (317..321): " 128" + Literal - (318..321): "128" + "128" (Number) + "," (Comma) + ListItem - (322..325): " 64" + Literal - (323..325): "64" + "64" (Number) + "," (Comma) + ListItem - (326..330): " 255" + Literal - (327..330): "255" + "255" (Number) + "}" (RCurly) + EntryTerminator - (332..333): "\n" + "\n" (Newline) + Entry - (333..371): "test_string: string = \"Hello, World!\"\n" + EntryKey - (333..344): "test_string" + "test_string" (Name) + ":" (Colon) + TypeExpr - (346..352): "string" + "string" (Name) + "=" (Eq) + EntryValue - (354..370): " \"Hello, World!\"" + Literal - (355..370): "\"Hello, World!\"" + "\"Hello, World!\"" (String) + EntryTerminator - (370..371): "\n" + "\n" (Newline) + Entry - (371..400): "test_hash: hash = 0xdeadbeef\n" + EntryKey - (371..380): "test_hash" + "test_hash" (Name) + ":" (Colon) + TypeExpr - (382..386): "hash" + "hash" (Name) + "=" (Eq) + EntryValue - (388..399): " 0xdeadbeef" + Literal - (389..399): "0xdeadbeef" + "0xdeadbeef" (HexLit) + EntryTerminator - (399..400): "\n" + "\n" (Newline) + Entry - (400..435): "test_link: link = \"path/to/object\"\n" + EntryKey - (400..409): "test_link" + "test_link" (Name) + ":" (Colon) + TypeExpr - (411..415): "link" + "link" (Name) + "=" (Eq) + EntryValue - (417..434): " \"path/to/object\"" + Literal - (418..434): "\"path/to/object\"" + "\"path/to/object\"" (String) + EntryTerminator - (434..435): "\n" + "\n" (Newline) + Entry - (435..459): "test_flag: flag = false\n" + EntryKey - (435..444): "test_flag" + "test_flag" (Name) + ":" (Colon) + TypeExpr - (446..450): "flag" + "flag" (Name) + "=" (Eq) + EntryValue - (452..458): " false" + Literal - (453..458): "false" + "false" (False) + EntryTerminator - (458..459): "\n" + "\n" (Newline) diff --git a/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot.snap b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot.snap new file mode 100644 index 00000000..a142b440 --- /dev/null +++ b/crates/ltk_ritobin/tests/snapshots/parse_sample__tree_snapshot.snap @@ -0,0 +1,62 @@ +--- +source: crates/ltk_ritobin/tests/parse_sample.rs +expression: debug +--- +File - (0..134): "\nentries: map[hash,embed] = {\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}\n" + Entry - (0..134): "\nentries: map[hash,embed] = {\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}\n" + EntryKey - (1..8): "entries" + "entries" (Name) + ":" (Colon) + TypeExpr - (10..25): "map[hash,embed]" + "map" (Name) + "[" (LBrack) + TypeArgList - (14..24): "hash,embed" + TypeArg - (14..18): "hash" + "hash" (Name) + "," (Comma) + TypeArg - (19..24): "embed" + "embed" (Name) + "]" (RBrack) + "=" (Eq) + EntryValue - (27..133): " {\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}" + Block - (28..133): "{\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n}" + "{" (LCurly) + Entry - (29..132): "\n \"Test/Path\" = TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }\n" + EntryKey - (34..45): "\"Test/Path\"" + "\"Test/Path\"" (String) + "=" (Eq) + EntryValue - (47..131): " TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }" + Class - (48..131): "TestClass {\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }" + "TestClass" (Name) + Block - (58..131): "{\n 0xcb13aff1: f32 = -40\n normalName: string = \"test\"\n }" + "{" (LCurly) + Entry - (59..98): "\n 0xcb13aff1: f32 = -40\n " + EntryKey - (68..78): "0xcb13aff1" + "0xcb13aff1" (HexLit) + ":" (Colon) + TypeExpr - (80..83): "f32" + "f32" (Name) + "=" (Eq) + EntryValue - (85..89): " -40" + Literal - (86..89): "-40" + "-40" (Number) + EntryTerminator - (89..98): "\n " + "\n " (Newline) + Entry - (98..130): "normalName: string = \"test\"\n " + EntryKey - (98..108): "normalName" + "normalName" (Name) + ":" (Colon) + TypeExpr - (110..116): "string" + "string" (Name) + "=" (Eq) + EntryValue - (118..125): " \"test\"" + Literal - (119..125): "\"test\"" + "\"test\"" (String) + EntryTerminator - (125..130): "\n " + "\n " (Newline) + "}" (RCurly) + EntryTerminator - (131..132): "\n" + "\n" (Newline) + "}" (RCurly) + EntryTerminator - (133..134): "\n" + "\n" (Newline) diff --git a/flake.nix b/flake.nix index ba881876..ad03c07b 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,32 @@ extensions = ["rust-src"]; })); + nightly = eachSystem (pkgs: + pkgs.rust-bin.selectLatestNightlyWith (t: + t.default.override { + extensions = ["rust-docs-json"]; + })); + + cargo-expand' = eachSystem (pkgs: let + nightly' = nightly.${pkgs.system}; + in + pkgs.writeShellScriptBin "cargo-expand" '' + export RUSTC="${nightly'}/bin/rustc"; + export CARGO="${nightly'}/bin/cargo"; + exec "${pkgs.cargo-expand}/bin/cargo-expand" "$@" + ''); + + cargo-public-api' = eachSystem (pkgs: let + nightly' = nightly.${pkgs.system}; + fakeRustup = pkgs.writeShellScriptBin "rustup" ''shift 3; ${pkgs.lib.getExe' nightly' "cargo"} "$@"''; + in + pkgs.writeShellScriptBin "cargo-public-api" '' + export RUSTC="${nightly'}/bin/rustc"; + export CARGO="${nightly'}/bin/cargo"; + export PATH="${fakeRustup}/bin:${nightly'}/bin:$PATH"; + exec "${pkgs.cargo-public-api}/bin/cargo-public-api" "$@" + ''); + treefmtEval = eachSystem (pkgs: treefmt-nix.lib.evalModule pkgs ./treefmt.nix); in { # You can use crane to build the Rust application with Nix @@ -49,8 +75,8 @@ devShells = eachSystem (pkgs: { # Based on a discussion at https://github.com/oxalica/rust-overlay/issues/129 - default = pkgs.mkShell (with pkgs; { - nativeBuildInputs = [ + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ clang gdb # Use mold when we are runnning in Linux @@ -58,17 +84,18 @@ ]; buildInputs = [ rustToolchain.${pkgs.system} - rust-analyzer-unwrapped - cargo - cargo-insta - cargo-hack - cargo-expand - bacon - # pkg-config - # openssl + pkgs.rust-analyzer-unwrapped + pkgs.cargo + pkgs.cargo-insta + pkgs.cargo-hack + cargo-expand'.${pkgs.system} + cargo-public-api'.${pkgs.system} + pkgs.bacon + # pkgs.pkg-config + # pkgs.openssl ]; - RUST_SRC_PATH = rustPlatform.rustLibSrc; - }); + RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc; + }; }); formatter = eachSystem (pkgs: treefmtEval.${pkgs.system}.config.build.wrapper);