Skip to main content

xidl_parser/hir/
spec.rs

1use super::{
2    Definition, FsIncludeResolver, InterfaceDcl, ModuleDcl, ParserProperties, Specification,
3    TypeDcl, VersionedIncludes, expand_annotations, interface_codegen, parse_xidlc_pragma,
4};
5use crate::jsonrpc_hir;
6use crate::parser::IncludeResolver;
7use crate::rest_hir::{self, HirProjectionKind, ProjectedHir};
8use crate::semantic;
9use crate::versioning::{self, VersionedProjection, VersioningRecord};
10use serde_json::Value;
11use std::path::Path;
12
13impl From<crate::typed_ast::Specification> for Specification {
14    fn from(value: crate::typed_ast::Specification) -> Self {
15        let mut resolver = FsIncludeResolver;
16        let mut includes = VersionedIncludes::new(&mut resolver, None);
17        spec_from_typed_ast(value, true, &mut includes)
18    }
19}
20
21impl Specification {
22    pub fn from_typed_ast_with_properties(
23        value: crate::typed_ast::Specification,
24        properties: ParserProperties,
25    ) -> Self {
26        let mut resolver = FsIncludeResolver;
27        let mut includes = VersionedIncludes::new(&mut resolver, None);
28        spec_from_typed_ast(value, expand_interface(&properties), &mut includes)
29    }
30
31    pub fn from_typed_ast_with_properties_and_path(
32        value: crate::typed_ast::Specification,
33        properties: ParserProperties,
34        path: impl AsRef<Path>,
35    ) -> crate::error::ParserResult<Self> {
36        let mut resolver = FsIncludeResolver;
37        let current = Self::current_path(path.as_ref());
38        let mut includes = VersionedIncludes::new(&mut resolver, current);
39        Self::lower(value, expand_interface(&properties), &mut includes)
40    }
41
42    pub fn from_typed_ast_with_path(
43        value: crate::typed_ast::Specification,
44        path: impl AsRef<Path>,
45    ) -> crate::error::ParserResult<Self> {
46        let mut resolver = FsIncludeResolver;
47        let current = Self::current_path(path.as_ref());
48        let mut includes = VersionedIncludes::new(&mut resolver, current);
49        Self::lower(value, true, &mut includes)
50    }
51
52    /// Lowers a typed AST and returns one HIR projection per requested version.
53    ///
54    /// Unversioned specifications produce exactly one projection without a
55    /// version. Versioned specifications produce one merged projection holding
56    /// one module per selected version (`merged` layout, the default), or one
57    /// projection per selected version (`per_version` layout).
58    pub fn project_typed_ast_versions(
59        value: crate::typed_ast::Specification,
60        properties: ParserProperties,
61        path: impl AsRef<Path>,
62    ) -> crate::error::ParserResult<Vec<VersionedProjection>> {
63        let mut resolver = FsIncludeResolver;
64        Self::project_typed_ast_versions_with_resolver(value, properties, path, &mut resolver)
65    }
66
67    /// Lowers a typed AST with `resolver` for the includes it names.
68    ///
69    /// Versioned includes need the resolver that resolved the unit's own
70    /// includes, so a caller that has one passes it here instead of letting the
71    /// filesystem resolver stand in.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error when a versioned include cannot be resolved, read,
76    /// projected, or spliced.
77    pub fn project_typed_ast_versions_with_resolver(
78        value: crate::typed_ast::Specification,
79        properties: ParserProperties,
80        path: impl AsRef<Path>,
81        resolver: &mut dyn IncludeResolver,
82    ) -> crate::error::ParserResult<Vec<VersionedProjection>> {
83        let current = Self::current_path(path.as_ref());
84        let mut includes = VersionedIncludes::new(resolver, current);
85        let layout = versioning::VersionLayout::from_properties(&properties)?;
86        let selection = versioning::selection_from_properties(&properties)?;
87        let expand = expand_interface(&properties);
88        let source = Self::lower_source(value, &mut includes)?;
89        let Some(record) = VersioningRecord::analyze(&source)? else {
90            if let Some(selection) = &selection {
91                return Err(versioning::unversioned_selection_error(selection));
92            }
93            let spec = Self::finalize(source, expand)?;
94            return Ok(vec![VersionedProjection {
95                versions: Vec::new(),
96                version: None,
97                hir: Self::project_hir(spec, &properties)?,
98            }]);
99        };
100        let versions = versioning::selected_versions(selection.as_ref(), &record)?;
101        match layout {
102            versioning::VersionLayout::Merged => {
103                let spec = record.merge(&source, &versions)?;
104                let spec = Self::finalize(spec, expand)?;
105                let selected = versions
106                    .iter()
107                    .filter_map(|version| record.timeline().get(*version).cloned())
108                    .collect();
109                Ok(vec![VersionedProjection {
110                    versions: selected,
111                    version: None,
112                    hir: Self::project_hir(spec, &properties)?,
113                }])
114            }
115            versioning::VersionLayout::PerVersion => {
116                let mut projections = Vec::new();
117                for version in versions {
118                    let spec = record.project(&source, version)?;
119                    let spec = Self::finalize(spec, expand)?;
120                    let definition = record.timeline().get(version).cloned();
121                    projections.push(VersionedProjection {
122                        versions: definition.clone().into_iter().collect(),
123                        version: definition,
124                        hir: Self::project_hir(spec, &properties)?,
125                    });
126                }
127                Ok(projections)
128            }
129        }
130    }
131
132    /// Lowers a typed AST and projects the first request of the merged layout.
133    ///
134    /// Callers that need per-version output use [`Self::project_typed_ast_versions`].
135    pub fn project_typed_ast_with_properties_and_path(
136        value: crate::typed_ast::Specification,
137        properties: ParserProperties,
138        path: impl AsRef<Path>,
139    ) -> crate::error::ParserResult<ProjectedHir> {
140        let mut projections = Self::project_typed_ast_versions(value, properties, path)?;
141        match projections.len() {
142            1 => Ok(projections.remove(0).hir),
143            _ => Err(crate::error::ParseError::Message(
144                "xidl versioning: per-version layout requires project_typed_ast_versions"
145                    .to_string(),
146            )),
147        }
148    }
149
150    fn project_hir(
151        spec: Specification,
152        properties: &ParserProperties,
153    ) -> crate::error::ParserResult<ProjectedHir> {
154        match hir_projection_kind(properties) {
155            HirProjectionKind::Rpc => Ok(ProjectedHir::Rpc(spec)),
156            HirProjectionKind::Http => rest_hir::project(&spec).map(ProjectedHir::Http),
157            HirProjectionKind::JsonRpc => jsonrpc_hir::project(&spec).map(ProjectedHir::JsonRpc),
158        }
159    }
160
161    /// Builds the source HIR: definitions plus shared source validation.
162    fn lower_source(
163        value: crate::typed_ast::Specification,
164        includes: &mut VersionedIncludes<'_>,
165    ) -> crate::error::ParserResult<Self> {
166        let mut definitions = Vec::new();
167        collect_defs(value.0, &mut definitions, includes)?;
168        let spec = Self(definitions);
169        // All source targets share HTTP declaration validation; builtins belong
170        // to its temporary type environment, not the generated user models.
171        rest_hir::HttpUnion::validate_source(&spec.0)?;
172        Ok(spec)
173    }
174
175    /// Runs interface expansion and semantic analysis on an already lowered spec.
176    fn finalize(mut spec: Self, expand_interfaces: bool) -> crate::error::ParserResult<Self> {
177        // Validate lexical source scopes before adding RPC wrapper declarations
178        // at their generated scopes.
179        if expand_interfaces {
180            Self::expand_interfaces(&mut spec.0, &mut Vec::new())?;
181        }
182        semantic::analyze(&mut spec);
183        Ok(spec)
184    }
185
186    fn lower(
187        value: crate::typed_ast::Specification,
188        expand_interfaces: bool,
189        includes: &mut VersionedIncludes<'_>,
190    ) -> crate::error::ParserResult<Self> {
191        let spec = Self::lower_source(value, includes)?;
192        // API versioning annotations are validated on every compile, even when
193        // no version is projected.
194        VersioningRecord::analyze(&spec)?;
195        Self::finalize(spec, expand_interfaces)
196    }
197
198    /// Path handed to include resolution, when it is valid utf-8.
199    fn current_path(path: &Path) -> Option<String> {
200        path.to_str().map(str::to_string)
201    }
202
203    fn expand_interfaces(
204        definitions: &mut Vec<Definition>,
205        modules: &mut Vec<String>,
206    ) -> crate::error::ParserResult<()> {
207        for mut definition in std::mem::take(definitions) {
208            match &mut definition {
209                Definition::ModuleDcl(module) => {
210                    modules.push(module.ident.clone());
211                    Self::expand_interfaces(&mut module.definition, modules)?;
212                    modules.pop();
213                }
214                Definition::InterfaceDcl(interface) => {
215                    definitions.extend(interface_codegen::expand_interface(interface, modules)?);
216                }
217                _ => {}
218            }
219            definitions.push(definition);
220        }
221        Ok(())
222    }
223}
224
225pub(crate) fn spec_from_typed_ast(
226    value: crate::typed_ast::Specification,
227    expand_interfaces: bool,
228    includes: &mut VersionedIncludes<'_>,
229) -> Specification {
230    Specification::lower(value, expand_interfaces, includes)
231        .expect("HIR conversion should not fail")
232}
233
234pub(super) fn collect_defs(
235    defs: Vec<crate::typed_ast::Definition>,
236    out: &mut Vec<Definition>,
237    includes: &mut VersionedIncludes<'_>,
238) -> crate::error::ParserResult<()> {
239    for def in defs {
240        match def {
241            crate::typed_ast::Definition::ModuleDcl(module) => {
242                let ident = module.ident.0;
243                let annotations = expand_annotations(module.annotations);
244                let mut inner = Vec::new();
245                collect_defs(module.definition, &mut inner, includes)?;
246                out.push(Definition::ModuleDcl(ModuleDcl {
247                    annotations,
248                    ident,
249                    definition: inner,
250                }));
251            }
252            crate::typed_ast::Definition::PreprocCall(call) => {
253                if let Some(pragma) = parse_xidlc_pragma(&call) {
254                    out.push(Definition::Pragma(pragma));
255                }
256            }
257            crate::typed_ast::Definition::TypeDcl(value) => {
258                out.push(Definition::TypeDcl(TypeDcl::from(value)))
259            }
260            crate::typed_ast::Definition::ConstDcl(value) => {
261                out.push(Definition::ConstDcl(value.into()))
262            }
263            crate::typed_ast::Definition::ExceptDcl(value) => {
264                out.push(Definition::ExceptDcl(value.into()))
265            }
266            crate::typed_ast::Definition::InterfaceDcl(value) => {
267                out.push(Definition::InterfaceDcl(InterfaceDcl::from(value)));
268            }
269            crate::typed_ast::Definition::PreprocInclude(include) => {
270                // Plain includes are handled at the tree-sitter stage. A
271                // versioned include reaches this point and is projected here.
272                if let Some(path) = include_path_text(&include) {
273                    includes.expand(&path, out)?;
274                }
275            }
276            crate::typed_ast::Definition::ImportDcl(_) => {
277                // Imports name another compilation unit. The unit graph owns
278                // them, so the lowered HIR of one unit stays unchanged.
279            }
280            crate::typed_ast::Definition::TemplateModuleDcl(_)
281            | crate::typed_ast::Definition::TemplateModuleInst(_)
282            | crate::typed_ast::Definition::PreprocDefine(_) => {}
283        }
284    }
285    Ok(())
286}
287
288/// The unquoted path of an include declaration, when it was written as one.
289fn include_path_text(include: &crate::typed_ast::PreprocInclude) -> Option<String> {
290    match &include.path {
291        crate::typed_ast::PreprocIncludePath::StringLiteral(text) => {
292            Some(text.trim().trim_matches('"').to_string())
293        }
294        crate::typed_ast::PreprocIncludePath::SystemLibString(_)
295        | crate::typed_ast::PreprocIncludePath::Identifier(_) => None,
296    }
297}
298
299fn expand_interface(properties: &ParserProperties) -> bool {
300    if let Some(expand) = properties.get("expand_interface").and_then(Value::as_bool) {
301        return expand;
302    }
303
304    matches!(hir_projection_kind(properties), HirProjectionKind::Rpc)
305}
306
307fn hir_projection_kind(properties: &ParserProperties) -> HirProjectionKind {
308    match properties.get("hir_kind").and_then(Value::as_str) {
309        Some(value) if value.eq_ignore_ascii_case("http") => HirProjectionKind::Http,
310        Some(value)
311            if value.eq_ignore_ascii_case("jsonrpc") || value.eq_ignore_ascii_case("json-rpc") =>
312        {
313            HirProjectionKind::JsonRpc
314        }
315        _ => HirProjectionKind::Rpc,
316    }
317}