Skip to main content

morphir_core/ir/v4/
mod.rs

1//! Morphir IR V4
2//!
3//! This module defines the structure for Morphir IR Version 4.
4//! It supports the Document Tree structure and Canonical Strings.
5//!
6//! V4 uses object wrapper format for enums and keyed objects (IndexMap) for
7//! dictionaries rather than arrays of tuples.
8
9use schemars::JsonSchema;
10use serde::Deserializer;
11use serde::ser::SerializeMap;
12use serde::{Deserialize, Serialize};
13
14use crate::format_version::{
15    CanonicalSpelling, NormalizedFormatVersion, ScalarValue, SupportTable,
16};
17
18// Submodules - Core IR types
19pub mod access;
20pub mod annotation;
21pub mod attributes;
22pub mod distribution;
23pub mod legacy;
24pub mod linked_metadata;
25mod linked_metadata_project;
26pub(crate) mod linked_metadata_scan;
27pub mod literal;
28pub mod module;
29pub mod package;
30pub mod pattern;
31pub mod serde_document;
32pub mod serde_tagged;
33pub mod serde_v4;
34pub mod tree_files;
35pub mod types;
36pub mod value;
37
38// Re-export naming types - Name now serializes as V4 canonical format (kebab-case string)
39pub use crate::naming::ModuleName;
40pub use crate::naming::Name;
41pub use crate::naming::PackageName;
42pub use crate::naming::Path;
43
44// Re-export access control
45pub use access::{Access, AccessControlled};
46
47// Re-export annotations, which specifications carry and definitions do not
48pub use annotation::{Annotation, AnnotationArgument, Annotations};
49
50// Re-export core expression types
51pub use crate::ir::decimal::{DecimalLiteral, InvalidDecimalLexeme};
52pub use attributes::{SourceLocation, TypeAttributes, TypeExpr, ValueAttributes, ValueExpr};
53pub use legacy::{SpellingMode, accept_member, take_warnings, with_spelling_mode};
54pub use linked_metadata::{DocumentMeta, MetadataScope};
55pub use linked_metadata_project::{
56    DocumentGraphError, expand_document_graph, expand_v4_single_file_graph,
57};
58pub use linked_metadata_scan::LinkedMetadataCarrier;
59pub use literal::{FloatLiteral, InvalidFloatLexeme, Literal};
60pub use pattern::Pattern;
61pub use serde_v4::{TypeEncoding, with_type_encoding};
62pub use types::{Field, Type};
63pub use value::{
64    HoleReason as ValueHoleReason, InputType, LetBinding, NativeHint as ValueNativeHint,
65    NativeInfo, PatternCase, RecordFieldEntry, Value, ValueBody as ValueExprBody,
66    ValueDefinition as ValueExprDefinition,
67};
68
69// Re-export distribution types
70pub use distribution::{
71    ApplicationContent, DefinitionDependencies, Dependencies, Distribution, EntryPoint,
72    EntryPointKind, EntryPoints, LibraryContent, SpecsContent,
73};
74
75// Re-export module types
76pub use module::{Documentation, Documented, ModuleDefinition, ModuleSpecification};
77
78// Re-export package types
79pub use package::{PackageDefinition, PackageSpecification};
80
81// Re-export the four files a document tree is made of
82pub use tree_files::{
83    DistributionKind, DistributionManifestFile, ExpectedEntries, FILE_STEM_PATTERN,
84    MIN_PATH_BUDGET, ModuleEntries, ModuleManifestFile, NodeFileBody, TypeDefinitionFile,
85    ValueDefinitionFile, is_escaped_stem,
86};
87
88// Re-export type definition types
89pub use types::{
90    ConstructorArg, ConstructorArgSpec, ConstructorDefinition, ConstructorSpecification,
91    Incompleteness, TypeDefinition, TypeSpecification,
92};
93
94// Re-export value definition types
95pub use value::{
96    ExternalBinding, HoleReason, NativeHint, ValueBody, ValueDefinition, ValueSpecification,
97};
98
99/// Top-level IR file structure.
100///
101/// `formatVersion` comes first and `distribution` second. The optional `$meta`
102/// belongs to the document in the proposed 4.1.0 profile; a document tree stores it
103/// in its manifest.
104#[derive(Debug, Clone, PartialEq)]
105pub struct IRFile {
106    pub format_version: FormatVersion,
107    pub distribution: Distribution,
108    /// Document-owned 4.1.0 metadata, separate from node-local carriers.
109    pub metadata: Option<Box<DocumentMeta>>,
110}
111
112impl IRFile {
113    /// Whether any node, specification, or document carrier contains linked metadata.
114    pub fn has_linked_metadata(&self) -> bool {
115        self.metadata.is_some() || self.distribution.contains_linked_metadata()
116    }
117}
118
119impl Serialize for IRFile {
120    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
121        let proposed = self.format_version == FormatVersion::String("4.1.0".to_owned());
122        if !proposed && self.has_linked_metadata() {
123            return Err(serde::ser::Error::custom(
124                "linked metadata requires formatVersion 4.1.0",
125            ));
126        }
127        let mut map = serializer.serialize_map(None)?;
128        map.serialize_entry("formatVersion", &self.format_version)?;
129        map.serialize_entry("distribution", &self.distribution)?;
130        if let Some(metadata) = &self.metadata {
131            map.serialize_entry("$meta", metadata)?;
132        }
133        map.end()
134    }
135}
136
137impl<'de> Deserialize<'de> for IRFile {
138    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
139    where
140        D: Deserializer<'de>,
141    {
142        serde_document::deserialize_with(deserializer, serde_document::decode_ir_file)
143    }
144}
145
146/// Decodes a version 4 document from an already-parsed value tree, with its warnings.
147///
148/// This is the entry a reader uses when it produced the value tree itself — the YAML profile
149/// reader, say — rather than handing a document to serde. It decodes exactly what
150/// `Deserialize for IRFile` decodes, and collects the `legacy_spelling` warnings the serde path
151/// leaves to its caller's `with_spelling_mode` scope.
152pub fn decode_ir_file_with_warnings(
153    value: &serde_json::Value,
154) -> Result<(IRFile, Vec<crate::ir::Warning>), crate::ir::DiagnosticError> {
155    let (decoded, warnings) = with_spelling_mode(SpellingMode::Current, || {
156        serde_document::decode_ir_file(value, "")
157    });
158    decoded
159        .map(|file| (file, warnings))
160        .map_err(crate::ir::DiagnosticError)
161}
162
163/// Format version - accepts both string "4.0.0" and integer 4 using the shared contract.
164#[derive(Debug, Clone, PartialEq, JsonSchema)]
165pub enum FormatVersion {
166    String(String),
167    Integer(u32),
168}
169
170impl From<CanonicalSpelling> for FormatVersion {
171    fn from(value: CanonicalSpelling) -> Self {
172        match value {
173            CanonicalSpelling::Integer(version) => Self::Integer(version),
174            CanonicalSpelling::String(version) => Self::String(version),
175        }
176    }
177}
178
179impl FormatVersion {
180    /// Normalize this wire spelling against the reference support table.
181    pub fn normalize(
182        &self,
183    ) -> Result<NormalizedFormatVersion, crate::format_version::FormatVersionDiagnostic> {
184        let scalar = match self {
185            Self::Integer(version) => ScalarValue::Integer(*version as u64),
186            Self::String(version) => ScalarValue::String(version.clone()),
187        };
188        NormalizedFormatVersion::from_scalar(&scalar, &SupportTable::reference())
189    }
190}
191
192impl Serialize for FormatVersion {
193    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
194    where
195        S: serde::Serializer,
196    {
197        match self {
198            FormatVersion::String(s) => serializer.serialize_str(s),
199            FormatVersion::Integer(n) => serializer.serialize_u32(*n),
200        }
201    }
202}
203
204impl<'de> Deserialize<'de> for FormatVersion {
205    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
206    where
207        D: Deserializer<'de>,
208    {
209        serde_document::deserialize_with(deserializer, serde_document::decode_format_version)
210    }
211}
212
213impl Default for FormatVersion {
214    fn default() -> Self {
215        FormatVersion::Integer(4)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_format_version_string() {
225        let v = FormatVersion::String("4.0.0".to_string());
226        let json = serde_json::to_string(&v).unwrap();
227        assert_eq!(json, "\"4.0.0\"");
228    }
229
230    #[test]
231    fn test_format_version_integer() {
232        let v = FormatVersion::Integer(4);
233        let json = serde_json::to_string(&v).unwrap();
234        assert_eq!(json, "4");
235    }
236}