use serde::{Deserialize, Serialize};
#[cfg(feature = "compiler")]
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticPhase {
Parse,
TypeCheck,
Load,
Lower,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ResourceLimitInfo {
pub resource: String,
pub limit: u64,
pub observed: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "RawSourceSpan")]
pub struct SourceSpan {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_byte: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_byte: Option<u64>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawSourceSpan {
#[serde(default)]
source_name: Option<String>,
#[serde(default)]
start_byte: Option<u64>,
#[serde(default)]
end_byte: Option<u64>,
}
impl TryFrom<RawSourceSpan> for SourceSpan {
type Error = String;
fn try_from(raw: RawSourceSpan) -> Result<Self, String> {
match (raw.start_byte.is_some(), raw.end_byte.is_some()) {
(true, true) => {}
(false, false) if raw.source_name.is_some() => {}
(false, false) => {
return Err("a source span names a source, carries offsets, or both".to_string())
}
_ => return Err("start_byte and end_byte are set together or not at all".to_string()),
}
Ok(Self {
source_name: raw.source_name,
start_byte: raw.start_byte,
end_byte: raw.end_byte,
})
}
}
impl SourceSpan {
pub fn exact(source_name: Option<String>, start_byte: u64, end_byte: u64) -> Self {
Self {
source_name,
start_byte: Some(start_byte),
end_byte: Some(end_byte),
}
}
pub fn source_only(source_name: impl Into<String>) -> Self {
Self {
source_name: Some(source_name.into()),
start_byte: None,
end_byte: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RelatedLocation {
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Diagnostic {
pub code: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<DiagnosticPhase>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity: Option<Severity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span: Option<SourceSpan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub related: Vec<RelatedLocation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource_limit: Option<ResourceLimitInfo>,
}
impl Diagnostic {
pub fn compiler(
code: impl Into<String>,
phase: DiagnosticPhase,
message: impl Into<String>,
) -> Self {
Self {
code: code.into(),
phase: Some(phase),
severity: Some(Severity::Error),
path: None,
message: message.into(),
span: None,
related: Vec::new(),
notes: Vec::new(),
resource_limit: None,
}
}
pub fn violation(
code: impl Into<String>,
path: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
code: code.into(),
phase: None,
severity: None,
path: Some(path.into()),
message: message.into(),
span: None,
related: Vec::new(),
notes: Vec::new(),
resource_limit: None,
}
}
pub fn resource_violation(resource: &str, limit: u64, observed: u64) -> Self {
Self::violation(
"resource_limit_exceeded",
"$",
format!("resource {resource} exceeded limit {limit}; observed {observed}"),
)
.with_resource_limit(ResourceLimitInfo {
resource: resource.to_string(),
limit,
observed,
})
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
pub fn with_span(mut self, span: SourceSpan) -> Self {
self.span = Some(span);
self
}
pub fn with_related(mut self, related: Vec<RelatedLocation>) -> Self {
self.related = related;
self
}
pub fn with_notes(mut self, notes: Vec<String>) -> Self {
self.notes = notes;
self
}
pub fn with_resource_limit(mut self, resource_limit: ResourceLimitInfo) -> Self {
self.resource_limit = Some(resource_limit);
self
}
}
#[cfg(feature = "compiler")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileError {
pub diagnostics: Vec<Diagnostic>,
}
#[cfg(feature = "compiler")]
impl CompileError {
pub(crate) fn single(
code: impl Into<String>,
phase: DiagnosticPhase,
message: impl Into<String>,
) -> Self {
Self {
diagnostics: vec![Diagnostic::compiler(code, phase, message)],
}
}
pub(crate) fn resource_limit(
resource: &str,
limit: usize,
observed: usize,
message: impl Into<String>,
) -> Self {
Self {
diagnostics: vec![Diagnostic::compiler(
"resource_limit_exceeded",
DiagnosticPhase::Load,
message,
)
.with_resource_limit(ResourceLimitInfo {
resource: resource.to_string(),
limit: crate::limits::portable_count(limit),
observed: crate::limits::portable_count(observed),
})],
}
}
}
#[cfg(feature = "compiler")]
impl fmt::Display for CompileError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(diagnostic) = self.diagnostics.first() {
write!(formatter, "{}: {}", diagnostic.code, diagnostic.message)
} else {
write!(formatter, "Candid compilation failed")
}
}
}
#[cfg(feature = "compiler")]
impl std::error::Error for CompileError {}