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