Skip to main content

codehelion_core/semantic/
normalization.rs

1use super::{
2    BTreeSet, DirectPropagation, FallibleKind, Language, OperationAttributes, OperationEdge,
3    OperationEdgeKind, OperationKind, OperationNode, SemanticGraphError, SemanticOperationGraph,
4    SemanticRuleMatcher, SemanticRuleScope, TypeTag, cross_language_api_correspondence,
5    match_same_variant_rule, registered_rules,
6};
7
8/// One compiler-independent observation eligible for a registered SOG rule.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct OperationObservation {
11    /// Source-order position supplied by the adapter; it is not a stable ID.
12    pub source_offset: u64,
13    /// Compiler-resolved API spelling, without a helper-specific wrapper.
14    pub api_name: String,
15    /// Resolved operated-value category, when the compiler supplied one.
16    pub type_tag: Option<TypeTag>,
17}
18
19/// One compiler-confirmed non-API operation eligible for a registered SOG rule.
20///
21/// The protocol adapter maps its helper-specific construct vocabulary into
22/// this closed core vocabulary before calling the normalizer.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ConstructObservation {
25    /// Source-order position supplied by the adapter; it is not a stable ID.
26    pub source_offset: u64,
27    /// Closed SOG operation the compiler established.
28    pub kind: OperationKind,
29    /// Standard fallible container the compiler resolved for this operation.
30    ///
31    /// `None` is never treated as interchangeable with either known variant
32    /// by a registered rule.
33    pub fallible_kind: Option<FallibleKind>,
34    /// Closed direct-propagation spelling the helper confirmed, when any.
35    pub direct_propagation: Option<DirectPropagation>,
36    /// Registered resource category for a compiler-confirmed acquire or
37    /// release operation. It is absent for every other construct.
38    pub resource_kind: Option<String>,
39}
40
41/// Ephemeral source range attached to one normalized operation.
42///
43/// This is reporting evidence rather than graph data. In particular, changing
44/// a range does not change [`SemanticOperationGraph`] serialization or its
45/// fingerprint.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
47pub struct SemanticSourceRange {
48    /// Inclusive source byte offset.
49    pub start: u64,
50    /// Exclusive source byte offset.
51    pub end: u64,
52}
53
54/// One bounded source fragment whose graph satisfies a registered rule when
55/// compared with itself.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct SemanticGraphWindow {
58    /// Normalized graph for this fragment only.
59    pub graph: SemanticOperationGraph,
60    /// Source range covering the retained operations.
61    pub source_range: SemanticSourceRange,
62}
63
64/// The result of normalizing registered API observations.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct ApiNormalization {
67    /// A graph when at least one observation matches a registered operation.
68    pub graph: Option<SemanticOperationGraph>,
69    /// Source ranges aligned with [`Self::graph`] nodes.
70    ///
71    /// These ranges are intentionally omitted from the graph and therefore do
72    /// not affect its schema or stable fingerprints.
73    pub node_source_ranges: Vec<SemanticSourceRange>,
74    /// Observations deliberately left outside the restricted vocabulary.
75    pub excluded_observations: usize,
76}
77
78/// Normalize only APIs covered by the initial, explicit operation registry.
79///
80/// The caller supplies compiler-resolved names and source order. Every
81/// unregistered name is counted but not approximated; this function therefore
82/// remains a core-side normalization stage rather than a helper dependency.
83///
84/// # Errors
85///
86/// Returns [`SemanticGraphError`] only if the deterministic graph assembled
87/// from registered operations violates its own invariant.
88pub fn normalize_registered_apis(
89    language: Language,
90    build_variant_fingerprint: [u8; 32],
91    observations: Vec<OperationObservation>,
92) -> Result<ApiNormalization, SemanticGraphError> {
93    normalize_registered_observations(
94        language,
95        build_variant_fingerprint,
96        observations,
97        Vec::new(),
98    )
99}
100
101/// Normalize registered API and compiler-confirmed construct observations.
102///
103/// API names remain fail-closed against the registry. Constructs have already
104/// crossed the compiler/protocol boundary and are accepted only when they use
105/// the fixed SOG vocabulary; no frontend syntax type enters core.
106///
107/// # Errors
108///
109/// Returns [`SemanticGraphError`] only if the deterministic graph assembled
110/// from registered operations violates its own invariant.
111pub fn normalize_registered_observations(
112    language: Language,
113    build_variant_fingerprint: [u8; 32],
114    observations: Vec<OperationObservation>,
115    constructs: Vec<ConstructObservation>,
116) -> Result<ApiNormalization, SemanticGraphError> {
117    let api_with_ranges = observations.into_iter().map(|observation| {
118        let range = SemanticSourceRange {
119            start: observation.source_offset,
120            end: observation.source_offset,
121        };
122        (observation, range)
123    });
124    let constructs_with_ranges = constructs.into_iter().map(|construct| {
125        let range = SemanticSourceRange {
126            start: construct.source_offset,
127            end: construct.source_offset,
128        };
129        (construct, range)
130    });
131    normalize_registered_observations_with_ranges(
132        language,
133        build_variant_fingerprint,
134        api_with_ranges.collect(),
135        constructs_with_ranges.collect(),
136    )
137}
138
139/// Normalize registered observations with their exact source ranges.
140///
141/// This variant is for protocol adapters that retain anchors. The ranges stay
142/// beside the normalized graph so a caller can report a bounded fragment
143/// without introducing source positions into fingerprints.
144///
145/// # Errors
146///
147/// Returns [`SemanticGraphError`] when a range is reversed or when the
148/// deterministic graph assembled from registered operations is invalid.
149pub fn normalize_registered_observations_with_ranges(
150    language: Language,
151    build_variant_fingerprint: [u8; 32],
152    observations: Vec<(OperationObservation, SemanticSourceRange)>,
153    constructs: Vec<(ConstructObservation, SemanticSourceRange)>,
154) -> Result<ApiNormalization, SemanticGraphError> {
155    if observations
156        .iter()
157        .map(|(_, range)| range)
158        .chain(constructs.iter().map(|(_, range)| range))
159        .any(|range| range.end < range.start)
160    {
161        return Err(SemanticGraphError::InvalidSourceRange);
162    }
163    let observation_count = observations.len();
164    let mut nodes: Vec<_> = observations
165        .into_iter()
166        .enumerate()
167        .filter_map(|(source_index, (observation, source_range))| {
168            let kind = registered_api_kind(language, &observation.api_name)?;
169            let order = observation.api_name.clone();
170            Some((
171                observation.source_offset,
172                source_index,
173                order,
174                source_range,
175                OperationNode {
176                    kind,
177                    attributes: OperationAttributes {
178                        type_tag: observation.type_tag,
179                        api_names: BTreeSet::from([observation.api_name]),
180                        resource_kind: None,
181                        fallible_kind: None,
182                        direct_propagation: None,
183                        structure_fingerprint: None,
184                    },
185                },
186                ObservationSource::Api,
187            ))
188        })
189        .collect();
190    let recognized_api_count = nodes.len();
191    nodes.extend(constructs.into_iter().enumerate().map(
192        |(source_index, (construct, source_range))| {
193            (
194                construct.source_offset,
195                source_index,
196                construct.kind.name().to_owned(),
197                source_range,
198                OperationNode {
199                    kind: construct.kind,
200                    attributes: OperationAttributes {
201                        fallible_kind: construct.fallible_kind,
202                        direct_propagation: construct.direct_propagation,
203                        resource_kind: construct.resource_kind,
204                        ..OperationAttributes::default()
205                    },
206                },
207                ObservationSource::Construct,
208            )
209        },
210    ));
211    nodes.sort_by(|left, right| {
212        left.0
213            .cmp(&right.0)
214            .then_with(|| left.1.cmp(&right.1))
215            .then_with(|| left.2.cmp(&right.2))
216    });
217    nodes.dedup_by(coincident_operation);
218    let node_source_ranges = nodes.iter().map(|(_, _, _, range, _, _)| *range).collect();
219    let nodes: Vec<_> = nodes
220        .into_iter()
221        .map(|(_, _, _, _, node, _)| node)
222        .collect();
223    // The initial registry covers only data-sequence APIs. It never guesses
224    // that an unrelated call is a transformation simply because it is nearby.
225    let excluded_observations = observation_count.saturating_sub(recognized_api_count);
226    if nodes.is_empty() {
227        return Ok(ApiNormalization {
228            graph: None,
229            node_source_ranges,
230            excluded_observations,
231        });
232    }
233    let edges = operation_edges(&nodes)?;
234    Ok(ApiNormalization {
235        graph: Some(SemanticOperationGraph::new(
236            language,
237            build_variant_fingerprint,
238            nodes,
239            edges,
240        )?),
241        node_source_ranges,
242        excluded_observations,
243    })
244}
245
246/// Build the ordered data edges and any explicit resource-lifetime edge.
247fn operation_edges(nodes: &[OperationNode]) -> Result<Vec<OperationEdge>, SemanticGraphError> {
248    let mut edges = (1..nodes.len())
249        .map(|index| {
250            Ok(OperationEdge {
251                from: u32::try_from(index - 1).map_err(|_| SemanticGraphError::GraphTooLarge)?,
252                to: u32::try_from(index).map_err(|_| SemanticGraphError::GraphTooLarge)?,
253                kind: OperationEdgeKind::Data,
254            })
255        })
256        .collect::<Result<Vec<_>, SemanticGraphError>>()?;
257    for (index, pair) in nodes.windows(2).enumerate() {
258        let [acquire, release] = pair else {
259            continue;
260        };
261        if acquire.kind == OperationKind::AcquireResource
262            && release.kind == OperationKind::ReleaseResource
263            && acquire.attributes.resource_kind == release.attributes.resource_kind
264        {
265            edges.push(OperationEdge {
266                from: u32::try_from(index).map_err(|_| SemanticGraphError::GraphTooLarge)?,
267                to: u32::try_from(index + 1).map_err(|_| SemanticGraphError::GraphTooLarge)?,
268                kind: OperationEdgeKind::ResourceLifetime,
269            });
270        }
271    }
272    Ok(edges)
273}
274
275/// Whether two construct/API observations describe one source operation.
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277enum ObservationSource {
278    Api,
279    Construct,
280}
281
282fn coincident_operation(
283    left: &mut (
284        u64,
285        usize,
286        String,
287        SemanticSourceRange,
288        OperationNode,
289        ObservationSource,
290    ),
291    right: &mut (
292        u64,
293        usize,
294        String,
295        SemanticSourceRange,
296        OperationNode,
297        ObservationSource,
298    ),
299) -> bool {
300    left.0 == right.0
301        && left.3 == right.3
302        && left.4.kind == right.4.kind
303        && (left.5 != right.5 || left.1 == right.1)
304}
305
306/// Extract the largest source-contiguous windows that a same-variant rule can
307/// justify on its own.
308///
309/// The extractor never enumerates arbitrary subgraphs. Sequence rules receive
310/// their maximal contiguous run, direct constructs receive one node, and a
311/// resource rule receives only an explicit acquire/release pair. This bounds
312/// partial matching before candidate indexing and preserves a concise rule
313/// explanation for every returned fragment.
314///
315/// # Errors
316///
317/// Returns [`SemanticGraphError`] only if a validated graph cannot be rebased
318/// into one of its own windows.
319pub fn registered_semantic_windows(
320    normalization: &ApiNormalization,
321) -> Result<Vec<SemanticGraphWindow>, SemanticGraphError> {
322    let Some(graph) = &normalization.graph else {
323        return Ok(Vec::new());
324    };
325    if graph.nodes.len() != normalization.node_source_ranges.len() {
326        return Err(SemanticGraphError::SourceRangeCountMismatch);
327    }
328    let mut windows = Vec::new();
329    for rule in registered_rules()
330        .iter()
331        .copied()
332        .filter(|rule| rule.scope == SemanticRuleScope::SameBuildVariant)
333    {
334        match rule.matcher {
335            SemanticRuleMatcher::EquivalentSequence => {
336                let mut start = 0;
337                while start < graph.nodes.len() {
338                    while start < graph.nodes.len()
339                        && !rule
340                            .pattern
341                            .permitted_kinds
342                            .contains(&graph.nodes[start].kind)
343                    {
344                        start += 1;
345                    }
346                    let end = graph.nodes[start..]
347                        .iter()
348                        .position(|node| !rule.pattern.permitted_kinds.contains(&node.kind))
349                        .map_or(graph.nodes.len(), |length| start + length);
350                    if start < end {
351                        let window = semantic_graph_window(
352                            graph,
353                            &normalization.node_source_ranges,
354                            start,
355                            end,
356                        )?;
357                        if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
358                            windows.push(window);
359                        }
360                    }
361                    start = end.saturating_add(1);
362                }
363            }
364            SemanticRuleMatcher::ExactApiSequence { api_names } => {
365                // Exact API rules prove a fixed-width sequence. A neighbouring
366                // operation of the same kind must not make that sequence
367                // disappear by extending the maximal general-purpose run.
368                if !api_names.is_empty() && api_names.len() <= graph.nodes.len() {
369                    for start in 0..=graph.nodes.len() - api_names.len() {
370                        let window = semantic_graph_window(
371                            graph,
372                            &normalization.node_source_ranges,
373                            start,
374                            start + api_names.len(),
375                        )?;
376                        if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
377                            windows.push(window);
378                        }
379                    }
380                }
381            }
382            SemanticRuleMatcher::DirectConstruct { .. } => {
383                for index in 0..graph.nodes.len() {
384                    let window = semantic_graph_window(
385                        graph,
386                        &normalization.node_source_ranges,
387                        index,
388                        index + 1,
389                    )?;
390                    if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
391                        windows.push(window);
392                    }
393                }
394            }
395            SemanticRuleMatcher::ResourceLifecycle => {
396                for index in 0..graph.nodes.len().saturating_sub(1) {
397                    let window = semantic_graph_window(
398                        graph,
399                        &normalization.node_source_ranges,
400                        index,
401                        index + 2,
402                    )?;
403                    if match_same_variant_rule(rule, &window.graph, &window.graph).is_some() {
404                        windows.push(window);
405                    }
406                }
407            }
408        }
409    }
410    windows.sort_by_key(|window| window.source_range);
411    windows.dedup_by(|left, right| {
412        left.source_range == right.source_range && left.graph == right.graph
413    });
414    Ok(windows)
415}
416
417fn semantic_graph_window(
418    graph: &SemanticOperationGraph,
419    ranges: &[SemanticSourceRange],
420    start: usize,
421    end: usize,
422) -> Result<SemanticGraphWindow, SemanticGraphError> {
423    let source_range = SemanticSourceRange {
424        start: ranges[start].start,
425        end: ranges[end - 1].end,
426    };
427    let offset = u32::try_from(start).map_err(|_| SemanticGraphError::GraphTooLarge)?;
428    let limit = u32::try_from(end).map_err(|_| SemanticGraphError::GraphTooLarge)?;
429    let edges = graph
430        .edges
431        .iter()
432        .filter(|edge| {
433            edge.from >= offset && edge.from < limit && edge.to >= offset && edge.to < limit
434        })
435        .map(|edge| OperationEdge {
436            from: edge.from - offset,
437            to: edge.to - offset,
438            kind: edge.kind,
439        })
440        .collect();
441    Ok(SemanticGraphWindow {
442        graph: SemanticOperationGraph::new(
443            graph.language,
444            graph.build_variant_fingerprint,
445            graph.nodes[start..end].to_vec(),
446            edges,
447        )?,
448        source_range,
449    })
450}
451
452fn registered_api_kind(language: Language, api_name: &str) -> Option<OperationKind> {
453    cross_language_api_correspondence(language, api_name)
454        .map(|entry| entry.operation)
455        .or_else(|| {
456            matches!(
457                (language, api_name),
458                (
459                    Language::Rust,
460                    "rust::ToString::to_string" | "rust::str::parse"
461                ) | (Language::Cpp, "std::to_string" | "std::stoull")
462            )
463            .then_some(OperationKind::Map)
464        })
465}