Skip to main content

codehelion_core/semantic/
rules.rs

1use super::{
2    CROSS_LANGUAGE_OPTIONAL_VALIDATION_RULE, CROSS_LANGUAGE_RESULT_DIRECT_PROPAGATION_RULE,
3    CROSS_LANGUAGE_RESULT_VALIDATION_RULE, CROSS_LANGUAGE_SEQUENCE_PIPELINE_RULE,
4    DirectPropagation, FallibleKind, OperationAttributes, OperationEdge, OperationEdgeKind,
5    OperationKind, OperationNode, SOG_SCHEMA_VERSION, SemanticOperationGraph, TypeTag,
6};
7
8/// A registered, explainable SOG correspondence rule.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct SemanticRule {
11    /// Stable registry identifier.
12    pub id: &'static str,
13    /// Rule semantics revision.
14    pub version: u32,
15    /// Conservative confidence before later data-flow evidence is applied.
16    pub confidence: f64,
17    /// Comparison domain where the rule may run.
18    pub scope: SemanticRuleScope,
19    /// Closed operation pattern the rule is allowed to explain.
20    pub pattern: SemanticRulePattern,
21    /// Closed matching strategy selected by this rule's declaration.
22    pub matcher: SemanticRuleMatcher,
23}
24
25/// The explicit comparison domain a registered rule may inspect.
26///
27/// A rule cannot cross from ordinary scan partitions into another language by
28/// omission: Rust-to-C++ matching is opt-in and carries a separate comparison
29/// identity.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SemanticRuleScope {
32    /// Two graphs produced under one complete build variant.
33    SameBuildVariant,
34    /// One caller-selected Rust graph and one caller-selected C++ graph.
35    RustCpp,
36}
37
38/// A declarative, closed SOG pattern for one registered semantic rule.
39///
40/// This deliberately describes only which operation kinds a rule may accept
41/// and its minimum length. A rule must still provide a concrete matcher; the
42/// pattern is a fail-closed precondition, not a generic graph-rewrite DSL.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct SemanticRulePattern {
45    /// The least number of operations that make the registered pattern useful.
46    pub minimum_operations: usize,
47    /// Every node kind the rule may explain, in any permitted source order.
48    pub permitted_kinds: &'static [OperationKind],
49}
50
51impl SemanticRulePattern {
52    /// Whether a graph lies entirely inside this rule's closed vocabulary.
53    #[must_use]
54    pub fn accepts(self, graph: &SemanticOperationGraph) -> bool {
55        graph.nodes.len() >= self.minimum_operations
56            && graph
57                .nodes
58                .iter()
59                .all(|node| self.permitted_kinds.contains(&node.kind))
60    }
61}
62
63/// A closed, declarative matching strategy for a registered semantic rule.
64///
65/// This is deliberately a small enum rather than an open rewrite language:
66/// adding an unreviewed syntax form must not turn the registry into a general
67/// equivalence engine. Rules select a pre-audited strategy and provide all
68/// values that strategy needs in their declaration.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum SemanticRuleMatcher {
71    /// Match equal-length operation sequences when every aligned node has the
72    /// same kind and compatible compiler-confirmed type evidence.
73    EquivalentSequence,
74    /// Match a single closed compiler-confirmed API sequence. The operation
75    /// kinds remain part of the rule pattern; these names prevent a generic
76    /// pair of value transformations from being described as serialization.
77    ExactApiSequence {
78        /// One resolved API name required at each aligned operation position.
79        api_names: &'static [&'static str],
80    },
81    /// Match exactly one compiler-confirmed construct with the supplied
82    /// operation, fallible family, and optional direct-propagation form.
83    DirectConstruct {
84        /// The only operation kind the construct may carry.
85        kind: OperationKind,
86        /// The standard fallible family the helper must have resolved.
87        fallible_kind: FallibleKind,
88        /// An additional closed direct-propagation fact, when required.
89        direct_propagation: Option<DirectPropagation>,
90    },
91    /// Match one compiler-confirmed acquire/release pair of the same closed
92    /// resource category, including its explicit lifetime edge.
93    ResourceLifecycle,
94}
95
96/// One successful registered rule application.
97#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct RuleMatch {
99    /// Rule that justified this match.
100    pub rule: SemanticRule,
101}
102
103const SEQUENCE_PIPELINE_RULE: SemanticRule = SemanticRule {
104    id: "sequence-pipeline-v1",
105    version: 1,
106    confidence: 0.7,
107    scope: SemanticRuleScope::SameBuildVariant,
108    pattern: SemanticRulePattern {
109        minimum_operations: 2,
110        permitted_kinds: &[
111            OperationKind::Source,
112            OperationKind::Filter,
113            OperationKind::Map,
114            OperationKind::Reduce,
115            OperationKind::Collect,
116        ],
117    },
118    matcher: SemanticRuleMatcher::EquivalentSequence,
119};
120
121/// Serialization and deserialization are a fixed two-step value conversion
122/// here, not a claim that arbitrary parsing or formatting is semantically
123/// interchangeable. Rust's standard `ToString` and `str::parse` are the only
124/// admitted implementation in this initial rule.
125const RUST_SERIALIZATION_ROUND_TRIP_RULE: SemanticRule = SemanticRule {
126    id: "rust-serialization-round-trip-v1",
127    version: 1,
128    confidence: 0.8,
129    scope: SemanticRuleScope::SameBuildVariant,
130    pattern: SemanticRulePattern {
131        minimum_operations: 2,
132        permitted_kinds: &[OperationKind::Map],
133    },
134    matcher: SemanticRuleMatcher::ExactApiSequence {
135        api_names: &["rust::ToString::to_string", "rust::str::parse"],
136    },
137};
138
139/// C++ records the analogous closed standard-library conversion pair. This is
140/// intentionally a separate rule: source language and exact resolved APIs
141/// remain evidence, rather than being erased behind a generic serialization
142/// label.
143const CPP_SERIALIZATION_ROUND_TRIP_RULE: SemanticRule = SemanticRule {
144    id: "cpp-serialization-round-trip-v1",
145    version: 1,
146    confidence: 0.8,
147    scope: SemanticRuleScope::SameBuildVariant,
148    pattern: SemanticRulePattern {
149        minimum_operations: 2,
150        permitted_kinds: &[OperationKind::Map],
151    },
152    matcher: SemanticRuleMatcher::ExactApiSequence {
153        api_names: &["std::to_string", "std::stoull"],
154    },
155};
156
157const RESULT_DIRECT_PROPAGATION_RULE: SemanticRule = SemanticRule {
158    id: "result-direct-propagation-v1",
159    version: 1,
160    confidence: 0.95,
161    scope: SemanticRuleScope::SameBuildVariant,
162    pattern: SemanticRulePattern {
163        minimum_operations: 1,
164        permitted_kinds: &[OperationKind::PropagateError],
165    },
166    matcher: SemanticRuleMatcher::DirectConstruct {
167        kind: OperationKind::PropagateError,
168        fallible_kind: FallibleKind::Result,
169        direct_propagation: Some(DirectPropagation::ResultAdapter),
170    },
171};
172
173const OPTION_DIRECT_PROPAGATION_RULE: SemanticRule = SemanticRule {
174    id: "option-direct-propagation-v1",
175    version: 1,
176    confidence: 0.95,
177    scope: SemanticRuleScope::SameBuildVariant,
178    pattern: SemanticRulePattern {
179        minimum_operations: 1,
180        permitted_kinds: &[OperationKind::PropagateError],
181    },
182    matcher: SemanticRuleMatcher::DirectConstruct {
183        kind: OperationKind::PropagateError,
184        fallible_kind: FallibleKind::Option,
185        direct_propagation: Some(DirectPropagation::OptionAdapter),
186    },
187};
188
189pub(super) const OPTIONAL_VALIDATION_RULE: SemanticRule = SemanticRule {
190    id: "optional-validation-v1",
191    version: 1,
192    confidence: 0.85,
193    scope: SemanticRuleScope::SameBuildVariant,
194    pattern: SemanticRulePattern {
195        minimum_operations: 1,
196        permitted_kinds: &[OperationKind::Validate],
197    },
198    matcher: SemanticRuleMatcher::DirectConstruct {
199        kind: OperationKind::Validate,
200        fallible_kind: FallibleKind::Option,
201        direct_propagation: None,
202    },
203};
204
205pub(super) const RESULT_VALIDATION_RULE: SemanticRule = SemanticRule {
206    id: "result-validation-v1",
207    version: 1,
208    confidence: 0.85,
209    scope: SemanticRuleScope::SameBuildVariant,
210    pattern: SemanticRulePattern {
211        minimum_operations: 1,
212        permitted_kinds: &[OperationKind::Validate],
213    },
214    matcher: SemanticRuleMatcher::DirectConstruct {
215        kind: OperationKind::Validate,
216        fallible_kind: FallibleKind::Result,
217        direct_propagation: None,
218    },
219};
220
221const RESOURCE_LIFECYCLE_RULE: SemanticRule = SemanticRule {
222    id: "resource-lifecycle-v1",
223    version: 1,
224    confidence: 0.9,
225    scope: SemanticRuleScope::SameBuildVariant,
226    pattern: SemanticRulePattern {
227        minimum_operations: 2,
228        permitted_kinds: &[
229            OperationKind::AcquireResource,
230            OperationKind::ReleaseResource,
231        ],
232    },
233    matcher: SemanticRuleMatcher::ResourceLifecycle,
234};
235
236/// Rules enabled by default because each has an explicit, bounded meaning.
237///
238/// Cross-language entries still require the separate opt-in comparison path;
239/// appearing here makes their enabled state visible and configurable beside
240/// same-variant rules.
241#[must_use]
242pub const fn registered_rules() -> &'static [SemanticRule] {
243    &[
244        RUST_SERIALIZATION_ROUND_TRIP_RULE,
245        CPP_SERIALIZATION_ROUND_TRIP_RULE,
246        SEQUENCE_PIPELINE_RULE,
247        RESULT_DIRECT_PROPAGATION_RULE,
248        OPTION_DIRECT_PROPAGATION_RULE,
249        OPTIONAL_VALIDATION_RULE,
250        RESULT_VALIDATION_RULE,
251        RESOURCE_LIFECYCLE_RULE,
252        CROSS_LANGUAGE_SEQUENCE_PIPELINE_RULE,
253        CROSS_LANGUAGE_OPTIONAL_VALIDATION_RULE,
254        CROSS_LANGUAGE_RESULT_VALIDATION_RULE,
255        CROSS_LANGUAGE_RESULT_DIRECT_PROPAGATION_RULE,
256    ]
257}
258
259/// Match two equivalent registered API pipelines without comparing API spelling.
260///
261/// This permits, for example, Rust `filter/map/collect` and C++
262/// `copy_if/transform/push_back` only when their closed operation sequences
263/// and known type categories agree. Different `BuildVariants` never match.
264#[must_use]
265pub fn match_registered_pipeline(
266    left: &SemanticOperationGraph,
267    right: &SemanticOperationGraph,
268) -> Option<RuleMatch> {
269    match_same_variant_rule(SEQUENCE_PIPELINE_RULE, left, right)
270}
271
272/// Match two graphs against one declared rule inside a single build variant.
273///
274/// The caller supplies a rule from the closed registry. A declaration that
275/// does not accept both graphs, or a graph from another schema or variant,
276/// fails rather than being coerced into a nearby rule.
277pub(super) fn match_same_variant_rule(
278    rule: SemanticRule,
279    left: &SemanticOperationGraph,
280    right: &SemanticOperationGraph,
281) -> Option<RuleMatch> {
282    if rule.scope != SemanticRuleScope::SameBuildVariant
283        || left.schema_version != SOG_SCHEMA_VERSION
284        || right.schema_version != SOG_SCHEMA_VERSION
285        || left.language != right.language
286        || left.build_variant_fingerprint != right.build_variant_fingerprint
287        || !rule.pattern.accepts(left)
288        || !rule.pattern.accepts(right)
289    {
290        return None;
291    }
292    let matches = match rule.matcher {
293        SemanticRuleMatcher::EquivalentSequence => {
294            left.nodes.len() == right.nodes.len()
295                && left
296                    .nodes
297                    .iter()
298                    .any(|node| node.kind != OperationKind::Map)
299                && left
300                    .nodes
301                    .iter()
302                    .zip(&right.nodes)
303                    .all(|(left, right)| compatible_nodes(left, right))
304        }
305        SemanticRuleMatcher::ExactApiSequence { api_names } => {
306            left.nodes.len() == api_names.len()
307                && right.nodes.len() == api_names.len()
308                && left.nodes.iter().zip(&right.nodes).zip(api_names).all(
309                    |((left, right), api_name)| {
310                        compatible_nodes(left, right)
311                            && left.attributes.api_names.len() == 1
312                            && right.attributes.api_names.len() == 1
313                            && left.attributes.api_names.contains(*api_name)
314                            && right.attributes.api_names.contains(*api_name)
315                    },
316                )
317        }
318        SemanticRuleMatcher::DirectConstruct {
319            kind,
320            fallible_kind,
321            direct_propagation,
322        } => {
323            direct_construct_matches(left, kind, fallible_kind, direct_propagation)
324                && direct_construct_matches(right, kind, fallible_kind, direct_propagation)
325        }
326        SemanticRuleMatcher::ResourceLifecycle => {
327            resource_lifecycle_matches(left) && resource_lifecycle_matches(right)
328        }
329    };
330    matches.then_some(RuleMatch { rule })
331}
332
333fn resource_lifecycle_matches(graph: &SemanticOperationGraph) -> bool {
334    matches!(
335        graph.nodes.as_slice(),
336        [OperationNode {
337            kind: OperationKind::AcquireResource,
338            attributes: OperationAttributes {
339                resource_kind: Some(acquired),
340                ..
341            },
342        }, OperationNode {
343            kind: OperationKind::ReleaseResource,
344            attributes: OperationAttributes {
345                resource_kind: Some(released),
346                ..
347            },
348        }] if acquired == released
349    ) && graph.edges.contains(&OperationEdge {
350        from: 0,
351        to: 1,
352        kind: OperationEdgeKind::ResourceLifetime,
353    })
354}
355
356/// Match any closed, registered SOG correspondence rule.
357#[must_use]
358pub fn match_registered_rule(
359    left: &SemanticOperationGraph,
360    right: &SemanticOperationGraph,
361) -> Option<RuleMatch> {
362    registered_rules()
363        .iter()
364        .copied()
365        .filter(|rule| rule.scope == SemanticRuleScope::SameBuildVariant)
366        .find_map(|rule| match_same_variant_rule(rule, left, right))
367}
368
369pub(super) fn only_api_name(node: &OperationNode) -> Option<&str> {
370    (node.attributes.api_names.len() == 1)
371        .then(|| node.attributes.api_names.iter().next())
372        .flatten()
373        .map(String::as_str)
374}
375
376pub(super) fn compatible_type_tags(left: Option<TypeTag>, right: Option<TypeTag>) -> bool {
377    match (left, right) {
378        (Some(left), Some(right)) => left == right,
379        _ => true,
380    }
381}
382
383pub(super) fn compatible_fallible_kinds(
384    left: Option<FallibleKind>,
385    right: Option<FallibleKind>,
386) -> bool {
387    match (left, right) {
388        (Some(left), Some(right)) => left == right,
389        _ => true,
390    }
391}
392
393/// Two operations correspond when the compiler resolved them to the same
394/// registered operation over compatible values.
395///
396/// The source-structure fingerprint deliberately takes no part in this. It
397/// separates occurrences inside the semantic digest, so two windows keep
398/// distinct identities; requiring it to agree here would restrict a registered
399/// rule to windows whose text already matches, which is what Type-1 and Type-2
400/// detection cover. The expressions handed to a registered API are exactly what
401/// a rule is meant to look past — the labelled corpus pairs a `filter` on odd
402/// values with a `filter` on even ones, and separates them from a pipeline
403/// whose operation sequence differs.
404fn compatible_nodes(left: &OperationNode, right: &OperationNode) -> bool {
405    left.kind == right.kind
406        && compatible_type_tags(left.attributes.type_tag, right.attributes.type_tag)
407        && compatible_fallible_kinds(
408            left.attributes.fallible_kind,
409            right.attributes.fallible_kind,
410        )
411}
412
413pub(super) fn direct_construct_matches(
414    graph: &SemanticOperationGraph,
415    kind: OperationKind,
416    fallible_kind: FallibleKind,
417    direct_propagation: Option<DirectPropagation>,
418) -> bool {
419    matches!(
420        graph.nodes.as_slice(),
421        [OperationNode {
422            kind: node_kind,
423            attributes: OperationAttributes {
424                fallible_kind: node_fallible_kind,
425                direct_propagation: node_direct_propagation,
426                ..
427            },
428        }] if *node_kind == kind
429            && *node_fallible_kind == Some(fallible_kind)
430            && direct_propagation.is_none_or(|required| {
431                *node_direct_propagation == Some(required)
432            })
433    )
434}