use super::*;
#[derive(Default)]
struct DirectiveState {
event: Option<Event>,
conditions: Vec<Expr>,
annotations: Vec<Annotation>,
disabled: bool,
delimiter: bool,
new_page: Option<String>,
}
impl Parser<'_> {
pub(super) fn parse_rule(
&mut self,
rules: &mut Vec<RuleEntry>,
rule_prefix: Option<String>,
) -> bool {
let start = self.advance();
let name = match self.peek_kind() {
TokenKind::String => self.advance().text,
TokenKind::LParen => {
let span = Span::new(start.span.file, start.span.start, self.peek().span.end);
self.report_workshop_source(
span,
"a `rule(\"name\")` header; OPY uses `rule \"name\":` plus an indented block",
);
self.skip_workshop_construct();
return false;
}
TokenKind::LBrace => {
self.report_workshop_source(self.peek().span, BRACE_BLOCKS);
self.skip_workshop_construct();
return false;
}
_ => {
self.error_at_current("expected a rule name string after `rule`".to_string());
return false;
}
};
let name_token_span = self.tokens[self.pos.saturating_sub(1)].span;
let name_span = Span::new(
name_token_span.file,
Position::new(name_token_span.start.line, name_token_span.start.col + 1),
Position::new(
name_token_span.end.line,
name_token_span
.end
.col
.saturating_sub(1)
.max(name_token_span.start.col + 1),
),
);
let line_indent = start.layout.start.col;
if self
.expect_block_indent(line_indent, "':' after the rule name")
.is_err()
{
return false;
}
let body_indent = line_indent + 1;
let mut directives = DirectiveState::default();
let mut actions = Vec::new();
loop {
self.skip_newlines();
if self.peek_kind() == TokenKind::Eof || self.peek().layout.start.col < body_indent {
break;
}
if self.peek_kind() == TokenKind::At {
if !self.parse_directive(&mut directives, false) {
self.recover_line();
}
continue;
}
match self.parse_statement() {
Ok(stmt) => actions.push(stmt),
Err(()) => self.recover_line(),
}
}
rules.push(RuleEntry::Rule(Rule {
name,
span: Span::new(start.span.file, start.span.start, name_token_span.end),
name_span,
disabled: directives.disabled,
delimiter: directives.delimiter,
new_page: directives.new_page,
annotations: directives.annotations,
rule_prefix,
event: directives.event.unwrap_or_else(|| Event {
name: "global".to_string(),
args: Vec::new(),
span: start.span,
}),
conditions: directives.conditions,
actions,
}));
true
}
fn parse_directive(&mut self, directives: &mut DirectiveState, subroutine: bool) -> bool {
let DirectiveState {
event,
conditions,
annotations,
disabled,
delimiter,
new_page,
} = directives;
let at = self.advance();
let name = match self.expect_ident("a directive name after '@'") {
Ok(name) => name,
Err(()) => return false,
};
if matches!(
name.as_str(),
"Event" | "Team" | "Slot" | "Hero" | "Name" | "Disabled" | "Delimiter" | "NewPage"
) && annotations.iter().any(|annotation| annotation.name == name)
{
self.error_at_current(format!("annotation '@{name}' was already declared"));
return false;
}
match name.as_str() {
"Event" => {
let event_name = match self.expect_ident("an event name after @Event") {
Ok(name) => name,
Err(()) => return false,
};
let event_annotation_arg = AnnotationArg {
text: event_name.clone(),
span: self.tokens[self.pos.saturating_sub(1)].span,
};
let mut args = Vec::new();
if self.peek_kind() == TokenKind::LParen
&& self.parse_event_args(&mut args).is_err()
{
return false;
}
let end = args
.last()
.map_or(event_annotation_arg.span.end, |arg| arg.span().end);
*event = Some(Event {
name: event_name,
args,
span: Span::new(at.span.file, at.span.start, end),
});
annotations.push(Annotation {
name,
args: vec![event_annotation_arg],
span: Span::new(at.span.file, at.span.start, end),
});
true
}
"Condition" => {
let start = self.pos;
match self.parse_expr() {
Ok(expr) => {
let end = expr.span().end;
conditions.push(expr);
annotations.push(Annotation {
name,
args: vec![self.raw_annotation_arg(start, self.pos)],
span: Span::new(at.span.file, at.span.start, end),
});
true
}
Err(()) => false,
}
}
"Team" | "Slot" | "Hero" => {
let args = self.consume_annotation_args();
if args.len() != 1 {
self.error_at_current(format!("@{name} expects exactly one argument"));
return false;
}
if subroutine {
self.error_at_current(format!("@{name} is not valid on a subroutine"));
return false;
}
if (name == "Slot"
&& annotations
.iter()
.any(|annotation| annotation.name == "Hero"))
|| (name == "Hero"
&& annotations
.iter()
.any(|annotation| annotation.name == "Slot"))
{
self.error_at_current("@Slot and @Hero cannot be used together".to_string());
return false;
}
annotations.push(annotation(name, args, at.span));
true
}
"Name" => {
let args = self.consume_annotation_args();
if args.len() != 1 || !self.annotation_arg_is_string(&args[0]) {
self.error_at_current(
"@Name expects exactly one plain string literal".to_string(),
);
return false;
}
annotations.push(annotation(name, args, at.span));
true
}
"SuppressWarnings" => {
let args = self.consume_annotation_args();
if args.is_empty() || args.iter().any(|arg| !is_identifier(&arg.text)) {
self.error_at_current(
"@SuppressWarnings expects one or more warning identifiers".to_string(),
);
return false;
}
annotations.push(annotation(name, args, at.span));
true
}
"Disabled" => {
if !self.expect_annotation_end("@Disabled") {
return false;
}
*disabled = true;
annotations.push(annotation(name, Vec::new(), at.span));
true
}
"Delimiter" => {
if !self.expect_annotation_end("@Delimiter") {
return false;
}
*delimiter = true;
annotations.push(annotation(name, Vec::new(), at.span));
true
}
"NewPage" => {
let args = self.consume_annotation_args();
if args.len() > 1
|| args
.first()
.is_some_and(|arg| !self.annotation_arg_is_string(arg))
{
self.error_at_current(
"@NewPage expects at most one plain string literal".to_string(),
);
return false;
}
*new_page = args.first().map(|arg| unquote_annotation_arg(&arg.text));
annotations.push(annotation(name, args, at.span));
true
}
other => {
self.error_at_current(format!("unsupported directive '@{other}'"));
false
}
}
}
pub(super) fn consume_annotation_args(&mut self) -> Vec<AnnotationArg> {
let start = self.pos;
while self.peek_kind() != TokenKind::Newline && self.peek_kind() != TokenKind::Eof {
self.bump();
}
if self.pos == start {
return Vec::new();
}
let tokens = &self.tokens[start..self.pos];
if tokens.len() == 3
&& tokens[1].kind == TokenKind::Dot
&& tokens[0].kind == TokenKind::Ident
{
return vec![AnnotationArg {
text: tokens.iter().map(|token| token.text.as_str()).collect(),
span: Span::new(
tokens[0].span.file,
tokens[0].span.start,
tokens[2].span.end,
),
}];
}
tokens
.iter()
.map(|token| AnnotationArg {
text: if token.kind == TokenKind::String {
format!("\"{}\"", token.text)
} else {
token.text.clone()
},
span: token.span,
})
.collect()
}
pub(super) fn raw_annotation_arg(&self, start: usize, end: usize) -> AnnotationArg {
let tokens = &self.tokens[start..end];
let first = tokens
.first()
.map(|token| token.span)
.unwrap_or(self.peek().span);
let last = tokens.last().map(|token| token.span).unwrap_or(first);
AnnotationArg {
text: tokens.iter().map(|token| token.text.as_str()).collect(),
span: Span::new(first.file, first.start, last.end),
}
}
pub(super) fn annotation_arg_is_string(&self, arg: &AnnotationArg) -> bool {
arg.text.starts_with('"') && arg.text.ends_with('"')
}
pub(super) fn expect_annotation_end(&mut self, name: &str) -> bool {
if self.peek_kind() == TokenKind::Newline || self.peek_kind() == TokenKind::Eof {
true
} else {
self.error_at_current(format!("{name} takes no arguments"));
false
}
}
pub(super) fn parse_def(
&mut self,
rules: &mut Vec<RuleEntry>,
rule_prefix: Option<String>,
) -> bool {
let start = self.advance();
let name_span = self.peek().span;
let name = match self.expect_ident("a subroutine name after `def`") {
Ok(name) => name,
Err(()) => return false,
};
if self.peek_kind() == TokenKind::LBrace {
self.report_workshop_source(self.peek().span, BRACE_BLOCKS);
self.skip_workshop_construct();
return false;
}
let params = match self.parse_param_list() {
Some(params) => params,
None => return false,
};
if !params.is_empty() {
self.error_at_current(
"subroutine parameters are outside the declared language support surface"
.to_string(),
);
return false;
}
let Ok(body_indent) =
self.expect_block_indent(start.layout.start.col, "':' after the subroutine signature")
else {
return false;
};
let mut directives = DirectiveState::default();
loop {
self.skip_newlines();
if self.peek_kind() != TokenKind::At {
break;
}
if !self.parse_directive(&mut directives, true) {
self.recover_line();
return false;
}
}
if directives.event.is_some() || !directives.conditions.is_empty() {
self.error_at_current("subroutines cannot have events or conditions".to_string());
return false;
}
let presentation_name = directives
.annotations
.iter()
.find(|annotation| annotation.name == "Name")
.and_then(|annotation| annotation.args.first())
.map(|arg| unquote_annotation_arg(&arg.text));
let body = self.parse_block(body_indent);
let span = Span::new(start.span.file, start.span.start, name_span.end);
rules.push(RuleEntry::SubroutineDef {
name,
presentation_name,
span,
name_span,
body,
annotations: directives.annotations,
rule_prefix,
});
true
}
}
fn annotation(name: String, args: Vec<AnnotationArg>, directive_span: Span) -> Annotation {
let end = args.last().map_or(directive_span.end, |arg| arg.span.end);
Annotation {
name,
args,
span: Span::new(directive_span.file, directive_span.start, end),
}
}