nomo/src/ast/mod.rs
Marcel Müller 605798674f Make usage of the $side metavariable
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-12 18:00:45 +01:00

1011 lines
28 KiB
Rust

use thiserror::Error;
use winnow::Parser;
use winnow::RecoverableParser;
use winnow::combinator::Infix::Left;
use winnow::combinator::alt;
use winnow::combinator::cut_err;
use winnow::combinator::delimited;
use winnow::combinator::dispatch;
use winnow::combinator::expression;
use winnow::combinator::not;
use winnow::combinator::opt;
use winnow::combinator::peek;
use winnow::combinator::preceded;
use winnow::combinator::repeat;
use winnow::combinator::repeat_till;
use winnow::combinator::trace;
use winnow::error::AddContext;
use winnow::error::FromRecoverableError;
use winnow::error::ModalError;
use winnow::error::ParserError;
use winnow::stream::Offset;
use winnow::stream::Recoverable;
use winnow::stream::Stream;
use winnow::stream::TokenSlice;
use winnow::token::any;
use crate::SourceSpan;
use crate::parser::TemplateToken;
use crate::parser::TokenKind;
use crate::parser::TokenOperator;
use crate::resume_after_cut;
use crate::value::NomoValue;
#[derive(Debug, Clone)]
pub struct TemplateAst<'input> {
root: Vec<TemplateAstExpr<'input>>,
}
impl TemplateAst<'_> {
pub fn root(&self) -> &[TemplateAstExpr<'_>] {
&self.root
}
}
#[derive(Debug, Clone)]
pub struct AstError {
pub(crate) message: Option<String>,
pub(crate) help: Option<String>,
pub(crate) span: Option<crate::SourceSpan>,
is_fatal: bool,
}
impl AstError {
fn ctx() -> Self {
AstError {
message: None,
help: None,
span: None,
is_fatal: false,
}
}
fn msg(mut self, message: &str) -> Self {
self.message = Some(message.to_string());
self
}
fn help(mut self, help: &str) -> Self {
self.help = Some(help.to_string());
self
}
}
impl ModalError for AstError {
fn cut(mut self) -> Self {
self.is_fatal = true;
self
}
fn backtrack(mut self) -> Self {
self.is_fatal = false;
self
}
}
impl FromRecoverableError<Input<'_>, AstError> for AstError {
fn from_recoverable_error(
token_start: &<Input as winnow::stream::Stream>::Checkpoint,
_err_start: &<Input as winnow::stream::Stream>::Checkpoint,
input: &Input,
mut e: AstError,
) -> Self {
e.span = e.span.or_else(|| {
let offset = input.offset_from(token_start);
let mut tokens = input
.previous_tokens()
.take(offset)
.filter(|t| t.kind() != TokenKind::Whitespace);
let last = tokens.next();
let first = tokens.last();
match (last, first) {
(None, None) => None,
(None, Some(single)) | (Some(single), None) => Some(SourceSpan {
range: single.source().get_range(),
}),
(Some(last), Some(first)) => {
let start = first.source().get_range().start;
let end = last.source().get_range().end;
Some(SourceSpan { range: start..end })
}
}
});
e
}
}
impl AddContext<Input<'_>, AstError> for AstError {
fn add_context(
mut self,
_input: &Input,
_token_start: &<Input as Stream>::Checkpoint,
context: AstError,
) -> Self {
self.message = context.message.or(self.message);
self.help = context.help.or(self.help);
self
}
}
impl ParserError<Input<'_>> for AstError {
type Inner = AstError;
fn from_input(_input: &Input<'_>) -> Self {
AstError::ctx()
}
fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
Ok(self)
}
fn is_backtrack(&self) -> bool {
!self.is_fatal
}
}
#[derive(Debug, Error)]
pub struct AstFailure {
errors: Vec<AstError>,
}
impl std::fmt::Display for AstFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("TODO")
}
}
impl AstFailure {
fn from_errors(errors: Vec<AstError>) -> AstFailure {
AstFailure { errors }
}
pub fn to_report(&self, source: &str) -> String {
let reports = self
.errors
.iter()
.map(|error| {
annotate_snippets::Level::ERROR
.primary_title(
error
.message
.as_deref()
.unwrap_or("An error occurred while producing an Ast"),
)
.element(
annotate_snippets::Snippet::source(source).annotation(
annotate_snippets::AnnotationKind::Primary
.span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)),
),
)
.elements(
error
.help
.as_ref()
.map(|help| annotate_snippets::Level::HELP.message(help)),
)
})
.collect::<Vec<_>>();
let renderer = annotate_snippets::Renderer::styled()
.decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
renderer.render(&reports)
}
}
type Input<'input> = Recoverable<TokenSlice<'input, TemplateToken>, AstError>;
impl<'input> Parser<Input<'input>, TemplateToken, AstError> for TokenKind {
fn parse_next(&mut self, input: &mut Input<'input>) -> winnow::Result<TemplateToken, AstError> {
winnow::token::literal(*self)
.parse_next(input)
.map(|t| t[0].clone())
}
}
pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input));
if errors.is_empty()
&& let Some(val) = val
{
Ok(TemplateAst { root: val })
} else {
Err(AstFailure::from_errors(errors))
}
}
#[derive(Debug, Clone)]
pub enum TemplateAstExpr<'input> {
StaticContent(TemplateToken),
Block {
prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
Interpolation {
prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
ConditionalChain {
chain: Vec<TemplateAstExpr<'input>>,
},
ForChain {
for_block: Box<TemplateAstExpr<'input>>,
content: Vec<TemplateAstExpr<'input>>,
else_block: Option<Box<TemplateAstExpr<'input>>>,
else_content: Option<Vec<TemplateAstExpr<'input>>>,
end_block: Box<TemplateAstExpr<'input>>,
},
For {
value_ident: TemplateToken,
value_expression: Box<TemplateAstExpr<'input>>,
},
ForElse,
VariableAccess(TemplateToken),
IfConditional {
expression: Box<TemplateAstExpr<'input>>,
},
ConditionalContent {
content: Vec<TemplateAstExpr<'input>>,
},
ElseConditional {
expression: Option<Box<TemplateAstExpr<'input>>>,
},
Invalid(&'input [TemplateToken]),
Operation {
op: TokenOperator,
lhs: Box<TemplateAstExpr<'input>>,
rhs: Box<TemplateAstExpr<'input>>,
},
EndBlock,
Literal {
source: TemplateToken,
value: NomoValue,
},
}
fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
repeat(0.., parse_ast).parse_next(input)
}
fn parse_ast<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
alt((
trace(
"content",
TokenKind::Content.map(TemplateAstExpr::StaticContent),
),
trace("interpolation", parse_interpolation),
parse_action,
))
.parse_next(input)
}
fn parse_interpolation<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
let expr_parser = resume_after_cut(
parse_expression,
repeat_till(
0..,
any,
peek(preceded(
opt(TokenKind::TrimWhitespace),
TokenKind::RightDelim,
)),
)
.map(|((), _)| ()),
)
.with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
let (
prev_whitespace,
_left,
left_trim,
_wants_output,
(expression, right_trim, _right, post_whitespace),
) = (
opt(TokenKind::Whitespace),
TokenKind::LeftDelim,
opt(TokenKind::TrimWhitespace),
TokenKind::WantsOutput,
cut_err((
surrounded(ws, expr_parser).map(Box::new),
opt(TokenKind::TrimWhitespace),
TokenKind::RightDelim,
opt(TokenKind::Whitespace),
)),
)
.parse_next(input)?;
Ok(TemplateAstExpr::Interpolation {
prev_whitespace_content: if left_trim.is_some() {
None
} else {
prev_whitespace
},
expression,
post_whitespace_content: if right_trim.is_some() {
None
} else {
post_whitespace
},
})
}
fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"action",
alt((
parse_conditional_chain,
parse_for_chain,
(parse_block(
cut_err(not(repeat_till(
0..,
any,
peek((ws, TokenKind::RightDelim)),
)
.map(|((), _)| ())))
.context(
AstError::ctx()
.msg("Standlone action block")
.help("If you want to output this expression, add a '=' to the block"),
)
.take()
.map(TemplateAstExpr::Invalid),
)),
)),
)
.parse_next(input)
}
fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace("for_loop", |input: &mut Input<'input>| {
let for_block = parse_for_loop.map(Box::new).parse_next(input)?;
let loop_end = (
opt((
parse_loop_else.map(Box::new),
repeat_till(0.., parse_ast, peek(parse_end)).map(|(val, _)| val),
)),
parse_end.map(Box::new),
);
let (content, taken) = resume_after_cut(
repeat_till(0.., parse_ast, loop_end),
repeat_till(0.., any, parse_end).map(|((), _)| ()),
)
.with_taken()
.parse_next(input)?;
let Some((content, (else_stuff, end_block))) = content else {
return Ok(TemplateAstExpr::Invalid(taken));
};
let (else_block, else_content) = else_stuff.unzip();
Ok(TemplateAstExpr::ForChain {
for_block,
content,
else_block,
else_content,
end_block,
})
})
.parse_next(input)
}
fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"for_loop_inner",
parse_block(
(
ws,
TokenKind::For,
ws,
TokenKind::Ident,
ws,
TokenKind::In,
ws,
parse_expression.map(Box::new),
)
.map(|(_, _for, _, value_ident, _, _in, _, value_expression)| {
TemplateAstExpr::For {
value_ident,
value_expression,
}
}),
),
)
.parse_next(input)
}
fn parse_conditional_chain<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace("conditional_chain", |input: &mut Input<'input>| {
let if_block = parse_conditional_if.parse_next(input)?;
let mut chain = vec![];
chain.push(if_block);
let content = resume_after_cut(
cut_err(inner_conditional_chain),
repeat_till(0.., any, parse_end).map(|((), _)| ()),
)
.parse_next(input)?;
chain.extend(content.into_iter().flatten());
Ok(TemplateAstExpr::ConditionalChain { chain })
})
.parse_next(input)
}
fn inner_conditional_chain<'input>(
input: &mut Input<'input>,
) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
let mut needs_end = false;
let mut chain = vec![];
loop {
let (content, end_block): (Vec<_>, _) = repeat_till(
0..,
parse_ast,
trace(
"conditional_chain_else/end",
alt((parse_end, parse_conditional_else)),
),
)
.parse_next(input)?;
chain.push(TemplateAstExpr::ConditionalContent { content });
let is_end = if let TemplateAstExpr::Block { ref expression, .. } = end_block
&& let TemplateAstExpr::EndBlock = &**expression
{
true
} else {
false
};
if !is_end && needs_end {
return Err(AstError::from_input(input));
}
if let TemplateAstExpr::Block { expression, .. } = &end_block
&& let TemplateAstExpr::ElseConditional { expression: None } = &**expression
{
needs_end = true;
}
chain.push(end_block);
if is_end {
break;
}
}
Ok(chain)
}
fn parse_conditional_if<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"conditional",
parse_block(
preceded(
TokenKind::ConditionalIf,
cut_err(
surrounded(ws, parse_expression)
.map(Box::new)
.context(AstError::ctx().msg("Expected an expression after 'if'")),
),
)
.map(|expression| TemplateAstExpr::IfConditional { expression }),
),
)
.parse_next(input)
}
fn parse_conditional_else<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"conditional_else",
parse_block(
preceded(
surrounded(ws, TokenKind::ConditionalElse),
opt(preceded(
TokenKind::ConditionalIf,
cut_err(surrounded(ws, parse_expression)).map(Box::new),
)),
)
.map(|else_expr| TemplateAstExpr::ElseConditional {
expression: else_expr,
}),
),
)
.parse_next(input)
}
fn parse_loop_else<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"end",
parse_block(
TokenKind::ConditionalElse
.value(TemplateAstExpr::ForElse)
.context(AstError::ctx().msg("Expected an else block here")),
),
)
.parse_next(input)
}
fn parse_end<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"end",
parse_block(
TokenKind::End
.value(TemplateAstExpr::EndBlock)
.context(AstError::ctx().msg("Expected an end block here")),
),
)
.parse_next(input)
}
fn parse_variable_access<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"variable_access",
TokenKind::Ident.map(TemplateAstExpr::VariableAccess),
)
.parse_next(input)
}
fn parse_block<'input, ParseNext>(
parser: ParseNext,
) -> impl Parser<Input<'input>, TemplateAstExpr<'input>, AstError>
where
ParseNext: Parser<Input<'input>, TemplateAstExpr<'input>, AstError>,
{
let expr_parser = resume_after_cut(
parser,
repeat_till(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()),
)
.with_taken()
.map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken)));
(
opt(TokenKind::Whitespace),
TokenKind::LeftDelim,
opt(TokenKind::TrimWhitespace),
not(TokenKind::WantsOutput),
(
surrounded(ws, expr_parser.map(Box::new)),
opt(TokenKind::TrimWhitespace),
TokenKind::RightDelim,
opt(TokenKind::Whitespace),
),
)
.map(
|(
prev_whitespace,
_left,
left_trim,
_not_token,
(expression, right_trim, _right, post_whitespace),
)| {
TemplateAstExpr::Block {
prev_whitespace_content: if left_trim.is_some() {
None
} else {
prev_whitespace
},
expression,
post_whitespace_content: if right_trim.is_some() {
None
} else {
post_whitespace
},
}
},
)
}
fn parse_operand<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace("operand", alt((parse_variable_access, parse_literal))).parse_next(input)
}
fn parse_literal<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace(
"literal",
any.verify_map(|token: &TemplateToken| match token.kind() {
TokenKind::Literal(literal) => Some(TemplateAstExpr::Literal {
source: token.clone(),
value: match literal {
crate::parser::TokenLiteral::Bool(bool) => NomoValue::Bool { value: bool },
crate::parser::TokenLiteral::Integer(int) => NomoValue::Integer { value: int },
},
}),
_ => None,
}),
)
.parse_next(input)
}
fn parse_operator<'input>(input: &mut Input<'input>) -> Result<TokenOperator, AstError> {
trace(
"operator",
any.verify_map(|t: &'input TemplateToken| match t.kind() {
TokenKind::Operator(op) => Some(op),
_ => None,
}),
)
.parse_next(input)
}
fn parse_expression<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
macro_rules! infix {
($parser:expr => [ $($side:tt $val:tt => $prec:expr),* $(,)? ]) => {
dispatch! { surrounded(ws, parse_operator);
$(
TokenOperator::$val => $side($prec, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::$val, lhs: Box::new(lhs), rhs: Box::new(rhs) }))
),*
}
};
}
trace(
"expression",
expression(surrounded(ws, parse_operand)).infix(infix! {
surrounded(ws, parse_operator) => [
Left Plus => 18,
Left Minus => 18,
Left Times => 20,
Left Divide => 20,
Left And => 10,
Left Or => 7,
Left Equal => 12,
Left NotEqual => 12,
Left Greater => 15,
Left GreaterOrEqual => 15,
Left Lesser => 15,
Left LesserOrEqual => 15,
]
}),
)
.parse_next(input)
}
fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> {
repeat(.., TokenKind::Whitespace).parse_next(input)
}
fn surrounded<Input, IgnoredParser, Ignored1, Output, Error, ParseNext>(
ignored: IgnoredParser,
parser: ParseNext,
) -> impl Parser<Input, Output, Error>
where
Input: Stream,
Error: ParserError<Input>,
IgnoredParser: Parser<Input, Ignored1, Error>,
IgnoredParser: Clone,
ParseNext: Parser<Input, Output, Error>,
{
delimited(ignored.clone(), parser, ignored)
}
#[cfg(test)]
mod tests {
use winnow::Parser;
use winnow::combinator::alt;
use winnow::combinator::fail;
use winnow::stream::TokenSlice;
use crate::ast::AstError;
use crate::ast::AstFailure;
use crate::ast::TemplateAst;
use crate::ast::TemplateAstExpr;
use crate::ast::parse;
use crate::ast::parse_block;
use crate::ast::parse_end;
use crate::parser::TokenKind;
fn panic_pretty<'a>(
input: &'_ str,
tokens: Result<TemplateAst<'a>, AstFailure>,
) -> TemplateAst<'a> {
match tokens {
Ok(ast) => ast,
Err(failure) => {
panic!("{}", failure.to_report(input));
}
}
}
#[test]
fn check_only_content() {
let input = "Hello World";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap();
insta::assert_debug_snapshot!(ast, @r#"
TemplateAst {
root: [
StaticContent(
[Content]"Hello World" (0..11),
),
],
}
"#);
}
#[test]
fn check_simple_variable_interpolation() {
let input = "Hello {{= world }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap();
insta::assert_debug_snapshot!(ast, @r#"
TemplateAst {
root: [
StaticContent(
[Content]"Hello" (0..5),
),
Interpolation {
prev_whitespace_content: Some(
[Whitespace]" " (5..6),
),
expression: VariableAccess(
[Ident]"world" (10..15),
),
post_whitespace_content: None,
},
],
}
"#);
}
#[test]
fn check_simple_if() {
let input = "{{ if foo }} Hiii {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast, @r#"
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"foo" (6..9),
),
},
post_whitespace_content: Some(
[Whitespace]" " (12..13),
),
},
ConditionalContent {
content: [
StaticContent(
[Content]"Hiii" (13..17),
),
],
},
Block {
prev_whitespace_content: Some(
[Whitespace]" " (17..18),
),
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}
"#);
}
#[test]
fn check_invalid_action() {
let input = r#"{{ value }}
{{ value }}
{{ value }}
{{ value }}
{{ value }}"#;
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = parse(parsed.tokens()).unwrap_err();
insta::assert_snapshot!(ast.to_report(input));
}
#[test]
fn check_nested_simple_if() {
let input = r#"{{ if foo }}
{{ if bar }}
Hiii
{{ end }}
{{ end }}
{{= value }}
"#;
let parsed = crate::parser::parse(input.into()).unwrap();
insta::assert_debug_snapshot!("simple_if_tokens", parsed);
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!("simple_if_ast", ast);
}
#[test]
fn check_parsing_block() {
use winnow::RecoverableParser;
let input = "{{ foo }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let result = alt((
parse_end,
parse_block(
(TokenKind::Ident.void(), fail::<_, (), _>)
.void()
.context(AstError::ctx().msg("No ident allowed"))
.take()
.map(TemplateAstExpr::Invalid),
),
))
.recoverable_parse(TokenSlice::new(parsed.tokens()));
insta::assert_debug_snapshot!(result, @r#"
(
[
[LeftDelim]"{{" (0..2),
[Whitespace]" " (2..3),
[Ident]"foo" (3..6),
[Whitespace]" " (6..7),
[RightDelim]"}}" (7..9),
],
None,
[
AstError {
message: Some(
"No ident allowed",
),
help: None,
span: Some(
SourceSpan {
range: 0..6,
},
),
is_fatal: false,
},
],
)
"#);
}
#[test]
fn check_empty_if_output() {
let input = "{{ if foo }}{{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast, @r#"
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"foo" (6..9),
),
},
post_whitespace_content: None,
},
ConditionalContent {
content: [],
},
Block {
prev_whitespace_content: None,
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}
"#);
}
#[test]
fn check_if_else() {
let input = "{{ if foo }} foo {{ else }} bar {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
#[test]
fn check_if_else_if() {
let input = "{{ if foo }} foo {{ else if bar }} bar {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
#[test]
fn check_trim_whitespace() {
let input = "{{ if foo -}} foo {{- else if bar -}} bar {{- end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
#[test]
fn check_for_loop() {
let input = "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
#[test]
fn check_math_expression() {
let input = "{{= 5 * 3 + 2 / 3 }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
#[test]
fn check_logical_expression() {
let input = "{{= true && false || 3 >= 2 && 5 == 2 }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
}