Metadata-Version: 2.4
Name: iqr_device_validator
Version: 0.1.1
Summary: Validator for Mitsubishi MELSEC iQ-R device and constant strings used in GX Works3
Author: mokouliszt
License-Expression: MIT
Project-URL: Homepage, https://github.com/mokouliszt/iqr_device_validator
Project-URL: Repository, https://github.com/mokouliszt/iqr_device_validator
Project-URL: Issues, https://github.com/mokouliszt/iqr_device_validator/issues
Project-URL: Changelog, https://github.com/mokouliszt/iqr_device_validator/blob/main/CHANGELOG.md
Keywords: melsec,iq-r,mitsubishi,mitsubishielectric,plc,gxworks3,validator,device,ladder
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Manufacturing
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Text Processing :: General
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# iqr_device_validator

[![PyPI version](https://img.shields.io/pypi/v/iqr_device_validator.svg)](https://pypi.org/project/iqr-device-validator/)
[![Python versions](https://img.shields.io/pypi/pyversions/iqr_device_validator.svg)](https://pypi.org/project/iqr-device-validator/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Mitsubishi MELSEC iQ-R シリーズ**のデバイス文字列・定数を検証する Python ライブラリです。GX Works3 で使われる表記 (`X0`, `D100.A`, `K4M0`, `J1\W10`, `SA\D100`, `K1234`, `T#1h30m` など) を正規表現＋構文木でパースし、**設定可能範囲の上限**と**修飾子の組み合わせ可否**をチェックします。

> A validator for device & constant strings used in Mitsubishi MELSEC iQ-R PLC programs (GX Works3 syntax). Validates not just the *form* of a device reference, but also whether the device number is within the configurable range and whether modifiers (digit/bit/index/local/indirect) are legal for that device kind.

---

## Features

- **General devices** — `X`, `Y`, `M`, `L`, `B`, `F`, `SB`, `V`, `S`, `T`, `ST`, `LT`, `LST`, `C`, `LC`, `D`, `W`, `SW`, `FX`, `FY`, `FD`, `SM`, `SD`, `Z`, `LZ`, `R`, `ZR`, `RD`, `P`, `I`, `N`
- **Direct access I/O** — `DX`, `DY` (e.g. `DX10`, `DY25`, `K4DX0`)
- **SFC devices** — `BL` (block device), `TR` (transition device), and the in-block step reference form `BL<n>\S<m>` (e.g. `BL0`, `BL10\S100`, `TR5`)
- **Link direct devices** — `J<n>\<dev>` (e.g. `J1\W10`, `J239\K4B0`)
- **Unit access devices** — `U<n>\G<addr>`, `U3E<n>\G<addr>`, `U3E<n>\HG<addr>`
- **Safety devices** — `SA\X`, `SA\Y`, `SA\M`, `SA\B`, `SA\T`, `SA\ST`, `SA\C`, `SA\D`, `SA\W`, `SA\SM`, `SA\SD` with the standard safety-program restrictions enforced (no index modification, no indirect, limited local prefix)
- **All modifiers** — bit (`.A`), digit (`K4`), index (`Z`/`LZ`/`ZZ`), indirect (`@`), local (`#`)
- **Continuity check** for digit specification: `K8X2FF0` is rejected because the 32 bits would extend past `X2FFF`
- **Constants** — `TRUE`/`FALSE`, decimal (`K123`/`123`), hex (`HFF`/`16#FF`), binary (`2#01101010`), octal (`8#377`), real (`E1.234`/`1.0E6`), string (`'ABC'`), wstring (`"ABC"`), time (`T#1d2h3m4s5ms`)
- **String escape sequences** — `$$` (literal `$`), `$'`/`$"` (escaped quotes), `$L`/`$N`/`$T`/`$R`/`$P` (LF/newline/tab/CR/FF, case-insensitive), and `$<HH>` (ASCII hex byte). Decoded `value` reflects the actual bytes, e.g. `'A$LB'` → `"A\nB"`.
- **Yen-mark normalization** — `\`, `¥`, `￥`, `＼` are all accepted as the path separator (common in Japanese docs)
- **No dependencies** — pure standard library, single file

---

## Installation

```bash
pip install iqr_device_validator
```

Requires Python 3.10 or later.

---

## Quick start

```python
from iqr_device_validator import validate

# Devices
print(validate("D100.A").ok)         # True   (D100, bit A specification)
print(validate("X3000").ok)          # False  (out of range; max is X2FFF)
print(validate("K8X2FF0").ok)        # False  (K8 would extend past X2FFF)
print(validate("@D10Z2.A").ok)       # True   (indirect + index + bit)
print(validate("DX10").ok)           # True   (direct access input)
print(validate("DY5E").ok)           # True   (direct access output)
print(validate("@DX0").ok)           # False  (indirect not allowed on direct I/O)
print(validate("BL10\\S100").ok)     # True   (SFC: step 100 of block 10)
print(validate("BL320\\S0").ok)      # False  (block out of range)
print(validate("SA\\D100").ok)       # True   (safety device)
print(validate("@SA\\D0").ok)        # False  (safety devices forbid @)

# Constants
print(validate("K1234").ok)          # True   (decimal constant)
print(validate("HFF").ok)            # True   (hex constant)
print(validate("E1.234").ok)         # True   (real constant)
print(validate("T#1h30m").ok)        # True   (time constant)
print(validate("'ABC'").ok)          # True   (string constant)
print(validate("'A$LB'").value)      # "A\nB" (escape decoded)
print(validate("'$X'").ok)           # False  (invalid escape)
```

---

## API

### `validate(text: str) -> ValidationResult`

The single entry point. Returns a `ValidationResult` dataclass:

```python
@dataclass
class ValidationResult:
    ok: bool                       # True if valid
    category: str                  # "device" | "constant"
    error: str                     # human-readable message when ok=False

    # --- device fields (when category == "device") ---
    kind: str                      # "simple" | "link_direct" | "un_g" | "u3e_g" | "safety"
    prefix: str                    # device prefix, e.g. "D", "J\\W", "SA\\D"
    number: int                    # parsed device number
    bit_no: Optional[int]          # 0..15 for word-bit specs
    digits: Optional[int]          # 1..8 for digit specs
    digit_end_no: Optional[int]    # base + digits*4 - 1
    network: Optional[int]         # network No. for J\
    unit: Optional[int]            # unit No. for U\G
    cpu: Optional[int]             # CPU No. for U3E\G
    area: Optional[str]            # "G" | "HG" for U3E
    indirect: bool                 # @ prefix present
    local: bool                    # # prefix present
    safety: bool                   # SA\ prefix present
    index: Optional[IndexMod]      # index modification, if any

    # --- constant fields (when category == "constant") ---
    const_type: str                # "bool"|"dec"|"hex"|"bin"|"oct"|"real"|"string"|"wstring"|"time"
    value: object                  # parsed value (int/float/bool/str/dict)
```

### Example: lint a list of operands

```python
from iqr_device_validator import validate

operands = ["X0", "D100", "K4M0", "X3000", "Z24", "@FX0", "K1234", "'hello'"]
for op in operands:
    r = validate(op)
    if r.ok:
        print(f"OK   {op:10s}  ({r.category} / {r.kind or r.const_type})")
    else:
        print(f"FAIL {op:10s}  {r.error}")
```

Output:

```
OK   X0          (device / simple)
OK   D100        (device / simple)
OK   K4M0        (device / simple)
FAIL X3000       device number out of range (got X3000, max X2FFF)
FAIL Z24         device number out of range (got Z24, max Z23)
FAIL @FX0        indirect (@) not supported by FX
OK   K1234       (constant / dec)
OK   'hello'     (constant / string)
```

---

## What gets validated

### Device range

Each device's max settable count is taken from the iQ-R reference manual *MELSEC iQ-R CPU User's Manual (sh082487h)*, section "デバイス点数の使用範囲" (P.410). The library uses the **maximum configurable** values (i.e. the upper bound of what GX Works3 will let you set), assuming the largest CPU model with extended SRAM cassette.

| Category | Examples |
|---|---|
| Bit devices | `X0..X2FFF`, `M0..M161882111`, `B0..B9A61FFF`, `L0..L32767`, `S0..S16383` |
| Direct I/O | `DX0..DX2FFF`, `DY0..DY2FFF` (same hex range as X/Y) |
| Word devices | `D0..D10117631`, `W0..W9A61FF`, `R0..R32767`, `RD0..RD1048575` |
| System | `SM0..SM4095`, `SD0..SD4095`, `FX0..FXF`, `FY0..FYF`, `FD0..FD4` |
| SFC | `BL0..BL319` (block), `TR0..TR16383` (transition), `BL<n>\S0..S511` (in-block step) |
| Index | `Z0..Z23`, `LZ0..LZ11` |
| Pointer/Nest | `P0..P32767`, `I0..I1023`, `N0..N14` |
| Safety | `SA\X0..SA\X2FFF`, `SA\M0..SA\M638975`, `SA\D0..SA\D39935`, ... |

### Modifier compatibility

The validator enforces the modifier-applicability matrix from the iQ-R Programming Manual (CPU Instructions / FUN/FB) §1.2 and the Application Manual §35.1 / §39.1.

| Modifier | Example | Allowed on |
|---|---|---|
| Bit spec `.X` | `D100.A` | Word devices: D, W, SW, FD, SD, R, ZR, RD, U\G, U3E\G, J\W, J\SW |
| Digit spec `K<n>` | `K4M0` | Bit devices: X, Y, M, L, B, F, SB, V, S, FX, FY, SM |
| Indirect `@` | `@D0` | T, ST, C, D, W, SW, FD, SD, R, ZR, RD, U\G, J\W, J\SW, U3E\G |
| Local `#` | `#D0` | M, V, T, ST, C, LC, LT, LST, D, Z, LZ |
| Index `Z` | `D0Z2` | Most word/bit devices (not FX, FY, N, Z itself) |
| Index `LZ` / `ZZ` | `D0LZ0` / `D0Z0Z1` | M, B, SB, T, ST, LT, LST, C, LC, D, W, SW, R, ZR, RD |
| Safety `SA\` | `SA\D100` | X, Y, M, B, T, ST, C, D, W, SM, SD |

### Combinational rules

- The prefix order is fixed: `[@] [SA\] [K<n>] [#] <base>` — out-of-order combinations are rejected.
- `@` with bit-spec is allowed (e.g. `@D100.A`) because indirect dereferences to a word.
- Bit-spec and digit-spec cannot both appear on one operand.
- `ZZ` notation requires consecutive registers: `Z3Z4` OK, `Z3Z2` and `Z3Z5` rejected.
- **Safety devices forbid `@` and index modification** (per Application Manual §39.1).
- The continuity check rejects `K<n><dev><base>` where `base + n*4 - 1 > max_no`.

### Constants

| Type | Examples | Range |
|---|---|---|
| Boolean | `TRUE`, `FALSE` | — |
| Decimal | `K123`, `+123`, `-123`, `12_3` | -2,147,483,648 .. 4,294,967,295 |
| Hex | `HFF`, `H1234`, `16#FF` | H0 .. HFFFFFFFF |
| Binary | `2#0110_1010` | (within the integer range) |
| Octal | `8#377` | (within the integer range) |
| Real | `E1.234`, `1.0E6`, `E1.001-6` | IEEE 754 double; flags single-precision fit |
| String | `'ABC'`, `'A$LB'`, `'$41$42'` | up to 255 bytes (CP932) |
| WString | `"ABC"`, `"$22quoted$22"` | up to 255 Unicode characters |
| Time | `T#1d2h3m4s5ms`, `TIME#-31m23s` | -24d20h31m23s648ms .. 24d20h31m23s647ms |

### String escape sequences

Inside a string literal, `$` is the escape character (per iQ-R Application Manual §26.5):

| Sequence | Meaning |
|---|---|
| `$$` | literal `$` |
| `$'` | literal `'` (use inside single-quoted strings) |
| `$"` | literal `"` (use inside double-quoted strings) |
| `$L` / `$l` | line feed (LF, 0x0A) |
| `$N` / `$n` | new line (LF, 0x0A) |
| `$P` / `$p` | form feed / page break (0x0C) |
| `$R` / `$r` | carriage return (0x0D) |
| `$T` / `$t` | horizontal tab (0x09) |
| `$<HH>` | the ASCII character with hex code `HH` (must be ≤ 0x7F) |

The validator decodes escape sequences in `result.value` (e.g. `'A$LB'` produces `value = "A\nB"`) and rejects malformed escapes such as `'$X'`, `'$1G'`, `'$80'` (out of ASCII range), or a trailing lone `$`.

---

## Devices and constructs that are NOT supported

The following are intentionally outside the scope of this validator. Strings using these forms will either be rejected, parsed differently than GX Works3 would, or accepted only because their textual form happens to overlap with a supported construct.

### Devices

| Construct | Example | Reason |
|---|---|---|
| **MELSEC-Q legacy devices** | `Q\X0` | Q-series compatibility prefixes are not common in iQ-R native programs |
| **Network No.0 link direct** | `J0\W0` | The iQ-R spec defines J1..J239 only |
| **Custom user devices via `DEVICE_SPECS` extension** | `MyDev0` | The validator uses a fixed device table; new device kinds require code changes |

### Labels

| Construct | Example | Reason |
|---|---|---|
| **Global / local labels** | `bMyFlag`, `gCounter` | Label resolution requires the project's GX Works3 label table; this validator works on raw device strings only |
| **Unit labels** | `RX1.bModuleReady`, `Q01\stEvent` | Generated by GX Works3 from each unit's FB reference; out of scope |
| **Structure / array members** | `stMotor.iSpeed`, `arrPos[5]`, `arrPos[5,2]` | Requires user-defined type information |
| **System labels** | `iQ Works system label name` | iQ Works integration data is not parsed |

### Constants and operands

| Construct | Example | Reason |
|---|---|---|
| **Constant range matching to instruction operand types** | `MOV K70000 D0` | Whether `K70000` fits a 16-bit signed operand depends on the *instruction* (`MOV` vs `DMOV`); this validator only checks the broadest 32-bit range |
| **Per-CPU model device counts** | R04CPU has different defaults than R120CPU | The validator uses the *maximum configurable* range across all CPU models. A device accepted here may not fit a small CPU's actual allocation |

### Project-aware constraints

The validator is **stateless** — it does not read CPU parameters or label tables. The following checks would require project-aware logic and are NOT performed:

- **User-allocated device range** — whether the device fits within the project's actual `デバイス点数` setting (rather than the maximum configurable count). For example, a project might allocate only 8K points to `M`, so `M10000` would be invalid in that project even though we accept `M0..M161882111`.
- **File register file selection** — `ZR` upper bound depends on which `.QDR` file is loaded and the extended SRAM cassette size.
- **Network No. existence** — `J239\W0` is syntactically valid but only meaningful if network 239 is actually configured.
- **Unit number existence** — `U7F\G100` is syntactically valid but only meaningful if a unit is mounted at I/O `7F0..7FF`.
- **Safety CPU vs. general CPU** — Safety device strings (`SA\...`) are accepted regardless of whether the target is actually a safety CPU model.
- **Read-only / write-only operand correctness** — e.g. `OUT X0` would compile-error in GX Works3 because `X` is read-only, but this validator only checks that `X0` is a valid device reference.

If your use case needs any of these, treat this validator as a fast first-pass screen and combine it with project-aware checks driven by the actual `.gx3` project data.

---

## Limitations

- The `ZR` upper bound is set to `0x7FFFFF` (assuming a 16 MB extended SRAM cassette). If your project uses a smaller file-register allocation, you can override it:

  ```python
  from iqr_device_validator import DEVICE_SPECS, DeviceSpec
  DEVICE_SPECS["ZR"] = DeviceSpec(radix=10, max_no=131071, ...)  # for 256KB
  ```

- For real constants near the boundary, the `fits_single` flag in the result indicates whether the value is representable in single-precision. The library always accepts the value if it fits in double-precision, since the actual instruction width determines which is required.

- The SFC transition device `TR<n>` is accepted syntactically, but per the iQ-R reference manual it is **only usable as a device comment, not as an instruction operand**. The validator does not enforce the comment-only restriction since that requires knowing the surrounding instruction context.

- The default SFC block range (`BL0..BL319`) reflects the maximum for R04CPU and above. R00/R01/R02CPU support only `BL0..BL127`; if you target those models, override `DEVICE_SPECS["BL"].max_no = 127`.

---

## Running tests

```bash
git clone https://github.com/mokouliszt/iqr_device_validator.git
cd iqr_device_validator
python -m pytest
```

The test suite includes 88+ cases covering every device kind, modifier combination, range boundary, and constant type.

---

## References

The behaviour is grounded in the following Mitsubishi reference manuals:

- **MELSEC iQ-R CPU ユニット ユーザーズマニュアル (sh082487h)** — general device list and configurable ranges (§27.1, §27.2)
- **MELSEC iQ-R CPU ユニット ユーザーズマニュアル (応用編)** — safety devices (§35.1, §39.1), constants (§26, §31)
- **MELSEC iQ-R プログラミングマニュアル (CPUユニット用命令/汎用FUN/汎用FB編)** — bit/digit specification syntax, indirect (`@`), index modification

This library is **not** affiliated with or endorsed by Mitsubishi Electric. MELSEC, iQ-R, and GX Works3 are trademarks of Mitsubishi Electric Corporation.

---

## License

MIT License. See [LICENSE](LICENSE) for details.

---

## Contributing

Bug reports and pull requests are welcome on GitHub. If you find a device range that disagrees with your project's setup, please file an issue with:

1. The exact device string that was rejected (or accepted incorrectly)
2. The CPU model and extended SRAM cassette in your project
3. The expected behaviour with a manual reference if possible
