From ac4797543e39e7d00da7cc5cde719da7f2ea1e8a Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Wed, 21 Oct 2026 16:39:11 +0300 Subject: [PATCH 01/16] refactor: add parse/ directory, move arbitrary impls into it --- src/parse.rs | 377 +---------------------------------- src/parse/arbitrary_impls.rs | 374 ++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 374 deletions(-) create mode 100644 src/parse/arbitrary_impls.rs diff --git a/src/parse.rs b/src/parse.rs index 6c14cc9b..ef1db83d 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,6 +1,9 @@ //! This module contains the parsing code to convert the //! tokens into an AST. +#[cfg(feature = "arbitrary")] +mod arbitrary_impls; + use std::fmt; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -286,25 +289,6 @@ impl UseDecl { // 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); @@ -612,22 +596,6 @@ pub struct EnumVariant { 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 @@ -695,25 +663,6 @@ impl AsRef for EnumDeclaration { } } -#[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())?; @@ -3281,326 +3230,6 @@ impl AsRef for Module { } } -#[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::*; diff --git a/src/parse/arbitrary_impls.rs b/src/parse/arbitrary_impls.rs new file mode 100644 index 00000000..730c0d99 --- /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, + }) + } +} + +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) +} + +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, + }) + } +} From 7eca40cd7a66606b4d86e6ef092a889f70c33b51 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Wed, 21 Oct 2026 16:51:26 +0300 Subject: [PATCH 02/16] refactor: move impl fmt::Display blocks to parse/display --- src/parse.rs | 392 +---------------------------------------- src/parse/display.rs | 405 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 406 insertions(+), 391 deletions(-) create mode 100644 src/parse/display.rs diff --git a/src/parse.rs b/src/parse.rs index ef1db83d..d91997ad 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -3,8 +3,8 @@ #[cfg(feature = "arbitrary")] mod arbitrary_impls; +mod display; -use std::fmt; use std::num::NonZeroUsize; use std::path::PathBuf; use std::str::FromStr; @@ -663,24 +663,6 @@ impl AsRef for EnumDeclaration { } } -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 { @@ -966,24 +948,6 @@ 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 @@ -1169,137 +1133,6 @@ impl Module { } } -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), @@ -1382,229 +1215,6 @@ impl TreeLike for ExprTree<'_> { } } -// 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 { 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"), + } + } +} From ded7eaff78249b89baad7b7510cfcbca4531727f Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 13:08:36 +0300 Subject: [PATCH 03/16] refactor: move fmt feature to parse/lossless --- src/parse.rs | 93 ++++--------------------------------------- src/parse/lossless.rs | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 85 deletions(-) create mode 100644 src/parse/lossless.rs diff --git a/src/parse.rs b/src/parse.rs index d91997ad..94035e73 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,6 +4,8 @@ #[cfg(feature = "arbitrary")] mod arbitrary_impls; mod display; +#[cfg(feature = "fmt")] +mod lossless; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -39,9 +41,6 @@ use crate::unstable::{ 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 { @@ -49,40 +48,6 @@ pub struct Program { 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 { @@ -96,50 +61,6 @@ impl 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); @@ -3177,6 +3098,8 @@ fn main() { #[cfg(test)] #[cfg(feature = "fmt")] mod fmt_parsing { + use crate::parse::lossless::ParsedSource; + use super::*; /// Parse `input`, appending any formatter diagnostics to `diagnostics`. @@ -3449,7 +3372,7 @@ fn main() { ] { let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::all()); assert!(!rejected, "parsing works"); - assert_eq!(program.unwrap().program.to_string(), format!("{input}\n")); + assert_eq!(program.unwrap().program().to_string(), format!("{input}\n")); } } @@ -3459,7 +3382,7 @@ fn main() { let (rejected, _, program) = parse_with_extended_out(input, &UnstableFeatures::none()); assert!(!rejected, "parsing works"); - let program = program.unwrap().program; + let program = program.unwrap().program().clone(); let Item::Function(function) = &program.items()[0] else { panic!("expected a function item"); @@ -3478,7 +3401,7 @@ fn main() { 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; + let program = program.unwrap().program().clone(); program.items().first().expect("expected one item").clone() } @@ -3550,7 +3473,7 @@ fn main() { ] { assert!( parsed.tokens().iter().any( - |(token, _)| matches!(token, FmtToken::Trivia(token_trivia) if token_trivia.kind() == trivia) + |(token, _)| matches!(token, crate::lexer::FmtToken::Trivia(token_trivia) if token_trivia.kind() == trivia) ), "formatter token stream should retain {trivia:?}" ); 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), + }) + } + } +} From af9104d26de5f6510fb2570802c277e9ccaef11a Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 13:27:20 +0300 Subject: [PATCH 04/16] refactor: move ChumskyParse and shared parser helpers to parse/grammar --- src/parse.rs | 102 +++------------------------------------ src/parse/grammar/mod.rs | 101 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 95 deletions(-) create mode 100644 src/parse/grammar/mod.rs diff --git a/src/parse.rs b/src/parse.rs index 94035e73..853624df 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,9 +4,12 @@ #[cfg(feature = "arbitrary")] mod arbitrary_impls; mod display; +mod grammar; #[cfg(feature = "fmt")] mod lossless; +pub use crate::parse::grammar::ChumskyParse; + use std::num::NonZeroUsize; use std::path::PathBuf; use std::str::FromStr; @@ -17,7 +20,7 @@ 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 chumsky::{select, IterParser, Parser}; use either::Either; use itertools::Itertools; @@ -29,6 +32,9 @@ use crate::error::{Diagnostic, Error, Span}; use crate::impl_eq_hash; use crate::lexer::{Token, Tokens}; use crate::num::NonZeroPow2Usize; +use crate::parse::grammar::{ + delimited_with_recovery, parse_token_with_recovery, ParseError, RESERVED_PATTERN_NAMES, +}; use crate::pattern::Pattern; use crate::str::{ AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, @@ -1136,29 +1142,6 @@ impl TreeLike for ExprTree<'_> { } } -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 {} @@ -1251,17 +1234,6 @@ mod pipeline { } } -/// 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 { @@ -1338,62 +1310,6 @@ impl ParseFromStrWithErrors for A { } } -/// 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 @@ -2014,10 +1930,6 @@ impl ChumskyParse for TypeAlias { } } -/// 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 diff --git a/src/parse/grammar/mod.rs b/src/parse/grammar/mod.rs new file mode 100644 index 00000000..36f9d83f --- /dev/null +++ b/src/parse/grammar/mod.rs @@ -0,0 +1,101 @@ +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>; +} + +pub(super) 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(crate) 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. +pub(super) 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). +pub(super) 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 + }) +} From 80d5af996dcd33782ce3bb6139d4a20129a9d1b8 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 13:40:47 +0300 Subject: [PATCH 05/16] refactor: move pipeline with traits to parse/pipeline --- src/parse.rs | 194 ++++-------------------------------------- src/parse/pipeline.rs | 177 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 178 deletions(-) create mode 100644 src/parse/pipeline.rs diff --git a/src/parse.rs b/src/parse.rs index 853624df..b82bfc46 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -7,15 +7,19 @@ mod display; mod grammar; #[cfg(feature = "fmt")] mod lossless; +mod pipeline; -pub use crate::parse::grammar::ChumskyParse; +pub use self::grammar::ChumskyParse; +#[cfg(feature = "fmt")] +pub use self::lossless::ParsedSource; +pub use self::pipeline::{ParseFromStr, ParseFromStrWithErrors}; use std::num::NonZeroUsize; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; -use chumsky::input::{Input, ValueInput}; +use chumsky::input::ValueInput; use chumsky::prelude::{ any, choice, empty, just, nested_delimiters, none_of, one_of, recursive, skip_then_retry_until, via_parser, @@ -26,11 +30,10 @@ use either::Either; use itertools::Itertools; use miniscript::iter::{Tree, TreeLike}; -use crate::driver::{CRATE_STR, MAIN_MODULE}; -use crate::error::DiagnosticManager; +use crate::driver::CRATE_STR; use crate::error::{Diagnostic, Error, Span}; use crate::impl_eq_hash; -use crate::lexer::{Token, Tokens}; +use crate::lexer::Token; use crate::num::NonZeroPow2Usize; use crate::parse::grammar::{ delimited_with_recovery, parse_token_with_recovery, ParseError, RESERVED_PATTERN_NAMES, @@ -41,10 +44,7 @@ use crate::str::{ SymbolName, }; use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType}; -use crate::unstable::{ - impl_require_feature, FeatureRequirement, RequireFeature, UnstableFeature, UnstableFeatures, -}; -use crate::version::SimcDirective; +use crate::unstable::{impl_require_feature, FeatureRequirement, RequireFeature, UnstableFeature}; use crate::TemplateProgramWitness; /// A program is a sequence of items. @@ -1142,174 +1142,6 @@ impl TreeLike for ExprTree<'_> { } } -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); - } - } -} - -/// 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 - } - } -} - impl ChumskyParse for AliasedType { fn parser<'tokens, 'src: 'tokens, I>() -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where @@ -2699,8 +2531,11 @@ mod type_alias { #[cfg(test)] mod regular_parsing { + use crate::driver::MAIN_MODULE; + use crate::error::DiagnosticManager; + use crate::{parse, UnstableFeatures}; + use super::*; - use crate::parse; impl UseDecl { /// Creates a dummy `UseDecl` specifically for testing `DependencyMap` resolution. @@ -3010,7 +2845,10 @@ fn main() { #[cfg(test)] #[cfg(feature = "fmt")] mod fmt_parsing { + use crate::driver::MAIN_MODULE; + use crate::error::DiagnosticManager; use crate::parse::lossless::ParsedSource; + use crate::UnstableFeatures; use super::*; 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); + } +} From e57602b1da7ab1d8d9d7a8b027485c9d72b75a3a Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 13:45:19 +0300 Subject: [PATCH 06/16] refactor: move ChumskyParse impl for types to parse/grammar/types --- src/parse.rs | 262 +---------------------------------- src/parse/grammar/mod.rs | 2 + src/parse/grammar/types.rs | 274 +++++++++++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 260 deletions(-) create mode 100644 src/parse/grammar/types.rs diff --git a/src/parse.rs b/src/parse.rs index b82bfc46..0ea2b5bf 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -16,13 +16,11 @@ pub use self::pipeline::{ParseFromStr, ParseFromStrWithErrors}; use std::num::NonZeroUsize; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; use chumsky::input::ValueInput; use chumsky::prelude::{ - any, choice, empty, just, nested_delimiters, none_of, one_of, recursive, skip_then_retry_until, - via_parser, + any, choice, just, nested_delimiters, one_of, recursive, skip_then_retry_until, via_parser, }; use chumsky::{select, IterParser, Parser}; @@ -43,7 +41,7 @@ use crate::str::{ AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, SymbolName, }; -use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType}; +use crate::types::{AliasedType, TypeConstructible}; use crate::unstable::{impl_require_feature, FeatureRequirement, RequireFeature, UnstableFeature}; use crate::TemplateProgramWitness; @@ -1142,140 +1140,6 @@ impl TreeLike for ExprTree<'_> { } } -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`]. /// @@ -1713,128 +1577,6 @@ impl ChumskyParse for CallName { } } -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(), - }) - } -} - /// 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 diff --git a/src/parse/grammar/mod.rs b/src/parse/grammar/mod.rs index 36f9d83f..43fab26e 100644 --- a/src/parse/grammar/mod.rs +++ b/src/parse/grammar/mod.rs @@ -1,3 +1,5 @@ +mod types; + use chumsky::input::ValueInput; use chumsky::prelude::{empty, just, nested_delimiters, via_parser}; use chumsky::{extra, select, Parser}; 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(), + }) + } +} From 61d6f2fd8a4eab5124cc8b2ed3875cf4457d17cf Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 13:51:14 +0300 Subject: [PATCH 07/16] refactor: move Statement impls to parse/grammar/statement --- src/parse.rs | 78 ----------------------------- src/parse/grammar/mod.rs | 1 + src/parse/grammar/statement.rs | 90 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 78 deletions(-) create mode 100644 src/parse/grammar/statement.rs diff --git a/src/parse.rs b/src/parse.rs index 0ea2b5bf..281aceda 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1346,84 +1346,6 @@ impl ChumskyParse for FunctionParam { } } -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, diff --git a/src/parse/grammar/mod.rs b/src/parse/grammar/mod.rs index 43fab26e..cff0f24c 100644 --- a/src/parse/grammar/mod.rs +++ b/src/parse/grammar/mod.rs @@ -1,3 +1,4 @@ +mod statement; mod types; use chumsky::input::ValueInput; diff --git a/src/parse/grammar/statement.rs b/src/parse/grammar/statement.rs new file mode 100644 index 00000000..6c32026f --- /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(crate) 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") + }) + } +} From 3d9544bf41c71e979533ba1ead8347bcdc033c0c Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 13:53:03 +0300 Subject: [PATCH 08/16] refactor: move Call impls to parse/grammar/call --- src/parse.rs | 157 +---------------------------------- src/parse/grammar/call.rs | 168 ++++++++++++++++++++++++++++++++++++++ src/parse/grammar/mod.rs | 1 + 3 files changed, 170 insertions(+), 156 deletions(-) create mode 100644 src/parse/grammar/call.rs diff --git a/src/parse.rs b/src/parse.rs index 281aceda..7f5a5cd0 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -33,9 +33,7 @@ use crate::error::{Diagnostic, Error, Span}; use crate::impl_eq_hash; use crate::lexer::Token; use crate::num::NonZeroPow2Usize; -use crate::parse::grammar::{ - delimited_with_recovery, parse_token_with_recovery, ParseError, RESERVED_PATTERN_NAMES, -}; +use crate::parse::grammar::{delimited_with_recovery, ParseError, RESERVED_PATTERN_NAMES}; use crate::pattern::Pattern; use crate::str::{ AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, @@ -1346,159 +1344,6 @@ impl ChumskyParse for FunctionParam { } } -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, - )) - } -} - /// 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 diff --git a/src/parse/grammar/call.rs b/src/parse/grammar/call.rs new file mode 100644 index 00000000..3239f3a0 --- /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(crate) 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/mod.rs b/src/parse/grammar/mod.rs index cff0f24c..18271bc6 100644 --- a/src/parse/grammar/mod.rs +++ b/src/parse/grammar/mod.rs @@ -1,3 +1,4 @@ +mod call; mod statement; mod types; From b61c30aada24a14f6d1bbb5753a534a4e975f493 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 14:13:41 +0300 Subject: [PATCH 09/16] refactor: move matching functionality to parse/grammar/matching --- src/parse.rs | 309 +------------------------------- src/parse/grammar/matching.rs | 322 ++++++++++++++++++++++++++++++++++ src/parse/grammar/mod.rs | 1 + 3 files changed, 327 insertions(+), 305 deletions(-) create mode 100644 src/parse/grammar/matching.rs diff --git a/src/parse.rs b/src/parse.rs index 7f5a5cd0..4859e2a7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -25,7 +25,6 @@ use chumsky::prelude::{ use chumsky::{select, IterParser, Parser}; use either::Either; -use itertools::Itertools; use miniscript::iter::{Tree, TreeLike}; use crate::driver::CRATE_STR; @@ -33,7 +32,8 @@ use crate::error::{Diagnostic, Error, Span}; use crate::impl_eq_hash; use crate::lexer::Token; use crate::num::NonZeroPow2Usize; -use crate::parse::grammar::{delimited_with_recovery, ParseError, RESERVED_PATTERN_NAMES}; +use crate::parse::grammar::matching::match_expr_parser; +use crate::parse::grammar::{delimited_with_recovery, ParseError}; use crate::pattern::Pattern; use crate::str::{ AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, @@ -1566,309 +1566,6 @@ impl SingleExpression { } } -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, @@ -2042,6 +1739,7 @@ mod type_alias { mod regular_parsing { use crate::driver::MAIN_MODULE; use crate::error::DiagnosticManager; + use crate::parse::grammar::RESERVED_PATTERN_NAMES; use crate::{parse, UnstableFeatures}; use super::*; @@ -2356,6 +2054,7 @@ fn main() { mod fmt_parsing { use crate::driver::MAIN_MODULE; use crate::error::DiagnosticManager; + use crate::parse::grammar::RESERVED_PATTERN_NAMES; use crate::parse::lossless::ParsedSource; use crate::UnstableFeatures; diff --git a/src/parse/grammar/matching.rs b/src/parse/grammar/matching.rs new file mode 100644 index 00000000..b3d6fd66 --- /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(crate) 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 index 18271bc6..912f2e23 100644 --- a/src/parse/grammar/mod.rs +++ b/src/parse/grammar/mod.rs @@ -1,4 +1,5 @@ mod call; +pub(crate) mod matching; mod statement; mod types; From 49d67c41f68c30d63af64b94e036a8bc4f85d8a8 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 14:26:42 +0300 Subject: [PATCH 10/16] refactor: move expression impls to parse/grammar/expression --- src/parse.rs | 227 +----------------------------- src/parse/grammar/expression.rs | 242 ++++++++++++++++++++++++++++++++ src/parse/grammar/mod.rs | 1 + 3 files changed, 244 insertions(+), 226 deletions(-) create mode 100644 src/parse/grammar/expression.rs diff --git a/src/parse.rs b/src/parse.rs index 4859e2a7..0640f93a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -19,9 +19,7 @@ use std::path::PathBuf; use std::sync::Arc; use chumsky::input::ValueInput; -use chumsky::prelude::{ - any, choice, just, nested_delimiters, one_of, recursive, skip_then_retry_until, via_parser, -}; +use chumsky::prelude::{any, choice, just, nested_delimiters, recursive, via_parser}; use chumsky::{select, IterParser, Parser}; use either::Either; @@ -32,7 +30,6 @@ use crate::error::{Diagnostic, Error, Span}; use crate::impl_eq_hash; use crate::lexer::Token; use crate::num::NonZeroPow2Usize; -use crate::parse::grammar::matching::match_expr_parser; use crate::parse::grammar::{delimited_with_recovery, ParseError}; use crate::pattern::Pattern; use crate::str::{ @@ -1344,228 +1341,6 @@ impl ChumskyParse for FunctionParam { } } -/// 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 Module { pub fn parser_with_items<'tokens, 'src: 'tokens, I>( item_parser: impl Parser<'tokens, I, Item, ParseError<'src>> + Clone + 'tokens, 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/mod.rs b/src/parse/grammar/mod.rs index 912f2e23..6813eb3d 100644 --- a/src/parse/grammar/mod.rs +++ b/src/parse/grammar/mod.rs @@ -1,4 +1,5 @@ mod call; +mod expression; pub(crate) mod matching; mod statement; mod types; From 412b3831c6f59270e2221f81c4b311a83103470e Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 14:38:58 +0300 Subject: [PATCH 11/16] refactor: move Program with other global impls to parse/grammar/program --- src/parse.rs | 274 --------------------------------- src/parse/grammar/mod.rs | 1 + src/parse/grammar/program.rs | 284 +++++++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+), 274 deletions(-) create mode 100644 src/parse/grammar/program.rs diff --git a/src/parse.rs b/src/parse.rs index 0640f93a..15063c61 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -18,19 +18,12 @@ use std::num::NonZeroUsize; use std::path::PathBuf; 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 either::Either; use miniscript::iter::{Tree, TreeLike}; -use crate::driver::CRATE_STR; use crate::error::{Diagnostic, Error, Span}; use crate::impl_eq_hash; -use crate::lexer::Token; use crate::num::NonZeroPow2Usize; -use crate::parse::grammar::{delimited_with_recovery, ParseError}; use crate::pattern::Pattern; use crate::str::{ AliasName, Binary, Decimal, FunctionName, Hexadecimal, Identifier, JetName, ModuleName, @@ -1135,273 +1128,6 @@ impl TreeLike for ExprTree<'_> { } } -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 - }) - } -} - impl<'a, A: AsRef> From<&'a A> for Span { fn from(value: &'a A) -> Self { *value.as_ref() diff --git a/src/parse/grammar/mod.rs b/src/parse/grammar/mod.rs index 6813eb3d..3e6bb571 100644 --- a/src/parse/grammar/mod.rs +++ b/src/parse/grammar/mod.rs @@ -1,6 +1,7 @@ mod call; mod expression; pub(crate) mod matching; +mod program; mod statement; mod types; 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 + }) + } +} From c515cc68da295dc793ffb619f0119e4c1bb1da9f Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 14:57:15 +0300 Subject: [PATCH 12/16] refactor: move fmt tests to parse/tests/lossless --- src/parse.rs | 474 +----------------------------------- src/parse/tests/lossless.rs | 464 +++++++++++++++++++++++++++++++++++ src/parse/tests/mod.rs | 2 + 3 files changed, 468 insertions(+), 472 deletions(-) create mode 100644 src/parse/tests/lossless.rs create mode 100644 src/parse/tests/mod.rs diff --git a/src/parse.rs b/src/parse.rs index 15063c61..36c34175 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -8,6 +8,8 @@ mod grammar; #[cfg(feature = "fmt")] mod lossless; mod pipeline; +#[cfg(test)] +mod tests; pub use self::grammar::ChumskyParse; #[cfg(feature = "fmt")] @@ -1549,475 +1551,3 @@ fn main() { } } } - -#[cfg(test)] -#[cfg(feature = "fmt")] -mod fmt_parsing { - use crate::driver::MAIN_MODULE; - use crate::error::DiagnosticManager; - use crate::parse::grammar::RESERVED_PATTERN_NAMES; - use crate::parse::lossless::ParsedSource; - use crate::UnstableFeatures; - - 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().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 [ - crate::lexer::TriviaKind::BlockComment, - crate::lexer::TriviaKind::LineComment, - crate::lexer::TriviaKind::Newline, - crate::lexer::TriviaKind::Whitespace, - ] { - assert!( - parsed.tokens().iter().any( - |(token, _)| matches!(token, crate::lexer::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/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..686931fa --- /dev/null +++ b/src/parse/tests/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "fmt")] +mod lossless; From bb438e562e51871783a90f42fb598f2ac4ea5fb1 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 15:03:42 +0300 Subject: [PATCH 13/16] refactor: move regular tests to parse/tests/regular --- src/parse.rs | 314 ------------------------------------ src/parse/tests/mod.rs | 1 + src/parse/tests/regular.rs | 322 +++++++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+), 314 deletions(-) create mode 100644 src/parse/tests/regular.rs diff --git a/src/parse.rs b/src/parse.rs index 36c34175..ab909c0a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1237,317 +1237,3 @@ mod type_alias { assert!(matches!(err.error(), Error::ReservedSimcKeyword)); } } - -#[cfg(test)] -mod regular_parsing { - use crate::driver::MAIN_MODULE; - use crate::error::DiagnosticManager; - use crate::parse::grammar::RESERVED_PATTERN_NAMES; - use crate::{parse, UnstableFeatures}; - - use super::*; - - 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/mod.rs b/src/parse/tests/mod.rs index 686931fa..27226925 100644 --- a/src/parse/tests/mod.rs +++ b/src/parse/tests/mod.rs @@ -1,2 +1,3 @@ #[cfg(feature = "fmt")] mod lossless; +mod regular; 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}" + ); + } +} From 9e6a5e853f5cc8ad70d5dfa8975ea1a229950d6e Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 15:20:28 +0300 Subject: [PATCH 14/16] refactor: move type alias to parse/tests/type_alias --- src/parse.rs | 24 ------------------------ src/parse/tests/mod.rs | 1 + src/parse/tests/type_alias.rs | 21 +++++++++++++++++++++ 3 files changed, 22 insertions(+), 24 deletions(-) create mode 100644 src/parse/tests/type_alias.rs diff --git a/src/parse.rs b/src/parse.rs index ab909c0a..fb10e6f4 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1213,27 +1213,3 @@ impl AsRef for Module { &self.span } } - -#[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)); - } -} diff --git a/src/parse/tests/mod.rs b/src/parse/tests/mod.rs index 27226925..d0658464 100644 --- a/src/parse/tests/mod.rs +++ b/src/parse/tests/mod.rs @@ -1,3 +1,4 @@ #[cfg(feature = "fmt")] mod lossless; mod regular; +mod type_alias; 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)); +} From 02475e87a8088435813636fa0a4d8ef733034316 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 15:22:44 +0300 Subject: [PATCH 15/16] refactor: move parse file to parse/mod --- src/{parse.rs => parse/mod.rs} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/{parse.rs => parse/mod.rs} (100%) diff --git a/src/parse.rs b/src/parse/mod.rs similarity index 100% rename from src/parse.rs rename to src/parse/mod.rs index fb10e6f4..605ce25c 100644 --- a/src/parse.rs +++ b/src/parse/mod.rs @@ -11,11 +11,6 @@ mod pipeline; #[cfg(test)] mod tests; -pub use self::grammar::ChumskyParse; -#[cfg(feature = "fmt")] -pub use self::lossless::ParsedSource; -pub use self::pipeline::{ParseFromStr, ParseFromStrWithErrors}; - use std::num::NonZeroUsize; use std::path::PathBuf; use std::sync::Arc; @@ -35,6 +30,11 @@ 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 { From 258e0279fc44de15a2421c8cc591feef510f47f4 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 22 Oct 2026 15:31:10 +0300 Subject: [PATCH 16/16] refactor: reduce visibility in parse module rename: parse.rs to parse/ in types/README.md --- src/parse/arbitrary_impls.rs | 2 +- src/parse/grammar/call.rs | 2 +- src/parse/grammar/matching.rs | 2 +- src/parse/grammar/mod.rs | 10 +++++----- src/parse/grammar/statement.rs | 2 +- src/types/README.md | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/parse/arbitrary_impls.rs b/src/parse/arbitrary_impls.rs index 730c0d99..4b7ae009 100644 --- a/src/parse/arbitrary_impls.rs +++ b/src/parse/arbitrary_impls.rs @@ -65,7 +65,7 @@ impl<'a> arbitrary::Arbitrary<'a> for EnumDeclaration { } } -pub(crate) fn generate_arbitrary_items<'a>( +fn generate_arbitrary_items<'a>( u: &mut arbitrary::Unstructured<'a>, ) -> arbitrary::Result> { let mut items_vec = Vec::new(); diff --git a/src/parse/grammar/call.rs b/src/parse/grammar/call.rs index 3239f3a0..216b3616 100644 --- a/src/parse/grammar/call.rs +++ b/src/parse/grammar/call.rs @@ -15,7 +15,7 @@ use super::super::{Call, CallName, Expression}; use super::{delimited_with_recovery, parse_token_with_recovery, ChumskyParse, ParseError}; impl Call { - pub(crate) fn parser<'tokens, 'src: 'tokens, I, E>( + pub(super) fn parser<'tokens, 'src: 'tokens, I, E>( expr: E, ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where diff --git a/src/parse/grammar/matching.rs b/src/parse/grammar/matching.rs index b3d6fd66..9c306695 100644 --- a/src/parse/grammar/matching.rs +++ b/src/parse/grammar/matching.rs @@ -290,7 +290,7 @@ fn assemble_match_arms( /// 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(crate) fn match_expr_parser<'tokens, 'src: 'tokens, I, E>( +pub(super) fn match_expr_parser<'tokens, 'src: 'tokens, I, E>( expr: E, ) -> impl Parser<'tokens, I, SingleExpressionInner, ParseError<'src>> + Clone where diff --git a/src/parse/grammar/mod.rs b/src/parse/grammar/mod.rs index 3e6bb571..83c3a40b 100644 --- a/src/parse/grammar/mod.rs +++ b/src/parse/grammar/mod.rs @@ -1,6 +1,6 @@ mod call; mod expression; -pub(crate) mod matching; +mod matching; mod program; mod statement; mod types; @@ -22,11 +22,11 @@ pub trait ChumskyParse: Sized { I: ValueInput<'tokens, Token = Token<'src>, Span = Span>; } -pub(super) type ParseError<'src> = extra::Err; +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(crate) const RESERVED_PATTERN_NAMES: [&str; 4] = ["Left", "Right", "Some", "None"]; +pub(super) const RESERVED_PATTERN_NAMES: [&str; 4] = ["Left", "Right", "Some", "None"]; macro_rules! impl_parse_wrapped_string { ($wrapper: ident, $label: literal) => { @@ -55,7 +55,7 @@ impl_parse_wrapped_string!(ModuleName, "module name"); /// /// Should be only used when we know that this token should be there. For example, type of /// `List` would require comma inside angle brackets. -pub(super) fn parse_token_with_recovery<'tokens, 'src: 'tokens, I>( +fn parse_token_with_recovery<'tokens, 'src: 'tokens, I>( tok: Token<'src>, ) -> impl Parser<'tokens, I, Token<'src>, ParseError<'src>> + Clone where @@ -67,7 +67,7 @@ where /// Parser with error recovery for expressions, which would always contains given delimiters. /// /// Can track span of open delimiter (if any). -pub(super) fn delimited_with_recovery<'tokens, 'src: 'tokens, I, P, T, F>( +fn delimited_with_recovery<'tokens, 'src: 'tokens, I, P, T, F>( parser: P, open: Token<'src>, close: Token<'src>, diff --git a/src/parse/grammar/statement.rs b/src/parse/grammar/statement.rs index 6c32026f..416c7844 100644 --- a/src/parse/grammar/statement.rs +++ b/src/parse/grammar/statement.rs @@ -12,7 +12,7 @@ use super::super::{Assignment, Expression, Statement}; use super::{delimited_with_recovery, parse_token_with_recovery, ChumskyParse, ParseError}; impl Statement { - pub(crate) fn parser<'tokens, 'src: 'tokens, I, E>( + pub(super) fn parser<'tokens, 'src: 'tokens, I, E>( expr: E, ) -> impl Parser<'tokens, I, Self, ParseError<'src>> + Clone where 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