diff --git a/src/parse.rs b/src/parse.rs deleted file mode 100644 index 6c14cc9b..00000000 --- a/src/parse.rs +++ /dev/null @@ -1,4402 +0,0 @@ -//! This module contains the parsing code to convert the -//! tokens into an AST. - -use std::fmt; -use std::num::NonZeroUsize; -use std::path::PathBuf; -use std::str::FromStr; -use std::sync::Arc; - -use chumsky::input::{Input, ValueInput}; -use chumsky::prelude::{ - any, choice, empty, just, nested_delimiters, none_of, one_of, recursive, skip_then_retry_until, - via_parser, -}; -use chumsky::{extra, select, IterParser, Parser}; - -use either::Either; -use itertools::Itertools; -use miniscript::iter::{Tree, TreeLike}; - -use crate::driver::{CRATE_STR, MAIN_MODULE}; -use crate::error::DiagnosticManager; -use crate::error::{Diagnostic, Error, Span}; -use crate::impl_eq_hash; -use crate::lexer::{Token, Tokens}; -use crate::num::NonZeroPow2Usize; -use crate::pattern::Pattern; -use crate::str::{ - AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, - SymbolName, -}; -use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType}; -use crate::unstable::{ - impl_require_feature, FeatureRequirement, RequireFeature, UnstableFeature, UnstableFeatures, -}; -use crate::version::SimcDirective; -use crate::TemplateProgramWitness; - -#[cfg(feature = "fmt")] -use crate::lexer::{FmtToken, FmtTokens}; - -/// A program is a sequence of items. -#[derive(Clone, Debug)] -pub struct Program { - items: Arc<[Item]>, - span: Span, -} - -/// Source-aware parse result used by formatters. -/// -/// The compiler keeps using [`Program`] directly. Formatters need the same -/// semantic tree plus the lossless trivia stream that the grammar intentionally -/// does not consume. -#[cfg(feature = "fmt")] -#[derive(Clone, Debug)] -pub struct ParsedSource<'src> { - program: Program, - tokens: FmtTokens<'src>, - prefix: Span, -} - -#[cfg(feature = "fmt")] -impl<'src> ParsedSource<'src> { - /// Access the semantic program used for formatting. - pub fn program(&self) -> &Program { - &self.program - } - - /// Access the lossless token stream in source order. - pub fn tokens(&self) -> &FmtTokens<'src> { - &self.tokens - } - - /// Access the source prefix skipped by the version-directive prescan. - /// - /// A formatter should preserve this range verbatim until the directive gains - /// its own formatting grammar. - pub fn prefix(&self) -> Span { - self.prefix - } -} - -impl Program { - // Need for driver usage - pub(crate) fn new(items: &[Item], span: Span) -> Self { - Self { - items: Arc::from(items), - span, - } - } - - /// Access the items of the program. - pub fn items(&self) -> &[Item] { - &self.items - } - - /// Parse source for formatting while retaining all comments and whitespace. - #[cfg(feature = "fmt")] - pub fn parse_with_errors_for_fmt<'src>( - file_id: usize, - source: &'src str, - unstable_features: &UnstableFeatures, - diagnostics: &mut DiagnosticManager, - ) -> Option> { - let before = diagnostics.error_count(); - - let start = pipeline::directive_prescan(source, file_id, diagnostics)?; - - let (tokens, lex_errors) = crate::lexer::lex_lossless(file_id, source, start); - let lex_ok = pipeline::is_lex_ok(lex_errors, diagnostics)?; - - let tokens = tokens?; - - let semantic_tokens = tokens - .iter() - .filter_map(|(token, span)| match token { - FmtToken::Token(token) => Some((token.clone(), *span)), - FmtToken::Trivia(_) => None, - }) - .collect::>(); - - let (program, parse_ok) = - pipeline::parse_ast(file_id, source, semantic_tokens, diagnostics); - - if parse_ok && lex_ok { - pipeline::post_check(unstable_features, program.as_ref(), diagnostics); - } - - if diagnostics.error_count() > before { - None - } else { - let program = program?; - Some(ParsedSource { - program, - tokens, - prefix: Span::new(file_id, 0..start), - }) - } - } -} - -impl_eq_hash!(Program; items); - -impl_require_feature!(Program { recurse: items; }); - -/// An item is a component of a program. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum Item { - /// A type alias. - TypeAlias(TypeAlias), - /// A function. - Function(Function), - /// An import declaration (e.g., `use math::add`) that brings another - /// [`Item`] into the current scope. - Use(UseDecl), - /// An enum declaration. - EnumDeclaration(EnumDeclaration), - /// A module containing a collection of nested [`Item`]. - Module(Module), - /// A placeholder used exclusively for error recovery during parsing. - /// - /// When the parser encounters a syntax error, it skips the malformed tokens - /// until it reaches a valid top-level keyword and inserts `Ignored` into the AST. - Ignored, -} - -impl Item { - /// Access the source span when this item was parsed successfully. - /// - /// Error-recovery placeholders have no source node to decorate. - pub fn span(&self) -> Option<&Span> { - match self { - Self::TypeAlias(alias) => Some(alias.span()), - Self::Function(function) => Some(function.span()), - Self::Use(use_decl) => Some(use_decl.span()), - Self::EnumDeclaration(declaration) => Some(declaration.span()), - Self::Module(module) => Some(module.span()), - Self::Ignored => None, - } - } -} - -impl_require_feature!(Item { - variants: - TypeAlias(alias), - Function(function), - Use(use_decl), - EnumDeclaration(decl), - Module(module), - Ignored, -}); - -#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub enum Visibility { - Public, - #[default] - Private, -} - -/// Represents an import declaration in the Abstract Syntax Tree. -/// -/// This structure defines how items from other modules or files are brought into the -/// current scope. Note that in this architecture, the first identifier in the path -/// is always treated as an dependency root path name bound to a specific physical path. -/// -/// # Example -/// ```text -/// pub use std::collections::{HashMap, HashSet}; -/// ``` -#[derive(Clone, Debug)] -pub struct UseDecl { - /// The visibility of the import (e.g., `pub use` vs `use`). - visibility: Visibility, - - /// The base path to the target file or module. - /// - /// The first element is always the registered dependency root path name for - /// the import path. Subsequent elements represent nested modules or directories. - path: Vec, - - /// The specific item or list of items being imported from the resolved path. - items: UseItems, - span: Span, -} - -impl_require_feature!(UseDecl { - requires: UnstableFeature::Imports, span: span; - recurse: items; -}); - -impl_require_feature!(UseItems { - variants: - Single(_), - List(_), -}); - -impl UseDecl { - pub fn visibility(&self) -> &Visibility { - &self.visibility - } - - /// Returns the full logical module path as a vector of string slices. - /// - /// This includes the Dependency Root Path Name (the first segment) - /// followed by all subsequent sub-module segments. - pub fn path(&self) -> &[Identifier] { - &self.path - } - - pub fn items(&self) -> &UseItems { - &self.items - } - - pub fn span(&self) -> &Span { - &self.span - } - - pub fn str_path(&self) -> String { - let path: PathBuf = self.path().iter().map(Identifier::as_str).collect(); - path.display().to_string() - } - - /// Extracts the Dependency Root Path Name (the very first segment) from this path. - /// - /// # Errors - /// - /// Returns a `Diagnostic` if the use declaration path is completely empty. - pub fn drp_name(&self) -> Result<&str, Diagnostic> { - let parts: Vec<&str> = self.path().iter().map(Identifier::as_str).collect(); - parts.first().copied().ok_or_else(|| { - Error::CannotParse { - msg: "Empty use path".to_string(), - } - .with_span(self.span) - }) - } - - pub(crate) fn set_path(&mut self, path: &[Identifier]) { - self.path = Vec::from(path) - } -} - -// `span` are required because `UseDecl` hashing is context-dependent. -// For instance, identical `use crate::...` paths differ between binary and library roots. -// Tested by: `functional_tests::identical_crate_uses_in_different_package_roots_do_not_poison_resolution_cache`. -impl_eq_hash!(UseDecl; visibility, path, items, span); - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for UseDecl { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let visibility = Visibility::arbitrary(u)?; - let path_len = u.int_in_range(2..=4)?; - let path = (0..path_len) - .map(|_| Identifier::arbitrary(u)) - .collect::>()?; - let items = UseItems::arbitrary(u)?; - - Ok(Self { - visibility, - path, - items, - span: Span::DUMMY, - }) - } -} - -/// Aliases the specific identifier of an imported type to a new, local identifier -pub type AliasedSymbolName = (SymbolName, Option); - -/// Specified the items being brought into scope at the end of a `use` declaration -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub enum UseItems { - /// A single item import. - /// - /// # Example - /// ```text - /// use core::math::add; - /// ``` - Single(AliasedSymbolName), - - /// A multiple item import grouped in a list. - /// - /// # Example - /// ```text - /// use core::math::{add, subtract}; - /// ``` - List(Vec), -} - -#[derive(Clone, Debug)] -pub struct Function { - visibility: Visibility, - name: FunctionName, - params: Arc<[FunctionParam]>, - ret: Option, - body: Expression, - span: Span, -} - -impl Function { - pub fn visibility(&self) -> &Visibility { - &self.visibility - } - - /// Access the name of the function. - pub fn name(&self) -> &FunctionName { - &self.name - } - - /// Access the parameters of the function. - pub fn params(&self) -> &[FunctionParam] { - &self.params - } - - /// Access the return type of the function. - /// - /// An empty return type means that the function returns the unit value. - pub fn ret(&self) -> Option<&AliasedType> { - self.ret.as_ref() - } - - /// Access the body of the function. - pub fn body(&self) -> &Expression { - &self.body - } - - /// Access the span of the function. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(Function; visibility, name, params, ret, body); - -impl_require_feature!(Function { recurse: params, ret, body; }); - -/// Parameter of a function. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub struct FunctionParam { - identifier: Identifier, - ty: AliasedType, - span: Span, -} - -impl FunctionParam { - /// Access the identifier of the parameter. - pub fn identifier(&self) -> &Identifier { - &self.identifier - } - - /// Access the type of the parameter. - pub fn ty(&self) -> &AliasedType { - &self.ty - } - - /// Access the source span of the complete parameter. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(FunctionParam; identifier, ty); - -impl_require_feature!(FunctionParam { recurse: ty; }); - -/// A statement is a component of a block expression. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum Statement { - /// A declaration of variables inside a pattern. - Assignment(Assignment), - /// An expression that returns nothing (the unit value). - Expression(Expression), -} - -impl Statement { - /// Access the span of the statement contents. - /// - /// The terminating semicolon is deliberately not part of this span: it is a - /// separator owned by the containing block's layout. - pub fn span(&self) -> &Span { - match self { - Self::Assignment(assignment) => assignment.span(), - Self::Expression(expression) => expression.span(), - } - } -} - -impl_require_feature!(Statement { - variants: - Assignment(assignment), - Expression(expr), -}); - -/// The output of an expression is assigned to a pattern. -#[derive(Clone, Debug)] -pub struct Assignment { - pattern: Pattern, - ty: AliasedType, - expression: Expression, - span: Span, -} - -impl Assignment { - /// Access the pattern of the assignment. - pub fn pattern(&self) -> &Pattern { - &self.pattern - } - - /// Access the return type of assigned expression. - pub fn ty(&self) -> &AliasedType { - &self.ty - } - - /// Access the assigned expression. - pub fn expression(&self) -> &Expression { - &self.expression - } - - /// Access the span of the expression. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(Assignment; pattern, ty, expression); - -impl_require_feature!(Assignment { recurse: pattern, ty, expression; }); - -/// Call expression. -#[derive(Clone, Debug)] -pub struct Call { - name: CallName, - args: Arc<[Expression]>, - span: Span, -} - -impl Call { - /// Access the name of the call. - pub fn name(&self) -> &CallName { - &self.name - } - - /// Access the arguments to the call. - pub fn args(&self) -> &[Expression] { - self.args.as_ref() - } - - /// Access the span of the call. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(Call; name, args); - -// Hand-written because `raw_hash` is gated per call name, while the macro -// can gate only an entire type, and `CallName` has no span of its own. -impl RequireFeature for Call { - fn feature_requirements(&self, out: &mut Vec) { - if let CallName::RawHash(_) = self.name { - out.push(FeatureRequirement::new(UnstableFeature::RawHash, self.span)); - } - - self.name.feature_requirements(out); - self.args.feature_requirements(out); - } -} - -/// Name of a call. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub enum CallName { - /// Name of a jet. - Jet(JetName), - /// [`Either::unwrap_left`]. - UnwrapLeft(AliasedType), - /// [`Either::unwrap_right`]. - UnwrapRight(AliasedType), - /// [`Option::unwrap`]. - Unwrap, - /// [`Option::is_none`]. - IsNone(AliasedType), - /// [`assert!`]. - Assert, - /// [`panic!`] without error message. - Panic, - /// [`dbg!`]. - Debug, - /// Cast from the given source type. - TypeCast(AliasedType), - /// Name of a custom function. - Custom(FunctionName), - /// Fold of a bounded list with the given function. - Fold(FunctionName, NonZeroPow2Usize), - /// Fold of an array with the given function. - ArrayFold(FunctionName, NonZeroUsize), - /// Loop over the given function a bounded number of times until it returns success. - ForWhile(FunctionName), - /// SHA-256 of the concatenated bytes of a tuple of unsigned integers of the given type. - RawHash(AliasedType), -} - -impl_require_feature!(CallName { - variants: - Jet(_), - UnwrapLeft(ty), - UnwrapRight(ty), - Unwrap, - IsNone(ty), - Assert, - Panic, - Debug, - TypeCast(ty), - Custom(_), - Fold(_, _), - ArrayFold(_, _), - ForWhile(_), - RawHash(ty), -}); - -/// A type alias. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub struct TypeAlias { - visibility: Visibility, - name: AliasName, - ty: AliasedType, - span: Span, -} - -impl TypeAlias { - pub fn visibility(&self) -> &Visibility { - &self.visibility - } - - /// Access the name of the alias. - pub fn name(&self) -> &AliasName { - &self.name - } - - /// Access the type that the alias resolves to. - /// - /// During the parsing stage, the resolved type may include aliases. - /// The compiler will later check if all contained aliases have been declared before. - pub fn ty(&self) -> &AliasedType { - &self.ty - } - - /// Access the span of the alias. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(TypeAlias; name, ty); - -impl_require_feature!(TypeAlias { recurse: ty; }); - -/// A single variant in an enum declaration. -/// -/// A variant's position among the declared variants determines its leaf -/// in the enum's balanced sum, i.e. its wire encoding. -/// A variant may carry payload types (`Refresh(Signature, u8)`). -/// A variant without payload is a unit variant. -#[derive(Clone, Debug)] -pub struct EnumVariant { - name: Identifier, - payload: Arc<[AliasedType]>, - span: Span, -} - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for EnumVariant { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let name = Identifier::arbitrary(u)?; - let len = u.int_in_range(0..=3)?; - let payload = (0..len) - .map(|_| AliasedType::arbitrary(u)) - .collect::>>()?; - Ok(Self { - name, - payload, - span: Span::DUMMY, - }) - } -} - -impl EnumVariant { - pub fn name(&self) -> &Identifier { - &self.name - } - - /// Access the payload types of the variant. Empty for unit variants. - pub fn payload(&self) -> &[AliasedType] { - &self.payload - } - - /// Access the source span of the complete variant. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(EnumVariant; name, payload); - -impl AsRef for EnumVariant { - fn as_ref(&self) -> &Span { - &self.span - } -} - -/// An enum declaration. -#[derive(Clone, Debug)] -pub struct EnumDeclaration { - visibility: Visibility, - name: AliasName, - variants: Arc<[EnumVariant]>, - span: Span, -} - -impl EnumDeclaration { - pub fn visibility(&self) -> &Visibility { - &self.visibility - } - - pub fn name(&self) -> &AliasName { - &self.name - } - - pub fn variants(&self) -> &[EnumVariant] { - &self.variants - } - - /// Access the source span of the complete declaration. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_require_feature!(EnumVariant { recurse: payload; }); - -impl_require_feature!(EnumDeclaration { - requires: UnstableFeature::Enums, span: span; - recurse: variants; -}); - -impl_eq_hash!(EnumDeclaration; name, variants); - -impl AsRef for EnumDeclaration { - fn as_ref(&self) -> &Span { - &self.span - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for EnumDeclaration { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let visibility = Visibility::arbitrary(u)?; - // `AliasName::arbitrary` already dodges every reserved alias name, so a generated declaration always re-parses - let name = AliasName::arbitrary(u)?; - let len = u.int_in_range(2..=8)?; - let variants = (0..len) - .map(|_| EnumVariant::arbitrary(u)) - .collect::>>()?; - Ok(Self { - visibility, - name, - variants, - span: Span::DUMMY, - }) - } -} - -impl fmt::Display for EnumDeclaration { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}enum {} {{", self.visibility(), self.name())?; - for variant in self.variants() { - write!(f, " {}", variant.name())?; - if let Some((first, rest)) = variant.payload().split_first() { - write!(f, "({first}")?; - for ty in rest { - write!(f, ", {ty}")?; - } - write!(f, ")")?; - } - write!(f, ",")?; - } - write!(f, " }}") - } -} - -/// An expression is something that returns a value. -#[derive(Clone, Debug)] -pub struct Expression { - inner: ExpressionInner, - span: Span, -} - -impl Expression { - /// Access the inner expression. - pub fn inner(&self) -> &ExpressionInner { - &self.inner - } - - /// Access the span of the expression. - pub fn span(&self) -> &Span { - &self.span - } - - /// Convert the expression into a block expression. - #[cfg(feature = "arbitrary")] - fn into_block(self) -> Self { - match self.inner { - ExpressionInner::Single(_) => Expression { - span: self.span, - inner: ExpressionInner::Block(Arc::from([]), Some(Arc::new(self))), - }, - _ => self, - } - } - - pub fn empty(span: Span) -> Self { - Self { - inner: ExpressionInner::Single(SingleExpression { - inner: SingleExpressionInner::Tuple(Arc::new([])), - span, - }), - span, - } - } -} - -impl_eq_hash!(Expression; inner); - -impl_require_feature!(Expression { recurse: inner; }); - -/// The kind of expression. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum ExpressionInner { - /// A single expression directly returns a value. - Single(SingleExpression), - /// A block expression first executes a series of statements inside a local scope. - /// Then, the block returns the value of its final expression. - /// The block returns nothing (unit) if there is no final expression. - Block(Arc<[Statement]>, Option>), -} - -impl_require_feature!(ExpressionInner { - variants: - Single(single), - Block(statements, maybe_expr), -}); - -/// A single expression directly returns a value. -#[derive(Clone, Debug)] -pub struct SingleExpression { - inner: SingleExpressionInner, - span: Span, -} - -impl SingleExpression { - /// Access the inner expression. - pub fn inner(&self) -> &SingleExpressionInner { - &self.inner - } - - /// Access the span of the expression. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(SingleExpression; inner); - -impl_require_feature!(SingleExpression {recurse: inner; }); - -/// The kind of single expression. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum SingleExpressionInner { - /// Either wrapper expression - Either(Either, Arc>), - /// Option wrapper expression - Option(Option>), - /// Boolean literal expression - Boolean(bool), - /// Decimal string literal. - Decimal(Decimal), - /// Binary string literal. - Binary(Binary), - /// Hexadecimal string literal. - Hexadecimal(Hexadecimal), - /// Witness value. - Witness(TemplateProgramWitness), - /// Parameter value. - Parameter(TemplateProgramWitness), - /// Variable identifier expression - Variable(Identifier), - /// Function call - Call(Call), - /// Expression in parentheses - Expression(Arc), - /// Match expression over a sum type - Match(Match), - /// Match expression over an enum's variants - EnumMatch(EnumMatch), - /// Construction of an enum variant - EnumConstruction(EnumConstruction), - /// Tuple wrapper expression - Tuple(Arc<[Expression]>), - /// Array wrapper expression - Array(Arc<[Expression]>), - /// List wrapper expression - /// - /// The exclusive upper bound on the list size is not known at this point - List(Arc<[Expression]>), -} - -impl_require_feature!(SingleExpressionInner { - variants: - Either(either), - Option(maybe_expr), - Boolean(_), - Decimal(_), - Binary(_), - Hexadecimal(_), - Witness(_), - Parameter(_), - Variable(_), - Call(call), - Expression(expr), - Match(match_), - EnumMatch(enum_match), - EnumConstruction(construction), - Tuple(exprs), - Array(exprs), - List(exprs), -}); - -/// Match expression. -#[derive(Clone, Debug)] -pub struct Match { - scrutinee: Arc, - left: MatchArm, - right: MatchArm, - span: Span, -} - -impl Match { - /// Access the expression that is matched. - pub fn scrutinee(&self) -> &Expression { - &self.scrutinee - } - - /// Access the match arm for left sum values. - pub fn left(&self) -> &MatchArm { - &self.left - } - - /// Access the match arm for right sum values. - pub fn right(&self) -> &MatchArm { - &self.right - } - - /// Access the span of the match statement. - pub fn span(&self) -> &Span { - &self.span - } - - /// Get the type of the expression that is matched. - pub fn scrutinee_type(&self) -> AliasedType { - match (&self.left.pattern, &self.right.pattern) { - (MatchPattern::Left(_, ty_l), MatchPattern::Right(_, ty_r)) => { - AliasedType::either(ty_l.clone(), ty_r.clone()) - } - (MatchPattern::None, MatchPattern::Some(_, ty_r)) => AliasedType::option(ty_r.clone()), - (MatchPattern::False, MatchPattern::True) => AliasedType::boolean(), - _ => unreachable!("Match expressions have valid left and right arms"), - } - } -} - -impl_eq_hash!(Match; scrutinee, left, right); - -impl_require_feature!(Match {recurse: scrutinee, left, right; }); - -/// Match expression over a named enum type. -#[derive(Clone, Debug)] -pub struct EnumMatch { - scrutinee: Arc, - arms: Arc<[EnumMatchArm]>, - span: Span, -} - -impl EnumMatch { - /// Access the expression that is matched. - pub fn scrutinee(&self) -> &Expression { - &self.scrutinee - } - - /// Access the match arms. - pub fn arms(&self) -> &[EnumMatchArm] { - &self.arms - } - - /// Access the span of the match statement. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_require_feature!(EnumMatch { - requires: UnstableFeature::Enums, span: span; - recurse: scrutinee, arms; -}); - -impl_eq_hash!(EnumMatch; scrutinee, arms); - -impl AsRef for EnumMatch { - fn as_ref(&self) -> &Span { - &self.span - } -} - -/// Arm of an enum match expression: `Enum::Variant(bindings..) => expression`. -/// -/// The arm type carries the classification the parser proved. -/// Every arm of an [`EnumMatch`] names an enum variant, so no other pattern kind is representable here. -#[derive(Clone, Debug)] -pub struct EnumMatchArm { - enum_path: Arc<[Identifier]>, - variant: Identifier, - bindings: Arc<[(Pattern, AliasedType)]>, - expression: Arc, - span: Span, -} - -impl EnumMatchArm { - /// Access the written enum path of the arm, e.g. `["m", "Choice"]`. - pub fn enum_path(&self) -> &[Identifier] { - &self.enum_path - } - - /// The written enum path as one string, e.g. `m::Choice`. - pub fn enum_path_string(&self) -> String { - self.enum_path - .iter() - .map(Identifier::as_str) - .collect::>() - .join("::") - } - - /// Access the name of the matched variant. - pub fn variant(&self) -> &Identifier { - &self.variant - } - - /// Access the payload bindings of the arm. Empty for unit variants. - pub fn bindings(&self) -> &[(Pattern, AliasedType)] { - &self.bindings - } - - /// Access the expression that is executed in the match arm. - pub fn expression(&self) -> &Expression { - &self.expression - } - - /// Access the source span of the complete arm, including its optional comma. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(EnumMatchArm; enum_path, variant, bindings, expression); - -impl_require_feature!(EnumMatchArm { recurse: expression; }); - -/// Displays the arm's head (`Enum::Variant(bindings..)`), without the body. -impl fmt::Display for EnumMatchArm { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}::{}", self.enum_path_string(), self.variant)?; - if self.bindings.is_empty() { - return Ok(()); - } - f.write_str("(")?; - for (i, (pattern, ty)) in self.bindings.iter().enumerate() { - if 0 < i { - f.write_str(", ")?; - } - write!(f, "{pattern}: {ty}")?; - } - f.write_str(")") - } -} - -/// Construction of an enum variant: `Action::Refresh(sig, 3)`. -/// -/// The written enum name is either an alias in scope (`MyChoice`) or the -/// enum's declared name itself (`Action`), which needs no scope. -/// Unit variants take no argument list. -#[derive(Clone, Debug)] -pub struct EnumConstruction { - enum_path: Arc<[Identifier]>, - variant: Identifier, - args: Arc<[Expression]>, - span: Span, -} - -impl EnumConstruction { - /// Access the written enum path, e.g. `["m", "Action"]`. - pub fn enum_path(&self) -> &[Identifier] { - &self.enum_path - } - - /// Access the name of the constructed variant. - pub fn variant(&self) -> &Identifier { - &self.variant - } - - /// Access the payload arguments. Empty for unit variants. - pub fn args(&self) -> &[Expression] { - &self.args - } - - /// Access the span of the construction expression. - pub fn span(&self) -> &Span { - &self.span - } - - /// The written enum path as one string, e.g. `m::Action`. - pub fn enum_path_string(&self) -> String { - self.enum_path - .iter() - .map(Identifier::as_str) - .collect::>() - .join("::") - } -} - -impl_eq_hash!(EnumConstruction; enum_path, variant, args); - -impl_require_feature!(EnumConstruction { - requires: UnstableFeature::Enums, span: span; - recurse: args; -}); - -impl AsRef for EnumConstruction { - fn as_ref(&self) -> &Span { - &self.span - } -} - -/// Arm of a match expression. -#[derive(Clone, Debug)] -pub struct MatchArm { - pattern: MatchPattern, - expression: Arc, - span: Span, -} - -impl MatchArm { - /// Access the pattern that guards the match arm. - pub fn pattern(&self) -> &MatchPattern { - &self.pattern - } - - /// Access the expression that is executed in the match arm. - pub fn expression(&self) -> &Expression { - &self.expression - } - - /// Access the source span of the complete arm, including its optional comma. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl_eq_hash!(MatchArm; pattern, expression); - -impl_require_feature!(MatchArm {recurse: pattern, expression; }); - -/// Pattern of a match arm. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] -pub enum MatchPattern { - /// Bind inner value of left value to a pattern. - Left(Pattern, AliasedType), - /// Bind inner value of right value to a pattern. - Right(Pattern, AliasedType), - /// Match none value (no binding). - None, - /// Bind inner value of some value to a pattern. - Some(Pattern, AliasedType), - /// Match false value (no binding). - False, - /// Match true value (no binding). - True, -} - -impl_require_feature!(MatchPattern { - variants: - Left(pattern, ty), - Right(pattern, ty), - None, - Some(pattern, ty), - False, - True, -}); - -impl MatchPattern { - /// Access the pattern of a match pattern that binds a variables. - pub fn as_pattern(&self) -> Option<&Pattern> { - match self { - MatchPattern::Left(i, _) | MatchPattern::Right(i, _) | MatchPattern::Some(i, _) => { - Some(i) - } - MatchPattern::None | MatchPattern::False | MatchPattern::True => None, - } - } - - /// Access the pattern and the type of a match pattern that binds a variables. - pub fn as_typed_pattern(&self) -> Option<(&Pattern, &AliasedType)> { - match self { - MatchPattern::Left(i, ty) | MatchPattern::Right(i, ty) | MatchPattern::Some(i, ty) => { - Some((i, ty)) - } - MatchPattern::None | MatchPattern::False | MatchPattern::True => None, - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct Module { - visibility: Visibility, - name: ModuleName, - items: Arc<[Item]>, - span: Span, -} - -impl_require_feature!(Module { - requires: UnstableFeature::Imports, span: span; - recurse: items; -}); - -impl Module { - /// Needed by the driver to wrap a single file into a module. - pub(crate) fn new( - file_id: usize, - visibility: Visibility, - name: ModuleName, - items: &[Item], - ) -> Module { - Self { - visibility, - name, - items: Arc::from(items), - span: Span::new(file_id, 0..0), - } - } - - pub fn visibility(&self) -> &Visibility { - &self.visibility - } - - /// Access the name of the module. - pub fn name(&self) -> &ModuleName { - &self.name - } - - /// Access the assignments of the module. - pub fn items(&self) -> &[Item] { - &self.items - } - - /// Access the span of the module. - pub fn span(&self) -> &Span { - &self.span - } -} - -impl fmt::Display for Program { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for item in self.items() { - writeln!(f, "{item}")?; - } - Ok(()) - } -} - -impl fmt::Display for Item { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TypeAlias(alias) => write!(f, "{alias}"), - Self::Function(function) => write!(f, "{function}"), - Self::Use(use_declaration) => write!(f, "{use_declaration}"), - Self::EnumDeclaration(decl) => write!(f, "{decl}"), - Self::Module(module) => write!(f, "{module}"), - Self::Ignored => Ok(()), - } - } -} - -impl fmt::Display for Visibility { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Public => write!(f, "pub "), - Self::Private => write!(f, ""), - } - } -} - -impl fmt::Display for TypeAlias { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}type {} = {};", - self.visibility(), - self.name(), - self.ty() - ) - } -} - -impl fmt::Display for Function { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}fn {}(", self.visibility(), self.name())?; - for (i, param) in self.params().iter().enumerate() { - if 0 < i { - write!(f, ", ")?; - } - write!(f, "{param}")?; - } - write!(f, ")")?; - if let Some(ty) = self.ret() { - write!(f, " -> {ty}")?; - } - write!(f, " {}", self.body()) - } -} - -impl fmt::Display for FunctionParam { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}", self.identifier(), self.ty()) - } -} - -impl fmt::Display for UseDecl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let _ = write!(f, "{}use ", self.visibility()); - - for (i, segment) in self.path.iter().enumerate() { - if i > 0 { - write!(f, "::")?; - } - write!(f, "{}", segment)?; - } - - if !self.path.is_empty() { - write!(f, "::")?; - } - - write!(f, "{};", self.items) - } -} - -impl fmt::Display for UseItems { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - UseItems::Single((ident, alias)) => { - write!(f, "{}", ident)?; - - if let Some(alias) = alias { - write!(f, " as {}", alias)?; - } - - Ok(()) - } - UseItems::List(aliased_idents) => { - let _ = write!(f, "{{"); - for (i, (ident, alias)) in aliased_idents.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{}", ident)?; - - if let Some(alias) = alias { - write!(f, " as {}", alias)?; - } - } - write!(f, "}}") - } - } - } -} - -impl fmt::Display for Module { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "{}mod {} {{", self.visibility(), self.name())?; - - for item in self.items() { - let item_str = item.to_string(); - - for line in item_str.lines() { - writeln!(f, " {line}")?; - } - } - - write!(f, "}}") - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub enum ExprTree<'a> { - Expression(&'a Expression), - Block(&'a [Statement], &'a Option>), - Statement(&'a Statement), - Assignment(&'a Assignment), - Single(&'a SingleExpression), - Call(&'a Call), - Match(&'a Match), - EnumMatch(&'a EnumMatch), - EnumConstruction(&'a EnumConstruction), -} - -impl TreeLike for ExprTree<'_> { - fn as_node(&self) -> Tree { - use SingleExpressionInner as S; - - match self { - Self::Expression(expr) => match expr.inner() { - ExpressionInner::Block(statements, maybe_expr) => { - Tree::Unary(Self::Block(statements, maybe_expr)) - } - ExpressionInner::Single(single) => Tree::Unary(Self::Single(single)), - }, - Self::Block(statements, maybe_expr) => Tree::Nary( - statements - .iter() - .map(Self::Statement) - .chain(maybe_expr.iter().map(Arc::as_ref).map(Self::Expression)) - .collect(), - ), - Self::Statement(statement) => match statement { - Statement::Assignment(assignment) => Tree::Unary(Self::Assignment(assignment)), - Statement::Expression(expression) => Tree::Unary(Self::Expression(expression)), - }, - Self::Assignment(assignment) => Tree::Unary(Self::Expression(assignment.expression())), - Self::Single(single) => match single.inner() { - S::Boolean(_) - | S::Binary(_) - | S::Decimal(_) - | S::Hexadecimal(_) - | S::Variable(_) - | S::Witness(_) - | S::Parameter(_) - | S::Option(None) => Tree::Nullary, - S::Option(Some(l)) - | S::Either(Either::Left(l)) - | S::Either(Either::Right(l)) - | S::Expression(l) => Tree::Unary(Self::Expression(l)), - S::Call(call) => Tree::Unary(Self::Call(call)), - S::Match(match_) => Tree::Unary(Self::Match(match_)), - S::EnumMatch(enum_match) => Tree::Unary(Self::EnumMatch(enum_match)), - S::EnumConstruction(construction) => { - Tree::Unary(Self::EnumConstruction(construction)) - } - S::Tuple(elements) | S::Array(elements) | S::List(elements) => { - Tree::Nary(elements.iter().map(Self::Expression).collect()) - } - }, - Self::Call(call) => Tree::Nary(call.args().iter().map(Self::Expression).collect()), - Self::EnumConstruction(construction) => { - Tree::Nary(construction.args().iter().map(Self::Expression).collect()) - } - Self::Match(match_) => Tree::Nary(Arc::new([ - Self::Expression(match_.scrutinee()), - Self::Expression(match_.left().expression()), - Self::Expression(match_.right().expression()), - ])), - Self::EnumMatch(enum_match) => Tree::Nary( - std::iter::once(Self::Expression(enum_match.scrutinee())) - .chain( - enum_match - .arms() - .iter() - .map(|arm| Self::Expression(arm.expression())), - ) - .collect(), - ), - } - } -} - -// TODO: Fix indentation and formatting logic. The current flat iterator approach cannot -// track AST depth, causing incorrect indentation for nested `Block` and `Match` nodes. -impl fmt::Display for ExprTree<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use SingleExpressionInner as S; - - for data in self.verbose_pre_order_iter() { - match &data.node { - Self::Statement(..) if data.is_complete => writeln!(f, ";")?, - Self::Expression(..) | Self::Statement(..) => {} - Self::Block(..) => { - if data.n_children_yielded == 0 { - writeln!(f, "{{")?; - } else if !data.is_complete { - write!(f, " ")?; - } - if data.is_complete { - writeln!(f, "}}")?; - } - } - Self::Assignment(assignment) => match data.n_children_yielded { - 0 => write!(f, "let {}: {} = ", assignment.pattern(), assignment.ty())?, - n => debug_assert_eq!(n, 1), - }, - Self::Single(single) => match single.inner() { - S::Boolean(bit) => write!(f, "{bit}")?, - S::Binary(binary) => write!(f, "0b{binary}")?, - S::Decimal(decimal) => write!(f, "{decimal}")?, - S::Hexadecimal(hexadecimal) => write!(f, "0x{hexadecimal}")?, - S::Variable(name) => write!(f, "{name}")?, - S::Witness(name) => write!(f, "witness::{name}")?, - S::Parameter(name) => write!(f, "param::{name}")?, - S::Option(None) => write!(f, "None")?, - S::Option(Some(_)) => match data.n_children_yielded { - 0 => write!(f, "Some(")?, - n => { - debug_assert_eq!(n, 1); - write!(f, ")")?; - } - }, - S::Either(Either::Left(_)) => match data.n_children_yielded { - 0 => write!(f, "Left(")?, - n => { - debug_assert_eq!(n, 1); - write!(f, ")")?; - } - }, - S::Either(Either::Right(_)) => match data.n_children_yielded { - 0 => write!(f, "Right(")?, - n => { - debug_assert_eq!(n, 1); - write!(f, ")")?; - } - }, - S::Expression(_) => match data.n_children_yielded { - 0 => write!(f, "(")?, - n => { - debug_assert_eq!(n, 1); - write!(f, ")")?; - } - }, - S::Call(..) | S::Match(..) | S::EnumMatch(..) | S::EnumConstruction(..) => {} - S::Tuple(tuple) => { - if data.n_children_yielded == 0 { - write!(f, "(")?; - } else if !data.is_complete || tuple.len() == 1 { - write!(f, ", ")?; - } - if data.is_complete { - write!(f, ")")?; - } - } - S::Array(..) => { - if data.n_children_yielded == 0 { - write!(f, "[")?; - } else if !data.is_complete { - write!(f, ", ")?; - } - if data.is_complete { - write!(f, "]")?; - } - } - S::List(..) => { - if data.n_children_yielded == 0 { - write!(f, "list![")?; - } else if !data.is_complete { - write!(f, ", ")?; - } - if data.is_complete { - write!(f, "]")?; - } - } - }, - Self::Call(call) => { - if data.n_children_yielded == 0 { - write!(f, "{}(", call.name())?; - } else if !data.is_complete { - write!(f, ", ")?; - } - if data.is_complete { - write!(f, ")")?; - } - } - Self::EnumConstruction(construction) => { - match data.n_children_yielded { - 0 => { - write!( - f, - "{}::{}", - construction.enum_path_string(), - construction.variant() - )?; - if !construction.args().is_empty() { - write!(f, "(")?; - } - } - _ if !data.is_complete => write!(f, ", ")?, - _ => {} - } - if data.is_complete && !construction.args().is_empty() { - write!(f, ")")?; - } - } - Self::Match(match_) => match data.n_children_yielded { - 0 => write!(f, "match ")?, - 1 => write!(f, "{{\n{} => ", match_.left().pattern())?, - 2 => write!(f, ",\n{} => ", match_.right().pattern())?, - n => { - debug_assert_eq!(n, 3); - write!(f, ",\n}}")?; - } - }, - Self::EnumMatch(enum_match) => match data.n_children_yielded { - 0 => write!(f, "match ")?, - 1 => write!(f, "{{\n{} => ", enum_match.arms()[0])?, - n if n <= enum_match.arms().len() => { - write!(f, ",\n{} => ", enum_match.arms()[n - 1])?; - } - _ => write!(f, ",\n}}")?, - }, - } - } - - Ok(()) - } -} - -impl fmt::Display for Expression { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", ExprTree::Expression(self)) - } -} - -impl fmt::Display for Statement { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", ExprTree::Statement(self)) - } -} - -impl fmt::Display for Assignment { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", ExprTree::Assignment(self)) - } -} - -impl fmt::Display for SingleExpression { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", ExprTree::Single(self)) - } -} - -impl fmt::Display for Call { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", ExprTree::Call(self)) - } -} - -impl fmt::Display for CallName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CallName::Jet(jet) => write!(f, "jet::{jet}"), - CallName::UnwrapLeft(ty) => write!(f, "unwrap_left::<{ty}>"), - CallName::UnwrapRight(ty) => write!(f, "unwrap_right::<{ty}>"), - CallName::Unwrap => write!(f, "unwrap"), - CallName::IsNone(ty) => write!(f, "is_none::<{ty}>"), - CallName::Assert => write!(f, "assert!"), - CallName::Panic => write!(f, "panic!"), - CallName::Debug => write!(f, "dbg!"), - CallName::TypeCast(ty) => write!(f, "<{ty}>::into"), - CallName::Custom(name) => write!(f, "{name}"), - CallName::Fold(name, bound) => write!(f, "fold::<{name}, {bound}>"), - CallName::ArrayFold(name, size) => write!(f, "array_fold::<{name}, {size}>"), - CallName::ForWhile(name) => write!(f, "for_while::<{name}>"), - CallName::RawHash(ty) => write!(f, "raw_hash::<{ty}>"), - } - } -} - -impl fmt::Display for Match { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", ExprTree::Match(self)) - } -} - -impl fmt::Display for EnumMatch { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", ExprTree::EnumMatch(self)) - } -} - -impl fmt::Display for MatchPattern { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - MatchPattern::Left(i, ty) => write!(f, "Left({i}: {ty})"), - MatchPattern::Right(i, ty) => write!(f, "Right({i}: {ty})"), - MatchPattern::None => write!(f, "None"), - MatchPattern::Some(i, ty) => write!(f, "Some({i}: {ty})"), - MatchPattern::False => write!(f, "false"), - MatchPattern::True => write!(f, "true"), - } - } -} - -macro_rules! impl_parse_wrapped_string { - ($wrapper: ident, $label: literal) => { - impl ChumskyParse for $wrapper { - fn parser<'tokens, 'src: 'tokens, I>( - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - select! { - Token::Ident(ident) => Self::from_str_unchecked(ident) - } - .labelled($label) - } - } - }; -} - -impl_parse_wrapped_string!(SymbolName, "unresolved symbol name"); -impl_parse_wrapped_string!(FunctionName, "function name"); -impl_parse_wrapped_string!(Identifier, "identifier"); -impl_parse_wrapped_string!(AliasName, "alias name"); -impl_parse_wrapped_string!(ModuleName, "module name"); - -trait AstNode: ChumskyParse + crate::unstable::RequireFeature + std::fmt::Debug {} - -impl AstNode for T where T: ChumskyParse + crate::unstable::RequireFeature + std::fmt::Debug {} - -/// Copy of [`FromStr`] that internally uses the `chumsky` parser. -pub trait ParseFromStr: Sized { - /// Parse a value from the string `s`. - fn parse_from_str(s: &str) -> Result; -} - -/// Trait for parsing with collection of errors. -pub trait ParseFromStrWithErrors: Sized { - /// Parse a value from the string `content` with Errors. - /// - /// Feature-gated syntax in the parsed AST is checked against - /// `unstable_features`; uses of disabled features are pushed to `diagnostics`. - fn parse_from_str_with_errors( - file_id: usize, - content: &str, - unstable_features: &UnstableFeatures, - diagnostics: &mut DiagnosticManager, - ) -> Option; -} - -mod pipeline { - use super::*; - /// Handle the `simc` directive before lexing: an incompatible or malformed - /// directive is reported as the only diagnostic (the rest is noise), and - /// lexing starts right after a valid one, so the lexer and grammar never - /// see it. - pub fn directive_prescan( - content: &str, - file_id: usize, - diagnostics: &mut DiagnosticManager, - ) -> Option { - match SimcDirective::prescan(content, file_id) { - Ok(start) => Some(start), - Err(box_error) => { - let (err, span) = *box_error; - diagnostics.push(Diagnostic::new(err, span)); - None - } - } - } - - pub fn is_lex_ok( - mut lex_errs: Vec, - diagnostics: &mut DiagnosticManager, - ) -> Option { - // A stray `simc` makes every other diagnostic noise — its `"";` remnant - // does not lex — so the reserved-keyword errors are reported alone. - if lex_errs - .iter() - .any(|e| matches!(e.error(), Error::ReservedSimcKeyword)) - { - lex_errs.retain(|e| matches!(e.error(), Error::ReservedSimcKeyword)); - diagnostics.extend(lex_errs); - None - } else { - let lex_ok = lex_errs.is_empty(); - diagnostics.extend(lex_errs); - Some(lex_ok) - } - } - - pub fn parse_ast( - file_id: usize, - src: &str, - tokens: Tokens<'_>, - diagnostics: &mut DiagnosticManager, - ) -> (Option, bool) { - let eoi = Span::eof(file_id, src.len()); - let (ast, parse_errs) = T::parser() - .parse(tokens.as_slice().map(eoi, |(t, s)| (t, s))) - .into_output_errors(); - - let parse_ok = parse_errs.is_empty(); - diagnostics.extend(parse_errs); - (ast, parse_ok) - } - - pub fn post_check( - unstable_features: &UnstableFeatures, - program: Option<&T>, - diagnostics: &mut DiagnosticManager, - ) { - if let Some(ast) = program { - unstable_features.check_program(ast, diagnostics); - } - } -} - -/// Trait for generating parsers of themselves. -/// -/// Replacement for previous `PestParse` trait. -pub trait ChumskyParse: Sized { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>; -} - -type ParseError<'src> = extra::Err; - -/// This implementation only returns first encountered error. -impl ParseFromStr for A { - fn parse_from_str(s: &str) -> Result { - let (tokens, mut lex_errs) = crate::lexer::lex(MAIN_MODULE, s, 0); - - // The `simc` directive is source-file syntax, so fragments have no prescan - // and `simc` lexes as a reserved keyword. Its `"";` remnant does not - // lex either, so the first reserved-keyword error is reported alone. - if let Some(err) = lex_errs - .iter() - .find(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)) - { - return Err(err.clone()); - } - - let Some(tokens) = tokens else { - return Err(lex_errs - .pop() - .unwrap_or(Diagnostic::global(Error::CannotParse { - msg: "Empty token stream without an error".to_string(), - }))); - }; - - let (ast, parse_errs) = A::parser() - .map_with(|parsed, _| parsed) - .parse( - tokens - .as_slice() - .map(Span::eof(MAIN_MODULE, s.len()), |(t, s)| (t, s)), - ) - .into_output_errors(); - - if parse_errs.is_empty() { - Ok(ast.ok_or(Diagnostic::global(Error::CannotParse { - msg: "Empty AST without an error.".to_string(), - }))?) - } else { - let err = parse_errs.first().unwrap().clone(); - Err(err) - } - } -} - -impl ParseFromStrWithErrors for A { - fn parse_from_str_with_errors( - file_id: usize, - content: &str, - unstable_features: &UnstableFeatures, - diagnostics: &mut DiagnosticManager, - ) -> Option { - let before = diagnostics.error_count(); - - let start = pipeline::directive_prescan(content, file_id, diagnostics)?; - - let (tokens, lex_errs) = crate::lexer::lex(file_id, content, start); - - let lex_ok = pipeline::is_lex_ok(lex_errs, diagnostics)?; - - let tokens = tokens?; - - let (ast, parse_status) = pipeline::parse_ast::(file_id, content, tokens, diagnostics); - - if lex_ok && parse_status { - let () = pipeline::post_check(unstable_features, ast.as_ref(), diagnostics); - } - - // TODO: We should return parsed result if we found errors, but because analyzing in `ast` module - // is not handling poisoned tree right now, we don't return parsed result - if diagnostics.error_count() > before { - None - } else { - ast - } - } -} - -/// Parse a token, and, if not found, place itself in place of missing one. -/// -/// Should be only used when we know that this token should be there. For example, type of -/// `List` would require comma inside angle brackets. -fn parse_token_with_recovery<'tokens, 'src: 'tokens, I>( - tok: Token<'src>, -) -> impl Parser<'tokens, I, Token<'src>, ParseError<'src>> + Clone -where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, -{ - just(tok.clone()).recover_with(via_parser(empty().to(tok))) -} - -/// Parser with error recovery for expressions, which would always contains given delimiters. -/// -/// Can track span of open delimiter (if any). -fn delimited_with_recovery<'tokens, 'src: 'tokens, I, P, T, F>( - parser: P, - open: Token<'src>, - close: Token<'src>, - fallback: F, -) -> impl Parser<'tokens, I, T, ParseError<'src>> + Clone -where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - P: Parser<'tokens, I, T, ParseError<'src>> + Clone, - T: Clone + 'tokens, - F: Fn(Span) -> T + Clone + 'tokens, -{ - just(open.clone()) - .map_with(|_, e| e.span()) - .then(parser.recover_with(via_parser(nested_delimiters( - open.clone(), - close.clone(), - [ - (Token::LParen, Token::RParen), - (Token::LBracket, Token::RBracket), - (Token::LBrace, Token::RBrace), - (Token::LAngle, Token::RAngle), - ], - fallback, - )))) - .then(just(close).or_not()) - // TODO: we should use information about open delimiter - .validate(move |((open_span, content), close_token), _, emit| { - if close_token.is_none() { - emit.emit( - Error::Grammar { - msg: format!("Unclosed delimiter {open}"), - } - .with_span(open_span), - ) - } - content - }) -} - -impl ChumskyParse for AliasedType { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let atom = select! { - Token::Ident(ident) => { - if ident == "bool" { - AliasedType::boolean() - } else if let Ok(uint_type) = UIntType::from_str(ident) { - AliasedType::from(uint_type) - } else if let Ok(builtin) = BuiltinAlias::from_str(ident) { - AliasedType::builtin(builtin) - } else { - AliasedType::alias(AliasName::from_str_unchecked(ident)) - } - } - }; - - let num = select! { - Token::DecLiteral(i) => i.clone() - } - .labelled("decimal number") - .recover_with(via_parser( - none_of([Token::RAngle, Token::RBracket]) - .ignored() - .or(empty()) - .to(Decimal::from_str_unchecked("0")), - )); - - recursive(|ty| { - let args = delimited_with_recovery( - ty.clone() - .then_ignore(parse_token_with_recovery(Token::Comma)) - .then(ty.clone()), - Token::LAngle, - Token::RAngle, - |_| { - ( - AliasedType::alias(AliasName::from_str_unchecked("error")), - AliasedType::alias(AliasName::from_str_unchecked("error")), - ) - }, - ); - - let sum_type = just(Token::Ident("Either")) - .ignore_then(args) - .map(|(left, right)| AliasedType::either(left, right)) - .labelled("Either"); - - let option_type = just(Token::Ident("Option")) - .ignore_then(delimited_with_recovery( - ty.clone(), - Token::LAngle, - Token::RAngle, - |_| AliasedType::alias(AliasName::from_str_unchecked("error")), - )) - .map(AliasedType::option) - .labelled("Option"); - - let tuple = delimited_with_recovery( - ty.clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect() - .map(|s: Vec| AliasedType::tuple(s)), - Token::LParen, - Token::RParen, - |_| AliasedType::tuple(Vec::new()), - ) - .labelled("tuple"); - - let array = delimited_with_recovery( - ty.clone() - .then_ignore(parse_token_with_recovery(Token::Semi)) - .then(num.clone()) - .map(|(ty, size)| { - let digits = - crate::str::underscore_parsing::strip_digit_separators(size.as_inner()); - - AliasedType::array(ty, usize::from_str(digits.as_ref()).unwrap_or_default()) - }), - Token::LBracket, - Token::RBracket, - |_| { - AliasedType::array( - AliasedType::alias(AliasName::from_str_unchecked("error")), - 0, - ) - }, - ) - .labelled("array"); - - let list = just(Token::Ident("List")) - .ignore_then(delimited_with_recovery( - ty.then_ignore(parse_token_with_recovery(Token::Comma)) - .then(num.clone().validate(|num, e, emit| -> NonZeroPow2Usize { - let digits = crate::str::underscore_parsing::strip_digit_separators( - num.as_inner(), - ); - - match NonZeroPow2Usize::from_str(digits.as_ref()) { - Ok(number) => number, - Err(err) => { - emit.emit( - Error::Grammar { - msg: format!("Cannot parse list bound: {err}"), - } - .with_span(e.span()), - ); - // fallback to default value - NonZeroPow2Usize::TWO - } - } - })), - Token::LAngle, - Token::RAngle, - |_| { - ( - AliasedType::alias(AliasName::from_str_unchecked("error")), - NonZeroPow2Usize::TWO, - ) - }, - )) - .map(|(ty, size)| AliasedType::list(ty, size)) - .labelled("List"); - - choice((sum_type, option_type, tuple, array, list, atom)) - .map_with(|inner, _| inner) - .labelled("type") - }) - } -} - -impl ChumskyParse for Program { - /// Parses a sequence of top-level [`Item`]s into a complete [`Program`]. - /// - /// If an invalid item is encountered, it will safely skip the broken tokens - /// until it finds a synchronization point. This prevents the parser from - /// failing completely, allowing it to report multiple syntax errors across the file - /// while substituting the unparseable sections with [`Item::Ignored`]. - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let skip_until_next_item = any() - .then( - any() - .filter(|t| { - !matches!( - t, - Token::Pub - | Token::Use - | Token::Fn - | Token::Type - | Token::Mod - | Token::Enum - ) - }) - .repeated(), - ) - .map_with(|_, _| Item::Ignored); - - Item::parser() - .recover_with(via_parser(skip_until_next_item)) - .repeated() - .collect::>() - .map_with(|items, e| Program { - items: Arc::from(items), - span: e.span(), - }) - } -} - -impl ChumskyParse for Item { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - recursive(|item| { - let func_parser = Function::parser().map(Item::Function); - let type_parser = TypeAlias::parser().map(Item::TypeAlias); - let use_parser = UseDecl::parser().map(Item::Use); - let enum_parser = EnumDeclaration::parser().map(Item::EnumDeclaration); - - // Lazy item here - let mod_parser = Module::parser_with_items(item).map(Item::Module); - - choice(( - func_parser, - use_parser, - type_parser, - enum_parser, - mod_parser, - )) - }) - } -} - -impl ChumskyParse for Function { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let visibility = just(Token::Pub) - .to(Visibility::Public) - .or_not() - .map(Option::unwrap_or_default) - .labelled("function visibility"); - - let params = delimited_with_recovery( - FunctionParam::parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>(), - Token::LParen, - Token::RParen, - |_| Vec::new(), - ) - .map(Arc::from) - .labelled("function parameters"); - - let ret = just(Token::Arrow) - .ignore_then(AliasedType::parser()) - .or_not() - .labelled("return type"); - - let body = just(Token::LBrace) - .rewind() - .ignore_then(Expression::parser()) - .recover_with(via_parser(nested_delimiters( - Token::LBrace, - Token::RBrace, - [ - (Token::LParen, Token::RParen), - (Token::LBracket, Token::RBracket), - ], - Expression::empty, - ))) - .labelled("function body"); - - visibility - .then_ignore(just(Token::Fn)) - .then(FunctionName::parser()) - .then(params) - .then(ret) - .then(body) - .map_with(|((((visibility, name), params), ret), body), e| Self { - visibility, - name, - params, - ret, - body, - span: e.span(), - }) - } -} - -impl ChumskyParse for UseDecl { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let visibility = just(Token::Pub) - .to(Visibility::Public) - .or_not() - .map(Option::unwrap_or_default); - - let first_segment = select! { - Token::Ident(ident) => Identifier::from_str_unchecked(ident), - Token::Crate => Identifier::from_str_unchecked(CRATE_STR), - }; - - // Parse the base path prefix (e.g., `dependency_root_path::file::`, `dependency_root_path::dir::file::`, - // or `crate::dir::file::`). We require at least 2 segments here because a valid import needs a minimum - // of 3 items total: the dependency root path (or `crate`), the file, and the specific item. - // - // With the introduction of `mod` keyword and single-file flattening, 2 total segments are now - // valid: `crate::item`, where `crate` is the program root. - let path = first_segment - .then_ignore(just(Token::DoubleColon)) - .then( - Identifier::parser() - .then_ignore(just(Token::DoubleColon)) - .repeated() - .collect::>(), - ) - .map(|(first, mut rest)| { - let mut path = vec![first]; - path.append(&mut rest); - path - }); - - let aliased_item = - SymbolName::parser().then(just(Token::As).ignore_then(SymbolName::parser()).or_not()); - - let list = aliased_item - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect() - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .map(UseItems::List); - let single = aliased_item.map(UseItems::Single); - let items = choice((list, single)); - - visibility - .then_ignore(just(Token::Use)) - .then(path) - .then(items) - .then_ignore(just(Token::Semi)) - .map_with(|((visibility, path), items), e| Self { - visibility, - path, - items, - span: e.span(), - }) - } -} - -impl ChumskyParse for FunctionParam { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let identifier = Identifier::parser(); - - let ty = AliasedType::parser(); - - identifier - .then_ignore(just(Token::Colon)) - .then(ty) - .map_with(|(identifier, ty), e| Self { - identifier, - ty, - span: e.span(), - }) - } -} - -impl Statement { - fn parser<'tokens, 'src: 'tokens, I, E>( - expr: E, - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, - { - let assignment = Assignment::parser(expr.clone()).map(Statement::Assignment); - - let expression = expr.map(Statement::Expression); - - choice((assignment, expression)) - } -} - -impl Assignment { - fn parser<'tokens, 'src: 'tokens, I, E>( - expr: E, - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, - { - just(Token::Let) - .ignore_then(Pattern::parser()) - .then_ignore(parse_token_with_recovery(Token::Colon)) - .then(AliasedType::parser()) - .then_ignore(parse_token_with_recovery(Token::Eq)) - .then(expr) - .map_with(|((pattern, ty), expression), e| Self { - pattern, - ty, - expression, - span: e.span(), - }) - } -} - -impl ChumskyParse for Pattern { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - recursive(|pat| { - let variable = Identifier::parser().map(Pattern::Identifier); - - let ignore = select! { - Token::Ident("_") => Pattern::Ignore, - }; - - let tuple = delimited_with_recovery( - pat.clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>(), - Token::LParen, - Token::RParen, - |_| Vec::new(), - ) - .map(Pattern::tuple); - - let array = delimited_with_recovery( - pat.clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>(), - Token::LBracket, - Token::RBracket, - |_| Vec::new(), - ) - .map(Pattern::array); - - choice((ignore, variable, tuple, array)).labelled("pattern") - }) - } -} - -impl Call { - fn parser<'tokens, 'src: 'tokens, I, E>( - expr: E, - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, - { - let args = delimited_with_recovery( - expr.separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>(), - Token::LParen, - Token::RParen, - |_| Vec::new(), - ) - .map(Arc::from) - .labelled("call arguments"); - - CallName::parser() - .then(args) - .map_with(|(name, args), e| Self { - name, - args, - span: e.span(), - }) - } -} - -impl ChumskyParse for CallName { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let turbofish_start = just(Token::DoubleColon).then(just(Token::LAngle)).ignored(); - - let generics_close = just(Token::RAngle); - - let type_cast = just(Token::LAngle) - .ignore_then(AliasedType::parser()) - .then_ignore(generics_close.clone()) - .then_ignore(just(Token::DoubleColon)) - .then_ignore(just(Token::Ident("into"))) - .map(CallName::TypeCast); - - let builtin_generic_ty = |name: &'static str, ctor: fn(AliasedType) -> Self| { - just(Token::Ident(name)) - .ignore_then(turbofish_start.clone()) - .ignore_then(AliasedType::parser()) - .then_ignore(generics_close.clone()) - .map(ctor) - }; - - let unwrap_left = builtin_generic_ty("unwrap_left", CallName::UnwrapLeft); - let unwrap_right = builtin_generic_ty("unwrap_right", CallName::UnwrapRight); - let is_none = builtin_generic_ty("is_none", CallName::IsNone); - let raw_hash = builtin_generic_ty("raw_hash", CallName::RawHash); - - let fold = just(Token::Ident("fold")) - .ignore_then(turbofish_start.clone()) - .ignore_then(FunctionName::parser()) - .then_ignore(parse_token_with_recovery(Token::Comma)) - .then(select! { Token::DecLiteral(s) => s }.labelled("list size")) - .then_ignore(generics_close.clone()) - .validate(|(func, bound_str), e, emit| { - let digits = - crate::str::underscore_parsing::strip_digit_separators(bound_str.as_inner()); - - let bound = match digits.parse::() { - Ok(num) => match NonZeroPow2Usize::new(num) { - Some(val) => val, - None => { - emit.emit(Error::ListBoundPow2 { bound: num }.with_span(e.span())); - NonZeroPow2Usize::TWO - } - }, - Err(_) => { - emit.emit( - Error::CannotParse { - msg: format!("Invalid number: {}", bound_str), - } - .with_span(e.span()), - ); - NonZeroPow2Usize::TWO - } - }; - - CallName::Fold(func, bound) - }); - - let array_fold = just(Token::Ident("array_fold")) - .ignore_then(turbofish_start.clone()) - .ignore_then(FunctionName::parser()) - .then_ignore(parse_token_with_recovery(Token::Comma)) - .then(select! { Token::DecLiteral(s) => s }.labelled("array size")) - .then_ignore(generics_close.clone()) - .validate(|(func, size_str), e, emit| { - let digits = - crate::str::underscore_parsing::strip_digit_separators(size_str.as_inner()); - - let size = match digits.parse::() { - Ok(0) => { - emit.emit(Error::ArraySizeNonZero { size: 0 }.with_span(e.span())); - NonZeroUsize::new(1).unwrap() - } - Ok(n) => NonZeroUsize::new(n).unwrap(), - Err(_) => { - emit.emit( - Error::CannotParse { - msg: format!("Invalid number: {}", size_str), - } - .with_span(e.span()), - ); - NonZeroUsize::new(1).unwrap() - } - }; - - CallName::ArrayFold(func, size) - }); - - let for_while = just(Token::Ident("for_while")) - .ignore_then(turbofish_start.clone()) - .ignore_then(FunctionName::parser()) - .then_ignore(generics_close.clone()) - .map(CallName::ForWhile); - - let simple_builtins = select! { - Token::Ident("unwrap") => CallName::Unwrap, - Token::Macro("assert!") => CallName::Assert, - Token::Macro("panic!") => CallName::Panic, - Token::Macro("dbg!") => CallName::Debug, - }; - - let jet = select! { Token::Jet(s) => JetName::from_str_unchecked(s) }.map(CallName::Jet); - - let custom_func = FunctionName::parser().map(CallName::Custom); - - choice(( - type_cast, - unwrap_left, - unwrap_right, - is_none, - fold, - array_fold, - for_while, - raw_hash, - simple_builtins, - jet, - custom_func, - )) - } -} - -impl ChumskyParse for TypeAlias { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let visibility = just(Token::Pub) - .to(Visibility::Public) - .or_not() - .map(Option::unwrap_or_default); - - let name = AliasName::parser() - .validate(|name, e, emit| { - let ident = name.as_str(); - let known_type = if ident == "bool" { - Some(AliasedType::boolean()) - } else if let Ok(uint_type) = UIntType::from_str(ident) { - Some(AliasedType::from(uint_type)) - } else if let Ok(builtin) = BuiltinAlias::from_str(ident) { - Some(AliasedType::builtin(builtin)) - } else { - None - }; - - if known_type.is_some() { - emit.emit( - Error::RedefinedAliasAsBuiltin { name: name.clone() }.with_span(e.span()), - ); - } - name - }) - .map_with(|name, e| (name, e.span())); - - visibility - .then( - just(Token::Type) - .ignore_then(name) - .then_ignore(parse_token_with_recovery(Token::Eq)) - .then(AliasedType::parser()) - .then_ignore(just(Token::Semi)), - ) - .map_with(|(visibility, (name, ty)), e| Self { - visibility, - name: name.0, - ty, - span: e.span(), - }) - } -} - -/// Identifiers of the built-in binary match patterns. -/// An enum may not use them as its name, because e.g. `Left::A` in a match arm would parse as the built-in `Left(..)` pattern. -pub(crate) const RESERVED_PATTERN_NAMES: [&str; 4] = ["Left", "Right", "Some", "None"]; - -impl ChumskyParse for EnumDeclaration { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let visibility = just(Token::Pub) - .to(Visibility::Public) - .or_not() - .map(Option::unwrap_or_default); - - let name = AliasName::parser().try_map(|name, span| { - if RESERVED_PATTERN_NAMES.contains(&name.as_str()) { - return Err(Diagnostic::new( - Error::Grammar { - msg: format!( - "enum name '{name}' is reserved for the built-in match pattern `{name}`" - ), - }, - span, - )); - } - // Reserved type names are rejected via the shared list, which - // also covers the generic constructors (`Either`, `Option`, - // `List`): `enum Signature` or `enum Option` would make - // constructions name the enum while type annotations resolve - // to the builtin, and the ABI would report the bare name - // ambiguously. - if crate::str::is_reserved_alias_name(name.as_str()) { - return Err(Diagnostic::new( - Error::RedefinedAliasAsBuiltin { name: name.clone() }, - span, - )); - } - Ok(name) - }); - - let payload = AliasedType::parser() - .separated_by(just(Token::Comma)) - .allow_trailing() - .at_least(1) - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|payload| Arc::from(payload.unwrap_or_default())); - - let variant = Identifier::parser() - .then(payload) - .map_with(|(name, payload), e| EnumVariant { - name, - payload, - span: e.span(), - }); - - let variants = variant - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .map(Arc::from); - - visibility - .then_ignore(just(Token::Enum)) - .then(name) - .then(variants) - .map_with(|((visibility, name), variants), e| Self { - visibility, - name, - variants, - span: e.span(), - }) - } -} - -/// Split a parsed block body into its statements and its optional final expression. -/// -/// Each element arrives paired with the `;` that followed it, if any. Only the last -/// element may omit its `;`, and only when it is an expression rather than an -/// assignment: that element is the block's final expression. Every other missing `;` -/// is reported, and the element is kept as a statement so that analysis of the rest -/// of the block still happens. -fn split_block_body( - elements: Vec<(Statement, Option>)>, - emit: &mut chumsky::input::Emitter, -) -> (Arc<[Statement]>, Option>) { - let count = elements.len(); - let mut statements = Vec::with_capacity(count); - let mut final_expr = None; - - for (index, (statement, semi)) in elements.into_iter().enumerate() { - if semi.is_some() { - statements.push(statement); - continue; - } - - match statement { - // A trailing expression without `;` is the block's value. - Statement::Expression(expression) if index + 1 == count => { - final_expr = Some(Arc::new(expression)); - } - statement => { - emit.emit( - Error::Grammar { - msg: "Expected `;` after statement".to_string(), - } - .with_span(*statement.span()), - ); - statements.push(statement); - } - } - } - - (Arc::from(statements), final_expr) -} - -impl ChumskyParse for Expression { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - recursive(|expr| { - let block = { - // A block body is a run of elements, each of which is a statement - // followed by `;`, except that the last one may drop the `;` and - // become the block's final expression. - let element = Statement::parser(expr.clone()).then(just(Token::Semi).or_not()); - - let block_recovery = nested_delimiters( - Token::LBrace, - Token::RBrace, - [ - (Token::LParen, Token::RParen), - (Token::LAngle, Token::RAngle), - (Token::LBracket, Token::RBracket), - ], - |span| Expression::empty(span).inner().clone(), - ); - - let body = element - .repeated() - .collect::>() - .validate(|elements, _, emit| split_block_body(elements, emit)) - .recover_with(skip_then_retry_until( - block_recovery.ignored().or(any().ignored()), - one_of([Token::Semi, Token::RParen, Token::RBracket, Token::RBrace]) - .ignored(), - )); - - delimited_with_recovery(body, Token::LBrace, Token::RBrace, |_| { - (Arc::from(Vec::new()), None) - }) - .map(|(stmts, end_expr)| ExpressionInner::Block(stmts, end_expr)) - }; - - let single = SingleExpression::parser(expr.clone()).map(ExpressionInner::Single); - - choice((block, single)) - .map_with(|inner, e| Expression { - inner, - span: e.span(), - }) - .labelled("expression") - }) - } -} - -impl SingleExpression { - fn parser<'tokens, 'src: 'tokens, I, E>( - expr: E, - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, - { - let wrapper = |name: &'static str| { - select! { Token::Ident(i) if i == name => i }.ignore_then( - expr.clone() - .delimited_by(just(Token::LParen), just(Token::RParen)), - ) - }; - - let left = - wrapper("Left").map(|e| SingleExpressionInner::Either(Either::Left(Arc::new(e)))); - - let right = - wrapper("Right").map(|e| SingleExpressionInner::Either(Either::Right(Arc::new(e)))); - - let some = wrapper("Some").map(|e| SingleExpressionInner::Option(Some(Arc::new(e)))); - - // Bare `None` is the option literal only when no `::` follows: - // `None::A` is a construction of an enum aliased as `None`, and - // must fall through to the enum-construction alternative. - let none = select! { Token::Ident("None") => SingleExpressionInner::Option(None) } - .then_ignore(just(Token::DoubleColon).not().rewind()); - - let boolean = select! { Token::Bool(b) => SingleExpressionInner::Boolean(b) }; - - let comma_separated = expr - .clone() - .separated_by(just(Token::Comma)) - .allow_trailing() - .collect::>(); - - let array = delimited_with_recovery( - comma_separated.clone(), - Token::LBracket, - Token::RBracket, - |_| Vec::new(), - ) - .map(|es| SingleExpressionInner::Array(Arc::from(es))); - - let list = just(Token::Macro("list!")) - .ignore_then(delimited_with_recovery( - comma_separated.clone(), - Token::LBracket, - Token::RBracket, - |_| Vec::new(), - )) - .map(|es| SingleExpressionInner::List(Arc::from(es))); - - let tuple = delimited_with_recovery( - comma_separated.clone(), - Token::LParen, - Token::RParen, - |_| Vec::new(), - ) - .map(|es| SingleExpressionInner::Tuple(Arc::from(es))); - - let literal = select! { - Token::DecLiteral(s) => SingleExpressionInner::Decimal(s), - Token::HexLiteral(s) => SingleExpressionInner::Hexadecimal(s), - Token::BinLiteral(s) => SingleExpressionInner::Binary(s), - Token::Witness(s) => SingleExpressionInner::Witness(TemplateProgramWitness::witness_from_str(s)), - Token::Param(s) => SingleExpressionInner::Parameter(TemplateProgramWitness::parameter_from_str(s)), - }; - - // Enum variant construction: `Path::To::Enum::Variant(args..)`. - // At least one `::` distinguishes the path from variables and calls. - // The built-in wrappers (Left, Some, ...) require `(` directly after - // their name, so they never reach this alternative. - let enum_construction = Identifier::parser() - .separated_by(just(Token::DoubleColon)) - .at_least(2) - .collect::>() - .then( - comma_separated - .clone() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not(), - ) - .map_with(|(mut segments, args), e| { - let variant = segments.pop().expect("the parser requires two segments"); - EnumConstruction { - enum_path: Arc::from(segments), - variant, - args: Arc::from(args.unwrap_or_default()), - span: e.span(), - } - }) - .map(SingleExpressionInner::EnumConstruction); - - let call = Call::parser(expr.clone()).map(SingleExpressionInner::Call); - - let match_expr = match_expr_parser(expr.clone()); - - let variable = Identifier::parser().map(SingleExpressionInner::Variable); - - // Expression delimeted by parentheses - let expression = expr - .clone() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .map(|es| SingleExpressionInner::Expression(Arc::from(es))); - - choice(( - left, - right, - some, - none, - boolean, - match_expr, - expression, - list, - array, - tuple, - enum_construction, - call, - literal, - variable, - )) - .map_with(|inner, e| Self { - inner, - span: e.span(), - }) - } -} - -impl ChumskyParse for MatchPattern { - fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let wrapper = |name: &'static str, ctor: fn(Pattern, AliasedType) -> Self| { - select! { Token::Ident(i) if i == name => i } - .ignore_then(delimited_with_recovery( - Pattern::parser() - .then_ignore(just(Token::Colon)) - .then(AliasedType::parser()), - Token::LParen, - Token::RParen, - |_| { - ( - Pattern::Ignore, - AliasedType::alias(AliasName::from_str_unchecked("error")), - ) - }, - )) - .map(move |(id, ty)| ctor(id, ty)) - }; - - choice(( - wrapper("Left", MatchPattern::Left), - wrapper("Right", MatchPattern::Right), - wrapper("Some", MatchPattern::Some), - select! { Token::Ident("None") => MatchPattern::None }, - select! { Token::Bool(true) => MatchPattern::True }, - select! { Token::Bool(false) => MatchPattern::False }, - )) - } -} - -/// Head of an enum match arm, before the arm body is attached: `EnumName::Variant` with optional payload bindings. -#[derive(Clone)] -struct EnumArmHead { - enum_path: Arc<[Identifier]>, - variant: Identifier, - bindings: Arc<[(Pattern, AliasedType)]>, -} - -impl EnumArmHead { - fn into_arm(self, expression: Arc, span: Span) -> EnumMatchArm { - EnumMatchArm { - enum_path: self.enum_path, - variant: self.variant, - bindings: self.bindings, - expression, - span, - } - } -} - -/// One parsed match arm. An enum variant head or a built-in pattern, -/// plus the arm body and its source boundaries. -type ParsedMatchArm = (Either, Arc, Span); - -/// Parser for the head of an enum match arm: `EnumName::Variant` with -/// optional payload bindings `(pattern: Type, ...)`. -/// -/// A non-reserved head identifier commits without backtracking: the -/// `select!` guard fails without consuming the token. A reserved pattern -/// name (`Left`, `Some`, ...) heads an enum path only when `::` follows, -/// so an alias of an enum that shadows a pattern name stays matchable -/// while `Left(x)` remains the built-in pattern. -fn enum_arm_head_parser<'tokens, 'src: 'tokens, I>( -) -> impl Parser<'tokens, I, EnumArmHead, ParseError<'src>> + Clone -where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, -{ - let bindings = Pattern::parser() - .then_ignore(just(Token::Colon)) - .then(AliasedType::parser()) - .separated_by(just(Token::Comma)) - .allow_trailing() - .at_least(1) - .collect::>() - .delimited_by(just(Token::LParen), just(Token::RParen)) - .or_not() - .map(|bindings| Arc::from(bindings.unwrap_or_default())); - - let head_ident = choice(( - select! { Token::Ident(name) if !RESERVED_PATTERN_NAMES.contains(&name) => Identifier::from_str_unchecked(name) }, - // The rewind leaves the `::` for the shared `then_ignore` below. - select! { Token::Ident(name) => Identifier::from_str_unchecked(name) } - .then_ignore(just(Token::DoubleColon).rewind()), - )); - - head_ident - .then_ignore(just(Token::DoubleColon)) - .then( - Identifier::parser() - .separated_by(just(Token::DoubleColon)) - .at_least(1) - .collect::>(), - ) - .then(bindings) - .map(|((first, mut rest), bindings)| { - let variant = rest.pop().expect("the parser requires one segment"); - let mut enum_path = vec![first]; - enum_path.extend(rest); - EnumArmHead { - enum_path: Arc::from(enum_path), - variant, - bindings, - } - }) -} - -/// Parser for one match arm. An arm head, `=>`, the arm body, and the -/// comma that is required unless the body is a block expression. -fn match_arm_parser<'tokens, 'src: 'tokens, I, E>( - expr: E, -) -> impl Parser<'tokens, I, ParsedMatchArm, ParseError<'src>> + Clone -where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, -{ - let arm_head = choice(( - enum_arm_head_parser().map(Either::Left), - MatchPattern::parser().map(Either::Right), - )); - - arm_head - .then_ignore(just(Token::FatArrow)) - .then(expr.map(Arc::new)) - .then(just(Token::Comma).or_not()) - .validate(|((head, expression), comma), e, emitter| { - let is_block = matches!(expression.as_ref().inner, ExpressionInner::Block(_, _)); - if !is_block && comma.is_none() { - emitter.emit( - Error::Grammar { - msg: "Missing ',' after a match arm that isn't block expression" - .to_string(), - } - .with_span(e.span()), - ); - } - (head, expression, e.span()) - }) -} - -/// A binary match with dummy arms, standing in for a malformed match so -/// parsing can continue after its error was reported. -fn placeholder_match(scrutinee: Arc, span: Span) -> SingleExpressionInner { - let fallback_arm = MatchArm { - expression: Arc::new(Expression::empty(Span::DUMMY)), - pattern: MatchPattern::False, - span: Span::DUMMY, - }; - SingleExpressionInner::Match(Match { - scrutinee, - left: fallback_arm.clone(), - right: fallback_arm, - span, - }) -} - -/// Do the two patterns complement each other in canonical order -/// (`Left`/`Right`, `None`/`Some`, `false`/`true`)? -fn patterns_in_canonical_order(left: &MatchPattern, right: &MatchPattern) -> bool { - matches!( - (left, right), - (MatchPattern::Left(..), MatchPattern::Right(..)) - | (MatchPattern::None, MatchPattern::Some(..)) - | (MatchPattern::False, MatchPattern::True) - ) -} - -/// Assemble parsed arms into an [`EnumMatch`] or a binary [`Match`] node. -/// -/// Malformed arms return the error to report together with a fallback -/// node, so the caller emits exactly one error per malformed match. -/// Running bad arms through the pattern pairing would emit a second, -/// bogus "incompatible match arms" error for the same span. -fn assemble_match_arms( - scrutinee: Arc, - arms: Vec, - span: Span, -) -> Result> { - if arms.is_empty() { - return Err(Box::new(( - Error::Grammar { - msg: "match expression has no arms".to_string(), - } - .with_span(span), - placeholder_match(scrutinee, span), - ))); - } - - // Split arms by the classification the arm heads carry. Any number of - // enum arms (including one) routes to the enum match, whose analysis - // reports missing variants. The binary arm-count error below would be misleading there. - let (enum_arms, builtin_arms): (Vec, Vec) = arms - .into_iter() - .partition_map(|(head, expression, arm_span)| match head { - Either::Left(head) => Either::Left(head.into_arm(expression, arm_span)), - Either::Right(pattern) => Either::Right(MatchArm { - pattern, - expression, - span: arm_span, - }), - }); - - if builtin_arms.is_empty() { - return Ok(SingleExpressionInner::EnumMatch(EnumMatch { - scrutinee, - arms: Arc::from(enum_arms), - span, - })); - } - - if let Some(enum_arm) = enum_arms.first() { - // Mixed arms: name the clash instead of an arm-count error. - return Err(Box::new(( - Error::Grammar { - msg: format!( - "match arms mix the enum variant pattern `{}` \ - with the built-in pattern `{}`", - enum_arm, builtin_arms[0].pattern - ), - } - .with_span(span), - placeholder_match(scrutinee, span), - ))); - } - - // Binary match: exactly 2 built-in arms, in either order. - let Ok([first, second]) = <[MatchArm; 2]>::try_from(builtin_arms) else { - return Err(Box::new(( - Error::Grammar { - msg: "binary match requires exactly 2 arms".to_string(), - } - .with_span(span), - placeholder_match(scrutinee, span), - ))); - }; - - let (left, right) = if patterns_in_canonical_order(&first.pattern, &second.pattern) { - (first, second) - } else if patterns_in_canonical_order(&second.pattern, &first.pattern) { - (second, first) - } else { - let error = Error::IncompatibleMatchArms { - first: Box::new(first.pattern.clone()), - second: Box::new(second.pattern.clone()), - } - .with_span(span); - // The arms still form a match; keep them so analysis can continue. - let node = SingleExpressionInner::Match(Match { - scrutinee, - left: first, - right: second, - span, - }); - return Err(Box::new((error, node))); - }; - - Ok(SingleExpressionInner::Match(Match { - scrutinee, - left, - right, - span, - })) -} - -/// Parser for `match` expressions. -/// -/// Handles both binary matches (exactly 2 arms: Left/Right, None/Some, -/// false/true) and enum matches (arms of the form `EnumName::Variant`). -/// Dispatches to [`Match`] or [`EnumMatch`] based on the patterns found. -fn match_expr_parser<'tokens, 'src: 'tokens, I, E>( - expr: E, -) -> impl Parser<'tokens, I, SingleExpressionInner, ParseError<'src>> + Clone -where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, -{ - let arms = delimited_with_recovery( - match_arm_parser(expr.clone()) - .repeated() - .collect::>(), - Token::LBrace, - Token::RBrace, - |_| vec![], - ); - - just(Token::Match) - .ignore_then(expr.map(Arc::new)) - .then(arms) - .validate(|(scrutinee, arms), e, emit| { - match assemble_match_arms(scrutinee, arms, e.span()) { - Ok(node) => node, - Err(boxed) => { - let (error, fallback) = *boxed; - emit.emit(error); - fallback - } - } - }) -} - -impl Module { - pub fn parser_with_items<'tokens, 'src: 'tokens, I>( - item_parser: impl Parser<'tokens, I, Item, ParseError<'src>> + Clone + 'tokens, - ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone - where - I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, - { - let visibility = just(Token::Pub) - .to(Visibility::Public) - .or_not() - .map(Option::unwrap_or_default); - - let name = ModuleName::parser().map_with(|name, e| (name, e.span())); - - let items = item_parser - .repeated() - .collect::>() - .delimited_by(just(Token::LBrace), just(Token::RBrace)) - .recover_with(via_parser(nested_delimiters( - Token::LBrace, - Token::RBrace, - [ - (Token::LParen, Token::RParen), - (Token::LBracket, Token::RBracket), - ], - |_| Vec::new(), - ))) - .map(Arc::from); - - visibility - .then(just(Token::Mod).ignore_then(name).then(items)) - .map_with(|(visibility, (name, items)), e| Self { - visibility, - name: name.0, - items, - span: e.span(), - }) - .validate(|module, _, emit| { - // TODO: Enums may only be declared at the top level of a file (done so to reduce scope of the PR). - // The bare name is the enum's identity in the ABI, and a module path would obscure it. - // Direct children suffice. Nested modules validate their own items. - for item in module.items.iter() { - if let Item::EnumDeclaration(decl) = item { - emit.emit( - Error::Grammar { - msg: format!( - "enum `{}` is declared inside `mod {}`; enums may \ - only be declared at the top level of a file", - decl.name(), - module.name - ), - } - .with_span(decl.into()), - ); - } - } - module - }) - } -} - -impl<'a, A: AsRef> From<&'a A> for Span { - fn from(value: &'a A) -> Self { - *value.as_ref() - } -} - -impl AsRef for Program { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for Function { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for FunctionParam { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for Assignment { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for TypeAlias { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for Expression { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for SingleExpression { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for Call { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for Match { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for MatchArm { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for EnumMatchArm { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for UseDecl { - fn as_ref(&self) -> &Span { - &self.span - } -} - -impl AsRef for Module { - fn as_ref(&self) -> &Span { - &self.span - } -} - -#[cfg(feature = "arbitrary")] -pub(crate) fn generate_arbitrary_items<'a>( - u: &mut arbitrary::Unstructured<'a>, -) -> arbitrary::Result> { - let mut items_vec = Vec::new(); - - let len = u.int_in_range(0..=2)?; - for _ in 0..len { - items_vec.push(::arbitrary(u)?); - } - - Ok(items_vec) -} - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for Program { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let mut items_vec = generate_arbitrary_items(u)?; - - // Enum declarations are valid only at the top level of a file, so - // they are generated here and not in `generate_arbitrary_items`, - // which `Module::arbitrary` reuses for nested items: a nested enum - // would display to source that no longer parses. - // - // TODO(enums): declarations alone leave construction, matching, - // payload binding, and compilation unreached — the expression - // generator does not produce `EnumConstruction`/`EnumMatch` - // correlated with the declared enums. Generate a coherent - // declaration plus an expression using one of its variants, and - // consider a two-stage serde target (serialize -> deserialize as - // `UnresolvedValues` -> resolve against the declared types) to - // cover enum witness serialization. - for _ in 0..u.int_in_range(0..=2u8)? { - items_vec.push(Item::EnumDeclaration(EnumDeclaration::arbitrary(u)?)); - } - - // Three equally-likely modes for how `fn main()` is injected: - // 0 — no explicit main (arbitrary items only) - // 1 — main with arbitrary params and return type - // 2 — main with no params and no return type (closest to valid) - match u.int_in_range(0..=2u8)? { - 0 => {} - 1 => { - let mut main_fn = ::arbitrary_rec(u, 3)?; - main_fn.name = FunctionName::main(); - items_vec.push(Item::Function(main_fn)); - } - _ => { - let mut main_fn = ::arbitrary_rec(u, 3)?; - main_fn.name = FunctionName::main(); - main_fn.params = Arc::from([]); - main_fn.ret = None; - items_vec.push(Item::Function(main_fn)); - } - } - - let items: Arc<[Item]> = items_vec.into(); - Ok(Self { - items, - span: Span::DUMMY, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for Item { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - match u.int_in_range(0..=3)? { - 0 => Ok(Item::TypeAlias(TypeAlias::arbitrary(u)?)), - 1 => Ok(Item::Function(Function::arbitrary(u)?)), - 2 => Ok(Item::Use(UseDecl::arbitrary(u)?)), - 3 => Ok(Item::Module(Module::arbitrary(u)?)), - _ => unreachable!(), - } - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for Function { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - ::arbitrary_rec(u, 3) - } -} - -#[cfg(feature = "arbitrary")] -impl crate::ArbitraryRec for Function { - fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { - use arbitrary::Arbitrary; - - let visibility = Visibility::arbitrary(u)?; - let name = FunctionName::arbitrary(u)?; - let len = u.int_in_range(0..=6)?; - let params = (0..len) - .map(|_| FunctionParam::arbitrary(u)) - .collect::>>()?; - let ret = Option::::arbitrary(u)?; - let body = Expression::arbitrary_rec(u, budget).map(Expression::into_block)?; - Ok(Self { - visibility, - name, - params, - ret, - body, - span: Span::DUMMY, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for Module { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let visibility = Visibility::arbitrary(u)?; - let name = ModuleName::arbitrary(u)?; - let items_vec = generate_arbitrary_items(u)?; - - Ok(Self { - visibility, - name, - items: items_vec.into(), - span: Span::DUMMY, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl crate::ArbitraryRec for Expression { - fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { - use arbitrary::Arbitrary; - - let inner = match budget.checked_sub(1) { - None => SingleExpression::arbitrary_rec(u, budget).map(ExpressionInner::Single), - Some(new_budget) => match bool::arbitrary(u)? { - false => SingleExpression::arbitrary_rec(u, budget).map(ExpressionInner::Single), - true => { - let len = u.int_in_range(0..=3)?; - let statements = (0..len) - .map(|_| Statement::arbitrary_rec(u, new_budget)) - .collect::>>()?; - let maybe_single = match bool::arbitrary(u)? { - false => None, - true => Expression::arbitrary_rec(u, new_budget) - .map(Arc::new) - .map(Some)?, - }; - Ok(ExpressionInner::Block(statements, maybe_single)) - } - }, - }?; - Ok(Self { - inner, - span: Span::DUMMY, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl crate::ArbitraryRec for Statement { - fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { - use arbitrary::Arbitrary; - - match bool::arbitrary(u)? { - false => Assignment::arbitrary_rec(u, budget).map(Self::Assignment), - true => Expression::arbitrary_rec(u, budget).map(Self::Expression), - } - } -} - -#[cfg(feature = "arbitrary")] -impl crate::ArbitraryRec for Assignment { - fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { - use arbitrary::Arbitrary; - - let pattern = Pattern::arbitrary(u)?; - let ty = AliasedType::arbitrary(u)?; - let expression = Expression::arbitrary_rec(u, budget)?; - - Ok(Self { - pattern, - ty, - expression, - span: Span::DUMMY, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl crate::ArbitraryRec for SingleExpression { - fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { - use arbitrary::Arbitrary; - use SingleExpressionInner as S; - - let inner = match budget.checked_sub(1) { - None => match u.int_in_range(0..=6)? { - 0 => bool::arbitrary(u).map(S::Boolean), - 1 => Binary::arbitrary(u).map(S::Binary), - 2 => Decimal::arbitrary(u).map(S::Decimal), - 3 => Hexadecimal::arbitrary(u).map(S::Hexadecimal), - 4 => Identifier::arbitrary(u).map(S::Variable), - 5 => TemplateProgramWitness::arbitrary(u).map(S::Witness), - 6 => Ok(S::Option(None)), - _ => unreachable!(), - }, - Some(new_budget) => match u.int_in_range(0..=15)? { - 0 => bool::arbitrary(u).map(S::Boolean), - 1 => Binary::arbitrary(u).map(S::Binary), - 2 => Decimal::arbitrary(u).map(S::Decimal), - 3 => Hexadecimal::arbitrary(u).map(S::Hexadecimal), - 4 => Identifier::arbitrary(u).map(S::Variable), - 5 => TemplateProgramWitness::arbitrary(u).map(S::Witness), - 6 => Ok(S::Option(None)), - 7 => Expression::arbitrary_rec(u, new_budget) - .map(Arc::new) - .map(Some) - .map(S::Option), - 8 => Expression::arbitrary_rec(u, new_budget) - .map(Arc::new) - .map(Either::Left) - .map(S::Either), - 9 => Expression::arbitrary_rec(u, new_budget) - .map(Arc::new) - .map(Either::Right) - .map(S::Either), - 10 => Expression::arbitrary_rec(u, new_budget) - .map(Arc::new) - .map(S::Expression), - 11 => Call::arbitrary_rec(u, new_budget).map(S::Call), - 12 => Match::arbitrary_rec(u, new_budget).map(S::Match), - 13 => { - let len = u.int_in_range(0..=3)?; - (0..len) - .map(|_| Expression::arbitrary_rec(u, new_budget)) - .collect::>>() - .map(S::Tuple) - } - 14 => { - let len = u.int_in_range(0..=3)?; - (0..len) - .map(|_| Expression::arbitrary_rec(u, new_budget)) - .collect::>>() - .map(S::Array) - } - 15 => { - let len = u.int_in_range(0..=3)?; - let elements = (0..len) - .map(|_| Expression::arbitrary_rec(u, new_budget)) - .collect::>>()?; - Ok(S::List(elements)) - } - _ => unreachable!(), - }, - }?; - Ok(Self { - inner, - span: Span::DUMMY, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl crate::ArbitraryRec for Call { - fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { - use arbitrary::Arbitrary; - - let name = CallName::arbitrary(u)?; - let len = u.int_in_range(0..=3)?; - let args = (0..len) - .map(|_| Expression::arbitrary_rec(u, budget)) - .collect::>>()?; - Ok(Self { - name, - args, - span: Span::DUMMY, - }) - } -} - -#[cfg(feature = "arbitrary")] -impl crate::ArbitraryRec for Match { - fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { - use arbitrary::Arbitrary; - - let scrutinee = Expression::arbitrary_rec(u, budget).map(Arc::new)?; - let (pat_l, pat_r) = match u.int_in_range(0..=2)? { - 0 => { - let id_l = Pattern::arbitrary(u)?; - let ty_l = AliasedType::arbitrary(u)?; - let pat_l = MatchPattern::Left(id_l, ty_l); - let id_r = Pattern::arbitrary(u)?; - let ty_r = AliasedType::arbitrary(u)?; - let pat_r = MatchPattern::Right(id_r, ty_r); - (pat_l, pat_r) - } - 1 => { - let id_r = Pattern::arbitrary(u)?; - let ty_r = AliasedType::arbitrary(u)?; - let pat_r = MatchPattern::Some(id_r, ty_r); - (MatchPattern::None, pat_r) - } - 2 => (MatchPattern::False, MatchPattern::True), - _ => unreachable!(), - }; - let expr_l = Expression::arbitrary_rec(u, budget).map(Arc::new)?; - let expr_r = Expression::arbitrary_rec(u, budget).map(Arc::new)?; - Ok(Self { - scrutinee, - left: MatchArm { - pattern: pat_l, - expression: expr_l, - span: Span::DUMMY, - }, - right: MatchArm { - pattern: pat_r, - expression: expr_r, - span: Span::DUMMY, - }, - span: Span::DUMMY, - }) - } -} - -#[cfg(test)] -mod type_alias { - use super::*; - - #[test] - fn test_reject_redefined_builtin_type() { - let ty = TypeAlias::parse_from_str("type Ctx8 = u32") - .expect_err("Redefining built-in alias should be rejected"); - - assert!(ty - .error() - .to_string() - .contains("Type alias `Ctx8` is already exists as built-in alias")); - } - - #[test] - fn test_fragment_rejects_version_directive() { - let err = TypeAlias::parse_from_str("simc \"*\"; type Ctx8 = u32") - .expect_err("a version directive must not be accepted in a fragment"); - - assert!(matches!(err.error(), Error::ReservedSimcKeyword)); - } -} - -#[cfg(test)] -mod regular_parsing { - use super::*; - use crate::parse; - - impl UseDecl { - /// Creates a dummy `UseDecl` specifically for testing `DependencyMap` resolution. - pub fn dummy_path(path: Vec) -> Self { - Self { - visibility: Visibility::default(), - path, - items: UseItems::List(Vec::new()), - span: Span::DUMMY, - } - } - } - - #[test] - fn test_double_colon() { - let input = "fn main() { let ab: u8 = <(u4, u4)> : :into((0b1011, 0b1101)); }"; - let mut diagnostics = DiagnosticManager::new(); - - let parsed_program = Program::parse_from_str_with_errors( - MAIN_MODULE, - input, - &UnstableFeatures::all(), - &mut diagnostics, - ); - - assert!(parsed_program.is_none()); - assert!(diagnostics.to_string().contains("Expected '::', found ':'")); - } - - #[test] - fn test_double_double_colon() { - let input = "fn main() { let pk: Pubkey = witnes::::PK; }"; - let mut diagnostics = DiagnosticManager::new(); - - let parsed_program = Program::parse_from_str_with_errors( - MAIN_MODULE, - input, - &UnstableFeatures::all(), - &mut diagnostics, - ); - - assert!(parsed_program.is_none()); - assert!( - diagnostics - .to_string() - .contains("Expected identifier, found ::"), - "the second :: should be reported as the error site" - ); - } - - /// Parse `input` and return whether it was rejected and the collected error text. - fn parse_with(input: &str, features: &UnstableFeatures) -> (bool, String) { - let mut diagnostics = DiagnosticManager::new(); - let program = - Program::parse_from_str_with_errors(MAIN_MODULE, input, features, &mut diagnostics); - - let rejected = program.is_none(); - let text = diagnostics.to_string(); - - (rejected, text) - } - - /// Nested blocks must parse in time linear in their depth. - #[test] - fn nested_blocks_parse_without_exponential_backtracking() { - let depth = 32; - let input = format!( - "fn main() {{\n let x: u32 = {}0{};\n assert!(jet::eq_32(x, 0));\n}}", - "{".repeat(depth), - "}".repeat(depth) - ); - - let (rejected, text) = parse_with(&input, &UnstableFeatures::all()); - assert!(!rejected, "nested blocks must parse: {text}"); - } - - #[test] - fn inverted_empty_span_from_token_gap_does_not_panic() { - // Fuzz-found (compile_text). - // - // The input is irreducible. The broken `fn` prefix forces the parser into delimiter recovery. - // The only path that requests an empty span at a lex-error gap and the - // NUL after `enum` creates that gap. - // Chumsky builds such spans as `next_token.start .. previous_token.end`, which is inverted - // across the gap and panicked the strict `Span` constructor. - // - // Simplifying any part (even NUL to a space) loses the crash. - let src = "fn`u({?\u{12}$0;;enum\0===lHf\u{15}"; - let mut diagnostics = DiagnosticManager::new(); - let program = Program::parse_from_str_with_errors( - MAIN_MODULE, - src, - &UnstableFeatures::all(), - &mut diagnostics, - ); - - assert!(program.is_none(), "garbage input must be rejected"); - assert!(diagnostics.has_errors()); - } - - #[test] - fn recovery_synchronizes_at_enum_declaration() { - // Discriminating setup: an invalid prefix followed by a malformed - // enum. Without `Token::Enum` in the synchronization set the whole - // enum is swallowed into the prefix's recovery span and only one - // error is reported; with it, the enum parses as its own item and - // its malformation is reported as a second error. - let mut diagnostics = DiagnosticManager::new(); - let program = Program::parse_from_str_with_errors( - MAIN_MODULE, - "let invalid = 1;\nenum Action { A B }\nfn main() {}", - &UnstableFeatures::all(), - &mut diagnostics, - ); - assert!(program.is_none(), "the invalid program must be rejected"); - assert_eq!( - 2, - diagnostics.error_count(), - "the invalid prefix and the malformed enum must each report an error" - ); - } - - #[test] - fn recovery_after_malformed_enum_reaches_next_item() { - let (rejected, _text) = parse_with( - "enum Action { A B }\nfn main() {}", - &UnstableFeatures::all(), - ); - assert!(rejected, "a malformed enum must fail compilation"); - } - - #[test] - fn malformed_construct_containing_enum_keyword_reports_errors() { - // `enum` inside a badly malformed nested construct may be mistaken - // for an item boundary; compilation must still fail cleanly. - let (rejected, _text) = parse_with( - "fn broken() { let x = (enum; }\nfn main() {}", - &UnstableFeatures::all(), - ); - assert!(rejected, "a malformed construct must fail compilation"); - } - - #[test] - fn test_gated_syntax_is_rejected_without_features() { - // Real `use`/`mod` syntax: rejected under none() (naming the feature + a - // -Z hint), accepted under all(). Delete when `imports` stabilizes. - for input in [ - "use crate::foo::bar;\nfn main() { }", - "mod inner { }\nfn main() { }", - ] { - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!( - rejected, - "gated syntax must be rejected without features (if this feature was \ - just stabilized, delete this test):\n{input}" - ); - assert!( - error.contains("imports") && error.contains("-Z"), - "rejection should name the feature and suggest -Z, got:\n{error}" - ); - - let (rejected, error) = parse_with(input, &UnstableFeatures::all()); - assert!( - !rejected, - "the same syntax must parse with all features enabled:\n{error}" - ); - } - } - - #[test] - fn test_type_heavy_program_needs_no_features() { - // Types are traversed by `RequireFeature` but not gated, so a type-heavy, - // import-free program must parse with no features (no false-positive gates). - let input = r#"type Alias = u32; -fn pick(e: Either) -> u32 { - match e { - Left(a: u32) => a, - Right(b: u32) => b, - } -} -fn main() { - let casted: u32 = <(u16, u16)>::into((0xbeef, 0xbabe)); - let chosen: Alias = pick(Left(casted)); - assert!(jet::eq_32(chosen, chosen)); -} -"#; - // `parse_from_str_with_errors` returns `Some` only when no errors were - // collected, so a non-rejection already proves there were no gate errors. - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!( - !rejected, - "type-heavy but import-free program should parse with no features:\n{error}" - ); - } - - #[test] - fn test_syntax_error_does_not_produce_spurious_feature_gate_error() { - // The gate check skips error-recovered ASTs, so a program with - // both a syntax error and gated syntax reports only the syntax error. - let input = "use crate::foo;\nfn main( {"; - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!(rejected, "broken program must be rejected"); - assert!( - !error.contains("-Z"), - "syntax error should not produce a spurious feature-gate hint, got:\n{error}" - ); - } - - #[test] - fn test_lexer_error_does_not_produce_spurious_feature_gate_error() { - // Lexer-side companion to the case above: the stray `@` lexes with an - // error but recovers a cleanly-parsing `use` AST, and a program that - // didn't lex cleanly skips the gate check — so no spurious `-Z` hint. - let input = "use crate@::foo;\nfn main() { }"; - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!(rejected, "program with a lexer error must be rejected"); - assert!( - !error.contains("-Z"), - "lexer error should not produce a spurious feature-gate hint, got:\n{error}" - ); - } - - #[test] - fn test_use_decl_display_round_trips_with_aliases() { - for input in [ - "use lib::A::foo;", - "use lib::A::foo as bar;", - "use lib::A::{foo, bar as baz};", - ] { - let program = parse::Program::parse_from_str(input).expect("parsing works"); - assert_eq!(program.to_string(), format!("{input}\n")); - } - } - - #[test] - fn statement_spans_exclude_terminating_semicolons() { - let input = "fn main() { let value: u8 = 0; value; }"; - let program = Program::parse_from_str(input).expect("program parses"); - - let Item::Function(function) = &program.items()[0] else { - panic!("expected a function item"); - }; - let ExpressionInner::Block(statements, None) = function.body().inner() else { - panic!("expected a block containing only statements"); - }; - - assert_eq!( - statements[0].span().to_slice(input), - Some("let value: u8 = 0") - ); - assert_eq!(statements[1].span().to_slice(input), Some("value")); - } - - fn parse_item(input: &str) -> Item { - let program = parse::Program::parse_from_str(input).expect("parsing should succeed"); - program.items().first().expect("expected one item").clone() - } - - #[test] - fn test_enum_declaration_basic() { - let item = parse_item("enum Path { Inherit, ColdSpend, RefreshSpend, }"); - let Item::EnumDeclaration(decl) = item else { - panic!("expected EnumDeclaration, got {item:?}"); - }; - assert_eq!(decl.name(), "Path"); - assert_eq!(decl.variants().len(), 3); - assert_eq!(decl.variants()[0].name(), "Inherit"); - assert_eq!(decl.variants()[2].name(), "RefreshSpend"); - } - - #[test] - fn test_enum_declaration_pub() { - let item = parse_item("pub enum Color { Red, Green, Blue, }"); - let Item::EnumDeclaration(decl) = item else { - panic!("expected EnumDeclaration"); - }; - assert_eq!(decl.visibility(), &Visibility::Public); - assert_eq!(decl.name(), "Color"); - } - - #[test] - fn test_enum_declaration_display_round_trip() { - let input = "enum Path { Inherit, ColdSpend, RefreshSpend, }"; - let item = parse_item(input); - let Item::EnumDeclaration(decl) = item else { - panic!("expected EnumDeclaration"); - }; - assert_eq!( - decl.to_string(), - "enum Path { Inherit, ColdSpend, RefreshSpend, }" - ); - } - - #[test] - fn test_enum_declaration_reserved_name() { - for reserved in RESERVED_PATTERN_NAMES { - let result = Program::parse_from_str(&format!("enum {reserved} {{ A, B, }}")); - let error = result.expect_err(&format!("enum name {reserved} is reserved")); - assert!( - error.to_string().contains("reserved"), - "error should say the name is reserved: {error}" - ); - } - } -} - -#[cfg(test)] -#[cfg(feature = "fmt")] -mod fmt_parsing { - use super::*; - - /// Parse `input`, appending any formatter diagnostics to `diagnostics`. - fn parse_with_diagnostics<'src>( - input: &'src str, - features: &UnstableFeatures, - diagnostics: &mut DiagnosticManager, - ) -> Option> { - Program::parse_with_errors_for_fmt(MAIN_MODULE, input, features, diagnostics) - } - - /// Parse `input` and return whether it was rejected and the collected error text. - fn parse_with(input: &str, features: &UnstableFeatures) -> (bool, String) { - let mut diagnostics = DiagnosticManager::new(); - let program = parse_with_diagnostics(input, features, &mut diagnostics); - - let rejected = program.is_none(); - let text = diagnostics.to_string(); - - (rejected, text) - } - - /// Parse `input` and return whether it was rejected and the collected error in `DiagnosticManager`. - fn parse_with_diagnosis(input: &str, features: &UnstableFeatures) -> (bool, DiagnosticManager) { - let mut diagnostics = DiagnosticManager::new(); - let program = parse_with_diagnostics(input, features, &mut diagnostics); - - let rejected = program.is_none(); - - (rejected, diagnostics) - } - - /// Parse `input` and return whether it was rejected and the collected error text together with `ParsedSource`. - fn parse_with_extended_out<'src>( - input: &'src str, - features: &UnstableFeatures, - ) -> (bool, String, Option>) { - let mut diagnostics = DiagnosticManager::new(); - let program = parse_with_diagnostics(input, features, &mut diagnostics); - - let rejected = program.is_none(); - let text = diagnostics.to_string(); - - (rejected, text, program) - } - - fn assert_formatter_matches_regular_parser(input: &str, features: &UnstableFeatures) { - let mut regular_diagnostics = DiagnosticManager::new(); - let regular = Program::parse_from_str_with_errors( - MAIN_MODULE, - input, - features, - &mut regular_diagnostics, - ); - - let mut formatting_diagnostics = DiagnosticManager::new(); - let formatting = parse_with_diagnostics(input, features, &mut formatting_diagnostics); - - assert_eq!( - formatting.as_ref().map(ParsedSource::program), - regular.as_ref(), - "formatter and regular parsers must produce the same AST for {input:?}" - ); - assert_eq!( - formatting_diagnostics.error_count(), - regular_diagnostics.error_count(), - "formatter and regular parsers must report the same number of errors for {input:?}, \n\ - fmt errors: \n\t '{formatting_diagnostics}, \n regular errors: \n\t '{regular_diagnostics}" - ); - } - - fn assert_lossless_source(input: &str, parsed: &ParsedSource<'_>) { - let prefix = parsed.prefix(); - assert_eq!(prefix.file_id, MAIN_MODULE); - - let mut reconstructed = prefix - .to_slice(input) - .expect("the directive prefix must be a valid source slice") - .to_owned(); - let mut cursor = prefix.end; - - for (_, span) in parsed.tokens() { - assert_eq!(span.file_id, MAIN_MODULE); - assert!( - span.start >= prefix.end, - "formatter tokens must not overlap the preserved directive prefix" - ); - assert_eq!( - span.start, cursor, - "formatter token spans must be contiguous and in source order" - ); - - let text = span - .to_slice(input) - .expect("formatter token spans must be valid source slices"); - assert!( - !text.is_empty(), - "the formatter token stream must not contain empty source slices" - ); - reconstructed.push_str(text); - cursor = span.end; - } - - assert_eq!(cursor, input.len(), "formatter tokens must reach EOF"); - assert_eq!(reconstructed, input, "formatter input must be lossless"); - } - - #[test] - fn test_double_colon() { - let input = "fn main() { let ab: u8 = <(u4, u4)> : :into((0b1011, 0b1101)); }"; - - let (rejected, text) = parse_with(input, &UnstableFeatures::all()); - - assert!(rejected); - assert!(text.contains("Expected '::', found ':'")); - } - - #[test] - fn test_double_double_colon() { - let input = "fn main() { let pk: Pubkey = witnes::::PK; }"; - - let (rejected, text) = parse_with(input, &UnstableFeatures::all()); - - assert!(rejected); - assert!( - text.contains("Expected identifier, found ::"), - "the second :: should be reported as the error site" - ); - } - - #[test] - fn inverted_empty_span_from_token_gap_does_not_panic() { - // Fuzz-found (compile_text). - // - // The input is irreducible. The broken `fn` prefix forces the parser into delimiter recovery. - // The only path that requests an empty span at a lex-error gap and the - // NUL after `enum` creates that gap. - // Chumsky builds such spans as `next_token.start .. previous_token.end`, which is inverted - // across the gap and panicked the strict `Span` constructor. - // - // Simplifying any part (even NUL to a space) loses the crash. - let src = "fn`u({?\u{12}$0;;enum\0===lHf\u{15}"; - let (rejected, text) = parse_with(src, &UnstableFeatures::all()); - - assert!(rejected, "garbage input must be rejected"); - assert!(!text.is_empty()); - } - - #[test] - fn recovery_synchronizes_at_enum_declaration() { - // Discriminating setup: an invalid prefix followed by a malformed - // enum. Without `Token::Enum` in the synchronization set the whole - // enum is swallowed into the prefix's recovery span and only one - // error is reported; with it, the enum parses as its own item and - // its malformation is reported as a second error. - - let src = "let invalid = 1;\nenum Action { A B }\nfn main() {}"; - let (rejected, diagnostics) = parse_with_diagnosis(src, &UnstableFeatures::all()); - - assert!(rejected, "the invalid program must be rejected"); - assert_eq!( - 2, - diagnostics.error_count(), - "the invalid prefix and the malformed enum must each report an error" - ); - } - - #[test] - fn recovery_after_malformed_enum_reaches_next_item() { - let src = "enum Action { A B }\nfn main() {}"; - let (rejected, _text) = parse_with(src, &UnstableFeatures::all()); - assert!(rejected, "a malformed enum must fail compilation"); - } - - #[test] - fn malformed_construct_containing_enum_keyword_reports_errors() { - // `enum` inside a badly malformed nested construct may be mistaken - // for an item boundary; compilation must still fail cleanly. - let src = "fn broken() { let x = (enum; }\nfn main() {}"; - let (rejected, _text) = parse_with(src, &UnstableFeatures::all()); - assert!(rejected, "a malformed construct must fail compilation"); - } - - #[test] - fn test_gated_syntax_is_rejected_without_features() { - // Real `use`/`mod` syntax: rejected under none() (naming the feature + a - // -Z hint), accepted under all(). Delete when `imports` stabilizes. - for input in [ - "use crate::foo::bar;\nfn main() { }", - "mod inner { }\nfn main() { }", - ] { - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!( - rejected, - "gated syntax must be rejected without features (if this feature was \ - just stabilized, delete this test):\n{input}" - ); - assert!( - error.contains("imports") && error.contains("-Z"), - "rejection should name the feature and suggest -Z, got:\n{error}" - ); - - let (rejected, error) = parse_with(input, &UnstableFeatures::all()); - assert!( - !rejected, - "the same syntax must parse with all features enabled:\n{error}" - ); - } - } - - #[test] - fn test_type_heavy_program_needs_no_features() { - // Types are traversed by `RequireFeature` but not gated, so a type-heavy, - // import-free program must parse with no features (no false-positive gates). - let input = r#"type Alias = u32; -fn pick(e: Either) -> u32 { - match e { - Left(a: u32) => a, - Right(b: u32) => b, - } -} -fn main() { - let casted: u32 = <(u16, u16)>::into((0xbeef, 0xbabe)); - let chosen: Alias = pick(Left(casted)); - assert!(jet::eq_32(chosen, chosen)); -} -"#; - // `parse_from_str_with_errors` returns `Some` only when no errors were - // collected, so a non-rejection already proves there were no gate errors. - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!( - !rejected, - "type-heavy but import-free program should parse with no features:\n{error}" - ); - } - - #[test] - fn test_syntax_error_does_not_produce_spurious_feature_gate_error() { - // The gate check skips error-recovered ASTs, so a program with - // both a syntax error and gated syntax reports only the syntax error. - let input = "use crate::foo;\nfn main( {"; - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!(rejected, "broken program must be rejected"); - assert!( - !error.contains("-Z"), - "syntax error should not produce a spurious feature-gate hint, got:\n{error}" - ); - } - - #[test] - fn test_lexer_error_does_not_produce_spurious_feature_gate_error() { - // Lexer-side companion to the case above: the stray `@` lexes with an - // error but recovers a cleanly-parsing `use` AST, and a program that - // didn't lex cleanly skips the gate check — so no spurious `-Z` hint. - let input = "use crate@::foo;\nfn main() { }"; - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - assert!(rejected, "program with a lexer error must be rejected"); - assert!( - !error.contains("-Z"), - "lexer error should not produce a spurious feature-gate hint, got:\n{error}" - ); - } - - #[test] - fn test_use_decl_display_round_trips_with_aliases() { - for input in [ - "use lib::A::foo;", - "use lib::A::foo as bar;", - "use lib::A::{foo, bar as baz};", - ] { - let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::all()); - assert!(!rejected, "parsing works"); - assert_eq!(program.unwrap().program.to_string(), format!("{input}\n")); - } - } - - #[test] - fn statement_spans_exclude_terminating_semicolons() { - let input = "fn main() { let value: u8 = 0; value; }"; - - let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::none()); - assert!(!rejected, "parsing works"); - let program = program.unwrap().program; - - let Item::Function(function) = &program.items()[0] else { - panic!("expected a function item"); - }; - let ExpressionInner::Block(statements, None) = function.body().inner() else { - panic!("expected a block containing only statements"); - }; - - assert_eq!( - statements[0].span().to_slice(input), - Some("let value: u8 = 0") - ); - assert_eq!(statements[1].span().to_slice(input), Some("value")); - } - - fn parse_item(input: &str) -> Item { - let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::all()); - assert!(!rejected, "parsing works"); - let program = program.unwrap().program; - - program.items().first().expect("expected one item").clone() - } - - #[test] - fn test_enum_declaration_basic() { - let item = parse_item("enum Path { Inherit, ColdSpend, RefreshSpend, }"); - let Item::EnumDeclaration(decl) = item else { - panic!("expected EnumDeclaration, got {item:?}"); - }; - assert_eq!(decl.name(), "Path"); - assert_eq!(decl.variants().len(), 3); - assert_eq!(decl.variants()[0].name(), "Inherit"); - assert_eq!(decl.variants()[2].name(), "RefreshSpend"); - } - - #[test] - fn test_enum_declaration_pub() { - let item = parse_item("pub enum Color { Red, Green, Blue, }"); - let Item::EnumDeclaration(decl) = item else { - panic!("expected EnumDeclaration"); - }; - assert_eq!(decl.visibility(), &Visibility::Public); - assert_eq!(decl.name(), "Color"); - } - - #[test] - fn test_enum_declaration_display_round_trip() { - let input = "enum Path { Inherit, ColdSpend, RefreshSpend, }"; - let item = parse_item(input); - let Item::EnumDeclaration(decl) = item else { - panic!("expected EnumDeclaration"); - }; - assert_eq!( - decl.to_string(), - "enum Path { Inherit, ColdSpend, RefreshSpend, }" - ); - } - - #[test] - fn test_enum_declaration_reserved_name() { - for reserved in RESERVED_PATTERN_NAMES { - let input = format!("enum {reserved} {{ A, B, }}"); - let (rejected, error) = parse_with(&input, &UnstableFeatures::none()); - assert!(rejected, "enum name {reserved} is reserved"); - assert!( - error.contains("reserved"), - "error should say the name is reserved: {error}" - ); - } - } - - #[test] - fn formatting_parse_is_lossless_after_directive_prescan() { - let input = "// header\r\nsimc \"*\";\r\n\t/* block */ fn main() { // body\r\n\t}\r"; - - let (_rejected, _error, parsed) = parse_with_extended_out(input, &UnstableFeatures::none()); - let parsed = parsed.expect("formatting parse should succeed"); - - assert_eq!( - parsed.prefix().to_slice(input), - Some("// header\r\nsimc \"*\";") - ); - for trivia in [ - crate::lexer::TriviaKind::BlockComment, - crate::lexer::TriviaKind::LineComment, - crate::lexer::TriviaKind::Newline, - crate::lexer::TriviaKind::Whitespace, - ] { - assert!( - parsed.tokens().iter().any( - |(token, _)| matches!(token, FmtToken::Trivia(token_trivia) if token_trivia.kind() == trivia) - ), - "formatter token stream should retain {trivia:?}" - ); - } - assert_lossless_source(input, &parsed); - assert_eq!(parsed.program().items().len(), 1); - } - - #[test] - fn formatting_parse_matches_regular_parser_pipeline() { - let no_features = UnstableFeatures::none(); - let all_features = UnstableFeatures::all(); - - for (input, features) in [ - ( - "/* header */\r\nfn\tmain() { // comment\r\n}\r", - &no_features, - ), - ("// version\r\nsimc \"*\";\r\nfn main() {}", &no_features), - ("use crate::foo::bar;\nfn main() {}", &no_features), - ("use crate::foo::bar;\nfn main() {}", &all_features), - ("fn main( {", &no_features), - ("fn main() { @// comment }", &no_features), - ] { - assert_formatter_matches_regular_parser(input, features); - } - } - - #[test] - fn formatting_parse_ignores_preexisting_errors() { - let input = "fn main() {}"; - let mut diagnostics = DiagnosticManager::new(); - diagnostics.push(Diagnostic::global(Error::CannotParse { - msg: "an error from an earlier source file".to_owned(), - })); - let before = diagnostics.error_count(); - - let parsed = parse_with_diagnostics(input, &UnstableFeatures::none(), &mut diagnostics); - - assert!( - parsed.is_some(), - "valid source must still produce ParsedSource" - ); - assert_eq!(diagnostics.error_count(), before); - assert_eq!(diagnostics.diagnostics().len(), before); - } - - #[test] - fn formatting_parse_stops_after_invalid_directive() { - for (input, is_malformed) in [ - ("simc \"*\"\nfn broken( @", true), - ("simc \">99.0.0\"; @", false), - ] { - let (rejected, diagnostics) = parse_with_diagnosis(input, &UnstableFeatures::none()); - - assert!(rejected, "invalid directive must abort formatting parse"); - assert_eq!( - diagnostics.error_count(), - 1, - "the invalid body must not add diagnostics after prescan aborts" - ); - if is_malformed { - assert!(matches!( - diagnostics.diagnostics()[0].error(), - Error::MalformedSimcDirective - )); - } else { - assert!(matches!( - diagnostics.diagnostics()[0].error(), - Error::SimcVersionMismatch { .. } - )); - } - } - } - - #[test] - fn formatting_parse_reports_missing_match_arm_comma() { - let input = "fn main() { match true { false => () true => (), } }"; - - let (rejected, error) = parse_with(input, &UnstableFeatures::none()); - - assert!(rejected); - assert!( - error.contains("Missing ',' after a match arm that isn't block expression"), - "expected the missing-comma diagnostic, got {error:?}" - ); - } -} diff --git a/src/parse/arbitrary_impls.rs b/src/parse/arbitrary_impls.rs new file mode 100644 index 00000000..4b7ae009 --- /dev/null +++ b/src/parse/arbitrary_impls.rs @@ -0,0 +1,374 @@ +use std::sync::Arc; + +use either::Either; + +use crate::error::Span; +use crate::pattern::Pattern; +use crate::str::{AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, ModuleName}; +use crate::types::AliasedType; +use crate::TemplateProgramWitness; + +use super::{ + Assignment, Call, CallName, EnumDeclaration, EnumVariant, Expression, ExpressionInner, + Function, FunctionParam, Item, Match, MatchArm, MatchPattern, Module, Program, + SingleExpression, SingleExpressionInner, Statement, TypeAlias, UseDecl, UseItems, Visibility, +}; + +impl<'a> arbitrary::Arbitrary<'a> for UseDecl { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let visibility = Visibility::arbitrary(u)?; + let path_len = u.int_in_range(2..=4)?; + let path = (0..path_len) + .map(|_| Identifier::arbitrary(u)) + .collect::>()?; + let items = UseItems::arbitrary(u)?; + + Ok(Self { + visibility, + path, + items, + span: Span::DUMMY, + }) + } +} + +impl<'a> arbitrary::Arbitrary<'a> for EnumVariant { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let name = Identifier::arbitrary(u)?; + let len = u.int_in_range(0..=3)?; + let payload = (0..len) + .map(|_| AliasedType::arbitrary(u)) + .collect::>>()?; + Ok(Self { + name, + payload, + span: Span::DUMMY, + }) + } +} + +impl<'a> arbitrary::Arbitrary<'a> for EnumDeclaration { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let visibility = Visibility::arbitrary(u)?; + // `AliasName::arbitrary` already dodges every reserved alias name, so a generated declaration always re-parses + let name = AliasName::arbitrary(u)?; + let len = u.int_in_range(2..=8)?; + let variants = (0..len) + .map(|_| EnumVariant::arbitrary(u)) + .collect::>>()?; + Ok(Self { + visibility, + name, + variants, + span: Span::DUMMY, + }) + } +} + +fn generate_arbitrary_items<'a>( + u: &mut arbitrary::Unstructured<'a>, +) -> arbitrary::Result> { + let mut items_vec = Vec::new(); + + let len = u.int_in_range(0..=2)?; + for _ in 0..len { + items_vec.push(::arbitrary(u)?); + } + + Ok(items_vec) +} + +impl<'a> arbitrary::Arbitrary<'a> for Program { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let mut items_vec = generate_arbitrary_items(u)?; + + // Enum declarations are valid only at the top level of a file, so + // they are generated here and not in `generate_arbitrary_items`, + // which `Module::arbitrary` reuses for nested items: a nested enum + // would display to source that no longer parses. + // + // TODO(enums): declarations alone leave construction, matching, + // payload binding, and compilation unreached — the expression + // generator does not produce `EnumConstruction`/`EnumMatch` + // correlated with the declared enums. Generate a coherent + // declaration plus an expression using one of its variants, and + // consider a two-stage serde target (serialize -> deserialize as + // `UnresolvedValues` -> resolve against the declared types) to + // cover enum witness serialization. + for _ in 0..u.int_in_range(0..=2u8)? { + items_vec.push(Item::EnumDeclaration(EnumDeclaration::arbitrary(u)?)); + } + + // Three equally-likely modes for how `fn main()` is injected: + // 0 — no explicit main (arbitrary items only) + // 1 — main with arbitrary params and return type + // 2 — main with no params and no return type (closest to valid) + match u.int_in_range(0..=2u8)? { + 0 => {} + 1 => { + let mut main_fn = ::arbitrary_rec(u, 3)?; + main_fn.name = FunctionName::main(); + items_vec.push(Item::Function(main_fn)); + } + _ => { + let mut main_fn = ::arbitrary_rec(u, 3)?; + main_fn.name = FunctionName::main(); + main_fn.params = Arc::from([]); + main_fn.ret = None; + items_vec.push(Item::Function(main_fn)); + } + } + + let items: Arc<[Item]> = items_vec.into(); + Ok(Self { + items, + span: Span::DUMMY, + }) + } +} + +impl<'a> arbitrary::Arbitrary<'a> for Item { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + match u.int_in_range(0..=3)? { + 0 => Ok(Item::TypeAlias(TypeAlias::arbitrary(u)?)), + 1 => Ok(Item::Function(Function::arbitrary(u)?)), + 2 => Ok(Item::Use(UseDecl::arbitrary(u)?)), + 3 => Ok(Item::Module(Module::arbitrary(u)?)), + _ => unreachable!(), + } + } +} + +impl<'a> arbitrary::Arbitrary<'a> for Function { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + ::arbitrary_rec(u, 3) + } +} + +impl crate::ArbitraryRec for Function { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + let visibility = Visibility::arbitrary(u)?; + let name = FunctionName::arbitrary(u)?; + let len = u.int_in_range(0..=6)?; + let params = (0..len) + .map(|_| FunctionParam::arbitrary(u)) + .collect::>>()?; + let ret = Option::::arbitrary(u)?; + let body = Expression::arbitrary_rec(u, budget).map(Expression::into_block)?; + Ok(Self { + visibility, + name, + params, + ret, + body, + span: Span::DUMMY, + }) + } +} + +impl<'a> arbitrary::Arbitrary<'a> for Module { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let visibility = Visibility::arbitrary(u)?; + let name = ModuleName::arbitrary(u)?; + let items_vec = generate_arbitrary_items(u)?; + + Ok(Self { + visibility, + name, + items: items_vec.into(), + span: Span::DUMMY, + }) + } +} + +impl crate::ArbitraryRec for Expression { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + let inner = match budget.checked_sub(1) { + None => SingleExpression::arbitrary_rec(u, budget).map(ExpressionInner::Single), + Some(new_budget) => match bool::arbitrary(u)? { + false => SingleExpression::arbitrary_rec(u, budget).map(ExpressionInner::Single), + true => { + let len = u.int_in_range(0..=3)?; + let statements = (0..len) + .map(|_| Statement::arbitrary_rec(u, new_budget)) + .collect::>>()?; + let maybe_single = match bool::arbitrary(u)? { + false => None, + true => Expression::arbitrary_rec(u, new_budget) + .map(Arc::new) + .map(Some)?, + }; + Ok(ExpressionInner::Block(statements, maybe_single)) + } + }, + }?; + Ok(Self { + inner, + span: Span::DUMMY, + }) + } +} + +impl crate::ArbitraryRec for Statement { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + match bool::arbitrary(u)? { + false => Assignment::arbitrary_rec(u, budget).map(Self::Assignment), + true => Expression::arbitrary_rec(u, budget).map(Self::Expression), + } + } +} + +impl crate::ArbitraryRec for Assignment { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + let pattern = Pattern::arbitrary(u)?; + let ty = AliasedType::arbitrary(u)?; + let expression = Expression::arbitrary_rec(u, budget)?; + + Ok(Self { + pattern, + ty, + expression, + span: Span::DUMMY, + }) + } +} + +impl crate::ArbitraryRec for SingleExpression { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + use SingleExpressionInner as S; + + let inner = match budget.checked_sub(1) { + None => match u.int_in_range(0..=6)? { + 0 => bool::arbitrary(u).map(S::Boolean), + 1 => Binary::arbitrary(u).map(S::Binary), + 2 => Decimal::arbitrary(u).map(S::Decimal), + 3 => Hexadecimal::arbitrary(u).map(S::Hexadecimal), + 4 => Identifier::arbitrary(u).map(S::Variable), + 5 => TemplateProgramWitness::arbitrary(u).map(S::Witness), + 6 => Ok(S::Option(None)), + _ => unreachable!(), + }, + Some(new_budget) => match u.int_in_range(0..=15)? { + 0 => bool::arbitrary(u).map(S::Boolean), + 1 => Binary::arbitrary(u).map(S::Binary), + 2 => Decimal::arbitrary(u).map(S::Decimal), + 3 => Hexadecimal::arbitrary(u).map(S::Hexadecimal), + 4 => Identifier::arbitrary(u).map(S::Variable), + 5 => TemplateProgramWitness::arbitrary(u).map(S::Witness), + 6 => Ok(S::Option(None)), + 7 => Expression::arbitrary_rec(u, new_budget) + .map(Arc::new) + .map(Some) + .map(S::Option), + 8 => Expression::arbitrary_rec(u, new_budget) + .map(Arc::new) + .map(Either::Left) + .map(S::Either), + 9 => Expression::arbitrary_rec(u, new_budget) + .map(Arc::new) + .map(Either::Right) + .map(S::Either), + 10 => Expression::arbitrary_rec(u, new_budget) + .map(Arc::new) + .map(S::Expression), + 11 => Call::arbitrary_rec(u, new_budget).map(S::Call), + 12 => Match::arbitrary_rec(u, new_budget).map(S::Match), + 13 => { + let len = u.int_in_range(0..=3)?; + (0..len) + .map(|_| Expression::arbitrary_rec(u, new_budget)) + .collect::>>() + .map(S::Tuple) + } + 14 => { + let len = u.int_in_range(0..=3)?; + (0..len) + .map(|_| Expression::arbitrary_rec(u, new_budget)) + .collect::>>() + .map(S::Array) + } + 15 => { + let len = u.int_in_range(0..=3)?; + let elements = (0..len) + .map(|_| Expression::arbitrary_rec(u, new_budget)) + .collect::>>()?; + Ok(S::List(elements)) + } + _ => unreachable!(), + }, + }?; + Ok(Self { + inner, + span: Span::DUMMY, + }) + } +} + +impl crate::ArbitraryRec for Call { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + let name = CallName::arbitrary(u)?; + let len = u.int_in_range(0..=3)?; + let args = (0..len) + .map(|_| Expression::arbitrary_rec(u, budget)) + .collect::>>()?; + Ok(Self { + name, + args, + span: Span::DUMMY, + }) + } +} + +impl crate::ArbitraryRec for Match { + fn arbitrary_rec(u: &mut arbitrary::Unstructured, budget: usize) -> arbitrary::Result { + use arbitrary::Arbitrary; + + let scrutinee = Expression::arbitrary_rec(u, budget).map(Arc::new)?; + let (pat_l, pat_r) = match u.int_in_range(0..=2)? { + 0 => { + let id_l = Pattern::arbitrary(u)?; + let ty_l = AliasedType::arbitrary(u)?; + let pat_l = MatchPattern::Left(id_l, ty_l); + let id_r = Pattern::arbitrary(u)?; + let ty_r = AliasedType::arbitrary(u)?; + let pat_r = MatchPattern::Right(id_r, ty_r); + (pat_l, pat_r) + } + 1 => { + let id_r = Pattern::arbitrary(u)?; + let ty_r = AliasedType::arbitrary(u)?; + let pat_r = MatchPattern::Some(id_r, ty_r); + (MatchPattern::None, pat_r) + } + 2 => (MatchPattern::False, MatchPattern::True), + _ => unreachable!(), + }; + let expr_l = Expression::arbitrary_rec(u, budget).map(Arc::new)?; + let expr_r = Expression::arbitrary_rec(u, budget).map(Arc::new)?; + Ok(Self { + scrutinee, + left: MatchArm { + pattern: pat_l, + expression: expr_l, + span: Span::DUMMY, + }, + right: MatchArm { + pattern: pat_r, + expression: expr_r, + span: Span::DUMMY, + }, + span: Span::DUMMY, + }) + } +} diff --git a/src/parse/display.rs b/src/parse/display.rs new file mode 100644 index 00000000..2e88ddad --- /dev/null +++ b/src/parse/display.rs @@ -0,0 +1,405 @@ +use std::fmt; + +use either::Either; +use miniscript::iter::TreeLike; + +use super::{ + Assignment, Call, CallName, EnumDeclaration, EnumMatch, EnumMatchArm, ExprTree, Expression, + Function, FunctionParam, Item, Match, MatchPattern, Module, Program, SingleExpression, + SingleExpressionInner, Statement, TypeAlias, UseDecl, UseItems, Visibility, +}; + +impl fmt::Display for EnumDeclaration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}enum {} {{", self.visibility(), self.name())?; + + for variant in self.variants() { + write!(f, " {}", variant.name())?; + + if let Some((first, rest)) = variant.payload().split_first() { + write!(f, "({first}")?; + + for ty in rest { + write!(f, ", {ty}")?; + } + write!(f, ")")?; + } + write!(f, ",")?; + } + write!(f, " }}") + } +} + +/// Displays the arm's head (`Enum::Variant(bindings..)`), without the body. +impl fmt::Display for EnumMatchArm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}::{}", self.enum_path_string(), self.variant)?; + + if self.bindings.is_empty() { + return Ok(()); + } + + f.write_str("(")?; + for (i, (pattern, ty)) in self.bindings.iter().enumerate() { + if 0 < i { + f.write_str(", ")?; + } + write!(f, "{pattern}: {ty}")?; + } + f.write_str(")") + } +} + +impl fmt::Display for Program { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for item in self.items() { + writeln!(f, "{item}")?; + } + Ok(()) + } +} + +impl fmt::Display for Item { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TypeAlias(alias) => write!(f, "{alias}"), + Self::Function(function) => write!(f, "{function}"), + Self::Use(use_declaration) => write!(f, "{use_declaration}"), + Self::EnumDeclaration(decl) => write!(f, "{decl}"), + Self::Module(module) => write!(f, "{module}"), + Self::Ignored => Ok(()), + } + } +} + +impl fmt::Display for Visibility { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Public => write!(f, "pub "), + Self::Private => write!(f, ""), + } + } +} + +impl fmt::Display for TypeAlias { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}type {} = {};", + self.visibility(), + self.name(), + self.ty() + ) + } +} + +impl fmt::Display for Function { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}fn {}(", self.visibility(), self.name())?; + for (i, param) in self.params().iter().enumerate() { + if 0 < i { + write!(f, ", ")?; + } + write!(f, "{param}")?; + } + write!(f, ")")?; + if let Some(ty) = self.ret() { + write!(f, " -> {ty}")?; + } + write!(f, " {}", self.body()) + } +} + +impl fmt::Display for FunctionParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.identifier(), self.ty()) + } +} + +impl fmt::Display for UseDecl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let _ = write!(f, "{}use ", self.visibility()); + + for (i, segment) in self.path.iter().enumerate() { + if i > 0 { + write!(f, "::")?; + } + write!(f, "{}", segment)?; + } + + if !self.path.is_empty() { + write!(f, "::")?; + } + + write!(f, "{};", self.items) + } +} + +impl fmt::Display for UseItems { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UseItems::Single((ident, alias)) => { + write!(f, "{}", ident)?; + + if let Some(alias) = alias { + write!(f, " as {}", alias)?; + } + + Ok(()) + } + UseItems::List(aliased_idents) => { + let _ = write!(f, "{{"); + for (i, (ident, alias)) in aliased_idents.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", ident)?; + + if let Some(alias) = alias { + write!(f, " as {}", alias)?; + } + } + write!(f, "}}") + } + } + } +} + +impl fmt::Display for Module { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{}mod {} {{", self.visibility(), self.name())?; + + for item in self.items() { + let item_str = item.to_string(); + + for line in item_str.lines() { + writeln!(f, " {line}")?; + } + } + + write!(f, "}}") + } +} + +// TODO: Fix indentation and formatting logic. The current flat iterator approach cannot +// track AST depth, causing incorrect indentation for nested `Block` and `Match` nodes. +impl fmt::Display for ExprTree<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use SingleExpressionInner as S; + + for data in self.verbose_pre_order_iter() { + match &data.node { + Self::Statement(..) if data.is_complete => writeln!(f, ";")?, + Self::Expression(..) | Self::Statement(..) => {} + Self::Block(..) => { + if data.n_children_yielded == 0 { + writeln!(f, "{{")?; + } else if !data.is_complete { + write!(f, " ")?; + } + if data.is_complete { + writeln!(f, "}}")?; + } + } + Self::Assignment(assignment) => match data.n_children_yielded { + 0 => write!(f, "let {}: {} = ", assignment.pattern(), assignment.ty())?, + n => debug_assert_eq!(n, 1), + }, + Self::Single(single) => match single.inner() { + S::Boolean(bit) => write!(f, "{bit}")?, + S::Binary(binary) => write!(f, "0b{binary}")?, + S::Decimal(decimal) => write!(f, "{decimal}")?, + S::Hexadecimal(hexadecimal) => write!(f, "0x{hexadecimal}")?, + S::Variable(name) => write!(f, "{name}")?, + S::Witness(name) => write!(f, "witness::{name}")?, + S::Parameter(name) => write!(f, "param::{name}")?, + S::Option(None) => write!(f, "None")?, + S::Option(Some(_)) => match data.n_children_yielded { + 0 => write!(f, "Some(")?, + n => { + debug_assert_eq!(n, 1); + write!(f, ")")?; + } + }, + S::Either(Either::Left(_)) => match data.n_children_yielded { + 0 => write!(f, "Left(")?, + n => { + debug_assert_eq!(n, 1); + write!(f, ")")?; + } + }, + S::Either(Either::Right(_)) => match data.n_children_yielded { + 0 => write!(f, "Right(")?, + n => { + debug_assert_eq!(n, 1); + write!(f, ")")?; + } + }, + S::Expression(_) => match data.n_children_yielded { + 0 => write!(f, "(")?, + n => { + debug_assert_eq!(n, 1); + write!(f, ")")?; + } + }, + S::Call(..) | S::Match(..) | S::EnumMatch(..) | S::EnumConstruction(..) => {} + S::Tuple(tuple) => { + if data.n_children_yielded == 0 { + write!(f, "(")?; + } else if !data.is_complete || tuple.len() == 1 { + write!(f, ", ")?; + } + if data.is_complete { + write!(f, ")")?; + } + } + S::Array(..) => { + if data.n_children_yielded == 0 { + write!(f, "[")?; + } else if !data.is_complete { + write!(f, ", ")?; + } + if data.is_complete { + write!(f, "]")?; + } + } + S::List(..) => { + if data.n_children_yielded == 0 { + write!(f, "list![")?; + } else if !data.is_complete { + write!(f, ", ")?; + } + if data.is_complete { + write!(f, "]")?; + } + } + }, + Self::Call(call) => { + if data.n_children_yielded == 0 { + write!(f, "{}(", call.name())?; + } else if !data.is_complete { + write!(f, ", ")?; + } + if data.is_complete { + write!(f, ")")?; + } + } + Self::EnumConstruction(construction) => { + match data.n_children_yielded { + 0 => { + write!( + f, + "{}::{}", + construction.enum_path_string(), + construction.variant() + )?; + if !construction.args().is_empty() { + write!(f, "(")?; + } + } + _ if !data.is_complete => write!(f, ", ")?, + _ => {} + } + if data.is_complete && !construction.args().is_empty() { + write!(f, ")")?; + } + } + Self::Match(match_) => match data.n_children_yielded { + 0 => write!(f, "match ")?, + 1 => write!(f, "{{\n{} => ", match_.left().pattern())?, + 2 => write!(f, ",\n{} => ", match_.right().pattern())?, + n => { + debug_assert_eq!(n, 3); + write!(f, ",\n}}")?; + } + }, + Self::EnumMatch(enum_match) => match data.n_children_yielded { + 0 => write!(f, "match ")?, + 1 => write!(f, "{{\n{} => ", enum_match.arms()[0])?, + n if n <= enum_match.arms().len() => { + write!(f, ",\n{} => ", enum_match.arms()[n - 1])?; + } + _ => write!(f, ",\n}}")?, + }, + } + } + + Ok(()) + } +} + +impl fmt::Display for Expression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::Expression(self)) + } +} + +impl fmt::Display for Statement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::Statement(self)) + } +} + +impl fmt::Display for Assignment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::Assignment(self)) + } +} + +impl fmt::Display for SingleExpression { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::Single(self)) + } +} + +impl fmt::Display for Call { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::Call(self)) + } +} + +impl fmt::Display for CallName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CallName::Jet(jet) => write!(f, "jet::{jet}"), + CallName::UnwrapLeft(ty) => write!(f, "unwrap_left::<{ty}>"), + CallName::UnwrapRight(ty) => write!(f, "unwrap_right::<{ty}>"), + CallName::Unwrap => write!(f, "unwrap"), + CallName::IsNone(ty) => write!(f, "is_none::<{ty}>"), + CallName::Assert => write!(f, "assert!"), + CallName::Panic => write!(f, "panic!"), + CallName::Debug => write!(f, "dbg!"), + CallName::TypeCast(ty) => write!(f, "<{ty}>::into"), + CallName::Custom(name) => write!(f, "{name}"), + CallName::Fold(name, bound) => write!(f, "fold::<{name}, {bound}>"), + CallName::ArrayFold(name, size) => write!(f, "array_fold::<{name}, {size}>"), + CallName::ForWhile(name) => write!(f, "for_while::<{name}>"), + CallName::RawHash(ty) => write!(f, "raw_hash::<{ty}>"), + } + } +} + +impl fmt::Display for Match { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::Match(self)) + } +} + +impl fmt::Display for EnumMatch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", ExprTree::EnumMatch(self)) + } +} + +impl fmt::Display for MatchPattern { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MatchPattern::Left(i, ty) => write!(f, "Left({i}: {ty})"), + MatchPattern::Right(i, ty) => write!(f, "Right({i}: {ty})"), + MatchPattern::None => write!(f, "None"), + MatchPattern::Some(i, ty) => write!(f, "Some({i}: {ty})"), + MatchPattern::False => write!(f, "false"), + MatchPattern::True => write!(f, "true"), + } + } +} diff --git a/src/parse/grammar/call.rs b/src/parse/grammar/call.rs new file mode 100644 index 00000000..216b3616 --- /dev/null +++ b/src/parse/grammar/call.rs @@ -0,0 +1,168 @@ +use std::num::NonZeroUsize; +use std::sync::Arc; + +use chumsky::input::ValueInput; +use chumsky::prelude::{choice, just}; +use chumsky::{select, IterParser, Parser}; + +use crate::error::{Error, Span}; +use crate::lexer::Token; +use crate::num::NonZeroPow2Usize; +use crate::str::{FunctionName, JetName}; +use crate::types::AliasedType; + +use super::super::{Call, CallName, Expression}; +use super::{delimited_with_recovery, parse_token_with_recovery, ChumskyParse, ParseError}; + +impl Call { + pub(super) fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + let args = delimited_with_recovery( + expr.separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LParen, + Token::RParen, + |_| Vec::new(), + ) + .map(Arc::from) + .labelled("call arguments"); + + CallName::parser() + .then(args) + .map_with(|(name, args), e| Self { + name, + args, + span: e.span(), + }) + } +} + +impl ChumskyParse for CallName { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let turbofish_start = just(Token::DoubleColon).then(just(Token::LAngle)).ignored(); + + let generics_close = just(Token::RAngle); + + let type_cast = just(Token::LAngle) + .ignore_then(AliasedType::parser()) + .then_ignore(generics_close.clone()) + .then_ignore(just(Token::DoubleColon)) + .then_ignore(just(Token::Ident("into"))) + .map(CallName::TypeCast); + + let builtin_generic_ty = |name: &'static str, ctor: fn(AliasedType) -> Self| { + just(Token::Ident(name)) + .ignore_then(turbofish_start.clone()) + .ignore_then(AliasedType::parser()) + .then_ignore(generics_close.clone()) + .map(ctor) + }; + + let unwrap_left = builtin_generic_ty("unwrap_left", CallName::UnwrapLeft); + let unwrap_right = builtin_generic_ty("unwrap_right", CallName::UnwrapRight); + let is_none = builtin_generic_ty("is_none", CallName::IsNone); + let raw_hash = builtin_generic_ty("raw_hash", CallName::RawHash); + + let fold = just(Token::Ident("fold")) + .ignore_then(turbofish_start.clone()) + .ignore_then(FunctionName::parser()) + .then_ignore(parse_token_with_recovery(Token::Comma)) + .then(select! { Token::DecLiteral(s) => s }.labelled("list size")) + .then_ignore(generics_close.clone()) + .validate(|(func, bound_str), e, emit| { + let digits = + crate::str::underscore_parsing::strip_digit_separators(bound_str.as_inner()); + + let bound = match digits.parse::() { + Ok(num) => match NonZeroPow2Usize::new(num) { + Some(val) => val, + None => { + emit.emit(Error::ListBoundPow2 { bound: num }.with_span(e.span())); + NonZeroPow2Usize::TWO + } + }, + Err(_) => { + emit.emit( + Error::CannotParse { + msg: format!("Invalid number: {}", bound_str), + } + .with_span(e.span()), + ); + NonZeroPow2Usize::TWO + } + }; + + CallName::Fold(func, bound) + }); + + let array_fold = just(Token::Ident("array_fold")) + .ignore_then(turbofish_start.clone()) + .ignore_then(FunctionName::parser()) + .then_ignore(parse_token_with_recovery(Token::Comma)) + .then(select! { Token::DecLiteral(s) => s }.labelled("array size")) + .then_ignore(generics_close.clone()) + .validate(|(func, size_str), e, emit| { + let digits = + crate::str::underscore_parsing::strip_digit_separators(size_str.as_inner()); + + let size = match digits.parse::() { + Ok(0) => { + emit.emit(Error::ArraySizeNonZero { size: 0 }.with_span(e.span())); + NonZeroUsize::new(1).unwrap() + } + Ok(n) => NonZeroUsize::new(n).unwrap(), + Err(_) => { + emit.emit( + Error::CannotParse { + msg: format!("Invalid number: {}", size_str), + } + .with_span(e.span()), + ); + NonZeroUsize::new(1).unwrap() + } + }; + + CallName::ArrayFold(func, size) + }); + + let for_while = just(Token::Ident("for_while")) + .ignore_then(turbofish_start.clone()) + .ignore_then(FunctionName::parser()) + .then_ignore(generics_close.clone()) + .map(CallName::ForWhile); + + let simple_builtins = select! { + Token::Ident("unwrap") => CallName::Unwrap, + Token::Macro("assert!") => CallName::Assert, + Token::Macro("panic!") => CallName::Panic, + Token::Macro("dbg!") => CallName::Debug, + }; + + let jet = select! { Token::Jet(s) => JetName::from_str_unchecked(s) }.map(CallName::Jet); + + let custom_func = FunctionName::parser().map(CallName::Custom); + + choice(( + type_cast, + unwrap_left, + unwrap_right, + is_none, + fold, + array_fold, + for_while, + raw_hash, + simple_builtins, + jet, + custom_func, + )) + } +} diff --git a/src/parse/grammar/expression.rs b/src/parse/grammar/expression.rs new file mode 100644 index 00000000..faf97263 --- /dev/null +++ b/src/parse/grammar/expression.rs @@ -0,0 +1,242 @@ +use std::sync::Arc; + +use chumsky::input::ValueInput; +use chumsky::prelude::{ + any, choice, just, nested_delimiters, one_of, recursive, skip_then_retry_until, +}; +use chumsky::{select, IterParser, Parser}; +use either::Either; + +use crate::error::{Diagnostic, Error, Span}; +use crate::lexer::Token; +use crate::str::Identifier; +use crate::TemplateProgramWitness; + +use super::super::{ + Call, EnumConstruction, Expression, ExpressionInner, SingleExpression, SingleExpressionInner, + Statement, +}; +use super::matching::match_expr_parser; +use super::{delimited_with_recovery, ChumskyParse, ParseError}; + +/// Split a parsed block body into its statements and its optional final expression. +/// +/// Each element arrives paired with the `;` that followed it, if any. Only the last +/// element may omit its `;`, and only when it is an expression rather than an +/// assignment: that element is the block's final expression. Every other missing `;` +/// is reported, and the element is kept as a statement so that analysis of the rest +/// of the block still happens. +fn split_block_body( + elements: Vec<(Statement, Option>)>, + emit: &mut chumsky::input::Emitter, +) -> (Arc<[Statement]>, Option>) { + let count = elements.len(); + let mut statements = Vec::with_capacity(count); + let mut final_expr = None; + + for (index, (statement, semi)) in elements.into_iter().enumerate() { + if semi.is_some() { + statements.push(statement); + continue; + } + + match statement { + // A trailing expression without `;` is the block's value. + Statement::Expression(expression) if index + 1 == count => { + final_expr = Some(Arc::new(expression)); + } + statement => { + emit.emit( + Error::Grammar { + msg: "Expected `;` after statement".to_string(), + } + .with_span(*statement.span()), + ); + statements.push(statement); + } + } + } + + (Arc::from(statements), final_expr) +} + +impl ChumskyParse for Expression { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + recursive(|expr| { + let block = { + // A block body is a run of elements, each of which is a statement + // followed by `;`, except that the last one may drop the `;` and + // become the block's final expression. + let element = Statement::parser(expr.clone()).then(just(Token::Semi).or_not()); + + let block_recovery = nested_delimiters( + Token::LBrace, + Token::RBrace, + [ + (Token::LParen, Token::RParen), + (Token::LAngle, Token::RAngle), + (Token::LBracket, Token::RBracket), + ], + |span| Expression::empty(span).inner().clone(), + ); + + let body = element + .repeated() + .collect::>() + .validate(|elements, _, emit| split_block_body(elements, emit)) + .recover_with(skip_then_retry_until( + block_recovery.ignored().or(any().ignored()), + one_of([Token::Semi, Token::RParen, Token::RBracket, Token::RBrace]) + .ignored(), + )); + + delimited_with_recovery(body, Token::LBrace, Token::RBrace, |_| { + (Arc::from(Vec::new()), None) + }) + .map(|(stmts, end_expr)| ExpressionInner::Block(stmts, end_expr)) + }; + + let single = SingleExpression::parser(expr.clone()).map(ExpressionInner::Single); + + choice((block, single)) + .map_with(|inner, e| Expression { + inner, + span: e.span(), + }) + .labelled("expression") + }) + } +} + +impl SingleExpression { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + let wrapper = |name: &'static str| { + select! { Token::Ident(i) if i == name => i }.ignore_then( + expr.clone() + .delimited_by(just(Token::LParen), just(Token::RParen)), + ) + }; + + let left = + wrapper("Left").map(|e| SingleExpressionInner::Either(Either::Left(Arc::new(e)))); + + let right = + wrapper("Right").map(|e| SingleExpressionInner::Either(Either::Right(Arc::new(e)))); + + let some = wrapper("Some").map(|e| SingleExpressionInner::Option(Some(Arc::new(e)))); + + // Bare `None` is the option literal only when no `::` follows: + // `None::A` is a construction of an enum aliased as `None`, and + // must fall through to the enum-construction alternative. + let none = select! { Token::Ident("None") => SingleExpressionInner::Option(None) } + .then_ignore(just(Token::DoubleColon).not().rewind()); + + let boolean = select! { Token::Bool(b) => SingleExpressionInner::Boolean(b) }; + + let comma_separated = expr + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(); + + let array = delimited_with_recovery( + comma_separated.clone(), + Token::LBracket, + Token::RBracket, + |_| Vec::new(), + ) + .map(|es| SingleExpressionInner::Array(Arc::from(es))); + + let list = just(Token::Macro("list!")) + .ignore_then(delimited_with_recovery( + comma_separated.clone(), + Token::LBracket, + Token::RBracket, + |_| Vec::new(), + )) + .map(|es| SingleExpressionInner::List(Arc::from(es))); + + let tuple = delimited_with_recovery( + comma_separated.clone(), + Token::LParen, + Token::RParen, + |_| Vec::new(), + ) + .map(|es| SingleExpressionInner::Tuple(Arc::from(es))); + + let literal = select! { + Token::DecLiteral(s) => SingleExpressionInner::Decimal(s), + Token::HexLiteral(s) => SingleExpressionInner::Hexadecimal(s), + Token::BinLiteral(s) => SingleExpressionInner::Binary(s), + Token::Witness(s) => SingleExpressionInner::Witness(TemplateProgramWitness::witness_from_str(s)), + Token::Param(s) => SingleExpressionInner::Parameter(TemplateProgramWitness::parameter_from_str(s)), + }; + + // Enum variant construction: `Path::To::Enum::Variant(args..)`. + // At least one `::` distinguishes the path from variables and calls. + // The built-in wrappers (Left, Some, ...) require `(` directly after + // their name, so they never reach this alternative. + let enum_construction = Identifier::parser() + .separated_by(just(Token::DoubleColon)) + .at_least(2) + .collect::>() + .then( + comma_separated + .clone() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not(), + ) + .map_with(|(mut segments, args), e| { + let variant = segments.pop().expect("the parser requires two segments"); + EnumConstruction { + enum_path: Arc::from(segments), + variant, + args: Arc::from(args.unwrap_or_default()), + span: e.span(), + } + }) + .map(SingleExpressionInner::EnumConstruction); + + let call = Call::parser(expr.clone()).map(SingleExpressionInner::Call); + + let match_expr = match_expr_parser(expr.clone()); + + let variable = Identifier::parser().map(SingleExpressionInner::Variable); + + // Expression delimeted by parentheses + let expression = expr + .clone() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .map(|es| SingleExpressionInner::Expression(Arc::from(es))); + + choice(( + left, + right, + some, + none, + boolean, + match_expr, + expression, + list, + array, + tuple, + enum_construction, + call, + literal, + variable, + )) + .map_with(|inner, e| Self { + inner, + span: e.span(), + }) + } +} diff --git a/src/parse/grammar/matching.rs b/src/parse/grammar/matching.rs new file mode 100644 index 00000000..9c306695 --- /dev/null +++ b/src/parse/grammar/matching.rs @@ -0,0 +1,322 @@ +use std::sync::Arc; + +use chumsky::input::ValueInput; +use chumsky::prelude::{choice, just}; +use chumsky::{select, IterParser, Parser}; +use either::Either; +use itertools::Itertools; + +use crate::error::{Diagnostic, Error, Span}; +use crate::lexer::Token; +use crate::pattern::Pattern; +use crate::str::{AliasName, Identifier}; +use crate::types::AliasedType; + +use super::super::{ + EnumMatch, EnumMatchArm, Expression, ExpressionInner, Match, MatchArm, MatchPattern, + SingleExpressionInner, +}; +use super::{delimited_with_recovery, ChumskyParse, ParseError, RESERVED_PATTERN_NAMES}; + +impl ChumskyParse for MatchPattern { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let wrapper = |name: &'static str, ctor: fn(Pattern, AliasedType) -> Self| { + select! { Token::Ident(i) if i == name => i } + .ignore_then(delimited_with_recovery( + Pattern::parser() + .then_ignore(just(Token::Colon)) + .then(AliasedType::parser()), + Token::LParen, + Token::RParen, + |_| { + ( + Pattern::Ignore, + AliasedType::alias(AliasName::from_str_unchecked("error")), + ) + }, + )) + .map(move |(id, ty)| ctor(id, ty)) + }; + + choice(( + wrapper("Left", MatchPattern::Left), + wrapper("Right", MatchPattern::Right), + wrapper("Some", MatchPattern::Some), + select! { Token::Ident("None") => MatchPattern::None }, + select! { Token::Bool(true) => MatchPattern::True }, + select! { Token::Bool(false) => MatchPattern::False }, + )) + } +} + +/// Head of an enum match arm, before the arm body is attached: `EnumName::Variant` with optional payload bindings. +#[derive(Clone)] +struct EnumArmHead { + enum_path: Arc<[Identifier]>, + variant: Identifier, + bindings: Arc<[(Pattern, AliasedType)]>, +} + +impl EnumArmHead { + fn into_arm(self, expression: Arc, span: Span) -> EnumMatchArm { + EnumMatchArm { + enum_path: self.enum_path, + variant: self.variant, + bindings: self.bindings, + expression, + span, + } + } +} + +/// One parsed match arm. An enum variant head or a built-in pattern, +/// plus the arm body and its source boundaries. +type ParsedMatchArm = (Either, Arc, Span); + +/// Parser for the head of an enum match arm: `EnumName::Variant` with +/// optional payload bindings `(pattern: Type, ...)`. +/// +/// A non-reserved head identifier commits without backtracking: the +/// `select!` guard fails without consuming the token. A reserved pattern +/// name (`Left`, `Some`, ...) heads an enum path only when `::` follows, +/// so an alias of an enum that shadows a pattern name stays matchable +/// while `Left(x)` remains the built-in pattern. +fn enum_arm_head_parser<'tokens, 'src: 'tokens, I>( +) -> impl Parser<'tokens, I, EnumArmHead, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, +{ + let bindings = Pattern::parser() + .then_ignore(just(Token::Colon)) + .then(AliasedType::parser()) + .separated_by(just(Token::Comma)) + .allow_trailing() + .at_least(1) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not() + .map(|bindings| Arc::from(bindings.unwrap_or_default())); + + let head_ident = choice(( + select! { Token::Ident(name) if !RESERVED_PATTERN_NAMES.contains(&name) => Identifier::from_str_unchecked(name) }, + // The rewind leaves the `::` for the shared `then_ignore` below. + select! { Token::Ident(name) => Identifier::from_str_unchecked(name) } + .then_ignore(just(Token::DoubleColon).rewind()), + )); + + head_ident + .then_ignore(just(Token::DoubleColon)) + .then( + Identifier::parser() + .separated_by(just(Token::DoubleColon)) + .at_least(1) + .collect::>(), + ) + .then(bindings) + .map(|((first, mut rest), bindings)| { + let variant = rest.pop().expect("the parser requires one segment"); + let mut enum_path = vec![first]; + enum_path.extend(rest); + EnumArmHead { + enum_path: Arc::from(enum_path), + variant, + bindings, + } + }) +} + +/// Parser for one match arm. An arm head, `=>`, the arm body, and the +/// comma that is required unless the body is a block expression. +fn match_arm_parser<'tokens, 'src: 'tokens, I, E>( + expr: E, +) -> impl Parser<'tokens, I, ParsedMatchArm, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, +{ + let arm_head = choice(( + enum_arm_head_parser().map(Either::Left), + MatchPattern::parser().map(Either::Right), + )); + + arm_head + .then_ignore(just(Token::FatArrow)) + .then(expr.map(Arc::new)) + .then(just(Token::Comma).or_not()) + .validate(|((head, expression), comma), e, emitter| { + let is_block = matches!(expression.as_ref().inner, ExpressionInner::Block(_, _)); + if !is_block && comma.is_none() { + emitter.emit( + Error::Grammar { + msg: "Missing ',' after a match arm that isn't block expression" + .to_string(), + } + .with_span(e.span()), + ); + } + (head, expression, e.span()) + }) +} + +/// A binary match with dummy arms, standing in for a malformed match so +/// parsing can continue after its error was reported. +fn placeholder_match(scrutinee: Arc, span: Span) -> SingleExpressionInner { + let fallback_arm = MatchArm { + expression: Arc::new(Expression::empty(Span::DUMMY)), + pattern: MatchPattern::False, + span: Span::DUMMY, + }; + SingleExpressionInner::Match(Match { + scrutinee, + left: fallback_arm.clone(), + right: fallback_arm, + span, + }) +} + +/// Do the two patterns complement each other in canonical order +/// (`Left`/`Right`, `None`/`Some`, `false`/`true`)? +fn patterns_in_canonical_order(left: &MatchPattern, right: &MatchPattern) -> bool { + matches!( + (left, right), + (MatchPattern::Left(..), MatchPattern::Right(..)) + | (MatchPattern::None, MatchPattern::Some(..)) + | (MatchPattern::False, MatchPattern::True) + ) +} + +/// Assemble parsed arms into an [`EnumMatch`] or a binary [`Match`] node. +/// +/// Malformed arms return the error to report together with a fallback +/// node, so the caller emits exactly one error per malformed match. +/// Running bad arms through the pattern pairing would emit a second, +/// bogus "incompatible match arms" error for the same span. +fn assemble_match_arms( + scrutinee: Arc, + arms: Vec, + span: Span, +) -> Result> { + if arms.is_empty() { + return Err(Box::new(( + Error::Grammar { + msg: "match expression has no arms".to_string(), + } + .with_span(span), + placeholder_match(scrutinee, span), + ))); + } + + // Split arms by the classification the arm heads carry. Any number of + // enum arms (including one) routes to the enum match, whose analysis + // reports missing variants. The binary arm-count error below would be misleading there. + let (enum_arms, builtin_arms): (Vec, Vec) = arms + .into_iter() + .partition_map(|(head, expression, arm_span)| match head { + Either::Left(head) => Either::Left(head.into_arm(expression, arm_span)), + Either::Right(pattern) => Either::Right(MatchArm { + pattern, + expression, + span: arm_span, + }), + }); + + if builtin_arms.is_empty() { + return Ok(SingleExpressionInner::EnumMatch(EnumMatch { + scrutinee, + arms: Arc::from(enum_arms), + span, + })); + } + + if let Some(enum_arm) = enum_arms.first() { + // Mixed arms: name the clash instead of an arm-count error. + return Err(Box::new(( + Error::Grammar { + msg: format!( + "match arms mix the enum variant pattern `{}` \ + with the built-in pattern `{}`", + enum_arm, builtin_arms[0].pattern + ), + } + .with_span(span), + placeholder_match(scrutinee, span), + ))); + } + + // Binary match: exactly 2 built-in arms, in either order. + let Ok([first, second]) = <[MatchArm; 2]>::try_from(builtin_arms) else { + return Err(Box::new(( + Error::Grammar { + msg: "binary match requires exactly 2 arms".to_string(), + } + .with_span(span), + placeholder_match(scrutinee, span), + ))); + }; + + let (left, right) = if patterns_in_canonical_order(&first.pattern, &second.pattern) { + (first, second) + } else if patterns_in_canonical_order(&second.pattern, &first.pattern) { + (second, first) + } else { + let error = Error::IncompatibleMatchArms { + first: Box::new(first.pattern.clone()), + second: Box::new(second.pattern.clone()), + } + .with_span(span); + // The arms still form a match; keep them so analysis can continue. + let node = SingleExpressionInner::Match(Match { + scrutinee, + left: first, + right: second, + span, + }); + return Err(Box::new((error, node))); + }; + + Ok(SingleExpressionInner::Match(Match { + scrutinee, + left, + right, + span, + })) +} + +/// Parser for `match` expressions. +/// +/// Handles both binary matches (exactly 2 arms: Left/Right, None/Some, +/// false/true) and enum matches (arms of the form `EnumName::Variant`). +/// Dispatches to [`Match`] or [`EnumMatch`] based on the patterns found. +pub(super) fn match_expr_parser<'tokens, 'src: 'tokens, I, E>( + expr: E, +) -> impl Parser<'tokens, I, SingleExpressionInner, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, +{ + let arms = delimited_with_recovery( + match_arm_parser(expr.clone()) + .repeated() + .collect::>(), + Token::LBrace, + Token::RBrace, + |_| vec![], + ); + + just(Token::Match) + .ignore_then(expr.map(Arc::new)) + .then(arms) + .validate(|(scrutinee, arms), e, emit| { + match assemble_match_arms(scrutinee, arms, e.span()) { + Ok(node) => node, + Err(boxed) => { + let (error, fallback) = *boxed; + emit.emit(error); + fallback + } + } + }) +} diff --git a/src/parse/grammar/mod.rs b/src/parse/grammar/mod.rs new file mode 100644 index 00000000..83c3a40b --- /dev/null +++ b/src/parse/grammar/mod.rs @@ -0,0 +1,108 @@ +mod call; +mod expression; +mod matching; +mod program; +mod statement; +mod types; + +use chumsky::input::ValueInput; +use chumsky::prelude::{empty, just, nested_delimiters, via_parser}; +use chumsky::{extra, select, Parser}; + +use crate::error::{Diagnostic, Error, Span}; +use crate::lexer::Token; +use crate::str::{AliasName, FunctionName, Identifier, ModuleName, SymbolName}; + +/// Trait for generating parsers of themselves. +/// +/// Replacement for previous `PestParse` trait. +pub trait ChumskyParse: Sized { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>; +} + +type ParseError<'src> = extra::Err; + +/// Identifiers of the built-in binary match patterns. +/// An enum may not use them as its name, because e.g. `Left::A` in a match arm would parse as the built-in `Left(..)` pattern. +pub(super) const RESERVED_PATTERN_NAMES: [&str; 4] = ["Left", "Right", "Some", "None"]; + +macro_rules! impl_parse_wrapped_string { + ($wrapper: ident, $label: literal) => { + impl ChumskyParse for $wrapper { + fn parser<'tokens, 'src: 'tokens, I>( + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + select! { + Token::Ident(ident) => Self::from_str_unchecked(ident) + } + .labelled($label) + } + } + }; +} + +impl_parse_wrapped_string!(SymbolName, "unresolved symbol name"); +impl_parse_wrapped_string!(FunctionName, "function name"); +impl_parse_wrapped_string!(Identifier, "identifier"); +impl_parse_wrapped_string!(AliasName, "alias name"); +impl_parse_wrapped_string!(ModuleName, "module name"); + +/// Parse a token, and, if not found, place itself in place of missing one. +/// +/// Should be only used when we know that this token should be there. For example, type of +/// `List` would require comma inside angle brackets. +fn parse_token_with_recovery<'tokens, 'src: 'tokens, I>( + tok: Token<'src>, +) -> impl Parser<'tokens, I, Token<'src>, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, +{ + just(tok.clone()).recover_with(via_parser(empty().to(tok))) +} + +/// Parser with error recovery for expressions, which would always contains given delimiters. +/// +/// Can track span of open delimiter (if any). +fn delimited_with_recovery<'tokens, 'src: 'tokens, I, P, T, F>( + parser: P, + open: Token<'src>, + close: Token<'src>, + fallback: F, +) -> impl Parser<'tokens, I, T, ParseError<'src>> + Clone +where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + P: Parser<'tokens, I, T, ParseError<'src>> + Clone, + T: Clone + 'tokens, + F: Fn(Span) -> T + Clone + 'tokens, +{ + just(open.clone()) + .map_with(|_, e| e.span()) + .then(parser.recover_with(via_parser(nested_delimiters( + open.clone(), + close.clone(), + [ + (Token::LParen, Token::RParen), + (Token::LBracket, Token::RBracket), + (Token::LBrace, Token::RBrace), + (Token::LAngle, Token::RAngle), + ], + fallback, + )))) + .then(just(close).or_not()) + // TODO: we should use information about open delimiter + .validate(move |((open_span, content), close_token), _, emit| { + if close_token.is_none() { + emit.emit( + Error::Grammar { + msg: format!("Unclosed delimiter {open}"), + } + .with_span(open_span), + ) + } + content + }) +} diff --git a/src/parse/grammar/program.rs b/src/parse/grammar/program.rs new file mode 100644 index 00000000..372456d8 --- /dev/null +++ b/src/parse/grammar/program.rs @@ -0,0 +1,284 @@ +use std::sync::Arc; + +use chumsky::input::ValueInput; +use chumsky::prelude::{any, choice, just, nested_delimiters, recursive, via_parser}; +use chumsky::{select, IterParser, Parser}; + +use crate::driver::CRATE_STR; +use crate::error::{Error, Span}; +use crate::lexer::Token; +use crate::str::{FunctionName, Identifier, ModuleName, SymbolName}; +use crate::types::AliasedType; + +use super::super::{ + EnumDeclaration, Expression, Function, FunctionParam, Item, Module, Program, TypeAlias, + UseDecl, UseItems, Visibility, +}; +use super::{delimited_with_recovery, ChumskyParse, ParseError}; + +impl ChumskyParse for Program { + /// Parses a sequence of top-level [`Item`]s into a complete [`Program`]. + /// + /// If an invalid item is encountered, it will safely skip the broken tokens + /// until it finds a synchronization point. This prevents the parser from + /// failing completely, allowing it to report multiple syntax errors across the file + /// while substituting the unparseable sections with [`Item::Ignored`]. + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let skip_until_next_item = any() + .then( + any() + .filter(|t| { + !matches!( + t, + Token::Pub + | Token::Use + | Token::Fn + | Token::Type + | Token::Mod + | Token::Enum + ) + }) + .repeated(), + ) + .map_with(|_, _| Item::Ignored); + + Item::parser() + .recover_with(via_parser(skip_until_next_item)) + .repeated() + .collect::>() + .map_with(|items, e| Program { + items: Arc::from(items), + span: e.span(), + }) + } +} + +impl ChumskyParse for Item { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + recursive(|item| { + let func_parser = Function::parser().map(Item::Function); + let type_parser = TypeAlias::parser().map(Item::TypeAlias); + let use_parser = UseDecl::parser().map(Item::Use); + let enum_parser = EnumDeclaration::parser().map(Item::EnumDeclaration); + + // Lazy item here + let mod_parser = Module::parser_with_items(item).map(Item::Module); + + choice(( + func_parser, + use_parser, + type_parser, + enum_parser, + mod_parser, + )) + }) + } +} + +impl ChumskyParse for Function { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default) + .labelled("function visibility"); + + let params = delimited_with_recovery( + FunctionParam::parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LParen, + Token::RParen, + |_| Vec::new(), + ) + .map(Arc::from) + .labelled("function parameters"); + + let ret = just(Token::Arrow) + .ignore_then(AliasedType::parser()) + .or_not() + .labelled("return type"); + + let body = just(Token::LBrace) + .rewind() + .ignore_then(Expression::parser()) + .recover_with(via_parser(nested_delimiters( + Token::LBrace, + Token::RBrace, + [ + (Token::LParen, Token::RParen), + (Token::LBracket, Token::RBracket), + ], + Expression::empty, + ))) + .labelled("function body"); + + visibility + .then_ignore(just(Token::Fn)) + .then(FunctionName::parser()) + .then(params) + .then(ret) + .then(body) + .map_with(|((((visibility, name), params), ret), body), e| Self { + visibility, + name, + params, + ret, + body, + span: e.span(), + }) + } +} + +impl ChumskyParse for UseDecl { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + + let first_segment = select! { + Token::Ident(ident) => Identifier::from_str_unchecked(ident), + Token::Crate => Identifier::from_str_unchecked(CRATE_STR), + }; + + // Parse the base path prefix (e.g., `dependency_root_path::file::`, `dependency_root_path::dir::file::`, + // or `crate::dir::file::`). We require at least 2 segments here because a valid import needs a minimum + // of 3 items total: the dependency root path (or `crate`), the file, and the specific item. + // + // With the introduction of `mod` keyword and single-file flattening, 2 total segments are now + // valid: `crate::item`, where `crate` is the program root. + let path = first_segment + .then_ignore(just(Token::DoubleColon)) + .then( + Identifier::parser() + .then_ignore(just(Token::DoubleColon)) + .repeated() + .collect::>(), + ) + .map(|(first, mut rest)| { + let mut path = vec![first]; + path.append(&mut rest); + path + }); + + let aliased_item = + SymbolName::parser().then(just(Token::As).ignore_then(SymbolName::parser()).or_not()); + + let list = aliased_item + .clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map(UseItems::List); + let single = aliased_item.map(UseItems::Single); + let items = choice((list, single)); + + visibility + .then_ignore(just(Token::Use)) + .then(path) + .then(items) + .then_ignore(just(Token::Semi)) + .map_with(|((visibility, path), items), e| Self { + visibility, + path, + items, + span: e.span(), + }) + } +} + +impl ChumskyParse for FunctionParam { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let identifier = Identifier::parser(); + + let ty = AliasedType::parser(); + + identifier + .then_ignore(just(Token::Colon)) + .then(ty) + .map_with(|(identifier, ty), e| Self { + identifier, + ty, + span: e.span(), + }) + } +} + +impl Module { + pub fn parser_with_items<'tokens, 'src: 'tokens, I>( + item_parser: impl Parser<'tokens, I, Item, ParseError<'src>> + Clone + 'tokens, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + + let name = ModuleName::parser().map_with(|name, e| (name, e.span())); + + let items = item_parser + .repeated() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .recover_with(via_parser(nested_delimiters( + Token::LBrace, + Token::RBrace, + [ + (Token::LParen, Token::RParen), + (Token::LBracket, Token::RBracket), + ], + |_| Vec::new(), + ))) + .map(Arc::from); + + visibility + .then(just(Token::Mod).ignore_then(name).then(items)) + .map_with(|(visibility, (name, items)), e| Self { + visibility, + name: name.0, + items, + span: e.span(), + }) + .validate(|module, _, emit| { + // TODO: Enums may only be declared at the top level of a file (done so to reduce scope of the PR). + // The bare name is the enum's identity in the ABI, and a module path would obscure it. + // Direct children suffice. Nested modules validate their own items. + for item in module.items.iter() { + if let Item::EnumDeclaration(decl) = item { + emit.emit( + Error::Grammar { + msg: format!( + "enum `{}` is declared inside `mod {}`; enums may \ + only be declared at the top level of a file", + decl.name(), + module.name + ), + } + .with_span(decl.into()), + ); + } + } + module + }) + } +} diff --git a/src/parse/grammar/statement.rs b/src/parse/grammar/statement.rs new file mode 100644 index 00000000..416c7844 --- /dev/null +++ b/src/parse/grammar/statement.rs @@ -0,0 +1,90 @@ +use chumsky::input::ValueInput; +use chumsky::prelude::{choice, just, recursive}; +use chumsky::{select, IterParser, Parser}; + +use crate::error::Span; +use crate::lexer::Token; +use crate::pattern::Pattern; +use crate::str::Identifier; +use crate::types::AliasedType; + +use super::super::{Assignment, Expression, Statement}; +use super::{delimited_with_recovery, parse_token_with_recovery, ChumskyParse, ParseError}; + +impl Statement { + pub(super) fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + let assignment = Assignment::parser(expr.clone()).map(Statement::Assignment); + + let expression = expr.map(Statement::Expression); + + choice((assignment, expression)) + } +} + +impl Assignment { + fn parser<'tokens, 'src: 'tokens, I, E>( + expr: E, + ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + E: Parser<'tokens, I, Expression, ParseError<'src>> + Clone + 'tokens, + { + just(Token::Let) + .ignore_then(Pattern::parser()) + .then_ignore(parse_token_with_recovery(Token::Colon)) + .then(AliasedType::parser()) + .then_ignore(parse_token_with_recovery(Token::Eq)) + .then(expr) + .map_with(|((pattern, ty), expression), e| Self { + pattern, + ty, + expression, + span: e.span(), + }) + } +} + +impl ChumskyParse for Pattern { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + recursive(|pat| { + let variable = Identifier::parser().map(Pattern::Identifier); + + let ignore = select! { + Token::Ident("_") => Pattern::Ignore, + }; + + let tuple = delimited_with_recovery( + pat.clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LParen, + Token::RParen, + |_| Vec::new(), + ) + .map(Pattern::tuple); + + let array = delimited_with_recovery( + pat.clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>(), + Token::LBracket, + Token::RBracket, + |_| Vec::new(), + ) + .map(Pattern::array); + + choice((ignore, variable, tuple, array)).labelled("pattern") + }) + } +} diff --git a/src/parse/grammar/types.rs b/src/parse/grammar/types.rs new file mode 100644 index 00000000..301bdeb5 --- /dev/null +++ b/src/parse/grammar/types.rs @@ -0,0 +1,274 @@ +use std::str::FromStr; +use std::sync::Arc; + +use chumsky::input::ValueInput; +use chumsky::prelude::{choice, empty, just, none_of, recursive, via_parser}; +use chumsky::{select, IterParser, Parser}; + +use crate::error::{Diagnostic, Error, Span}; +use crate::lexer::Token; +use crate::num::NonZeroPow2Usize; +use crate::str::{AliasName, Decimal, Identifier}; +use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType}; + +use super::super::{EnumDeclaration, EnumVariant, TypeAlias, Visibility}; +use super::{ + delimited_with_recovery, parse_token_with_recovery, ChumskyParse, ParseError, + RESERVED_PATTERN_NAMES, +}; + +impl ChumskyParse for AliasedType { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let atom = select! { + Token::Ident(ident) => { + if ident == "bool" { + AliasedType::boolean() + } else if let Ok(uint_type) = UIntType::from_str(ident) { + AliasedType::from(uint_type) + } else if let Ok(builtin) = BuiltinAlias::from_str(ident) { + AliasedType::builtin(builtin) + } else { + AliasedType::alias(AliasName::from_str_unchecked(ident)) + } + } + }; + + let num = select! { + Token::DecLiteral(i) => i.clone() + } + .labelled("decimal number") + .recover_with(via_parser( + none_of([Token::RAngle, Token::RBracket]) + .ignored() + .or(empty()) + .to(Decimal::from_str_unchecked("0")), + )); + + recursive(|ty| { + let args = delimited_with_recovery( + ty.clone() + .then_ignore(parse_token_with_recovery(Token::Comma)) + .then(ty.clone()), + Token::LAngle, + Token::RAngle, + |_| { + ( + AliasedType::alias(AliasName::from_str_unchecked("error")), + AliasedType::alias(AliasName::from_str_unchecked("error")), + ) + }, + ); + + let sum_type = just(Token::Ident("Either")) + .ignore_then(args) + .map(|(left, right)| AliasedType::either(left, right)) + .labelled("Either"); + + let option_type = just(Token::Ident("Option")) + .ignore_then(delimited_with_recovery( + ty.clone(), + Token::LAngle, + Token::RAngle, + |_| AliasedType::alias(AliasName::from_str_unchecked("error")), + )) + .map(AliasedType::option) + .labelled("Option"); + + let tuple = delimited_with_recovery( + ty.clone() + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect() + .map(|s: Vec| AliasedType::tuple(s)), + Token::LParen, + Token::RParen, + |_| AliasedType::tuple(Vec::new()), + ) + .labelled("tuple"); + + let array = delimited_with_recovery( + ty.clone() + .then_ignore(parse_token_with_recovery(Token::Semi)) + .then(num.clone()) + .map(|(ty, size)| { + let digits = + crate::str::underscore_parsing::strip_digit_separators(size.as_inner()); + + AliasedType::array(ty, usize::from_str(digits.as_ref()).unwrap_or_default()) + }), + Token::LBracket, + Token::RBracket, + |_| { + AliasedType::array( + AliasedType::alias(AliasName::from_str_unchecked("error")), + 0, + ) + }, + ) + .labelled("array"); + + let list = just(Token::Ident("List")) + .ignore_then(delimited_with_recovery( + ty.then_ignore(parse_token_with_recovery(Token::Comma)) + .then(num.clone().validate(|num, e, emit| -> NonZeroPow2Usize { + let digits = crate::str::underscore_parsing::strip_digit_separators( + num.as_inner(), + ); + + match NonZeroPow2Usize::from_str(digits.as_ref()) { + Ok(number) => number, + Err(err) => { + emit.emit( + Error::Grammar { + msg: format!("Cannot parse list bound: {err}"), + } + .with_span(e.span()), + ); + // fallback to default value + NonZeroPow2Usize::TWO + } + } + })), + Token::LAngle, + Token::RAngle, + |_| { + ( + AliasedType::alias(AliasName::from_str_unchecked("error")), + NonZeroPow2Usize::TWO, + ) + }, + )) + .map(|(ty, size)| AliasedType::list(ty, size)) + .labelled("List"); + + choice((sum_type, option_type, tuple, array, list, atom)) + .map_with(|inner, _| inner) + .labelled("type") + }) + } +} + +impl ChumskyParse for TypeAlias { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + + let name = AliasName::parser() + .validate(|name, e, emit| { + let ident = name.as_str(); + let known_type = if ident == "bool" { + Some(AliasedType::boolean()) + } else if let Ok(uint_type) = UIntType::from_str(ident) { + Some(AliasedType::from(uint_type)) + } else if let Ok(builtin) = BuiltinAlias::from_str(ident) { + Some(AliasedType::builtin(builtin)) + } else { + None + }; + + if known_type.is_some() { + emit.emit( + Error::RedefinedAliasAsBuiltin { name: name.clone() }.with_span(e.span()), + ); + } + name + }) + .map_with(|name, e| (name, e.span())); + + visibility + .then( + just(Token::Type) + .ignore_then(name) + .then_ignore(parse_token_with_recovery(Token::Eq)) + .then(AliasedType::parser()) + .then_ignore(just(Token::Semi)), + ) + .map_with(|(visibility, (name, ty)), e| Self { + visibility, + name: name.0, + ty, + span: e.span(), + }) + } +} + +impl ChumskyParse for EnumDeclaration { + fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone + where + I: ValueInput<'tokens, Token = Token<'src>, Span = Span>, + { + let visibility = just(Token::Pub) + .to(Visibility::Public) + .or_not() + .map(Option::unwrap_or_default); + + let name = AliasName::parser().try_map(|name, span| { + if RESERVED_PATTERN_NAMES.contains(&name.as_str()) { + return Err(Diagnostic::new( + Error::Grammar { + msg: format!( + "enum name '{name}' is reserved for the built-in match pattern `{name}`" + ), + }, + span, + )); + } + // Reserved type names are rejected via the shared list, which + // also covers the generic constructors (`Either`, `Option`, + // `List`): `enum Signature` or `enum Option` would make + // constructions name the enum while type annotations resolve + // to the builtin, and the ABI would report the bare name + // ambiguously. + if crate::str::is_reserved_alias_name(name.as_str()) { + return Err(Diagnostic::new( + Error::RedefinedAliasAsBuiltin { name: name.clone() }, + span, + )); + } + Ok(name) + }); + + let payload = AliasedType::parser() + .separated_by(just(Token::Comma)) + .allow_trailing() + .at_least(1) + .collect::>() + .delimited_by(just(Token::LParen), just(Token::RParen)) + .or_not() + .map(|payload| Arc::from(payload.unwrap_or_default())); + + let variant = Identifier::parser() + .then(payload) + .map_with(|(name, payload), e| EnumVariant { + name, + payload, + span: e.span(), + }); + + let variants = variant + .separated_by(just(Token::Comma)) + .allow_trailing() + .collect::>() + .delimited_by(just(Token::LBrace), just(Token::RBrace)) + .map(Arc::from); + + visibility + .then_ignore(just(Token::Enum)) + .then(name) + .then(variants) + .map_with(|((visibility, name), variants), e| Self { + visibility, + name, + variants, + span: e.span(), + }) + } +} diff --git a/src/parse/lossless.rs b/src/parse/lossless.rs new file mode 100644 index 00000000..265cc03f --- /dev/null +++ b/src/parse/lossless.rs @@ -0,0 +1,82 @@ +use crate::error::{DiagnosticManager, Span}; +use crate::lexer::{FmtToken, FmtTokens}; +use crate::unstable::UnstableFeatures; + +use super::{pipeline, Program}; + +/// Source-aware parse result used by formatters. +/// +/// The compiler keeps using [`Program`] directly. Formatters need the same +/// semantic tree plus the lossless trivia stream that the grammar intentionally +/// does not consume. +#[derive(Clone, Debug)] +pub struct ParsedSource<'src> { + program: Program, + tokens: FmtTokens<'src>, + prefix: Span, +} + +impl<'src> ParsedSource<'src> { + /// Access the semantic program used for formatting. + pub fn program(&self) -> &Program { + &self.program + } + + /// Access the lossless token stream in source order. + pub fn tokens(&self) -> &FmtTokens<'src> { + &self.tokens + } + + /// Access the source prefix skipped by the version-directive prescan. + /// + /// A formatter should preserve this range verbatim until the directive gains + /// its own formatting grammar. + pub fn prefix(&self) -> Span { + self.prefix + } +} + +impl Program { + /// Parse source for formatting while retaining all comments and whitespace. + pub fn parse_with_errors_for_fmt<'src>( + file_id: usize, + source: &'src str, + unstable_features: &UnstableFeatures, + diagnostics: &mut DiagnosticManager, + ) -> Option> { + let before = diagnostics.error_count(); + + let start = pipeline::directive_prescan(source, file_id, diagnostics)?; + + let (tokens, lex_errors) = crate::lexer::lex_lossless(file_id, source, start); + let lex_ok = pipeline::is_lex_ok(lex_errors, diagnostics)?; + + let tokens = tokens?; + + let semantic_tokens = tokens + .iter() + .filter_map(|(token, span)| match token { + FmtToken::Token(token) => Some((token.clone(), *span)), + FmtToken::Trivia(_) => None, + }) + .collect::>(); + + let (program, parse_ok) = + pipeline::parse_ast(file_id, source, semantic_tokens, diagnostics); + + if parse_ok && lex_ok { + pipeline::post_check(unstable_features, program.as_ref(), diagnostics); + } + + if diagnostics.error_count() > before { + None + } else { + let program = program?; + Some(ParsedSource { + program, + tokens, + prefix: Span::new(file_id, 0..start), + }) + } + } +} diff --git a/src/parse/mod.rs b/src/parse/mod.rs new file mode 100644 index 00000000..605ce25c --- /dev/null +++ b/src/parse/mod.rs @@ -0,0 +1,1215 @@ +//! This module contains the parsing code to convert the +//! tokens into an AST. + +#[cfg(feature = "arbitrary")] +mod arbitrary_impls; +mod display; +mod grammar; +#[cfg(feature = "fmt")] +mod lossless; +mod pipeline; +#[cfg(test)] +mod tests; + +use std::num::NonZeroUsize; +use std::path::PathBuf; +use std::sync::Arc; + +use either::Either; +use miniscript::iter::{Tree, TreeLike}; + +use crate::error::{Diagnostic, Error, Span}; +use crate::impl_eq_hash; +use crate::num::NonZeroPow2Usize; +use crate::pattern::Pattern; +use crate::str::{ + AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, + SymbolName, +}; +use crate::types::{AliasedType, TypeConstructible}; +use crate::unstable::{impl_require_feature, FeatureRequirement, RequireFeature, UnstableFeature}; +use crate::TemplateProgramWitness; + +pub use self::grammar::ChumskyParse; +#[cfg(feature = "fmt")] +pub use self::lossless::ParsedSource; +pub use self::pipeline::{ParseFromStr, ParseFromStrWithErrors}; + +/// A program is a sequence of items. +#[derive(Clone, Debug)] +pub struct Program { + items: Arc<[Item]>, + span: Span, +} + +impl Program { + // Need for driver usage + pub(crate) fn new(items: &[Item], span: Span) -> Self { + Self { + items: Arc::from(items), + span, + } + } + + /// Access the items of the program. + pub fn items(&self) -> &[Item] { + &self.items + } +} + +impl_eq_hash!(Program; items); + +impl_require_feature!(Program { recurse: items; }); + +/// An item is a component of a program. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum Item { + /// A type alias. + TypeAlias(TypeAlias), + /// A function. + Function(Function), + /// An import declaration (e.g., `use math::add`) that brings another + /// [`Item`] into the current scope. + Use(UseDecl), + /// An enum declaration. + EnumDeclaration(EnumDeclaration), + /// A module containing a collection of nested [`Item`]. + Module(Module), + /// A placeholder used exclusively for error recovery during parsing. + /// + /// When the parser encounters a syntax error, it skips the malformed tokens + /// until it reaches a valid top-level keyword and inserts `Ignored` into the AST. + Ignored, +} + +impl Item { + /// Access the source span when this item was parsed successfully. + /// + /// Error-recovery placeholders have no source node to decorate. + pub fn span(&self) -> Option<&Span> { + match self { + Self::TypeAlias(alias) => Some(alias.span()), + Self::Function(function) => Some(function.span()), + Self::Use(use_decl) => Some(use_decl.span()), + Self::EnumDeclaration(declaration) => Some(declaration.span()), + Self::Module(module) => Some(module.span()), + Self::Ignored => None, + } + } +} + +impl_require_feature!(Item { + variants: + TypeAlias(alias), + Function(function), + Use(use_decl), + EnumDeclaration(decl), + Module(module), + Ignored, +}); + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum Visibility { + Public, + #[default] + Private, +} + +/// Represents an import declaration in the Abstract Syntax Tree. +/// +/// This structure defines how items from other modules or files are brought into the +/// current scope. Note that in this architecture, the first identifier in the path +/// is always treated as an dependency root path name bound to a specific physical path. +/// +/// # Example +/// ```text +/// pub use std::collections::{HashMap, HashSet}; +/// ``` +#[derive(Clone, Debug)] +pub struct UseDecl { + /// The visibility of the import (e.g., `pub use` vs `use`). + visibility: Visibility, + + /// The base path to the target file or module. + /// + /// The first element is always the registered dependency root path name for + /// the import path. Subsequent elements represent nested modules or directories. + path: Vec, + + /// The specific item or list of items being imported from the resolved path. + items: UseItems, + span: Span, +} + +impl_require_feature!(UseDecl { + requires: UnstableFeature::Imports, span: span; + recurse: items; +}); + +impl_require_feature!(UseItems { + variants: + Single(_), + List(_), +}); + +impl UseDecl { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + /// Returns the full logical module path as a vector of string slices. + /// + /// This includes the Dependency Root Path Name (the first segment) + /// followed by all subsequent sub-module segments. + pub fn path(&self) -> &[Identifier] { + &self.path + } + + pub fn items(&self) -> &UseItems { + &self.items + } + + pub fn span(&self) -> &Span { + &self.span + } + + pub fn str_path(&self) -> String { + let path: PathBuf = self.path().iter().map(Identifier::as_str).collect(); + path.display().to_string() + } + + /// Extracts the Dependency Root Path Name (the very first segment) from this path. + /// + /// # Errors + /// + /// Returns a `Diagnostic` if the use declaration path is completely empty. + pub fn drp_name(&self) -> Result<&str, Diagnostic> { + let parts: Vec<&str> = self.path().iter().map(Identifier::as_str).collect(); + parts.first().copied().ok_or_else(|| { + Error::CannotParse { + msg: "Empty use path".to_string(), + } + .with_span(self.span) + }) + } + + pub(crate) fn set_path(&mut self, path: &[Identifier]) { + self.path = Vec::from(path) + } +} + +// `span` are required because `UseDecl` hashing is context-dependent. +// For instance, identical `use crate::...` paths differ between binary and library roots. +// Tested by: `functional_tests::identical_crate_uses_in_different_package_roots_do_not_poison_resolution_cache`. +impl_eq_hash!(UseDecl; visibility, path, items, span); + +/// Aliases the specific identifier of an imported type to a new, local identifier +pub type AliasedSymbolName = (SymbolName, Option); + +/// Specified the items being brought into scope at the end of a `use` declaration +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum UseItems { + /// A single item import. + /// + /// # Example + /// ```text + /// use core::math::add; + /// ``` + Single(AliasedSymbolName), + + /// A multiple item import grouped in a list. + /// + /// # Example + /// ```text + /// use core::math::{add, subtract}; + /// ``` + List(Vec), +} + +#[derive(Clone, Debug)] +pub struct Function { + visibility: Visibility, + name: FunctionName, + params: Arc<[FunctionParam]>, + ret: Option, + body: Expression, + span: Span, +} + +impl Function { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + /// Access the name of the function. + pub fn name(&self) -> &FunctionName { + &self.name + } + + /// Access the parameters of the function. + pub fn params(&self) -> &[FunctionParam] { + &self.params + } + + /// Access the return type of the function. + /// + /// An empty return type means that the function returns the unit value. + pub fn ret(&self) -> Option<&AliasedType> { + self.ret.as_ref() + } + + /// Access the body of the function. + pub fn body(&self) -> &Expression { + &self.body + } + + /// Access the span of the function. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(Function; visibility, name, params, ret, body); + +impl_require_feature!(Function { recurse: params, ret, body; }); + +/// Parameter of a function. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct FunctionParam { + identifier: Identifier, + ty: AliasedType, + span: Span, +} + +impl FunctionParam { + /// Access the identifier of the parameter. + pub fn identifier(&self) -> &Identifier { + &self.identifier + } + + /// Access the type of the parameter. + pub fn ty(&self) -> &AliasedType { + &self.ty + } + + /// Access the source span of the complete parameter. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(FunctionParam; identifier, ty); + +impl_require_feature!(FunctionParam { recurse: ty; }); + +/// A statement is a component of a block expression. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum Statement { + /// A declaration of variables inside a pattern. + Assignment(Assignment), + /// An expression that returns nothing (the unit value). + Expression(Expression), +} + +impl Statement { + /// Access the span of the statement contents. + /// + /// The terminating semicolon is deliberately not part of this span: it is a + /// separator owned by the containing block's layout. + pub fn span(&self) -> &Span { + match self { + Self::Assignment(assignment) => assignment.span(), + Self::Expression(expression) => expression.span(), + } + } +} + +impl_require_feature!(Statement { + variants: + Assignment(assignment), + Expression(expr), +}); + +/// The output of an expression is assigned to a pattern. +#[derive(Clone, Debug)] +pub struct Assignment { + pattern: Pattern, + ty: AliasedType, + expression: Expression, + span: Span, +} + +impl Assignment { + /// Access the pattern of the assignment. + pub fn pattern(&self) -> &Pattern { + &self.pattern + } + + /// Access the return type of assigned expression. + pub fn ty(&self) -> &AliasedType { + &self.ty + } + + /// Access the assigned expression. + pub fn expression(&self) -> &Expression { + &self.expression + } + + /// Access the span of the expression. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(Assignment; pattern, ty, expression); + +impl_require_feature!(Assignment { recurse: pattern, ty, expression; }); + +/// Call expression. +#[derive(Clone, Debug)] +pub struct Call { + name: CallName, + args: Arc<[Expression]>, + span: Span, +} + +impl Call { + /// Access the name of the call. + pub fn name(&self) -> &CallName { + &self.name + } + + /// Access the arguments to the call. + pub fn args(&self) -> &[Expression] { + self.args.as_ref() + } + + /// Access the span of the call. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(Call; name, args); + +// Hand-written because `raw_hash` is gated per call name, while the macro +// can gate only an entire type, and `CallName` has no span of its own. +impl RequireFeature for Call { + fn feature_requirements(&self, out: &mut Vec) { + if let CallName::RawHash(_) = self.name { + out.push(FeatureRequirement::new(UnstableFeature::RawHash, self.span)); + } + + self.name.feature_requirements(out); + self.args.feature_requirements(out); + } +} + +/// Name of a call. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum CallName { + /// Name of a jet. + Jet(JetName), + /// [`Either::unwrap_left`]. + UnwrapLeft(AliasedType), + /// [`Either::unwrap_right`]. + UnwrapRight(AliasedType), + /// [`Option::unwrap`]. + Unwrap, + /// [`Option::is_none`]. + IsNone(AliasedType), + /// [`assert!`]. + Assert, + /// [`panic!`] without error message. + Panic, + /// [`dbg!`]. + Debug, + /// Cast from the given source type. + TypeCast(AliasedType), + /// Name of a custom function. + Custom(FunctionName), + /// Fold of a bounded list with the given function. + Fold(FunctionName, NonZeroPow2Usize), + /// Fold of an array with the given function. + ArrayFold(FunctionName, NonZeroUsize), + /// Loop over the given function a bounded number of times until it returns success. + ForWhile(FunctionName), + /// SHA-256 of the concatenated bytes of a tuple of unsigned integers of the given type. + RawHash(AliasedType), +} + +impl_require_feature!(CallName { + variants: + Jet(_), + UnwrapLeft(ty), + UnwrapRight(ty), + Unwrap, + IsNone(ty), + Assert, + Panic, + Debug, + TypeCast(ty), + Custom(_), + Fold(_, _), + ArrayFold(_, _), + ForWhile(_), + RawHash(ty), +}); + +/// A type alias. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct TypeAlias { + visibility: Visibility, + name: AliasName, + ty: AliasedType, + span: Span, +} + +impl TypeAlias { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + /// Access the name of the alias. + pub fn name(&self) -> &AliasName { + &self.name + } + + /// Access the type that the alias resolves to. + /// + /// During the parsing stage, the resolved type may include aliases. + /// The compiler will later check if all contained aliases have been declared before. + pub fn ty(&self) -> &AliasedType { + &self.ty + } + + /// Access the span of the alias. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(TypeAlias; name, ty); + +impl_require_feature!(TypeAlias { recurse: ty; }); + +/// A single variant in an enum declaration. +/// +/// A variant's position among the declared variants determines its leaf +/// in the enum's balanced sum, i.e. its wire encoding. +/// A variant may carry payload types (`Refresh(Signature, u8)`). +/// A variant without payload is a unit variant. +#[derive(Clone, Debug)] +pub struct EnumVariant { + name: Identifier, + payload: Arc<[AliasedType]>, + span: Span, +} + +impl EnumVariant { + pub fn name(&self) -> &Identifier { + &self.name + } + + /// Access the payload types of the variant. Empty for unit variants. + pub fn payload(&self) -> &[AliasedType] { + &self.payload + } + + /// Access the source span of the complete variant. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(EnumVariant; name, payload); + +impl AsRef for EnumVariant { + fn as_ref(&self) -> &Span { + &self.span + } +} + +/// An enum declaration. +#[derive(Clone, Debug)] +pub struct EnumDeclaration { + visibility: Visibility, + name: AliasName, + variants: Arc<[EnumVariant]>, + span: Span, +} + +impl EnumDeclaration { + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + pub fn name(&self) -> &AliasName { + &self.name + } + + pub fn variants(&self) -> &[EnumVariant] { + &self.variants + } + + /// Access the source span of the complete declaration. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_require_feature!(EnumVariant { recurse: payload; }); + +impl_require_feature!(EnumDeclaration { + requires: UnstableFeature::Enums, span: span; + recurse: variants; +}); + +impl_eq_hash!(EnumDeclaration; name, variants); + +impl AsRef for EnumDeclaration { + fn as_ref(&self) -> &Span { + &self.span + } +} + +/// An expression is something that returns a value. +#[derive(Clone, Debug)] +pub struct Expression { + inner: ExpressionInner, + span: Span, +} + +impl Expression { + /// Access the inner expression. + pub fn inner(&self) -> &ExpressionInner { + &self.inner + } + + /// Access the span of the expression. + pub fn span(&self) -> &Span { + &self.span + } + + /// Convert the expression into a block expression. + #[cfg(feature = "arbitrary")] + fn into_block(self) -> Self { + match self.inner { + ExpressionInner::Single(_) => Expression { + span: self.span, + inner: ExpressionInner::Block(Arc::from([]), Some(Arc::new(self))), + }, + _ => self, + } + } + + pub fn empty(span: Span) -> Self { + Self { + inner: ExpressionInner::Single(SingleExpression { + inner: SingleExpressionInner::Tuple(Arc::new([])), + span, + }), + span, + } + } +} + +impl_eq_hash!(Expression; inner); + +impl_require_feature!(Expression { recurse: inner; }); + +/// The kind of expression. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum ExpressionInner { + /// A single expression directly returns a value. + Single(SingleExpression), + /// A block expression first executes a series of statements inside a local scope. + /// Then, the block returns the value of its final expression. + /// The block returns nothing (unit) if there is no final expression. + Block(Arc<[Statement]>, Option>), +} + +impl_require_feature!(ExpressionInner { + variants: + Single(single), + Block(statements, maybe_expr), +}); + +/// A single expression directly returns a value. +#[derive(Clone, Debug)] +pub struct SingleExpression { + inner: SingleExpressionInner, + span: Span, +} + +impl SingleExpression { + /// Access the inner expression. + pub fn inner(&self) -> &SingleExpressionInner { + &self.inner + } + + /// Access the span of the expression. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(SingleExpression; inner); + +impl_require_feature!(SingleExpression {recurse: inner; }); + +/// The kind of single expression. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum SingleExpressionInner { + /// Either wrapper expression + Either(Either, Arc>), + /// Option wrapper expression + Option(Option>), + /// Boolean literal expression + Boolean(bool), + /// Decimal string literal. + Decimal(Decimal), + /// Binary string literal. + Binary(Binary), + /// Hexadecimal string literal. + Hexadecimal(Hexadecimal), + /// Witness value. + Witness(TemplateProgramWitness), + /// Parameter value. + Parameter(TemplateProgramWitness), + /// Variable identifier expression + Variable(Identifier), + /// Function call + Call(Call), + /// Expression in parentheses + Expression(Arc), + /// Match expression over a sum type + Match(Match), + /// Match expression over an enum's variants + EnumMatch(EnumMatch), + /// Construction of an enum variant + EnumConstruction(EnumConstruction), + /// Tuple wrapper expression + Tuple(Arc<[Expression]>), + /// Array wrapper expression + Array(Arc<[Expression]>), + /// List wrapper expression + /// + /// The exclusive upper bound on the list size is not known at this point + List(Arc<[Expression]>), +} + +impl_require_feature!(SingleExpressionInner { + variants: + Either(either), + Option(maybe_expr), + Boolean(_), + Decimal(_), + Binary(_), + Hexadecimal(_), + Witness(_), + Parameter(_), + Variable(_), + Call(call), + Expression(expr), + Match(match_), + EnumMatch(enum_match), + EnumConstruction(construction), + Tuple(exprs), + Array(exprs), + List(exprs), +}); + +/// Match expression. +#[derive(Clone, Debug)] +pub struct Match { + scrutinee: Arc, + left: MatchArm, + right: MatchArm, + span: Span, +} + +impl Match { + /// Access the expression that is matched. + pub fn scrutinee(&self) -> &Expression { + &self.scrutinee + } + + /// Access the match arm for left sum values. + pub fn left(&self) -> &MatchArm { + &self.left + } + + /// Access the match arm for right sum values. + pub fn right(&self) -> &MatchArm { + &self.right + } + + /// Access the span of the match statement. + pub fn span(&self) -> &Span { + &self.span + } + + /// Get the type of the expression that is matched. + pub fn scrutinee_type(&self) -> AliasedType { + match (&self.left.pattern, &self.right.pattern) { + (MatchPattern::Left(_, ty_l), MatchPattern::Right(_, ty_r)) => { + AliasedType::either(ty_l.clone(), ty_r.clone()) + } + (MatchPattern::None, MatchPattern::Some(_, ty_r)) => AliasedType::option(ty_r.clone()), + (MatchPattern::False, MatchPattern::True) => AliasedType::boolean(), + _ => unreachable!("Match expressions have valid left and right arms"), + } + } +} + +impl_eq_hash!(Match; scrutinee, left, right); + +impl_require_feature!(Match {recurse: scrutinee, left, right; }); + +/// Match expression over a named enum type. +#[derive(Clone, Debug)] +pub struct EnumMatch { + scrutinee: Arc, + arms: Arc<[EnumMatchArm]>, + span: Span, +} + +impl EnumMatch { + /// Access the expression that is matched. + pub fn scrutinee(&self) -> &Expression { + &self.scrutinee + } + + /// Access the match arms. + pub fn arms(&self) -> &[EnumMatchArm] { + &self.arms + } + + /// Access the span of the match statement. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_require_feature!(EnumMatch { + requires: UnstableFeature::Enums, span: span; + recurse: scrutinee, arms; +}); + +impl_eq_hash!(EnumMatch; scrutinee, arms); + +impl AsRef for EnumMatch { + fn as_ref(&self) -> &Span { + &self.span + } +} + +/// Arm of an enum match expression: `Enum::Variant(bindings..) => expression`. +/// +/// The arm type carries the classification the parser proved. +/// Every arm of an [`EnumMatch`] names an enum variant, so no other pattern kind is representable here. +#[derive(Clone, Debug)] +pub struct EnumMatchArm { + enum_path: Arc<[Identifier]>, + variant: Identifier, + bindings: Arc<[(Pattern, AliasedType)]>, + expression: Arc, + span: Span, +} + +impl EnumMatchArm { + /// Access the written enum path of the arm, e.g. `["m", "Choice"]`. + pub fn enum_path(&self) -> &[Identifier] { + &self.enum_path + } + + /// The written enum path as one string, e.g. `m::Choice`. + pub fn enum_path_string(&self) -> String { + self.enum_path + .iter() + .map(Identifier::as_str) + .collect::>() + .join("::") + } + + /// Access the name of the matched variant. + pub fn variant(&self) -> &Identifier { + &self.variant + } + + /// Access the payload bindings of the arm. Empty for unit variants. + pub fn bindings(&self) -> &[(Pattern, AliasedType)] { + &self.bindings + } + + /// Access the expression that is executed in the match arm. + pub fn expression(&self) -> &Expression { + &self.expression + } + + /// Access the source span of the complete arm, including its optional comma. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(EnumMatchArm; enum_path, variant, bindings, expression); + +impl_require_feature!(EnumMatchArm { recurse: expression; }); + +/// Construction of an enum variant: `Action::Refresh(sig, 3)`. +/// +/// The written enum name is either an alias in scope (`MyChoice`) or the +/// enum's declared name itself (`Action`), which needs no scope. +/// Unit variants take no argument list. +#[derive(Clone, Debug)] +pub struct EnumConstruction { + enum_path: Arc<[Identifier]>, + variant: Identifier, + args: Arc<[Expression]>, + span: Span, +} + +impl EnumConstruction { + /// Access the written enum path, e.g. `["m", "Action"]`. + pub fn enum_path(&self) -> &[Identifier] { + &self.enum_path + } + + /// Access the name of the constructed variant. + pub fn variant(&self) -> &Identifier { + &self.variant + } + + /// Access the payload arguments. Empty for unit variants. + pub fn args(&self) -> &[Expression] { + &self.args + } + + /// Access the span of the construction expression. + pub fn span(&self) -> &Span { + &self.span + } + + /// The written enum path as one string, e.g. `m::Action`. + pub fn enum_path_string(&self) -> String { + self.enum_path + .iter() + .map(Identifier::as_str) + .collect::>() + .join("::") + } +} + +impl_eq_hash!(EnumConstruction; enum_path, variant, args); + +impl_require_feature!(EnumConstruction { + requires: UnstableFeature::Enums, span: span; + recurse: args; +}); + +impl AsRef for EnumConstruction { + fn as_ref(&self) -> &Span { + &self.span + } +} + +/// Arm of a match expression. +#[derive(Clone, Debug)] +pub struct MatchArm { + pattern: MatchPattern, + expression: Arc, + span: Span, +} + +impl MatchArm { + /// Access the pattern that guards the match arm. + pub fn pattern(&self) -> &MatchPattern { + &self.pattern + } + + /// Access the expression that is executed in the match arm. + pub fn expression(&self) -> &Expression { + &self.expression + } + + /// Access the source span of the complete arm, including its optional comma. + pub fn span(&self) -> &Span { + &self.span + } +} + +impl_eq_hash!(MatchArm; pattern, expression); + +impl_require_feature!(MatchArm {recurse: pattern, expression; }); + +/// Pattern of a match arm. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub enum MatchPattern { + /// Bind inner value of left value to a pattern. + Left(Pattern, AliasedType), + /// Bind inner value of right value to a pattern. + Right(Pattern, AliasedType), + /// Match none value (no binding). + None, + /// Bind inner value of some value to a pattern. + Some(Pattern, AliasedType), + /// Match false value (no binding). + False, + /// Match true value (no binding). + True, +} + +impl_require_feature!(MatchPattern { + variants: + Left(pattern, ty), + Right(pattern, ty), + None, + Some(pattern, ty), + False, + True, +}); + +impl MatchPattern { + /// Access the pattern of a match pattern that binds a variables. + pub fn as_pattern(&self) -> Option<&Pattern> { + match self { + MatchPattern::Left(i, _) | MatchPattern::Right(i, _) | MatchPattern::Some(i, _) => { + Some(i) + } + MatchPattern::None | MatchPattern::False | MatchPattern::True => None, + } + } + + /// Access the pattern and the type of a match pattern that binds a variables. + pub fn as_typed_pattern(&self) -> Option<(&Pattern, &AliasedType)> { + match self { + MatchPattern::Left(i, ty) | MatchPattern::Right(i, ty) | MatchPattern::Some(i, ty) => { + Some((i, ty)) + } + MatchPattern::None | MatchPattern::False | MatchPattern::True => None, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct Module { + visibility: Visibility, + name: ModuleName, + items: Arc<[Item]>, + span: Span, +} + +impl_require_feature!(Module { + requires: UnstableFeature::Imports, span: span; + recurse: items; +}); + +impl Module { + /// Needed by the driver to wrap a single file into a module. + pub(crate) fn new( + file_id: usize, + visibility: Visibility, + name: ModuleName, + items: &[Item], + ) -> Module { + Self { + visibility, + name, + items: Arc::from(items), + span: Span::new(file_id, 0..0), + } + } + + pub fn visibility(&self) -> &Visibility { + &self.visibility + } + + /// Access the name of the module. + pub fn name(&self) -> &ModuleName { + &self.name + } + + /// Access the assignments of the module. + pub fn items(&self) -> &[Item] { + &self.items + } + + /// Access the span of the module. + pub fn span(&self) -> &Span { + &self.span + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum ExprTree<'a> { + Expression(&'a Expression), + Block(&'a [Statement], &'a Option>), + Statement(&'a Statement), + Assignment(&'a Assignment), + Single(&'a SingleExpression), + Call(&'a Call), + Match(&'a Match), + EnumMatch(&'a EnumMatch), + EnumConstruction(&'a EnumConstruction), +} + +impl TreeLike for ExprTree<'_> { + fn as_node(&self) -> Tree { + use SingleExpressionInner as S; + + match self { + Self::Expression(expr) => match expr.inner() { + ExpressionInner::Block(statements, maybe_expr) => { + Tree::Unary(Self::Block(statements, maybe_expr)) + } + ExpressionInner::Single(single) => Tree::Unary(Self::Single(single)), + }, + Self::Block(statements, maybe_expr) => Tree::Nary( + statements + .iter() + .map(Self::Statement) + .chain(maybe_expr.iter().map(Arc::as_ref).map(Self::Expression)) + .collect(), + ), + Self::Statement(statement) => match statement { + Statement::Assignment(assignment) => Tree::Unary(Self::Assignment(assignment)), + Statement::Expression(expression) => Tree::Unary(Self::Expression(expression)), + }, + Self::Assignment(assignment) => Tree::Unary(Self::Expression(assignment.expression())), + Self::Single(single) => match single.inner() { + S::Boolean(_) + | S::Binary(_) + | S::Decimal(_) + | S::Hexadecimal(_) + | S::Variable(_) + | S::Witness(_) + | S::Parameter(_) + | S::Option(None) => Tree::Nullary, + S::Option(Some(l)) + | S::Either(Either::Left(l)) + | S::Either(Either::Right(l)) + | S::Expression(l) => Tree::Unary(Self::Expression(l)), + S::Call(call) => Tree::Unary(Self::Call(call)), + S::Match(match_) => Tree::Unary(Self::Match(match_)), + S::EnumMatch(enum_match) => Tree::Unary(Self::EnumMatch(enum_match)), + S::EnumConstruction(construction) => { + Tree::Unary(Self::EnumConstruction(construction)) + } + S::Tuple(elements) | S::Array(elements) | S::List(elements) => { + Tree::Nary(elements.iter().map(Self::Expression).collect()) + } + }, + Self::Call(call) => Tree::Nary(call.args().iter().map(Self::Expression).collect()), + Self::EnumConstruction(construction) => { + Tree::Nary(construction.args().iter().map(Self::Expression).collect()) + } + Self::Match(match_) => Tree::Nary(Arc::new([ + Self::Expression(match_.scrutinee()), + Self::Expression(match_.left().expression()), + Self::Expression(match_.right().expression()), + ])), + Self::EnumMatch(enum_match) => Tree::Nary( + std::iter::once(Self::Expression(enum_match.scrutinee())) + .chain( + enum_match + .arms() + .iter() + .map(|arm| Self::Expression(arm.expression())), + ) + .collect(), + ), + } + } +} + +impl<'a, A: AsRef> From<&'a A> for Span { + fn from(value: &'a A) -> Self { + *value.as_ref() + } +} + +impl AsRef for Program { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Function { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for FunctionParam { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Assignment { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for TypeAlias { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Expression { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for SingleExpression { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Call { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Match { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for MatchArm { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for EnumMatchArm { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for UseDecl { + fn as_ref(&self) -> &Span { + &self.span + } +} + +impl AsRef for Module { + fn as_ref(&self) -> &Span { + &self.span + } +} diff --git a/src/parse/pipeline.rs b/src/parse/pipeline.rs new file mode 100644 index 00000000..f082cba6 --- /dev/null +++ b/src/parse/pipeline.rs @@ -0,0 +1,177 @@ +use chumsky::input::Input; +use chumsky::Parser; + +use crate::driver::MAIN_MODULE; +use crate::error::{Diagnostic, DiagnosticManager, Error, Span}; +use crate::lexer::Tokens; +use crate::unstable::{RequireFeature, UnstableFeatures}; +use crate::version::SimcDirective; + +use super::grammar::ChumskyParse; + +/// Copy of [`FromStr`] that internally uses the `chumsky` parser. +/// +/// [`FromStr`]: std::str::FromStr +pub trait ParseFromStr: Sized { + /// Parse a value from the string `s`. + fn parse_from_str(s: &str) -> Result; +} + +/// Trait for parsing with collection of errors. +pub trait ParseFromStrWithErrors: Sized { + /// Parse a value from the string `content` with Errors. + /// + /// Feature-gated syntax in the parsed AST is checked against + /// `unstable_features`; uses of disabled features are pushed to `diagnostics`. + fn parse_from_str_with_errors( + file_id: usize, + content: &str, + unstable_features: &UnstableFeatures, + diagnostics: &mut DiagnosticManager, + ) -> Option; +} + +trait AstNode: ChumskyParse + crate::unstable::RequireFeature + std::fmt::Debug {} + +impl AstNode for T where T: ChumskyParse + crate::unstable::RequireFeature + std::fmt::Debug {} + +/// This implementation only returns first encountered error. +impl ParseFromStr for A { + fn parse_from_str(s: &str) -> Result { + let (tokens, mut lex_errs) = crate::lexer::lex(MAIN_MODULE, s, 0); + + // The `simc` directive is source-file syntax, so fragments have no prescan + // and `simc` lexes as a reserved keyword. Its `"";` remnant does not + // lex either, so the first reserved-keyword error is reported alone. + if let Some(err) = lex_errs + .iter() + .find(|diag| matches!(diag.error(), Error::ReservedSimcKeyword)) + { + return Err(err.clone()); + } + + let Some(tokens) = tokens else { + return Err(lex_errs + .pop() + .unwrap_or(Diagnostic::global(Error::CannotParse { + msg: "Empty token stream without an error".to_string(), + }))); + }; + + let (ast, parse_errs) = A::parser() + .map_with(|parsed, _| parsed) + .parse( + tokens + .as_slice() + .map(Span::eof(MAIN_MODULE, s.len()), |(t, s)| (t, s)), + ) + .into_output_errors(); + + if parse_errs.is_empty() { + Ok(ast.ok_or(Diagnostic::global(Error::CannotParse { + msg: "Empty AST without an error.".to_string(), + }))?) + } else { + let err = parse_errs.first().unwrap().clone(); + Err(err) + } + } +} + +impl ParseFromStrWithErrors for A { + fn parse_from_str_with_errors( + file_id: usize, + content: &str, + unstable_features: &UnstableFeatures, + diagnostics: &mut DiagnosticManager, + ) -> Option { + let before = diagnostics.error_count(); + + let start = directive_prescan(content, file_id, diagnostics)?; + + let (tokens, lex_errs) = crate::lexer::lex(file_id, content, start); + + let lex_ok = is_lex_ok(lex_errs, diagnostics)?; + + let tokens = tokens?; + + let (ast, parse_status) = parse_ast::(file_id, content, tokens, diagnostics); + + if lex_ok && parse_status { + let () = post_check(unstable_features, ast.as_ref(), diagnostics); + } + + // TODO: We should return parsed result if we found errors, but because analyzing in `ast` module + // is not handling poisoned tree right now, we don't return parsed result + if diagnostics.error_count() > before { + None + } else { + ast + } + } +} + +/// Handle the `simc` directive before lexing: an incompatible or malformed +/// directive is reported as the only diagnostic (the rest is noise), and +/// lexing starts right after a valid one, so the lexer and grammar never +/// see it. +pub(super) fn directive_prescan( + content: &str, + file_id: usize, + diagnostics: &mut DiagnosticManager, +) -> Option { + match SimcDirective::prescan(content, file_id) { + Ok(start) => Some(start), + Err(box_error) => { + let (err, span) = *box_error; + diagnostics.push(Diagnostic::new(err, span)); + None + } + } +} + +pub(super) fn is_lex_ok( + mut lex_errs: Vec, + diagnostics: &mut DiagnosticManager, +) -> Option { + // A stray `simc` makes every other diagnostic noise — its `"";` remnant + // does not lex — so the reserved-keyword errors are reported alone. + if lex_errs + .iter() + .any(|e| matches!(e.error(), Error::ReservedSimcKeyword)) + { + lex_errs.retain(|e| matches!(e.error(), Error::ReservedSimcKeyword)); + diagnostics.extend(lex_errs); + None + } else { + let lex_ok = lex_errs.is_empty(); + diagnostics.extend(lex_errs); + Some(lex_ok) + } +} + +pub(super) fn parse_ast( + file_id: usize, + src: &str, + tokens: Tokens<'_>, + diagnostics: &mut DiagnosticManager, +) -> (Option, bool) { + let eoi = Span::eof(file_id, src.len()); + let (ast, parse_errs) = T::parser() + .parse(tokens.as_slice().map(eoi, |(t, s)| (t, s))) + .into_output_errors(); + + let parse_ok = parse_errs.is_empty(); + diagnostics.extend(parse_errs); + (ast, parse_ok) +} + +pub(super) fn post_check( + unstable_features: &UnstableFeatures, + program: Option<&T>, + diagnostics: &mut DiagnosticManager, +) { + if let Some(ast) = program { + unstable_features.check_program(ast, diagnostics); + } +} diff --git a/src/parse/tests/lossless.rs b/src/parse/tests/lossless.rs new file mode 100644 index 00000000..f4bc8c0d --- /dev/null +++ b/src/parse/tests/lossless.rs @@ -0,0 +1,464 @@ +use crate::driver::MAIN_MODULE; +use crate::error::{Diagnostic, DiagnosticManager, Error}; +use crate::lexer::{FmtToken, TriviaKind}; +use crate::parse::grammar::RESERVED_PATTERN_NAMES; +use crate::parse::{ + ExpressionInner, Item, ParseFromStrWithErrors, ParsedSource, Program, Visibility, +}; +use crate::UnstableFeatures; + +/// Parse `input`, appending any formatter diagnostics to `diagnostics`. +fn parse_with_diagnostics<'src>( + input: &'src str, + features: &UnstableFeatures, + diagnostics: &mut DiagnosticManager, +) -> Option> { + Program::parse_with_errors_for_fmt(MAIN_MODULE, input, features, diagnostics) +} + +/// Parse `input` and return whether it was rejected and the collected error text. +fn parse_with(input: &str, features: &UnstableFeatures) -> (bool, String) { + let mut diagnostics = DiagnosticManager::new(); + let program = parse_with_diagnostics(input, features, &mut diagnostics); + + let rejected = program.is_none(); + let text = diagnostics.to_string(); + + (rejected, text) +} + +/// Parse `input` and return whether it was rejected and the collected error in `DiagnosticManager`. +fn parse_with_diagnosis(input: &str, features: &UnstableFeatures) -> (bool, DiagnosticManager) { + let mut diagnostics = DiagnosticManager::new(); + let program = parse_with_diagnostics(input, features, &mut diagnostics); + + let rejected = program.is_none(); + + (rejected, diagnostics) +} + +/// Parse `input` and return whether it was rejected and the collected error text together with `ParsedSource`. +fn parse_with_extended_out<'src>( + input: &'src str, + features: &UnstableFeatures, +) -> (bool, String, Option>) { + let mut diagnostics = DiagnosticManager::new(); + let program = parse_with_diagnostics(input, features, &mut diagnostics); + + let rejected = program.is_none(); + let text = diagnostics.to_string(); + + (rejected, text, program) +} + +fn assert_formatter_matches_regular_parser(input: &str, features: &UnstableFeatures) { + let mut regular_diagnostics = DiagnosticManager::new(); + let regular = + Program::parse_from_str_with_errors(MAIN_MODULE, input, features, &mut regular_diagnostics); + + let mut formatting_diagnostics = DiagnosticManager::new(); + let formatting = parse_with_diagnostics(input, features, &mut formatting_diagnostics); + + assert_eq!( + formatting.as_ref().map(ParsedSource::program), + regular.as_ref(), + "formatter and regular parsers must produce the same AST for {input:?}" + ); + assert_eq!( + formatting_diagnostics.error_count(), + regular_diagnostics.error_count(), + "formatter and regular parsers must report the same number of errors for {input:?}, \n\ + fmt errors: \n\t '{formatting_diagnostics}, \n regular errors: \n\t '{regular_diagnostics}" + ); +} + +fn assert_lossless_source(input: &str, parsed: &ParsedSource<'_>) { + let prefix = parsed.prefix(); + assert_eq!(prefix.file_id, MAIN_MODULE); + + let mut reconstructed = prefix + .to_slice(input) + .expect("the directive prefix must be a valid source slice") + .to_owned(); + let mut cursor = prefix.end; + + for (_, span) in parsed.tokens() { + assert_eq!(span.file_id, MAIN_MODULE); + assert!( + span.start >= prefix.end, + "formatter tokens must not overlap the preserved directive prefix" + ); + assert_eq!( + span.start, cursor, + "formatter token spans must be contiguous and in source order" + ); + + let text = span + .to_slice(input) + .expect("formatter token spans must be valid source slices"); + assert!( + !text.is_empty(), + "the formatter token stream must not contain empty source slices" + ); + reconstructed.push_str(text); + cursor = span.end; + } + + assert_eq!(cursor, input.len(), "formatter tokens must reach EOF"); + assert_eq!(reconstructed, input, "formatter input must be lossless"); +} + +#[test] +fn test_double_colon() { + let input = "fn main() { let ab: u8 = <(u4, u4)> : :into((0b1011, 0b1101)); }"; + + let (rejected, text) = parse_with(input, &UnstableFeatures::all()); + + assert!(rejected); + assert!(text.contains("Expected '::', found ':'")); +} + +#[test] +fn test_double_double_colon() { + let input = "fn main() { let pk: Pubkey = witnes::::PK; }"; + + let (rejected, text) = parse_with(input, &UnstableFeatures::all()); + + assert!(rejected); + assert!( + text.contains("Expected identifier, found ::"), + "the second :: should be reported as the error site" + ); +} + +#[test] +fn inverted_empty_span_from_token_gap_does_not_panic() { + // Fuzz-found (compile_text). + // + // The input is irreducible. The broken `fn` prefix forces the parser into delimiter recovery. + // The only path that requests an empty span at a lex-error gap and the + // NUL after `enum` creates that gap. + // Chumsky builds such spans as `next_token.start .. previous_token.end`, which is inverted + // across the gap and panicked the strict `Span` constructor. + // + // Simplifying any part (even NUL to a space) loses the crash. + let src = "fn`u({?\u{12}$0;;enum\0===lHf\u{15}"; + let (rejected, text) = parse_with(src, &UnstableFeatures::all()); + + assert!(rejected, "garbage input must be rejected"); + assert!(!text.is_empty()); +} + +#[test] +fn recovery_synchronizes_at_enum_declaration() { + // Discriminating setup: an invalid prefix followed by a malformed + // enum. Without `Token::Enum` in the synchronization set the whole + // enum is swallowed into the prefix's recovery span and only one + // error is reported; with it, the enum parses as its own item and + // its malformation is reported as a second error. + + let src = "let invalid = 1;\nenum Action { A B }\nfn main() {}"; + let (rejected, diagnostics) = parse_with_diagnosis(src, &UnstableFeatures::all()); + + assert!(rejected, "the invalid program must be rejected"); + assert_eq!( + 2, + diagnostics.error_count(), + "the invalid prefix and the malformed enum must each report an error" + ); +} + +#[test] +fn recovery_after_malformed_enum_reaches_next_item() { + let src = "enum Action { A B }\nfn main() {}"; + let (rejected, _text) = parse_with(src, &UnstableFeatures::all()); + assert!(rejected, "a malformed enum must fail compilation"); +} + +#[test] +fn malformed_construct_containing_enum_keyword_reports_errors() { + // `enum` inside a badly malformed nested construct may be mistaken + // for an item boundary; compilation must still fail cleanly. + let src = "fn broken() { let x = (enum; }\nfn main() {}"; + let (rejected, _text) = parse_with(src, &UnstableFeatures::all()); + assert!(rejected, "a malformed construct must fail compilation"); +} + +#[test] +fn test_gated_syntax_is_rejected_without_features() { + // Real `use`/`mod` syntax: rejected under none() (naming the feature + a + // -Z hint), accepted under all(). Delete when `imports` stabilizes. + for input in [ + "use crate::foo::bar;\nfn main() { }", + "mod inner { }\nfn main() { }", + ] { + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + assert!( + rejected, + "gated syntax must be rejected without features (if this feature was \ + just stabilized, delete this test):\n{input}" + ); + assert!( + error.contains("imports") && error.contains("-Z"), + "rejection should name the feature and suggest -Z, got:\n{error}" + ); + + let (rejected, error) = parse_with(input, &UnstableFeatures::all()); + assert!( + !rejected, + "the same syntax must parse with all features enabled:\n{error}" + ); + } +} + +#[test] +fn test_type_heavy_program_needs_no_features() { + // Types are traversed by `RequireFeature` but not gated, so a type-heavy, + // import-free program must parse with no features (no false-positive gates). + let input = r#"type Alias = u32; +fn pick(e: Either) -> u32 { + match e { + Left(a: u32) => a, + Right(b: u32) => b, + } +} +fn main() { + let casted: u32 = <(u16, u16)>::into((0xbeef, 0xbabe)); + let chosen: Alias = pick(Left(casted)); + assert!(jet::eq_32(chosen, chosen)); +} +"#; + // `parse_from_str_with_errors` returns `Some` only when no errors were + // collected, so a non-rejection already proves there were no gate errors. + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + assert!( + !rejected, + "type-heavy but import-free program should parse with no features:\n{error}" + ); +} + +#[test] +fn test_syntax_error_does_not_produce_spurious_feature_gate_error() { + // The gate check skips error-recovered ASTs, so a program with + // both a syntax error and gated syntax reports only the syntax error. + let input = "use crate::foo;\nfn main( {"; + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + assert!(rejected, "broken program must be rejected"); + assert!( + !error.contains("-Z"), + "syntax error should not produce a spurious feature-gate hint, got:\n{error}" + ); +} + +#[test] +fn test_lexer_error_does_not_produce_spurious_feature_gate_error() { + // Lexer-side companion to the case above: the stray `@` lexes with an + // error but recovers a cleanly-parsing `use` AST, and a program that + // didn't lex cleanly skips the gate check — so no spurious `-Z` hint. + let input = "use crate@::foo;\nfn main() { }"; + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + assert!(rejected, "program with a lexer error must be rejected"); + assert!( + !error.contains("-Z"), + "lexer error should not produce a spurious feature-gate hint, got:\n{error}" + ); +} + +#[test] +fn test_use_decl_display_round_trips_with_aliases() { + for input in [ + "use lib::A::foo;", + "use lib::A::foo as bar;", + "use lib::A::{foo, bar as baz};", + ] { + let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::all()); + assert!(!rejected, "parsing works"); + assert_eq!(program.unwrap().program().to_string(), format!("{input}\n")); + } +} + +#[test] +fn statement_spans_exclude_terminating_semicolons() { + let input = "fn main() { let value: u8 = 0; value; }"; + + let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::none()); + assert!(!rejected, "parsing works"); + let program = program.unwrap().program().clone(); + + let Item::Function(function) = &program.items()[0] else { + panic!("expected a function item"); + }; + let ExpressionInner::Block(statements, None) = function.body().inner() else { + panic!("expected a block containing only statements"); + }; + + assert_eq!( + statements[0].span().to_slice(input), + Some("let value: u8 = 0") + ); + assert_eq!(statements[1].span().to_slice(input), Some("value")); +} + +fn parse_item(input: &str) -> Item { + let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::all()); + assert!(!rejected, "parsing works"); + let program = program.unwrap().program().clone(); + + program.items().first().expect("expected one item").clone() +} + +#[test] +fn test_enum_declaration_basic() { + let item = parse_item("enum Path { Inherit, ColdSpend, RefreshSpend, }"); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration, got {item:?}"); + }; + assert_eq!(decl.name(), "Path"); + assert_eq!(decl.variants().len(), 3); + assert_eq!(decl.variants()[0].name(), "Inherit"); + assert_eq!(decl.variants()[2].name(), "RefreshSpend"); +} + +#[test] +fn test_enum_declaration_pub() { + let item = parse_item("pub enum Color { Red, Green, Blue, }"); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration"); + }; + assert_eq!(decl.visibility(), &Visibility::Public); + assert_eq!(decl.name(), "Color"); +} + +#[test] +fn test_enum_declaration_display_round_trip() { + let input = "enum Path { Inherit, ColdSpend, RefreshSpend, }"; + let item = parse_item(input); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration"); + }; + assert_eq!( + decl.to_string(), + "enum Path { Inherit, ColdSpend, RefreshSpend, }" + ); +} + +#[test] +fn test_enum_declaration_reserved_name() { + for reserved in RESERVED_PATTERN_NAMES { + let input = format!("enum {reserved} {{ A, B, }}"); + let (rejected, error) = parse_with(&input, &UnstableFeatures::none()); + assert!(rejected, "enum name {reserved} is reserved"); + assert!( + error.contains("reserved"), + "error should say the name is reserved: {error}" + ); + } +} + +#[test] +fn formatting_parse_is_lossless_after_directive_prescan() { + let input = "// header\r\nsimc \"*\";\r\n\t/* block */ fn main() { // body\r\n\t}\r"; + + let (_rejected, _error, parsed) = parse_with_extended_out(input, &UnstableFeatures::none()); + let parsed = parsed.expect("formatting parse should succeed"); + + assert_eq!( + parsed.prefix().to_slice(input), + Some("// header\r\nsimc \"*\";") + ); + for trivia in [ + TriviaKind::BlockComment, + TriviaKind::LineComment, + TriviaKind::Newline, + TriviaKind::Whitespace, + ] { + assert!( + parsed.tokens().iter().any( + |(token, _)| matches!(token, FmtToken::Trivia(token_trivia) if token_trivia.kind() == trivia) + ), + "formatter token stream should retain {trivia:?}" + ); + } + assert_lossless_source(input, &parsed); + assert_eq!(parsed.program().items().len(), 1); +} + +#[test] +fn formatting_parse_matches_regular_parser_pipeline() { + let no_features = UnstableFeatures::none(); + let all_features = UnstableFeatures::all(); + + for (input, features) in [ + ( + "/* header */\r\nfn\tmain() { // comment\r\n}\r", + &no_features, + ), + ("// version\r\nsimc \"*\";\r\nfn main() {}", &no_features), + ("use crate::foo::bar;\nfn main() {}", &no_features), + ("use crate::foo::bar;\nfn main() {}", &all_features), + ("fn main( {", &no_features), + ("fn main() { @// comment }", &no_features), + ] { + assert_formatter_matches_regular_parser(input, features); + } +} + +#[test] +fn formatting_parse_ignores_preexisting_errors() { + let input = "fn main() {}"; + let mut diagnostics = DiagnosticManager::new(); + diagnostics.push(Diagnostic::global(Error::CannotParse { + msg: "an error from an earlier source file".to_owned(), + })); + let before = diagnostics.error_count(); + + let parsed = parse_with_diagnostics(input, &UnstableFeatures::none(), &mut diagnostics); + + assert!( + parsed.is_some(), + "valid source must still produce ParsedSource" + ); + assert_eq!(diagnostics.error_count(), before); + assert_eq!(diagnostics.diagnostics().len(), before); +} + +#[test] +fn formatting_parse_stops_after_invalid_directive() { + for (input, is_malformed) in [ + ("simc \"*\"\nfn broken( @", true), + ("simc \">99.0.0\"; @", false), + ] { + let (rejected, diagnostics) = parse_with_diagnosis(input, &UnstableFeatures::none()); + + assert!(rejected, "invalid directive must abort formatting parse"); + assert_eq!( + diagnostics.error_count(), + 1, + "the invalid body must not add diagnostics after prescan aborts" + ); + if is_malformed { + assert!(matches!( + diagnostics.diagnostics()[0].error(), + Error::MalformedSimcDirective + )); + } else { + assert!(matches!( + diagnostics.diagnostics()[0].error(), + Error::SimcVersionMismatch { .. } + )); + } + } +} + +#[test] +fn formatting_parse_reports_missing_match_arm_comma() { + let input = "fn main() { match true { false => () true => (), } }"; + + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + + assert!(rejected); + assert!( + error.contains("Missing ',' after a match arm that isn't block expression"), + "expected the missing-comma diagnostic, got {error:?}" + ); +} diff --git a/src/parse/tests/mod.rs b/src/parse/tests/mod.rs new file mode 100644 index 00000000..d0658464 --- /dev/null +++ b/src/parse/tests/mod.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "fmt")] +mod lossless; +mod regular; +mod type_alias; diff --git a/src/parse/tests/regular.rs b/src/parse/tests/regular.rs new file mode 100644 index 00000000..79e7a378 --- /dev/null +++ b/src/parse/tests/regular.rs @@ -0,0 +1,322 @@ +use crate::driver::MAIN_MODULE; +use crate::error::{DiagnosticManager, Span}; +use crate::parse::grammar::RESERVED_PATTERN_NAMES; +use crate::parse::{ + self, ExpressionInner, Item, ParseFromStr, ParseFromStrWithErrors, Program, UseDecl, UseItems, + Visibility, +}; +use crate::str::Identifier; +use crate::UnstableFeatures; + +impl UseDecl { + /// Creates a dummy `UseDecl` specifically for testing `DependencyMap` resolution. + pub fn dummy_path(path: Vec) -> Self { + Self { + visibility: Visibility::default(), + path, + items: UseItems::List(Vec::new()), + span: Span::DUMMY, + } + } +} + +#[test] +fn test_double_colon() { + let input = "fn main() { let ab: u8 = <(u4, u4)> : :into((0b1011, 0b1101)); }"; + let mut diagnostics = DiagnosticManager::new(); + + let parsed_program = Program::parse_from_str_with_errors( + MAIN_MODULE, + input, + &UnstableFeatures::all(), + &mut diagnostics, + ); + + assert!(parsed_program.is_none()); + assert!(diagnostics.to_string().contains("Expected '::', found ':'")); +} + +#[test] +fn test_double_double_colon() { + let input = "fn main() { let pk: Pubkey = witnes::::PK; }"; + let mut diagnostics = DiagnosticManager::new(); + + let parsed_program = Program::parse_from_str_with_errors( + MAIN_MODULE, + input, + &UnstableFeatures::all(), + &mut diagnostics, + ); + + assert!(parsed_program.is_none()); + assert!( + diagnostics + .to_string() + .contains("Expected identifier, found ::"), + "the second :: should be reported as the error site" + ); +} + +/// Parse `input` and return whether it was rejected and the collected error text. +fn parse_with(input: &str, features: &UnstableFeatures) -> (bool, String) { + let mut diagnostics = DiagnosticManager::new(); + let program = + Program::parse_from_str_with_errors(MAIN_MODULE, input, features, &mut diagnostics); + + let rejected = program.is_none(); + let text = diagnostics.to_string(); + + (rejected, text) +} + +/// Nested blocks must parse in time linear in their depth. +#[test] +fn nested_blocks_parse_without_exponential_backtracking() { + let depth = 32; + let input = format!( + "fn main() {{\n let x: u32 = {}0{};\n assert!(jet::eq_32(x, 0));\n}}", + "{".repeat(depth), + "}".repeat(depth) + ); + + let (rejected, text) = parse_with(&input, &UnstableFeatures::all()); + assert!(!rejected, "nested blocks must parse: {text}"); +} + +#[test] +fn inverted_empty_span_from_token_gap_does_not_panic() { + // Fuzz-found (compile_text). + // + // The input is irreducible. The broken `fn` prefix forces the parser into delimiter recovery. + // The only path that requests an empty span at a lex-error gap and the + // NUL after `enum` creates that gap. + // Chumsky builds such spans as `next_token.start .. previous_token.end`, which is inverted + // across the gap and panicked the strict `Span` constructor. + // + // Simplifying any part (even NUL to a space) loses the crash. + let src = "fn`u({?\u{12}$0;;enum\0===lHf\u{15}"; + let mut diagnostics = DiagnosticManager::new(); + let program = Program::parse_from_str_with_errors( + MAIN_MODULE, + src, + &UnstableFeatures::all(), + &mut diagnostics, + ); + + assert!(program.is_none(), "garbage input must be rejected"); + assert!(diagnostics.has_errors()); +} + +#[test] +fn recovery_synchronizes_at_enum_declaration() { + // Discriminating setup: an invalid prefix followed by a malformed + // enum. Without `Token::Enum` in the synchronization set the whole + // enum is swallowed into the prefix's recovery span and only one + // error is reported; with it, the enum parses as its own item and + // its malformation is reported as a second error. + let mut diagnostics = DiagnosticManager::new(); + let program = Program::parse_from_str_with_errors( + MAIN_MODULE, + "let invalid = 1;\nenum Action { A B }\nfn main() {}", + &UnstableFeatures::all(), + &mut diagnostics, + ); + + assert!(program.is_none(), "the invalid program must be rejected"); + assert_eq!( + 2, + diagnostics.error_count(), + "the invalid prefix and the malformed enum must each report an error" + ); +} + +#[test] +fn recovery_after_malformed_enum_reaches_next_item() { + let (rejected, _text) = parse_with( + "enum Action { A B }\nfn main() {}", + &UnstableFeatures::all(), + ); + + assert!(rejected, "a malformed enum must fail compilation"); +} + +#[test] +fn malformed_construct_containing_enum_keyword_reports_errors() { + // `enum` inside a badly malformed nested construct may be mistaken + // for an item boundary; compilation must still fail cleanly. + let (rejected, _text) = parse_with( + "fn broken() { let x = (enum; }\nfn main() {}", + &UnstableFeatures::all(), + ); + + assert!(rejected, "a malformed construct must fail compilation"); +} + +#[test] +fn test_gated_syntax_is_rejected_without_features() { + // Real `use`/`mod` syntax: rejected under none() (naming the feature + a + // -Z hint), accepted under all(). Delete when `imports` stabilizes. + for input in [ + "use crate::foo::bar;\nfn main() { }", + "mod inner { }\nfn main() { }", + ] { + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + assert!( + rejected, + "gated syntax must be rejected without features (if this feature was \ + just stabilized, delete this test):\n{input}" + ); + assert!( + error.contains("imports") && error.contains("-Z"), + "rejection should name the feature and suggest -Z, got:\n{error}" + ); + + let (rejected, error) = parse_with(input, &UnstableFeatures::all()); + assert!( + !rejected, + "the same syntax must parse with all features enabled:\n{error}" + ); + } +} + +#[test] +fn test_type_heavy_program_needs_no_features() { + // Types are traversed by `RequireFeature` but not gated, so a type-heavy, + // import-free program must parse with no features (no false-positive gates). + let input = r#"type Alias = u32; +fn pick(e: Either) -> u32 { + match e { + Left(a: u32) => a, + Right(b: u32) => b, + } +} +fn main() { + let casted: u32 = <(u16, u16)>::into((0xbeef, 0xbabe)); + let chosen: Alias = pick(Left(casted)); + assert!(jet::eq_32(chosen, chosen)); +} +"#; + // `parse_from_str_with_errors` returns `Some` only when no errors were + // collected, so a non-rejection already proves there were no gate errors. + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + assert!( + !rejected, + "type-heavy but import-free program should parse with no features:\n{error}" + ); +} + +#[test] +fn test_syntax_error_does_not_produce_spurious_feature_gate_error() { + // The gate check skips error-recovered ASTs, so a program with + // both a syntax error and gated syntax reports only the syntax error. + let input = "use crate::foo;\nfn main( {"; + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + + assert!(rejected, "broken program must be rejected"); + assert!( + !error.contains("-Z"), + "syntax error should not produce a spurious feature-gate hint, got:\n{error}" + ); +} + +#[test] +fn test_lexer_error_does_not_produce_spurious_feature_gate_error() { + // Lexer-side companion to the case above: the stray `@` lexes with an + // error but recovers a cleanly-parsing `use` AST, and a program that + // didn't lex cleanly skips the gate check — so no spurious `-Z` hint. + let input = "use crate@::foo;\nfn main() { }"; + let (rejected, error) = parse_with(input, &UnstableFeatures::none()); + + assert!(rejected, "program with a lexer error must be rejected"); + assert!( + !error.contains("-Z"), + "lexer error should not produce a spurious feature-gate hint, got:\n{error}" + ); +} + +#[test] +fn test_use_decl_display_round_trips_with_aliases() { + for input in [ + "use lib::A::foo;", + "use lib::A::foo as bar;", + "use lib::A::{foo, bar as baz};", + ] { + let program = parse::Program::parse_from_str(input).expect("parsing works"); + assert_eq!(program.to_string(), format!("{input}\n")); + } +} + +#[test] +fn statement_spans_exclude_terminating_semicolons() { + let input = "fn main() { let value: u8 = 0; value; }"; + let program = Program::parse_from_str(input).expect("program parses"); + + let Item::Function(function) = &program.items()[0] else { + panic!("expected a function item"); + }; + let ExpressionInner::Block(statements, None) = function.body().inner() else { + panic!("expected a block containing only statements"); + }; + + assert_eq!( + statements[0].span().to_slice(input), + Some("let value: u8 = 0") + ); + assert_eq!(statements[1].span().to_slice(input), Some("value")); +} + +fn parse_item(input: &str) -> Item { + let program = parse::Program::parse_from_str(input).expect("parsing should succeed"); + program.items().first().expect("expected one item").clone() +} + +#[test] +fn test_enum_declaration_basic() { + let item = parse_item("enum Path { Inherit, ColdSpend, RefreshSpend, }"); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration, got {item:?}"); + }; + + assert_eq!(decl.name(), "Path"); + assert_eq!(decl.variants().len(), 3); + assert_eq!(decl.variants()[0].name(), "Inherit"); + assert_eq!(decl.variants()[2].name(), "RefreshSpend"); +} + +#[test] +fn test_enum_declaration_pub() { + let item = parse_item("pub enum Color { Red, Green, Blue, }"); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration"); + }; + + assert_eq!(decl.visibility(), &Visibility::Public); + assert_eq!(decl.name(), "Color"); +} + +#[test] +fn test_enum_declaration_display_round_trip() { + let input = "enum Path { Inherit, ColdSpend, RefreshSpend, }"; + let item = parse_item(input); + let Item::EnumDeclaration(decl) = item else { + panic!("expected EnumDeclaration"); + }; + + assert_eq!( + decl.to_string(), + "enum Path { Inherit, ColdSpend, RefreshSpend, }" + ); +} + +#[test] +fn test_enum_declaration_reserved_name() { + for reserved in RESERVED_PATTERN_NAMES { + let result = Program::parse_from_str(&format!("enum {reserved} {{ A, B, }}")); + let error = result.expect_err(&format!("enum name {reserved} is reserved")); + + assert!( + error.to_string().contains("reserved"), + "error should say the name is reserved: {error}" + ); + } +} diff --git a/src/parse/tests/type_alias.rs b/src/parse/tests/type_alias.rs new file mode 100644 index 00000000..500a9790 --- /dev/null +++ b/src/parse/tests/type_alias.rs @@ -0,0 +1,21 @@ +use crate::error::Error; +use crate::parse::{ParseFromStr, TypeAlias}; + +#[test] +fn test_reject_redefined_builtin_type() { + let ty = TypeAlias::parse_from_str("type Ctx8 = u32") + .expect_err("Redefining built-in alias should be rejected"); + + assert!(ty + .error() + .to_string() + .contains("Type alias `Ctx8` is already exists as built-in alias")); +} + +#[test] +fn test_fragment_rejects_version_directive() { + let err = TypeAlias::parse_from_str("simc \"*\"; type Ctx8 = u32") + .expect_err("a version directive must not be accepted in a fragment"); + + assert!(matches!(err.error(), Error::ReservedSimcKeyword)); +} diff --git a/src/types/README.md b/src/types/README.md index 40ee3ddf..98db78cd 100644 --- a/src/types/README.md +++ b/src/types/README.md @@ -14,7 +14,7 @@ There are three type structures in SimplicityHL: explicitly call `unify` on the Simplicity type inference engine to enforce this. * `AliasedType` is a copy of `ResolvedType` where everything is a (re)name. Essentially they are - "AST types". They feature primarily in parse.rs and ast/. Before these can be used, we call + "AST types". They feature primarily in parse/ and ast/. Before these can be used, we call `aliased_type.resolve()` to get a `ResolvedType`. SimplicityHL does *not* currently support any form of nominal typing. All structurally equal types