rpkbin 0.1.0__tar.gz

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 (45) hide show
  1. rpkbin-0.1.0/LICENSE +21 -0
  2. rpkbin-0.1.0/PKG-INFO +114 -0
  3. rpkbin-0.1.0/README.md +78 -0
  4. rpkbin-0.1.0/pyproject.toml +51 -0
  5. rpkbin-0.1.0/rpkbin/__init__.py +5 -0
  6. rpkbin-0.1.0/rpkbin/cfg/__init__.py +85 -0
  7. rpkbin-0.1.0/rpkbin/cfg/analysis.py +281 -0
  8. rpkbin-0.1.0/rpkbin/cfg/block.py +141 -0
  9. rpkbin-0.1.0/rpkbin/cfg/cfg.py +1125 -0
  10. rpkbin-0.1.0/rpkbin/cfg/fsm.py +226 -0
  11. rpkbin-0.1.0/rpkbin/cfg/mcu.py +266 -0
  12. rpkbin-0.1.0/rpkbin/cfg/program.py +95 -0
  13. rpkbin-0.1.0/rpkbin/excel_extractor/__init__.py +13 -0
  14. rpkbin-0.1.0/rpkbin/excel_extractor/matcher.py +452 -0
  15. rpkbin-0.1.0/rpkbin/excel_extractor/normalizer.py +215 -0
  16. rpkbin-0.1.0/rpkbin/excel_extractor/result.py +51 -0
  17. rpkbin-0.1.0/rpkbin/excel_extractor/template.py +245 -0
  18. rpkbin-0.1.0/rpkbin/excel_extractor/types.py +126 -0
  19. rpkbin-0.1.0/rpkbin/job_manager/__init__.py +15 -0
  20. rpkbin-0.1.0/rpkbin/job_manager/cmd_job.py +184 -0
  21. rpkbin-0.1.0/rpkbin/job_manager/func_job.py +93 -0
  22. rpkbin-0.1.0/rpkbin/job_manager/job.py +340 -0
  23. rpkbin-0.1.0/rpkbin/job_manager/manager.py +573 -0
  24. rpkbin-0.1.0/rpkbin/mapbv.py +534 -0
  25. rpkbin-0.1.0/rpkbin/numbv/__init__.py +41 -0
  26. rpkbin-0.1.0/rpkbin/numbv/_backend.py +155 -0
  27. rpkbin-0.1.0/rpkbin/numbv/core.py +1536 -0
  28. rpkbin-0.1.0/rpkbin/utils/__init__.py +3 -0
  29. rpkbin-0.1.0/rpkbin/utils/stage_tracker.py +621 -0
  30. rpkbin-0.1.0/rpkbin/utils/text_diff.py +686 -0
  31. rpkbin-0.1.0/rpkbin/wave/__init__.py +15 -0
  32. rpkbin-0.1.0/rpkbin/wave/cli.py +42 -0
  33. rpkbin-0.1.0/rpkbin/wave/hook.py +370 -0
  34. rpkbin-0.1.0/rpkbin/wave/job.py +575 -0
  35. rpkbin-0.1.0/rpkbin/wave/runner.py +796 -0
  36. rpkbin-0.1.0/rpkbin/wave/session.py +570 -0
  37. rpkbin-0.1.0/rpkbin/wave/tui/__init__.py +5 -0
  38. rpkbin-0.1.0/rpkbin/wave/tui/app.py +1123 -0
  39. rpkbin-0.1.0/rpkbin.egg-info/PKG-INFO +114 -0
  40. rpkbin-0.1.0/rpkbin.egg-info/SOURCES.txt +43 -0
  41. rpkbin-0.1.0/rpkbin.egg-info/dependency_links.txt +1 -0
  42. rpkbin-0.1.0/rpkbin.egg-info/entry_points.txt +2 -0
  43. rpkbin-0.1.0/rpkbin.egg-info/requires.txt +23 -0
  44. rpkbin-0.1.0/rpkbin.egg-info/top_level.txt +1 -0
  45. rpkbin-0.1.0/setup.cfg +4 -0
rpkbin-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 redpigkiller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rpkbin-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: rpkbin
3
+ Version: 0.1.0
4
+ Summary: Personal collection of small scripts and helpers for work.
5
+ Author-email: redpigkiller <hongrunlu511@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/redpigkiller/rpkbin
8
+ Project-URL: Repository, https://github.com/redpigkiller/rpkbin
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: click>=8.3.1
17
+ Provides-Extra: cfg
18
+ Requires-Dist: networkx>=3.6.1; extra == "cfg"
19
+ Provides-Extra: excel
20
+ Requires-Dist: openpyxl>=3.1.5; extra == "excel"
21
+ Requires-Dist: xlrd>=2.0.2; extra == "excel"
22
+ Requires-Dist: rapidfuzz>=3.14.3; extra == "excel"
23
+ Provides-Extra: wave
24
+ Requires-Dist: textual>=8.0.2; extra == "wave"
25
+ Requires-Dist: prompt_toolkit>=3.0.52; extra == "wave"
26
+ Requires-Dist: rich>=14.2.0; extra == "wave"
27
+ Provides-Extra: all
28
+ Requires-Dist: networkx>=3.6.1; extra == "all"
29
+ Requires-Dist: openpyxl>=3.1.5; extra == "all"
30
+ Requires-Dist: xlrd>=2.0.2; extra == "all"
31
+ Requires-Dist: rapidfuzz>=3.14.3; extra == "all"
32
+ Requires-Dist: textual>=8.0.2; extra == "all"
33
+ Requires-Dist: prompt_toolkit>=3.0.52; extra == "all"
34
+ Requires-Dist: rich>=14.2.0; extra == "all"
35
+ Dynamic: license-file
36
+
37
+ # rpkbin — Core IC Design & Verification Utilities
38
+
39
+ [![English](https://img.shields.io/badge/Language-English-blue.svg)](README.md)
40
+ [![繁體中文](https://img.shields.io/badge/語言-繁體中文-blue.svg)](README_zh.md)
41
+
42
+ `rpkbin` is a comprehensive toolkit providing essential data types and utilities for hardware design and verification flows.
43
+
44
+ ## Core Features
45
+
46
+ ### 1. MapBV — BitVector with Bidirectional Mapping
47
+ MapBV allows you to define a hierarchy of bit-fields that stay synchronized automatically.
48
+ - **Hierarchical Slicing**: Define a 32-bit register and slice it into named fields.
49
+ - **Bidirectional Linking**: Use `concat` to build a new view from existing variables; updating the view updates the sources.
50
+ - **Symbolic Evaluation**: Use `.eval()` to test "what-if" scenarios without changing actual values.
51
+
52
+ [Learn more in MapBV Documentation](docs/mapbv/mapbv.md)
53
+
54
+ ### 2. NumBV — Bit-True Fixed-Point Arithmetic
55
+ Bit-exact fixed-point simulation for DSP pipeline verification. Pure NumPy, no external dependency.
56
+ - **Two-layer API**: Operator layer (`+`, `*`) for convenience; function layer (`nbv.add()`, `nbv.mul()`) for explicit pipeline staging.
57
+ - **Five Rounding Modes**: `trunc`, `round`, `round_half_even` (convergent, Xilinx DSP48), `ceil`, `round_to_zero`.
58
+ - **Unified Operations**: One `NumBV` class handles both scalar and array computations. Backed by NumPy by default, with an optional drop-in JAX backend for transparent XLA hardware acceleration.
59
+
60
+ [Learn more in NumBV Documentation](docs/numbv/numbv.md)
61
+
62
+ ### 3. Excel Extractor — Template-Based Extraction
63
+ Intelligently extract data from complex spreadsheets.
64
+ - **Layout Description**: Define the "shape" of data instead of hardcoded coordinates.
65
+ - **Fuzzy Matching**: Matches headers even with slight spelling variations.
66
+ - **Merged Cell Support**: Correctly resolves values spanning across merged rows/cols.
67
+
68
+ [Learn more in Excel Extractor Documentation](docs/excel_extractor/excel_extractor.md)
69
+
70
+ ### 4. Job Manager
71
+ A practical, cross-platform job manager for running shell commands and Python callables safely in parallel.
72
+
73
+ - **One API for Common Workloads**: Run local functions (`FuncJob`) and CLI tasks (`CmdJob`) with the same manager.
74
+ - **Resource-Aware Scheduling**: Limit concurrency by global resources such as GPU count or license tokens.
75
+ - **Operationally Friendly**: Built-in cancellation, retries, live logs, and callbacks for automation pipelines.
76
+
77
+ [Job Manager Documentation](docs/job_manager/job_manager.md)
78
+
79
+ ### 5. Wave — Batch Workflow Orchestration with Live TUI
80
+ A workflow layer built on top of Job Manager for declaring and observing long-running batch flows.
81
+
82
+ - **Wave File Pattern**: Declare jobs, parsers, and hooks in a plain Python file. Run it with `rpk-wave run`.
83
+ - **Live TUI**: A full-screen Textual interface with a job dashboard, per-job log/data/event panels, and a built-in command bar.
84
+ - **Headless REPL**: For CI or terminal sessions where a TUI isn't wanted — full inspection and control commands via stdin.
85
+ - **Hooks & Parsers**: React to log output, structured parsed data, elapsed time, or lifecycle events automatically.
86
+ - **Job Events with Source Tracking**: `job.emit()` records named events; `source="system"` marks internal errors (parser/hook failures) for easy filtering in the TUI.
87
+ - **Operational Control**: Pause/resume dispatch, gracefully stop jobs, force-cancel jobs, or target active jobs by tag from either the TUI command bar or headless REPL.
88
+ - **Fast Job Inspection**: Open `show`, `logs`, `data`, or `events <job>` directly in JOB DETAIL, then switch between jobs with `[` / `]` or between running jobs with `{` / `}`.
89
+ - **Intentional Cancellation Semantics**: User-cancelled jobs are reported as `cancelled` without turning the whole Wave run into a failed exit; real job failures and session timeouts still return a non-zero exit code.
90
+
91
+ [Wave Documentation](docs/wave/wave.md)
92
+
93
+ ## Installation
94
+
95
+ ```bash
96
+ # Core only (zero dependencies)
97
+ pip install -e .
98
+
99
+ # Install specific features
100
+ pip install -e .[wave] # Installs textual, prompt_toolkit, rich
101
+ pip install -e .[excel] # Installs openpyxl, xlrd, rapidfuzz
102
+ pip install -e .[cfg] # Installs networkx
103
+
104
+ # Install everything
105
+ pip install -e .[all]
106
+ ```
107
+
108
+ ## Testing
109
+
110
+ Run tests using `pytest` from the root directory:
111
+ ```bash
112
+ pytest tests/ -v
113
+ ```
114
+ *(All tests require only `numpy` — no optional dependencies needed.)*
rpkbin-0.1.0/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # rpkbin — Core IC Design & Verification Utilities
2
+
3
+ [![English](https://img.shields.io/badge/Language-English-blue.svg)](README.md)
4
+ [![繁體中文](https://img.shields.io/badge/語言-繁體中文-blue.svg)](README_zh.md)
5
+
6
+ `rpkbin` is a comprehensive toolkit providing essential data types and utilities for hardware design and verification flows.
7
+
8
+ ## Core Features
9
+
10
+ ### 1. MapBV — BitVector with Bidirectional Mapping
11
+ MapBV allows you to define a hierarchy of bit-fields that stay synchronized automatically.
12
+ - **Hierarchical Slicing**: Define a 32-bit register and slice it into named fields.
13
+ - **Bidirectional Linking**: Use `concat` to build a new view from existing variables; updating the view updates the sources.
14
+ - **Symbolic Evaluation**: Use `.eval()` to test "what-if" scenarios without changing actual values.
15
+
16
+ [Learn more in MapBV Documentation](docs/mapbv/mapbv.md)
17
+
18
+ ### 2. NumBV — Bit-True Fixed-Point Arithmetic
19
+ Bit-exact fixed-point simulation for DSP pipeline verification. Pure NumPy, no external dependency.
20
+ - **Two-layer API**: Operator layer (`+`, `*`) for convenience; function layer (`nbv.add()`, `nbv.mul()`) for explicit pipeline staging.
21
+ - **Five Rounding Modes**: `trunc`, `round`, `round_half_even` (convergent, Xilinx DSP48), `ceil`, `round_to_zero`.
22
+ - **Unified Operations**: One `NumBV` class handles both scalar and array computations. Backed by NumPy by default, with an optional drop-in JAX backend for transparent XLA hardware acceleration.
23
+
24
+ [Learn more in NumBV Documentation](docs/numbv/numbv.md)
25
+
26
+ ### 3. Excel Extractor — Template-Based Extraction
27
+ Intelligently extract data from complex spreadsheets.
28
+ - **Layout Description**: Define the "shape" of data instead of hardcoded coordinates.
29
+ - **Fuzzy Matching**: Matches headers even with slight spelling variations.
30
+ - **Merged Cell Support**: Correctly resolves values spanning across merged rows/cols.
31
+
32
+ [Learn more in Excel Extractor Documentation](docs/excel_extractor/excel_extractor.md)
33
+
34
+ ### 4. Job Manager
35
+ A practical, cross-platform job manager for running shell commands and Python callables safely in parallel.
36
+
37
+ - **One API for Common Workloads**: Run local functions (`FuncJob`) and CLI tasks (`CmdJob`) with the same manager.
38
+ - **Resource-Aware Scheduling**: Limit concurrency by global resources such as GPU count or license tokens.
39
+ - **Operationally Friendly**: Built-in cancellation, retries, live logs, and callbacks for automation pipelines.
40
+
41
+ [Job Manager Documentation](docs/job_manager/job_manager.md)
42
+
43
+ ### 5. Wave — Batch Workflow Orchestration with Live TUI
44
+ A workflow layer built on top of Job Manager for declaring and observing long-running batch flows.
45
+
46
+ - **Wave File Pattern**: Declare jobs, parsers, and hooks in a plain Python file. Run it with `rpk-wave run`.
47
+ - **Live TUI**: A full-screen Textual interface with a job dashboard, per-job log/data/event panels, and a built-in command bar.
48
+ - **Headless REPL**: For CI or terminal sessions where a TUI isn't wanted — full inspection and control commands via stdin.
49
+ - **Hooks & Parsers**: React to log output, structured parsed data, elapsed time, or lifecycle events automatically.
50
+ - **Job Events with Source Tracking**: `job.emit()` records named events; `source="system"` marks internal errors (parser/hook failures) for easy filtering in the TUI.
51
+ - **Operational Control**: Pause/resume dispatch, gracefully stop jobs, force-cancel jobs, or target active jobs by tag from either the TUI command bar or headless REPL.
52
+ - **Fast Job Inspection**: Open `show`, `logs`, `data`, or `events <job>` directly in JOB DETAIL, then switch between jobs with `[` / `]` or between running jobs with `{` / `}`.
53
+ - **Intentional Cancellation Semantics**: User-cancelled jobs are reported as `cancelled` without turning the whole Wave run into a failed exit; real job failures and session timeouts still return a non-zero exit code.
54
+
55
+ [Wave Documentation](docs/wave/wave.md)
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ # Core only (zero dependencies)
61
+ pip install -e .
62
+
63
+ # Install specific features
64
+ pip install -e .[wave] # Installs textual, prompt_toolkit, rich
65
+ pip install -e .[excel] # Installs openpyxl, xlrd, rapidfuzz
66
+ pip install -e .[cfg] # Installs networkx
67
+
68
+ # Install everything
69
+ pip install -e .[all]
70
+ ```
71
+
72
+ ## Testing
73
+
74
+ Run tests using `pytest` from the root directory:
75
+ ```bash
76
+ pytest tests/ -v
77
+ ```
78
+ *(All tests require only `numpy` — no optional dependencies needed.)*
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rpkbin"
7
+ version = "0.1.0"
8
+ description = "Personal collection of small scripts and helpers for work."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "redpigkiller", email = "hongrunlu511@gmail.com" }
12
+ ]
13
+ license = { text = "MIT" }
14
+ requires-python = ">=3.8"
15
+ dependencies = [
16
+ "click>=8.3.1",
17
+ ]
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Development Status :: 3 - Alpha",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ cfg = ["networkx>=3.6.1"]
27
+ excel = ["openpyxl>=3.1.5", "xlrd>=2.0.2", "rapidfuzz>=3.14.3"]
28
+ wave = ["textual>=8.0.2", "prompt_toolkit>=3.0.52", "rich>=14.2.0"]
29
+ all = [
30
+ "networkx>=3.6.1",
31
+ "openpyxl>=3.1.5",
32
+ "xlrd>=2.0.2",
33
+ "rapidfuzz>=3.14.3",
34
+ "textual>=8.0.2",
35
+ "prompt_toolkit>=3.0.52",
36
+ "rich>=14.2.0"
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/redpigkiller/rpkbin"
41
+ Repository = "https://github.com/redpigkiller/rpkbin"
42
+
43
+ [project.scripts]
44
+ rpk-wave = "rpkbin.wave.cli:main"
45
+
46
+ [tool.setuptools.packages.find]
47
+ include = ["rpkbin*"]
48
+
49
+ [tool.setuptools]
50
+ include-package-data = true
51
+
@@ -0,0 +1,5 @@
1
+ """rpkbin — A collection of utilities for IC design & verification."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = []
@@ -0,0 +1,85 @@
1
+ """rpkbin.cfg — Control Flow Graph IR, analysis, and domain-specific tools.
2
+
3
+ Public API
4
+ ----------
5
+
6
+ Core IR::
7
+
8
+ from rpkbin.cfg import CFG, BasicBlock
9
+ from rpkbin.cfg import Assignment, CallRef, OtherInsn, Insn
10
+ from rpkbin.cfg import NaturalLoop, Program
11
+
12
+ Merging::
13
+
14
+ from rpkbin.cfg import merge_cfgs
15
+ from rpkbin.cfg import (
16
+ CFGMergeError, DuplicateLabelError, InsnConflictError,
17
+ EdgeConflictError, MetaConflictError,
18
+ )
19
+ merged = merge_cfgs(flow1, flow2, flow3)
20
+ merged.set_entry("IDLE")
21
+
22
+ Interprocedural analysis::
23
+
24
+ from rpkbin.cfg.analysis import (
25
+ build_call_graph,
26
+ check_call_depth,
27
+ interprocedural_liveness,
28
+ FunctionSummary,
29
+ LivenessResult,
30
+ )
31
+
32
+ FSM tools::
33
+
34
+ from rpkbin.cfg import fsm
35
+ fsm.find_dead_states(program)
36
+ fsm.find_sink_sccs(program)
37
+ fsm.check_conditions_complete(program)
38
+ layout = fsm.linearize(program) # → FSMLayout
39
+
40
+ MCU tools::
41
+
42
+ from rpkbin.cfg import mcu
43
+ mcu.find_dead_loops(program, exit_block="HALT")
44
+ mcu.dead_code_elimination(cfg)
45
+ layout = mcu.linearize(program) # → MCULayout
46
+ """
47
+
48
+ from .block import Assignment, CallRef, Insn, OtherInsn, BasicBlock
49
+ from .cfg import (
50
+ CFG,
51
+ NaturalLoop,
52
+ CFGMergeError,
53
+ DuplicateLabelError,
54
+ InsnConflictError,
55
+ EdgeConflictError,
56
+ MetaConflictError,
57
+ merge_cfgs,
58
+ )
59
+ from .program import Program
60
+ from . import fsm
61
+ from . import mcu
62
+
63
+ __all__ = [
64
+ # IR types
65
+ "BasicBlock",
66
+ "Assignment",
67
+ "CallRef",
68
+ "OtherInsn",
69
+ "Insn",
70
+ # CFG
71
+ "CFG",
72
+ "NaturalLoop",
73
+ # Merge
74
+ "merge_cfgs",
75
+ "CFGMergeError",
76
+ "DuplicateLabelError",
77
+ "InsnConflictError",
78
+ "EdgeConflictError",
79
+ "MetaConflictError",
80
+ # Program container
81
+ "Program",
82
+ # Domain-specific sub-modules
83
+ "fsm",
84
+ "mcu",
85
+ ]
@@ -0,0 +1,281 @@
1
+ """Interprocedural analysis utilities for CFG Programs.
2
+
3
+ This module provides analysis functions that operate across multiple CFGs
4
+ (i.e. across function boundaries) using the :class:`~rpkbin.cfg.program.Program`
5
+ container.
6
+
7
+ Key features
8
+ ------------
9
+ * :func:`build_call_graph` — scans all ``CallRef`` instructions to construct
10
+ the program's call graph automatically; no manual registration needed.
11
+ * :func:`check_call_depth` — verifies the call graph is a DAG and its depth
12
+ does not exceed a hardware limit.
13
+ * :func:`interprocedural_liveness` — computes live-in / live-out sets for
14
+ every block in every function using a bottom-up summary-based approach.
15
+
16
+ def/use derivation
17
+ ------------------
18
+ ``Assignment(lhs, rhs)``
19
+ def = {lhs}, use = set(rhs)
20
+
21
+ ``CallRef(callee)``
22
+ def = callee's FunctionSummary.defs
23
+ use = callee's FunctionSummary.uses
24
+
25
+ ``OtherInsn(defs, uses)``
26
+ def = defs, use = uses
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from dataclasses import dataclass, field
32
+ from typing import FrozenSet
33
+
34
+ import networkx as nx
35
+
36
+ from .block import Assignment, CallRef, Insn, OtherInsn
37
+ from .cfg import CFG
38
+ from .program import Program
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # def/use helper
43
+ # ---------------------------------------------------------------------------
44
+
45
+ def _block_def_use(
46
+ insn: Insn,
47
+ summaries: dict[str, "FunctionSummary"],
48
+ ) -> tuple[set[str], set[str]]:
49
+ """Return ``(defs, uses)`` for a single instruction.
50
+
51
+ For :class:`CallRef`, the summary of the callee is used if available;
52
+ otherwise both sets are empty (safe but imprecise — run bottom-up to avoid
53
+ this case).
54
+ """
55
+ if isinstance(insn, Assignment):
56
+ return {insn.lhs}, set(insn.rhs)
57
+ if isinstance(insn, CallRef):
58
+ s = summaries.get(insn.callee)
59
+ if s is not None:
60
+ return set(s.defs), set(s.uses)
61
+ return set(), set() # callee not analysed yet (cycle guard)
62
+ if isinstance(insn, OtherInsn):
63
+ return set(insn.defs), set(insn.uses)
64
+ return set(), set() # unreachable with the current Insn union
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Data structures
69
+ # ---------------------------------------------------------------------------
70
+
71
+ @dataclass
72
+ class FunctionSummary:
73
+ """Liveness summary exported from a single function.
74
+
75
+ Attributes:
76
+ defs: Variables the function *may* write on any execution path.
77
+ uses: Variables the function reads before (possibly) writing them —
78
+ i.e. the live-in set at the function's entry block.
79
+ """
80
+
81
+ defs: set[str] = field(default_factory=set)
82
+ uses: set[str] = field(default_factory=set)
83
+
84
+ def __repr__(self) -> str:
85
+ return f"FunctionSummary(defs={sorted(self.defs)}, uses={sorted(self.uses)})"
86
+
87
+
88
+ @dataclass
89
+ class LivenessResult:
90
+ """Per-block liveness sets for a single function.
91
+
92
+ Attributes:
93
+ live_in: Mapping from block id to the set of variables live at the
94
+ *entry* of that block.
95
+ live_out: Mapping from block id to the set of variables live at the
96
+ *exit* of that block.
97
+ """
98
+
99
+ live_in: dict[str, FrozenSet[str]] = field(default_factory=dict)
100
+ live_out: dict[str, FrozenSet[str]] = field(default_factory=dict)
101
+
102
+ def is_live_at_entry(self, block_id: str, var: str) -> bool:
103
+ """Return ``True`` if *var* is live at the entry of *block_id*."""
104
+ return var in self.live_in.get(block_id, frozenset())
105
+
106
+ def is_live_at_exit(self, block_id: str, var: str) -> bool:
107
+ """Return ``True`` if *var* is live at the exit of *block_id*."""
108
+ return var in self.live_out.get(block_id, frozenset())
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Call graph
113
+ # ---------------------------------------------------------------------------
114
+
115
+ def build_call_graph(program: Program) -> nx.DiGraph:
116
+ """Build a directed call graph by scanning ``CallRef`` instructions.
117
+
118
+ Each node in the returned graph is a function name (a key in
119
+ ``program.cfgs``). An edge ``(caller, callee)`` is added for every
120
+ :class:`~rpkbin.cfg.block.CallRef` found in *caller*'s blocks.
121
+
122
+ Returns:
123
+ A :class:`networkx.DiGraph` with one node per function.
124
+
125
+ Raises:
126
+ KeyError: If a ``CallRef.callee`` does not exist in ``program.cfgs``.
127
+ """
128
+ cg: nx.DiGraph = nx.DiGraph()
129
+ cg.add_nodes_from(program.cfgs.keys())
130
+
131
+ for fn_name, cfg in program.cfgs.items():
132
+ for bb in cfg.blocks:
133
+ for insn in bb.insns:
134
+ if isinstance(insn, CallRef):
135
+ if insn.callee not in program.cfgs:
136
+ raise KeyError(
137
+ f"CallRef to unknown function {insn.callee!r} "
138
+ f"in block {bb.id!r} of function {fn_name!r}."
139
+ )
140
+ cg.add_edge(fn_name, insn.callee)
141
+
142
+ return cg
143
+
144
+
145
+ def check_call_depth(program: Program, max_depth: int | None = None) -> int:
146
+ """Verify the call graph is acyclic and within the depth limit.
147
+
148
+ Args:
149
+ program: The program to analyse.
150
+ max_depth: Maximum allowed call-stack depth. If ``None``, only the
151
+ actual depth is returned without raising.
152
+
153
+ Returns:
154
+ The actual maximum call depth (longest path length in the call graph).
155
+
156
+ Raises:
157
+ ValueError: If the call graph contains a cycle (recursive call).
158
+ ValueError: If the actual depth exceeds *max_depth*.
159
+ """
160
+ cg = build_call_graph(program)
161
+ try:
162
+ depth: int = nx.dag_longest_path_length(cg)
163
+ except nx.NetworkXUnfeasible:
164
+ raise ValueError(
165
+ "Call graph contains a cycle — recursive calls are not allowed."
166
+ )
167
+ if max_depth is not None and depth > max_depth:
168
+ raise ValueError(
169
+ f"Call depth {depth} exceeds the allowed maximum of {max_depth}."
170
+ )
171
+ return depth
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Intraprocedural liveness (single CFG)
176
+ # ---------------------------------------------------------------------------
177
+
178
+ def _intraprocedural_liveness(
179
+ cfg: CFG,
180
+ summaries: dict[str, FunctionSummary],
181
+ ) -> tuple[LivenessResult, FunctionSummary]:
182
+ """Run liveness analysis on a single CFG using pre-computed summaries.
183
+
184
+ Returns the per-block :class:`LivenessResult` and the derived
185
+ :class:`FunctionSummary` for this function.
186
+ """
187
+ g = cfg._graph
188
+
189
+ # Compute per-block def/use sets (in sequential instruction order)
190
+ block_def: dict[str, set[str]] = {}
191
+ block_use: dict[str, set[str]] = {}
192
+ for bb in cfg.blocks:
193
+ blk_def: set[str] = set()
194
+ blk_use: set[str] = set()
195
+ for insn in bb.insns:
196
+ d, u = _block_def_use(insn, summaries)
197
+ # A variable used before it is defined in this block is live-in
198
+ blk_use |= u - blk_def
199
+ blk_def |= d
200
+ block_def[bb.id] = blk_def
201
+ block_use[bb.id] = blk_use
202
+
203
+ # Initialize live sets
204
+ live_in: dict[str, FrozenSet[str]] = {bb.id: frozenset() for bb in cfg.blocks}
205
+ live_out: dict[str, FrozenSet[str]] = {bb.id: frozenset() for bb in cfg.blocks}
206
+
207
+ # Iterative worklist (backward)
208
+ worklist: set[str] = {bb.id for bb in cfg.blocks}
209
+ while worklist:
210
+ bid = worklist.pop()
211
+
212
+ new_out: set[str] = set()
213
+ for succ in g.successors(bid):
214
+ new_out |= live_in[succ]
215
+ new_out_f = frozenset(new_out)
216
+
217
+ new_in = frozenset(block_use[bid] | (new_out - block_def[bid]))
218
+
219
+ if new_out_f != live_out[bid] or new_in != live_in[bid]:
220
+ live_out[bid] = new_out_f
221
+ live_in[bid] = new_in
222
+ for pred in g.predecessors(bid):
223
+ worklist.add(pred)
224
+
225
+ result = LivenessResult(live_in=live_in, live_out=live_out)
226
+
227
+ # Build summary: defs = union of all block defs; uses = live_in at entry
228
+ all_defs: set[str] = set()
229
+ for d in block_def.values():
230
+ all_defs |= d
231
+ entry_uses: set[str] = set(live_in[cfg._entry]) if cfg._entry else set()
232
+ summary = FunctionSummary(defs=all_defs, uses=entry_uses)
233
+
234
+ return result, summary
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Interprocedural liveness
239
+ # ---------------------------------------------------------------------------
240
+
241
+ def interprocedural_liveness(
242
+ program: Program,
243
+ ) -> dict[str, LivenessResult]:
244
+ """Bottom-up interprocedural liveness analysis.
245
+
246
+ Analyses each function in reverse topological order of the call graph
247
+ (leaf functions first) so that every callee's :class:`FunctionSummary`
248
+ is available before its callers are analysed.
249
+
250
+ The call graph must be a DAG (no recursion). Use :func:`check_call_depth`
251
+ to verify this beforehand if needed.
252
+
253
+ Args:
254
+ program: The program to analyse.
255
+
256
+ Returns:
257
+ A mapping from function name to its :class:`LivenessResult`.
258
+
259
+ Raises:
260
+ ValueError: If the call graph contains a cycle.
261
+ """
262
+ cg = build_call_graph(program)
263
+
264
+ # Topological order: process leaves (callees) before callers
265
+ try:
266
+ topo_order: list[str] = list(nx.topological_sort(cg))
267
+ except nx.NetworkXUnfeasible:
268
+ raise ValueError(
269
+ "Call graph contains a cycle — recursive calls are not allowed."
270
+ )
271
+
272
+ summaries: dict[str, FunctionSummary] = {}
273
+ results: dict[str, LivenessResult] = {}
274
+
275
+ for fn_name in reversed(topo_order): # reversed → leaves first
276
+ cfg = program.cfgs[fn_name]
277
+ result, summary = _intraprocedural_liveness(cfg, summaries)
278
+ results[fn_name] = result
279
+ summaries[fn_name] = summary
280
+
281
+ return results