use std::fmt;
use crate::type_pattern::{Bound, TypePattern};
pub const INDEX_READ: &str = "[]";
pub const INDEX_STORE: &str = "[]=";
pub const INDEX_STORE_MIN: &str = "[]min=";
pub const INDEX_STORE_MAX: &str = "[]max=";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Purity {
Pure,
Impure,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum MethodLowering {
RuntimeSymbol(crate::abi::RuntimeSymbol),
Intrinsic(&'static str),
ScalarPrimitive(crate::abi::RuntimeSymbol),
}
#[derive(Clone, Debug)]
pub struct MethodEntry {
pub receiver: TypePattern,
pub name: &'static str,
pub params: Vec<TypePattern>,
pub result: TypePattern,
pub purity: Purity,
pub lowering: MethodLowering,
pub doc: &'static str,
}
impl MethodEntry {
pub fn arity(&self) -> usize {
self.params.len()
}
pub fn allocates(&self) -> bool {
match self.lowering {
MethodLowering::RuntimeSymbol(sym) | MethodLowering::ScalarPrimitive(sym) => {
sym.allocates()
}
MethodLowering::Intrinsic(_) => false,
}
}
pub fn can_fault(&self) -> bool {
match self.lowering {
MethodLowering::RuntimeSymbol(sym) | MethodLowering::ScalarPrimitive(sym) => {
sym.faults()
}
MethodLowering::Intrinsic(_) => false,
}
}
#[must_use]
pub fn bounds(&self) -> Vec<(&'static str, Bound)> {
let mut all = Vec::new();
self.receiver.collect_bounds(&mut all);
for p in &self.params {
p.collect_bounds(&mut all);
}
self.result.collect_bounds(&mut all);
let mut seen: Vec<(&'static str, Bound)> = Vec::new();
for entry in all {
if !seen.contains(&entry) {
seen.push(entry);
}
}
seen
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MethodCatalogError {
Duplicate {
receiver: TypePattern,
name: &'static str,
arity: usize,
},
ConflictingBound {
method: &'static str,
var: &'static str,
first: Bound,
second: Bound,
},
AmbiguousWithIterable {
receiver: TypePattern,
name: &'static str,
arity: usize,
},
AmbiguousIterablePair { name: &'static str, arity: usize },
IterableOutsideReceiver { method: &'static str, arity: usize },
}
impl fmt::Display for MethodCatalogError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MethodCatalogError::Duplicate {
receiver,
name,
arity,
} => write!(
f,
"duplicate catalog entry: {receiver}.{name}/{arity} already defined"
),
MethodCatalogError::ConflictingBound {
method,
var,
first,
second,
} => write!(
f,
"catalog entry `{method}` bounds `{var}` as both {first:?} and {second:?}"
),
MethodCatalogError::AmbiguousWithIterable {
receiver,
name,
arity,
} => write!(
f,
"catalog entry {receiver}.{name}/{arity} shadows the generic \
Iterable.{name}/{arity}: both match this receiver, and which \
one a call resolves to would be insertion order"
),
MethodCatalogError::AmbiguousIterablePair { name, arity } => write!(
f,
"two generic Iterable.{name}/{arity} rows differ only in the \
bound on their item: both match every receiver, and which one \
a call resolves to would be insertion order"
),
MethodCatalogError::IterableOutsideReceiver { method, arity } => write!(
f,
"catalog entry `{method}`/{arity} writes an Iterable pattern \
outside its receiver: the pipeline generalizes over what it \
walks, not over every sequence a row mentions"
),
}
}
}
impl std::error::Error for MethodCatalogError {}
#[derive(Clone, Debug, Default)]
pub struct MethodCatalog {
entries: Vec<MethodEntry>,
}
impl MethodCatalog {
pub fn build() -> MethodCatalogBuilder {
MethodCatalogBuilder::default()
}
pub fn entries(&self) -> &[MethodEntry] {
&self.entries
}
pub fn by_receiver_and_name<'a>(
&'a self,
receiver: &'a TypePattern,
name: &'a str,
) -> impl Iterator<Item = &'a MethodEntry> + 'a {
self.entries
.iter()
.filter(move |e| &e.receiver == receiver && e.name == name)
}
pub fn has_name_at_arity(&self, name: &str, arity: usize) -> bool {
self.entries
.iter()
.any(|e| e.name == name && e.arity() == arity)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Default)]
pub struct MethodCatalogBuilder {
entries: Vec<MethodEntry>,
}
impl MethodCatalogBuilder {
pub fn entry(mut self, entry: MethodEntry) -> Self {
self.entries.push(entry);
self
}
pub fn finish(self) -> Result<MethodCatalog, MethodCatalogError> {
for (i, a) in self.entries.iter().enumerate() {
let nested_in_receiver = match &a.receiver {
TypePattern::Iterable { item } => mentions_iterable(item),
other => mentions_iterable(other),
};
if nested_in_receiver || a.params.iter().chain([&a.result]).any(mentions_iterable) {
return Err(MethodCatalogError::IterableOutsideReceiver {
method: a.name,
arity: a.arity(),
});
}
for b in self.entries.iter().skip(i + 1) {
if a.receiver == b.receiver && a.name == b.name && a.arity() == b.arity() {
return Err(MethodCatalogError::Duplicate {
receiver: a.receiver.clone(),
name: a.name,
arity: a.arity(),
});
}
if let Some(concrete) = shadowed_by_iterable(a, b) {
return Err(MethodCatalogError::AmbiguousWithIterable {
receiver: concrete.receiver.clone(),
name: concrete.name,
arity: concrete.arity(),
});
}
if a.name == b.name
&& a.arity() == b.arity()
&& matches!(a.receiver, TypePattern::Iterable { .. })
&& matches!(b.receiver, TypePattern::Iterable { .. })
{
return Err(MethodCatalogError::AmbiguousIterablePair {
name: a.name,
arity: a.arity(),
});
}
}
let bounds = a.bounds();
for (j, (var, first)) in bounds.iter().enumerate() {
if let Some((_, second)) = bounds.iter().skip(j + 1).find(|(v, _)| v == var) {
return Err(MethodCatalogError::ConflictingBound {
method: a.name,
var,
first: *first,
second: *second,
});
}
}
}
Ok(MethodCatalog {
entries: self.entries,
})
}
}
fn mentions_iterable(pat: &TypePattern) -> bool {
match pat {
TypePattern::Iterable { .. } => true,
TypePattern::Collection { args, .. } | TypePattern::Tuple(args) => {
args.iter().any(mentions_iterable)
}
TypePattern::Option(inner) => mentions_iterable(inner),
TypePattern::Record { fields, .. } => fields.iter().any(|(_, f)| mentions_iterable(f)),
TypePattern::Function { params, result } => {
params.iter().any(mentions_iterable) || mentions_iterable(result)
}
TypePattern::Scalar(_) | TypePattern::Var { .. } | TypePattern::Unit => false,
}
}
fn shadowed_by_iterable<'e>(a: &'e MethodEntry, b: &'e MethodEntry) -> Option<&'e MethodEntry> {
if a.name != b.name || a.arity() != b.arity() {
return None;
}
let concrete = match (&a.receiver, &b.receiver) {
(TypePattern::Iterable { .. }, TypePattern::Iterable { .. }) => return None,
(TypePattern::Iterable { .. }, _) => b,
(_, TypePattern::Iterable { .. }) => a,
_ => return None,
};
crate::type_pattern::is_pipeline_receiver(&concrete.receiver).then_some(concrete)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::type_pattern::{CollectionCtor, ScalarType};
fn vec_of_t() -> TypePattern {
TypePattern::Collection {
ctor: CollectionCtor::Vec,
args: vec![TypePattern::var("T")],
}
}
fn vec_push() -> MethodEntry {
MethodEntry {
receiver: vec_of_t(),
name: "push",
params: vec![TypePattern::var("T")],
result: TypePattern::Unit,
purity: Purity::Impure,
lowering: MethodLowering::RuntimeSymbol(crate::abi::RuntimeSymbol::VecPush),
doc: "Append a value to the end of the vector.",
}
}
fn vec_len() -> MethodEntry {
MethodEntry {
receiver: vec_of_t(),
name: "len",
params: vec![],
result: TypePattern::Scalar(ScalarType::Int),
purity: Purity::Pure,
lowering: MethodLowering::RuntimeSymbol(crate::abi::RuntimeSymbol::VecLen),
doc: "Number of elements in the vector.",
}
}
#[test]
fn finish_accepts_distinct_entries() {
let catalog = MethodCatalog::build()
.entry(vec_push())
.entry(vec_len())
.finish()
.expect("distinct entries");
assert_eq!(catalog.len(), 2);
let names: Vec<_> = catalog
.by_receiver_and_name(&vec_of_t(), "push")
.map(|e| e.name)
.collect();
assert_eq!(names, vec!["push"]);
}
#[test]
fn finish_rejects_duplicate_triple() {
let dup = MethodEntry {
doc: "alternate overload that the language does not allow",
..vec_push()
};
let err = MethodCatalog::build()
.entry(vec_push())
.entry(dup)
.finish()
.unwrap_err();
match err {
MethodCatalogError::Duplicate { name, arity, .. } => {
assert_eq!(name, "push");
assert_eq!(arity, 1);
}
other => panic!("expected a duplicate, got {other}"),
}
}
#[test]
fn finish_rejects_two_bounds_on_one_variable() {
let conflicted = MethodEntry {
receiver: TypePattern::Collection {
ctor: CollectionCtor::Vec,
args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
},
params: vec![TypePattern::is_scalar("T", ScalarType::Text)],
..vec_push()
};
let err = MethodCatalog::build()
.entry(conflicted)
.finish()
.unwrap_err();
match err {
MethodCatalogError::ConflictingBound { method, var, .. } => {
assert_eq!(method, "push");
assert_eq!(var, "T");
}
other => panic!("expected a conflicting bound, got {other}"),
}
let restated = MethodEntry {
receiver: TypePattern::Collection {
ctor: CollectionCtor::Vec,
args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
},
params: vec![TypePattern::is_scalar("T", ScalarType::Int)],
..vec_push()
};
let bounds = restated.bounds();
assert_eq!(bounds, vec![("T", Bound::Is(ScalarType::Int))]);
assert!(MethodCatalog::build().entry(restated).finish().is_ok());
}
#[test]
fn bounds_are_found_in_every_position() {
let on_receiver = MethodEntry {
receiver: TypePattern::Collection {
ctor: CollectionCtor::Vec,
args: vec![TypePattern::is_scalar("T", ScalarType::Int)],
},
..vec_len()
};
assert_eq!(
on_receiver.bounds(),
vec![("T", Bound::Is(ScalarType::Int))]
);
let in_a_closure = MethodEntry {
params: vec![TypePattern::Function {
params: vec![TypePattern::is_scalar("U", ScalarType::Char)],
result: Box::new(TypePattern::Unit),
}],
..vec_len()
};
assert_eq!(
in_a_closure.bounds(),
vec![("U", Bound::Is(ScalarType::Char))]
);
let in_the_result = MethodEntry {
result: TypePattern::Tuple(vec![
TypePattern::Scalar(ScalarType::Int),
TypePattern::is_scalar("V", ScalarType::Byte),
]),
..vec_len()
};
assert_eq!(
in_the_result.bounds(),
vec![("V", Bound::Is(ScalarType::Byte))]
);
assert!(vec_push().bounds().is_empty());
}
#[test]
fn same_name_different_arity_is_allowed() {
let other = MethodEntry {
params: vec![TypePattern::Scalar(ScalarType::Int)],
..vec_len()
};
let catalog = MethodCatalog::build()
.entry(vec_len())
.entry(other)
.finish()
.expect("different arity is not a duplicate");
assert_eq!(catalog.len(), 2);
}
#[test]
fn finish_rejects_a_concrete_row_that_shadows_the_generic_one() {
let generic = MethodEntry {
receiver: TypePattern::iterable(TypePattern::var("T")),
name: "map",
params: vec![TypePattern::Function {
params: vec![TypePattern::var("T")],
result: Box::new(TypePattern::var("U")),
}],
result: TypePattern::Collection {
ctor: CollectionCtor::Vec,
args: vec![TypePattern::var("U")],
},
purity: Purity::Pure,
lowering: MethodLowering::Intrinsic("seq_map"),
doc: "Apply a function to each element.",
};
let on_a_set = MethodEntry {
receiver: TypePattern::Collection {
ctor: CollectionCtor::Set,
args: vec![TypePattern::var("T")],
},
..generic.clone()
};
let err = MethodCatalog::build()
.entry(generic.clone())
.entry(on_a_set)
.finish()
.unwrap_err();
match err {
MethodCatalogError::AmbiguousWithIterable { name, arity, .. } => {
assert_eq!((name, arity), ("map", 1));
}
other => panic!("expected an Iterable shadow, got {other}"),
}
let on_a_set = MethodEntry {
receiver: TypePattern::Collection {
ctor: CollectionCtor::Set,
args: vec![TypePattern::var("T")],
},
..generic.clone()
};
assert!(
MethodCatalog::build()
.entry(on_a_set)
.entry(generic.clone())
.finish()
.is_err()
);
let on_a_grid = MethodEntry {
receiver: TypePattern::Collection {
ctor: CollectionCtor::Grid,
args: vec![TypePattern::var("T")],
},
result: TypePattern::Collection {
ctor: CollectionCtor::Grid,
args: vec![TypePattern::var("U")],
},
..generic.clone()
};
assert!(
MethodCatalog::build()
.entry(generic.clone())
.entry(on_a_grid)
.finish()
.is_ok()
);
let different_arity = MethodEntry {
receiver: TypePattern::Collection {
ctor: CollectionCtor::Set,
args: vec![TypePattern::var("T")],
},
params: vec![],
..generic.clone()
};
assert!(
MethodCatalog::build()
.entry(generic)
.entry(different_arity)
.finish()
.is_ok()
);
}
#[test]
fn finish_rejects_two_generic_rows_at_one_arity() {
let of_text = MethodEntry {
receiver: TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Text)),
name: "join",
params: vec![TypePattern::Scalar(ScalarType::Text)],
result: TypePattern::Scalar(ScalarType::Text),
purity: Purity::Pure,
lowering: MethodLowering::Intrinsic("seq_join"),
doc: "These Text items concatenated.",
};
let of_char = MethodEntry {
receiver: TypePattern::iterable(TypePattern::is_scalar("T", ScalarType::Char)),
..of_text.clone()
};
let err = MethodCatalog::build()
.entry(of_text.clone())
.entry(of_char)
.finish()
.unwrap_err();
match err {
MethodCatalogError::AmbiguousIterablePair { name, arity } => {
assert_eq!((name, arity), ("join", 1));
}
other => panic!("expected an ambiguous generic pair, got {other}"),
}
let nullary = MethodEntry {
params: vec![],
..of_text.clone()
};
assert!(
MethodCatalog::build()
.entry(of_text)
.entry(nullary)
.finish()
.is_ok()
);
}
#[test]
fn finish_rejects_an_iterable_written_outside_the_receiver() {
let iterable = || TypePattern::iterable(TypePattern::var("T"));
let base = MethodEntry {
receiver: iterable(),
name: "zip",
params: vec![],
result: TypePattern::Unit,
purity: Purity::Pure,
lowering: MethodLowering::Intrinsic("seq_zip"),
doc: "Pair elements with another sequence.",
};
for offender in [
MethodEntry {
params: vec![iterable()],
..base.clone()
},
MethodEntry {
params: vec![TypePattern::Function {
params: vec![TypePattern::var("T")],
result: Box::new(iterable()),
}],
..base.clone()
},
MethodEntry {
result: TypePattern::Collection {
ctor: CollectionCtor::Vec,
args: vec![iterable()],
},
..base.clone()
},
MethodEntry {
receiver: TypePattern::iterable(iterable()),
..base.clone()
},
] {
let err = MethodCatalog::build().entry(offender).finish().unwrap_err();
assert!(
matches!(err, MethodCatalogError::IterableOutsideReceiver { .. }),
"expected the parameter rule, got {err}"
);
}
assert!(MethodCatalog::build().entry(base).finish().is_ok());
}
#[test]
fn entry_reports_capabilities() {
let e = vec_push();
assert_eq!(e.arity(), 1);
assert!(e.allocates());
assert!(vec_len().allocates());
assert!(e.can_fault(), "praxis_vec_push raises TypeMismatch");
assert!(!vec_len().can_fault());
assert_eq!(e.purity, Purity::Impure);
}
}