use crate::semantic::type_expr::TypeExpr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueKind {
Int,
Float,
Decimal,
Number,
String,
Datetime,
Duration,
Array,
Set,
Object,
Other,
}
impl ValueKind {
fn is_number(self) -> bool {
matches!(self, Self::Int | Self::Float | Self::Decimal | Self::Number)
}
pub fn as_type(self) -> TypeExpr {
let name = match self {
Self::Int => "int",
Self::Float => "float",
Self::Decimal => "decimal",
Self::Number => "number",
Self::String => "string",
Self::Datetime => "datetime",
Self::Duration => "duration",
Self::Array => "array",
Self::Set => "set",
Self::Object => "object",
Self::Other => return TypeExpr::Unknown,
};
TypeExpr::Scalar(name.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithOp {
Add,
Sub,
Mul,
Div,
Pow,
}
impl ArithOp {
pub fn parse(spelling: &str) -> Option<Self> {
Some(match spelling {
"+" => Self::Add,
"-" => Self::Sub,
"*" | "×" => Self::Mul,
"/" | "÷" => Self::Div,
"**" => Self::Pow,
_ => return None,
})
}
pub fn can_fail(self) -> bool {
!matches!(self, Self::Div)
}
pub fn failure_message(self, lhs: &TypeExpr, rhs: &TypeExpr) -> String {
let verb = match self {
Self::Add => "addition",
Self::Sub => "subtraction",
Self::Mul => "multiplication",
Self::Div => "division",
Self::Pow => {
return format!("Cannot raise the value `{lhs}` with `{rhs}`.");
}
};
format!("Cannot perform {verb} with `{lhs}` and `{rhs}`.")
}
}
pub fn arith_result(op: ArithOp, lhs: ValueKind, rhs: ValueKind) -> Option<ValueKind> {
use ArithOp::{Add, Div, Mul, Pow, Sub};
use ValueKind::{Array, Datetime, Duration, Object, Set, String};
if lhs.is_number() && rhs.is_number() {
return Some(number_result(lhs, rhs));
}
Some(match (op, lhs, rhs) {
(Add, String, String) => String,
(Add, Datetime, Duration) | (Add, Duration, Datetime) => Datetime,
(Add, Duration, Duration) => Duration,
(Add, Array, Array) | (Add, Array, Set) => Array,
(Add, Set, Set) | (Add, Set, Array) => Set,
(Add, Object, Object) => Object,
(Sub, Datetime, Datetime) => Duration,
(Sub, Datetime, Duration) | (Sub, Duration, Datetime) => Datetime,
(Sub, Duration, Duration) => Duration,
(Sub, Array, Array) | (Sub, Array, Set) => Array,
(Sub, Set, Set) | (Sub, Set, Array) => Set,
(Mul, Duration, right) if right.is_number() => Duration,
(Div, Duration, right) if right.is_number() => Duration,
(Div, _, _) => ValueKind::Float,
(Pow, _, _) => return None,
_ => return None,
})
}
fn number_result(lhs: ValueKind, rhs: ValueKind) -> ValueKind {
use ValueKind::{Decimal, Float, Int, Number};
match (lhs, rhs) {
(Number, _) | (_, Number) => Number,
(Int, Int) => Int,
(Float, Float) | (Int, Float) | (Float, Int) => Float,
_ => Decimal,
}
}
pub fn value_kind(ty: &TypeExpr) -> Option<ValueKind> {
match ty {
TypeExpr::Scalar(name) => scalar_kind(name),
TypeExpr::Array(_) | TypeExpr::Tuple(_) => Some(ValueKind::Array),
TypeExpr::Set(_) => Some(ValueKind::Set),
TypeExpr::Object(_) => Some(ValueKind::Object),
TypeExpr::Record(_) => Some(ValueKind::Other),
TypeExpr::Literal(_) => crate::semantic::assign::widen(ty)
.as_ref()
.and_then(value_kind),
TypeExpr::Unknown | TypeExpr::Other(_) | TypeExpr::Option(_) | TypeExpr::Union(_) => None,
}
}
fn scalar_kind(name: &str) -> Option<ValueKind> {
Some(match name.to_ascii_lowercase().as_str() {
"int" => ValueKind::Int,
"float" => ValueKind::Float,
"decimal" => ValueKind::Decimal,
"number" => ValueKind::Number,
"string" => ValueKind::String,
"datetime" => ValueKind::Datetime,
"duration" => ValueKind::Duration,
"array" => ValueKind::Array,
"set" => ValueKind::Set,
"object" => ValueKind::Object,
"bool" | "bytes" | "uuid" | "regex" | "geometry" | "point" | "file" | "range"
| "function" | "record" | "table" | "none" | "null" => ValueKind::Other,
_ => return None,
})
}
pub fn binding_power(spelling: &str) -> u8 {
match spelling {
"**" => 3,
"*" | "×" | "/" | "÷" => 2,
"+" | "-" => 1,
_ => 0,
}
}
pub fn short_circuits(spelling: &str) -> bool {
matches!(
spelling.to_ascii_uppercase().as_str(),
"??" | "?:" | "&&" | "||" | "AND" | "OR"
)
}
pub fn normalize_operator(raw: &str) -> String {
raw.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
fn scalar(name: &str) -> TypeExpr {
TypeExpr::Scalar(name.to_string())
}
fn kind(name: &str) -> ValueKind {
value_kind(&scalar(name)).expect("a known kind")
}
#[test]
fn the_reported_case_is_a_failure() {
assert_eq!(
arith_result(ArithOp::Add, kind("string"), kind("string")),
Some(ValueKind::String)
);
assert_eq!(
arith_result(ArithOp::Add, kind("string"), kind("int")),
None
);
}
#[test]
fn the_cast_forms_of_the_reported_case_are_accepted() {
assert_eq!(
arith_result(ArithOp::Add, kind("string"), kind("string")),
Some(ValueKind::String)
);
assert_eq!(
arith_result(ArithOp::Add, kind("int"), kind("int")),
Some(ValueKind::Int)
);
}
#[test]
fn numbers_promote_along_the_engine_chain() {
for (left, right, expected) in [
("int", "int", ValueKind::Int),
("int", "float", ValueKind::Float),
("float", "int", ValueKind::Float),
("float", "float", ValueKind::Float),
("decimal", "decimal", ValueKind::Decimal),
("int", "decimal", ValueKind::Decimal),
("float", "decimal", ValueKind::Decimal),
("number", "int", ValueKind::Number),
] {
assert_eq!(
arith_result(ArithOp::Add, kind(left), kind(right)),
Some(expected),
"{left} + {right}"
);
}
}
#[test]
fn multiplication_by_a_duration_is_one_directional() {
assert_eq!(
arith_result(ArithOp::Mul, kind("duration"), kind("int")),
Some(ValueKind::Duration)
);
assert_eq!(
arith_result(ArithOp::Mul, kind("int"), kind("duration")),
None
);
}
#[test]
fn collections_combine_but_do_not_take_a_scalar() {
assert_eq!(
arith_result(ArithOp::Add, kind("array"), kind("set")),
Some(ValueKind::Array)
);
assert_eq!(
arith_result(ArithOp::Add, kind("set"), kind("array")),
Some(ValueKind::Set)
);
for op in [ArithOp::Add, ArithOp::Sub] {
assert_eq!(arith_result(op, kind("array"), kind("int")), None);
assert_eq!(arith_result(op, kind("set"), kind("int")), None);
}
}
#[test]
fn duration_arithmetic_follows_the_corpus() {
assert_eq!(
arith_result(ArithOp::Add, kind("duration"), kind("duration")),
Some(ValueKind::Duration)
);
assert_eq!(
arith_result(ArithOp::Sub, kind("datetime"), kind("datetime")),
Some(ValueKind::Duration)
);
assert_eq!(
arith_result(ArithOp::Add, kind("datetime"), kind("duration")),
Some(ValueKind::Datetime)
);
assert_eq!(
arith_result(ArithOp::Mul, kind("duration"), kind("duration")),
None
);
assert_eq!(
arith_result(ArithOp::Pow, kind("duration"), kind("duration")),
None
);
}
#[test]
fn division_never_fails() {
for left in ["array", "string", "object", "bool"] {
assert!(
arith_result(ArithOp::Div, kind(left), kind("int")).is_some(),
"{left} / int must not be reported"
);
}
assert!(!ArithOp::Div.can_fail());
assert!(ArithOp::Add.can_fail());
}
#[test]
fn a_concrete_kind_with_no_arm_is_provably_wrong() {
for name in ["bool", "bytes", "uuid", "regex", "none", "null", "record"] {
assert_eq!(value_kind(&scalar(name)), Some(ValueKind::Other), "{name}");
assert_eq!(
arith_result(ArithOp::Add, kind(name), kind("int")),
None,
"{name} + int"
);
}
}
#[test]
fn the_gate_refuses_everything_it_cannot_prove() {
let unprovable = [
TypeExpr::Unknown,
TypeExpr::Other("weird<thing>".to_string()),
TypeExpr::Option(Box::new(scalar("int"))),
TypeExpr::Union(vec![scalar("int"), scalar("string")]),
scalar("any"),
scalar("value"),
scalar("quaternion"),
];
for ty in unprovable {
assert_eq!(value_kind(&ty), None, "{ty} must not be judged");
}
}
#[test]
fn a_literal_behaves_as_its_family() {
assert_eq!(
value_kind(&TypeExpr::Literal("'x'".to_string())),
Some(ValueKind::String)
);
assert_eq!(
value_kind(&TypeExpr::Literal("42".to_string())),
Some(ValueKind::Int)
);
assert_eq!(
value_kind(&TypeExpr::Literal("1h".to_string())),
Some(ValueKind::Duration)
);
}
#[test]
fn arithmetic_binds_tighter_than_anything_else() {
assert!(binding_power("**") > binding_power("*"));
assert!(binding_power("*") > binding_power("+"));
assert_eq!(binding_power("+"), binding_power("-"));
assert_eq!(binding_power("*"), binding_power("×"));
for other in [
"==", "&&", "??", "?:", "IN", "CONTAINS", "<|2|>", "@1@", "+=",
] {
assert!(
binding_power(other) < binding_power("+"),
"{other} must bind looser than `+`"
);
}
}
#[test]
fn only_arithmetic_spellings_parse_as_operators() {
assert_eq!(ArithOp::parse("+"), Some(ArithOp::Add));
assert_eq!(ArithOp::parse("×"), Some(ArithOp::Mul));
assert_eq!(ArithOp::parse("÷"), Some(ArithOp::Div));
assert_eq!(ArithOp::parse("**"), Some(ArithOp::Pow));
for spelling in ["*=", "+=", "-=", "==", "?=", "IN", "@@"] {
assert_eq!(ArithOp::parse(spelling), None, "{spelling}");
}
}
#[test]
fn the_message_matches_the_engine_wording() {
let string = scalar("string");
let int = scalar("int");
assert_eq!(
ArithOp::Add.failure_message(&string, &int),
"Cannot perform addition with `string` and `int`."
);
assert_eq!(
ArithOp::Pow.failure_message(&string, &int),
"Cannot raise the value `string` with `int`."
);
}
#[test]
fn the_short_circuiting_operators_are_recognised() {
for spelling in ["??", "?:", "&&", "||", "AND", "and", "OR"] {
assert!(short_circuits(spelling), "{spelling} short-circuits");
}
for spelling in ["+", "-", "*", "**", "==", "IN"] {
assert!(!short_circuits(spelling), "{spelling} does not");
}
}
#[test]
fn operator_text_collapses_interior_whitespace() {
assert_eq!(normalize_operator("NOT IN"), "NOT IN");
assert_eq!(normalize_operator("IS\n NOT"), "IS NOT");
assert_eq!(normalize_operator("+"), "+");
}
}