bms-rs 1.0.0

The BMS format parser.
Documentation
//! Parsing Bms from [`super::lex::TokenStream`].
//!
//! Raw [String] == `lex` ==> [`super::lex::TokenStream`] (in [`super::lex::LexOutput`]) == `parse` ==> [`super::model::Bms`] (in
//! [`ParseOutput`])
//!
//! This module also defines enums of errors and warnings on parse process.

pub mod check_playing;
pub mod prompt;
pub mod token_processor;
pub mod validity;

use std::ops::RangeInclusive;

use num::BigUint;
use thiserror::Error;

#[cfg(feature = "diagnostics")]
use crate::diagnostics::{SimpleSource, ToAriadne};
#[cfg(feature = "diagnostics")]
use ariadne::{Color, Label, Report, ReportKind};

use crate::bms::{
    ParseConfig,
    command::{
        ObjId,
        channel::{Channel, mapper::KeyLayoutMapper},
        mixin::SourceRangeMixin,
        time::{ObjTime, Track},
    },
    lex::token::TokenWithRange,
    model::Bms,
    rng::Rng,
};

use self::{
    prompt::Prompter,
    token_processor::{ProcessContext, TokenModifier, TokenProcessor},
};

/// An error occurred when parsing the [`super::lex::TokenStream`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ParseError {
    /// Unexpected control flow.
    #[error("unexpected control flow {0}")]
    UnexpectedControlFlow(&'static str),
    /// [`Rng`] generated a value outside the required [`RangeInclusive`] for a random block.
    #[error("random generated value out of range: expected {expected:?}, got {actual}")]
    RandomGeneratedValueOutOfRange {
        /// The expected range of the random block.
        expected: RangeInclusive<BigUint>,
        /// The actual value generated by the [`Rng`].
        actual: BigUint,
    },
    /// [`Rng`] generated a value outside the required [`RangeInclusive`] for a switch block.
    #[error("switch generated value out of range: expected {expected:?}, got {actual}")]
    SwitchGeneratedValueOutOfRange {
        /// The expected range of the switch block.
        expected: RangeInclusive<BigUint>,
        /// The actual value generated by the [`Rng`].
        actual: BigUint,
    },
}

/// A parse error with position information.
pub type ParseErrorWithRange = SourceRangeMixin<ParseError>;

#[cfg(feature = "diagnostics")]
impl ToAriadne for ParseErrorWithRange {
    fn to_report<'a>(
        &self,
        src: &SimpleSource<'a>,
    ) -> Report<'a, (String, std::ops::Range<usize>)> {
        let (start, end) = self.as_span();
        let filename = src.name().to_string();
        Report::build(ReportKind::Error, (filename.clone(), start..end))
            .with_message(format!("parse error: {}", self.content()))
            .with_label(Label::new((filename, start..end)).with_color(Color::Red))
            .finish()
    }
}

/// A warning occurred when parsing the [`super::lex::TokenStream`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ParseWarning {
    /// Syntax formed from the commands was invalid.
    #[error("syntax error: {0}")]
    SyntaxError(String),
    /// The object has required but not defined,
    #[error("undefined object: {0:?}")]
    UndefinedObject(ObjId),
    /// Has duplicated definition, that `prompt_handler` returned [`prompt::DuplicationWorkaround::WarnAndUseOlder`].
    #[error("duplicating definition: {0}")]
    DuplicatingDef(ObjId),
    /// Has duplicated track object, that `prompt_handler` returned [`prompt::DuplicationWorkaround::WarnAndUseOlder`].
    #[error("duplicating track object: {0} {1}")]
    DuplicatingTrackObj(Track, Channel),
    /// Has duplicated channel object, that `prompt_handler` returned [`prompt::DuplicationWorkaround::WarnAndUseOlder`].
    #[error("duplicating channel object: {0} {1}")]
    DuplicatingChannelObj(ObjTime, Channel),
    /// Failed to convert a byte into a base-62 character `0-9A-Za-z`.
    #[error("expected id format is base 62 (`0-9A-Za-z`)")]
    OutOfBase62,
}

/// A parse warning with position information.
pub type ParseWarningWithRange = SourceRangeMixin<ParseWarning>;

/// Result type for parse operations with `ParseWarning`.
pub(crate) type Result<T> = core::result::Result<T, ParseWarning>;

#[cfg(feature = "diagnostics")]
impl ToAriadne for ParseWarningWithRange {
    fn to_report<'a>(
        &self,
        src: &SimpleSource<'a>,
    ) -> Report<'a, (String, std::ops::Range<usize>)> {
        let (start, end) = self.as_span();
        let filename = src.name().to_string();
        Report::build(ReportKind::Warning, (filename.clone(), start..end))
            .with_message(format!("parse: {}", self.content()))
            .with_label(Label::new((filename, start..end)).with_color(Color::Blue))
            .finish()
    }
}

/// Bms Parse Output
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[must_use]
pub struct ParseOutput {
    /// The output Bms.
    pub bms: core::result::Result<Bms, ParseErrorWithRange>,
    /// Warnings that occurred during parsing.
    pub parse_warnings: Vec<ParseWarningWithRange>,
}

impl Bms {
    /// Parses a token stream into [`Bms`] without AST.
    pub fn from_token_stream<'a, T: KeyLayoutMapper, P: Prompter, R: Rng, M: TokenModifier>(
        token_iter: impl IntoIterator<Item = &'a TokenWithRange<'a>>,
        config: ParseConfig<T, P, R, M>,
    ) -> ParseOutput {
        let tokens: Vec<_> = token_iter.into_iter().collect();
        let mut tokens_slice = tokens.as_slice();
        let (proc, prompter) = config.build();
        let mut ctx = ProcessContext::new(&mut tokens_slice, &prompter);
        let res = proc.process(&mut ctx);
        ParseOutput {
            bms: res,
            parse_warnings: ctx.into_warnings(),
        }
    }
}