opy-rs 0.1.7

Standalone OverPy-compatible .opy implementation with parsing, tooling, and bounded Workshop compilation.
Documentation
//! Opy HIR v2 — the OPY semantic model owned by `opy-rs`.
//!
//! The wire contract is the `wright/opy-hir` protocol, major version 2
//! (produced as `2.0.0`), specified in `docs/hir/opy-hir-v2.md`. This module provides the serde
//! protocol types, envelope and structural validation, and a deterministic
//! debug dump.
//!
//! Ingestion order follows the spec (§8): envelope identity/version first,
//! then unknown-node-kind rejection, then deserialization, then invariant
//! validation. Every failure is a structured [`HirError`].

pub mod dump;
pub mod error;
pub mod types;
mod validate;

pub use error::HirError;
pub use types::{
    Annotation, AnnotationArg, Declaration, DictEntry, DirectiveRecord, DirectiveValue, Event,
    Expr, Generator, OptimizationState, Position, PreprocessingSnapshot, PreprocessingState,
    Program, Protocol, Rule, RuleEntry, Settings, SettingsListElement, SettingsNode, SourceFile,
    Span, Stmt, SwitchArm, TranslationState, default_var_index,
};

use serde_json::Value;

/// Parse and validate an Opy HIR v2 payload from a JSON string.
///
/// Returns a structured [`HirError`] for malformed JSON, an unsupported
/// protocol identity or major version, unknown node kinds, or invariant
/// violations.
pub fn parse_str(input: &str) -> Result<Program, HirError> {
    let value: Value = serde_json::from_str(input)?;
    parse_value(value)
}

/// Parse and validate an Opy HIR v2 payload from a JSON value.
pub fn parse_value(value: Value) -> Result<Program, HirError> {
    validate::check_envelope(&value)?;
    validate::check_unknown_kinds(&value)?;
    let program: Program = serde_json::from_value(value)?;
    program.validate()?;
    Ok(program)
}

impl Program {
    /// Validate structural invariants of this program (spans, identifiers,
    /// references). Envelope and node-kind checks are performed by
    /// [`parse_str`]/[`parse_value`].
    pub fn validate(&self) -> Result<(), HirError> {
        validate::validate_program(self)
    }

    /// Render a deterministic debug dump suitable for tests and issue
    /// reports.
    pub fn dump(&self) -> String {
        dump::dump(self)
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::parse_value;
    use super::validate::check_envelope;

    #[test]
    fn v2_envelope_is_accepted_and_v1_is_rejected_before_body_inspection() {
        let v2 = json!({
            "protocol": { "name": "wright/opy-hir", "version": "2.0.0" }
        });
        assert!(check_envelope(&v2).is_ok());

        let v1 = json!({
            "protocol": { "name": "wright/opy-hir", "version": "1.1.0" },
            "rules": [{ "kind": "malformed-v1-body" }]
        });
        let error = check_envelope(&v1).expect_err("v1 payload must not enter the v2 parser");
        assert_eq!(error.code(), "incompatible-protocol");
    }

    #[test]
    fn unknown_conditional_condition_kind_preserves_unsupported_node_span() {
        let error = parse_value(json!({
            "protocol": { "name": "wright/opy-hir", "version": "2.0.0" },
            "rules": [{
                "event": { "args": [] },
                "conditions": [],
                "actions": [{
                    "kind": "expr",
                    "expr": {
                        "kind": "conditional",
                        "thenValue": { "kind": "number", "value": 1 },
                        "condition": {
                            "kind": "future-expression",
                            "span": {
                                "file": 0,
                                "start": { "line": 4, "col": 12 },
                                "end": { "line": 4, "col": 20 }
                            }
                        },
                        "elseValue": { "kind": "number", "value": 0 }
                    }
                }]
            }]
        }))
        .expect_err("unknown kinds nested in conditional conditions must be rejected");

        assert_eq!(error.code(), "unsupported-node");
        assert_eq!(error.message(), "unsupported node kind 'future-expression'");
        assert_eq!(error.span().unwrap().start.line, 4);
        assert_eq!(error.span().unwrap().start.col, 12);
    }
}