1use std::collections::{HashMap, HashSet};
6
7use crate::algebra::{Algebra, binomial, versor_parity};
8use crate::discovery::{
9 EntityBladeSet, ProductType, infer_all_products, infer_all_products_blades,
10};
11
12use super::error::{MissingProduct, ParseError};
13use super::ir::{
14 AlgebraSpec, BasisVector, FieldSpec, InvolutionKind, NormSpec, ProductEntry, ProductsSpec,
15 SignatureSpec, TypeSpec, VersorSpec,
16};
17use super::raw::{RawAlgebraSpec, RawNormSpec, RawSignature, RawTypeSpec};
18
19const MAX_DIM: usize = 6;
21
22pub fn parse_spec(toml_content: &str) -> Result<AlgebraSpec, ParseError> {
57 let raw: RawAlgebraSpec = toml::from_str(toml_content)?;
58
59 let signature = parse_signature(&raw.signature)?;
61
62 let norm = parse_norm(&raw.norm)?;
64
65 let blade_names = parse_blade_names(&raw.blades, &signature)?;
67
68 let types = parse_types(&raw.types, &signature, &blade_names)?;
70
71 let products = infer_products_from_types(&types, &signature);
73
74 validate_spec(&types)?;
76
77 let complete = raw.algebra.complete;
79 if complete {
80 let missing = check_algebra_completeness(&types, &signature);
81 if !missing.is_empty() {
82 return Err(format_completeness_error(&raw.algebra.name, missing));
83 }
84 }
85
86 Ok(AlgebraSpec {
87 name: raw.algebra.name,
88 module_path: raw.algebra.module_path,
89 description: raw.algebra.description,
90 signature,
91 norm,
92 blade_names,
93 types,
94 products,
95 complete,
96 })
97}
98
99fn parse_signature(raw: &RawSignature) -> Result<SignatureSpec, ParseError> {
113 let p = raw.positive.len();
114 let q = raw.negative.len();
115 let r = raw.zero.len();
116 let dim = p + q + r;
117
118 if dim == 0 {
119 return Err(ParseError::EmptySignature);
120 }
121 if dim > MAX_DIM {
122 return Err(ParseError::DimensionTooLarge(dim));
123 }
124
125 let mut names = HashSet::new();
127 for name in raw
128 .positive
129 .iter()
130 .chain(raw.negative.iter())
131 .chain(raw.zero.iter())
132 {
133 if !names.insert(name) {
134 return Err(ParseError::DuplicateBasisName(name.clone()));
135 }
136 }
137
138 let all_names: Vec<&String> = raw
141 .positive
142 .iter()
143 .chain(raw.negative.iter())
144 .chain(raw.zero.iter())
145 .collect();
146
147 let use_numeric_indexing = all_names
148 .iter()
149 .all(|name| try_parse_basis_index(name, dim).is_some());
150
151 let mut basis = Vec::with_capacity(dim);
153
154 if use_numeric_indexing {
155 for name in &raw.positive {
158 let index = try_parse_basis_index(name, dim).unwrap();
159 basis.push(BasisVector {
160 name: name.clone(),
161 index,
162 metric: 1,
163 });
164 }
165 for name in &raw.negative {
166 let index = try_parse_basis_index(name, dim).unwrap();
167 basis.push(BasisVector {
168 name: name.clone(),
169 index,
170 metric: -1,
171 });
172 }
173 for name in &raw.zero {
174 let index = try_parse_basis_index(name, dim).unwrap();
175 basis.push(BasisVector {
176 name: name.clone(),
177 index,
178 metric: 0,
179 });
180 }
181
182 basis.sort_by_key(|b| b.index);
184
185 for (expected, bv) in basis.iter().enumerate() {
187 if bv.index != expected {
188 return Err(ParseError::NonContiguousBasisIndices {
189 expected,
190 found: bv.index,
191 name: bv.name.clone(),
192 });
193 }
194 }
195 } else {
196 let mut index = 0;
199 for name in &raw.positive {
200 basis.push(BasisVector {
201 name: name.clone(),
202 index,
203 metric: 1,
204 });
205 index += 1;
206 }
207 for name in &raw.negative {
208 basis.push(BasisVector {
209 name: name.clone(),
210 index,
211 metric: -1,
212 });
213 index += 1;
214 }
215 for name in &raw.zero {
216 basis.push(BasisVector {
217 name: name.clone(),
218 index,
219 metric: 0,
220 });
221 index += 1;
222 }
223 }
224
225 Ok(SignatureSpec { basis, p, q, r })
226}
227
228fn try_parse_basis_index(name: &str, dim: usize) -> Option<usize> {
238 if !name.starts_with('e') {
239 return None;
240 }
241
242 let digits = &name[1..];
243 if digits.is_empty() {
244 return None;
245 }
246
247 let num: usize = digits.parse().ok()?;
248
249 if num == 0 {
251 return Some(dim - 1);
252 }
253
254 if num > dim {
255 return None;
256 }
257
258 Some(num - 1)
260}
261
262fn parse_norm(raw: &RawNormSpec) -> Result<NormSpec, ParseError> {
272 let primary_involution = match raw.primary_involution.as_deref() {
273 None | Some("reverse") => InvolutionKind::Reverse,
274 Some("grade_involution") => InvolutionKind::GradeInvolution,
275 Some("clifford_conjugate") => InvolutionKind::CliffordConjugate,
276 Some(other) => {
277 return Err(ParseError::InvalidValue {
278 field: "norm.primary_involution".to_string(),
279 value: other.to_string(),
280 expected: "\"reverse\", \"grade_involution\", or \"clifford_conjugate\""
281 .to_string(),
282 });
283 }
284 };
285
286 Ok(NormSpec { primary_involution })
287}
288
289fn parse_blade_names(
291 raw: &HashMap<String, String>,
292 sig: &SignatureSpec,
293) -> Result<HashMap<usize, String>, ParseError> {
294 let dim = sig.dim();
295 let mut blade_names = HashMap::new();
296
297 for (blade_name, field_name) in raw {
298 let index = parse_blade_index(blade_name, dim)?;
299 blade_names.insert(index, field_name.clone());
300 }
301
302 Ok(blade_names)
303}
304
305#[derive(Debug, Clone, Copy)]
307pub struct BladeParseResult {
308 pub index: usize,
310 pub sign: i8,
313 pub grade: usize,
315}
316
317fn parse_blade_with_sign(name: &str, dim: usize) -> Result<BladeParseResult, ParseError> {
335 if name == "s" {
337 return Ok(BladeParseResult {
338 index: 0,
339 sign: 1,
340 grade: 0,
341 });
342 }
343
344 if !name.starts_with('e') {
346 return Err(ParseError::InvalidBladeName {
347 name: name.to_string(),
348 });
349 }
350
351 let digits = &name[1..];
352 if digits.is_empty() {
353 return Err(ParseError::InvalidBladeName {
354 name: name.to_string(),
355 });
356 }
357
358 let mut indices: Vec<usize> = Vec::new();
360 for c in digits.chars() {
361 let digit = c.to_digit(10).ok_or_else(|| ParseError::InvalidBladeName {
362 name: name.to_string(),
363 })? as usize;
364
365 if digit == 0 || digit > dim {
366 return Err(ParseError::BladeIndexOutOfBounds {
367 name: name.to_string(),
368 index: digit,
369 dim,
370 });
371 }
372
373 let idx = digit - 1;
375
376 if indices.contains(&idx) {
378 return Err(ParseError::InvalidBladeName {
379 name: name.to_string(),
380 });
381 }
382 indices.push(idx);
383 }
384
385 let mut index = 0usize;
387 for &idx in &indices {
388 index |= 1 << idx;
389 }
390
391 let sign = permutation_sign(&indices);
394
395 let grade = indices.len();
396
397 Ok(BladeParseResult { index, sign, grade })
398}
399
400fn permutation_sign(indices: &[usize]) -> i8 {
404 let mut inversions = 0;
405 for i in 0..indices.len() {
406 for j in (i + 1)..indices.len() {
407 if indices[i] > indices[j] {
408 inversions += 1;
409 }
410 }
411 }
412 if inversions % 2 == 0 { 1 } else { -1 }
413}
414
415fn parse_blade_index(name: &str, dim: usize) -> Result<usize, ParseError> {
420 Ok(parse_blade_with_sign(name, dim)?.index)
421}
422
423fn parse_types(
425 raw: &HashMap<String, RawTypeSpec>,
426 sig: &SignatureSpec,
427 blade_names: &HashMap<usize, String>,
428) -> Result<Vec<TypeSpec>, ParseError> {
429 let dim = sig.dim();
430 let mut types = Vec::with_capacity(raw.len());
431 let mut type_names = HashSet::new();
432
433 for (name, raw_type) in raw {
434 if !type_names.insert(name.clone()) {
435 return Err(ParseError::DuplicateTypeName(name.clone()));
436 }
437
438 let type_spec = parse_type(name, raw_type, dim, sig, blade_names)?;
439 types.push(type_spec);
440 }
441
442 types.sort_by(|a, b| a.name.cmp(&b.name));
444
445 Ok(types)
446}
447
448fn parse_type(
450 name: &str,
451 raw: &RawTypeSpec,
452 dim: usize,
453 _sig: &SignatureSpec,
454 _blade_names: &HashMap<usize, String>,
455) -> Result<TypeSpec, ParseError> {
456 for &grade in &raw.grades {
458 if grade > dim {
459 return Err(ParseError::InvalidGrade {
460 type_name: name.to_string(),
461 grade,
462 max: dim,
463 });
464 }
465 }
466
467 if raw.alias_of.is_none() && raw.field_map.is_empty() {
469 return Err(ParseError::MissingFieldMap {
470 type_name: name.to_string(),
471 });
472 }
473
474 let fields = build_fields_from_field_map(&raw.field_map, &raw.grades, dim, name)?;
476
477 let mut field_names = HashSet::new();
479 for field in &fields {
480 if !field_names.insert(&field.name) {
481 return Err(ParseError::DuplicateFieldName {
482 type_name: name.to_string(),
483 field: field.name.clone(),
484 });
485 }
486 }
487
488 if let Some(alias) = &raw.alias_of {
490 if alias == name {
491 return Err(ParseError::SelfAlias {
492 type_name: name.to_string(),
493 });
494 }
495 }
496
497 let versor = if versor_parity(&raw.grades).is_some() {
501 Some(VersorSpec {
502 is_unit: false,
504 sandwich_targets: Vec::new(),
506 })
507 } else {
508 None
509 };
510
511 let expected_blade_count: usize = raw.grades.iter().map(|&g| binomial(dim, g)).sum();
513 let is_sparse = fields.len() < expected_blade_count;
514
515 Ok(TypeSpec {
516 name: name.to_string(),
517 grades: raw.grades.clone(),
518 description: raw.description.clone(),
519 fields,
520 alias_of: raw.alias_of.clone(),
521 versor,
522 is_sparse,
523 inverse_sandwich_targets: raw.inverse_sandwich_targets.clone(),
524 })
525}
526
527fn build_fields_from_field_map(
533 field_map: &[super::raw::RawFieldMapping],
534 grades: &[usize],
535 dim: usize,
536 type_name: &str,
537) -> Result<Vec<FieldSpec>, ParseError> {
538 let mut fields = Vec::with_capacity(field_map.len());
539
540 for mapping in field_map {
541 let blade_result = parse_blade_with_sign(&mapping.blade, dim)?;
543
544 if !grades.contains(&blade_result.grade) {
546 return Err(ParseError::FieldMapGradeMismatch {
547 type_name: type_name.to_string(),
548 field: mapping.name.clone(),
549 blade: mapping.blade.clone(),
550 blade_grade: blade_result.grade,
551 grades: grades.to_vec(),
552 });
553 }
554
555 fields.push(FieldSpec {
556 name: mapping.name.clone(),
557 blade_index: blade_result.index,
558 grade: blade_result.grade,
559 sign: blade_result.sign,
560 });
561 }
562
563 Ok(fields)
564}
565
566#[cfg(test)]
576pub fn validate_canonical_field_order(ty: &TypeSpec) -> bool {
577 if ty.fields.is_empty() {
578 return true;
579 }
580
581 let mut prev_grade = 0;
582 let mut prev_blade_index = 0;
583
584 for (i, field) in ty.fields.iter().enumerate() {
585 if field.grade < prev_grade {
587 return false;
588 }
589
590 if field.grade == prev_grade && i > 0 && field.blade_index <= prev_blade_index {
592 return false;
593 }
594
595 prev_grade = field.grade;
596 prev_blade_index = field.blade_index;
597 }
598
599 true
600}
601
602fn infer_products_from_types(types: &[TypeSpec], signature: &SignatureSpec) -> ProductsSpec {
611 let algebra = Algebra::from_metrics(signature.metrics_by_index());
613 let dim = signature.dim();
614
615 let has_sparse = types.iter().any(|t| t.is_sparse && t.alias_of.is_none());
617
618 if has_sparse {
619 infer_products_blade_level(types, &algebra, dim)
621 } else {
622 infer_products_grade_level(types, &algebra)
624 }
625}
626
627fn infer_products_grade_level(types: &[TypeSpec], algebra: &Algebra) -> ProductsSpec {
629 let entities: Vec<(String, Vec<usize>)> = types
631 .iter()
632 .filter(|t| t.alias_of.is_none())
633 .map(|t| (t.name.clone(), t.grades.clone()))
634 .collect();
635
636 let convert_entries = |table: crate::discovery::ProductTable2D| -> Vec<ProductEntry> {
638 table
639 .entries
640 .into_iter()
641 .filter(|(_, _, result)| !result.is_zero && result.matching_entity.is_some())
642 .map(|(lhs, rhs, result)| {
643 let output = result.matching_entity.unwrap();
644 ProductEntry {
645 lhs,
646 rhs,
647 output: output.clone(),
648 output_constrained: false,
649 }
650 })
651 .collect()
652 };
653
654 ProductsSpec {
655 geometric: convert_entries(infer_all_products(
656 &entities,
657 ProductType::Geometric,
658 algebra,
659 )),
660 wedge: convert_entries(infer_all_products(
661 &entities,
662 ProductType::Exterior,
663 algebra,
664 )),
665 left_contraction: convert_entries(infer_all_products(
666 &entities,
667 ProductType::LeftContraction,
668 algebra,
669 )),
670 right_contraction: convert_entries(infer_all_products(
671 &entities,
672 ProductType::RightContraction,
673 algebra,
674 )),
675 antiwedge: convert_entries(infer_all_products(
676 &entities,
677 ProductType::Regressive,
678 algebra,
679 )),
680 scalar: convert_entries(infer_all_products(&entities, ProductType::Scalar, algebra)),
681 antigeometric: convert_entries(infer_all_products(
682 &entities,
683 ProductType::Antigeometric,
684 algebra,
685 )),
686 antiscalar: convert_entries(infer_all_products(
687 &entities,
688 ProductType::Antiscalar,
689 algebra,
690 )),
691 bulk_contraction: convert_entries(infer_all_products(
692 &entities,
693 ProductType::BulkContraction,
694 algebra,
695 )),
696 weight_contraction: convert_entries(infer_all_products(
697 &entities,
698 ProductType::WeightContraction,
699 algebra,
700 )),
701 bulk_expansion: convert_entries(infer_all_products(
702 &entities,
703 ProductType::BulkExpansion,
704 algebra,
705 )),
706 weight_expansion: convert_entries(infer_all_products(
707 &entities,
708 ProductType::WeightExpansion,
709 algebra,
710 )),
711 dot: convert_entries(infer_all_products(&entities, ProductType::Dot, algebra)),
712 antidot: convert_entries(infer_all_products(&entities, ProductType::Antidot, algebra)),
713 project: convert_entries(infer_all_products(&entities, ProductType::Project, algebra)),
714 antiproject: convert_entries(infer_all_products(
715 &entities,
716 ProductType::Antiproject,
717 algebra,
718 )),
719 }
720}
721
722fn infer_products_blade_level(types: &[TypeSpec], algebra: &Algebra, dim: usize) -> ProductsSpec {
724 let entities: Vec<EntityBladeSet> = types
726 .iter()
727 .filter(|t| t.alias_of.is_none())
728 .map(|t| {
729 if t.is_sparse {
730 let blades = t.fields.iter().map(|f| f.blade_index);
732 EntityBladeSet::new(t.name.clone(), blades)
733 } else {
734 EntityBladeSet::from_grades(t.name.clone(), t.grades.clone(), dim)
736 }
737 })
738 .collect();
739
740 let convert_entries =
742 |results: Vec<(String, String, crate::discovery::BladeProductResult)>| -> Vec<ProductEntry> {
743 results
744 .into_iter()
745 .filter(|(_, _, result)| !result.is_zero && result.matching_entity.is_some())
746 .map(|(lhs, rhs, result)| {
747 let output = result.matching_entity.unwrap();
748 ProductEntry {
749 lhs,
750 rhs,
751 output: output.clone(),
752 output_constrained: false,
753 }
754 })
755 .collect()
756 };
757
758 ProductsSpec {
759 geometric: convert_entries(infer_all_products_blades(
760 &entities,
761 ProductType::Geometric,
762 algebra,
763 )),
764 wedge: convert_entries(infer_all_products_blades(
765 &entities,
766 ProductType::Exterior,
767 algebra,
768 )),
769 left_contraction: convert_entries(infer_all_products_blades(
770 &entities,
771 ProductType::LeftContraction,
772 algebra,
773 )),
774 right_contraction: convert_entries(infer_all_products_blades(
775 &entities,
776 ProductType::RightContraction,
777 algebra,
778 )),
779 antiwedge: convert_entries(infer_all_products_blades(
780 &entities,
781 ProductType::Regressive,
782 algebra,
783 )),
784 scalar: convert_entries(infer_all_products_blades(
785 &entities,
786 ProductType::Scalar,
787 algebra,
788 )),
789 antigeometric: convert_entries(infer_all_products_blades(
790 &entities,
791 ProductType::Antigeometric,
792 algebra,
793 )),
794 antiscalar: convert_entries(infer_all_products_blades(
795 &entities,
796 ProductType::Antiscalar,
797 algebra,
798 )),
799 bulk_contraction: convert_entries(infer_all_products_blades(
800 &entities,
801 ProductType::BulkContraction,
802 algebra,
803 )),
804 weight_contraction: convert_entries(infer_all_products_blades(
805 &entities,
806 ProductType::WeightContraction,
807 algebra,
808 )),
809 bulk_expansion: convert_entries(infer_all_products_blades(
810 &entities,
811 ProductType::BulkExpansion,
812 algebra,
813 )),
814 weight_expansion: convert_entries(infer_all_products_blades(
815 &entities,
816 ProductType::WeightExpansion,
817 algebra,
818 )),
819 dot: convert_entries(infer_all_products_blades(
820 &entities,
821 ProductType::Dot,
822 algebra,
823 )),
824 antidot: convert_entries(infer_all_products_blades(
825 &entities,
826 ProductType::Antidot,
827 algebra,
828 )),
829 project: convert_entries(infer_all_products_blades(
830 &entities,
831 ProductType::Project,
832 algebra,
833 )),
834 antiproject: convert_entries(infer_all_products_blades(
835 &entities,
836 ProductType::Antiproject,
837 algebra,
838 )),
839 }
840}
841
842fn validate_spec(types: &[TypeSpec]) -> Result<(), ParseError> {
844 let type_names: HashSet<_> = types.iter().map(|t| t.name.as_str()).collect();
845
846 for ty in types {
848 if let Some(alias) = &ty.alias_of {
849 if !type_names.contains(alias.as_str()) {
850 return Err(ParseError::UnknownType(alias.clone()));
851 }
852 }
853 }
854
855 for ty in types {
857 if let Some(alias) = &ty.alias_of {
858 let target = types.iter().find(|t| t.name == *alias);
859 if let Some(target_type) = target {
860 if target_type.alias_of.as_ref() == Some(&ty.name) {
861 return Err(ParseError::AliasCycle {
862 type_name: ty.name.clone(),
863 });
864 }
865 }
866 }
867 }
868
869 Ok(())
870}
871
872fn check_algebra_completeness(
877 types: &[TypeSpec],
878 signature: &SignatureSpec,
879) -> Vec<MissingProduct> {
880 let algebra = Algebra::from_metrics(signature.metrics_by_index());
881
882 let entities: Vec<(String, Vec<usize>)> = types
885 .iter()
886 .filter(|t| t.alias_of.is_none() && !t.is_sparse)
887 .map(|t| (t.name.clone(), t.grades.clone()))
888 .collect();
889
890 let mut missing = Vec::new();
891
892 let product_types = [
894 ProductType::Geometric,
895 ProductType::Exterior,
896 ProductType::LeftContraction,
897 ProductType::RightContraction,
898 ProductType::Regressive,
899 ];
900
901 for product_type in &product_types {
902 let table = infer_all_products(&entities, *product_type, &algebra);
903
904 for (lhs, rhs, result) in table.entries {
905 if !result.is_zero && result.matching_entity.is_none() {
906 missing.push(MissingProduct {
907 lhs,
908 rhs,
909 product_type: product_type.toml_name().to_string(),
910 output_grades: result.output_grades,
911 });
912 }
913 }
914 }
915
916 missing
917}
918
919fn format_completeness_error(name: &str, missing: Vec<MissingProduct>) -> ParseError {
921 let mut by_grades: std::collections::HashMap<Vec<usize>, Vec<String>> =
923 std::collections::HashMap::new();
924
925 for m in &missing {
926 let entry = by_grades.entry(m.output_grades.clone()).or_default();
927 entry.push(format!("{} {} {}", m.lhs, m.product_type, m.rhs));
928 }
929
930 let mut details = String::new();
932 for (grades, products) in &by_grades {
933 details.push_str(&format!(" Missing type for grades {:?}:\n", grades));
934 for (i, product) in products.iter().enumerate() {
935 if i < 3 {
936 details.push_str(&format!(" - {}\n", product));
937 } else if i == 3 {
938 details.push_str(&format!(" ... and {} more\n", products.len() - 3));
939 break;
940 }
941 }
942 }
943
944 ParseError::IncompleteAlgebra {
945 name: name.to_string(),
946 count: missing.len(),
947 details,
948 }
949}
950
951#[cfg(test)]
952mod tests {
953 use super::*;
954
955 #[test]
956 fn parse_minimal_spec() {
957 let spec = parse_spec(
958 r#"
959 [algebra]
960 name = "test"
961
962 [signature]
963 positive = ["e1", "e2"]
964 "#,
965 )
966 .unwrap();
967
968 assert_eq!(spec.name, "test");
969 assert_eq!(spec.signature.p, 2);
970 assert_eq!(spec.signature.q, 0);
971 assert_eq!(spec.signature.r, 0);
972 assert_eq!(spec.signature.dim(), 2);
973 }
974
975 #[test]
976 fn parse_with_types() {
977 let spec = parse_spec(
978 r#"
979 [algebra]
980 name = "euclidean2"
981
982 [signature]
983 positive = ["e1", "e2"]
984
985 [types.Scalar]
986 grades = [0]
987 field_map = [{ name = "s", blade = "s" }]
988
989 [types.Vector]
990 grades = [1]
991 field_map = [
992 { name = "x", blade = "e1" },
993 { name = "y", blade = "e2" }
994 ]
995
996 [types.Bivector]
997 grades = [2]
998 field_map = [{ name = "b", blade = "e12" }]
999
1000 [types.Rotor]
1001 grades = [0, 2]
1002 field_map = [
1003 { name = "s", blade = "s" },
1004 { name = "xy", blade = "e12" }
1005 ]
1006 "#,
1007 )
1008 .unwrap();
1009
1010 assert_eq!(spec.types.len(), 4);
1011
1012 let vector = spec.types.iter().find(|t| t.name == "Vector").unwrap();
1013 assert_eq!(vector.grades, vec![1]);
1014 assert_eq!(vector.fields.len(), 2);
1015
1016 let rotor = spec.types.iter().find(|t| t.name == "Rotor").unwrap();
1017 assert_eq!(rotor.grades, vec![0, 2]);
1018 assert_eq!(rotor.fields.len(), 2);
1019 }
1020
1021 #[test]
1022 fn parse_blade_names_section() {
1023 let spec = parse_spec(
1024 r#"
1025 [algebra]
1026 name = "test"
1027
1028 [signature]
1029 positive = ["e1", "e2"]
1030
1031 [blades]
1032 e1 = "x"
1033 e2 = "y"
1034 e12 = "xy"
1035 "#,
1036 )
1037 .unwrap();
1038
1039 assert_eq!(spec.blade_names.get(&1), Some(&"x".to_string()));
1040 assert_eq!(spec.blade_names.get(&2), Some(&"y".to_string()));
1041 assert_eq!(spec.blade_names.get(&3), Some(&"xy".to_string()));
1042 }
1043
1044 #[test]
1045 fn reject_empty_signature() {
1046 let result = parse_spec(
1047 r#"
1048 [algebra]
1049 name = "test"
1050
1051 [signature]
1052 "#,
1053 );
1054
1055 assert!(matches!(result, Err(ParseError::EmptySignature)));
1056 }
1057
1058 #[test]
1059 fn reject_dimension_too_large() {
1060 let result = parse_spec(
1061 r#"
1062 [algebra]
1063 name = "test"
1064
1065 [signature]
1066 positive = ["e1", "e2", "e3", "e4", "e5", "e6", "e7"]
1067 "#,
1068 );
1069
1070 assert!(matches!(result, Err(ParseError::DimensionTooLarge(7))));
1071 }
1072
1073 #[test]
1074 fn reject_duplicate_basis_name() {
1075 let result = parse_spec(
1076 r#"
1077 [algebra]
1078 name = "test"
1079
1080 [signature]
1081 positive = ["e1", "e1"]
1082 "#,
1083 );
1084
1085 assert!(matches!(result, Err(ParseError::DuplicateBasisName(_))));
1086 }
1087
1088 #[test]
1089 fn reject_invalid_grade() {
1090 let result = parse_spec(
1091 r#"
1092 [algebra]
1093 name = "test"
1094
1095 [signature]
1096 positive = ["e1", "e2"]
1097
1098 [types.Bad]
1099 grades = [5]
1100 "#,
1101 );
1102
1103 assert!(matches!(
1104 result,
1105 Err(ParseError::InvalidGrade {
1106 grade: 5,
1107 max: 2,
1108 ..
1109 })
1110 ));
1111 }
1112
1113 #[test]
1114 fn sparse_type_with_subset_of_blades() {
1115 let spec = parse_spec(
1118 r#"
1119 [algebra]
1120 name = "test"
1121 complete = false
1122
1123 [signature]
1124 positive = ["e1", "e2", "e3"]
1125
1126 [types.Vector]
1127 grades = [1]
1128 field_map = [
1129 { name = "x", blade = "e1" },
1130 { name = "y", blade = "e2" }
1131 ]
1132 "#,
1133 )
1134 .unwrap();
1135
1136 let vector = spec.types.iter().find(|t| t.name == "Vector").unwrap();
1137 assert!(
1138 vector.is_sparse,
1139 "Type with subset of blades should be sparse"
1140 );
1141 assert_eq!(vector.fields.len(), 2);
1142 }
1143
1144 #[test]
1145 fn reject_duplicate_field_name() {
1146 let result = parse_spec(
1147 r#"
1148 [algebra]
1149 name = "test"
1150
1151 [signature]
1152 positive = ["e1", "e2"]
1153
1154 [types.Vector]
1155 grades = [1]
1156 field_map = [
1157 { name = "x", blade = "e1" },
1158 { name = "x", blade = "e2" }
1159 ]
1160 "#,
1161 );
1162
1163 assert!(matches!(result, Err(ParseError::DuplicateFieldName { .. })));
1164 }
1165
1166 #[test]
1167 fn reject_self_alias() {
1168 let result = parse_spec(
1169 r#"
1170 [algebra]
1171 name = "test"
1172
1173 [signature]
1174 positive = ["e1", "e2"]
1175
1176 [types.Rotor]
1177 grades = [0, 2]
1178 alias_of = "Rotor"
1179 "#,
1180 );
1181
1182 assert!(matches!(result, Err(ParseError::SelfAlias { .. })));
1183 }
1184
1185 #[test]
1186 fn parse_pga_signature() {
1187 let spec = parse_spec(
1188 r#"
1189 [algebra]
1190 name = "pga3"
1191
1192 [signature]
1193 positive = ["e1", "e2", "e3"]
1194 zero = ["e0"]
1195 "#,
1196 )
1197 .unwrap();
1198
1199 assert_eq!(spec.signature.p, 3);
1200 assert_eq!(spec.signature.q, 0);
1201 assert_eq!(spec.signature.r, 1);
1202 assert_eq!(spec.signature.dim(), 4);
1203 }
1204
1205 #[test]
1206 fn parse_cga_signature() {
1207 let spec = parse_spec(
1208 r#"
1209 [algebra]
1210 name = "cga3"
1211
1212 [signature]
1213 positive = ["e1", "e2", "e3", "ep"]
1214 negative = ["em"]
1215 "#,
1216 )
1217 .unwrap();
1218
1219 assert_eq!(spec.signature.p, 4);
1220 assert_eq!(spec.signature.q, 1);
1221 assert_eq!(spec.signature.r, 0);
1222 assert_eq!(spec.signature.dim(), 5);
1223 }
1224
1225 #[test]
1226 fn products_are_inferred_euclidean3() {
1227 let spec = parse_spec(include_str!("../../algebras/euclidean3.toml")).unwrap();
1228
1229 assert!(
1231 !spec.products.geometric.is_empty(),
1232 "Geometric products should be inferred"
1233 );
1234 assert!(
1235 !spec.products.wedge.is_empty(),
1236 "Wedge products should be inferred"
1237 );
1238 assert!(
1239 !spec.products.left_contraction.is_empty(),
1240 "Left contraction products should be inferred"
1241 );
1242 }
1243
1244 #[test]
1245 fn blade_indices_are_canonical_euclidean2() {
1246 let spec = parse_spec(include_str!("../../algebras/euclidean2.toml")).unwrap();
1247
1248 for ty in &spec.types {
1249 assert!(
1250 super::validate_canonical_field_order(ty),
1251 "Type {} in euclidean2.toml has non-canonical field ordering:\n{:?}",
1252 ty.name,
1253 ty.fields
1254 );
1255 }
1256 }
1257
1258 #[test]
1259 fn blade_indices_are_canonical_euclidean3() {
1260 let spec = parse_spec(include_str!("../../algebras/euclidean3.toml")).unwrap();
1261
1262 for ty in &spec.types {
1263 assert!(
1264 super::validate_canonical_field_order(ty),
1265 "Type {} in euclidean3.toml has non-canonical field ordering:\n{:?}",
1266 ty.name,
1267 ty.fields
1268 );
1269 }
1270 }
1271
1272 #[test]
1273 fn validate_canonical_order_function() {
1274 use super::super::ir::FieldSpec;
1276
1277 let valid_type = super::super::ir::TypeSpec {
1279 name: "Bivector".to_string(),
1280 grades: vec![2],
1281 description: None,
1282 fields: vec![
1283 FieldSpec {
1284 name: "xy".to_string(),
1285 blade_index: 3,
1286 grade: 2,
1287 sign: 1,
1288 },
1289 FieldSpec {
1290 name: "xz".to_string(),
1291 blade_index: 5,
1292 grade: 2,
1293 sign: 1,
1294 },
1295 FieldSpec {
1296 name: "yz".to_string(),
1297 blade_index: 6,
1298 grade: 2,
1299 sign: 1,
1300 },
1301 ],
1302 alias_of: None,
1303 versor: None,
1304 is_sparse: false,
1305 inverse_sandwich_targets: vec![],
1306 };
1307 assert!(super::validate_canonical_field_order(&valid_type));
1308
1309 let invalid_type = super::super::ir::TypeSpec {
1311 name: "Bivector".to_string(),
1312 grades: vec![2],
1313 description: None,
1314 fields: vec![
1315 FieldSpec {
1316 name: "yz".to_string(),
1317 blade_index: 6,
1318 grade: 2,
1319 sign: 1,
1320 }, FieldSpec {
1322 name: "xz".to_string(),
1323 blade_index: 5,
1324 grade: 2,
1325 sign: 1,
1326 }, FieldSpec {
1328 name: "xy".to_string(),
1329 blade_index: 3,
1330 grade: 2,
1331 sign: 1,
1332 }, ],
1334 alias_of: None,
1335 versor: None,
1336 is_sparse: false,
1337 inverse_sandwich_targets: vec![],
1338 };
1339 assert!(!super::validate_canonical_field_order(&invalid_type));
1340
1341 let valid_rotor = super::super::ir::TypeSpec {
1343 name: "Rotor".to_string(),
1344 grades: vec![0, 2],
1345 description: None,
1346 fields: vec![
1347 FieldSpec {
1348 name: "s".to_string(),
1349 blade_index: 0,
1350 grade: 0,
1351 sign: 1,
1352 },
1353 FieldSpec {
1354 name: "xy".to_string(),
1355 blade_index: 3,
1356 grade: 2,
1357 sign: 1,
1358 },
1359 FieldSpec {
1360 name: "xz".to_string(),
1361 blade_index: 5,
1362 grade: 2,
1363 sign: 1,
1364 },
1365 FieldSpec {
1366 name: "yz".to_string(),
1367 blade_index: 6,
1368 grade: 2,
1369 sign: 1,
1370 },
1371 ],
1372 alias_of: None,
1373 versor: None,
1374 is_sparse: false,
1375 inverse_sandwich_targets: vec![],
1376 };
1377 assert!(super::validate_canonical_field_order(&valid_rotor));
1378 }
1379
1380 #[test]
1381 fn parse_norm_default() {
1382 use super::super::ir::InvolutionKind;
1383
1384 let spec = parse_spec(
1385 r#"
1386 [algebra]
1387 name = "test"
1388
1389 [signature]
1390 positive = ["e1", "e2"]
1391 "#,
1392 )
1393 .unwrap();
1394
1395 assert_eq!(spec.norm.primary_involution, InvolutionKind::Reverse);
1397 }
1398
1399 #[test]
1400 fn parse_norm_reverse_explicit() {
1401 use super::super::ir::InvolutionKind;
1402
1403 let spec = parse_spec(
1404 r#"
1405 [algebra]
1406 name = "test"
1407
1408 [signature]
1409 positive = ["e1", "e2"]
1410
1411 [norm]
1412 primary_involution = "reverse"
1413 "#,
1414 )
1415 .unwrap();
1416
1417 assert_eq!(spec.norm.primary_involution, InvolutionKind::Reverse);
1418 }
1419
1420 #[test]
1421 fn parse_norm_grade_involution() {
1422 use super::super::ir::InvolutionKind;
1423
1424 let spec = parse_spec(
1425 r#"
1426 [algebra]
1427 name = "hyperbolic"
1428
1429 [signature]
1430 positive = ["e1"]
1431
1432 [norm]
1433 primary_involution = "grade_involution"
1434 "#,
1435 )
1436 .unwrap();
1437
1438 assert_eq!(
1439 spec.norm.primary_involution,
1440 InvolutionKind::GradeInvolution
1441 );
1442 }
1443
1444 #[test]
1445 fn parse_norm_clifford_conjugate() {
1446 use super::super::ir::InvolutionKind;
1447
1448 let spec = parse_spec(
1449 r#"
1450 [algebra]
1451 name = "test"
1452
1453 [signature]
1454 positive = ["e1"]
1455
1456 [norm]
1457 primary_involution = "clifford_conjugate"
1458 "#,
1459 )
1460 .unwrap();
1461
1462 assert_eq!(
1463 spec.norm.primary_involution,
1464 InvolutionKind::CliffordConjugate
1465 );
1466 }
1467
1468 #[test]
1469 fn reject_invalid_norm_involution() {
1470 let result = parse_spec(
1471 r#"
1472 [algebra]
1473 name = "test"
1474
1475 [signature]
1476 positive = ["e1"]
1477
1478 [norm]
1479 primary_involution = "invalid_involution"
1480 "#,
1481 );
1482
1483 assert!(matches!(result, Err(ParseError::InvalidValue { .. })));
1484 }
1485
1486 #[test]
1487 fn parse_sparse_type() {
1488 let spec = parse_spec(
1489 r#"
1490 [algebra]
1491 name = "conformal3"
1492 complete = false
1493
1494 [signature]
1495 positive = ["e1", "e2", "e3", "e4"]
1496 negative = ["e5"]
1497
1498 [types.Line]
1499 grades = [3]
1500 description = "Line (circle through infinity)"
1501 field_map = [
1502 { name = "vx", blade = "e145" },
1503 { name = "vy", blade = "e245" },
1504 { name = "vz", blade = "e345" },
1505 { name = "mx", blade = "e235" },
1506 { name = "my", blade = "e135" },
1507 { name = "mz", blade = "e125" }
1508 ]
1509 "#,
1510 )
1511 .unwrap();
1512
1513 let line = spec.types.iter().find(|t| t.name == "Line").unwrap();
1514 assert!(line.is_sparse, "Line should be marked as sparse");
1515 assert_eq!(line.grades, vec![3]);
1516 assert_eq!(line.fields.len(), 6, "Line should have 6 fields");
1517
1518 assert_eq!(
1521 line.fields[0].blade_index, 25,
1522 "e145 should be blade index 25"
1523 );
1524 assert_eq!(
1526 line.fields[3].blade_index, 22,
1527 "e235 should be blade index 22"
1528 );
1529 assert_eq!(
1531 line.fields[5].blade_index, 19,
1532 "e125 should be blade index 19"
1533 );
1534 }
1535
1536 #[test]
1537 fn reject_field_map_grade_mismatch() {
1538 let result = parse_spec(
1540 r#"
1541 [algebra]
1542 name = "test"
1543
1544 [signature]
1545 positive = ["e1", "e2", "e3"]
1546
1547 [types.Bad]
1548 grades = [2]
1549 field_map = [
1550 { name = "xyz", blade = "e123" }
1551 ]
1552 "#,
1553 );
1554
1555 assert!(matches!(
1556 result,
1557 Err(ParseError::FieldMapGradeMismatch { blade_grade: 3, .. })
1558 ));
1559 }
1560
1561 #[test]
1562 fn completeness_check_passes_for_complete_algebra() {
1563 let result = parse_spec(
1565 r#"
1566 [algebra]
1567 name = "euclidean2"
1568 complete = true
1569
1570 [signature]
1571 positive = ["e1", "e2"]
1572
1573 [types.Scalar]
1574 grades = [0]
1575 field_map = [{ name = "s", blade = "s" }]
1576
1577 [types.Vector]
1578 grades = [1]
1579 field_map = [
1580 { name = "x", blade = "e1" },
1581 { name = "y", blade = "e2" }
1582 ]
1583
1584 [types.Bivector]
1585 grades = [2]
1586 field_map = [{ name = "b", blade = "e12" }]
1587
1588 [types.Rotor]
1589 grades = [0, 2]
1590 field_map = [
1591 { name = "s", blade = "s" },
1592 { name = "xy", blade = "e12" }
1593 ]
1594 "#,
1595 );
1596
1597 assert!(
1598 result.is_ok(),
1599 "Complete euclidean2 algebra should pass: {:?}",
1600 result
1601 );
1602 }
1603
1604 #[test]
1605 fn completeness_check_fails_for_incomplete_algebra() {
1606 let result = parse_spec(
1608 r#"
1609 [algebra]
1610 name = "incomplete"
1611 complete = true
1612
1613 [signature]
1614 positive = ["e1", "e2"]
1615
1616 [types.Scalar]
1617 grades = [0]
1618 field_map = [{ name = "s", blade = "s" }]
1619
1620 [types.Vector]
1621 grades = [1]
1622 field_map = [
1623 { name = "x", blade = "e1" },
1624 { name = "y", blade = "e2" }
1625 ]
1626
1627 [types.Bivector]
1628 grades = [2]
1629 field_map = [{ name = "b", blade = "e12" }]
1630 "#,
1631 );
1632
1633 assert!(matches!(result, Err(ParseError::IncompleteAlgebra { .. })));
1634 if let Err(ParseError::IncompleteAlgebra { count, details, .. }) = result {
1635 assert!(count > 0, "Should have missing products");
1636 assert!(details.contains("[0, 2]"), "Should mention grades [0, 2]");
1637 }
1638 }
1639
1640 #[test]
1641 fn completeness_check_enabled_by_default() {
1642 let result = parse_spec(
1645 r#"
1646 [algebra]
1647 name = "incomplete"
1648
1649 [signature]
1650 positive = ["e1", "e2"]
1651
1652 [types.Scalar]
1653 grades = [0]
1654 field_map = [{ name = "s", blade = "s" }]
1655
1656 [types.Vector]
1657 grades = [1]
1658 field_map = [
1659 { name = "x", blade = "e1" },
1660 { name = "y", blade = "e2" }
1661 ]
1662
1663 [types.Bivector]
1664 grades = [2]
1665 field_map = [{ name = "b", blade = "e12" }]
1666 "#,
1667 );
1668
1669 assert!(matches!(result, Err(ParseError::IncompleteAlgebra { .. })));
1671 }
1672
1673 #[test]
1674 fn completeness_check_can_be_disabled() {
1675 let result = parse_spec(
1677 r#"
1678 [algebra]
1679 name = "incomplete"
1680 complete = false
1681
1682 [signature]
1683 positive = ["e1", "e2"]
1684
1685 [types.Scalar]
1686 grades = [0]
1687 field_map = [{ name = "s", blade = "s" }]
1688
1689 [types.Vector]
1690 grades = [1]
1691 field_map = [
1692 { name = "x", blade = "e1" },
1693 { name = "y", blade = "e2" }
1694 ]
1695
1696 [types.Bivector]
1697 grades = [2]
1698 field_map = [{ name = "b", blade = "e12" }]
1699 "#,
1700 );
1701
1702 assert!(
1704 result.is_ok(),
1705 "Incomplete algebra with complete=false should succeed: {:?}",
1706 result
1707 );
1708 }
1709
1710 #[test]
1711 fn require_field_map_for_types() {
1712 let result = parse_spec(
1714 r#"
1715 [algebra]
1716 name = "test"
1717 complete = false
1718
1719 [signature]
1720 positive = ["e1", "e2"]
1721
1722 [types.Vector]
1723 grades = [1]
1724 "#,
1725 );
1726
1727 assert!(matches!(result, Err(ParseError::MissingFieldMap { .. })));
1728 }
1729}