use crate::bindings::{MutableStackEnvironment, MutableStackMap, StackEnvironment, StackMap};
use crate::expressions::{DimExpr, TryMatchResult};
use crate::shape_argument::ShapeArgument;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt::{Display, Formatter};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DimMatcher<'a> {
Any {
label: Option<&'a str>,
},
Ellipsis {
label: Option<&'a str>,
},
Expr {
label: Option<&'a str>,
expr: DimExpr<'a>,
},
}
impl<'a> DimMatcher<'a> {
pub const fn any() -> Self {
DimMatcher::Any { label: None }
}
pub const fn ellipsis() -> Self {
DimMatcher::Ellipsis { label: None }
}
pub const fn expr(expr: DimExpr<'a>) -> Self {
DimMatcher::Expr { label: None, expr }
}
pub const fn label(&self) -> Option<&'a str> {
match self {
DimMatcher::Any { label } => *label,
DimMatcher::Ellipsis { label } => *label,
DimMatcher::Expr { label, .. } => *label,
}
}
pub const fn with_label(self, label: Option<&'a str>) -> Self {
match self {
DimMatcher::Any { .. } => DimMatcher::Any { label },
DimMatcher::Ellipsis { .. } => DimMatcher::Ellipsis { label },
DimMatcher::Expr { expr, .. } => DimMatcher::Expr { label, expr },
}
}
}
impl Display for DimMatcher<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
if let Some(label) = self.label() {
write!(f, "{label}=")?;
}
match self {
DimMatcher::Any { label: _ } => write!(f, "_"),
DimMatcher::Ellipsis { label: _ } => write!(f, "..."),
DimMatcher::Expr { label: _, expr } => write!(f, "{expr}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShapeContract<'a> {
pub terms: &'a [DimMatcher<'a>],
pub ellipsis_pos: Option<usize>,
}
impl Display for ShapeContract<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "[")?;
for (idx, expr) in self.terms.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
write!(f, "{expr}")?;
}
write!(f, "]")
}
}
impl<'a> ShapeContract<'a> {
pub const fn new(terms: &'a [DimMatcher<'a>]) -> Self {
let mut i = 0;
let mut ellipsis_pos: Option<usize> = None;
while i < terms.len() {
if matches!(terms[i], DimMatcher::Ellipsis { label: _ }) {
match ellipsis_pos {
Some(_) => panic!("Multiple ellipses in pattern"),
None => ellipsis_pos = Some(i),
}
}
i += 1;
}
ShapeContract {
terms,
ellipsis_pos,
}
}
#[inline(always)]
pub fn assert_shape<S>(&'a self, shape: S, env: StackEnvironment<'a>)
where
S: ShapeArgument,
{
let result = self.try_assert_shape(shape, env);
if result.is_err() {
panic!("{}", result.unwrap_err());
}
}
#[inline(always)]
pub fn try_assert_shape<S>(&'a self, shape: S, env: StackEnvironment<'a>) -> Result<(), String>
where
S: ShapeArgument,
{
let mut mut_env = MutableStackEnvironment::new(env);
self.try_resolve_match(shape, &mut mut_env)
}
#[must_use]
#[inline(always)]
pub fn unpack_shape<S, const K: usize>(
&'a self,
shape: S,
keys: &[&'a str; K],
env: StackEnvironment<'a>,
) -> [usize; K]
where
S: ShapeArgument,
{
match self.try_unpack_shape(shape, keys, env) {
Ok(values) => values,
Err(msg) => panic!("{msg}"),
}
}
#[must_use]
#[inline(always)]
pub fn try_unpack_shape<S, const K: usize>(
&'a self,
shape: S,
keys: &[&'a str; K],
env: StackEnvironment<'a>,
) -> Result<[usize; K], String>
where
S: ShapeArgument,
{
let mut mut_env = MutableStackEnvironment::new(env);
self.try_resolve_match(shape, &mut mut_env)?;
Ok(mut_env.export_key_values(keys))
}
#[must_use]
#[inline(always)]
fn try_resolve_match<S>(
&'a self,
shape: S,
env: &mut MutableStackEnvironment<'a>,
) -> Result<(), String>
where
S: ShapeArgument,
{
let shape = &shape.get_shape_vec();
let fail = |msg| -> String {
format!(
"Shape Error:: {msg}\n shape:\n {shape:?}\n expected:\n {self}\n {{{}}}",
env.backing
.iter()
.map(|(k, v)| format!("\"{k}\": {v}"))
.collect::<Vec<_>>()
.join(", ")
)
};
let fail_at = |shape_idx, term_idx, msg| -> String {
fail(format!(
"{} !~ {} :: {msg}",
shape[shape_idx], self.terms[term_idx]
))
};
let rank = shape.len();
let (e_start, e_size) = match self.try_ellipsis_split(rank) {
Ok((e_start, e_size)) => (e_start, e_size),
Err(msg) => return Err(fail(msg)),
};
for (shape_idx, &dim_size) in shape.iter().enumerate() {
let term_idx = if shape_idx < e_start {
shape_idx
} else if shape_idx < (e_start + e_size) {
continue;
} else {
shape_idx + 1 - e_size
};
let matcher = &self.terms[term_idx];
if let Some(label) = matcher.label() {
match env.lookup(label) {
Some(value) => {
if value != dim_size {
return Err(fail_at(
shape_idx,
term_idx,
"Value MissMatch.".to_string(),
));
}
}
None => {
env.bind(label, dim_size);
}
}
}
let expr = match matcher {
DimMatcher::Any { label: _ } => continue,
DimMatcher::Ellipsis { label: _ } => {
unreachable!("Ellipsis should have been handled before")
}
DimMatcher::Expr { label: _, expr } => expr,
};
match expr.try_match(dim_size as isize, env) {
Ok(TryMatchResult::Match) => continue,
Ok(TryMatchResult::Conflict) => {
return Err(fail_at(shape_idx, term_idx, "Value MissMatch.".to_string()));
}
Ok(TryMatchResult::ParamConstraint(param_name, value)) => {
env.bind(param_name, value as usize);
}
Err(msg) => return Err(fail_at(shape_idx, term_idx, msg)),
}
}
Ok(())
}
#[inline(always)]
#[must_use]
fn try_ellipsis_split(&self, rank: usize) -> Result<(usize, usize), String> {
let k = self.terms.len();
match self.ellipsis_pos {
None => {
if rank != k {
Err(format!("Shape rank {rank} != pattern dim count {k}",))
} else {
Ok((k, 0))
}
}
Some(pos) => {
let non_ellipsis_terms = k - 1;
if rank < non_ellipsis_terms {
return Err(format!(
"Shape rank {rank} < non-ellipsis pattern term count {non_ellipsis_terms}",
));
}
Ok((pos, rank - non_ellipsis_terms))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::{DimMatcher, ShapeContract};
use alloc::string::ToString;
use bimm_contracts_macros::shape_contract;
use indoc::indoc;
#[test]
fn test_dim_matcher_builders() {
assert_eq!(DimMatcher::any(), DimMatcher::Any { label: None });
assert_eq!(DimMatcher::ellipsis(), DimMatcher::Ellipsis { label: None });
assert_eq!(
DimMatcher::expr(DimExpr::Param("a")),
DimMatcher::Expr {
label: None,
expr: DimExpr::Param("a")
}
);
}
#[test]
fn test_with_label() {
assert_eq!(
DimMatcher::any().with_label(Some("abc")),
DimMatcher::Any { label: Some("abc") }
);
assert_eq!(
DimMatcher::ellipsis().with_label(Some("abc")),
DimMatcher::Ellipsis { label: Some("abc") }
);
assert_eq!(
DimMatcher::expr(DimExpr::Param("a")).with_label(Some("abc")),
DimMatcher::Expr {
label: Some("abc"),
expr: DimExpr::Param("a")
}
);
}
#[should_panic(expected = "Multiple ellipses in pattern")]
#[test]
fn test_bad_new() {
let _ = ShapeContract::new(&[
DimMatcher::any(),
DimMatcher::ellipsis(),
DimMatcher::ellipsis(),
]);
}
#[test]
fn test_shape_contract_macro() {
use crate as bimm_contracts;
static CONTRACT: ShapeContract = shape_contract![
...,
"height" = "hwins" * "window",
"width" = "wwins" * "window",
"color",
];
let hwins = 2;
let wwins = 3;
let window = 4;
let color = 3;
let shape = [1, 2, 3, hwins * window, wwins * window, color];
let [u_hwins, u_wwins, u_height] = CONTRACT.unpack_shape(
&shape,
&["hwins", "wwins", "height"],
&[
("height", hwins * window),
("window", window),
("color", color),
],
);
assert_eq!(u_hwins, hwins);
assert_eq!(u_wwins, wwins);
assert_eq!(u_height, hwins * window);
assert_eq!(
CONTRACT
.try_unpack_shape(
&shape,
&["hwins", "wwins"],
&[("window", window + 1), ("color", color),]
)
.unwrap_err(),
indoc! {r#"
Shape Error:: 8 !~ height=(hwins*window) :: No integer solution.
shape:
[1, 2, 3, 8, 12, 3]
expected:
[..., height=(hwins*window), width=(wwins*window), color]
{"window": 5, "color": 3}"#
},
);
assert_eq!(
CONTRACT
.try_unpack_shape(
&shape,
&["hwins", "wwins"],
&[("height", 1), ("window", window), ("color", color),]
)
.unwrap_err(),
indoc! {r#"
Shape Error:: 8 !~ height=(hwins*window) :: Value MissMatch.
shape:
[1, 2, 3, 8, 12, 3]
expected:
[..., height=(hwins*window), width=(wwins*window), color]
{"height": 1, "window": 4, "color": 3}"#
},
);
}
#[test]
fn test_check_ellipsis_split() {
{
static PATTERN: ShapeContract = ShapeContract::new(&[
DimMatcher::any(),
DimMatcher::ellipsis(),
DimMatcher::expr(DimExpr::Param("b")),
]);
assert_eq!(PATTERN.try_ellipsis_split(2), Ok((1, 0)));
assert_eq!(PATTERN.try_ellipsis_split(3), Ok((1, 1)));
assert_eq!(PATTERN.try_ellipsis_split(4), Ok((1, 2)));
assert_eq!(
PATTERN.try_ellipsis_split(1),
Err("Shape rank 1 < non-ellipsis pattern term count 2".to_string())
);
}
{
static PATTERN: ShapeContract =
ShapeContract::new(&[DimMatcher::any(), DimMatcher::expr(DimExpr::Param("b"))]);
assert_eq!(PATTERN.try_ellipsis_split(2), Ok((2, 0)));
assert_eq!(
PATTERN.try_ellipsis_split(1),
Err("Shape rank 1 != pattern dim count 2".to_string())
);
}
}
#[test]
fn test_format_pattern() {
static PATTERN: ShapeContract = ShapeContract::new(&[
DimMatcher::any(),
DimMatcher::ellipsis(),
DimMatcher::expr(DimExpr::Param("b")),
DimMatcher::expr(DimExpr::Prod(&[
DimExpr::Param("h"),
DimExpr::Sum(&[DimExpr::Param("a"), DimExpr::Negate(&DimExpr::Param("b"))]),
])),
DimMatcher::expr(DimExpr::Pow(&DimExpr::Param("h"), 2)),
]);
assert_eq!(PATTERN.to_string(), "[_, ..., b, (h*(a+(-b))), (h)^2]");
}
#[test]
fn test_unpack_shape() {
static CONTRACT: ShapeContract = ShapeContract::new(&[
DimMatcher::any(),
DimMatcher::expr(DimExpr::Param("b")),
DimMatcher::ellipsis(),
DimMatcher::expr(DimExpr::Prod(&[DimExpr::Param("h"), DimExpr::Param("p")])),
DimMatcher::expr(DimExpr::Prod(&[DimExpr::Param("w"), DimExpr::Param("p")])),
DimMatcher::expr(DimExpr::Pow(&DimExpr::Param("z"), 3)),
DimMatcher::expr(DimExpr::Param("c")),
]);
let b = 2;
let h = 3;
let w = 2;
let p = 4;
let c = 5;
let z = 4;
let shape = [12, b, 1, 2, 3, h * p, w * p, z * z * z, c];
let env = [("p", p), ("c", c)];
CONTRACT.assert_shape(&shape, &env);
let [u_b, u_h, u_w, u_z] = CONTRACT.unpack_shape(&shape, &["b", "h", "w", "z"], &env);
assert_eq!(u_b, b);
assert_eq!(u_h, h);
assert_eq!(u_w, w);
assert_eq!(u_z, z);
}
#[should_panic(expected = "Shape Error:: 1 !~ a :: Value MissMatch.")]
#[test]
fn test_unpack_shape_panic() {
use crate as bimm_contracts;
static CONTRACT: ShapeContract = shape_contract!["a", "b", "c"];
let _ignore = CONTRACT.unpack_shape(&[1, 2, 3], &["a", "b", "c"], &[("a", 7)]);
}
#[should_panic(expected = "Shape rank 3 != pattern dim count 1")]
#[test]
fn test_shape_mismatch_no_ellipsis() {
static PATTERN: ShapeContract =
ShapeContract::new(&[DimMatcher::expr(DimExpr::Param("a"))]);
let shape = [1, 2, 3];
PATTERN.assert_shape(&shape, &[]);
}
#[should_panic(expected = "Shape rank 3 < non-ellipsis pattern term count 4")]
#[test]
fn test_shape_mismatch_with_ellipsis() {
static PATTERN: ShapeContract = ShapeContract::new(&[
DimMatcher::any(),
DimMatcher::any(),
DimMatcher::ellipsis(),
DimMatcher::expr(DimExpr::Param("b")),
DimMatcher::expr(DimExpr::Param("c")),
]);
let shape = [1, 2, 3];
PATTERN.assert_shape(&shape, &[]);
}
#[should_panic(expected = "Value MissMatch")]
#[test]
fn test_shape_mismatch_value() {
static PATTERN: ShapeContract = ShapeContract::new(&[
DimMatcher::expr(DimExpr::Param("a")),
DimMatcher::expr(DimExpr::Param("b")),
]);
let shape = [2, 3];
PATTERN.assert_shape(&shape, &[("a", 2), ("b", 4)]);
}
}