use henad_compute::entry::{ModelEntry, ModelSet};
use henad_compute::simulation::RunSetup;
use henad_core::action::Schedule;
use henad_core::explore::spec::ACTION_COLUMN_PREFIX;
use henad_core::metadata::{Backend, Structure};
use henad_core::topology::TopologyHint;
use super::{declared_defaults, error_text};
pub(super) fn model_id(entry: &ModelEntry) -> Result<(), String> {
ModelSet::new(crate::ENGINE_BUILD)
.insert(entry.clone())
.map(|_| ())
.map_err(|error| format!("The set refuses the entry: {error}."))
}
pub(super) fn param_ids(entry: &ModelEntry) -> Result<(), String> {
let ids = entry.param_descriptors().iter().map(|descriptor| descriptor.id);
let unnamed = unnameable(ids, |id| {
id.starts_with(ACTION_COLUMN_PREFIX)
.then_some("starts with 'action.', the prefix of an action's tick")
});
let message = [
repeated_param_ids(entry),
unnamed.map(|reasons| named_ids("parameter", &reasons)),
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ");
if message.is_empty() { Ok(()) } else { Err(message) }
}
fn repeated_param_ids(entry: &ModelEntry) -> Option<String> {
let repeated = repeated(entry.param_descriptors().iter().map(|descriptor| descriptor.id));
if repeated.is_empty() {
return None;
}
let engine_ids = prepended_ids(&entry.metadata().structure);
let prepended: Vec<&str> = repeated.iter().copied().filter(|id| engine_ids.contains(id)).collect();
let mut message = declared_twice("Parameter id", "Parameter ids", &repeated);
match prepended.as_slice() {
[] => {}
[id] => message.push_str(&format!(
" The engine prepends '{id}', and a model does not declare it itself."
)),
_ => message.push_str(&format!(
" The engine prepends {}, and a model declares none of them itself.",
quoted(&prepended)
)),
}
Some(message)
}
fn prepended_ids(structure: &Structure) -> &'static [&'static str] {
match structure {
Structure::Grid { .. } => &["grid_width", "grid_height"],
Structure::Agents { .. } | Structure::Network { .. } => &["num_agents", "world_width", "world_height"],
Structure::GpuGrid { .. } | Structure::GpuAgents { .. } => &[],
}
}
pub(super) fn stat_labels(entry: &ModelEntry) -> Result<(), String> {
let repeated = repeated(entry.stat_descriptors().iter().map(|descriptor| descriptor.label));
if repeated.is_empty() {
return Ok(());
}
Err(declared_twice("Stat label", "Stat labels", &repeated))
}
pub(super) fn action_ids(entry: &ModelEntry) -> Result<(), String> {
let ids = || entry.action_descriptors().iter().map(|action| action.id);
let repeated = repeated(ids());
let repeated = (!repeated.is_empty()).then(|| declared_twice("Action id", "Action ids", &repeated));
let unnamed = unnameable(ids(), |_| None).map(|reasons| named_ids("action", &reasons));
let message = [repeated, unnamed].into_iter().flatten().collect::<Vec<_>>().join(" ");
if message.is_empty() { Ok(()) } else { Err(message) }
}
fn unnameable<'a>(
ids: impl Iterator<Item = &'a str>,
extra: impl Fn(&str) -> Option<&'static str>,
) -> Option<Vec<(&'a str, &'static str)>> {
let refused: Vec<(&str, &str)> = ids
.filter_map(|id| {
let reason = if id.is_empty() {
Some("is empty")
} else if id.contains(char::is_whitespace) {
Some("holds whitespace")
} else if id.contains('=') {
Some("holds '='")
} else {
extra(id)
};
reason.map(|reason| (id, reason))
})
.collect();
(!refused.is_empty()).then_some(refused)
}
fn named_ids(kind: &str, refused: &[(&str, &str)]) -> String {
let mut message: String = refused
.iter()
.map(|(id, reason)| format!("The {kind} id '{id}' {reason}. "))
.collect();
message.push_str(match refused {
[_] => "The command line and a design table cannot name it.",
_ => "The command line and a design table cannot name them.",
});
message
}
pub(super) fn palette(entry: &ModelEntry) -> Result<(), String> {
match entry.metadata().palette {
Some([]) => Err("The declared palette holds no colors.".to_owned()),
_ => Ok(()),
}
}
pub(super) fn metadata(entry: &ModelEntry) -> Result<(), String> {
let metadata = entry.metadata();
let backend = metadata.backend;
let gpu = backend == Backend::Gpu;
let mut problems = Vec::new();
match entry.demand(&declared_defaults(entry), &wgpu::Limits::default()) {
Some(_) if !gpu => problems.push(format!("declares {backend:?}, but carries a device capacity")),
Some(demand) if demand.bytes() == 0 => problems.push("declares a device demand of no bytes".to_owned()),
None if gpu => problems.push(format!("declares {backend:?}, but carries no device capacity")),
_ => {}
}
if gpu != entry.gpu_needs().is_some() {
problems.push(format!(
"declares {backend:?}, but {} device needs",
if gpu { "declares no" } else { "declares" }
));
}
if !gpu && !metadata.replays_exactly {
problems.push("declares that a CPU model does not replay exactly".to_owned());
}
let hint = entry.topology_hint();
let agrees = match &metadata.structure {
Structure::Grid { .. } | Structure::GpuGrid { .. } => hint == TopologyHint::GRID,
Structure::Agents { .. } | Structure::GpuAgents { .. } => hint.agents,
Structure::Network { .. } => hint.agents && hint.edges,
};
if !agrees {
problems.push(format!(
"declares a structure that the topology hint {hint:?} disagrees with"
));
}
if gpu
!= matches!(
metadata.structure,
Structure::GpuGrid { .. } | Structure::GpuAgents { .. }
)
{
problems.push(format!("declares {backend:?}, but a structure for the other backend"));
}
if problems.is_empty() {
Ok(())
} else {
Err(format!("The entry {}.", problems.join(", and it ")))
}
}
pub(super) fn default_setup(entry: &ModelEntry) -> Result<(), String> {
let setup = entry.setup();
let defaults = declared_defaults(entry);
if setup.values() != defaults.as_slice() {
return Err("The setup at the defaults holds other values than the declared defaults.".to_owned());
}
let checked = RunSetup::from_parts(entry, setup.values(), None, Schedule::default()).map_err(|error| {
format!(
"RunSetup::from_parts refuses the declared defaults: {}.",
error_text(&error)
)
})?;
if checked.values() != setup.values() {
return Err("RunSetup::from_parts changed the declared defaults.".to_owned());
}
Ok(())
}
pub(super) fn defaults_fit(entry: &ModelEntry) -> Result<(), String> {
let shortfalls = entry.shortfalls(&declared_defaults(entry), &wgpu::Limits::default());
if shortfalls.is_empty() {
return Ok(());
}
Err(format!(
"At the declared defaults, the model exceeds the WebGPU baseline: {}.",
shortfalls.join("; ")
))
}
fn repeated<'a>(items: impl Iterator<Item = &'a str>) -> Vec<&'a str> {
let mut seen = Vec::new();
let mut repeated = Vec::new();
for item in items {
if seen.contains(&item) {
if !repeated.contains(&item) {
repeated.push(item);
}
} else {
seen.push(item);
}
}
repeated
}
fn declared_twice(singular: &str, plural: &str, repeated: &[&str]) -> String {
match repeated {
[item] => format!("{singular} '{item}' is declared more than once."),
_ => format!("{plural} {} are declared more than once.", quoted(repeated)),
}
}
fn quoted(items: &[&str]) -> String {
items
.iter()
.map(|item| format!("'{item}'"))
.collect::<Vec<_>>()
.join(", ")
}