1use std::collections::HashMap;
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub struct Id(u32);
60
61impl Id {
62 fn index(self) -> usize {
63 self.0 as usize
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub struct NodeId(pub u32);
70
71#[derive(Clone, Debug, PartialEq, Eq, Hash)]
79pub enum Expr {
80 Num(u16),
82 Var(NodeId),
84 Add(Id, Id),
86 Sub(Id, Id),
88 Max(Id, Id),
90 Min(Id, Id),
92 Div(Id, Id),
94 Mul(Id, Id),
96 Clamp { min: Id, max: Id, val: Id },
98 HFlex(Vec<Id>),
100 VFlex(Vec<Id>),
102 Fill(Id),
104}
105
106#[derive(Debug)]
114pub struct EGraph {
115 parents: Vec<u32>,
117 ranks: Vec<u8>,
119 nodes: Vec<Vec<Expr>>,
121 memo: HashMap<Expr, Id>,
123 last_apply_count: usize,
125}
126
127impl EGraph {
128 pub fn new() -> Self {
130 Self {
131 parents: Vec::new(),
132 ranks: Vec::new(),
133 nodes: Vec::new(),
134 memo: HashMap::new(),
135 last_apply_count: 0,
136 }
137 }
138
139 pub fn add(&mut self, expr: Expr) -> Id {
144 let canonical = self.canonicalize(&expr);
145 if let Some(&id) = self.memo.get(&canonical) {
146 return self.find(id);
147 }
148
149 let id = Id(self.parents.len() as u32);
150 self.parents.push(id.0);
151 self.ranks.push(0);
152 self.nodes.push(vec![canonical.clone()]);
153 self.memo.insert(canonical, id);
154 id
155 }
156
157 pub fn equiv(&self, a: Id, b: Id) -> bool {
159 self.find(a) == self.find(b)
160 }
161
162 pub fn merge(&mut self, a: Id, b: Id) -> Id {
164 let a = self.find(a);
165 let b = self.find(b);
166 if a == b {
167 return a;
168 }
169
170 let (winner, loser) = if self.ranks[a.index()] >= self.ranks[b.index()] {
172 (a, b)
173 } else {
174 (b, a)
175 };
176
177 self.parents[loser.index()] = winner.0;
178 if self.ranks[winner.index()] == self.ranks[loser.index()] {
179 self.ranks[winner.index()] += 1;
180 }
181
182 let loser_nodes = std::mem::take(&mut self.nodes[loser.index()]);
184 self.nodes[winner.index()].extend(loser_nodes);
185
186 winner
187 }
188
189 pub fn find(&self, mut id: Id) -> Id {
191 while self.parents[id.index()] != id.0 {
192 id = Id(self.parents[id.index()]);
193 }
194 id
195 }
196
197 pub fn class_count(&self) -> usize {
199 (0..self.parents.len())
200 .filter(|&i| self.parents[i] == i as u32)
201 .count()
202 }
203
204 pub fn node_count(&self) -> usize {
206 self.nodes.iter().map(|n| n.len()).sum()
207 }
208
209 pub fn last_apply_count(&self) -> usize {
211 self.last_apply_count
212 }
213
214 pub fn apply_rules(&mut self) -> usize {
219 self.apply_rules_with_budget(100)
220 }
221
222 pub fn apply_rules_with_budget(&mut self, max_iterations: usize) -> usize {
224 let mut total = 0;
225 for _ in 0..max_iterations {
226 let applied = self.apply_rules_once();
227 total += applied;
228 self.last_apply_count = total;
229 if applied == 0 {
230 break;
231 }
232 }
233 total
234 }
235
236 fn apply_rules_once(&mut self) -> usize {
238 let mut merges: Vec<(Id, Id)> = Vec::new();
239 let mut new_nodes: Vec<(Id, Expr)> = Vec::new();
240
241 let class_count = self.parents.len();
242 for class_idx in 0..class_count {
243 let class_id = Id(class_idx as u32);
244 let canonical = self.find(class_id);
245 if canonical != class_id {
246 continue;
247 }
248
249 let nodes = self.nodes[class_idx].clone();
250 for expr in &nodes {
251 if let Some(target) = self.rewrite_simplify(expr)
253 && self.find(class_id) != self.find(target)
254 {
255 merges.push((class_id, target));
256 }
257
258 self.rewrite_construct(expr, class_id, &mut new_nodes);
260 }
261 }
262
263 for (class_id, expr) in new_nodes {
265 let new_id = self.add(expr);
266 if self.find(class_id) != self.find(new_id) {
267 merges.push((class_id, new_id));
268 }
269 }
270
271 merges.sort_by_key(|&(a, b)| (self.find(a).0, self.find(b).0));
273 merges
274 .dedup_by(|a, b| self.find(a.0) == self.find(b.0) && self.find(a.1) == self.find(b.1));
275
276 let count = merges
277 .iter()
278 .filter(|&&(a, b)| self.find(a) != self.find(b))
279 .count();
280
281 for (a, b) in merges {
282 self.merge(a, b);
283 }
284 count
285 }
286
287 fn rewrite_simplify(&self, expr: &Expr) -> Option<Id> {
289 match expr {
290 Expr::Add(a, b) => {
292 if self.is_zero(*b) {
293 return Some(*a);
294 }
295 if self.is_zero(*a) {
296 return Some(*b);
297 }
298 None
300 }
301
302 Expr::Sub(a, b) => {
304 if self.is_zero(*b) {
305 return Some(*a);
306 }
307 if self.find(*a) == self.find(*b) {
308 return self.find_num(0);
311 }
312 None
313 }
314
315 Expr::Mul(a, b) => {
317 if self.is_one(*b) {
318 return Some(*a);
319 }
320 if self.is_one(*a) {
321 return Some(*b);
322 }
323 if self.is_zero(*a) {
324 return Some(*a);
325 }
326 if self.is_zero(*b) {
327 return Some(*b);
328 }
329 None
330 }
331
332 Expr::Max(a, b) => {
334 if self.find(*a) == self.find(*b) {
335 return Some(*a);
336 }
337 if self.is_zero(*b) {
338 return Some(*a);
339 }
340 if self.is_zero(*a) {
341 return Some(*b);
342 }
343 None
344 }
345
346 Expr::Min(a, b) => {
348 if self.find(*a) == self.find(*b) {
349 return Some(*a);
350 }
351 None
352 }
353
354 Expr::Clamp { min, max, val } => {
356 if self.is_zero(*min) && self.is_max(*max) {
357 return Some(*val);
358 }
359 if self.find(*min) == self.find(*max) {
361 return Some(*min);
362 }
363 if self.find(*val) == self.find(*min) {
365 return Some(*min);
366 }
367 if self.find(*val) == self.find(*max) {
368 return Some(*max);
369 }
370 None
371 }
372
373 Expr::Div(a, b) => {
375 if self.is_one(*b) {
376 return Some(*a);
377 }
378 if self.find(*a) == self.find(*b) && !self.is_zero(*a) {
379 return self.find_num(1);
380 }
381 None
382 }
383
384 _ => None,
387 }
388 }
389
390 fn rewrite_construct(&self, expr: &Expr, _class_id: Id, out: &mut Vec<(Id, Expr)>) {
393 match expr {
394 Expr::Add(a, b) if self.find(*a) != self.find(*b) => {
396 out.push((_class_id, Expr::Add(*b, *a)));
397 }
398
399 Expr::Max(a, b) if self.find(*a) != self.find(*b) => {
401 out.push((_class_id, Expr::Max(*b, *a)));
402 }
403
404 Expr::Min(a, b) if self.find(*a) != self.find(*b) => {
406 out.push((_class_id, Expr::Min(*b, *a)));
407 }
408
409 Expr::Mul(a, b) if self.find(*a) != self.find(*b) => {
411 out.push((_class_id, Expr::Mul(*b, *a)));
412 }
413
414 _ => {}
415 }
416 }
417
418 fn find_num(&self, n: u16) -> Option<Id> {
420 self.memo.get(&Expr::Num(n)).map(|&id| self.find(id))
421 }
422
423 fn is_zero(&self, id: Id) -> bool {
425 let id = self.find(id);
426 self.nodes[id.index()]
427 .iter()
428 .any(|e| matches!(e, Expr::Num(0)))
429 }
430
431 fn is_one(&self, id: Id) -> bool {
433 let id = self.find(id);
434 self.nodes[id.index()]
435 .iter()
436 .any(|e| matches!(e, Expr::Num(1)))
437 }
438
439 fn is_max(&self, id: Id) -> bool {
441 let id = self.find(id);
442 self.nodes[id.index()]
443 .iter()
444 .any(|e| matches!(e, Expr::Num(u16::MAX)))
445 }
446
447 fn canonicalize(&self, expr: &Expr) -> Expr {
450 match expr {
451 Expr::Num(n) => Expr::Num(*n),
452 Expr::Var(v) => Expr::Var(*v),
453 Expr::Add(a, b) => Expr::Add(self.find(*a), self.find(*b)),
454 Expr::Sub(a, b) => Expr::Sub(self.find(*a), self.find(*b)),
455 Expr::Max(a, b) => Expr::Max(self.find(*a), self.find(*b)),
456 Expr::Min(a, b) => Expr::Min(self.find(*a), self.find(*b)),
457 Expr::Div(a, b) => Expr::Div(self.find(*a), self.find(*b)),
458 Expr::Mul(a, b) => Expr::Mul(self.find(*a), self.find(*b)),
459 Expr::Clamp { min, max, val } => Expr::Clamp {
460 min: self.find(*min),
461 max: self.find(*max),
462 val: self.find(*val),
463 },
464 Expr::HFlex(ids) => Expr::HFlex(ids.iter().map(|id| self.find(*id)).collect()),
465 Expr::VFlex(ids) => Expr::VFlex(ids.iter().map(|id| self.find(*id)).collect()),
466 Expr::Fill(id) => Expr::Fill(self.find(*id)),
467 }
468 }
469
470 pub fn extract(&self, id: Id) -> Expr {
474 let id = self.find(id);
475 self.nodes[id.index()]
476 .iter()
477 .min_by_key(|e| Self::cost(e))
478 .cloned()
479 .expect("e-class has no expressions")
480 }
481
482 fn cost(expr: &Expr) -> u32 {
487 match expr {
488 Expr::Num(_) => 1,
489 Expr::Var(_) => 2,
490 Expr::Add(_, _) | Expr::Sub(_, _) | Expr::Max(_, _) | Expr::Min(_, _) => 3,
491 Expr::Mul(_, _) | Expr::Div(_, _) => 4,
492 Expr::Fill(_) => 3,
493 Expr::Clamp { .. } => 5,
494 Expr::HFlex(ids) | Expr::VFlex(ids) => 10 + ids.len() as u32,
495 }
496 }
497}
498
499impl Default for EGraph {
500 fn default() -> Self {
501 Self::new()
502 }
503}
504
505pub fn encode_constraint(graph: &mut EGraph, constraint: &crate::Constraint, total: u16) -> Id {
509 match constraint {
510 crate::Constraint::Fixed(n) => graph.add(Expr::Num(*n)),
511 crate::Constraint::Min(n) => {
512 let min = graph.add(Expr::Num(*n));
513 let total_id = graph.add(Expr::Num(total));
514 graph.add(Expr::Clamp {
515 min,
516 max: total_id,
517 val: min,
518 })
519 }
520 crate::Constraint::Max(n) => {
521 let max = graph.add(Expr::Num(*n));
522 let zero = graph.add(Expr::Num(0));
523 graph.add(Expr::Clamp {
524 min: zero,
525 max,
526 val: max,
527 })
528 }
529 crate::Constraint::Percentage(pct) => {
530 let scaled = ((*pct / 100.0) * total as f32) as u16;
531 graph.add(Expr::Num(scaled))
532 }
533 crate::Constraint::Ratio(num, den) => {
534 let result = (total as u32)
535 .checked_mul(*num)
536 .and_then(|v| v.checked_div(*den))
537 .unwrap_or(0) as u16;
538 graph.add(Expr::Num(result))
539 }
540 crate::Constraint::Fill | crate::Constraint::FitContent | crate::Constraint::FitMin => {
541 let total_id = graph.add(Expr::Num(total));
542 graph.add(Expr::Fill(total_id))
543 }
544 crate::Constraint::FitContentBounded { min, max } => {
545 let min_id = graph.add(Expr::Num(*min));
546 let max_id = graph.add(Expr::Num(*max));
547 let preferred = graph.add(Expr::Num(total));
548 graph.add(Expr::Clamp {
549 min: min_id,
550 max: max_id,
551 val: preferred,
552 })
553 }
554 }
555}
556
557pub fn encode_flex(graph: &mut EGraph, children: &[Id], horizontal: bool) -> Id {
559 if horizontal {
560 graph.add(Expr::HFlex(children.to_vec()))
561 } else {
562 graph.add(Expr::VFlex(children.to_vec()))
563 }
564}
565
566#[derive(Clone, Debug)]
570pub struct SaturationConfig {
571 pub node_budget: usize,
573 pub iteration_limit: usize,
575 pub time_limit_us: u64,
577 pub memory_limit: usize,
579}
580
581impl Default for SaturationConfig {
582 fn default() -> Self {
583 Self {
584 node_budget: 10_000,
585 iteration_limit: 100,
586 time_limit_us: 5_000, memory_limit: 10 * 1024 * 1024, }
589 }
590}
591
592impl SaturationConfig {
593 pub fn from_env() -> Self {
601 let mut config = Self::default();
602
603 if let Ok(val) = std::env::var("FRANKENTUI_EGRAPH_NODE_BUDGET")
604 && let Ok(n) = val.parse::<usize>()
605 {
606 config.node_budget = n;
607 }
608 if let Ok(val) = std::env::var("FRANKENTUI_EGRAPH_TIMEOUT_MS")
609 && let Ok(ms) = val.parse::<u64>()
610 {
611 config.time_limit_us = ms * 1000;
612 }
613 if let Ok(val) = std::env::var("FRANKENTUI_EGRAPH_MAX_ITERS")
614 && let Ok(n) = val.parse::<usize>()
615 {
616 config.iteration_limit = n;
617 }
618 if let Ok(val) = std::env::var("FRANKENTUI_EGRAPH_MEMORY_MB")
619 && let Ok(mb) = val.parse::<usize>()
620 {
621 config.memory_limit = mb * 1024 * 1024;
622 }
623
624 config
625 }
626}
627
628#[derive(Clone, Copy, Debug, PartialEq, Eq)]
630pub enum GuardTriggered {
631 None,
633 NodeBudget,
635 Timeout,
637 Memory,
639 IterationLimit,
641}
642
643#[derive(Clone, Debug)]
645pub struct SaturationResult {
646 pub rewrites: usize,
648 pub iterations: usize,
650 pub saturated: bool,
652 pub stopped_early: bool,
654 pub node_count: usize,
656 pub guard: GuardTriggered,
658 pub time_us: u64,
660 pub memory_bytes: usize,
662}
663
664impl EGraph {
665 pub fn saturate(&mut self, config: &SaturationConfig) -> SaturationResult {
671 let start = std::time::Instant::now();
672 let mut total_rewrites = 0;
673 let mut iterations = 0;
674
675 let make_result = |this: &Self, rewrites, iterations, saturated, guard: GuardTriggered| {
676 let elapsed = start.elapsed().as_micros() as u64;
677 SaturationResult {
678 rewrites,
679 iterations,
680 saturated,
681 stopped_early: guard != GuardTriggered::None,
682 node_count: this.node_count(),
683 guard,
684 time_us: elapsed,
685 memory_bytes: this.memory_usage(),
686 }
687 };
688
689 for _ in 0..config.iteration_limit {
690 if self.node_count() >= config.node_budget {
692 return make_result(
693 self,
694 total_rewrites,
695 iterations,
696 false,
697 GuardTriggered::NodeBudget,
698 );
699 }
700
701 if config.time_limit_us > 0
703 && start.elapsed().as_micros() as u64 >= config.time_limit_us
704 {
705 return make_result(
706 self,
707 total_rewrites,
708 iterations,
709 false,
710 GuardTriggered::Timeout,
711 );
712 }
713
714 if config.memory_limit > 0 && self.memory_usage() >= config.memory_limit {
716 return make_result(
717 self,
718 total_rewrites,
719 iterations,
720 false,
721 GuardTriggered::Memory,
722 );
723 }
724
725 let applied = self.apply_rules_once();
726 total_rewrites += applied;
727 iterations += 1;
728
729 if applied == 0 {
730 self.last_apply_count = total_rewrites;
731 return make_result(self, total_rewrites, iterations, true, GuardTriggered::None);
732 }
733 }
734
735 self.last_apply_count = total_rewrites;
736 make_result(
737 self,
738 total_rewrites,
739 iterations,
740 false,
741 GuardTriggered::IterationLimit,
742 )
743 }
744
745 pub fn memory_usage(&self) -> usize {
747 let parents = self.parents.capacity() * std::mem::size_of::<u32>();
748 let ranks = self.ranks.capacity();
749 let nodes: usize = self
750 .nodes
751 .iter()
752 .map(|v| v.capacity() * std::mem::size_of::<Expr>())
753 .sum();
754 let nodes_vec = self.nodes.capacity() * std::mem::size_of::<Vec<Expr>>();
755 let memo = self.memo.capacity() * (std::mem::size_of::<Expr>() + std::mem::size_of::<Id>());
756 parents + ranks + nodes + nodes_vec + memo
757 }
758}
759
760pub fn solve_layout(
769 constraints: &[crate::Constraint],
770 total: u16,
771 config: &SaturationConfig,
772) -> (Vec<u16>, SaturationResult) {
773 let mut graph = EGraph::new();
774
775 let ids: Vec<Id> = constraints
777 .iter()
778 .map(|c| encode_constraint(&mut graph, c, total))
779 .collect();
780
781 let result = graph.saturate(config);
783
784 let sizes: Vec<u16> = ids
786 .iter()
787 .map(|&id| {
788 let expr = graph.extract(id);
789 match expr {
790 Expr::Num(n) => n,
791 _ => total,
793 }
794 })
795 .collect();
796
797 (sizes, result)
798}
799
800pub fn solve_layout_default(constraints: &[crate::Constraint], total: u16) -> Vec<u16> {
802 solve_layout(constraints, total, &SaturationConfig::default()).0
803}
804
805#[derive(Clone, Debug)]
809pub struct EvidenceRecord {
810 pub test_name: String,
811 pub constraint_count: usize,
812 pub total_space: u16,
813 pub nodes_at_completion: usize,
814 pub iterations: usize,
815 pub time_us: u64,
816 pub memory_bytes: usize,
817 pub guard_triggered: GuardTriggered,
818 pub saturated: bool,
819 pub rewrites: usize,
820}
821
822impl EvidenceRecord {
823 pub fn from_result(
825 test_name: &str,
826 constraint_count: usize,
827 total: u16,
828 result: &SaturationResult,
829 ) -> Self {
830 Self {
831 test_name: test_name.to_string(),
832 constraint_count,
833 total_space: total,
834 nodes_at_completion: result.node_count,
835 iterations: result.iterations,
836 time_us: result.time_us,
837 memory_bytes: result.memory_bytes,
838 guard_triggered: result.guard,
839 saturated: result.saturated,
840 rewrites: result.rewrites,
841 }
842 }
843
844 pub fn to_jsonl(&self) -> String {
846 format!(
847 concat!(
848 "{{\"test\":\"{}\",\"constraints\":{},\"total\":{},",
849 "\"nodes\":{},\"iterations\":{},\"time_us\":{},",
850 "\"memory_bytes\":{},\"guard\":\"{:?}\",",
851 "\"saturated\":{},\"rewrites\":{}}}"
852 ),
853 self.test_name,
854 self.constraint_count,
855 self.total_space,
856 self.nodes_at_completion,
857 self.iterations,
858 self.time_us,
859 self.memory_bytes,
860 self.guard_triggered,
861 self.saturated,
862 self.rewrites,
863 )
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870
871 #[test]
874 fn add_num() {
875 let mut g = EGraph::new();
876 let a = g.add(Expr::Num(42));
877 assert_eq!(g.node_count(), 1);
878 assert_eq!(g.extract(a), Expr::Num(42));
879 }
880
881 #[test]
882 fn add_dedup() {
883 let mut g = EGraph::new();
884 let a = g.add(Expr::Num(42));
885 let b = g.add(Expr::Num(42));
886 assert_eq!(a, b);
887 assert_eq!(g.node_count(), 1);
888 }
889
890 #[test]
891 fn merge_classes() {
892 let mut g = EGraph::new();
893 let a = g.add(Expr::Num(1));
894 let b = g.add(Expr::Num(2));
895 assert!(!g.equiv(a, b));
896 g.merge(a, b);
897 assert!(g.equiv(a, b));
898 }
899
900 #[test]
901 fn merge_idempotent() {
902 let mut g = EGraph::new();
903 let a = g.add(Expr::Num(1));
904 let first = g.merge(a, a);
905 assert_eq!(first, a);
906 }
907
908 #[test]
911 fn add_zero_identity() {
912 let mut g = EGraph::new();
913 let a = g.add(Expr::Num(100));
914 let zero = g.add(Expr::Num(0));
915 let sum = g.add(Expr::Add(a, zero));
916 g.apply_rules();
917 assert!(g.equiv(sum, a), "Add(x, 0) should equal x");
918 }
919
920 #[test]
921 fn add_zero_identity_left() {
922 let mut g = EGraph::new();
923 let a = g.add(Expr::Num(50));
924 let zero = g.add(Expr::Num(0));
925 let sum = g.add(Expr::Add(zero, a));
926 g.apply_rules();
927 assert!(g.equiv(sum, a), "Add(0, x) should equal x");
928 }
929
930 #[test]
931 fn sub_zero_identity() {
932 let mut g = EGraph::new();
933 let a = g.add(Expr::Num(100));
934 let zero = g.add(Expr::Num(0));
935 let diff = g.add(Expr::Sub(a, zero));
936 g.apply_rules();
937 assert!(g.equiv(diff, a), "Sub(x, 0) should equal x");
938 }
939
940 #[test]
941 fn mul_one_identity() {
942 let mut g = EGraph::new();
943 let a = g.add(Expr::Num(42));
944 let one = g.add(Expr::Num(1));
945 let prod = g.add(Expr::Mul(a, one));
946 g.apply_rules();
947 assert!(g.equiv(prod, a), "Mul(x, 1) should equal x");
948 }
949
950 #[test]
951 fn mul_zero_annihilation() {
952 let mut g = EGraph::new();
953 let a = g.add(Expr::Num(42));
954 let zero = g.add(Expr::Num(0));
955 let prod = g.add(Expr::Mul(a, zero));
956 g.apply_rules();
957 assert!(g.equiv(prod, zero), "Mul(x, 0) should equal 0");
958 }
959
960 #[test]
961 fn max_idempotent() {
962 let mut g = EGraph::new();
963 let a = g.add(Expr::Num(42));
964 let m = g.add(Expr::Max(a, a));
965 g.apply_rules();
966 assert!(g.equiv(m, a), "Max(x, x) should equal x");
967 }
968
969 #[test]
970 fn min_idempotent() {
971 let mut g = EGraph::new();
972 let a = g.add(Expr::Num(42));
973 let m = g.add(Expr::Min(a, a));
974 g.apply_rules();
975 assert!(g.equiv(m, a), "Min(x, x) should equal x");
976 }
977
978 #[test]
979 fn clamp_unclamped() {
980 let mut g = EGraph::new();
981 let zero = g.add(Expr::Num(0));
982 let max = g.add(Expr::Num(u16::MAX));
983 let val = g.add(Expr::Num(50));
984 let clamped = g.add(Expr::Clamp {
985 min: zero,
986 max,
987 val,
988 });
989 g.apply_rules();
990 assert!(g.equiv(clamped, val), "Clamp(0, MAX, x) should equal x");
991 }
992
993 #[test]
996 fn extract_prefers_simpler() {
997 let mut g = EGraph::new();
998 let a = g.add(Expr::Num(100));
999 let zero = g.add(Expr::Num(0));
1000 let sum = g.add(Expr::Add(a, zero));
1001 g.apply_rules();
1002 let extracted = g.extract(sum);
1004 assert_eq!(extracted, Expr::Num(100));
1005 }
1006
1007 #[test]
1008 fn extract_var_over_complex() {
1009 let mut g = EGraph::new();
1010 let v = g.add(Expr::Var(NodeId(0)));
1011 let zero = g.add(Expr::Num(0));
1012 let sum = g.add(Expr::Add(v, zero));
1013 g.apply_rules();
1014 let extracted = g.extract(sum);
1016 assert_eq!(extracted, Expr::Var(NodeId(0)));
1017 }
1018
1019 #[test]
1022 fn counts_after_merge() {
1023 let mut g = EGraph::new();
1024 let a = g.add(Expr::Num(1));
1025 let b = g.add(Expr::Num(2));
1026 assert_eq!(g.class_count(), 2);
1027 g.merge(a, b);
1028 assert_eq!(g.class_count(), 1);
1029 assert_eq!(g.node_count(), 2); }
1031
1032 #[test]
1035 fn encode_fixed_constraint() {
1036 let mut g = EGraph::new();
1037 let id = encode_constraint(&mut g, &crate::Constraint::Fixed(50), 200);
1038 assert_eq!(g.extract(id), Expr::Num(50));
1039 }
1040
1041 #[test]
1042 fn encode_percentage_constraint() {
1043 let mut g = EGraph::new();
1044 let id = encode_constraint(&mut g, &crate::Constraint::Percentage(50.0), 200);
1045 assert_eq!(g.extract(id), Expr::Num(100)); }
1047
1048 #[test]
1049 fn encode_ratio_constraint() {
1050 let mut g = EGraph::new();
1051 let id = encode_constraint(&mut g, &crate::Constraint::Ratio(1, 4), 200);
1052 assert_eq!(g.extract(id), Expr::Num(50)); }
1054
1055 #[test]
1056 fn encode_ratio_zero_denom() {
1057 let mut g = EGraph::new();
1058 let id = encode_constraint(&mut g, &crate::Constraint::Ratio(1, 0), 200);
1059 assert_eq!(g.extract(id), Expr::Num(0));
1060 }
1061
1062 #[test]
1063 fn encode_flex_horizontal() {
1064 let mut g = EGraph::new();
1065 let a = g.add(Expr::Num(100));
1066 let b = g.add(Expr::Num(200));
1067 let flex = encode_flex(&mut g, &[a, b], true);
1068 assert!(matches!(g.extract(flex), Expr::HFlex(_)));
1069 }
1070
1071 #[test]
1072 fn encode_flex_vertical() {
1073 let mut g = EGraph::new();
1074 let a = g.add(Expr::Num(100));
1075 let b = g.add(Expr::Num(200));
1076 let flex = encode_flex(&mut g, &[a, b], false);
1077 assert!(matches!(g.extract(flex), Expr::VFlex(_)));
1078 }
1079
1080 #[test]
1083 fn saturation_terminates() {
1084 let mut g = EGraph::new();
1085 let a = g.add(Expr::Num(10));
1086 let zero = g.add(Expr::Num(0));
1087 let one = g.add(Expr::Num(1));
1088 let mul = g.add(Expr::Mul(a, one));
1090 let sum = g.add(Expr::Add(mul, zero));
1091 let total = g.apply_rules();
1092 assert!(total > 0, "rules should fire");
1093 assert!(g.equiv(sum, a), "expression should simplify to a");
1094 }
1095
1096 #[test]
1097 fn apply_rules_returns_zero_when_saturated() {
1098 let mut g = EGraph::new();
1099 let a = g.add(Expr::Num(10));
1100 let b = g.add(Expr::Num(20));
1101 let _sum = g.add(Expr::Add(a, b));
1102 let total = g.apply_rules();
1104 assert!(total >= 1, "commutativity should fire");
1105 let second = g.apply_rules();
1107 assert_eq!(second, 0, "already saturated");
1108 }
1109
1110 #[test]
1113 fn chained_identity_simplification() {
1114 let mut g = EGraph::new();
1115 let x = g.add(Expr::Num(42));
1116 let zero = g.add(Expr::Num(0));
1117 let one = g.add(Expr::Num(1));
1118 let mul = g.add(Expr::Mul(x, one));
1120 let sub = g.add(Expr::Sub(mul, zero));
1121 let add = g.add(Expr::Add(sub, zero));
1122 g.apply_rules();
1123 assert!(g.equiv(add, x));
1124 }
1125
1126 #[test]
1129 fn typical_layout_size() {
1130 let mut g = EGraph::new();
1132 let widgets: Vec<_> = (0..5).map(|i| g.add(Expr::Var(NodeId(i)))).collect();
1133 let _flex = encode_flex(&mut g, &widgets, true);
1134 assert!(g.node_count() <= 10, "small layout should be compact");
1136 }
1137
1138 #[test]
1139 fn medium_layout_size() {
1140 let mut g = EGraph::new();
1142 let widgets: Vec<_> = (0..100).map(|i| g.add(Expr::Var(NodeId(i)))).collect();
1143 let _flex = encode_flex(&mut g, &widgets, true);
1144 assert!(g.node_count() <= 200);
1145 }
1146
1147 #[test]
1150 fn sub_self_is_zero() {
1151 let mut g = EGraph::new();
1152 let a = g.add(Expr::Num(42));
1153 let zero = g.add(Expr::Num(0));
1154 let sub = g.add(Expr::Sub(a, a));
1155 g.apply_rules();
1156 assert!(g.equiv(sub, zero), "Sub(x, x) should equal 0");
1157 }
1158
1159 #[test]
1160 fn div_one_identity() {
1161 let mut g = EGraph::new();
1162 let a = g.add(Expr::Num(42));
1163 let one = g.add(Expr::Num(1));
1164 let div = g.add(Expr::Div(a, one));
1165 g.apply_rules();
1166 assert!(g.equiv(div, a), "Div(x, 1) should equal x");
1167 }
1168
1169 #[test]
1170 fn div_self_is_one() {
1171 let mut g = EGraph::new();
1172 let a = g.add(Expr::Num(42));
1173 let one = g.add(Expr::Num(1));
1174 let div = g.add(Expr::Div(a, a));
1175 g.apply_rules();
1176 assert!(g.equiv(div, one), "Div(x, x) should equal 1");
1177 }
1178
1179 #[test]
1180 fn max_zero_identity() {
1181 let mut g = EGraph::new();
1182 let a = g.add(Expr::Num(42));
1183 let zero = g.add(Expr::Num(0));
1184 let m = g.add(Expr::Max(a, zero));
1185 g.apply_rules();
1186 assert!(g.equiv(m, a), "Max(x, 0) should equal x");
1187 }
1188
1189 #[test]
1190 fn max_zero_identity_left() {
1191 let mut g = EGraph::new();
1192 let a = g.add(Expr::Num(42));
1193 let zero = g.add(Expr::Num(0));
1194 let m = g.add(Expr::Max(zero, a));
1195 g.apply_rules();
1196 assert!(g.equiv(m, a), "Max(0, x) should equal x");
1197 }
1198
1199 #[test]
1200 fn clamp_equal_bounds() {
1201 let mut g = EGraph::new();
1202 let bound = g.add(Expr::Num(50));
1203 let val = g.add(Expr::Num(100));
1204 let clamped = g.add(Expr::Clamp {
1205 min: bound,
1206 max: bound,
1207 val,
1208 });
1209 g.apply_rules();
1210 assert!(g.equiv(clamped, bound), "Clamp(x, x, _) should equal x");
1211 }
1212
1213 #[test]
1214 fn clamp_val_equals_min() {
1215 let mut g = EGraph::new();
1216 let min = g.add(Expr::Num(10));
1217 let max = g.add(Expr::Num(100));
1218 let clamped = g.add(Expr::Clamp { min, max, val: min });
1219 g.apply_rules();
1220 assert!(
1221 g.equiv(clamped, min),
1222 "Clamp(min, max, min) should equal min"
1223 );
1224 }
1225
1226 #[test]
1229 fn add_commutativity() {
1230 let mut g = EGraph::new();
1231 let a = g.add(Expr::Num(10));
1232 let b = g.add(Expr::Num(20));
1233 let ab = g.add(Expr::Add(a, b));
1234 let ba = g.add(Expr::Add(b, a));
1235 g.apply_rules();
1236 assert!(g.equiv(ab, ba), "Add(a, b) should equal Add(b, a)");
1237 }
1238
1239 #[test]
1240 fn max_commutativity() {
1241 let mut g = EGraph::new();
1242 let a = g.add(Expr::Num(10));
1243 let b = g.add(Expr::Num(20));
1244 let ab = g.add(Expr::Max(a, b));
1245 let ba = g.add(Expr::Max(b, a));
1246 g.apply_rules();
1247 assert!(g.equiv(ab, ba), "Max(a, b) should equal Max(b, a)");
1248 }
1249
1250 #[test]
1251 fn min_commutativity() {
1252 let mut g = EGraph::new();
1253 let a = g.add(Expr::Num(10));
1254 let b = g.add(Expr::Num(20));
1255 let ab = g.add(Expr::Min(a, b));
1256 let ba = g.add(Expr::Min(b, a));
1257 g.apply_rules();
1258 assert!(g.equiv(ab, ba), "Min(a, b) should equal Min(b, a)");
1259 }
1260
1261 #[test]
1262 fn mul_commutativity() {
1263 let mut g = EGraph::new();
1264 let a = g.add(Expr::Num(3));
1265 let b = g.add(Expr::Num(7));
1266 let ab = g.add(Expr::Mul(a, b));
1267 let ba = g.add(Expr::Mul(b, a));
1268 g.apply_rules();
1269 assert!(g.equiv(ab, ba), "Mul(a, b) should equal Mul(b, a)");
1270 }
1271
1272 #[test]
1275 fn saturate_basic() {
1276 let mut g = EGraph::new();
1277 let a = g.add(Expr::Num(100));
1278 let zero = g.add(Expr::Num(0));
1279 let sum = g.add(Expr::Add(a, zero));
1280 let result = g.saturate(&SaturationConfig::default());
1281 assert!(result.saturated);
1282 assert!(!result.stopped_early);
1283 assert!(result.rewrites > 0);
1284 assert!(g.equiv(sum, a));
1285 }
1286
1287 #[test]
1288 fn saturate_with_node_budget() {
1289 let mut g = EGraph::new();
1290 for i in 0..50u16 {
1292 let v = g.add(Expr::Var(NodeId(i as u32)));
1293 let n = g.add(Expr::Num(i + 1));
1294 g.add(Expr::Add(v, n));
1295 }
1296 let initial = g.node_count();
1297 let config = SaturationConfig {
1298 node_budget: initial + 10,
1299 iteration_limit: 1000,
1300 time_limit_us: 0,
1301 memory_limit: 0,
1302 };
1303 let result = g.saturate(&config);
1304 assert!(result.stopped_early, "should stop due to node budget");
1305 assert_eq!(result.guard, GuardTriggered::NodeBudget);
1306 }
1307
1308 #[test]
1309 fn saturate_with_iteration_limit() {
1310 let mut g = EGraph::new();
1311 let a = g.add(Expr::Num(10));
1312 let b = g.add(Expr::Num(20));
1313 let _sum = g.add(Expr::Add(a, b));
1314 let config = SaturationConfig {
1315 node_budget: 10_000,
1316 iteration_limit: 1,
1317 time_limit_us: 0,
1318 memory_limit: 0,
1319 };
1320 let result = g.saturate(&config);
1321 assert!(result.iterations <= 1);
1322 assert_eq!(result.guard, GuardTriggered::IterationLimit);
1323 }
1324
1325 #[test]
1326 fn saturate_with_memory_limit() {
1327 let mut g = EGraph::new();
1328 for i in 0..200u16 {
1329 let v = g.add(Expr::Var(NodeId(i as u32)));
1330 let n = g.add(Expr::Num(i + 1));
1331 g.add(Expr::Add(v, n));
1332 }
1333 let config = SaturationConfig {
1334 node_budget: 100_000,
1335 iteration_limit: 1000,
1336 time_limit_us: 0,
1337 memory_limit: 1, };
1339 let result = g.saturate(&config);
1340 assert!(result.stopped_early);
1341 assert_eq!(result.guard, GuardTriggered::Memory);
1342 }
1343
1344 #[test]
1345 fn saturate_guard_none_on_completion() {
1346 let mut g = EGraph::new();
1347 let a = g.add(Expr::Num(100));
1348 let zero = g.add(Expr::Num(0));
1349 let _sum = g.add(Expr::Add(a, zero));
1350 let result = g.saturate(&SaturationConfig::default());
1351 assert!(result.saturated);
1352 assert_eq!(result.guard, GuardTriggered::None);
1353 assert!(result.time_us > 0 || result.iterations > 0);
1354 assert!(result.memory_bytes > 0);
1355 }
1356
1357 #[test]
1358 fn saturate_result_has_timing() {
1359 let mut g = EGraph::new();
1360 for i in 0..100u16 {
1361 let v = g.add(Expr::Var(NodeId(i as u32)));
1362 let zero = g.add(Expr::Num(0));
1363 g.add(Expr::Add(v, zero));
1364 }
1365 let result = g.saturate(&SaturationConfig::default());
1366 assert!(result.memory_bytes > 0);
1368 assert!(result.node_count > 0);
1369 }
1370
1371 #[test]
1372 fn saturate_memory_bounded() {
1373 let mut g = EGraph::new();
1374 for i in 0..500u16 {
1375 let v = g.add(Expr::Var(NodeId(i as u32)));
1376 let zero = g.add(Expr::Num(0));
1377 g.add(Expr::Add(v, zero));
1378 }
1379 g.saturate(&SaturationConfig::default());
1380 let mem = g.memory_usage();
1381 assert!(mem < 10 * 1024 * 1024, "memory {} exceeds 10MB budget", mem);
1382 }
1383
1384 #[test]
1387 fn solve_fixed_constraints() {
1388 let constraints = vec![
1389 crate::Constraint::Fixed(50),
1390 crate::Constraint::Fixed(100),
1391 crate::Constraint::Fixed(50),
1392 ];
1393 let sizes = solve_layout_default(&constraints, 200);
1394 assert_eq!(sizes, vec![50, 100, 50]);
1395 }
1396
1397 #[test]
1398 fn solve_percentage_constraints() {
1399 let constraints = vec![
1400 crate::Constraint::Percentage(25.0),
1401 crate::Constraint::Percentage(75.0),
1402 ];
1403 let sizes = solve_layout_default(&constraints, 200);
1404 assert_eq!(sizes, vec![50, 150]);
1405 }
1406
1407 #[test]
1408 fn solve_ratio_constraints() {
1409 let constraints = vec![
1410 crate::Constraint::Ratio(1, 3),
1411 crate::Constraint::Ratio(2, 3),
1412 ];
1413 let sizes = solve_layout_default(&constraints, 300);
1414 assert_eq!(sizes, vec![100, 200]);
1415 }
1416
1417 #[test]
1418 fn solve_fill_constraint() {
1419 let constraints = vec![crate::Constraint::Fill];
1420 let sizes = solve_layout_default(&constraints, 120);
1421 assert_eq!(sizes, vec![120]);
1423 }
1424
1425 #[test]
1426 fn solve_mixed_constraints() {
1427 let constraints = vec![
1428 crate::Constraint::Fixed(30),
1429 crate::Constraint::Percentage(50.0),
1430 crate::Constraint::Ratio(1, 4),
1431 ];
1432 let sizes = solve_layout_default(&constraints, 200);
1433 assert_eq!(sizes, vec![30, 100, 50]);
1434 }
1435
1436 #[test]
1437 fn solve_empty_constraints() {
1438 let constraints: Vec<crate::Constraint> = vec![];
1439 let sizes = solve_layout_default(&constraints, 200);
1440 assert!(sizes.is_empty());
1441 }
1442
1443 #[test]
1444 fn solve_returns_saturation_result() {
1445 let constraints = vec![crate::Constraint::Fixed(50)];
1446 let (sizes, result) = solve_layout(&constraints, 200, &SaturationConfig::default());
1447 assert_eq!(sizes, vec![50]);
1448 assert!(result.saturated);
1449 }
1450
1451 #[test]
1452 fn solve_500_widgets() {
1453 let constraints: Vec<_> = (0..500)
1454 .map(|i| crate::Constraint::Fixed(i as u16 % 100))
1455 .collect();
1456 let config = SaturationConfig::default();
1457 let (sizes, result) = solve_layout(&constraints, 1000, &config);
1458 assert_eq!(sizes.len(), 500);
1459 for (i, &s) in sizes.iter().enumerate() {
1461 assert_eq!(s, i as u16 % 100);
1462 }
1463 assert!(!result.stopped_early || result.node_count <= config.node_budget + 500);
1464 }
1465
1466 #[test]
1467 fn solve_fit_content_bounded() {
1468 let constraints = vec![crate::Constraint::FitContentBounded { min: 10, max: 50 }];
1469 let sizes = solve_layout_default(&constraints, 200);
1470 assert_eq!(sizes.len(), 1);
1474 }
1475
1476 #[test]
1479 fn config_default_values() {
1480 let config = SaturationConfig::default();
1481 assert_eq!(config.node_budget, 10_000);
1482 assert_eq!(config.time_limit_us, 5_000);
1483 assert_eq!(config.iteration_limit, 100);
1484 assert_eq!(config.memory_limit, 10 * 1024 * 1024);
1485 }
1486
1487 #[test]
1488 fn from_env_returns_valid_config() {
1489 let config = SaturationConfig::from_env();
1491 assert!(config.node_budget > 0);
1492 assert!(config.iteration_limit > 0);
1493 }
1494
1495 #[test]
1498 fn random_constraints_never_oom_or_hang() {
1499 let constraint_sets: Vec<Vec<crate::Constraint>> = vec![
1501 (0..1000)
1502 .map(|i| crate::Constraint::Fixed(i as u16))
1503 .collect(),
1504 (0..500).map(|_| crate::Constraint::Fill).collect(),
1505 (0..200)
1506 .map(|i| crate::Constraint::Percentage(i as f32 * 0.5))
1507 .collect(),
1508 (0..100)
1509 .map(|i| crate::Constraint::Ratio(i + 1, 100))
1510 .collect(),
1511 (0..300).map(|i| crate::Constraint::Min(i as u16)).collect(),
1512 (0..300).map(|i| crate::Constraint::Max(i as u16)).collect(),
1513 (0..500)
1515 .map(|i| crate::Constraint::FitContentBounded {
1516 min: i as u16,
1517 max: i as u16 + 100,
1518 })
1519 .collect(),
1520 ];
1521
1522 let config = SaturationConfig {
1523 node_budget: 10_000,
1524 iteration_limit: 100,
1525 time_limit_us: 50_000, memory_limit: 10 * 1024 * 1024,
1527 };
1528
1529 for constraints in &constraint_sets {
1530 let (sizes, result) = solve_layout(constraints, 1000, &config);
1531 assert_eq!(sizes.len(), constraints.len());
1532 assert!(
1533 result.memory_bytes <= config.memory_limit + 1024 * 1024,
1534 "memory {} exceeded limit + margin for {:?}",
1535 result.memory_bytes,
1536 result.guard,
1537 );
1538 }
1539 }
1540
1541 #[test]
1544 fn fuzz_1000_random_constraint_sets() {
1545 let mut seed: u32 = 42;
1547 let mut rng = || -> u32 {
1548 seed ^= seed << 13;
1549 seed ^= seed >> 17;
1550 seed ^= seed << 5;
1551 seed
1552 };
1553
1554 let config = SaturationConfig {
1555 node_budget: 10_000,
1556 iteration_limit: 100,
1557 time_limit_us: 50_000,
1558 memory_limit: 10 * 1024 * 1024,
1559 };
1560
1561 for _ in 0..1000 {
1562 let count = (rng() % 50 + 1) as usize;
1563 let total = (rng() % 500 + 1) as u16;
1564
1565 let constraints: Vec<_> = (0..count)
1566 .map(|_| {
1567 let kind = rng() % 7;
1568 match kind {
1569 0 => crate::Constraint::Fixed((rng() % (total as u32 + 1)) as u16),
1570 1 => crate::Constraint::Percentage((rng() % 101) as f32),
1571 2 => crate::Constraint::Min((rng() % (total as u32 + 1)) as u16),
1572 3 => crate::Constraint::Max((rng() % (total as u32 + 1)) as u16),
1573 4 => {
1574 let den = rng() % 10 + 1;
1575 let num = rng() % (den + 1);
1576 crate::Constraint::Ratio(num, den)
1577 }
1578 5 => crate::Constraint::Fill,
1579 _ => {
1580 let min = (rng() % (total as u32 + 1)) as u16;
1581 let max = min.saturating_add((rng() % 100) as u16);
1582 crate::Constraint::FitContentBounded { min, max }
1583 }
1584 }
1585 })
1586 .collect();
1587
1588 let (sizes, result) = solve_layout(&constraints, total, &config);
1589
1590 assert_eq!(sizes.len(), constraints.len());
1592 assert!(result.memory_bytes < config.memory_limit + 2 * 1024 * 1024);
1593 }
1594 }
1595
1596 #[test]
1599 fn deterministic_across_runs() {
1600 let constraints = vec![
1602 crate::Constraint::Fixed(30),
1603 crate::Constraint::Percentage(50.0),
1604 crate::Constraint::Ratio(1, 4),
1605 crate::Constraint::Fill,
1606 crate::Constraint::Min(10),
1607 crate::Constraint::Max(80),
1608 ];
1609
1610 let config = SaturationConfig {
1611 node_budget: 10_000,
1612 iteration_limit: 100,
1613 time_limit_us: 0, memory_limit: 0,
1615 };
1616
1617 let (sizes1, r1) = solve_layout(&constraints, 200, &config);
1618 let (sizes2, r2) = solve_layout(&constraints, 200, &config);
1619
1620 assert_eq!(sizes1, sizes2, "sizes must be deterministic");
1621 assert_eq!(r1.rewrites, r2.rewrites, "rewrites must be deterministic");
1622 assert_eq!(
1623 r1.iterations, r2.iterations,
1624 "iterations must be deterministic"
1625 );
1626 assert_eq!(
1627 r1.node_count, r2.node_count,
1628 "node_count must be deterministic"
1629 );
1630 assert_eq!(r1.guard, r2.guard, "guard must be deterministic");
1631 }
1632
1633 #[test]
1636 fn evidence_record_from_result() {
1637 let constraints = vec![
1638 crate::Constraint::Fixed(50),
1639 crate::Constraint::Percentage(50.0),
1640 ];
1641 let (_, result) = solve_layout(&constraints, 200, &SaturationConfig::default());
1642 let record = EvidenceRecord::from_result("test_case", 2, 200, &result);
1643
1644 assert_eq!(record.test_name, "test_case");
1645 assert_eq!(record.constraint_count, 2);
1646 assert_eq!(record.total_space, 200);
1647 assert!(record.nodes_at_completion > 0);
1648 }
1649
1650 #[test]
1651 fn evidence_record_to_jsonl() {
1652 let result = SaturationResult {
1653 rewrites: 5,
1654 iterations: 3,
1655 saturated: true,
1656 stopped_early: false,
1657 node_count: 42,
1658 guard: GuardTriggered::None,
1659 time_us: 1234,
1660 memory_bytes: 8192,
1661 };
1662 let record = EvidenceRecord::from_result("demo", 10, 200, &result);
1663 let jsonl = record.to_jsonl();
1664
1665 assert!(jsonl.starts_with('{'));
1666 assert!(jsonl.ends_with('}'));
1667 assert!(jsonl.contains("\"test\":\"demo\""));
1668 assert!(jsonl.contains("\"constraints\":10"));
1669 assert!(jsonl.contains("\"nodes\":42"));
1670 assert!(jsonl.contains("\"saturated\":true"));
1671 assert!(jsonl.contains("\"guard\":\"None\""));
1672 }
1673
1674 #[test]
1675 fn evidence_records_for_all_guards() {
1676 for guard in [
1678 GuardTriggered::None,
1679 GuardTriggered::NodeBudget,
1680 GuardTriggered::Timeout,
1681 GuardTriggered::Memory,
1682 GuardTriggered::IterationLimit,
1683 ] {
1684 let result = SaturationResult {
1685 rewrites: 0,
1686 iterations: 1,
1687 saturated: guard == GuardTriggered::None,
1688 stopped_early: guard != GuardTriggered::None,
1689 node_count: 10,
1690 guard,
1691 time_us: 100,
1692 memory_bytes: 1024,
1693 };
1694 let record = EvidenceRecord::from_result("guard_test", 1, 100, &result);
1695 let jsonl = record.to_jsonl();
1696 assert!(jsonl.contains(&format!("{guard:?}")));
1697 }
1698 }
1699
1700 #[test]
1703 fn empty_input_produces_empty_layout() {
1704 let (sizes, result) = solve_layout(&[], 200, &SaturationConfig::default());
1705 assert!(sizes.is_empty());
1706 assert!(result.saturated);
1707 }
1708
1709 #[test]
1710 fn single_constraint_no_rewriting_needed() {
1711 let (sizes, result) = solve_layout(
1712 &[crate::Constraint::Fixed(42)],
1713 200,
1714 &SaturationConfig::default(),
1715 );
1716 assert_eq!(sizes, vec![42]);
1717 assert!(result.saturated);
1718 assert_eq!(result.guard, GuardTriggered::None);
1719 }
1720
1721 #[test]
1722 fn zero_total_space() {
1723 let constraints = vec![
1724 crate::Constraint::Fixed(50),
1725 crate::Constraint::Percentage(50.0),
1726 crate::Constraint::Fill,
1727 ];
1728 let (sizes, _) = solve_layout(&constraints, 0, &SaturationConfig::default());
1729 assert_eq!(sizes.len(), 3);
1730 assert_eq!(sizes[0], 50); assert_eq!(sizes[1], 0); }
1733}