#!/usr/bin/env python3 """Decode engine-visible channel state words into named fields, and diff captures. During GR bring-up the answer often hides in a handful of 32-bit words inside the INSTANCE block, the RAMFC chunk and context-buffer headers: one bit turns "nothing happens" into an engine that runs or refuses. Raw hex hides that; this tool turns a captured dump into a named field table and makes two captures differ field-by-field. Schemas: * ``greeneye-state-decode/1`` -- the decoded report produced by ``decode``. * ``greeneye-state-diff/1`` -- the word/field diff produced by ``diff``. * ``greeneye-server-snapshot/1`` -- an input snapshot emitted elsewhere:: {"schema": "greeneye-server-snapshot/1", "source": "...", "chunks": [{"label": "INSTANCE", "base": "0x0", "words": ["0x...", "..."]}]} (one hex string or integer per 32-bit word; accepted exactly). * ``greeneye-state-map/1`` -- an optional field map (``--map``), see below. Input formats (auto-detected per ``--input``): (a) text lines ``LABEL[OFFSET]=VALUE`` -- label case-insensitive, offset and value hex (``0x..``, letters, or leading-zero padded) or decimal; every other line is ignored; (b) a dump row with a bracketed chunk offset and words, e.g. ``... INSTANCE[0040]: 00100000 00030000 ...`` -- this is the shape live ladder traces print (8 words per row; any count accepted); (c) plain hexdump rows ``00000000: 000002d2 00000000 ...`` named with ``--label``/``--base``; (d) raw little-endian u32 words (``--bin``, or auto-detected when a text parse finds nothing and the bytes are control-heavy and 4-byte aligned); (e) the snapshot JSON above; (f) aggregate state groups -- one ``GROUP: key=value ...`` group per line, e.g. ``stage: primed=1 kicked=1 tx_seq_num=106``; the token right after the group colon must itself be a ``key=value`` token, and a line may carry several ``|``-separated groups (``rm_call_userd: userd=sysmem (host-writable) | rm_call_pdb: vaBase=0x100000 ...``). Each group becomes a chunk named ``PHYS_`` whose fields are the kv pairs: values are kept as integers when the module's number convention parses them (``0x..``, bare hex letters, leading-zero hex, decimal, signed), everything else stays a raw string (``variant=V6``, ``userd=sysmem``), and each group's raw text is preserved on the chunk. Aggregate chunks carry no words, so the map does not apply to them; the diff compares their kvs and classifies every row as ``added`` / ``removed`` / ``changed``. Offsets inside a chunk are byte offsets relative to the chunk start (all inputs above are u32 word dumps, so offsets are multiples of 4). ``base`` is the engine-visible address of chunk-relative offset 0: it is recorded as provenance and for the diff, but it does not shift map lookup. Additive evolution (2026-10, aggregate inputs; no key renamed or removed): aggregate chunks add ``group`` / ``kv_count`` / ``raw`` chunk keys, kv fields add ``kind: "kv"`` / ``index`` (duplicate-key occurrence) / ``numeric``, diff chunks and ``summary`` add ``fields_added`` / ``fields_removed`` / ``fields_changed``, and every ``changed_fields`` row now carries ``status`` (``added`` / ``removed`` / ``changed``). Bundled map (deliberately small; extend or override it from a map file, never by guessing here). Provenance is live bring-up observation (2026-10 GSP ladder runs): * ``INSTANCE`` 0x010: magic ``0xface``. * ``INSTANCE`` 0x048: GPFIFO VA, observed as ``0x100000``. * ``RAMFC`` 0x048: ``RAMFC_TARGET`` -- bits 16/17 ``ENG_CTX_VALID`` / ``CE_CTX_VALID``; the observed live success word ``0x00030000`` sets both. The bit assignment is observation-driven and fully configurable via the map. Map file:: {"format": "greeneye-state-map/1", "mode": "extend" | "replace", "entries": [ {"chunk": "RAMFC", "label": "RAMFC_TARGET", "offset": "0x48", "width": 4, "bits": [{"name": "ENG_CTX_VALID", "lo": 16, "hi": 16, "values": {"0": "clear", "1": "set"}}], "note": "..."} ]} A user entry with the same ``chunk`` and ``offset`` replaces the bundled entry; new chunk/offset pairs extend the table. ``mode: replace`` drops the bundled table first. An entry without ``bits`` decodes as a named value. Honesty: this tool names observed words; it does not prove their meaning. An unmapped word is silence, not absence. Field offsets without evidence stay unnamed until a map supplies them. Offline only: no hardware, no module, no guest, no network. """ from __future__ import annotations import argparse import json import os import re import shutil import sys import tempfile from typing import Any DECODE_FORMAT = "greeneye-state-decode/1" DIFF_FORMAT = "greeneye-state-diff/1" SNAPSHOT_FORMAT = "greeneye-server-snapshot/1" MAP_FORMAT = "greeneye-state-map/1" DEMO_FORMAT = "greeneye-state-decode-demo/1" WORD_BYTES = 4 WORD_MASK = 0xFFFFFFFF WORD_WIDTHS = (4, 8) AGGREGATE_FORMAT = "aggregate" AGGREGATE_PREFIX = "PHYS_" KV_KIND = "kv" class StateDecodeError(ValueError): """An input, map or request the tool cannot honour.""" # --------------------------------------------------------------------------- # Bundled field table # --------------------------------------------------------------------------- # Every entry is an observation from live GSP bring-up traces (2026-10). The # table is intentionally tiny: unnamed offsets stay unnamed until evidence # earns them a name, and the map file carries everything else. BUILTIN_MAP: dict[str, Any] = { "format": MAP_FORMAT, "mode": "extend", "entries": [ { "chunk": "INSTANCE", "label": "MAGIC_0FACE", "offset": "0x010", "width": 4, "note": "Instance magic observed as 0xface at chunk byte 0x010.", }, { "chunk": "INSTANCE", "label": "GPFIFO_VA", "offset": "0x048", "width": 4, "note": "GPFIFO VA observed as 0x100000 at chunk byte 0x048.", }, { "chunk": "RAMFC", "label": "RAMFC_TARGET", "offset": "0x048", "width": 4, "bits": [ {"name": "ENG_CTX_VALID", "lo": 16, "hi": 16}, {"name": "CE_CTX_VALID", "lo": 17, "hi": 17}, ], "note": ("Engine/CE context-valid bits; the observed live success " "word 0x00030000 sets both. Bits are configurable via " "--map."), }, ], } # --------------------------------------------------------------------------- # Number and token parsing # --------------------------------------------------------------------------- _EQ_RE = re.compile( r"^\s*(?P