Skip to main content

opy_rs/hir/
mod.rs

1//! Opy HIR v2 — the OPY semantic model owned by `opy-rs`.
2//!
3//! The wire contract is the `wright/opy-hir` protocol, major version 2
4//! (produced as `2.0.0`), specified in `docs/hir/opy-hir-v2.md`. This module provides the serde
5//! protocol types, envelope and structural validation, and a deterministic
6//! debug dump.
7//!
8//! Ingestion order follows the spec (§8): envelope identity/version first,
9//! then unknown-node-kind rejection, then deserialization, then invariant
10//! validation. Every failure is a structured [`HirError`].
11
12pub mod dump;
13pub mod error;
14pub mod types;
15mod validate;
16
17pub use error::HirError;
18pub use types::{
19    Annotation, AnnotationArg, Declaration, DictEntry, DirectiveRecord, DirectiveValue, Event,
20    Expr, Generator, OptimizationState, Position, PreprocessingSnapshot, PreprocessingState,
21    Program, Protocol, Rule, RuleEntry, Settings, SettingsListElement, SettingsNode, SourceFile,
22    Span, Stmt, SwitchArm, TranslationState, default_var_index,
23};
24
25use serde_json::Value;
26
27/// Parse and validate an Opy HIR v2 payload from a JSON string.
28///
29/// Returns a structured [`HirError`] for malformed JSON, an unsupported
30/// protocol identity or major version, unknown node kinds, or invariant
31/// violations.
32pub fn parse_str(input: &str) -> Result<Program, HirError> {
33    let value: Value = serde_json::from_str(input)?;
34    parse_value(value)
35}
36
37/// Parse and validate an Opy HIR v2 payload from a JSON value.
38pub fn parse_value(value: Value) -> Result<Program, HirError> {
39    validate::check_envelope(&value)?;
40    validate::check_unknown_kinds(&value)?;
41    let program: Program = serde_json::from_value(value)?;
42    program.validate()?;
43    Ok(program)
44}
45
46impl Program {
47    /// Validate structural invariants of this program (spans, identifiers,
48    /// references). Envelope and node-kind checks are performed by
49    /// [`parse_str`]/[`parse_value`].
50    pub fn validate(&self) -> Result<(), HirError> {
51        validate::validate_program(self)
52    }
53
54    /// Render a deterministic debug dump suitable for tests and issue
55    /// reports.
56    pub fn dump(&self) -> String {
57        dump::dump(self)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use serde_json::json;
64
65    use super::parse_value;
66    use super::validate::check_envelope;
67
68    #[test]
69    fn v2_envelope_is_accepted_and_v1_is_rejected_before_body_inspection() {
70        let v2 = json!({
71            "protocol": { "name": "wright/opy-hir", "version": "2.0.0" }
72        });
73        assert!(check_envelope(&v2).is_ok());
74
75        let v1 = json!({
76            "protocol": { "name": "wright/opy-hir", "version": "1.1.0" },
77            "rules": [{ "kind": "malformed-v1-body" }]
78        });
79        let error = check_envelope(&v1).expect_err("v1 payload must not enter the v2 parser");
80        assert_eq!(error.code(), "incompatible-protocol");
81    }
82
83    #[test]
84    fn unknown_conditional_condition_kind_preserves_unsupported_node_span() {
85        let error = parse_value(json!({
86            "protocol": { "name": "wright/opy-hir", "version": "2.0.0" },
87            "rules": [{
88                "event": { "args": [] },
89                "conditions": [],
90                "actions": [{
91                    "kind": "expr",
92                    "expr": {
93                        "kind": "conditional",
94                        "thenValue": { "kind": "number", "value": 1 },
95                        "condition": {
96                            "kind": "future-expression",
97                            "span": {
98                                "file": 0,
99                                "start": { "line": 4, "col": 12 },
100                                "end": { "line": 4, "col": 20 }
101                            }
102                        },
103                        "elseValue": { "kind": "number", "value": 0 }
104                    }
105                }]
106            }]
107        }))
108        .expect_err("unknown kinds nested in conditional conditions must be rejected");
109
110        assert_eq!(error.code(), "unsupported-node");
111        assert_eq!(error.message(), "unsupported node kind 'future-expression'");
112        assert_eq!(error.span().unwrap().start.line, 4);
113        assert_eq!(error.span().unwrap().start.col, 12);
114    }
115}