//! The five W3 §3E invariants (`spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md` //! lines 472-487), **asserted**, not merely honoured (task requirement, //! recipe §5). Every checker here returns `Result<(), String>` naming the //! fixture and the invariant, so a caller can either `.expect()` it (the //! generator does, and a violation is a hard failure) or assert it rejects a //! deliberately broken case (the mutation tests in this module's `tests` //! submodule do that, per the task's mutation-first requirement). use crate::types::{SpikeCluster, SpikeResolvedText, SpikeShapedSegment}; /// Invariant 1: every `ClusterMap` offset and every `ShapedSegment::source` /// bound is a valid UTF-8 boundary in `text`. pub fn assert_utf8_boundaries(fixture_id: &str, rt: &SpikeResolvedText) -> Result<(), String> { let check = |label: &str, offset: u32| -> Result<(), String> { let o = offset as usize; if o > rt.text.len() || !rt.text.is_char_boundary(o) { return Err(format!( "{fixture_id}: invariant 1 (UTF-8 boundaries) violated: {label} offset {o} is not \ a char boundary in a {}-byte string", rt.text.len() )); } Ok(()) }; for (i, seg) in rt.segments.iter().enumerate() { check(&format!("segment[{i}].source.start"), seg.source.start)?; check(&format!("segment[{i}].source.end"), seg.source.end)?; } for (i, c) in rt.clusters.clusters.iter().enumerate() { check(&format!("cluster[{i}].source.start"), c.source.start)?; check(&format!("cluster[{i}].source.end"), c.source.end)?; for (j, stop) in c.caret_stops.iter().enumerate() { check( &format!("cluster[{i}].caret_stops[{j}].source_offset"), stop.source_offset, )?; } } Ok(()) } /// Invariant 2: segment source ranges cover the whole string, totally and /// without invalid overlap, in **logical** order (visual order may differ /// under bidi — this crate stores `segments` in logical order throughout, /// see `crate::shape`). pub fn assert_segments_partition_totally( fixture_id: &str, rt: &SpikeResolvedText, ) -> Result<(), String> { let mut expected_next: u32 = 0; for (i, seg) in rt.segments.iter().enumerate() { if seg.source.start != expected_next { return Err(format!( "{fixture_id}: invariant 2 (total partition) violated: segment[{i}] starts at byte \ {}, but the partition left off at {expected_next} — a gap or overlap", seg.source.start )); } if seg.source.end < seg.source.start { return Err(format!( "{fixture_id}: invariant 2 violated: segment[{i}] has end {} < start {}", seg.source.end, seg.source.start )); } expected_next = seg.source.end; } let total = rt.text.len() as u32; if expected_next != total { return Err(format!( "{fixture_id}: invariant 2 (total partition) violated: segments cover up to byte \ {expected_next}, but the string is {total} bytes" )); } Ok(()) } /// Invariant 3: every cluster carries its source range (structural — always /// present), its glyph indices, and its caret stops, **each stop with a /// geometric position and a bidi affinity** (structural — the type has no /// way to omit either). What is *not* structurally guaranteed, and is /// checked here: every cluster has **at least one** caret stop (a cluster /// with none would silently have no caret contract at all), and every glyph /// index a resolved cluster names actually exists in its segment's glyph /// list. pub fn assert_clusters_carry_required_fields( fixture_id: &str, rt: &SpikeResolvedText, ) -> Result<(), String> { for (i, c) in rt.clusters.clusters.iter().enumerate() { if c.caret_stops.is_empty() { return Err(format!( "{fixture_id}: invariant 3 violated: cluster[{i}] ({:?}) carries no caret stops", c.source )); } if c.segment >= rt.segments.len() { return Err(format!( "{fixture_id}: invariant 3 violated: cluster[{i}] names segment {} but there are \ only {} segments", c.segment, rt.segments.len() )); } let seg = &rt.segments[c.segment]; for &gi in &c.glyph_indices { if gi as usize >= seg.glyphs.len() { return Err(format!( "{fixture_id}: invariant 3 violated: cluster[{i}] names glyph index {gi} in \ segment {}, which has only {} glyphs", c.segment, seg.glyphs.len() )); } } } Ok(()) } /// Invariant 4: a cluster that shaping could not resolve is represented /// **diagnostically** (an explicit unresolved marker), never dropped. /// `expect_unresolved` names whether *this* fixture is expected to contain /// one (only F-C does); when it is, this also checks the marker's segment /// carries no glyphs and no face (`crate::findings::W3_F3`) — a "resolved: /// false" flag with glyphs attached would be a marker that lies. pub fn assert_unresolved_clusters_are_diagnostic( fixture_id: &str, rt: &SpikeResolvedText, expect_unresolved: bool, ) -> Result<(), String> { let unresolved: Vec<&SpikeCluster> = rt .clusters .clusters .iter() .filter(|c| !c.resolved) .collect(); if expect_unresolved && unresolved.is_empty() { return Err(format!( "{fixture_id}: invariant 4 violated: this fixture is expected to carry an unresolved \ cluster, but none is present — an uncovered codepoint must never be silently dropped" )); } if !expect_unresolved && !unresolved.is_empty() { return Err(format!( "{fixture_id}: invariant 4 check mismatch: this fixture is not expected to carry an \ unresolved cluster, but {} are present", unresolved.len() )); } for c in &unresolved { if !c.glyph_indices.is_empty() { return Err(format!( "{fixture_id}: invariant 4 violated: unresolved cluster {:?} carries glyph \ indices — an unresolved marker that also claims glyphs is not diagnostic, it is \ a silent substitution", c.source )); } let seg = &rt.segments[c.segment]; if seg.face.is_some() { return Err(format!( "{fixture_id}: invariant 4 violated: unresolved cluster {:?} belongs to a segment \ that names a face ({:?}) — an unresolved cluster's segment must be the \ `face: None` marker (crate::findings::W3_F3)", c.source, seg.face )); } } Ok(()) } /// Invariant 5: positions are staff-space, y-up, quantized on the /// `crate::QUANTIZE_GRID` (`1/1024`) grid — the same convention as glyph /// positions, so text quantization is not a second convention. pub fn assert_positions_quantized(fixture_id: &str, rt: &SpikeResolvedText) -> Result<(), String> { let check = |label: String, x: f64, y: f64| -> Result<(), String> { if !crate::quantize::is_on_grid(x) || !crate::quantize::is_on_grid(y) { return Err(format!( "{fixture_id}: invariant 5 (1/1024 grid) violated: {label} at ({x}, {y}) is not \ on the grid" )); } Ok(()) }; for (i, seg) in rt.segments.iter().enumerate() { for (j, g) in seg.glyphs.iter().enumerate() { check( format!("segment[{i}].glyphs[{j}].offset"), g.offset.x, g.offset.y, )?; } } for (i, c) in rt.clusters.clusters.iter().enumerate() { for (j, s) in c.caret_stops.iter().enumerate() { check( format!("cluster[{i}].caret_stops[{j}].position"), s.position.x, s.position.y, )?; } } check("origin".to_string(), rt.origin.x, rt.origin.y)?; check( "bounds.left/bottom".to_string(), rt.bounds.left, rt.bounds.bottom, )?; check( "bounds.right/top".to_string(), rt.bounds.right, rt.bounds.top, )?; check( "reserved_box.left/bottom".to_string(), rt.reserved_box.left, rt.reserved_box.bottom, )?; check( "reserved_box.right/top".to_string(), rt.reserved_box.right, rt.reserved_box.top, )?; Ok(()) } /// Extra, recipe-specific check (§7, F-D): a direction-boundary offset /// carries two caret stops with **different affinities at different /// geometric positions** — not merely two stops that happen to coincide, /// which would satisfy invariant 3's cardinality but defeat the reason /// affinity exists. /// /// **Applied to F-D and deliberately NOT to F-C** (recipe §12, W3-F4). F-C's /// byte 5 is also a direction boundary — Latin LTR into Arabic RTL — but its /// downstream side is an *unresolved, zero-advance* cluster, so both /// affinities necessarily land on the identical position (measured: staff-space /// x = 3.130859375 for both). Enforcing distinctness there would demand a /// difference that cannot exist, so the exemption is correct; it is written /// down because an unstated exemption is indistinguishable from having /// forgotten the case. The caller decides which boundaries to check, and the /// recipe records why. pub fn assert_direction_boundary_stops_differ( fixture_id: &str, rt: &SpikeResolvedText, boundary_offset: u32, ) -> Result<(), String> { let stops: Vec<_> = rt .clusters .clusters .iter() .flat_map(|c| c.caret_stops.iter()) .filter(|s| s.source_offset == boundary_offset) .collect(); if stops.len() != 2 { return Err(format!( "{fixture_id}: byte {boundary_offset} must carry exactly 2 caret stops (one per \ affinity), found {}", stops.len() )); } let (a, b) = (stops[0], stops[1]); if a.affinity == b.affinity { return Err(format!( "{fixture_id}: byte {boundary_offset}'s two stops share affinity {:?} instead of one \ each", a.affinity )); } let dx = (a.position.x - b.position.x).abs(); let dy = (a.position.y - b.position.y).abs(); if dx < 1e-9 && dy < 1e-9 { return Err(format!( "{fixture_id}: byte {boundary_offset}'s two stops sit at the identical position \ {:?} — affinity exists precisely so these differ", a.position )); } Ok(()) } /// Runs invariants 1, 2, 3, 5 (universal) plus invariant 4 (parameterized by /// whether this fixture is expected to carry an unresolved cluster). Every /// fixture in this recipe calls this; F-D additionally calls /// [`assert_direction_boundary_stops_differ`] at its two direction /// boundaries (see `crate::fixtures`). pub fn assert_all(fixture_id: &str, rt: &SpikeResolvedText, expect_unresolved: bool) { assert_utf8_boundaries(fixture_id, rt).expect("invariant 1"); assert_segments_partition_totally(fixture_id, rt).expect("invariant 2"); assert_clusters_carry_required_fields(fixture_id, rt).expect("invariant 3"); assert_unresolved_clusters_are_diagnostic(fixture_id, rt, expect_unresolved) .expect("invariant 4"); assert_positions_quantized(fixture_id, rt).expect("invariant 5"); } /// Exposed for `crate::output`'s validator and for cross-crate tests that /// want to confirm a `SpikeShapedSegment`'s face/glyph coherence without /// depending on `crate::shape` directly. pub fn segment_is_unresolved_marker(seg: &SpikeShapedSegment) -> bool { seg.face.is_none() && seg.glyphs.is_empty() } #[cfg(test)] mod tests { use super::*; use crate::identity::{ SemVerRecord, SpikeShaperId, SpikeTextShapingIdentity, SpikeUnicodeComponent, }; use crate::types::{ SpikeBoundingBox, SpikeCaretAffinity, SpikeCaretStop, SpikeCluster, SpikeClusterMap, SpikeGlyphStyle, SpikeLanguageTag, SpikePoint, SpikePositionedGlyph, SpikeProvenance, SpikeResolvedText, SpikeScriptTag, SpikeShapedSegment, SpikeStaffSpace, SpikeTextAlign, SpikeTextDirection, }; fn dummy_identity() -> SpikeTextShapingIdentity { SpikeTextShapingIdentity { faces: Vec::new(), shaper: SpikeShaperId("rustybuzz".to_string()), shaper_version: SemVerRecord { major: 0, minor: 20, patch: 1, }, features: Vec::new(), unicode_bidi: SpikeUnicodeComponent { impl_name: "unicode-bidi".to_string(), crate_version: "0.3.18".to_string(), unicode_version: Some("16.0.0".to_string()), }, unicode_segmentation: SpikeUnicodeComponent { impl_name: "unicode-segmentation".to_string(), crate_version: "1.13.3".to_string(), unicode_version: Some("17.0.0".to_string()), }, } } fn dummy_provenance() -> SpikeProvenance { SpikeProvenance { source: crate::types::SpikeTypedObjectId { discriminant: 0, canonical_bytes_hex: "00".repeat(18), }, synthesis: None, dependencies: Vec::new(), stable_id: 0, } } /// A minimal, valid two-byte "ab" run: one segment, one cluster per /// grapheme, both invariants-satisfying. Every mutation test below /// starts from this and breaks exactly one thing. fn minimal_valid() -> SpikeResolvedText { SpikeResolvedText { provenance: dummy_provenance(), text: "ab".to_string(), shaping: dummy_identity(), segments: vec![SpikeShapedSegment { face: Some(0), glyphs: vec![ SpikePositionedGlyph { glyph_id: 1, offset: SpikePoint::new(0.0, 0.0), transform: None, }, SpikePositionedGlyph { glyph_id: 2, offset: SpikePoint::new(0.5, 0.0), transform: None, }, ], source: 0..2, direction: SpikeTextDirection::Ltr, script: SpikeScriptTag("Latn".to_string()), language: SpikeLanguageTag(None), size: SpikeStaffSpace(1.28), }], clusters: SpikeClusterMap { clusters: vec![ SpikeCluster { source: 0..1, segment: 0, glyph_indices: vec![0], resolved: true, grapheme_count: 1, caret_stops: vec![SpikeCaretStop { source_offset: 0, position: SpikePoint::new(0.0, 0.0), affinity: SpikeCaretAffinity::Downstream, }], }, SpikeCluster { source: 1..2, segment: 0, glyph_indices: vec![1], resolved: true, grapheme_count: 1, caret_stops: vec![SpikeCaretStop { source_offset: 1, position: SpikePoint::new(0.5, 0.0), affinity: SpikeCaretAffinity::Downstream, }], }, ], }, bounds: SpikeBoundingBox { left: 0.0, bottom: 0.0, right: 1.0, top: 1.0, }, reserved_box: SpikeBoundingBox { left: 0.0, bottom: 0.0, right: 1.0, top: 1.0, }, // Recipe §3's nominal `1.6` is not itself on the `1/1024` grid // (see `crate::quantize`'s doc comment) — this test fixture uses // the honestly-quantized value, `1638/1024`, exactly as // `crate::fixtures::build_fixture` does for the real run. origin: SpikePoint::new(1638.0 / 1024.0, 0.0), align: SpikeTextAlign::Start, style: SpikeGlyphStyle { rgba: 0x0000_00ff }, layer: 0, } } #[test] fn minimal_valid_passes_every_invariant() { let rt = minimal_valid(); assert_utf8_boundaries("T", &rt).unwrap(); assert_segments_partition_totally("T", &rt).unwrap(); assert_clusters_carry_required_fields("T", &rt).unwrap(); assert_unresolved_clusters_are_diagnostic("T", &rt, false).unwrap(); assert_positions_quantized("T", &rt).unwrap(); } // ---- Invariant 1: UTF-8 boundary mutations ---- #[test] fn invariant_1_kills_a_mid_char_segment_bound() { let mut rt = minimal_valid(); rt.text = "é".to_string(); // 2-byte UTF-8, so offset 1 is mid-char rt.segments[0].source = 0..1; // deliberately not a char boundary let err = assert_utf8_boundaries("T", &rt).unwrap_err(); assert!(err.contains("invariant 1"), "{err}"); } #[test] fn invariant_1_kills_an_out_of_range_cluster_offset() { let mut rt = minimal_valid(); rt.clusters.clusters[0].caret_stops[0].source_offset = 99; let err = assert_utf8_boundaries("T", &rt).unwrap_err(); assert!(err.contains("invariant 1"), "{err}"); } // ---- Invariant 2: partition mutations ---- #[test] fn invariant_2_kills_a_gap_between_segments() { let mut rt = minimal_valid(); rt.segments.push(SpikeShapedSegment { face: Some(0), glyphs: vec![], source: 2..2, // no-op segment, but let's actually make a real gap instead direction: SpikeTextDirection::Ltr, script: SpikeScriptTag("Latn".to_string()), language: SpikeLanguageTag(None), size: SpikeStaffSpace(1.28), }); // Force a genuine gap: first segment now claims to stop at byte 1, // leaving byte 1..2 uncovered. rt.segments[0].source = 0..1; rt.segments[1].source = 2..2; let err = assert_segments_partition_totally("T", &rt).unwrap_err(); assert!(err.contains("invariant 2"), "{err}"); } #[test] fn invariant_2_kills_coverage_stopping_short_of_the_string_end() { let mut rt = minimal_valid(); rt.segments[0].source = 0..1; // string is 2 bytes; this covers only 1 let err = assert_segments_partition_totally("T", &rt).unwrap_err(); assert!(err.contains("invariant 2"), "{err}"); } // ---- Invariant 3: cluster field mutations ---- #[test] fn invariant_3_kills_a_cluster_with_no_caret_stops() { let mut rt = minimal_valid(); rt.clusters.clusters[0].caret_stops.clear(); let err = assert_clusters_carry_required_fields("T", &rt).unwrap_err(); assert!(err.contains("invariant 3"), "{err}"); } #[test] fn invariant_3_kills_a_glyph_index_out_of_range() { let mut rt = minimal_valid(); rt.clusters.clusters[0].glyph_indices = vec![99]; let err = assert_clusters_carry_required_fields("T", &rt).unwrap_err(); assert!(err.contains("invariant 3"), "{err}"); } // ---- Invariant 4: unresolved-marker mutations ---- #[test] fn invariant_4_kills_a_dropped_unresolved_cluster() { let rt = minimal_valid(); // This fixture carries zero unresolved clusters; asking the checker // to require one (as F-C's caller does) must fail. let err = assert_unresolved_clusters_are_diagnostic("T", &rt, true).unwrap_err(); assert!(err.contains("invariant 4"), "{err}"); } #[test] fn invariant_4_kills_an_unresolved_cluster_that_still_carries_glyphs() { let mut rt = minimal_valid(); rt.segments.push(SpikeShapedSegment { face: None, glyphs: vec![], source: 2..2, direction: SpikeTextDirection::Ltr, script: SpikeScriptTag("Zzzz".to_string()), language: SpikeLanguageTag(None), size: SpikeStaffSpace(1.28), }); rt.clusters.clusters.push(SpikeCluster { source: 2..2, segment: 1, glyph_indices: vec![0], // a marker that also claims a glyph — invalid resolved: false, grapheme_count: 1, caret_stops: vec![SpikeCaretStop { source_offset: 2, position: SpikePoint::new(1.0, 0.0), affinity: SpikeCaretAffinity::Downstream, }], }); let err = assert_unresolved_clusters_are_diagnostic("T", &rt, true).unwrap_err(); assert!(err.contains("invariant 4"), "{err}"); } #[test] fn invariant_4_kills_an_unresolved_marker_whose_segment_names_a_face() { let mut rt = minimal_valid(); rt.segments.push(SpikeShapedSegment { face: Some(0), // should be None for an unresolved marker glyphs: vec![], source: 2..2, direction: SpikeTextDirection::Ltr, script: SpikeScriptTag("Zzzz".to_string()), language: SpikeLanguageTag(None), size: SpikeStaffSpace(1.28), }); rt.clusters.clusters.push(SpikeCluster { source: 2..2, segment: 1, glyph_indices: vec![], resolved: false, grapheme_count: 1, caret_stops: vec![SpikeCaretStop { source_offset: 2, position: SpikePoint::new(1.0, 0.0), affinity: SpikeCaretAffinity::Downstream, }], }); let err = assert_unresolved_clusters_are_diagnostic("T", &rt, true).unwrap_err(); assert!(err.contains("invariant 4"), "{err}"); } // ---- Invariant 5: quantization mutations ---- #[test] fn invariant_5_kills_an_off_grid_glyph_offset() { let mut rt = minimal_valid(); rt.segments[0].glyphs[0].offset.x = 0.1234567; // not a multiple of 1/1024 let err = assert_positions_quantized("T", &rt).unwrap_err(); assert!(err.contains("invariant 5"), "{err}"); } #[test] fn invariant_5_kills_an_off_grid_caret_stop() { let mut rt = minimal_valid(); rt.clusters.clusters[0].caret_stops[0].position.y = 0.0009; let err = assert_positions_quantized("T", &rt).unwrap_err(); assert!(err.contains("invariant 5"), "{err}"); } // ---- Direction-boundary distinctness ---- #[test] fn boundary_check_kills_two_stops_at_the_same_position() { let mut rt = minimal_valid(); rt.clusters.clusters[0].caret_stops.push(SpikeCaretStop { source_offset: 0, position: SpikePoint::new(0.0, 0.0), // identical to the existing stop at offset 0 affinity: SpikeCaretAffinity::Upstream, }); let err = assert_direction_boundary_stops_differ("T", &rt, 0).unwrap_err(); assert!(err.contains("identical position"), "{err}"); } #[test] fn boundary_check_kills_two_stops_sharing_one_affinity() { let mut rt = minimal_valid(); rt.clusters.clusters[0].caret_stops.push(SpikeCaretStop { source_offset: 0, position: SpikePoint::new(9.0, 0.0), // different position... affinity: SpikeCaretAffinity::Downstream, // ...but same affinity as the existing one }); let err = assert_direction_boundary_stops_differ("T", &rt, 0).unwrap_err(); assert!(err.contains("share affinity"), "{err}"); } #[test] fn boundary_check_kills_a_missing_second_stop() { let rt = minimal_valid(); // Only one stop exists at offset 0. let err = assert_direction_boundary_stops_differ("T", &rt, 0).unwrap_err(); assert!(err.contains("exactly 2"), "{err}"); } #[test] fn boundary_check_accepts_two_distinct_stops() { let mut rt = minimal_valid(); rt.clusters.clusters[0].caret_stops.push(SpikeCaretStop { source_offset: 0, position: SpikePoint::new(-1.0, 0.0), affinity: SpikeCaretAffinity::Upstream, }); assert_direction_boundary_stops_differ("T", &rt, 0).unwrap(); } }