use std::path::PathBuf;
use codespan_reporting::diagnostic::{Diagnostic as CsDiagnostic, Label};
use thiserror::Error;
use crate::common::{
BUG_URL, Diagnostic, FileId, InternalError, Span, primary_label, secondary_label,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectiveKind {
Input,
Output,
PrintSize,
}
impl std::fmt::Display for DirectiveKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
DirectiveKind::Input => ".input",
DirectiveKind::Output => ".output",
DirectiveKind::PrintSize => ".printsize",
})
}
}
fn dup_labels(span: Span, prior: Span, here: &str, first: &str) -> Vec<Label<FileId>> {
[
primary_label(span).map(|l| l.with_message(here)),
secondary_label(prior).map(|l| l.with_message(first)),
]
.into_iter()
.flatten()
.collect()
}
fn primary_only(span: Span) -> Vec<Label<FileId>> {
primary_label(span).into_iter().collect()
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum ParseError {
#[error("syntax error: {message}")]
Syntax { span: Span, message: String },
#[error("duplicate declaration of relation `{name}`")]
DuplicateDecl {
span: Span,
prior: Span,
name: String,
},
#[error("duplicate attribute `{name}` in relation `{relation}`")]
DuplicateAttribute {
span: Span,
prior: Span,
relation: String,
name: String,
},
#[error("duplicate {kind} directive for relation `{name}`")]
DuplicateDirective {
span: Span,
prior: Span,
kind: DirectiveKind,
name: String,
},
#[error("{kind} directive references undeclared relation `{name}`")]
UndeclaredInDirective {
span: Span,
kind: DirectiveKind,
name: String,
},
#[error(
"relation `{name}` has both `.output` and `.printsize`; \
both write `{name}.csv` — pick one"
)]
OutputAndPrintsizeConflict { span: Span, name: String },
#[error("iterative list references undeclared relation `{name}`")]
UndeclaredInIterativeList { span: Span, name: String },
#[error("loop condition references undeclared relation `{name}`")]
UndeclaredLoopCondition { span: Span, name: String },
#[error("rule references undeclared relation `{name}`")]
UndeclaredInRule { span: Span, name: String },
#[error("fact references undeclared relation `{name}`")]
UndeclaredInFact { span: Span, name: String },
#[error("`loop`/`fixpoint` blocks require `--mode extend-batch` or `extend-inc`")]
LoopBlockInStandardMode { span: Span },
#[error("loop condition relation `{name}` must be nullary, but is declared with arity {arity}")]
NonNullaryLoopCondition {
span: Span,
name: String,
arity: usize,
},
#[error("`_` placeholder is not allowed in arguments to UDF `{udf_name}`")]
PlaceholderInUdf { span: Span, udf_name: String },
#[error("built-in `{op}` expects {expected} argument(s) but got {found}")]
BuiltinArity {
span: Span,
op: &'static str,
expected: usize,
found: usize,
},
#[error("failed to read included file `{}`: {source}", path.display())]
IncludeIo {
span: Span,
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("circular include of `{}`", path.display())]
CircularInclude {
span: Span,
path: PathBuf,
chain: Vec<PathBuf>,
},
#[error("duplicate `.type` declaration of `{name}`")]
DuplicateTypeDecl {
span: Span,
prior: Span,
name: String,
},
#[error("`.type {name} = ...` references unknown type `{parent}`")]
UnknownTypeParent {
span: Span,
name: String,
parent: String,
},
#[error("attribute references unknown type `{name}`")]
UnknownAttributeType { span: Span, name: String },
#[error("unknown component `{name}`")]
UnknownComponent { span: Span, name: String },
#[error("circular component inheritance involving `{name}`")]
CircularInheritance { span: Span, name: String },
#[error("component `{name}` expects {expected} type argument(s) but got {found}")]
ComponentArityMismatch {
span: Span,
name: String,
expected: usize,
found: usize,
},
#[error("unresolved qualified reference `{path}`")]
UnresolvedQualifiedRef { span: Span, path: String },
#[error("`overridable` is only allowed on a `.decl` inside a `.comp` body")]
OverridableOutsideComp { span: Span, name: String },
#[error("override of undeclared relation `{name}`")]
OverrideUnknownRelation { span: Span, name: String },
#[error("override of non-overridable relation `{name}`")]
OverrideOfNonOverridable {
span: Span,
prior: Span,
name: String,
},
#[error("override of non-inherited relation `{name}`")]
OverrideRedeclaresRelation {
span: Span,
prior: Span,
name: String,
},
#[error("`.plan` has no preceding rule to attach to")]
PlanOrphan { span: Span },
#[error("`.plan` expects {expected} index(es) (one per positive body atom) but got {found}")]
PlanArityMismatch {
span: Span,
expected: usize,
found: usize,
},
#[error("`.plan` index {index} is out of range (valid: 1..={max})")]
PlanIndexOutOfRange {
span: Span,
index: usize,
max: usize,
},
#[error("`.plan` lists positive-atom index {index} more than once")]
PlanDuplicateIndex { span: Span, index: usize },
#[error(
"assignment-bound variable `{var}` cannot be used in a negated atom with a computed value"
)]
AssignmentVarInNegation { span: Span, var: String },
#[error("rule body reduces to nothing but its head is not a constant fact")]
GroundRuleNotConst { span: Span },
#[error(transparent)]
Internal(#[from] InternalError),
}
impl ParseError {
pub(crate) fn syntax_from_pest(
err: &pest::error::Error<crate::parser::Rule>,
file: FileId,
) -> Self {
use pest::error::InputLocation;
let (start, end) = match err.location {
InputLocation::Pos(p) => (p as u32, p as u32),
InputLocation::Span((s, e)) => (s as u32, e as u32),
};
ParseError::Syntax {
span: Span::new(file, start, end),
message: err.variant.message().into_owned(),
}
}
}
impl Diagnostic for ParseError {
fn to_diagnostic(&self) -> CsDiagnostic<FileId> {
if let ParseError::Internal(e) = self {
return e.to_diagnostic();
}
let base = CsDiagnostic::error().with_message(self.to_string());
match self {
ParseError::DuplicateDecl { span, prior, .. } => {
base.with_labels(dup_labels(*span, *prior, "redeclared here", "first declared here"))
}
ParseError::DuplicateDirective { span, prior, .. } => base.with_labels(dup_labels(
*span,
*prior,
"duplicate directive",
"first directive here",
)),
ParseError::OutputAndPrintsizeConflict { span, .. } => base
.with_labels(primary_only(*span))
.with_notes(vec![
"remove either the `.output` or the `.printsize` directive for this relation"
.to_string(),
]),
ParseError::DuplicateAttribute { span, prior, .. } => base.with_labels(dup_labels(
*span,
*prior,
"duplicate attribute here",
"first declared here",
)),
ParseError::UndeclaredInDirective { span, name, .. } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"add a `.decl {name}(...)` before this directive"
)]),
ParseError::UndeclaredInIterativeList { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"either `.decl {name}(...)` it, or drop `{name}` from the iterative list"
)]),
ParseError::UndeclaredLoopCondition { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"declare `{name}` as a nullary relation with `.decl {name}()` and derive it inside the loop"
)]),
ParseError::UndeclaredInRule { span, name }
| ParseError::UndeclaredInFact { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"add a matching `.decl {name}(...)` declaration, or remove the reference"
)]),
ParseError::CircularInclude { span, chain, .. } => {
let mut diag = base.with_labels(primary_only(*span));
if !chain.is_empty() {
let shown: Vec<String> = chain.iter().map(|p| p.display().to_string()).collect();
diag = diag.with_notes(vec![format!("include chain: {}", shown.join(" → "))]);
}
diag
}
ParseError::DuplicateTypeDecl { span, prior, .. } => base.with_labels(dup_labels(
*span,
*prior,
"redeclared here",
"first declared here",
)),
ParseError::UnknownTypeParent { span, parent, .. } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"declare `{parent}` with a `.type {parent} = ...` (or `<:`) earlier in the program"
)]),
ParseError::UnknownAttributeType { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"either use a built-in primitive or add `.type {name} = ...`"
)]),
ParseError::UnknownComponent { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"declare `{name}` with a `.comp {name} {{ ... }}` block"
)]),
ParseError::CircularInheritance { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"`.comp {name}` inherits transitively from itself; break the cycle"
)]),
ParseError::ComponentArityMismatch { span, .. } => {
base.with_labels(primary_only(*span))
}
ParseError::UnresolvedQualifiedRef { span, path } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"the first segment of `{path}` must be either a nested `.init` instance in this component or a bound type-parameter"
)]),
ParseError::OverridableOutsideComp { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"remove `overridable` from this top-level `.decl {name}`, or move the declaration inside a `.comp` body"
)]),
ParseError::OverrideUnknownRelation { span, name } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"no inherited `.decl {name}(...) overridable` was found in any parent component"
)]),
ParseError::OverrideOfNonOverridable { span, prior, name } => base.with_labels(dup_labels(
*span,
*prior,
"override target is not `overridable`",
"declared without `overridable` here",
)).with_notes(vec![format!(
"add `overridable` to the parent `.decl {name}` to allow this override"
)]),
ParseError::OverrideRedeclaresRelation { span, prior, name } => base.with_labels(dup_labels(
*span,
*prior,
"`.override` here",
"relation redeclared in this comp here",
)).with_notes(vec![format!(
"`.override {name}` may only target an inherited relation; drop the local `.decl {name}` from this comp"
)]),
ParseError::PlanOrphan { span } => base
.with_labels(primary_only(*span))
.with_notes(vec![
"place `.plan (...)` immediately after the rule it pins".into(),
]),
ParseError::PlanArityMismatch { span, expected, .. } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"supply exactly {expected} 1-based index(es), one per positive body atom"
)]),
ParseError::PlanIndexOutOfRange { span, max, .. } => base
.with_labels(primary_only(*span))
.with_notes(vec![format!(
"use a 1-based index in 1..={max} (the rule has {max} positive atom(s))"
)]),
ParseError::PlanDuplicateIndex { span, .. } => base
.with_labels(primary_only(*span))
.with_notes(vec![
"`.plan` must be a permutation: each positive-atom index appears exactly once"
.into(),
]),
ParseError::Syntax { span, .. }
| ParseError::LoopBlockInStandardMode { span }
| ParseError::NonNullaryLoopCondition { span, .. }
| ParseError::PlaceholderInUdf { span, .. }
| ParseError::BuiltinArity { span, .. }
| ParseError::AssignmentVarInNegation { span, .. }
| ParseError::GroundRuleNotConst { span }
| ParseError::IncludeIo { span, .. } => base.with_labels(primary_only(*span)),
ParseError::Internal(_) => unreachable!("handled above"),
}
}
fn is_internal(&self) -> bool {
matches!(self, ParseError::Internal(_))
}
}
pub(crate) fn grammar_bug(detail: impl Into<String>) -> ParseError {
ParseError::Internal(InternalError::new("parser", detail, BUG_URL))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::SourceMap;
use crate::common::{BoxError, emit};
fn make_sm_with(text: &str) -> (SourceMap, FileId) {
let mut sm = SourceMap::new();
let f = sm.add("t.dl".into(), text.into());
(sm, f)
}
fn render(err: ParseError, sm: &SourceMap) -> String {
let err: BoxError = err.into();
let mut buf: Vec<u8> = Vec::new();
emit(&err, sm, &mut buf).unwrap();
String::from_utf8(buf).unwrap()
}
#[test]
fn duplicate_decl_labels_both_sites() {
let (sm, f) = make_sm_with(".decl Foo(x: int)\n.decl Foo(y: int)\n");
let out = render(
ParseError::DuplicateDecl {
span: Span::new(f, 24, 27),
prior: Span::new(f, 6, 9),
name: "Foo".into(),
},
&sm,
);
assert!(out.contains("duplicate declaration"), "got: {out}");
assert!(out.contains("redeclared here"), "got: {out}");
assert!(out.contains("first declared here"), "got: {out}");
}
#[test]
fn undeclared_in_directive_includes_help_note() {
let (sm, f) = make_sm_with(".input Bar(filename=\"b.csv\")\n");
let out = render(
ParseError::UndeclaredInDirective {
span: Span::new(f, 7, 10),
kind: DirectiveKind::Input,
name: "Bar".into(),
},
&sm,
);
assert!(out.contains(".input"), "got: {out}");
assert!(out.contains("undeclared"), "got: {out}");
assert!(out.contains("add a `.decl Bar"), "got: {out}");
}
#[test]
fn internal_variant_renders_bug_note() {
let (sm, _) = make_sm_with("");
let out = render(grammar_bug("ghosts in the AST"), &sm);
assert!(out.contains("bug"), "got: {out}");
assert!(out.contains("ghosts in the AST"), "got: {out}");
assert!(out.contains(BUG_URL), "got: {out}");
}
}