From 22d25e5cb261e9f97eef9d98384b9ae3cd845099 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Mon, 30 Mar 2026 15:16:19 +0100 Subject: [PATCH] Podlang syntax for quoted predicates (#495) --- src/frontend/custom.rs | 6 ++ src/lang/diagnostics.rs | 11 +++ src/lang/error.rs | 3 + src/lang/frontend_ast.rs | 40 ++++++++++ src/lang/frontend_ast_lower.rs | 103 ++++++++++++++++++++++++-- src/lang/frontend_ast_split.rs | 2 +- src/lang/frontend_ast_validate.rs | 88 +++++++++++++++++++++- src/lang/grammar.pest | 11 ++- src/lang/module.rs | 118 +++++++++++++++++++++++++++++- src/lang/parser.rs | 30 ++++++++ src/lang/pretty_print.rs | 55 +++++++++++++- 11 files changed, 453 insertions(+), 14 deletions(-) diff --git a/src/frontend/custom.rs b/src/frontend/custom.rs index f3a8115..a2614a0 100644 --- a/src/frontend/custom.rs +++ b/src/frontend/custom.rs @@ -176,6 +176,12 @@ impl CustomPredicateBatchBuilder { priv_args: &[&str], sts: &[StatementTmplBuilder], ) -> Result { + if self.predicates.iter().any(|p| p.name == name) { + return Err(Error::custom(format!( + "Duplicate predicate name '{}' in batch", + name + ))); + } if self.predicates.len() >= Params::max_custom_batch_size() { return Err(Error::max_length( "self.predicates.len".to_string(), diff --git a/src/lang/diagnostics.rs b/src/lang/diagnostics.rs index ea528ef..0a1d770 100644 --- a/src/lang/diagnostics.rs +++ b/src/lang/diagnostics.rs @@ -287,6 +287,17 @@ fn render_validation_error( ValidationError::NoRequestBlock => { render_title_only(renderer, "requests must contain a REQUEST block") } + + ValidationError::SelfReferentialPredicateLiteralNotAllowedInRequests { span } => { + render_with_optional_span( + renderer, + source, + path, + "self-referential predicate literal not allowed in requests", + span.as_ref(), + "not allowed here", + ) + } } } diff --git a/src/lang/error.rs b/src/lang/error.rs index 944988c..769faf6 100644 --- a/src/lang/error.rs +++ b/src/lang/error.rs @@ -165,6 +165,9 @@ pub enum ValidationError { #[error("Modules must contain at least one predicate definition")] NoPredicatesInModule, + #[error("Self-referential predicate literal not allowed in requests")] + SelfReferentialPredicateLiteralNotAllowedInRequests { span: Option }, + #[error("Requests must contain a REQUEST block")] NoRequestBlock, } diff --git a/src/lang/frontend_ast.rs b/src/lang/frontend_ast.rs index 4ca7fe4..dd0052c 100644 --- a/src/lang/frontend_ast.rs +++ b/src/lang/frontend_ast.rs @@ -116,6 +116,8 @@ pub enum StatementTmplArg { Literal(LiteralValue), Wildcard(Identifier), AnchoredKey(AnchoredKey), + /// Hash of a same-module predicate, resolved at batch finalization time. + SelfPredicateHash(Identifier), } /// Anchored key: Var["key"] or Var.key @@ -168,6 +170,13 @@ pub enum LiteralValue { Array(LiteralArray), Set(LiteralSet), Dict(LiteralDict), + /// Hash of a native predicate (resolved immediately). + NativePredicateHash(Identifier), + /// Hash of an external module's predicate (resolved immediately). + ExternalPredicateHash { + module: Identifier, + predicate: Identifier, + }, } /// Integer literal @@ -391,6 +400,9 @@ impl fmt::Display for StatementTmplArg { StatementTmplArg::Literal(lit) => write!(f, "{}", lit), StatementTmplArg::Wildcard(id) => write!(f, "{}", id), StatementTmplArg::AnchoredKey(ak) => write!(f, "{}", ak), + StatementTmplArg::SelfPredicateHash(id) => { + write!(f, "@self_predicate({})", id) + } } } } @@ -422,6 +434,12 @@ impl fmt::Display for LiteralValue { LiteralValue::Array(a) => write!(f, "{}", a), LiteralValue::Set(s) => write!(f, "{}", s), LiteralValue::Dict(d) => write!(f, "{}", d), + LiteralValue::NativePredicateHash(id) => { + write!(f, "@native_predicate({})", id) + } + LiteralValue::ExternalPredicateHash { + module, predicate, .. + } => write!(f, "@external_predicate({}, {})", module, predicate), } } } @@ -769,6 +787,10 @@ pub mod parse { let inner = pair.into_inner().next().unwrap(); match inner.as_rule() { + Rule::predicate_hash_self => { + let id = parse_identifier(inner.into_inner().next().unwrap()); + Ok(StatementTmplArg::SelfPredicateHash(id)) + } Rule::literal_value => Ok(StatementTmplArg::Literal(parse_literal_value(inner)?)), Rule::identifier => Ok(StatementTmplArg::Wildcard(parse_identifier(inner))), Rule::anchored_key => Ok(StatementTmplArg::AnchoredKey(parse_anchored_key(inner)?)), @@ -823,6 +845,16 @@ pub mod parse { Rule::literal_array => Ok(LiteralValue::Array(parse_literal_array(inner)?)), Rule::literal_set => Ok(LiteralValue::Set(parse_literal_set(inner)?)), Rule::literal_dict => Ok(LiteralValue::Dict(parse_literal_dict(inner)?)), + Rule::predicate_hash_native => { + let id = parse_identifier(inner.into_inner().next().unwrap()); + Ok(LiteralValue::NativePredicateHash(id)) + } + Rule::predicate_hash_external => { + let mut parts = inner.into_inner(); + let module = parse_identifier(parts.next().unwrap()); + let predicate = parse_identifier(parts.next().unwrap()); + Ok(LiteralValue::ExternalPredicateHash { module, predicate }) + } _ => unreachable!("Unexpected literal value rule: {:?}", inner.as_rule()), } } @@ -1104,6 +1136,7 @@ mod tests { AnchoredKeyPath::Dot(id) => id.span = None, } } + StatementTmplArg::SelfPredicateHash(id) => id.span = None, } } } @@ -1139,6 +1172,13 @@ mod tests { clear_literal_spans(&mut pair.value); } } + LiteralValue::NativePredicateHash(id) => id.span = None, + LiteralValue::ExternalPredicateHash { + module, predicate, .. + } => { + module.span = None; + predicate.span = None; + } } } diff --git a/src/lang/frontend_ast_lower.rs b/src/lang/frontend_ast_lower.rs index fe9b745..fb00def 100644 --- a/src/lang/frontend_ast_lower.rs +++ b/src/lang/frontend_ast_lower.rs @@ -157,8 +157,10 @@ fn resolve_local_predicate( /// Lower a literal value from AST to middleware Value. /// -/// This is a pure conversion that cannot fail. -pub fn lower_literal(lit: &LiteralValue) -> Value { +/// This is a pure conversion that cannot fail for context-free literals. +/// Panics on ExternalPredicateHash — use `lower_literal_with_context` when +/// external predicate references may appear (e.g. inside containers). +pub(crate) fn lower_literal(lit: &LiteralValue) -> Value { match lit { LiteralValue::Int(i) => Value::from(i.value), LiteralValue::Bool(b) => Value::from(b.value), @@ -190,13 +192,83 @@ pub fn lower_literal(lit: &LiteralValue) -> Value { let dict = containers::Dictionary::new(pairs); Value::from(dict) } + LiteralValue::NativePredicateHash(id) => { + let np = NativePredicate::from_str(&id.name).expect("validated native predicate"); + Value::from(Predicate::Native(np).hash()) + } + LiteralValue::ExternalPredicateHash { .. } => { + unreachable!( + "ExternalPredicateHash must be lowered with context via lower_literal_with_context" + ) + } + } +} + +/// Lower a literal value, resolving external predicate references using the symbol table. +pub fn lower_literal_with_context( + lit: &LiteralValue, + symbols: &SymbolTable, + context: &ResolutionContext, +) -> Result { + match lit { + LiteralValue::ExternalPredicateHash { module, predicate } => { + let pred_or_wc = resolve_predicate_ref( + &PredicateRef::Qualified { + module: module.clone(), + predicate: predicate.clone(), + }, + symbols, + context, + ) + .ok_or_else(|| LoweringError::PredicateNotFound { + name: format!("{}::{}", module.name, predicate.name), + })?; + let pred = match pred_or_wc { + crate::frontend::PredicateOrWildcard::Predicate(p) => p, + _ => unreachable!( + "`resolve_predicate_ref` always returns `PredicateOrWildcard::Predicate` on `PredicateRef::Qualified`" + ) + }; + Ok(Value::from(pred.hash())) + } + LiteralValue::Array(a) => { + let elements: Vec<_> = a + .elements + .iter() + .map(|e| lower_literal_with_context(e, symbols, context)) + .collect::>()?; + Ok(Value::from(containers::Array::new(elements))) + } + LiteralValue::Set(s) => { + let elements: std::collections::HashSet<_> = s + .elements + .iter() + .map(|e| lower_literal_with_context(e, symbols, context)) + .collect::>()?; + Ok(Value::from(containers::Set::new(elements))) + } + LiteralValue::Dict(d) => { + let pairs: HashMap<_, _> = d + .pairs + .iter() + .map(|pair| { + let key = Key::from(pair.key.value.as_str()); + let value = lower_literal_with_context(&pair.value, symbols, context)?; + Ok((key, value)) + }) + .collect::>()?; + Ok(Value::from(containers::Dictionary::new(pairs))) + } + // All other variants are context-free + other => Ok(lower_literal(other)), } } /// Lower a statement argument from AST to BuilderArg. /// -/// This is a pure conversion that cannot fail. -pub fn lower_statement_arg(arg: &StatementTmplArg) -> BuilderArg { +/// Context-free for most arg types. Panics on ExternalPredicateHash inside literals — +/// use `lower_statement_arg_with_context` when external predicate references may appear. +pub(crate) fn lower_statement_arg(arg: &StatementTmplArg) -> BuilderArg { match arg { StatementTmplArg::Literal(lit) => { let value = lower_literal(lit); @@ -210,6 +282,25 @@ pub fn lower_statement_arg(arg: &StatementTmplArg) -> BuilderArg { }; BuilderArg::Key(ak.root.name.clone(), key_str) } + StatementTmplArg::SelfPredicateHash(id) => BuilderArg::SelfPredicateHash(id.name.clone()), + } +} + +/// Lower a statement argument, resolving external predicate references using the symbol table. +pub fn lower_statement_arg_with_context( + arg: &StatementTmplArg, + symbols: &SymbolTable, + context: &ResolutionContext, +) -> Result { + match arg { + StatementTmplArg::Literal(lit) => { + let value = lower_literal_with_context(lit, symbols, context)?; + Ok(BuilderArg::Literal(value)) + } + StatementTmplArg::SelfPredicateHash(id) => { + Ok(BuilderArg::SelfPredicateHash(id.name.clone())) + } + other => Ok(lower_statement_arg(other)), } } @@ -324,7 +415,7 @@ impl<'a> Lowerer<'a> { // Create a builder with the resolved predicate and desugar let mut builder = StatementTmplBuilder::new(predicate.clone()); for arg in &stmt.args { - let builder_arg = lower_statement_arg(arg); + let builder_arg = lower_statement_arg_with_context(arg, symbols, &context)?; builder = builder.arg(builder_arg); } let desugared = builder.desugar(); @@ -402,7 +493,7 @@ impl<'a> Lowerer<'a> { names.push(ak.root.name.clone()); } } - StatementTmplArg::Literal(_) => {} + StatementTmplArg::Literal(_) | StatementTmplArg::SelfPredicateHash(_) => {} } } } diff --git a/src/lang/frontend_ast_split.rs b/src/lang/frontend_ast_split.rs index 0d17217..482db7a 100644 --- a/src/lang/frontend_ast_split.rs +++ b/src/lang/frontend_ast_split.rs @@ -123,7 +123,7 @@ fn collect_wildcards_from_statement(stmt: &StatementTmpl) -> HashSet { StatementTmplArg::AnchoredKey(ak) => { wildcards.insert(ak.root.name.clone()); } - StatementTmplArg::Literal(_) => {} + StatementTmplArg::Literal(_) | StatementTmplArg::SelfPredicateHash(_) => {} } } diff --git a/src/lang/frontend_ast_validate.rs b/src/lang/frontend_ast_validate.rs index 49575b5..0b7737d 100644 --- a/src/lang/frontend_ast_validate.rs +++ b/src/lang/frontend_ast_validate.rs @@ -559,7 +559,12 @@ impl Validator { } } } - StatementTmplArg::Literal(_) => {} + StatementTmplArg::Literal(lit) => { + self.validate_literal_value(lit)?; + } + StatementTmplArg::SelfPredicateHash(id) => { + self.validate_self_predicate_hash(id, wildcard_context)?; + } } } } else { @@ -588,13 +593,92 @@ impl Validator { } } } - StatementTmplArg::Literal(_) => {} + StatementTmplArg::Literal(lit) => { + self.validate_literal_value(lit)?; + } + StatementTmplArg::SelfPredicateHash(id) => { + self.validate_self_predicate_hash(id, wildcard_context)?; + } } } } Ok(()) } + + /// Validate a @self_predicate reference: the name must be a custom predicate in this module. + fn validate_self_predicate_hash( + &self, + id: &Identifier, + wildcard_context: Option<(&str, &WildcardScope)>, + ) -> Result<(), ValidationError> { + // @self_predicate only makes sense inside module predicate definitions + if wildcard_context.is_none() { + return Err( + ValidationError::SelfReferentialPredicateLiteralNotAllowedInRequests { + span: id.span, + }, + ); + } + // Must refer to a custom predicate defined in this module (not intro/imported) + match self.symbols.predicates.get(&id.name) { + Some(info) if matches!(info.kind, PredicateKind::Custom { .. }) => Ok(()), + _ => Err(ValidationError::UndefinedPredicate { + name: id.name.clone(), + span: id.span, + }), + } + } + + /// Recursively validate a literal value, checking predicate hash references. + fn validate_literal_value(&self, lit: &LiteralValue) -> Result<(), ValidationError> { + match lit { + LiteralValue::NativePredicateHash(id) => { + if NativePredicate::from_str(&id.name).is_err() { + return Err(ValidationError::UndefinedPredicate { + name: id.name.clone(), + span: id.span, + }); + } + Ok(()) + } + LiteralValue::ExternalPredicateHash { module, predicate } => { + if let Some(imported) = self.symbols.imported_modules.get(&module.name) { + if !imported.predicate_index.contains_key(&predicate.name) { + return Err(ValidationError::UndefinedPredicate { + name: format!("{}::{}", module.name, predicate.name), + span: predicate.span, + }); + } + } else { + return Err(ValidationError::ModuleNotFound { + name: module.name.clone(), + span: module.span, + }); + } + Ok(()) + } + LiteralValue::Array(a) => { + for elem in &a.elements { + self.validate_literal_value(elem)?; + } + Ok(()) + } + LiteralValue::Set(s) => { + for elem in &s.elements { + self.validate_literal_value(elem)?; + } + Ok(()) + } + LiteralValue::Dict(d) => { + for pair in &d.pairs { + self.validate_literal_value(&pair.value)?; + } + Ok(()) + } + _ => Ok(()), + } + } } #[cfg(test)] diff --git a/src/lang/grammar.pest b/src/lang/grammar.pest index 3002d15..1c11baa 100644 --- a/src/lang/grammar.pest +++ b/src/lang/grammar.pest @@ -49,7 +49,14 @@ custom_predicate_def = { statement_list = { statement+ } -statement_arg = { literal_value | anchored_key | identifier } +// Predicate hash literals: resolve to the predicate's identity hash as a value. +// @native_predicate and @external_predicate are in literal_value (usable in containers). +// @self_predicate is only in statement_arg (not in containers — deferred resolution). +predicate_hash_native = { "@native_predicate" ~ "(" ~ identifier ~ ")" } +predicate_hash_external = { "@external_predicate" ~ "(" ~ identifier ~ "," ~ identifier ~ ")" } +predicate_hash_self = { "@self_predicate" ~ "(" ~ identifier ~ ")" } + +statement_arg = { predicate_hash_self | literal_value | anchored_key | identifier } statement_arg_list = { statement_arg ~ ("," ~ statement_arg)* } // Predicate reference: either qualified (module::predicate) or local (predicate) @@ -74,6 +81,8 @@ literal_value = { literal_bool | literal_raw | literal_string | + predicate_hash_native | + predicate_hash_external | literal_int } diff --git a/src/lang/module.rs b/src/lang/module.rs index 78fb22e..b926871 100644 --- a/src/lang/module.rs +++ b/src/lang/module.rs @@ -11,7 +11,9 @@ use crate::{ lang::{ error::BatchingError, frontend_ast::{ConjunctionType, CustomPredicateDef}, - frontend_ast_lower::{lower_statement_arg, resolve_predicate_ref, ResolutionContext}, + frontend_ast_lower::{ + lower_statement_arg_with_context, resolve_predicate_ref, ResolutionContext, + }, frontend_ast_split::{SplitChainInfo, SplitResult}, frontend_ast_validate::SymbolTable, }, @@ -374,7 +376,13 @@ fn build_statement_with_resolved_refs( let mut builder = StatementTmplBuilder::new(pred_or_wc); for arg in &stmt.args { - builder = builder.arg(lower_statement_arg(arg)); + let builder_arg = + lower_statement_arg_with_context(arg, symbols, &context).map_err(|e| { + BatchingError::Internal { + message: format!("Failed to lower argument: {}", e), + } + })?; + builder = builder.arg(builder_arg); } Ok(builder) @@ -670,4 +678,110 @@ mod tests { PredicateOrWildcard::Predicate(Predicate::Custom(ordering_ref)) ); } + + #[test] + fn test_self_predicate_hash_podlang() { + let params = Params::default(); + let module = load_module( + r#" + pred_A(x, y) = AND( + Equal(x, y) + ) + + pred_B(x) = AND( + Equal(x, @self_predicate(pred_A)) + ) + "#, + "test", + ¶ms, + &[], + ) + .unwrap(); + + let batch = &module.batch; + + // pred_B is at index 1, its template should have SelfPredicateHash(0) resolved + // to a Literal containing pred_A's hash after normalization + let pred_a_ref = CustomPredicateRef::new(batch.clone(), 0); + let pred_a_hash = crate::middleware::Value::from(Predicate::Custom(pred_a_ref).hash()); + + // Use normalized_predicate to resolve + let pred_b_ref = CustomPredicateRef::new(batch.clone(), 1); + let normalized = pred_b_ref.normalized_predicate(); + assert_eq!( + normalized.statements[0].args[1], + crate::middleware::StatementTmplArg::Literal(pred_a_hash) + ); + } + + #[test] + fn test_self_predicate_hash_podlang_cyclic() { + let params = Params::default(); + let module = load_module( + r#" + pred_A(x) = AND( + Equal(x, @self_predicate(pred_B)) + ) + + pred_B(x) = AND( + Equal(x, @self_predicate(pred_A)) + ) + "#, + "test", + ¶ms, + &[], + ) + .unwrap(); + + let batch = &module.batch; + let pred_a_ref = CustomPredicateRef::new(batch.clone(), 0); + let pred_b_ref = CustomPredicateRef::new(batch.clone(), 1); + let pred_a_hash = + crate::middleware::Value::from(Predicate::Custom(pred_a_ref.clone()).hash()); + let pred_b_hash = + crate::middleware::Value::from(Predicate::Custom(pred_b_ref.clone()).hash()); + + // pred_A's normalized form should contain pred_B's hash + let norm_a = pred_a_ref.normalized_predicate(); + assert_eq!( + norm_a.statements[0].args[1], + crate::middleware::StatementTmplArg::Literal(pred_b_hash) + ); + + // pred_B's normalized form should contain pred_A's hash + let norm_b = pred_b_ref.normalized_predicate(); + assert_eq!( + norm_b.statements[0].args[1], + crate::middleware::StatementTmplArg::Literal(pred_a_hash) + ); + } + + #[test] + fn test_native_predicate_hash_podlang() { + let params = Params::default(); + let module = load_module( + r#" + pred_C(x) = AND( + Equal(x, @native_predicate(Equal)) + ) + "#, + "test", + ¶ms, + &[], + ) + .unwrap(); + + let batch = &module.batch; + let pred_c_ref = CustomPredicateRef::new(batch.clone(), 0); + let pred_c = pred_c_ref.predicate(); + + // The second arg should be a Literal containing Equal's predicate hash + let equal_hash = crate::middleware::Value::from( + Predicate::Native(crate::middleware::NativePredicate::Equal).hash(), + ); + assert_eq!( + pred_c.statements[0].args[1], + crate::middleware::StatementTmplArg::Literal(equal_hash) + ); + } } diff --git a/src/lang/parser.rs b/src/lang/parser.rs index 000e683..1a29113 100644 --- a/src/lang/parser.rs +++ b/src/lang/parser.rs @@ -137,6 +137,9 @@ mod tests { assert_inner(&Rule::anchored_key, "someVar[\"key\"]"); assert_inner(&Rule::literal_value, "true"); assert_inner(&Rule::literal_value, "PublicKey(abc)"); + assert_inner(&Rule::predicate_hash_self, "@self_predicate(foo)"); + assert_inner(&Rule::literal_value, "@native_predicate(Equal)"); + assert_inner(&Rule::literal_value, "@external_predicate(mod_a, pred_b)"); } #[test] @@ -207,6 +210,33 @@ mod tests { "{ \"raw_val\": Raw(0x0000000000000000000000000000000000000000000000000000000000000000) } ", ); assert_fails(Rule::literal_dict, "{ name: \"Alice\" }"); // Key must be string literal with quotes + + // Predicate hash literals + assert_parses(Rule::predicate_hash_native, "@native_predicate(Equal)"); + assert_parses(Rule::predicate_hash_native, "@native_predicate(Lt)"); + assert_parses( + Rule::predicate_hash_external, + "@external_predicate(my_module, my_pred)", + ); + assert_parses(Rule::predicate_hash_self, "@self_predicate(local_pred)"); + + // Predicate hashes inside containers (native and external only) + assert_parses( + Rule::literal_array, + "[1, @native_predicate(Equal), @external_predicate(m, p)]", + ); + assert_parses( + Rule::literal_set, + "#[@native_predicate(Equal), @native_predicate(Lt)]", + ); + assert_parses( + Rule::literal_dict, + "{ \"pred\": @external_predicate(m, p) }", + ); + + // @self_predicate is NOT a literal_value, so it cannot appear inside containers + assert_fails(Rule::test_literal_value, "@self_predicate(local_pred)"); + assert_fails(Rule::literal_array, "[@self_predicate(foo)]"); } #[test] diff --git a/src/lang/pretty_print.rs b/src/lang/pretty_print.rs index bd912cb..8e4819d 100644 --- a/src/lang/pretty_print.rs +++ b/src/lang/pretty_print.rs @@ -92,7 +92,7 @@ impl StatementTmpl { if i > 0 { write!(w, ", ")?; } - arg.fmt_podlang(w)?; + arg.fmt_podlang_with_batch_context(w, batch_context)?; } write!(w, ")")?; @@ -102,7 +102,30 @@ impl StatementTmpl { impl PrettyPrint for StatementTmplArg { fn fmt_podlang_with_indent(&self, w: &mut dyn Write, _indent: usize) -> std::fmt::Result { - write!(w, "{}", self) + self.fmt_podlang_with_batch_context(w, None) + } +} + +impl StatementTmplArg { + fn fmt_podlang_with_batch_context( + &self, + w: &mut dyn Write, + batch_context: Option<&CustomPredicateBatch>, + ) -> std::fmt::Result { + match self { + StatementTmplArg::SelfPredicateHash(index) => { + if let Some(batch) = batch_context { + if let Some(predicate) = batch.predicates().get(*index) { + write!(w, "@self_predicate({})", predicate.name) + } else { + write!(w, "@self_predicate(self_{})", index) + } + } else { + write!(w, "@self_predicate(self_{})", index) + } + } + other => write!(w, "{}", other), + } } } @@ -540,6 +563,34 @@ mod tests { assert_round_trip(&input); } + #[test] + fn test_round_trip_self_predicate_hash() { + let input = r#" + pred_A(x, y) = AND( + Equal(x, y) + ) + + pred_B(x) = AND( + Equal(x, @self_predicate(pred_A)) + ) + "#; + assert_round_trip(input); + } + + #[test] + fn test_round_trip_self_predicate_hash_cyclic() { + let input = r#" + pred_A(x) = AND( + Equal(x, @self_predicate(pred_B)) + ) + + pred_B(x) = AND( + Equal(x, @self_predicate(pred_A)) + ) + "#; + assert_round_trip(input); + } + #[test] fn test_pretty_print_demonstration() { let input = r#"