# Tag Structures

Advanced patterns for UDTs, named arrays, and block configuration. For basic tag and UDT usage, see [Core Concepts](../getting-started/concepts.md).

## UDT naming

A singleton UDT (`count=1`) generates compact tag names — no instance number:

```python
@udt()
class Motor:
    running: Bool
    speed: Int

# Tags: Motor_running, Motor_speed
```

With `count > 1`, tags are numbered by instance:

```python
@udt(count=3)
class Pump:
    running: Bool
    flow: Real

# Tags: Pump1_running, Pump1_flow, Pump2_running, ...
```

If you want numbered names even for a singleton, use `always_number=True`:

```python
@udt(count=1, always_number=True)
class Heater:
    on: Bool
    temp: Real

# Tags: Heater1_on, Heater1_temp (not Heater_on)
```

This is useful when your naming convention requires consistency across singletons and counted structures.

## Field options

Plain annotations give you the type default (0 for `Int`, `False` for `Bool`, etc.). `Field` lets you override:

```python
@udt(count=3)
class Alarm:
    id: Int = Field(default=100)
    active: Bool
    message: Char = Field(retentive=True)
```

`retentive=True` means the field survives a STOP→RUN transition. By default, UDT fields inherit the type's retentive policy.

You can also assign a plain literal as a default:

```python
@udt()
class Config:
    mode: Int = 2
    threshold: Real = 75.0
```

### Per-instance sequences with auto()

`auto()` generates a different default for each instance — useful for IDs and addresses:

```python
@udt(count=3)
class Alarm:
    id: Int = auto(start=10, step=5)
    active: Bool

# Alarm[1].id defaults to 10
# Alarm[2].id defaults to 15
# Alarm[3].id defaults to 20
```

`auto()` only works on numeric types: `Int`, `Dint`, `Word`.

## Named arrays

A `@named_array` is a single-type structure where all fields share the same `TagType`. Fields are declared as class attributes (not annotations):

```python
from pyrung import named_array

@named_array(Int, count=4)
class Sensor:
    reading = 0
    setpoint = 100
```

This creates 4 instances, each with an `Int`-typed `reading` and `setpoint`. Access works the same as UDTs:

```python
Sensor[1].reading   # first sensor's reading
Sensor[3].setpoint  # third sensor's setpoint
```

### Selecting whole instances

You can select one or more complete instances as a contiguous `BlockRange`. This works with both dense and sparse layouts — stride is known, so instance boundaries are always well-defined:

```python
@named_array(Int, count=3)
class RecipeProfile:
    mix_seconds = 0
    hold_seconds = 0
    target_temp = 0

RecipeProfile.instance(2)              # one complete profile
RecipeProfile.instance_select(1, 2)    # the first two profiles
```

This is useful with range-based instructions such as `blockcopy()` and `fill()`:

```python
blockcopy(RecipeProfile.instance(2), ds.select(201, 203))
fill(0, RecipeProfile.instance_select(1, 2))
```

For sparse layouts the returned `BlockRange` spans the full stride (including gap slots), while the tag list contains only the named fields.

### Stride

`stride` controls how many hardware slots each instance spans. When stride exceeds the field count, the extra slots are gaps:

```python
@named_array(Int, count=2, stride=4)
class DataPack:
    id = auto()
    value = 0
```

Instance 1 occupies slots 1–4, instance 2 occupies slots 5–8. Only slots 1–2 and 5–6 hold named fields; slots 3–4 and 7–8 are gaps. This matters when mapping to hardware with fixed slot widths.

## Cloning

`.clone()` creates an independent copy of a structure with a new name. Same field layout, fresh tags:

```python
@udt(count=2)
class Motor:
    running: Bool
    speed: Int

Pump = Motor.clone("Pump")           # Same layout, count=2
Fan = Motor.clone("Fan", count=4)    # Same layout, 4 instances
```

For named arrays, you can also override stride:

```python
@named_array(Int, count=2, stride=3)
class Slot:
    id = auto()
    value = 0

WideSlot = Slot.clone("WideSlot", stride=5)
```

Use case: define a template structure once, clone it for each subsystem.

## Mapping to hardware

Named arrays can map their interleaved layout onto a hardware block range with `.map_to()`:

```python
from pyrung.click import ds

@named_array(Int, count=3, stride=2)
class Channel:
    id = auto()
    value = 0

entries = Channel.map_to(ds.select(101, 106))
```

This maps:

- Channel[1].id → DS101, Channel[1].value → DS102
- Channel[2].id → DS103, Channel[2].value → DS104
- Channel[3].id → DS105, Channel[3].value → DS106

The target range must have exactly `count * stride` addresses. Each instance claims `stride` consecutive slots, with fields filling from the front and gaps (if any) at the end.

For UDTs, use `TagMap` to map individual field blocks — see the [Click Dialect](../dialects/click.md) guide.

## Block configuration

Blocks support per-slot overrides for name, retentive policy, default value, and comment. All configuration must happen **before** you index the slot (before tag materialization). The unified `slot()` method handles inspection, configuration, and reset.

### Configuring slots

```python
ds = Block("DS", TagType.INT, 1, 100)

# Name a slot
ds.slot(1, name="SpeedCommand")
ds.slot(2, name="SpeedFeedback")

ds[1].name   # "SpeedCommand"
ds[2].name   # "SpeedFeedback"
ds[3].name   # "DS3" (default)

# Override retentive policy and default
ds.slot(10, retentive=True, default=999)

# Configure a range (retentive and default only)
ds.slot(20, 30, retentive=True)
```

Range configuration applies to all valid addresses in the inclusive window. For sparse blocks, it only affects addresses within the block's valid ranges.

### default_factory

Set a function that computes defaults by address when creating a block:

```python
ds = Block("DS", TagType.INT, 1, 10, default_factory=lambda addr: addr * 10)

ds[1].default   # 10
ds[5].default   # 50
```

Per-slot overrides from `slot()` take precedence over `default_factory`.

### Inspecting slots

`slot()` without configuration kwargs returns a live `SlotView` — no tag materialization:

```python
sv = ds.slot(10)

sv.name                 # Effective tag name
sv.retentive            # Effective retentive policy
sv.default              # Effective default value
sv.name_overridden      # True if name was overridden
sv.retentive_overridden # True if retentive was overridden
sv.default_overridden   # True if default was overridden
```

The `*_overridden` flags tell you whether a value comes from an explicit override or from the block's inherited defaults.

### Resetting overrides

```python
ds.slot(10).reset()        # Clear all overrides for slot 10
ds.slot(20, 30).reset()    # Clear all overrides for range 20–30
```
