Skip to main content

xidl_parser/hir/
spec.rs

1use super::{
2    Definition, InterfaceDcl, ModuleDcl, ParserProperties, Specification, TypeDcl,
3    expand_annotations, interface_codegen, parse_xidlc_pragma,
4};
5use crate::jsonrpc_hir;
6use crate::rest_hir::{self, HirProjectionKind, ProjectedHir};
7use crate::semantic;
8use crate::versioning::{self, VersionedProjection, VersioningRecord};
9use serde_json::Value;
10use std::path::Path;
11
12impl From<crate::typed_ast::Specification> for Specification {
13    fn from(value: crate::typed_ast::Specification) -> Self {
14        spec_from_typed_ast(value, true)
15    }
16}
17
18impl Specification {
19    pub fn from_typed_ast_with_properties(
20        value: crate::typed_ast::Specification,
21        properties: ParserProperties,
22    ) -> Self {
23        spec_from_typed_ast(value, expand_interface(&properties))
24    }
25
26    pub fn from_typed_ast_with_properties_and_path(
27        value: crate::typed_ast::Specification,
28        properties: ParserProperties,
29        _path: impl AsRef<Path>,
30    ) -> crate::error::ParserResult<Self> {
31        Self::lower(value, expand_interface(&properties))
32    }
33
34    pub fn from_typed_ast_with_path(
35        value: crate::typed_ast::Specification,
36        _path: impl AsRef<Path>,
37    ) -> crate::error::ParserResult<Self> {
38        Self::lower(value, true)
39    }
40
41    /// Lowers a typed AST and returns one HIR projection per requested version.
42    ///
43    /// Unversioned specifications produce exactly one projection without a
44    /// version. Versioned specifications produce one merged projection holding
45    /// one module per selected version (`merged` layout, the default), or one
46    /// projection per selected version (`per_version` layout).
47    pub fn project_typed_ast_versions(
48        value: crate::typed_ast::Specification,
49        properties: ParserProperties,
50        _path: impl AsRef<Path>,
51    ) -> crate::error::ParserResult<Vec<VersionedProjection>> {
52        let layout = versioning::VersionLayout::from_properties(&properties)?;
53        let selection = versioning::selection_from_properties(&properties)?;
54        let expand = expand_interface(&properties);
55        let source = Self::lower_source(value)?;
56        let Some(record) = VersioningRecord::analyze(&source)? else {
57            if let Some(selection) = &selection {
58                return Err(versioning::unversioned_selection_error(selection));
59            }
60            let spec = Self::finalize(source, expand)?;
61            return Ok(vec![VersionedProjection {
62                versions: Vec::new(),
63                version: None,
64                hir: Self::project_hir(spec, &properties)?,
65            }]);
66        };
67        let versions = versioning::selected_versions(selection.as_ref(), &record)?;
68        match layout {
69            versioning::VersionLayout::Merged => {
70                let spec = record.merge(&source, &versions)?;
71                let spec = Self::finalize(spec, expand)?;
72                let selected = versions
73                    .iter()
74                    .filter_map(|version| record.timeline().get(*version).cloned())
75                    .collect();
76                Ok(vec![VersionedProjection {
77                    versions: selected,
78                    version: None,
79                    hir: Self::project_hir(spec, &properties)?,
80                }])
81            }
82            versioning::VersionLayout::PerVersion => {
83                let mut projections = Vec::new();
84                for version in versions {
85                    let spec = record.project(&source, version)?;
86                    let spec = Self::finalize(spec, expand)?;
87                    let definition = record.timeline().get(version).cloned();
88                    projections.push(VersionedProjection {
89                        versions: definition.clone().into_iter().collect(),
90                        version: definition,
91                        hir: Self::project_hir(spec, &properties)?,
92                    });
93                }
94                Ok(projections)
95            }
96        }
97    }
98
99    /// Lowers a typed AST and projects the first request of the merged layout.
100    ///
101    /// Callers that need per-version output use [`Self::project_typed_ast_versions`].
102    pub fn project_typed_ast_with_properties_and_path(
103        value: crate::typed_ast::Specification,
104        properties: ParserProperties,
105        path: impl AsRef<Path>,
106    ) -> crate::error::ParserResult<ProjectedHir> {
107        let mut projections = Self::project_typed_ast_versions(value, properties, path)?;
108        match projections.len() {
109            1 => Ok(projections.remove(0).hir),
110            _ => Err(crate::error::ParseError::Message(
111                "xidl versioning: per-version layout requires project_typed_ast_versions"
112                    .to_string(),
113            )),
114        }
115    }
116
117    fn project_hir(
118        spec: Specification,
119        properties: &ParserProperties,
120    ) -> crate::error::ParserResult<ProjectedHir> {
121        match hir_projection_kind(properties) {
122            HirProjectionKind::Rpc => Ok(ProjectedHir::Rpc(spec)),
123            HirProjectionKind::Http => rest_hir::project(&spec).map(ProjectedHir::Http),
124            HirProjectionKind::JsonRpc => jsonrpc_hir::project(&spec).map(ProjectedHir::JsonRpc),
125        }
126    }
127
128    /// Builds the source HIR: definitions plus shared source validation.
129    fn lower_source(value: crate::typed_ast::Specification) -> crate::error::ParserResult<Self> {
130        let mut definitions = Vec::new();
131        collect_defs(value.0, &mut definitions);
132        let spec = Self(definitions);
133        // All source targets share HTTP declaration validation; builtins belong
134        // to its temporary type environment, not the generated user models.
135        rest_hir::HttpUnion::validate_source(&spec.0)?;
136        Ok(spec)
137    }
138
139    /// Runs interface expansion and semantic analysis on an already lowered spec.
140    fn finalize(mut spec: Self, expand_interfaces: bool) -> crate::error::ParserResult<Self> {
141        // Validate lexical source scopes before adding RPC wrapper declarations
142        // at their generated scopes.
143        if expand_interfaces {
144            Self::expand_interfaces(&mut spec.0, &mut Vec::new())?;
145        }
146        semantic::analyze(&mut spec);
147        Ok(spec)
148    }
149
150    fn lower(
151        value: crate::typed_ast::Specification,
152        expand_interfaces: bool,
153    ) -> crate::error::ParserResult<Self> {
154        let spec = Self::lower_source(value)?;
155        // API versioning annotations are validated on every compile, even when
156        // no version is projected.
157        VersioningRecord::analyze(&spec)?;
158        Self::finalize(spec, expand_interfaces)
159    }
160
161    fn expand_interfaces(
162        definitions: &mut Vec<Definition>,
163        modules: &mut Vec<String>,
164    ) -> crate::error::ParserResult<()> {
165        for mut definition in std::mem::take(definitions) {
166            match &mut definition {
167                Definition::ModuleDcl(module) => {
168                    modules.push(module.ident.clone());
169                    Self::expand_interfaces(&mut module.definition, modules)?;
170                    modules.pop();
171                }
172                Definition::InterfaceDcl(interface) => {
173                    definitions.extend(interface_codegen::expand_interface(interface, modules)?);
174                }
175                _ => {}
176            }
177            definitions.push(definition);
178        }
179        Ok(())
180    }
181}
182
183pub(crate) fn spec_from_typed_ast(
184    value: crate::typed_ast::Specification,
185    expand_interfaces: bool,
186) -> Specification {
187    Specification::lower(value, expand_interfaces).expect("HIR conversion should not fail")
188}
189
190pub(super) fn collect_defs(defs: Vec<crate::typed_ast::Definition>, out: &mut Vec<Definition>) {
191    for def in defs {
192        match def {
193            crate::typed_ast::Definition::ModuleDcl(module) => {
194                let ident = module.ident.0;
195                let annotations = expand_annotations(module.annotations);
196                let mut inner = Vec::new();
197                collect_defs(module.definition, &mut inner);
198                out.push(Definition::ModuleDcl(ModuleDcl {
199                    annotations,
200                    ident,
201                    definition: inner,
202                }));
203            }
204            crate::typed_ast::Definition::PreprocCall(call) => {
205                if let Some(pragma) = parse_xidlc_pragma(&call) {
206                    out.push(Definition::Pragma(pragma));
207                }
208            }
209            crate::typed_ast::Definition::TypeDcl(value) => {
210                out.push(Definition::TypeDcl(TypeDcl::from(value)))
211            }
212            crate::typed_ast::Definition::ConstDcl(value) => {
213                out.push(Definition::ConstDcl(value.into()))
214            }
215            crate::typed_ast::Definition::ExceptDcl(value) => {
216                out.push(Definition::ExceptDcl(value.into()))
217            }
218            crate::typed_ast::Definition::InterfaceDcl(value) => {
219                out.push(Definition::InterfaceDcl(InterfaceDcl::from(value)));
220            }
221            crate::typed_ast::Definition::PreprocInclude(_) => {
222                // Includes are now handled at the tree-sitter stage.
223                // If we see a PreprocInclude here, it means it was not expanded.
224            }
225            crate::typed_ast::Definition::TemplateModuleDcl(_)
226            | crate::typed_ast::Definition::TemplateModuleInst(_)
227            | crate::typed_ast::Definition::PreprocDefine(_) => {}
228        }
229    }
230}
231
232fn expand_interface(properties: &ParserProperties) -> bool {
233    if let Some(expand) = properties.get("expand_interface").and_then(Value::as_bool) {
234        return expand;
235    }
236
237    matches!(hir_projection_kind(properties), HirProjectionKind::Rpc)
238}
239
240fn hir_projection_kind(properties: &ParserProperties) -> HirProjectionKind {
241    match properties.get("hir_kind").and_then(Value::as_str) {
242        Some(value) if value.eq_ignore_ascii_case("http") => HirProjectionKind::Http,
243        Some(value)
244            if value.eq_ignore_ascii_case("jsonrpc") || value.eq_ignore_ascii_case("json-rpc") =>
245        {
246            HirProjectionKind::JsonRpc
247        }
248        _ => HirProjectionKind::Rpc,
249    }
250}