Skip to main content

clifford_codegen/spec/
parser.rs

1//! Parser for algebra specifications.
2//!
3//! Converts raw TOML structures to validated IR types.
4
5use 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
19/// Maximum supported dimension.
20const MAX_DIM: usize = 6;
21
22/// Parses a TOML specification into the IR.
23///
24/// # Arguments
25///
26/// * `toml_content` - The TOML specification as a string.
27///
28/// # Returns
29///
30/// A validated `AlgebraSpec` or an error describing what went wrong.
31///
32/// # Example
33///
34/// ```
35/// use clifford_codegen::spec::parse_spec;
36///
37/// let spec = parse_spec(r#"
38/// [algebra]
39/// name = "euclidean2"
40/// complete = false
41///
42/// [signature]
43/// positive = ["e1", "e2"]
44///
45/// [types.Vector]
46/// grades = [1]
47/// field_map = [
48///   { name = "x", blade = "e1" },
49///   { name = "y", blade = "e2" }
50/// ]
51/// "#).unwrap();
52///
53/// assert_eq!(spec.name, "euclidean2");
54/// assert_eq!(spec.signature.dim(), 2);
55/// ```
56pub fn parse_spec(toml_content: &str) -> Result<AlgebraSpec, ParseError> {
57    let raw: RawAlgebraSpec = toml::from_str(toml_content)?;
58
59    // Build signature
60    let signature = parse_signature(&raw.signature)?;
61
62    // Build norm configuration
63    let norm = parse_norm(&raw.norm)?;
64
65    // Build blade name map
66    let blade_names = parse_blade_names(&raw.blades, &signature)?;
67
68    // Build types
69    let types = parse_types(&raw.types, &signature, &blade_names)?;
70
71    // Auto-infer products from types (products section in TOML is ignored)
72    let products = infer_products_from_types(&types, &signature);
73
74    // Validate the complete specification
75    validate_spec(&types)?;
76
77    // Check algebra completeness if requested
78    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
99/// Parses the signature section.
100///
101/// Basis vectors can be specified in any order across the positive/negative/zero
102/// arrays. The index of each basis is determined by the number in its name
103/// (e.g., "e1" → index 0, "e2" → index 1), NOT by its position in the array.
104///
105/// This allows physics conventions like Minkowski with space (e1) negative and
106/// time (e2) positive:
107/// ```toml
108/// [signature]
109/// positive = ["e2"]  # e2² = +1 (time)
110/// negative = ["e1"]  # e1² = -1 (space)
111/// ```
112fn 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    // Check for duplicate names
126    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    // Determine indexing mode: numeric (e1, e2, etc.) or positional (ep, em, etc.)
139    // If ALL names are numeric, use numeric indexing; otherwise use positional
140    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    // Build basis vectors
152    let mut basis = Vec::with_capacity(dim);
153
154    if use_numeric_indexing {
155        // Numeric indexing: extract index from name (e.g., "e1" → index 0)
156        // This allows arbitrary metric assignment regardless of array order
157        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        // Sort by index for consistent ordering
183        basis.sort_by_key(|b| b.index);
184
185        // Validate that indices are contiguous 0..dim
186        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        // Positional indexing: assign indices in order (positive, then negative, then zero)
197        // Used for non-numeric basis names like "ep", "em" in CGA
198        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
228/// Tries to parse a basis vector name like "e1" or "e2" into its 0-based index.
229///
230/// Returns `Some(index)` if the name follows a numeric pattern, `None` otherwise.
231/// This is used to determine whether to use numeric or positional indexing for signatures.
232///
233/// # Naming Conventions
234///
235/// - Standard bases: "e1" → 0, "e2" → 1, "e3" → 2, etc. (1-indexed names, 0-indexed result)
236/// - PGA convention: "e0" → last index (dim-1) for backward compatibility
237fn 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    // Special case: e0 maps to the last index (PGA convention for degenerate basis)
250    if num == 0 {
251        return Some(dim - 1);
252    }
253
254    if num > dim {
255        return None;
256    }
257
258    // Convert to 0-based (e1 → 0, e2 → 1, etc.)
259    Some(num - 1)
260}
261
262/// Parses the norm configuration section.
263///
264/// The `[norm]` section specifies which involution the algebra uses for its
265/// canonical norm computation. This affects how `Involute` is generated.
266///
267/// Options for `primary_involution`:
268/// - `"reverse"` (default): Uses reverse involution, `(-1)^(k(k-1)/2)` for grade k
269/// - `"grade_involution"`: Uses grade involution, `(-1)^k` for grade k
270/// - `"clifford_conjugate"`: Uses Clifford conjugate, `(-1)^(k(k+1)/2)` for grade k
271fn 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
289/// Parses the blade names section.
290fn 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/// Result of parsing a blade name, including sign correction.
306#[derive(Debug, Clone, Copy)]
307pub struct BladeParseResult {
308    /// Canonical blade index (bitmask with bits in standard order).
309    pub index: usize,
310    /// Sign relative to canonical ordering (+1 or -1).
311    /// e.g., e20 = -e02, so sign = -1.
312    pub sign: i8,
313    /// Grade of the blade (number of basis vectors).
314    pub grade: usize,
315}
316
317/// Parses a blade name like "e12", "e20", "e312", or "s" into its canonical index and sign.
318///
319/// Non-canonical orderings (e.g., "e20" instead of "e02") are supported and will
320/// compute the appropriate sign correction based on permutation parity.
321///
322/// # Special cases
323///
324/// - "s" → index=0, sign=+1, grade=0 (scalar blade)
325///
326/// # Examples
327///
328/// - "s" → index=0, sign=+1, grade=0 (scalar)
329/// - "e1" → index=1 (0b1), sign=+1, grade=1
330/// - "e12" → index=3 (0b11), sign=+1 (already canonical)
331/// - "e21" → index=3 (0b11), sign=-1 (one swap needed)
332/// - "e20" → index=5 (0b101 for bases 0,2), sign=-1 (one swap: 20→02)
333/// - "e312" → index=7 (0b111), sign=+1 (312→132→123 = 2 swaps = even)
334fn parse_blade_with_sign(name: &str, dim: usize) -> Result<BladeParseResult, ParseError> {
335    // Special case: "s" for scalar (grade 0)
336    if name == "s" {
337        return Ok(BladeParseResult {
338            index: 0,
339            sign: 1,
340            grade: 0,
341        });
342    }
343
344    // Must start with 'e'
345    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    // Parse indices in the order given
359    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        // Convert to 0-based
374        let idx = digit - 1;
375
376        // Check for duplicates
377        if indices.contains(&idx) {
378            return Err(ParseError::InvalidBladeName {
379                name: name.to_string(),
380            });
381        }
382        indices.push(idx);
383    }
384
385    // Compute canonical bitmask
386    let mut index = 0usize;
387    for &idx in &indices {
388        index |= 1 << idx;
389    }
390
391    // Compute sign from permutation parity
392    // Count inversions: pairs (i,j) where i < j but indices[i] > indices[j]
393    let sign = permutation_sign(&indices);
394
395    let grade = indices.len();
396
397    Ok(BladeParseResult { index, sign, grade })
398}
399
400/// Computes the sign of a permutation by counting inversions.
401///
402/// Returns +1 for even permutations, -1 for odd permutations.
403fn 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
415/// Parses a blade name like "e12" or "e123" into its canonical index (legacy compatibility).
416///
417/// This is a wrapper around `parse_blade_with_sign` that only returns the index,
418/// ignoring the sign. Use `parse_blade_with_sign` for full sign support.
419fn parse_blade_index(name: &str, dim: usize) -> Result<usize, ParseError> {
420    Ok(parse_blade_with_sign(name, dim)?.index)
421}
422
423/// Parses the types section.
424fn 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    // Sort by name for deterministic output
443    types.sort_by(|a, b| a.name.cmp(&b.name));
444
445    Ok(types)
446}
447
448/// Parses a single type definition.
449fn 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    // Validate grades
457    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    // field_map is required for all non-alias types
468    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    // Build fields from field_map
475    let fields = build_fields_from_field_map(&raw.field_map, &raw.grades, dim, name)?;
476
477    // Check for duplicate field names
478    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    // Check self-alias
489    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    // Auto-identify versors based on grade parity
498    // A type is a versor if all grades have the same parity (all even or all odd)
499    // This includes single-grade types: vectors are reflectors, circles in CGA are inversors
500    let versor = if versor_parity(&raw.grades).is_some() {
501        Some(VersorSpec {
502            // is_unit will be determined by inferred constraints in Phase 4
503            is_unit: false,
504            // sandwich_targets are auto-inferred during code generation
505            sandwich_targets: Vec::new(),
506        })
507    } else {
508        None
509    };
510
511    // Determine if this is a sparse type (not all blades of the grade are present)
512    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
527/// Builds field specs from explicit field_map entries.
528///
529/// Each entry in field_map specifies a field name and its corresponding blade.
530/// Blade names can use non-canonical ordering (e.g., "e20" instead of "e02"),
531/// and the appropriate sign correction will be computed.
532fn 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        // Parse blade name with sign detection
542        let blade_result = parse_blade_with_sign(&mapping.blade, dim)?;
543
544        // Validate blade grade matches specified grades
545        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/// Validates that fields in a TypeSpec have canonical blade ordering.
567///
568/// Fields must be ordered by grade first (ascending), then by blade index
569/// within each grade (ascending). This ensures consistent behavior in
570/// product computations and conversions.
571///
572/// # Returns
573///
574/// `true` if the fields are in canonical order, `false` otherwise.
575#[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        // Fields must be ordered by grade first
586        if field.grade < prev_grade {
587            return false;
588        }
589
590        // Within a grade, blade indices must be ascending
591        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
602/// Infers products automatically from types.
603///
604/// Products are always auto-inferred from the defined types.
605/// All standard product types are generated: geometric, exterior, inner, left/right contraction,
606/// regressive, scalar, antigeometric, and antiscalar.
607///
608/// Sparse types are included in product inference using blade-level computation (PRD-45).
609/// This ensures that sparse types like Line and Plane can participate in products.
610fn infer_products_from_types(types: &[TypeSpec], signature: &SignatureSpec) -> ProductsSpec {
611    // Build algebra for product computation
612    let algebra = Algebra::from_metrics(signature.metrics_by_index());
613    let dim = signature.dim();
614
615    // Check if any types are sparse
616    let has_sparse = types.iter().any(|t| t.is_sparse && t.alias_of.is_none());
617
618    if has_sparse {
619        // Use blade-level inference for sparse type support
620        infer_products_blade_level(types, &algebra, dim)
621    } else {
622        // Use grade-level inference for better performance
623        infer_products_grade_level(types, &algebra)
624    }
625}
626
627/// Grade-level product inference (fast path for non-sparse algebras).
628fn infer_products_grade_level(types: &[TypeSpec], algebra: &Algebra) -> ProductsSpec {
629    // Build entity list for inference (exclude aliases)
630    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    // Convert inferred products to ProductEntry format
637    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
722/// Blade-level product inference (supports sparse types).
723fn infer_products_blade_level(types: &[TypeSpec], algebra: &Algebra, dim: usize) -> ProductsSpec {
724    // Build EntityBladeSet for each type
725    let entities: Vec<EntityBladeSet> = types
726        .iter()
727        .filter(|t| t.alias_of.is_none())
728        .map(|t| {
729            if t.is_sparse {
730                // Use exact blade indices from fields
731                let blades = t.fields.iter().map(|f| f.blade_index);
732                EntityBladeSet::new(t.name.clone(), blades)
733            } else {
734                // Use all blades of the grades
735                EntityBladeSet::from_grades(t.name.clone(), t.grades.clone(), dim)
736            }
737        })
738        .collect();
739
740    // Convert blade-level results to ProductEntry format
741    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
842/// Validates the complete specification.
843fn validate_spec(types: &[TypeSpec]) -> Result<(), ParseError> {
844    let type_names: HashSet<_> = types.iter().map(|t| t.name.as_str()).collect();
845
846    // Check alias references
847    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    // Check for alias cycles (simple check - no transitive cycles)
856    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
872/// Checks if all products between defined types have matching output types.
873///
874/// Returns a list of products that don't have matching output types.
875/// This is used when `complete = true` to enforce algebra completeness.
876fn check_algebra_completeness(
877    types: &[TypeSpec],
878    signature: &SignatureSpec,
879) -> Vec<MissingProduct> {
880    let algebra = Algebra::from_metrics(signature.metrics_by_index());
881
882    // Build entity list (exclude sparse and alias types for now)
883    // TODO: PRD-45 will add blade-level inference for sparse types
884    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    // Check all product types
893    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
919/// Formats a completeness error with detailed information about missing products.
920fn format_completeness_error(name: &str, missing: Vec<MissingProduct>) -> ParseError {
921    // Group by output grades
922    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    // Format details
931    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        // With field_map, providing fewer blades than the grade has is valid
1116        // and marks the type as sparse
1117        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        // All product types should be inferred
1230        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        // Test the validation function with synthetic data
1275        use super::super::ir::FieldSpec;
1276
1277        // Valid canonical ordering for grade 2 in 3D
1278        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        // Invalid: wrong order within grade
1310        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                }, // Wrong!
1321                FieldSpec {
1322                    name: "xz".to_string(),
1323                    blade_index: 5,
1324                    grade: 2,
1325                    sign: 1,
1326                }, // Wrong!
1327                FieldSpec {
1328                    name: "xy".to_string(),
1329                    blade_index: 3,
1330                    grade: 2,
1331                    sign: 1,
1332                }, // Wrong!
1333            ],
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        // Valid multi-grade type
1342        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        // Default should be Reverse
1396        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        // Verify blade indices were parsed correctly
1519        // e145 -> bits 0, 3, 4 -> 0b11001 = 25
1520        assert_eq!(
1521            line.fields[0].blade_index, 25,
1522            "e145 should be blade index 25"
1523        );
1524        // e235 -> bits 1, 2, 4 -> 0b10110 = 22
1525        assert_eq!(
1526            line.fields[3].blade_index, 22,
1527            "e235 should be blade index 22"
1528        );
1529        // e125 -> bits 0, 1, 4 -> 0b10011 = 19
1530        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        // Test that a blade grade mismatch in field_map is rejected
1539        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        // Euclidean 2D has all required types
1564        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        // Missing Rotor type means Vector × Vector has no output
1607        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        // Same incomplete algebra but without complete = true
1643        // Since default is complete = true, this should fail
1644        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        // Should fail because complete defaults to true
1670        assert!(matches!(result, Err(ParseError::IncompleteAlgebra { .. })));
1671    }
1672
1673    #[test]
1674    fn completeness_check_can_be_disabled() {
1675        // Same incomplete algebra with complete = false
1676        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        // Should succeed because complete = false explicitly disables the check
1703        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        // Test that types without field_map are rejected
1713        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}