use std::sync::Arc;
use sim_kernel::{Cx, DefaultFactory, Expr, NoopEvalPolicy, ShapeMatch, Symbol};
use crate::kinds::{KIND_KEY, is_known_kind};
const HEATMAP_KIND: &str = "heatmap";
pub const HEATMAP_PALETTES: &[&str] = &["viridis", "blue-red", "cyclic-phase"];
pub const HEATMAP_BYTES_PER_CELL: u64 =
core::mem::size_of::<f64>() as u64 + core::mem::size_of::<bool>() as u64;
pub fn heatmap_payload_bytes(
cells: u64,
label: &str,
detector: &str,
advisory: Option<&str>,
) -> Option<u64> {
let cell_bytes = cells.checked_mul(HEATMAP_BYTES_PER_CELL)?;
[Some(label), Some(detector), advisory]
.into_iter()
.flatten()
.try_fold(cell_bytes, |total, text| {
total.checked_add(u64::try_from(text.len()).ok()?)
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SceneBudget {
pub nodes: usize,
pub depth: usize,
pub encoded_bytes: usize,
pub face_bytes: usize,
}
impl SceneBudget {
pub const fn new(nodes: usize, depth: usize, encoded_bytes: usize, face_bytes: usize) -> Self {
Self {
nodes,
depth,
encoded_bytes,
face_bytes,
}
}
pub const fn interactive() -> Self {
Self::new(512, 32, 256 * 1024, 8 * 1024)
}
pub const fn compact() -> Self {
Self::new(64, 12, 32 * 1024, 1024)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SceneBudgetState {
budget: SceneBudget,
nodes_used: usize,
encoded_bytes_used: usize,
}
impl SceneBudgetState {
pub fn new(budget: SceneBudget) -> Self {
Self {
budget,
nodes_used: 0,
encoded_bytes_used: 0,
}
}
pub fn budget(&self) -> &SceneBudget {
&self.budget
}
pub fn nodes_used(&self) -> usize {
self.nodes_used
}
pub fn encoded_bytes_used(&self) -> usize {
self.encoded_bytes_used
}
pub fn admit(
&mut self,
depth: usize,
face: Option<&str>,
encoded_bytes: usize,
) -> Result<(), SceneBudgetExhausted> {
if self.nodes_used >= self.budget.nodes {
return Err(SceneBudgetExhausted::Nodes {
limit: self.budget.nodes,
});
}
if depth > self.budget.depth {
return Err(SceneBudgetExhausted::Depth {
limit: self.budget.depth,
});
}
if let Some(face) = face
&& face.len() > self.budget.face_bytes
{
return Err(SceneBudgetExhausted::FaceBytes {
limit: self.budget.face_bytes,
});
}
if self.encoded_bytes_used.saturating_add(encoded_bytes) > self.budget.encoded_bytes {
return Err(SceneBudgetExhausted::EncodedBytes {
limit: self.budget.encoded_bytes,
});
}
self.nodes_used += 1;
self.encoded_bytes_used = self.encoded_bytes_used.saturating_add(encoded_bytes);
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SceneBudgetExhausted {
Nodes {
limit: usize,
},
Depth {
limit: usize,
},
EncodedBytes {
limit: usize,
},
FaceBytes {
limit: usize,
},
}
impl SceneBudgetExhausted {
pub fn reason(&self) -> &'static str {
match self {
Self::Nodes { .. } => "nodes",
Self::Depth { .. } => "depth",
Self::EncodedBytes { .. } => "encoded-bytes",
Self::FaceBytes { .. } => "face-bytes",
}
}
pub fn limit(&self) -> usize {
match self {
Self::Nodes { limit }
| Self::Depth { limit }
| Self::EncodedBytes { limit }
| Self::FaceBytes { limit } => *limit,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SceneError {
pub path: Vec<String>,
pub message: String,
}
impl SceneError {
fn at(path: &[String], message: impl Into<String>) -> Self {
Self {
path: path.to_vec(),
message: message.into(),
}
}
pub fn path_string(&self) -> String {
if self.path.is_empty() {
"<root>".to_owned()
} else {
self.path.join("")
}
}
}
impl core::fmt::Display for SceneError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}: {}", self.path_string(), self.message)
}
}
pub use sim_value::build::map;
pub fn node(kind_name: &str, entries: Vec<(&str, Expr)>) -> Expr {
let mut pairs = Vec::with_capacity(entries.len() + 1);
pairs.push((
Expr::Symbol(Symbol::new(KIND_KEY)),
Expr::Symbol(Symbol::qualified(crate::kinds::SCENE_NAMESPACE, kind_name)),
));
for (key, value) in entries {
pairs.push((Expr::Symbol(Symbol::new(key)), value));
}
Expr::Map(pairs)
}
pub fn node_kind(expr: &Expr) -> Option<Symbol> {
sim_value::access::field_sym(expr, KIND_KEY)
}
fn kind_entry(map: &Expr) -> Option<&Expr> {
sim_value::access::field(map, KIND_KEY)
}
fn has_kind_key(map: &Expr) -> bool {
kind_entry(map).is_some()
}
pub fn validate_scene(expr: &Expr) -> Result<(), SceneError> {
let mut path = Vec::new();
validate_node(expr, &mut path)
}
fn validate_node(expr: &Expr, path: &mut Vec<String>) -> Result<(), SceneError> {
let shape_error = check_scene_shape(expr, path)?;
let Expr::Map(entries) = expr else {
return Err(SceneError::at(
path,
"expected a scene node map (an Expr::Map tagged with a kind)",
));
};
match kind_entry(expr) {
None => {
return Err(SceneError::at(path, "scene node is missing a 'kind' tag"));
}
Some(Expr::Symbol(kind)) => {
if !is_known_kind(kind) {
return Err(SceneError::at(
path,
format!(
"unrecognized scene kind '{kind}' -- if this is a plain data map, \
rename its 'kind' field (scene node maps reserve 'kind')"
),
));
}
}
Some(_) => {
return Err(SceneError::at(path, "scene node 'kind' must be a symbol"));
}
}
if let Some(message) = shape_error {
return Err(SceneError::at(path, message));
}
if matches!(
node_kind(expr),
Some(kind)
if kind.namespace.as_deref() == Some(crate::kinds::SCENE_NAMESPACE)
&& &*kind.name == HEATMAP_KIND
) {
validate_heatmap(expr, path)?;
}
validate_children(entries, path)
}
fn validate_heatmap(expr: &Expr, path: &[String]) -> Result<(), SceneError> {
let rows = heatmap_u64(expr, "rows", path)?;
let cols = heatmap_u64(expr, "cols", path)?;
if rows == 0 || cols == 0 {
return Err(SceneError::at(
path,
"scene/heatmap rows and cols must be non-zero",
));
}
let cells = rows.checked_mul(cols).ok_or_else(|| {
SceneError::at(path, "scene/heatmap rows * cols overflows the cell count")
})?;
let cell_count = usize::try_from(cells).map_err(|_| {
SceneError::at(
path,
"scene/heatmap cell count cannot be represented on this host",
)
})?;
let values = heatmap_list(expr, "values", path)?;
let valid = heatmap_list(expr, "valid", path)?;
if values.len() != cell_count {
return Err(SceneError::at(
path,
format!(
"scene/heatmap rows * cols is {cells}, but values has {} entries",
values.len()
),
));
}
if valid.len() != cell_count {
return Err(SceneError::at(
path,
format!(
"scene/heatmap rows * cols is {cells}, but valid has {} entries",
valid.len()
),
));
}
for (index, value) in values.iter().enumerate() {
let Some(value) = sim_value::access::as_f64(value) else {
return Err(SceneError::at(
path,
format!("scene/heatmap values[{index}] must be a number"),
));
};
if !value.is_finite() {
return Err(SceneError::at(
path,
format!("scene/heatmap values[{index}] must be finite"),
));
}
}
if let Some(index) = valid
.iter()
.position(|value| !matches!(value, Expr::Bool(_)))
{
return Err(SceneError::at(
path,
format!("scene/heatmap valid[{index}] must be a bool"),
));
}
let min = heatmap_f64(expr, "min", path)?;
let max = heatmap_f64(expr, "max", path)?;
if !min.is_finite() || !max.is_finite() || min > max {
return Err(SceneError::at(
path,
"scene/heatmap range must be finite with min <= max",
));
}
let palette = sim_value::access::field_sym(expr, "palette")
.filter(|palette| palette.namespace.is_none())
.ok_or_else(|| {
SceneError::at(path, "scene/heatmap palette must be an unqualified symbol")
})?;
if !HEATMAP_PALETTES.contains(&palette.name.as_ref()) {
return Err(SceneError::at(
path,
format!("scene/heatmap palette '{}' is not recognized", palette.name),
));
}
let label = heatmap_nonempty_text(expr, "label", path)?;
let detector = heatmap_nonempty_text(expr, "detector", path)?;
let advisory = sim_value::access::field(expr, "advisory")
.map(|_| heatmap_nonempty_text(expr, "advisory", path))
.transpose()?;
let footprint = sim_value::access::field(expr, "footprint")
.ok_or_else(|| SceneError::at(path, "scene/heatmap footprint is required"))?;
let footprint_cells = heatmap_u64(footprint, "cells", path)?;
if footprint_cells != cells {
return Err(SceneError::at(
path,
format!("scene/heatmap footprint cells is {footprint_cells}, expected {cells}"),
));
}
let payload_bytes = heatmap_payload_bytes(cells, label, detector, advisory)
.ok_or_else(|| SceneError::at(path, "scene/heatmap byte footprint overflowed"))?;
let footprint_bytes = heatmap_u64(footprint, "bytes", path)?;
if footprint_bytes != payload_bytes {
return Err(SceneError::at(
path,
format!("scene/heatmap footprint bytes is {footprint_bytes}, expected {payload_bytes}"),
));
}
Ok(())
}
fn heatmap_list<'a>(expr: &'a Expr, name: &str, path: &[String]) -> Result<&'a [Expr], SceneError> {
match sim_value::access::field(expr, name) {
Some(Expr::List(items)) => Ok(items),
_ => Err(SceneError::at(
path,
format!("scene/heatmap {name} must be a list"),
)),
}
}
fn heatmap_u64(expr: &Expr, name: &str, path: &[String]) -> Result<u64, SceneError> {
sim_value::access::field(expr, name)
.and_then(|value| match value {
Expr::Number(number)
if matches!(number.domain.name.as_ref(), "i64" | "u64")
&& number.domain.namespace.is_none() =>
{
number.canonical.parse::<u64>().ok()
}
_ => None,
})
.ok_or_else(|| {
SceneError::at(
path,
format!("scene/heatmap {name} must be a non-negative integer number"),
)
})
}
fn heatmap_f64(expr: &Expr, name: &str, path: &[String]) -> Result<f64, SceneError> {
sim_value::access::field(expr, name)
.and_then(sim_value::access::as_f64)
.ok_or_else(|| SceneError::at(path, format!("scene/heatmap {name} must be a number")))
}
fn heatmap_nonempty_text<'a>(
expr: &'a Expr,
name: &str,
path: &[String],
) -> Result<&'a str, SceneError> {
let text = sim_value::access::field_str(expr, name)
.ok_or_else(|| SceneError::at(path, format!("scene/heatmap {name} must be a string")))?;
if text.trim().is_empty() {
return Err(SceneError::at(
path,
format!("scene/heatmap {name} must not be empty"),
));
}
Ok(text)
}
fn check_scene_shape(expr: &Expr, path: &[String]) -> Result<Option<String>, SceneError> {
let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
let matched = crate::shapes::scene_shape()
.check_expr(&mut cx, expr)
.map_err(|error| SceneError::at(path, format!("scene shape check failed: {error}")))?;
Ok((!matched.accepted)
.then(|| rejection_message(&matched, "value is not a recognized scene node")))
}
fn rejection_message(matched: &ShapeMatch, fallback: &str) -> String {
matched
.diagnostics
.first()
.map(|diagnostic| diagnostic.message.clone())
.unwrap_or_else(|| fallback.to_owned())
}
fn validate_children(entries: &[(Expr, Expr)], path: &mut Vec<String>) -> Result<(), SceneError> {
for (key, value) in entries {
let label = match key {
Expr::Symbol(symbol) => format!(".{}", symbol.as_qualified_str()),
other => format!(".{other:?}"),
};
path.push(label);
validate_data(value, path)?;
path.pop();
}
Ok(())
}
fn validate_data(expr: &Expr, path: &mut Vec<String>) -> Result<(), SceneError> {
match expr {
Expr::Map(_) if has_kind_key(expr) => validate_node(expr, path),
Expr::Map(entries) => validate_children(entries, path),
Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
for (index, item) in items.iter().enumerate() {
path.push(format!("[{index}]"));
validate_data(item, path)?;
path.pop();
}
Ok(())
}
_ => Ok(()),
}
}