use std::{future::Future, pin::Pin};
use indexmap::IndexMap;
use rustpython_parser::ast;
use crate::{
error::{EvalError, EvalResult, InterpreterError},
eval::{eval_body, eval_expr, functions::resolve_proxy, literals::value_to_key},
state::InterpreterState,
tools::Tools,
value::{Value, ValueKey, shared_list},
};
pub async fn eval_match(
state: &mut InterpreterState,
node: &ast::StmtMatch,
tools: &Tools,
) -> EvalResult {
let subject = eval_expr(state, &node.subject, tools).await?;
let subject = resolve_proxy(&subject).await?;
for case in &node.cases {
let mut bindings: Vec<(String, Value)> = Vec::new();
if pattern_matches(state, &case.pattern, &subject, &mut bindings, tools).await? {
for (name, value) in bindings {
state.set_variable(&name, value).map_err(EvalError::Interpreter)?;
}
if let Some(ref guard) = case.guard {
let pass = eval_expr(state, guard, tools).await?;
if !pass.is_truthy() {
continue;
}
}
return eval_body(state, &case.body, tools).await;
}
}
Ok(Value::None)
}
fn pattern_matches<'a>(
state: &'a mut InterpreterState,
pattern: &'a ast::Pattern,
subject: &'a Value,
bindings: &'a mut Vec<(String, Value)>,
tools: &'a Tools,
) -> Pin<Box<dyn Future<Output = Result<bool, EvalError>> + Send + 'a>> {
Box::pin(async move {
match pattern {
ast::Pattern::MatchValue(p) => {
let v = eval_expr(state, &p.value, tools).await?;
Ok(crate::eval::operations::values_equal_pub(&v, subject))
}
ast::Pattern::MatchSingleton(p) => {
let v = crate::eval::literals::eval_constant(&p.value);
Ok(crate::eval::operations::values_equal_pub(&v, subject))
}
ast::Pattern::MatchAs(p) => {
let matched = match &p.pattern {
Some(inner) => pattern_matches(state, inner, subject, bindings, tools).await?,
None => true,
};
if matched {
if let Some(name) = &p.name {
bindings.push((name.as_str().to_string(), subject.clone()));
}
}
Ok(matched)
}
ast::Pattern::MatchOr(p) => {
for alt in &p.patterns {
let mut alt_bindings = Vec::new();
if pattern_matches(state, alt, subject, &mut alt_bindings, tools).await? {
bindings.extend(alt_bindings);
return Ok(true);
}
}
Ok(false)
}
ast::Pattern::MatchSequence(p) => {
match_sequence(state, &p.patterns, subject, bindings, tools).await
}
ast::Pattern::MatchMapping(p) => {
match_mapping(state, p, subject, bindings, tools).await
}
ast::Pattern::MatchStar(_) => Ok(false),
ast::Pattern::MatchClass(p) => match_class(state, p, subject, bindings, tools).await,
}
})
}
async fn match_class(
state: &mut InterpreterState,
pattern: &ast::PatternMatchClass,
subject: &Value,
bindings: &mut Vec<(String, Value)>,
tools: &Tools,
) -> Result<bool, EvalError> {
let cls_name = match pattern.cls.as_ref() {
ast::Expr::Name(n) => n.id.as_str().to_string(),
ast::Expr::Attribute(a) => a.attr.as_str().to_string(),
_ => {
return Err(InterpreterError::Runtime(
"class pattern class must be a bare name or module attribute".into(),
)
.into());
}
};
if !subject_is_instance_of(state, subject, &cls_name) {
return Ok(false);
}
let builtin_single_positional = matches!(
cls_name.as_str(),
"int"
| "str"
| "float"
| "bool"
| "bytes"
| "bytearray"
| "dict"
| "frozenset"
| "list"
| "set"
| "tuple"
| "Counter"
);
if builtin_single_positional {
if pattern.patterns.len() > 1 {
return Err(InterpreterError::TypeError(format!(
"{cls_name}() accepts 0 positional sub-patterns ({} given)",
pattern.patterns.len()
))
.into());
}
if let Some(first) = pattern.patterns.first() {
if !pattern_matches(state, first, subject, bindings, tools).await? {
return Ok(false);
}
}
for (name, sub) in pattern.kwd_attrs.iter().zip(&pattern.kwd_patterns) {
let attr_val = lookup_attribute_for_match(state, subject, name.as_str())?;
if !pattern_matches(state, sub, &attr_val, bindings, tools).await? {
return Ok(false);
}
}
return Ok(true);
}
if !pattern.patterns.is_empty() {
let match_args = lookup_match_args(state, &cls_name)?;
if pattern.patterns.len() > match_args.len() {
return Err(InterpreterError::TypeError(format!(
"{cls_name}() accepts {} positional sub-patterns ({} given)",
match_args.len(),
pattern.patterns.len()
))
.into());
}
for (idx, sub) in pattern.patterns.iter().enumerate() {
let attr_name = &match_args[idx];
if pattern.kwd_attrs.iter().any(|k| k.as_str() == attr_name) {
return Err(InterpreterError::TypeError(format!(
"{cls_name}() got multiple sub-patterns for attribute '{attr_name}'"
))
.into());
}
let attr_val = lookup_attribute_for_match(state, subject, attr_name)?;
if !pattern_matches(state, sub, &attr_val, bindings, tools).await? {
return Ok(false);
}
}
}
for (name, sub) in pattern.kwd_attrs.iter().zip(&pattern.kwd_patterns) {
let attr_val = lookup_attribute_for_match(state, subject, name.as_str())?;
if !pattern_matches(state, sub, &attr_val, bindings, tools).await? {
return Ok(false);
}
}
Ok(true)
}
fn subject_is_instance_of(state: &InterpreterState, obj: &Value, type_name: &str) -> bool {
if type_name == "object" {
return true;
}
if let Value::Instance(inst) = obj {
if inst.class_name == type_name {
return true;
}
if let Some(class) = state.classes.get(&inst.class_name) {
return class.mro.iter().any(|ancestor| ancestor == type_name);
}
return false;
}
obj.type_name() == type_name
|| matches!((obj, type_name), (Value::Bool(_), "int") | (Value::Counter(_), "dict"))
}
fn lookup_match_args(
state: &InterpreterState,
class_name: &str,
) -> Result<Vec<compact_str::CompactString>, EvalError> {
let Some(class) = state.classes.get(class_name) else {
return Ok(Vec::new());
};
let Some(value) = class.class_attrs.get("__match_args__") else {
return Ok(Vec::new());
};
let Value::Tuple(items) = value else {
return Err(InterpreterError::TypeError(format!(
"{class_name}.__match_args__ must be a tuple (got '{}')",
value.type_name()
))
.into());
};
let mut names = Vec::with_capacity(items.len());
for item in items {
let Value::String(s) = item else {
return Err(InterpreterError::TypeError(format!(
"{class_name}.__match_args__ entries must be strings (got '{}')",
item.type_name()
))
.into());
};
names.push(s.clone());
}
Ok(names)
}
fn lookup_attribute_for_match(state: &InterpreterState, subject: &Value, attr: &str) -> EvalResult {
if let Value::Instance(inst) = subject {
return crate::eval::classes::instance_attribute(state, inst, attr);
}
if let Value::Date(date) = subject {
return crate::eval::modules::datetime::date_attribute(*date, attr);
}
Err(InterpreterError::AttributeError(format!(
"'{}' object has no attribute '{attr}'",
subject.type_name()
))
.into())
}
async fn match_sequence(
state: &mut InterpreterState,
patterns: &[ast::Pattern],
subject: &Value,
bindings: &mut Vec<(String, Value)>,
tools: &Tools,
) -> Result<bool, EvalError> {
let items: Vec<Value> = match subject {
Value::List(items) => items.lock().clone(),
Value::Tuple(items) => items.clone(),
_ => return Ok(false),
};
let star_pos = patterns.iter().position(|p| matches!(p, ast::Pattern::MatchStar(_)));
let Some(star) = star_pos else {
if items.len() != patterns.len() {
return Ok(false);
}
for (pat, item) in patterns.iter().zip(&items) {
if !pattern_matches(state, pat, item, bindings, tools).await? {
return Ok(false);
}
}
return Ok(true);
};
let before = &patterns[..star];
let after = &patterns[star + 1..];
if items.len() < before.len() + after.len() {
return Ok(false);
}
for (pat, item) in before.iter().zip(&items) {
if !pattern_matches(state, pat, item, bindings, tools).await? {
return Ok(false);
}
}
let after_start = items.len() - after.len();
for (pat, item) in after.iter().zip(&items[after_start..]) {
if !pattern_matches(state, pat, item, bindings, tools).await? {
return Ok(false);
}
}
if let ast::Pattern::MatchStar(s) = &patterns[star] {
if let Some(name) = &s.name {
let middle = items[before.len()..after_start].to_vec();
bindings.push((name.as_str().to_string(), Value::List(shared_list(middle))));
}
}
Ok(true)
}
async fn match_mapping(
state: &mut InterpreterState,
pattern: &ast::PatternMatchMapping,
subject: &Value,
bindings: &mut Vec<(String, Value)>,
tools: &Tools,
) -> Result<bool, EvalError> {
let Value::Dict(map) = subject else {
return Ok(false);
};
let map = map.clone();
let mut matched_keys: Vec<ValueKey> = Vec::new();
for (key_expr, sub) in pattern.keys.iter().zip(&pattern.patterns) {
let key_val = eval_expr(state, key_expr, tools).await?;
let key = value_to_key(&key_val)?;
let Some(value) = map.get(&key).cloned() else {
return Ok(false);
};
if !pattern_matches(state, sub, &value, bindings, tools).await? {
return Ok(false);
}
matched_keys.push(key);
}
if let Some(rest) = &pattern.rest {
let remaining: IndexMap<ValueKey, Value> = map
.iter()
.filter(|(k, _)| !matched_keys.contains(k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
bindings.push((rest.as_str().to_string(), Value::Dict(remaining)));
}
Ok(true)
}