wgsl-parser 0.5.0

A zero-copy recursive-descent parser for WebGPU shading language
Documentation
use gramatika::{DebugLisp, Parse, ParseStreamer, Span, Spanned, SpannedError, Substr, Token as _};

use crate::{token::comment_start, ParseStream, Text, Token, TokenKind};

#[derive(DebugLisp)]
pub enum Comment {
	Line(LineComment),
	Block(BlockComment),
}

#[derive(DebugLisp)]
pub struct LineComment {
	pub start: Token,
	pub text: Text,
}

#[derive(DebugLisp)]
pub struct BlockComment {
	pub start: Token,
	pub children: Vec<BlockCommentChild>,
	pub end: Token,
}

#[derive(DebugLisp)]
pub enum BlockCommentChild {
	Text(Text),
	Comment(BlockComment),
}

impl Parse for Comment {
	type Stream = ParseStream;

	fn parse(input: &mut Self::Stream) -> gramatika::Result<Self> {
		use TokenKind::*;

		match input.peek() {
			Some(token) => match token.as_matchable() {
				(CommentStart, "//", _) => Ok(Comment::Line(input.parse()?)),
				(CommentStart, "/*", _) => Ok(Comment::Block(input.parse()?)),
				(_, _, span) => Err(SpannedError {
					message: "Expected `//` or `/*`".into(),
					source: input.source(),
					span: Some(span),
				}),
			},
			None => Err(SpannedError {
				message: "Unexpected end of input".into(),
				source: input.source(),
				span: input.prev().map(|token| token.span()),
			}),
		}
	}
}

impl Parse for LineComment {
	type Stream = ParseStream;

	fn parse(input: &mut Self::Stream) -> gramatika::Result<Self> {
		let start = input.consume(comment_start!["//"])?;
		let start_span = start.span();
		let text = match text_until(input, |peeked| {
			peeked.span().start.line > start_span.start.line
		}) {
			Some(text) => text,
			None => {
				let content = Substr::new();
				let span = Span {
					start: start_span.end,
					end: start_span.end,
				};
				Text(content, span)
			}
		};

		Ok(Self { start, text })
	}
}

impl Parse for BlockComment {
	type Stream = ParseStream;

	fn parse(input: &mut Self::Stream) -> gramatika::Result<Self> {
		let start = input.consume(comment_start!["/*"])?;
		let start_span = start.span();
		let mut children: Vec<BlockCommentChild> = vec![];
		loop {
			use TokenKind::*;
			match input.peek().map(|peeked| peeked.as_matchable()) {
				Some((CommentStart, "/*", _)) => {
					children.push(BlockCommentChild::Comment(input.parse()?));
				}
				Some((CommentEnd, "*/", _)) => {
					let end = input.next().unwrap();
					break Ok(Self {
						start,
						children,
						end,
					});
				}
				Some(_) => {
					let text = match text_until(input, |peeked| {
						matches!(
							peeked.as_matchable(),
							(CommentStart, "/*", _) | (CommentEnd, _, _)
						)
					}) {
						Some(text) => text,
						None => {
							let content = Substr::new();
							let span = Span {
								start: start_span.end,
								end: start_span.end,
							};
							Text(content, span)
						}
					};
					children.push(BlockCommentChild::Text(text));
				}
				None => {
					return Err(SpannedError {
						message: "Unexpected end of input".into(),
						source: input.source(),
						span: input.prev().map(|token| token.span()),
					});
				}
			}
		}
	}
}

impl Spanned for Comment {
	fn span(&self) -> Span {
		match self {
			Comment::Line(inner) => inner.span(),
			Comment::Block(inner) => inner.span(),
		}
	}
}

impl Spanned for BlockComment {
	fn span(&self) -> Span {
		self.start.span().through(self.end.span())
	}
}

impl Spanned for LineComment {
	fn span(&self) -> Span {
		self.start.span().through(self.text.span())
	}
}

fn text_until<F>(input: &mut ParseStream, end_condition: F) -> Option<Text>
where F: Fn(&Token) -> bool {
	let mut result: Option<Text> = None;
	loop {
		match input.peek() {
			Some(token) if !end_condition(token) => {
				let kind = token.kind();
				let token = input.consume_as(kind, Token::plain).unwrap();
				result = match result.take() {
					Some(text) => Some(text.concat(token)),
					None => Some(Text::from(token)),
				}
			}
			_ => {
				break result;
			}
		}
	}
}