use crate::ast::{BooleanExpr, GeomStmt, MetaStmt};
use crate::parser::geom::GeometryStatement;
use crate::{BoxPair, ParseError};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub struct SourceLocation {
pub tuple_idx: usize,
pub stmt_idx: usize,
}
impl std::fmt::Display for SourceLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "tuple {} statement {}", self.tuple_idx, self.stmt_idx)
}
}
impl std::error::Error for SourceLocation {}
impl SourceLocation {
pub fn new(tuple_idx: usize, stmt_idx: usize) -> Self {
Self {
tuple_idx,
stmt_idx,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RegionEntry {
Accumulator {
boxes: Vec<BoxPair>,
sources: Vec<SourceLocation>,
},
Defined {
expr: BooleanExpr,
source: SourceLocation,
},
Anonymous {
box_pair: BoxPair,
source: SourceLocation,
},
}
impl RegionEntry {
pub fn sources(&self) -> Vec<&SourceLocation> {
match self {
RegionEntry::Accumulator { sources, .. } => sources.iter().collect(),
RegionEntry::Defined { source, .. } | RegionEntry::Anonymous { source, .. } => {
vec![source]
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RegionTable {
pub regions: BTreeMap<String, RegionEntry>,
}
impl RegionTable {
pub fn new() -> Self {
Self {
regions: BTreeMap::new(),
}
}
pub fn add_geometry(&mut self, stmt: &GeomStmt, offset: [i32; 3]) -> Result<(), ParseError> {
let source = SourceLocation::new(stmt.tuple_idx, stmt.stmt_idx);
match &stmt.statement {
GeometryStatement::Expression { region, expr } => {
self.add_defined_region(region.clone(), expr.clone(), source)?;
}
geom_stmt => {
let box_pair =
geom_stmt
.to_box_pair(offset)
.ok_or_else(|| ParseError::Internal {
message: "Geometry statement should produce a box pair".to_string(),
position: 0,
})?;
if let Some(region) = stmt.region() {
self.add_accumulator_box(region.to_string(), box_pair, source)?;
} else {
let key = stmt.anonymous_key();
self.add_anonymous_region(key, box_pair, source);
}
}
}
Ok(())
}
fn add_defined_region(
&mut self,
region: String,
expr: BooleanExpr,
source: SourceLocation,
) -> Result<(), ParseError> {
match self.regions.get(®ion) {
Some(RegionEntry::Accumulator { sources, .. }) => {
return Err(ParseError::MixedRegionMode(Box::new(
crate::MixedRegionModeError {
region,
accumulator_sources: sources.clone(),
defined_source: source,
},
)));
}
Some(RegionEntry::Defined {
source: existing_source,
..
}) => {
return Err(ParseError::DuplicateRegionDefinition(Box::new(
crate::DuplicateRegionDefinitionError {
region,
first_source: existing_source.clone(),
duplicate_source: source,
},
)));
}
Some(RegionEntry::Anonymous { .. }) => {
return Err(ParseError::Internal {
message: "Anonymous region with named key".to_string(),
position: 0,
});
}
None => {
self.regions
.insert(region, RegionEntry::Defined { expr, source });
}
}
Ok(())
}
fn add_accumulator_box(
&mut self,
region: String,
box_pair: BoxPair,
source: SourceLocation,
) -> Result<(), ParseError> {
match self.regions.get_mut(®ion) {
Some(RegionEntry::Accumulator { boxes, sources }) => {
boxes.push(box_pair);
sources.push(source);
}
Some(RegionEntry::Defined {
source: defined_source,
..
}) => {
return Err(ParseError::MixedRegionMode(Box::new(
crate::MixedRegionModeError {
region,
accumulator_sources: vec![source],
defined_source: defined_source.clone(),
},
)));
}
Some(RegionEntry::Anonymous { .. }) => {
return Err(ParseError::Internal {
message: "Anonymous region with named key".to_string(),
position: 0,
});
}
None => {
self.regions.insert(
region,
RegionEntry::Accumulator {
boxes: vec![box_pair],
sources: vec![source],
},
);
}
}
Ok(())
}
fn add_anonymous_region(&mut self, key: String, box_pair: BoxPair, source: SourceLocation) {
self.regions
.insert(key, RegionEntry::Anonymous { box_pair, source });
}
}
impl Default for RegionTable {
fn default() -> Self {
Self::new()
}
}
pub fn assemble_region_table(
geom_stmts: Vec<GeomStmt>,
_meta_stmts: Vec<MetaStmt>, units: &[([i32; 3], String)],
) -> Result<RegionTable, ParseError> {
let mut table = RegionTable::new();
for stmt in geom_stmts {
let offset = units
.get(stmt.tuple_idx)
.map(|(pos, _)| *pos)
.unwrap_or([0, 0, 0]);
table.add_geometry(&stmt, offset)?;
}
Ok(table)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::BooleanExpr;
use crate::parser::geom::GeometryStatement;
fn make_geom_stmt(tuple_idx: usize, stmt_idx: usize, statement: GeometryStatement) -> GeomStmt {
GeomStmt::new(tuple_idx, stmt_idx, statement)
}
fn make_rc(region: Option<String>, corners: ([i32; 3], [i32; 3])) -> GeometryStatement {
GeometryStatement::RelativeCoordinate { region, corners }
}
fn make_ac(region: Option<String>, corners: ([i32; 3], [i32; 3])) -> GeometryStatement {
GeometryStatement::AbsoluteCoordinate { region, corners }
}
fn make_expr(region: String, expr: BooleanExpr) -> GeometryStatement {
GeometryStatement::Expression { region, expr }
}
#[test]
fn test_accumulate_multiple_boxes() {
let mut table = RegionTable::new();
let stmt1 = make_geom_stmt(
0,
0,
make_rc(Some("test".to_string()), ([0, 0, 0], [1, 1, 1])),
);
table.add_geometry(&stmt1, [10, 20, 30]).unwrap();
let stmt2 = make_geom_stmt(
0,
1,
make_ac(Some("test".to_string()), ([5, 5, 5], [6, 6, 6])),
);
table.add_geometry(&stmt2, [0, 0, 0]).unwrap();
assert_eq!(table.regions.len(), 1);
match table.regions.get("test").unwrap() {
RegionEntry::Accumulator { boxes, sources } => {
assert_eq!(boxes.len(), 2);
assert_eq!(sources.len(), 2);
assert_eq!(boxes[0], ([10, 20, 30], [11, 21, 31]));
assert_eq!(boxes[1], ([5, 5, 5], [6, 6, 6]));
assert_eq!(sources[0], SourceLocation::new(0, 0));
assert_eq!(sources[1], SourceLocation::new(0, 1));
}
_ => panic!("Expected accumulator region"),
}
}
#[test]
fn test_mixed_region_mode_error() {
let mut table = RegionTable::new();
let stmt1 = make_geom_stmt(
0,
0,
make_rc(Some("test".to_string()), ([0, 0, 0], [1, 1, 1])),
);
table.add_geometry(&stmt1, [0, 0, 0]).unwrap();
let expr = BooleanExpr::RegionRef("other".to_string());
let stmt2 = make_geom_stmt(1, 0, make_expr("test".to_string(), expr));
let result = table.add_geometry(&stmt2, [0, 0, 0]);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::MixedRegionMode(err) => {
assert_eq!(err.region, "test");
assert_eq!(err.accumulator_sources.len(), 1);
assert_eq!(err.accumulator_sources[0], SourceLocation::new(0, 0));
assert_eq!(err.defined_source, SourceLocation::new(1, 0));
}
_ => panic!("Expected MixedRegionMode error"),
}
}
#[test]
fn test_mixed_region_mode_reverse_order() {
let mut table = RegionTable::new();
let expr = BooleanExpr::RegionRef("other".to_string());
let stmt1 = make_geom_stmt(0, 0, make_expr("test".to_string(), expr));
table.add_geometry(&stmt1, [0, 0, 0]).unwrap();
let stmt2 = make_geom_stmt(
1,
0,
make_rc(Some("test".to_string()), ([0, 0, 0], [1, 1, 1])),
);
let result = table.add_geometry(&stmt2, [0, 0, 0]);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::MixedRegionMode(err) => {
assert_eq!(err.region, "test");
assert_eq!(err.accumulator_sources.len(), 1);
assert_eq!(err.accumulator_sources[0], SourceLocation::new(1, 0));
assert_eq!(err.defined_source, SourceLocation::new(0, 0));
}
_ => panic!("Expected MixedRegionMode error"),
}
}
#[test]
fn test_anonymous_region_stability() {
let mut table = RegionTable::new();
let stmt1 = make_geom_stmt(0, 0, make_rc(None, ([0, 0, 0], [1, 1, 1])));
let stmt2 = make_geom_stmt(0, 1, make_ac(None, ([5, 5, 5], [6, 6, 6])));
let stmt3 = make_geom_stmt(1, 0, make_rc(None, ([10, 10, 10], [11, 11, 11])));
table.add_geometry(&stmt1, [0, 0, 0]).unwrap();
table.add_geometry(&stmt2, [0, 0, 0]).unwrap();
table.add_geometry(&stmt3, [0, 0, 0]).unwrap();
assert_eq!(table.regions.len(), 3);
let keys: Vec<_> = table.regions.keys().collect();
assert!(keys.contains(&&"__anon_0_0".to_string()));
assert!(keys.contains(&&"__anon_0_1".to_string()));
assert!(keys.contains(&&"__anon_1_0".to_string()));
match table.regions.get("__anon_0_0").unwrap() {
RegionEntry::Anonymous { box_pair, source } => {
assert_eq!(*box_pair, ([0, 0, 0], [1, 1, 1]));
assert_eq!(*source, SourceLocation::new(0, 0));
}
_ => panic!("Expected anonymous region"),
}
match table.regions.get("__anon_0_1").unwrap() {
RegionEntry::Anonymous { box_pair, source } => {
assert_eq!(*box_pair, ([5, 5, 5], [6, 6, 6]));
assert_eq!(*source, SourceLocation::new(0, 1));
}
_ => panic!("Expected anonymous region"),
}
match table.regions.get("__anon_1_0").unwrap() {
RegionEntry::Anonymous { box_pair, source } => {
assert_eq!(*box_pair, ([10, 10, 10], [11, 11, 11]));
assert_eq!(*source, SourceLocation::new(1, 0));
}
_ => panic!("Expected anonymous region"),
}
}
#[test]
fn test_assemble_region_table() {
let units = vec![
([10, 20, 30], "@test=rc([0,0,0],[1,1,1])".to_string()),
([5, 10, 15], "@ac([0,0,0],[2,2,2])".to_string()),
];
let geom_stmts = vec![
make_geom_stmt(
0,
0,
make_rc(Some("test".to_string()), ([0, 0, 0], [1, 1, 1])),
),
make_geom_stmt(1, 0, make_ac(None, ([0, 0, 0], [2, 2, 2]))),
];
let table = assemble_region_table(geom_stmts, vec![], &units).unwrap();
assert_eq!(table.regions.len(), 2);
match table.regions.get("test").unwrap() {
RegionEntry::Accumulator { boxes, .. } => {
assert_eq!(boxes.len(), 1);
assert_eq!(boxes[0], ([10, 20, 30], [11, 21, 31]));
}
_ => panic!("Expected accumulator region"),
}
match table.regions.get("__anon_1_0").unwrap() {
RegionEntry::Anonymous { box_pair, .. } => {
assert_eq!(*box_pair, ([0, 0, 0], [2, 2, 2]));
}
_ => panic!("Expected anonymous region"),
}
}
#[test]
fn test_duplicate_region_definition() {
let mut table = RegionTable::new();
let expr1 = BooleanExpr::RegionRef("other1".to_string());
let stmt1 = make_geom_stmt(0, 0, make_expr("test".to_string(), expr1));
table.add_geometry(&stmt1, [0, 0, 0]).unwrap();
let expr2 = BooleanExpr::RegionRef("other2".to_string());
let stmt2 = make_geom_stmt(1, 0, make_expr("test".to_string(), expr2));
let result = table.add_geometry(&stmt2, [0, 0, 0]);
assert!(result.is_err());
match result.unwrap_err() {
ParseError::DuplicateRegionDefinition(err) => {
assert_eq!(err.region, "test");
assert_eq!(err.first_source, SourceLocation::new(0, 0));
assert_eq!(err.duplicate_source, SourceLocation::new(1, 0));
}
_ => panic!("Expected DuplicateRegionDefinition error"),
}
}
}