use codespan_reporting::diagnostic::Diagnostic as CsDiagnostic;
use thiserror::Error;
use crate::common::{
BUG_URL, Diagnostic, FileId, InternalError, Span, labels, primary_label, secondary_label,
};
use crate::parser::{
AggregationOperator, ArithmeticOperator, BuiltinOperator, ComparisonOperator, DataType,
};
#[derive(Debug, Error)]
pub enum TypeCheckError {
#[error("variable `{var}` bound as `{first_ty:?}` but used as `{later_ty:?}`")]
TypeMismatch {
var: String,
first_ty: DataType,
first_span: Span,
later_ty: DataType,
later_span: Span,
},
#[error("mixed types in arithmetic expression: `{left:?}` and `{right:?}`")]
ArithmeticTypeMismatch {
span: Span,
left: DataType,
right: DataType,
},
#[error("arithmetic operator `{op}` is not allowed on `{ty:?}`")]
ArithmeticOpNotAllowed {
span: Span,
op: ArithmeticOperator,
ty: DataType,
},
#[error("comparison sides disagree: `{left:?}` {op} `{right:?}`")]
ComparisonTypeMismatch {
span: Span,
op: ComparisonOperator,
left: DataType,
right: DataType,
},
#[error("comparison operator `{op}` is not allowed on `{ty:?}`")]
ComparisonOpNotAllowed {
span: Span,
op: ComparisonOperator,
ty: DataType,
},
#[error("literal `{literal}` does not fit column type `{expected:?}`")]
LiteralColumnMismatch {
span: Span,
literal: String,
expected: DataType,
},
#[error("call to undeclared UDF `{name}`")]
UndeclaredUdf { span: Span, name: String },
#[error("UDF `{name}` expects {expected} argument(s) but got {found}")]
UdfArity {
span: Span,
name: String,
expected: usize,
found: usize,
},
#[error("UDF `{name}` parameter `{param}` expects `{expected:?}` but got `{found:?}`")]
UdfArgType {
span: Span,
name: String,
param: String,
expected: DataType,
found: DataType,
},
#[error("built-in `{op}` argument {arg_index} expects `{expected:?}` but got `{found:?}`")]
BuiltinArgType {
span: Span,
op: BuiltinOperator,
arg_index: usize,
expected: DataType,
found: DataType,
},
#[error("built-in `ord` requires `--str-intern` to be enabled")]
OrdRequiresStrIntern { span: Span },
#[error("aggregation `{op:?}` requires a numeric input but got `{ty:?}`")]
AggregationInputNotNumeric {
span: Span,
op: AggregationOperator,
ty: DataType,
},
#[error("aggregation `{op:?}` cannot produce result of type `{declared:?}`")]
AggregationOutputType {
span: Span,
op: AggregationOperator,
declared: DataType,
},
#[error("head column {col} of `{rel}` expects `{expected:?}` but produces `{found:?}`")]
HeadColumnType {
span: Span,
rel: String,
col: usize,
expected: DataType,
found: DataType,
},
#[error("head `{rel}` expects arity {expected} but got {found}")]
HeadArity {
span: Span,
rel: String,
expected: usize,
found: usize,
},
#[error(
"variable `{var}` declared as `{first_ty}` but later used as `{later_ty}` (no common subtype)"
)]
SubtypeMismatch {
var: String,
first_ty: String,
first_span: Span,
later_ty: String,
later_span: Span,
},
#[error("comparison operands have incompatible subtypes: `{left_ty}` and `{right_ty}`")]
ComparisonSubtypeMismatch {
span: Span,
left_ty: String,
right_ty: String,
},
#[error(
"head column {col} of `{rel}` expects `{expected}` but receives `{found}` (use `as(expr, {expected})` to narrow)"
)]
HeadSubtypeMismatch {
span: Span,
rel: String,
col: usize,
expected: String,
found: String,
},
#[error("illegal cast: cannot cast `{from}` to `{to}` (different primitive roots)")]
IllegalCast { span: Span, from: String, to: String },
#[error("unknown cast target type `{name}`")]
UnknownCastType { span: Span, name: String },
#[error(transparent)]
Internal(#[from] InternalError),
}
impl TypeCheckError {
pub(super) fn internal(detail: impl Into<String>) -> Self {
Self::Internal(InternalError::new("typechecker", detail, BUG_URL))
}
}
impl Diagnostic for TypeCheckError {
fn to_diagnostic(&self) -> CsDiagnostic<FileId> {
let base = CsDiagnostic::error().with_message(self.to_string());
match self {
TypeCheckError::TypeMismatch {
var,
first_ty,
first_span,
later_ty,
later_span,
} => {
let mut label_vec = Vec::new();
if let Some(l) = primary_label(*later_span) {
label_vec.push(l.with_message(format!("`{var}` used as `{later_ty:?}` here")));
}
if let Some(l) = secondary_label(*first_span) {
label_vec
.push(l.with_message(format!("`{var}` first bound as `{first_ty:?}`")));
}
base.with_labels(label_vec).with_notes(vec![
"a variable's type is fixed by its first positive-atom occurrence; \
all later uses must agree"
.into(),
])
}
TypeCheckError::ArithmeticTypeMismatch { span, left, right } => {
base.with_labels(labels(
*span,
format!("`{left:?}` and `{right:?}` cannot be combined"),
))
}
TypeCheckError::ArithmeticOpNotAllowed { span, op, ty } => base
.with_labels(labels(*span, format!("`{op}` cannot apply to `{ty:?}`")))
.with_notes(vec![
"numeric operators (`+`, `-`, `*`, `/`, `%`) require numeric factors; \
`cat` requires strings; `Bool` has no arithmetic"
.into(),
]),
TypeCheckError::ComparisonTypeMismatch {
span, left, right, ..
} => base.with_labels(labels(
*span,
format!("`{left:?}` cannot be compared with `{right:?}`"),
)),
TypeCheckError::ComparisonOpNotAllowed { span, op, ty } => base
.with_labels(labels(*span, format!("`{op}` cannot apply to `{ty:?}`")))
.with_notes(vec![
"ordering comparisons (`<`, `<=`, `>`, `>=`) require numeric or \
string operands; `=` and `!=` work on any matching types"
.into(),
]),
TypeCheckError::UndeclaredUdf { span, name } => base
.with_labels(labels(*span, format!("`{name}` is never declared")))
.with_notes(vec![format!(
"add a matching `.extern fn {name}(...): ...` declaration, \
or remove the call"
)]),
TypeCheckError::UdfArity {
span,
name,
expected,
found,
} => base.with_labels(labels(
*span,
format!("`{name}` expects {expected} argument(s), got {found}"),
)),
TypeCheckError::UdfArgType {
span,
name,
param,
expected,
found,
} => base.with_labels(labels(
*span,
format!("`{name}` param `{param}`: expected `{expected:?}`, got `{found:?}`"),
)),
TypeCheckError::BuiltinArgType {
span,
op,
arg_index,
expected,
found,
} => base.with_labels(labels(
*span,
format!(
"built-in `{op}` arg {arg_index}: expected `{expected:?}`, got `{found:?}`"
),
)),
TypeCheckError::OrdRequiresStrIntern { span } => base
.with_labels(labels(*span, "`ord` used here"))
.with_notes(vec![
"ord returns the symbol's intern key — a unique per-string \
integer that only exists when strings are interned. Compile \
with `--str-intern` (binary mode) or `.string_intern(true)` \
(library mode) to use it."
.into(),
]),
TypeCheckError::AggregationInputNotNumeric { span, op, ty } => {
base.with_labels(labels(
*span,
format!("`{op:?}` requires a numeric column but found `{ty:?}`"),
))
}
TypeCheckError::AggregationOutputType { span, op, declared } => {
base.with_labels(labels(
*span,
format!("declared as `{declared:?}`, incompatible with `{op:?}`"),
))
}
TypeCheckError::HeadColumnType {
span,
rel,
col,
expected,
found,
} => base.with_labels(labels(
*span,
format!("`{rel}` column {col} expects `{expected:?}`, got `{found:?}`"),
)),
TypeCheckError::HeadArity {
span,
rel,
expected,
found,
} => base.with_labels(labels(
*span,
format!("`{rel}` expects {expected} column(s), got {found}"),
)),
TypeCheckError::LiteralColumnMismatch {
span,
literal,
expected,
} => base.with_labels(labels(
*span,
format!("`{literal}` does not fit `{expected:?}`"),
)),
TypeCheckError::SubtypeMismatch {
var,
first_ty,
first_span,
later_ty,
later_span,
} => {
let mut label_vec = Vec::new();
if let Some(l) = primary_label(*later_span) {
label_vec.push(l.with_message(format!("`{var}` used as `{later_ty}` here")));
}
if let Some(l) = secondary_label(*first_span) {
label_vec.push(l.with_message(format!("`{var}` first bound as `{first_ty}`")));
}
base.with_labels(label_vec).with_notes(vec![
"sibling subtypes of the same primitive are intentionally incompatible — \
wrap one side with `as(expr, OtherType)` if you really mean to join them"
.into(),
])
}
TypeCheckError::ComparisonSubtypeMismatch {
span,
left_ty,
right_ty,
} => base
.with_labels(labels(
*span,
format!("`{left_ty}` and `{right_ty}` have no common subtype"),
))
.with_notes(vec![
"wrap one side with `as(expr, OtherType)` to assert they should compare"
.into(),
]),
TypeCheckError::HeadSubtypeMismatch {
span,
rel,
col,
expected,
found,
} => base
.with_labels(labels(
*span,
format!("`{rel}` column {col} expects `{expected}`, found `{found}`"),
))
.with_notes(vec![
"head columns allow implicit widening (subtype → parent), \
but narrowing (parent → subtype) requires `as(expr, TargetType)`"
.into(),
]),
TypeCheckError::IllegalCast { span, from, to } => base
.with_labels(labels(*span, format!("`{from}` cannot be cast to `{to}`")))
.with_notes(vec![
"`as()` only casts within the same primitive root \
(e.g. between two `<: number` subtypes)"
.into(),
]),
TypeCheckError::UnknownCastType { span, name } => base
.with_labels(labels(*span, format!("`{name}` is not a declared type")))
.with_notes(vec![format!(
"use a built-in primitive or add `.type {name} = ...` (or `<:`)"
)]),
TypeCheckError::Internal(ie) => ie.to_diagnostic(),
}
}
fn is_internal(&self) -> bool {
matches!(self, TypeCheckError::Internal(_))
}
}