1use std::{
4 collections::{HashMap, HashSet},
5 sync::Arc,
6};
7
8use miden_mast_package::Package;
9use miden_protocol::MAX_NOTE_STORAGE_ITEMS;
10use midenc_frontend_wasm_metadata::{
11 PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, package_note_storage_schema_section_id,
12 trim_trailing_nuls,
13};
14use wit_parser::{Resolve, Type, TypeDefKind, TypeId, TypeOwner};
15
16use crate::{
17 CodecRegistry, DecodedValue, Error, NoteStorage, NoteStorageBuilder, Result, StandardLeaf,
18 codec::FELT_FQN,
19};
20
21pub const MAX_NOTE_STORAGE_SCHEMA_BYTES: usize = MAX_NOTE_STORAGE_ITEMS * 64;
26
27pub const MAX_NOTE_STORAGE_SCHEMA_TYPES: usize = MAX_NOTE_STORAGE_ITEMS;
29
30pub const MAX_NOTE_STORAGE_SCHEMA_DEPTH: usize = MAX_NOTE_STORAGE_ITEMS / 8;
36
37pub const MAX_NOTE_STORAGE_SCHEMA_FELTS: usize = MAX_NOTE_STORAGE_ITEMS;
39
40pub const MAX_NOTE_STORAGE_SCHEMA_NODES: usize = MAX_NOTE_STORAGE_ITEMS * 4;
49
50pub const MAX_NOTE_CODEC_COMPONENT_BYTES: usize = 4 * 1024 * 1024;
56
57pub const NOTE_CODEC_GUEST_RUSTFLAGS: &str = "-C target-feature=-simd128";
64
65const _: () = assert!(MAX_NOTE_STORAGE_SCHEMA_DEPTH > 0);
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub struct FeltLayout {
70 minimum: usize,
71 maximum: usize,
72}
73
74impl FeltLayout {
75 pub const fn minimum(self) -> usize {
77 self.minimum
78 }
79
80 pub const fn maximum(self) -> usize {
82 self.maximum
83 }
84
85 pub const fn fixed_width(self) -> Option<usize> {
87 if self.minimum == self.maximum {
88 Some(self.minimum)
89 } else {
90 None
91 }
92 }
93
94 const fn fixed(width: usize) -> Self {
96 Self {
97 minimum: width,
98 maximum: width,
99 }
100 }
101
102 fn concatenate(self, other: Self) -> Result<Self> {
104 let minimum = self
105 .minimum
106 .checked_add(other.minimum)
107 .ok_or_else(|| Error::new("note storage layout minimum width is too large"))?;
108 let maximum = self
109 .maximum
110 .checked_add(other.maximum)
111 .ok_or_else(|| Error::new("note storage layout maximum width is too large"))?;
112 Self::bounded(minimum, maximum)
113 }
114
115 fn bounded(minimum: usize, maximum: usize) -> Result<Self> {
120 if maximum > MAX_NOTE_STORAGE_SCHEMA_FELTS {
121 return Err(Error::new(format!(
122 "note storage schema layout has maximum width {maximum} felts; the protocol limit \
123 is {MAX_NOTE_STORAGE_SCHEMA_FELTS}"
124 )));
125 }
126 Ok(Self { minimum, maximum })
127 }
128}
129
130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub enum PrimitiveType {
133 U64,
135 U32,
137 U8,
139 Bool,
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
145pub enum SchemaTypeKind {
146 Felt,
148 Primitive(PrimitiveType),
150 Record(Vec<SchemaField>),
152 Option(Arc<SchemaType>),
154 Variant(Vec<SchemaCase>),
156}
157
158#[derive(Clone, Debug, Eq, PartialEq)]
160pub struct SchemaType {
161 name: Option<String>,
162 fqn: Option<String>,
163 docs: Option<String>,
164 kind: SchemaTypeKind,
165 layout: FeltLayout,
166}
167
168impl SchemaType {
169 pub fn name(&self) -> Option<&str> {
171 self.name.as_deref()
172 }
173
174 pub fn fqn(&self) -> Option<&str> {
176 self.fqn.as_deref()
177 }
178
179 pub fn docs(&self) -> Option<&str> {
181 self.docs.as_deref()
182 }
183
184 pub const fn kind(&self) -> &SchemaTypeKind {
186 &self.kind
187 }
188
189 pub const fn layout(&self) -> FeltLayout {
191 self.layout
192 }
193
194 pub fn standard_leaf(&self) -> Option<StandardLeaf> {
196 self.fqn.as_deref().and_then(StandardLeaf::from_fqn)
197 }
198}
199
200#[derive(Clone, Debug, Eq, PartialEq)]
202pub struct SchemaField {
203 name: String,
204 docs: Option<String>,
205 ty: Arc<SchemaType>,
206}
207
208impl SchemaField {
209 pub fn name(&self) -> &str {
211 &self.name
212 }
213
214 pub fn docs(&self) -> Option<&str> {
216 self.docs.as_deref()
217 }
218
219 pub fn ty(&self) -> &SchemaType {
221 self.ty.as_ref()
222 }
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
227pub struct SchemaCase {
228 name: String,
229 docs: Option<String>,
230 payload: Option<Arc<SchemaType>>,
231}
232
233impl SchemaCase {
234 pub fn name(&self) -> &str {
236 &self.name
237 }
238
239 pub fn docs(&self) -> Option<&str> {
241 self.docs.as_deref()
242 }
243
244 pub fn payload(&self) -> Option<&SchemaType> {
246 self.payload.as_deref()
247 }
248}
249
250#[derive(Clone)]
252pub struct NoteStorageSchema {
253 wit_text: String,
254 root: Arc<SchemaType>,
255 codecs: CodecRegistry,
256}
257
258impl NoteStorageSchema {
259 pub fn from_package(package: &Package) -> Result<Self> {
261 let bytes = crate::section::unique_package_section(
262 package,
263 package_note_storage_schema_section_id(),
264 PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID,
265 )?;
266 ensure_schema_byte_limit(bytes.len())?;
267 let text = core::str::from_utf8(trim_trailing_nuls(bytes)).map_err(|err| {
268 Error::new(format!("note storage schema section is not valid UTF-8: {err}"))
269 })?;
270 Self::from_wit_text(text)
271 }
272
273 pub fn from_wit_text(wit_text: &str) -> Result<Self> {
275 ensure_schema_byte_limit(wit_text.len())?;
276 let wit_text = wit_text.trim_end_matches('\0');
277 let mut resolve = Resolve::default();
278 let package_id = resolve.push_str("note-storage-schema.wit", wit_text).map_err(|err| {
279 Error::new(format!("failed to resolve note storage schema WIT: {err:#}"))
280 })?;
281 if resolve.types.len() > MAX_NOTE_STORAGE_SCHEMA_TYPES {
282 return Err(Error::new(format!(
283 "note storage schema defines {} WIT types; the limit is \
284 {MAX_NOTE_STORAGE_SCHEMA_TYPES}",
285 resolve.types.len()
286 )));
287 }
288 let package = &resolve.packages[package_id];
289 let interface_id = package.interfaces.get("note-storage").copied().ok_or_else(|| {
290 Error::new(format!(
291 "schema package `{}` does not define the `note-storage` interface",
292 package.name
293 ))
294 })?;
295 let interface = &resolve.interfaces[interface_id];
296 let storage_id = interface.types.get("storage").copied().ok_or_else(|| {
297 Error::new("the `note-storage` interface does not define the `storage` type alias")
298 })?;
299 validate_resolved_core_types(&resolve)?;
300 let root = ModelBuilder::new(&resolve).build(Type::Id(storage_id))?;
301 if !matches!(root.kind(), SchemaTypeKind::Record(_)) {
302 return Err(Error::new(format!(
303 "the `note-storage.storage` alias must resolve to a record, found {}",
304 kind_name(root.kind())
305 )));
306 }
307 let schema = Self {
308 wit_text: wit_text.to_owned(),
309 root,
310 codecs: CodecRegistry::default(),
311 };
312 schema.validate_native_leaf_shapes()?;
313 Ok(schema)
314 }
315
316 pub fn wit_text(&self) -> &str {
318 &self.wit_text
319 }
320
321 pub fn root(&self) -> &SchemaType {
323 self.root.as_ref()
324 }
325
326 pub fn validate_native_leaf_shapes(&self) -> Result<()> {
328 validate_model_type_shapes(&self.root, &mut HashSet::new())
329 }
330
331 #[cfg(feature = "codec-component")]
333 pub(crate) fn custom_type_fqns(&self) -> HashSet<String> {
334 let mut fqns = HashSet::new();
335 collect_custom_type_fqns(&self.root, &mut HashSet::new(), &mut fqns);
336 fqns
337 }
338
339 pub fn layout(&self) -> FeltLayout {
341 self.root.layout
342 }
343
344 pub const fn codecs(&self) -> &CodecRegistry {
346 &self.codecs
347 }
348
349 pub fn with_codec_registry(mut self, codecs: CodecRegistry) -> Self {
351 self.codecs = codecs;
352 self
353 }
354
355 pub fn builder(&self) -> NoteStorageBuilder<'_> {
357 self.builder_with_registry(&self.codecs)
358 }
359
360 pub fn builder_with_registry<'a>(
362 &'a self,
363 registry: &'a CodecRegistry,
364 ) -> NoteStorageBuilder<'a> {
365 NoteStorageBuilder::new(self, registry)
366 }
367
368 pub fn decode(&self, storage: &NoteStorage) -> Result<DecodedValue> {
370 self.decode_with_registry(storage, &self.codecs)
371 }
372
373 pub fn decode_with_registry(
375 &self,
376 storage: &NoteStorage,
377 registry: &CodecRegistry,
378 ) -> Result<DecodedValue> {
379 crate::value::decode(&self.root, storage, registry)
380 }
381}
382
383fn ensure_schema_byte_limit(byte_len: usize) -> Result<()> {
385 if byte_len > MAX_NOTE_STORAGE_SCHEMA_BYTES {
386 return Err(Error::new(format!(
387 "note storage schema section is {byte_len} bytes; the limit is \
388 {MAX_NOTE_STORAGE_SCHEMA_BYTES}"
389 )));
390 }
391 Ok(())
392}
393
394fn ensure_expanded_node_limit(ty: &SchemaType, expanded_nodes: usize) -> Result<()> {
400 let limit = MAX_NOTE_STORAGE_SCHEMA_NODES;
401 if expanded_nodes > limit {
402 let name = ty.fqn().or_else(|| ty.name()).unwrap_or("<anonymous>");
403 return Err(Error::new(format!(
404 "note storage type `{name}` expands to {expanded_nodes} nodes; the limit is {limit}"
405 )));
406 }
407 Ok(())
408}
409
410fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> {
412 let Some((_, package_id)) = resolve.package_names.iter().find(|(name, _)| {
413 name.namespace == "miden"
414 && name.name == "base"
415 && name.version.as_ref().is_some_and(|version| version.to_string() == "1.0.0")
416 }) else {
417 return Ok(());
418 };
419 let package = &resolve.packages[*package_id];
420 let Some(interface_id) = package.interfaces.get("core-types").copied() else {
421 return Ok(());
422 };
423 let interface = &resolve.interfaces[interface_id];
424
425 for leaf in StandardLeaf::ALL {
426 let name = standard_leaf_name(leaf);
427 let fields = standard_leaf_fields(leaf);
428 let Some(type_id) = interface.types.get(name).copied() else {
429 continue;
430 };
431 let type_id = follow_resolved_aliases(resolve, type_id)?;
432 let TypeDefKind::Record(record) = &resolve.types[type_id].kind else {
433 return Err(core_shape_error(name, fields));
434 };
435 if record.fields.len() != fields.len()
436 || record
437 .fields
438 .iter()
439 .zip(fields)
440 .any(|(field, expected)| field.name != *expected)
441 {
442 return Err(core_shape_error(name, fields));
443 }
444 if leaf == StandardLeaf::Felt {
445 if !resolves_to_primitive(resolve, record.fields[0].ty, Type::F32)? {
447 return Err(core_shape_error(name, fields));
448 }
449 } else {
450 for field in &record.fields {
451 if !resolves_to_fqn(resolve, field.ty, crate::FELT_FQN)? {
452 return Err(core_shape_error(name, fields));
453 }
454 }
455 }
456 }
457 Ok(())
458}
459
460fn follow_resolved_aliases(resolve: &Resolve, mut id: TypeId) -> Result<TypeId> {
462 let mut visited = HashSet::new();
463 loop {
464 if !visited.insert(id) {
465 return Err(Error::new("cyclic WIT type aliases are not supported"));
466 }
467 match resolve.types[id].kind {
468 TypeDefKind::Type(Type::Id(next)) => id = next,
469 _ => return Ok(id),
470 }
471 }
472}
473
474fn resolves_to_primitive(resolve: &Resolve, mut ty: Type, expected: Type) -> Result<bool> {
476 let mut visited = HashSet::new();
477 loop {
478 match ty {
479 Type::Id(id) => {
480 if !visited.insert(id) {
481 return Err(Error::new("cyclic WIT type aliases are not supported"));
482 }
483 let TypeDefKind::Type(next) = resolve.types[id].kind else {
484 return Ok(false);
485 };
486 ty = next;
487 }
488 primitive => return Ok(primitive == expected),
489 }
490 }
491}
492
493fn resolves_to_fqn(resolve: &Resolve, ty: Type, expected: &str) -> Result<bool> {
495 let Type::Id(id) = ty else {
496 return Ok(false);
497 };
498 let id = follow_resolved_aliases(resolve, id)?;
499 Ok(ModelBuilder::new(resolve).type_fqn(id)?.as_deref() == Some(expected))
500}
501
502fn validate_model_type_shapes(ty: &SchemaType, seen: &mut HashSet<String>) -> Result<()> {
504 if let Some(fqn) = ty.fqn()
505 && !seen.insert(fqn.to_owned())
506 {
507 return Ok(());
508 }
509
510 match ty.standard_leaf() {
511 Some(StandardLeaf::Felt) if !matches!(ty.kind(), SchemaTypeKind::Felt) => {
512 return Err(core_shape_error(
513 standard_leaf_name(StandardLeaf::Felt),
514 standard_leaf_fields(StandardLeaf::Felt),
515 ));
516 }
517 Some(StandardLeaf::Felt) => {}
518 Some(leaf) => {
519 validate_model_record(
520 ty,
521 standard_leaf_name(leaf),
522 standard_leaf_fields(leaf),
523 crate::FELT_FQN,
524 )?;
525 }
526 None => {}
527 }
528
529 match ty.kind() {
530 SchemaTypeKind::Record(fields) => {
531 for field in fields {
532 validate_model_type_shapes(field.ty(), seen)?;
533 }
534 }
535 SchemaTypeKind::Option(payload) => validate_model_type_shapes(payload, seen)?,
536 SchemaTypeKind::Variant(cases) => {
537 for payload in cases.iter().filter_map(SchemaCase::payload) {
538 validate_model_type_shapes(payload, seen)?;
539 }
540 }
541 SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {}
542 }
543 Ok(())
544}
545
546fn standard_leaf_name(leaf: StandardLeaf) -> &'static str {
548 leaf.fqn()
549 .rsplit_once('.')
550 .expect("standard-leaf FQNs always contain an interface separator")
551 .1
552}
553
554fn standard_leaf_fields(leaf: StandardLeaf) -> &'static [&'static str] {
556 match leaf {
557 StandardLeaf::Felt | StandardLeaf::AssetAmount => &["inner"],
558 StandardLeaf::Word => &["a", "b", "c", "d"],
559 StandardLeaf::AccountId => &["prefix", "suffix"],
560 }
561}
562
563fn validate_model_record(
565 ty: &SchemaType,
566 name: &str,
567 expected_fields: &[&str],
568 expected_field_fqn: &str,
569) -> Result<()> {
570 let SchemaTypeKind::Record(fields) = ty.kind() else {
571 return Err(core_shape_error(name, expected_fields));
572 };
573 if fields.len() != expected_fields.len()
574 || fields.iter().zip(expected_fields).any(|(field, expected)| {
575 field.name() != *expected || field.ty().fqn() != Some(expected_field_fqn)
576 })
577 {
578 return Err(core_shape_error(name, expected_fields));
579 }
580 Ok(())
581}
582
583fn core_shape_error(name: &str, fields: &[&str]) -> Error {
585 let field_shape = if name == "felt" {
586 "inner: f32".to_owned()
587 } else {
588 fields
589 .iter()
590 .map(|field| format!("{field}: felt"))
591 .collect::<Vec<_>>()
592 .join(", ")
593 };
594 Error::new(format!(
595 "embedded WIT type `miden:base/core-types@1.0.0.{name}` does not match the pinned \
596 canonical shape `record {name} {{ {field_shape} }}`"
597 ))
598}
599
600#[cfg(feature = "codec-component")]
602fn collect_custom_type_fqns(
603 ty: &SchemaType,
604 seen: &mut HashSet<*const SchemaType>,
605 fqns: &mut HashSet<String>,
606) {
607 if !seen.insert(core::ptr::from_ref(ty)) {
609 return;
610 }
611 if let Some(fqn) = ty.fqn()
612 && ty.standard_leaf().is_none()
613 {
614 fqns.insert(fqn.to_owned());
615 }
616 match ty.kind() {
617 SchemaTypeKind::Record(fields) => {
618 for field in fields {
619 collect_custom_type_fqns(field.ty(), seen, fqns);
620 }
621 }
622 SchemaTypeKind::Option(payload) => collect_custom_type_fqns(payload, seen, fqns),
623 SchemaTypeKind::Variant(cases) => {
624 for payload in cases.iter().filter_map(SchemaCase::payload) {
625 collect_custom_type_fqns(payload, seen, fqns);
626 }
627 }
628 SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {}
629 }
630}
631
632#[derive(Clone)]
634struct MemoizedSchemaType {
635 ty: Arc<SchemaType>,
637 maximum_subtree_depth: usize,
642 expanded_nodes: usize,
644}
645
646struct ModelBuilder<'a> {
648 resolve: &'a Resolve,
650 active: HashSet<TypeId>,
652 memo: HashMap<TypeId, MemoizedSchemaType>,
654}
655
656impl<'a> ModelBuilder<'a> {
657 fn new(resolve: &'a Resolve) -> Self {
659 Self {
660 resolve,
661 active: HashSet::new(),
662 memo: HashMap::new(),
663 }
664 }
665
666 fn build(mut self, ty: Type) -> Result<Arc<SchemaType>> {
668 self.build_type(ty, 0).map(|memoized| memoized.ty)
669 }
670
671 fn build_type(&mut self, ty: Type, depth: usize) -> Result<MemoizedSchemaType> {
673 if depth > MAX_NOTE_STORAGE_SCHEMA_DEPTH {
674 return Err(Error::new(format!(
675 "note storage schema nesting depth {depth} exceeds the limit of \
676 {MAX_NOTE_STORAGE_SCHEMA_DEPTH}"
677 )));
678 }
679 match ty {
680 Type::Id(id) => self.build_type_id(id, depth),
681 Type::U64 => self.primitive(PrimitiveType::U64, None, None, None),
682 Type::U32 => self.primitive(PrimitiveType::U32, None, None, None),
683 Type::U8 => self.primitive(PrimitiveType::U8, None, None, None),
684 Type::Bool => self.primitive(PrimitiveType::Bool, None, None, None),
685 unsupported => Err(Error::new(format!(
686 "WIT primitive `{unsupported:?}` is not supported in note storage schemas"
687 ))),
688 }
689 }
690
691 fn build_type_id(&mut self, id: TypeId, depth: usize) -> Result<MemoizedSchemaType> {
693 let id = self.follow_aliases(id)?;
694 if let Some(memoized) = self.memo.get(&id) {
695 let maximum_depth = depth.saturating_add(memoized.maximum_subtree_depth);
696 if maximum_depth > MAX_NOTE_STORAGE_SCHEMA_DEPTH {
697 return Err(Error::new(format!(
698 "note storage schema nesting depth {maximum_depth} exceeds the limit of \
699 {MAX_NOTE_STORAGE_SCHEMA_DEPTH}"
700 )));
701 }
702 return Ok(memoized.clone());
703 }
704 if !self.active.insert(id) {
705 return Err(Error::new(
706 "recursive WIT types are not supported in note storage schemas",
707 ));
708 }
709
710 let definition = self.resolve.types[id].clone();
711 let name = definition.name.clone();
712 let docs = definition.docs.contents.clone();
713 let fqn = self.type_fqn(id)?;
714 let result = if fqn.as_deref() == Some(FELT_FQN) {
715 Ok(MemoizedSchemaType {
716 ty: Arc::new(SchemaType {
717 name,
718 fqn,
719 docs,
720 kind: SchemaTypeKind::Felt,
721 layout: FeltLayout::fixed(1),
722 }),
723 maximum_subtree_depth: 0,
724 expanded_nodes: 1,
725 })
726 } else {
727 match definition.kind {
728 TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs),
729 TypeDefKind::Record(record) => {
730 let mut fields = Vec::with_capacity(record.fields.len());
731 let mut layout = FeltLayout::fixed(0);
732 let mut maximum_subtree_depth = 0;
733 let mut expanded_nodes = 1usize;
734 for field in record.fields {
735 let memoized = self.build_type(field.ty, depth + 1)?;
736 maximum_subtree_depth =
737 maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth);
738 expanded_nodes = expanded_nodes.saturating_add(memoized.expanded_nodes);
739 layout = layout.concatenate(memoized.ty.layout)?;
740 fields.push(SchemaField {
741 name: field.name,
742 docs: field.docs.contents,
743 ty: memoized.ty,
744 });
745 }
746 Ok(MemoizedSchemaType {
747 ty: Arc::new(SchemaType {
748 name,
749 fqn,
750 docs,
751 kind: SchemaTypeKind::Record(fields),
752 layout,
753 }),
754 maximum_subtree_depth,
755 expanded_nodes,
756 })
757 }
758 TypeDefKind::Option(payload) => {
759 let payload = self.build_type(payload, depth + 1)?;
760 let maximum = 1usize
761 .checked_add(payload.ty.layout.maximum)
762 .ok_or_else(|| Error::new("option layout maximum width is too large"))?;
763 let layout = FeltLayout::bounded(1, maximum)?;
764 Ok(MemoizedSchemaType {
765 maximum_subtree_depth: 1 + payload.maximum_subtree_depth,
766 expanded_nodes: payload.expanded_nodes.saturating_add(1),
767 ty: Arc::new(SchemaType {
768 name,
769 fqn,
770 docs,
771 kind: SchemaTypeKind::Option(payload.ty),
772 layout,
773 }),
774 })
775 }
776 TypeDefKind::Variant(variant) => {
777 let mut cases = Vec::with_capacity(variant.cases.len());
778 let mut maximum_subtree_depth = 0;
779 let mut expanded_nodes = 1usize;
780 for case in variant.cases {
781 let payload = match case.ty {
782 Some(ty) => {
783 let memoized = self.build_type(ty, depth + 1)?;
784 maximum_subtree_depth =
785 maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth);
786 expanded_nodes =
787 expanded_nodes.saturating_add(memoized.expanded_nodes);
788 Some(memoized.ty)
789 }
790 None => None,
791 };
792 cases.push(SchemaCase {
793 name: case.name,
794 docs: case.docs.contents,
795 payload,
796 });
797 }
798 let layout = variant_layout(&cases)?;
799 Ok(MemoizedSchemaType {
800 ty: Arc::new(SchemaType {
801 name,
802 fqn,
803 docs,
804 kind: SchemaTypeKind::Variant(cases),
805 layout,
806 }),
807 maximum_subtree_depth,
808 expanded_nodes,
809 })
810 }
811 TypeDefKind::Enum(enum_) => {
812 let cases = enum_
813 .cases
814 .into_iter()
815 .map(|case| SchemaCase {
816 name: case.name,
817 docs: case.docs.contents,
818 payload: None,
819 })
820 .collect::<Vec<_>>();
821 let layout = variant_layout(&cases)?;
822 Ok(MemoizedSchemaType {
823 ty: Arc::new(SchemaType {
824 name,
825 fqn,
826 docs,
827 kind: SchemaTypeKind::Variant(cases),
828 layout,
829 }),
830 maximum_subtree_depth: 0,
831 expanded_nodes: 1,
832 })
833 }
834 unsupported => Err(Error::new(format!(
835 "WIT {} `{}` is not supported in note storage schemas",
836 unsupported.as_str(),
837 fqn.as_deref().or(name.as_deref()).unwrap_or("<anonymous>")
838 ))),
839 }
840 };
841 self.active.remove(&id);
842 if let Ok(memoized) = &result {
843 ensure_expanded_node_limit(&memoized.ty, memoized.expanded_nodes)?;
844 self.memo.insert(id, memoized.clone());
845 }
846 result
847 }
848
849 fn build_named_alias(
853 &mut self,
854 ty: Type,
855 name: Option<String>,
856 fqn: Option<String>,
857 docs: Option<String>,
858 ) -> Result<MemoizedSchemaType> {
859 match ty {
860 Type::Id(_) => unreachable!("build_type_id must follow ID aliases first"),
861 Type::U64 => self.primitive(PrimitiveType::U64, name, fqn, docs),
862 Type::U32 => self.primitive(PrimitiveType::U32, name, fqn, docs),
863 Type::U8 => self.primitive(PrimitiveType::U8, name, fqn, docs),
864 Type::Bool => self.primitive(PrimitiveType::Bool, name, fqn, docs),
865 unsupported => Err(Error::new(format!(
866 "WIT primitive alias `{unsupported:?}` is not supported in note storage schemas"
867 ))),
868 }
869 }
870
871 fn primitive(
873 &self,
874 primitive: PrimitiveType,
875 name: Option<String>,
876 fqn: Option<String>,
877 docs: Option<String>,
878 ) -> Result<MemoizedSchemaType> {
879 let width = match primitive {
880 PrimitiveType::U64 => 2,
881 PrimitiveType::U32 | PrimitiveType::U8 | PrimitiveType::Bool => 1,
882 };
883 Ok(MemoizedSchemaType {
884 ty: Arc::new(SchemaType {
885 name,
886 fqn,
887 docs,
888 kind: SchemaTypeKind::Primitive(primitive),
889 layout: FeltLayout::fixed(width),
890 }),
891 maximum_subtree_depth: 0,
892 expanded_nodes: 1,
893 })
894 }
895
896 fn follow_aliases(&self, mut id: TypeId) -> Result<TypeId> {
898 let mut visited = HashSet::new();
899 loop {
900 if !visited.insert(id) {
901 return Err(Error::new("cyclic WIT type aliases are not supported"));
902 }
903 match self.resolve.types[id].kind {
904 TypeDefKind::Type(Type::Id(next)) => id = next,
905 _ => return Ok(id),
906 }
907 }
908 }
909
910 fn type_fqn(&self, id: TypeId) -> Result<Option<String>> {
912 let definition = &self.resolve.types[id];
913 let Some(type_name) = definition.name.as_deref() else {
914 return Ok(None);
915 };
916 let TypeOwner::Interface(interface_id) = definition.owner else {
917 return Err(Error::new(format!(
918 "named WIT type `{type_name}` is not owned by an interface"
919 )));
920 };
921 let interface = &self.resolve.interfaces[interface_id];
922 let interface_name = interface.name.as_deref().ok_or_else(|| {
923 Error::new(format!("type `{type_name}` belongs to an unnamed interface"))
924 })?;
925 let package_id = interface.package.ok_or_else(|| {
926 Error::new(format!("interface `{interface_name}` does not belong to a package"))
927 })?;
928 let package_name = &self.resolve.packages[package_id].name;
929 let mut fqn =
930 format!("{}:{}/{}", package_name.namespace, package_name.name, interface_name);
931 if let Some(version) = &package_name.version {
932 fqn.push('@');
933 fqn.push_str(&version.to_string());
934 }
935 fqn.push('.');
936 fqn.push_str(type_name);
937 Ok(Some(fqn))
938 }
939}
940
941fn variant_layout(cases: &[SchemaCase]) -> Result<FeltLayout> {
943 if cases.is_empty() {
944 return Err(Error::new("a note storage variant must define at least one case"));
945 }
946 let minimum_payload = cases
947 .iter()
948 .map(|case| case.payload.as_ref().map_or(0, |ty| ty.layout.minimum))
949 .min()
950 .unwrap_or(0);
951 let maximum_payload = cases
952 .iter()
953 .map(|case| case.payload.as_ref().map_or(0, |ty| ty.layout.maximum))
954 .max()
955 .unwrap_or(0);
956 let minimum = 1usize
957 .checked_add(minimum_payload)
958 .ok_or_else(|| Error::new("variant layout minimum width is too large"))?;
959 let maximum = 1usize
960 .checked_add(maximum_payload)
961 .ok_or_else(|| Error::new("variant layout maximum width is too large"))?;
962 FeltLayout::bounded(minimum, maximum)
963}
964
965fn kind_name(kind: &SchemaTypeKind) -> &'static str {
967 match kind {
968 SchemaTypeKind::Felt => "felt",
969 SchemaTypeKind::Primitive(_) => "primitive",
970 SchemaTypeKind::Record(_) => "record",
971 SchemaTypeKind::Option(_) => "option",
972 SchemaTypeKind::Variant(_) => "variant",
973 }
974}
975
976pub(crate) fn normalize_name(name: &str) -> String {
978 name.trim().replace('_', "-")
979}