Skip to main content

miden_assembly_syntax/ast/
type.rs

1use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec};
2
3use miden_debug_types::{SourceManager, SourceSpan, Span, Spanned};
4use midenc_hir_type::{AddressSpace, Type, TypeRepr, TypeTemplate};
5
6use super::{
7    ConstantExpr, DocString, GlobalItemIndex, Ident, ItemIndex, Path, SymbolResolution,
8    SymbolResolutionError, Visibility, types,
9};
10
11/// Maximum allowed nesting depth of type expressions during resolution.
12///
13/// This limit is intended to prevent stack overflows from maliciously deep type expressions while
14/// remaining far above typical type nesting in real programs.
15const MAX_TYPE_EXPR_NESTING: usize = 256;
16
17/// Abstracts over resolving an item to a concrete [Type], using one of:
18///
19/// * A [GlobalItemIndex]
20/// * An [ItemIndex]
21/// * A [Path]
22/// * A [TypeExpr]
23///
24/// Since type resolution happens in two different contexts during assembly, this abstraction allows
25/// us to share more of the resolution logic in both places.
26///
27/// NOTE: Most methods of this trait take a mutable reference to the resolver, so that the resolver
28/// can mutate its own state as necessary during resolution (e.g. to manage a cache, or other side
29/// table-like data structures).
30pub trait TypeResolver<E> {
31    fn source_manager(&self) -> Arc<dyn SourceManager>;
32    /// Should be called by consumers of this resolver to convert a [SymbolResolutionError] to the
33    /// error type used by the [TypeResolver] implementation.
34    fn resolve_local_failed(&self, err: SymbolResolutionError) -> E;
35    /// Resolve the item given by `gid` to a type template.
36    ///
37    /// This yields a template rather than a [Type] because a declaration may be part of a
38    /// recursive group that is still being resolved, in which case the only thing that can be
39    /// produced for it is a back-reference. Nothing becomes a [Type] until the whole group is
40    /// known; see [`Self::finalize`].
41    fn get_type(
42        &mut self,
43        context: SourceSpan,
44        gid: GlobalItemIndex,
45    ) -> Result<Option<TypeTemplate>, E>;
46    /// Resolve the item in the current module given by `id` to a type template.
47    fn get_local_type(
48        &mut self,
49        context: SourceSpan,
50        id: ItemIndex,
51    ) -> Result<Option<TypeTemplate>, E>;
52    /// Attempt to resolve a symbol path, given by a `TypeExpr::Ref`, to an item
53    fn resolve_type_ref(&mut self, ty: Span<&Path>) -> Result<SymbolResolution, E>;
54    /// Materialize a template as a concrete [Type], building any recursive group it takes part in.
55    fn finalize(&mut self, context: SourceSpan, template: TypeTemplate) -> Result<Type, E>;
56    /// Resolve a [TypeExpr] to a concrete [Type]
57    fn resolve(&mut self, ty: &TypeExpr) -> Result<Option<Type>, E> {
58        match ty.resolve_template(self)? {
59            Some(template) => self.finalize(ty.span(), template).map(Some),
60            None => Ok(None),
61        }
62    }
63}
64
65// TYPE DECLARATION
66// ================================================================================================
67
68/// An abstraction over the different types of type declarations allowed in Miden Assembly
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum TypeDecl {
71    /// A named type, i.e. a type alias
72    Alias(TypeAlias),
73    /// A C-like enumeration type with associated constants
74    Enum(EnumType),
75}
76
77impl TypeDecl {
78    /// Adds documentation to this type alias
79    pub fn with_docs(self, docs: Option<Span<String>>) -> Self {
80        match self {
81            Self::Alias(ty) => Self::Alias(ty.with_docs(docs)),
82            Self::Enum(ty) => Self::Enum(ty.with_docs(docs)),
83        }
84    }
85
86    /// Get the name assigned to this type declaration
87    pub fn name(&self) -> &Ident {
88        match self {
89            Self::Alias(ty) => &ty.name,
90            Self::Enum(ty) => &ty.name,
91        }
92    }
93
94    /// Get the visibility of this type declaration
95    pub const fn visibility(&self) -> Visibility {
96        match self {
97            Self::Alias(ty) => ty.visibility,
98            Self::Enum(ty) => ty.visibility,
99        }
100    }
101
102    /// Get the documentation of this enum type
103    pub fn docs(&self) -> Option<Span<&str>> {
104        match self {
105            Self::Alias(ty) => ty.docs(),
106            Self::Enum(ty) => ty.docs(),
107        }
108    }
109
110    /// Get the type expression associated with this declaration
111    pub fn ty(&self) -> TypeExpr {
112        match self {
113            Self::Alias(ty) => ty.ty.clone(),
114            Self::Enum(ty) => TypeExpr::Primitive(Span::new(ty.span, ty.ty.clone())),
115        }
116    }
117}
118
119impl Spanned for TypeDecl {
120    fn span(&self) -> SourceSpan {
121        match self {
122            Self::Alias(spanned) => spanned.span,
123            Self::Enum(spanned) => spanned.span,
124        }
125    }
126}
127
128impl From<TypeAlias> for TypeDecl {
129    fn from(value: TypeAlias) -> Self {
130        Self::Alias(value)
131    }
132}
133
134impl From<EnumType> for TypeDecl {
135    fn from(value: EnumType) -> Self {
136        Self::Enum(value)
137    }
138}
139
140impl crate::prettier::PrettyPrint for TypeDecl {
141    fn render(&self) -> crate::prettier::Document {
142        match self {
143            Self::Alias(ty) => ty.render(),
144            Self::Enum(ty) => ty.render(),
145        }
146    }
147}
148
149// FUNCTION TYPE
150// ================================================================================================
151
152/// A procedure type signature
153#[derive(Debug, Clone)]
154pub struct FunctionType {
155    pub span: SourceSpan,
156    pub cc: types::CallConv,
157    pub args: Vec<TypeExpr>,
158    pub results: Vec<TypeExpr>,
159}
160
161impl Eq for FunctionType {}
162
163impl PartialEq for FunctionType {
164    fn eq(&self, other: &Self) -> bool {
165        self.cc == other.cc && self.args == other.args && self.results == other.results
166    }
167}
168
169impl core::hash::Hash for FunctionType {
170    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
171        self.cc.hash(state);
172        self.args.hash(state);
173        self.results.hash(state);
174    }
175}
176
177impl Spanned for FunctionType {
178    fn span(&self) -> SourceSpan {
179        self.span
180    }
181}
182
183impl FunctionType {
184    pub fn new(cc: types::CallConv, args: Vec<TypeExpr>, results: Vec<TypeExpr>) -> Self {
185        Self {
186            span: SourceSpan::UNKNOWN,
187            cc,
188            args,
189            results,
190        }
191    }
192
193    /// Override the default source span
194    #[inline]
195    pub fn with_span(mut self, span: SourceSpan) -> Self {
196        self.span = span;
197        self
198    }
199}
200
201impl crate::prettier::PrettyPrint for FunctionType {
202    fn render(&self) -> crate::prettier::Document {
203        use crate::prettier::*;
204
205        let singleline_args = self
206            .args
207            .iter()
208            .map(PrettyPrint::render)
209            .reduce(|acc, arg| acc + const_text(", ") + arg)
210            .unwrap_or(Document::Empty);
211        let multiline_args = indent(
212            4,
213            nl() + self
214                .args
215                .iter()
216                .map(PrettyPrint::render)
217                .reduce(|acc, arg| acc + const_text(",") + nl() + arg)
218                .unwrap_or(Document::Empty),
219        ) + nl();
220        let args = singleline_args | multiline_args;
221        let args = const_text("(") + args + const_text(")");
222
223        match self.results.len() {
224            0 => args,
225            1 => args + const_text(" -> ") + self.results[0].render(),
226            _ => {
227                let results = self
228                    .results
229                    .iter()
230                    .map(PrettyPrint::render)
231                    .reduce(|acc, r| acc + const_text(", ") + r)
232                    .unwrap_or(Document::Empty);
233                args + const_text(" -> ") + const_text("(") + results + const_text(")")
234            },
235        }
236    }
237}
238
239// TYPE EXPRESSION
240// ================================================================================================
241
242/// A syntax-level type expression (i.e. primitive type, reference to nominal type, etc.)
243#[derive(Debug, Clone, Eq, PartialEq, Hash)]
244pub enum TypeExpr {
245    /// A primitive integral type, e.g. `i1`, `u16`
246    Primitive(Span<Type>),
247    /// A pointer type expression, e.g. `*u8`
248    Ptr(PointerType),
249    /// An array type expression, e.g. `[u8; 32]`
250    Array(ArrayType),
251    /// A struct type expression, e.g. `struct { a: u32 }`
252    Struct(StructType),
253    /// A reference to a type aliased by name, e.g. `Foo`
254    Ref(Span<Arc<Path>>),
255}
256
257impl TypeExpr {
258    /// Set the name associated with this type expression, if applicable.
259    ///
260    /// Currently this just sets the name of struct types, but if we add other types with names in
261    /// the future, we can support them here.
262    pub fn set_name(&mut self, name: Ident) {
263        match self {
264            Self::Struct(struct_ty) => {
265                struct_ty.name = Some(name);
266            },
267            Self::Primitive(_) | Self::Ptr(_) | Self::Array(_) | Self::Ref(_) => (),
268        }
269    }
270
271    /// Get any references to other types present in this expression
272    pub fn references(&self) -> Vec<Span<Arc<Path>>> {
273        use alloc::collections::BTreeSet;
274
275        let mut worklist = smallvec::SmallVec::<[_; 4]>::from_slice(&[self]);
276        let mut references = BTreeSet::new();
277
278        while let Some(ty) = worklist.pop() {
279            match ty {
280                Self::Primitive(_) => {},
281                Self::Ptr(ty) => {
282                    worklist.push(&ty.pointee);
283                },
284                Self::Array(ty) => {
285                    worklist.push(&ty.elem);
286                },
287                Self::Struct(ty) => {
288                    for field in ty.fields.iter() {
289                        worklist.push(&field.ty);
290                    }
291                },
292                Self::Ref(ty) => {
293                    references.insert(ty.clone());
294                },
295            }
296        }
297
298        references.into_iter().collect()
299    }
300
301    /// Resolve this type expression to a concrete type, using `resolver`
302    /// Resolve this expression to a template, leaving references to declarations which are still
303    /// being resolved as back-references.
304    pub fn resolve_template<E, R>(&self, resolver: &mut R) -> Result<Option<TypeTemplate>, E>
305    where
306        R: ?Sized + TypeResolver<E>,
307    {
308        self.resolve_template_with_depth(resolver, 0)
309    }
310
311    fn resolve_template_with_depth<E, R>(
312        &self,
313        resolver: &mut R,
314        depth: usize,
315    ) -> Result<Option<TypeTemplate>, E>
316    where
317        R: ?Sized + TypeResolver<E>,
318    {
319        if depth > MAX_TYPE_EXPR_NESTING {
320            let source_manager = resolver.source_manager();
321            return Err(resolver.resolve_local_failed(
322                SymbolResolutionError::type_expression_depth_exceeded(
323                    self.span(),
324                    MAX_TYPE_EXPR_NESTING,
325                    source_manager.as_ref(),
326                ),
327            ));
328        }
329
330        match self {
331            TypeExpr::Ref(path) => {
332                let mut current_path = path.clone();
333                loop {
334                    match resolver.resolve_type_ref(current_path.as_deref())? {
335                        SymbolResolution::Local(item) => {
336                            return resolver.get_local_type(current_path.span(), item.into_inner());
337                        },
338                        SymbolResolution::External(path) => {
339                            // We don't have a definition for this type yet
340                            if path == current_path {
341                                break Ok(None);
342                            }
343                            current_path = path;
344                        },
345                        SymbolResolution::Exact { gid, .. } => {
346                            return resolver.get_type(current_path.span(), gid);
347                        },
348                        SymbolResolution::Module { path: module_path, .. } => {
349                            break Err(resolver.resolve_local_failed(
350                                SymbolResolutionError::invalid_symbol_type(
351                                    path.span(),
352                                    "type",
353                                    module_path.span(),
354                                    &resolver.source_manager(),
355                                ),
356                            ));
357                        },
358                        SymbolResolution::MastRoot(item) => {
359                            break Err(resolver.resolve_local_failed(
360                                SymbolResolutionError::invalid_symbol_type(
361                                    path.span(),
362                                    "type",
363                                    item.span(),
364                                    &resolver.source_manager(),
365                                ),
366                            ));
367                        },
368                    }
369                }
370            },
371            TypeExpr::Primitive(t) => Ok(Some(TypeTemplate::Type(t.inner().clone()))),
372            TypeExpr::Array(t) => Ok(t
373                .elem
374                .resolve_template_with_depth(resolver, depth + 1)?
375                .map(|elem| TypeTemplate::array(elem, t.arity))),
376            TypeExpr::Ptr(ty) => Ok(ty
377                .pointee
378                .resolve_template_with_depth(resolver, depth + 1)?
379                .map(TypeTemplate::ptr)),
380            TypeExpr::Struct(t) => {
381                let mut fields = Vec::with_capacity(t.fields.len());
382                for field in t.fields.iter() {
383                    let field_ty = field.ty.resolve_template_with_depth(resolver, depth + 1)?;
384                    if let Some(field_ty) = field_ty {
385                        fields.push(types::FieldTemplate {
386                            name: Some(field.name.clone().into_inner()),
387                            ty: field_ty,
388                        });
389                    } else {
390                        return Ok(None);
391                    }
392                }
393                Ok(Some(TypeTemplate::Struct(Box::new(types::StructTemplate {
394                    name: t.name.clone().map(Ident::into_inner),
395                    repr: t.repr.into_inner(),
396                    fields,
397                }))))
398            },
399        }
400    }
401}
402
403impl From<Type> for TypeExpr {
404    fn from(ty: Type) -> Self {
405        let mut expanding = Vec::new();
406        type_expr_from(ty, &mut expanding)
407    }
408}
409
410/// Convert a [Type] to a [TypeExpr], rendering a recursive aggregate's backedge as a reference by
411/// name rather than expanding it again.
412///
413/// `expanding` holds the recursive definitions whose bodies are currently being written out.
414/// Without it a recursive struct expands forever: the body is unfolded, its pointer field is
415/// converted, and converting the pointee unfolds the same body again.
416fn type_expr_from(ty: Type, expanding: &mut Vec<types::RecTypeRef>) -> TypeExpr {
417    match ty {
418        Type::Array(t) => TypeExpr::Array(ArrayType::new(
419            type_expr_from(t.element_type().clone(), expanding),
420            t.len(),
421        )),
422        Type::Struct(t) => {
423            let name = t.name().and_then(|name| Ident::new(name.as_ref()).ok());
424
425            // A backedge to a definition already being written out becomes a reference to it,
426            // which is how it would have been written in source in the first place.
427            if let Some(rec) = t.as_recursive() {
428                if expanding.contains(rec) {
429                    let name = name.unwrap_or_else(|| {
430                        panic!(
431                            "unrepresentable type value: a recursive struct without a name cannot \
432                             be referred to as a type expression"
433                        )
434                    });
435                    return TypeExpr::Ref(Span::unknown(
436                        Path::from_ident(&name).into_owned().into(),
437                    ));
438                }
439                expanding.push(rec.clone());
440            }
441
442            let is_recursive = t.is_recursive();
443            let body = t.get();
444            let fields = body
445                .fields()
446                .iter()
447                .enumerate()
448                .map(|(i, ft)| {
449                    let name = ft
450                        .name
451                        .as_deref()
452                        .map(Ident::new)
453                        .and_then(Result::ok)
454                        .unwrap_or_else(|| Ident::new(format!("field{i}")).unwrap());
455                    StructField {
456                        span: SourceSpan::UNKNOWN,
457                        name,
458                        ty: type_expr_from(ft.ty.clone(), expanding),
459                    }
460                })
461                .collect::<Vec<_>>();
462            let converted = TypeExpr::Struct(
463                StructType::new(name, fields)
464                    .with_repr(Span::unknown(body.repr()))
465                    .with_span(SourceSpan::UNKNOWN),
466            );
467
468            if is_recursive {
469                expanding.pop();
470            }
471            converted
472        },
473        Type::Ptr(t) => TypeExpr::Ptr(
474            PointerType::new(type_expr_from(t.pointee().clone(), expanding))
475                .with_address_space(t.addrspace()),
476        ),
477        Type::Function(_) => {
478            TypeExpr::Ptr(PointerType::new(TypeExpr::Primitive(Span::unknown(Type::Felt))))
479        },
480        Type::List(t) => TypeExpr::Ptr(
481            PointerType::new(type_expr_from((*t).clone(), expanding))
482                .with_address_space(AddressSpace::Byte),
483        ),
484        Type::Unknown | Type::Never | Type::F64 => {
485            panic!("unrepresentable type value: {ty}")
486        },
487        ty => TypeExpr::Primitive(Span::unknown(ty)),
488    }
489}
490
491impl Spanned for TypeExpr {
492    fn span(&self) -> SourceSpan {
493        match self {
494            Self::Primitive(spanned) => spanned.span(),
495            Self::Ptr(spanned) => spanned.span(),
496            Self::Array(spanned) => spanned.span(),
497            Self::Struct(spanned) => spanned.span(),
498            Self::Ref(spanned) => spanned.span(),
499        }
500    }
501}
502
503impl crate::prettier::PrettyPrint for TypeExpr {
504    fn render(&self) -> crate::prettier::Document {
505        use crate::prettier::*;
506
507        match self {
508            Self::Primitive(ty) => display(ty),
509            Self::Ptr(ty) => ty.render(),
510            Self::Array(ty) => ty.render(),
511            Self::Struct(ty) => ty.render(),
512            Self::Ref(ty) => display(ty),
513        }
514    }
515}
516
517// POINTER TYPE
518// ================================================================================================
519
520#[derive(Debug, Clone)]
521pub struct PointerType {
522    pub span: SourceSpan,
523    pub pointee: Box<TypeExpr>,
524    addrspace: Option<AddressSpace>,
525}
526
527impl From<types::PointerType> for PointerType {
528    fn from(ty: types::PointerType) -> Self {
529        let types::PointerType { addrspace, pointee } = ty;
530        let pointee = Box::new(TypeExpr::from(pointee));
531        Self {
532            span: SourceSpan::UNKNOWN,
533            pointee,
534            addrspace: Some(addrspace),
535        }
536    }
537}
538
539impl Eq for PointerType {}
540
541impl PartialEq for PointerType {
542    fn eq(&self, other: &Self) -> bool {
543        self.address_space() == other.address_space() && self.pointee == other.pointee
544    }
545}
546
547impl core::hash::Hash for PointerType {
548    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
549        self.pointee.hash(state);
550        self.address_space().hash(state);
551    }
552}
553
554impl Spanned for PointerType {
555    fn span(&self) -> SourceSpan {
556        self.span
557    }
558}
559
560impl PointerType {
561    pub fn new(pointee: TypeExpr) -> Self {
562        Self {
563            span: SourceSpan::UNKNOWN,
564            pointee: Box::new(pointee),
565            addrspace: None,
566        }
567    }
568
569    /// Override the default source span
570    #[inline]
571    pub fn with_span(mut self, span: SourceSpan) -> Self {
572        self.span = span;
573        self
574    }
575
576    /// Override the default address space
577    #[inline]
578    pub fn with_address_space(mut self, addrspace: AddressSpace) -> Self {
579        self.addrspace = Some(addrspace);
580        self
581    }
582
583    /// Get the address space of this pointer type
584    #[inline]
585    pub fn address_space(&self) -> AddressSpace {
586        self.addrspace.unwrap_or(AddressSpace::Element)
587    }
588}
589
590impl crate::prettier::PrettyPrint for PointerType {
591    fn render(&self) -> crate::prettier::Document {
592        use crate::prettier::*;
593
594        let doc = const_text("ptr<") + self.pointee.render();
595        if let Some(addrspace) = self.addrspace.as_ref() {
596            doc + const_text(", ") + text(format!("addrspace({addrspace})")) + const_text(">")
597        } else {
598            doc + const_text(">")
599        }
600    }
601}
602
603// ARRAY TYPE
604// ================================================================================================
605
606#[derive(Debug, Clone)]
607pub struct ArrayType {
608    pub span: SourceSpan,
609    pub elem: Box<TypeExpr>,
610    pub arity: usize,
611}
612
613impl Eq for ArrayType {}
614
615impl PartialEq for ArrayType {
616    fn eq(&self, other: &Self) -> bool {
617        self.arity == other.arity && self.elem == other.elem
618    }
619}
620
621impl core::hash::Hash for ArrayType {
622    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
623        self.elem.hash(state);
624        self.arity.hash(state);
625    }
626}
627
628impl Spanned for ArrayType {
629    fn span(&self) -> SourceSpan {
630        self.span
631    }
632}
633
634impl ArrayType {
635    pub fn new(elem: TypeExpr, arity: usize) -> Self {
636        Self {
637            span: SourceSpan::UNKNOWN,
638            elem: Box::new(elem),
639            arity,
640        }
641    }
642
643    /// Override the default source span
644    #[inline]
645    pub fn with_span(mut self, span: SourceSpan) -> Self {
646        self.span = span;
647        self
648    }
649}
650
651impl crate::prettier::PrettyPrint for ArrayType {
652    fn render(&self) -> crate::prettier::Document {
653        use crate::prettier::*;
654
655        const_text("[")
656            + self.elem.render()
657            + const_text("; ")
658            + display(self.arity)
659            + const_text("]")
660    }
661}
662
663// STRUCT TYPE
664// ================================================================================================
665
666#[derive(Debug, Clone)]
667pub struct StructType {
668    pub span: SourceSpan,
669    pub name: Option<Ident>,
670    pub repr: Span<TypeRepr>,
671    pub fields: Vec<StructField>,
672}
673
674impl Eq for StructType {}
675
676impl PartialEq for StructType {
677    fn eq(&self, other: &Self) -> bool {
678        self.name == other.name && self.repr == other.repr && self.fields == other.fields
679    }
680}
681
682impl core::hash::Hash for StructType {
683    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
684        self.name.hash(state);
685        self.repr.hash(state);
686        self.fields.hash(state);
687    }
688}
689
690impl Spanned for StructType {
691    fn span(&self) -> SourceSpan {
692        self.span
693    }
694}
695
696impl StructType {
697    pub fn new(name: Option<Ident>, fields: impl IntoIterator<Item = StructField>) -> Self {
698        Self {
699            span: SourceSpan::UNKNOWN,
700            name,
701            repr: Span::unknown(TypeRepr::Default),
702            fields: fields.into_iter().collect(),
703        }
704    }
705
706    /// Override the default struct representation
707    #[inline]
708    pub fn with_repr(mut self, repr: Span<TypeRepr>) -> Self {
709        self.repr = repr;
710        self
711    }
712
713    /// Override the default source span
714    #[inline]
715    pub fn with_span(mut self, span: SourceSpan) -> Self {
716        self.span = span;
717        self
718    }
719}
720
721impl crate::prettier::PrettyPrint for StructType {
722    fn render(&self) -> crate::prettier::Document {
723        use crate::prettier::*;
724
725        let repr = match &*self.repr {
726            TypeRepr::Default => Document::Empty,
727            repr @ (TypeRepr::Align(_) | TypeRepr::Packed(_) | TypeRepr::Transparent) => {
728                text(format!(" @{repr}"))
729            },
730        };
731
732        let singleline_body = self
733            .fields
734            .iter()
735            .map(PrettyPrint::render)
736            .reduce(|acc, field| acc + const_text(", ") + field)
737            .unwrap_or(Document::Empty);
738        let multiline_body = indent(
739            4,
740            nl() + self
741                .fields
742                .iter()
743                .map(PrettyPrint::render)
744                .reduce(|acc, field| acc + const_text(",") + nl() + field)
745                .unwrap_or(Document::Empty),
746        ) + nl();
747        let body = singleline_body | multiline_body;
748
749        const_text("struct") + repr + const_text(" { ") + body + const_text(" }")
750    }
751}
752
753// STRUCT FIELD
754// ================================================================================================
755
756#[derive(Debug, Clone)]
757pub struct StructField {
758    pub span: SourceSpan,
759    pub name: Ident,
760    pub ty: TypeExpr,
761}
762
763impl Eq for StructField {}
764
765impl PartialEq for StructField {
766    fn eq(&self, other: &Self) -> bool {
767        self.name == other.name && self.ty == other.ty
768    }
769}
770
771impl core::hash::Hash for StructField {
772    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
773        self.name.hash(state);
774        self.ty.hash(state);
775    }
776}
777
778impl Spanned for StructField {
779    fn span(&self) -> SourceSpan {
780        self.span
781    }
782}
783
784impl crate::prettier::PrettyPrint for StructField {
785    fn render(&self) -> crate::prettier::Document {
786        use crate::prettier::*;
787
788        display(&self.name) + const_text(": ") + self.ty.render()
789    }
790}
791
792// TYPE ALIAS
793// ================================================================================================
794
795/// A [TypeAlias] represents a named [Type].
796///
797/// Type aliases correspond to type declarations in Miden Assembly source files. They are called
798/// aliases, rather than declarations, as the type system for Miden Assembly is structural, rather
799/// than nominal, and so two aliases with the same underlying type are considered equivalent.
800#[derive(Debug, Clone)]
801pub struct TypeAlias {
802    span: SourceSpan,
803    /// The documentation string attached to this definition.
804    docs: Option<DocString>,
805    /// The visibility of this type alias
806    pub visibility: Visibility,
807    /// The name of this type alias
808    pub name: Ident,
809    /// The concrete underlying type
810    pub ty: TypeExpr,
811}
812
813impl TypeAlias {
814    /// Create a new type alias from a name and type
815    pub fn new(visibility: Visibility, name: Ident, ty: TypeExpr) -> Self {
816        Self {
817            span: name.span(),
818            docs: None,
819            visibility,
820            name,
821            ty,
822        }
823    }
824
825    /// Adds documentation to this type alias
826    pub fn with_docs(mut self, docs: Option<Span<String>>) -> Self {
827        self.docs = docs.map(DocString::new);
828        self
829    }
830
831    /// Override the default source span
832    #[inline]
833    pub fn with_span(mut self, span: SourceSpan) -> Self {
834        self.span = span;
835        self
836    }
837
838    /// Set the source span
839    #[inline]
840    pub fn set_span(&mut self, span: SourceSpan) {
841        self.span = span;
842    }
843
844    /// Returns the documentation associated with this item.
845    pub fn docs(&self) -> Option<Span<&str>> {
846        self.docs.as_ref().map(|docstring| docstring.as_spanned_str())
847    }
848
849    /// Get the name of this type alias
850    pub fn name(&self) -> &Ident {
851        &self.name
852    }
853
854    /// Get the visibility of this type alias
855    #[inline]
856    pub const fn visibility(&self) -> Visibility {
857        self.visibility
858    }
859}
860
861impl Eq for TypeAlias {}
862
863impl PartialEq for TypeAlias {
864    fn eq(&self, other: &Self) -> bool {
865        self.visibility == other.visibility
866            && self.name == other.name
867            && self.docs == other.docs
868            && self.ty == other.ty
869    }
870}
871
872impl core::hash::Hash for TypeAlias {
873    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
874        let Self { span: _, docs, visibility, name, ty } = self;
875        docs.hash(state);
876        visibility.hash(state);
877        name.hash(state);
878        ty.hash(state);
879    }
880}
881
882impl Spanned for TypeAlias {
883    fn span(&self) -> SourceSpan {
884        self.span
885    }
886}
887
888impl crate::prettier::PrettyPrint for TypeAlias {
889    fn render(&self) -> crate::prettier::Document {
890        use crate::prettier::*;
891
892        let mut doc = self.docs.as_ref().map(PrettyPrint::render).unwrap_or(Document::Empty);
893
894        if self.visibility.is_public() {
895            doc += display(self.visibility) + const_text(" ");
896        }
897
898        doc + const_text("type")
899            + const_text(" ")
900            + display(&self.name)
901            + const_text(" = ")
902            + self.ty.render()
903    }
904}
905
906// ENUM TYPE
907// ================================================================================================
908
909/// A combined type alias and constant declaration corresponding to a C-like enumeration.
910///
911/// C-style enumerations are effectively a type alias for an integer type with a limited set of
912/// valid values with associated names (referred to as _variants_ of the enum type).
913///
914/// In Miden Assembly, these provide a means for a procedure to declare that it expects an argument
915/// of the underlying integral type, but that values other than those of the declared variants are
916/// illegal/invalid. Currently, these are unchecked, and are only used to convey semantic
917/// information. In the future, we may perform static analysis to try and identify invalid instances
918/// of the enumeration when derived from a constant.
919#[derive(Debug, Clone)]
920pub struct EnumType {
921    span: SourceSpan,
922    /// The documentation string attached to this definition.
923    docs: Option<DocString>,
924    /// The visibility of this enum type
925    visibility: Visibility,
926    /// The enum name
927    name: Ident,
928    /// The type of the discriminant value used for this enum's variants
929    ///
930    /// NOTE: The type must be an integral value, and this is enforced by [`Self::new`].
931    ty: Type,
932    /// The enum variants
933    variants: Vec<Variant>,
934}
935
936impl EnumType {
937    /// Construct a new enum type with the given name and variants
938    ///
939    /// The caller is assumed to have already validated that `ty` is an integral type, and this
940    /// function will assert that this is the case.
941    pub fn new(
942        visibility: Visibility,
943        name: Ident,
944        ty: Type,
945        variants: impl IntoIterator<Item = Variant>,
946    ) -> Self {
947        assert!(ty.is_integer(), "only integer types are allowed in enum type definitions");
948        Self {
949            span: name.span(),
950            docs: None,
951            visibility,
952            name,
953            ty,
954            variants: Vec::from_iter(variants),
955        }
956    }
957
958    /// Adds documentation to this enum declaration.
959    pub fn with_docs(mut self, docs: Option<Span<String>>) -> Self {
960        self.docs = docs.map(DocString::new);
961        self
962    }
963
964    /// Override the default source span
965    pub fn with_span(mut self, span: SourceSpan) -> Self {
966        self.span = span;
967        self
968    }
969
970    /// Returns true if this is a C-style enum where the discriminant is the value
971    pub fn is_c_like(&self) -> bool {
972        !self.variants.is_empty() && self.variants.iter().all(|v| v.value_ty.is_none())
973    }
974
975    /// Set the source span
976    pub fn set_span(&mut self, span: SourceSpan) {
977        self.span = span;
978    }
979
980    /// Get the name of this enum type
981    pub fn name(&self) -> &Ident {
982        &self.name
983    }
984
985    /// Get the visibility of this enum type
986    pub const fn visibility(&self) -> Visibility {
987        self.visibility
988    }
989
990    /// Returns the documentation associated with this item.
991    pub fn docs(&self) -> Option<Span<&str>> {
992        self.docs.as_ref().map(|docstring| docstring.as_spanned_str())
993    }
994
995    /// Get the concrete type of this enum's variants
996    pub fn ty(&self) -> &Type {
997        &self.ty
998    }
999
1000    /// Get the variants of this enum type
1001    pub fn variants(&self) -> &[Variant] {
1002        &self.variants
1003    }
1004
1005    /// Get the variants of this enum type, mutably
1006    pub fn variants_mut(&mut self) -> &mut Vec<Variant> {
1007        &mut self.variants
1008    }
1009
1010    /// Split this definition into its type alias and variant parts
1011    pub fn into_parts(self) -> (TypeAlias, Vec<Variant>) {
1012        let Self {
1013            span,
1014            docs,
1015            visibility,
1016            name,
1017            ty,
1018            variants,
1019        } = self;
1020        let alias = TypeAlias {
1021            span,
1022            docs,
1023            visibility,
1024            name,
1025            ty: TypeExpr::Primitive(Span::new(span, ty)),
1026        };
1027        (alias, variants)
1028    }
1029}
1030
1031impl Spanned for EnumType {
1032    fn span(&self) -> SourceSpan {
1033        self.span
1034    }
1035}
1036
1037impl Eq for EnumType {}
1038
1039impl PartialEq for EnumType {
1040    fn eq(&self, other: &Self) -> bool {
1041        self.visibility == other.visibility
1042            && self.name == other.name
1043            && self.docs == other.docs
1044            && self.ty == other.ty
1045            && self.variants == other.variants
1046    }
1047}
1048
1049impl core::hash::Hash for EnumType {
1050    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1051        let Self {
1052            span: _,
1053            docs,
1054            visibility,
1055            name,
1056            ty,
1057            variants,
1058        } = self;
1059        docs.hash(state);
1060        visibility.hash(state);
1061        name.hash(state);
1062        ty.hash(state);
1063        variants.hash(state);
1064    }
1065}
1066
1067impl crate::prettier::PrettyPrint for EnumType {
1068    fn render(&self) -> crate::prettier::Document {
1069        use crate::prettier::*;
1070
1071        let mut doc = self.docs.as_ref().map(PrettyPrint::render).unwrap_or(Document::Empty);
1072
1073        let variants = self
1074            .variants
1075            .iter()
1076            .map(PrettyPrint::render)
1077            .reduce(|acc, v| acc + const_text(",") + nl() + v)
1078            .unwrap_or(Document::Empty);
1079
1080        if self.visibility.is_public() {
1081            doc += display(self.visibility) + const_text(" ");
1082        }
1083
1084        doc + const_text("enum")
1085            + const_text(" ")
1086            + display(&self.name)
1087            + const_text(" : ")
1088            + self.ty.render()
1089            + const_text(" {")
1090            + nl()
1091            + variants
1092            + const_text("}")
1093    }
1094}
1095
1096// ENUM VARIANT
1097// ================================================================================================
1098
1099/// A variant of an [EnumType].
1100///
1101/// See the [EnumType] docs for more information.
1102#[derive(Debug, Clone)]
1103pub struct Variant {
1104    pub span: SourceSpan,
1105    /// The documentation string attached to the constant derived from this variant.
1106    pub docs: Option<DocString>,
1107    /// The name of this enum variant
1108    pub name: Ident,
1109    /// The payload value type of this variant
1110    ///
1111    /// NOTE: This is not supported in Miden Assembly text format yet, but can be set when lowering
1112    /// directly to the AST.
1113    pub value_ty: Option<TypeExpr>,
1114    /// The discriminant value associated with this variant
1115    pub discriminant: ConstantExpr,
1116}
1117
1118impl Variant {
1119    /// Construct a new variant of an [EnumType], with the given name and discriminant value.
1120    pub fn new(name: Ident, discriminant: ConstantExpr, payload: Option<TypeExpr>) -> Self {
1121        Self {
1122            span: name.span(),
1123            docs: None,
1124            name,
1125            value_ty: payload,
1126            discriminant,
1127        }
1128    }
1129
1130    /// Override the span for this variant
1131    pub fn with_span(mut self, span: SourceSpan) -> Self {
1132        self.span = span;
1133        self
1134    }
1135
1136    /// Adds documentation to this variant
1137    pub fn with_docs(mut self, docs: Option<Span<String>>) -> Self {
1138        self.docs = docs.map(DocString::new);
1139        self
1140    }
1141
1142    /// Used to validate that this variant's discriminant value is an instance of `ty`,
1143    /// which must be a type valid for use as the underlying representation for an enum, i.e. an
1144    /// integer type up to 64 bits in size.
1145    ///
1146    /// It is expected that the discriminant expression has been folded to an integer value by the
1147    /// time this is called. If the discriminant has not been fully folded, then an error will be
1148    /// returned.
1149    pub fn assert_instance_of(&self, ty: &Type) -> Result<(), crate::SemanticAnalysisError> {
1150        use crate::{FIELD_MODULUS, SemanticAnalysisError};
1151
1152        let value = match &self.discriminant {
1153            ConstantExpr::Int(value) => value.as_int(),
1154            _ => {
1155                return Err(SemanticAnalysisError::InvalidEnumDiscriminant {
1156                    span: self.discriminant.span(),
1157                    repr: ty.clone(),
1158                });
1159            },
1160        };
1161
1162        match ty {
1163            Type::Felt if value >= FIELD_MODULUS => {
1164                Err(SemanticAnalysisError::InvalidEnumDiscriminant {
1165                    span: self.discriminant.span(),
1166                    repr: ty.clone(),
1167                })
1168            },
1169            // IntValue is represented as an unsigned integer, so negative discriminants
1170            // are rejected during constant evaluation.
1171            Type::Felt => Ok(()),
1172            Type::I1 if value > 1 => Err(SemanticAnalysisError::InvalidEnumDiscriminant {
1173                span: self.discriminant.span(),
1174                repr: ty.clone(),
1175            }),
1176            Type::I1 => Ok(()),
1177            Type::I8 | Type::U8 if value > u8::MAX as u64 => {
1178                Err(SemanticAnalysisError::InvalidEnumDiscriminant {
1179                    span: self.discriminant.span(),
1180                    repr: ty.clone(),
1181                })
1182            },
1183            Type::I8 | Type::U8 => Ok(()),
1184            Type::I16 | Type::U16 if value > u16::MAX as u64 => {
1185                Err(SemanticAnalysisError::InvalidEnumDiscriminant {
1186                    span: self.discriminant.span(),
1187                    repr: ty.clone(),
1188                })
1189            },
1190            Type::I16 | Type::U16 => Ok(()),
1191            Type::I32 | Type::U32 if value > u32::MAX as u64 => {
1192                Err(SemanticAnalysisError::InvalidEnumDiscriminant {
1193                    span: self.discriminant.span(),
1194                    repr: ty.clone(),
1195                })
1196            },
1197            Type::I32 | Type::U32 => Ok(()),
1198            Type::I64 | Type::U64 if value >= FIELD_MODULUS => {
1199                Err(SemanticAnalysisError::InvalidEnumDiscriminant {
1200                    span: self.discriminant.span(),
1201                    repr: ty.clone(),
1202                })
1203            },
1204            _ => Err(SemanticAnalysisError::InvalidEnumRepr { span: self.span }),
1205        }
1206    }
1207}
1208
1209impl Spanned for Variant {
1210    fn span(&self) -> SourceSpan {
1211        self.span
1212    }
1213}
1214
1215impl Eq for Variant {}
1216
1217impl PartialEq for Variant {
1218    fn eq(&self, other: &Self) -> bool {
1219        self.name == other.name
1220            && self.value_ty == other.value_ty
1221            && self.discriminant == other.discriminant
1222            && self.docs == other.docs
1223    }
1224}
1225
1226impl core::hash::Hash for Variant {
1227    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1228        let Self {
1229            span: _,
1230            docs,
1231            name,
1232            value_ty,
1233            discriminant,
1234        } = self;
1235        docs.hash(state);
1236        name.hash(state);
1237        value_ty.hash(state);
1238        discriminant.hash(state);
1239    }
1240}
1241
1242impl crate::prettier::PrettyPrint for Variant {
1243    fn render(&self) -> crate::prettier::Document {
1244        use crate::prettier::*;
1245
1246        let doc = self.docs.as_ref().map(PrettyPrint::render).unwrap_or(Document::Empty);
1247
1248        let name = display(&self.name);
1249        let name_and_payload = if let Some(value_ty) = self.value_ty.as_ref() {
1250            name + const_text("(") + value_ty.render() + const_text(")")
1251        } else {
1252            name
1253        };
1254        doc + name_and_payload + const_text(" = ") + self.discriminant.render()
1255    }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260    use alloc::{string::ToString, sync::Arc};
1261    use core::str::FromStr;
1262
1263    use miden_debug_types::{DefaultSourceManager, SourceFile, SourceId, SourceLanguage, Uri};
1264
1265    use super::*;
1266    use crate::{ast::Form, prettier::PrettyPrint};
1267
1268    struct DummyResolver {
1269        source_manager: Arc<dyn SourceManager>,
1270    }
1271
1272    impl DummyResolver {
1273        fn new() -> Self {
1274            Self {
1275                source_manager: Arc::new(DefaultSourceManager::default()),
1276            }
1277        }
1278    }
1279
1280    impl TypeResolver<SymbolResolutionError> for DummyResolver {
1281        fn source_manager(&self) -> Arc<dyn SourceManager> {
1282            self.source_manager.clone()
1283        }
1284
1285        fn resolve_local_failed(&self, err: SymbolResolutionError) -> SymbolResolutionError {
1286            err
1287        }
1288
1289        fn get_type(
1290            &mut self,
1291            context: SourceSpan,
1292            _gid: GlobalItemIndex,
1293        ) -> Result<Option<TypeTemplate>, SymbolResolutionError> {
1294            Err(SymbolResolutionError::undefined(context, self.source_manager.as_ref()))
1295        }
1296
1297        fn get_local_type(
1298            &mut self,
1299            _context: SourceSpan,
1300            _id: ItemIndex,
1301        ) -> Result<Option<TypeTemplate>, SymbolResolutionError> {
1302            Ok(None)
1303        }
1304
1305        fn resolve_type_ref(
1306            &mut self,
1307            ty: Span<&Path>,
1308        ) -> Result<SymbolResolution, SymbolResolutionError> {
1309            Err(SymbolResolutionError::undefined(ty.span(), self.source_manager.as_ref()))
1310        }
1311
1312        fn finalize(
1313            &mut self,
1314            context: SourceSpan,
1315            template: TypeTemplate,
1316        ) -> Result<Type, SymbolResolutionError> {
1317            // This resolver never produces back-references, so closing can never fail on one.
1318            midenc_hir_type::close_template(&template, |_| None).map_err(|_| {
1319                SymbolResolutionError::undefined(context, self.source_manager.as_ref())
1320            })
1321        }
1322    }
1323
1324    fn nested_type_expr(depth: usize) -> TypeExpr {
1325        let mut expr = TypeExpr::Primitive(Span::unknown(Type::Felt));
1326        for i in 0..depth {
1327            expr = match i % 3 {
1328                0 => TypeExpr::Ptr(PointerType::new(expr)),
1329                1 => TypeExpr::Array(ArrayType::new(expr, 1)),
1330                _ => {
1331                    let field = StructField {
1332                        span: SourceSpan::UNKNOWN,
1333                        name: Ident::from_str("field").expect("valid ident"),
1334                        ty: expr,
1335                    };
1336                    TypeExpr::Struct(StructType::new(None, [field]))
1337                },
1338            };
1339        }
1340        expr
1341    }
1342
1343    fn test_source_file(source: &str) -> Arc<SourceFile> {
1344        Arc::new(SourceFile::new(
1345            SourceId::default(),
1346            SourceLanguage::Masm,
1347            Uri::new("memory:///type-expr-test.masm"),
1348            source.to_string().into_boxed_str(),
1349        ))
1350    }
1351
1352    fn parse_type_alias_expr(source: &str) -> TypeExpr {
1353        let mut forms =
1354            crate::parser::parse_forms(test_source_file(source)).expect("type alias should parse");
1355        assert_eq!(forms.len(), 1, "expected exactly one parsed form");
1356        match forms.pop().expect("expected parsed form") {
1357            Form::Type(alias) => alias.ty,
1358            form => panic!("expected type alias form, got {form:?}"),
1359        }
1360    }
1361
1362    fn repr_round_trip_struct(repr: TypeRepr) -> TypeExpr {
1363        TypeExpr::Struct(
1364            StructType::new(
1365                None,
1366                [
1367                    StructField {
1368                        span: SourceSpan::UNKNOWN,
1369                        name: Ident::from_str("prefix").expect("valid ident"),
1370                        ty: TypeExpr::Primitive(Span::unknown(Type::Felt)),
1371                    },
1372                    StructField {
1373                        span: SourceSpan::UNKNOWN,
1374                        name: Ident::from_str("suffix").expect("valid ident"),
1375                        ty: TypeExpr::Primitive(Span::unknown(Type::U32)),
1376                    },
1377                ],
1378            )
1379            .with_repr(Span::unknown(repr)),
1380        )
1381    }
1382
1383    #[test]
1384    fn type_expr_depth_boundary() {
1385        let mut resolver = DummyResolver::new();
1386
1387        let ok_expr = nested_type_expr(MAX_TYPE_EXPR_NESTING);
1388        assert!(ok_expr.resolve_template(&mut resolver).is_ok());
1389
1390        let err_expr = nested_type_expr(MAX_TYPE_EXPR_NESTING + 1);
1391        let err = err_expr
1392            .resolve_template(&mut resolver)
1393            .expect_err("expected depth-exceeded error");
1394        assert!(
1395            matches!(err, SymbolResolutionError::TypeExpressionDepthExceeded { max_depth, .. }
1396                if max_depth == MAX_TYPE_EXPR_NESTING)
1397        );
1398    }
1399
1400    #[test]
1401    fn struct_type_expr_render_round_trips_non_default_reprs() {
1402        for repr in [
1403            TypeRepr::align(16),
1404            TypeRepr::packed(1),
1405            TypeRepr::packed(2),
1406            TypeRepr::Transparent,
1407        ] {
1408            let rendered = repr_round_trip_struct(repr).to_pretty_string();
1409            assert!(
1410                rendered.starts_with("struct @"),
1411                "non-default struct repr should render after `struct`: {rendered}"
1412            );
1413
1414            let parsed = parse_type_alias_expr(&format!("type RoundTrip = {rendered}\n"));
1415            let TypeExpr::Struct(parsed) = parsed else {
1416                panic!("expected rendered type to parse back as a struct");
1417            };
1418            assert_eq!(*parsed.repr, repr);
1419            assert_eq!(parsed.fields[0].name.as_str(), "prefix");
1420            assert_eq!(parsed.fields[1].name.as_str(), "suffix");
1421        }
1422    }
1423
1424    #[test]
1425    fn type_expr_from_type_preserves_wide_integer_primitives() {
1426        for ty in [Type::I64, Type::U64, Type::I128, Type::U128] {
1427            let expr = TypeExpr::from(ty.clone());
1428            let TypeExpr::Primitive(actual) = expr else {
1429                panic!("expected primitive type expression for {ty}, got {expr:?}");
1430            };
1431            assert_eq!(actual.into_inner(), ty);
1432        }
1433    }
1434
1435    #[test]
1436    fn type_expr_from_type_preserves_struct_metadata() {
1437        let ty = Type::from(Arc::new(types::StructType::from_parts(
1438            Some(Arc::from("miden:base/core-types@1.0.0/account-id")),
1439            TypeRepr::align(16),
1440            [
1441                (Arc::<str>::from("prefix"), Type::Felt),
1442                (Arc::<str>::from("suffix"), Type::Felt),
1443            ],
1444        )));
1445
1446        let TypeExpr::Struct(actual) = TypeExpr::from(ty) else {
1447            panic!("expected struct type expression");
1448        };
1449        assert_eq!(
1450            actual.name.as_ref().map(Ident::as_str),
1451            Some("miden:base/core-types@1.0.0/account-id"),
1452        );
1453        assert_eq!(*actual.repr, TypeRepr::align(16));
1454        assert_eq!(actual.fields[0].name.as_str(), "prefix");
1455        assert_eq!(actual.fields[1].name.as_str(), "suffix");
1456    }
1457
1458    #[test]
1459    fn type_expr_conversion_of_a_recursive_struct_terminates() {
1460        use midenc_hir_type::{RecursiveTypeBuilder, StructTemplate, TypeRepr, TypeTemplate};
1461
1462        let mut builder = RecursiveTypeBuilder::new();
1463        builder.define_struct(
1464            "Node",
1465            StructTemplate::named(
1466                "Node",
1467                TypeRepr::Default,
1468                [("next", TypeTemplate::ptr(TypeTemplate::rec("Node")))],
1469            ),
1470        );
1471        let node = builder.build().unwrap().remove("Node").unwrap();
1472
1473        // The backedge must come back as a reference by name, not as another copy of the body,
1474        // or the conversion never terminates.
1475        let TypeExpr::Struct(converted) = TypeExpr::from(node) else {
1476            panic!("expected a struct type expression");
1477        };
1478        let TypeExpr::Ptr(pointer) = &converted.fields[0].ty else {
1479            panic!("expected a pointer");
1480        };
1481        let TypeExpr::Ref(target) = pointer.pointee.as_ref() else {
1482            panic!("expected the pointee to be a reference, got {:?}", pointer.pointee);
1483        };
1484        assert_eq!(target.inner().to_string(), "Node");
1485    }
1486
1487    #[test]
1488    fn parsed_struct_type_preserves_field_names_through_resolution() {
1489        let expr = parse_type_alias_expr(
1490            "type AccountId = struct @align(16) { prefix: felt, suffix: felt }\n",
1491        );
1492
1493        let mut resolver = DummyResolver::new();
1494        let resolved = TypeResolver::resolve(&mut resolver, &expr)
1495            .expect("struct type should resolve")
1496            .expect("struct type should be concrete");
1497        let Type::Struct(resolved_struct) = &resolved else {
1498            panic!("expected resolved struct type, got {resolved:?}");
1499        };
1500        assert_eq!(resolved_struct.repr(), TypeRepr::align(16));
1501        let resolved_fields = resolved_struct.get();
1502        assert_eq!(resolved_fields.fields()[0].name.as_deref(), Some("prefix"));
1503        assert_eq!(resolved_fields.fields()[1].name.as_deref(), Some("suffix"));
1504
1505        let TypeExpr::Struct(converted) = TypeExpr::from(resolved) else {
1506            panic!("expected concrete struct to convert back to struct type expression");
1507        };
1508        assert_eq!(*converted.repr, TypeRepr::align(16));
1509        assert_eq!(converted.fields[0].name.as_str(), "prefix");
1510        assert_eq!(converted.fields[1].name.as_str(), "suffix");
1511    }
1512}