use crate::grammar::{GENERATED_RECEIVERS, GeneratedMethod, builtin_return_type};
use crate::semantic::type_expr::TypeExpr;
pub fn receiver_kind(ty: &TypeExpr) -> Option<&'static str> {
match ty {
TypeExpr::Array(_) | TypeExpr::Tuple(_) => Some("Array"),
TypeExpr::Set(_) => Some("Set"),
TypeExpr::Object(_) => Some("Object"),
TypeExpr::Record(_) => Some("RecordId"),
TypeExpr::Scalar(name) => match name.to_ascii_lowercase().as_str() {
"int" | "float" | "decimal" | "number" => Some("Number"),
"string" => Some("String"),
"datetime" => Some("Datetime"),
"duration" => Some("Duration"),
"bytes" => Some("Bytes"),
"file" => Some("File"),
"object" => Some("Object"),
"array" => Some("Array"),
"set" => Some("Set"),
"record" => Some("RecordId"),
"geometry" | "point" => Some("Geometry"),
"bool" | "uuid" | "regex" | "range" | "table" | "function" | "none" | "null" => {
Some("")
}
_ => None,
},
TypeExpr::Unknown | TypeExpr::Other(_) | TypeExpr::Option(_) | TypeExpr::Union(_) => None,
TypeExpr::Literal(_) => crate::semantic::assign::widen(ty)
.as_ref()
.and_then(receiver_kind),
}
}
fn candidate_kinds(ty: &TypeExpr) -> Vec<&'static str> {
let Some(kind) = receiver_kind(ty) else {
return Vec::new();
};
if kind == "Object" && looks_like_geojson(ty) {
return vec!["Object", "Geometry"];
}
vec![kind]
}
fn looks_like_geojson(ty: &TypeExpr) -> bool {
let TypeExpr::Object(fields) = ty else {
return false;
};
let has = |wanted: &str| {
fields
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case(wanted))
};
has("type") && has("coordinates")
}
pub fn methods_for(kind: &str) -> &'static [GeneratedMethod] {
GENERATED_RECEIVERS
.iter()
.find(|receiver| receiver.kind == kind)
.map(|receiver| receiver.methods)
.unwrap_or(&[])
}
pub fn resolve(ty: &TypeExpr, name: &str) -> Option<&'static GeneratedMethod> {
candidate_kinds(ty).into_iter().find_map(|kind| {
methods_for(kind)
.iter()
.find(|method| method.method == name)
})
}
pub fn kind_label(kind: &str) -> &'static str {
match kind {
"RecordId" => "record",
"Datetime" => "datetime",
"Number" => "number",
"String" => "string",
"Duration" => "duration",
"Bytes" => "bytes",
"Geometry" => "geometry",
"Object" => "object",
"Array" => "array",
"Set" => "set",
"File" => "file",
_ => "this value",
}
}
pub fn return_type(function: &str) -> Option<TypeExpr> {
builtin_return_type(function)
}
#[cfg(test)]
mod tests {
use super::*;
fn scalar(name: &str) -> TypeExpr {
TypeExpr::Scalar(name.to_string())
}
#[test]
fn the_three_remapped_receivers_resolve() {
for (ty, method, expected) in [
(scalar("int"), "round", "math::round"),
(scalar("float"), "abs", "math::abs"),
(scalar("datetime"), "year", "time::year"),
(scalar("geometry"), "area", "geo::area"),
(scalar("point"), "centroid", "geo::centroid"),
] {
let resolved = resolve(&ty, method).unwrap_or_else(|| panic!("{ty}.{method}()"));
assert_eq!(resolved.function, expected, "{ty}.{method}()");
}
}
#[test]
fn the_identity_receivers_still_resolve() {
for (ty, method, expected) in [
(scalar("string"), "len", "string::len"),
(scalar("duration"), "days", "duration::days"),
(scalar("bytes"), "len", "bytes::len"),
(
TypeExpr::Array(Box::new(scalar("int"))),
"len",
"array::len",
),
(TypeExpr::Set(Box::new(scalar("int"))), "len", "set::len"),
(TypeExpr::Object(Vec::new()), "values", "object::values"),
(
TypeExpr::Record(vec!["person".to_string()]),
"id",
"record::id",
),
] {
let resolved = resolve(&ty, method).unwrap_or_else(|| panic!("{ty}.{method}()"));
assert_eq!(resolved.function, expected, "{ty}.{method}()");
}
}
#[test]
fn the_shared_block_reaches_every_receiver() {
for ty in [
scalar("string"),
scalar("int"),
scalar("bool"),
scalar("uuid"),
TypeExpr::Array(Box::new(scalar("int"))),
] {
assert_eq!(
resolve(&ty, "to_string").map(|found| found.function),
Some("type::string"),
"{ty}.to_string()"
);
assert_eq!(
resolve(&ty, "is_number").map(|found| found.function),
Some("type::is_number"),
"{ty}.is_number()"
);
}
}
#[test]
fn the_string_table_shadows_the_shared_block() {
let string = scalar("string");
assert_eq!(
resolve(&string, "repeat").map(|found| found.function),
Some("string::repeat")
);
assert_eq!(
resolve(&string, "is_datetime").map(|found| found.function),
Some("string::is_datetime")
);
assert!(resolve(&string, "is_set").is_none());
assert!(resolve(&scalar("int"), "is_set").is_some());
}
#[test]
fn the_catch_all_serves_the_untabled_receivers() {
for name in ["bool", "uuid", "regex", "range", "none", "null"] {
assert_eq!(receiver_kind(&scalar(name)), Some(""), "{name}");
assert!(resolve(&scalar(name), "to_string").is_some(), "{name}");
}
}
#[test]
fn a_geojson_object_literal_reaches_both_tables() {
let geojson = TypeExpr::Object(vec![
("type".to_string(), scalar("string")),
(
"coordinates".to_string(),
TypeExpr::Array(Box::new(scalar("int"))),
),
]);
assert_eq!(
resolve(&geojson, "area").map(|found| found.function),
Some("geo::area")
);
assert_eq!(
resolve(&geojson, "values").map(|found| found.function),
Some("object::values")
);
let plain = TypeExpr::Object(vec![("a".to_string(), scalar("int"))]);
assert!(resolve(&plain, "area").is_none());
assert!(resolve(&plain, "values").is_some());
}
#[test]
fn the_gate_refuses_what_it_cannot_prove() {
for ty in [
TypeExpr::Unknown,
TypeExpr::Other("weird".to_string()),
TypeExpr::Option(Box::new(scalar("string"))),
TypeExpr::Union(vec![scalar("int"), scalar("string")]),
scalar("any"),
scalar("value"),
scalar("quaternion"),
] {
assert_eq!(receiver_kind(&ty), None, "{ty}");
assert!(resolve(&ty, "len").is_none(), "{ty}");
}
}
#[test]
fn a_literal_receiver_behaves_as_its_family() {
assert_eq!(
resolve(&TypeExpr::Literal("'x'".to_string()), "len").map(|found| found.function),
Some("string::len")
);
}
#[test]
fn an_alias_names_the_implementation() {
let array = TypeExpr::Array(Box::new(scalar("int")));
assert_eq!(
resolve(&array, "every").map(|found| found.function),
Some("array::all")
);
assert_eq!(
resolve(&array, "all").map(|found| found.function),
Some("array::all")
);
}
#[test]
fn a_file_method_is_marked_experimental() {
let file = resolve(&scalar("file"), "get").expect("file::get");
assert_eq!(file.experimental, Some("Files"));
let string = resolve(&scalar("string"), "len").expect("string::len");
assert_eq!(string.experimental, None);
}
#[test]
fn the_return_types_that_were_hand_written_are_still_right() {
for (function, expected) in [
("type::is_record", "bool"),
("type::is_number", "bool"),
("type::float", "float"),
("type::int", "int"),
("type::string_lossy", "string"),
("math::round", "number"),
("math::abs", "number"),
("duration::days", "int"),
("time::year", "int"),
("time::is_leap_year", "bool"),
] {
assert_eq!(
return_type(function),
Some(TypeExpr::Scalar(expected.to_string())),
"{function}"
);
}
}
#[test]
fn a_namespace_the_hand_written_tables_never_had_now_answers() {
for (function, expected) in [
("geo::area", "float"),
("vector::dot", "float"),
("crypto::sha256", "string"),
("rand::uuid::v4", "uuid"),
("time::now", "datetime"),
("array::len", "int"),
] {
assert_eq!(
return_type(function),
Some(TypeExpr::Scalar(expected.to_string())),
"{function}"
);
}
}
#[test]
fn a_return_type_that_follows_an_argument_stays_unknown() {
for function in [
"array::group",
"array::first",
"object::values",
"object::entries",
"array::at",
] {
assert_eq!(return_type(function), None, "{function}");
}
}
}