@@ -183,14 +183,18 @@ def _resolve_field(
183183 Either resolved_inflater or bitfield_field will be non-None (not both).
184184 explicit_offset is set when an OffsetAttribute is present.
185185 """
186- # Unwrap Annotated[type, metadata...] — extract the real type and any metadata
187186 annotated_offset = None
188187 if get_origin (annotation ) is Annotated :
189188 ann_args = get_args (annotation )
190189 annotation = ann_args [0 ]
191- for meta in ann_args [1 :]:
192- if isinstance (meta , OffsetAttribute ):
193- annotated_offset = meta .offset
190+ ann_offsets = [m .offset for m in ann_args [1 :] if isinstance (m , OffsetAttribute )]
191+ if len (ann_offsets ) > 1 :
192+ raise ValueError (
193+ f"Field { name !r} has multiple OffsetAttribute entries in its Annotated metadata; "
194+ f"only one is allowed." ,
195+ )
196+ if ann_offsets :
197+ annotated_offset = ann_offsets [0 ]
194198
195199 if name not in reference .__dict__ :
196200 return inflater .inflater_for (annotation , owner = owner ), None , annotated_offset
@@ -202,6 +206,13 @@ def _resolve_field(
202206 if sum (isinstance (attr , Field ) for attr in attrs ) > 1 :
203207 raise ValueError ("Only one Field is allowed per attribute." )
204208
209+ attr_offsets = sum (isinstance (a , OffsetAttribute ) for a in attrs )
210+ if attr_offsets + (1 if annotated_offset is not None else 0 ) > 1 :
211+ raise ValueError (
212+ f"Field { name !r} has multiple OffsetAttribute entries (across Annotated metadata "
213+ f"and attribute tuple); only one is allowed." ,
214+ )
215+
205216 resolved_type = None
206217 bitfield_field = None
207218 explicit_offset = annotated_offset
@@ -231,6 +242,7 @@ def compute_own_size(cls: type[struct_impl], reference_type: type) -> None:
231242 bf_tracker = BitfieldTracker ()
232243 aligned = getattr (reference_type , "_aligned_" , False )
233244 seen_vla = False
245+ seen_names : set [str ] = set ()
234246
235247 for name , annotation , reference in iterate_annotation_chain (reference_type , terminate_at = struct ):
236248 if name == "_aligned_" :
@@ -245,13 +257,24 @@ def compute_own_size(cls: type[struct_impl], reference_type: type) -> None:
245257 # Detect VLA from default value or subscript annotation
246258 default = getattr (reference , name , None ) if hasattr (reference , name ) else None
247259 is_vla = isinstance (default , VLAField )
248- if not is_vla and isinstance (annotation , GenericAlias ):
260+ count_field_name : str | None = None
261+ if is_vla :
262+ count_field_name = default .count_field
263+ elif isinstance (annotation , GenericAlias ):
249264 args = annotation .__args__
250265 if len (args ) == 2 and isinstance (args [1 ], str ):
251266 is_vla = True
267+ count_field_name = args [1 ]
252268 if is_vla :
269+ if count_field_name is not None and count_field_name not in seen_names :
270+ raise ValueError (
271+ f"VLA field { name !r} references undefined count field { count_field_name !r} . "
272+ f"The count field must be declared before the VLA in the same struct." ,
273+ )
253274 seen_vla = True
254275
276+ seen_names .add (name )
277+
255278 resolved_type , bitfield_field , explicit_offset = struct_impl ._resolve_field (
256279 name , annotation , reference , cls ._inflater , owner = (None , cls ),
257280 )
@@ -351,13 +374,13 @@ def freeze(self: struct_impl) -> None:
351374 super ().freeze ()
352375
353376 def reset (self : struct_impl ) -> None :
354- """Reset each member to its frozen value ."""
377+ """Restore the struct's memory region to the bytes captured at freeze time ."""
355378 if not object .__getattribute__ (self , "_frozen" ):
356379 raise RuntimeError ("Cannot reset a struct that has not been frozen." )
357380
358- members = object .__getattribute__ (self , "_members " )
359- for member in members . values ():
360- member . reset ( )
381+ resolver = object .__getattribute__ (self , "resolver " )
382+ frozen_bytes = object . __getattribute__ ( self , "_frozen_struct_bytes" )
383+ resolver . modify ( len ( frozen_bytes ), 0 , frozen_bytes )
361384
362385 def to_str (self : struct_impl , indent : int = 0 ) -> str :
363386 """Return a string representation of the struct."""
@@ -384,6 +407,8 @@ def __repr__(self: struct_impl) -> str:
384407 }}
385408}}"""
386409
410+ __hash__ = object .__hash__
411+
387412 def __eq__ (self : struct_impl , value : object ) -> bool :
388413 """Return whether the struct is equal to the given value."""
389414 if not isinstance (value , struct_impl ):
0 commit comments