Skip to main content

hyperlight_component_util/
wf.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4//! Component type well-formedness
5//!
6//! This is a pretty direct port of the relevant sections of the OCaml
7//! reference interpreter.
8use itertools::Itertools;
9
10use crate::etypes::{
11    BoundedTyvar, Component, Ctx, Defined, ExternDecl, ExternDesc, Func, Handleable, Instance,
12    Name, Param, QualifiedInstance, RecordField, TypeBound, Value, VariantCase,
13};
14use crate::substitute::{Substitution, Unvoidable};
15use crate::subtype;
16
17/// The various position metadata that affect what value types are
18/// well-formed
19#[derive(Clone, Copy)]
20struct ValueTypePosition {
21    /// Is this well-formedness check for a type that is part of the
22    /// parameter type of a function? (Borrows should be allowed)
23    is_param: bool,
24    dtp: DefinedTypePosition,
25}
26
27impl From<DefinedTypePosition> for ValueTypePosition {
28    fn from(p: DefinedTypePosition) -> ValueTypePosition {
29        ValueTypePosition {
30            is_param: false,
31            dtp: p,
32        }
33    }
34}
35impl ValueTypePosition {
36    fn not_anon_export(self) -> Self {
37        ValueTypePosition {
38            dtp: self.dtp.not_anon_export(),
39            ..self
40        }
41    }
42    fn anon_export(self) -> Self {
43        ValueTypePosition {
44            dtp: self.dtp.anon_export(),
45            ..self
46        }
47    }
48}
49
50/// The various position metadata that affect what defined types are
51/// well-formed
52#[derive(Clone, Copy)]
53pub struct DefinedTypePosition {
54    /// Is this well-formedness check for a type one that should be
55    /// exportable (e.g. one that is being
56    /// exported/imported/outer-aliased-through-an-outer-boundary)?
57    /// (Bare resource types should be disallowed)
58    is_export: bool,
59    /// Is this well-formedness check for a type that should be
60    /// allowed in an "unnamed" export (i.e. nested under some other
61    /// type constructor in an export)? (Record, variant, enum, and
62    /// flags types, which must always be named in exports due to WIT
63    /// constraints, should not be allowed).
64    is_anon_export: bool,
65}
66impl DefinedTypePosition {
67    pub fn internal() -> Self {
68        DefinedTypePosition {
69            is_export: false,
70            is_anon_export: false,
71        }
72    }
73    pub fn export() -> Self {
74        DefinedTypePosition {
75            is_export: true,
76            is_anon_export: false,
77        }
78    }
79    fn not_anon_export(self) -> Self {
80        DefinedTypePosition {
81            is_anon_export: false,
82            ..self
83        }
84    }
85    fn anon_export(self) -> Self {
86        DefinedTypePosition {
87            is_anon_export: true,
88            ..self
89        }
90    }
91}
92
93/// There are several ways in which a type may be ill-formed:
94#[derive(Debug)]
95#[allow(dead_code)]
96pub enum Error<'a> {
97    /// A component/instance exported a bare resource type not behind
98    /// a tyvar (and therefore not named)
99    BareResourceExport,
100    /// A component/instance exported certain complex value types not
101    /// behind a tyvar (and therefore not named)
102    BareComplexValTypeExport(Value<'a>),
103    /// A record has multiple fields with the same name
104    DuplicateRecordField(Name<'a>),
105    /// A variant has multiple cases with the same name
106    DuplicateVariantField(Name<'a>),
107    /// A flags has multiple flags with the same name
108    DuplicateFlagsName(Name<'a>),
109    /// An enum has multiple cases with the same name
110    DuplicateEnumName(Name<'a>),
111    /// An import/export has the same name as another; the boolean is
112    /// true if it is an import
113    DuplicateExternName(&'a str, bool),
114    /// A value type owns or borrows a type that is not a resource type
115    NotAResource(subtype::Error<'a>),
116    /// A borrow type exists somewhere other than a function parameter
117    BorrowOutsideParam,
118}
119
120fn error_if_duplicates_by<T, U: Eq + std::hash::Hash, E>(
121    i: impl Iterator<Item = T>,
122    f: impl FnMut(&T) -> U,
123    e: impl Fn(T) -> E,
124) -> Result<(), E> {
125    let mut duplicates = i.duplicates_by(f);
126    if let Some(x) = duplicates.next() {
127        Err(e(x))
128    } else {
129        Ok(())
130    }
131}
132
133/// # Well-formedness
134///
135/// Most of this is a very direct translation of the specification
136/// (Well-formedness subsections of section 3.4 Type Elaboration).
137impl<'p, 'a> Ctx<'p, 'a> {
138    fn wf_record_fields<'r>(
139        &'r self,
140        p: ValueTypePosition,
141        rfs: &'r [RecordField<'a>],
142    ) -> Result<(), Error<'a>> {
143        rfs.iter()
144            .try_for_each(|rf: &'r RecordField<'a>| self.wf_value(p, &rf.ty))?;
145        error_if_duplicates_by(
146            rfs.iter(),
147            |&rf| rf.name.name,
148            |rf| Error::DuplicateRecordField(rf.name),
149        )?;
150        Ok(())
151    }
152    fn wf_variant_cases<'r>(
153        &'r self,
154        p: ValueTypePosition,
155        vcs: &'r [VariantCase<'a>],
156    ) -> Result<(), Error<'a>> {
157        vcs.iter()
158            .try_for_each(|vc: &'r VariantCase<'a>| self.wf_value_option(p, &vc.ty))?;
159        error_if_duplicates_by(
160            vcs.iter(),
161            |&vc| vc.name.name,
162            |vc| Error::DuplicateVariantField(vc.name),
163        )?;
164        Ok(())
165    }
166    fn wf_value<'r>(&'r self, p: ValueTypePosition, vt: &'r Value<'a>) -> Result<(), Error<'a>> {
167        let anon_err: Result<(), Error<'a>> = if p.dtp.is_export && p.dtp.is_anon_export {
168            Err(Error::BareComplexValTypeExport(vt.clone()))
169        } else {
170            Ok(())
171        };
172        let p_ = p.anon_export();
173        let resource_err = |h| {
174            self.wf_handleable(p.dtp, h).and(
175                self.subtype_handleable_is_resource(h)
176                    .map_err(Error::NotAResource),
177            )
178        };
179        match vt {
180            Value::Bool => Ok(()),
181            Value::S(_) => Ok(()),
182            Value::U(_) => Ok(()),
183            Value::F(_) => Ok(()),
184            Value::Char => Ok(()),
185            Value::String => Ok(()),
186            Value::List(vt) => self.wf_value(p_, vt),
187            Value::FixList(vt, _) => self.wf_value(p_, vt),
188            Value::Record(rfs) => anon_err.and(self.wf_record_fields(p_, rfs)),
189            Value::Variant(vcs) => anon_err.and(self.wf_variant_cases(p_, vcs)),
190            Value::Flags(ns) => anon_err.and(error_if_duplicates_by(
191                ns.iter(),
192                |&n| n.name,
193                |n| Error::DuplicateFlagsName(*n),
194            )),
195            Value::Enum(ns) => anon_err.and(error_if_duplicates_by(
196                ns.iter(),
197                |&n| n.name,
198                |n| Error::DuplicateEnumName(*n),
199            )),
200            Value::Option(vt) => self.wf_value(p_, vt),
201            Value::Tuple(vs) => vs
202                .iter()
203                .try_for_each(|vt: &'r Value<'a>| self.wf_value(p_, vt)),
204            Value::Result(vt1, vt2) => self
205                .wf_value_option(p_, vt1)
206                .and(self.wf_value_option(p_, vt2)),
207            Value::Own(h) => resource_err(h),
208            Value::Borrow(h) => {
209                if p.is_param {
210                    resource_err(h)
211                } else {
212                    Err(Error::BorrowOutsideParam)
213                }
214            }
215            Value::Var(tv, vt) => tv
216                .as_ref()
217                .map(|tv| self.wf_type_bound(p.dtp, self.var_bound(tv)))
218                .unwrap_or(Ok(()))
219                .and(self.wf_value(p.not_anon_export(), vt)),
220        }
221    }
222    fn wf_value_option<'r>(
223        &'r self,
224        p: ValueTypePosition,
225        vt: &'r Option<Value<'a>>,
226    ) -> Result<(), Error<'a>> {
227        vt.as_ref().map_or(Ok(()), |ty| self.wf_value(p, ty))
228    }
229    fn wf_func<'r>(&'r self, p: DefinedTypePosition, ft: &'r Func<'a>) -> Result<(), Error<'a>> {
230        let p_ = p.anon_export();
231        let param_pos = ValueTypePosition {
232            is_param: true,
233            dtp: p_,
234        };
235        let result_pos = ValueTypePosition {
236            is_param: false,
237            dtp: p_,
238        };
239        ft.params
240            .iter()
241            .try_for_each(|fp: &'r Param<'a>| self.wf_value(param_pos, &fp.ty))?;
242        match &ft.result {
243            Some(vt) => self.wf_value(result_pos, vt),
244            None => Ok(()),
245        }
246    }
247    fn wf_type_bound<'r>(
248        &'r self,
249        p: DefinedTypePosition,
250        tb: &'r TypeBound<'a>,
251    ) -> Result<(), Error<'a>> {
252        match tb {
253            TypeBound::SubResource => Ok(()),
254            TypeBound::Eq(dt) => self.wf_defined(p.not_anon_export(), dt),
255        }
256    }
257    fn wf_bounded_tyvar<'r>(
258        &'r self,
259        p: DefinedTypePosition,
260        btv: &'r BoundedTyvar<'a>,
261    ) -> Result<(), Error<'a>> {
262        match &btv.bound {
263            TypeBound::SubResource => Ok(()),
264            TypeBound::Eq(dt) => self.wf_defined(p, dt),
265        }
266    }
267
268    fn wf_handleable<'r>(
269        &'r self,
270        p: DefinedTypePosition,
271        ht: &'r Handleable,
272    ) -> Result<(), Error<'a>> {
273        match ht {
274            Handleable::Var(tv) => self.wf_type_bound(p, self.var_bound(tv)),
275            Handleable::Resource(rid) => {
276                if p.is_export {
277                    Err(Error::BareResourceExport)
278                } else {
279                    // Internal invariant: rtidx should always exist
280                    assert!((rid.id as usize) < self.rtypes.len());
281                    Ok(())
282                }
283            }
284        }
285    }
286    pub fn wf_defined<'r>(
287        &'r self,
288        p: DefinedTypePosition,
289        dt: &'r Defined<'a>,
290    ) -> Result<(), Error<'a>> {
291        match dt {
292            Defined::Handleable(ht) => self.wf_handleable(p, ht),
293            Defined::Value(vt) => self.wf_value(p.into(), vt),
294            Defined::Func(ft) => self.wf_func(p, ft),
295            Defined::Instance(it) => self.wf_qualified_instance(p, it),
296            Defined::Component(ct) => self.wf_component(p, ct),
297        }
298    }
299    fn wf_extern_desc<'r>(
300        &self,
301        p: DefinedTypePosition,
302        ed: &'r ExternDesc<'a>,
303    ) -> Result<(), Error<'a>> {
304        match ed {
305            ExternDesc::CoreModule(_) => Ok(()),
306            ExternDesc::Func(ft) => self.wf_func(p, ft),
307            ExternDesc::Type(dt) => self.wf_defined(p, dt),
308            ExternDesc::Instance(it) => self.wf_instance(p, it),
309            ExternDesc::Component(ct) => self.wf_component(p, ct),
310        }
311    }
312    fn wf_extern_decl<'r>(
313        &self,
314        p: DefinedTypePosition,
315        ed: &'r ExternDecl<'a>,
316    ) -> Result<(), Error<'a>> {
317        self.wf_extern_desc(p, &ed.desc)
318    }
319    fn wf_instance<'r>(
320        &self,
321        p: DefinedTypePosition,
322        it: &'r Instance<'a>,
323    ) -> Result<(), Error<'a>> {
324        error_if_duplicates_by(
325            it.exports.iter(),
326            |&ex| ex.kebab_name,
327            |ex| Error::DuplicateExternName(ex.kebab_name, false),
328        )?;
329        it.exports
330            .iter()
331            .try_for_each(|ed| self.wf_extern_decl(p, ed))
332    }
333    pub fn wf_qualified_instance<'r>(
334        &self,
335        p: DefinedTypePosition,
336        qit: &'r QualifiedInstance<'a>,
337    ) -> Result<(), Error<'a>> {
338        let mut ctx_ = self.clone();
339        let subst = ctx_.bound_to_evars(None, &qit.evars);
340        ctx_.evars
341            .iter()
342            .try_for_each(|(btv, _)| ctx_.wf_bounded_tyvar(p, btv))?;
343        let it = subst.instance(&qit.unqualified).not_void();
344        ctx_.wf_instance(p, &it)
345    }
346    pub fn wf_component<'r>(
347        &self,
348        p: DefinedTypePosition,
349        ct: &'r Component<'a>,
350    ) -> Result<(), Error<'a>> {
351        let mut ctx_ = self.clone();
352        let subst = ctx_.bound_to_uvars(None, &ct.uvars, false);
353        ctx_.uvars
354            .iter()
355            .try_for_each(|(btv, _)| ctx_.wf_bounded_tyvar(p, btv))?;
356        error_if_duplicates_by(
357            ct.imports.iter(),
358            |&im| im.kebab_name,
359            |im| Error::DuplicateExternName(im.kebab_name, true),
360        )?;
361        ct.imports
362            .iter()
363            .map(|ed| subst.extern_decl(ed).not_void())
364            .try_for_each(|ed| ctx_.wf_extern_decl(p, &ed))?;
365        let it = subst.qualified_instance(&ct.instance).not_void();
366        ctx_.wf_qualified_instance(p, &it)
367    }
368}