Repository navigation
Expand file tree
/
Copy pathstructs.rs
More file actions
2555 lines (2424 loc) · 120 KB
/
Copy pathstructs.rs
File metadata and controls
2555 lines (2424 loc) · 120 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
use super::*;
#[derive(Clone, Debug)]
pub enum EnumVariantData {
Inlined(RustRecord),
RustType(RustType),
}
// rep is Optional - None means we just serialize raw, ie for type choices
#[derive(Clone)]
pub struct EnumVariant {
pub name: VariantIdent,
/// Whether `name` was derived from `data` rather than explicitly requested with `@name`.
/// Deferred generic inline choices rewrite a parser-only placeholder in `data` to a concrete
/// enum owner, and only a derived variant label may follow that rewrite.
pub derived_name: bool,
pub data: EnumVariantData,
pub serialize_as_embedded_group: bool,
pub doc: Option<String>,
/// For a map-representation group choice arm that collapsed a single keyed entry
/// (`{ a: uint // ... }` → `A(u64)`), this holds the fixed member key (`a`). The value
/// still lives in `data`; the key is written before it on serialization and read+verified
/// before it on deserialization. `None` for array reps, type choices, and keyless entries.
pub key: Option<FixedValue>,
}
impl std::fmt::Debug for EnumVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// `derived_name` is parser/materialization bookkeeping rather than a semantic IR field.
// Keeping it out of the debug projection preserves the IR snapshot contract, which records
// emitted-shape semantics rather than the path that settled a variant's spelling.
f.debug_struct("EnumVariant")
.field("name", &self.name)
.field("data", &self.data)
.field(
"serialize_as_embedded_group",
&self.serialize_as_embedded_group,
)
.field("doc", &self.doc)
.field("key", &self.key)
.finish()
}
}
impl EnumVariant {
pub fn new(
name: VariantIdent,
rust_type: RustType,
serialize_as_embedded_group: bool,
doc: Option<String>,
) -> Self {
Self {
name,
derived_name: false,
data: EnumVariantData::RustType(rust_type),
serialize_as_embedded_group,
doc,
key: None,
}
}
pub fn new_embedded(
name: VariantIdent,
embedded_record: RustRecord,
doc: Option<String>,
) -> Self {
Self {
name,
derived_name: false,
data: EnumVariantData::Inlined(embedded_record),
serialize_as_embedded_group: false,
doc,
key: None,
}
}
pub fn with_derived_name(mut self) -> Self {
self.derived_name = true;
self
}
/// Builder for the collapse site: attach the fixed member key of a collapsed map-rep arm.
pub fn with_key(mut self, key: Option<FixedValue>) -> Self {
self.key = key;
self
}
/// Gets the next CBOR type after the passed in rep (array/map) tag
/// Returns None if this is not possible and brute-force deserialization
/// trying every variant should be used instead
pub fn cbor_types_inner(
&self,
types: &IntermediateTypes,
outer_rep: Option<Representation>,
) -> Option<Vec<CBORType>> {
// A collapsed map-rep arm dispatches on the type of its fixed member KEY, not its value:
// spec-valid bytes for `{ a: uint // ... }` start with the key `"a"`, so the first CBOR
// type after the map tag is the key's. (Array reps / type choices leave `key == None`.)
if outer_rep == Some(Representation::Map)
&& let Some(key) = &self.key
{
return Some(vec![fixed_value_cbor_type(key)]);
}
match &self.data {
EnumVariantData::RustType(ty) => {
if ty.encodings.is_empty() && outer_rep.is_some() {
if let ConceptualRustType::Rust(ident) =
ty.conceptual_type.resolve_alias_shallow()
{
match types.rust_struct(ident).unwrap().variant() {
// we can't know this unless there's a way to provide this info
RustStructType::Extern => None,
RustStructType::Record(record) => {
// Reconcile with the serializer's `serialize_as_embedded_group`
// split (`generate_enum`): an embedded plain-group arm flattens
// the record's fields directly after the outer header, so the arm
// discriminant peeks the record's FIRST FIELD. A non-embedded
// named/aliased record arm instead serializes NESTED — the outer
// header then the record's OWN Array/Map header (or struct-level
// tag) — so the discriminant must peek that own wire type, which
// `RustType::cbor_types` already derives from `record.rep` and any
// struct tag. Hand-mapping `record.rep` here would drop the tag.
if self.serialize_as_embedded_group {
Self::record_first_cbor_types(types, record)
} else {
Some(ty.cbor_types(types))
}
}
// Conservative, not a gap: `None` = brute-force try-each-variant,
// which is always correct. By the same wire-form analysis as the
// Record arm above, a NON-embedded group-choice arm could dispatch on
// its own rep header (its serialize writes outer header then its own
// per-arm header), i.e. `Some(ty.cbor_types(types))` — but no row/cell
// pins that shape's dispatch today, so keep brute force until one does
// (enumerate the row first, per cddl-matrix/roadmap.toml's
// "Intra-alternative variation rows" rule).
RustStructType::GroupChoice { .. } => None,
_ => Some(ty.cbor_types(types)),
}
} else {
Some(ty.cbor_types(types))
}
} else {
Some(ty.cbor_types(types))
}
}
EnumVariantData::Inlined(record) => {
if outer_rep.is_some() {
Self::record_first_cbor_types(types, record)
} else {
Some(match record.rep {
Representation::Array => vec![CBORType::Array],
Representation::Map => vec![CBORType::Map],
})
}
}
}
}
/// The set of CBOR types the first data item of a record-representation arm can take, used for
/// enum dispatch. Arrays are positional: only the first (non-optional-prefix) field matters. A
/// map presents its entries in any order, so the first item is any KEY — dispatch must union
/// every field's key type. A non-fixed key (`k => v`) is unknowable → `None` (brute force).
fn record_first_cbor_types(
types: &IntermediateTypes,
record: &RustRecord,
) -> Option<Vec<CBORType>> {
match record.rep {
Representation::Map => {
let mut ret = vec![];
for field in record.fields.iter() {
ret.push(fixed_value_cbor_type(field.key.as_ref()?));
}
Some(ret)
}
Representation::Array => {
let mut ret = vec![];
for field in record.fields.iter() {
ret.extend(field.rust_type.cbor_types(types));
if !field.optional {
break;
}
}
Some(ret)
}
}
}
/// The Record fields a GROUP choice expands into this variant's `new_<variant>` ctor as
/// direct parameters (both the rust and the wasm pass), when the variant is a NAMED type
/// resolving to a Record struct — `None` for every other variant shape (incl. aliases to
/// records: the emitters match the bare `Rust` ident only, deliberately not
/// `resolve_alias_shallow`, and an alias arm gets the single-arg ctor instead).
/// `scope_references` marks exactly these field types so a ctor expanded from a record in
/// ANOTHER module still imports what its parameters name; the emitters and that import walk
/// all go through this one helper so the parameter list and the import set can't drift.
/// Panics on an unresolvable ident (`enum_ident` names the enum for the message) — the
/// resolution the emitters previously did inline.
pub fn group_ctor_record_fields<'a>(
&self,
types: &'a IntermediateTypes,
enum_ident: &RustIdent,
) -> Option<Vec<&'a RustField>> {
let EnumVariantData::RustType(ty) = &self.data else {
return None;
};
let ConceptualRustType::Rust(ident) = &ty.conceptual_type else {
return None;
};
let resolved = types.rust_struct(ident).unwrap_or_else(|| {
// Constant text LEADS so the recombination sweep's panic-class ledger can key on the
// message (its key-shape floor rejects site-only keys; the idents vary per spec).
panic!("variant ctor refers to undefined ident: {enum_ident} -> {ident}")
});
let RustStructType::Record(record) = resolved.variant() else {
return None;
};
Some(
record
.fields
.iter()
.filter(|f| !f.optional && !f.rust_type.conceptual_type.is_fixed_value())
.collect(),
)
}
// Can only be used on RustType variants, panics otherwise.
// So don't call this when we're embedding the variant types
pub fn rust_type(&self) -> &RustType {
match &self.data {
EnumVariantData::RustType(ty) => ty,
EnumVariantData::Inlined(_) => {
panic!("only call rust_type() when you know it can't be inlined")
}
}
}
pub fn name_as_var(&self) -> String {
let snake = convert_to_snake_case(&self.name.to_string());
// we can't use (rust) reserved keywords as param: eg new_u32(u32: u32)
// TODO: do we need to cover any other (rust) reserved keywords?
String::from(match snake.as_str() {
"u8" | "u16" | "u32" | "u64" => "uint",
"i8" | "i16" | "i32" | "i64" => "int",
"f32" => "float32",
"f64" => "float64",
x => x,
})
}
pub fn can_embed_fields(types: &IntermediateTypes, ty: &ConceptualRustType) -> bool {
match ty {
ConceptualRustType::Rust(ident) => {
if let RustStructType::Record(record) = types.rust_struct(ident).unwrap().variant()
{
// Only ARRAY records can be inlined into an enum variant: the inlined
// serializer (`generate_array_struct_serialization`) asserts array rep, and a
// map record's keys must still be written/read via the record path. A map arm
// with <=1 non-fixed fields therefore stays a named `GroupN` reference handled
// by the (key-aware) record path instead of being embedded (which panicked).
return record.rep == Representation::Array
&& record
.fields
.iter()
.filter(|field| !field.rust_type.conceptual_type.is_fixed_value())
.count()
<= 1;
}
false
}
ConceptualRustType::Alias(_, ty) => Self::can_embed_fields(types, ty),
_ => false,
}
}
}
/// The CBOR major type a fixed value serializes to — used to dispatch enum variants on their fixed
/// map key. Mirrors `RustType::cbor_types` for `ConceptualRustType::Fixed`.
fn fixed_value_cbor_type(value: &FixedValue) -> CBORType {
match value {
FixedValue::Uint(_) => CBORType::UnsignedInteger,
FixedValue::Nint(_) => CBORType::NegativeInteger,
FixedValue::Float(_) => CBORType::Special,
FixedValue::Text(_) => CBORType::Text,
FixedValue::Bytes(_) => CBORType::Bytes,
FixedValue::Null => CBORType::Special,
FixedValue::Undefined => CBORType::Special,
FixedValue::Bool(_) => CBORType::Special,
}
}
#[derive(Clone, Debug)]
pub struct RustField {
pub name: String,
/// Original group-entry order. Most emitters deliberately use their own canonical ordering,
/// but transparent group-body projection must interleave value fields with exact-zero metadata.
pub source_index: usize,
/// Invariant held by the record-field construction seam (`parse_record_from_group_choice`):
/// `rust_type.config.default` is `Some` only when `optional` — a `.default` on a mandatory
/// member is inert by RFC 8610 §3.8.2 (a default fills an ABSENT value) and is stripped, with
/// a warning, before the field is built. Every face may therefore read a defaulted field as
/// an optional one.
pub rust_type: RustType,
pub optional: bool,
// None for array fields, Some for map fields. FixedValue for (de)serialization for map keys
pub key: Option<FixedValue>,
// comment DSL metadata applied to this field
pub rule_metadata: RuleMetadata,
}
impl RustField {
pub fn new(
name: String,
rust_type: RustType,
optional: bool,
key: Option<FixedValue>,
rule_metadata: RuleMetadata,
) -> Self {
Self {
name,
source_index: usize::MAX,
rust_type,
optional,
key,
rule_metadata,
}
}
pub fn with_source_index(mut self, source_index: usize) -> Self {
self.source_index = source_index;
self
}
/// Whether this member's rust type is a nested `Option<Option<…>>`: an OPTIONAL member
/// (`? f: …`, which the struct stores behind a presence-`Option`) whose own type is NULLABLE
/// (`T / null` → `Option<T>`). The single source of truth for that shape — the serde
/// `double_option` adapter's emission condition, the runtime module's usage gate and the wasm
/// face's flatten condition are all the same predicate, so they cannot drift.
///
/// A member with a `.default` is NOT stored behind a presence-`Option` (the default fills the
/// absent case), so it is excluded: its rust type is the nullable's single `Option`.
pub fn is_double_option(&self) -> bool {
self.optional
&& self.rust_type.config.default.is_none()
&& matches!(
self.rust_type.conceptual_type.resolve_alias_shallow(),
ConceptualRustType::Optional(_)
)
}
pub fn to_embedded_rust_type(&self) -> Cow<'_, RustType> {
if self.optional {
Cow::Owned(RustType::new(ConceptualRustType::Optional(Box::new(
self.rust_type.clone(),
))))
} else {
Cow::Borrowed(&self.rust_type)
}
}
}
#[derive(Clone, Debug, Copy)]
pub enum RustStructCBORLen {
// always a fixed number of CBOR length
Fixed(usize),
// can vary with no min/max
Dynamic,
// has optional fields. (mandatory fields) - skips over type choices (including T / nil -> Option<T>)
OptionalFields(usize),
}
#[derive(Clone, Debug, Default)]
pub struct RustStructConfig {
pub custom_json: bool,
pub custom_serialize: Option<String>,
pub custom_deserialize: Option<String>,
/// `@custom_encodings` written on the RULE. Carried for exactly one reader:
/// `IntermediateTypes::finalize`'s refusal of a declaration on a rule that mints a STRUCT. A
/// struct owns its encoding metadata inside itself (its `encodings` member), so no codec-visible
/// tuple crosses the boundary for a declaration to describe — honoring it is impossible and
/// dropping it silently is the class this DSL rejects. The declaration is honored only on a
/// transparent ALIAS rule (where it rides `AliasInfo::rule_metadata`) or on a FIELD (where it
/// rides `RustField::rule_metadata`).
pub custom_encodings: Option<Vec<crate::comment_ast::EncodingKind>>,
/// `@custom_wire_major` written on the RULE. Carried for exactly one reader:
/// `IntermediateTypes::finalize`'s refusal of a declaration on a rule that mints a STRUCT. The
/// declared major is read only through the ALIAS channel (`AliasInfo::rule_metadata`), when the
/// rule keys an OPEN TABLE's typed row or proves a variable middle ARRAY boundary — a
/// struct-minting rule has no such channel, so honoring it is impossible and dropping it
/// silently is the class this DSL rejects.
pub custom_wire_major: Option<crate::comment_ast::WireMajor>,
pub doc: Option<String>,
pub newtype_getter: Option<Option<String>>,
/// `@duplicates` policy for a collection rule (`[* a]` / `[+ a]` / the tag-258 set idiom / a
/// table). Carried onto the registered transparent alias's `RustType` at `register_rust_struct`
/// so every embed site (and generic use-site re-resolution) sees the policy.
pub duplicates: Option<DuplicatesPolicy>,
/// A named non-generic SET rule (the tag-258 idiom or single-arm mandatory-258 form) nominalized
/// into a `Wrapper` struct that OWNS its `{tag, len, elem}` encodings (Phase 2.2). Distinct from
/// a plain `@newtype` wrapper: the set nominal suppresses the inherent `get()` (it shadows
/// `OrderedSet::get(index)` through `Deref` — E0061), emits the set ergonomics
/// (`Deref`/`DerefMut`/`IntoIterator`/`From`/`TryFrom`), and mandates always-on
/// encodings-ignored comparison derives for parity with `OrderedSet`'s unconditional derives.
pub set_nominal: bool,
}
impl RustStructConfig {
/// Whether the rule carries `@duplicates reject`. The struct-config twin of
/// `RustType::duplicates_reject`.
pub fn duplicates_reject(&self) -> bool {
self.duplicates == Some(DuplicatesPolicy::Reject)
}
/// Whether the rule carries `@duplicates preserve`.
pub fn duplicates_preserve(&self) -> bool {
self.duplicates == Some(DuplicatesPolicy::Preserve)
}
}
impl From<Option<&RuleMetadata>> for RustStructConfig {
fn from(rule_metadata: Option<&RuleMetadata>) -> Self {
match rule_metadata {
Some(rule_metadata) => Self {
custom_json: rule_metadata.custom_json,
custom_serialize: rule_metadata.custom_serialize.clone(),
custom_deserialize: rule_metadata.custom_deserialize.clone(),
custom_encodings: rule_metadata.custom_encodings.clone(),
custom_wire_major: rule_metadata.custom_wire_major,
doc: rule_metadata.doc.clone(),
newtype_getter: rule_metadata.newtype.clone(),
duplicates: rule_metadata.duplicates,
set_nominal: false,
},
None => Self::default(),
}
}
}
/// A struct's structural/wire identity: the `Debug` rendering of `(tag, tag_optional, config,
/// variant)`. Hand-written `Debug` omits `RustType::generic_param_binding` and
/// `EnumVariant::derived_name`; neither field bears identity. Compare only; never order or render.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StructuralFingerprint(String);
#[derive(Clone, Debug)]
pub struct RustStruct {
pub(super) ident: RustIdent,
pub(super) tag: Option<usize>,
/// When `tag` is set, whether that tag is OPTIONALLY present on the wire (the transparent
/// tag-set idiom, `x = #6.N([* a]) / [* a]`) rather than mandatory. Only ever true on an
/// `Array`/`Table` variant produced by the `parse_type_choices` collapse; drives
/// `register_rust_struct` to attach `OptionallyTagged` instead of `Tagged` to the registered
/// alias. `false` everywhere else, so every pre-existing tagged rule stays byte-identical.
pub(super) tag_optional: bool,
config: RustStructConfig,
pub(crate) variant: RustStructType,
}
// Keep the public Table domain/range payloads inline as owned RustTypes. The bounds-role tag
// increases the size gap past Clippy's threshold; retain this representation without adding
// per-table allocations or changing those public field types solely to satisfy that threshold.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum RustStructType {
Record(RustRecord),
Table {
domain: RustType,
range: RustType,
/// occurrence-count bounds (`+` / `n*m`) — a cardinality constraint on the table itself.
/// [`OccurrenceWindow`] retains authored optional endpoints; use its `raw` method for the legacy tuple.
/// `None` is the unbounded `*` table, the `+` / `1*` shape `(Some(1), None)` selects
/// `NonEmptyMap`, and every other window selects `BoundedMap` (`NonEmptyPairMap` /
/// `BoundedPairMap` under `@duplicates preserve`). Rides the registered alias's `RustType` so
/// embed sites enforce it, exactly like `Array` bounds.
bounds: Option<OccurrenceWindow>,
},
Array {
element_type: RustType,
/// occurrence-count bounds (`+` / `n*m`) — a LENGTH constraint on the array itself.
/// [`OccurrenceWindow`] retains authored optional endpoints; use its `raw` method for the legacy tuple.
/// Applied to the registered alias RustType's config so embed sites enforce it; kept off
/// the element_type so it can't be misread as an element VALUE bound.
bounds: Option<OccurrenceWindow>,
},
TypeChoice {
variants: Vec<EnumVariant>,
},
GroupChoice {
variants: Vec<EnumVariant>,
rep: Representation,
},
Wrapper {
wrapped: RustType,
min_max: Option<IntWindow>,
/// NaN-safe float window for a float-typed wrapper (`c = 0.5..10.5`, `#6.5(0.5..10.5)`).
/// Mutually exclusive with `min_max` (a wrapper never carries both). Its presence — like a
/// `Some` `min_max` — makes the wrapper's `new()`/deserialize fallible.
float_min_max: Option<FloatWindow>,
},
/// This is a no-op in generation but to prevent lookups of things in the prelude
/// e.g. `int` from not being resolved while still being able to detect it when
/// referring to a struct that doesn't exist even after generation.
Extern,
CStyleEnum {
variants: Vec<EnumVariant>,
},
RawBytesType,
}
impl RustStruct {
pub fn new_record(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
record: RustRecord,
) -> Self {
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::Record(record),
}
}
pub fn new_table(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
domain: RustType,
range: RustType,
bounds: Option<IntWindow>,
) -> Self {
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::Table {
domain,
range,
bounds: bounds.map(OccurrenceWindow::from_raw),
},
}
}
pub fn new_array(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
element_type: RustType,
bounds: Option<IntWindow>,
) -> Self {
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::Array {
element_type,
bounds: bounds.map(OccurrenceWindow::from_raw),
},
}
}
/// This will automatically check if it's a c-stlye enum and use that instead if possible
pub fn new_type_choice(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
variants: Vec<EnumVariant>,
cli: &Cli,
) -> Self {
// we could potentially push these encoding vars out too but this is extremely low priority
// unless people want to have tagged c-style enums encoded in different ways
let cant_store_tag = tag.is_some() && cli.preserve_encodings;
let not_fixed_or_cant_store_enc_vars_or_outer_len =
variants.iter().any(|ev: &EnumVariant| {
ev.serialize_as_embedded_group
|| (cli.preserve_encodings && !ev.rust_type().encodings.is_empty())
|| !matches!(
ev.rust_type().conceptual_type.resolve_alias_shallow(),
ConceptualRustType::Fixed(_)
)
});
if cant_store_tag
|| not_fixed_or_cant_store_enc_vars_or_outer_len
|| (cli.preserve_encodings && !enum_variants_have_same_encoding_var(&variants))
{
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::TypeChoice { variants },
}
} else {
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::CStyleEnum { variants },
}
}
}
/// A nominal owner for one fixed value. Deliberately bypasses `new_type_choice`'s all-fixed
/// C-style-enum optimization: only `TypeChoice` owns standalone codecs and preserve encoding
/// metadata, which a named fixed rule (and a fixed/null inner) needs.
pub fn new_fixed_singleton(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
fixed_type: RustType,
) -> Self {
let fixed = match fixed_type.conceptual_type.resolve_alias_shallow() {
ConceptualRustType::Fixed(fixed) => fixed,
other => panic!("fixed singleton must resolve to Fixed, got {other:?}"),
};
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::TypeChoice {
variants: vec![EnumVariant::new(
VariantIdent::new_custom(format!("Value{}", fixed.singleton_name_fragment())),
fixed_type,
false,
None,
)],
},
}
}
pub fn new_group_choice(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
variants: Vec<EnumVariant>,
rep: Representation,
) -> Self {
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::GroupChoice { variants, rep },
}
}
pub fn new_wrapper(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
wrapped_type: RustType,
min_max: Option<IntWindow>,
) -> Self {
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::Wrapper {
wrapped: wrapped_type,
min_max,
float_min_max: None,
},
}
}
/// A float-windowed wrapper (`c = 0.5..10.5`, `#6.5(0.5..10.5)`). Parallel to `new_wrapper` but
/// carries a NaN-safe `FloatWindow` instead of an integer `min_max`.
pub fn new_wrapper_float(
ident: RustIdent,
tag: Option<usize>,
rule_metadata: Option<&RuleMetadata>,
wrapped_type: RustType,
float_min_max: Option<FloatWindow>,
) -> Self {
Self {
ident,
tag,
tag_optional: false,
config: RustStructConfig::from(rule_metadata),
variant: RustStructType::Wrapper {
wrapped: wrapped_type,
min_max: None,
float_min_max,
},
}
}
pub fn new_extern(ident: RustIdent) -> Self {
Self {
ident,
tag: None,
tag_optional: false,
config: RustStructConfig::default(),
variant: RustStructType::Extern,
}
}
pub fn new_raw_bytes(ident: RustIdent) -> Self {
Self {
ident,
tag: None,
tag_optional: false,
config: RustStructConfig::default(),
variant: RustStructType::RawBytesType,
}
}
pub fn ident(&self) -> &RustIdent {
&self.ident
}
pub fn tag(&self) -> Option<usize> {
self.tag
}
/// Mark this struct's tag as OPTIONALLY present on the wire (the transparent tag-set collapse).
/// See the `tag_optional` field. Consuming builder, like `RustType::as_alias`/`as_bytes`.
#[allow(clippy::wrong_self_convention)]
pub fn as_optionally_tagged(mut self) -> Self {
self.tag_optional = true;
self
}
/// Mark this `Wrapper` struct as a nominalized SET (Phase 2.2). See `RustStructConfig::set_nominal`.
#[allow(clippy::wrong_self_convention)]
pub fn as_set_nominal(mut self) -> Self {
self.config.set_nominal = true;
self
}
pub fn tag_optional(&self) -> bool {
self.tag_optional
}
pub fn config(&self) -> &RustStructConfig {
&self.config
}
/// Attach a rule-level `@doc` this struct's own config does not already carry. Used for the
/// structs whose config comes from somewhere other than the annotated rule — a generic
/// INSTANCE's config is the generic DEFINITION's, so the binding rule's own doc has no other
/// route in. Never overwrites: a config that already has a doc got it from the rule that owns
/// the struct.
pub fn set_doc_if_absent(&mut self, doc: &str) {
if self.config.doc.is_none() {
self.config.doc = Some(doc.to_owned());
}
}
/// Attach a rule-level `@custom_json` this struct's own config could not carry — the generic
/// INSTANCE whose config belongs to the generic definition, and the plain GROUP rule whose
/// metadata is read off a comment slot cddl leaves empty. Set-only, never cleared: the flag is a
/// statement by the rule that owns the struct either way, so the two sources cannot disagree.
pub fn set_custom_json(&mut self) {
self.config.custom_json = true;
}
pub fn variant(&self) -> &RustStructType {
&self.variant
}
/// Whether `self` and `other` describe the same generated type up to their NAME — same wire
/// shape, same fields, same config, same tag.
///
/// The global Rust-struct registration guard uses this to retain the first owner for every
/// generated-name overlap, reusing a structurally equivalent claimant and rejecting an
/// incompatible one before it can retarget shared IR. Multi-arm group-choice arms in different
/// rules are the original benign case: generic arm names like `first`/`second` are idiomatic and
/// recur across rules, so identical arms share one generated struct while differing arms are
/// rejected.
///
/// Compared through `Debug` rather than a derived `PartialEq`: equality on the IR type tree
/// would be a much broader semantic commitment (what `encodings` or a doc comment mean for
/// "equal") than this one question needs, and the `Debug` rendering is already the IR's
/// structural fingerprint elsewhere (`api::ir_structs_debug`). `ident` is deliberately excluded —
/// it is the very thing that differs — and it does not appear in any of the compared fields.
pub fn structurally_equivalent(&self, other: &RustStruct) -> bool {
self.structural_fingerprint() == other.structural_fingerprint()
}
/// The deterministic structural/wire identity used by the nominal-name mint registry. Keep
/// this exactly aligned with [`Self::structurally_equivalent`]: the registry is an earlier
/// observation point for the global registration guard, not a second ownership policy.
pub fn structural_fingerprint(&self) -> StructuralFingerprint {
StructuralFingerprint(format!(
"{:?}",
(&self.tag, self.tag_optional, &self.config, &self.variant)
))
}
// The following methods are used internally to generate serialize/deserialize code
// INSIDE of the serialize/deserialize implementations for this specific type.
// You probably aren't interested in this from outside of that use-case.
// Some(count) if it always has the same number of fields (ie no optional fields), None otherwise
pub fn fixed_field_count(&self, types: &IntermediateTypes) -> Option<usize> {
match &self.variant {
RustStructType::Record(record) => record.fixed_field_count(types),
RustStructType::Table { .. } => None,
RustStructType::Array { .. } => None,
// TODO: investigate if we should be supporting this for TypeChoice (also wrapper?)
//RustStructType::TypeChoice { .. } => None,
RustStructType::TypeChoice { .. } => {
unreachable!("I don't think type choices should be using length?")
}
RustStructType::GroupChoice { .. } => {
unreachable!("I don't think group choices should be using length?")
}
RustStructType::Wrapper { .. } => unreachable!("wrapper types don't use length"),
RustStructType::Extern => panic!(
"do we need to look this up ever? will the prelude have structs with fields?"
),
RustStructType::CStyleEnum { .. } => Some(1),
RustStructType::RawBytesType => Some(1),
}
}
/// Even if fixed_field_count() == None, this will return an expression for
/// a definite length, e.g. with optional field checks in the expression
/// This is useful for definite-length serialization
/// self_expr is an expression that evaluates to this struct (e.g. "self") at the point where
/// the return of this function will be used.
/// self_is_ref whether the above expression is by-ref
pub fn definite_info(
&self,
self_expr: &str,
self_is_ref: bool,
types: &IntermediateTypes,
cli: &Cli,
) -> String {
match &self.variant {
RustStructType::Record(record) => {
record.definite_info(self_expr, self_is_ref, types, cli)
}
RustStructType::Table { .. } => format!("{self_expr}.0.len() as u64"),
RustStructType::Array { .. } => format!("{self_expr}.0.len() as u64"),
RustStructType::TypeChoice { .. } => {
unreachable!("I don't think type choices should be using length?")
}
RustStructType::GroupChoice { .. } => {
unreachable!("I don't think group choices should be using length?")
}
RustStructType::Wrapper { .. } => unreachable!("wrapper types don't use length"),
RustStructType::Extern => panic!(
"do we need to look this up ever? will the prelude have structs with fields?"
),
RustStructType::CStyleEnum { .. } => "1".into(),
RustStructType::RawBytesType => "1".into(),
}
}
// the minimum cbor length of this struct - can be useful for deserialization length checks
// does not count ANY type choice like types including Optional UNLESS the option Some type
// has cbor len 1 too - to be consistent with expanded_field_count
pub fn expanded_mandatory_field_count(&self, types: &IntermediateTypes) -> usize {
match &self.variant {
RustStructType::Record(record) => record.expanded_mandatory_field_count(types),
RustStructType::Table { .. } => 0,
RustStructType::Array { .. } => 0,
//RustStructType::TypeChoice{ .. } => 0,
RustStructType::TypeChoice { .. } => {
unreachable!("I don't think type choices should be using length?")
}
RustStructType::GroupChoice { .. } => {
unreachable!("I don't think group choices should be using length?")
}
RustStructType::Wrapper { .. } => unreachable!("wrapper types don't use length"),
RustStructType::Extern => panic!(
"do we need to look this up ever? will the prelude have structs with fields?"
),
RustStructType::CStyleEnum { .. } => 1,
RustStructType::RawBytesType => 1,
}
}
pub fn cbor_len_info(&self, types: &IntermediateTypes) -> RustStructCBORLen {
match &self.variant {
RustStructType::Record(record) => record.cbor_len_info(types),
RustStructType::Table { .. } => RustStructCBORLen::Dynamic,
RustStructType::Array { .. } => RustStructCBORLen::Dynamic,
//RustStructType::TypeChoice{ .. } => RustStructCBORLen::Dynamic,
RustStructType::TypeChoice { .. } => {
unreachable!("I don't think type choices should be using length?")
}
RustStructType::GroupChoice { .. } => {
unreachable!("I don't think group choices should be using length?")
}
RustStructType::Wrapper { .. } => unreachable!("wrapper types don't use length"),
RustStructType::Extern => panic!(
"do we need to look this up ever? will the prelude have structs with fields?"
),
RustStructType::CStyleEnum { .. } => RustStructCBORLen::Fixed(1),
RustStructType::RawBytesType => RustStructCBORLen::Fixed(1),
}
}
pub fn visit_types<F: FnMut(&ConceptualRustType)>(&self, types: &IntermediateTypes, f: &mut F) {
self.visit_types_excluding(types, f, &mut BTreeSet::new())
}
pub fn visit_types_excluding<F: FnMut(&ConceptualRustType)>(
&self,
types: &IntermediateTypes,
f: &mut F,
already_visited: &mut BTreeSet<RustIdent>,
) {
match &self.variant {
RustStructType::Array { element_type, .. } => element_type
.conceptual_type
.visit_types_excluding(types, f, already_visited),
RustStructType::GroupChoice { variants, .. }
| RustStructType::TypeChoice { variants, .. }
| RustStructType::CStyleEnum { variants } => {
variants.iter().for_each(|v| match &v.data {
EnumVariantData::RustType(ty) => {
ty.conceptual_type
.visit_types_excluding(types, f, already_visited)
}
EnumVariantData::Inlined(record) => {
record.fields.iter().for_each(|field| {
field.rust_type.conceptual_type.visit_types_excluding(
types,
f,
already_visited,
)
});
record.forbidden_fields.iter().for_each(|field| {
field.rust_type.conceptual_type.visit_types_excluding(
types,
f,
already_visited,
)
});
}
})
}
RustStructType::Record(record) => {
record.fields.iter().for_each(|field| {
field
.rust_type
.conceptual_type
.visit_types_excluding(types, f, already_visited)
});
record.forbidden_fields.iter().for_each(|field| {
field
.rust_type
.conceptual_type
.visit_types_excluding(types, f, already_visited)
});
// Open rest (map `* k => v` row or array `* t` tail): the inner type(s) are real
// occurrences (they can reference named rules), so a reference reachable ONLY through
// the rest stays visible to `is_referenced` and friends. Walk the map K/V pair or the
// array element per the rest kind.
// An open table's TYPED row is the same shape and the same kind of occurrence, so it
// walks through the identical arm — iterating both members rather than only `rest`
// (a reference reachable only through `K_t`/`V_t` must stay visible too).
for row in record.dynamic_rows() {
match &row.kind {
RestKind::MapEntries { domain, range, .. } => {
domain
.conceptual_type
.visit_types_excluding(types, f, already_visited);
range
.conceptual_type
.visit_types_excluding(types, f, already_visited);
}
RestKind::ArrayTail { element, .. } => {
element.conceptual_type.visit_types_excluding(
types,
f,
already_visited,
);
}
}
}
}
RustStructType::Table { domain, range, .. } => {
domain
.conceptual_type
.visit_types_excluding(types, f, already_visited);
range
.conceptual_type
.visit_types_excluding(types, f, already_visited);
}
RustStructType::Wrapper { wrapped, .. } => wrapped
.conceptual_type
.visit_types_excluding(types, f, already_visited),
RustStructType::Extern => (),
RustStructType::RawBytesType => (),
}
}
}
// Regular struct with fields and such