diff --git a/crates/doc/src/config_clone.rs b/crates/doc/src/config_clone.rs index efb174c44d..c85eed89e4 100644 --- a/crates/doc/src/config_clone.rs +++ b/crates/doc/src/config_clone.rs @@ -1,7 +1,7 @@ //! Configuration graph remapping for independent component copies. use super::{Column, ConfigTable}; use crate::{DocError, Result}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; #[derive(Clone, Debug, Default)] pub struct ConfigCloneMaps { @@ -102,6 +102,9 @@ impl ConfigTable { } } } + // The ids taken so far, looked up rather than scanned: copies of rules whose ids share + // their first 40 characters try one id after another. + let mut taken: BTreeSet = next.rules.iter().map(|rule| rule.id.clone()).collect(); for rule in &self.rules { if let Some(mut copied) = rule.clone_for_targets(maps) { if next.rules.len() >= super::MAX_RULES { @@ -110,8 +113,9 @@ impl ConfigTable { let stem: String = rule.id.chars().take(40).collect(); copied.id = (1..=super::MAX_RULES + 1) .map(|i| format!("{stem}_copy{i}")) - .find(|id| !next.rules.iter().any(|rule| rule.id == *id)) + .find(|id| !taken.contains(id)) .ok_or_else(|| DocError::Invalid("no configuration rule ID available".into()))?; + taken.insert(copied.id.clone()); copied.name = format!("{} (copy)", rule.name.chars().take(120).collect::()); next.rules.push(copied); } diff --git a/crates/doc/src/config_rules.rs b/crates/doc/src/config_rules.rs index 87dea95f3c..1aa1f78cc4 100644 --- a/crates/doc/src/config_rules.rs +++ b/crates/doc/src/config_rules.rs @@ -6,7 +6,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::{BTreeMap, BTreeSet}; -pub const MAX_RULES: usize = 128; +/// Most configuration rules a design keeps. A rule drives one feature's suppression, so an +/// assembly that switches a few hundred parts on and off, by two rules each, needs hundreds. +pub const MAX_RULES: usize = 2048; const MAX_NODES: usize = 64; const MAX_DEPTH: usize = 8; @@ -626,7 +628,16 @@ mod tests { .unwrap(); assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":[valid.clone()]}})).is_ok()); assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":null}})).is_err()); - assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":vec![valid.clone();129]}})).is_err()); + // An assembly that switches a few hundred parts on and off, by two rules each, keeps them. + let many: Vec = (0..600) + .map(|i| { + let mut r = valid.clone(); + r["id"] = serde_json::json!(format!("r{i}")); + r + }) + .collect(); + assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":many}})).is_ok()); + assert!(validate_rules_json(&serde_json::json!({"configs":{"rules":vec![valid.clone();MAX_RULES + 1]}})).is_err()); let mut condition = serde_json::json!({"type":"always"}); for _ in 0..8 { condition = serde_json::json!({"type":"not","condition":condition}); @@ -690,7 +701,7 @@ mod tests { Value::Null, serde_json::json!([deep]), serde_json::json!([wide]), - serde_json::json!(vec![valid; 129]), + serde_json::json!(vec![valid; MAX_RULES + 1]), serde_json::json!([invalid_context]), ] { let mut saved = serde_json::to_value(design()).unwrap(); diff --git a/docs/configurations.md b/docs/configurations.md index 11e43c7110..12c46bfc9f 100644 --- a/docs/configurations.md +++ b/docs/configurations.md @@ -147,7 +147,7 @@ or restrictions leave the previous document intact and explain the failure. Dele before removing the last row. Parameter renames update rule targets and expressions; rule references prevent parameter deletion until removed. -Tables support at most 128 rules; conditions allow eight levels and 64 nodes, and allowed +Tables support at most 2048 rules; conditions allow eight levels and 64 nodes, and allowed value lists support up to 128 expressions. These bounds are checked before saved rules are decoded. Driven appearance/material rules and repeated fixed-point evaluation are outside the current rule scope.