use std::collections::BTreeMap;
use crate::ast::{Declaration, EffectDefinition, Program};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpResolution {
Resolved(String),
Ambiguous(Vec<String>),
Undeclared,
}
#[derive(Debug, Default, Clone)]
pub struct EffectCatalog {
effects: BTreeMap<String, Vec<String>>,
by_operation: BTreeMap<String, Vec<String>>,
arity: BTreeMap<(String, String), usize>,
}
impl EffectCatalog {
pub fn from_program(program: &Program) -> Self {
let mut catalog = Self::default();
for decl in &program.declarations {
if let Declaration::Effect(eff) = decl {
catalog.insert(eff);
}
}
for owners in catalog.by_operation.values_mut() {
owners.sort();
owners.dedup();
}
catalog
}
fn insert(&mut self, eff: &EffectDefinition) {
let ops: Vec<String> = eff.operations.iter().map(|o| o.name.clone()).collect();
for op in &eff.operations {
self.by_operation
.entry(op.name.clone())
.or_default()
.push(eff.name.clone());
self.arity
.insert((eff.name.clone(), op.name.clone()), op.parameters.len());
}
self.effects.entry(eff.name.clone()).or_default().extend(ops);
}
pub fn declares_effect(&self, effect: &str) -> bool {
self.effects.contains_key(effect)
}
pub fn declares_operation(&self, effect: &str, operation: &str) -> bool {
self.arity.contains_key(&(effect.to_string(), operation.to_string()))
}
pub fn arity_of(&self, effect: &str, operation: &str) -> Option<usize> {
self.arity
.get(&(effect.to_string(), operation.to_string()))
.copied()
}
pub fn effect_names(&self) -> Vec<&str> {
self.effects.keys().map(|s| s.as_str()).collect()
}
pub fn resolve_bare(&self, operation: &str) -> OpResolution {
match self.by_operation.get(operation) {
None => OpResolution::Undeclared,
Some(owners) if owners.len() == 1 => OpResolution::Resolved(owners[0].clone()),
Some(owners) => OpResolution::Ambiguous(owners.clone()),
}
}
pub fn resolve_site(&self, explicit: Option<&str>, operation: &str) -> OpResolution {
match explicit {
Some(effect) => OpResolution::Resolved(effect.to_string()),
None => self.resolve_bare(operation),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
fn catalog(src: &str) -> EffectCatalog {
let tokens = Lexer::new(src, "<catalog-test>").tokenize().expect("lex");
let program = Parser::new(tokens).parse().expect("parse");
EffectCatalog::from_program(&program)
}
#[test]
fn one_declarer_resolves() {
let c = catalog("effect SSE { Emit(token: Token) -> Unit }");
assert_eq!(
c.resolve_bare("Emit"),
OpResolution::Resolved("SSE".to_string())
);
}
#[test]
fn two_declarers_are_ambiguous_and_both_are_named() {
let c = catalog(
"effect SSE { Emit(token: Token) -> Unit }\n\
effect Log { Emit(message: Text) -> Unit }",
);
match c.resolve_bare("Emit") {
OpResolution::Ambiguous(owners) => {
assert_eq!(owners, vec!["Log".to_string(), "SSE".to_string()]);
}
other => panic!("a colliding operation must be ambiguous, got {other:?}"),
}
}
#[test]
fn undeclared_operation_does_not_resolve() {
let c = catalog("effect SSE { Emit(token: Token) -> Unit }");
assert_eq!(c.resolve_bare("Nope"), OpResolution::Undeclared);
}
#[test]
fn qualifying_escapes_the_ambiguity() {
let c = catalog(
"effect SSE { Emit(token: Token) -> Unit }\n\
effect Log { Emit(message: Text) -> Unit }",
);
assert_eq!(
c.resolve_site(Some("SSE"), "Emit"),
OpResolution::Resolved("SSE".to_string())
);
}
#[test]
fn arity_and_membership_come_from_the_declaration() {
let c = catalog("effect SSE { Emit(token: Token) -> Unit Done() -> Never }");
assert!(c.declares_effect("SSE"));
assert!(!c.declares_effect("Nope"));
assert!(c.declares_operation("SSE", "Done"));
assert!(!c.declares_operation("SSE", "Missing"));
assert_eq!(c.arity_of("SSE", "Emit"), Some(1));
assert_eq!(c.arity_of("SSE", "Done"), Some(0));
assert_eq!(c.arity_of("SSE", "Missing"), None);
}
}