sysml-v2-parser 0.13.0

SysML v2 textual notation parser for Rust
Documentation
//! Attribute definition and usage parsing.

use crate::ast::{AttributeBody, AttributeDef, AttributeUsage, Node};
use crate::parser::expr::expression;
use crate::parser::lex::{
    identification, name, qualified_name, skip_until_brace_end, starts_with_keyword,
    take_until_terminator, ws1, ws_and_comments,
};
use crate::parser::node_from_to;
use crate::parser::with_span;
use crate::parser::Input;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::combinator::map;
use nom::sequence::{delimited, preceded};
use nom::IResult;
use nom::Parser;

fn local_name_from_qualified_name(qname: &str) -> String {
    qname.rsplit("::").next().unwrap_or(qname).to_string()
}

/// Type after `attribute` name: optionally `:>` or `:` plus qualified name. Does not consume `:>>`.
/// When `allow_colon_gt` is false (after `attribute :>>` redefinition), only `:` is accepted so a
/// following `:>` can be parsed as subsetting (e.g. `attribute :>> outlet :> electricGrid.outlets`).
fn attribute_typing(
    input: Input<'_>,
    allow_colon_gt: bool,
) -> IResult<Input<'_>, (crate::ast::Span, String)> {
    let (input, _) = ws_and_comments(input)?;
    let peek = input.fragment();
    if starts_with_keyword(peek, b":>>") {
        return Err(nom::Err::Error(nom::error::Error::new(
            input,
            nom::error::ErrorKind::Tag,
        )));
    }
    if allow_colon_gt && starts_with_keyword(peek, b":>") {
        let (input, _) = tag(&b":>"[..]).parse(input)?;
        let (input, _) = ws_and_comments(input)?;
        return with_span(qualified_name).parse(input);
    }
    if peek.first() == Some(&b':')
        && !starts_with_keyword(peek, b"::")
        && !starts_with_keyword(peek, b":>")
    {
        let (input, _) = tag(&b":"[..]).parse(input)?;
        let (input, _) = ws_and_comments(input)?;
        return with_span(qualified_name).parse(input);
    }
    Err(nom::Err::Error(nom::error::Error::new(
        input,
        nom::error::ErrorKind::Tag,
    )))
}

fn is_reserved_shorthand_starter(name: &str) -> bool {
    matches!(
        name,
        "interface"
            | "part"
            | "connect"
            | "bind"
            | "perform"
            | "allocate"
            | "port"
            | "state"
            | "satisfy"
            | "action"
            | "attribute"
            | "ref"
            | "doc"
            | "metadata"
            | "filter"
            | "use"
            | "view"
            | "viewpoint"
            | "render"
            | "rendering"
            | "requirement"
            | "require"
            | "concern"
            | "actor"
            | "item"
            | "individual"
            | "constraint"
            | "calc"
            | "enum"
            | "occurrence"
    )
}

/// Value part: `= expr` | `:= expr` | `default = expr` | `default := expr` (BNF FeatureValue).
fn value_part(input: Input<'_>) -> IResult<Input<'_>, Node<crate::ast::Expression>> {
    let (input, _) = ws_and_comments(input)?;
    let (input, _) = alt((
        preceded(tag(&b"="[..]), ws_and_comments),
        preceded(tag(&b":="[..]), ws_and_comments),
        preceded(
            preceded(tag(&b"default"[..]), ws1),
            preceded(alt((tag(&b"="[..]), tag(&b":="[..]))), ws_and_comments),
        ),
    ))
    .parse(input)?;
    expression(input)
}

/// Attribute body: ';' or '{' ... '}' (skip content inside braces)
pub(crate) fn attribute_body(input: Input<'_>) -> IResult<Input<'_>, AttributeBody> {
    let (input, _) = ws_and_comments(input)?;
    alt((
        map(tag(&b";"[..]), |_| AttributeBody::Semicolon),
        map(
            delimited(
                tag(&b"{"[..]),
                skip_until_brace_end,
                preceded(ws_and_comments, tag(&b"}"[..])),
            ),
            |_| AttributeBody::Brace,
        ),
    ))
    .parse(input)
}

/// Attribute definition: 'attribute' name ( ':>' | ':' )? qualified_name? body
///
/// When `disambiguate_from_usage` is true (definition bodies that also accept usages), untyped
/// `attribute name = value` is left for [`attribute_usage`]. Package-level attributes pass false.
pub(crate) fn attribute_def(
    input: Input<'_>,
    disambiguate_from_usage: bool,
) -> IResult<Input<'_>, Node<AttributeDef>> {
    let start = input;
    let (input, _) = ws_and_comments(input)?;
    let (input, _) = nom::combinator::opt(preceded(
        alt((
            tag(&b"private"[..]),
            tag(&b"protected"[..]),
            tag(&b"public"[..]),
        )),
        ws1,
    ))
    .parse(input)?;
    let (input, _) = nom::combinator::opt(preceded(tag(&b"abstract"[..]), ws1)).parse(input)?;
    let (input, _) = tag(&b"attribute"[..]).parse(input)?;
    let (input, _) = ws1(input)?;
    let (input, has_def) = nom::combinator::opt(preceded(tag(&b"def"[..]), ws1)).parse(input)?;
    let has_def = has_def.is_some();
    let (input, ident) = identification(input)?;
    let Some(name_str) = ident.name.clone() else {
        return Err(nom::Err::Error(nom::error::Error::new(
            input,
            nom::error::ErrorKind::Tag,
        )));
    };
    if disambiguate_from_usage {
        let (peek_input, _) = ws_and_comments(input)?;
        let peek = peek_input.fragment();
        if starts_with_keyword(peek, b"redefines") || starts_with_keyword(peek, b":>>") {
            return Err(nom::Err::Error(nom::error::Error::new(
                peek_input,
                nom::error::ErrorKind::Tag,
            )));
        }
    }
    let (input, typing_result) =
        nom::combinator::opt(|i| attribute_typing(i, true)).parse(input)?;
    let (typing_span, typing) = typing_result
        .map(|(span, s)| (Some(span), Some(s)))
        .unwrap_or((None, None));
    if disambiguate_from_usage && !has_def && typing.is_none() {
        let (peek_input, _) = ws_and_comments(input)?;
        let peek = peek_input.fragment();
        if peek.starts_with(b"=") || peek.starts_with(b":=") {
            return Err(nom::Err::Error(nom::error::Error::new(
                peek_input,
                nom::error::ErrorKind::Tag,
            )));
        }
    }
    let (input, value) =
        nom::combinator::opt(preceded(ws_and_comments, value_part)).parse(input)?;
    let (input, _) = ws_and_comments(input)?;
    let (input, _) = take_until_terminator(input, b";{")?;
    let (input, body) = attribute_body(input)?;
    Ok((
        input,
        node_from_to(
            start,
            input,
            AttributeDef {
                name: name_str,
                typing,
                value,
                body,
                name_span: None,
                typing_span,
            },
        ),
    ))
}

/// Attribute usage:
/// - `attribute` name ( (`:>` | `:`) type )? ( `redefines` qualified_name )? ( '=' value )? body
/// - `attribute :>>` qualified_name ( '=' value )? body
pub(crate) fn attribute_usage(input: Input<'_>) -> IResult<Input<'_>, Node<AttributeUsage>> {
    enum AttributeUsageHead {
        Named {
            name_span: crate::ast::Span,
            name: String,
        },
        PrefixRedefines {
            redefines_span: crate::ast::Span,
            redefines: String,
        },
    }

    let start = input;
    let (input, _) = ws_and_comments(input)?;
    let (input, _) = tag(&b"attribute"[..]).parse(input)?;
    let (input, _) = ws1(input)?;
    let (input, usage_head) = alt((
        map(
            preceded(
                preceded(ws_and_comments, tag(&b":>>"[..])),
                preceded(ws_and_comments, with_span(qualified_name)),
            ),
            |(redefines_span, redefines)| AttributeUsageHead::PrefixRedefines {
                redefines_span,
                redefines,
            },
        ),
        map(with_span(name), |(name_span, name)| {
            AttributeUsageHead::Named { name_span, name }
        }),
    ))
    .parse(input)?;
    let (
        input,
        name_span,
        name_str,
        typing_span,
        typing,
        redefines_span,
        redefines,
    ) = match usage_head {
        AttributeUsageHead::PrefixRedefines {
            redefines_span,
            redefines,
        } => {
            let (input, typing_result) =
                nom::combinator::opt(|i| attribute_typing(i, false)).parse(input)?;
            let (typing_span, typing) = typing_result
                .map(|(span, s)| (Some(span), Some(s)))
                .unwrap_or((None, None));
            (
                input,
                None,
                local_name_from_qualified_name(&redefines),
                typing_span,
                typing,
                Some(redefines_span),
                Some(redefines),
            )
        }
        AttributeUsageHead::Named { name_span, name } => {
            let (input, typing_result) =
                nom::combinator::opt(|i| attribute_typing(i, true)).parse(input)?;
            let (typing_span, typing) = typing_result
                .map(|(span, s)| (Some(span), Some(s)))
                .unwrap_or((None, None));
            let (input, redefines_result) = nom::combinator::opt(alt((
                preceded(
                    preceded(ws_and_comments, tag(&b"redefines"[..])),
                    preceded(ws1, with_span(qualified_name)),
                ),
                preceded(
                    preceded(ws_and_comments, tag(&b":>>"[..])),
                    preceded(ws_and_comments, with_span(qualified_name)),
                ),
            )))
            .parse(input)?;
            let (redefines_span, redefines) = redefines_result
                .map(|(span, s)| (Some(span), Some(s)))
                .unwrap_or((None, None));
            (
                input,
                Some(name_span),
                name,
                typing_span,
                typing,
                redefines_span,
                redefines,
            )
        }
    };
    let (input, value) =
        nom::combinator::opt(preceded(ws_and_comments, value_part)).parse(input)?;
    // Accept trailing subsetting forms used in libraries and examples, e.g.
    // `attribute :>> outlet :> electricGrid.outlets;`
    // while preserving existing AST shape (AttributeUsage currently has no subsets field).
    let (input, _) = nom::combinator::opt(preceded(
        alt((
            preceded(ws_and_comments, tag(&b":>"[..])),
            preceded(ws_and_comments, tag(&b"subsets"[..])),
        )),
        preceded(ws_and_comments, |i| take_until_terminator(i, b";{")),
    ))
    .parse(input)?;
    let (input, body) = attribute_body(input)?;
    Ok((
        input,
        node_from_to(
            start,
            input,
            AttributeUsage {
                name: name_str,
                typing,
                redefines,
                value,
                body,
                name_span,
                typing_span,
                redefines_span,
            },
        ),
    ))
}

/// Shorthand attribute usage (no `attribute` keyword) commonly used inside part bodies.
///
/// Supports:
/// - `name : Type ;`
/// - `name : Type = expr ;`
/// - `:>> name : Type = expr ;` (leading `:>>` ignored; treated as a usage)
pub(crate) fn attribute_usage_shorthand(
    input: Input<'_>,
) -> IResult<Input<'_>, Node<AttributeUsage>> {
    let start = input;
    let (input, _) = ws_and_comments(input)?;
    let (input, _) =
        nom::combinator::opt(preceded(ws_and_comments, tag(&b":>>"[..]))).parse(input)?;
    let (input, (name_span, name_str)) = with_span(name).parse(input)?;
    if is_reserved_shorthand_starter(&name_str) {
        return Err(nom::Err::Error(nom::error::Error::new(
            start,
            nom::error::ErrorKind::Tag,
        )));
    }
    let (input, _) = preceded(ws_and_comments, tag(&b":"[..])).parse(input)?;
    // Parse (and ignore) the declared type.
    let (input, _) = preceded(ws_and_comments, qualified_name).parse(input)?;
    // Keep shorthand values on the shared expression path so precedence/parentheses are preserved.
    let (input, value) =
        nom::combinator::opt(preceded(ws_and_comments, value_part)).parse(input)?;
    let (input, _) = preceded(ws_and_comments, tag(&b";"[..])).parse(input)?;
    Ok((
        input,
        node_from_to(
            start,
            input,
            AttributeUsage {
                name: name_str,
                typing: None,
                redefines: None,
                value,
                body: AttributeBody::Semicolon,
                name_span: Some(name_span),
                typing_span: None,
                redefines_span: None,
            },
        ),
    ))
}