use crate::error::Error;
use crate::limits::ResourceLimits;
use crate::parsing::ast::{try_parse_type_constraint_command, *};
use crate::parsing::lexer::{
can_be_label, can_be_reference_segment, can_be_repository_qualifier_segment,
conversion_target_from_token, is_boolean_keyword, is_math_function, is_spec_body_keyword,
is_structural_keyword, is_type_keyword, token_kind_to_boolean_value, token_kind_to_primitive,
Lexer, LexerCheckpoint, Token, TokenKind,
};
use crate::parsing::source::Source;
use indexmap::IndexMap;
use rust_decimal::Decimal;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug)]
pub struct ParseResult {
pub repositories: IndexMap<Arc<LemmaRepository>, Vec<LemmaSpec>>,
pub expression_count: usize,
}
impl ParseResult {
#[must_use]
pub fn flatten_specs(&self) -> Vec<&LemmaSpec> {
self.repositories
.values()
.flat_map(|specs| specs.iter())
.collect()
}
#[must_use]
pub fn into_flattened_specs(self) -> Vec<LemmaSpec> {
self.repositories.into_values().flatten().collect()
}
}
pub fn parse(
content: &str,
source_type: crate::parsing::source::SourceType,
limits: &ResourceLimits,
) -> Result<ParseResult, Error> {
if content.len() > limits.max_source_size_bytes {
return Err(Error::resource_limit_exceeded(
"max_source_size_bytes",
format!(
"{} bytes ({} MB)",
limits.max_source_size_bytes,
limits.max_source_size_bytes / (1024 * 1024)
),
format!(
"{} bytes ({:.2} MB)",
content.len(),
content.len() as f64 / (1024.0 * 1024.0)
),
"Reduce source size or split into multiple specs",
None,
None,
None,
));
}
let mut parser = Parser::new(content, source_type, limits);
let repositories = parser.parse_file()?;
Ok(ParseResult {
repositories,
expression_count: parser.expression_count,
})
}
struct Parser {
lexer: Lexer,
source_type: crate::parsing::source::SourceType,
depth_tracker: DepthTracker,
expression_count: usize,
max_expression_count: usize,
max_spec_name_length: usize,
max_data_name_length: usize,
max_rule_name_length: usize,
last_span: Span,
}
impl Parser {
fn new(
content: &str,
source_type: crate::parsing::source::SourceType,
limits: &ResourceLimits,
) -> Self {
Parser {
lexer: Lexer::new(content, &source_type),
source_type,
depth_tracker: DepthTracker::with_max_depth(limits.max_expression_depth),
expression_count: 0,
max_expression_count: limits.max_expression_count,
max_spec_name_length: crate::limits::MAX_SPEC_NAME_LENGTH,
max_data_name_length: crate::limits::MAX_DATA_NAME_LENGTH,
max_rule_name_length: crate::limits::MAX_RULE_NAME_LENGTH,
last_span: Span {
start: 0,
end: 0,
line: 1,
col: 0,
},
}
}
fn source_type(&self) -> crate::parsing::source::SourceType {
self.source_type.clone()
}
fn peek(&mut self) -> Result<&Token, Error> {
self.lexer.peek()
}
fn next(&mut self) -> Result<Token, Error> {
let token = self.lexer.next_token()?;
self.last_span = token.span.clone();
Ok(token)
}
fn at(&mut self, kind: &TokenKind) -> Result<bool, Error> {
Ok(&self.peek()?.kind == kind)
}
fn at_any(&mut self, kinds: &[TokenKind]) -> Result<bool, Error> {
let current = &self.peek()?.kind;
Ok(kinds.contains(current))
}
fn checkpoint(&self) -> (LexerCheckpoint, usize) {
(self.lexer.checkpoint(), self.expression_count)
}
fn restore(&mut self, checkpoint: (LexerCheckpoint, usize)) {
self.lexer.restore(checkpoint.0);
self.expression_count = checkpoint.1;
}
fn expect(&mut self, kind: &TokenKind) -> Result<Token, Error> {
let token = self.next()?;
if &token.kind == kind {
Ok(token)
} else {
Err(self.error_at_token(&token, format!("Expected {}, found {}", kind, token.kind)))
}
}
fn error_at_token(&self, token: &Token, message: impl Into<String>) -> Error {
Error::parsing(
message,
Source::new(self.source_type(), token.span.clone()),
None::<String>,
)
}
fn error_at_token_with_suggestion(
&self,
token: &Token,
message: impl Into<String>,
suggestion: impl Into<String>,
) -> Error {
Error::parsing(
message,
Source::new(self.source_type(), token.span.clone()),
Some(suggestion),
)
}
fn parse_spec_ref_trailing_effective(&mut self) -> Result<Option<DateTimeValue>, Error> {
let mut effective = None;
if self.at(&TokenKind::NumberLit)? {
let peeked = self.peek()?;
if peeked.text.len() == 4 && peeked.text.chars().all(|c| c.is_ascii_digit()) {
effective = self.try_parse_effective_from()?;
}
}
Ok(effective)
}
fn make_source(&self, span: Span) -> Source {
Source::new(self.source_type(), span)
}
fn span_from(&self, start: &Span) -> Span {
Span {
start: start.start,
end: start.end.max(start.start),
line: start.line,
col: start.col,
}
}
fn span_covering(&self, start: &Span, end: &Span) -> Span {
Span {
start: start.start,
end: end.end,
line: start.line,
col: start.col,
}
}
fn parse_file(&mut self) -> Result<IndexMap<Arc<LemmaRepository>, Vec<LemmaSpec>>, Error> {
let mut map: IndexMap<Arc<LemmaRepository>, Vec<LemmaSpec>> = IndexMap::new();
let mut current_repo = Arc::new(LemmaRepository::new(None));
loop {
if self.at(&TokenKind::Eof)? {
break;
}
if self.at(&TokenKind::Repo)? {
let repo_token = self.expect(&TokenKind::Repo)?;
let start_line = repo_token.span.line;
let (qualifier, _) = self.parse_repository_qualifier()?;
crate::limits::check_max_length(
&qualifier.name,
self.max_spec_name_length,
"repository name",
Some(Source::new(self.source_type(), repo_token.span)),
)?;
current_repo = Arc::new(
LemmaRepository::new(Some(qualifier.name)).with_start_line(start_line),
);
map.entry(Arc::clone(¤t_repo)).or_default();
continue;
}
if self.at(&TokenKind::Spec)? {
let spec = self.parse_spec()?;
map.entry(Arc::clone(¤t_repo)).or_default().push(spec);
continue;
}
let token = self.next()?;
return Err(self.error_at_token_with_suggestion(
&token,
format!(
"Expected a top-level `repo` or `spec` declaration, found {}",
token.kind
),
"Each Lemma file is a sequence of optional `repo <name>` sections followed by `spec <name>` blocks",
));
}
Ok(map)
}
fn parse_spec(&mut self) -> Result<LemmaSpec, Error> {
let spec_token = self.expect(&TokenKind::Spec)?;
let start_line = spec_token.span.line;
let (name, name_span) = self.parse_spec_name()?;
crate::limits::check_max_length(
&name,
self.max_spec_name_length,
"spec",
Some(Source::new(self.source_type(), name_span)),
)?;
let effective_from = self.try_parse_effective_from()?;
let commentary = self.try_parse_commentary()?;
let mut spec = LemmaSpec::new(name.clone())
.with_source_type(self.source_type())
.with_start_line(start_line);
spec.effective_from = crate::parsing::ast::EffectiveDate::from_option(effective_from);
if let Some(commentary_text) = commentary {
spec = spec.set_commentary(commentary_text);
}
let mut data = Vec::new();
let mut rules = Vec::new();
let mut meta_fields = Vec::new();
loop {
let peek_kind = self.peek()?.kind.clone();
match peek_kind {
TokenKind::Data => {
let datum = self.parse_data()?;
data.push(datum);
}
TokenKind::Fill => {
let datum = self.parse_fill()?;
data.push(datum);
}
TokenKind::Rule => {
let rule = self.parse_rule()?;
rules.push(rule);
}
TokenKind::Type => {
let token = self.next()?;
return Err(self.error_at_token_with_suggestion(
&token,
"'type' cannot start a declaration here",
"Declare types with 'data', e.g. 'data age: number -> minimum 0'",
));
}
TokenKind::Meta => {
let meta = self.parse_meta()?;
meta_fields.push(meta);
}
TokenKind::Uses => {
let uses_data = self.parse_uses_statement()?;
data.extend(uses_data);
}
TokenKind::Spec | TokenKind::Repo | TokenKind::Eof => break,
_ => {
let token = self.next()?;
return Err(self.error_at_token_with_suggestion(
&token,
format!(
"Expected 'data', 'fill', 'rule', 'meta', 'uses', or a new 'spec', found '{}'",
token.text
),
"Check the spelling or add the appropriate keyword",
));
}
}
}
for data in data {
spec = spec.add_data(data);
}
for rule in rules {
spec = spec.add_rule(rule);
}
for meta in meta_fields {
spec = spec.add_meta_field(meta);
}
Ok(spec)
}
fn parse_spec_name(&mut self) -> Result<(String, Span), Error> {
if self.at(&TokenKind::At)? {
let at_tok = self.next()?;
return Err(Error::parsing(
"'@' is not allowed in spec names; it is valid for repository names (`repo @org/name`) and qualifiers (`uses @org/name`)",
self.make_source(at_tok.span),
Some(
"Write `spec my_spec`, then reference registry specs as `uses alias: @org/repo spec_name` or `data x: alias.TypeName` after importing with `uses`.",
),
));
}
let first = self.next()?;
if !first.kind.is_identifier_like() {
return Err(self.error_at_token(
&first,
format!("Expected a spec name, found {}", first.kind),
));
}
let mut name = first.text.clone();
let start_span = first.span.clone();
let mut end_span = first.span.clone();
loop {
if self.at(&TokenKind::Slash)? {
self.next()?;
let seg = self.next()?;
if !seg.kind.is_identifier_like() {
return Err(self.error_at_token(
&seg,
format!(
"Expected identifier after '/' in spec name, found {}",
seg.kind
),
));
}
name.push('/');
name.push_str(&seg.text);
end_span = seg.span.clone();
} else if self.at(&TokenKind::Dot)? {
self.next()?;
let seg = self.next()?;
if !seg.kind.is_identifier_like() {
return Err(self.error_at_token(
&seg,
format!(
"Expected identifier after '.' in spec name, found {}",
seg.kind
),
));
}
name.push('.');
name.push_str(&seg.text);
end_span = seg.span.clone();
} else if self.at(&TokenKind::Minus)? {
let minus_span = self.peek()?.span.clone();
self.next()?;
let peeked = self.peek()?;
if !peeked.kind.is_identifier_like() {
let span = self.span_covering(&start_span, &minus_span);
return Err(Error::parsing(
"Trailing '-' after spec name",
self.make_source(span),
None::<String>,
));
}
let seg = self.next()?;
name.push('-');
name.push_str(&seg.text);
end_span = seg.span.clone();
} else {
break;
}
}
let full_span = self.span_covering(&start_span, &end_span);
Ok((name, full_span))
}
fn parse_repository_qualifier(&mut self) -> Result<(RepositoryQualifier, Span), Error> {
let has_at = self.at(&TokenKind::At)?;
let start_span = if has_at {
let at_tok = self.next()?;
at_tok.span.clone()
} else {
Span {
start: 0,
end: 0,
line: 0,
col: 0,
}
};
let first = self.next()?;
if !can_be_repository_qualifier_segment(&first.kind) {
return Err(self.error_at_token(
&first,
format!(
"Expected a repository qualifier segment, found {}",
first.kind
),
));
}
if !has_at && is_structural_keyword(&first.kind) {
return Err(self.error_at_token(
&first,
format!(
"'{}' is a reserved keyword and cannot be used as a repository name",
first.text
),
));
}
let start_span = if has_at {
start_span
} else {
first.span.clone()
};
let mut name = first.text.clone();
loop {
let next_kind = self.peek()?.kind.clone();
match next_kind {
TokenKind::Slash => {
self.next()?;
name.push('/');
let seg = self.next()?;
if !can_be_repository_qualifier_segment(&seg.kind) {
return Err(self.error_at_token(
&seg,
format!(
"Expected identifier after '/' in repository qualifier segment, found {}",
seg.kind
),
));
}
name.push_str(&seg.text);
}
TokenKind::Dot => {
self.next()?;
name.push('.');
let seg = self.next()?;
if !can_be_repository_qualifier_segment(&seg.kind) {
return Err(self.error_at_token(
&seg,
format!(
"Expected identifier after '.' in repository qualifier segment, found {}",
seg.kind
),
));
}
name.push_str(&seg.text);
}
TokenKind::Minus => {
let minus_text_peek = self.lexer.peek_second()?;
if !can_be_repository_qualifier_segment(&minus_text_peek.kind) {
break;
}
self.next()?;
name.push('-');
let seg = self.next()?;
name.push_str(&seg.text);
}
_ => break,
}
}
if has_at {
name.insert(0, '@');
}
let full_span = self.span_covering(&start_span, &self.last_span);
Ok((RepositoryQualifier { name }, full_span))
}
pub fn parse_spec_ref_target(&mut self) -> Result<SpecRef, Error> {
let mut repository = None;
let mut repository_span = None;
if self.at(&TokenKind::At)? {
let (q, span) = self.parse_repository_qualifier()?;
repository = Some(q);
repository_span = Some(span);
} else {
let saved_state = self.lexer.clone();
if let Ok((potential_repository, span)) = self.parse_repository_qualifier() {
if let Ok(next_tok) = self.peek() {
if next_tok.kind.is_identifier_like() {
repository = Some(potential_repository);
repository_span = Some(span);
} else {
self.lexer = saved_state;
}
} else {
self.lexer = saved_state;
}
} else {
self.lexer = saved_state;
}
}
let (spec_name, spec_name_span) = self.parse_spec_name()?;
let effective = self.parse_spec_ref_trailing_effective()?;
let target_span = self.span_covering(&spec_name_span, &self.last_span);
let has_repository = repository.is_some();
Ok(SpecRef {
name: spec_name,
repository,
effective,
repository_span: if has_repository {
repository_span
} else {
None
},
target_span: Some(target_span),
})
}
fn try_parse_effective_from(&mut self) -> Result<Option<DateTimeValue>, Error> {
if !self.at(&TokenKind::NumberLit)? {
return Ok(None);
}
let peeked = self.peek()?;
let peeked_text = peeked.text.clone();
let peeked_span = peeked.span.clone();
if peeked_text.len() == 4 && peeked_text.chars().all(|c| c.is_ascii_digit()) {
let mut dt_str = String::new();
let num_tok = self.next()?; dt_str.push_str(&num_tok.text);
while self.at(&TokenKind::Minus)? {
self.next()?; dt_str.push('-');
let part = self.next()?;
dt_str.push_str(&part.text);
}
if self.at(&TokenKind::Identifier)? {
let peeked = self.peek()?;
if peeked.text.starts_with('T') || peeked.text.starts_with('t') {
let time_part = self.next()?;
dt_str.push_str(&time_part.text);
while self.at(&TokenKind::Colon)? {
self.next()?;
dt_str.push(':');
let part = self.next()?;
dt_str.push_str(&part.text);
}
if self.at(&TokenKind::Plus)? {
self.next()?;
dt_str.push('+');
let tz_part = self.next()?;
dt_str.push_str(&tz_part.text);
if self.at(&TokenKind::Colon)? {
self.next()?;
dt_str.push(':');
let tz_min = self.next()?;
dt_str.push_str(&tz_min.text);
}
}
}
}
if let Ok(dtv) = dt_str.parse::<DateTimeValue>() {
return Ok(Some(dtv));
}
return Err(Error::parsing(
format!("Invalid date/time in spec declaration: '{}'", dt_str),
self.make_source(peeked_span),
None::<String>,
));
}
Ok(None)
}
fn try_parse_commentary(&mut self) -> Result<Option<String>, Error> {
if !self.at(&TokenKind::Commentary)? {
return Ok(None);
}
let token = self.next()?;
let trimmed = token.text.trim().to_string();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed))
}
}
fn parse_data(&mut self) -> Result<LemmaData, Error> {
let data_token = self.expect(&TokenKind::Data)?;
let start_span = data_token.span.clone();
let reference = self.parse_reference()?;
for segment in reference
.segments
.iter()
.chain(std::iter::once(&reference.name))
{
crate::limits::check_max_length(
segment,
self.max_data_name_length,
"data",
Some(Source::new(self.source_type(), start_span.clone())),
)?;
}
self.expect(&TokenKind::Colon)?;
if !reference.segments.is_empty() {
let tok = self.peek()?.clone();
return Err(self.error_at_token_with_suggestion(
&tok,
"Dotted paths require `fill`; `data` declares types and values on local names only.",
"Use `fill path.to.slot: <value or reference>` to assign on an imported or nested slot.",
));
}
let value = self.parse_data_value()?;
let span = self.span_covering(&start_span, &self.last_span);
let source = self.make_source(span);
Ok(LemmaData::new(reference, value, source))
}
fn parse_fill(&mut self) -> Result<LemmaData, Error> {
let fill_token = self.expect(&TokenKind::Fill)?;
let start_span = fill_token.span.clone();
let reference = self.parse_reference()?;
for segment in reference
.segments
.iter()
.chain(std::iter::once(&reference.name))
{
crate::limits::check_max_length(
segment,
self.max_data_name_length,
"fill",
Some(Source::new(self.source_type(), start_span.clone())),
)?;
}
self.expect(&TokenKind::Colon)?;
let value = self.parse_fill_value()?;
let span = self.span_covering(&start_span, &self.last_span);
let source = self.make_source(span);
Ok(LemmaData::new(reference, value, source))
}
fn fill_rhs_starts_as_literal(&self, kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::StringLit | TokenKind::NumberLit | TokenKind::Minus | TokenKind::Plus
) || is_boolean_keyword(kind)
}
fn parse_fill_value(&mut self) -> Result<DataValue, Error> {
let peek_kind = self.peek()?.kind.clone();
if self.fill_rhs_starts_as_literal(&peek_kind) {
let value = self.parse_literal_value()?;
return Ok(DataValue::Fill(FillRhs::Literal(value)));
}
if can_be_label(&peek_kind) || is_type_keyword(&peek_kind) {
let target = self.parse_reference()?;
if self.at(&TokenKind::Arrow)? {
let tok = self.peek()?.clone();
return Err(self.error_at_token_with_suggestion(
&tok,
"Constraint chains (`-> ...`) are not allowed on `fill`; use `data` to declare types and constraints.",
"Use `data name: <type> -> ...` for constraints, then `fill name: <reference or literal>` to assign.",
));
}
return Ok(DataValue::Fill(FillRhs::Reference { target }));
}
let tok = self.peek()?.clone();
Err(self.error_at_token(
&tok,
format!(
"Expected a reference or literal after `fill ...:`, found {}",
tok.kind
),
))
}
fn parse_reference(&mut self) -> Result<Reference, Error> {
let mut segments = Vec::new();
let first = self.next()?;
if is_structural_keyword(&first.kind) {
return Err(self.error_at_token_with_suggestion(
&first,
format!(
"'{}' is a reserved keyword and cannot be used as a name",
first.text
),
"Choose a different name that is not a reserved keyword",
));
}
if !can_be_reference_segment(&first.kind) {
return Err(self.error_at_token(
&first,
format!("Expected an identifier, found {}", first.kind),
));
}
segments.push(first.text.clone());
while self.at(&TokenKind::Dot)? {
self.next()?; let seg = self.next()?;
if !can_be_reference_segment(&seg.kind) {
return Err(self.error_at_token(
&seg,
format!("Expected an identifier after '.', found {}", seg.kind),
));
}
segments.push(seg.text.clone());
}
Ok(Reference::from_path(segments))
}
fn parse_data_value(&mut self) -> Result<DataValue, Error> {
if self.at(&TokenKind::Spec)? {
let token = self.next()?;
return Err(self.error_at_token_with_suggestion(
&token,
"Cannot import a spec with `data`; use `uses`",
"Use `uses <spec_name>` or `uses <alias>: <spec_name>`",
));
}
let peek_kind = self.peek()?.kind.clone();
if token_kind_to_primitive(&peek_kind).is_some() || can_be_label(&peek_kind) {
let (base, constraints) = self.parse_type_arrow_chain()?;
return Ok(DataValue::Definition {
base: Some(base),
constraints,
value: None,
});
}
let value = self.parse_literal_value()?;
Ok(DataValue::Definition {
base: None,
constraints: None,
value: Some(value),
})
}
fn parse_uses_item(&mut self, start_span: &Span) -> Result<LemmaData, Error> {
let explicit_alias = if can_be_reference_segment(&self.peek()?.kind)
&& self.lexer.peek_second()?.kind == TokenKind::Colon
{
let alias_tok = self.next()?;
self.expect(&TokenKind::Colon)?;
Some(alias_tok)
} else {
None
};
let spec_ref = self.parse_spec_ref_target()?;
let spec_name_source = spec_ref
.target_span
.as_ref()
.map(|sp| Source::new(self.source_type(), sp.clone()));
crate::limits::check_max_length(
&spec_ref.name,
self.max_spec_name_length,
"spec",
spec_name_source.clone(),
)?;
let alias = if let Some(ref alias_tok) = explicit_alias {
crate::limits::check_max_length(
&alias_tok.text,
self.max_data_name_length,
"data",
Some(Source::new(self.source_type(), alias_tok.span.clone())),
)?;
alias_tok.text.clone()
} else {
let implicit = spec_ref.name.clone();
crate::limits::check_max_length(
&implicit,
self.max_data_name_length,
"data",
spec_name_source,
)?;
implicit
};
let span = self.span_covering(start_span, &self.last_span);
Ok(LemmaData::new(
Reference::local(alias),
DataValue::Import(spec_ref),
self.make_source(span),
))
}
fn parse_uses_statement(&mut self) -> Result<Vec<LemmaData>, Error> {
let uses_token = self.expect(&TokenKind::Uses)?;
let start_span = uses_token.span.clone();
let mut results = Vec::new();
results.push(self.parse_uses_item(&start_span)?);
while self.at(&TokenKind::Comma)? {
self.next()?;
results.push(self.parse_uses_item(&start_span)?);
}
Ok(results)
}
fn parse_rule(&mut self) -> Result<LemmaRule, Error> {
let rule_token = self.expect(&TokenKind::Rule)?;
let start_span = rule_token.span.clone();
let name_tok = self.next()?;
if is_structural_keyword(&name_tok.kind) {
return Err(self.error_at_token_with_suggestion(
&name_tok,
format!(
"'{}' is a reserved keyword and cannot be used as a rule name",
name_tok.text
),
"Choose a different name that is not a reserved keyword",
));
}
if !can_be_label(&name_tok.kind) && !is_type_keyword(&name_tok.kind) {
return Err(self.error_at_token(
&name_tok,
format!("Expected a rule name, found {}", name_tok.kind),
));
}
let rule_name = name_tok.text.clone();
crate::limits::check_max_length(
&rule_name,
self.max_rule_name_length,
"rule",
Some(Source::new(self.source_type(), name_tok.span.clone())),
)?;
self.expect(&TokenKind::Colon)?;
let expression = if self.at(&TokenKind::Veto)? && !self.at_bare_veto_followed_by_is()? {
self.parse_veto_expression()?
} else {
self.parse_expression()?
};
let mut unless_clauses = Vec::new();
while self.at(&TokenKind::Unless)? {
unless_clauses.push(self.parse_unless_clause()?);
}
let end_span = if let Some(last_unless) = unless_clauses.last() {
last_unless.source_location.span.clone()
} else if let Some(ref loc) = expression.source_location {
loc.span.clone()
} else {
start_span.clone()
};
let span = self.span_covering(&start_span, &end_span);
Ok(LemmaRule {
name: rule_name,
expression,
unless_clauses,
source_location: self.make_source(span),
})
}
fn parse_veto_expression(&mut self) -> Result<Expression, Error> {
let veto_tok = self.expect(&TokenKind::Veto)?;
let start_span = veto_tok.span.clone();
let message = if self.at(&TokenKind::StringLit)? {
let str_tok = self.next()?;
let content = unquote_string(&str_tok.text);
Some(content)
} else {
None
};
let span = self.span_from(&start_span);
self.new_expression(
ExpressionKind::Veto(VetoExpression { message }),
self.make_source(span),
)
}
fn parse_unless_clause(&mut self) -> Result<UnlessClause, Error> {
let unless_tok = self.expect(&TokenKind::Unless)?;
let start_span = unless_tok.span.clone();
let condition = self.parse_expression()?;
self.expect(&TokenKind::Then)?;
let result = if self.at(&TokenKind::Veto)? {
self.parse_veto_expression()?
} else {
self.parse_expression()?
};
let end_span = result
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
Ok(UnlessClause {
condition,
result,
source_location: self.make_source(span),
})
}
fn parse_leaf_parent_type(&mut self) -> Result<ParentType, Error> {
let name_tok = self.next()?;
self.parse_leaf_parent_type_from_first_token(name_tok)
}
fn parse_leaf_parent_type_from_first_token(
&mut self,
name_tok: Token,
) -> Result<ParentType, Error> {
if let Some(kind) = token_kind_to_primitive(&name_tok.kind) {
let primitive = if self.at(&TokenKind::Identifier)?
&& self.peek()?.text.eq_ignore_ascii_case("range")
{
let range_tok = self.peek()?.clone();
match kind {
PrimitiveKind::Date => {
self.next()?;
PrimitiveKind::DateRange
}
PrimitiveKind::Number => {
self.next()?;
PrimitiveKind::NumberRange
}
PrimitiveKind::Quantity => {
self.next()?;
PrimitiveKind::QuantityRange
}
PrimitiveKind::Ratio => {
self.next()?;
PrimitiveKind::RatioRange
}
PrimitiveKind::Calendar => {
self.next()?;
PrimitiveKind::CalendarRange
}
_ => {
return Err(self.error_at_token(
&range_tok,
format!(
"'{} range' is not a valid type. Supported range types: calendar range, date range, number range, quantity range, ratio range",
name_tok.text
),
));
}
}
} else {
kind
};
Ok(ParentType::Primitive { primitive })
} else if can_be_label(&name_tok.kind) {
Ok(ParentType::Custom {
name: name_tok.text.clone(),
})
} else {
Err(self.error_at_token(
&name_tok,
format!("Expected a type name, found {}", name_tok.kind),
))
}
}
fn parse_type_arrow_chain(&mut self) -> Result<(ParentType, Option<Vec<Constraint>>), Error> {
let first = self.parse_leaf_parent_type()?;
let base = if let ParentType::Custom { name } = &first {
if self.at(&TokenKind::Dot)? {
self.next()?;
let inner = self.parse_leaf_parent_type()?;
ParentType::Qualified {
spec_alias: name.clone(),
inner: Box::new(inner),
}
} else {
first
}
} else {
if self.at(&TokenKind::Dot)? {
let dot_tok = self.peek()?.clone();
return Err(self.error_at_token_with_suggestion(
&dot_tok,
"A primitive type cannot be the left segment of a qualified parent path",
"Use `data name: alias.typename` where `alias` is the `uses` import name and `typename` is the parent type.",
));
}
first
};
let constraints = self.parse_trailing_constraints()?;
Ok((base, constraints))
}
fn parse_trailing_constraints(&mut self) -> Result<Option<Vec<Constraint>>, Error> {
let mut commands = Vec::new();
while self.at(&TokenKind::Arrow)? {
self.next()?;
let (cmd, cmd_args) = self.parse_command()?;
commands.push((cmd, cmd_args));
}
let constraints = if commands.is_empty() {
None
} else {
Some(commands)
};
Ok(constraints)
}
fn parse_command(&mut self) -> Result<(TypeConstraintCommand, Vec<CommandArg>), Error> {
let name_tok = self.next()?;
if !can_be_label(&name_tok.kind) && !is_type_keyword(&name_tok.kind) {
return Err(self.error_at_token(
&name_tok,
format!("Expected a command name, found {}", name_tok.kind),
));
}
let cmd = try_parse_type_constraint_command(&name_tok.text).ok_or_else(|| {
self.error_at_token(
&name_tok,
format!(
"Unknown constraint command '{}'. Valid commands: help, default, unit, trait, minimum, maximum, decimals, option, options, length",
name_tok.text
),
)
})?;
let args = if cmd == TypeConstraintCommand::Unit {
self.parse_unit_command_args()?
} else {
self.parse_generic_command_args()?
};
Ok((cmd, args))
}
fn parse_generic_command_args(&mut self) -> Result<Vec<CommandArg>, Error> {
let mut args = Vec::new();
loop {
if self.at(&TokenKind::Arrow)?
|| self.at(&TokenKind::Eof)?
|| is_spec_body_keyword(&self.peek()?.kind)
|| self.at(&TokenKind::Spec)?
{
break;
}
let peek_kind = self.peek()?.kind.clone();
match peek_kind {
TokenKind::NumberLit
| TokenKind::Minus
| TokenKind::Plus
| TokenKind::StringLit => {
let value = self.parse_literal_value()?;
args.push(CommandArg::Literal(value));
}
ref k if is_boolean_keyword(k) => {
let value = self.parse_literal_value()?;
args.push(CommandArg::Literal(value));
}
ref k if can_be_label(k) || is_type_keyword(k) => {
let tok = self.next()?;
args.push(CommandArg::Label(tok.text));
}
_ => break,
}
}
Ok(args)
}
fn parse_scalar_literal_value(&mut self) -> Result<Value, Error> {
let peeked = self.peek()?;
match &peeked.kind {
TokenKind::StringLit => {
let tok = self.next()?;
let content = unquote_string(&tok.text);
Ok(Value::Text(content))
}
k if is_boolean_keyword(k) => {
let tok = self.next()?;
Ok(Value::Boolean(token_kind_to_boolean_value(&tok.kind)))
}
TokenKind::NumberLit => self.parse_number_literal(),
TokenKind::Minus | TokenKind::Plus => self.parse_signed_number_literal(),
_ => {
let tok = self.next()?;
Err(self.error_at_token(
&tok,
format!(
"Expected a value (number, text, boolean, date, etc.), found '{}'",
tok.text
),
))
}
}
}
fn at_command_terminator(&mut self) -> Result<bool, Error> {
if self.at(&TokenKind::Arrow)? || self.at(&TokenKind::Eof)? || self.at(&TokenKind::Spec)? {
return Ok(true);
}
Ok(is_spec_body_keyword(&self.peek()?.kind))
}
fn parse_unit_command_args(&mut self) -> Result<Vec<CommandArg>, Error> {
if self.at_command_terminator()? {
return Ok(Vec::new());
}
let peek_kind = self.peek()?.kind.clone();
if !can_be_label(&peek_kind) && !is_type_keyword(&peek_kind) {
return Ok(Vec::new());
}
let unit_name_tok = self.next()?;
let unit_name_arg = CommandArg::Label(unit_name_tok.text.clone());
let numeric_prefix: Option<Decimal> = if self.at(&TokenKind::NumberLit)? {
let num_tok = self.next()?;
match Decimal::from_str(&num_tok.text) {
Ok(d) => Some(d),
Err(_) => {
return Err(self.error_at_token(
&num_tok,
format!(
"Invalid numeric factor '{}' in unit declaration",
num_tok.text
),
));
}
}
} else {
None
};
let peek_kind_after_prefix = self.peek()?.kind.clone();
let has_compound_expr = (can_be_label(&peek_kind_after_prefix)
|| is_type_keyword(&peek_kind_after_prefix))
&& !self.at_command_terminator()?;
if has_compound_expr {
let factors = self.parse_unit_factors()?;
let prefix = numeric_prefix.unwrap_or(Decimal::ONE);
let unit_arg = CommandArg::UnitExpr(UnitArg::Expr(prefix, factors));
Ok(vec![unit_name_arg, unit_arg])
} else if let Some(factor) = numeric_prefix {
let unit_arg = CommandArg::UnitExpr(UnitArg::Factor(factor));
Ok(vec![unit_name_arg, unit_arg])
} else {
Ok(vec![unit_name_arg])
}
}
fn parse_unit_factors(&mut self) -> Result<Vec<UnitFactor>, Error> {
let mut factors: Vec<UnitFactor> = Vec::new();
let mut denominator_mode = false;
let mut operator_just_consumed = true;
loop {
if self.at_command_terminator()? {
if !operator_just_consumed {
break;
}
break;
}
if self.at(&TokenKind::Star)? {
if operator_just_consumed && !factors.is_empty() {
let bad_tok = self.next()?;
return Err(self.error_at_token(
&bad_tok,
"Unexpected '*' in unit expression: two consecutive operators".to_string(),
));
}
self.next()?;
denominator_mode = false;
operator_just_consumed = true;
continue;
}
if self.at(&TokenKind::Slash)? {
if operator_just_consumed && !factors.is_empty() {
let bad_tok = self.next()?;
return Err(self.error_at_token(
&bad_tok,
"Unexpected '/' in unit expression: two consecutive operators".to_string(),
));
}
self.next()?;
denominator_mode = true;
operator_just_consumed = true;
continue;
}
let peek_kind = self.peek()?.kind.clone();
if !can_be_label(&peek_kind) && !is_type_keyword(&peek_kind) {
break;
}
if !operator_just_consumed {
break;
}
operator_just_consumed = false;
let quantity_ref_tok = self.next()?;
let quantity_ref = quantity_ref_tok.text.clone();
let explicit_exp: Option<i32> = if self.at(&TokenKind::Caret)? {
self.next()?;
let negative = if self.at(&TokenKind::Minus)? {
self.next()?; true
} else {
false
};
if !self.at(&TokenKind::NumberLit)? {
let bad_tok = self.next()?;
return Err(self.error_at_token(
&bad_tok,
format!(
"Expected an integer exponent after '^' in unit expression, found {}",
bad_tok.kind
),
));
}
let exp_tok = self.next()?;
let raw: i32 = exp_tok.text.parse::<i32>().map_err(|_| {
self.error_at_token(
&exp_tok,
format!(
"Exponent '{}' is not a valid integer in unit expression",
exp_tok.text
),
)
})?;
if raw == 0 {
return Err(self.error_at_token(
&exp_tok,
"Exponent cannot be zero in a unit expression".to_string(),
));
}
Some(if negative { -raw } else { raw })
} else {
None
};
let final_exp = match (explicit_exp, denominator_mode) {
(Some(e), true) => -e,
(Some(e), false) => e,
(None, true) => -1,
(None, false) => 1,
};
factors.push(UnitFactor {
quantity_ref,
exp: final_exp,
});
}
Ok(factors)
}
fn parse_meta(&mut self) -> Result<MetaField, Error> {
let meta_tok = self.expect(&TokenKind::Meta)?;
let start_span = meta_tok.span.clone();
let key_tok = self.next()?;
let key = key_tok.text.clone();
self.expect(&TokenKind::Colon)?;
let value = self.parse_meta_value()?;
let span = self.span_covering(&start_span, &self.last_span);
Ok(MetaField {
key,
value,
source_location: self.make_source(span),
})
}
fn parse_meta_value(&mut self) -> Result<MetaValue, Error> {
let peeked = self.peek()?;
match &peeked.kind {
TokenKind::StringLit => {
let value = self.parse_literal_value()?;
return Ok(MetaValue::Literal(value));
}
TokenKind::NumberLit => {
let value = self.parse_literal_value()?;
return Ok(MetaValue::Literal(value));
}
k if is_boolean_keyword(k) => {
let value = self.parse_literal_value()?;
return Ok(MetaValue::Literal(value));
}
_ => {}
}
let mut ident = String::new();
loop {
let peeked = self.peek()?;
match &peeked.kind {
k if k.is_identifier_like() => {
let tok = self.next()?;
ident.push_str(&tok.text);
}
TokenKind::Dot => {
self.next()?;
ident.push('.');
}
TokenKind::Slash => {
self.next()?;
ident.push('/');
}
TokenKind::Minus => {
self.next()?;
ident.push('-');
}
TokenKind::NumberLit => {
let tok = self.next()?;
ident.push_str(&tok.text);
}
_ => break,
}
}
if ident.is_empty() {
let tok = self.peek()?.clone();
return Err(self.error_at_token(&tok, "Expected a meta value"));
}
Ok(MetaValue::Unquoted(ident))
}
fn parse_literal_value(&mut self) -> Result<Value, Error> {
let left = self.parse_scalar_literal_value()?;
if self.at(&TokenKind::Ellipsis)? {
self.next()?;
let right = self.parse_scalar_literal_value()?;
Ok(Value::Range(Box::new(left), Box::new(right)))
} else {
Ok(left)
}
}
fn parse_signed_number_literal(&mut self) -> Result<Value, Error> {
let sign_tok = self.next()?;
let sign_span = sign_tok.span.clone();
let is_negative = sign_tok.kind == TokenKind::Minus;
if !self.at(&TokenKind::NumberLit)? {
let tok = self.peek()?.clone();
return Err(self.error_at_token(
&tok,
format!(
"Expected a number after '{}', found '{}'",
sign_tok.text, tok.text
),
));
}
let value = self.parse_number_literal()?;
if !is_negative {
return Ok(value);
}
match value {
Value::Number(d) => Ok(Value::Number(-d)),
Value::NumberWithUnit(d, unit) => Ok(Value::NumberWithUnit(-d, unit)),
Value::Calendar(d, unit) => Ok(Value::Calendar(-d, unit)),
other => Err(Error::parsing(
format!("Cannot negate this value: {}", other),
self.make_source(sign_span),
None::<String>,
)),
}
}
fn parse_number_literal(&mut self) -> Result<Value, Error> {
let num_tok = self.next()?;
let num_text = &num_tok.text;
let num_span = num_tok.span.clone();
if num_text.len() == 4
&& num_text.chars().all(|c| c.is_ascii_digit())
&& self.at(&TokenKind::Minus)?
{
return self.parse_date_literal(num_text.clone(), num_span);
}
let peeked = self.peek()?;
if num_text.len() == 2
&& num_text.chars().all(|c| c.is_ascii_digit())
&& peeked.kind == TokenKind::Colon
{
return self.try_parse_time_literal(num_text.clone(), num_span);
}
if peeked.kind == TokenKind::PercentPercent {
let pp_tok = self.next()?;
if let Ok(next_peek) = self.peek() {
if next_peek.kind == TokenKind::NumberLit {
return Err(self.error_at_token(
&pp_tok,
"Permille literal cannot be followed by a digit",
));
}
}
let decimal = parse_decimal_string(num_text, &num_span, self)?;
return Ok(Value::NumberWithUnit(decimal, "permille".to_string()));
}
if peeked.kind == TokenKind::Percent {
let pct_tok = self.next()?;
if let Ok(next_peek) = self.peek() {
if next_peek.kind == TokenKind::NumberLit || next_peek.kind == TokenKind::Percent {
return Err(self.error_at_token(
&pct_tok,
"Percent literal cannot be followed by a digit",
));
}
}
let decimal = parse_decimal_string(num_text, &num_span, self)?;
return Ok(Value::NumberWithUnit(decimal, "percent".to_string()));
}
if peeked.kind == TokenKind::PercentKw {
self.next()?; let decimal = parse_decimal_string(num_text, &num_span, self)?;
return Ok(Value::NumberWithUnit(decimal, "percent".to_string()));
}
if peeked.kind == TokenKind::Permille {
self.next()?; let decimal = parse_decimal_string(num_text, &num_span, self)?;
return Ok(Value::NumberWithUnit(decimal, "permille".to_string()));
}
if can_be_label(&peeked.kind) {
let unit_tok = self.next()?;
let decimal = parse_decimal_string(num_text, &num_span, self)?;
if let Some(calendar_unit) = CalendarUnit::from_keyword(&unit_tok.text) {
return Ok(Value::Calendar(decimal, calendar_unit));
}
return Ok(Value::NumberWithUnit(decimal, unit_tok.text.clone()));
}
let decimal = parse_decimal_string(num_text, &num_span, self)?;
Ok(Value::Number(decimal))
}
fn parse_date_literal(&mut self, year_text: String, start_span: Span) -> Result<Value, Error> {
let mut dt_str = year_text;
self.expect(&TokenKind::Minus)?;
dt_str.push('-');
let month_tok = self.expect(&TokenKind::NumberLit)?;
dt_str.push_str(&month_tok.text);
self.expect(&TokenKind::Minus)?;
dt_str.push('-');
let day_tok = self.expect(&TokenKind::NumberLit)?;
dt_str.push_str(&day_tok.text);
if self.at(&TokenKind::Identifier)? {
let peeked = self.peek()?;
if peeked.text.len() >= 2
&& (peeked.text.starts_with('T') || peeked.text.starts_with('t'))
{
let t_tok = self.next()?;
dt_str.push_str(&t_tok.text);
if self.at(&TokenKind::Colon)? {
self.next()?;
dt_str.push(':');
let min_tok = self.next()?;
dt_str.push_str(&min_tok.text);
if self.at(&TokenKind::Colon)? {
self.next()?;
dt_str.push(':');
let sec_tok = self.next()?;
dt_str.push_str(&sec_tok.text);
if self.at(&TokenKind::Dot)? {
self.next()?;
dt_str.push('.');
let frac_tok = self.expect(&TokenKind::NumberLit)?;
dt_str.push_str(&frac_tok.text);
}
}
}
self.try_consume_timezone(&mut dt_str)?;
}
}
if let Ok(dtv) = dt_str.parse::<crate::literals::DateTimeValue>() {
return Ok(Value::Date(dtv));
}
Err(Error::parsing(
format!("Invalid date/time format: '{}'", dt_str),
self.make_source(start_span),
None::<String>,
))
}
fn try_consume_timezone(&mut self, dt_str: &mut String) -> Result<(), Error> {
if self.at(&TokenKind::Identifier)? {
let peeked = self.peek()?;
if (peeked.text == "Z" || peeked.text == "z") && peeked.span.start == self.last_span.end
{
let z_tok = self.next()?;
dt_str.push_str(&z_tok.text);
return Ok(());
}
}
if self.at(&TokenKind::Plus)? || self.at(&TokenKind::Minus)? {
let mut lookahead = self.lexer.clone();
let sign_tok = lookahead.next_token()?;
let hour_tok = lookahead.next_token()?;
let colon_tok = lookahead.next_token()?;
let minute_tok = lookahead.next_token()?;
let attached = sign_tok.span.start == self.last_span.end;
let is_timezone_shape = hour_tok.kind == TokenKind::NumberLit
&& colon_tok.kind == TokenKind::Colon
&& minute_tok.kind == TokenKind::NumberLit;
if attached && is_timezone_shape {
let sign_tok = self.next()?;
dt_str.push_str(&sign_tok.text);
let hour_tok = self.expect(&TokenKind::NumberLit)?;
dt_str.push_str(&hour_tok.text);
self.expect(&TokenKind::Colon)?;
dt_str.push(':');
let min_tok = self.expect(&TokenKind::NumberLit)?;
dt_str.push_str(&min_tok.text);
}
}
Ok(())
}
fn try_parse_time_literal(
&mut self,
hour_text: String,
start_span: Span,
) -> Result<Value, Error> {
let mut time_str = hour_text;
self.expect(&TokenKind::Colon)?;
time_str.push(':');
let min_tok = self.expect(&TokenKind::NumberLit)?;
time_str.push_str(&min_tok.text);
if self.at(&TokenKind::Colon)? {
self.next()?;
time_str.push(':');
let sec_tok = self.expect(&TokenKind::NumberLit)?;
time_str.push_str(&sec_tok.text);
if self.at(&TokenKind::Dot)? {
self.next()?;
time_str.push('.');
let frac_tok = self.expect(&TokenKind::NumberLit)?;
time_str.push_str(&frac_tok.text);
}
}
self.try_consume_timezone(&mut time_str)?;
if let Ok(t) = time_str.parse::<TimeValue>() {
return Ok(Value::Time(TimeValue {
hour: t.hour,
minute: t.minute,
second: t.second,
microsecond: t.microsecond,
timezone: t.timezone,
}));
}
Err(Error::parsing(
format!("Invalid time format: '{}'", time_str),
self.make_source(start_span),
None::<String>,
))
}
fn new_expression(
&mut self,
kind: ExpressionKind,
source: Source,
) -> Result<Expression, Error> {
self.expression_count += 1;
if self.expression_count > self.max_expression_count {
return Err(Error::resource_limit_exceeded(
"max_expression_count",
self.max_expression_count.to_string(),
self.expression_count.to_string(),
"Split logic into multiple rules to reduce expression count",
Some(source),
None,
None,
));
}
Ok(Expression::new(kind, source))
}
fn check_depth(&mut self) -> Result<(), Error> {
if let Err(actual) = self.depth_tracker.push_depth() {
let span = self.peek()?.span.clone();
self.depth_tracker.pop_depth();
return Err(Error::resource_limit_exceeded(
"max_expression_depth",
self.depth_tracker.max_depth().to_string(),
actual.to_string(),
"Simplify nested expressions or break into separate rules",
Some(self.make_source(span)),
None,
None,
));
}
Ok(())
}
fn parse_expression(&mut self) -> Result<Expression, Error> {
self.check_depth()?;
let result = self.parse_and_expression();
self.depth_tracker.pop_depth();
result
}
fn parse_and_expression(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let mut left = self.parse_and_operand()?;
while self.at(&TokenKind::And)? {
self.next()?; let right = self.parse_and_operand()?;
let span = self.span_covering(
&start_span,
&right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone()),
);
left = self.new_expression(
ExpressionKind::LogicalAnd(Arc::new(left), Arc::new(right)),
self.make_source(span),
)?;
}
Ok(left)
}
fn at_bare_veto_token(&mut self) -> Result<bool, Error> {
if !self.at(&TokenKind::Veto)? {
return Ok(false);
}
let checkpoint = self.checkpoint();
self.next()?;
let bare = !self.at(&TokenKind::StringLit)?;
self.restore(checkpoint);
Ok(bare)
}
fn at_bare_veto_followed_by_is(&mut self) -> Result<bool, Error> {
if !self.at_bare_veto_token()? {
return Ok(false);
}
let checkpoint = self.checkpoint();
self.next()?;
let followed = self.at(&TokenKind::Is)?;
self.restore(checkpoint);
Ok(followed)
}
fn at_not_bare_veto_followed_by_is(&mut self) -> Result<bool, Error> {
if !self.at(&TokenKind::Not)? {
return Ok(false);
}
let checkpoint = self.checkpoint();
self.next()?;
if !self.at(&TokenKind::Veto)? {
self.restore(checkpoint);
return Ok(false);
}
self.next()?;
if self.at(&TokenKind::StringLit)? {
self.restore(checkpoint);
return Ok(false);
}
let followed = self.at(&TokenKind::Is)?;
self.restore(checkpoint);
Ok(followed)
}
fn wrap_result_is_veto_expression(
&mut self,
operand: Expression,
operator_is_not: bool,
keyword_was_negated: bool,
start_span: Span,
) -> Result<Expression, Error> {
let negate = operator_is_not ^ keyword_was_negated;
let end_span = operand
.source_location
.as_ref()
.map(|source| source.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
let core = self.new_expression(
ExpressionKind::ResultIsVeto(Arc::new(operand)),
self.make_source(span.clone()),
)?;
if negate {
self.new_expression(
ExpressionKind::LogicalNegation(Arc::new(core), NegationType::Not),
self.make_source(span),
)
} else {
Ok(core)
}
}
fn parse_veto_status_lhs_is_comparison(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let keyword_was_negated = if self.at(&TokenKind::Not)? {
self.next()?;
true
} else {
false
};
self.expect(&TokenKind::Veto)?;
if self.at(&TokenKind::StringLit)? {
let tok = self.peek()?.clone();
return Err(self.error_at_token(
&tok,
"veto with a message is only valid as a rule or unless result, not in `is veto` comparisons",
));
}
let operator = self.parse_comparison_operator()?;
let operator_is_not = matches!(operator, ComparisonComputation::IsNot);
if !matches!(
operator,
ComparisonComputation::Is | ComparisonComputation::IsNot
) {
let tok = self.peek()?.clone();
return Err(self.error_at_token(
&tok,
"Expected `is` or `is not` after `veto` in a veto-status comparison",
));
}
let operand = self.parse_range_expression()?;
self.wrap_result_is_veto_expression(
operand,
operator_is_not,
keyword_was_negated,
start_span,
)
}
fn parse_and_operand(&mut self) -> Result<Expression, Error> {
if self.at_not_bare_veto_followed_by_is()? || self.at_bare_veto_followed_by_is()? {
return self.parse_veto_status_lhs_is_comparison();
}
if self.at(&TokenKind::Not)? {
return self.parse_not_expression();
}
self.parse_repository_with_suffix()
}
fn parse_not_expression(&mut self) -> Result<Expression, Error> {
let not_tok = self.expect(&TokenKind::Not)?;
let start_span = not_tok.span.clone();
self.check_depth()?;
let operand = self.parse_and_operand()?;
self.depth_tracker.pop_depth();
let end_span = operand
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
self.new_expression(
ExpressionKind::LogicalNegation(Arc::new(operand), NegationType::Not),
self.make_source(span),
)
}
fn parse_repository_with_suffix(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let repository = self.parse_range_expression()?;
self.continue_repository_operand(repository, start_span)
}
fn continue_repository_operand(
&mut self,
mut expr: Expression,
start_span: Span,
) -> Result<Expression, Error> {
loop {
let peeked = self.peek()?;
if is_comparison_operator(&peeked.kind) {
return self.parse_comparison_suffix(expr, start_span);
}
if peeked.kind == TokenKind::Not {
expr = self.parse_not_in_calendar_suffix(expr, start_span.clone())?;
continue;
}
if peeked.kind == TokenKind::In {
expr = self.parse_in_suffix(expr, start_span.clone())?;
continue;
}
break;
}
if self.at_expression_suffix_end()? {
return Ok(expr);
}
let tok = self.peek()?.clone();
Err(self.error_at_token(
&tok,
format!("Unexpected token '{}' after expression", tok.text),
))
}
fn parse_comparison_suffix(
&mut self,
left: Expression,
start_span: Span,
) -> Result<Expression, Error> {
let operator = self.parse_comparison_operator()?;
let operator_is_not = matches!(operator, ComparisonComputation::IsNot);
if matches!(
operator,
ComparisonComputation::Is | ComparisonComputation::IsNot
) && self.at_bare_veto_token()?
{
self.expect(&TokenKind::Veto)?;
if self.at(&TokenKind::StringLit)? {
let tok = self.peek()?.clone();
return Err(self.error_at_token(
&tok,
"veto with a message is only valid as a rule or unless result, not in `is veto` comparisons",
));
}
return self.wrap_result_is_veto_expression(left, operator_is_not, false, start_span);
}
let right = if self.at(&TokenKind::Not)? {
self.parse_not_expression()?
} else {
self.parse_range_expression()?
};
let end_span = right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
self.new_expression(
ExpressionKind::Comparison(Arc::new(left), operator, Arc::new(right)),
self.make_source(span),
)
}
fn parse_comparison_operator(&mut self) -> Result<ComparisonComputation, Error> {
let tok = self.next()?;
match tok.kind {
TokenKind::Gt => Ok(ComparisonComputation::GreaterThan),
TokenKind::Lt => Ok(ComparisonComputation::LessThan),
TokenKind::Gte => Ok(ComparisonComputation::GreaterThanOrEqual),
TokenKind::Lte => Ok(ComparisonComputation::LessThanOrEqual),
TokenKind::Is => {
if self.at(&TokenKind::Not)? {
self.next()?; Ok(ComparisonComputation::IsNot)
} else {
Ok(ComparisonComputation::Is)
}
}
_ => Err(self.error_at_token(
&tok,
format!("Expected a comparison operator, found {}", tok.kind),
)),
}
}
fn parse_not_in_calendar_suffix(
&mut self,
repository: Expression,
start_span: Span,
) -> Result<Expression, Error> {
self.expect(&TokenKind::Not)?;
self.expect(&TokenKind::In)?;
self.expect(&TokenKind::Calendar)?;
let unit = self.parse_calendar_unit()?;
let end = self.peek()?.span.clone();
let span = self.span_covering(&start_span, &end);
self.new_expression(
ExpressionKind::DateCalendar(DateCalendarKind::NotIn, unit, Arc::new(repository)),
self.make_source(span),
)
}
fn parse_in_suffix(
&mut self,
repository: Expression,
start_span: Span,
) -> Result<Expression, Error> {
self.expect(&TokenKind::In)?;
let peeked = self.peek()?;
if peeked.kind == TokenKind::Past || peeked.kind == TokenKind::Future {
let direction = self.next()?;
let rel_kind = if direction.kind == TokenKind::Past {
DateRelativeKind::InPast
} else {
DateRelativeKind::InFuture
};
if self.at(&TokenKind::Calendar)? {
self.next()?; let cal_kind = if direction.kind == TokenKind::Past {
DateCalendarKind::Past
} else {
DateCalendarKind::Future
};
let unit = self.parse_calendar_unit()?;
let end = self.peek()?.span.clone();
let span = self.span_covering(&start_span, &end);
return self.new_expression(
ExpressionKind::DateCalendar(cal_kind, unit, Arc::new(repository)),
self.make_source(span),
);
}
if self.at(&TokenKind::And)?
|| self.at(&TokenKind::Unless)?
|| self.at(&TokenKind::Then)?
|| self.at(&TokenKind::RParen)?
|| self.at(&TokenKind::Eof)?
|| is_comparison_operator(&self.peek()?.kind)
{
let end = self.peek()?.span.clone();
let span = self.span_covering(&start_span, &end);
return self.new_expression(
ExpressionKind::DateRelative(rel_kind, Arc::new(repository)),
self.make_source(span),
);
}
let offset = self.parse_repository_expression()?;
let offset_end_span = offset
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let range = self.new_expression(
ExpressionKind::PastFutureRange(rel_kind, Arc::new(offset)),
self.make_source(self.span_covering(&direction.span, &offset_end_span)),
)?;
let span = self.span_covering(&start_span, &offset_end_span);
return self.new_expression(
ExpressionKind::RangeContainment(Arc::new(repository), Arc::new(range)),
self.make_source(span),
);
}
if peeked.kind == TokenKind::Calendar {
self.next()?; let unit = self.parse_calendar_unit()?;
let end = self.peek()?.span.clone();
let span = self.span_covering(&start_span, &end);
return self.new_expression(
ExpressionKind::DateCalendar(DateCalendarKind::Current, unit, Arc::new(repository)),
self.make_source(span),
);
}
let range = self.parse_range_expression()?;
let end_span = range
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
self.new_expression(
ExpressionKind::RangeContainment(Arc::new(repository), Arc::new(range)),
self.make_source(span),
)
}
fn parse_as_chain(
&mut self,
mut expr: Expression,
start_span: Span,
) -> Result<Expression, Error> {
while self.at(&TokenKind::As)? {
self.expect(&TokenKind::As)?;
let target_tok = self.next()?;
let target = conversion_target_from_token(&target_tok.kind, &target_tok.text);
expr = self.new_expression(
ExpressionKind::UnitConversion(Arc::new(expr), target),
self.make_source(self.span_covering(&start_span, &target_tok.span)),
)?;
}
Ok(expr)
}
fn is_plain_number_literal(expr: &Expression) -> bool {
matches!(expr.kind, ExpressionKind::Literal(Value::Number(_)))
}
fn is_unit_conversion(expr: &Expression) -> bool {
matches!(expr.kind, ExpressionKind::UnitConversion(..))
}
fn at_expression_suffix_end(&mut self) -> Result<bool, Error> {
Ok(self.at(&TokenKind::And)?
|| self.at(&TokenKind::Unless)?
|| self.at(&TokenKind::Then)?
|| self.at(&TokenKind::RParen)?
|| self.at(&TokenKind::Eof)?
|| self.at(&TokenKind::Spec)?
|| self.at(&TokenKind::Repo)?
|| self.at(&TokenKind::Uses)?
|| is_spec_body_keyword(&self.peek()?.kind))
}
fn parse_calendar_unit(&mut self) -> Result<CalendarPeriodUnit, Error> {
let tok = self.next()?;
if let Some(unit) = CalendarPeriodUnit::from_keyword(&tok.text) {
return Ok(unit);
}
Err(self.error_at_token(
&tok,
format!("Expected 'year', 'month', or 'week', found '{}'", tok.text),
))
}
fn parse_range_expression(&mut self) -> Result<Expression, Error> {
self.parse_repository_expression()
}
fn parse_range_operand(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let checkpoint = self.checkpoint();
let left = self.parse_range_ellipsis_bound()?;
if !self.at(&TokenKind::Ellipsis)? {
self.restore(checkpoint);
return self.parse_factor();
}
self.next()?;
let right = self.parse_power_for_range_bound()?;
let end_span = right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
self.new_expression(
ExpressionKind::RangeLiteral(Arc::new(left), Arc::new(right)),
self.make_source(span),
)
}
fn parse_range_ellipsis_bound(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let mut left = self.parse_power_for_range_bound()?;
while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
let op_tok = self.next()?;
let operation = match op_tok.kind {
TokenKind::Plus => ArithmeticComputation::Add,
TokenKind::Minus => ArithmeticComputation::Subtract,
_ => unreachable!("BUG: only + and - should reach here"),
};
let right = self.parse_power_for_range_bound()?;
let end_span = right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
left = self.new_expression(
ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
self.make_source(span),
)?;
}
Ok(left)
}
fn parse_power_for_range_bound(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let left = self.parse_factor()?;
if self.at(&TokenKind::Caret)? {
self.next()?;
self.check_depth()?;
let right = self.parse_power_for_range_bound()?;
self.depth_tracker.pop_depth();
let end_span = right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
return self.new_expression(
ExpressionKind::Arithmetic(
Arc::new(left),
ArithmeticComputation::Power,
Arc::new(right),
),
self.make_source(span),
);
}
Ok(left)
}
fn parse_repository_expression(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let mut left = self.parse_term()?;
while self.at_any(&[TokenKind::Plus, TokenKind::Minus])? {
let op_tok = self.next()?;
let operation = match op_tok.kind {
TokenKind::Plus => ArithmeticComputation::Add,
TokenKind::Minus => ArithmeticComputation::Subtract,
_ => unreachable!("BUG: only + and - should reach here"),
};
let right = self.parse_term()?;
if Self::is_plain_number_literal(&left) && Self::is_unit_conversion(&right) {
let source = right
.source_location
.clone()
.unwrap_or_else(|| self.make_source(start_span.clone()));
return Err(Error::parsing(
"Cannot add a plain number to a converted value; convert each operand before \
'+' (e.g. '5 as usd + c as usd')",
source,
None::<String>,
));
}
let end_span = right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
left = self.new_expression(
ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
self.make_source(span),
)?;
}
Ok(left)
}
fn parse_term(&mut self) -> Result<Expression, Error> {
self.parse_term_with_as(true)
}
fn parse_term_with_as(&mut self, allow_as: bool) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let mut left = self.parse_power()?;
if allow_as {
left = self.parse_as_chain(left, start_span.clone())?;
}
while self.at_any(&[TokenKind::Star, TokenKind::Slash, TokenKind::Percent])? {
let op_tok = self.next()?;
let operation = match op_tok.kind {
TokenKind::Star => ArithmeticComputation::Multiply,
TokenKind::Slash => ArithmeticComputation::Divide,
TokenKind::Percent => ArithmeticComputation::Modulo,
_ => unreachable!("BUG: only *, /, % should reach here"),
};
let right_start_span = self.peek()?.span.clone();
let mut right = self.parse_power()?;
if allow_as {
right = self.parse_as_chain(right, right_start_span)?;
}
let end_span = right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
left = self.new_expression(
ExpressionKind::Arithmetic(Arc::new(left), operation, Arc::new(right)),
self.make_source(span),
)?;
}
Ok(left)
}
fn parse_power(&mut self) -> Result<Expression, Error> {
let start_span = self.peek()?.span.clone();
let left = self.parse_range_operand()?;
if self.at(&TokenKind::Caret)? {
self.next()?;
self.check_depth()?;
let right = self.parse_power()?;
self.depth_tracker.pop_depth();
let end_span = right
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
return self.new_expression(
ExpressionKind::Arithmetic(
Arc::new(left),
ArithmeticComputation::Power,
Arc::new(right),
),
self.make_source(span),
);
}
Ok(left)
}
fn parse_factor(&mut self) -> Result<Expression, Error> {
let peeked = self.peek()?;
let start_span = peeked.span.clone();
if peeked.kind == TokenKind::Minus {
self.next()?;
let operand = self.parse_primary_or_math()?;
let end_span = operand
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
let zero = self.new_expression(
ExpressionKind::Literal(Value::Number(Decimal::ZERO)),
self.make_source(start_span),
)?;
return self.new_expression(
ExpressionKind::Arithmetic(
Arc::new(zero),
ArithmeticComputation::Subtract,
Arc::new(operand),
),
self.make_source(span),
);
}
if peeked.kind == TokenKind::Plus {
self.next()?;
return self.parse_primary_or_math();
}
self.parse_primary_or_math()
}
fn parse_primary_or_math(&mut self) -> Result<Expression, Error> {
let peeked = self.peek()?;
if is_math_function(&peeked.kind) {
return self.parse_math_function();
}
self.parse_primary()
}
fn parse_math_function(&mut self) -> Result<Expression, Error> {
let func_tok = self.next()?;
let start_span = func_tok.span.clone();
let operator = match func_tok.kind {
TokenKind::Sqrt => MathematicalComputation::Sqrt,
TokenKind::Sin => MathematicalComputation::Sin,
TokenKind::Cos => MathematicalComputation::Cos,
TokenKind::Tan => MathematicalComputation::Tan,
TokenKind::Asin => MathematicalComputation::Asin,
TokenKind::Acos => MathematicalComputation::Acos,
TokenKind::Atan => MathematicalComputation::Atan,
TokenKind::Log => MathematicalComputation::Log,
TokenKind::Exp => MathematicalComputation::Exp,
TokenKind::Abs => MathematicalComputation::Abs,
TokenKind::Floor => MathematicalComputation::Floor,
TokenKind::Ceil => MathematicalComputation::Ceil,
TokenKind::Round => MathematicalComputation::Round,
_ => unreachable!("BUG: only math functions should reach here"),
};
self.check_depth()?;
let operand = self.parse_repository_expression()?;
self.depth_tracker.pop_depth();
let end_span = operand
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or_else(|| start_span.clone());
let span = self.span_covering(&start_span, &end_span);
self.new_expression(
ExpressionKind::MathematicalComputation(operator, Arc::new(operand)),
self.make_source(span),
)
}
fn parse_primary(&mut self) -> Result<Expression, Error> {
let peeked = self.peek()?;
let start_span = peeked.span.clone();
match &peeked.kind {
TokenKind::LParen => {
self.next()?; let inner = self.parse_expression()?;
self.expect(&TokenKind::RParen)?;
Ok(inner)
}
TokenKind::Now => {
let tok = self.next()?;
self.new_expression(ExpressionKind::Now, self.make_source(tok.span))
}
TokenKind::Past | TokenKind::Future => {
let tok = self.next()?;
let kind = if tok.kind == TokenKind::Past {
DateRelativeKind::InPast
} else {
DateRelativeKind::InFuture
};
let offset = self.parse_repository_expression()?;
let span = self.span_covering(
&start_span,
&offset
.source_location
.as_ref()
.map(|s| s.span.clone())
.unwrap_or(start_span.clone()),
);
self.new_expression(
ExpressionKind::PastFutureRange(kind, Arc::new(offset)),
self.make_source(span),
)
}
TokenKind::StringLit => {
let tok = self.next()?;
let content = unquote_string(&tok.text);
self.new_expression(
ExpressionKind::Literal(Value::Text(content)),
self.make_source(tok.span),
)
}
k if is_boolean_keyword(k) => {
let tok = self.next()?;
self.new_expression(
ExpressionKind::Literal(Value::Boolean(token_kind_to_boolean_value(&tok.kind))),
self.make_source(tok.span),
)
}
TokenKind::NumberLit => self.parse_number_expression(),
k if can_be_reference_segment(k) => {
let reference = self.parse_expression_reference()?;
let span = self.span_covering(&start_span, &self.last_span);
self.new_expression(ExpressionKind::Reference(reference), self.make_source(span))
}
_ => {
let tok = self.next()?;
Err(self.error_at_token(
&tok,
format!("Expected an expression, found '{}'", tok.text),
))
}
}
}
fn parse_number_expression(&mut self) -> Result<Expression, Error> {
let num_tok = self.next()?;
let num_text = num_tok.text.clone();
let start_span = num_tok.span.clone();
if num_text.len() == 4
&& num_text.chars().all(|c| c.is_ascii_digit())
&& self.at(&TokenKind::Minus)?
{
let minus_span = self.peek()?.span.clone();
if minus_span.start == start_span.end {
let value = self.parse_date_literal(num_text, start_span.clone())?;
return self
.new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
}
}
if num_text.len() == 2
&& num_text.chars().all(|c| c.is_ascii_digit())
&& self.at(&TokenKind::Colon)?
{
let colon_span = self.peek()?.span.clone();
if colon_span.start == start_span.end {
let value = self.try_parse_time_literal(num_text, start_span.clone())?;
return self
.new_expression(ExpressionKind::Literal(value), self.make_source(start_span));
}
}
if self.at(&TokenKind::PercentPercent)? {
let pp_tok = self.next()?;
if let Ok(next_peek) = self.peek() {
if next_peek.kind == TokenKind::NumberLit {
return Err(self.error_at_token(
&pp_tok,
"Permille literal cannot be followed by a digit",
));
}
}
let decimal = parse_decimal_string(&num_text, &start_span, self)?;
return self.new_expression(
ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
self.make_source(start_span),
);
}
if self.at(&TokenKind::Percent)? {
let pct_span = self.peek()?.span.clone();
let pct_tok = self.next()?;
if let Ok(next_peek) = self.peek() {
if next_peek.kind == TokenKind::NumberLit || next_peek.kind == TokenKind::Percent {
return Err(self.error_at_token(
&pct_tok,
"Percent literal cannot be followed by a digit",
));
}
}
let decimal = parse_decimal_string(&num_text, &start_span, self)?;
return self.new_expression(
ExpressionKind::Literal(Value::NumberWithUnit(decimal, "percent".to_string())),
self.make_source(self.span_covering(&start_span, &pct_span)),
);
}
if self.at(&TokenKind::PercentKw)? {
self.next()?;
let decimal = parse_decimal_string(&num_text, &start_span, self)?;
return self.new_expression(
ExpressionKind::Literal(Value::NumberWithUnit(decimal, "percent".to_string())),
self.make_source(start_span),
);
}
if self.at(&TokenKind::Permille)? {
self.next()?;
let decimal = parse_decimal_string(&num_text, &start_span, self)?;
return self.new_expression(
ExpressionKind::Literal(Value::NumberWithUnit(decimal, "permille".to_string())),
self.make_source(start_span),
);
}
if can_be_label(&self.peek()?.kind) {
let unit_tok = self.next()?;
let decimal = parse_decimal_string(&num_text, &start_span, self)?;
if let Some(calendar_unit) = CalendarUnit::from_keyword(&unit_tok.text) {
return self.new_expression(
ExpressionKind::Literal(Value::Calendar(decimal, calendar_unit)),
self.make_source(self.span_covering(&start_span, &unit_tok.span)),
);
}
return self.new_expression(
ExpressionKind::Literal(Value::NumberWithUnit(decimal, unit_tok.text.clone())),
self.make_source(self.span_covering(&start_span, &unit_tok.span)),
);
}
let decimal = parse_decimal_string(&num_text, &start_span, self)?;
self.new_expression(
ExpressionKind::Literal(Value::Number(decimal)),
self.make_source(start_span),
)
}
fn parse_expression_reference(&mut self) -> Result<Reference, Error> {
let mut segments = Vec::new();
let first = self.next()?;
segments.push(first.text.clone());
while self.at(&TokenKind::Dot)? {
self.next()?; let seg = self.next()?;
if !can_be_reference_segment(&seg.kind) {
return Err(self.error_at_token(
&seg,
format!("Expected an identifier after '.', found {}", seg.kind),
));
}
segments.push(seg.text.clone());
}
Ok(Reference::from_path(segments))
}
}
fn unquote_string(s: &str) -> String {
if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
fn parse_decimal_string(text: &str, span: &Span, parser: &Parser) -> Result<Decimal, Error> {
let clean = text.replace(['_', ','], "");
Decimal::from_str(&clean).map_err(|_| {
Error::parsing(
format!(
"Invalid number: '{}'. Expected a valid decimal number (e.g., 42, 3.14, 1_000_000)",
text
),
parser.make_source(span.clone()),
None::<String>,
)
})
}
fn is_comparison_operator(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::Gt | TokenKind::Lt | TokenKind::Gte | TokenKind::Lte | TokenKind::Is
)
}
impl TokenKind {
fn is_identifier_like(&self) -> bool {
matches!(self, TokenKind::Identifier)
|| can_be_label(self)
|| is_type_keyword(self)
|| is_boolean_keyword(self)
|| is_math_function(self)
}
}