nadata 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. nadata/__init__.py +15 -0
  2. nadata/api.py +894 -0
  3. nadata/cli.py +445 -0
  4. nadata/console.py +486 -0
  5. nadata/core/__init__.py +0 -0
  6. nadata/core/database/__init__.py +38 -0
  7. nadata/core/database/builtins.py +87 -0
  8. nadata/core/database/database.py +958 -0
  9. nadata/core/database/dumper.py +285 -0
  10. nadata/core/database/entity_id.py +142 -0
  11. nadata/core/database/instance.py +706 -0
  12. nadata/core/database/network_index.py +1142 -0
  13. nadata/core/database/page_manager.py +175 -0
  14. nadata/core/database/persistence/__init__.py +28 -0
  15. nadata/core/database/persistence/binary_cells.py +1141 -0
  16. nadata/core/database/persistence/compaction.py +97 -0
  17. nadata/core/database/persistence/dumper.py +798 -0
  18. nadata/core/database/persistence/lazy_instances.py +1537 -0
  19. nadata/core/database/persistence/loader.py +424 -0
  20. nadata/core/database/persistence/record_store.py +228 -0
  21. nadata/core/database/persistence/serialization.py +605 -0
  22. nadata/core/database/query_engine.py +222 -0
  23. nadata/core/database/queryset.py +508 -0
  24. nadata/core/database/reconciliation.py +287 -0
  25. nadata/core/database/slotted_page.py +190 -0
  26. nadata/core/database/spatial_index.py +1080 -0
  27. nadata/core/expressions/__init__.py +44 -0
  28. nadata/core/expressions/autograd.py +2909 -0
  29. nadata/core/expressions/builtins.py +4673 -0
  30. nadata/core/expressions/expression.py +2658 -0
  31. nadata/core/expressions/io_builtins.py +972 -0
  32. nadata/core/expressions/scope.py +336 -0
  33. nadata/core/expressions/statement.py +953 -0
  34. nadata/core/struct/__init__.py +14 -0
  35. nadata/core/struct/struct.py +218 -0
  36. nadata/core/struct/struct_attribute.py +100 -0
  37. nadata/core/struct/struct_method.py +324 -0
  38. nadata/core/types/__init__.py +33 -0
  39. nadata/core/types/bits/__init__.py +0 -0
  40. nadata/core/types/bits/bits.py +686 -0
  41. nadata/core/types/home_types/__init__.py +19 -0
  42. nadata/core/types/home_types/color.py +102 -0
  43. nadata/core/types/home_types/coordinate.py +1008 -0
  44. nadata/core/types/home_types/datetime.py +362 -0
  45. nadata/core/types/home_types/geogrid.py +92 -0
  46. nadata/core/types/home_types/geometry.py +1223 -0
  47. nadata/core/types/home_types/graph.py +0 -0
  48. nadata/core/types/home_types/planet.py +359 -0
  49. nadata/core/types/home_types/sea_level.py +98 -0
  50. nadata/core/types/home_types/vec.py +0 -0
  51. nadata/core/types/types.py +1345 -0
  52. nadata/core/values/__init__.py +14 -0
  53. nadata/core/values/values.py +1568 -0
  54. nadata/parser/__init__.py +0 -0
  55. nadata/parser/expressions/__init__.py +6 -0
  56. nadata/parser/expressions/dependencies.py +318 -0
  57. nadata/parser/expressions/expression_parser.py +993 -0
  58. nadata/parser/expressions/splitter.py +98 -0
  59. nadata/parser/expressions/statement_parser.py +1425 -0
  60. nadata/parser/expressions/tokenizer.py +402 -0
  61. nadata/parser/expressions/type_parser.py +187 -0
  62. nadata/parser/nadata_loader/__init__.py +8 -0
  63. nadata/parser/nadata_loader/loader.py +951 -0
  64. nadata/parser/nastruct_parser/__init__.py +0 -0
  65. nadata/parser/nastruct_parser/declaration.py +105 -0
  66. nadata/parser/nastruct_parser/imports.py +164 -0
  67. nadata/parser/nastruct_parser/nastruct_parser.py +209 -0
  68. nadata/parser/nastruct_parser/parse_attributes.py +149 -0
  69. nadata/parser/nastruct_parser/parse_callables.py +436 -0
  70. nadata/parser/nastruct_parser/parse_expression.py +59 -0
  71. nadata/parser/nastruct_parser/parse_structs.py +206 -0
  72. nadata/parser/nastruct_parser/parser_state.py +84 -0
  73. nadata/parser/nastruct_parser/resolver.py +1412 -0
  74. nadata/parser/nastruct_parser/struct_body_state.py +50 -0
  75. nadata/parser/nastruct_parser/tmp_structs.py +156 -0
  76. nadata/parser/utils/__init__.py +20 -0
  77. nadata/parser/utils/brackets.py +252 -0
  78. nadata/parser/utils/comments_remove.py +149 -0
  79. nadata/parser/utils/identifiers.py +121 -0
  80. nadata/session.py +868 -0
  81. nadata-0.1.0.dist-info/METADATA +191 -0
  82. nadata-0.1.0.dist-info/RECORD +86 -0
  83. nadata-0.1.0.dist-info/WHEEL +5 -0
  84. nadata-0.1.0.dist-info/entry_points.txt +2 -0
  85. nadata-0.1.0.dist-info/licenses/LICENSE +21 -0
  86. nadata-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,706 @@
1
+ """
2
+ StructInstance runtime object for Nadata.
3
+
4
+ Represents an active instance of a Struct schema with typed attributes,
5
+ a globally unique EntityId, and bound method execution.
6
+ """
7
+
8
+ # Import Modules
9
+ from __future__ import annotations
10
+ from collections import deque
11
+ from typing import Any, Optional
12
+
13
+ from nadata.core.expressions.builtins import (
14
+ TensorInit,
15
+ build_type_globals,
16
+ materialize_tensor_init,
17
+ )
18
+ from nadata.core.expressions.expression import Expression
19
+ from nadata.core.expressions.scope import ExecutionScope
20
+ from nadata.core.types.types import (
21
+ TypeInt,
22
+ TypePriorityQueue,
23
+ TypeQueue,
24
+ TypeTensor,
25
+ flatten_nested_lists,
26
+ )
27
+ from nadata.core.values.values import (
28
+ Value,
29
+ ValueArray,
30
+ ValuePriorityQueue,
31
+ ValueQueue,
32
+ ValueTensor,
33
+ make_value,
34
+ )
35
+
36
+ from nadata.core.values.values import Value, make_value
37
+ from nadata.core.struct.struct import Struct
38
+ from nadata.core.struct.struct_attribute import StructAttribute
39
+ from nadata.core.database.entity_id import EntityId
40
+
41
+
42
+ class StructInstance:
43
+ """
44
+ Runtime instance of a Struct with attributes and bound methods.
45
+
46
+ Attributes:
47
+ struct (Struct): Schema struct definition.
48
+ entity_id (EntityId): Globally unique 64-bit entity handle.
49
+ attributes (dict[str, Value]): Stored attribute Values.
50
+ dirty (bool): True when the instance differs from its stored
51
+ (or not-yet-stored) state; set by mutation, cleared by the
52
+ flush write-back. New instances start dirty.
53
+ pinned (bool): True when explicitly kept resident; pinned
54
+ instances are never evicted from the decode cache.
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ struct: Struct,
60
+ entity_id: EntityId,
61
+ attributes: Optional[dict[str, Value]] = None
62
+ ) -> None:
63
+ """
64
+ Initializes the StructInstance.
65
+
66
+ Args:
67
+ struct (Struct): Struct schema definition.
68
+ entity_id (EntityId): Globally unique 64-bit entity handle.
69
+ attributes (Optional[dict[str, Value]], optional): Attribute values. Defaults to None.
70
+ """
71
+
72
+ self.struct: Struct = struct
73
+ self.entity_id: EntityId = entity_id
74
+ self.attributes: dict[str, Value] = attributes if attributes is not None else {}
75
+ self.dirty: bool = True
76
+ self.pinned: bool = False
77
+ self.nid: Optional[str] = None
78
+
79
+ # Owning table (set by StructTable.insert); maintained hash
80
+ # indexes update through it on writes
81
+ self.table_ref: Optional[Any] = None
82
+
83
+ def get_attribute(self, name: str) -> Value:
84
+ """
85
+ Retrieves an attribute Value by name.
86
+
87
+ Args:
88
+ name (str): Attribute name.
89
+
90
+ Returns:
91
+ Value: Attribute Value.
92
+
93
+ Raises:
94
+ AttributeError: If attribute is not found.
95
+ """
96
+
97
+ if name in self.attributes:
98
+ return self.attributes[name]
99
+
100
+ # Check default value in struct schema definition
101
+ attr_def = self.struct.get_attribute(name=name)
102
+
103
+ if attr_def is not None and attr_def.has_default:
104
+
105
+ # Expression-backed defaults (tensor initializers, derived
106
+ # consts) materialize through their expression. Tensor results
107
+ # are cached so identity stays stable across reads: autograd
108
+ # flags, gradients, and version counters live on the tensor
109
+ # object itself.
110
+ if attr_def.default_expr is not None:
111
+
112
+ value: Value = self._evaluate_derived_default(
113
+ attr_def=attr_def
114
+ )
115
+
116
+ if isinstance(attr_def.attr_type, TypeTensor):
117
+ self.attributes[name] = value
118
+
119
+ return value
120
+
121
+ if isinstance(attr_def.attr_type, TypeTensor):
122
+
123
+ self.attributes[name] = make_value(
124
+ value_type=attr_def.attr_type,
125
+ raw=attr_def.default_value
126
+ )
127
+
128
+ return self.attributes[name]
129
+
130
+ return make_value(
131
+ value_type=attr_def.attr_type,
132
+ raw=attr_def.default_value
133
+ )
134
+
135
+ raise AttributeError(f"Instance of '{self.struct.name}' has no attribute '{name}'")
136
+
137
+ def ensure_attribute(self, name: str) -> None:
138
+ """
139
+ Materializes a lazily-defaulted attribute onto this instance.
140
+
141
+ Copies container defaults (lists, dicts) so mutation through an
142
+ assignment target never aliases the schema's shared default value.
143
+
144
+ Args:
145
+ name (str): Attribute name.
146
+ """
147
+
148
+ if name in self.attributes:
149
+ return None
150
+
151
+ attr_def: Any = self.struct.get_attribute(name=name)
152
+
153
+ if attr_def is None:
154
+ return None
155
+
156
+ # Derived constants evaluate their expression against the bound
157
+ # constants (used when creation paths were bypassed)
158
+ if attr_def.default_expr is not None:
159
+ self.attributes[name] = self._evaluate_derived_default(
160
+ attr_def=attr_def
161
+ )
162
+
163
+ return None
164
+
165
+ if not attr_def.has_default:
166
+ return None
167
+
168
+ raw_default: Any = attr_def.default_value
169
+
170
+ # Copy container defaults to avoid mutating the schema's shared value
171
+ if isinstance(raw_default, (list, dict, set, deque)):
172
+ raw_default = raw_default.copy()
173
+
174
+ self.attributes[name] = make_value(
175
+ value_type=attr_def.attr_type,
176
+ raw=raw_default,
177
+ )
178
+
179
+ # Stamp resolved extents onto symbolic tensor defaults when every
180
+ # constant has a schema default (otherwise extents stay unbound)
181
+ stamped: Value = self.attributes[name]
182
+
183
+ if (
184
+ isinstance(attr_def.attr_type, TypeTensor)
185
+ and attr_def.attr_type.is_symbolic
186
+ and isinstance(stamped, ValueTensor)
187
+ ):
188
+ self._stamp_default_dims(
189
+ tensor_type=attr_def.attr_type,
190
+ tensor_value=stamped,
191
+ )
192
+
193
+ def _stamp_default_dims(self, tensor_type: TypeTensor, tensor_value: ValueTensor) -> None:
194
+ """
195
+ Stamps concrete extents onto a symbolic tensor default value.
196
+
197
+ Extents resolve from the const attributes' schema defaults; values
198
+ already bound on the instance take precedence.
199
+
200
+ Args:
201
+ tensor_type (TypeTensor): Declared tensor type.
202
+ tensor_value (ValueTensor): The freshly materialized value.
203
+ """
204
+
205
+ extents: dict[str, int] = {}
206
+
207
+ for dim_name in tensor_type.symbolic_dims():
208
+ dim_attr: Optional[StructAttribute] = self.struct.get_attribute(
209
+ name=dim_name
210
+ )
211
+
212
+ if dim_attr is None or not dim_attr.has_default:
213
+ return
214
+
215
+ self.ensure_attribute(name=dim_name)
216
+
217
+ bound_value: Optional[Value] = self.attributes.get(dim_name)
218
+
219
+ if bound_value is None:
220
+ return
221
+
222
+ raw_dim: Any = bound_value.get()
223
+
224
+ if isinstance(raw_dim, bool) or not isinstance(raw_dim, int):
225
+ return
226
+
227
+ extents[dim_name] = raw_dim
228
+
229
+ tensor_value.concrete_dims = [
230
+ dim if isinstance(dim, int) else extents[dim]
231
+ for dim in tensor_type.dims
232
+ ]
233
+
234
+ def set_attribute(self, name: str, value: Value, allow_const: bool = False) -> None:
235
+ """
236
+ Sets an attribute Value by name after verifying schema constraints.
237
+
238
+ Args:
239
+ name (str): Attribute name.
240
+ value (Value): Value to store.
241
+ allow_const (bool): Whether constant attributes may be written
242
+ (creation-time binding only). Defaults to False.
243
+
244
+ Raises:
245
+ AttributeError: If attribute is not declared in struct.
246
+ TypeError: If value violates declared attribute type, the
247
+ attribute is immutable, or a symbolic tensor shape does not
248
+ match the instance's bound constants.
249
+ """
250
+
251
+ attr_def = self.struct.get_attribute(name=name)
252
+
253
+ if attr_def is None:
254
+ raise AttributeError(
255
+ f"Cannot set undeclared attribute '{name}' on Struct '{self.struct.name}'"
256
+ )
257
+
258
+ # Immutable after construction: binding happens in the constructor
259
+ if attr_def.is_const and not allow_const:
260
+ raise TypeError(
261
+ f"Cannot assign to immutable attribute '{name}' on Struct "
262
+ f"'{self.struct.name}': const attributes bind at construction"
263
+ )
264
+
265
+ # Pending tensor initializers (zeros()/fill()/eye()) materialize
266
+ # against the declared type, resolving symbolic dims from the
267
+ # instance's bound constants
268
+ if isinstance(attr_def.attr_type, TypeTensor) and isinstance(value, TensorInit):
269
+ value = self._materialize_attribute_init(
270
+ tensor_type=attr_def.attr_type,
271
+ init=value,
272
+ attribute_name=name,
273
+ )
274
+
275
+ # Flatten nested array literals into the flat row-major payload of
276
+ # a tensor declaration (shape checked by the type below)
277
+ if isinstance(attr_def.attr_type, TypeTensor) and isinstance(value, ValueArray):
278
+ value = make_value(
279
+ value_type=attr_def.attr_type,
280
+ raw=flatten_nested_lists(raw=value.get()),
281
+ )
282
+
283
+ # Validate type
284
+ attr_def.attr_type.check_value(value=value.get())
285
+
286
+ # Symbolic tensor shapes validate against the instance's constants
287
+ if (
288
+ isinstance(attr_def.attr_type, TypeTensor)
289
+ and attr_def.attr_type.is_symbolic
290
+ ):
291
+ self._validate_symbolic_shape(
292
+ tensor_type=attr_def.attr_type,
293
+ value=value,
294
+ attribute_name=name,
295
+ )
296
+
297
+ # Re-wrap raw payloads passed under a queue declaration (e.g. a
298
+ # plain array literal in a constructor call)
299
+ if isinstance(attr_def.attr_type, (TypeQueue, TypePriorityQueue)) and not isinstance(
300
+ value, (ValueQueue, ValuePriorityQueue)
301
+ ):
302
+ value = make_value(value_type=attr_def.attr_type, raw=value.get())
303
+
304
+ # Capture the previously indexed value before overwriting
305
+ tracks_index: bool = (
306
+ self.table_ref is not None and name in self.table_ref.indexes
307
+ )
308
+
309
+ old_indexed: Optional[Any] = None
310
+
311
+ if tracks_index and name in self.attributes:
312
+ old_indexed = self.attributes[name].get()
313
+
314
+ # Spatial indexes re-index on any write to their attribute
315
+ tracks_spatial: bool = (
316
+ self.table_ref is not None
317
+ and name in self.table_ref.spatial_indexes
318
+ )
319
+
320
+ self.attributes[name] = value
321
+ self.dirty = True
322
+
323
+ # Keep hash indexes consistent with the write
324
+ if tracks_index and self.table_ref is not None:
325
+ self.table_ref.index_update(
326
+ attribute=name,
327
+ old_raw=old_indexed,
328
+ new_raw=value.get(),
329
+ local_id=self.entity_id.local_id,
330
+ )
331
+
332
+ # Keep spatial indexes consistent with the write
333
+ if tracks_spatial and self.table_ref is not None:
334
+ self.table_ref.spatial_index_update(
335
+ attribute=name,
336
+ local_id=self.entity_id.local_id,
337
+ )
338
+
339
+ # Keep routing networks consistent with the write
340
+ if self.table_ref is not None and self.table_ref.networks:
341
+ for network in self.table_ref.networks:
342
+ network.on_update(instance=self)
343
+
344
+ def apply_upsert(self, values: dict[str, Value]) -> None:
345
+ """
346
+ Re-binds creation attributes onto an existing (nid'd) instance.
347
+
348
+ Partial update semantics: passed attributes are re-written
349
+ (constants included — upsert is re-construction); attributes
350
+ not passed keep their current value. Derived constants cannot
351
+ be set directly, matching creation semantics.
352
+
353
+ Args:
354
+ values (dict[str, Value]): Attribute values by name.
355
+
356
+ Raises:
357
+ AttributeError: If a name is not declared in the schema.
358
+ TypeError: If a derived constant is passed directly.
359
+ """
360
+
361
+ for name, value in values.items():
362
+
363
+ attr_def: Optional[StructAttribute] = self.struct.get_attribute(
364
+ name=name
365
+ )
366
+
367
+ if attr_def is None:
368
+ raise AttributeError(
369
+ f"Cannot set undeclared attribute '{name}' on Struct "
370
+ f"'{self.struct.name}'"
371
+ )
372
+
373
+ if attr_def.is_const and attr_def.default_expr is not None:
374
+ raise TypeError(
375
+ f"Cannot set '{name}' directly: it is "
376
+ f"derived from other constants"
377
+ )
378
+
379
+ self.set_attribute(name=name, value=value, allow_const=True)
380
+
381
+ def set_creation_attributes(self, values: dict[str, Value]) -> None:
382
+ """
383
+ Binds construction-time attributes, constants first.
384
+
385
+ Constants are written before everything else so symbolic tensor
386
+ shapes validate against already-bound extents regardless of the
387
+ order the arguments appear in the constructor call. Derived
388
+ constants (expression defaults referencing `self.<const>`) are
389
+ evaluated after the plain constants, in declaration order.
390
+
391
+ Args:
392
+ values (dict[str, Value]): Attribute values by name.
393
+ """
394
+
395
+ # Constants first (they seed the symbolic tensor shapes)
396
+ for const_name, const_value in values.items():
397
+ attr_def = self.struct.get_attribute(name=const_name)
398
+
399
+ if attr_def is not None and attr_def.is_const:
400
+ self.set_attribute(
401
+ name=const_name, value=const_value, allow_const=True
402
+ )
403
+
404
+ # Derived constants next (in declaration order; each may reference
405
+ # the constants bound above or earlier-derived ones)
406
+ for attr_def in self.struct.get_all_attributes().values():
407
+
408
+ if attr_def.is_const and attr_def.default_expr is not None:
409
+
410
+ if attr_def.name in values:
411
+ raise TypeError(
412
+ f"Cannot set '{attr_def.name}' directly: it is "
413
+ f"derived from other constants"
414
+ )
415
+
416
+ self.set_attribute(
417
+ name=attr_def.name,
418
+ value=self._evaluate_derived_default(attr_def=attr_def),
419
+ allow_const=True,
420
+ )
421
+
422
+ # Everything else (unknown names raise in set_attribute)
423
+ for other_name, other_value in values.items():
424
+ attr_def = self.struct.get_attribute(name=other_name)
425
+
426
+ if attr_def is None or not attr_def.is_const:
427
+ self.set_attribute(name=other_name, value=other_value)
428
+
429
+ def _evaluate_derived_default(self, attr_def: StructAttribute) -> Value:
430
+ """
431
+ Evaluates a derived const's expression against the bound constants.
432
+
433
+ The expression evaluates in a fresh scope exposing `self` and every
434
+ currently bound attribute value by name (so both `self.Dim / 2`
435
+ and bare `Dim / 2` forms work).
436
+
437
+ Args:
438
+ attr_def (StructAttribute): The derived const definition.
439
+
440
+ Returns:
441
+ Value: The evaluated value.
442
+
443
+ Raises:
444
+ TypeError: If a referenced constant is not yet bound.
445
+ """
446
+
447
+ parameters: dict[str, Value] = {}
448
+
449
+ for name, bound_value in self.attributes.items():
450
+ attr: Optional[StructAttribute] = self.struct.get_attribute(name=name)
451
+
452
+ if attr is not None and attr.is_const:
453
+ parameters[name] = bound_value
454
+
455
+ scope: ExecutionScope = ExecutionScope(
456
+ self_instance=self,
457
+ parameters=parameters,
458
+ globals_dict=build_type_globals(),
459
+ )
460
+
461
+ expression: Optional[Expression] = attr_def.default_expr
462
+ assert expression is not None
463
+
464
+ value: Value = expression.evaluate(scope=scope)
465
+
466
+ # Pending tensor initializers materialize against the declared
467
+ # type (symbolic dims resolve from the bound constants)
468
+ if isinstance(attr_def.attr_type, TypeTensor) and isinstance(value, TensorInit):
469
+ return self._materialize_attribute_init(
470
+ tensor_type=attr_def.attr_type,
471
+ init=value,
472
+ attribute_name=attr_def.name,
473
+ )
474
+
475
+ # Python's '/' yields floats; an integral float binding to a basic
476
+ # integer const coerces (2.0 -> 2). Non-integral values still fail
477
+ # the declared type check in set_attribute.
478
+ raw_value: Any = value.get()
479
+
480
+ if (
481
+ isinstance(attr_def.attr_type, TypeInt)
482
+ and isinstance(raw_value, float)
483
+ and raw_value.is_integer()
484
+ ):
485
+ return make_value(value_type=attr_def.attr_type, raw=int(raw_value))
486
+
487
+ return value
488
+
489
+ def _materialize_attribute_init(
490
+ self,
491
+ tensor_type: TypeTensor,
492
+ init: TensorInit,
493
+ attribute_name: str,
494
+ ) -> ValueTensor:
495
+ """
496
+ Materializes a pending tensor initializer for an attribute.
497
+
498
+ Symbolic dimensions resolve from the instance's bound constants
499
+ (schema defaults are materialized first).
500
+
501
+ Args:
502
+ tensor_type (TypeTensor): Declared tensor type.
503
+ init (TensorInit): Pending initialization request.
504
+ attribute_name (str): Attribute being set (for errors).
505
+
506
+ Returns:
507
+ ValueTensor: The initialized tensor.
508
+
509
+ Raises:
510
+ TypeError: If a symbolic dimension is not bound on this
511
+ instance.
512
+ """
513
+
514
+ extents: dict[str, int] = {}
515
+
516
+ for dim_name in tensor_type.symbolic_dims():
517
+
518
+ # Materialize schema defaults of unbound constants
519
+ self.ensure_attribute(name=dim_name)
520
+
521
+ dim_value: Optional[Value] = self.attributes.get(dim_name)
522
+
523
+ if dim_value is None:
524
+ raise TypeError(
525
+ f"Cannot initialize '{attribute_name}': tensor dimension "
526
+ f"'{dim_name}' is not bound on this instance"
527
+ )
528
+
529
+ raw_dim: Any = dim_value.get()
530
+
531
+ if isinstance(raw_dim, bool) or not isinstance(raw_dim, int):
532
+ raise TypeError(
533
+ f"Cannot initialize '{attribute_name}': tensor dimension "
534
+ f"'{dim_name}' must be an integer"
535
+ )
536
+
537
+ extents[dim_name] = raw_dim
538
+
539
+ return materialize_tensor_init(
540
+ init=init,
541
+ tensor_type=tensor_type,
542
+ extents=extents or None,
543
+ )
544
+
545
+ def _validate_symbolic_shape(
546
+ self,
547
+ tensor_type: TypeTensor,
548
+ value: Value,
549
+ attribute_name: str,
550
+ ) -> None:
551
+ """
552
+ Validates a symbolic tensor payload against the instance constants.
553
+
554
+ Every symbolic dimension must be bound on this instance (explicitly
555
+ or through its schema default); the product of the resolved extents
556
+ must equal the payload length. On success the resolved shape is
557
+ stamped onto the value for shape() introspection.
558
+
559
+ Args:
560
+ tensor_type (TypeTensor): Declared tensor type.
561
+ value (Value): The (flat) tensor value being stored.
562
+ attribute_name (str): Attribute being set (for error messages).
563
+
564
+ Raises:
565
+ TypeError: If a constant is unbound, non-integer, or the shape
566
+ mismatches the payload length.
567
+ """
568
+
569
+ extents: dict[str, int] = {}
570
+
571
+ for dim_name in tensor_type.symbolic_dims():
572
+
573
+ # Materialize schema defaults of unbound constants
574
+ self.ensure_attribute(name=dim_name)
575
+
576
+ dim_value: Optional[Value] = self.attributes.get(dim_name)
577
+
578
+ if dim_value is None:
579
+ raise TypeError(
580
+ f"Cannot set '{attribute_name}': tensor dimension "
581
+ f"'{dim_name}' is not bound on this instance"
582
+ )
583
+
584
+ raw_dim: Any = dim_value.get()
585
+
586
+ if isinstance(raw_dim, bool) or not isinstance(raw_dim, int):
587
+ raise TypeError(
588
+ f"Cannot set '{attribute_name}': tensor dimension "
589
+ f"'{dim_name}' must be an integer, got "
590
+ f"{type(raw_dim).__name__}"
591
+ )
592
+
593
+ extents[dim_name] = raw_dim
594
+
595
+ expected: int = 1
596
+
597
+ for dim in tensor_type.dims:
598
+
599
+ if isinstance(dim, str):
600
+ expected *= extents[dim]
601
+
602
+ else:
603
+ expected *= dim
604
+
605
+ payload: list[Any] = value.get()
606
+
607
+ if len(payload) != expected:
608
+ raise TypeError(
609
+ f"Cannot set '{attribute_name}': shape "
610
+ f"{tensor_type.dims} with bound dims {extents} requires "
611
+ f"{expected} elements, got {len(payload)}"
612
+ )
613
+
614
+ # Stamp the resolved shape onto the value (used by shape())
615
+ if isinstance(value, ValueTensor):
616
+ value.concrete_dims = [
617
+ dim if isinstance(dim, int) else extents[dim]
618
+ for dim in tensor_type.dims
619
+ ]
620
+
621
+ def call_method(
622
+ self,
623
+ name: str,
624
+ args: Optional[list[Any]] = None,
625
+ kwargs: Optional[dict[str, Any]] = None,
626
+ globals_dict: Optional[dict[str, Any]] = None
627
+ ) -> Optional[Value]:
628
+ """
629
+ Invokes a struct method bound to this instance.
630
+
631
+ Args:
632
+ name (str): Method name.
633
+ args (Optional[list[Any]], optional): Positional arguments. Defaults to None.
634
+ kwargs (Optional[dict[str, Any]], optional): Keyword arguments. Defaults to None.
635
+ globals_dict (Optional[dict[str, Any]], optional): Global scope. Defaults to None.
636
+
637
+ Returns:
638
+ Optional[Value]: Method execution return Value.
639
+
640
+ Raises:
641
+ AttributeError: If method is not declared on struct.
642
+ """
643
+
644
+ method = self.struct.get_method(name=name)
645
+
646
+ if method is None:
647
+ raise AttributeError(
648
+ f"Struct '{self.struct.name}' has no method '@{name}'"
649
+ )
650
+
651
+ resolved_args: list[Any] = args if args is not None else []
652
+ resolved_kwargs: dict[str, Any] = kwargs if kwargs is not None else {}
653
+ resolved_globals: dict[str, Any] = globals_dict if globals_dict is not None else {}
654
+
655
+ return method.execute(
656
+ self_instance=self,
657
+ args=resolved_args,
658
+ kwargs=resolved_kwargs,
659
+ globals_dict=resolved_globals
660
+ )
661
+
662
+
663
+ def __getattr__(self, name: str) -> Any:
664
+ """
665
+ Dynamic fallback to access attributes or bind methods directly.
666
+
667
+ Args:
668
+ name (str): Member name.
669
+
670
+ Returns:
671
+ Any: Value or callable bound method.
672
+
673
+ Raises:
674
+ AttributeError: If member is not found.
675
+ """
676
+
677
+ if name in self.attributes:
678
+ return self.attributes[name]
679
+
680
+ # Check struct methods for direct python invocation
681
+ method = self.struct.get_method(name=name)
682
+
683
+ if method is not None:
684
+ return lambda *args, **kwargs: self.call_method(
685
+ name=name,
686
+ args=list(args),
687
+ kwargs=kwargs
688
+ )
689
+
690
+ # Check default attribute value
691
+ attr_def = self.struct.get_attribute(name=name)
692
+
693
+ if attr_def is not None and attr_def.has_default:
694
+ return self.get_attribute(name=name)
695
+
696
+ raise AttributeError(f"'{self.struct.name}' instance has no member '{name}'")
697
+
698
+ def __repr__(self) -> str:
699
+ """
700
+ Returns string representation of StructInstance.
701
+
702
+ Returns:
703
+ str: Struct name with entity_id.
704
+ """
705
+
706
+ return f"<{self.struct.name} id={self.entity_id.local_id}>"