diff --git a/.vscode/settings.json b/.vscode/settings.json index 9ff86758..ca0cb7e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "rust-analyzer.cargo.allTargets": true + "rust-analyzer.cargo.allTargets": true, + "nixEnvSelector.suggestion": false } diff --git a/Cargo.toml b/Cargo.toml index 357d3341..8bd39861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ insta = { version = "1.39.0", features = ["ron"] } camino = "1.2.1" pretty_assertions = "1.4.1" +proptest = "1.5.0" criterion = {version = "0.8", features = ["html_reports"] } diff --git a/crates/ltk_meta/src/property/kind.rs b/crates/ltk_meta/src/property/kind.rs index ec00a113..2655b73f 100644 --- a/crates/ltk_meta/src/property/kind.rs +++ b/crates/ltk_meta/src/property/kind.rs @@ -85,7 +85,7 @@ impl Kind { /// Whether this property kind is a primitive type. (i8, u8, .. u32, u64, f32, Vector2, Vector3, Vector4, Matrix44, Color, String, Hash, WadChunkLink), #[inline(always)] #[must_use] - pub fn is_primitive(&self) -> bool { + pub const fn is_primitive(&self) -> bool { use Kind::*; matches!( self, @@ -123,7 +123,7 @@ impl Kind { /// shipped bin in the client keys a map on any of them. #[inline(always)] #[must_use] - pub fn is_valid_map_key(&self) -> bool { + pub const fn is_valid_map_key(&self) -> bool { use Kind::*; matches!( self, @@ -151,13 +151,13 @@ impl Kind { /// Whether this property kind is a container type (container, unordered container, optional, map). #[inline(always)] #[must_use] - pub fn is_container(&self) -> bool { + pub const fn is_container(&self) -> bool { self.subtype_count() > 0 } #[inline(always)] #[must_use] - pub fn subtype_count(&self) -> u8 { + pub const fn subtype_count(&self) -> u8 { use Kind::*; match self { Container | UnorderedContainer | Optional => 1, diff --git a/crates/ltk_ritobin/Cargo.toml b/crates/ltk_ritobin/Cargo.toml index c07e71e4..ad28dccc 100644 --- a/crates/ltk_ritobin/Cargo.toml +++ b/crates/ltk_ritobin/Cargo.toml @@ -21,6 +21,10 @@ harness = false name = "e2e" harness = false +[[bench]] +name = "ast" +harness = false + [lints] workspace = true @@ -28,6 +32,7 @@ workspace = true default = [] debug = [] serde = ["dep:serde", "ltk_meta/serde"] +salsa = ["dep:salsa"] [dependencies] nom = "7.1" @@ -46,11 +51,12 @@ ltk_hash = { version = "0.4.0", path = "../ltk_hash" } ltk_primitives = { version = "0.3.5", path = "../ltk_primitives" } serde = { workspace = true, optional = true } -salsa = "0.22.0" +salsa = { version = "0.22.0", optional = true } [dev-dependencies] insta.workspace = true pretty_assertions.workspace = true +proptest.workspace = true serde.workspace = true criterion.workspace = true ltk_ritobin = { path = ".", features = ["serde"] } diff --git a/crates/ltk_ritobin/benches/ast.rs b/crates/ltk_ritobin/benches/ast.rs new file mode 100644 index 00000000..717e122a --- /dev/null +++ b/crates/ltk_ritobin/benches/ast.rs @@ -0,0 +1,86 @@ +//! Compares the two typecheckers' phases against the same parsed input: `typecheck::walk` +//! (`Cst::build_bin`, CST -> `Bin` in one pass) vs. the new `ast` engine's two halves +//! (`Cst::build_ast`, CST -> `Ast`; and `Ast::to_bin`, `Ast` -> `Bin`) - see the crate's design +//! notes for why `ast` is a second, independent implementation rather than a shared pipeline. +//! +//! Parsing itself is excluded (see `benches/parse.rs`): the `Cst` (and, for the `ast_to_bin` +//! group, the `Ast`) is built once outside every timed closure, so each group measures only the +//! one phase it names. + +use std::fs::read_to_string; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use ltk_ritobin::Cst; + +fn criterion_benchmark(c: &mut Criterion) { + let dir = env!("CARGO_MANIFEST_DIR"); + let samples = [ + read_to_string(format!("{dir}/samples/aatrox.rito")).unwrap(), + read_to_string(format!("{dir}/samples/azirultsoldier.rito")).unwrap(), + read_to_string(format!("{dir}/samples/big.rito")).unwrap(), + read_to_string(format!("{dir}/samples/skin38.rito")).unwrap(), + read_to_string(format!("{dir}/samples/test.rito")).unwrap(), + read_to_string(format!("{dir}/samples/zaahen.rito")).unwrap(), + ]; + + { + let mut group = c.benchmark_group("cst_to_bin"); + for sample in &samples { + let size = sample.len(); + let cst = Cst::parse(sample); + + group.throughput(Throughput::Bytes(size.try_into().unwrap())); + group.bench_with_input( + BenchmarkId::from_parameter(size), + &(cst, sample), + |b, (cst, sample)| { + b.iter(|| { + let _partial = std::hint::black_box(cst.build_bin(sample)); + }) + }, + ); + } + } + + { + let mut group = c.benchmark_group("cst_to_ast"); + for sample in &samples { + let size = sample.len(); + let cst = Cst::parse(sample); + + group.throughput(Throughput::Bytes(size.try_into().unwrap())); + group.bench_with_input( + BenchmarkId::from_parameter(size), + &(cst, sample), + |b, (cst, sample)| { + b.iter(|| { + let _ast = std::hint::black_box(cst.build_ast(sample)); + }) + }, + ); + } + } + + { + let mut group = c.benchmark_group("ast_to_bin"); + for sample in &samples { + let size = sample.len(); + let cst = Cst::parse(sample); + let ast = cst.build_ast(sample); + + group.throughput(Throughput::Bytes(size.try_into().unwrap())); + group.bench_with_input( + BenchmarkId::from_parameter(size), + &(ast, sample), + |b, (ast, sample)| { + b.iter(|| { + let _bin = std::hint::black_box(ast.to_bin(sample)); + }) + }, + ); + } + } +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/crates/ltk_ritobin/benches/e2e.rs b/crates/ltk_ritobin/benches/e2e.rs index bc9097ff..1a510e7d 100644 --- a/crates/ltk_ritobin/benches/e2e.rs +++ b/crates/ltk_ritobin/benches/e2e.rs @@ -33,9 +33,9 @@ fn criterion_benchmark(c: &mut Criterion) { fn e2e(txt: &str) -> Cst { let cst = Cst::parse(txt); - let (bin, _errs) = cst.build_bin(txt); + let partial = cst.build_bin(txt); let mut str = String::new(); - bin.print_to_writer(&mut str).unwrap(); + partial.bin.print_to_writer(&mut str).unwrap(); cst } diff --git a/crates/ltk_ritobin/benches/parse.rs b/crates/ltk_ritobin/benches/parse.rs index bb81a8e1..c7eccd1b 100644 --- a/crates/ltk_ritobin/benches/parse.rs +++ b/crates/ltk_ritobin/benches/parse.rs @@ -40,7 +40,7 @@ fn criterion_benchmark(c: &mut Criterion) { &(cst, sample), |b, (cst, sample)| { b.iter(|| { - let (_bin, _errs) = black_box(cst.build_bin(sample)); + let _partial = black_box(cst.build_bin(sample)); }) }, ); diff --git a/crates/ltk_ritobin/benches/print.rs b/crates/ltk_ritobin/benches/print.rs index 31071cb0..ae75c45a 100644 --- a/crates/ltk_ritobin/benches/print.rs +++ b/crates/ltk_ritobin/benches/print.rs @@ -20,7 +20,7 @@ fn criterion_benchmark(c: &mut Criterion) { for sample in &samples { let size = sample.len(); let cst = Cst::parse(sample); - let (bin, _errs) = cst.build_bin(sample); + let bin = cst.build_bin(sample).bin; group.throughput(Throughput::Bytes(size.try_into().unwrap())); group.bench_with_input(BenchmarkId::from_parameter(size), &bin, |b, bin| { diff --git a/crates/ltk_ritobin/examples/rito_to_bin.rs b/crates/ltk_ritobin/examples/rito_to_bin.rs index 6037e402..9b9c8eea 100644 --- a/crates/ltk_ritobin/examples/rito_to_bin.rs +++ b/crates/ltk_ritobin/examples/rito_to_bin.rs @@ -23,14 +23,16 @@ fn main() { return; } - let (bin, errors) = cst.build_bin(&text); - if !errors.is_empty() { - eprintln!("Errors while converting to bin:"); - for err in errors { - eprintln!("- {err:#?}"); + let bin = match cst.build_bin(&text).into_result() { + Ok(bin) => bin, + Err(partial) => { + eprintln!("Errors while converting to bin:"); + for diag in &partial.diagnostics { + eprintln!("- {diag:#?}"); + } + return; } - return; - } + }; let mut file = File::create(output_path).unwrap(); bin.to_writer(&mut file).unwrap(); diff --git a/crates/ltk_ritobin/src/ast.rs b/crates/ltk_ritobin/src/ast.rs new file mode 100644 index 00000000..b8ee6ab8 --- /dev/null +++ b/crates/ltk_ritobin/src/ast.rs @@ -0,0 +1,44 @@ +pub mod builder; +pub mod diagnostics; +pub mod hash; +pub mod node; +pub mod query; +pub mod resolve; +pub mod visitor; + +mod to_bin; + +#[cfg(test)] +mod tests; + +pub use crate::Spanned; +pub use node::{Object, Property, RootEntry, Value}; +pub use to_bin::PartialBin; + +use crate::{ + ast::{diagnostics::DiagnosticWithSpan, node::roots::Roots}, + Cst, +}; + +#[cfg(not(feature = "salsa"))] +pub(crate) type Ptr = Box; +#[cfg(feature = "salsa")] +pub(crate) type Ptr = std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct Ast { + pub roots: Roots, + pub diagnostics: Vec, +} + +impl Ast { + pub fn root_entries(&self) -> impl Iterator { + self.roots.entries().unwrap_or_default().iter() + } +} + +impl Cst { + pub fn build_ast(&self, text: &str) -> crate::ast::Ast { + crate::ast::Ast::from_cst(self, text) + } +} diff --git a/crates/ltk_ritobin/src/ast/builder.rs b/crates/ltk_ritobin/src/ast/builder.rs new file mode 100644 index 00000000..9144b217 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/builder.rs @@ -0,0 +1,35 @@ +use crate::{ + ast::{diagnostics::DiagnosticWithSpan, Ast}, + cst::Cst, +}; + +mod root_entry; +pub use root_entry::*; + +impl Ast { + pub fn from_cst(cst: &Cst, text: &str) -> Self { + let ctx = Builder { + cst, + text, + diagnostics: Vec::new(), + }; + ctx.build() + } +} + +#[derive(Debug, Clone)] +pub(super) struct Builder<'a> { + pub cst: &'a Cst, + pub text: &'a str, + pub diagnostics: Vec, +} + +impl<'a> Builder<'a> { + pub(super) fn cst(&self) -> &'a Cst { + self.cst + } + + pub(super) fn push(&mut self, d: DiagnosticWithSpan) { + self.diagnostics.push(d); + } +} diff --git a/crates/ltk_ritobin/src/ast/builder/root_entry.rs b/crates/ltk_ritobin/src/ast/builder/root_entry.rs new file mode 100644 index 00000000..ec94978c --- /dev/null +++ b/crates/ltk_ritobin/src/ast/builder/root_entry.rs @@ -0,0 +1,242 @@ +use crate::{ + ast::{ + diagnostics::Diagnostic as D, + node::{ + root::{KnownRoot, Root, RootKind, RootValue}, + roots::Roots, + TypeExpr, + }, + RootEntry, Value, + }, + cst::Kind, + parse::Span, + Node, Spanned, SpannedExt, +}; + +use super::*; + +use ltk_meta::PropertyKind::{self}; + +#[derive(Debug, Clone)] +pub struct RawRootProperty { + pub key: Spanned, + pub type_expr: Spanned>, + pub value: Option, +} + +impl<'a> Builder<'a> { + pub(crate) fn build(mut self) -> Ast { + let root_node = self.cst.root(); + + let mut file_type = None; + let mut version = None; + let mut linked = None; + let mut entries = None; + + let mut idx = 0; + let mut roots = Roots::new(root_node.children.get(self.cst).iter().filter_map(|child| { + let node = child.tree(self.cst)?; + let root = self.resolve_root(node, idx)?; + idx += 1; + Some(root) + })); + + // we don't need to coerce here since we collected these roots via Self::resolve_entry, who + // handles coercion already + + for (idx, root) in roots.iter_mut().enumerate() { + if let Some(expected_type) = root.name.expected_type() { + match root.type_expr.value { + Some(type_expr) => { + if type_expr != expected_type { + self.push( + D::InvalidRootEntryType { + root_kind: *root.name, + key_span: root.name.span, + type_span: root.type_expr.span, + got: type_expr.into(), + expected: expected_type, + } + .unwrap(), + ); + } + } + None => { + self.push( + D::MissingEntryType { + key_span: root.name.span, + } + .unwrap(), + ); + } + } + + if let Some(RootValue::Value(value)) = root.value.as_ref() { + if let Some(got) = value.rito_type() { + if got != expected_type { + self.push( + D::TypeMismatch { + span: value.span(), + expected: expected_type.into(), + expected_span: None, + got: got.into(), + } + .unwrap(), + ); + continue; + } + } + } + } + match *root.name { + RootKind::Unknown => { + self.push( + D::MissingEntryValue { + key_span: root.name.span, + expected: root + .name + .expected_type() + .map(|t| t.with_span(root.type_expr.span)), + } + .unwrap(), + ); + } + RootKind::Version => { + if let Some(RootValue::Value(Value::U32(v))) = &root.value { + if let Some(existing) = version.replace(KnownRoot { + idx, + value: v.value, + }) { + self.push( + D::ShadowedRoot { + shadower: idx, + shadowee: existing.idx, + } + .default_span(Span::empty(0)), + ); + } + } + } + RootKind::Type => { + if let Some(v) = root + .value + .as_ref() + .and_then(|v| v.as_value()) + .and_then(|v| v.as_string()) + { + if let Some(existing) = file_type.replace(KnownRoot { + idx, + value: v.parse().unwrap(), + }) { + self.push( + D::ShadowedRoot { + shadower: idx, + shadowee: existing.idx, + } + .default_span(Span::empty(0)), + ); + } + } + } + RootKind::Linked => { + let Some(value) = &root.value else { + continue; + }; + match value { + RootValue::Value(Value::Container { items, .. }) => { + if let Some(existing) = linked.replace(KnownRoot { + idx, + value: items + .iter() + .filter_map(|v| { + v.clone() + .try_coerce_to(PropertyKind::String) + .ok() + .and_then(|v| v.into_string()) + }) + .collect(), + }) { + self.push( + D::ShadowedRoot { + shadower: idx, + shadowee: existing.idx, + } + .default_span(Span::empty(0)), + ); + } + } + _ => { + continue; + } + } + } + RootKind::Entries => match root.value.take() { + Some(RootValue::Value(Value::Map { + entries: map, span, .. + })) => { + let items = map + .into_iter() + .filter_map(|(k, v)| match (k, v) { + (Value::Hash(path_hash), Some(Value::Embedded(object))) => { + Some(RootEntry { path_hash, object }) + } + _ => None, + }) + .collect(); + root.value = Some(RootValue::Entries(Spanned::new(span, items))); + if let Some(shadowee) = entries.replace(idx) { + self.push( + D::ShadowedRoot { + shadower: idx, + shadowee, + } + .default_span(Span::empty(0)), + ); + } + } + // not a well-formed map: leave the raw value in place to navigate/diagnose + other => root.value = other, + }, + } + } + + roots.file_type = file_type; + roots.version = version; + roots.linked = linked; + roots.entries = entries; + + for kind in roots.missing() { + self.push(D::MissingRootEntry { root_kind: kind }.default_span(Span::empty(0))); + } + + Ast { + roots, + diagnostics: self.diagnostics, + } + } + + fn resolve_root(&mut self, node: &Node, idx: usize) -> Option { + match node.kind { + Kind::Comment | Kind::ErrorTree => return None, + Kind::Entry => match self.resolve_entry(node, None, None) { + Ok(entry) => { + let kind = RootKind::from_value(&entry.key); + + return Some(Root { + idx, + name: kind.with_span(entry.key.span()), + type_expr: entry.type_expr, + value: entry.value.map(RootValue::Value), + }); + } + Err(e) => { + self.push(e.fallback(node.span)); + } + }, + _ => { + self.push(D::RootNonEntry.default_span(node.span)); + } + } + None + } +} diff --git a/crates/ltk_ritobin/src/typecheck/diagnostics.rs b/crates/ltk_ritobin/src/ast/diagnostics.rs similarity index 83% rename from crates/ltk_ritobin/src/typecheck/diagnostics.rs rename to crates/ltk_ritobin/src/ast/diagnostics.rs index f0d920e9..a57dd2c2 100644 --- a/crates/ltk_ritobin/src/typecheck/diagnostics.rs +++ b/crates/ltk_ritobin/src/ast/diagnostics.rs @@ -3,40 +3,15 @@ use std::{fmt::Display, num::IntErrorKind}; use ltk_meta::PropertyKind; use crate::{ + ast::node::root::RootKind, cst, parse::{Span, TokenKind}, - ItemShape, RitoType, + ItemShape, RitoType, Spanned, }; -/// One of the four entries every ritobin file has at its root. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum RootKind { - Type, - Version, - Linked, - Entries, -} - -impl RootKind { - /// The name this root entry is written with in a ritobin file. - pub fn as_str(&self) -> &'static str { - match self { - RootKind::Type => "type", - RootKind::Version => "version", - RootKind::Linked => "linked", - RootKind::Entries => "entries", - } - } -} - -impl Display for RootKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - #[derive(Debug, Clone, Copy)] pub enum RitoTypeOrVirtual { + Unknown, RitoType(RitoType), Numeric, StructOrEmbedded, @@ -50,6 +25,15 @@ impl RitoTypeOrVirtual { } } +impl From> for RitoTypeOrVirtual { + fn from(value: Option) -> Self { + match value { + Some(value) => value.into(), + None => Self::Unknown, + } + } +} + impl From for RitoTypeOrVirtual { fn from(value: RitoType) -> Self { RitoTypeOrVirtual::RitoType(value) @@ -59,6 +43,7 @@ impl From for RitoTypeOrVirtual { impl Display for RitoTypeOrVirtual { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::Unknown => f.write_str("unknown type"), Self::RitoType(rito_type) => Display::fmt(rito_type, f), Self::Numeric => f.write_str("numeric type"), Self::StructOrEmbedded => f.write_str("struct/embedded"), @@ -134,21 +119,9 @@ pub enum Diagnostic { UnknownType(Span), MissingType(Span), - MissingRootEntry { - root_kind: RootKind, - }, - - InvalidRootEntryType { - root_kind: RootKind, - key_span: Span, - type_span: Span, - got: RitoType, - expected: RitoType, - }, - TypeMismatch { span: Span, - expected: RitoType, + expected: RitoTypeOrVirtual, expected_span: Option, got: RitoTypeOrVirtual, }, @@ -217,10 +190,35 @@ pub enum Diagnostic { /// span of the unrecognised entry's name span: Span, }, + MissingRootEntry { + root_kind: RootKind, + }, + /// An entry is missing its value + MissingEntryValue { + key_span: Span, + expected: Option>, + }, + MissingEntryType { + key_span: Span, + }, + + InvalidRootEntryType { + root_kind: RootKind, + key_span: Span, + type_span: Span, + got: RitoTypeOrVirtual, + expected: RitoType, + }, + ShadowedEntry { shadowee: Span, shadower: Span, }, + /// [`Self::ShadowedEntry`], but for a pair of root indices + ShadowedRoot { + shadowee: usize, + shadower: usize, + }, InvalidHash(Span), @@ -234,6 +232,22 @@ pub enum Diagnostic { span: Span, base_type: Span, }, + + /// A container/map/optional's declared item type is itself container-shaped + /// (list/list2/map/option) + InvalidNesting { + /// span of the offending subtype token + span: Span, + /// the container-shaped type that cannot be nested + kind: RitoType, + }, + /// A map's declared key type cannot key a map, such as `map[link,u32]` + InvalidMapKey { + /// span of the offending key subtype token + span: Span, + /// the type that cannot key a map + kind: RitoType, + }, } impl Display for Diagnostic { @@ -261,14 +275,6 @@ impl Display for Diagnostic { f.write_str("Missing type - entries are written 'name: type = value'") } - MissingRootEntry { root_kind } => write!(f, "Missing root entry '{root_kind}'"), - InvalidRootEntryType { - root_kind, - got, - expected, - .. - } => write!(f, "Root entry '{root_kind}' must be {expected}, got {got}"), - TypeMismatch { expected, got, .. } => { write!(f, "Type mismatch - expected {expected}, got {got}") } @@ -327,7 +333,27 @@ impl Display for Diagnostic { RootNonEntry => f.write_str("Top-level bin entries are written 'name: type = value'"), UnknownRoot { .. } => f.write_str("Unknown root entry"), - ShadowedEntry { .. } => f.write_str("Entry shadows a previous entry with the same key"), + MissingRootEntry { root_kind } => write!(f, "Missing root entry '{root_kind}'"), + InvalidRootEntryType { + root_kind, + got, + expected, + .. + } => write!(f, "Root entry '{root_kind}' must be {expected}, got {got}"), + MissingEntryValue { + key_span: _, + expected, + } => { + f.write_str("Entry is missing value")?; + if let Some(expected) = expected { + write!(f, " (expected {})", expected.value)?; + } + Ok(()) + } + MissingEntryType { key_span: _ } => f.write_str("Entry is missing type expression"), + ShadowedEntry { .. } | ShadowedRoot { .. } => { + f.write_str("Entry shadows a previous entry with the same key") + } InvalidHash(_) => f.write_str("Invalid hash"), @@ -335,6 +361,13 @@ impl Display for Diagnostic { write!(f, "Expected {expected} type parameters, got {got}") } UnexpectedSubtypes { .. } => f.write_str("This type does not accept type parameters"), + + InvalidNesting { kind, .. } => { + write!(f, "{kind} cannot be nested inside a container") + } + InvalidMapKey { kind, .. } => { + write!(f, "{kind} is not a valid map key type") + } } } } @@ -347,6 +380,7 @@ impl Diagnostic { | EmptyTree(_) | MissingToken(_) | RootNonEntry + | ShadowedRoot { .. } | ResolveLiteral | MissingRootEntry { .. } => None, UnknownType(span) @@ -361,12 +395,16 @@ impl Diagnostic { | QuotedPropertyName { span, .. } | MissingType(span) | TypeMismatch { span, .. } + | MissingEntryType { key_span: span, .. } + | MissingEntryValue { key_span: span, .. } | ShadowedEntry { shadower: span, .. } | InvalidHash(span) | AmbiguousNumeric(span) | ParseNumericError { span, .. } | NotEnoughItems { span, .. } | TooManyItems { span, .. } + | InvalidNesting { span, .. } + | InvalidMapKey { span, .. } | InvalidRootEntryType { key_span: span, .. } => Some(span), } } diff --git a/crates/ltk_ritobin/src/ast/hash.rs b/crates/ltk_ritobin/src/ast/hash.rs new file mode 100644 index 00000000..cbf69e03 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/hash.rs @@ -0,0 +1,89 @@ +use std::fmt::Display; + +use crate::{parse::Span, Spanned}; + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct HashedLiteral { + pub value: H, + /// What this hash was originally, before being coerced to a hash + pub originally: Spanned, +} + +/// See [`HashedLiteral::originally`] for information. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Originally { + /// A `HexLit` token (`0xdeadbeef`), its value was used directly & no coercion was needed + #[default] + HexLit, + /// A `String` token (`"hello"`), its text value was hashed + /// (i.e. with all escapes resolved & quotes excluded) + String, + /// A `Name` token (`helloWorld`), the text of that token was hashed + Name, +} + +impl HashedLiteral { + #[inline(always)] + #[must_use] + pub fn new(span: Span, originally: Originally, value: H) -> Self { + Self { + value, + originally: Spanned::new(span, originally), + } + } + + #[inline(always)] + #[must_use] + pub fn with_span(mut self, span: Span) -> Self { + self.originally.span = span; + self + } + + #[inline(always)] + #[must_use] + pub fn with_value(self, value: NewHash) -> HashedLiteral { + HashedLiteral { + value, + originally: self.originally, + } + } + + /// The span of the originating token that coerced to this hash + #[inline(always)] + #[must_use] + pub fn span(&self) -> Span { + self.originally.span + } + + /// What this hash was originally, before being coerced to a hash + #[inline(always)] + #[must_use] + pub fn original_kind(&self) -> Originally { + self.originally.value + } + + #[inline(always)] + #[must_use] + /// Whether this hash was originally a hash literal (`HexLit` token) + pub fn was_hash(&self) -> bool { + matches!(self.original_kind(), Originally::HexLit) + } + #[inline(always)] + #[must_use] + /// Whether this hash was originally a string literal, like for a property key (`String` token) + pub fn was_str(&self) -> bool { + matches!(self.original_kind(), Originally::String) + } + #[inline(always)] + #[must_use] + /// Whether this hash was originally a raw name, like for a class hash (`Name` token) + pub fn was_name(&self) -> bool { + matches!(self.original_kind(), Originally::Name) + } +} + +impl Display for HashedLiteral { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "0x{}", self.value) + } +} diff --git a/crates/ltk_ritobin/src/ast/node.rs b/crates/ltk_ritobin/src/ast/node.rs new file mode 100644 index 00000000..51e3cfa0 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node.rs @@ -0,0 +1,28 @@ +mod kind; +mod object; +mod property; +mod refs; +mod root_object; + +pub mod root; +pub mod roots; +pub mod value; + +pub use kind::*; +pub use object::*; +pub use property::*; +pub use refs::*; +pub use root_object::*; +pub use value::*; + +use crate::ast::hash::HashedLiteral; +use ltk_hash::BinHash; + +pub trait NodeExt { + #[must_use] + fn kind(&self) -> NodeKind; + + /// This node's own class, if it's an object or struct. + #[must_use] + fn class_hash(&self) -> Option>; +} diff --git a/crates/ltk_ritobin/src/ast/node/kind.rs b/crates/ltk_ritobin/src/ast/node/kind.rs new file mode 100644 index 00000000..b012a7b5 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/kind.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum NodeKind { + Root, + RootEntry, + Object, + Property, + Value, +} diff --git a/crates/ltk_ritobin/src/ast/node/object.rs b/crates/ltk_ritobin/src/ast/node/object.rs new file mode 100644 index 00000000..446d7bb2 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/object.rs @@ -0,0 +1,23 @@ +use ltk_hash::BinHash; + +use crate::{ + ast::{hash::HashedLiteral, node::Property}, + parse::Span, +}; + +#[derive(Debug, Clone)] +pub struct Object { + pub class_hash: HashedLiteral, + /// The entire `ClassName { .. }` span + pub span: Span, + pub properties: Vec, +} + +impl Object { + pub fn properties_span(&self) -> Option { + self.properties + .first() + .zip(self.properties.last()) + .map(|(l, r)| Span::new(l.span().start, r.span().end)) + } +} diff --git a/crates/ltk_ritobin/src/ast/node/property.rs b/crates/ltk_ritobin/src/ast/node/property.rs new file mode 100644 index 00000000..a8ceb70f --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/property.rs @@ -0,0 +1,72 @@ +use ltk_hash::BinHash; + +use crate::{ + ast::{diagnostics::RitoTypeOrVirtual, hash::HashedLiteral, node::Value}, + parse::Span, + RitoType, Spanned, +}; + +#[derive(Debug, Clone)] +pub struct Property { + pub name: HashedLiteral, + pub type_expr: Spanned>, + pub value: Option, +} + +impl Property { + /// Get the span of the whole property + #[inline(always)] + #[must_use] + pub fn span(&self) -> Span { + self.name.span().cover( + self.value + .as_ref() + .map(|v| v.span()) + .unwrap_or(self.type_expr.span), + ) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeExpr { + Unresolved, + Resolved(RitoType), +} + +impl TypeExpr { + pub fn as_resolved(self) -> Option { + match self { + TypeExpr::Unresolved => None, + TypeExpr::Resolved(rito_type) => Some(rito_type), + } + } +} + +impl PartialEq for TypeExpr { + fn eq(&self, other: &RitoType) -> bool { + match self { + TypeExpr::Unresolved => false, + TypeExpr::Resolved(rito_type) => rito_type.eq(other), + } + } +} +impl PartialEq for RitoType { + fn eq(&self, other: &TypeExpr) -> bool { + other == self + } +} + +impl From for TypeExpr { + fn from(value: RitoType) -> Self { + Self::Resolved(value) + } +} + +impl From for RitoTypeOrVirtual { + fn from(value: TypeExpr) -> Self { + match value { + TypeExpr::Unresolved => Self::Unknown, + TypeExpr::Resolved(rito_type) => rito_type.into(), + } + } +} diff --git a/crates/ltk_ritobin/src/ast/node/refs.rs b/crates/ltk_ritobin/src/ast/node/refs.rs new file mode 100644 index 00000000..751a28ae --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/refs.rs @@ -0,0 +1,191 @@ +use ltk_hash::BinHash; + +use crate::{ + ast::{ + hash::HashedLiteral, + node::{root::Root, NodeExt, NodeKind}, + query::{ + AstObjectDetail, AstPropertyDetail, AstRootDetail, AstRootEntryDetail, NodeDetail, + }, + Object, Property, RootEntry, Value, + }, + parse::Span, +}; + +/// A reference to a node in an [`Ast`]. +#[derive(Debug, Clone, Copy)] +pub enum NodeRef<'a> { + Root(&'a Root), + RootEntry(&'a RootEntry), + Object(&'a Object), + Property(&'a Property), + Value(&'a Value), +} + +/// A detailed reference to a node in an [`Ast`], down to the field level. +#[derive(Debug, Clone, Copy)] +pub enum SubNodeRef<'a> { + Root(&'a Root, AstRootDetail), + RootEntry(&'a RootEntry, AstRootEntryDetail), + Object(&'a Object, AstObjectDetail), + Property(&'a Property, AstPropertyDetail), + Value(&'a Value), +} + +impl<'a> NodeRef<'a> { + pub fn span(&self) -> Span { + match self { + NodeRef::Root(r) => r.span(), + NodeRef::RootEntry(o) => o.span(), + NodeRef::Object(s) => s.span, + NodeRef::Property(p) => p.span(), + NodeRef::Value(v) => v.span(), + } + } +} +impl<'a> SubNodeRef<'a> { + #[inline(always)] + #[must_use] + pub fn detail(&self) -> NodeDetail { + match self { + SubNodeRef::Root(_, d) => (*d).into(), + SubNodeRef::RootEntry(_, d) => (*d).into(), + SubNodeRef::Object(_, d) => (*d).into(), + SubNodeRef::Property(_, d) => (*d).into(), + SubNodeRef::Value(_) => NodeDetail::Value, + } + } + + #[inline(always)] + #[must_use] + pub fn trivia_from(node: &NodeRef<'a>) -> Self { + match node { + NodeRef::Root(r) => Self::Root(r, AstRootDetail::Trivia), + NodeRef::RootEntry(v) => Self::RootEntry(v, AstRootEntryDetail::Trivia), + NodeRef::Object(v) => Self::Object(v, AstObjectDetail::Trivia), + NodeRef::Property(v) => Self::Property(v, AstPropertyDetail::Trivia), + NodeRef::Value(v) => Self::Value(v), + } + } + + #[inline(always)] + #[must_use] + pub fn span(&self) -> Span { + match self { + SubNodeRef::Root(v, f) => match f { + AstRootDetail::Node | AstRootDetail::Trivia => v.span(), + AstRootDetail::Name => v.name.span, + AstRootDetail::TypeExpr => v.type_expr.span, + }, + SubNodeRef::RootEntry(v, f) => match f { + AstRootEntryDetail::Node | AstRootEntryDetail::Trivia => v.span(), + AstRootEntryDetail::PathHash => v.path_hash.span(), + }, + SubNodeRef::Object(v, f) => match f { + AstObjectDetail::Node | AstObjectDetail::Trivia => v.span, + AstObjectDetail::ClassHash => v.class_hash.span(), + }, + SubNodeRef::Property(v, f) => match f { + AstPropertyDetail::Node | AstPropertyDetail::Trivia => v.span(), + AstPropertyDetail::Name => v.name.span(), + AstPropertyDetail::TypeExpr => v.type_expr.span, + }, + SubNodeRef::Value(v) => v.span(), + } + } +} + +impl NodeExt for NodeRef<'_> { + fn kind(&self) -> NodeKind { + match self { + NodeRef::Root(..) => NodeKind::Root, + NodeRef::RootEntry(_) => NodeKind::RootEntry, + NodeRef::Object(_) => NodeKind::Object, + NodeRef::Property(_) => NodeKind::Property, + NodeRef::Value(_) => NodeKind::Value, + } + } + + fn class_hash(&self) -> Option> { + match self { + NodeRef::RootEntry(o) => Some(o.object.class_hash), + NodeRef::Object(s) => Some(s.class_hash), + NodeRef::Property(_) | NodeRef::Value(_) | NodeRef::Root(..) => None, + } + } +} + +impl NodeExt for SubNodeRef<'_> { + #[inline(always)] + fn kind(&self) -> NodeKind { + match self { + SubNodeRef::Root(..) => NodeKind::Root, + SubNodeRef::RootEntry(_, _) => NodeKind::RootEntry, + SubNodeRef::Object(_, _) => NodeKind::Object, + SubNodeRef::Property(_, _) => NodeKind::Property, + SubNodeRef::Value(_) => NodeKind::Value, + } + } + + #[inline(always)] + fn class_hash(&self) -> Option> { + match self { + Self::RootEntry(o, _) => Some(o.object.class_hash), + Self::Object(s, _) => Some(s.class_hash), + Self::Property(_, _) | Self::Value(_) | Self::Root(..) => None, + } + } +} + +impl<'a> From<&'a RootEntry> for NodeRef<'a> { + fn from(value: &'a RootEntry) -> Self { + Self::RootEntry(value) + } +} +impl<'a> From<&'a Object> for NodeRef<'a> { + fn from(value: &'a Object) -> Self { + Self::Object(value) + } +} +impl<'a> From<&'a Property> for NodeRef<'a> { + fn from(value: &'a Property) -> Self { + Self::Property(value) + } +} +impl<'a> From<&'a Value> for NodeRef<'a> { + fn from(value: &'a Value) -> Self { + Self::Value(value) + } +} + +impl<'a> From<&'a RootEntry> for SubNodeRef<'a> { + fn from(value: &'a RootEntry) -> Self { + Self::RootEntry(value, AstRootEntryDetail::Node) + } +} +impl<'a> From<&'a Object> for SubNodeRef<'a> { + fn from(value: &'a Object) -> Self { + Self::Object(value, AstObjectDetail::Node) + } +} +impl<'a> From<&'a Property> for SubNodeRef<'a> { + fn from(value: &'a Property) -> Self { + Self::Property(value, AstPropertyDetail::Node) + } +} +impl<'a> From<&'a Value> for SubNodeRef<'a> { + fn from(value: &'a Value) -> Self { + Self::Value(value) + } +} +impl<'a> From> for SubNodeRef<'a> { + fn from(value: NodeRef<'a>) -> Self { + match value { + NodeRef::Root(r) => Self::Root(r, AstRootDetail::Node), + NodeRef::RootEntry(v) => v.into(), + NodeRef::Object(v) => v.into(), + NodeRef::Property(v) => v.into(), + NodeRef::Value(v) => v.into(), + } + } +} diff --git a/crates/ltk_ritobin/src/ast/node/root.rs b/crates/ltk_ritobin/src/ast/node/root.rs new file mode 100644 index 00000000..be39a685 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/root.rs @@ -0,0 +1,96 @@ +use std::{convert::Infallible, str::FromStr}; + +use crate::{ + ast::{ + node::{roots::Roots, TypeExpr}, + RootEntry, Value, + }, + parse::Span, + Spanned, +}; + +mod kind; +pub use kind::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FileKind { + Prop, + Patch, + Unknown, +} + +impl FromStr for FileKind { + type Err = Infallible; + + fn from_str(s: &str) -> Result { + Ok(match s { + "PROP" => Self::Prop, + "PTCH" => Self::Patch, + _ => Self::Unknown, + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct KnownRoot { + pub(crate) idx: usize, + pub value: V, +} + +impl KnownRoot { + /// Get the original root that this information was derived from + pub fn original<'a>(&self, roots: &'a Roots) -> &'a Root { + &roots.all[self.idx] + } + + pub fn into_inner(self) -> V { + self.value + } +} + +#[derive(Debug, Clone)] +/// A root is a special-cased property (`key: type = value`), that exists at the top level of a +/// ritobin file. +pub struct Root { + pub idx: usize, + pub name: Spanned, + pub type_expr: Spanned>, + pub value: Option, +} + +impl Root { + /// Get the span of the whole root property + #[inline(always)] + #[must_use] + pub fn span(&self) -> Span { + self.name.span.cover( + self.value + .as_ref() + .map(RootValue::span) + .unwrap_or(self.type_expr.span), + ) + } +} + +#[derive(Debug, Clone)] +pub enum RootValue { + Value(Value), + /// The resolved entries of the `entries` root. Stores extra span to preserve the original `Value::Map` span + Entries(Spanned>), +} + +impl RootValue { + pub fn as_value(&self) -> Option<&Value> { + match self { + RootValue::Value(v) => Some(v), + _ => None, + } + } + + pub fn span(&self) -> Span { + match self { + RootValue::Value(v) => v.span(), + RootValue::Entries(e) => e.span, + } + } +} diff --git a/crates/ltk_ritobin/src/ast/node/root/kind.rs b/crates/ltk_ritobin/src/ast/node/root/kind.rs new file mode 100644 index 00000000..7cc307a7 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/root/kind.rs @@ -0,0 +1,69 @@ +use std::{fmt, str::FromStr}; + +use crate::{ast::Value, rito, RitoType}; + +/// One of the four entries every ritobin file has at its root, or [`Self::Unknown`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum RootKind { + #[default] + Unknown, + Version, + Type, + Linked, + Entries, +} + +impl RootKind { + /// The key this root entry uses in a ritobin file. + /// [`Self::Unknown`] is not an actual root entry, + /// but is written as `"unknown"` in this method. + pub fn as_str(&self) -> &'static str { + match self { + Self::Type => "type", + Self::Version => "version", + Self::Linked => "linked", + Self::Entries => "entries", + Self::Unknown => "unknown", + } + } + + /// What type this kind of root expects + pub fn expected_type(&self) -> Option { + Some(match self { + RootKind::Unknown => return None, + RootKind::Version => rito!(U32), + RootKind::Type => rito!(String), + RootKind::Linked => rito!(Container[String]), + RootKind::Entries => rito!(Map[Hash, Embedded]), + }) + } + + pub fn from_value(value: &Value) -> Self { + let Value::String(string) = value else { + return Self::Unknown; + }; + + let value = string.value.as_str(); + value.parse().unwrap_or(Self::Unknown) + } +} + +impl fmt::Display for RootKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for RootKind { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "type" => Self::Type, + "version" => Self::Version, + "linked" => Self::Linked, + "entries" => Self::Entries, + _ => return Err(()), + }) + } +} diff --git a/crates/ltk_ritobin/src/ast/node/root_object.rs b/crates/ltk_ritobin/src/ast/node/root_object.rs new file mode 100644 index 00000000..72b73143 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/root_object.rs @@ -0,0 +1,20 @@ +use ltk_hash::BinHash; + +use crate::{ + ast::{hash::HashedLiteral, node::Object}, + parse::Span, +}; + +#[derive(Debug, Clone)] +pub struct RootEntry { + pub path_hash: HashedLiteral, + pub object: Object, +} + +impl RootEntry { + #[inline(always)] + #[must_use] + pub fn span(&self) -> Span { + Span::new(self.path_hash.span().start, self.object.span.end) + } +} diff --git a/crates/ltk_ritobin/src/ast/node/roots.rs b/crates/ltk_ritobin/src/ast/node/roots.rs new file mode 100644 index 00000000..38be56ab --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/roots.rs @@ -0,0 +1,86 @@ +use crate::ast::{ + node::root::{FileKind, KnownRoot, Root, RootKind, RootValue}, + RootEntry, +}; + +pub type VersionRoot = KnownRoot; +pub type FileTypeRoot = KnownRoot; +pub type LinkedRoot = KnownRoot>; + +#[derive(Debug, Clone, Default)] +pub struct Roots { + pub(crate) file_type: Option, + pub(crate) version: Option, + pub(crate) linked: Option, + pub(crate) entries: Option, + + /// Ordered list of all top level roots + pub all: Vec, +} + +impl Roots { + pub fn file_type(&self) -> Option> { + self.file_type + } + + pub fn version(&self) -> Option> { + self.version + } + + pub fn linked(&self) -> Option<&KnownRoot>> { + self.linked.as_ref() + } + + pub fn new(roots: impl IntoIterator) -> Self { + Self { + all: roots.into_iter().collect(), + ..Default::default() + } + } + + pub fn iter(&self) -> impl Iterator { + self.all.iter() + } + pub fn iter_mut(&mut self) -> impl Iterator { + self.all.iter_mut() + } + + /// The resolved entries of the `entries` root, if present and well-formed. + pub fn entries(&self) -> Option<&[RootEntry]> { + match &self.all[self.entries?].value { + Some(RootValue::Entries(e)) => Some(e.as_slice()), + _ => None, + } + } + + pub fn contains(&self, kind: RootKind) -> bool { + match kind { + RootKind::Unknown => false, + RootKind::Version => self.version.is_some(), + RootKind::Type => self.file_type.is_some(), + RootKind::Linked => self.linked.is_some(), + RootKind::Entries => self.entries.is_some(), + } + } + + pub fn missing(&self) -> impl Iterator + use<'_> { + [ + RootKind::Version, + RootKind::Type, + RootKind::Linked, + RootKind::Entries, + ] + .into_iter() + .filter(|k| !self.contains(*k)) + } +} + +impl<'a> IntoIterator for &'a Roots { + type Item = &'a Root; + + type IntoIter = core::slice::Iter<'a, Root>; + + fn into_iter(self) -> Self::IntoIter { + self.all.iter() + } +} diff --git a/crates/ltk_ritobin/src/ast/node/value.rs b/crates/ltk_ritobin/src/ast/node/value.rs new file mode 100644 index 00000000..dc7ecce8 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/value.rs @@ -0,0 +1,339 @@ +use std::fmt::Display; + +use ltk_hash::{BinHash, WadHash}; +use ltk_meta::{property::values, traits::PropertyExt as _, PropertyKind, PropertyValueEnum}; +use ltk_primitives::Color; + +mod coerce; +pub use coerce::CanCoerce; + +use crate::{ + ast::{hash::HashedLiteral, Object}, + parse::Span, + RitoType, RitobinName, Spanned, +}; + +#[derive(Debug, Clone)] +pub enum Value { + Unresolved { + span: Span, + kind: PropertyKind, + }, + Unknown(Span), + //--------------------- + None(Span), + Bool(Spanned), + BitBool(Spanned), + I8(values::I8), + U8(values::U8), + I16(values::I16), + U16(values::U16), + I32(values::I32), + U32(values::U32), + I64(values::I64), + U64(values::U64), + F32(values::F32), + Vector2(values::Vector2), + Vector3(values::Vector3), + Vector4(values::Vector4), + Matrix44(values::Matrix44), + Color(Spanned>), + String(Spanned), // TODO: intern this string when no escapes needed + Hash(HashedLiteral), + WadChunkLink(HashedLiteral), + ObjectLink(HashedLiteral), + //--------------------- + Struct(Object), + Embedded(Object), + Container { + item_kind: PropertyKind, + items: Vec, + span: Span, + }, + UnorderedContainer { + item_kind: PropertyKind, + items: Vec, + span: Span, + }, + Map { + key_kind: PropertyKind, + value_kind: PropertyKind, + entries: Vec<(Value, Option)>, + span: Span, + }, + Optional { + item_kind: Option, + value: Option>, + span: Span, + }, +} + +impl Value { + #[inline(always)] + #[must_use] + /// Whether the value is container-like - (unordered) container, map, optional + pub fn is_containerlike(&self) -> bool { + matches!( + self, + Value::Container { .. } + | Value::UnorderedContainer { .. } + | Value::Map { .. } + | Value::Optional { .. } + ) + } + + pub fn as_string(&self) -> Option<&String> { + match self { + Self::String(s) => Some(s), + _ => None, + } + } + pub fn into_string(self) -> Option { + match self { + Self::String(s) => Some(s.value), + _ => None, + } + } +} + +impl Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Unresolved { kind, .. } => write!(f, "unresolved {}", kind.to_rito_name()), + Value::Unknown(_) => f.write_str("unknown"), + Value::None(_) => f.write_str("null"), + Value::Bool(v) => v.fmt(f), + Value::BitBool(v) => v.fmt(f), + Value::I8(v) => v.fmt(f), + Value::U8(v) => v.fmt(f), + Value::I16(v) => v.fmt(f), + Value::U16(v) => v.fmt(f), + Value::I32(v) => v.fmt(f), + Value::U32(v) => v.fmt(f), + Value::I64(v) => v.fmt(f), + Value::U64(v) => v.fmt(f), + Value::F32(v) => v.fmt(f), + Value::Vector2(v) => v.fmt(f), + Value::Vector3(v) => v.fmt(f), + Value::Vector4(v) => v.fmt(f), + Value::Matrix44(v) => v.fmt(f), + Value::Color(v) => write!(f, "r: {}, g: {}, b: {}, a: {}", v.r, v.g, v.b, v.a), + Value::String(v) => v.fmt(f), + Value::Hash(v) => v.fmt(f), + Value::WadChunkLink(v) => v.fmt(f), + Value::ObjectLink(v) => v.fmt(f), + Value::Struct(_) => f.write_str("{ ... }"), + Value::Embedded(_) => f.write_str("{ ... }"), + Value::Container { items, .. } | Value::UnorderedContainer { items, .. } => { + f.write_str("[")?; + let len = items.len(); + for (i, item) in items.iter().enumerate() { + item.fmt(f)?; + if i + 1 < len { + f.write_str(", ")?; + } + } + f.write_str("]")?; + Ok(()) + } + Value::Map { .. } => f.write_str("{ ... }"), + Value::Optional { value, .. } => match value { + Some(v) => v.fmt(f), + None => f.write_str("{}"), + }, + } + } +} + +impl Value { + pub fn default_for(kind: RitoType, span: Span) -> Value { + use PropertyKind as K; + match kind.base { + K::Map => Value::Map { + key_kind: kind.subtype(0), + value_kind: kind.subtype(1), + entries: Vec::new(), + span, + }, + K::Container => Value::Container { + item_kind: kind.subtype(0), + items: Vec::new(), + span, + }, + K::UnorderedContainer => Value::UnorderedContainer { + item_kind: kind.subtype(0), + items: Vec::new(), + span, + }, + K::Optional => Value::Optional { + item_kind: kind.subtypes[0], + value: None, + span, + }, + K::Struct => Value::Struct(Object { + class_hash: HashedLiteral::default().with_span(Span::new(span.start, span.start)), + span, + properties: Vec::new(), + }), + K::Embedded => Value::Embedded(Object { + class_hash: HashedLiteral::default().with_span(Span::new(span.start, span.start)), + span, + properties: Vec::new(), + }), + K::Hash => Value::Hash(HashedLiteral::default().with_span(span)), + K::WadChunkLink => Value::WadChunkLink(HashedLiteral::default().with_span(span)), + K::ObjectLink => Value::ObjectLink(HashedLiteral::default().with_span(span)), + + other => Value::try_from({ + let mut v = other.default_value::(); + *v.meta_mut() = span; + v + }).unwrap(/* Safety: all arms that error in try_from should be handled by previous arms in this match. */), + } + } +} + +impl TryFrom> for Value { + type Error = (); + fn try_from(value: PropertyValueEnum) -> Result { + Ok(match value { + PropertyValueEnum::None(values::None { meta }) => Value::None(meta), + PropertyValueEnum::Bool(values::Bool { value, meta }) => { + Value::Bool(Spanned::new(meta, value)) + } + PropertyValueEnum::BitBool(values::BitBool { value, meta }) => { + Value::BitBool(Spanned::new(meta, value)) + } + PropertyValueEnum::I8(v) => Value::I8(v), + PropertyValueEnum::U8(v) => Value::U8(v), + PropertyValueEnum::I16(v) => Value::I16(v), + PropertyValueEnum::U16(v) => Value::U16(v), + PropertyValueEnum::I32(v) => Value::I32(v), + PropertyValueEnum::U32(v) => Value::U32(v), + PropertyValueEnum::I64(v) => Value::I64(v), + PropertyValueEnum::U64(v) => Value::U64(v), + PropertyValueEnum::F32(v) => Value::F32(v), + PropertyValueEnum::Vector2(v) => Value::Vector2(v), + PropertyValueEnum::Vector3(v) => Value::Vector3(v), + PropertyValueEnum::Vector4(v) => Value::Vector4(v), + PropertyValueEnum::Matrix44(v) => Value::Matrix44(v), + PropertyValueEnum::Color(values::Color { value, meta }) => { + Value::Color(Spanned::new(meta, value)) + } + PropertyValueEnum::String(values::String { meta, value }) => { + Value::String(Spanned::new(meta, value)) + } + _ => return Err(()), + }) + } +} + +impl Value { + /// `None` when we are [`Value::Unresolved`]. + pub fn kind(&self) -> Option { + use PropertyKind as K; + Some(match self { + Value::Unresolved { kind, .. } => *kind, + Value::Unknown(_) => return None, + Value::None(_) => K::None, + Value::Bool(_) => K::Bool, + Value::BitBool(_) => K::BitBool, + Value::I8(_) => K::I8, + Value::U8(_) => K::U8, + Value::I16(_) => K::I16, + Value::U16(_) => K::U16, + Value::I32(_) => K::I32, + Value::U32(_) => K::U32, + Value::I64(_) => K::I64, + Value::U64(_) => K::U64, + Value::F32(_) => K::F32, + Value::Vector2(_) => K::Vector2, + Value::Vector3(_) => K::Vector3, + Value::Vector4(_) => K::Vector4, + Value::Matrix44(_) => K::Matrix44, + Value::Color(_) => K::Color, + Value::String(_) => K::String, + Value::Hash(_) => K::Hash, + Value::WadChunkLink(_) => K::WadChunkLink, + Value::ObjectLink(_) => K::ObjectLink, + Value::Struct(_) => K::Struct, + Value::Embedded(_) => K::Embedded, + Value::Container { .. } => K::Container, + Value::UnorderedContainer { .. } => K::UnorderedContainer, + Value::Map { .. } => K::Map, + Value::Optional { .. } => K::Optional, + }) + } + + pub fn span(&self) -> Span { + match self { + Value::Unresolved { span, .. } => *span, + Value::Unknown(span) => *span, + Value::None(v) => *v, + Value::Bool(v) => v.span, + Value::BitBool(v) => v.span, + Value::I8(v) => v.meta, + Value::U8(v) => v.meta, + Value::I16(v) => v.meta, + Value::U16(v) => v.meta, + Value::I32(v) => v.meta, + Value::U32(v) => v.meta, + Value::I64(v) => v.meta, + Value::U64(v) => v.meta, + Value::F32(v) => v.meta, + Value::Vector2(v) => v.meta, + Value::Vector3(v) => v.meta, + Value::Vector4(v) => v.meta, + Value::Matrix44(v) => v.meta, + Value::Color(v) => v.span, + Value::String(v) => v.span, + Value::Hash(v) => v.span(), + Value::WadChunkLink(v) => v.span(), + Value::ObjectLink(v) => v.span(), + Value::Struct(s) | Value::Embedded(s) => s.span, + Value::Container { span, .. } + | Value::UnorderedContainer { span, .. } + | Value::Map { span, .. } + | Value::Optional { span, .. } => *span, + } + } + + pub fn rito_type(&self) -> Option { + Some(match self { + Value::Container { item_kind, .. } | Value::UnorderedContainer { item_kind, .. } => { + RitoType { + base: self.kind()?, + subtypes: [Some(*item_kind), None], + } + } + Value::Map { + key_kind, + value_kind, + .. + } => RitoType { + base: self.kind()?, + subtypes: [Some(*key_kind), Some(*value_kind)], + }, + Value::Optional { item_kind, .. } => RitoType { + base: self.kind()?, + subtypes: [*item_kind, None], + }, + _ => RitoType::simple(self.kind()?), + }) + } +} + +impl Value { + pub fn bool(span: Span, value: bool) -> Self { + Self::Bool(Spanned::new(span, value)) + } + pub fn bitbool(span: Span, value: bool) -> Self { + Self::BitBool(Spanned::new(span, value)) + } +} + +impl From> for Value { + fn from(values::String { value, meta }: values::String) -> Self { + Self::String(Spanned::new(meta, value)) + } +} diff --git a/crates/ltk_ritobin/src/ast/node/value/coerce.rs b/crates/ltk_ritobin/src/ast/node/value/coerce.rs new file mode 100644 index 00000000..e9fb9ca5 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/node/value/coerce.rs @@ -0,0 +1,109 @@ +use ltk_hash::{BinHash, Hash as _, WadHash}; +use ltk_meta::PropertyKind; + +use crate::{ + ast::{node::Value, Ptr}, + RitoType, +}; + +use crate::ast::hash::{HashedLiteral, Originally}; + +pub trait CanCoerce { + fn can_coerce(self, from: Self) -> bool; +} + +impl CanCoerce for PropertyKind { + fn can_coerce(self, from: Self) -> bool { + let to = self; + if to == from { + return true; + } + use PropertyKind as K; + match (to, from) { + (K::Optional, from) if !from.is_container() => true, + (K::Hash, K::String) + | (K::WadChunkLink | K::ObjectLink, K::Hash | K::String) + | (K::BitBool | K::Bool, K::Bool | K::BitBool) => true, + _ => false, + } + } +} +impl CanCoerce for RitoType { + fn can_coerce(self, from: Self) -> bool { + if !self.base.can_coerce(from.base) { + return false; + } + for i in 0..1 { + if (self.subtypes[i].zip(from.subtypes[i])) + .is_some_and(|(to, from)| !to.can_coerce(from)) + { + return false; + } + } + true + } +} + +impl Value { + pub fn coerce_to(self, to: PropertyKind) -> Self { + match self.try_coerce_to(to) { + Ok(v) => v, + Err(v) => v, + } + } + + pub fn try_coerce_to(self, to: PropertyKind) -> Result { + Ok(match to { + to if self.kind().is_some_and(|k| k == to) => self, + // Unknown values can be coerced into any value (as an unresolved variant) + to if self.kind().is_none() => Self::Unresolved { + span: self.span(), + kind: to, + }, + + PropertyKind::Optional => Self::Optional { + item_kind: self.kind(), + span: self.span(), + value: Some(Ptr::new(self)), + }, + + PropertyKind::Hash => match self { + Self::String(str) => Self::Hash(HashedLiteral::new( + str.span, + Originally::String, + BinHash::hash_str(&str), + )), + _ => return Err(self), + }, + PropertyKind::ObjectLink => match self { + Self::Hash(hash) => Self::ObjectLink(hash), + Self::String(str) => Self::ObjectLink(HashedLiteral::new( + str.span, + Originally::String, + BinHash::hash_str(&str), + )), + _ => return Err(self), + }, + PropertyKind::WadChunkLink => match self { + Self::Hash(hash) => { + Self::WadChunkLink(hash.with_value(WadHash((*hash.value).into()))) + } + Self::String(str) => Self::WadChunkLink(HashedLiteral::new( + str.span, + Originally::String, + WadHash::hash_str(str.as_str()), + )), + _ => return Err(self), + }, + PropertyKind::BitBool => match self { + Self::Bool(bool) => Self::BitBool(bool), + _ => return Err(self), + }, + PropertyKind::Bool => match self { + Self::BitBool(bool) => Self::Bool(bool), + _ => return Err(self), + }, + _ => return Err(self), + }) + } +} diff --git a/crates/ltk_ritobin/src/ast/query.rs b/crates/ltk_ritobin/src/ast/query.rs new file mode 100644 index 00000000..c0f3f41f --- /dev/null +++ b/crates/ltk_ritobin/src/ast/query.rs @@ -0,0 +1,7 @@ +mod children; +mod detail; +mod nodes; + +pub mod path; + +pub use detail::*; diff --git a/crates/ltk_ritobin/src/ast/query/children.rs b/crates/ltk_ritobin/src/ast/query/children.rs new file mode 100644 index 00000000..fe4649fb --- /dev/null +++ b/crates/ltk_ritobin/src/ast/query/children.rs @@ -0,0 +1,107 @@ +use std::iter::{empty, once}; + +use crate::ast::{ + node::{ + root::{Root, RootValue}, + NodeRef, SubNodeRef, + }, + Object, Property, RootEntry, Value, +}; + +use super::*; + +impl<'a> NodeRef<'a> { + pub fn children(&self) -> Box> + 'a> { + match self { + NodeRef::Root(r) => r.children(), + NodeRef::RootEntry(o) => Box::new(std::iter::once(NodeRef::Object(&o.object))), + NodeRef::Object(s) => Box::new(s.properties.iter().map(NodeRef::Property)), + NodeRef::Property(p) => Box::new(p.value.as_ref().map(NodeRef::Value).into_iter()), + NodeRef::Value(v) => v.children(), + } + } +} + +impl Root { + pub fn children<'a>(&'a self) -> Box> + 'a> { + match &self.value { + Some(RootValue::Entries(e)) => Box::new(e.iter().map(NodeRef::RootEntry)), + Some(RootValue::Value(value)) => Box::new(once(NodeRef::Value(value))), + // roots w/ simple values aren't worth a dedicated node at this resolution + None => Box::new(empty()), + } + } + pub fn detailed_children<'a>(&'a self) -> impl Iterator> { + [ + SubNodeRef::Root(self, AstRootDetail::Name), + SubNodeRef::Root(self, AstRootDetail::TypeExpr), + ] + .into_iter() + .chain(self.children().map(SubNodeRef::from)) + .chain(once(SubNodeRef::Root(self, AstRootDetail::Trivia))) + } +} + +impl RootEntry { + pub fn children<'a>(&'a self) -> impl Iterator> { + once(NodeRef::Object(&self.object)) + } + pub fn detailed_children<'a>(&'a self) -> impl Iterator> { + [ + SubNodeRef::RootEntry(self, AstRootEntryDetail::PathHash), + SubNodeRef::Object(&self.object, AstObjectDetail::Node), + SubNodeRef::RootEntry(self, AstRootEntryDetail::Trivia), + ] + .into_iter() + } +} + +impl Object { + pub fn children<'a>(&'a self) -> impl Iterator> { + self.properties.iter().map(NodeRef::Property) + } + pub fn detailed_children<'a>(&'a self) -> impl Iterator> { + once(SubNodeRef::Object(self, AstObjectDetail::ClassHash)) + .chain( + self.properties + .iter() + .map(|v| SubNodeRef::Property(v, AstPropertyDetail::Node)), + ) + .chain(once(SubNodeRef::Object(self, AstObjectDetail::Trivia))) + } +} + +impl Property { + pub fn children<'a>(&'a self) -> impl Iterator> { + self.value.as_ref().map(NodeRef::Value).into_iter() + } + pub fn detailed_children<'a>(&'a self) -> impl Iterator> { + [ + SubNodeRef::Property(self, AstPropertyDetail::Name), + SubNodeRef::Property(self, AstPropertyDetail::TypeExpr), + ] + .into_iter() + .chain(self.value.as_ref().map(SubNodeRef::Value)) + .chain(once(SubNodeRef::Property(self, AstPropertyDetail::Trivia))) + } +} + +impl Value { + pub fn children(&self) -> Box> + '_> { + match self { + Value::Struct(s) | Value::Embedded(s) => Box::new(std::iter::once(NodeRef::Object(s))), + Value::Container { items, .. } | Value::UnorderedContainer { items, .. } => { + Box::new(items.iter().map(NodeRef::Value)) + } + Value::Map { entries, .. } => { + Box::new(entries.iter().flat_map(|(k, v)| { + once(NodeRef::Value(k)).chain(v.as_ref().map(NodeRef::Value)) + })) + } + Value::Optional { + value: Some(inner), .. + } => Box::new(std::iter::once(NodeRef::Value(inner))), + _ => Box::new(std::iter::empty()), + } + } +} diff --git a/crates/ltk_ritobin/src/ast/query/detail.rs b/crates/ltk_ritobin/src/ast/query/detail.rs new file mode 100644 index 00000000..2128fe03 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/query/detail.rs @@ -0,0 +1,90 @@ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum AstRootDetail { + Node, + Name, + TypeExpr, + Trivia, +} +impl AstRootDetail { + pub fn is_node(&self) -> bool { + matches!(self, Self::Node) + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum AstRootEntryDetail { + Node, + PathHash, + Trivia, +} +impl AstRootEntryDetail { + pub fn is_node(&self) -> bool { + matches!(self, Self::Node) + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum AstObjectDetail { + Node, + ClassHash, + Trivia, +} +impl AstObjectDetail { + pub fn is_node(&self) -> bool { + matches!(self, Self::Node) + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum AstPropertyDetail { + Node, + Name, + TypeExpr, + Trivia, +} +impl AstPropertyDetail { + pub fn is_node(&self) -> bool { + matches!(self, Self::Node) + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum NodeDetail { + Root(AstRootDetail), + Object(AstRootEntryDetail), + Struct(AstObjectDetail), + Property(AstPropertyDetail), + Value, +} +impl NodeDetail { + pub fn is_node(&self) -> bool { + match self { + NodeDetail::Root(d) => d.is_node(), + NodeDetail::Object(d) => d.is_node(), + NodeDetail::Struct(d) => d.is_node(), + NodeDetail::Property(d) => d.is_node(), + NodeDetail::Value => true, + } + } +} + +impl From for NodeDetail { + fn from(value: AstRootDetail) -> Self { + Self::Root(value) + } +} +impl From for NodeDetail { + fn from(value: AstRootEntryDetail) -> Self { + Self::Object(value) + } +} +impl From for NodeDetail { + fn from(value: AstObjectDetail) -> Self { + Self::Struct(value) + } +} +impl From for NodeDetail { + fn from(value: AstPropertyDetail) -> Self { + Self::Property(value) + } +} diff --git a/crates/ltk_ritobin/src/ast/query/nodes.rs b/crates/ltk_ritobin/src/ast/query/nodes.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/query/nodes.rs @@ -0,0 +1 @@ + diff --git a/crates/ltk_ritobin/src/ast/query/path.rs b/crates/ltk_ritobin/src/ast/query/path.rs new file mode 100644 index 00000000..fb21a3ea --- /dev/null +++ b/crates/ltk_ritobin/src/ast/query/path.rs @@ -0,0 +1,2 @@ +mod impls; +pub mod iter; diff --git a/crates/ltk_ritobin/src/ast/query/path/impls.rs b/crates/ltk_ritobin/src/ast/query/path/impls.rs new file mode 100644 index 00000000..977eb44e --- /dev/null +++ b/crates/ltk_ritobin/src/ast/query/path/impls.rs @@ -0,0 +1,31 @@ +use super::iter::*; +use crate::ast::{ + node::{NodeRef, SubNodeRef}, + Ast, +}; + +impl Ast { + /// The chain of nodes on the way to `offset`, outermost first. + pub fn coarse_path_to(&self, offset: u32) -> AstPathIter<'_> { + AstPathIter::from_ast(self, offset) + } + /// The chain of nodes on the way to `offset`, outermost first + pub fn fine_path_to(&self, offset: u32) -> AstFinePathIter<'_> { + AstFinePathIter::from_ast(self, offset) + } + + /// The most specific node containing `offset`. See [`Self::path_to`] if you need the full path. + pub fn coarse_find_node(&self, offset: u32) -> Option> { + self.coarse_path_to(offset).last() + } + pub fn fine_find_node(&self, offset: u32) -> Option> { + self.fine_path_to(offset).last() + } +} + +// impl<'a> NodeRef<'a> { +// /// The chain of nodes on the way to `offset`, including this node. +// pub fn path_to(&self, offset: u32) -> AstPathIter<'a> { +// AstPathIter::from_node(*self, offset) +// } +// } diff --git a/crates/ltk_ritobin/src/ast/query/path/iter.rs b/crates/ltk_ritobin/src/ast/query/path/iter.rs new file mode 100644 index 00000000..bd308047 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/query/path/iter.rs @@ -0,0 +1,102 @@ +use crate::ast::{ + node::{NodeRef, SubNodeRef}, + Ast, +}; + +/// Iterator of every [`Node`] on the way to a given offset, from the top level. +/// +/// Use [`Ast::coarse_path_to`] to construct this iterator. +#[derive(Clone)] +pub struct AstPathIter<'a> { + next: Option>, + offset: u32, +} + +impl<'a> AstPathIter<'a> { + pub(crate) fn from_ast(ast: &'a Ast, offset: u32) -> Self { + Self { + next: ast + .root_entries() + .find(|o| o.span().contains_inclusive(offset)) + .map(NodeRef::RootEntry), + offset, + } + } + pub(crate) fn from_node(node: NodeRef<'a>, offset: u32) -> Self { + Self { + next: node.span().contains_inclusive(offset).then_some(node), + offset, + } + } +} + +impl<'a> Iterator for AstPathIter<'a> { + type Item = NodeRef<'a>; + + fn next(&mut self) -> Option> { + let current = self.next.take()?; + self.next = current + .children() + .find(|c| c.span().contains_inclusive(self.offset)); + Some(current) + } +} + +/// Iterator of every [`Node`] on the way to a given offset, from the top level. +/// +/// Use [`Ast::fine_path_to`] to construct this iterator. +#[derive(Clone)] +pub struct AstFinePathIter<'a> { + next: Option>, + offset: u32, +} +impl<'a> AstFinePathIter<'a> { + pub(crate) fn from_ast(ast: &'a Ast, offset: u32) -> Self { + Self { + next: ast + .roots + .iter() + .find(|r| r.span().contains_inclusive(offset)) + .map(|r| SubNodeRef::Root(r, crate::ast::query::AstRootDetail::Node)), + offset, + } + } + // pub(crate) fn from_node(node: Node<'a>, offset: u32) -> Self { + // Self { + // next: node.span().contains(offset).then_some(node), + // offset, + // } + // } +} + +impl<'a> Iterator for AstFinePathIter<'a> { + type Item = SubNodeRef<'a>; + + fn next(&mut self) -> Option> { + let current = self.next.take()?; + + // only recurse on nodes with detail = Node, since that means we have more resolution + if current.detail().is_node() { + self.next = match current { + SubNodeRef::Root(v, _) => v + .detailed_children() + .find(|c| c.span().contains_inclusive(self.offset)), + SubNodeRef::RootEntry(v, _) => v + .detailed_children() + .find(|c| c.span().contains_inclusive(self.offset)), + SubNodeRef::Object(v, _) => v + .detailed_children() + .find(|c| c.span().contains_inclusive(self.offset)), + SubNodeRef::Property(v, _) => v + .detailed_children() + .find(|c| c.span().contains_inclusive(self.offset)), + SubNodeRef::Value(v) => v + .children() + .find(|c| c.span().contains_inclusive(self.offset)) + .map(|c| c.into()), + }; + } + + Some(current) + } +} diff --git a/crates/ltk_ritobin/src/ast/resolve.rs b/crates/ltk_ritobin/src/ast/resolve.rs new file mode 100644 index 00000000..ebf30814 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve.rs @@ -0,0 +1,7 @@ +mod block_value; +mod class; +mod entry; +mod listlikes; +pub mod literals; +mod type_expr; +mod value; diff --git a/crates/ltk_ritobin/src/ast/resolve/block_value.rs b/crates/ltk_ritobin/src/ast/resolve/block_value.rs new file mode 100644 index 00000000..212e4db2 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve/block_value.rs @@ -0,0 +1,327 @@ +use ltk_meta::PropertyKind; + +use crate::{ + ast::{ + builder::Builder, + diagnostics::{Diagnostic::*, MaybeSpanDiag}, + Property, Value, + }, + cst::Kind, + parse::Span, + Node, RitoType, +}; + +impl<'a> Builder<'a> { + /// Attempt to resolve a `Block`/`ListItemBlock` node to a value + pub(crate) fn resolve_block_value( + &mut self, + block: &Node, + hint: RitoType, + hint_span: Option, + ) -> Result { + use PropertyKind as K; + + match hint.base { + K::Struct | K::Embedded => { + self.push( + MissingClassName { + span: block.open_brace_span(self.cst), + expected: hint, + } + .unwrap(), + ); + + Ok(Value::Unresolved { + span: block.span, + kind: hint.base, + }) + } + K::Vector2 | K::Vector3 | K::Vector4 | K::Color | K::Matrix44 => { + Ok(self.resolve_listlike(block, hint.base, hint_span)) + } + K::Map => { + let key_kind = hint.subtype(0); + let value_kind = hint.subtype(1); + let entries = self.resolve_body_map_entries(block, key_kind, value_kind, hint_span); + Ok(Value::Map { + key_kind, + value_kind, + entries, + span: block.span, + }) + } + K::Container | K::UnorderedContainer => { + let item_kind = hint.subtype(0); + let items = self.resolve_body_items(block, item_kind, hint_span); + Ok(match hint.base { + K::Container => Value::Container { + item_kind, + items, + span: block.span, + }, + K::UnorderedContainer => Value::UnorderedContainer { + item_kind, + items, + span: block.span, + }, + _ => unreachable!(), + }) + } + K::Optional => { + let item_kind = hint.subtype(0); + let item_hint = RitoType::simple(item_kind); + if matches!( + item_kind, + K::Vector2 | K::Vector3 | K::Vector4 | K::Color | K::Matrix44 + ) { + let content: Vec<&Node> = block + .children + .get(self.cst) + .iter() + .filter_map(|c| c.tree(self.cst)) + .filter(|n| n.kind != Kind::Comment) + .collect(); + + // `option[vec3] = { { 0.5, 5.3, -0.2 } }` - the listlike wrapped in its own + // block, the same shape a listlike takes as a `list[vec3]` item. + if let [only] = content[..] { + if only.kind == Kind::ListItemBlock { + let inner = self.resolve_list_item_block(only, item_hint, hint_span)?; + return Ok(Value::Optional { + item_kind: Some(item_kind), + value: Some(Box::new(inner)), + span: block.span, + }); + } + } + + if content.is_empty() { + return Ok(Value::Optional { + item_kind: Some(item_kind), + value: None, + span: block.span, + }); + } + // an optional listlike spells its components flat, same as a bare listlike + let inner = self.resolve_listlike(block, item_kind, hint_span); + return Ok(Value::Optional { + item_kind: Some(item_kind), + value: Some(Box::new(inner)), + span: block.span, + }); + } + let mut value = None; + for child in block.children.get(self.cst).iter() { + let Some(node) = child.tree(self.cst) else { + continue; + }; + match node.kind { + Kind::Comment => continue, + Kind::ListItem => { + match self.resolve_value(node, Some(item_hint), hint_span) { + Ok(v) => match v.try_coerce_to(item_kind) { + Ok(coerced) => value = Some(coerced), + Err(v) => self.push( + TypeMismatch { + span: v.span(), + expected: RitoType::simple(item_kind).into(), + expected_span: hint_span, + got: v.rito_type().into(), + } + .unwrap(), + ), + }, + Err(e) => self.push(e.default_span(node.span)), + } + } + Kind::ListItemBlock => { + match self.resolve_list_item_block(node, item_hint, hint_span) { + Ok(v) => value = Some(v), + Err(e) => self.push(e.fallback(node.span)), + } + } + _ => self.push( + UnexpectedItem { + span: node.trimmed_span(self.cst), + parent: hint, + expected: crate::ItemShape::Value, + } + .unwrap(), + ), + } + } + Ok(Value::Optional { + item_kind: Some(item_kind), + value: value.map(Box::new), + span: block.span, + }) + } + _ => Err(UnexpectedContainerItem { + span: block.span, + expected: hint, + expected_span: hint_span, + } + .into()), + } + } + + pub(crate) fn resolve_body_properties( + &mut self, + block: &Node, + hint: RitoType, + ) -> Vec { + let mut properties = Vec::new(); + for child in block.children.get(self.cst).iter() { + let Some(node) = child.tree(self.cst) else { + continue; + }; + match node.kind { + Kind::Comment => continue, + Kind::Entry => match self.resolve_entry(node, Some(hint), None) { + Ok(entry) => match entry.key.try_coerce_to(PropertyKind::Hash) { + Ok(Value::Hash(hash)) => properties.push(Property { + name: hash, + type_expr: entry.type_expr, + value: entry.value, + }), + Ok(value) | Err(value) => self.push( + TypeMismatch { + span: value.span(), + expected: RitoType::simple(PropertyKind::Hash).into(), + expected_span: None, + got: value.rito_type().into(), + } + .unwrap(), + ), + }, + Err(e) => self.push(e.fallback(node.span)), + }, + Kind::ListItem | Kind::ListItemBlock => self.push( + UnexpectedItem { + span: node.trimmed_span(self.cst), + parent: hint, + expected: crate::ItemShape::Entry, + } + .unwrap(), + ), + _ => {} + } + } + properties + } + + fn resolve_body_map_entries( + &mut self, + block: &Node, + key_kind: PropertyKind, + value_kind: PropertyKind, + hint_span: Option, + ) -> Vec<(Value, Option)> { + let hint = RitoType::map(key_kind, value_kind); + let mut entries = Vec::new(); + for child in block.children.get(self.cst).iter() { + let Some(node) = child.tree(self.cst) else { + continue; + }; + match node.kind { + Kind::Comment => continue, + Kind::Entry => match self.resolve_entry(node, Some(hint), hint_span) { + Ok(entry) => match entry.key.try_coerce_to(key_kind) { + Ok(key) => { + match entry.value.as_ref() { + Some(value) if value.kind().is_some_and(|k| k != value_kind) => { + self.push( + TypeMismatch { + span: value.span(), + expected: RitoType::simple(value_kind).into(), + expected_span: hint_span, + got: value.rito_type().into(), + } + .unwrap(), + ); + } + _ => { + // reporting the error for not having a value should be handled already + } + } + entries.push((key, entry.value)); + } + Err(key) => self.push( + TypeMismatch { + span: key.span(), + expected: RitoType::simple(key_kind).into(), + expected_span: hint_span, + got: key.rito_type().into(), + } + .unwrap(), + ), + }, + Err(e) => self.push(e.fallback(node.span)), + }, + Kind::ListItem | Kind::ListItemBlock => self.push( + UnexpectedItem { + span: node.trimmed_span(self.cst), + parent: hint, + expected: crate::ItemShape::Entry, + } + .unwrap(), + ), + _ => {} + } + } + entries + } + + fn resolve_body_items( + &mut self, + block: &Node, + item_kind: PropertyKind, + hint_span: Option, + ) -> Vec { + let item_hint = RitoType::simple(item_kind); + let mut items = Vec::new(); + for child in block.children.get(self.cst).iter() { + let Some(node) = child.tree(self.cst) else { + continue; + }; + match node.kind { + Kind::Comment => continue, + Kind::ListItem => { + match self + .resolve_value(node, Some(item_hint), hint_span) + .and_then(|value| { + value + .try_coerce_to(item_kind) + .map_err(|value| TypeMismatch { + span: value.span(), + expected: RitoType::simple(item_kind).into(), + expected_span: hint_span, + got: value.rito_type().into(), + }) + }) { + Ok(value) => { + items.push(value); + } + Err(e) => self.push(e.default_span(node.span)), + } + } + Kind::ListItemBlock => { + match self.resolve_list_item_block(node, item_hint, hint_span) { + Ok(v) => items.push(v), + Err(e) => self.push(e.fallback(node.span)), + } + } + Kind::Entry => self.push( + UnexpectedItem { + span: node.trimmed_span(self.cst), + parent: RitoType::container(item_kind), + expected: crate::ItemShape::Value, + } + .unwrap(), + ), + _ => {} + } + } + items + } +} diff --git a/crates/ltk_ritobin/src/ast/resolve/class.rs b/crates/ltk_ritobin/src/ast/resolve/class.rs new file mode 100644 index 00000000..761b72f6 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve/class.rs @@ -0,0 +1,85 @@ +use ltk_hash::{BinHash, Hash as _}; +use ltk_meta::PropertyKind; + +use crate::{ + ast::{ + builder::Builder, + diagnostics::{ + Diagnostic::{self, *}, + RitoTypeOrVirtual, + }, + hash::HashedLiteral, + Object, Value, + }, + cst::{ChildrenExt as _, Kind}, + parse::{Token, TokenKind}, + Node, RitoType, +}; + +impl<'a> Builder<'a> { + pub(crate) fn resolve_class_hash( + &mut self, + token: &Token, + ) -> Result, Diagnostic> { + match token { + Token { + kind: TokenKind::Name, + span, + } => Ok(HashedLiteral::new( + *span, + crate::ast::hash::Originally::Name, + BinHash::hash_str(&self.text[span]), + )), + Token { + kind: TokenKind::HexLit, + span, + } => match Value::eval_unknown_hash(self.text, *span)? { + Value::Hash(hash) => Ok(hash), + value => Err(TypeMismatch { + span: value.span(), + expected: RitoType::simple(PropertyKind::Hash).into(), + expected_span: None, + got: value.rito_type().into(), + }), + }, + _ => Err(InvalidHash(token.span)), + } + } + pub(crate) fn resolve_class( + &mut self, + class: &Node, + hint: RitoType, + ) -> Result { + let children = class.children.get(self.cst); + let Some(name_token) = children.first().and_then(|c| c.token(self.cst)) else { + return Err(InvalidHash(class.span)); + }; + let class_hash = self.resolve_class_hash(name_token)?; + + if !matches!(hint.base, PropertyKind::Struct | PropertyKind::Embedded) { + return Err(TypeMismatch { + span: name_token.span, + expected: RitoType::simple(hint.base).into(), + expected_span: None, + got: RitoTypeOrVirtual::StructOrEmbedded, + }); + } + + let properties = match children.find_tree(self.cst, Kind::Block) { + Some(block) => self.resolve_body_properties(block, hint), + None => Vec::new(), + }; + + let ast_struct = Object { + class_hash, + span: class.span, + properties, + }; + + Ok(match hint.base { + PropertyKind::Struct => Value::Struct(ast_struct), + PropertyKind::Embedded => Value::Embedded(ast_struct), + _ => unreachable!(), + }) + } +} diff --git a/crates/ltk_ritobin/src/ast/resolve/entry.rs b/crates/ltk_ritobin/src/ast/resolve/entry.rs new file mode 100644 index 00000000..5c961a45 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve/entry.rs @@ -0,0 +1,192 @@ +use ltk_meta::{property::values, PropertyKind}; + +use crate::{ + ast::{ + builder::Builder, + diagnostics::{ + Diagnostic::{ + self, InvalidHash, MissingTree, MissingType, QuotedPropertyName, TypeMismatch, + }, + MaybeSpanDiag, + }, + node::TypeExpr, + resolve::literals::{self}, + Value, + }, + cst::{ChildrenExt as _, Kind}, + parse::{Span, Token, TokenKind}, + Node, RitoType, Spanned, SpannedExt, +}; + +pub struct RawEntry { + pub key: Value, + pub type_expr: Spanned>, + pub value: Option, +} + +impl<'a> Builder<'a> { + pub fn resolve_entry_key( + &mut self, + key_node: &Node, + parent_value_kind: Option, + parent_type_span: Option, + ) -> Result { + let token = key_node + .children + .get(self.cst) + .first() + .ok_or(InvalidHash(key_node.span))? + .token(self.cst); + + Ok(match token { + Some(Token { + kind: TokenKind::Name, + span, + }) => Value::String(Spanned::new(*span, self.text[span].into())), + Some(Token { + kind: TokenKind::String, + span, + }) => { + if let Some(parent) = parent_value_kind + .filter(|p| matches!(p.base, PropertyKind::Struct | PropertyKind::Embedded)) + { + self.push( + QuotedPropertyName { + span: *span, + parent, + } + .unwrap(), + ); + } + Value::from(values::String::new_with_meta( + self.text[Span::new(span.start + 1, span.end - 1)].into(), + *span, + )) + } + Some(Token { + kind: TokenKind::HexLit, + span, + }) => Value::Hash(literals::eval_hash(self.text, *span)?), + Some(token) => self.resolve_literal( + self.text, + token, + parent_value_kind + .and_then(|k| k.subtypes[0]) + .map(RitoType::simple), + parent_type_span, + ), + None => return Err(InvalidHash(key_node.span)), + }) + } + + /// Resolves an `Entry` node + pub fn resolve_entry( + &mut self, + entry: &Node, + parent_value_kind: Option, + parent_type_span: Option, + ) -> Result { + let children = entry.children.get(self.cst); + let key_node = children + .find_tree(self.cst, Kind::EntryKey) + .ok_or(MissingTree(Kind::EntryKey))?; + let key = self.resolve_entry_key(key_node, parent_value_kind, parent_type_span)?; + + let parent_value_kind = parent_value_kind + .and_then(|p| p.value_subtype()) + .map(RitoType::simple); + + let type_expr_node = children.find_tree(self.cst, Kind::TypeExpr); + let type_expr_span = type_expr_node.map(|k| k.span); + let type_expr = type_expr_node.and_then(|t| self.resolve_type_expr(t)); + + let value_node = children + .find_tree(self.cst, Kind::EntryValue) + .ok_or(MissingTree(Kind::EntryValue))?; + + let desired_kind = type_expr.or(parent_value_kind.map(|k| k.into())); + let type_span = type_expr_span.or(parent_type_span); + + let value = match self.resolve_value( + value_node, + desired_kind.and_then(|k| k.as_resolved()), + type_span, + ) { + Ok(v) => Some(v), + Err(e) => { + if !matches!(desired_kind, Some(TypeExpr::Unresolved)) { + // don't report the value error if the type expr was unresolved - we have + // bigger fish to fry, so reporting this error isn't needed + self.push(e.default_span(entry.span)); + } + None + } + }; + + let value = value.map(|value| match desired_kind { + Some(TypeExpr::Resolved(kind)) => match value.try_coerce_to(kind.base) { + Ok(value) => value, + Err(value) => value, + }, + _ => value, + }); + + match (desired_kind, value.as_ref()) { + (Some(kind), Some(value)) => { + if value.rito_type().is_some_and(|k| k != kind) { + self.push( + TypeMismatch { + span: value.span(), + expected: kind.into(), + expected_span: type_expr_span, + got: value.rito_type().into(), + } + .unwrap(), + ) + } + } + (None, None) => { + self.push(MissingType(key.span()).unwrap()); + } + // only report missing value if the type expression resolved properly (bigger fish) + (Some(TypeExpr::Resolved(kind)), None) => { + self.push( + Diagnostic::MissingEntryValue { + key_span: key_node.span, + expected: type_expr_span.map(|span| Spanned::new(span, kind)), + } + .unwrap(), + ); + } + _ => {} + }; + + Ok(RawEntry { + key, + value, + type_expr: type_expr.with_span( + (match type_expr_span { + Some(span) if !span.is_empty() => Some(span), + _ => None, + }) + .unwrap_or_else(|| { + let colon = entry + .children + .find_token(self.cst, TokenKind::Colon) + .map(|t| t.span); + let eq = entry + .children + .find_token(self.cst, TokenKind::Eq) + .map(|t| t.span); + + match (colon, eq) { + (None, None) => Span::new(key_node.span.end, entry.span.end), + (None, Some(eq)) => Span::new(key_node.span.start, eq.start), + (Some(colon), None) => Span::new(colon.end, entry.span.end), + (Some(colon), Some(eq)) => Span::new(colon.end, eq.start), + } + }), + ), + }) + } +} diff --git a/crates/ltk_ritobin/src/ast/resolve/listlikes.rs b/crates/ltk_ritobin/src/ast/resolve/listlikes.rs new file mode 100644 index 00000000..54b9a298 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve/listlikes.rs @@ -0,0 +1,208 @@ +use ltk_meta::{property::values, PropertyKind}; + +use crate::{ + ast::{ + builder::Builder, + diagnostics::{Diagnostic, ListLike, MaybeSpanDiag}, + Value, + }, + cst::{Child, Cst, Kind, Node}, + parse::Span, + RitoType, Spanned, +}; + +use Diagnostic::*; + +struct ListIter<'a, 'b, 'c> { + ctx: &'a mut Builder<'b>, + children: std::slice::Iter<'c, Child>, + cst: &'c Cst, + span: Span, + type_span: Option, + count: u8, +} + +impl<'c> ListIter<'_, '_, 'c> { + fn next_value(&mut self, expected: PropertyKind) -> Option> { + for child in self.children.by_ref() { + let Some(node) = child.tree(self.cst) else { + continue; + }; + if node.kind == Kind::Comment { + continue; + } + self.count += 1; + self.span = node.span; + return Some( + self.ctx + .resolve_numeric(node, expected, self.type_span) + .map_err(MaybeSpanDiag::from), + ); + } + None + } + + fn expect_next( + &mut self, + expected_kind: PropertyKind, + expected: ListLike, + ) -> Result { + match self.next_value(expected_kind) { + Some(v) => v, + None => Err(NotEnoughItems { + span: self.span, + got: self.count, + expected, + } + .into()), + } + } + + fn read_floats( + &mut self, + expected: ListLike, + ) -> Result<[f32; N], MaybeSpanDiag> { + let mut out = [0.0f32; N]; + for slot in &mut out { + *slot = match self.expect_next(PropertyKind::F32, expected)? { + Value::F32(v) => v.value, + other => { + return Err(TypeMismatch { + span: other.span(), + expected: RitoType::simple(PropertyKind::F32).into(), + expected_span: self.type_span, + got: other.kind().map(RitoType::simple).into(), + } + .into()) + } + }; + } + Ok(out) + } + + fn read_u8s(&mut self, expected: ListLike) -> Result<[u8; N], MaybeSpanDiag> { + let mut out = [0u8; N]; + for slot in &mut out { + *slot = match self.expect_next(PropertyKind::U8, expected)? { + Value::U8(v) => v.value, + other => { + return Err(TypeMismatch { + span: other.span(), + expected: RitoType::simple(PropertyKind::U8).into(), + expected_span: self.type_span, + got: other.kind().map(RitoType::simple).into(), + } + .into()) + } + }; + } + Ok(out) + } +} + +impl<'a> Builder<'a> { + /// `node` is the `ListItem` wrapping the literal. + pub(crate) fn resolve_numeric( + &mut self, + node: &Node, + expected: PropertyKind, + hint_span: Option, + ) -> Result { + self.resolve_value(node, Some(RitoType::simple(expected)), hint_span) + } + + pub(crate) fn resolve_listlike_fallable( + &mut self, + block: &Node, + kind: PropertyKind, + type_span: Option, + ) -> Result { + let cst = self.cst(); + let span = block.span; + let mut items = ListIter { + ctx: self, + children: block.children.get(cst).iter(), + cst, + span, + type_span, + count: 0, + }; + + let value = match kind { + PropertyKind::Vector2 => { + let [x, y] = items.read_floats::<2>(ListLike::Vec2)?; + Value::Vector2(values::Vector2::new_with_meta([x, y].into(), span)) + } + PropertyKind::Vector3 => { + let [x, y, z] = items.read_floats::<3>(ListLike::Vec3)?; + Value::Vector3(values::Vector3::new_with_meta([x, y, z].into(), span)) + } + PropertyKind::Vector4 => { + let [x, y, z, w] = items.read_floats::<4>(ListLike::Vec4)?; + Value::Vector4(values::Vector4::new_with_meta([x, y, z, w].into(), span)) + } + PropertyKind::Color => { + let [r, g, b, a] = items.read_u8s::<4>(ListLike::Color)?; + Value::Color(Spanned::new(span, ltk_primitives::Color { r, g, b, a })) + } + PropertyKind::Matrix44 => { + let x_axis = items.read_floats::<4>(ListLike::Mat44)?; + let y_axis = items.read_floats::<4>(ListLike::Mat44)?; + let z_axis = items.read_floats::<4>(ListLike::Mat44)?; + let w_axis = items.read_floats::<4>(ListLike::Mat44)?; + let mat = glam::Mat4::from_cols( + x_axis.into(), + y_axis.into(), + z_axis.into(), + w_axis.into(), + ) + .transpose(); + Value::Matrix44(values::Matrix44::new_with_meta(mat, span)) + } + _ => unreachable!("resolve_listlike called with a non-listlike kind"), + }; + + let expected = match kind { + PropertyKind::Vector2 => ListLike::Vec2, + PropertyKind::Vector3 => ListLike::Vec3, + PropertyKind::Vector4 => ListLike::Vec4, + PropertyKind::Color => ListLike::Color, + PropertyKind::Matrix44 => ListLike::Mat44, + _ => unreachable!(), + }; + if let Some(extra) = items.next_value(PropertyKind::F32) { + let extra = extra?; + let count = 1 + items.children.count(); + return Err(TooManyItems { + span: extra.span(), + extra: count as _, + expected, + } + .into()); + } + Ok(value) + } + + /// Resolves a `Block`/`ListItemBlock` node whose body is a flat list of bare numbers into + /// one packed [`AstValue`] of `kind` (`Vector2`/`Vector3`/`Vector4`/`Color`/`Matrix44`). + /// + /// Any errors in resolution are automatically pushed, and a [`Value::Unresolved`] is returned. + pub(super) fn resolve_listlike( + &mut self, + block: &Node, + kind: PropertyKind, + type_span: Option, + ) -> Value { + match self.resolve_listlike_fallable(block, kind, type_span) { + Ok(value) => value, + + Err(e) => { + self.push(e.fallback(block.span)); + Value::Unresolved { + kind, + span: block.span, + } + } + } + } +} diff --git a/crates/ltk_ritobin/src/ast/resolve/literals.rs b/crates/ltk_ritobin/src/ast/resolve/literals.rs new file mode 100644 index 00000000..54933e67 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve/literals.rs @@ -0,0 +1,166 @@ +use std::{borrow::Cow, str::FromStr}; + +use ltk_hash::{BinHash, WadHash}; +use ltk_meta::{property::values, PropertyKind}; + +use crate::{ + ast::{ + diagnostics::{Diagnostic, RitoTypeOrVirtual}, + hash::{HashedLiteral, Originally}, + Value, + }, + parse::{Span, Token, TokenKind}, + RitoType, Spanned, +}; + +use Diagnostic::*; + +impl Value { + pub(crate) fn eval_unknown_hash(text: &str, span: Span) -> Result { + // TODO: better errs here? + let src = text[span].strip_prefix("0x").ok_or(InvalidHash(span))?; + + // since we can't know whether bin/wad was intended, we will just try fit it in the smallest hash that allows it. + // we can then safely coerce the type upwards when we are given type information + Ok(match BinHash::from_str_radix(src, 16) { + Ok(hash) => Self::Hash(HashedLiteral::new(span, Originally::HexLit, hash)), + Err(_) => match WadHash::from_str_radix(src, 16) { + Ok(hash) => Self::WadChunkLink(HashedLiteral::new(span, Originally::HexLit, hash)), + Err(_) => return Err(InvalidHash(span)), + }, + }) + } +} + +pub(crate) fn eval_hash( + text: &str, + span: Span, +) -> Result, Diagnostic> { + // TODO: better errs here? + let src = text[span].strip_prefix("0x").ok_or(InvalidHash(span))?; + H::from_str(src) + .map_err(|_| InvalidHash(span)) + .map(|value| HashedLiteral::new(span, Originally::HexLit, value)) +} + +fn parse_int>( + txt: &str, + kind_hint: PropertyKind, + span: Span, + wrap: impl FnOnce(T, Span) -> Value, +) -> Result { + txt.parse::() + .map(|v| wrap(v, span)) + .map_err(|e| Diagnostic::ParseNumericError { + expected: kind_hint, + error: Some(*e.kind()), + span, + }) +} + +impl Value { + /// Evaluate a literal token into a value + /// + /// # Errors + /// If the literal does not fit `kind_hint`, or if it is ambiguous and there is no hint to pick + /// with - a bare `5` on its own has no type. + pub(crate) fn eval( + text: &str, + token: &Token, + kind_hint: Option, + kind_hint_span: Option, + ) -> Result { + use PropertyKind as K; + Ok(match token { + Token { + kind: TokenKind::String, + span, + } => Self::String(Spanned::new( + *span, + text[Span::new(span.start + 1, span.end - 1)].into(), + )), + + Token { + kind: TokenKind::Null, + span, + } => Self::None(*span), + + Token { + kind: TokenKind::True, + span, + } => Self::bool(*span, true), + Token { + kind: TokenKind::False, + span, + } => Self::bool(*span, false), + + Token { + kind: TokenKind::HexLit, + span, + } => Self::eval_unknown_hash(text, *span)?, + Token { + kind: TokenKind::Number, + span, + } => { + let txt = &text[span]; + let Some(kind_hint) = kind_hint else { + return Err(AmbiguousNumeric(*span)); + }; + + let txt = match txt.contains('_') { + true => Cow::Owned(txt.replace('_', "")), + false => Cow::Borrowed(txt), + }; + + let kind_hint = match kind_hint.base { + K::Optional => kind_hint.value_subtype().unwrap_or(kind_hint.base), + base => base, + }; + + match kind_hint { + K::U8 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::U8(values::U8::new_with_meta(v, s)) + })?, + K::U16 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::U16(values::U16::new_with_meta(v, s)) + })?, + K::U32 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::U32(values::U32::new_with_meta(v, s)) + })?, + K::U64 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::U64(values::U64::new_with_meta(v, s)) + })?, + K::I8 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::I8(values::I8::new_with_meta(v, s)) + })?, + K::I16 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::I16(values::I16::new_with_meta(v, s)) + })?, + K::I32 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::I32(values::I32::new_with_meta(v, s)) + })?, + K::I64 => parse_int::(&txt, kind_hint, *span, |v, s| { + Self::I64(values::I64::new_with_meta(v, s)) + })?, + K::F32 => Self::F32(values::F32::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ParseNumericError { + expected: kind_hint, + error: None, + span: *span, + })?, + *span, + )), + _ => { + return Err(TypeMismatch { + span: *span, + expected: RitoType::simple(kind_hint).into(), + expected_span: kind_hint_span, + got: RitoTypeOrVirtual::numeric(), + }); + } + } + } + _ => return Err(Diagnostic::ResolveLiteral), + }) + } +} diff --git a/crates/ltk_ritobin/src/ast/resolve/type_expr.rs b/crates/ltk_ritobin/src/ast/resolve/type_expr.rs new file mode 100644 index 00000000..85beded0 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve/type_expr.rs @@ -0,0 +1,116 @@ +use ltk_meta::PropertyKind; + +use crate::{ + ast::{ + builder::Builder, + diagnostics::Diagnostic::{self, *}, + node::TypeExpr, + }, + cst::{ChildrenExt as _, Kind}, + parse::{Span, TokenKind}, + Node, RitoType, RitobinName as _, +}; + +impl<'a> Builder<'a> { + pub fn resolve_type_expr(&mut self, tree: &Node) -> Option { + match self.resolve_type_expr_fallable(tree) { + Ok(rito) => Some(TypeExpr::Resolved(rito)), + Err(e @ MissingToken(_)) => { + self.push(e.default_span(tree.span)); + None + } + Err(e) => { + self.push(e.default_span(tree.span)); + Some(TypeExpr::Unresolved) + } + } + } + pub fn resolve_type_expr_fallable(&mut self, tree: &Node) -> Result { + let children = tree.children.get(self.cst); + + let base = children + .find_token(self.cst, TokenKind::Name) + .ok_or(MissingToken(TokenKind::Name))?; + let base_span = base.span; + let base = + PropertyKind::from_rito_name(&self.text[base.span]).ok_or(UnknownType(base.span))?; + + let subtypes = match children.find_tree(self.cst, Kind::TypeArgList) { + Some(subtypes_node) => { + let subtypes_span = subtypes_node.span; + let expected = base.subtype_count(); + + if expected == 0 { + return Err(UnexpectedSubtypes { + span: subtypes_span, + base_type: base_span, + }); + } + + let subtypes = subtypes_node + .children + .get(self.cst) + .iter() + .filter_map(|c| c.tree(self.cst).filter(|t| t.kind == Kind::TypeArg)) + .enumerate() + .map(|(i, t)| { + let resolved = PropertyKind::from_rito_name(&self.text[t.span]); + match resolved { + None => self.push(UnknownType(t.span).unwrap()), + Some(kind) if kind.is_container() => { + self.push( + InvalidNesting { + span: t.span, + kind: RitoType::simple(kind), + } + .unwrap(), + ); + } + Some(kind) + if base == PropertyKind::Map + && i == 0 + && !kind.is_valid_map_key() => + { + self.push( + InvalidMapKey { + span: t.span, + kind: RitoType::simple(kind), + } + .unwrap(), + ); + } + Some(_) => {} + } + (resolved, t.span) + }) + .collect::>(); + + if subtypes.len() != expected.into() { + let span = if subtypes.len() > expected.into() { + subtypes[expected as _..] + .iter() + .map(|s| s.1) + .reduce(|acc, s| Span::new(acc.start, s.end)) + .unwrap_or(subtypes_span) + } else { + subtypes.last().map(|s| s.1).unwrap_or(subtypes_span) + }; + return Err(SubtypeCountMismatch { + 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 }) + } +} diff --git a/crates/ltk_ritobin/src/ast/resolve/value.rs b/crates/ltk_ritobin/src/ast/resolve/value.rs new file mode 100644 index 00000000..39c9cd2a --- /dev/null +++ b/crates/ltk_ritobin/src/ast/resolve/value.rs @@ -0,0 +1,108 @@ +use crate::{ + ast::{ + builder::Builder, + diagnostics::{ + Diagnostic::{self}, + MaybeSpanDiag, + }, + Value, + }, + cst::Kind, + parse::{Span, Token}, + Node, RitoType, +}; + +impl<'a> Builder<'a> { + pub(crate) fn resolve_literal( + &mut self, + text: &str, + token: &Token, + kind_hint: Option, + kind_hint_span: Option, + ) -> Value { + match Value::eval(text, token, kind_hint, kind_hint_span) { + Ok(value) => value, + Err(e) => { + self.push(e.default_span(token.span)); + kind_hint + .map(|k| Value::Unresolved { + span: token.span, + kind: k.base, + }) + .unwrap_or(Value::Unknown(token.span)) + } + } + } + pub(crate) fn resolve_value( + &mut self, + wrapper: &Node, + hint: Option, + hint_span: Option, + ) -> Result { + let Some(child) = wrapper.children.get(self.cst).first() else { + return Err(Diagnostic::CustomSpan( + "[resolve_value] node has no children", + wrapper.span, + )); + }; + let Some(node) = child.tree(self.cst) else { + return Err(Diagnostic::CustomSpan( + "[resolve_value] first child is not a node", + wrapper.span, + )); + }; + match node.kind { + Kind::Class => { + let Some(hint) = hint else { + return Err(Diagnostic::CustomSpan( + "Cannot resolve class block with no type hint", + node.span, + )); + }; + self.resolve_class(node, hint) + } + Kind::Block => { + let Some(hint) = hint else { + return Err(Diagnostic::CustomSpan( + "Cannot resolve block with no type hint", + node.span, + )); + }; + self.resolve_block_value(node, hint, hint_span) + .map_err(|e| e.fallback(node.span).diagnostic) + } + Kind::Literal => { + let Some(token_child) = node.children.get(self.cst).first() else { + return Err(Diagnostic::CustomSpan( + "[resolve_value] literal node has no children", + wrapper.span, + )); + }; + let Some(token) = token_child.token(self.cst) else { + return Err(Diagnostic::CustomSpan( + "[resolve_value] literal node's first child is not a token", + wrapper.span, + )); + }; + Ok(self.resolve_literal(self.text, token, hint, hint_span)) + } + Kind::ErrorTree => Ok(Value::Unknown(node.span)), + kind => { + eprintln!("cannot resolve {kind:?}"); + Err(Diagnostic::CustomSpan( + "[resolve_value] cannot resolve this node kind", + node.span, + )) + } + } + } + + pub(crate) fn resolve_list_item_block( + &mut self, + node: &Node, + hint: RitoType, + hint_span: Option, + ) -> Result { + self.resolve_block_value(node, hint, hint_span) + } +} diff --git a/crates/ltk_ritobin/src/ast/tests.rs b/crates/ltk_ritobin/src/ast/tests.rs new file mode 100644 index 00000000..ec315475 --- /dev/null +++ b/crates/ltk_ritobin/src/ast/tests.rs @@ -0,0 +1,877 @@ +use glam::{Vec3, Vec4}; +use ltk_hash::BinHash; +use ltk_meta::{ + property::{values, NoMeta}, + Bin, BinObject, ObjectBuilder, PropertyKind, PropertyValueEnum, +}; + +use crate::{ + ast::{ + diagnostics::{Diagnostic, DiagnosticWithSpan, RitoTypeOrVirtual}, + node::root::RootKind, + }, + Cst, ItemShape, RitoType, +}; + +fn wrap(input: &str) -> String { + format!( + r#" +#PROP_text +type: string = "PROP" +version: u32 = 3 +linked: list[string] = {{}} +entries: map[hash,embed] = {{ + 0xDEADBEEF = 0x1234123 {{ + {input} + }} +}}"# + ) +} + +fn assert ObjectBuilder>(input: &str, is: F) { + let input = wrap(input); + + let cst = Cst::parse(&input); + let mut str = String::new(); + + cst.print(&mut str, &input); + eprintln!("#### CST:\n{str}"); + + let ast = cst.build_ast(&input); + + assert!( + ast.diagnostics.is_empty(), + "Typecheck errors: {:#?}", + ast.diagnostics + ); + let bin = ast.to_bin(&input); + + let obj = (is)(BinObject::::builder(0xDEADBEEF, 0x1234123)).build(); + pretty_assertions::assert_eq!(bin, Bin::builder().object(obj).build()); +} + +/// Builds a full object body (see [`wrap`]) from `input` and returns the +/// typecheck diagnostics without asserting they're empty - for exercising +/// error paths. +fn build_errs(input: &str) -> Vec { + let input = wrap(input); + let cst = Cst::parse(&input); + + if option_env!("PRINT_CST").is_some() { + let mut str = String::new(); + cst.print(&mut str, &input); + eprintln!("####\n{str}\n#####"); + } + + cst.build_ast(&input).diagnostics +} + +#[test] +fn option() { + assert(r#"0x1: option[vec3] = { { 0.5, 5.3, -0.20 } }"#, |obj| { + obj.property( + 0x1, + values::Optional::from(values::Vector3::from(Vec3::new(0.5, 5.3, -0.2))), + ) + }); +} +#[test] +fn option_coerce() { + assert(r#"0x1: option[vec3] = { 0.5, 5.3, -0.20 }"#, |obj| { + obj.property( + 0x1, + values::Optional::from(values::Vector3::from(Vec3::new(0.5, 5.3, -0.2))), + ) + }); +} + +#[test] +fn list() { + assert( + r#" + values: list[vec4] = { + { 1, 1, 1, 1 } + { 1, 1, 1, 1 } + { 1, 1, 1, 0 } + } + "#, + |obj| { + obj.property( + 0x34474c3b, + values::Container::from_iter([ + values::Vector4::from(Vec4::new(1., 1., 1., 1.)), + values::Vector4::from(Vec4::new(1., 1., 1., 1.)), + values::Vector4::from(Vec4::new(1., 1., 1., 0.)), + ]), + ) + }, + ); +} + +#[test] +fn u8_map() { + assert( + r#" + 0xe6d60f41: map[u8,string] = { + 1 = "hello" + } + "#, + |obj| { + obj.property( + 0xe6d60f41, + values::Map::new( + PropertyKind::U8, + PropertyKind::String, + vec![( + values::U8::from(1).into(), + values::String::from("hello").into(), + )], + ) + .unwrap(), + ) + }, + ); +} + +#[test] +fn matrix() { + assert( + r#" + 0x1: mtx44 = { + 0.1, 0.2, 0.3, 0.4, + 1.1, 1.2, 1.3, 1.4, + 2.1, 2.2, 2.3, 2.4, + 3.1, 3.2, 3.3, 3.4 + } + "#, + |obj| { + obj.property( + 0x1, + values::Matrix44::from(glam::Mat4::from_cols_array_2d(&[ + [0.1, 1.1, 2.1, 3.1], + [0.2, 1.2, 2.2, 3.2], + [0.3, 1.3, 2.3, 3.3], + [0.4, 1.4, 2.4, 3.4], + ])), + ) + }, + ); +} + +#[test] +fn numeric_parse_error() { + let errs = build_errs("0x1: u8 = 999999"); + assert_eq!(errs.len(), 1, "{errs:#?}"); + assert!( + matches!( + errs[0].diagnostic, + Diagnostic::ParseNumericError { + expected: PropertyKind::U8, + .. + } + ), + "{:#?}", + errs[0] + ); +} + +#[test] +fn subtype_count_mismatch_too_many() { + // Container/list takes exactly 1 subtype + let errs = build_errs("0x1: list[u8,u8] = {}"); + assert_eq!(errs.len(), 1, "{errs:#?}"); + assert!( + matches!( + errs[0].diagnostic, + Diagnostic::SubtypeCountMismatch { + expected: 1, + got: 2, + .. + } + ), + "{:#?}", + errs[0] + ); +} + +#[test] +fn subtype_count_mismatch_too_few() { + // Map takes exactly 2 subtypes + let errs = build_errs("0x1: map[u8] = {}"); + assert_eq!(errs.len(), 1, "{errs:#?}"); + assert!( + matches!( + errs[0].diagnostic, + Diagnostic::SubtypeCountMismatch { + expected: 2, + got: 1, + .. + } + ), + "{:#?}", + errs[0] + ); +} + +#[test] +fn missing_linked_root_entry_reports_diagnostic_without_panicking() { + let input = r#" +type: string = "PROP" +version: u32 = 3 +entries: map[hash,embed] = {} +"#; + let cst = Cst::parse(input); + let errs = cst.build_ast(input).diagnostics; + assert!( + errs.iter().any(|e| matches!( + e.diagnostic, + Diagnostic::MissingRootEntry { + root_kind: RootKind::Linked + } + )), + "{errs:#?}" + ); +} + +#[test] +fn missing_entries_root_entry_reports_diagnostic_without_panicking() { + let input = r#" +type: string = "PROP" +version: u32 = 3 +linked: list[string] = {} +"#; + let cst = Cst::parse(input); + let errs = cst.build_ast(input).diagnostics; + assert!( + errs.iter().any(|e| matches!( + e.diagnostic, + Diagnostic::MissingRootEntry { + root_kind: RootKind::Entries + } + )), + "{errs:#?}" + ); +} + +#[test] +fn missing_type_root_entry_reports_type_not_version() { + let input = r#" +version: u32 = 3 +linked: list[string] = {} +entries: map[hash,embed] = {} +"#; + let cst = Cst::parse(input); + let errs = cst.build_ast(input).diagnostics; + assert!( + errs.iter().any(|e| matches!( + e.diagnostic, + Diagnostic::MissingRootEntry { + root_kind: RootKind::Type + } + )), + "{errs:#?}" + ); +} + +#[test] +fn invalid_type_root_entry_reports_type_not_version() { + let input = r#" +type: u32 = 3 +version: u32 = 3 +linked: list[string] = {} +entries: map[hash,embed] = {} +"#; + let cst = Cst::parse(input); + let errs = cst.build_ast(input).diagnostics; + assert!( + errs.iter().any(|e| matches!( + e.diagnostic, + Diagnostic::InvalidRootEntryType { + root_kind: RootKind::Type, + .. + } + )), + "{errs:#?}" + ); +} + +#[test] +fn empty_vec3_reports_not_enough_items() { + let errs = build_errs("0x1: vec3 = {}"); + assert_eq!(errs.len(), 1, "{errs:#?}"); + assert!( + matches!( + errs[0].diagnostic, + Diagnostic::NotEnoughItems { got: 0, .. } + ), + "{:#?}", + errs[0] + ); +} + +#[test] +fn empty_color_reports_not_enough_items() { + let errs = build_errs("0x1: rgba = {}"); + assert_eq!(errs.len(), 1, "err count != 1, got: {errs:#?}"); + assert!( + matches!( + errs[0].diagnostic, + Diagnostic::NotEnoughItems { got: 0, .. } + ), + "errors dont match, got: {:#?}", + errs + ); +} + +#[test] +fn empty_mtx44_reports_not_enough_items() { + let errs = build_errs("0x1: mtx44 = {}"); + assert_eq!(errs.len(), 1, "err count != 1, got: {errs:#?}"); + assert!( + matches!( + errs[0].diagnostic, + Diagnostic::NotEnoughItems { got: 0, .. } + ), + "errors dont match, got: {:#?}", + errs + ); +} + +/// Asserts `input` produces exactly one diagnostic, and hands it to `is`. +fn assert_one_err bool>(input: &str, is: F) -> DiagnosticWithSpan { + let errs = build_errs(input); + assert_eq!(errs.len(), 1, "err count != 1, got: {errs:#?}"); + assert!( + (is)(&errs[0].diagnostic), + "errors dont match, got: {:#?}", + errs[0] + ); + errs[0] +} + +/// `pointer`/`embed` values are written `ClassName { .. }`. Deleting the class name +/// used to default-construct a class-hash-0 struct without a word. +#[test] +fn missing_class_name_in_a_list_item_is_reported() { + let err = assert_one_err( + r#" + paramValues: list[embed] = { + StaticMaterialShaderParamDef { + name: string = "A" + } + { + name: string = "B" + } + } + "#, + |d| { + matches!( + d, + Diagnostic::MissingClassName { + expected: RitoType { + base: PropertyKind::Embedded, + .. + }, + .. + } + ) + }, + ); + // points at the `{` the class name should precede, not the whole block + assert_eq!(err.span.len(), 1); + assert_eq!( + err.diagnostic.to_string(), + "Missing class name - embed values are written 'ClassName { .. }'" + ); +} + +#[test] +fn missing_class_name_in_a_map_entry_is_reported() { + assert_one_err( + r#" + items: map[hash,pointer] = { + 0xc8fd50ab = { + name: string = "a" + } + } + "#, + |d| { + matches!( + d, + Diagnostic::MissingClassName { + expected: RitoType { + base: PropertyKind::Struct, + .. + }, + .. + } + ) + }, + ); +} + +#[test] +fn nested_container_fails() { + let errs = build_errs(r#"0x1: list[list[u32]] = { { 1 2 } { 3 4 } }"#); + // assert_eq!(errs.len(), 1, "{errs:#?}"); // TODO: reassert this later (blocked on parser + // changes) + assert!( + matches!(errs[0].diagnostic, Diagnostic::InvalidNesting { .. }), + "{:#?}", + errs[0] + ); +} + +#[test] +fn nested_map_key_type_fails() { + let errs = build_errs(r#"0x1: map[list,u32] = {}"#); + assert_eq!(errs.len(), 1, "{errs:#?}"); + assert!( + matches!(errs[0].diagnostic, Diagnostic::InvalidNesting { .. }), + "{:#?}", + errs[0] + ); +} + +#[test] +fn non_primitive_map_key_type_fails() { + let errs = build_errs(r#"0x1: map[link,u32] = {}"#); + assert_eq!(errs.len(), 1, "{errs:#?}"); + assert!( + matches!(errs[0].diagnostic, Diagnostic::InvalidMapKey { .. }), + "{:#?}", + errs[0] + ); +} + +#[test] +fn nested_optional_type_fails() { + let errs = build_errs(r#"0x1: option[map] = {}"#); + assert_eq!(errs.len(), 1, "{errs:#?}"); + assert!( + matches!(errs[0].diagnostic, Diagnostic::InvalidNesting { .. }), + "{:#?}", + errs[0] + ); +} + +/// So must one that is properly introduced by a class name. +#[test] +fn a_named_class_list_item_is_fine() { + let errs = build_errs( + r#" + paramValues: list[embed] = { + StaticMaterialShaderParamDef { + name: string = "A" + } + 0xdeadbeef { + name: string = "B" + } + } + "#, + ); + assert!(errs.is_empty(), "{errs:#?}"); +} + +/// The reported case: `""` where a property name belongs. `merge_ir` used to drop any +/// child whose shape didn't fit, with a `trace!` and no diagnostic. +#[test] +fn a_bare_value_in_a_class_body_is_reported() { + assert_one_err( + r#" + name: string = "A" + "" + flags: u32 = 1 + "#, + |d| { + matches!( + d, + Diagnostic::UnexpectedItem { + expected: ItemShape::Entry, + parent: RitoType { + base: PropertyKind::Embedded, + .. + }, + .. + } + ) + }, + ); +} + +#[test] +fn a_type_mismatch_blames_the_type_expression_that_set_it() { + // the LSP renders `expected_span` as "due to this type expression", so it has to point + // at an actual type expression - not at the container's braces, and not at all when the + // expectation was not written down anywhere + let blamed = |input: &str| { + let text = wrap(input); + let errs = Cst::parse(&text).build_ast(&text).diagnostics; + errs.into_iter() + .find_map(|e| match e.diagnostic { + Diagnostic::TypeMismatch { expected_span, .. } => Some(expected_span), + _ => None, + }) + .expect("expected a TypeMismatch") + .map(|span| text[span].to_owned()) + }; + + assert_eq!( + blamed(r#"0x1: list[u32] = { "a" }"#).as_deref(), + Some("list[u32]") + ); + assert_eq!( + blamed(r#"0x1: map[u32,u32] = { "5" = 1 }"#).as_deref(), + Some("map[u32,u32]") + ); + assert_eq!( + blamed(r#"0x1: option[u32] = { "a" }"#).as_deref(), + Some("option[u32]") + ); + // a numeric literal resolved against a hint that takes no number + assert_eq!(blamed(r#"0x1: string = 5"#).as_deref(), Some("string")); + // listlike components answer to the type that made them components + assert_eq!( + blamed(r#"0x1: vec3 = { 1, "a", 3 }"#).as_deref(), + Some("vec3") + ); + assert_eq!( + blamed(r#"0x1: rgba = { 1, "a", 3, 4 }"#).as_deref(), + Some("rgba") + ); + // ... and a listlike written as a list item falls back to the container's subtype + assert_eq!( + blamed(r#"0x1: list[vec3] = { { 1, "a", 3 } }"#).as_deref(), + Some("list[vec3]") + ); + // a property name is a hash because it is a property name - no type expression said so + assert_eq!(blamed("true: u32 = 3").as_deref(), None); +} + +#[test] +fn a_wrong_shaped_item_is_underlined_whole() { + // a parent rejects the item, not a part of it - from the list's point of view the whole + // 'key: u32 = 1' is the mistake, even though '1' on its own would be a fine list item + let underlined = |input: &str| { + let err = assert_one_err(input, |d| matches!(d, Diagnostic::UnexpectedItem { .. })); + wrap(input)[err.span].to_owned() + }; + + assert_eq!( + underlined(r#"0x1: list[u32] = { key: u32 = 1 }"#), + "key: u32 = 1" + ); + assert_eq!( + underlined(r#"0x1: list[u32] = { 0xdead = 1 }"#), + "0xdead = 1" + ); + assert_eq!( + underlined(r#"0x1: list[u32] = { "key" = 1 }"#), + r#""key" = 1"# + ); + assert_eq!( + underlined(r#"0x1: option[u32] = { key: u32 = 1 }"#), + "key: u32 = 1" + ); + assert_eq!(underlined(r#"0x1: map[hash,u32] = { 5 }"#), "5"); +} + +#[test] +fn an_unexpected_item_names_the_shape_its_parent_wants() { + let is_shape = |d: &Diagnostic| matches!(d, Diagnostic::UnexpectedItem { .. }); + + let map = assert_one_err(r#"0x1: map[hash,u32] = { 5 }"#, is_shape); + assert_eq!( + map.diagnostic.to_string(), + "map[hash,u32] takes an entry ('name: type = value')" + ); + + let class = assert_one_err( + r#" + name: string = "A" + "" + flags: u32 = 1 + "#, + is_shape, + ); + assert_eq!( + class.diagnostic.to_string(), + "embed takes an entry ('name: type = value')" + ); + + let list = assert_one_err(r#"0x1: list[u32] = { key: u32 = 1 }"#, is_shape); + assert_eq!(list.diagnostic.to_string(), "list[u32] takes a value"); +} + +/// A map entry takes its value type from the map's subtype, but writing it out is allowed - +/// it just has to agree with what the map declared. +#[test] +fn a_map_entry_may_declare_its_value_type() { + assert(r#"0x1: map[hash,u32] = { 0xdead: u32 = 1 }"#, |obj| { + obj.property( + 0x1, + values::Map::new( + PropertyKind::Hash, + PropertyKind::U32, + vec![( + values::Hash::from(BinHash::from(0xdeadu32)).into(), + values::U32::from(1u32).into(), + )], + ) + .unwrap(), + ) + }); + + let err = assert_one_err(r#"0x1: map[hash,u32] = { 0xdead: string = "a" }"#, |d| { + matches!(d, Diagnostic::TypeMismatch { .. }) + }); + assert_eq!( + err.diagnostic.to_string(), + "Type mismatch - expected u32, got string" + ); +} + +#[test] +fn an_entry_in_a_list_is_reported() { + assert_one_err(r#"0x1: list[u32] = { key: u32 = 1 }"#, |d| { + matches!( + d, + Diagnostic::UnexpectedItem { + expected: ItemShape::Value, + parent: RitoType { + base: PropertyKind::Container, + .. + }, + .. + } + ) + }); +} + +#[test] +fn a_bare_value_in_a_map_is_reported() { + assert_one_err(r#"0x1: map[hash,u32] = { 5 }"#, |d| { + matches!( + d, + Diagnostic::UnexpectedItem { + expected: ItemShape::Entry, + parent: RitoType { + base: PropertyKind::Map, + .. + }, + .. + } + ) + }); +} + +#[test] +fn an_entry_in_an_option_is_reported() { + assert_one_err(r#"0x1: option[u32] = { key: u32 = 1 }"#, |d| { + matches!( + d, + Diagnostic::UnexpectedItem { + expected: ItemShape::Value, + parent: RitoType { + base: PropertyKind::Optional, + .. + }, + .. + } + ) + }); +} + +#[test] +fn bad_option_coerce_is_reported() { + assert_one_err(r#"0x1: option[u32] = false"#, |d| { + matches!( + d, + Diagnostic::TypeMismatch { + expected: RitoTypeOrVirtual::RitoType(RitoType { + base: PropertyKind::Optional, + subtypes: [Some(PropertyKind::U32), None], + }), + got: RitoTypeOrVirtual::RitoType(RitoType { + base: PropertyKind::Optional, + subtypes: [Some(PropertyKind::Bool), None], + }), + .. + } + ) + }); +} + +/// A key that can't become a hash was dropped silently by `merge_ir`. +#[test] +fn a_property_name_that_cannot_be_hashed_is_reported() { + let err = assert_one_err(r#"true: u32 = 3"#, |d| { + matches!( + d, + Diagnostic::TypeMismatch { + expected: RitoTypeOrVirtual::RitoType(RitoType { + base: PropertyKind::Hash, + .. + }), + .. + } + ) + }); + assert_eq!( + err.diagnostic.to_string(), + "Type mismatch - expected hash, got bool" + ); +} + +#[test] +fn a_quoted_property_name_produces_the_same_property_as_a_bare_one() { + let quoted = wrap(r#""skinClassification": u32 = 1"#); + let bare = wrap(r#"skinClassification: u32 = 1"#); + + let quoted_ast = Cst::parse("ed).build_ast("ed); + let bare_ast = Cst::parse(&bare).build_ast(&bare); + + assert_eq!( + quoted_ast.diagnostics.len(), + 1, + "{:#?}", + quoted_ast.diagnostics + ); + assert!( + bare_ast.diagnostics.is_empty(), + "{:#?}", + bare_ast.diagnostics + ); + pretty_assertions::assert_eq!(quoted_ast.to_bin("ed), bare_ast.to_bin(&bare)); +} + +/// Quoted property names are hashed as written - `""` becomes `hash("")`. +#[test] +fn a_quoted_property_name_is_reported() { + let err = assert_one_err(r#""": u32 = 1"#, |d| { + matches!( + d, + Diagnostic::QuotedPropertyName { + parent: RitoType { + base: PropertyKind::Embedded, + .. + }, + .. + } + ) + }); + assert_eq!( + err.diagnostic.to_string(), + "Quoted property name - embed bodies take 'name: type = value', with the name \ + unquoted or a '0x..' hash" + ); +} + +#[test] +fn map_keys_of_every_key_type_keep_their_pair() { + for (ty, key) in [ + ("hash", r#"0x1"#), + ("hash", r#""Characters/Aatrox/Skins/Skin0""#), + ("string", r#""Characters/Aatrox/Skins/Skin0""#), + // `link` is deliberately absent: no shipped bin keys a map on it, and ltk_meta's + // map constructors reject it (Kind::is_valid_map_key) - see + // non_primitive_map_key_type_fails. + ("file", r#""ASSETS/Maps/Textures/Bloom.tex""#), + ("file", r#"0x1"#), + ("u8", "1"), + ("u16", "1"), + ("u32", "1"), + ("u64", "1"), + ("i8", "-1"), + ("i16", "-1"), + ("i32", "-1"), + ("i64", "-1"), + ("f32", "1.5"), + ] { + let input = wrap(&format!("0x1: map[{ty},u32] = {{ {key} = 1 }}")); + let ast = Cst::parse(&input).build_ast(&input); + assert!( + ast.diagnostics.is_empty(), + "map[{ty},u32] with key {key}: {:#?}", + ast.diagnostics + ); + let bin = ast.to_bin(&input); + + // No diagnostics is not the same as no data lost - that is the whole failure + // mode this change is about, so check the pair actually reached the bin. + let PropertyValueEnum::Map(map) = + &bin.objects[&BinHash(0xDEADBEEF)].properties[&BinHash(1)] + else { + panic!("map[{ty},u32] with key {key}: property is not a map"); + }; + assert_eq!( + map.entries().len(), + 1, + "map[{ty},u32] with key {key}: pair was dropped" + ); + } +} + +/// Numeric map keys are written and read bare. Upstream's `read_word` accepts only +/// `[A-Za-z0-9_+-.]`, so on `"7"` it stops at the quote and hands `read_number` an empty +/// word, which fails - see +/// . +/// +/// The syntax is rejected rather than parsed out of the quotes, so a key whose contents +/// would have been a perfectly good number (`"7"`) is reported just the same as one that +/// could never be (`"abc"`). +#[test] +fn a_quoted_numeric_map_key_is_reported() { + for ty in ["u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "f32"] { + for key in ["7", "0.5", "abc", ""] { + let errs = build_errs(&format!(r#"0x1: map[{ty},u32] = {{ "{key}" = 1 }}"#)); + assert_eq!(errs.len(), 1, "map[{ty},u32] key {key:?}: {errs:#?}"); + assert_eq!( + errs[0].diagnostic.to_string(), + format!("Type mismatch - expected {ty}, got string"), + "map[{ty},u32] key {key:?}" + ); + } + } +} + +/// Reporting a rejected key does not recover the pair - it is still missing from the +/// bin. The diagnostic is the whole of the fix here. +#[test] +fn a_rejected_map_key_still_drops_the_pair() { + let input = wrap(r#"0x1: map[u32,u32] = { "0.5" = 1 }"#); + let ast = Cst::parse(&input).build_ast(&input); + + assert_eq!(ast.diagnostics.len(), 1, "{:#?}", ast.diagnostics); + pretty_assertions::assert_eq!( + ast.to_bin(&input), + Bin::builder() + .object( + BinObject::::builder(0xDEADBEEF, 0x1234123) + .property( + 0x1, + values::Map::new(PropertyKind::U32, PropertyKind::U32, vec![]).unwrap() + ) + .build() + ) + .build() + ); +} + +/// The root `entries` map is the one every file has - its keys are quoted paths. +#[test] +fn a_quoted_root_entry_key_is_fine() { + let input = r#" +type: string = "PROP" +version: u32 = 3 +linked: list[string] = {} +entries: map[hash,embed] = { + "Characters/Aatrox/Skins/Skin0" = 0x1234123 { + 0x1: u32 = 1 + } +} +"#; + let cst = Cst::parse(input); + let errs = cst.build_ast(input).diagnostics; + assert!(errs.is_empty(), "{errs:#?}"); +} diff --git a/crates/ltk_ritobin/src/ast/to_bin.rs b/crates/ltk_ritobin/src/ast/to_bin.rs new file mode 100644 index 00000000..4291d79f --- /dev/null +++ b/crates/ltk_ritobin/src/ast/to_bin.rs @@ -0,0 +1,196 @@ +use ltk_meta::{ + property::values, traits::PropertyExt as _, Bin, BinObject, Error as MetaError, PropertyKind, + PropertyValueEnum, +}; + +use crate::{ + ast::{diagnostics::DiagnosticWithSpan, Ast, Object, RootEntry, Value}, + parse::Span, + Spanned, +}; + +pub struct PartialBin { + pub bin: Bin, + pub diagnostics: Vec, +} + +impl PartialBin { + #[allow(clippy::result_large_err)] + #[inline(always)] + pub fn into_result(self) -> Result { + if self.diagnostics.is_empty() { + Ok(self.bin) + } else { + Err(self) + } + } +} + +impl Ast { + pub fn to_bin(&self, _text: &str) -> Bin { + let objects = self + .root_entries() + .map(|RootEntry { path_hash, object }| { + let struct_val = object.to_bin_value().no_meta(); + BinObject { + path_hash: path_hash.value, + class_hash: struct_val.class_hash, + properties: struct_val.properties, + } + }) + .collect::>(); + + let dependencies: Vec = self + .roots + .linked + .clone() + .map(|linked| linked.into_inner()) + .unwrap_or_default(); + + Bin::new(objects, dependencies) + } + + pub fn into_partial_bin(self, text: &str) -> PartialBin { + let bin = self.to_bin(text); + PartialBin { + bin, + diagnostics: self.diagnostics, + } + } +} + +impl Object { + pub fn to_bin_value(&self) -> values::Struct { + values::Struct { + class_hash: self.class_hash.value, + properties: self + .properties + .iter() + .filter_map(|p| Some((p.name.value, p.value.as_ref()?.to_bin_value()?))) + .collect(), + meta: self.span, + } + } +} + +fn assert(result: Result, fallback: impl FnOnce() -> T) -> T { + match result { + Ok(v) => v, + Err(e) => { + debug_assert!(false, "ast::build should have prevented this: {e:?}"); + fallback() + } + } +} + +impl Value { + /// Recursively converts this value into an equivalent `PropertyValueEnum`. + pub fn to_bin_value(&self) -> Option> { + use PropertyValueEnum as P; + Some(match self { + Value::Unknown(_) => return None, + Value::Unresolved { kind, .. } => kind.default_value(), + Value::None(v) => P::None(values::None::new(*v)), + Value::Bool(Spanned { value, span }) => { + P::Bool(values::Bool::new_with_meta(*value, *span)) + } + Value::BitBool(Spanned { value, span }) => { + P::BitBool(values::BitBool::new_with_meta(*value, *span)) + } + Value::I8(v) => P::I8(v.clone()), + Value::U8(v) => P::U8(v.clone()), + Value::I16(v) => P::I16(v.clone()), + Value::U16(v) => P::U16(v.clone()), + Value::I32(v) => P::I32(v.clone()), + Value::U32(v) => P::U32(v.clone()), + Value::I64(v) => P::I64(v.clone()), + Value::U64(v) => P::U64(v.clone()), + Value::F32(v) => P::F32(v.clone()), + Value::Vector2(v) => P::Vector2(v.clone()), + Value::Vector3(v) => P::Vector3(v.clone()), + Value::Vector4(v) => P::Vector4(v.clone()), + Value::Matrix44(v) => P::Matrix44(v.clone()), + Value::Color(Spanned { value, span }) => { + P::Color(values::Color::new_with_meta(*value, *span)) + } + Value::String(Spanned { value, span }) => { + P::String(values::String::new_with_meta(value.clone(), *span)) + } + Value::Hash(v) => P::Hash(values::Hash::new_with_meta(v.value, v.span())), + Value::WadChunkLink(v) => { + P::WadChunkLink(values::WadChunkLink::new_with_meta(v.value, v.span())) + } + Value::ObjectLink(v) => { + P::ObjectLink(values::ObjectLink::new_with_meta(v.value, v.span())) + } + Value::Struct(s) => P::Struct(s.to_bin_value()), + Value::Embedded(s) => P::Embedded(values::Embedded(s.to_bin_value())), + Value::Container { + item_kind, + items, + span, + } => P::Container(container_from(*item_kind, items, *span)), + Value::UnorderedContainer { + item_kind, + items, + span, + } => P::UnorderedContainer(values::UnorderedContainer(container_from( + *item_kind, items, *span, + ))), + Value::Map { + key_kind, + value_kind, + entries, + span, + } => { + let mut map = assert(values::Map::empty(*key_kind, *value_kind), || { + values::Map::empty(PropertyKind::None, PropertyKind::None) + .expect("None is always a valid map key and value kind") + }); + for (k, v) in entries { + if let Some((k, v)) = k + .to_bin_value() + .zip(v.as_ref().and_then(|v| v.to_bin_value())) + { + assert(map.push(k, v), || ()); + } + } + *map.meta_mut() = *span; + P::Map(map) + } + Value::Optional { + item_kind, + value, + span, + } => { + let inner = value.as_deref().and_then(Value::to_bin_value); + let item_kind = (*item_kind)?; + let optional = assert( + values::Optional::new_with_meta(item_kind, inner, *span), + || values::Optional::empty(item_kind).unwrap_or_else(|_| none_optional(*span)), + ); + P::Optional(optional) + } + }) + } +} + +fn container_from(item_kind: PropertyKind, items: &[Value], span: Span) -> values::Container { + let mut container = assert(values::Container::empty(item_kind), || { + values::Container::empty(PropertyKind::None).expect("None is always a valid item kind") + }); + for item in items { + if let Some(value) = item.to_bin_value() { + assert(container.push(value), || ()); + } + } + *container.meta_mut() = span; + container +} + +fn none_optional(span: Span) -> values::Optional { + let mut optional = values::Optional::empty(PropertyKind::None) + .expect("None is always a valid item kind for Optional"); + *optional.meta_mut() = span; + optional +} diff --git a/crates/ltk_ritobin/src/ast/visitor.rs b/crates/ltk_ritobin/src/ast/visitor.rs new file mode 100644 index 00000000..5451c91e --- /dev/null +++ b/crates/ltk_ritobin/src/ast/visitor.rs @@ -0,0 +1,215 @@ +use std::ops::ControlFlow::{self}; + +use crate::ast::{Ast, Object, Property, RootEntry, Value}; + +#[allow(unused_variables)] +/// [Visitor pattern](https://rust-unofficial.github.io/patterns/patterns/behavioural/visitor.html) +/// for walking an [`Ast`]. +/// +/// Every AST node has a matching `enter_*`/`exit_*` pair, which are called before/after walking a +/// node & it's children. +/// +/// `enter_*` methods return an [`EnterFlow`], choosing whether to [`Descend`] into the node's +/// children. `exit_*` methods return an [`ExitFlow`], choosing whether to [`Continue`] with the +/// node's remaining siblings. Both share [`Break`], to stop or abort the walk early. +pub trait Visitor { + /// Called before walking a [`RootEntry`]'s object (see [`Self::enter_object`]). + fn enter_root_entry(&mut self, object: &RootEntry) -> EnterFlow { + Descend::Children.into() + } + /// Called after a [`RootEntry`] has been walked. + fn exit_root_entry(&mut self, object: &RootEntry) -> ExitFlow { + Continue::Siblings.into() + } + + /// Called before walking an [`Object`]'s properties. + /// (see [`Self::enter_property`]). + fn enter_object(&mut self, object: &Object) -> EnterFlow { + Descend::Children.into() + } + /// Called after an [`Object`] has been walked. + fn exit_object(&mut self, object: &Object) -> ExitFlow { + Continue::Siblings.into() + } + + /// Called before walking a [`Property`]'s children (its value - see [`Self::enter_value`]). + fn enter_property(&mut self, property: &Property) -> EnterFlow { + Descend::Children.into() + } + /// Called after a property's value has been walked. + fn exit_property(&mut self, property: &Property) -> ExitFlow { + Continue::Siblings.into() + } + + /// Called before walking a [`Value`]'s children (if it has any). + fn enter_value(&mut self, value: &Value) -> EnterFlow { + Descend::Children.into() + } + + /// Called after a value has been walked. + fn exit_value(&mut self, value: &Value) -> ExitFlow { + Continue::Siblings.into() + } +} + +pub trait VisitorExt: Sized + Visitor { + fn walk(mut self, ast: &Ast) -> Self { + ast.walk(&mut self); + self + } +} + +impl VisitorExt for T {} + +pub enum Break { + /// Stop the walk. The matching exit callback still runs for every open node, bottom-up. + Stop, + /// Abort the walk immediately. No further callbacks run. + Abort, +} + +/// The continuation returned by `exit_*` methods. +pub enum Continue { + /// With the parent's remaining children. + Siblings, + /// At the parent's exit callback: this node's exit returned [`Continue::Parent`], pruning the + /// remaining siblings. + Parent, +} + +/// The continuation returned by `enter_*` methods. +pub enum Descend { + /// Descend into this node's children. + Children, + /// Skip this node's children; its `exit_*` still runs. + Skip, +} + +/// Returned by `enter_*` [`Visitor`] methods. +pub type EnterFlow = ControlFlow; +/// Returned by `exit_*` [`Visitor`] methods. +pub type ExitFlow = ControlFlow; + +impl From for EnterFlow { + fn from(descend: Descend) -> Self { + ControlFlow::Continue(descend) + } +} + +impl From for ExitFlow { + fn from(cont: Continue) -> Self { + ControlFlow::Continue(cont) + } +} + +fn walk_inner( + visitor: &mut V, + node: &T, + enter: fn(&mut V, &T) -> EnterFlow, + exit: fn(&mut V, &T) -> ExitFlow, + children: impl FnOnce(&mut V) -> ControlFlow, +) -> ExitFlow { + let walked = match enter(visitor, node) { + ControlFlow::Break(b) => ControlFlow::Break(b), + ControlFlow::Continue(Descend::Skip) => ControlFlow::Continue(()), + ControlFlow::Continue(Descend::Children) => children(visitor), + }; + + // an abort skips the remaining exits entirely + if let ControlFlow::Break(Break::Abort) = walked { + return ControlFlow::Break(Break::Abort); + } + + match (walked, exit(visitor, node)) { + (_, ControlFlow::Break(Break::Abort)) => ControlFlow::Break(Break::Abort), + (ControlFlow::Break(Break::Stop), _) | (_, ControlFlow::Break(Break::Stop)) => { + ControlFlow::Break(Break::Stop) + } + (_, exit_result) => exit_result, + } +} + +/// Walks `items`, stopping the loop early on [`Continue::Parent`] and propagating any [`Break`]. +fn walk_all( + visitor: &mut V, + items: impl IntoIterator, + walk: fn(&mut V, T) -> ExitFlow, +) -> ControlFlow { + for item in items { + match walk(visitor, item)? { + Continue::Siblings => {} + Continue::Parent => break, + } + } + ControlFlow::Continue(()) +} + +fn walk_root_object(visitor: &mut V, object: &RootEntry) -> ExitFlow { + walk_inner( + visitor, + object, + V::enter_root_entry, + V::exit_root_entry, + |v| walk_object(v, &object.object).map_continue(|_| ()), + ) +} + +fn walk_object(visitor: &mut V, s: &Object) -> ExitFlow { + walk_inner(visitor, s, V::enter_object, V::exit_object, |v| { + walk_all(v, &s.properties, walk_property) + }) +} + +fn walk_property(visitor: &mut V, property: &Property) -> ExitFlow { + walk_inner( + visitor, + property, + V::enter_property, + V::exit_property, + |v| { + property + .value + .as_ref() + .map(|value| walk_value(v, value).map_continue(|_| ())) + .unwrap_or(ControlFlow::Continue(())) + }, + ) +} + +fn walk_value(visitor: &mut V, value: &Value) -> ExitFlow { + walk_inner( + visitor, + value, + V::enter_value, + V::exit_value, + |v| match value { + Value::Struct(s) | Value::Embedded(s) => walk_object(v, s).map_continue(|_| ()), + Value::Container { items, .. } | Value::UnorderedContainer { items, .. } => { + walk_all(v, items, walk_value) + } + Value::Map { entries, .. } => walk_all(v, entries, |v, (key, value)| { + match walk_value(v, key)? { + Continue::Siblings => {} + // the key's exit pruned the entry's remaining sibling (its value) and, + // transitively, the rest of the map's entries + Continue::Parent => return Continue::Parent.into(), + } + match value { + Some(value) => walk_value(v, value), + None => Continue::Siblings.into(), + } + }), + Value::Optional { + value: Some(inner), .. + } => walk_value(v, inner).map_continue(|_| ()), + _ => ControlFlow::Continue(()), + }, + ) +} + +impl Ast { + /// Walk a [`Visitor`] over every object in this tree. + pub fn walk(&self, visitor: &mut V) { + let _ = walk_all(visitor, self.root_entries(), walk_root_object); + } +} diff --git a/crates/ltk_ritobin/src/cst/builder.rs b/crates/ltk_ritobin/src/cst/builder.rs index bcfb93cb..8dc38409 100644 --- a/crates/ltk_ritobin/src/cst/builder.rs +++ b/crates/ltk_ritobin/src/cst/builder.rs @@ -494,12 +494,14 @@ mod test { "errors parsing ritobin - {:#?}", cst2.errors ); - let (bin2, errors) = cst2.build_bin(&str); + let partial2 = cst2.build_bin(&str); assert!( - errors.is_empty(), - "errors building tree from reparsed ritobin - {errors:#?}" + partial2.diagnostics.is_empty(), + "errors building tree from reparsed ritobin - {:#?}", + partial2.diagnostics ); + let bin2 = partial2.bin; pretty_assertions::assert_eq!(bin2, bin); } diff --git a/crates/ltk_ritobin/src/cst/tree.rs b/crates/ltk_ritobin/src/cst/tree.rs index 975f5cf4..45723387 100644 --- a/crates/ltk_ritobin/src/cst/tree.rs +++ b/crates/ltk_ritobin/src/cst/tree.rs @@ -1,8 +1,7 @@ use std::fmt::{self, Display}; -use ltk_meta::Bin; - use crate::{ + ast::PartialBin, cst::{ visitor::{Visit, VisitCtx}, ChildRange, ErrorRange, NodeId, TokenId, Visitor, @@ -12,7 +11,6 @@ use crate::{ tokenizer::{self, Token}, Error, ErrorPropagation, Parser, Span, TokenKind, }, - typecheck::diagnostics::DiagnosticWithSpan, }; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -143,6 +141,51 @@ impl Node { .filter(|t| t.kind == TokenKind::LCurly) .map_or(self.span, |t| t.span) } + + pub fn trimmed_span(&self, cst: &Cst) -> Span { + let children = self.children.get(cst); + match self.kind { + Kind::Entry => { + let key = children.find_tree(cst, Kind::EntryKey); + let value = children.find_tree(cst, Kind::EntryValue); + match (key, value) { + (Some(key), Some(value)) => Span::new(key.span.start, value.span.end), + _ => self.span, + } + } + Kind::ListItem => children + .iter() + .find_map(|c| c.tree(cst)) + .map(|t| t.span) + .unwrap_or(self.span), + _ => self.span, + } + } +} + +pub trait ChildrenExt { + fn find_tree<'c>(&'c self, cst: &'c Cst, kind: Kind) -> Option<&'c Node>; + fn find_token<'c>(&'c self, cst: &'c Cst, kind: TokenKind) -> Option<&'c Token>; +} + +impl ChildrenExt for [Child] { + fn find_tree<'c>(&'c self, cst: &'c Cst, kind: Kind) -> Option<&'c Node> { + self.iter() + .find_map(|c| c.tree(cst).filter(|t| t.kind == kind)) + } + fn find_token<'c>(&'c self, cst: &'c Cst, kind: TokenKind) -> Option<&'c Token> { + self.iter() + .find_map(|c| c.token(cst).filter(|t| t.kind == kind)) + } +} + +impl ChildrenExt for ChildRange { + fn find_tree<'c>(&'c self, cst: &'c Cst, kind: Kind) -> Option<&'c Node> { + self.get(cst).find_tree(cst, kind) + } + fn find_token<'c>(&'c self, cst: &'c Cst, kind: TokenKind) -> Option<&'c Token> { + self.get(cst).find_token(cst, kind) + } } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -210,12 +253,11 @@ impl Cst { p.build_tree(error_propagation) } - /// Construct a best-effort [`Bin`] from this tree, returning any errors. If there are any - /// errors returned, the [`Bin`] may only be partially constructed. - pub fn build_bin(&self, text: &str) -> (Bin, Vec) { - let mut checker = crate::typecheck::TypeChecker::new(text); - self.walk(&mut checker); - checker.collect_to_bin() + /// Construct a best-effort [`Bin`] from this tree, along with any diagnostics. If there + /// are any diagnostics, the [`Bin`] may only be partially/best-effort constructed - use + /// [`PartialBin::finish`] to get a quick Result type. + pub fn build_bin(&self, text: &str) -> PartialBin { + self.build_ast(text).into_partial_bin(text) } /// Print this tree to a string for debugging purposes. This does **NOT** output ritobin, see [`crate::Print`] for diff --git a/crates/ltk_ritobin/src/lib.rs b/crates/ltk_ritobin/src/lib.rs index dd23bf0f..83bb6898 100644 --- a/crates/ltk_ritobin/src/lib.rs +++ b/crates/ltk_ritobin/src/lib.rs @@ -21,11 +21,11 @@ //! let cst = Cst::parse(text); //! assert!(cst.errors.is_empty()); //! -//! let (bin, bin_errors) = cst.build_bin(text); -//! assert!(bin_errors.is_empty()); +//! let partial = cst.build_bin(text); +//! assert!(partial.diagnostics.is_empty()); //! //! // Write back to text -//! let output = bin.print().unwrap(); +//! let output = partial.bin.print().unwrap(); //! //! assert_eq!(text, output); //! ``` @@ -73,19 +73,23 @@ //! assert_eq!(cst.errors.len(), 1); // the unexpected "!!" in the value //! ``` //! -//! `Cst::build_bin` follows the same philosophy: it returns `(Bin, Vec)` -//! so type errors don't prevent you from getting a best-effort `Bin` back. This matters for -//! editor use cases: between keystrokes a buffer is almost always temporarily invalid, and -//! tooling still needs to render it, navigate it, and report problems with precise spans. +//! `Cst::build_bin` follows the same philosophy: it returns a [`ast::PartialBin`], pairing a +//! best-effort `Bin` with any diagnostics, so type errors don't prevent you from getting a +//! `Bin` back. This matters for editor use cases: between keystrokes a buffer is almost always +//! temporarily invalid, and tooling still needs to render it, navigate it, and report problems +//! with precise spans. Use [`ast::PartialBin::finish`] where you instead want a `Result` that +//! only succeeds on a clean build. + +use std::ops::{Deref, DerefMut}; #[allow(unused, reason = "for module level doc link")] use ltk_meta::Bin; +pub mod ast; pub mod cst; pub mod hashes; pub mod parse; pub mod print; -pub mod typecheck; pub mod types; pub use hashes::*; @@ -94,3 +98,60 @@ pub use types::*; pub use cst::Cst; pub use cst::Node; pub use print::Print; + +use crate::parse::Span; + +pub trait SpannedExt { + fn with_span(self, span: Span) -> Spanned + where + Self: Sized; +} + +impl SpannedExt for T { + fn with_span(self, span: Span) -> Spanned { + Spanned::new(span, self) + } +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Spanned { + pub span: Span, + pub value: T, +} + +impl Spanned { + pub fn new(span: Span, value: T) -> Self { + Self { span, value } + } +} + +impl DerefMut for Spanned { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + +impl Deref for Spanned { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl AsMut for Spanned { + fn as_mut(&mut self) -> &mut T { + &mut self.value + } +} + +impl AsRef for Spanned { + fn as_ref(&self) -> &T { + &self.value + } +} + +impl AsRef for Spanned { + fn as_ref(&self) -> &str { + &self.value + } +} diff --git a/crates/ltk_ritobin/src/literals.rs b/crates/ltk_ritobin/src/literals.rs new file mode 100644 index 00000000..42033eea --- /dev/null +++ b/crates/ltk_ritobin/src/literals.rs @@ -0,0 +1,253 @@ +use std::{borrow::Cow, fmt::Debug, str::FromStr}; + +use ltk_hash::{BinHash, Hash as _, WadHash}; +use ltk_meta::{property::values, PropertyKind, PropertyValueEnum}; + +use crate::{ + parse::{Span, Token, TokenKind}, + typecheck::diagnostics::{Diagnostic, RitoTypeOrVirtual}, + RitoType, +}; + +use Diagnostic::*; + +pub trait CanCoerce { + fn can_coerce(self, from: Self) -> bool; +} + +pub trait CoerceFrom { + fn coerce_from( + self, + value: PropertyValueEnum, + ) -> Option>; +} + +impl CanCoerce for PropertyKind { + fn can_coerce(self, from: Self) -> bool { + let to = self; + if to == from { + return true; + } + use PropertyKind as K; + match (to, from) { + (K::Optional, from) if !from.is_container() => true, + (K::Hash, K::String) + | (K::WadChunkLink | K::ObjectLink, K::Hash | K::String) + | (K::BitBool | K::Bool, K::Bool | K::BitBool) => true, + _ => false, + } + } +} +impl CanCoerce for RitoType { + fn can_coerce(self, from: Self) -> bool { + if !self.base.can_coerce(from.base) { + return false; + } + for i in 0..1 { + if (self.subtypes[i].zip(from.subtypes[i])) + .is_some_and(|(to, from)| !to.can_coerce(from)) + { + return false; + } + } + true + } +} +impl CoerceFrom for PropertyKind { + fn coerce_from( + self, + value: PropertyValueEnum, + ) -> Option> { + let to = self; + match to { + to if to == value.kind() => Some(value), + + PropertyKind::Optional => Some(values::Optional::try_from(value).ok()?.into()), + + PropertyKind::Hash => match value { + PropertyValueEnum::String(str) => { + Some(values::Hash::new_with_meta(BinHash::hash_str(&str), str.meta).into()) + } + _ => None, + }, + PropertyKind::ObjectLink => match value { + PropertyValueEnum::Hash(hash) => { + Some(values::ObjectLink::new_with_meta(*hash, hash.meta).into()) + } + PropertyValueEnum::String(str) => Some( + values::ObjectLink::new_with_meta(BinHash::hash_str(&str), str.meta).into(), + ), + _ => None, + }, + PropertyKind::WadChunkLink => match value { + PropertyValueEnum::Hash(hash) => Some( + values::WadChunkLink::new_with_meta(WadHash((**hash).into()), hash.meta).into(), + ), + PropertyValueEnum::String(str) => Some( + values::WadChunkLink::new_with_meta(WadHash::hash_str(str.as_str()), str.meta) + .into(), + ), + _ => None, + }, + PropertyKind::BitBool => match value { + PropertyValueEnum::Bool(bool) => { + Some(values::BitBool::new_with_meta(*bool, bool.meta).into()) + } + _ => None, + }, + PropertyKind::Bool => match value { + PropertyValueEnum::BitBool(bool) => { + Some(values::Bool::new_with_meta(*bool, bool.meta).into()) + } + _ => None, + }, + _ => None, + } + } +} + +pub(crate) fn eval_unknown_hash( + text: &str, + span: Span, +) -> Result, Diagnostic> { + // TODO: better errs here? + let src = text[span].strip_prefix("0x").ok_or(InvalidHash(span))?; + + // since we can't know whether bin/wad was intended, we will just try fit it in the smallest hash that allows it. + // we can then safely coerce the type upwards when we are given type information + Ok(match BinHash::from_str_radix(src, 16) { + Ok(hash) => PropertyValueEnum::Hash(values::Hash::new_with_meta(hash, span)), + Err(_) => match WadHash::from_str_radix(src, 16) { + Ok(hash) => { + PropertyValueEnum::WadChunkLink(values::WadChunkLink::new_with_meta(hash, span)) + } + Err(_) => return Err(InvalidHash(span)), + }, + }) +} + +// pub(crate) fn eval_hash( +// text: &str, +// span: Span, +// ) -> Result { +// // TODO: better errs here? +// let src = text[span].strip_prefix("0x").ok_or(InvalidHash(span))?; +// H::from_str(src).map_err(|_| InvalidHash(span)) +// } + +pub(crate) fn parse_int>( + txt: &str, + kind_hint: PropertyKind, + span: Span, + wrap: impl FnOnce(T, Span) -> PropertyValueEnum, +) -> Result, Diagnostic> { + txt.parse::() + .map(|v| wrap(v, span)) + .map_err(|e| Diagnostic::ParseNumericError { + expected: kind_hint, + error: Some(*e.kind()), + span, + }) +} + +/// Evaluate a literal token into a value +/// +/// # Errors +/// If the literal does not fit `kind_hint`, or if it is ambiguous and there is no hint to pick +/// with - a bare `5` on its own has no type. +pub(crate) fn eval( + text: &str, + token: &Token, + kind_hint: Option, + kind_hint_span: Option, +) -> Result>, Diagnostic> { + use PropertyKind as K; + use PropertyValueEnum as P; + Ok(Some(match token { + Token { + kind: TokenKind::String, + span, + } => values::String::new_with_meta( + text[Span::new(span.start + 1, span.end - 1)].into(), + *span, + ) + .into(), + + Token { + kind: TokenKind::True, + span, + } => values::Bool::new_with_meta(true, *span).into(), + Token { + kind: TokenKind::False, + span, + } => values::Bool::new_with_meta(false, *span).into(), + + Token { + kind: TokenKind::HexLit, + span, + } => eval_unknown_hash(text, *span)?, + Token { + kind: TokenKind::Number, + span, + } => { + let txt = &text[span]; + let Some(kind_hint) = kind_hint else { + return Err(AmbiguousNumeric(*span)); + }; + + let txt = match txt.contains('_') { + true => Cow::Owned(txt.replace('_', "")), + false => Cow::Borrowed(txt), + }; + + let kind_hint = match kind_hint.base { + K::Optional => kind_hint.value_subtype().unwrap(), + base => base, + }; + + match kind_hint { + K::U8 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::U8(values::U8::new_with_meta(v, s)) + })?, + K::U16 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::U16(values::U16::new_with_meta(v, s)) + })?, + K::U32 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::U32(values::U32::new_with_meta(v, s)) + })?, + K::U64 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::U64(values::U64::new_with_meta(v, s)) + })?, + K::I8 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::I8(values::I8::new_with_meta(v, s)) + })?, + K::I16 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::I16(values::I16::new_with_meta(v, s)) + })?, + K::I32 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::I32(values::I32::new_with_meta(v, s)) + })?, + K::I64 => parse_int::(&txt, kind_hint, *span, |v, s| { + P::I64(values::I64::new_with_meta(v, s)) + })?, + K::F32 => P::F32(values::F32::new_with_meta( + txt.parse().map_err(|_| Diagnostic::ParseNumericError { + expected: kind_hint, + error: None, + span: *span, + })?, + *span, + )), + _ => { + return Err(TypeMismatch { + span: *span, + expected: RitoType::simple(kind_hint), + expected_span: kind_hint_span, + got: RitoTypeOrVirtual::numeric(), + }); + } + } + } + _ => return Ok(None), + })) +} diff --git a/crates/ltk_ritobin/src/parse.rs b/crates/ltk_ritobin/src/parse.rs index 3aecd53e..d76e7fd6 100644 --- a/crates/ltk_ritobin/src/parse.rs +++ b/crates/ltk_ritobin/src/parse.rs @@ -29,7 +29,6 @@ mod test { use crate::{ cst::{Cst, Kind}, print::CstPrinter, - typecheck::TypeChecker, }; fn assert_success(text: &str) -> Cst { @@ -167,9 +166,9 @@ linked: list[string] = { } "#; let cst = assert_success(text); - let (_bin, errors) = cst.build_bin(text); + let partial = cst.build_bin(text); assert!( - !errors.is_empty(), + !partial.diagnostics.is_empty(), "There should be an error for the naked 'ooo' in the class block" ); } @@ -202,14 +201,12 @@ entries: map[hash, embed] = { assert!(errors.is_empty()); - let mut checker = TypeChecker::new(text); - cst.walk(&mut checker); - - let (tree, errors) = checker.collect_to_bin(); + let ast = cst.build_ast(&str); + let tree = ast.to_bin(&str); eprintln!("{str}\n====== type errors: ======\n"); for err in errors { - eprintln!("{:?}: {:#?}", &text[err.span], err.diagnostic); + eprintln!("{:?}: {:#?}", &text[err.span], err); } eprintln!("==== FINAL TREE =====\n{tree:#?}"); diff --git a/crates/ltk_ritobin/src/parse/span.rs b/crates/ltk_ritobin/src/parse/span.rs index ccf9fd4c..821e9dd5 100644 --- a/crates/ltk_ritobin/src/parse/span.rs +++ b/crates/ltk_ritobin/src/parse/span.rs @@ -1,7 +1,9 @@ +use std::cmp; + /// A span of text in the source file - `[start, end)` in bytes. /// `end` marks the offset after the last byte of the span #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub struct Span { pub start: u32, pub end: u32, @@ -14,12 +16,25 @@ impl Span { Self { start, end } } - /// Whether this span contains `offset` + #[must_use] + #[inline] + /// Create a zero-length span at the specified offset (at..at) + pub fn empty(at: u32) -> Self { + Self { start: at, end: at } + } + + /// Whether this span contains `offset`. The end index is considered excluded. #[must_use] #[inline] pub fn contains(&self, offset: u32) -> bool { self.start <= offset && offset < self.end } + /// Whether this span contains `offset`. The end index is considered included. + #[must_use] + #[inline] + pub fn contains_inclusive(&self, offset: u32) -> bool { + self.start <= offset && offset <= self.end + } /// Whether two span ranges intersect #[must_use] @@ -41,6 +56,24 @@ impl Span { pub fn is_empty(&self) -> bool { self.end <= self.start } + + /// Extend the span to cover another span + #[must_use] + #[inline] + pub fn cover(self, other: Span) -> Self { + let start = cmp::min(self.start, other.start); + let end = cmp::max(self.end, other.end); + Self::new(start, end) + } + + /// Extend the span to cover the given offset + #[must_use] + #[inline] + pub fn cover_offset(self, offset: u32) -> Self { + let start = cmp::min(self.start, offset); + let end = cmp::max(self.end, offset); + Self::new(start, end) + } } impl std::ops::Index for str { diff --git a/crates/ltk_ritobin/src/typecheck.rs b/crates/ltk_ritobin/src/typecheck.rs deleted file mode 100644 index 3239d52c..00000000 --- a/crates/ltk_ritobin/src/typecheck.rs +++ /dev/null @@ -1,797 +0,0 @@ -//! CST visitor that validates and resolves types into Bin's -//! -//! You should only use types from here directly if you know what you are doing - see -//! [`crate::Cst::build_bin`] -//! -//! TODO: better explanation of the type checking impl - -pub mod diagnostics; -pub mod ir; -pub mod state; - -mod collect; -mod listlikes; -mod resolve; -mod trace; -mod walk; - -pub use state::TypeChecker; - -#[cfg(test)] -mod test { - use glam::{Vec3, Vec4}; - use ltk_hash::BinHash; - use ltk_meta::{ - property::{values, NoMeta}, - Bin, BinObject, ObjectBuilder, PropertyKind, PropertyValueEnum, - }; - - use crate::{ - typecheck::diagnostics::{Diagnostic, DiagnosticWithSpan, RootKind}, - Cst, ItemShape, RitoType, - }; - - fn wrap(input: &str) -> String { - format!( - r#" -#PROP_text -type: string = "PROP" -version: u32 = 3 -linked: list[string] = {{}} -entries: map[hash,embed] = {{ - 0xDEADBEEF = 0x1234123 {{ - {input} - }} -}}"# - ) - } - - fn assert ObjectBuilder>(input: &str, is: F) { - let input = wrap(input); - - let cst = Cst::parse(&input); - let mut str = String::new(); - - cst.print(&mut str, &input); - eprintln!("#### CST:\n{str}"); - - let (bin, errs) = cst.build_bin(&input); - - assert!(errs.is_empty(), "Typecheck errors: {:#?}", errs); - - let obj = (is)(BinObject::::builder(0xDEADBEEF, 0x1234123)).build(); - pretty_assertions::assert_eq!(bin, Bin::builder().object(obj).build()); - } - - /// Builds a full object body (see [`wrap`]) from `input` and returns the - /// typecheck diagnostics without asserting they're empty - for exercising - /// error paths. - fn build_errs(input: &str) -> Vec { - let input = wrap(input); - let cst = Cst::parse(&input); - let (_, errs) = cst.build_bin(&input); - errs - } - - #[test] - fn option_coerce() { - assert(r#"0x1: option[vec3] = { 0.5, 5.3, -0.20 }"#, |obj| { - obj.property( - 0x1, - values::Optional::from(values::Vector3::from(Vec3::new(0.5, 5.3, -0.2))), - ) - }); - } - - #[test] - fn list() { - assert( - r#" - values: list[vec4] = { - { 1, 1, 1, 1 } - { 1, 1, 1, 1 } - { 1, 1, 1, 0 } - } - "#, - |obj| { - obj.property( - 0x34474c3b, - values::Container::from_iter([ - values::Vector4::from(Vec4::new(1., 1., 1., 1.)), - values::Vector4::from(Vec4::new(1., 1., 1., 1.)), - values::Vector4::from(Vec4::new(1., 1., 1., 0.)), - ]), - ) - }, - ); - } - - #[test] - fn u8_map() { - assert( - r#" - 0xe6d60f41: map[u8,string] = { - 1 = "hello" - } - "#, - |obj| { - obj.property( - 0xe6d60f41, - values::Map::new( - PropertyKind::U8, - PropertyKind::String, - vec![( - values::U8::from(1).into(), - values::String::from("hello").into(), - )], - ) - .unwrap(), - ) - }, - ); - } - - #[test] - fn matrix() { - assert( - r#" - 0x1: mtx44 = { - 0.1, 0.2, 0.3, 0.4, - 1.1, 1.2, 1.3, 1.4, - 2.1, 2.2, 2.3, 2.4, - 3.1, 3.2, 3.3, 3.4 - } - "#, - |obj| { - obj.property( - 0x1, - values::Matrix44::from(glam::Mat4::from_cols_array_2d(&[ - [0.1, 1.1, 2.1, 3.1], - [0.2, 1.2, 2.2, 3.2], - [0.3, 1.3, 2.3, 3.3], - [0.4, 1.4, 2.4, 3.4], - ])), - ) - }, - ); - } - - #[test] - fn numeric_parse_error() { - let errs = build_errs("0x1: u8 = 999999"); - assert_eq!(errs.len(), 1, "{errs:#?}"); - assert!( - matches!( - errs[0].diagnostic, - Diagnostic::ParseNumericError { - expected: PropertyKind::U8, - .. - } - ), - "{:#?}", - errs[0] - ); - } - - #[test] - fn subtype_count_mismatch_too_many() { - // Container/list takes exactly 1 subtype - let errs = build_errs("0x1: list[u8,u8] = {}"); - assert_eq!(errs.len(), 1, "{errs:#?}"); - assert!( - matches!( - errs[0].diagnostic, - Diagnostic::SubtypeCountMismatch { - expected: 1, - got: 2, - .. - } - ), - "{:#?}", - errs[0] - ); - } - - #[test] - fn subtype_count_mismatch_too_few() { - // Map takes exactly 2 subtypes - let errs = build_errs("0x1: map[u8] = {}"); - assert_eq!(errs.len(), 1, "{errs:#?}"); - assert!( - matches!( - errs[0].diagnostic, - Diagnostic::SubtypeCountMismatch { - expected: 2, - got: 1, - .. - } - ), - "{:#?}", - errs[0] - ); - } - - #[test] - fn missing_linked_root_entry_reports_diagnostic_without_panicking() { - let input = r#" -type: string = "PROP" -version: u32 = 3 -entries: map[hash,embed] = {} -"#; - let cst = Cst::parse(input); - let (_, errs) = cst.build_bin(input); - assert!( - errs.iter().any(|e| matches!( - e.diagnostic, - Diagnostic::MissingRootEntry { - root_kind: RootKind::Linked - } - )), - "{errs:#?}" - ); - } - - #[test] - fn missing_entries_root_entry_reports_diagnostic_without_panicking() { - let input = r#" -type: string = "PROP" -version: u32 = 3 -linked: list[string] = {} -"#; - let cst = Cst::parse(input); - let (_, errs) = cst.build_bin(input); - assert!( - errs.iter().any(|e| matches!( - e.diagnostic, - Diagnostic::MissingRootEntry { - root_kind: RootKind::Entries - } - )), - "{errs:#?}" - ); - } - - #[test] - fn missing_type_root_entry_reports_type_not_version() { - let input = r#" -version: u32 = 3 -linked: list[string] = {} -entries: map[hash,embed] = {} -"#; - let cst = Cst::parse(input); - let (_, errs) = cst.build_bin(input); - assert!( - errs.iter().any(|e| matches!( - e.diagnostic, - Diagnostic::MissingRootEntry { - root_kind: RootKind::Type - } - )), - "{errs:#?}" - ); - } - - #[test] - fn invalid_type_root_entry_reports_type_not_version() { - let input = r#" -type: u32 = 3 -version: u32 = 3 -linked: list[string] = {} -entries: map[hash,embed] = {} -"#; - let cst = Cst::parse(input); - let (_, errs) = cst.build_bin(input); - assert!( - errs.iter().any(|e| matches!( - e.diagnostic, - Diagnostic::InvalidRootEntryType { - root_kind: RootKind::Type, - .. - } - )), - "{errs:#?}" - ); - } - - #[test] - fn empty_vec3_reports_not_enough_items() { - let errs = build_errs("0x1: vec3 = {}"); - assert_eq!(errs.len(), 1, "{errs:#?}"); - assert!( - matches!( - errs[0].diagnostic, - Diagnostic::NotEnoughItems { got: 0, .. } - ), - "{:#?}", - errs[0] - ); - } - - #[test] - fn empty_color_reports_not_enough_items() { - let errs = build_errs("0x1: rgba = {}"); - assert_eq!(errs.len(), 1, "{errs:#?}"); - assert!( - matches!( - errs[0].diagnostic, - Diagnostic::NotEnoughItems { got: 0, .. } - ), - "{:#?}", - errs[0] - ); - } - - #[test] - fn empty_mtx44_reports_not_enough_items() { - let errs = build_errs("0x1: mtx44 = {}"); - assert_eq!(errs.len(), 1, "{errs:#?}"); - assert!( - matches!( - errs[0].diagnostic, - Diagnostic::NotEnoughItems { got: 0, .. } - ), - "{:#?}", - errs[0] - ); - } - - /// Asserts `input` produces exactly one diagnostic, and hands it to `is`. - fn assert_one_err bool>(input: &str, is: F) -> DiagnosticWithSpan { - let errs = build_errs(input); - assert_eq!(errs.len(), 1, "{errs:#?}"); - assert!((is)(&errs[0].diagnostic), "{:#?}", errs[0]); - errs[0] - } - - /// `pointer`/`embed` values are written `ClassName { .. }`. Deleting the class name - /// used to default-construct a class-hash-0 struct without a word. - #[test] - fn missing_class_name_in_a_list_item_is_reported() { - let err = assert_one_err( - r#" - paramValues: list[embed] = { - StaticMaterialShaderParamDef { - name: string = "A" - } - { - name: string = "B" - } - } - "#, - |d| { - matches!( - d, - Diagnostic::MissingClassName { - expected: RitoType { - base: PropertyKind::Embedded, - .. - }, - .. - } - ) - }, - ); - // points at the `{` the class name should precede, not the whole block - assert_eq!(err.span.len(), 1); - assert_eq!( - err.diagnostic.to_string(), - "Missing class name - embed values are written 'ClassName { .. }'" - ); - } - - #[test] - fn missing_class_name_in_a_map_entry_is_reported() { - assert_one_err( - r#" - items: map[hash,pointer] = { - 0xc8fd50ab = { - name: string = "a" - } - } - "#, - |d| { - matches!( - d, - Diagnostic::MissingClassName { - expected: RitoType { - base: PropertyKind::Struct, - .. - }, - .. - } - ) - }, - ); - } - - /// A block list item is how nested containers are written - that must stay quiet. - #[test] - fn a_block_list_item_in_a_nested_container_is_fine() { - let errs = build_errs(r#"0x1: list[list[u32]] = { { 1 2 } { 3 4 } }"#); - assert!(errs.is_empty(), "{errs:#?}"); - } - - /// So must one that is properly introduced by a class name. - #[test] - fn a_named_class_list_item_is_fine() { - let errs = build_errs( - r#" - paramValues: list[embed] = { - StaticMaterialShaderParamDef { - name: string = "A" - } - 0xdeadbeef { - name: string = "B" - } - } - "#, - ); - assert!(errs.is_empty(), "{errs:#?}"); - } - - /// The reported case: `""` where a property name belongs. `merge_ir` used to drop any - /// child whose shape didn't fit, with a `trace!` and no diagnostic. - #[test] - fn a_bare_value_in_a_class_body_is_reported() { - assert_one_err( - r#" - name: string = "A" - "" - flags: u32 = 1 - "#, - |d| { - matches!( - d, - Diagnostic::UnexpectedItem { - expected: ItemShape::Entry, - parent: RitoType { - base: PropertyKind::Embedded, - .. - }, - .. - } - ) - }, - ); - } - - #[test] - fn a_type_mismatch_blames_the_type_expression_that_set_it() { - // the LSP renders `expected_span` as "due to this type expression", so it has to point - // at an actual type expression - not at the container's braces, and not at all when the - // expectation was not written down anywhere - let blamed = |input: &str| { - let text = wrap(input); - let (_, errs) = Cst::parse(&text).build_bin(&text); - errs.into_iter() - .find_map(|e| match e.diagnostic { - Diagnostic::TypeMismatch { expected_span, .. } => Some(expected_span), - _ => None, - }) - .expect("expected a TypeMismatch") - .map(|span| text[span].to_owned()) - }; - - assert_eq!( - blamed(r#"0x1: list[u32] = { "a" }"#).as_deref(), - Some("list[u32]") - ); - assert_eq!( - blamed(r#"0x1: map[u32,u32] = { "5" = 1 }"#).as_deref(), - Some("map[u32,u32]") - ); - assert_eq!( - blamed(r#"0x1: option[u32] = { "a" }"#).as_deref(), - Some("option[u32]") - ); - // a numeric literal resolved against a hint that takes no number - assert_eq!(blamed(r#"0x1: string = 5"#).as_deref(), Some("string")); - // listlike components answer to the type that made them components - assert_eq!( - blamed(r#"0x1: vec3 = { 1, "a", 3 }"#).as_deref(), - Some("vec3") - ); - assert_eq!( - blamed(r#"0x1: rgba = { 1, "a", 3, 4 }"#).as_deref(), - Some("rgba") - ); - // ... and a listlike written as a list item falls back to the container's subtype - assert_eq!( - blamed(r#"0x1: list[vec3] = { { 1, "a", 3 } }"#).as_deref(), - Some("list[vec3]") - ); - // a property name is a hash because it is a property name - no type expression said so - assert_eq!(blamed("true: u32 = 3").as_deref(), None); - } - - #[test] - fn a_wrong_shaped_item_is_underlined_whole() { - // a parent rejects the item, not a part of it - from the list's point of view the whole - // 'key: u32 = 1' is the mistake, even though '1' on its own would be a fine list item - let underlined = |input: &str| { - let err = assert_one_err(input, |d| matches!(d, Diagnostic::UnexpectedItem { .. })); - wrap(input)[err.span].to_owned() - }; - - assert_eq!( - underlined(r#"0x1: list[u32] = { key: u32 = 1 }"#), - "key: u32 = 1" - ); - assert_eq!( - underlined(r#"0x1: list[u32] = { 0xdead = 1 }"#), - "0xdead = 1" - ); - assert_eq!( - underlined(r#"0x1: list[u32] = { "key" = 1 }"#), - r#""key" = 1"# - ); - assert_eq!( - underlined(r#"0x1: option[u32] = { key: u32 = 1 }"#), - "key: u32 = 1" - ); - assert_eq!(underlined(r#"0x1: map[hash,u32] = { 5 }"#), "5"); - } - - #[test] - fn an_unexpected_item_names_the_shape_its_parent_wants() { - let is_shape = |d: &Diagnostic| matches!(d, Diagnostic::UnexpectedItem { .. }); - - let map = assert_one_err(r#"0x1: map[hash,u32] = { 5 }"#, is_shape); - assert_eq!( - map.diagnostic.to_string(), - "map[hash,u32] takes an entry ('name: type = value')" - ); - - let class = assert_one_err( - r#" - name: string = "A" - "" - flags: u32 = 1 - "#, - is_shape, - ); - assert_eq!( - class.diagnostic.to_string(), - "embed takes an entry ('name: type = value')" - ); - - let list = assert_one_err(r#"0x1: list[u32] = { key: u32 = 1 }"#, is_shape); - assert_eq!(list.diagnostic.to_string(), "list[u32] takes a value"); - } - - /// A map entry takes its value type from the map's subtype, but writing it out is allowed - - /// it just has to agree with what the map declared. - #[test] - fn a_map_entry_may_declare_its_value_type() { - assert(r#"0x1: map[hash,u32] = { 0xdead: u32 = 1 }"#, |obj| { - obj.property( - 0x1, - values::Map::new( - PropertyKind::Hash, - PropertyKind::U32, - vec![( - values::Hash::from(BinHash::from(0xdeadu32)).into(), - values::U32::from(1u32).into(), - )], - ) - .unwrap(), - ) - }); - - let err = assert_one_err(r#"0x1: map[hash,u32] = { 0xdead: string = "a" }"#, |d| { - matches!(d, Diagnostic::TypeMismatch { .. }) - }); - assert_eq!( - err.diagnostic.to_string(), - "Type mismatch - expected u32, got string" - ); - } - - #[test] - fn an_entry_in_a_list_is_reported() { - assert_one_err(r#"0x1: list[u32] = { key: u32 = 1 }"#, |d| { - matches!( - d, - Diagnostic::UnexpectedItem { - expected: ItemShape::Value, - parent: RitoType { - base: PropertyKind::Container, - .. - }, - .. - } - ) - }); - } - - #[test] - fn a_bare_value_in_a_map_is_reported() { - assert_one_err(r#"0x1: map[hash,u32] = { 5 }"#, |d| { - matches!( - d, - Diagnostic::UnexpectedItem { - expected: ItemShape::Entry, - parent: RitoType { - base: PropertyKind::Map, - .. - }, - .. - } - ) - }); - } - - #[test] - fn an_entry_in_an_option_is_reported() { - assert_one_err(r#"0x1: option[u32] = { key: u32 = 1 }"#, |d| { - matches!( - d, - Diagnostic::UnexpectedItem { - expected: ItemShape::Value, - parent: RitoType { - base: PropertyKind::Optional, - .. - }, - .. - } - ) - }); - } - - /// A key that can't become a hash was dropped silently by `merge_ir`. - #[test] - fn a_property_name_that_cannot_be_hashed_is_reported() { - let err = assert_one_err(r#"true: u32 = 3"#, |d| { - matches!( - d, - Diagnostic::TypeMismatch { - expected: RitoType { - base: PropertyKind::Hash, - .. - }, - .. - } - ) - }); - assert_eq!( - err.diagnostic.to_string(), - "Type mismatch - expected hash, got bool" - ); - } - - #[test] - fn a_quoted_property_name_produces_the_same_property_as_a_bare_one() { - let quoted = wrap(r#""skinClassification": u32 = 1"#); - let bare = wrap(r#"skinClassification: u32 = 1"#); - - let (quoted_bin, quoted_errs) = Cst::parse("ed).build_bin("ed); - let (bare_bin, bare_errs) = Cst::parse(&bare).build_bin(&bare); - - assert_eq!(quoted_errs.len(), 1, "{quoted_errs:#?}"); - assert!(bare_errs.is_empty(), "{bare_errs:#?}"); - pretty_assertions::assert_eq!(quoted_bin, bare_bin); - } - - /// Quoted property names are hashed as written - `""` becomes `hash("")`. - #[test] - fn a_quoted_property_name_is_reported() { - let err = assert_one_err(r#""": u32 = 1"#, |d| { - matches!( - d, - Diagnostic::QuotedPropertyName { - parent: RitoType { - base: PropertyKind::Embedded, - .. - }, - .. - } - ) - }); - assert_eq!( - err.diagnostic.to_string(), - "Quoted property name - embed bodies take 'name: type = value', with the name \ - unquoted or a '0x..' hash" - ); - } - - #[test] - fn map_keys_of_every_key_type_keep_their_pair() { - for (ty, key) in [ - ("hash", r#"0x1"#), - ("hash", r#""Characters/Aatrox/Skins/Skin0""#), - ("string", r#""Characters/Aatrox/Skins/Skin0""#), - ("file", r#""ASSETS/Maps/Textures/Bloom.tex""#), - ("file", r#"0x1"#), - ("u8", "1"), - ("u16", "1"), - ("u32", "1"), - ("u64", "1"), - ("i8", "-1"), - ("i16", "-1"), - ("i32", "-1"), - ("i64", "-1"), - ("f32", "1.5"), - ] { - let input = wrap(&format!("0x1: map[{ty},u32] = {{ {key} = 1 }}")); - let (bin, errs) = Cst::parse(&input).build_bin(&input); - assert!(errs.is_empty(), "map[{ty},u32] with key {key}: {errs:#?}"); - - // No diagnostics is not the same as no data lost - that is the whole failure - // mode this change is about, so check the pair actually reached the bin. - let PropertyValueEnum::Map(map) = - &bin.objects[&BinHash(0xDEADBEEF)].properties[&BinHash(1)] - else { - panic!("map[{ty},u32] with key {key}: property is not a map"); - }; - assert_eq!( - map.entries().len(), - 1, - "map[{ty},u32] with key {key}: pair was dropped" - ); - } - } - - /// Numeric map keys are written and read bare. Upstream's `read_word` accepts only - /// `[A-Za-z0-9_+-.]`, so on `"7"` it stops at the quote and hands `read_number` an empty - /// word, which fails - see - /// . - /// - /// The syntax is rejected rather than parsed out of the quotes, so a key whose contents - /// would have been a perfectly good number (`"7"`) is reported just the same as one that - /// could never be (`"abc"`). - #[test] - fn a_quoted_numeric_map_key_is_reported() { - for ty in ["u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "f32"] { - for key in ["7", "0.5", "abc", ""] { - let errs = build_errs(&format!(r#"0x1: map[{ty},u32] = {{ "{key}" = 1 }}"#)); - assert_eq!(errs.len(), 1, "map[{ty},u32] key {key:?}: {errs:#?}"); - assert_eq!( - errs[0].diagnostic.to_string(), - format!("Type mismatch - expected {ty}, got string"), - "map[{ty},u32] key {key:?}" - ); - } - } - } - - /// Reporting a rejected key does not recover the pair - it is still missing from the - /// bin. The diagnostic is the whole of the fix here. - #[test] - fn a_rejected_map_key_still_drops_the_pair() { - let input = wrap(r#"0x1: map[u32,u32] = { "0.5" = 1 }"#); - let (bin, errs) = Cst::parse(&input).build_bin(&input); - - assert_eq!(errs.len(), 1, "{errs:#?}"); - pretty_assertions::assert_eq!( - bin, - Bin::builder() - .object( - BinObject::::builder(0xDEADBEEF, 0x1234123) - .property( - 0x1, - values::Map::new(PropertyKind::U32, PropertyKind::U32, vec![]).unwrap() - ) - .build() - ) - .build() - ); - } - - /// The root `entries` map is the one every file has - its keys are quoted paths. - #[test] - fn a_quoted_root_entry_key_is_fine() { - let input = r#" -type: string = "PROP" -version: u32 = 3 -linked: list[string] = {} -entries: map[hash,embed] = { - "Characters/Aatrox/Skins/Skin0" = 0x1234123 { - 0x1: u32 = 1 - } -} -"#; - let cst = Cst::parse(input); - let (_, errs) = cst.build_bin(input); - assert!(errs.is_empty(), "{errs:#?}"); - } -} diff --git a/crates/ltk_ritobin/src/typecheck/collect.rs b/crates/ltk_ritobin/src/typecheck/collect.rs deleted file mode 100644 index 90af1265..00000000 --- a/crates/ltk_ritobin/src/typecheck/collect.rs +++ /dev/null @@ -1,240 +0,0 @@ -use ltk_meta::{ - property::values, traits::PropertyExt, Bin, BinObject, PropertyKind, PropertyValueEnum, -}; - -use crate::{ - parse::Span, - typecheck::{ - diagnostics::{self, RootKind}, - resolve::CoerceFrom, - }, - RitoType, -}; - -use super::state::{RootEntry, RootKindOrUnknown, TypeChecker}; - -use diagnostics::Diagnostic::*; - -impl<'a> TypeChecker<'a> { - /// Pops `entry`'s value out if `extract` succeeds; otherwise pushes an - /// `InvalidRootEntryType` diagnostic (using `extract`'s returned value to report what was - /// actually found) and returns `None`. Does not handle the "entry is absent" case - callers - /// do that themselves before calling this. - fn take_root_value( - &mut self, - root_kind: RootKind, - entry: RootEntry, - type_span: Span, - expected: PropertyKind, - extract: impl FnOnce(PropertyValueEnum) -> Result>, - ) -> Option { - let key_span = *entry.key.meta(); - match extract(entry.value) { - Ok(v) => Some(v), - Err(got) => { - self.ctx.diagnostics.push( - InvalidRootEntryType { - root_kind, - key_span, - type_span, - got: RitoType::simple(got.kind()), - expected: RitoType::simple(expected), - } - .unwrap(), - ); - None - } - } - } - - pub fn collect_to_bin(mut self) -> (Bin, Vec) { - let dependencies = self - .root - .swap_remove(&RootKindOrUnknown::Known(RootKind::Linked)); - - if dependencies.is_none() { - self.ctx.diagnostics.push( - MissingRootEntry { - root_kind: RootKind::Linked, - } - .default_span(Span::default()), - ); - } - - let dependencies = dependencies - .and_then(|e| { - let type_span = e.type_span; - self.take_root_value( - RootKind::Linked, - e, - type_span, - PropertyKind::Container, - |value| match value { - PropertyValueEnum::Container(list) => Ok(list), - other => Err(other), - }, - ) - }) - .map(|list| { - list.into_items() - .into_iter() - .filter_map(|value| { - let span = *value.meta(); - let PropertyValueEnum::String(dependency) = - PropertyKind::String.coerce_from(value)? - else { - self.ctx.diagnostics.push( - UnexpectedContainerItem { - span, - expected: RitoType::simple(PropertyKind::String), - expected_span: None, - } - .unwrap(), - ); - return None; - }; - Some(dependency.value) - }) - .collect::>() - }); - - let objects = self - .root - .swap_remove(&RootKindOrUnknown::Known(RootKind::Entries)); - - if objects.is_none() { - self.ctx.diagnostics.push( - MissingRootEntry { - root_kind: RootKind::Entries, - } - .default_span(Span::default()), - ); - } - - let objects = objects - .and_then(|e| { - let type_span = *e.key.meta(); - self.take_root_value( - RootKind::Entries, - e, - type_span, - PropertyKind::Map, - |value| match value { - PropertyValueEnum::Map(map) => Ok(map), - other => Err(other), - }, - ) - }) - .map(|map| { - map.into_entries() - .into_iter() - .filter_map(|(key, value)| { - let PropertyValueEnum::Hash(path_hash) = - PropertyKind::Hash.coerce_from(key)? - else { - return None; - }; - - if let PropertyValueEnum::Embedded(values::Embedded(struct_val)) = value { - let struct_val = struct_val.no_meta(); - Some(BinObject { - path_hash: *path_hash, - class_hash: struct_val.class_hash, - properties: struct_val.properties, - }) - } else { - None - } - }) - .collect::>() - }); - - match self.root.swap_remove(&RootKind::Type) { - Some(bin_type) => { - let type_span = *bin_type.key.meta(); - if let Some(type_value) = self.take_root_value( - RootKind::Type, - bin_type, - type_span, - PropertyKind::String, - |value| match value { - PropertyValueEnum::String(s) => Ok(s), - other => Err(other), - }, - ) { - match type_value.as_str() { - "PROP" => {} - "PTCH" => { - self.ctx.diagnostics.push( - CustomSpan("Patch bins are not supported yet", *type_value.meta()) - .unwrap(), - ); - } - _other => { - self.ctx - .diagnostics - .push(CustomSpan("Unknown bin type", *type_value.meta()).unwrap()); - } - } - } - } - None => { - self.ctx.diagnostics.push( - MissingRootEntry { - root_kind: RootKind::Type, - } - .default_span(Span::default()), - ); - } - } - match self.root.swap_remove(&RootKind::Version) { - Some(version) => { - let type_span = *version.key.meta(); - if let Some(version_value) = self.take_root_value( - RootKind::Version, - version, - type_span, - PropertyKind::U32, - |value| match value { - PropertyValueEnum::U32(v) => Ok(v), - other => Err(other), - }, - ) { - match *version_value { - 3 => {} - _other => { - self.ctx.diagnostics.push( - CustomSpan("Bin version should be '3'", *version_value.meta()) - .unwrap(), - ); - } - } - } - } - None => { - self.ctx.diagnostics.push( - MissingRootEntry { - root_kind: RootKind::Version, - } - .default_span(Span::default()), - ); - } - } - - for (_key, unknown) in self.root { - self.ctx.diagnostics.push( - UnknownRoot { - span: *unknown.key.meta(), - } - .default_span(Span::default()), - ); - } - - let tree = Bin::new( - objects.unwrap_or_default(), - dependencies.unwrap_or_default(), - ); - - (tree, self.ctx.diagnostics) - } -} diff --git a/crates/ltk_ritobin/src/typecheck/ir.rs b/crates/ltk_ritobin/src/typecheck/ir.rs deleted file mode 100644 index 246406b9..00000000 --- a/crates/ltk_ritobin/src/typecheck/ir.rs +++ /dev/null @@ -1,87 +0,0 @@ -use ltk_meta::{traits::PropertyExt as _, PropertyValueEnum}; - -use crate::parse::Span; - -#[derive(Debug, Clone)] -pub struct IrEntry { - pub key: PropertyValueEnum, - pub value: PropertyValueEnum, - /// Span of the type expression this entry's value type came from, if any. - pub type_span: Option, -} - -#[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, - } - } - - /// Span of the whole item, an entry's key through its value. - /// - /// An [`IrListItem`] has no key, so there it is just the value. A parent that rejects an item - /// rejects all of it, so this is what a diagnostic about the item underlines. - pub fn span(&self) -> Span { - match self { - IrItem::Entry(IrEntry { key, value, .. }) => { - let (key, value) = (*key.meta(), *value.meta()); - // a recovered tree can hand us a value that starts before its own key - Span::new(key.start, value.end.max(key.end)) - } - IrItem::ListItem(IrListItem(value)) => *value.meta(), - } - } - - /// Span of the type expression this item's type came from, if any. - /// - /// An [`IrListItem`] takes its type from its parent's subtype rather than declaring one, so it - /// never has a type expression of its own to point at. - pub fn type_span(&self) -> Option { - match self { - IrItem::Entry(entry) => entry.type_span, - IrItem::ListItem(_) => None, - } - } - - pub fn into_value(self) -> PropertyValueEnum { - match self { - IrItem::Entry(i) => i.value, - IrItem::ListItem(i) => i.0, - } - } -} diff --git a/crates/ltk_ritobin/src/typecheck/listlikes.rs b/crates/ltk_ritobin/src/typecheck/listlikes.rs deleted file mode 100644 index 3a7f26c5..00000000 --- a/crates/ltk_ritobin/src/typecheck/listlikes.rs +++ /dev/null @@ -1,241 +0,0 @@ -use ltk_meta::{ - property::{values, ValueMut}, - traits::PropertyExt, - PropertyKind, PropertyValueEnum, -}; - -use crate::{ - parse::Span, - typecheck::{ - diagnostics::{self, Diagnostic, ListLike, MaybeSpanDiag, RitoTypeOrVirtual}, - ir::{IrItem, IrListItem}, - }, - RitoType, -}; - -use diagnostics::Diagnostic::*; - -fn resolve_f32( - n: PropertyValueEnum, - expected_span: Option, -) -> Result { - match n { - PropertyValueEnum::F32(values::F32 { value: n, .. }) => Ok(n), - _ => Err(TypeMismatch { - span: *n.meta(), - expected: RitoType::simple(PropertyKind::F32), - expected_span, - got: RitoTypeOrVirtual::RitoType(RitoType::simple(n.kind())), - } - .into()), - } -} - -fn resolve_u8( - n: PropertyValueEnum, - expected_span: Option, -) -> Result { - match n { - PropertyValueEnum::U8(values::U8 { value: n, .. }) => Ok(n), - _ => Err(TypeMismatch { - span: *n.meta(), - expected: RitoType::simple(PropertyKind::U8), - expected_span, - got: RitoTypeOrVirtual::RitoType(RitoType::simple(n.kind())), - } - .into()), - } -} - -struct ListIter> { - items: I, - span: Span, - /// span of the type expression that made this a listlike, to blame component types on - type_span: Option, - count: u8, -} - -impl> ListIter { - fn new(span: Span, type_span: Option, items: I) -> Self { - Self { - items, - span, - type_span, - count: 0, - } - } - - fn next(&mut self) -> Option { - let item = self.items.next(); - if item.is_some() { - self.count += 1; - } - item - } - - fn into_inner(self) -> I { - self.items - } - - fn expect_next(&mut self, expected: ListLike) -> Result, Diagnostic> { - let item = self - .next() - .ok_or(NotEnoughItems { - span: self.span, - got: self.count, - expected, - })? - .0; - self.span = *item.meta(); - Ok(item) - } - fn read_floats( - &mut self, - expected: ListLike, - ) -> Result<[f32; N], MaybeSpanDiag> { - let mut out = [0.0f32; N]; - for slot in &mut out { - *slot = resolve_f32(self.expect_next(expected)?, self.type_span)?; - } - Ok(out) - } - fn read_u8s(&mut self, expected: ListLike) -> Result<[u8; N], MaybeSpanDiag> { - let mut out = [0u8; N]; - for slot in &mut out { - *slot = resolve_u8(self.expect_next(expected)?, self.type_span)?; - } - Ok(out) - } - - fn inject_vec2( - &mut self, - v: &mut values::Vector2, - expected: ListLike, - ) -> Result { - let expect = ListLike::Vec2; - v.value = self.read_floats::<2>(expected)?.into(); - Ok(expect) - } - - fn inject_vec3( - &mut self, - v: &mut values::Vector3, - expected: ListLike, - ) -> Result { - let expect = ListLike::Vec3; - v.value = self.read_floats::<3>(expected)?.into(); - Ok(expect) - } - - fn inject_vec4( - &mut self, - v: &mut values::Vector4, - expected: ListLike, - ) -> Result { - let expect = ListLike::Vec4; - v.value = self.read_floats::<4>(expected)?.into(); - Ok(expect) - } - - fn inject_color( - &mut self, - v: &mut values::Color, - expected: ListLike, - ) -> Result { - let expect = ListLike::Color; - let [r, g, b, a] = self.read_u8s(expected)?; - let values::Color { value: color, .. } = v; - color.r = r; - color.g = g; - color.b = b; - color.a = a; - Ok(expect) - } - - fn inject_mat44( - &mut self, - v: &mut values::Matrix44, - expected: ListLike, - ) -> Result { - let expect = ListLike::Mat44; - let values::Matrix44 { value: mat, .. } = v; - mat.x_axis = self.read_floats::<4>(expected)?.into(); - mat.y_axis = self.read_floats::<4>(expected)?.into(); - mat.z_axis = self.read_floats::<4>(expected)?.into(); - mat.w_axis = self.read_floats::<4>(expected)?.into(); - *mat = mat.transpose(); - Ok(expect) - } -} - -/// Fills a listlike (vec, mtx44, rgba, option[listlike]) from the items collected for it. -/// -/// A listlike spells its components out as bare values, so they arrive as ordinary list items and -/// have to be folded back into one value once the block closes. -/// -/// - `target` - the value to fill; left alone when it turns out not to be a listlike -/// - `items` - the queued list items, drained into `target` -/// - `type_span` - where `target`'s type was written, so a component of the wrong type can point -/// at the `vec3`/`rgba`/... that decided what its components had to be -/// -/// # Returns -/// `Ok(())` when `target` is not a listlike, so callers can hand every value to it. -/// -/// # Errors -/// If `target` is a listlike and `items` does not fill it - too few components, too many, or one -/// that is not the `f32`/`u8` the shape needs. -pub(crate) fn try_populate_listlike( - target: &mut IrItem, - items: &mut Vec, - type_span: Option, -) -> Result<(), MaybeSpanDiag> { - use PropertyValueEnum as V; - - // empty options look like empty lists, return early so we don't complain about missing items - if items.is_empty() && matches!(target.value(), V::Optional(_)) { - return Ok(()); - } - - // TODO: is this the right span to start with? - let mut items = ListIter::new(*target.value().meta(), type_span, items.drain(..)); - - let mut inject = - |target: &mut PropertyValueEnum| -> Result, MaybeSpanDiag> { - Ok(Some(match target { - V::Vector2(v) => items.inject_vec2(v, ListLike::Vec2)?, - V::Vector3(v) => items.inject_vec3(v, ListLike::Vec3)?, - V::Vector4(v) => items.inject_vec4(v, ListLike::Vec4)?, - V::Color(v) => items.inject_color(v, ListLike::Color)?, - V::Matrix44(v) => items.inject_mat44(v, ListLike::Mat44)?, - // Check the item kind before reaching in: `slot_or_insert_default` would - // otherwise fill an option that turns out not to hold a listlike at all. - V::Optional(opt) if ListLike::from_kind(opt.item_kind()).is_some() => { - match opt.slot_or_insert_default().as_mut() { - ValueMut::Vector2(v) => items.inject_vec2(v, ListLike::Vec2)?, - ValueMut::Vector3(v) => items.inject_vec3(v, ListLike::Vec3)?, - ValueMut::Vector4(v) => items.inject_vec4(v, ListLike::Vec4)?, - ValueMut::Color(v) => items.inject_color(v, ListLike::Color)?, - ValueMut::Matrix44(v) => items.inject_mat44(v, ListLike::Mat44)?, - _ => return Ok(None), - } - } - _ => return Ok(None), - })) - }; - - let Some(expected) = inject(target.value_mut())? else { - // we weren't a listlike - return Ok(()); - }; - - if let Some(extra) = items.next() { - let count = 1 + items.into_inner().count(); - return Err(TooManyItems { - span: *extra.0.meta(), - extra: count as _, - expected, - } - .into()); - } - Ok(()) -} diff --git a/crates/ltk_ritobin/src/typecheck/resolve.rs b/crates/ltk_ritobin/src/typecheck/resolve.rs deleted file mode 100644 index 167560da..00000000 --- a/crates/ltk_ritobin/src/typecheck/resolve.rs +++ /dev/null @@ -1,640 +0,0 @@ -use std::{borrow::Cow, fmt::Debug}; - -use ltk_hash::{BinHash, Hash as _, WadHash}; -use ltk_meta::{property::values, traits::PropertyExt, PropertyKind, PropertyValueEnum}; - -use crate::{ - cst::{self, visitor::VisitCtx, Kind, Node}, - parse::{Span, Token, TokenKind}, - typecheck::{ - diagnostics::{self, Diagnostic, MaybeSpanDiag, RitoTypeOrVirtual}, - ir::IrEntry, - }, - Cst, PropertyValueExt as _, RitoType, RitobinName, -}; - -use super::{state::Ctx, trace::trace}; - -use diagnostics::Diagnostic::*; - -trait TreeIterExt<'a>: Iterator { - fn expect_tree(&mut self, cst: &'a Cst, kind: cst::Kind) -> Result<&'a Node, Diagnostic>; - fn expect_token(&mut self, cst: &'a Cst, kind: TokenKind) -> Result<&'a Token, Diagnostic>; -} - -impl<'a, I> TreeIterExt<'a> for I -where - I: Iterator, -{ - fn expect_tree(&mut self, cst: &'a Cst, kind: cst::Kind) -> Result<&'a Node, Diagnostic> { - self.find_map(|c| c.tree(cst).filter(|t| t.kind == kind)) - .ok_or(MissingTree(kind)) - } - fn expect_token(&mut self, cst: &'a Cst, kind: TokenKind) -> Result<&'a Token, Diagnostic> { - self.find_map(|c| c.token(cst).filter(|t| t.kind == kind)) - .ok_or(MissingToken(kind)) - } -} - -pub trait CanCoerce { - fn can_coerce(self, from: Self) -> bool; -} - -pub trait CoerceFrom { - fn coerce_from( - self, - value: PropertyValueEnum, - ) -> Option>; -} - -impl CanCoerce for PropertyKind { - fn can_coerce(self, from: Self) -> bool { - let to = self; - if to == from { - return true; - } - use PropertyKind as K; - match (to, from) { - (K::Optional, from) if !from.is_container() => true, - (K::Hash, K::String) - | (K::WadChunkLink | K::ObjectLink, K::Hash | K::String) - | (K::BitBool | K::Bool, K::Bool | K::BitBool) => true, - _ => false, - } - } -} -impl CanCoerce for RitoType { - fn can_coerce(self, from: Self) -> bool { - if !self.base.can_coerce(from.base) { - return false; - } - for i in 0..1 { - if (self.subtypes[i].zip(from.subtypes[i])) - .is_some_and(|(to, from)| !to.can_coerce(from)) - { - return false; - } - } - true - } -} -impl CoerceFrom for PropertyKind { - fn coerce_from( - self, - value: PropertyValueEnum, - ) -> Option> { - let to = self; - match to { - to if to == value.kind() => Some(value), - - PropertyKind::Optional => Some(values::Optional::try_from(value).ok()?.into()), - - PropertyKind::Hash => match value { - PropertyValueEnum::String(str) => { - Some(values::Hash::new_with_meta(BinHash::hash_str(&str), str.meta).into()) - } - _ => None, - }, - PropertyKind::ObjectLink => match value { - PropertyValueEnum::Hash(hash) => { - Some(values::ObjectLink::new_with_meta(*hash, hash.meta).into()) - } - PropertyValueEnum::String(str) => Some( - values::ObjectLink::new_with_meta(BinHash::hash_str(&str), str.meta).into(), - ), - _ => None, - }, - PropertyKind::WadChunkLink => match value { - PropertyValueEnum::Hash(hash) => Some( - values::WadChunkLink::new_with_meta(WadHash((**hash).into()), hash.meta).into(), - ), - PropertyValueEnum::String(str) => Some( - values::WadChunkLink::new_with_meta(WadHash::hash_str(str.as_str()), str.meta) - .into(), - ), - _ => None, - }, - PropertyKind::BitBool => match value { - PropertyValueEnum::Bool(bool) => { - Some(values::BitBool::new_with_meta(*bool, bool.meta).into()) - } - _ => None, - }, - PropertyKind::Bool => match value { - PropertyValueEnum::BitBool(bool) => { - Some(values::Bool::new_with_meta(*bool, bool.meta).into()) - } - _ => None, - }, - _ => None, - } - } -} - -fn resolve_rito_type( - ctx: &mut Ctx<'_>, - visit_ctx: &VisitCtx, - tree: &Node, -) -> Result { - let mut c = tree.children.get(visit_ctx.cst).iter(); - - let base = c.expect_token(visit_ctx.cst, 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(visit_ctx.cst) - .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 - .get(visit_ctx.cst) - .iter() - .filter_map(|c| c.tree(visit_ctx.cst).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() { - let span = if subtypes.len() > expected.into() { - subtypes[expected as _..] - .iter() - .map(|s| s.1) - .reduce(|acc, s| Span::new(acc.start, s.end)) - .unwrap_or(subtypes_span) - } else { - subtypes.last().map(|s| s.1).unwrap_or(subtypes_span) - }; - return Err(SubtypeCountMismatch { - 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))?; - - // since we can't know whether bin/wad was intended, we will just try fit it in the smallest hash that allows it. - // we can then safely coerce the type upwards when we are given type information - Ok(match BinHash::from_str_radix(src, 16) { - Ok(hash) => PropertyValueEnum::Hash(values::Hash::new_with_meta(hash, span)), - Err(_) => match WadHash::from_str_radix(src, 16) { - Ok(hash) => { - PropertyValueEnum::WadChunkLink(values::WadChunkLink::new_with_meta(hash, span)) - } - Err(_) => return Err(InvalidHash(span)), - }, - }) -} - -fn parse_int>( - txt: &str, - kind_hint: PropertyKind, - span: Span, - wrap: impl FnOnce(T, Span) -> PropertyValueEnum, -) -> Result, Diagnostic> { - txt.parse::() - .map(|v| wrap(v, span)) - .map_err(|e| Diagnostic::ParseNumericError { - expected: kind_hint, - error: Some(*e.kind()), - span, - }) -} - -/// Resolves a single literal token into the value it spells. -/// -/// - `ctx` - typecheck state; diagnostics found along the way are pushed here -/// - `token` - the literal to resolve -/// - `kind_hint` - the type to read the literal as. A number, `true` or a string can be several -/// types, so without a hint an ambiguous literal cannot be resolved at all -/// - `kind_hint_span` - where `kind_hint` was written, so a mismatch can point at it -/// -/// # Errors -/// If the literal does not fit `kind_hint`, or if it is ambiguous and there is no hint to pick -/// with - a bare `5` on its own has no type. -fn resolve_literal( - ctx: &mut Ctx, - token: &Token, - kind_hint: Option, - kind_hint_span: Option, -) -> Result>, Diagnostic> { - use PropertyKind as K; - use PropertyValueEnum as P; - Ok(Some(match token { - Token { - kind: TokenKind::String, - span, - } => values::String::new_with_meta( - ctx.text[Span::new(span.start + 1, span.end - 1)].into(), - *span, - ) - .into(), - - Token { - kind: TokenKind::True, - span, - } => values::Bool::new_with_meta(true, *span).into(), - Token { - kind: TokenKind::False, - span, - } => values::Bool::new_with_meta(false, *span).into(), - - Token { - kind: TokenKind::HexLit, - span, - } => resolve_hash(ctx, *span)?, - Token { - kind: TokenKind::Number, - span, - } => { - let txt = &ctx.text[span]; - let Some(kind_hint) = kind_hint else { - return Err(AmbiguousNumeric(*span)); - }; - - let txt = match txt.contains('_') { - true => Cow::Owned(txt.replace('_', "")), - false => Cow::Borrowed(txt), - }; - - let kind_hint = match kind_hint.base { - K::Optional => kind_hint.value_subtype().unwrap(), - base => base, - }; - - match kind_hint { - K::U8 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::U8(values::U8::new_with_meta(v, s)) - })?, - K::U16 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::U16(values::U16::new_with_meta(v, s)) - })?, - K::U32 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::U32(values::U32::new_with_meta(v, s)) - })?, - K::U64 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::U64(values::U64::new_with_meta(v, s)) - })?, - K::I8 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::I8(values::I8::new_with_meta(v, s)) - })?, - K::I16 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::I16(values::I16::new_with_meta(v, s)) - })?, - K::I32 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::I32(values::I32::new_with_meta(v, s)) - })?, - K::I64 => parse_int::(&txt, kind_hint, *span, |v, s| { - P::I64(values::I64::new_with_meta(v, s)) - })?, - K::F32 => P::F32(values::F32::new_with_meta( - txt.parse().map_err(|_| Diagnostic::ParseNumericError { - expected: kind_hint, - error: None, - span: *span, - })?, - *span, - )), - _ => { - return Err(TypeMismatch { - span: *span, - expected: RitoType::simple(kind_hint), - expected_span: kind_hint_span, - got: RitoTypeOrVirtual::numeric(), - }); - } - } - } - _ => return Ok(None), - })) -} - -/// Resolves an `EntryValue` or `ListItem` tree into the value it describes. -/// -/// - `ctx` - typecheck state; diagnostics found along the way are pushed here -/// - `visit_ctx` - the CST being walked -/// - `tree` - the tree holding the value -/// - `kind_hint` - the type the value is expected to have, used to resolve literals that cannot -/// type themselves - a bare `5` is only a `u8` because something said so -/// - `kind_hint_span` - where `kind_hint` was written, so a mismatch can point at it -/// -/// # Returns -/// `Ok(None)` when the tree holds nothing resolvable, which the caller reports in its own terms - -/// an empty list item is a different mistake from an empty entry. -/// -/// # Errors -/// If the value cannot be read as `kind_hint` - a literal of the wrong type, a number that does -/// not fit, or an ambiguous literal with no hint to resolve it against. -pub(crate) fn resolve_value( - ctx: &mut Ctx, - visit_ctx: &VisitCtx, - tree: &Node, - kind_hint: Option, - kind_hint_span: Option, -) -> Result>, Diagnostic> { - use PropertyKind as K; - use PropertyValueEnum as P; - - let Some(child) = tree.children.get(visit_ctx.cst).first() else { - return Ok(None); - }; - Ok(Some(match child.tree(visit_ctx.cst) { - Some(Node { - kind: Kind::Class, - children, - span, - .. - }) => { - let Some(kind_hint) = kind_hint else { - return Ok(None); // TODO: err - }; - let Some(class) = children - .get(visit_ctx.cst) - .first() - .and_then(|t| t.token(visit_ctx.cst)) - else { - return Err(InvalidHash(*span)); - }; - - let class_hash = match class { - Token { - kind: TokenKind::Name, - span, - } => BinHash::hash_str(&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.base { - 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 => { - trace!("can't create class value from kind {other:?}"); - return Err(TypeMismatch { - span: class.span, - expected: RitoType::simple(other), - expected_span: None, - got: RitoTypeOrVirtual::StructOrEmbedded, - }); - } - } - } - - // Matches a block with no class name - { .. } - Some( - block @ Node { - kind: Kind::Block, .. - }, - ) => { - let Some(kind_hint) = kind_hint else { - return Ok(None); - }; - if !matches!(kind_hint.base, K::Struct | K::Embedded) { - return Ok(None); - } - - // Structs and embedded values must have a class name before the block - return Err(MissingClassName { - span: block.open_brace_span(visit_ctx.cst), - expected: kind_hint, - }); - } - Some(Node { - kind: Kind::Literal, - children, - .. - }) => { - let Some(child) = children.get(visit_ctx.cst).first() else { - return Ok(None); - }; - return resolve_literal( - ctx, - child.token(visit_ctx.cst).unwrap(), - kind_hint, - kind_hint_span, - ); - } - _ => return Ok(None), - })) -} - -/// Resolves an `Entry` tree into the key/value pair it describes. -/// -/// - `ctx` - typecheck state; diagnostics found along the way are pushed here -/// - `visit_ctx` - the CST being walked -/// - `tree` - the `Entry` tree to resolve -/// - `parent_value_kind` - the type the enclosing container gives its values, so an entry that -/// wrote no `: type` can still be resolved. `None` at the root -/// - `parent_type_span` - where that type was written, so a mismatch can point at it. `None` at -/// the root, and for a container that took its type from a subtype rather than a type expression -/// -/// # Errors -/// If the tree is not a well-formed entry - no key, no value, or a key that is not a hash. -/// A well-formed entry that fails to type-check resolves to a default value instead, so the walk -/// keeps going and the diagnostic lands in `ctx`. -pub(crate) fn resolve_entry( - ctx: &mut Ctx, - visit_ctx: &VisitCtx, - tree: &Node, - parent_value_kind: Option, - parent_type_span: Option, -) -> Result { - let mut c = tree.children.get(visit_ctx.cst).iter(); - - let key = c.expect_tree(visit_ctx.cst, Kind::EntryKey)?; - - let key = match key - .children - .get(visit_ctx.cst) - .first() - .ok_or(InvalidHash(key.span))? - .token(visit_ctx.cst) - { - Some(Token { - kind: TokenKind::Name, - span, - }) => PropertyValueEnum::from(values::String::new_with_meta(ctx.text[span].into(), *span)), - Some(Token { - kind: TokenKind::String, - span, - }) => { - // We can support quoted property names by just hashing the string - // The original ritobin compiler has no support for it and we prefer - // unquoted names - emit diagnostic - if let Some(parent) = parent_value_kind - .filter(|p| matches!(p.base, PropertyKind::Struct | PropertyKind::Embedded)) - { - ctx.diagnostics.push( - QuotedPropertyName { - span: *span, - parent, - } - .unwrap(), - ); - } - - PropertyValueEnum::from(values::String::new_with_meta( - ctx.text[Span::new(span.start + 1, span.end - 1)].into(), - *span, - )) - } - Some(Token { - kind: TokenKind::HexLit, - span, - }) => resolve_hash(ctx, *span)?, - Some(token) => resolve_literal( - ctx, - token, - parent_value_kind - .and_then(|k| k.subtypes[0]) - .map(RitoType::simple), - parent_type_span, - )? - .ok_or(CustomSpan("erm idk bad literal", key.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(visit_ctx.cst).filter(|t| t.kind == Kind::TypeExpr)); - let kind_span = kind.map(|k| k.span); - let kind = kind - .map(|t| resolve_rito_type(ctx, visit_ctx, t)) - .transpose()?; - - let value = c.expect_tree(visit_ctx.cst, 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.can_coerce(*kind) { - ctx.diagnostics.push( - TypeMismatch { - span: kind_span, - expected: *parent, - expected_span: parent_type_span, - got: (*kind).into(), - } - .unwrap(), - ); - return Ok(IrEntry { - key, - // we fell back to the parent's type, so that is what declared this value - type_span: parent_type_span, - value: parent.make_default(value.span), - }); - } - } - } - - let kind = kind.or(parent_value_kind); - let type_span = kind_span.or(parent_type_span); - - let resolved_val = match resolve_value(ctx, visit_ctx, value, kind, type_span) { - Ok(v) => v, - Err(e) => Some(match kind { - Some(kind) => { - ctx.diagnostics.push(e.default_span(tree.span)); - kind.make_default(value.span) - } - None => { - return Err(e.into()); - } - }), - }; - - let resolved_val = resolved_val.map(|value| match kind { - Some(kind) if value.kind() == kind.base => value, - Some(kind) => kind.base.coerce_from(value.clone()).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, - type_span, - }) -} diff --git a/crates/ltk_ritobin/src/typecheck/state.rs b/crates/ltk_ritobin/src/typecheck/state.rs deleted file mode 100644 index a128c477..00000000 --- a/crates/ltk_ritobin/src/typecheck/state.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::borrow::Cow; - -use indexmap::{Equivalent, IndexMap}; -use ltk_meta::{traits::PropertyExt, PropertyValueEnum}; - -use crate::{ - parse::Span, - typecheck::{ - diagnostics::{DiagnosticWithSpan, RootKind}, - ir::{IrItem, IrListItem}, - }, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RootKindOrUnknown<'a> { - Known(RootKind), - Unknown(Cow<'a, str>), -} - -impl std::hash::Hash for RootKindOrUnknown<'_> { - fn hash(&self, state: &mut H) { - match self { - RootKindOrUnknown::Known(root_kind) => root_kind.hash(state), - RootKindOrUnknown::Unknown(cow) => cow.hash(state), - } - } -} - -impl Equivalent> for RootKind { - #[inline(always)] - fn equivalent(&self, key: &RootKindOrUnknown<'_>) -> bool { - match key { - RootKindOrUnknown::Known(root_kind) => self == root_kind, - RootKindOrUnknown::Unknown(_) => false, - } - } -} -impl Equivalent> for Cow<'_, str> { - #[inline(always)] - fn equivalent(&self, key: &RootKindOrUnknown<'_>) -> bool { - match key { - RootKindOrUnknown::Known(_) => false, - RootKindOrUnknown::Unknown(cow) => self == cow, - } - } -} -impl Equivalent> for str { - #[inline(always)] - fn equivalent(&self, key: &RootKindOrUnknown<'_>) -> bool { - match key { - RootKindOrUnknown::Known(_) => false, - RootKindOrUnknown::Unknown(cow) => self == cow, - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn root_kind_eq() { - let mut root: IndexMap, ()> = Default::default(); - - root.insert(RootKind::Version.into(), ()); - root.insert(RootKind::Entries.into(), ()); - root.insert(RootKindOrUnknown::Unknown("foo".into()), ()); - root.insert(RootKindOrUnknown::Unknown("bar".into()), ()); - - assert!(root.swap_remove(&RootKind::Version).is_some()); - assert!(root.swap_remove(&RootKind::Entries).is_some()); - assert!(root - .swap_remove(&RootKindOrUnknown::Unknown("bar".into())) - .is_some()); - - assert_eq!(root.len(), 1); - } -} - -impl<'a> RootKindOrUnknown<'a> { - pub fn from_value(src: &'a str, value: &PropertyValueEnum) -> Self { - let PropertyValueEnum::String(string) = value else { - return Self::Unknown(src[*value.meta()].into()); - }; - - match string.as_str() { - "type" => RootKind::Type.into(), - "version" => RootKind::Version.into(), - "linked" => RootKind::Linked.into(), - "entries" => RootKind::Entries.into(), - _ => Self::Unknown(src[*value.meta()].into()), - } - } -} - -impl From for RootKindOrUnknown<'_> { - #[inline(always)] - fn from(value: RootKind) -> Self { - Self::Known(value) - } -} -impl<'a> From> for RootKindOrUnknown<'a> { - #[inline(always)] - fn from(value: Cow<'a, str>) -> Self { - Self::Unknown(value) - } -} - -#[derive(Debug, Clone)] -pub struct RootEntry { - pub(crate) key: PropertyValueEnum, - pub(crate) type_span: Span, - pub(crate) value: PropertyValueEnum, -} - -pub struct TypeChecker<'a> { - pub(crate) ctx: Ctx<'a>, - pub root: IndexMap, RootEntry>, - pub(crate) stack: Vec<(u32, IrItem)>, - pub(crate) list_queue: Vec, - pub(crate) 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(crate) struct Ctx<'a> { - pub(crate) text: &'a str, - pub(crate) diagnostics: Vec, -} diff --git a/crates/ltk_ritobin/src/typecheck/trace.rs b/crates/ltk_ritobin/src/typecheck/trace.rs deleted file mode 100644 index bf59ae68..00000000 --- a/crates/ltk_ritobin/src/typecheck/trace.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::cst::Kind; - -use super::state::TypeChecker; - -impl TypeChecker<'_> { - /// Prints the current traversal stack on tree enter/exit. `arrow` is `">"` on enter, `"<"` - /// on exit. No-op unless the `debug` feature is enabled and `RB_STACK` is set. - #[cfg(feature = "debug")] - pub(super) fn trace_stack(&self, depth: u32, arrow: &str, kind: Kind) { - if std::env::var("RB_STACK").is_err() { - return; - } - let indent = " ".repeat(depth.saturating_sub(1) as _); - eprintln!("{indent}{arrow} d:{depth} | {kind:?}"); - eprint!("{indent} stack: "); - if self.stack.is_empty() { - eprint!("empty"); - } - eprintln!(); - for s in &self.stack { - eprintln!("{indent} - {}: {:?}", s.0, s.1); - } - } - #[cfg(not(feature = "debug"))] - pub(super) fn trace_stack(&self, _depth: u32, _arrow: &str, _kind: Kind) {} - - /// Prints the depth of a just-popped stack entry when exiting a tree, indented to - /// `indent_depth`. No-op unless the `debug` feature is enabled and `RB_STACK` is set. - #[cfg(feature = "debug")] - pub(super) fn trace_popped(&self, indent_depth: u32, popped_depth: u32) { - if std::env::var("RB_STACK").is_ok() { - let indent = " ".repeat(indent_depth.saturating_sub(1) as _); - eprintln!("{indent}< popped {popped_depth}"); - } - } - #[cfg(not(feature = "debug"))] - pub(super) fn trace_popped(&self, _indent_depth: u32, _popped_depth: u32) {} -} - -/// One-off debug trace message. No-op unless the `debug` feature is enabled and `RB_STACK` is -/// set - arguments are only evaluated when tracing is active. -macro_rules! trace { - ($($arg:tt)*) => { - #[cfg(feature = "debug")] - if ::std::env::var("RB_STACK").is_ok() { - eprintln!($($arg)*); - } - }; -} -pub(super) use trace; diff --git a/crates/ltk_ritobin/src/typecheck/walk.rs b/crates/ltk_ritobin/src/typecheck/walk.rs deleted file mode 100644 index ec614adc..00000000 --- a/crates/ltk_ritobin/src/typecheck/walk.rs +++ /dev/null @@ -1,445 +0,0 @@ -use ltk_meta::{property::values, traits::PropertyExt, PropertyKind, PropertyValueEnum}; - -use crate::{ - cst::{ - self, - visitor::{Visit, VisitCtx}, - Kind, NodeId, Visitor, - }, - parse::Span, - typecheck::{ - diagnostics::{self, RitoTypeOrVirtual}, - ir::{IrEntry, IrItem, IrListItem}, - resolve::CoerceFrom, - }, - PropertyValueExt as _, RitoType, -}; - -use super::{ - listlikes::try_populate_listlike, - resolve::{resolve_entry, resolve_value}, - state::{RootEntry, RootKindOrUnknown, TypeChecker}, - trace::trace, -}; - -use diagnostics::Diagnostic::*; - -impl<'a> TypeChecker<'a> { - /// Reports whatever a container had to say about the value just pushed into it. - /// - /// - `span` - the value that was pushed, to underline - /// - `expected_span` - where the container's type was written, so a rejection can point at it - /// - `result` - what the push returned - fn handle_container_res( - &mut self, - span: Span, - expected_span: Option, - result: Result<(), ltk_meta::Error>, - ) { - match result { - Ok(()) => {} - Err(ltk_meta::Error::MismatchedContainerTypes { expected, got }) => { - self.ctx.diagnostics.push( - TypeMismatch { - span, - expected: RitoType::simple(expected), - expected_span, - got: RitoType::simple(got).into(), - } - .unwrap(), - ); - } - Err(_e) => { - todo!("handle unexpected error"); - } - } - } - - /// Reports a child whose shape its parent does not accept. - /// - /// - `child` - the offending item, underlined whole because that is what the parent rejected - /// - `parent` - the type that rejected it, which also fixes the shape it wanted - /// - /// # Panics - /// If `parent` has no body to hold items. Only the arms of [`Self::merge_ir`] that accept - /// children reach this - anything else lands in its catch-all as an `UnexpectedContainerItem`. - fn report_wrong_item_shape(&mut self, child: &IrItem, parent: RitoType) { - let expected = parent - .item_shape() - .expect("only a parent with a body can reject an item shape"); - - self.ctx.diagnostics.push( - UnexpectedItem { - span: child.span(), - parent, - expected, - } - .unwrap(), - ); - } - - /// Reports an entry key that cannot become the key type its parent needs. - /// - /// - `span` - the key, to underline - /// - `got` - the type the key resolved to - /// - `expected` - the key type the parent needs - /// - `expected_span` - where that type was written, or `None` when nothing wrote it - a - /// property name is a hash because it is a property name, not because of a type expression - fn report_bad_entry_key( - &mut self, - span: Span, - got: RitoType, - expected: PropertyKind, - expected_span: Option, - ) { - self.ctx.diagnostics.push( - TypeMismatch { - span, - expected: RitoType::simple(expected), - expected_span, - got: got.into(), - } - .unwrap(), - ); - } - - fn merge_ir(&mut self, mut parent: IrItem, child: IrItem) -> IrItem { - let parent_type = parent.value().rito_type(); - let expected_span = parent.type_span(); - - match &mut parent.value_mut() { - PropertyValueEnum::Container(list) - | PropertyValueEnum::UnorderedContainer(values::UnorderedContainer(list)) => { - match child { - IrItem::ListItem(IrListItem(mut value)) => { - if value.kind() != list.item_kind() { - value = list.item_kind().coerce_from(value.clone()).unwrap_or(value); - } - - let span = *value.meta(); - let result = list.push(value); - self.handle_container_res(span, expected_span, result); - } - child @ IrItem::Entry(_) => { - self.report_wrong_item_shape(&child, parent_type); - return parent; - } - } - } - PropertyValueEnum::Struct(struct_val) - | PropertyValueEnum::Embedded(values::Embedded(struct_val)) => { - let IrEntry { key, value, .. } = match child { - IrItem::Entry(entry) => entry, - child => { - self.report_wrong_item_shape(&child, parent_type); - return parent; - } - }; - - let (key_span, key_type) = (*key.meta(), key.rito_type()); - let Some(PropertyValueEnum::Hash(key)) = PropertyKind::Hash.coerce_from(key) else { - self.report_bad_entry_key(key_span, key_type, PropertyKind::Hash, None); - return parent; - }; - - struct_val.properties.insert(*key, value); - } - PropertyValueEnum::Map(map_value) => { - let IrEntry { key, value, .. } = match child { - IrItem::Entry(entry) => entry, - child => { - self.report_wrong_item_shape(&child, parent_type); - return parent; - } - }; - let span = *value.meta(); - let key_kind = map_value.key_kind(); - let (key_span, key_type) = (*key.meta(), key.rito_type()); - let Some(key) = key_kind.coerce_from(key) else { - self.report_bad_entry_key(key_span, key_type, key_kind, expected_span); - return parent; - }; - let result = map_value.push(key, value); - self.handle_container_res(span, expected_span, result); - } - PropertyValueEnum::Optional(option) => { - let IrListItem(child) = match child { - IrItem::ListItem(item) => item, - child => { - self.report_wrong_item_shape(&child, parent_type); - return parent; - } - }; - let child_span = *child.meta(); - let child_type = child.rito_type(); - let Some(child) = option.item_kind().coerce_from(child) else { - self.ctx.diagnostics.push( - TypeMismatch { - span: child_span, - expected: RitoType::simple(option.item_kind()), - expected_span, - got: child_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(), - ); - - trace!("cant inject into {:?}", other.kind()); - } - } - parent - } -} - -impl Visitor for TypeChecker<'_> { - fn enter_tree(&mut self, ctx: &VisitCtx, tree: NodeId) -> Visit { - let tree = ctx.node(tree).unwrap(); - self.depth += 1; - let depth = self.depth; - - self.trace_stack(depth, ">", tree.kind); - - 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"); - - if matches!(value_type, K::Struct | K::Embedded) { - self.ctx.diagnostics.push( - MissingClassName { - span: tree.open_brace_span(ctx.cst), - expected: RitoType::simple(value_type), - } - .unwrap(), - ); - } - - self.stack.push(( - depth, - IrItem::ListItem(IrListItem({ - let mut v = value_type.default_value(); - *v.meta_mut() = tree.span; - v - })), - )); - } - _parent_type => { - self.ctx.diagnostics.push( - UnexpectedTree { - tree: tree.kind, - expected: Some(Kind::Entry), - span: tree.span, - } - .unwrap(), - ); - } - } - } - 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 get_color_vec_type = |kind: PropertyKind| match kind { - K::Vector2 | K::Vector3 | K::Vector4 | K::Matrix44 => Some(K::F32), - K::Color => Some(K::U8), - _ => None, - }; - - let color_vec_type = get_color_vec_type(parent_type.base) - .or(parent_type.value_subtype().and_then(get_color_vec_type)); - - let value_hint = color_vec_type - .or(parent_type.value_subtype()) - .map(RitoType::simple); - - let type_span = parent.type_span(); - - match resolve_value(&mut self.ctx, ctx, tree, value_hint, type_span) { - Ok(Some(item)) => { - trace!(" 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) => { - trace!(" ERROR empty item"); - for child in tree.children.get(ctx.cst).iter() { - let (got, span) = match child { - cst::Child::Token(token_id) => { - let tok = ctx.cst.token(*token_id).unwrap(); - (RitoTypeOrVirtual::Token(tok.kind), tok.span) - } - cst::Child::Tree(node_id) => { - let node = ctx.cst.node(*node_id).unwrap(); - (RitoTypeOrVirtual::Tree(node.kind), node.span) - } - }; - self.ctx.diagnostics.push( - TypeMismatch { - span, - got, - expected: value_hint - .unwrap_or(RitoType::simple(PropertyKind::None)), - expected_span: type_span, - } - .unwrap(), - ); - } - } - Err(e) => self.ctx.diagnostics.push(e.default_span(tree.span)), - } - } - - Kind::Entry => { - match resolve_entry( - &mut self.ctx, - ctx, - tree, - parent.map(|p| p.1.value().rito_type()), - parent.and_then(|p| p.1.type_span()), - ) - .map_err(|e| e.fallback(tree.span)) - { - Ok(entry) => { - self.stack.push((depth, IrItem::Entry(entry))); - } - Err(e) => self.ctx.diagnostics.push(e), - } - } - - _ => {} - } - - match self.stack.last() { - Some(_) => {} - None => match tree.kind { - Kind::Entry | Kind::Comment | Kind::File => return Visit::Continue, - _ => { - if depth == 2 { - self.ctx - .diagnostics - .push(RootNonEntry.default_span(tree.span)); - } - return Visit::Skip; - } - }, - } - - Visit::Continue - } - - fn exit_tree(&mut self, ctx: &VisitCtx, tree: NodeId) -> Visit { - let tree = ctx.node(tree).unwrap(); - let depth = self.depth; - self.depth -= 1; - - self.trace_stack(depth, "<", tree.kind); - if tree.kind == cst::Kind::ErrorTree { - return Visit::Continue; - } - - if let Some(mut ir) = self.stack.pop() { - self.trace_popped(depth, ir.0); - if ir.0 != depth { - self.stack.push(ir); - return Visit::Continue; - } - - // a listlike written as a list item declares no type of its own - let type_span = - ir.1.type_span() - .or_else(|| self.stack.last().and_then(|(_, parent)| parent.type_span())); - - if let Err(e) = try_populate_listlike(&mut ir.1, &mut self.list_queue, type_span) { - self.ctx.diagnostics.push(e.fallback(*ir.1.value().meta())); - } - - match self.stack.pop() { - Some((d, parent)) => { - let parent = self.merge_ir(parent, ir.1); - self.stack.push((d, parent)); - } - None => { - if depth != 2 { - return Visit::Continue; - } - let IrItem::Entry(IrEntry { - key: key @ PropertyValueEnum::String(values::String { .. }), - value, - .. - }) = ir.1 - else { - self.ctx - .diagnostics - .push(RootNonEntry.default_span(tree.span)); - return Visit::Continue; - }; - let key_span = *key.meta(); - if let Some(existing) = self.root.insert( - RootKindOrUnknown::from_value(self.ctx.text, &key), - RootEntry { - key, - type_span: key_span, - value, - }, // FIXME: get real type span in here - ) { - self.ctx.diagnostics.push( - ShadowedEntry { - shadowee: *existing.key.meta(), - shadower: key_span, - } - .unwrap(), - ); - } - } - } - } - - Visit::Continue - } -} diff --git a/crates/ltk_ritobin/src/types.rs b/crates/ltk_ritobin/src/types.rs index a127e699..a813a637 100644 --- a/crates/ltk_ritobin/src/types.rs +++ b/crates/ltk_ritobin/src/types.rs @@ -92,6 +92,7 @@ pub struct RitoType { 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()), @@ -103,14 +104,136 @@ impl Display for RitoType { } } +#[macro_export] +macro_rules! rito { + ($kind:ident) => { + const { + match RitoType::try_new(ltk_meta::PropertyKind::$kind, [None, None]) { + Ok(t) => t, + Err(_) => panic!("invalid simple rito type"), + } + } + }; + ($kind:ident [ $sub:ident ]) => { + const { + match RitoType::try_new( + ltk_meta::PropertyKind::$kind, + [Some(ltk_meta::PropertyKind::$sub), None], + ) { + Ok(t) => t, + Err(_) => panic!("invalid rito type"), + } + } + }; + ($kind:ident [ $a:ident, $b:ident ]) => { + const { + match RitoType::try_new( + ltk_meta::PropertyKind::$kind, + [ + Some(ltk_meta::PropertyKind::$a), + Some(ltk_meta::PropertyKind::$b), + ], + ) { + Ok(t) => t, + Err(_) => panic!("invalid rito type"), + } + } + }; +} + +#[derive(thiserror::Error, Debug)] +pub enum ConstructError { + #[error("Got {got} subtypes, expected {expected}")] + BadSubtypeCount { got: u8, expected: u8 }, + #[error("{got:?} is not primitive - containers can only hold primitive types")] + NonPrimitiveContainer { got: PropertyKind }, + #[error("{got:?} is not a valid map key")] + BadMapKey { got: PropertyKind }, + #[error("{got:?} is not a valid map value")] + BadMapValue { got: PropertyKind }, +} + impl RitoType { - pub fn simple(kind: PropertyKind) -> Self { + pub const fn simple(kind: PropertyKind) -> Self { Self { base: kind, subtypes: [None, None], } } + pub const fn single(base: PropertyKind, sub: PropertyKind) -> Option { + if base.subtype_count() != 1 { + return None; + } + Some(Self { + base, + subtypes: [Some(sub), None], + }) + } + pub const fn new(base: PropertyKind, subtypes: [Option; 2]) -> Self { + Self { base, subtypes } + } + + pub const fn try_new( + base: PropertyKind, + subtypes: [Option; 2], + ) -> Result { + use ConstructError::*; + + match base.subtype_count() { + 0 => match subtypes { + [None, None] => Ok(Self::simple(base)), + [Some(_), None] | [None, Some(_)] => Err(BadSubtypeCount { + got: 1, + expected: 0, + }), + [Some(_), Some(_)] => Err(BadSubtypeCount { + got: 2, + expected: 0, + }), + }, + 1 => match subtypes { + [Some(sub), None] | [None, Some(sub)] => { + if !sub.is_primitive() { + return Err(NonPrimitiveContainer { got: sub }); + } + Ok(RitoType { + base, + subtypes: [Some(sub), None], + }) + } + [None, None] => Err(BadSubtypeCount { + got: 0, + expected: 1, + }), + [Some(_), Some(_)] => Err(BadSubtypeCount { + got: 2, + expected: 1, + }), + }, + 2 => match subtypes { + [None, None] => Err(BadSubtypeCount { + got: 0, + expected: 2, + }), + [Some(_), None] | [None, Some(_)] => Err(BadSubtypeCount { + got: 1, + expected: 2, + }), + [Some(a), Some(b)] => { + if !a.is_valid_map_key() { + return Err(BadMapKey { got: a }); + } + if b.is_container() { + return Err(BadMapValue { got: b }); + } + Ok(RitoType { base, subtypes }) + } + }, + _ => unreachable!(), + } + } + pub fn container(value: PropertyKind) -> Self { Self { base: PropertyKind::Container, diff --git a/crates/ltk_ritobin/tests/no_panic.rs b/crates/ltk_ritobin/tests/no_panic.rs new file mode 100644 index 00000000..b98a5dee --- /dev/null +++ b/crates/ltk_ritobin/tests/no_panic.rs @@ -0,0 +1,14 @@ +use ltk_ritobin::cst::Cst; + +use proptest::prelude::*; + +proptest! { + #![proptest_config(ProptestConfig::with_cases(256))] + + //TODO: better arbitrary source gen + #[test] + fn build_bin_never_panics_on_arbitrary_text(text in ".{0,400}") { + let cst = Cst::parse(&text); + let _ = cst.build_bin(&text); + } +} diff --git a/crates/ltk_ritobin/tests/parse_sample.rs b/crates/ltk_ritobin/tests/parse_sample.rs index 41a6fd39..62ec9773 100644 --- a/crates/ltk_ritobin/tests/parse_sample.rs +++ b/crates/ltk_ritobin/tests/parse_sample.rs @@ -56,8 +56,13 @@ fn tree(input: &str) -> String { #[test] fn test_roundtrip() { let cst = Cst::parse(SAMPLE_RITOBIN); - let (tree, errors) = cst.build_bin(SAMPLE_RITOBIN); - assert!(errors.is_empty(), "errors = {errors:#?}"); + let partial = cst.build_bin(SAMPLE_RITOBIN); + assert!( + partial.diagnostics.is_empty(), + "errors = {:#?}", + partial.diagnostics + ); + let tree = partial.bin; // Write back to text let output = tree.print().expect("Failed to write"); @@ -76,8 +81,13 @@ fn test_roundtrip() { cst2.print(&mut str, &output); println!("reparsed:\n{str}"); - let (tree2, errors) = cst2.build_bin(&output); - assert!(errors.is_empty(), "build bin errors = {errors:#?}"); + let partial2 = cst2.build_bin(&output); + assert!( + partial2.diagnostics.is_empty(), + "build bin errors = {:#?}", + partial2.diagnostics + ); + let tree2 = partial2.bin; // Verify structure is preserved assert_eq!(tree.version, tree2.version);