diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..554e0af --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,46 @@ +name: Clippy Lint + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + clippy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: clippy + override: true + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 840e4dc..d64558d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,7 +22,7 @@ jobs: uses: actions/cache@v4 with: path: ~/tools - key: ${{ runner.os }}-tools-iverilog-v11-verilator-v5.020 + key: ${{ runner.os }}-tools-iverilog-v11-verilator-feat-cpp-linecount - name: Install dependencies run: | @@ -41,18 +41,20 @@ jobs: make -j 4 make install - - name: Install Verilator v5.020 from source + - name: Install Verilator feat/cpp-linecount from source if: steps.cache-tools.outputs.cache-hit != 'true' run: | cd /tmp - git clone https://github.com/verilator/verilator.git + git clone https://github.com/AS-SiliconMind/verilator.git cd verilator - git checkout v5.020 + git checkout feat/cpp-linecount unset VERILATOR_ROOT autoconf - ./configure --prefix=$HOME/tools - make -j 4 + ./configure --prefix=$HOME/tools --with-parsetree + make -j `nproc` make install + cd - + rm -rf /tmp/verilator - name: Add tools to PATH and verify run: | diff --git a/.gitignore b/.gitignore index 530f4b9..b428569 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,7 @@ obj_dir/ # Environment variables .env + +# Python files +__pycache__ +.venv diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c4ca9c9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,467 @@ +# AGENTS.md - AI Assistant Notes + +This document provides guidance for AI assistants working on the STG-Rust codebase. + +## Project Overview + +**STG (Structured Testbench Generation)** is a Rust tool that automatically generates comprehensive testbenches for Verilog/SystemVerilog designs. It compares a Design Under Test (DUT) against a golden reference model. + +### Core Functionality +- Parse Verilog/SystemVerilog modules and extract port information +- Classify signals into control vs. data, and inputs vs. outputs +- Generate semi-exhaustive test patterns with intelligent coverage +- Support multiple testbench modes: SystemVerilog, C++, and SystemC +- Compare DUT outputs against golden reference and report statistics + +### Key Design Principles +1. **Type Safety**: Leverage Rust's type system to prevent entire classes of bugs +2. **Performance**: Native binary with no runtime interpreter overhead +3. **Flexibility**: Support multiple modes (SV, CC, SC) and design types +4. **Portability**: Single binary that can be shipped anywhere + +--- + +## Architecture + +### Directory Structure + +``` +stg-rust/ +├── src/ +│ ├── main.rs # Entry point, CLI parsing +│ ├── lib.rs # Library exports +│ ├── cli.rs # CLI argument definitions (using clap) +│ ├── python_runtime.rs # Python environment management for LM features +│ ├── commands/ # Command implementations +│ │ ├── generate.rs # Main testbench generation logic +│ │ ├── generate_fsm.rs # FSM-based coverage-enhanced generation +│ │ ├── compile.rs # Testbench compilation +│ │ ├── identify.rs # Signal classification +│ │ └── parse.rs # Module parsing and priority sorting +│ └── tools/ # Core functionality modules +│ ├── verilog_parser.rs # Verilog parsing (sv-parser + iverilog) +│ ├── signal_classification.rs # Signal type identification +│ ├── generator.rs # Testbench template generation +│ ├── compiler.rs # iverilog/Verilator compilation +│ ├── emplace_verilog.rs # Module emplacement (SV mode) +│ ├── fsm_extractor.rs # FSM extraction for coverage +│ ├── fsm_analysis.rs # FSM analysis and DFS engine +│ └── file_utils.rs # File I/O utilities +├── examples/ # Test cases and examples +│ ├── ALU/ # Combinational logic example +│ ├── ALU_cc/ # C++ testbench example +│ ├── pingpong/ # Sequential clocked example +│ ├── seq_detector/ # FSM coverage example +│ └── traffic_light/ # Traffic light FSM example +├── tests/ # Rust integration tests +└── docs/ # Sphinx documentation +``` + +### Key Components + +#### 1. Verilog Parser (`verilog_parser.rs`) +- **Hybrid approach**: Uses sv-parser for Rust-based parsing, falls back to iverilog for port information +- **Module priority sorting**: Analyzes instantiation hierarchy and submodule counts +- **Port extraction**: Identifies inputs, outputs, widths, and signal types + +#### 2. Signal Classification (`signal_classification.rs`) +- Identifies control signals (opcodes, enables, modes) vs. data signals (values, addresses) +- Detects special signals: clock, reset, done/valid +- Uses heuristics: signal width, naming patterns, module context + +#### 3. Generator (`generator.rs`) +- Generates testbench templates from Tera templating engine +- Three modes: SystemVerilog (SV), C++ (CC), SystemC (SC) +- Test pattern generation strategy: + - **Control signals**: Exhaustive enumeration (up to 2^26 combinations) + - **Data signals**: Random sampling (default: 1024 samples per control vector) + +#### 4. Compiler (`compiler.rs`) +- **iverilog**: Traditional SystemVerilog compilation +- **Verilator**: High-performance C++ conversion with optional MPI support +- Coverage analysis support (generates .dat files) +- Multi-DUT support with unique prefixes (V0_, V1_, V2_...) + +#### 5. FSM Tools (`fsm_extractor.rs`, `fsm_analysis.rs`) +- Extract FSM state machines from Verilog (deterministic or LM-assisted) +- Generate state transition graphs +- DFS-based test generation for complete state coverage +- Hierarchical signal access for internal state monitoring + +--- + +## Design Types + +STG supports three fundamental design types: + +### 1. Combinational (`combinational`) +- Pure combinational logic (no clock, no state) +- Test approach: Enumerate control signals, random data signals +- Examples: ALU, multiplexers, decoders +- Golden model: Implements `eval()` function + +### 2. Sequential Clocked (`seq_clocked`) +- Clocked sequential design with continuous operation +- Requires clock and usually reset signals +- Test approach: Apply random control/data patterns over multiple clock cycles +- Examples: Counters, shift registers, timers +- Golden model: Implements `posedge_clk()` function + +### 3. Sequential Done (`seq_done`) +- Sequential design with transaction-based operation +- Requires clock, reset, and done/valid signal +- Test approach: Start transaction, wait for done signal, check results +- Examples: GCD, dividers, state machines with completion signals +- Golden model: Implements `posedge_clk()` and tracks completion + +--- + +## Testbench Modes + +### SystemVerilog Mode (SV) +- Both DUT and golden are Verilog modules +- Uses iverilog or Verilator for compilation +- Single-stage workflow +- Good for: Simple designs, standard workflow + +### C++ Mode (CC) +- DUT is Verilog, golden is C++ header file +- **Two-stage workflow**: + 1. Generate template with `--out-header golden_model.h` + 2. Implement golden model, then compile with `--golden golden_model.h` +- Uses Verilator for DUT compilation +- Good for: Complex golden models, custom logic, performance + +### SystemC Mode (SC) +- Similar to CC but uses SystemC types (`sc_uint`) +- Two-stage workflow like CC +- Good for: SystemC ecosystem integration, bit-accurate types + +--- + +## Multi-DUT Support + +STG can test multiple DUT implementations simultaneously against one golden reference. + +### Key Features +- Compare multiple implementations in one run +- Individual statistics for each DUT in `test_stats.json` +- Automatic unique prefixes (V0_, V1_, V2_...) +- In SV mode: Use `--emplace-module` to avoid module name conflicts +- In CC/SC mode: Verilator handles renaming automatically + +### Module Specification Methods +1. **Single name**: `--module alu` (used for all DUTs) +2. **Comma-separated**: `--module alu1,alu2,alu3` +3. **Interleaved**: `--verilog dut1.v --module alu1 --verilog dut2.v --module alu2` + +--- + +## Important Conventions + +### Signal Naming +- Clock signals: `clk`, `clock` +- Reset signals: `rst`, `rst_n`, `reset`, `reset_n` +- Done/Valid signals: `done`, `valid`, `ready` +- Control signals: Usually narrow (1-4 bits), named like `op`, `mode`, `cmd` +- Data signals: Usually wider (8+ bits), named like `a`, `b`, `data`, `addr` + +### Coding Patterns +- **Error handling**: Use `Result` for recoverable errors +- **File paths**: Always use `PathBuf` and handle path operations carefully +- **Template rendering**: Use Tera for all code generation +- **External tools**: Shell out to iverilog, verilator using `std::process::Command` + +### Testing +- Examples in `examples/` serve as integration tests +- Each example has a README.md and Makefile +- Run `cargo test` for unit tests +- Test examples with their Makefiles: `make -C examples/ALU` + +--- + +## Common Workflows + +### Adding a New Feature to Testbench Generation + +1. **Update CLI** (`cli.rs`): + ```rust + #[arg(long, help = "Your new feature")] + pub new_feature: bool, + ``` + +2. **Modify Generator** (`tools/generator.rs`): + - Add template variables in the generation context + - Update Tera template rendering + +3. **Update Templates** (embedded in code or separate files): + - Add new template sections + - Use Tera syntax: `{% if new_feature %}...{% endif %}` + +4. **Test**: + - Create example in `examples/new_feature/` + - Add README.md and test files + - Run and verify output + +### Adding Support for a New Signal Type + +1. **Update Classification** (`tools/signal_classification.rs`): + - Add detection logic in signal classification algorithm + - Update `SignalInfo` struct if needed + +2. **Update Generator** (`tools/generator.rs`): + - Add handling for new signal type in testbench templates + - Update test pattern generation + +3. **Update Documentation**: + - Add to USAGE.md with examples + - Update CLI help text + +### Debugging Verilog Parsing Issues + +1. **Enable debug output**: `--debug` flag +2. **Check parser fallback**: sv-parser fails gracefully, iverilog provides backup +3. **Verify module names**: Use `stg parse` to see what modules are detected +4. **Check port extraction**: Use `stg identify` to see classified signals + +--- + +## FSM Coverage Enhancement + +### Overview +New feature for achieving higher coverage in sequential designs by extracting FSM structure. + +**Documentation**: See `docs/source/user_guide/fsm_coverage.md` for the generate-fsm command guide. + +### Two Approaches + +#### Option A: LM-Based Extraction +- Uses language models (OpenAI, Gemini) to identify FSMs +- Python-based, managed through `python_runtime.rs` +- Environment setup with `uv` package manager +- API keys from `.env` file (e.g., `GOOGLE_API_KEY`) + +#### Option B: Deterministic Extraction +- Parser/AST-based approach using iverilog or sv-parser +- Extract state variables, state transitions +- More reliable but less flexible than LM approach + +### DFS Test Generation +- Uses extracted state transition graph +- Performs depth-first search to cover all states +- Generates targeted input sequences +- **Key advantage**: Achieves states that random testing misses + +### Hierarchical Signal Access +- Verilator supports accessing internal signals: `dut->module->signal` +- Condition monitoring: Wait for internal states (e.g., counters reaching values) +- Enables state-aware testing beyond just input/output + +### Examples +- `examples/seq_detector/`: Sequence detector FSM +- `examples/traffic_light/`: Traffic light controller FSM +- Compare coverage: traditional vs. FSM-enhanced + +--- + +## Common Pitfalls and Warnings + +### 1. Module Name Conflicts +**Problem**: Multiple Verilog files with same module names cause compilation errors +**Solution**: +- SV mode: Use `--emplace-module` to add prefixes +- CC/SC mode: Verilator handles automatically with unique prefixes + +### 2. Signal Classification Errors +**Problem**: Important control signals classified as data (or vice versa) +**Solution**: Explicitly specify with `--control-signals` or `--data-signals` + +### 3. Path Handling +**Problem**: Relative paths may break in different contexts +**Solution**: Convert to absolute paths early using `std::fs::canonicalize()` + +### 4. Golden Model Template +**Problem**: Users forget two-stage workflow in CC/SC mode +**Solution**: Clear error messages mentioning stage 1 and stage 2 + +### 5. Verilator Version Compatibility +**Problem**: Older Verilator versions may not support required features +**Solution**: Recommend Verilator v5.020+ in documentation and error messages + +### 6. Coverage File Conflicts +**Problem**: MPI processes write to same coverage.dat file +**Solution**: In CC/SC mode, use rank-specific filenames; SV mode doesn't support MPI coverage + +### 7. Random Seed Consistency +**Problem**: Non-deterministic test results make debugging hard +**Solution**: Use fixed seed for reproducibility (consider adding `--seed` flag) + +--- + +## Testing Strategy + +### Unit Tests +- Located in `tests/` directory +- Test individual components: parser, classifier, generator +- Run with `cargo test` + +### Integration Tests +- Examples serve as integration tests +- Each example has Makefile with test target +- Verify generated testbench compiles and runs without errors + +### Regression Testing +- Keep `test_stats.json` outputs for examples +- Compare statistics after changes to detect regressions +- Golden references should maintain 100% score + +### Coverage Testing +- FSM examples test coverage enhancement features +- Compare deterministic vs. LM-based vs. traditional coverage +- Look for state coverage gaps in `.dat` files + +--- + +## Key Files Reference + +| File | Purpose | When to Modify | +|------|---------|---------------| +| `cli.rs` | Command-line interface | Adding new flags/options | +| `commands/generate.rs` | Main testbench generation | Changing generation logic | +| `tools/verilog_parser.rs` | Verilog parsing | Parser improvements | +| `tools/signal_classification.rs` | Signal type detection | Classification algorithm changes | +| `tools/generator.rs` | Template generation | Testbench template updates | +| `tools/compiler.rs` | Compilation backend | Compiler flag changes | +| `tools/fsm_analysis.rs` | FSM coverage | DFS engine improvements | +| `python_runtime.rs` | Python environment | LM integration changes | + +--- + +## External Dependencies + +### Required Tools +- **iverilog**: SystemVerilog simulation (v11+ recommended) +- **Verilator**: C++ conversion and high-performance simulation (v5.020+ required) +- **Python**: For LM-based FSM extraction (managed through `uv`) + +### Rust Crates +- `clap`: CLI parsing +- `serde`: Serialization (YAML, JSON) +- `tera`: Template engine +- `sv-parser`: Verilog parsing +- `regex`: Pattern matching + +--- + +## Development Guidelines + +### Before Committing +1. Run `cargo fmt` to format code +2. Run `cargo clippy` to check for warnings +3. Run `cargo test` to verify unit tests +4. Test relevant examples: `make -C examples/ALU` +5. Update USAGE.md if adding user-facing features +6. Update AGENTS.md if adding significant architectural changes + +### Adding Examples +1. Create directory under `examples/` +2. Add DUT Verilog file(s) +3. Add golden reference (Verilog or C++ header) +4. Create README.md explaining the example +5. Add Makefile with targets: `all`, `clean`, `test` +6. Document command-line usage in example README + +### Error Messages +- Be specific: Include file names, line numbers, signal names +- Be helpful: Suggest fixes or next steps +- Be consistent: Follow existing error message patterns +- Example: "Module 'alu' not found in dut.v. Available modules: [add, sub]. Use --module to specify." + +--- + +## Future Directions + +### Potential Improvements +- **Coverage analysis**: Integrate Verilator coverage more deeply +- **Smart test generation**: ML-guided test pattern selection +- **Assertion support**: Generate SVA properties from golden model +- **Waveform comparison**: VCD-based debugging for mismatches +- **Parallel DUT compilation**: Speed up multi-DUT builds +- **Interactive mode**: REPL for exploring designs + +### FSM Enhancement +- Automatic state variable identification +- Better LM prompt engineering for complex FSMs +- Support for hierarchical/nested FSMs +- Visualization of state transition graphs + +--- + +## Resources + +### Documentation +- README.md: Quick start and installation +- docs/source/getting_started.md: Installation and quick start guide +- docs/source/user_guide/: Mode guides (SV, CC/SC), multi-DUT, FSM coverage, advanced features +- docs/source/reference/cli.md: Complete CLI reference for all commands +- docs/source/examples.md: Examples by design type +- docs/source/troubleshooting.md: Common issues and solutions +- examples/*/README.md: Specific example guides + +### External Links +- [Original Python STG](https://github.com/AS-SiliconMind/Structured-Testbench-Generation) +- [Verilator Documentation](https://verilator.org/guide/latest/) +- [sv-parser](https://github.com/dalance/sv-parser) +- [Icarus Verilog](http://iverilog.icarus.com/) + +--- + +## Tips for AI Assistants + +1. **Read before modifying**: Always read relevant source files before suggesting changes +2. **Test your changes**: Verify with examples before claiming success +3. **Follow Rust conventions**: Use `Result`, `Option`, proper error handling +4. **Preserve existing behavior**: Don't break working examples +5. **Document decisions**: Update this file when architecture changes +6. **Be explicit**: Don't guess module names or file paths - verify them +7. **Check dependencies**: Ensure external tools (iverilog, verilator) are available +8. **Understand modes**: SV vs. CC/SC have different workflows and constraints +9. **Multi-DUT awareness**: Many features must handle multiple DUTs correctly +10. **FSM features are new**: The FSM coverage enhancement is actively being developed + +--- + +## Quick Reference Commands + +```bash +# Build and install +cargo build --release +cargo install --path . + +# Run tests +cargo test +make -C examples/ALU test + +# Generate testbench (SV mode) +stg generate --verilog dut.v --golden golden.v --type combinational --out tb.sv + +# Generate testbench (CC mode, two-stage) +stg generate --verilog dut.v --type combinational --out tb.cpp --out-header golden.h --cc +# ... implement golden.h ... +stg generate --verilog dut.v --golden golden.h --type combinational --out tb.cpp --out-exe tb_exe --cc + +# Multi-DUT +stg generate --verilog dut1.v --verilog dut2.v --golden golden.h --type combinational --out tb.cpp --out-exe tb_exe --cc + +# Parse and identify +stg parse --verilog design.v --out modules.yaml +stg identify --verilog design.v --module top --type seq_clocked --out signals.yaml + +# FSM-enhanced coverage (future) +stg generate --verilog dut.v --golden golden.v --type seq_clocked --out tb.cpp --fsm-coverage +``` + +--- + +_Last updated: 2026-02-09_ +_Maintained for AI assistants working on STG-Rust_ diff --git a/Cargo.lock b/Cargo.lock index 0b1ea4e..99e61a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,25 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.12.0" @@ -994,15 +1013,18 @@ dependencies = [ [[package]] name = "stg" -version = "0.3.3" +version = "0.4.0-beta" dependencies = [ "anyhow", "clap", "env_logger", "glob", + "include_dir", + "libc", "log", "regex", "serde", + "serde_json", "serde_yaml", "sv-parser", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index b2ed810..c3cbac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stg" -version = "0.3.3" +version = "0.4.0-beta" edition = "2024" authors = ["En-Ming Huang (samuel21119)"] description = "Structured Testbench Generation - Automated SystemVerilog testbench generator" @@ -13,6 +13,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5", features = ["derive", "cargo"] } serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" serde_yaml = "0.9" tera = "1.19" regex = "1.10" @@ -22,3 +23,8 @@ glob = "0.3.3" log = "0.4" env_logger = "0.11" sv-parser = "0.13.3" +include_dir = "0.7" +libc = "0.2" + +[build-dependencies] +include_dir = "0.7" diff --git a/README.md b/README.md index d137b2f..353e25c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ STG automatically generates comprehensive testbenches for digital designs by: ### Prerequisites - Rust toolchain (1.70 or later) -- iverilog (for SystemVerilog mode) or Verilator (for high-performance testing) +- [Custom Verilator](https://github.com/AS-SiliconMind/verilator/) (branch `feat/cpp-linecount`, based on v5.044) +- iverilog (optional, for SystemVerilog mode fallback parsing) ### Install Rust ```bash @@ -31,27 +32,32 @@ This process compiles `stg` on your computer and installs it to `$HOME/.cargo/bi By default, after installing Rust's toolchain, `$HOME/.cargo/bin` will be added to your `$PATH` environment variable. Therefore, you can invoke `stg` directly from anywhere, without setting up a virtual environment like Python. #### Install Verilator -This repository is tested using Verilator v5.020, and older version which is shipped with the Linux distribution may not be supported. Use the following script to install a newer Verilator from source. +This repository requires a [custom Verilator](https://github.com/AS-SiliconMind/verilator/) (modified from Verilator v5.044) on the `feat/cpp-linecount` branch. The stock Verilator from your distribution will **not** work. Use the following script to build and install it to a custom path: ```bash -VERILATOR_VERSION=v5.020 -PREFIX=$HOME/.usr - -pushd /tmp/ -git clone https://github.com/verilator/verilator.git -cd verilator -git checkout v5.020 -# Every time you need to build: -unset VERILATOR_ROOT # For bash -autoconf # Create ./configure script -./configure --prefix=$PREFIX # Configure and create Makefile +# Prerequisites (Ubuntu/Debian): +sudo apt-get install git help2man perl python3 make autoconf g++ flex bison ccache +sudo apt-get install libgoogle-perftools-dev numactl perl-doc +sudo apt-get install libfl2 libfl-dev # Ubuntu only (ignore errors) +sudo apt-get install zlibc zlib1g zlib1g-dev # Ubuntu only (ignore errors) + +# Set your desired install location +PREFIX=$HOME/.local/verilator + +git clone https://github.com/AS-SiliconMind/verilator.git /tmp/verilator +cd /tmp/verilator +git switch feat/cpp-linecount + +unset VERILATOR_ROOT +autoconf +./configure --prefix=$PREFIX make -j `nproc` make install -popd -rm -r /tmp/verilator +cd - +rm -rf /tmp/verilator -# Set the PATH environment variable -export PATH=${PREFIX}/bin:$PATH -echo export PATH=${PREFIX}/bin:\$PATH >> ~/.bashrc +# Add Verilator to PATH (current session + persist across logins) +export PATH=$PREFIX/bin:$PATH +echo 'export PATH=$HOME/.local/verilator/bin:$PATH' >> ~/.bashrc ``` #### Install Icarus Verilog (iVerilog) diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5a034c6 --- /dev/null +++ b/build.rs @@ -0,0 +1,4 @@ +fn main() { + // Embed the fsm_analyzer Python package + println!("cargo:rerun-if-changed=tools/fsm_analyzer"); +} diff --git a/docs/source/cli_reference.md b/docs/source/cli_reference.md deleted file mode 100644 index b95447e..0000000 --- a/docs/source/cli_reference.md +++ /dev/null @@ -1,162 +0,0 @@ -# CLI Reference - -This page provides a comprehensive reference for all the command-line flags available in `stg`. - -## Global Options - -These options can be used with any `stg` command. - -- `-h, --help`: Print the help message for a command or subcommand. -- `-V, --version`: Print the version of `stg`. - -## `stg generate` - -The `generate` command is used to create a new testbench. - -```bash -stg generate [OPTIONS] [VERILOG_FILES...] --out --type -``` - -**Note:** Verilog files can be specified either as positional arguments or using the `--verilog` flag. Both approaches are equivalent. - -### File and Module Options - -- `[VERILOG_FILES...]` or `--verilog `: (Required) Path to the DUT (Design Under Test) Verilog/SystemVerilog file(s). - - Positional: `stg generate dut.v --type combinational --out tb.sv` - - With flag: `stg generate --verilog dut.v --type combinational --out tb.sv` - - Multiple files: `stg generate dut1.v dut2.v ...` or `--verilog dut1.v --verilog dut2.v` -- `--module `: The name of the DUT (Device Under Test) module to be tested. This is strongly recommended if the Verilog file contains more than one module. If no `--module` is specified, we use a smart algorithm to decide the best module by parsing the module instantiation graph and selecting the one with the least in-degree. For multi-DUT testing, you can provide a comma-separated list (e.g., `--module dut0,dut1`) or use the option multiple times (e.g., `--module dut0 --module dut1`). If there are multiple DUTs but only one `--module` is specified, STG will reference all DUTs using the same module name. -**Note**: in SystemVerilog-mode, `--emplace-module` should be given so that there is no naming conflicts if the modules are having same name. However, in C++/System-C mode, this renaming mechanism is handled by Verilator automatically. -- `--golden `: Path to the golden reference model. This can be a Verilog/SystemVerilog file or a C++/SystemC header file (when using `--cc` or `--sc`). -- `--golden-module `: The name of the golden module, if it's a Verilog/SystemVerilog file with multiple modules. -- `--out `: (Required) The path for the generated testbench file (e.g., `tb.sv` or `tb.cpp`). -- `--out-exe `: The path for the compiled executable. If provided, `stg` will automatically compile the generated testbench. -- `--out-header `: In C++/SystemC mode (stage 1), this specifies the output path for the golden model header template. - -### Design Type and Signal Options - -- `--type `: (Required) The type of design being tested. - - `combinational`: For purely combinational logic. - - `seq_clocked`: For sequential designs with a clock. - - `seq_done`: For sequential designs that have a "done" or "valid" signal to indicate completion. -- `--clock `: The name of the clock signal (for `seq_clocked` and `seq_done` types). -- `--reset `: The name of the reset signal (for `seq_clocked` and `seq_done` types). -- `--reset-active `: The active polarity of the reset signal. Can be `high`, `low`, or `unknown`. -- `--done `: The name of the done/valid signal (for `seq_done` type). -- `--control-signals [...]`: A list of input signal names to be treated as control signals, which will be exhaustively tested. -- `--data-signals [...]`: A list of input signal names to be treated as data signals, which will be randomly sampled. - -### Test Generation Options - -- `--random-samples `: The number of random samples to generate for each combination of control signals. Default: `1024`. -- `--max-enumeration `: The maximum width of control signals to enumerate exhaustively (2^N). Default: `26`. -- `--timeout `: The simulation timeout in nanoseconds. Default: `1000000000`. -- `--debug`: Enables debug mode, which prints more detailed information during the test run. -- `--exit-on-error`: If set, the testbench will exit immediately upon the first mismatch. - -### Compilation and Verilator Options - -- `--verilator`: Use Verilator for compilation, which is much faster for large designs. -- `--verilator-mpi`: Enable MPI support for parallel test execution with Verilator. -- `--verilator-jobs `: The number of parallel jobs to use during Verilator compilation. Default: `4`. -- `--verilator-coverage`: Enable coverage analysis with Verilator. -- `--verilator-ignore-warnings`: Ignore common Verilator warnings like `WIDTHTRUNC`, `WIDTHCONCAT`, and `WIDTHEXPAND`. --g-rust/docs/source/cli_reference.md -- `--compile-flags `: Pass additional flags to the compiler (iverilog or Verilator). -- `--emplace-module`: Embed the DUT and golden modules directly into the testbench file. Useful for SV mode with naming conflicts. - -### C++/SystemC Options - -- `--cc`: Generate a C++ testbench using Verilator. -- `--sc`: Generate a SystemC testbench using Verilator. - -### Other Options - -- `--config `: Path to a YAML configuration file to load options from. - -## `stg identify` - -The `identify` command classifies the signals of a module. - -```bash -stg identify [OPTIONS] [VERILOG_FILE] --type --out -``` - -- `[VERILOG_FILE]` or `--verilog `: (Required) Path to the Verilog/SystemVerilog file. - - Positional: `stg identify module.v --type combinational --out signals.yaml` - - With flag: `stg identify --verilog module.v --type combinational --out signals.yaml` -- `--module `: The name of the module to identify signals from. -- `--type `: (Required) The design type (`combinational`, `seq_clocked`, `seq_done`). -- `--out `: (Required) The output path for the signal list in YAML format. -- `--config `: Path to a YAML configuration file. -- `--control-signals [...]`: Explicitly define control signals. -- `--data-signals [...]`: Explicitly define data signals. - -## `stg parse` - -The `parse` command parses Verilog/SystemVerilog files and generates a module priority list. - -```bash -stg parse [VERILOG_FILES...] --out -``` - -- `[VERILOG_FILES...]` or `--verilog `: (Required) Path to the Verilog/SystemVerilog file(s) to parse. - - Single file: `stg parse module.v --out modules.yaml` - - Multiple files (concatenated): `stg parse file1.v file2.v file3.v --out modules.yaml` - - With flag: `stg parse --verilog module.v --out modules.yaml` -- `--out `: (Required) Output path for the module priority list in YAML format. - -**Description:** - -The `parse` command analyzes Verilog/SystemVerilog files to extract module information and their dependencies. When multiple files are provided, they are automatically concatenated before parsing. It performs the following: - -1. Parses all modules in the specified file -2. Builds a module instantiation graph to understand dependencies -3. Calculates the total submodule count for each module (including nested submodules) -4. Sorts modules by priority using a smart algorithm: - - Modules with fewer dependencies (lower in-degree) come first - - Among modules with the same in-degree, those with more submodules come first -5. Outputs detailed information in YAML format, including module names, submodule counts, and port information - -**Output Format:** - -```yaml -- name: module_name - submodule_count: - ports: - - name: port_name - direction: input/output - width: -``` - -**Use Cases:** -- Understanding module hierarchy in complex designs -- Identifying the top-level module when `--module` is not specified in other commands -- Analyzing design complexity based on submodule counts -- Debugging module dependencies and instantiation relationships - -## `stg compile` - -The `compile` command compiles a user-provided testbench. - -```bash -stg compile [OPTIONS] [VERILOG_FILES...] --golden --testbench --out-exe -``` - -- `[VERILOG_FILES...]` or `--verilog `: (Required) Path to the DUT Verilog/SystemVerilog file(s). - - Single file: `stg compile dut.v --golden golden.v --testbench tb.sv --out-exe exe` - - Multiple files: `stg compile dut1.v dut2.v --golden golden.v --testbench tb.sv --out-exe exe` - - With flag: `stg compile --verilog dut.v --golden golden.v --testbench tb.sv --out-exe exe` -- `--module `: The name of the DUT module. -- `--golden `: (Required) Path to the golden reference file. -- `--testbench `: (Required) Path to the testbench file (`.sv` or `.cpp`). -- `--out-exe `: (Required) The path for the compiled executable. -- `--compile-flags `: Additional compiler flags. -- `--emplace-module`: Embed DUT and golden modules in the testbench. -- `--verilator`: Use Verilator for compilation. -- `--verilator-mpi`: Enable MPI support with Verilator. -- `--verilator-ignore-warnings`: Ignore common Verilator warnings. -- `--verilator-jobs `: Number of jobs for Verilator compilation. -- `--verilator-coverage`: Enable coverage analysis. -- `--cc`: For C++ testbenches. -- `--sc`: For SystemC testbenches. diff --git a/docs/source/conf.py b/docs/source/conf.py index 5513af1..03bcd6b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,9 +7,9 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'STG-Rust' -copyright = '2025, AS-SiliconMind' +copyright = '2025-2026, AS-SiliconMind' author = 'AS-SiliconMind' -release = '0.3.2' +release = '0.4.0-beta' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/examples.md b/docs/source/examples.md new file mode 100644 index 0000000..04d95e9 --- /dev/null +++ b/docs/source/examples.md @@ -0,0 +1,194 @@ +# Examples + +The repository includes several examples demonstrating different design types and testbench modes. Each example is in the `examples/` directory with its own README and Makefile. + +## Combinational Logic — ALU + +### SystemVerilog Mode + +```bash +cd examples/ALU + +stg generate \ + --verilog gate_level.v \ + --module alu_gate_level \ + --golden golden.v \ + --golden-module alu_golden \ + --type combinational \ + --out tb_alu.sv \ + --out-exe tb_alu_exe \ + --control-signals op + +./tb_alu_exe +# Generates test_stats.json with detailed statistics +``` + +### C++ Mode + +```bash +cd examples/ALU_cc + +# Stage 1: Generate template +stg generate \ + --verilog gate_level.v \ + --module alu_gate_level \ + --type combinational \ + --out tb.cpp \ + --out-header my_golden.h \ + --cc \ + --control-signals op + +# Stage 2: Edit my_golden.h, then compile +stg generate \ + --verilog gate_level.v \ + --module alu_gate_level \ + --golden my_golden.h \ + --type combinational \ + --out tb.cpp \ + --out-exe tb_exe \ + --cc \ + --control-signals op + +./tb_exe +``` + +## Sequential Clocked — Counter + +### SystemVerilog Mode + +```bash +cd examples/pingpong + +stg generate \ + --verilog up_only.v \ + --module counter_up_only \ + --golden golden.v \ + --golden-module counter_golden \ + --type seq_clocked \ + --out tb.sv \ + --out-exe tb_exe \ + --clock clk \ + --reset rst_n \ + --reset-active low \ + --random-samples 1000 + +./tb_exe +``` + +### SystemC Golden Model + +```bash +cd examples/pingpong_sc + +stg generate \ + --verilog up_only.v \ + --golden golden_model_sc.h \ + --type seq_clocked \ + --out tb.cpp \ + --out-exe tb_exe \ + --cc \ + --clock clk \ + --reset rst_n \ + --reset-active low \ + --random-samples 100 + +./tb_exe +``` + +## Sequential with Done Signal — GCD + +```bash +cd examples/GCD + +stg generate \ + --verilog gcd_buggy.v \ + --module gcd \ + --golden gcd_golden.v \ + --golden-module gcd_golden \ + --type seq_done \ + --out tb.sv \ + --out-exe tb_exe \ + --clock clk \ + --reset rst \ + --reset-active high \ + --done done \ + --control-signals "" \ + --data-signals a b \ + --random-samples 100 + +./tb_exe +``` + +## Multi-DUT Comparison — ALU Implementations + +Compare multiple ALU implementations against the same golden model: + +```bash +cd examples/multi_dut/ALU + +stg generate \ + --verilog dut1_buggy_add.v --module alu_v1 \ + --verilog dut2_buggy_sub.v --module alu_v2 \ + --verilog dut3_same_name.v --module alu_v1 \ + --golden golden_model.h \ + --type combinational \ + --out tb_multi.cpp \ + --out-exe tb_multi_exe \ + --cc \ + --control-signals op \ + --random-samples 500 + +./tb_multi_exe +cat test_stats.json +``` + +**Example output:** +```json +{ + "dut0": {"out": {"tests": 4000, "success": 3500, "score": 87.50}}, + "dut1": {"out": {"tests": 4000, "success": 3000, "score": 75.00}}, + "dut2": {"out": {"tests": 4000, "success": 4000, "score": 100.00}} +} +``` + +## FSM Coverage — Traffic Light + +```bash +cd examples/traffic_light + +stg generate-fsm traffic_light_controller.sv \ + --golden traffic_light_controller_golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --clock clk \ + --reset rst_n \ + --reset-active low \ + --fsm-method deterministic \ + --verilator-coverage + +./tb +``` + +## FSM Coverage — Sequence Detector + +```bash +cd examples/seq_detector + +# Set API key: export GOOGLE_API_KEY="..." +stg generate-fsm seq_detector.sv \ + --golden seq_detector_golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --fsm-method lm \ + --lm-provider gemini \ + --state-analysis state_analysis.json + +./tb +``` + +## See Also + +- [Getting Started](getting_started.md) — Installation and quick start +- [Modes Overview](user_guide/modes_overview.md) — Understanding modes and design types +- [Multi-DUT Support](user_guide/multi_dut.md) — Multi-DUT details +- [FSM Coverage](user_guide/fsm_coverage.md) — FSM-based testing guide diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md new file mode 100644 index 0000000..9b23352 --- /dev/null +++ b/docs/source/getting_started.md @@ -0,0 +1,188 @@ +# Getting Started + +A high-performance Rust implementation of the [Structured Testbench Generation](https://github.com/AS-SiliconMind/Structured-Testbench-Generation) tool for automated Verilog/SystemVerilog testbench creation. + +## What is STG? + +STG automatically generates comprehensive testbenches for digital designs by: + +- Parsing your Verilog/SystemVerilog modules +- Classifying signals (control vs. data, inputs vs. outputs) +- Generating semi-exhaustive test patterns +- Comparing DUT (Design Under Test) against a golden reference +- Supporting both SystemVerilog and C++/SystemC testbenches + +The Rust implementation offers: + +- **Better error messages** with detailed context +- **Type safety** preventing entire classes of bugs +- **Native binary** — no Python interpreter required; the compiled binary can be shipped anywhere + +## Prerequisites + +- Rust toolchain (1.70 or later) +- [Custom Verilator](https://github.com/AS-SiliconMind/verilator/) (branch `feat/cpp-linecount`, based on v5.044) +- [iverilog](http://iverilog.icarus.com/) (optional, for fallback parsing) + +## Installation + +### Install Rust + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +# Restart terminal to update environment variables +``` + +### Install STG System-Wide (Recommended) + +```bash +cargo install --path . +``` + +This compiles `stg` and installs it to `$HOME/.cargo/bin/`. By default, after installing Rust's toolchain, `$HOME/.cargo/bin` is added to your `$PATH`, so you can invoke `stg` directly from anywhere. + +### Build from Source + +```bash +cd stg-rust +cargo build --release +``` + +The binary will be available at `target/release/stg`. + +### Install Verilator + +This repository requires a [custom Verilator](https://github.com/AS-SiliconMind/verilator/) (modified from Verilator v5.044) on the `feat/cpp-linecount` branch. The stock Verilator from your Linux distribution will **not** work. + +Install the build prerequisites first: + +```bash +# Prerequisites (Ubuntu/Debian): +sudo apt-get install git help2man perl python3 make autoconf g++ flex bison ccache +sudo apt-get install libgoogle-perftools-dev numactl perl-doc +sudo apt-get install libfl2 libfl-dev # Ubuntu only (ignore errors) +sudo apt-get install zlibc zlib1g zlib1g-dev # Ubuntu only (ignore errors) +``` + +Then build and install to a custom path: + +```bash +# Set your desired install location +PREFIX=$HOME/.local/verilator + +git clone https://github.com/AS-SiliconMind/verilator.git /tmp/verilator +cd /tmp/verilator +git witch feat/cpp-linecount + +unset VERILATOR_ROOT +autoconf +./configure --prefix=$PREFIX +make -j `nproc` +make install +cd - +rm -rf /tmp/verilator + +# Add Verilator to PATH (current session + persist across logins) +export PATH=$PREFIX/bin:$PATH +echo 'export PATH=$HOME/.local/verilator/bin:$PATH' >> ~/.bashrc +``` + +You can change `PREFIX` to any path you prefer (e.g., `$HOME/.usr`, `/opt/verilator`). Just make sure `$PREFIX/bin` is on your `PATH`. + +### Install Icarus Verilog (iVerilog) + +STG uses iVerilog v11 as a fallback parser when [sv-parser](https://github.com/dalance/sv-parser) cannot parse modules correctly. It is recommended but not strictly required. + +```bash +IVERILOG_VERSION=v11-branch +PREFIX=$HOME/.usr + +pushd /tmp/ +git clone https://github.com/steveicarus/iverilog.git +cd iverilog +git checkout ${IVERILOG_VERSION} + +sh autoconf.sh +./configure --prefix=${PREFIX} +make -j `nproc` +make install +popd +rm -r /tmp/iverilog + +export PATH=${PREFIX}/bin:$PATH +echo export PATH=${PREFIX}/bin:\$PATH >> ~/.bashrc +``` + +### Running Tests + +```bash +cargo test +``` + +See [tests/README.md](https://github.com/AS-SiliconMind/stg-rust/blob/main/tests/README.md) for detailed test information. + +## Quick Start + +### SystemVerilog Mode + +```bash +stg generate \ + --verilog examples/ALU/gate_level.v \ + --module alu_gate_level \ + --golden examples/ALU/golden.v \ + --golden-module alu_golden \ + --type combinational \ + --out tb_alu.sv \ + --out-exe tb_alu_exe \ + --control-signals op + +./tb_alu_exe +``` + +### C++ Mode (Two-Stage Workflow) + +**Stage 1: Generate golden model template** +```bash +stg generate \ + --verilog examples/ALU/gate_level.v \ + --type combinational \ + --out tb.cpp \ + --out-header golden_model.h \ + --cc \ + --control-signals op +``` + +**Stage 2: Implement golden model in `golden_model.h`, then compile** +```bash +stg generate \ + --verilog examples/ALU/gate_level.v \ + --golden golden_model.h \ + --type combinational \ + --out tb.cpp \ + --out-exe tb_exe \ + --cc \ + --control-signals op + +./tb_exe +``` + +STG also supports SystemC — replace `--cc` with `--sc`. + +## Available Commands + +| Command | Description | +|---------|-------------| +| `stg generate` | Generate testbench (and optionally compile) | +| `stg generate-fsm` | Generate FSM-coverage-enhanced testbench | +| `stg identify` | Identify and classify signals in a module | +| `stg parse` | Parse modules and show hierarchy | +| `stg compile` | Compile a user-provided testbench | + +Run `stg --help` or `stg --help` for detailed options. + +## What's Next? + +- [Modes Overview](user_guide/modes_overview.md) — Understand the three testbench modes and design types +- [SystemVerilog Mode](user_guide/sv_mode.md) — Full SV workflow guide +- [C++/SystemC Mode](user_guide/cc_sc_mode.md) — Two-stage CC/SC workflow guide +- [CLI Reference](reference/cli.md) — Complete flag reference for all commands diff --git a/docs/source/index.rst b/docs/source/index.rst index 3a15a27..e7fef25 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,18 +1,37 @@ -.. STG-Rust documentation master file, created by - sphinx-quickstart on Wed Jul 31 15:51:53 2024. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - Welcome to STG-Rust's documentation! ====================================== +STG (Structured Testbench Generation) is a high-performance Rust tool that automatically generates comprehensive testbenches for Verilog/SystemVerilog designs. + +.. toctree:: + :maxdepth: 2 + :caption: Getting Started + + getting_started + +.. toctree:: + :maxdepth: 2 + :caption: User Guide + + user_guide/modes_overview + user_guide/sv_mode + user_guide/cc_sc_mode + user_guide/multi_dut + user_guide/fsm_coverage + user_guide/advanced + +.. toctree:: + :maxdepth: 2 + :caption: Reference + + reference/cli + .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Additional Resources - intro.md - usage.md - cli_reference.md + examples + troubleshooting Indices and tables diff --git a/docs/source/intro.md b/docs/source/intro.md deleted file mode 100644 index d137b2f..0000000 --- a/docs/source/intro.md +++ /dev/null @@ -1,179 +0,0 @@ -# STG-Rust - Structured Testbench Generation - -A high-performance Rust implementation of the [Structured Testbench Generation](https://github.com/AS-SiliconMind/Structured-Testbench-Generation) tool for automated Verilog/SystemVerilog testbench creation. - -## What is STG? - -STG automatically generates comprehensive testbenches for digital designs by: -- Parsing your Verilog/SystemVerilog modules -- Classifying signals (control vs. data, inputs vs. outputs) -- Generating semi-exhaustive test patterns -- Comparing DUT (Design Under Test) against a golden reference -- Supporting both SystemVerilog and C++/SystemC testbenches - -## Installation - -### Prerequisites -- Rust toolchain (1.70 or later) -- iverilog (for SystemVerilog mode) or Verilator (for high-performance testing) - -### Install Rust -```bash -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -# Restart terminal to update environment variables -``` -### Install System-Wide (Recommended) -```bash -cargo install --path . -``` - -This process compiles `stg` on your computer and installs it to `$HOME/.cargo/bin/`. The compilation process involves compiling third-party dependencies, but it will not take long. -By default, after installing Rust's toolchain, `$HOME/.cargo/bin` will be added to your `$PATH` environment variable. Therefore, you can invoke `stg` directly from anywhere, without setting up a virtual environment like Python. - -#### Install Verilator -This repository is tested using Verilator v5.020, and older version which is shipped with the Linux distribution may not be supported. Use the following script to install a newer Verilator from source. -```bash -VERILATOR_VERSION=v5.020 -PREFIX=$HOME/.usr - -pushd /tmp/ -git clone https://github.com/verilator/verilator.git -cd verilator -git checkout v5.020 -# Every time you need to build: -unset VERILATOR_ROOT # For bash -autoconf # Create ./configure script -./configure --prefix=$PREFIX # Configure and create Makefile -make -j `nproc` -make install -popd -rm -r /tmp/verilator - -# Set the PATH environment variable -export PATH=${PREFIX}/bin:$PATH -echo export PATH=${PREFIX}/bin:\$PATH >> ~/.bashrc -``` - -#### Install Icarus Verilog (iVerilog) -This repository utilizes iVerilog v11 for module's name and port parsing if [sv-parser](https://github.com/dalance/sv-parser) does not parse the modules correctly. -It is recommended to install iVerilog, but it is not nessecerly. -``` -IVERILOG_VERSION=v11-branch -PREFIX=$HOME/.usr - -pushd /tmp/ -git clone https://github.com/steveicarus/iverilog.git -cd iverilog -git checkout ${IVERILOG_VERSION} - -sh autoconf.sh -./configure --prefix=${PREFIX} -make -j `nproc` -make install -popd -rm -r /tmp/iverilog - -# Set the PATH environment variable -export PATH=${PREFIX}/bin:$PATH -echo export PATH=${PREFIX}/bin:\$PATH >> ~/.bashrc -``` - -### Build STG from Source -```bash -cd stg-rust -cargo build --release -``` - -The binary will be available at `target/release/stg`. - -### Tests -A group of tests are provided to verify if the current stg works as expected, but it does not cover all combinations. Check [tests/README.md](tests/README.md) for detailed information. -``` -# Perform all tests -cargo test -``` - -## Quick Start - -### Basic Testbench Generation (SystemVerilog) - -```bash -stg generate \ - --verilog examples/ALU/gate_level.v \ - --module alu_gate_level \ - --golden examples/ALU/golden.v \ - --golden-module alu_golden \ - --type combinational \ - --out tb_alu.sv \ - --out-exe tb_alu_exe \ - --control-signals op - -# Run the testbench -./tb_alu_exe -``` - -### C++ Testbench with Custom Golden Model in C++ -STG also supports System-C, replace `--cc` with `--sc` to use System-C. - -**Stage 1: Generate template** -```bash -stg generate \ - --verilog examples/ALU/gate_level.v \ - --type combinational \ - --out tb.cpp \ - --out-header golden_model.h \ - --cc \ - --control-signals op -``` - -**Stage 2: Implement golden model in `golden_model.h`, then compile** -```bash -stg generate \ - --verilog examples/ALU/gate_level.v \ - --golden golden_model.h \ - --type combinational \ - --out tb.cpp \ - --out-exe tb_exe \ - --cc \ - --control-signals op - -# Run the testbench -./tb_exe -``` - - -## Performance Benefits - -The Rust implementation offers: -- **Better error messages** with detailed context -- **Type safety** preventing entire classes of bugs -- **Native binary** - no Python interpreter required, the compiled binary can be shipped to anywhere - -## Documentation - -- **[USAGE.md](USAGE.md)** - Comprehensive usage guide covering: - - SystemVerilog mode (traditional) - - C++/SystemC mode (two-stage workflow) - - Compilation options and flags - - Advanced features and examples - -## Available Commands - -- `stg generate` - Generate testbench (and optionally compile) -- `stg identify` - Identify and classify signals in a module -- `stg compile` - Compile user-provided testbench files - -Run `stg --help` or `stg --help` for detailed options. - -## Examples - -The repository includes several examples demonstrating different use cases: -- `examples/ALU/` - Combinational logic (ALU with 8 operations) -- `examples/ALU_cc/` - Same ALU with C++ testbench -- `examples/pingpong/` - Sequential clocked design (counter) -- `examples/pingpong_sc/` - Same counter with SystemC golden model -- `examples/GCD/` - seq_done design (GCD algorithm) - -## License - -MIT (same as the Python version) diff --git a/docs/source/reference/cli.md b/docs/source/reference/cli.md new file mode 100644 index 0000000..e79b6f9 --- /dev/null +++ b/docs/source/reference/cli.md @@ -0,0 +1,222 @@ +# CLI Reference + +This page provides a complete reference for all command-line options in `stg`. + +## Global Options + +- `-h, --help` — Print the help message for a command or subcommand +- `-V, --version` — Print the version of `stg` + +--- + +## `stg generate` + +Generate a testbench and optionally compile it. + +```bash +stg generate [OPTIONS] [VERILOG_FILES...] --out --type +``` + +Verilog files can be specified as positional arguments or with `--verilog`. Both are equivalent. + +### File and Module Options + +- `[VERILOG_FILES...]` or `--verilog ` — **(Required)** Path to DUT Verilog file(s). + - Single: `stg generate dut.v ...` or `--verilog dut.v` + - Multiple: `stg generate dut1.v dut2.v ...` or `--verilog dut1.v --verilog dut2.v` +- `--module ` — DUT module name(s). Strongly recommended when a file contains multiple modules. For multi-DUT, use comma-separated names (`--module dut0,dut1`) or repeat the flag (`--module dut0 --module dut1`). A single name is used for all DUTs. +- `--golden ` — Path to the golden reference (Verilog file or C++/SystemC header). +- `--golden-module ` — Golden module name (for Verilog golden files with multiple modules). +- `--out ` — **(Required)** Output testbench path (e.g., `tb.sv` or `tb.cpp`). +- `--out-exe ` — Compile and output an executable. If provided, STG compiles automatically. +- `--out-header ` — Output path for golden model header template (CC/SC stage 1). + +### Design Type and Signal Options + +- `--type ` — **(Required)** Design type: + - `combinational` — Pure combinational logic + - `seq_clocked` — Sequential design with clock + - `seq_done` — Sequential design with done/valid signal +- `--clock ` — Clock signal name (for `seq_clocked` and `seq_done`). +- `--reset ` — Reset signal name. +- `--reset-active ` — Reset polarity: `high`, `low`, or `unknown`. +- `--done ` — Done/valid signal name (for `seq_done`). +- `--control-signals [...]` — Signals to enumerate exhaustively. +- `--data-signals [...]` — Signals to sample randomly. + +### Test Generation Options + +- `--random-samples ` — Random samples per control vector. Default: `1024`. +- `--max-enumeration ` — Max control signal width to enumerate (2^N). Default: `26`. +- `--timeout ` — Simulation timeout in nanoseconds. Default: `1000000000`. +- `--debug` — Enable debug output with detailed signal values. +- `--exit-on-error` — Exit immediately on first mismatch. + +### Compilation and Verilator Options + +- `--verilator` — Use Verilator instead of iverilog. +- `--verilator-mpi` — Enable MPI for parallel test execution. +- `--verilator-jobs ` — Parallel Verilator compilation jobs. Default: `4`. +- `--verilator-coverage` — Enable coverage instrumentation. +- `--verilator-ignore-warnings` — Ignore common width warnings. +- `--compile-flags ` — Additional compiler flags. +- `--emplace-module` — Embed DUT and golden modules in the testbench file (SV mode only). + +### C++/SystemC Options + +- `--cc` — Generate a C++ testbench using Verilator. +- `--sc` — Generate a SystemC testbench using Verilator. + +### Other Options + +- `--config ` — Load options from a YAML configuration file. + +--- + +## `stg generate-fsm` + +Generate an FSM-coverage-enhanced C++ testbench using DFS over the state graph. + +```bash +stg generate-fsm [OPTIONS] [VERILOG_FILES...] --golden --out +``` + +### Required Options + +- `[VERILOG_FILES...]` or `--verilog ` — DUT Verilog file(s). +- `--golden ` — Golden reference Verilog file. +- `--out ` — Output testbench path (`.cpp`). + +### Common Options + +- `--module ` — DUT module name. Default: first module in file. +- `--golden-module ` — Golden module name. Default: first module in file. +- `--out-exe ` — Also compile to executable. +- `--clock ` — Clock signal name. Default: `clk`. +- `--reset ` — Reset signal name. Default: `rst`. +- `--reset-active ` — Reset polarity: `high` or `low`. Default: `low`. +- `--fsm-method ` — FSM extraction method: `lm` or `deterministic`. Default: `lm`. +- `--state-analysis ` — Load/save FSM analysis JSON (caching). + +### FSM / LM Options + +- `--lm-provider ` — LLM provider: `gemini`, `openai`, `openrouter`. +- `--lm-name ` — Model name (e.g., `gemini-2.5-flash`). +- `--lm-endpoint ` — Custom API endpoint. +- `--dfs-passes ` — DFS passes with random data. Default: `5`. +- `--transition-timeout ` — Clock cycles to wait for transition conditions. Default: `100`. +- `--random-samples ` — Extra random samples after DFS. Default: `32`. + +### Compilation Options + +- `--verilator-coverage` — Enable coverage instrumentation. +- `--line-coverage-check` — (generate-fsm only) Require runtime line/block counter increments when validating transitions. Auto-enables Verilator `--coverage` during compile. +- `--compile-flags ` — Extra flags passed to Verilator. +- `--verilator-jobs ` — Parallel compile jobs. + +--- + +## `stg identify` + +Classify signals in a Verilog module. + +```bash +stg identify [OPTIONS] [VERILOG_FILE] --type --out +``` + +- `[VERILOG_FILE]` or `--verilog ` — **(Required)** Path to Verilog file. +- `--module ` — Module name to identify signals from. +- `--type ` — **(Required)** Design type (`combinational`, `seq_clocked`, `seq_done`). +- `--out ` — **(Required)** Output path for signal list (YAML). +- `--config ` — YAML configuration file. +- `--control-signals [...]` — Explicitly define control signals. +- `--data-signals [...]` — Explicitly define data signals. + +**Output format:** +```yaml +clock_inputs: [clk] +reset_inputs: [rst_n] +done_outputs: [] +control_inputs: [op, mode] +data_inputs: [a, b, data_in] +outputs: [result, valid] +reset_active_high: false +``` + +--- + +## `stg parse` + +Parse Verilog files and generate a module priority list based on the instantiation hierarchy. + +```bash +stg parse [VERILOG_FILES...] --out +``` + +- `[VERILOG_FILES...]` or `--verilog ` — **(Required)** Verilog file(s) to parse. Multiple files are concatenated before parsing. +- `--out ` — **(Required)** Output path for module list (YAML). + +The command: +1. Parses all modules in the file +2. Builds a module instantiation graph +3. Calculates total submodule counts (including nested) +4. Sorts by priority: fewer dependencies first, then by submodule complexity + +**Output format:** +```yaml +- name: submodule_a + submodule_count: 0 + ports: + - name: in + direction: input + width: 8 + - name: out + direction: output + width: 8 +- name: top_module + submodule_count: 5 + ports: + - name: clk + direction: input + width: 1 + - name: data_out + direction: output + width: 32 +``` + +--- + +## `stg compile` + +Compile a user-provided testbench with DUT and golden model. + +```bash +stg compile [OPTIONS] [VERILOG_FILES...] --golden --testbench --out-exe +``` + +- `[VERILOG_FILES...]` or `--verilog ` — **(Required)** DUT Verilog file(s). +- `--module ` — DUT module name. +- `--golden ` — **(Required)** Golden reference file. +- `--testbench ` — **(Required)** Testbench file (`.sv` or `.cpp`). +- `--out-exe ` — **(Required)** Output executable path. +- `--compile-flags ` — Additional compiler flags. +- `--emplace-module` — Embed DUT and golden modules in testbench. +- `--verilator` — Use Verilator for compilation. +- `--verilator-mpi` — Enable MPI support. +- `--verilator-ignore-warnings` — Ignore common Verilator warnings. +- `--verilator-jobs ` — Parallel Verilator jobs. +- `--verilator-coverage` — Enable coverage analysis. +- `--cc` — For C++ testbenches. +- `--sc` — For SystemC testbenches. + +--- + +## Runtime Arguments + +These arguments are passed to the compiled testbench executable (not to `stg`): + +- `+STATS_FILE=` — Custom path for the statistics JSON output. Default: `test_stats.json`. + +```bash +./tb_exe +STATS_FILE=my_results.json +``` diff --git a/docs/source/troubleshooting.md b/docs/source/troubleshooting.md new file mode 100644 index 0000000..90070bb --- /dev/null +++ b/docs/source/troubleshooting.md @@ -0,0 +1,135 @@ +# Troubleshooting + +Common issues and solutions when using STG. + +## Verilog Parsing Issues + +### "No modules found in DUT file" + +Make sure your Verilog file contains valid module definitions and iverilog is installed: + +```bash +iverilog -t null -g2009 your_file.v +``` + +You can also use `stg parse` to inspect what modules are detected: + +```bash +stg parse --verilog your_file.v --out modules.yaml +``` + +### "Multiple modules found, please specify --module" + +Your file contains multiple module definitions. Specify which one to test: + +```bash +stg generate --verilog file.v --module my_module ... +``` + +Use `stg parse` to see all available modules and their hierarchy. + +### Parser fallback + +STG uses a hybrid parsing approach: [sv-parser](https://github.com/dalance/sv-parser) for Rust-native parsing, with iverilog as a fallback. If sv-parser fails on your file, ensure iverilog v11+ is installed. Use `--debug` for detailed parser output. + +## C++/SystemC Mode Issues + +### "There are two stages to compile the testbench" + +You're in stage 1 of the CC/SC workflow. First generate the template: + +```bash +stg generate --verilog dut.v --type combinational --out tb.cpp --out-header golden.h --cc +``` + +Then implement the golden model in `golden.h` and run stage 2: + +```bash +stg generate --verilog dut.v --golden golden.h --type combinational --out tb.cpp --out-exe exe --cc +``` + +See the [C++/SystemC Mode Guide](user_guide/cc_sc_mode.md) for the full two-stage workflow. + +## Compilation Issues + +### Verilator compilation errors + +Try ignoring width warnings: + +```bash +stg generate ... --verilator --compile-flags -Wno-WIDTH +``` + +### Verilator version compatibility + +STG requires Verilator v5.020 or later. Older versions shipped with Linux distributions may not support required features. See the [installation guide](getting_started.md#install-verilator) for building from source. + +### Module name conflicts (SV mode) + +When multiple Verilog files contain modules with the same name, compilation will fail. In SV mode, use `--emplace-module` to add prefixes: + +```bash +stg generate --verilog dut1.v --verilog dut2.v --golden golden.v \ + --type combinational --out tb.sv --emplace-module +``` + +In CC/SC mode, Verilator handles renaming automatically. + +## Signal Classification Issues + +### Important signals misclassified + +If control signals are classified as data (or vice versa), specify them explicitly: + +```bash +stg generate ... --control-signals op mode --data-signals a b +``` + +Use `stg identify` to preview the automatic classification: + +```bash +stg identify --verilog dut.v --module my_module --type combinational --out signals.yaml +``` + +## FSM Coverage Issues + +### "No state machines found" + +The design may not contain a detectable FSM, or the coding style is not recognized by the deterministic parser. Try LLM-based extraction: + +```bash +stg generate-fsm design.sv --golden golden.sv --out tb.cpp --fsm-method lm --lm-provider gemini +``` + +### LM method fails + +- Ensure `uv` is installed: +- Check API keys in `.env` or environment variables +- Supported keys: `GOOGLE_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY` + +### Missed states in coverage + +- Increase `--dfs-passes` for more edge coverage +- Increase `--transition-timeout` if internal conditions (counters, timers) take many cycles +- Inspect the extracted FSM with `--state-analysis fsm.json` and verify correctness + +## Runtime Issues + +### Non-deterministic test results + +Random test generation uses different seeds across runs. For reproducibility, consider using a fixed seed (if supported) or saving the `test_stats.json` output for comparison. + +### Coverage file conflicts with MPI + +In SV mode, MPI processes write to the same `coverage.dat` file, which causes conflicts. Use CC/SC mode for MPI coverage — rank-specific filenames are generated automatically. + +### Path issues + +Relative paths may break in different execution contexts. Use absolute paths or ensure you run STG from the correct working directory. + +## Getting Help + +- Run `stg --help` or `stg --help` for command-line usage +- Use `--debug` for detailed diagnostic output +- Check the [CLI Reference](reference/cli.md) for all available options +- See the [examples](examples.md) for working configurations diff --git a/docs/source/usage.md b/docs/source/usage.md deleted file mode 100644 index 67d5b7d..0000000 --- a/docs/source/usage.md +++ /dev/null @@ -1,1030 +0,0 @@ -# STG Usage Guide - -This guide provides comprehensive documentation for using STG (Structured Testbench Generation) in different modes and scenarios. - -## Table of Contents - -- [Modes Overview](#modes-overview) -- [SystemVerilog Mode](#systemverilog-mode-traditional) -- [C++/SystemC Mode](#ccsc-mode) -- [Command Reference](#command-reference) -- [Multi-DUT Support](#multi-dut-support) -- [Advanced Features](#advanced-features) -- [Examples by Design Type](#examples-by-design-type) - ---- - -## Modes Overview - -STG supports two main testbench generation modes SV and CC/SC mode: - -| Mode | Testbench Language | Golden Model | Compiler | Use Case | -|------|-------------------|--------------|----------|----------| -| **SV** (Traditional) | SystemVerilog | Verilog/SystemVerilog | iverilog or Verilator | Standard workflow, easy setup | -| **CC** | C++ | C++ header | Verilator | Custom golden model, SystemC types optional | -| **SC** | C++ with SystemC | SystemC header | Verilator | Full SystemC support with `sc_uint` types | - ---- - -## SystemVerilog Mode (Traditional) - -### Basic Workflow - -In SV mode, both your DUT and golden reference are Verilog/SystemVerilog modules. - -**Note:** Verilog files can be specified as positional arguments or with `--verilog` flag. Both styles are equivalent: -```bash -# Using positional arguments (shorter) -stg generate path/to/dut.v --golden path/to/golden.v --type --out testbench.sv - -# Using --verilog flag (explicit) -stg generate --verilog path/to/dut.v --golden path/to/golden.v --type --out testbench.sv -``` - -#### Generate Testbench Only - -```bash -# Using positional arguments -stg generate \ - path/to/dut.v \ - --module dut_module_name \ - --golden path/to/golden.v \ - --golden-module golden_module_name \ - --type \ - --out testbench.sv - -# Or using --verilog flag -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --golden path/to/golden.v \ - --golden-module golden_module_name \ - --type \ - --out testbench.sv -``` - -#### Generate and Compile (iverilog) - -```bash -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --golden path/to/golden.v \ - --golden-module golden_module_name \ - --type \ - --out testbench.sv \ - --out-exe testbench_exe - -# Run the testbench -./testbench_exe -``` - -#### Generate and Compile (Verilator) - -```bash -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --golden path/to/golden.v \ - --golden-module golden_module_name \ - --type \ - --out testbench.sv \ - --out-exe testbench_exe \ - --verilator - -# Run the testbench -./testbench_exe -``` - -#### With Verilator MPI (Parallel Execution) - -```bash -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --golden path/to/golden.v \ - --golden-module golden_module_name \ - --type \ - --out testbench.sv \ - --out-exe testbench_exe \ - --verilator-mpi - -# Run with 4 MPI processes -mpirun -np 4 ./testbench_exe -``` - -### Recompiling Modified Testbench - -If you modify the generated testbench file manually, use `stg compile`: - -```bash -stg compile \ - --verilog path/to/dut.v \ - --golden path/to/golden.v \ - --testbench testbench.sv \ - --out-exe testbench_exe -``` - -With Verilator: - -```bash -stg compile \ - --verilog path/to/dut.v \ - --golden path/to/golden.v \ - --testbench testbench.sv \ - --out-exe testbench_exe \ - --verilator -``` - ---- - -## CC/SC Mode - -C++/SystemC mode provides a **two-stage workflow** where you implement a golden model in C++ or SystemC, allowing for more flexible and powerful reference implementations. - -### CC Mode (C++ Testbench) - -Use `--cc` for C++ testbenches with optional SystemC data types in the golden model. - -#### Stage 1: Generate Golden Model Template - -```bash -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --type \ - --out testbench.cpp \ - --out-header golden_model.h \ - --cc \ - [additional options...] -``` - -**What this does:** -- Generates `golden_model.h` - A C++ header template with signal declarations -- Generates `testbench.cpp` - The C++ testbench that includes your golden model -- You need to implement the golden model logic in the header file - -**Generated golden model structure:** -```cpp -#ifndef GOLDEN_MODEL_H -#define GOLDEN_MODEL_H - -#include - -class GoldenModel { -public: - // Input signals - uint8_t a; - uint8_t b; - uint8_t op; - - // Output signals - uint8_t out; - - // Implement your golden model logic here - void eval() { - // TODO: Implement combinational logic - } - - // For sequential designs - void posedge_clk() { - // TODO: Implement sequential logic - } -}; - -#endif -``` - -#### Stage 2: Implement Golden Model - -Edit `golden_model.h` to implement your golden model. Example for an ALU: - -```cpp -void eval() { - switch (op) { - case 0: out = a + b; break; - case 1: out = a - b; break; - case 2: out = a & b; break; - case 3: out = a | b; break; - case 4: out = a ^ b; break; - case 5: out = a << b; break; - case 6: out = a >> b; break; - case 7: out = (int8_t)a >> b; break; // arithmetic shift - } -} -``` - -#### Stage 3: Compile with Implemented Golden Model - -```bash -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --golden golden_model.h \ - --type \ - --out testbench.cpp \ - --out-exe testbench_exe \ - --cc \ - [additional options...] - -# Run the testbench -./testbench_exe -``` - -### SC Mode (SystemC Testbench) - -Use `--sc` for SystemC-style golden models with `sc_uint` data types. - -#### Stage 1: Generate SystemC Golden Model Template - -```bash -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --type \ - --out testbench.cpp \ - --out-header golden_model_sc.h \ - --sc \ - [additional options...] -``` - -**Generated SystemC golden model:** -```cpp -#ifndef GOLDEN_MODEL_SC_H -#define GOLDEN_MODEL_SC_H - -#include - -class GoldenModel { -public: - // Using SystemC types for inputs - sc_dt::sc_uint<4> a; - sc_dt::sc_uint<4> b; - sc_dt::sc_uint<3> op; - - // Using SystemC types for outputs - sc_dt::sc_uint<4> out; - - void eval() { - // TODO: Implement combinational logic using sc_uint - } -}; - -#endif -``` - -#### Stage 2: Implement with SystemC Types - -```cpp -void posedge_clk() { - if (rst_n == 0) { - out = 0; - return; - } - - if (en) { - // Use .to_uint() for arithmetic operations - uint32_t val = out.to_uint(); - out = (val + 1) & 0xF; - } -} -``` - -#### Stage 3: Compile with SystemC Golden Model - -```bash -stg generate \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --golden golden_model_sc.h \ - --type \ - --out testbench.cpp \ - --out-exe testbench_exe \ - --sc \ - [additional options...] - -./testbench_exe -``` - -### Recompiling Modified C++/SystemC Testbench - -If you modify the generated C++ testbench, use `stg compile`: - -```bash -stg compile \ - --verilog path/to/dut.v \ - --module dut_module_name \ - --golden golden_model.h \ - --testbench testbench.cpp \ - --out-exe testbench_exe \ - --cc -``` - ---- - -## Command Reference - -### `stg generate` - -Generate a testbench and optionally compile it. - -#### Required Arguments - -| Argument | Description | Example | -|----------|-------------|---------| -| `--verilog ` | Path to DUT Verilog file(s) - supports multiple DUTs | `--verilog dut.v` or `--verilog dut1.v --verilog dut2.v` | -| `--type ` | Design type | `--type combinational` | -| `--out ` | Output testbench path | `--out tb.sv` or `--out tb.cpp` | - -**Design Types:** -- `combinational` - Pure combinational logic -- `seq_clocked` - Sequential design with clock -- `seq_done` - Sequential design with done/valid signal - -#### Optional Arguments - -**Module Selection:** -| Argument | Description | -|----------|-------------| -| `--module ` | DUT module name(s) - single name for all DUTs, comma-separated list, or interleaved. **If multiple names are given, STG matches it with the DUT files in serial.** | `--module alu` or `--module alu1,alu2` or `--module alu1 --module alu2` | -| `--golden ` | Golden reference file (Verilog or C++/SystemC header) | -| `--golden-module ` | Golden module name (for Verilog golden) | - -**CC/SC Mode:** -| Argument | Description | -|----------|-------------| -| `--cc` | Generate C++ testbench with Verilator | -| `--sc` | Generate SystemC golden model template | -| `--out-header ` | Output path for golden model header (stage 1) | - -**Compilation:** -| Argument | Description | -|----------|-------------| -| `--out-exe ` | Compile and output executable | -| `--verilator` | Use Verilator instead of iverilog | -| `--verilator-mpi` | Enable MPI support (parallel execution) | -| `--compile-flags ` | Additional compiler flags | - -**Sequential Design Options:** -| Argument | Description | Example | -|----------|-------------|---------| -| `--clock ` | Clock signal name | `--clock clk` | -| `--reset ` | Reset signal name | `--reset rst_n` | -| `--reset-active ` | Reset polarity | `--reset-active low` | -| `--done ` | Done/valid signal name | `--done valid` | - -**Signal Classification:** -| Argument | Description | -|----------|-------------| -| `--control-signals ` | Explicit control signal names | -| `--data-signals ` | Explicit data signal names | - -**Test Configuration:** -| Argument | Default | Description | -|----------|---------|-------------| -| `--random-samples ` | 1024 | Random samples per control vector | -| `--max-enumeration ` | 26 | Maximum enumeration (2^N combinations) | -| `--timeout ` | 1e11 | Timeout in nanoseconds | -| `--debug` | false | Enable debug output | -| `--exit-on-error` | false | Exit immediately on first error | - -**Runtime Arguments (pass to generated executable):** -| Argument | Default | Description | -|----------|---------|-------------| -| `+STATS_FILE=` | test_stats.json | Custom path for statistics JSON output | - -**Advanced:** -| Argument | Description | -|----------|-------------| -| `--emplace-module` | Embed DUT and golden in testbench file | -| `--config ` | Load options from YAML config | -| `--verilator-jobs ` | Parallel jobs for Verilator (default: 4) | -| `--verilator-coverage` | Enable coverage analysis | -| `--verilator-ignore-warnings` | Ignore width warnings (default: true) | - -### `stg identify` - -Identify and classify signals in a Verilog module. - -```bash -stg identify \ - --verilog path/to/module.v \ - --module module_name \ - --type \ - --out signals.yaml -``` - -**Output format (YAML):** -```yaml -clock_inputs: [clk] -reset_inputs: [rst_n] -done_outputs: [] -control_inputs: [op, mode] -data_inputs: [a, b, data_in] -outputs: [result, valid] -reset_active_high: false -``` - -### `stg parse` - -Parse Verilog/SystemVerilog files and generate a module priority list. This command analyzes the module instantiation hierarchy and outputs modules sorted by priority (least dependencies first, then by submodule complexity). - -```bash -stg parse \ - --verilog path/to/module.v \ - --out modules.yaml -``` - -**What this does:** -- Parses all modules in the Verilog file -- Analyzes the module instantiation graph -- Calculates the total number of submodules for each module (including nested submodules) -- Sorts modules by priority: modules with fewer dependencies come first, followed by those with more submodules -- Outputs detailed module information in YAML format - -**Output format (YAML):** -```yaml -- name: submodule_a - submodule_count: 0 - ports: - - name: in - direction: input - width: 8 - - name: out - direction: output - width: 8 -- name: top_module - submodule_count: 5 - ports: - - name: clk - direction: input - width: 1 - - name: data_in - direction: input - width: 32 - - name: data_out - direction: output - width: 32 -``` - -**Use cases:** -- Understanding the module hierarchy in complex Verilog files -- Identifying the top-level module when `--module` is not specified -- Analyzing module complexity based on submodule count -- Debugging module dependencies - -### `stg compile` - -Compile user-provided testbench with DUT and golden model. - -**SystemVerilog:** -```bash -stg compile \ - --verilog dut.v \ - --golden golden.v \ - --testbench tb.sv \ - --out-exe tb_exe \ - [--verilator] - -# Multi-DUT SystemVerilog -stg compile \ - --verilog dut1.v --verilog dut2.v \ - --golden golden.v \ - --testbench tb.sv \ - --out-exe tb_exe \ - [--verilator] -``` - -**C++/SystemC:** -```bash -stg compile \ - --verilog dut.v \ - --module dut_module \ - --golden golden_model.h \ - --testbench tb.cpp \ - --out-exe tb_exe \ - --cc - -# Multi-DUT C++/SystemC (comma-separated modules) -stg compile \ - --verilog dut1.v --verilog dut2.v \ - --module dut_module1,dut_module2 \ - --golden golden_model.h \ - --testbench tb.cpp \ - --out-exe tb_exe \ - --cc - -# Multi-DUT C++/SystemC (interleaved specification) -stg compile \ - --verilog dut1.v --module dut_module1 \ - --verilog dut2.v --module dut_module2 \ - --golden golden_model.h \ - --testbench tb.cpp \ - --out-exe tb_exe \ - --cc - -# SystemC support -stg compile \ - --verilog dut.v \ - --module dut_module \ - --golden golden_model.h \ - --testbench tb.cpp \ - --out-exe tb_exe \ - --sc -``` - ---- - -## Multi-DUT Support - -STG supports testing multiple Design Under Test (DUT) files simultaneously, allowing you to compare different implementations against the same golden reference. In SystemVerilog mode, `--emplace-module` should be specified if modules have the same name in multiple DUT files; however, this is not required in C++/System-C mode as this is handled through [Verilator's renaming machanism](https://verilator.org/guide/latest/exe_verilator.html#cmdoption-prefix). - -### Basic Multi-DUT Usage - -**Multiple DUT files:** -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v --verilog dut3.v \ - --golden golden_model.h \ - --type combinational \ - --out testbench.cpp \ - --out-exe testbench_exe \ - --cc -``` - -**Alternative syntax (comma-separated):** -```bash -stg generate \ - --verilog dut1.v,dut2.v,dut3.v \ - --golden golden_model.h \ - --type combinational \ - --out testbench.cpp \ - --out-exe testbench_exe \ - --cc -``` - -### Module Selection for Multi-DUT - -STG provides three ways to specify modules for multiple DUTs: - -**Single module name (used for all DUTs):** -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v \ - --module alu_v1 \ - --golden golden_model.h \ - --type combinational \ - --out testbench.cpp \ - --cc -``` - -**Multiple module names (matched serially with DUTs):** -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v --verilog dut3.v \ - --module alu_v1,alu_v2,alu_v1 \ - --golden golden_model.h \ - --type combinational \ - --out testbench.cpp \ - --cc -``` - -**Interleaved verilog/module specification (explicit pairing):** -```bash -stg generate \ - --verilog dut1.v --module alu_v1 \ - --verilog dut2.v --module alu_v2 \ - --verilog dut3.v --module alu_v1 \ - --golden golden_model.h \ - --type combinational \ - --out testbench.cpp \ - --cc -``` - -**No module specified (uses first module from each DUT file):** -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v \ - --golden golden_model.h \ - --type combinational \ - --out testbench.cpp \ - --cc -``` - -### Multi-DUT in SystemVerilog Mode - -For SystemVerilog mode, use `--emplace-module` when DUT modules have naming conflicts: - -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v \ - --golden golden.v \ - --type combinational \ - --out testbench.sv \ - --out-exe testbench_exe \ - --emplace-module -``` - -This adds prefixes like `V0_`, `V1_` to differentiate DUT modules in the generated testbench. - -### Multi-DUT in C++/SystemC Mode - -In C++/SystemC mode, each DUT is compiled with a unique prefix automatically: - -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v --verilog dut3.v \ - --golden golden_model.h \ - --type combinational \ - --out testbench.cpp \ - --out-exe testbench_exe \ - --cc -``` - -This generates classes like `V0_DUT`, `V1_DUT`, `V2_DUT` for each DUT. - -### Test Statistics and JSON Output - -Multi-DUT testbenches automatically generate detailed statistics for each DUT in JSON format: - -**Generated `test_stats.json`:** -```json -{ - "dut0": { - "out": {"tests": 1024, "success": 1020, "score": 99.61} - }, - "dut1": { - "out": {"tests": 1024, "success": 856, "score": 83.59} - }, - "dut2": { - "out": {"tests": 1024, "success": 1024, "score": 100.00} - } -} -``` - -The JSON file contains: -- **tests**: Total number of test cases for each output signal -- **success**: Number of successful comparisons -- **score**: Success rate as a percentage - -#### Customizing Statistics File Location - -You can specify a custom location for the statistics file using the `+STATS_FILE=` runtime argument: - -**SystemVerilog mode:** -```bash -./tb_exe +STATS_FILE=my_results.json -``` - -**C++/SystemC mode:** -```bash -./tb_exe +STATS_FILE=my_results.json -``` - -This is particularly useful when: -- Running multiple test configurations -- Integrating with automated test frameworks -- Organizing results in specific directory structures - -**Example with custom output:** -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v \ - --golden golden_model.h \ - --type combinational \ - --out tb.cpp \ - --out-exe tb_exe \ - --cc - -./tb_exe +STATS_FILE=results/comparison_$(date +%Y%m%d).json -``` - -### Multi-DUT Sequential Designs - -Multi-DUT works with all design types, including sequential designs: - -```bash -stg generate \ - --verilog gcd_impl1.sv --verilog gcd_impl2.sv --verilog gcd_impl3.sv \ - --golden gcd_golden.h \ - --type seq_done \ - --out testbench.cpp \ - --out-exe testbench_exe \ - --cc \ - --clock clk \ - --reset rst_n \ - --done done \ - --random-samples 50 -``` - -In `seq_done` mode, the testbench waits for all DUTs to complete before comparing results. - ---- - -## Advanced Features - -### Module Emplacement - -Embed DUT and golden modules directly in the testbench file if there are modules with same name in dut and golden (only works in the SystemVerilog mode): - -```bash -stg generate \ - --verilog dut.v \ - --golden golden.v \ - --type combinational \ - --out tb.sv \ - --emplace-module -``` - -This creates a single `.sv` file containing everything, and the module names have either "dut" or "golden" as their prefix. - -### YAML Configuration - -Create a config file to avoid repeating arguments: - -**config.yaml:** -```yaml -module: my_alu -golden_module: alu_golden -clock: clk -reset: rst_n -reset_active: low -control_signals: - - op - - mode -data_signals: - - a - - b -``` - -**Usage:** -```bash -stg generate \ - --verilog dut.v \ - --golden golden.v \ - --type combinational \ - --out tb.sv \ - --config config.yaml -``` - -### Verilator Coverage Analysis -Note: The current traditional SV mode does not support coverage analysis with MPI mode. The reason is that multiple MPI processes will write to the same file. However, in C++/SystemC mode, the DAT filenames have suffixes denoting which rank they belong to. - -Coverage analysis works with both single and multi-DUT configurations: - -**Single DUT:** -```bash -stg generate \ - --verilog dut.v \ - --golden golden.v \ - --type combinational \ - --out tb.sv \ - --out-exe tb_exe \ - --verilator \ - --verilator-coverage - -./tb_exe -# Coverage data written to coverage.dat -verilator_coverage --annotate coverage_report coverage.dat -``` - -**Multi-DUT (C++/SystemC mode):** -```bash -stg generate \ - --verilog dut1.v --verilog dut2.v \ - --golden golden_model.h \ - --type combinational \ - --out tb.cpp \ - --out-exe tb_exe \ - --cc \ - --verilator-coverage - -./tb_exe -# Coverage data written to coverage.dat (covers all DUTs) -verilator_coverage --annotate coverage_report coverage.dat -``` - -### Custom Compile Flags - -Pass additional flags to the compiler: - -```bash -stg generate \ - --verilog dut.v \ - --golden golden.v \ - --type combinational \ - --out tb.sv \ - --out-exe tb_exe \ - --verilator \ - --compile-flags --trace --trace-fst -``` - ---- - -## Examples by Design Type - -### Combinational Logic (ALU) - -```bash -cd examples/ALU - -# SystemVerilog mode -stg generate \ - --verilog gate_level.v \ - --module alu_gate_level \ - --golden golden.v \ - --golden-module alu_golden \ - --type combinational \ - --out tb_alu.sv \ - --out-exe tb_alu_exe \ - --control-signals op - -./tb_alu_exe -# Generates test_stats.json with detailed statistics -``` - -**C++ mode:** -```bash -cd examples/ALU_cc - -# Stage 1: Generate template -stg generate \ - --verilog gate_level.v \ - --module alu_gate_level \ - --type combinational \ - --out tb.cpp \ - --out-header my_golden.h \ - --cc \ - --control-signals op - -# Stage 2: Edit my_golden.h, then compile -stg generate \ - --verilog gate_level.v \ - --module alu_gate_level \ - --golden my_golden.h \ - --type combinational \ - --out tb.cpp \ - --out-exe tb_exe \ - --cc \ - --control-signals op - -./tb_exe -``` - -### Sequential Clocked (Counter) - -```bash -cd examples/pingpong - -stg generate \ - --verilog up_only.v \ - --module counter_up_only \ - --golden golden.v \ - --golden-module counter_golden \ - --type seq_clocked \ - --out tb.sv \ - --out-exe tb_exe \ - --clock clk \ - --reset rst_n \ - --reset-active low \ - --random-samples 1000 - -./tb_exe -``` - -**With SystemC golden model:** -```bash -cd examples/pingpong_sc - -# Using pre-made SystemC golden model -stg generate \ - --verilog up_only.v \ - --golden golden_model_sc.h \ - --type seq_clocked \ - --out tb.cpp \ - --out-exe tb_exe \ - --cc \ - --clock clk \ - --reset rst_n \ - --reset-active low \ - --random-samples 100 - -./tb_exe -``` - -### Sequential with Done Signal (GCD) - -```bash -cd examples/GCD - -stg generate \ - --verilog gcd_buggy.v \ - --module gcd \ - --golden gcd_golden.v \ - --golden-module gcd_golden \ - --type seq_done \ - --out tb.sv \ - --out-exe tb_exe \ - --clock clk \ - --reset rst \ - --reset-active high \ - --done done \ - --control-signals "" \ - --data-signals a b \ - --random-samples 100 - -./tb_exe -``` - -### Multi-DUT Comparison (ALU Implementations) - -Compare multiple ALU implementations against the same golden model: - -```bash -cd examples/multi_dut/ALU - -# Test multiple DUT implementations (using interleaved specification) -stg generate \ - --verilog dut1_buggy_add.v --module alu_v1 \ - --verilog dut2_buggy_sub.v --module alu_v2 \ - --verilog dut3_same_name.v --module alu_v1 \ - --golden golden_model.h \ - --type combinational \ - --out tb_multi.cpp \ - --out-exe tb_multi_exe \ - --cc \ - --control-signals op \ - --random-samples 500 - -./tb_multi_exe - -# Check detailed statistics for each DUT -cat test_stats.json -``` - -**Example output:** -```json -{ - "dut0": {"out": {"tests": 4000, "success": 3500, "score": 87.50}}, - "dut1": {"out": {"tests": 4000, "success": 3000, "score": 75.00}}, - "dut2": {"out": {"tests": 4000, "success": 4000, "score": 100.00}} -} -``` - ---- - -## Troubleshooting - -### "No modules found in DUT file" - -Make sure your Verilog file contains valid module definitions and iverilog is installed: -```bash -iverilog -t null -g2009 your_file.v -``` - -### "Multiple modules found, please specify --module" - -Use `--module` to specify which module to test: -```bash -stg generate --verilog file.v --module my_module ... -``` - -### C++/SC mode: "There are two stages to compile the testbench" - -You're in stage 1. First generate the template: -```bash -stg generate --verilog dut.v --type combinational --out tb.cpp --out-header golden.h --cc -``` - -Then implement the golden model and run stage 2: -```bash -stg generate --verilog dut.v --golden golden.h --type combinational --out tb.cpp --out-exe exe --cc -``` - -### Verilator compilation errors - -Try adding compile flags to ignore width warnings: -```bash -stg generate ... --verilator --compile-flags -Wno-WIDTH -``` - ---- - -## Tips and Best Practices - -1. **Start with SV mode** - It's simpler and good for most cases -2. **Use `--cc` for complex golden models** - C++ gives you more flexibility -3. **Specify control signals explicitly** - Use `--control-signals` for better test coverage -4. **Use Verilator for large designs** - Much faster than iverilog -5. **Enable MPI for very large designs** - Parallel execution can save hours -6. **Use `--debug` to troubleshoot** - Shows detailed signal values -7. **Save configs in YAML** - Reusable and version-controllable -8. **Use `stg compile` for iteration** - Faster when modifying testbenches -9. **Check `test_stats.json`** - Automatically generated with detailed test statistics for each DUT and signal -10. **Use multi-DUT for comparison** - Test multiple implementations simultaneously with `--verilog dut1.v --verilog dut2.v` -11. **Customize statistics output** - Use `+STATS_FILE=custom_name.json` at runtime to specify a different output file for test statistics - ---- - -For more information, see the [README.md](README.md) or run `stg --help`. - diff --git a/docs/source/user_guide/advanced.md b/docs/source/user_guide/advanced.md new file mode 100644 index 0000000..1b0ddb2 --- /dev/null +++ b/docs/source/user_guide/advanced.md @@ -0,0 +1,144 @@ +# Advanced Features + +This page covers advanced STG features including YAML configuration, coverage analysis, custom compiler flags, and best practices. + +## YAML Configuration + +Create a config file to avoid repeating command-line arguments: + +**config.yaml:** +```yaml +module: my_alu +golden_module: alu_golden +clock: clk +reset: rst_n +reset_active: low +control_signals: + - op + - mode +data_signals: + - a + - b +``` + +**Usage:** +```bash +stg generate \ + --verilog dut.v \ + --golden golden.v \ + --type combinational \ + --out tb.sv \ + --config config.yaml +``` + +Command-line arguments override config file values. + +## Verilator Coverage Analysis + +Enable coverage instrumentation to measure how well your tests exercise the DUT. + +```{note} +SV mode does not support coverage analysis with MPI, as multiple processes write to the same file. In CC/SC mode, coverage filenames include rank suffixes to avoid conflicts. +``` + +### Single DUT + +```bash +stg generate \ + --verilog dut.v \ + --golden golden.v \ + --type combinational \ + --out tb.sv \ + --out-exe tb_exe \ + --verilator \ + --verilator-coverage + +./tb_exe +# Coverage data written to coverage.dat +verilator_coverage --annotate coverage_report coverage.dat +``` + +### FSM Runtime Line Checks (`generate-fsm`) + +`generate-fsm` supports stricter transition validation with line execution counters: + +```bash +stg generate-fsm dut.sv \ + --golden golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --fsm-method deterministic \ + --line-coverage-check +``` + +Notes: + +- `--line-coverage-check` automatically adds Verilator `--coverage` for this flow. +- Transition success requires both state/wait-condition success and coverage counter movement. +- `test_stats.json` includes: + - `line_coverage_check_enabled` + - `line_check_passed_transitions` + - `line_check_failed_transitions` + - per-edge `lines_executed` + +### Verilator Binary Path + +STG looks for `verilator` on your `PATH` by default. + +To override with a specific binary: + +```bash +export STG_VERILATOR_PATH=/path/to/verilator +``` + +### Multi-DUT (C++/SystemC Mode) + +```bash +stg generate \ + --verilog dut1.v --verilog dut2.v \ + --golden golden_model.h \ + --type combinational \ + --out tb.cpp \ + --out-exe tb_exe \ + --cc \ + --verilator-coverage + +./tb_exe +verilator_coverage --annotate coverage_report coverage.dat +``` + +## Custom Compiler Flags + +Pass additional flags to iverilog or Verilator: + +```bash +stg generate \ + --verilog dut.v \ + --golden golden.v \ + --type combinational \ + --out tb.sv \ + --out-exe tb_exe \ + --verilator \ + --compile-flags --trace --trace-fst +``` + +## Tips and Best Practices + +1. **Start with SV mode** — It's simpler and works for most cases +2. **Use `--cc` for complex golden models** — C++ gives you more flexibility +3. **Specify control signals explicitly** — `--control-signals` improves test coverage +4. **Use Verilator for large designs** — Much faster than iverilog +5. **Enable MPI for very large designs** — Parallel execution can save hours +6. **Use `--debug` to troubleshoot** — Shows detailed signal values +7. **Save configs in YAML** — Reusable and version-controllable +8. **Use `stg compile` for iteration** — Faster when modifying testbenches manually +9. **Check `test_stats.json`** — Automatically generated with per-DUT, per-signal statistics +10. **Use multi-DUT for comparison** — Test multiple implementations simultaneously +11. **Customize statistics output** — Use `+STATS_FILE=custom_name.json` at runtime + +## See Also + +- [Modes Overview](modes_overview.md) — Compare testbench modes +- [Multi-DUT Support](multi_dut.md) — Multi-DUT testing details +- [CLI Reference](../reference/cli.md) — Complete flag reference +- [Troubleshooting](../troubleshooting.md) — Common issues and solutions diff --git a/docs/source/user_guide/cc_sc_mode.md b/docs/source/user_guide/cc_sc_mode.md new file mode 100644 index 0000000..824d151 --- /dev/null +++ b/docs/source/user_guide/cc_sc_mode.md @@ -0,0 +1,181 @@ +# C++ / SystemC Mode + +C++/SystemC mode provides a **two-stage workflow** where you implement a golden model in C++ (or SystemC), giving you maximum flexibility for reference implementations. + +## CC Mode (C++ Testbench) + +Use `--cc` for C++ testbenches. The golden model uses standard C++ integer types. + +### Stage 1: Generate Golden Model Template + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --type \ + --out testbench.cpp \ + --out-header golden_model.h \ + --cc \ + [additional options...] +``` + +This generates: +- `golden_model.h` — A C++ header template with signal declarations +- `testbench.cpp` — The C++ testbench that includes the golden model + +**Generated golden model structure:** +```cpp +#ifndef GOLDEN_MODEL_H +#define GOLDEN_MODEL_H + +#include + +class GoldenModel { +public: + // Input signals + uint8_t a; + uint8_t b; + uint8_t op; + + // Output signals + uint8_t out; + + // Implement your golden model logic here + void eval() { + // TODO: Implement combinational logic + } + + // For sequential designs + void posedge_clk() { + // TODO: Implement sequential logic + } +}; + +#endif +``` + +### Stage 2: Implement the Golden Model + +Edit `golden_model.h` to implement your logic. Example for an ALU: + +```cpp +void eval() { + switch (op) { + case 0: out = a + b; break; + case 1: out = a - b; break; + case 2: out = a & b; break; + case 3: out = a | b; break; + case 4: out = a ^ b; break; + case 5: out = a << b; break; + case 6: out = a >> b; break; + case 7: out = (int8_t)a >> b; break; + } +} +``` + +### Stage 3: Compile with Implemented Golden Model + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --golden golden_model.h \ + --type \ + --out testbench.cpp \ + --out-exe testbench_exe \ + --cc \ + [additional options...] + +./testbench_exe +``` + +## SC Mode (SystemC Testbench) + +Use `--sc` for SystemC-style golden models with `sc_uint` data types. + +### Stage 1: Generate SystemC Golden Model Template + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --type \ + --out testbench.cpp \ + --out-header golden_model_sc.h \ + --sc \ + [additional options...] +``` + +**Generated SystemC golden model:** +```cpp +#ifndef GOLDEN_MODEL_SC_H +#define GOLDEN_MODEL_SC_H + +#include + +class GoldenModel { +public: + sc_dt::sc_uint<4> a; + sc_dt::sc_uint<4> b; + sc_dt::sc_uint<3> op; + sc_dt::sc_uint<4> out; + + void eval() { + // TODO: Implement combinational logic using sc_uint + } +}; + +#endif +``` + +### Stage 2: Implement with SystemC Types + +```cpp +void posedge_clk() { + if (rst_n == 0) { + out = 0; + return; + } + + if (en) { + uint32_t val = out.to_uint(); + out = (val + 1) & 0xF; + } +} +``` + +### Stage 3: Compile with SystemC Golden Model + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --golden golden_model_sc.h \ + --type \ + --out testbench.cpp \ + --out-exe testbench_exe \ + --sc \ + [additional options...] + +./testbench_exe +``` + +## Recompiling a Modified C++/SystemC Testbench + +If you modify the generated C++ testbench, use `stg compile`: + +```bash +stg compile \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --golden golden_model.h \ + --testbench testbench.cpp \ + --out-exe testbench_exe \ + --cc # or --sc for SystemC +``` + +## See Also + +- [Modes Overview](modes_overview.md) — Compare SV, CC, and SC modes +- [Multi-DUT Support](multi_dut.md) — Test multiple implementations at once +- [CLI Reference](../reference/cli.md) — Full flag reference for `stg generate` diff --git a/docs/source/user_guide/fsm_coverage.md b/docs/source/user_guide/fsm_coverage.md new file mode 100644 index 0000000..1fe0416 --- /dev/null +++ b/docs/source/user_guide/fsm_coverage.md @@ -0,0 +1,191 @@ +# FSM-Based Coverage + +`generate-fsm` creates state-coverage-enhanced C++ testbenches for sequential designs with finite state machines (FSMs). Instead of random input patterns, it uses **depth-first search (DFS)** over the FSM state graph to reach every state and transition. + +## When to Use It + +- **Sequential designs with FSMs** (clocks, resets, internal state) +- **Random testing misses states** because reaching them requires specific input sequences +- **Designs with internal timers/counters** that gate transitions + +Traditional `stg generate` relies on random inputs, which often fails to hit states like `S_PED_WALK` that need multi-cycle sequences. `generate-fsm` discovers the FSM structure and generates targeted tests. + +## Quick Start + +### Without LLM (Deterministic) + +```bash +stg generate-fsm design.sv \ + --golden golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --fsm-method deterministic +``` + +### With LLM (More Flexible) + +Requires an API key (`GOOGLE_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY` in `.env` or environment): + +```bash +stg generate-fsm design.sv \ + --golden golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --fsm-method lm \ + --lm-provider gemini +``` + +## FSM Extraction Methods + +| Method | Description | Requirements | +|--------|-------------|--------------| +| `deterministic` | Parser-based (iverilog/sv-parser) | None | +| `lm` | LLM identifies FSM from source | API key, `uv` installed | + +- **deterministic**: Fast and reliable. Works for standard FSM coding styles. +- **lm**: Better for complex or unusual FSMs. Uses an LLM to interpret the RTL. + +## State Analysis Caching + +Use `--state-analysis` to save and reuse FSM analysis: + +```bash +# First run: analyze design and save to fsm.json +stg generate-fsm design.sv --golden golden.sv --out tb.cpp \ + --state-analysis fsm.json + +# Later runs: load from fsm.json (skips analysis) +stg generate-fsm design.sv --golden golden.sv --out tb.cpp \ + --state-analysis fsm.json +``` + +- If the file **exists**: load it and skip FSM identification. +- If it **does not exist**: run analysis and save to that path. + +## Examples + +### Traffic Light (Deterministic) + +```bash +cd examples/traffic_light + +stg generate-fsm traffic_light_controller.sv \ + --golden traffic_light_controller_golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --clock clk \ + --reset rst_n \ + --reset-active low \ + --fsm-method deterministic \ + --verilator-coverage + +./tb +``` + +### Sequence Detector (LM-Based) + +```bash +cd examples/seq_detector + +# Set API key: export GOOGLE_API_KEY="..." +stg generate-fsm seq_detector.sv \ + --golden seq_detector_golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --fsm-method lm \ + --lm-provider gemini \ + --state-analysis state_analysis.json + +./tb +``` + +## Output + +- **Testbench**: C++ file using Verilator +- **State analysis JSON**: FSM states, transitions, control/data signals +- **Executable** (with `--out-exe`): Compiled testbench binary + +The testbench traverses the FSM via DFS, applies inputs that satisfy transition conditions, and compares DUT outputs to the golden model. + +## Transition Verification Model + +For each attempted transition edge: + +1. Apply `input_condition` to controllable inputs. +2. Wait up to `--transition-timeout` cycles for `wait_condition`. +3. Compare DUT outputs against golden outputs. +4. Read DUT internal FSM state and verify expected `to_state`. + +By default, a transition is considered successful when steps 2-4 pass. + +## Optional Line Execution Check + +Use `--line-coverage-check` to add an extra verification gate based on Verilator runtime counters (`getCounters()`): + +```bash +stg generate-fsm design.sv \ + --golden golden.sv \ + --out tb.cpp \ + --out-exe tb \ + --fsm-method deterministic \ + --line-coverage-check +``` + +When enabled: + +- STG auto-enables Verilator `--coverage` for `generate-fsm` compilation. +- The generated testbench snapshots coverage counters before/after each transition attempt. +- A transition passes only if: + - wait-condition succeeds, + - state reaches expected target, + - and at least one relevant line/block counter increased. + +This helps distinguish "state appeared correct" from "transition logic actually executed." + +## Edge Status in Statistics + +`test_stats.json` includes per-edge `status` and `lines_executed`: + +- `covered`: transition verified successfully +- `impossible`: input condition unsatisfiable +- `timeout`: wait condition did not become true in time +- `state_mismatch`: wait succeeded but target state mismatched +- `no_lines_executed`: state/wait checks passed but no line counter increase (only when line check enabled) +- `unreachable`: DFS did not reach/close the edge + +Summary fields include: + +- `line_coverage_check_enabled` +- `line_check_passed_transitions` +- `line_check_failed_transitions` + +## Verilator Binary Selection + +For Verilator-based compilation, STG uses: + +- Default: `verilator` from your `PATH` +- Override: set `STG_VERILATOR_PATH` to another Verilator binary + +## Troubleshooting + +### "No state machines found" + +The design may have no detectable FSM, or the coding style is not recognized. Try `--fsm-method lm` for LLM-based extraction. + +### LM method fails + +- Ensure `uv` is installed: +- Check API keys in `.env` or environment +- Supported keys: `GOOGLE_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY` + +### Missed states in coverage + +- Increase `--dfs-passes` for more edge coverage +- Increase `--transition-timeout` if internal conditions take many cycles +- Use `--state-analysis` to inspect the extracted FSM; fix or adjust if extraction is wrong + +## See Also + +- [Modes Overview](modes_overview.md) — General STG modes and design types +- [CLI Reference](../reference/cli.md) — Full `generate-fsm` flag reference +- [Advanced Features](advanced.md) — Coverage analysis with Verilator diff --git a/docs/source/user_guide/modes_overview.md b/docs/source/user_guide/modes_overview.md new file mode 100644 index 0000000..66ea109 --- /dev/null +++ b/docs/source/user_guide/modes_overview.md @@ -0,0 +1,109 @@ +# Modes and Design Types + +This page explains the testbench generation modes and the supported design types in STG. + +## Testbench Modes + +STG supports two main approaches to testbench generation: **SystemVerilog (SV)** mode and **C++/SystemC (CC/SC)** mode. + +| Mode | Testbench Language | Golden Model | Compiler | Use Case | +|------|-------------------|--------------|----------|----------| +| **SV** (default) | SystemVerilog | Verilog/SystemVerilog | iverilog or Verilator | Standard workflow, easy setup | +| **CC** | C++ | C++ header | Verilator | Custom golden model, high performance | +| **SC** | C++ with SystemC | SystemC header | Verilator | SystemC ecosystem, `sc_uint` types | + +### SystemVerilog Mode (SV) + +- Both DUT and golden reference are Verilog/SystemVerilog modules +- Uses iverilog or Verilator for compilation +- **Single-stage workflow**: provide DUT, golden, and generate +- Best for: simple designs, standard verification workflow + +See the [SystemVerilog Mode Guide](sv_mode.md) for full details. + +### C++ Mode (CC) + +- DUT is Verilog, golden model is a C++ header file +- Uses Verilator exclusively for DUT compilation +- **Two-stage workflow**: + 1. Generate a golden model header template with `--out-header` + 2. Implement the golden model, then compile with `--golden` +- Best for: complex golden models, custom C++ logic, maximum performance + +### SystemC Mode (SC) + +- Similar to CC mode but uses SystemC types (`sc_uint`) +- Two-stage workflow like CC mode +- Best for: SystemC ecosystem integration, bit-accurate types + +See the [C++/SystemC Mode Guide](cc_sc_mode.md) for full details on both CC and SC modes. + +## Design Types + +STG supports three fundamental design types, specified with `--type`: + +### `combinational` + +Pure combinational logic with no clock or state elements. + +- **Test approach**: Exhaustively enumerate control signals, randomly sample data signals +- **Examples**: ALUs, multiplexers, decoders, encoders +- **Golden model function**: `eval()` + +```bash +stg generate --verilog dut.v --golden golden.v --type combinational --out tb.sv +``` + +### `seq_clocked` + +Clocked sequential design with continuous operation. + +- **Requires**: clock signal (and usually a reset signal) +- **Test approach**: Apply random control/data patterns over multiple clock cycles +- **Examples**: Counters, shift registers, timers, pipelines +- **Golden model function**: `posedge_clk()` + +```bash +stg generate --verilog dut.v --golden golden.v --type seq_clocked --out tb.sv \ + --clock clk --reset rst_n --reset-active low +``` + +### `seq_done` + +Sequential design with transaction-based operation that signals completion. + +- **Requires**: clock, reset, and a done/valid signal +- **Test approach**: Start a transaction, wait for the done signal, then check results +- **Examples**: GCD calculators, dividers, multi-cycle state machines +- **Golden model function**: `posedge_clk()` (with completion tracking) + +```bash +stg generate --verilog dut.v --golden golden.v --type seq_done --out tb.sv \ + --clock clk --reset rst --reset-active high --done done +``` + +## Signal Classification + +STG automatically classifies input signals into two categories: + +| Category | Behavior | Typical Signals | +|----------|----------|-----------------| +| **Control signals** | Exhaustively enumerated (up to 2^26 combinations) | `op`, `mode`, `cmd`, `sel` (narrow, 1–4 bits) | +| **Data signals** | Randomly sampled (default: 1024 per control vector) | `a`, `b`, `data`, `addr` (wider, 8+ bits) | + +You can override automatic classification with `--control-signals` and `--data-signals`. Use `stg identify` to preview what STG detects: + +```bash +stg identify --verilog dut.v --module my_module --type combinational --out signals.yaml +``` + +## Choosing the Right Mode + +| Scenario | Recommended Mode | +|----------|-----------------| +| Quick verification with Verilog golden | SV mode | +| Complex golden model logic | CC mode | +| Need SystemC types for bit accuracy | SC mode | +| Large design, need speed | CC/SC mode with Verilator | +| Multiple DUT comparison | CC/SC mode (automatic prefix handling) | +| FSM state coverage | `generate-fsm` command (see [FSM Coverage](fsm_coverage.md)) | diff --git a/docs/source/user_guide/multi_dut.md b/docs/source/user_guide/multi_dut.md new file mode 100644 index 0000000..2a25b24 --- /dev/null +++ b/docs/source/user_guide/multi_dut.md @@ -0,0 +1,178 @@ +# Multi-DUT Support + +STG can test multiple Design Under Test (DUT) implementations simultaneously against the same golden reference, making it easy to compare different implementations. + +## Basic Multi-DUT Usage + +Specify multiple DUT files with repeated `--verilog` flags: + +```bash +stg generate \ + --verilog dut1.v --verilog dut2.v --verilog dut3.v \ + --golden golden_model.h \ + --type combinational \ + --out testbench.cpp \ + --out-exe testbench_exe \ + --cc +``` + +Or use comma-separated syntax: + +```bash +stg generate \ + --verilog dut1.v,dut2.v,dut3.v \ + --golden golden_model.h \ + --type combinational \ + --out testbench.cpp \ + --out-exe testbench_exe \ + --cc +``` + +## Module Selection + +STG provides three ways to specify modules for multiple DUTs: + +### Single Module Name (Used for All DUTs) + +When all DUT files contain the same module name: + +```bash +stg generate \ + --verilog dut1.v --verilog dut2.v \ + --module alu_v1 \ + --golden golden_model.h \ + --type combinational \ + --out testbench.cpp \ + --cc +``` + +### Comma-Separated Module Names + +Match modules serially with DUT files: + +```bash +stg generate \ + --verilog dut1.v --verilog dut2.v --verilog dut3.v \ + --module alu_v1,alu_v2,alu_v1 \ + --golden golden_model.h \ + --type combinational \ + --out testbench.cpp \ + --cc +``` + +### Interleaved Specification (Explicit Pairing) + +Pair each `--verilog` with a `--module` for clarity: + +```bash +stg generate \ + --verilog dut1.v --module alu_v1 \ + --verilog dut2.v --module alu_v2 \ + --verilog dut3.v --module alu_v1 \ + --golden golden_model.h \ + --type combinational \ + --out testbench.cpp \ + --cc +``` + +### No Module Specified + +STG uses the first module from each DUT file: + +```bash +stg generate \ + --verilog dut1.v --verilog dut2.v \ + --golden golden_model.h \ + --type combinational \ + --out testbench.cpp \ + --cc +``` + +## Multi-DUT in SystemVerilog Mode + +In SV mode, use `--emplace-module` when DUT modules have naming conflicts: + +```bash +stg generate \ + --verilog dut1.v --verilog dut2.v \ + --golden golden.v \ + --type combinational \ + --out testbench.sv \ + --out-exe testbench_exe \ + --emplace-module +``` + +This adds prefixes like `V0_`, `V1_` to differentiate DUT modules. + +## Multi-DUT in C++/SystemC Mode + +In CC/SC mode, each DUT is compiled with a unique prefix automatically (no `--emplace-module` needed): + +```bash +stg generate \ + --verilog dut1.v --verilog dut2.v --verilog dut3.v \ + --golden golden_model.h \ + --type combinational \ + --out testbench.cpp \ + --out-exe testbench_exe \ + --cc +``` + +This generates classes like `V0_DUT`, `V1_DUT`, `V2_DUT` for each DUT. + +## Multi-DUT with Sequential Designs + +Multi-DUT works with all design types: + +```bash +stg generate \ + --verilog gcd_impl1.sv --verilog gcd_impl2.sv --verilog gcd_impl3.sv \ + --golden gcd_golden.h \ + --type seq_done \ + --out testbench.cpp \ + --out-exe testbench_exe \ + --cc \ + --clock clk \ + --reset rst_n \ + --done done \ + --random-samples 50 +``` + +In `seq_done` mode, the testbench waits for all DUTs to complete before comparing results. + +## Test Statistics (JSON Output) + +Multi-DUT testbenches generate per-DUT statistics in `test_stats.json`: + +```json +{ + "dut0": { + "out": {"tests": 1024, "success": 1020, "score": 99.61} + }, + "dut1": { + "out": {"tests": 1024, "success": 856, "score": 83.59} + }, + "dut2": { + "out": {"tests": 1024, "success": 1024, "score": 100.00} + } +} +``` + +Each entry contains: +- **tests**: Total number of test cases per output signal +- **success**: Number of successful comparisons +- **score**: Success rate as a percentage + +### Custom Statistics File Location + +Use the `+STATS_FILE=` runtime argument to specify a custom output path: + +```bash +./tb_exe +STATS_FILE=results/comparison_$(date +%Y%m%d).json +``` + +## See Also + +- [SystemVerilog Mode](sv_mode.md) — SV mode details including module emplacement +- [C++/SystemC Mode](cc_sc_mode.md) — CC/SC mode two-stage workflow +- [Examples](../examples.md) — Multi-DUT example with ALU implementations diff --git a/docs/source/user_guide/sv_mode.md b/docs/source/user_guide/sv_mode.md new file mode 100644 index 0000000..d8ab65d --- /dev/null +++ b/docs/source/user_guide/sv_mode.md @@ -0,0 +1,126 @@ +# SystemVerilog Mode + +In SystemVerilog (SV) mode, both your DUT and golden reference are Verilog/SystemVerilog modules. This is the simplest workflow and is the default mode. + +## Basic Workflow + +Verilog files can be specified as positional arguments or with the `--verilog` flag — both are equivalent: + +```bash +# Positional arguments +stg generate path/to/dut.v --golden path/to/golden.v --type --out tb.sv + +# Explicit --verilog flag +stg generate --verilog path/to/dut.v --golden path/to/golden.v --type --out tb.sv +``` + +## Generate Testbench Only + +Generate a testbench file without compiling: + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --golden path/to/golden.v \ + --golden-module golden_module_name \ + --type \ + --out testbench.sv +``` + +## Generate and Compile + +### With iverilog + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --golden path/to/golden.v \ + --golden-module golden_module_name \ + --type \ + --out testbench.sv \ + --out-exe testbench_exe + +./testbench_exe +``` + +### With Verilator + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --golden path/to/golden.v \ + --golden-module golden_module_name \ + --type \ + --out testbench.sv \ + --out-exe testbench_exe \ + --verilator + +./testbench_exe +``` + +### With Verilator MPI (Parallel Execution) + +For large designs, MPI enables parallel test execution: + +```bash +stg generate \ + --verilog path/to/dut.v \ + --module dut_module_name \ + --golden path/to/golden.v \ + --golden-module golden_module_name \ + --type \ + --out testbench.sv \ + --out-exe testbench_exe \ + --verilator-mpi + +mpirun -np 4 ./testbench_exe +``` + +## Recompiling a Modified Testbench + +If you manually edit the generated testbench, use `stg compile` to recompile: + +```bash +# With iverilog +stg compile \ + --verilog path/to/dut.v \ + --golden path/to/golden.v \ + --testbench testbench.sv \ + --out-exe testbench_exe + +# With Verilator +stg compile \ + --verilog path/to/dut.v \ + --golden path/to/golden.v \ + --testbench testbench.sv \ + --out-exe testbench_exe \ + --verilator +``` + +## Module Emplacement + +When DUT and golden modules share the same name, use `--emplace-module` to embed both into the testbench file with automatic prefix renaming: + +```bash +stg generate \ + --verilog dut.v \ + --golden golden.v \ + --type combinational \ + --out tb.sv \ + --emplace-module +``` + +This creates a single `.sv` file where module names are prefixed with `dut_` or `golden_` to avoid conflicts. + +```{note} +Module emplacement is only available in SV mode. In CC/SC mode, Verilator handles renaming automatically. +``` + +## See Also + +- [Modes Overview](modes_overview.md) — Compare SV, CC, and SC modes +- [Multi-DUT Support](multi_dut.md) — Test multiple implementations at once +- [CLI Reference](../reference/cli.md) — Full flag reference for `stg generate` diff --git a/examples/ALU/README.md b/examples/V1/ALU/README.md similarity index 100% rename from examples/ALU/README.md rename to examples/V1/ALU/README.md diff --git a/examples/ALU/gate_level.v b/examples/V1/ALU/gate_level.v similarity index 100% rename from examples/ALU/gate_level.v rename to examples/V1/ALU/gate_level.v diff --git a/examples/ALU/golden.v b/examples/V1/ALU/golden.v similarity index 100% rename from examples/ALU/golden.v rename to examples/V1/ALU/golden.v diff --git a/examples/pingpong/README.md b/examples/V1/pingpong/README.md similarity index 100% rename from examples/pingpong/README.md rename to examples/V1/pingpong/README.md diff --git a/examples/pingpong/golden.v b/examples/V1/pingpong/golden.v similarity index 100% rename from examples/pingpong/golden.v rename to examples/V1/pingpong/golden.v diff --git a/examples/pingpong/no_en.v b/examples/V1/pingpong/no_en.v similarity index 100% rename from examples/pingpong/no_en.v rename to examples/V1/pingpong/no_en.v diff --git a/examples/pingpong/up_only.v b/examples/V1/pingpong/up_only.v similarity index 100% rename from examples/pingpong/up_only.v rename to examples/V1/pingpong/up_only.v diff --git a/examples/multi_dut/ALU/dut1_buggy_add.v b/examples/V3.2/multi_dut/ALU/dut1_buggy_add.v similarity index 100% rename from examples/multi_dut/ALU/dut1_buggy_add.v rename to examples/V3.2/multi_dut/ALU/dut1_buggy_add.v diff --git a/examples/multi_dut/ALU/dut2_buggy_sub.v b/examples/V3.2/multi_dut/ALU/dut2_buggy_sub.v similarity index 100% rename from examples/multi_dut/ALU/dut2_buggy_sub.v rename to examples/V3.2/multi_dut/ALU/dut2_buggy_sub.v diff --git a/examples/multi_dut/ALU/dut3_same_name.v b/examples/V3.2/multi_dut/ALU/dut3_same_name.v similarity index 100% rename from examples/multi_dut/ALU/dut3_same_name.v rename to examples/V3.2/multi_dut/ALU/dut3_same_name.v diff --git a/examples/multi_dut/ALU/golden.v b/examples/V3.2/multi_dut/ALU/golden.v similarity index 100% rename from examples/multi_dut/ALU/golden.v rename to examples/V3.2/multi_dut/ALU/golden.v diff --git a/examples/multi_dut/ALU/golden_model.h b/examples/V3.2/multi_dut/ALU/golden_model.h similarity index 100% rename from examples/multi_dut/ALU/golden_model.h rename to examples/V3.2/multi_dut/ALU/golden_model.h diff --git a/examples/multi_dut/GCD/dut1_always_one.sv b/examples/V3.2/multi_dut/GCD/dut1_always_one.sv similarity index 100% rename from examples/multi_dut/GCD/dut1_always_one.sv rename to examples/V3.2/multi_dut/GCD/dut1_always_one.sv diff --git a/examples/multi_dut/GCD/dut2_off_by_one.sv b/examples/V3.2/multi_dut/GCD/dut2_off_by_one.sv similarity index 100% rename from examples/multi_dut/GCD/dut2_off_by_one.sv rename to examples/V3.2/multi_dut/GCD/dut2_off_by_one.sv diff --git a/examples/multi_dut/GCD/dut3_same_name.sv b/examples/V3.2/multi_dut/GCD/dut3_same_name.sv similarity index 100% rename from examples/multi_dut/GCD/dut3_same_name.sv rename to examples/V3.2/multi_dut/GCD/dut3_same_name.sv diff --git a/examples/multi_dut/GCD/golden.sv b/examples/V3.2/multi_dut/GCD/golden.sv similarity index 100% rename from examples/multi_dut/GCD/golden.sv rename to examples/V3.2/multi_dut/GCD/golden.sv diff --git a/examples/multi_dut/GCD/golden_model.h b/examples/V3.2/multi_dut/GCD/golden_model.h similarity index 100% rename from examples/multi_dut/GCD/golden_model.h rename to examples/V3.2/multi_dut/GCD/golden_model.h diff --git a/examples/multi_dut/pingpong/dut1_up_only.v b/examples/V3.2/multi_dut/pingpong/dut1_up_only.v similarity index 100% rename from examples/multi_dut/pingpong/dut1_up_only.v rename to examples/V3.2/multi_dut/pingpong/dut1_up_only.v diff --git a/examples/multi_dut/pingpong/dut2_down_only.v b/examples/V3.2/multi_dut/pingpong/dut2_down_only.v similarity index 100% rename from examples/multi_dut/pingpong/dut2_down_only.v rename to examples/V3.2/multi_dut/pingpong/dut2_down_only.v diff --git a/examples/multi_dut/pingpong/dut3_same_name.v b/examples/V3.2/multi_dut/pingpong/dut3_same_name.v similarity index 100% rename from examples/multi_dut/pingpong/dut3_same_name.v rename to examples/V3.2/multi_dut/pingpong/dut3_same_name.v diff --git a/examples/multi_dut/pingpong/golden.v b/examples/V3.2/multi_dut/pingpong/golden.v similarity index 100% rename from examples/multi_dut/pingpong/golden.v rename to examples/V3.2/multi_dut/pingpong/golden.v diff --git a/examples/ALU_cc/README.md b/examples/V3/ALU_cc/README.md similarity index 100% rename from examples/ALU_cc/README.md rename to examples/V3/ALU_cc/README.md diff --git a/examples/ALU_cc/gate_level.v b/examples/V3/ALU_cc/gate_level.v similarity index 100% rename from examples/ALU_cc/gate_level.v rename to examples/V3/ALU_cc/gate_level.v diff --git a/examples/ALU_cc/golden.v b/examples/V3/ALU_cc/golden.v similarity index 100% rename from examples/ALU_cc/golden.v rename to examples/V3/ALU_cc/golden.v diff --git a/examples/ALU_cc/golden_model.h b/examples/V3/ALU_cc/golden_model.h similarity index 100% rename from examples/ALU_cc/golden_model.h rename to examples/V3/ALU_cc/golden_model.h diff --git a/examples/ALU_sc/README.md b/examples/V3/ALU_sc/README.md similarity index 100% rename from examples/ALU_sc/README.md rename to examples/V3/ALU_sc/README.md diff --git a/examples/ALU_sc/gate_level.v b/examples/V3/ALU_sc/gate_level.v similarity index 100% rename from examples/ALU_sc/gate_level.v rename to examples/V3/ALU_sc/gate_level.v diff --git a/examples/ALU_sc/golden.v b/examples/V3/ALU_sc/golden.v similarity index 100% rename from examples/ALU_sc/golden.v rename to examples/V3/ALU_sc/golden.v diff --git a/examples/ALU_sc/golden_model_sc.h b/examples/V3/ALU_sc/golden_model_sc.h similarity index 100% rename from examples/ALU_sc/golden_model_sc.h rename to examples/V3/ALU_sc/golden_model_sc.h diff --git a/examples/GCD/README.md b/examples/V3/GCD/README.md similarity index 100% rename from examples/GCD/README.md rename to examples/V3/GCD/README.md diff --git a/examples/GCD/gcd.sv b/examples/V3/GCD/gcd.sv similarity index 100% rename from examples/GCD/gcd.sv rename to examples/V3/GCD/gcd.sv diff --git a/examples/GCD/gcd_buggy.sv b/examples/V3/GCD/gcd_buggy.sv similarity index 100% rename from examples/GCD/gcd_buggy.sv rename to examples/V3/GCD/gcd_buggy.sv diff --git a/examples/GCD/gcd_golden.h b/examples/V3/GCD/gcd_golden.h similarity index 100% rename from examples/GCD/gcd_golden.h rename to examples/V3/GCD/gcd_golden.h diff --git a/examples/GCD/gcd_golden.sv b/examples/V3/GCD/gcd_golden.sv similarity index 100% rename from examples/GCD/gcd_golden.sv rename to examples/V3/GCD/gcd_golden.sv diff --git a/examples/pingpong_cpp/README.md b/examples/V3/pingpong_cpp/README.md similarity index 100% rename from examples/pingpong_cpp/README.md rename to examples/V3/pingpong_cpp/README.md diff --git a/examples/pingpong_cpp/golden.v b/examples/V3/pingpong_cpp/golden.v similarity index 100% rename from examples/pingpong_cpp/golden.v rename to examples/V3/pingpong_cpp/golden.v diff --git a/examples/pingpong_cpp/golden_model.h b/examples/V3/pingpong_cpp/golden_model.h similarity index 100% rename from examples/pingpong_cpp/golden_model.h rename to examples/V3/pingpong_cpp/golden_model.h diff --git a/examples/pingpong_cpp/up_only.v b/examples/V3/pingpong_cpp/up_only.v similarity index 100% rename from examples/pingpong_cpp/up_only.v rename to examples/V3/pingpong_cpp/up_only.v diff --git a/examples/pingpong_sc/README.md b/examples/V3/pingpong_sc/README.md similarity index 100% rename from examples/pingpong_sc/README.md rename to examples/V3/pingpong_sc/README.md diff --git a/examples/pingpong_sc/golden.v b/examples/V3/pingpong_sc/golden.v similarity index 100% rename from examples/pingpong_sc/golden.v rename to examples/V3/pingpong_sc/golden.v diff --git a/examples/pingpong_sc/golden_model.h b/examples/V3/pingpong_sc/golden_model.h similarity index 100% rename from examples/pingpong_sc/golden_model.h rename to examples/V3/pingpong_sc/golden_model.h diff --git a/examples/pingpong_sc/golden_model_sc.h b/examples/V3/pingpong_sc/golden_model_sc.h similarity index 100% rename from examples/pingpong_sc/golden_model_sc.h rename to examples/V3/pingpong_sc/golden_model_sc.h diff --git a/examples/pingpong_sc/up_only.v b/examples/V3/pingpong_sc/up_only.v similarity index 100% rename from examples/pingpong_sc/up_only.v rename to examples/V3/pingpong_sc/up_only.v diff --git a/examples/V4/ineq_conditions/README.md b/examples/V4/ineq_conditions/README.md new file mode 100644 index 0000000..ae6cfe6 --- /dev/null +++ b/examples/V4/ineq_conditions/README.md @@ -0,0 +1,31 @@ +# Inequality Condition Solver Example + +This example validates `generate-fsm` behavior for: + +- impossible edge condition: `a > 10 && a < 9` +- redundant bound simplification: `a > 11 && a > 5` (effective bound is `a > 11`) + +## Run + +From project root: + +```bash +cargo run -- generate-fsm \ + ineq_fsm.sv \ + --golden ineq_fsm_golden.sv \ + --out tb_state_coverage.cpp \ + --out-exe tb_state_coverage_exe \ + --clock clk \ + --reset rst_n \ + --reset-active low \ + --fsm-method deterministic \ + --debug + +./tb_state_coverage_exe +STATS_FILE=examples/ineq_conditions/test_stats.json +``` + +## Expected + +- One edge should be reported as `impossible` for condition `a > 8'd10 && a < 8'd9`. +- The satisfiable edge with `a > 8'd11 && a > 8'd5` should be exercised. +- `test_stats.json` should contain non-zero `impossible_transitions`. diff --git a/examples/V4/ineq_conditions/ineq_fsm.sv b/examples/V4/ineq_conditions/ineq_fsm.sv new file mode 100644 index 0000000..8a4bad6 --- /dev/null +++ b/examples/V4/ineq_conditions/ineq_fsm.sv @@ -0,0 +1,51 @@ +module ineq_fsm ( + input logic clk, + input logic rst_n, + input logic [7:0] a, + output logic hit +); + + typedef enum logic [0:0] { + S_IDLE = 1'b0, + S_HIT = 1'b1 + } state_t; + + state_t state, next_state; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + state <= S_IDLE; + end else begin + state <= next_state; + end + end + + always_comb begin + next_state = state; + case (state) + S_IDLE: begin + // Impossible branch: no 8-bit value can satisfy this. + if (a > 10 && a < 9) begin + next_state = S_HIT; + // Redundant lower bound: effective condition is a > 11. + end else if (a > 8'd11 * 2 && a > 8'd5 * 2) begin + $display("a: %d", a); + next_state = S_HIT; + end else begin + next_state = S_IDLE; + end + end + S_HIT: begin + next_state = S_IDLE; + end + default: begin + next_state = S_IDLE; + end + endcase + end + + always_comb begin + hit = (state == S_HIT); + end + +endmodule diff --git a/examples/V4/ineq_conditions/ineq_fsm_golden.sv b/examples/V4/ineq_conditions/ineq_fsm_golden.sv new file mode 100644 index 0000000..ab82743 --- /dev/null +++ b/examples/V4/ineq_conditions/ineq_fsm_golden.sv @@ -0,0 +1,48 @@ +module ineq_fsm_golden ( + input logic clk, + input logic rst_n, + input logic [7:0] a, + output logic hit +); + + typedef enum logic [0:0] { + S_IDLE = 1'b0, + S_HIT = 1'b1 + } state_t; + + state_t state, next_state; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + state <= S_IDLE; + end else begin + state <= next_state; + end + end + + always_comb begin + next_state = state; + case (state) + S_IDLE: begin + if (a > 10 && a < 9) begin + next_state = S_HIT; + end else if (a > 11 && a > 5) begin + next_state = S_HIT; + end else begin + next_state = S_IDLE; + end + end + S_HIT: begin + next_state = S_IDLE; + end + default: begin + next_state = S_IDLE; + end + endcase + end + + always_comb begin + hit = (state == S_HIT); + end + +endmodule diff --git a/examples/V4/seq_detector/Makefile b/examples/V4/seq_detector/Makefile new file mode 100644 index 0000000..3e9ed33 --- /dev/null +++ b/examples/V4/seq_detector/Makefile @@ -0,0 +1,522 @@ +# Makefile for Sequence Detector State Coverage Test +# Tests the generate-fsm feature with LLM or deterministic FSM analysis + +# Paths +STG := cargo run -- +DUT := seq_detector.sv +GOLDEN := seq_detector_golden.sv +TB_OUT := tb_state_coverage.cpp +TB_EXE := tb_state_coverage_exe + +# Analysis JSON files (method-specific) +ANALYSIS_JSON_DETERMINISTIC := tb_state_coverage.deterministic.state_analysis.json +ANALYSIS_JSON_LLM := tb_state_coverage.llm.state_analysis.json + +# Traditional STG (no LLM) outputs +TB_TRADITIONAL := tb_traditional.sv +TB_TRADITIONAL_EXE := tb_traditional_exe +COVERAGE_TRADITIONAL_DIR := coverage_traditional +COVERAGE_TRADITIONAL_DAT := coverage_traditional.dat + +# Deterministic coverage outputs +COVERAGE_DETERMINISTIC_DIR := coverage_deterministic +COVERAGE_DETERMINISTIC_DAT := coverage_deterministic.dat + +# LLM coverage outputs +COVERAGE_LLM_DIR := coverage_llm +COVERAGE_LLM_DAT := coverage_llm.dat + +# LLM Configuration (can be overridden from command line) +LM_PROVIDER ?= gemini +LM_NAME ?= gemini-2.5-flash +LM_ENDPOINT ?= + +# Design Configuration +CLOCK := clk +RESET := rst_n +RESET_ACTIVE := low +RANDOM_SAMPLES := 32 + +# Compiler +SIMULATOR ?= iverilog +VERILATOR_FLAGS := --verilator + +.PHONY: all generate generate-deterministic generate-compile-deterministic compile run test clean help check-stg check-api-key +.PHONY: compile-coverage run-coverage coverage show-coverage test-coverage +.PHONY: generate-traditional compile-traditional run-traditional test-traditional +.PHONY: test-traditional-coverage compare-coverage + +# Default target +all: help + +# Help message +help: + @echo "Sequence Detector - State Coverage Test" + @echo "========================================" + @echo "" + @echo "Usage:" + @echo " make generate - Generate C++ testbench using LLM-based FSM analysis" + @echo " make generate-deterministic - Generate C++ testbench using deterministic FSM extraction (no LLM)" + @echo " make generate-compile-deterministic - Generate + compile in one step (no LLM)" + @echo " make compile - Compile the generated testbench" + @echo " make run - Run the compiled testbench" + @echo " make test - Full test: generate + compile + run" + @echo " make clean - Remove generated files" + @echo "" + @echo "Coverage (Verilator):" + @echo " make compile-coverage - Compile with Verilator coverage enabled" + @echo " make run-coverage - Run and generate coverage data" + @echo " make coverage - Generate annotated coverage report" + @echo " make show-coverage - Open coverage report" + @echo " make test-coverage - Full pipeline with coverage" + @echo "" + @echo "Comparison (STG-only vs Deterministic vs LLM-enhanced):" + @echo " make test-traditional-coverage - Run traditional STG with coverage" + @echo " make compare-coverage - Run all three methods and compare results" + @echo "" + @echo "LLM Options (can be overridden):" + @echo " LM_PROVIDER=openai - LLM provider: openai, gemini, openrouter" + @echo " LM_NAME=gpt-4 - Model name" + @echo " LM_ENDPOINT= - Custom API endpoint (optional)" + @echo "" + @echo "Examples:" + @echo " make test # Use default OpenAI gpt-4" + @echo " make test LM_PROVIDER=gemini LM_NAME=gemini-1.5-pro" + @echo " make test LM_PROVIDER=openrouter LM_NAME=anthropic/claude-3-opus" + @echo " make generate LM_ENDPOINT=http://localhost:8000/v1 LM_NAME=local-model" + @echo " make test-coverage # Full test with coverage report" + @echo " make compare-coverage # Compare STG-only vs LLM-enhanced" + @echo "" + +# Check if stg binary exists +check-stg: + @if [ ! -f $(STG) ]; then \ + echo "Error: stg binary not found at $(STG)"; \ + echo "Please run 'cargo build --release' in the project root first."; \ + exit 1; \ + fi + +# Check if API key is set +check-api-key: + @if [ "$(LM_PROVIDER)" = "openai" ] && [ -z "$$OPENAI_API_KEY" ]; then \ + echo "Error: OPENAI_API_KEY environment variable not set"; \ + echo "Run: export OPENAI_API_KEY='your-api-key'"; \ + exit 1; \ + fi + @if [ "$(LM_PROVIDER)" = "gemini" ] && [ -z "$$GOOGLE_API_KEY" ]; then \ + echo "Error: GOOGLE_API_KEY environment variable not set"; \ + echo "Run: export GOOGLE_API_KEY='your-api-key'"; \ + exit 1; \ + fi + @if [ "$(LM_PROVIDER)" = "openrouter" ] && [ -z "$$OPENROUTER_API_KEY" ]; then \ + echo "Error: OPENROUTER_API_KEY environment variable not set"; \ + echo "Run: export OPENROUTER_API_KEY='your-api-key'"; \ + exit 1; \ + fi + +# Generate state coverage testbench (LM-based) +generate: check-stg check-api-key + @echo "Generating state coverage testbench (LM-based)..." + @echo " LLM Provider: $(LM_PROVIDER)" + @echo " Model: $(LM_NAME)" + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method lm \ + --lm-provider $(LM_PROVIDER) \ + --lm-name $(LM_NAME) \ + $(if $(LM_ENDPOINT),--lm-endpoint $(LM_ENDPOINT),) + @echo "" + @echo "Generated files:" + @echo " - $(TB_OUT)" + @echo " - $(ANALYSIS_JSON_LLM)" + @echo "" + @echo "State analysis summary:" + @if [ -f $(ANALYSIS_JSON_LLM) ]; then \ + python3 -c "import json; d=json.load(open('$(ANALYSIS_JSON_LLM)')); print(' State machines:', len(d.get('state_machines', []))); [print(' -', sm.get('state_variable') + ':', len(sm.get('states', [])), 'states,', len(sm.get('transitions', [])), 'transitions') for sm in d.get('state_machines', [])]"; \ + fi + +# Generate state coverage testbench (deterministic, no LLM needed) +generate-deterministic: check-stg + @echo "Generating state coverage testbench (deterministic)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic \ + --state-analysis $(ANALYSIS_JSON_DETERMINISTIC) + @echo "" + @echo "Generated files:" + @echo " - $(TB_OUT)" + @echo " - $(ANALYSIS_JSON_DETERMINISTIC)" + +# Generate + compile in one step (deterministic) +generate-compile-deterministic: check-stg + @echo "Generating and compiling state coverage testbench (deterministic)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --out-exe $(TB_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic + @echo "Generated and compiled: $(TB_EXE)" + +# Compile C++ testbench (using stg generate-fsm --out-exe) +compile: $(TB_OUT) + @echo "Compiling C++ testbench with Verilator..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --out-exe $(TB_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic + @mv tb_state_coverage.state_analysis.json $(ANALYSIS_JSON_DETERMINISTIC) 2>/dev/null || true + @echo "Compiled: $(TB_EXE)" + +# Compile with Verilator (using generate-fsm --out-exe for C++ testbenches) +compile-verilator: check-stg $(TB_OUT) + @echo "Compiling C++ testbench with Verilator..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --out-exe $(TB_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic + @mv tb_state_coverage.state_analysis.json $(ANALYSIS_JSON_DETERMINISTIC) 2>/dev/null || true + @echo "Compiled: $(TB_EXE)" + +# Compile with Verilator coverage enabled (C++ testbench) +compile-coverage: check-stg $(TB_OUT) + @echo "Compiling C++ testbench with Verilator (coverage enabled)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --out-exe $(TB_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic \ + --verilator-coverage + @mv tb_state_coverage.state_analysis.json $(ANALYSIS_JSON_DETERMINISTIC) 2>/dev/null || true + @echo "Compiled with coverage: $(TB_EXE)" + +# Run testbench and generate coverage data +run-coverage: $(TB_EXE) + @echo "Running testbench with coverage..." + @echo "========================================" + ./$(TB_EXE) + @mv coverage.dat $(COVERAGE_DETERMINISTIC_DAT) 2>/dev/null || true + @echo "========================================" + @echo "Coverage data written to: $(COVERAGE_DETERMINISTIC_DAT)" + +# Generate annotated coverage report +coverage: $(COVERAGE_DETERMINISTIC_DAT) + @echo "Generating coverage report..." + @mkdir -p $(COVERAGE_DETERMINISTIC_DIR) + verilator_coverage --annotate $(COVERAGE_DETERMINISTIC_DIR) $(COVERAGE_DETERMINISTIC_DAT) + @echo "" + @echo "Coverage report generated in: $(COVERAGE_DETERMINISTIC_DIR)/" + @echo "" + @echo "Coverage summary:" + @verilator_coverage --annotate-min 1 $(COVERAGE_DETERMINISTIC_DAT) 2>/dev/null | head -20 || true + +# Show coverage report summary +show-coverage: $(COVERAGE_DETERMINISTIC_DIR) + @echo "Coverage Report Files:" + @echo "========================================" + @ls -la $(COVERAGE_DETERMINISTIC_DIR)/ + @echo "" + @echo "Annotated source files (showing uncovered lines):" + @for f in $(COVERAGE_DETERMINISTIC_DIR)/*.v $(COVERAGE_DETERMINISTIC_DIR)/*.sv 2>/dev/null; do \ + if [ -f "$$f" ]; then \ + echo ""; \ + echo "=== $$f ==="; \ + cat "$$f"; \ + fi; \ + done + +# Full test pipeline with coverage +test-coverage: generate-deterministic compile-coverage run-coverage coverage + @echo "" + @echo "Test with coverage completed!" + @echo "View detailed report: make show-coverage" + +# Run testbench +run: $(TB_EXE) + @echo "Running testbench..." + @echo "========================================" + ./$(TB_EXE) + @echo "========================================" + @echo "Test completed." + +# Full test pipeline +test: generate compile run + +# Generate only (for inspection) +generate-only: generate + @echo "" + @echo "Generated testbench: $(TB_OUT)" + @echo "You can inspect the state analysis in: $(ANALYSIS_JSON)" + +# Show state analysis +show-analysis: $(ANALYSIS_JSON_DETERMINISTIC) + @echo "State Machine Analysis (Deterministic):" + @echo "========================================" + @python3 -c "import json; print(json.dumps(json.load(open('$(ANALYSIS_JSON_DETERMINISTIC)')), indent=2))" + +# Clean generated files +clean: + rm -f $(TB_OUT) $(TB_EXE) $(ANALYSIS_JSON_DETERMINISTIC) $(ANALYSIS_JSON_LLM) + rm -f tb_state_coverage_cpp.cpp tb_state_coverage_cpp_exe tb_state_coverage_cpp.state_analysis.json + rm -f $(TB_TRADITIONAL) $(TB_TRADITIONAL_EXE) + rm -f *.vcd $(COVERAGE_DETERMINISTIC_DAT) $(COVERAGE_LLM_DAT) $(COVERAGE_TRADITIONAL_DAT) + rm -f test_stats.json + rm -rf obj_dir $(COVERAGE_DETERMINISTIC_DIR) $(COVERAGE_LLM_DIR) $(COVERAGE_TRADITIONAL_DIR) + @echo "Cleaned generated files." + +# Check dependencies +check-deps: + @echo "Checking dependencies..." + @which python3 > /dev/null || (echo "Error: python3 not found" && exit 1) + @which iverilog > /dev/null || echo "Warning: iverilog not found (needed for compile)" + @python3 -c "import openai" 2>/dev/null || echo "Warning: openai package not installed (needed for OpenAI provider)" + @python3 -c "import google.generativeai" 2>/dev/null || echo "Warning: google-generativeai not installed (needed for Gemini provider)" + @echo "Dependency check complete." + +# ============================================================================ +# Traditional STG (No LLM) - For Coverage Comparison +# ============================================================================ + +# Generate traditional testbench using stg generate (no LLM) +generate-traditional: check-stg + @echo "Generating traditional testbench (STG-only, no LLM)..." + $(STG) generate \ + $(DUT) \ + --golden $(GOLDEN) \ + --type seq_clocked \ + --out $(TB_TRADITIONAL) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) + @echo "Generated: $(TB_TRADITIONAL)" + +# Compile traditional testbench with coverage +compile-traditional-coverage: check-stg $(TB_TRADITIONAL) + @echo "Compiling traditional testbench with Verilator (coverage enabled)..." + $(STG) compile \ + $(DUT) \ + --golden $(GOLDEN) \ + --testbench $(TB_TRADITIONAL) \ + --out-exe $(TB_TRADITIONAL_EXE) \ + --verilator \ + --verilator-coverage + @echo "Compiled with coverage: $(TB_TRADITIONAL_EXE)" + +# Run traditional testbench with coverage +run-traditional-coverage: $(TB_TRADITIONAL_EXE) + @echo "Running traditional testbench..." + @echo "========================================" + ./$(TB_TRADITIONAL_EXE) + @mv coverage.dat $(COVERAGE_TRADITIONAL_DAT) 2>/dev/null || true + @echo "========================================" + @echo "Coverage data written to: $(COVERAGE_TRADITIONAL_DAT)" + +# Generate traditional coverage report +coverage-traditional: $(COVERAGE_TRADITIONAL_DAT) + @echo "Generating traditional coverage report..." + @mkdir -p $(COVERAGE_TRADITIONAL_DIR) + verilator_coverage --annotate $(COVERAGE_TRADITIONAL_DIR) $(COVERAGE_TRADITIONAL_DAT) + @echo "Coverage report generated in: $(COVERAGE_TRADITIONAL_DIR)/" + +# Full traditional test with coverage +test-traditional-coverage: generate-traditional compile-traditional-coverage run-traditional-coverage coverage-traditional + @echo "" + @echo "Traditional STG test with coverage completed!" + +# ============================================================================ +# LLM-Enhanced State Coverage - For Coverage Comparison +# ============================================================================ + +# Compile LLM-enhanced testbench with coverage +compile-llm-coverage: check-stg check-api-key + @echo "Generating and compiling LLM-enhanced testbench with Verilator (coverage enabled)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --out-exe $(TB_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method lm \ + --lm-provider $(LM_PROVIDER) \ + --lm-name $(LM_NAME) \ + $(if $(LM_ENDPOINT),--lm-endpoint $(LM_ENDPOINT),) \ + --verilator-coverage + @mv tb_state_coverage.state_analysis.json $(ANALYSIS_JSON_LLM) 2>/dev/null || true + @echo "Compiled with coverage: $(TB_EXE)" + +# Run LLM-enhanced testbench with coverage +run-llm-coverage: $(TB_EXE) + @echo "Running LLM-enhanced testbench..." + @echo "========================================" + ./$(TB_EXE) + @mv coverage.dat $(COVERAGE_LLM_DAT) 2>/dev/null || true + @echo "========================================" + @echo "Coverage data written to: $(COVERAGE_LLM_DAT)" + +# Generate LLM-enhanced coverage report +coverage-llm: $(COVERAGE_LLM_DAT) + @echo "Generating LLM-enhanced coverage report..." + @mkdir -p $(COVERAGE_LLM_DIR) + verilator_coverage --annotate $(COVERAGE_LLM_DIR) $(COVERAGE_LLM_DAT) + @echo "Coverage report generated in: $(COVERAGE_LLM_DIR)/" + +# ============================================================================ +# Deterministic State Coverage - For Coverage Comparison +# ============================================================================ + +# Compile deterministic testbench with coverage +compile-deterministic-coverage: check-stg + @echo "Compiling deterministic testbench with Verilator (coverage enabled)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_OUT) \ + --out-exe $(TB_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic \ + --verilator-coverage + @echo "Compiled with coverage: $(TB_EXE)" + +# Run deterministic testbench with coverage +run-deterministic-coverage: $(TB_EXE) + @echo "Running deterministic testbench..." + @echo "========================================" + ./$(TB_EXE) + @mv coverage.dat $(COVERAGE_DETERMINISTIC_DAT) 2>/dev/null || true + @echo "========================================" + @echo "Coverage data written to: $(COVERAGE_DETERMINISTIC_DAT)" + +# Generate deterministic coverage report +coverage-deterministic: $(COVERAGE_DETERMINISTIC_DAT) + @echo "Generating deterministic coverage report..." + @mkdir -p $(COVERAGE_DETERMINISTIC_DIR) + verilator_coverage --annotate $(COVERAGE_DETERMINISTIC_DIR) $(COVERAGE_DETERMINISTIC_DAT) + @echo "Coverage report generated in: $(COVERAGE_DETERMINISTIC_DIR)/" + +# ============================================================================ +# Coverage Comparison: STG-only vs Deterministic vs LLM-enhanced +# ============================================================================ + +# Coverage directories and data files for deterministic method +COVERAGE_DETERMINISTIC_DIR := coverage_deterministic +COVERAGE_DETERMINISTIC_DAT := coverage_deterministic.dat + +compare-coverage: clean + @echo "============================================================" + @echo "Coverage Comparison: STG-only vs Deterministic vs LLM-enhanced" + @echo "============================================================" + @echo "" + @echo ">>> Step 1: Running Traditional STG (Random Testing)..." + @echo "" + @$(MAKE) generate-traditional + @$(MAKE) compile-traditional-coverage + @$(MAKE) run-traditional-coverage + @$(MAKE) coverage-traditional + @echo "" + @echo ">>> Step 2: Running Deterministic State Coverage..." + @echo "" + @$(MAKE) generate-deterministic + @$(MAKE) compile-coverage + @$(MAKE) run-coverage + @$(MAKE) coverage + @echo "" + @echo ">>> Step 3: Running LLM-enhanced State Coverage..." + @echo "" + @$(MAKE) compile-llm-coverage + @$(MAKE) run-llm-coverage + @$(MAKE) coverage-llm + + @echo "" + @echo "============================================================" + @echo "COVERAGE COMPARISON RESULTS" + @echo "============================================================" + @echo "" + @echo "--- Traditional STG (Random Testing) ---" + @if [ -f $(COVERAGE_TRADITIONAL_DAT) ]; then \ + verilator_coverage $(COVERAGE_TRADITIONAL_DAT) 2>/dev/null | grep "Total coverage" || echo " See $(COVERAGE_TRADITIONAL_DIR)/"; \ + fi + @echo "" + @echo "--- Deterministic State Coverage ---" + @if [ -f $(COVERAGE_DETERMINISTIC_DAT) ]; then \ + verilator_coverage $(COVERAGE_DETERMINISTIC_DAT) 2>/dev/null | grep "Total coverage" || echo " See $(COVERAGE_DETERMINISTIC_DIR)/"; \ + fi + @echo "" + @echo "--- LLM-enhanced State Coverage (Targeted Testing) ---" + @if [ -f $(COVERAGE_LLM_DAT) ]; then \ + verilator_coverage $(COVERAGE_LLM_DAT) 2>/dev/null | grep "Total coverage" || echo " See $(COVERAGE_LLM_DIR)/"; \ + fi + @echo "" + @echo "============================================================" + @echo "DUT State Coverage Analysis (seq_detector.sv)" + @echo "============================================================" + @echo "" + @echo "--- Traditional STG ---" + @if [ -f $(COVERAGE_TRADITIONAL_DIR)/seq_detector.sv ]; then \ + echo "States hit (lines with hits > 0):"; \ + grep -E "^\s*[0-9]+ +S[0-9]+:" $(COVERAGE_TRADITIONAL_DIR)/seq_detector.sv 2>/dev/null | wc -l | xargs -I {} echo " {} states covered"; \ + echo "States missed (lines with %000000):"; \ + grep -E "^%000000 +S[0-9]+:" $(COVERAGE_TRADITIONAL_DIR)/seq_detector.sv 2>/dev/null | wc -l | xargs -I {} echo " {} states NOT covered"; \ + fi + @echo "" + @echo "--- Deterministic State Coverage ---" + @if [ -f $(COVERAGE_DETERMINISTIC_DIR)/seq_detector.sv ]; then \ + echo "States hit (lines with hits > 0):"; \ + grep -E "^\s*[0-9]+ +S[0-9]+:" $(COVERAGE_DETERMINISTIC_DIR)/seq_detector.sv 2>/dev/null | wc -l | xargs -I {} echo " {} states covered"; \ + echo "States missed (lines with %000000):"; \ + grep -E "^%000000 +S[0-9]+:" $(COVERAGE_DETERMINISTIC_DIR)/seq_detector.sv 2>/dev/null | wc -l | xargs -I {} echo " {} states NOT covered"; \ + fi + @echo "" + @echo "--- LLM-enhanced ---" + @if [ -f $(COVERAGE_LLM_DIR)/seq_detector.sv ]; then \ + echo "States hit (lines with hits > 0):"; \ + grep -E "^\s*[0-9]+ +S[0-9]+:" $(COVERAGE_LLM_DIR)/seq_detector.sv 2>/dev/null | wc -l | xargs -I {} echo " {} states covered"; \ + echo "States missed (lines with %000000):"; \ + grep -E "^%000000 +S[0-9]+:" $(COVERAGE_LLM_DIR)/seq_detector.sv 2>/dev/null | wc -l | xargs -I {} echo " {} states NOT covered"; \ + fi + @echo "" + @echo "============================================================" + @echo "View detailed reports:" + @echo " Traditional: $(COVERAGE_TRADITIONAL_DIR)/seq_detector.sv" + @echo " Deterministic: $(COVERAGE_DETERMINISTIC_DIR)/seq_detector.sv" + @echo " LLM-enhanced: $(COVERAGE_LLM_DIR)/seq_detector.sv" + @echo "============================================================" diff --git a/examples/V4/seq_detector/README.md b/examples/V4/seq_detector/README.md new file mode 100644 index 0000000..1081c3d --- /dev/null +++ b/examples/V4/seq_detector/README.md @@ -0,0 +1,115 @@ +# Mealy Sequence Detector Example + +This example demonstrates the `generate-fsm` feature using a Mealy-style finite state machine that detects the pattern "101001010010100" (15-bit) in a serial input stream. + +## Design Description + +- **Type**: Sequential (clocked) Mealy FSM +- **Pattern**: Detects "101001010010100" (15 bits) +- **Overlapping**: Yes (sliding window detection) +- **DUT**: Traditional FSM with 15 states (no shift register) +- **Golden**: Behavioral model using shift register + +### State Machine + +``` +States (15 states for 15-bit pattern): + S0: Initial state (no match) + S1: Matched "1" + S2: Matched "10" + S3: Matched "101" + S4: Matched "1010" + S5: Matched "10100" + S6: Matched "101001" + S7: Matched "1010010" + S8: Matched "10100101" + S9: Matched "101001010" + S10: Matched "1010010100" + S11: Matched "10100101001" + S12: Matched "101001010010" + S13: Matched "1010010100101" + S14: Matched "10100101001010" + +Detection occurs when in S14 and din=0 (Mealy output) + +Key failure transitions (for sliding window): + S3 --din=1--> S1 ("1011" -> keep "1") + S4 --din=1--> S3 ("10101" -> keep "101") + S9 --din=1--> S3 ("1010010101" -> keep "101") + S12 --din=0--> S5 ("1010010100100" -> keep "10100") + S14 --din=1--> S3 ("101001010010101" -> keep "101") + S14 --din=0--> S5 (DETECT! "101001010010100" -> keep "10100") +``` + +## Files + +- `seq_detector.sv` - DUT: Mealy sequence detector (traditional FSM) +- `seq_detector_golden.sv` - Golden model using shift register +- `Makefile` - Build and test automation with coverage support + +## Usage + +### Prerequisites + +1. Set your LLM API key: + ```bash + # For OpenAI + export OPENAI_API_KEY="your-api-key" + + # For Gemini + export GOOGLE_API_KEY="your-api-key" + + # For OpenRouter + export OPENROUTER_API_KEY="your-api-key" + ``` + +2. Install `uv` and make sure it is visible in `$PATH` + +### Running Tests + +```bash +# Generate state coverage testbench (default: OpenAI gpt-4) +make generate + +# Generate with different LLM providers +make generate LM_PROVIDER=gemini LM_NAME=gemini-1.5-pro +make generate LM_PROVIDER=openrouter LM_NAME=anthropic/claude-3-opus + +# Compile and run the testbench +make run + +# Clean generated files +make clean + +# Full test (generate + compile + run) +make test +``` + +### Manual Usage + +```bash +# Using stg generate-fsm directly +../../target/release/stg generate-fsm \ + seq_detector.sv \ + --golden seq_detector_golden.sv \ + --out tb_state_coverage.sv \ + --out-exe tb_state_coverage_exe \ + --clock clk \ + --reset rst_n \ + --reset-active low \ + --lm-provider openai \ + --lm-name gpt-4 + +# Run the generated testbench +./tb_state_coverage_exe +``` + +## Expected Output + +The LLM should identify: +- 4-5 states (S0, S1, S2, S3, possibly S4) +- 8 transitions covering all state changes +- Control signal: `din` +- Clock: `clk`, Reset: `rst_n` (active low) + +The DFS algorithm will generate test sequences to cover all state transitions. diff --git a/examples/V4/traffic_light/Makefile b/examples/V4/traffic_light/Makefile new file mode 100644 index 0000000..dc3e9ed --- /dev/null +++ b/examples/V4/traffic_light/Makefile @@ -0,0 +1,419 @@ +# Makefile for Traffic Light Controller - Coverage Comparison +# Compares traditional STG (random testing) vs coverage-enhanced STG (DFS-driven) + +# Paths +STG := cargo run -- +DUT := traffic_light_controller.sv +GOLDEN := traffic_light_controller_golden.sv + +# Enhanced state coverage outputs +TB_ENHANCED := tb_state_coverage.cpp +TB_ENHANCED_EXE := tb_state_coverage_exe + +# Analysis JSON files (method-specific) +ANALYSIS_JSON_DETERMINISTIC := tb_state_coverage.deterministic.state_analysis.json +ANALYSIS_JSON_LLM := tb_state_coverage.llm.state_analysis.json + +# Traditional STG outputs +TB_TRADITIONAL := tb_traditional.sv +TB_TRADITIONAL_EXE := tb_traditional_exe + +# Coverage outputs +COVERAGE_DETERMINISTIC_DIR := coverage_deterministic +COVERAGE_DETERMINISTIC_DAT := coverage_deterministic.dat +COVERAGE_LLM_DIR := coverage_llm +COVERAGE_LLM_DAT := coverage_llm.dat +COVERAGE_TRADITIONAL_DIR := coverage_traditional +COVERAGE_TRADITIONAL_DAT := coverage_traditional.dat + +# LLM Configuration (can be overridden from command line) +LM_PROVIDER ?= gemini +LM_NAME ?= gemini-2.5-flash +LM_ENDPOINT ?= + +# Design Configuration +CLOCK := clk +RESET := rst_n +RESET_ACTIVE := low +RANDOM_SAMPLES := 32 + +.PHONY: all help check-stg check-api-key +.PHONY: generate generate-deterministic generate-compile-deterministic compile run test +.PHONY: compile-coverage run-coverage coverage show-coverage test-coverage +.PHONY: generate-traditional compile-traditional run-traditional test-traditional +.PHONY: compile-traditional-coverage run-traditional-coverage coverage-traditional test-traditional-coverage +.PHONY: compare-coverage clean + +# Default target +all: help + +# Help message +help: + @echo "Traffic Light Controller - Coverage Comparison" + @echo "===============================================" + @echo "" + @echo "Enhanced State Coverage (DFS-driven testing):" + @echo " make generate - Generate C++ testbench (LLM-based FSM)" + @echo " make generate-deterministic - Generate C++ testbench (deterministic FSM)" + @echo " make generate-compile-deterministic - Generate + compile (deterministic, no LLM)" + @echo " make compile - Compile the generated testbench" + @echo " make run - Run the compiled testbench" + @echo " make test - Full: generate + compile + run" + @echo "" + @echo "Traditional STG (random testing):" + @echo " make generate-traditional - Generate SV testbench (random only)" + @echo " make test-traditional - Full: generate + compile + run" + @echo "" + @echo "Coverage Comparison:" + @echo " make test-coverage - Enhanced STG with Verilator coverage" + @echo " make test-traditional-coverage - Traditional STG with Verilator coverage" + @echo " make compare-coverage - Run all three methods and compare results" + @echo "" + @echo "LLM Options (can be overridden):" + @echo " LM_PROVIDER=gemini - LLM provider: openai, gemini, openrouter" + @echo " LM_NAME=gemini-2.5-flash - Model name" + @echo " LM_ENDPOINT= - Custom API endpoint (optional)" + @echo "" + @echo "Examples:" + @echo " make compare-coverage # Three-way comparison" + @echo " make test LM_PROVIDER=gemini LM_NAME=gemini-2.5-flash # Use Gemini" + @echo " make generate-compile-deterministic # Quick no-LLM test" + @echo "" + +# Check if stg binary exists +check-stg: + @if [ ! -f $(STG) ]; then \ + echo "Error: stg binary not found at $(STG)"; \ + echo "Please run 'cargo build --release' in the project root first."; \ + exit 1; \ + fi + +# Check if API key is set +check-api-key: + @if [ "$(LM_PROVIDER)" = "openai" ] && [ -z "$$OPENAI_API_KEY" ]; then \ + echo "Error: OPENAI_API_KEY environment variable not set"; \ + exit 1; \ + fi + @if [ "$(LM_PROVIDER)" = "gemini" ] && [ -z "$$GOOGLE_API_KEY" ]; then \ + echo "Error: GOOGLE_API_KEY environment variable not set"; \ + exit 1; \ + fi + +# ============================================================================ +# Enhanced State Coverage (DFS-driven) +# ============================================================================ + +# Generate state coverage testbench (LM-based) +generate: check-stg check-api-key + @echo "Generating state coverage testbench (LM-based)..." + @echo " LLM Provider: $(LM_PROVIDER)" + @echo " Model: $(LM_NAME)" + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_ENHANCED) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method lm \ + --lm-provider $(LM_PROVIDER) \ + --lm-name $(LM_NAME) \ + $(if $(LM_ENDPOINT),--lm-endpoint $(LM_ENDPOINT),) + @echo "" + @echo "Generated: $(TB_ENHANCED)" + @echo "" + @echo "State analysis summary:" + @if [ -f $(ANALYSIS_JSON_LLM) ]; then \ + python3 -c "import json; d=json.load(open('$(ANALYSIS_JSON_LLM)')); print(' State machines:', len(d.get('state_machines', []))); [print(' -', sm.get('state_variable') + ':', len(sm.get('states', [])), 'states,', len(sm.get('transitions', [])), 'transitions') for sm in d.get('state_machines', [])]"; \ + fi + +# Generate state coverage testbench (deterministic, no LLM needed) +generate-deterministic: check-stg + @echo "Generating state coverage testbench (deterministic)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_ENHANCED) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic + @echo "" + @echo "Generated: $(TB_ENHANCED)" + @echo "State analysis: $(ANALYSIS_JSON_DETERMINISTIC)" + +# Generate + compile in one step (deterministic) +generate-compile-deterministic: check-stg + @echo "Generating and compiling state coverage testbench (deterministic)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_ENHANCED) \ + --out-exe $(TB_ENHANCED_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic + @echo "Generated and compiled: $(TB_ENHANCED_EXE)" + +# Compile C++ testbench +compile: $(TB_ENHANCED) + @echo "Compiling C++ testbench with Verilator..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_ENHANCED) \ + --out-exe $(TB_ENHANCED_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic + @mv tb_state_coverage.state_analysis.json $(ANALYSIS_JSON_DETERMINISTIC) 2>/dev/null || true + @echo "Compiled: $(TB_ENHANCED_EXE)" + +# Compile with Verilator coverage enabled (C++ testbench) +compile-coverage: check-stg $(TB_ENHANCED) + @echo "Compiling C++ testbench with Verilator (coverage enabled)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_ENHANCED) \ + --out-exe $(TB_ENHANCED_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method deterministic \ + --verilator-coverage + @mv tb_state_coverage.state_analysis.json $(ANALYSIS_JSON_DETERMINISTIC) 2>/dev/null || true + @echo "Compiled with coverage: $(TB_ENHANCED_EXE)" + +# Run testbench +run: $(TB_ENHANCED_EXE) + @echo "Running enhanced state coverage testbench..." + @echo "========================================" + ./$(TB_ENHANCED_EXE) + @echo "========================================" + @echo "Test completed." + +# Run testbench with coverage +run-coverage: $(TB_ENHANCED_EXE) + @echo "Running deterministic testbench with coverage..." + @echo "========================================" + ./$(TB_ENHANCED_EXE) + @mv coverage.dat $(COVERAGE_DETERMINISTIC_DAT) 2>/dev/null || true + @echo "========================================" + @echo "Coverage data: $(COVERAGE_DETERMINISTIC_DAT)" + +# Generate coverage report +coverage: $(COVERAGE_DETERMINISTIC_DAT) + @echo "Generating deterministic coverage report..." + @mkdir -p $(COVERAGE_DETERMINISTIC_DIR) + @verilator_coverage --annotate $(COVERAGE_DETERMINISTIC_DIR) $(COVERAGE_DETERMINISTIC_DAT) + @echo "Report: $(COVERAGE_DETERMINISTIC_DIR)/" + +# Full test pipeline +test: generate-compile-deterministic run + +# Full test with coverage +test-coverage: generate-deterministic compile-coverage run-coverage coverage + @echo "" + @echo "Deterministic state coverage test with coverage completed!" + @echo "View detailed report: ls $(COVERAGE_DETERMINISTIC_DIR)/" + +# ============================================================================ +# Traditional STG (Random Testing) +# ============================================================================ + +# Generate traditional testbench using stg generate +generate-traditional: check-stg + @echo "Generating traditional testbench (random only)..." + $(STG) generate \ + $(DUT) \ + --golden $(GOLDEN) \ + --type seq_clocked \ + --out $(TB_TRADITIONAL) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) + @echo "Generated: $(TB_TRADITIONAL)" + +# Compile traditional testbench with coverage +compile-traditional-coverage: check-stg $(TB_TRADITIONAL) + @echo "Compiling traditional testbench with Verilator (coverage enabled)..." + $(STG) compile \ + $(DUT) \ + --golden $(GOLDEN) \ + --testbench $(TB_TRADITIONAL) \ + --out-exe $(TB_TRADITIONAL_EXE) \ + --verilator \ + --verilator-coverage + @echo "Compiled with coverage: $(TB_TRADITIONAL_EXE)" + +# Run traditional testbench with coverage +run-traditional-coverage: $(TB_TRADITIONAL_EXE) + @echo "Running traditional testbench..." + @echo "========================================" + ./$(TB_TRADITIONAL_EXE) + @mv coverage.dat $(COVERAGE_TRADITIONAL_DAT) 2>/dev/null || true + @echo "========================================" + @echo "Coverage data: $(COVERAGE_TRADITIONAL_DAT)" + +# Generate traditional coverage report +coverage-traditional: $(COVERAGE_TRADITIONAL_DAT) + @echo "Generating traditional coverage report..." + @mkdir -p $(COVERAGE_TRADITIONAL_DIR) + verilator_coverage --annotate $(COVERAGE_TRADITIONAL_DIR) $(COVERAGE_TRADITIONAL_DAT) + @echo "Report: $(COVERAGE_TRADITIONAL_DIR)/" + +# Full traditional test with coverage +test-traditional-coverage: generate-traditional compile-traditional-coverage run-traditional-coverage coverage-traditional + @echo "" + @echo "Traditional STG test with coverage completed!" + +# ============================================================================ +# LLM-Enhanced State Coverage - For Coverage Comparison +# ============================================================================ + +# Compile LLM-enhanced testbench with coverage +compile-llm-coverage: check-stg check-api-key + @echo "Generating and compiling LLM-enhanced testbench with Verilator (coverage enabled)..." + $(STG) generate-fsm \ + $(DUT) \ + --golden $(GOLDEN) \ + --out $(TB_ENHANCED) \ + --out-exe $(TB_ENHANCED_EXE) \ + --clock $(CLOCK) \ + --reset $(RESET) \ + --reset-active $(RESET_ACTIVE) \ + --random-samples $(RANDOM_SAMPLES) \ + --fsm-method lm \ + --lm-provider $(LM_PROVIDER) \ + --lm-name $(LM_NAME) \ + $(if $(LM_ENDPOINT),--lm-endpoint $(LM_ENDPOINT),) \ + --verilator-coverage + @mv tb_state_coverage.state_analysis.json $(ANALYSIS_JSON_LLM) 2>/dev/null || true + @echo "Compiled with coverage: $(TB_ENHANCED_EXE)" + +# Run LLM-enhanced testbench with coverage +run-llm-coverage: $(TB_ENHANCED_EXE) + @echo "Running LLM-enhanced testbench..." + @echo "========================================" + ./$(TB_ENHANCED_EXE) + @mv coverage.dat $(COVERAGE_LLM_DAT) 2>/dev/null || true + @echo "========================================" + @echo "Coverage data: $(COVERAGE_LLM_DAT)" + +# Generate LLM-enhanced coverage report +coverage-llm: $(COVERAGE_LLM_DAT) + @echo "Generating LLM-enhanced coverage report..." + @mkdir -p $(COVERAGE_LLM_DIR) + @verilator_coverage --annotate $(COVERAGE_LLM_DIR) $(COVERAGE_LLM_DAT) + @echo "Report: $(COVERAGE_LLM_DIR)/" + +# Run traditional test (no coverage) +test-traditional: generate-traditional + @echo "Compiling and running traditional testbench..." + $(STG) compile \ + $(DUT) \ + --golden $(GOLDEN) \ + --testbench $(TB_TRADITIONAL) \ + --out-exe $(TB_TRADITIONAL_EXE) \ + --verilator + ./$(TB_TRADITIONAL_EXE) + +# ============================================================================ +# Coverage Comparison: Traditional STG vs Deterministic vs LLM-enhanced +# ============================================================================ + +compare-coverage: clean + @echo "============================================================" + @echo "Coverage Comparison: STG-only vs Deterministic vs LLM-enhanced" + @echo "============================================================" + @echo "" + @echo ">>> Step 1: Running Traditional STG (random testing)..." + @echo "" + @$(MAKE) generate-traditional + @$(MAKE) compile-traditional-coverage + @$(MAKE) run-traditional-coverage + @$(MAKE) coverage-traditional + @echo "" + @echo ">>> Step 2: Running Deterministic State Coverage..." + @echo "" + @$(MAKE) generate-deterministic + @$(MAKE) compile-coverage + @$(MAKE) run-coverage + @$(MAKE) coverage + @echo "" + @echo ">>> Step 3: Running LLM-enhanced State Coverage..." + @echo "" + @$(MAKE) compile-llm-coverage + @$(MAKE) run-llm-coverage + @$(MAKE) coverage-llm + @echo "" + @echo "============================================================" + @echo "COVERAGE COMPARISON RESULTS" + @echo "============================================================" + @echo "" + @echo "--- Traditional STG (Random Testing) ---" + @if [ -f $(COVERAGE_TRADITIONAL_DAT) ]; then \ + verilator_coverage $(COVERAGE_TRADITIONAL_DAT) 2>/dev/null | grep -i "total\|point" || echo " See $(COVERAGE_TRADITIONAL_DIR)/"; \ + fi + @echo "" + @echo "--- Deterministic State Coverage ---" + @if [ -f $(COVERAGE_DETERMINISTIC_DAT) ]; then \ + verilator_coverage $(COVERAGE_DETERMINISTIC_DAT) 2>/dev/null | grep -i "total\|point" || echo " See $(COVERAGE_DETERMINISTIC_DIR)/"; \ + fi + @echo "" + @echo "--- LLM-enhanced State Coverage (Targeted Testing) ---" + @if [ -f $(COVERAGE_LLM_DAT) ]; then \ + verilator_coverage $(COVERAGE_LLM_DAT) 2>/dev/null | grep -i "total\|point" || echo " See $(COVERAGE_LLM_DIR)/"; \ + fi + @echo "" + @echo "============================================================" + @echo "DUT State Coverage Analysis ($(DUT))" + @echo "============================================================" + @echo "" + @echo "--- Traditional STG ---" + @if [ -f $(COVERAGE_TRADITIONAL_DIR)/$(DUT) ]; then \ + echo "Lines hit (hits > 0):"; \ + grep -cE "^\s+[0-9]+" $(COVERAGE_TRADITIONAL_DIR)/$(DUT) 2>/dev/null | xargs -I {} echo " {} lines covered"; \ + echo "Lines missed (%000000):"; \ + grep -c "%000000" $(COVERAGE_TRADITIONAL_DIR)/$(DUT) 2>/dev/null | xargs -I {} echo " {} lines NOT covered"; \ + fi + @echo "" + @echo "--- Deterministic State Coverage ---" + @if [ -f $(COVERAGE_DETERMINISTIC_DIR)/$(DUT) ]; then \ + echo "Lines hit (hits > 0):"; \ + grep -cE "^\s+[0-9]+" $(COVERAGE_DETERMINISTIC_DIR)/$(DUT) 2>/dev/null | xargs -I {} echo " {} lines covered"; \ + echo "Lines missed (%000000):"; \ + grep -c "%000000" $(COVERAGE_DETERMINISTIC_DIR)/$(DUT) 2>/dev/null | xargs -I {} echo " {} lines NOT covered"; \ + fi + @echo "" + @echo "--- LLM-enhanced ---" + @if [ -f $(COVERAGE_LLM_DIR)/$(DUT) ]; then \ + echo "Lines hit (hits > 0):"; \ + grep -cE "^\s+[0-9]+" $(COVERAGE_LLM_DIR)/$(DUT) 2>/dev/null | xargs -I {} echo " {} lines covered"; \ + echo "Lines missed (%000000):"; \ + grep -c "%000000" $(COVERAGE_LLM_DIR)/$(DUT) 2>/dev/null | xargs -I {} echo " {} lines NOT covered"; \ + fi + @echo "" + @echo "============================================================" + @echo "View detailed reports:" + @echo " Traditional: $(COVERAGE_TRADITIONAL_DIR)/$(DUT)" + @echo " Deterministic: $(COVERAGE_DETERMINISTIC_DIR)/$(DUT)" + @echo " LLM-enhanced: $(COVERAGE_LLM_DIR)/$(DUT)" + @echo "============================================================" + +# Clean generated files +clean: + rm -f $(TB_ENHANCED) $(TB_ENHANCED_EXE) $(ANALYSIS_JSON_DETERMINISTIC) $(ANALYSIS_JSON_LLM) + rm -f $(TB_TRADITIONAL) $(TB_TRADITIONAL_EXE) + rm -f *.vcd *.dat test_stats.json + rm -rf obj_dir $(COVERAGE_DETERMINISTIC_DIR) $(COVERAGE_LLM_DIR) $(COVERAGE_TRADITIONAL_DIR) + @echo "Cleaned generated files." diff --git a/examples/V4/traffic_light/README.md b/examples/V4/traffic_light/README.md new file mode 100644 index 0000000..3564c77 --- /dev/null +++ b/examples/V4/traffic_light/README.md @@ -0,0 +1,107 @@ +# Traffic Light Controller Example + +This example demonstrates the coverage advantage of `generate-fsm` (DFS-driven testing) versus traditional STG (random testing) using a traffic light controller with internal counters. + +## Design Description + +- **Type**: Sequential (clocked) FSM with internal timer +- **States**: 8 states controlling a main/side road intersection with pedestrian crossing +- **Inputs**: `clk`, `rst_n`, `sensor` (side road vehicle), `ped_request` (pedestrian button) +- **Outputs**: `main_light[1:0]`, `side_light[1:0]`, `ped_signal`, `walk_active` +- **No latches**: All signals assigned in all branches via `always_comb` defaults + +### Why This Is Hard for Random Testing + +The FSM uses an **internal counter** (`timer`) to gate most state transitions. For example: +- Reaching `S_SIDE_GREEN` from reset requires **15 clock cycles** (10 green + 3 yellow + 2 all-red) AND `sensor=1` at the right moment +- Reaching `S_PED_WALK` requires `ped_request=1` to be latched AND the timer to expire in `S_ALL_RED_1` or `S_ALL_RED_2` without `sensor=1` +- Random toggling of inputs is unlikely to satisfy these multi-cycle timing constraints + +### State Machine + +``` + timer >= 9 + S_MAIN_GREEN ──────────────────────────> S_MAIN_YELLOW + ^ | + | timer >= 2 + | v + | !sensor && !ped_latch S_ALL_RED_1 + +<─────────────────────────────┐ | + | | timer >= 1 + | | ┌────┴────┐ + | | | | + | !sensor sensor ped_latch + | !ped | | + | | v v + | | S_SIDE_GREEN S_PED_WALK + | | | | + | | timer>=9 timer>=5 + | | v v + | | S_SIDE_YELLOW S_PED_CLEAR + | | | | + | | timer>=2 timer>=3 + | | v | + | !ped_latch | S_ALL_RED_2 | + +<─────────────────────────────+────┘ | + +<────────────────────────────────────────────┘ +``` + +## Files + +- `traffic_light_controller.sv` - DUT: 8-state FSM with internal timer +- `traffic_light_controller_golden.sv` - Golden model: independent behavioral implementation +- `Makefile` - Build automation with coverage comparison support + +## Usage + +### Quick Test (no LLM needed) + +```bash +# Build stg first (from project root) +cargo build --release + +# Generate, compile, and run with deterministic FSM extraction +make generate-compile-deterministic +make run +``` + +### Coverage Comparison + +```bash +# Compare traditional (random) vs enhanced (DFS-driven) coverage +make compare-coverage +``` + +This runs both approaches with Verilator coverage instrumentation and shows side-by-side results. + +### Individual Commands + +```bash +# Enhanced state coverage (deterministic - no LLM) +make generate-deterministic # Generate C++ testbench +make compile # Compile with Verilator +make run # Execute + +# Enhanced state coverage (LLM-based) +make generate LM_PROVIDER=gemini LM_NAME=gemini-2.5-flash + +# Traditional STG (random testing) +make test-traditional + +# Clean all generated files +make clean +``` + +## Expected Results + +The coverage comparison should show that: + +1. **Traditional STG** (random inputs): Likely misses `S_PED_WALK` and `S_PED_CLEAR` states because reaching them requires: + - `ped_request` to be asserted and latched + - `sensor` to be low when `S_ALL_RED_1` timer expires + - All within the right timing window + +2. **Enhanced state coverage** (DFS-driven): Achieves full state/transition coverage because it: + - Identifies the FSM structure and internal counter conditions + - Generates targeted input sequences via DFS traversal + - Uses hierarchical signal access to monitor internal state diff --git a/src/cli.rs b/src/cli.rs index b126bdd..f485772 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,6 +24,9 @@ pub enum Commands { Compile(Box), /// Parse Verilog modules and print the module priority list Parse(Box), + /// Generate a state-coverage-enhanced C++ testbench using FSM analysis (LM or deterministic) + #[command(name = "generate-fsm")] + GenerateFSM(Box), } #[derive(Args, Debug)] @@ -323,6 +326,122 @@ impl ParseArgs { } } +#[derive(Args, Debug)] +pub struct GenerateFSMArgs { + /// Path to DUT Verilog/SystemVerilog file (can be specified as positional arguments or with --verilog) + #[arg(long, value_delimiter(','))] + pub verilog: Option>, + + /// Positional Verilog files (if --verilog not specified) + #[arg(value_name = "VERILOG_FILES")] + pub verilog_files: Vec, + + /// DUT module name (if file has multiple) + #[arg(long, value_delimiter(','))] + pub module: Option>, + + /// Path to golden reference Verilog/SystemVerilog file + #[arg(long, required = true)] + pub golden: PathBuf, + + /// Golden module name (if file has multiple) + #[arg(long)] + pub golden_module: Option, + + /// Output testbench path (.cpp) + #[arg(long, required = true)] + pub out: PathBuf, + + /// Output executable path. If not specified, no executable will be generated + #[arg(long)] + pub out_exe: Option, + + /// Clock signal name + #[arg(long)] + pub clock: Option, + + /// Reset signal name + #[arg(long)] + pub reset: Option, + + /// Reset polarity + #[arg(long, value_parser = ["high", "low", "unknown"])] + pub reset_active: Option, + + /// Random samples for additional random testing after DFS sequences + #[arg(long, default_value_t = 32)] + pub random_samples: i64, + + /// FSM identification method + #[arg(long, value_parser = ["lm", "deterministic"], default_value = "lm")] + pub fsm_method: String, + + /// LLM provider (for --fsm-method lm) + #[arg(long, value_parser = ["openai", "gemini", "openrouter"])] + pub lm_provider: Option, + + /// LLM model name (for --fsm-method lm) + #[arg(long)] + pub lm_name: Option, + + /// Custom LLM API endpoint (for --fsm-method lm) + #[arg(long)] + pub lm_endpoint: Option, + + /// Path to state analysis JSON file. If the file exists, it will be loaded + /// (skipping FSM identification). If it does not exist, FSM analysis will run + /// and the result will be saved to this path for future reuse. + #[arg(long)] + pub state_analysis: Option, + + /// Ignore some Verilator warnings: WIDTHTRUNC, WIDTHCONCAT, WIDTHEXPAND + #[arg(long, default_value_t = true)] + pub verilator_ignore_warnings: bool, + + /// Number of jobs to use for Verilator compilation + #[arg(long, default_value_t = 1)] + pub verilator_jobs: i32, + + /// Enable Verilator coverage analysis + #[arg(long, default_value_t = false)] + pub verilator_coverage: bool, + + /// Enable line execution count checks for FSM transition verification + #[arg(long, default_value_t = false)] + pub line_coverage_check: bool, + + /// Compile flags + #[arg(long)] + pub compile_flags: Option>, + + /// Transition timeout in clock cycles (for wait conditions) + #[arg(long, default_value_t = 100)] + pub transition_timeout: i64, + + /// Number of DFS passes with randomized data signals for edge coverage + #[arg(long, default_value_t = 5)] + pub dfs_passes: i64, + + /// Enable debug mode + #[arg(long, default_value_t = false)] + pub debug: bool, +} + +impl GenerateFSMArgs { + /// Get the Verilog files from either --verilog flag or positional arguments + pub fn get_verilog_files(&self) -> Result> { + if let Some(ref verilog) = self.verilog { + Ok(verilog.clone()) + } else if !self.verilog_files.is_empty() { + Ok(self.verilog_files.clone()) + } else { + anyhow::bail!( + "No Verilog files specified. Use --verilog flag or provide files as positional arguments" + ) + } + } +} + pub fn string_or_vec<'de, D>(deserializer: D) -> Result>, D::Error> where D: serde::Deserializer<'de>, diff --git a/src/commands/compile.rs b/src/commands/compile.rs index 63cd246..15b7741 100644 --- a/src/commands/compile.rs +++ b/src/commands/compile.rs @@ -72,6 +72,7 @@ pub fn run_compile(args: CompileCommandArgs) -> Result<()> { emplace_module: args.emplace_module, verilator_ignore_warnings: args.verilator_ignore_warnings, verilator_mpi: args.verilator_mpi, + line_coverage_check: false, compile_flags: args.compile_flags, verilator_jobs: args.verilator_jobs, }; diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 6d5fdd1..0512ae3 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -606,6 +606,7 @@ pub fn run_generate(args: GenerateArgs) -> Result<()> { emplace_module: args.emplace_module, verilator_ignore_warnings: args.verilator_ignore_warnings, verilator_mpi: args.verilator_mpi, + line_coverage_check: false, compile_flags: args.compile_flags, verilator_jobs: args.verilator_jobs, }; diff --git a/src/commands/generate_fsm.rs b/src/commands/generate_fsm.rs new file mode 100644 index 0000000..2286aef --- /dev/null +++ b/src/commands/generate_fsm.rs @@ -0,0 +1,546 @@ +use crate::cli::GenerateFSMArgs; +use crate::tools::{ + compiler::{CompileArgs, compile_state_coverage_cpp_verilator}, + fsm::{ + condition_splitter::split_transition_conditions, + extractor, + lm_analyzer::run_lm_fsm_analysis, + template::{TemplateState, TemplateStateMachine, parse_state_value}, + types::{FsmAnalysis, load_fsm_analysis, save_fsm_analysis}, + }, + verilog_parser::{ModuleInfo, PortInfo, parse_verilog_modules}, +}; +use anyhow::{Context, Result}; +use serde::Serialize; +use std::fs; +use std::path::{Path, PathBuf}; +use tera::{Context as TeraContext, Tera}; + +fn load_state_coverage_cpp_template() -> Result { + Ok(include_str!("../templates/testbench_state_coverage_cpp.j2").to_string()) +} + +#[derive(Debug, Clone, Serialize)] +struct SerializablePortInfo { + name: String, + direction: String, + width: usize, +} + +impl From<&PortInfo> for SerializablePortInfo { + fn from(port: &PortInfo) -> Self { + Self { + name: port.name.clone(), + direction: port.direction.clone(), + width: port.width, + } + } +} + +/// Configuration for C++ testbench generation +struct TestbenchConfig<'a> { + analysis: &'a FsmAnalysis, + dut_module: &'a ModuleInfo, + golden_module: &'a ModuleInfo, + template_sms: &'a [TemplateStateMachine], + clock_name: &'a str, + reset_name: &'a str, + reset_active_high: bool, + args: &'a GenerateFSMArgs, + out_path: &'a Path, +} + +pub fn run_generate_fsm(args: GenerateFSMArgs) -> Result<()> { + let verilog_files = args.get_verilog_files()?; + let verilog_paths = verilog_files + .iter() + .map(|path| { + path.canonicalize() + .with_context(|| format!("Failed to canonicalize DUT file: {}", path.display())) + }) + .collect::>>()?; + + let golden_path = args.golden.canonicalize().with_context(|| { + format!( + "Failed to canonicalize golden file: {}", + args.golden.display() + ) + })?; + + let out_path = std::path::absolute(&args.out) + .with_context(|| format!("Failed to resolve output file: {}", args.out.display()))?; + + // ======================================================================== + // Step 1: Parse DUT and golden Verilog files + // ======================================================================== + log::info!("Parsing DUT Verilog files..."); + let all_dut_modules: Vec> = verilog_paths + .iter() + .map(|path| { + parse_verilog_modules(path) + .with_context(|| format!("Failed to parse DUT file: {}", path.display())) + }) + .collect::>>>()?; + + for (idx, modules) in all_dut_modules.iter().enumerate() { + if modules.is_empty() { + anyhow::bail!( + "No modules found in DUT file {}", + verilog_paths[idx].display() + ); + } + } + + log::info!("Parsing golden Verilog file..."); + let golden_modules = parse_verilog_modules(&golden_path) + .with_context(|| format!("Failed to parse golden file: {}", golden_path.display()))?; + if golden_modules.is_empty() { + anyhow::bail!("No modules found in golden file {}", golden_path.display()); + } + + // Select DUT module + let dut_module = select_dut_module(&args, &all_dut_modules, &verilog_paths)?; + + // Select golden module + let golden_module = select_golden_module(&args, &golden_modules)?; + + // Resolve clock and reset names + let clock_name = args.clock.as_deref().unwrap_or("clk").to_string(); + let reset_name = args.reset.as_deref().unwrap_or("rst").to_string(); + let reset_active_high = args + .reset_active + .as_deref() + .map(|s| s == "high") + .unwrap_or(false); + + log::info!("DUT module: {}", dut_module.name); + log::info!("Golden module: {}", golden_module.name); + log::info!( + "Clock: {}, Reset: {} (active {})", + clock_name, + reset_name, + if reset_active_high { "high" } else { "low" } + ); + + // ======================================================================== + // Step 2: FSM Identification + // ======================================================================== + let analysis = identify_fsm( + &args, + &verilog_paths[0], + &dut_module, + &out_path, + &clock_name, + &reset_name, + reset_active_high, + )?; + + // Print summary + for sm in &analysis.state_machines { + log::info!( + "State machine '{}': {} states, {} transitions", + sm.state_variable, + sm.states.len(), + sm.transitions.len() + ); + } + + if analysis.state_machines.is_empty() { + anyhow::bail!( + "No state machines found in the design. Cannot generate state coverage testbench." + ); + } + + // ======================================================================== + // Step 2.5: Parse DUT for signal classification and split conditions + // ======================================================================== + let analysis = post_process_analysis(analysis, &clock_name, &reset_name); + + // Save the analysis JSON + let analysis_path = determine_analysis_path(&args, &out_path); + save_fsm_analysis(&analysis, &analysis_path)?; + log::info!("State analysis saved to: {}", analysis_path.display()); + + // ======================================================================== + // Step 3: Prepare template data (graph data for C++ DFS engine) + // ======================================================================== + log::info!("Preparing FSM graph data for C++ DFS engine..."); + let template_sms = prepare_template_data(&analysis); + let total_edges: usize = template_sms.iter().map(|sm| sm.transitions.len()).sum(); + log::info!( + "FSM graph: {} state machine(s), {} total edges for DFS coverage", + template_sms.len(), + total_edges + ); + + // ======================================================================== + // Step 4: Generate C++ testbench with DFS engine + // ======================================================================== + log::info!("Generating C++ testbench with DFS engine..."); + let config = TestbenchConfig { + analysis: &analysis, + dut_module: &dut_module, + golden_module: &golden_module, + template_sms: &template_sms, + clock_name: &clock_name, + reset_name: &reset_name, + reset_active_high, + args: &args, + out_path: &out_path, + }; + generate_cpp_testbench(config)?; + + // ======================================================================== + // Step 5: Compile (if --out-exe specified) + // ======================================================================== + if let Some(ref out_exe) = args.out_exe { + compile_testbench( + &args, + out_exe, + &verilog_paths, + &dut_module, + &golden_path, + &golden_module, + &out_path, + )?; + } + + Ok(()) +} + +/// Select the DUT module from parsed modules +fn select_dut_module( + args: &GenerateFSMArgs, + all_dut_modules: &[Vec], + verilog_paths: &[PathBuf], +) -> Result { + if let Some(ref module_names) = args.module { + let name = &module_names[0]; + all_dut_modules[0] + .iter() + .find(|m| &m.name == name) + .with_context(|| { + format!( + "DUT module '{}' not found in {}", + name, + verilog_paths[0].display() + ) + }) + .cloned() + } else { + if all_dut_modules[0].len() > 1 { + log::warn!( + "Multiple DUT modules found, using first: {}", + all_dut_modules[0][0].name + ); + } + Ok(all_dut_modules[0][0].clone()) + } +} + +/// Select the golden module from parsed modules +fn select_golden_module( + args: &GenerateFSMArgs, + golden_modules: &[ModuleInfo], +) -> Result { + if let Some(ref name) = args.golden_module { + golden_modules + .iter() + .find(|m| &m.name == name) + .with_context(|| format!("Golden module '{}' not found", name)) + .cloned() + } else { + if golden_modules.len() > 1 { + log::warn!( + "Multiple golden modules found, using first: {}", + golden_modules[0].name + ); + } + Ok(golden_modules[0].clone()) + } +} + +/// Identify FSM using the specified method (LM or deterministic) +fn identify_fsm( + args: &GenerateFSMArgs, + verilog_path: &Path, + dut_module: &ModuleInfo, + out_path: &Path, + clock_name: &str, + reset_name: &str, + reset_active_high: bool, +) -> Result { + if let Some(ref state_analysis_path) = args.state_analysis { + if state_analysis_path.exists() { + // Use pre-computed analysis (cache hit) + log::info!( + "Loading pre-computed state analysis from: {}", + state_analysis_path.display() + ); + return load_fsm_analysis(state_analysis_path); + } else { + // File doesn't exist yet — run analysis and save to this path + log::info!( + "State analysis file not found at: {}. Running FSM analysis...", + state_analysis_path.display() + ); + } + } + + // Run FSM analysis + if args.fsm_method == "lm" { + run_lm_fsm_analysis( + verilog_path, + out_path, + args.lm_provider.as_deref(), + args.lm_name.as_deref(), + args.lm_endpoint.as_deref(), + ) + } else { + log::info!("Running deterministic FSM extraction..."); + extractor::extract_fsm( + verilog_path, + dut_module, + clock_name, + reset_name, + reset_active_high, + ) + } +} + +/// Post-process FSM analysis: build control signals set and split conditions +fn post_process_analysis( + mut analysis: FsmAnalysis, + clock_name: &str, + reset_name: &str, +) -> FsmAnalysis { + // Build control signals set for condition splitting + let mut control_signal_names: std::collections::HashSet = + std::collections::HashSet::new(); + control_signal_names.insert(clock_name.to_string()); + control_signal_names.insert(reset_name.to_string()); + for sm in &analysis.state_machines { + for sig in &sm.control_signals { + control_signal_names.insert(sig.clone()); + } + } + + // Post-process transitions to split input vs wait conditions + for sm in &mut analysis.state_machines { + split_transition_conditions(sm, &control_signal_names); + } + + analysis +} + +/// Determine where to save the analysis JSON +fn determine_analysis_path(args: &GenerateFSMArgs, out_path: &Path) -> PathBuf { + if let Some(ref state_analysis_path) = args.state_analysis { + state_analysis_path.clone() + } else { + out_path.with_extension("state_analysis.json") + } +} + +/// Prepare template data structures from FSM analysis +fn prepare_template_data(analysis: &FsmAnalysis) -> Vec { + analysis + .state_machines + .iter() + .map(|sm| TemplateStateMachine { + state_variable: sm.state_variable.clone(), + state_type: sm.state_type.clone(), + initial_state: sm.initial_state.clone(), + states: sm + .states + .iter() + .map(|s| TemplateState { + name: s.name.clone(), + value: s.value.clone(), + value_numeric: parse_state_value(&s.value), + }) + .collect(), + transitions: sm.transitions.clone(), + control_signals: sm.control_signals.clone(), + data_signals: sm.data_signals.clone(), + internal_signals: sm.internal_signals.clone(), + parameters: sm.parameters.clone(), + }) + .collect() +} + +/// Generate C++ testbench from template +fn generate_cpp_testbench(config: TestbenchConfig) -> Result<()> { + let template_text = load_state_coverage_cpp_template()?; + let mut tera = Tera::default(); + tera.add_raw_template("testbench_state_coverage_cpp.j2", &template_text) + .context("Failed to parse state coverage C++ template")?; + + // Gather signal information + let input_ports: Vec<&PortInfo> = config + .dut_module + .ports + .iter() + .filter(|p| p.direction == "input") + .collect(); + + let output_ports: Vec = config + .dut_module + .ports + .iter() + .filter(|p| p.direction == "output") + .map(SerializablePortInfo::from) + .collect(); + + // Build control signals set + let mut control_signal_names: std::collections::HashSet = + std::collections::HashSet::new(); + control_signal_names.insert(config.clock_name.to_string()); + control_signal_names.insert(config.reset_name.to_string()); + for sm in &config.analysis.state_machines { + for sig in &sm.control_signals { + control_signal_names.insert(sig.clone()); + } + } + + let control_signals: Vec = input_ports + .iter() + .filter(|p| { + control_signal_names.contains(&p.name) + && p.name != config.clock_name + && p.name != config.reset_name + }) + .map(|p| SerializablePortInfo::from(*p)) + .collect(); + + let data_signals: Vec = input_ports + .iter() + .filter(|p| { + !control_signal_names.contains(&p.name) + && p.name != config.clock_name + && p.name != config.reset_name + }) + .map(|p| SerializablePortInfo::from(*p)) + .collect(); + + // Collect runtime signals and parameters from all state machines (deduplicated) + let (all_runtime_signals, all_parameters) = collect_runtime_data(config.analysis); + + log::info!( + "Runtime signals: {:?}, Parameters: {:?}", + all_runtime_signals, + all_parameters + .iter() + .map(|p| format!("{}={}", p.name, p.value)) + .collect::>() + ); + + let mut context = TeraContext::new(); + context.insert("module_name", &config.analysis.module_name); + context.insert("dut_module_name", &config.dut_module.name); + context.insert("golden_module_name", &config.golden_module.name); + context.insert("clock_signal", config.clock_name); + context.insert("reset_signal", config.reset_name); + context.insert("reset_active_high", &config.reset_active_high); + context.insert("state_machines", config.template_sms); + context.insert("outputs", &output_ports); + context.insert("control_signals", &control_signals); + context.insert("data_signals", &data_signals); + context.insert("runtime_signals", &all_runtime_signals); + context.insert("parameters", &all_parameters); + context.insert("transition_timeout", &config.args.transition_timeout); + context.insert("dfs_passes", &config.args.dfs_passes); + context.insert("random_samples", &config.args.random_samples); + context.insert("debug", &config.args.debug); + context.insert("line_coverage_check", &config.args.line_coverage_check); + + let cpp_text = tera + .render("testbench_state_coverage_cpp.j2", &context) + .context("Failed to render state coverage C++ testbench template")?; + + // Write output + if let Some(parent) = config.out_path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(config.out_path, &cpp_text)?; + log::info!("Wrote C++ testbench: {}", config.out_path.display()); + + Ok(()) +} + +/// Collect runtime signals and parameters from all state machines +fn collect_runtime_data( + analysis: &FsmAnalysis, +) -> (Vec, Vec) { + let mut all_runtime_signals: Vec = Vec::new(); + let mut all_parameters: Vec = Vec::new(); + + for sm in &analysis.state_machines { + for sig in &sm.internal_signals { + if !all_runtime_signals.contains(sig) { + all_runtime_signals.push(sig.clone()); + } + } + for param in &sm.parameters { + if !all_parameters.iter().any(|p| p.name == param.name) { + all_parameters.push(param.clone()); + } + } + } + + (all_runtime_signals, all_parameters) +} + +/// Compile the generated testbench +fn compile_testbench( + args: &GenerateFSMArgs, + out_exe: &Path, + verilog_paths: &[PathBuf], + dut_module: &ModuleInfo, + golden_path: &Path, + golden_module: &ModuleInfo, + out_path: &Path, +) -> Result<()> { + let out_exe_path = std::path::absolute(out_exe).with_context(|| { + format!( + "Failed to resolve output executable path: {}", + out_exe.display() + ) + })?; + + log::info!("Compiling testbench..."); + + let mut compile_flags_vec = args.compile_flags.clone(); + if args.verilator_coverage || args.line_coverage_check { + let flags = compile_flags_vec.get_or_insert_with(Vec::new); + if !flags.iter().any(|f| f.contains("--coverage")) { + flags.push("--coverage".to_string()); + } + } + + let compile_args = CompileArgs { + emplace_module: false, + verilator_ignore_warnings: args.verilator_ignore_warnings, + verilator_mpi: false, + verilator_jobs: args.verilator_jobs, + line_coverage_check: args.line_coverage_check, + compile_flags: compile_flags_vec, + }; + + let success = compile_state_coverage_cpp_verilator( + &compile_args, + verilog_paths, + &dut_module.name, + golden_path, + &golden_module.name, + out_path, + &out_exe_path, + )?; + + if success { + log::info!("Wrote executable: {}", out_exe_path.display()); + Ok(()) + } else { + anyhow::bail!("Failed to compile executable: {}", out_exe_path.display()) + } +} diff --git a/src/commands/parse.rs b/src/commands/parse.rs index 0fee5a3..eb51c36 100644 --- a/src/commands/parse.rs +++ b/src/commands/parse.rs @@ -24,7 +24,7 @@ pub fn run_parse(args: ParseArgs) -> Result<()> { for module in modules { file_content.push_str(&format!("- name: {}\n", module.name)); file_content.push_str(&format!(" submodule_count: {}\n", module.submodule_count)); - file_content.push_str(&format!(" ports:\n")); + file_content.push_str(" ports:\n"); for port in module.ports { file_content.push_str(&format!(" - name: {}\n", port.name)); file_content.push_str(&format!(" direction: {}\n", port.direction)); diff --git a/src/lib.rs b/src/lib.rs index b4a1ce0..1ccba0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ /// Structured Testbench Generation library pub mod cli; +pub mod python_runtime; pub mod tools { pub mod compiler; pub mod emplace_verilog; pub mod file_utils; + pub mod fsm; pub mod generator; pub mod signal_classification; pub mod verilog_parser; @@ -15,6 +17,7 @@ pub mod tools { pub mod commands { pub mod compile; pub mod generate; + pub mod generate_fsm; pub mod identify; pub mod parse; } diff --git a/src/main.rs b/src/main.rs index 89091ea..ffd4599 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::process; use stg::cli::{Cli, Commands}; use stg::commands::compile::run_compile; use stg::commands::generate::run_generate; +use stg::commands::generate_fsm::run_generate_fsm; use stg::commands::identify::run_identify; use stg::commands::parse::run_parse; @@ -16,6 +17,7 @@ fn main() { Commands::Identify(args) => run_identify(*args), Commands::Compile(args) => run_compile(*args), Commands::Parse(args) => run_parse(*args), + Commands::GenerateFSM(args) => run_generate_fsm(*args), }; if let Err(e) = result { diff --git a/src/python_runtime.rs b/src/python_runtime.rs new file mode 100644 index 0000000..312c840 --- /dev/null +++ b/src/python_runtime.rs @@ -0,0 +1,145 @@ +//! Python Runtime Management +//! +//! This module handles embedding, extraction, and execution of Python-based analysis tools. +//! The fsm_analyzer Python package is embedded into the binary at compile time and extracted +//! to a cache directory (~/.stg/ or /tmp/.stg-{uid}) on first use. Dependencies are managed +//! via uv, with caching to avoid redundant syncs across runs. + +use anyhow::{Context, Result}; +use include_dir::{include_dir, Dir}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +// Embed the fsm_analyzer directory at compile time +static FSM_ANALYZER_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/tools/fsm_analyzer"); + +/// Get or create the STG cache directory +/// Priority: ~/.stg/ > /tmp/.stg-{uid} +pub fn get_cache_dir() -> Result { + // Try home directory first + if let Ok(home) = std::env::var("HOME") { + let cache_dir = Path::new(&home).join(".stg"); + if cache_dir.exists() || fs::create_dir_all(&cache_dir).is_ok() { + return Ok(cache_dir); + } + } + + // Fall back to /tmp with uid suffix for multi-user safety + let uid = unsafe { libc::getuid() }; + let tmp_dir = PathBuf::from(format!("/tmp/.stg-{}", uid)); + fs::create_dir_all(&tmp_dir) + .context("Failed to create cache directory in /tmp")?; + Ok(tmp_dir) +} + +/// Check if uv is installed +pub fn check_uv_installed() -> Result<()> { + Command::new("uv") + .arg("--version") + .output() + .context("uv is not installed or not in PATH. Please install uv: https://docs.astral.sh/uv/getting-started/installation/")?; + Ok(()) +} + +/// Extract embedded fsm_analyzer to cache directory if not present or outdated +/// Returns the path to the extracted fsm_analyzer directory +pub fn extract_fsm_analyzer() -> Result { + let cache_dir = get_cache_dir()?; + let fsm_dir = cache_dir.join("fsm_analyzer"); + + // Check if we need to extract (directory doesn't exist or version mismatch) + let version_file = fsm_dir.join(".version"); + let current_version = env!("CARGO_PKG_VERSION"); + let needs_extraction = !fsm_dir.exists() + || !version_file.exists() + || fs::read_to_string(&version_file).unwrap_or_default().trim() != current_version; + + if needs_extraction { + log::info!("Extracting fsm_analyzer to cache directory: {}", fsm_dir.display()); + + // Remove old directory if it exists + if fsm_dir.exists() { + fs::remove_dir_all(&fsm_dir) + .context("Failed to remove old fsm_analyzer directory")?; + } + + // Extract all files + extract_dir(&FSM_ANALYZER_DIR, &fsm_dir) + .context("Failed to extract fsm_analyzer")?; + + // Write version file + fs::write(&version_file, current_version) + .context("Failed to write version file")?; + + log::info!("fsm_analyzer extracted successfully"); + } else { + log::debug!("Using cached fsm_analyzer from: {}", fsm_dir.display()); + } + + Ok(fsm_dir) +} + +/// Recursively extract a directory +fn extract_dir(dir: &Dir, target: &Path) -> Result<()> { + fs::create_dir_all(target)?; + + for file in dir.files() { + // file.path() is relative to the Dir root, so we need just the filename + if let Some(filename) = file.path().file_name() { + let file_path = target.join(filename); + fs::write(&file_path, file.contents())?; + } + } + + for subdir in dir.dirs() { + // Use only the directory name, not the full path + if let Some(dirname) = subdir.path().file_name() { + let subdir_path = target.join(dirname); + extract_dir(subdir, &subdir_path)?; + } + } + + Ok(()) +} + +/// Run uv sync in the fsm_analyzer directory if needed +pub fn ensure_fsm_analyzer_deps(fsm_dir: &Path) -> Result<()> { + let lock_file = fsm_dir.join("uv.lock"); + let venv_dir = fsm_dir.join(".venv"); + + // Check if we need to sync (no venv or lock file changed) + let needs_sync = !venv_dir.exists() || !lock_file.exists(); + + if needs_sync { + log::info!("Running uv sync for fsm_analyzer dependencies..."); + let status = Command::new("uv") + .arg("sync") + .current_dir(fsm_dir) + .status() + .context("Failed to run uv sync")?; + + if !status.success() { + anyhow::bail!("uv sync failed with exit code: {}", status); + } + log::info!("fsm_analyzer dependencies synced successfully"); + } else { + log::debug!("fsm_analyzer dependencies already synced"); + } + + Ok(()) +} + +/// Get the path to the fsm_analyzer directory, extracting and setting up if needed +pub fn get_fsm_analyzer_path() -> Result { + // Check uv is installed first + check_uv_installed()?; + + // Extract to cache + let fsm_dir = extract_fsm_analyzer()?; + + // Ensure dependencies are synced + ensure_fsm_analyzer_deps(&fsm_dir)?; + + Ok(fsm_dir) +} diff --git a/src/templates/testbench_state_coverage_cpp.j2 b/src/templates/testbench_state_coverage_cpp.j2 new file mode 100644 index 0000000..615b996 --- /dev/null +++ b/src/templates/testbench_state_coverage_cpp.j2 @@ -0,0 +1,1249 @@ +// C++ State Coverage Testbench generated by STG generate-fsm +// Module: {{ module_name }} +// This testbench performs runtime DFS to cover all FSM state transitions. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "V{{ dut_module_name }}.h" +#include "V{{ golden_module_name }}.h" +#include "V{{ dut_module_name }}___024root.h" + +#if defined(COVERAGE) || defined(LINE_COVERAGE_CHECK) +#include +#endif + +// ============================================================================ +// Random Number Generator +// ============================================================================ + +std::mt19937_64 rng; + +uint64_t get_random_bits(int bits) { + if (bits >= 64) return rng(); + return rng() & ((1ULL << bits) - 1); +} + +// ============================================================================ +// Test Statistics +// ============================================================================ + +int64_t total_transitions_tested = 0; +int64_t covered_transitions = 0; +int64_t timeout_transitions = 0; +int64_t impossible_transitions = 0; +int64_t dfs_passes_completed = 0; +bool simulation_passed = true; +int64_t line_check_passed_transitions = 0; +int64_t line_check_failed_transitions = 0; + +{%- for p in outputs %} +int64_t {{ p.name }}_total_checks = 0; +int64_t {{ p.name }}_error_checks = 0; +{%- endfor %} + +// ============================================================================ +// FSM Graph Data Structures +// ============================================================================ + +struct Edge { + int id; + std::string from_state; + std::string to_state; + std::string input_condition; + std::string wait_condition; + int priority; +}; + +struct FSMGraph { + std::string state_variable; + std::string initial_state; + std::vector state_names; + std::vector state_values; + std::vector edges; + std::map> adj; + + void build() { + adj.clear(); + for (size_t i = 0; i < edges.size(); i++) { + adj[edges[i].from_state].push_back(static_cast(i)); + } + for (auto& [_, indices] : adj) { + std::sort(indices.begin(), indices.end(), + [this](int a, int b) { return edges[a].priority < edges[b].priority; }); + } + } + + std::string value_to_name(uint64_t val) const { + for (size_t i = 0; i < state_values.size(); i++) { + if (state_values[i] == val) return state_names[i]; + } + return "UNKNOWN(" + std::to_string(val) + ")"; + } +}; + +// ============================================================================ +// Coverage Snapshot Helpers (Line Execution Validation) +// ============================================================================ + +#ifdef LINE_COVERAGE_CHECK +struct CovSnapshot { + std::map line_counts; +}; + +CovSnapshot take_coverage_snapshot() { + CovSnapshot snapshot; + auto* contextp = Verilated::threadContextp(); + if (!contextp || !contextp->coveragep()) return snapshot; + auto counters = contextp->coveragep()->getCounters(); + for (const auto& counter : counters) { + if (counter.type == "line" || counter.type == "block" || counter.type.empty()) { + const std::string key = + counter.filename + ":" + std::to_string(counter.lineno) + ":" + counter.hier; + snapshot.line_counts[key] = counter.count; + } + } + return snapshot; +} + +bool any_lines_executed(const CovSnapshot& before, const CovSnapshot& after) { + for (const auto& [key, count] : after.line_counts) { + const auto before_it = before.line_counts.find(key); + const uint64_t before_count = + (before_it == before.line_counts.end()) ? 0 : before_it->second; + if (count > before_count) return true; + } + return false; +} +#endif + +// ============================================================================ +// Expression Parser — Recursive Descent for Verilog-style Conditions +// ============================================================================ +// +// Supports: identifiers, integer literals (decimal + Verilog N'bX/N'dX/N'hX/N'oX), +// && || & | ! ~ == != >= <= > < + - * ( ) +// +// Operator precedence (lowest to highest): +// ||, | → &&, & → ==, != → >=, <=, >, < → +, - → * → !, ~ → primary + +// ---- Token types ---- + +enum class TokType { + LIT, ID, LPAREN, RPAREN, + AND, OR, NOT, + EQ, NE, GE, LE, GT, LT, + PLUS, MINUS, MUL, MOD, + BAND, BOR, BXOR, + SHL, SHR, + END_TOK +}; + +struct Tok { + TokType ty; + std::string text; + uint64_t num; +}; + +// ---- Lexer / Tokenizer ---- + +class Lexer { + const std::string& src_; + size_t pos_; + + void ws() { + while (pos_ < src_.size() && std::isspace(static_cast(src_[pos_]))) + ++pos_; + } + +public: + explicit Lexer(const std::string& s) : src_(s), pos_(0) {} + + Tok next() { + ws(); + if (pos_ >= src_.size()) return {TokType::END_TOK, "", 0}; + + // Two-character operators (check first) + if (pos_ + 1 < src_.size()) { + char a = src_[pos_], b = src_[pos_ + 1]; + if (a == '&' && b == '&') { pos_ += 2; return {TokType::AND, "&&", 0}; } + if (a == '|' && b == '|') { pos_ += 2; return {TokType::OR, "||", 0}; } + if (a == '=' && b == '=') { pos_ += 2; return {TokType::EQ, "==", 0}; } + if (a == '!' && b == '=') { pos_ += 2; return {TokType::NE, "!=", 0}; } + if (a == '>' && b == '=') { pos_ += 2; return {TokType::GE, ">=", 0}; } + if (a == '<' && b == '=') { pos_ += 2; return {TokType::LE, "<=", 0}; } + if (a == '<' && b == '<') { pos_ += 2; return {TokType::SHL, "<<", 0}; } + if (a == '>' && b == '>') { pos_ += 2; return {TokType::SHR, ">>", 0}; } + } + + char c = src_[pos_]; + + // Single-character operators + switch (c) { + case '!': ++pos_; return {TokType::NOT, "!", 0}; + case '~': ++pos_; return {TokType::NOT, "~", 0}; + case '>': ++pos_; return {TokType::GT, ">", 0}; + case '<': ++pos_; return {TokType::LT, "<", 0}; + case '+': ++pos_; return {TokType::PLUS, "+", 0}; + case '-': ++pos_; return {TokType::MINUS, "-", 0}; + case '*': ++pos_; return {TokType::MUL, "*", 0}; + case '%': ++pos_; return {TokType::MOD, "%", 0}; + case '^': ++pos_; return {TokType::BXOR, "^", 0}; + case '(': ++pos_; return {TokType::LPAREN, "(", 0}; + case ')': ++pos_; return {TokType::RPAREN, ")", 0}; + case '&': ++pos_; return {TokType::BAND, "&", 0}; + case '|': ++pos_; return {TokType::BOR, "|", 0}; + default: break; + } + + // Number literal (decimal or Verilog N'bX / N'dX / N'hX / N'oX) + if (std::isdigit(static_cast(c))) return lex_num(); + + // Verilog literal without width prefix: 'b0, 'hFF, etc. + if (c == '\'') return lex_verilog_no_width(); + + // Identifier or keyword + if (std::isalpha(static_cast(c)) || c == '_') return lex_id(); + + // Unknown character — skip it + ++pos_; + return {TokType::END_TOK, "", 0}; + } + +private: + Tok lex_num() { + size_t s = pos_; + while (pos_ < src_.size() && std::isdigit(static_cast(src_[pos_]))) ++pos_; + + // Check for Verilog literal suffix: N'bXXX / N'dXXX / N'hXXX / N'oXXX + if (pos_ < src_.size() && src_[pos_] == '\'') { + ++pos_; // skip ' + if (pos_ < src_.size()) { + char base = src_[pos_]; ++pos_; + size_t ds = pos_; + while (pos_ < src_.size() && + (std::isxdigit(static_cast(src_[pos_])) + || src_[pos_] == '_' || src_[pos_] == 'x' || src_[pos_] == 'X' + || src_[pos_] == 'z' || src_[pos_] == 'Z')) ++pos_; + std::string digits; + for (size_t k = ds; k < pos_; ++k) + if (src_[k] != '_') digits += src_[k]; + int radix = 10; + if (base == 'b' || base == 'B') radix = 2; + else if (base == 'o' || base == 'O') radix = 8; + else if (base == 'h' || base == 'H') radix = 16; + uint64_t v = 0; + try { v = std::stoull(digits, nullptr, radix); } catch (...) {} + return {TokType::LIT, src_.substr(s, pos_ - s), v}; + } + } + + uint64_t v = 0; + try { v = std::stoull(src_.substr(s, pos_ - s)); } catch (...) {} + return {TokType::LIT, src_.substr(s, pos_ - s), v}; + } + + Tok lex_verilog_no_width() { + // Handle 'b0, 'h1A, etc. (no width prefix) + size_t s = pos_; + ++pos_; // skip ' + if (pos_ < src_.size()) { + char base = src_[pos_]; ++pos_; + size_t ds = pos_; + while (pos_ < src_.size() && + (std::isxdigit(static_cast(src_[pos_])) + || src_[pos_] == '_' || src_[pos_] == 'x' || src_[pos_] == 'X' + || src_[pos_] == 'z' || src_[pos_] == 'Z')) ++pos_; + std::string digits; + for (size_t k = ds; k < pos_; ++k) + if (src_[k] != '_') digits += src_[k]; + int radix = 10; + if (base == 'b' || base == 'B') radix = 2; + else if (base == 'o' || base == 'O') radix = 8; + else if (base == 'h' || base == 'H') radix = 16; + uint64_t v = 0; + try { v = std::stoull(digits, nullptr, radix); } catch (...) {} + return {TokType::LIT, src_.substr(s, pos_ - s), v}; + } + return {TokType::LIT, "'", 0}; + } + + Tok lex_id() { + size_t s = pos_; + while (pos_ < src_.size() && + (std::isalnum(static_cast(src_[pos_])) || src_[pos_] == '_')) + ++pos_; + std::string w = src_.substr(s, pos_ - s); + if (w == "true") return {TokType::LIT, w, 1}; + if (w == "false") return {TokType::LIT, w, 0}; + if (w == "default") return {TokType::LIT, w, 1}; + return {TokType::ID, w, 0}; + } +}; + +// ---- AST Node ---- + +struct ASTNode { + enum Kind { LIT, SIG, UNOT, BIN } kind; + uint64_t val; // for LIT + std::string name; // for SIG + std::string op; // for BIN + std::unique_ptr lhs, rhs; + ASTNode() : kind(LIT), val(0) {} +}; + +using AST = std::unique_ptr; + +static inline AST mk_lit(uint64_t v) { + auto n = std::make_unique(); n->kind = ASTNode::LIT; n->val = v; return n; +} +static inline AST mk_sig(const std::string& s) { + auto n = std::make_unique(); n->kind = ASTNode::SIG; n->name = s; return n; +} +static inline AST mk_not(AST c) { + auto n = std::make_unique(); n->kind = ASTNode::UNOT; n->lhs = std::move(c); return n; +} +static inline AST mk_bin(const std::string& o, AST l, AST r) { + auto n = std::make_unique(); n->kind = ASTNode::BIN; n->op = o; + n->lhs = std::move(l); n->rhs = std::move(r); return n; +} + +// ---- Recursive-descent parser ---- +// +// Grammar (by precedence, lowest first): +// or_expr = xor_expr ( ("||" | "|") xor_expr )* +// xor_expr = and_expr ( "^" and_expr )* +// and_expr = eq_expr ( ("&&" | "&") eq_expr )* +// eq_expr = cmp_expr ( ("==" | "!=") cmp_expr )* +// cmp_expr = shift_expr ( (">=" | "<=" | ">" | "<") shift_expr )* +// shift_expr = add_expr ( ("<<" | ">>") add_expr )* +// add_expr = mul_expr ( ("+" | "-") mul_expr )* +// mul_expr = unary ( ("*" | "%") unary )* +// unary = ("!" | "~") unary | primary +// primary = NUMBER | IDENTIFIER | "(" or_expr ")" + +class ExprParser { + std::vector ts_; + size_t pos_; + const Tok& peek() const { return ts_[pos_]; } + Tok adv() { return ts_[pos_++]; } + bool is(TokType t) const { return peek().ty == t; } + +public: + explicit ExprParser(const std::string& expr) : pos_(0) { + Lexer lx(expr); + Tok t; + do { t = lx.next(); ts_.push_back(t); } while (t.ty != TokType::END_TOK); + } + + AST parse() { return p_or(); } + +private: + AST p_or() { + auto n = p_xor(); + while (is(TokType::OR) || is(TokType::BOR)) { + adv(); n = mk_bin("||", std::move(n), p_xor()); + } + return n; + } + AST p_xor() { + auto n = p_and(); + while (is(TokType::BXOR)) { + adv(); n = mk_bin("^", std::move(n), p_and()); + } + return n; + } + AST p_and() { + auto n = p_eq(); + while (is(TokType::AND) || is(TokType::BAND)) { + adv(); n = mk_bin("&&", std::move(n), p_eq()); + } + return n; + } + AST p_eq() { + auto n = p_cmp(); + while (is(TokType::EQ) || is(TokType::NE)) { + auto o = adv().text; n = mk_bin(o, std::move(n), p_cmp()); + } + return n; + } + AST p_cmp() { + auto n = p_shift(); + while (is(TokType::GE) || is(TokType::LE) || is(TokType::GT) || is(TokType::LT)) { + auto o = adv().text; n = mk_bin(o, std::move(n), p_shift()); + } + return n; + } + AST p_shift() { + auto n = p_add(); + while (is(TokType::SHL) || is(TokType::SHR)) { + auto o = adv().text; n = mk_bin(o, std::move(n), p_add()); + } + return n; + } + AST p_add() { + auto n = p_mul(); + while (is(TokType::PLUS) || is(TokType::MINUS)) { + auto o = adv().text; n = mk_bin(o, std::move(n), p_mul()); + } + return n; + } + AST p_mul() { + auto n = p_unary(); + while (is(TokType::MUL) || is(TokType::MOD)) { + auto o = adv().text; n = mk_bin(o, std::move(n), p_unary()); + } + return n; + } + AST p_unary() { + if (is(TokType::NOT)) { adv(); return mk_not(p_unary()); } + return p_primary(); + } + AST p_primary() { + if (is(TokType::LIT)) { auto t = adv(); return mk_lit(t.num); } + if (is(TokType::ID)) { auto t = adv(); return mk_sig(t.text); } + if (is(TokType::LPAREN)) { + adv(); + auto n = p_or(); + if (is(TokType::RPAREN)) adv(); + return n; + } + // Fallback — consume and return 0 + if (!is(TokType::END_TOK)) adv(); + return mk_lit(0); + } +}; + +// ---- Constraint extraction for apply_input_condition ---- +// Builds per-signal constraints, detects contradictions, and picks satisfying assignments. +// This allows conjunction simplification (e.g. a > 11 && a > 5 -> a > 11) +// and impossibility detection (e.g. a > 10 && a < 9). + +static std::string negate_op(const std::string& op) { + if (op == "==") return "!="; + if (op == "!=") return "=="; + if (op == ">") return "<="; + if (op == ">=") return "<"; + if (op == "<") return ">="; + if (op == "<=") return ">"; + return op; +} + +struct SignalConstraint { + bool has_min = false; + bool has_max = false; + uint64_t min_value = 0; + uint64_t max_value = 0; + std::set excluded; + + bool contradictory() const { + if (has_min && has_max && min_value > max_value) return true; + if (has_min && has_max && min_value == max_value && excluded.count(min_value) > 0) return true; + return false; + } + + void add_min(uint64_t v) { + if (!has_min || v > min_value) { + min_value = v; + has_min = true; + } + } + + void add_max(uint64_t v) { + if (!has_max || v < max_value) { + max_value = v; + has_max = true; + } + } + + bool pick_value(uint64_t& out) const { + if (contradictory()) return false; + uint64_t lo = has_min ? min_value : 0; + uint64_t hi = has_max ? max_value : std::numeric_limits::max(); + for (uint64_t v = lo;; ++v) { + if (excluded.count(v) == 0) { + out = v; + return true; + } + if (v == hi) break; + } + return false; + } +}; + +// Evaluate an AST subtree only if it is constant (no signal references). +// Returns false when expression depends on runtime signals. +static bool eval_const_expr(const ASTNode* n, uint64_t& out) { + if (!n) return false; + switch (n->kind) { + case ASTNode::LIT: + out = n->val; + return true; + case ASTNode::SIG: + return false; + case ASTNode::UNOT: { + uint64_t v = 0; + if (!eval_const_expr(n->lhs.get(), v)) return false; + out = v ? 0 : 1; + return true; + } + case ASTNode::BIN: { + uint64_t lv = 0, rv = 0; + if (!eval_const_expr(n->lhs.get(), lv) || !eval_const_expr(n->rhs.get(), rv)) return false; + if (n->op == "&&") { out = (lv && rv) ? 1 : 0; return true; } + if (n->op == "||") { out = (lv || rv) ? 1 : 0; return true; } + if (n->op == "==") { out = (lv == rv) ? 1 : 0; return true; } + if (n->op == "!=") { out = (lv != rv) ? 1 : 0; return true; } + if (n->op == ">=") { out = (lv >= rv) ? 1 : 0; return true; } + if (n->op == "<=") { out = (lv <= rv) ? 1 : 0; return true; } + if (n->op == ">") { out = (lv > rv) ? 1 : 0; return true; } + if (n->op == "<") { out = (lv < rv) ? 1 : 0; return true; } + if (n->op == "+") { out = lv + rv; return true; } + if (n->op == "-") { out = lv - rv; return true; } + if (n->op == "*") { out = lv * rv; return true; } + if (n->op == "%") { if (rv == 0) return false; out = lv % rv; return true; } + if (n->op == "<<") { out = lv << (rv & 63); return true; } + if (n->op == ">>") { out = lv >> (rv & 63); return true; } + if (n->op == "^") { out = lv ^ rv; return true; } + return false; + } + } + return false; +} + +static bool normalize_comparison(const ASTNode* n, const std::string& effective_op, + std::string& signal_name, std::string& normalized_op, uint64_t& rhs_value) { + const ASTNode* sig_node = nullptr; + bool sig_on_left = false; + uint64_t const_val = 0; + if (n->lhs && n->lhs->kind == ASTNode::SIG && n->rhs && eval_const_expr(n->rhs.get(), const_val)) { + sig_node = n->lhs.get(); + sig_on_left = true; + } else if (n->rhs && n->rhs->kind == ASTNode::SIG && n->lhs && eval_const_expr(n->lhs.get(), const_val)) { + sig_node = n->rhs.get(); + sig_on_left = false; + } else { + return false; + } + normalized_op = effective_op; + if (!sig_on_left) { + if (normalized_op == ">") normalized_op = "<"; + else if (normalized_op == "<") normalized_op = ">"; + else if (normalized_op == ">=") normalized_op = "<="; + else if (normalized_op == "<=") normalized_op = ">="; + } + signal_name = sig_node->name; + rhs_value = const_val; + return true; +} + +static bool add_comparison_constraint(SignalConstraint& c, const std::string& op, uint64_t rhs) { + if (op == "==") { + c.add_min(rhs); + c.add_max(rhs); + } else if (op == "!=") { + c.excluded.insert(rhs); + } else if (op == ">") { + if (rhs == std::numeric_limits::max()) return false; + c.add_min(rhs + 1); + } else if (op == ">=") { + c.add_min(rhs); + } else if (op == "<") { + if (rhs == 0) return false; + c.add_max(rhs - 1); + } else if (op == "<=") { + c.add_max(rhs); + } + return !c.contradictory(); +} + +static bool collect_constraints(const ASTNode* n, std::map& out, + bool negate = false) { + if (!n) return true; + switch (n->kind) { + case ASTNode::LIT: + return negate ? (n->val == 0) : (n->val != 0); + case ASTNode::SIG: { + std::string op = negate ? "==" : "!="; + return add_comparison_constraint(out[n->name], op, 0); + } + case ASTNode::UNOT: + return collect_constraints(n->lhs.get(), out, !negate); + case ASTNode::BIN: + if (n->op == "&&") { + if (!negate) { + if (!collect_constraints(n->lhs.get(), out, false)) return false; + return collect_constraints(n->rhs.get(), out, false); + } + // !(a && b) == !a || !b : try either branch + auto left_try = out; + if (collect_constraints(n->lhs.get(), left_try, true)) { + out = std::move(left_try); + return true; + } + auto right_try = out; + if (collect_constraints(n->rhs.get(), right_try, true)) { + out = std::move(right_try); + return true; + } + return false; + } else if (n->op == "||") { + if (!negate) { + // a || b : try to satisfy left first, then right + auto left_try = out; + if (collect_constraints(n->lhs.get(), left_try, false)) { + out = std::move(left_try); + return true; + } + auto right_try = out; + if (collect_constraints(n->rhs.get(), right_try, false)) { + out = std::move(right_try); + return true; + } + return false; + } + // !(a || b) == !a && !b + if (!collect_constraints(n->lhs.get(), out, true)) return false; + return collect_constraints(n->rhs.get(), out, true); + } else if (n->op == "==" || n->op == "!=" || + n->op == ">" || n->op == ">=" || + n->op == "<" || n->op == "<=") { + std::string eff_op = negate ? negate_op(n->op) : n->op; + std::string sig, op; + uint64_t rhs = 0; + if (!normalize_comparison(n, eff_op, sig, op, rhs)) { + // Unsupported comparison form (e.g. arithmetic on both sides): + // keep edge as potentially satisfiable and rely on runtime check. + return true; + } + return add_comparison_constraint(out[sig], op, rhs); + } + // Unsupported operator for solving: keep satisfiable conservatively + return true; + } + return true; +} + +static bool extract_input_assignments(const ASTNode* n, std::map& assignments) { + std::map constraints; + if (!collect_constraints(n, constraints, false)) return false; + for (const auto& [sig, c] : constraints) { + uint64_t v = 0; + if (!c.pick_value(v)) return false; + assignments[sig] = v; + } + return true; +} + +// ============================================================================ +// Testbench Class +// ============================================================================ + +class StateCoverageTB { +public: + V{{ dut_module_name }}* dut; + V{{ golden_module_name }}* golden; + uint64_t sim_time; + int transition_timeout; + + StateCoverageTB(int timeout = {{ transition_timeout }}) : sim_time(0), transition_timeout(timeout) { + dut = new V{{ dut_module_name }}("DUT"); + golden = new V{{ golden_module_name }}("GOLDEN"); + } + ~StateCoverageTB() { delete dut; delete golden; } + + void eval() { dut->eval(); golden->eval(); sim_time++; } + void tick() { + dut->{{ clock_signal }} = 1; golden->{{ clock_signal }} = 1; eval(); + dut->{{ clock_signal }} = 0; golden->{{ clock_signal }} = 0; eval(); + } + + void do_reset() { +{%- if reset_active_high %} + dut->{{ reset_signal }} = 1; golden->{{ reset_signal }} = 1; +{%- else %} + dut->{{ reset_signal }} = 0; golden->{{ reset_signal }} = 0; +{%- endif %} + tick(); +{%- if reset_active_high %} + dut->{{ reset_signal }} = 0; golden->{{ reset_signal }} = 0; +{%- else %} + dut->{{ reset_signal }} = 1; golden->{{ reset_signal }} = 1; +{%- endif %} + eval(); + } + + void randomize_data_inputs() { +{%- for p in data_signals %} +{%- if p.width <= 64 %} + { uint64_t val = get_random_bits({{ p.width }}); dut->{{ p.name }} = val; golden->{{ p.name }} = val; } +{%- endif %} +{%- endfor %} + } + + void compare_outputs() { +{%- for p in outputs %} + {{ p.name }}_total_checks++; + if (dut->{{ p.name }} != golden->{{ p.name }}) { + {{ p.name }}_error_checks++; + simulation_passed = false; +{%- if debug %} + std::cerr << "MISMATCH {{ p.name }}: dut=0x" << std::hex << +dut->{{ p.name }} + << " golden=0x" << +golden->{{ p.name }} << std::dec << std::endl; +{%- endif %} + } +{%- endfor %} + } + + // ---- Signal Access ---- +{% for p in parameters %} + static uint64_t get_{{ p.name }}() { return {{ p.value }}ULL; } +{% endfor %} +{% for sig in runtime_signals %} + uint64_t get_{{ sig }}() { return dut->rootp->{{ module_name }}__DOT__{{ sig }}; } +{% endfor %} +{% for sm in state_machines %} + uint64_t get_state_raw_{{ loop.index0 }}() { return dut->rootp->{{ module_name }}__DOT__{{ sm.state_variable }}; } +{% endfor %} + + uint64_t resolve_signal(const std::string& name) { +{%- for p in control_signals %} + if (name == "{{ p.name }}") return static_cast(dut->{{ p.name }}); +{%- endfor %} +{%- for p in data_signals %} + if (name == "{{ p.name }}") return static_cast(dut->{{ p.name }}); +{%- endfor %} +{%- for sig in runtime_signals %} + if (name == "{{ sig }}") return get_{{ sig }}(); +{%- endfor %} +{%- for p in parameters %} + if (name == "{{ p.name }}") return get_{{ p.name }}(); +{%- endfor %} + return 0; + } + + // ---- Expression Evaluation (recursive AST walk) ---- + + uint64_t eval_ast(const ASTNode* n) { + if (!n) return 0; + switch (n->kind) { + case ASTNode::LIT: return n->val; + case ASTNode::SIG: return resolve_signal(n->name); + case ASTNode::UNOT: return eval_ast(n->lhs.get()) ? 0 : 1; + case ASTNode::BIN: { + // Short-circuit for logical operators + if (n->op == "&&") return (eval_ast(n->lhs.get()) && eval_ast(n->rhs.get())) ? 1 : 0; + if (n->op == "||") return (eval_ast(n->lhs.get()) || eval_ast(n->rhs.get())) ? 1 : 0; + uint64_t lv = eval_ast(n->lhs.get()), rv = eval_ast(n->rhs.get()); + if (n->op == "==") return lv == rv ? 1 : 0; + if (n->op == "!=") return lv != rv ? 1 : 0; + if (n->op == ">=") return lv >= rv ? 1 : 0; + if (n->op == "<=") return lv <= rv ? 1 : 0; + if (n->op == ">") return lv > rv ? 1 : 0; + if (n->op == "<") return lv < rv ? 1 : 0; + if (n->op == "+") return lv + rv; + if (n->op == "-") return lv - rv; + if (n->op == "*") return lv * rv; + if (n->op == "%") return rv != 0 ? lv % rv : 0; + if (n->op == "<<") return lv << (rv & 63); + if (n->op == ">>") return lv >> (rv & 63); + if (n->op == "^") return lv ^ rv; + return 0; + } + } + return 0; + } + + // ---- Condition Application & Evaluation ---- + + bool apply_input_condition(const std::string& cond) { + if (cond.empty() || cond == "true" || cond == "1" || cond == "1'b1" || cond == "default") return true; + randomize_data_inputs(); + // Parse the condition into an AST and extract signal assignments + ExprParser parser(cond); + auto ast = parser.parse(); + std::map assignments; + if (!extract_input_assignments(ast.get(), assignments)) return false; + // Apply extracted assignments to control signals only +{%- for p in control_signals %} + if (assignments.count("{{ p.name }}")) { + uint64_t v = assignments["{{ p.name }}"]; + dut->{{ p.name }} = v; golden->{{ p.name }} = v; + } +{%- endfor %} + return true; + } + + bool evaluate_internal_condition(const std::string& cond) { + if (cond.empty() || cond == "true" || cond == "1" || cond == "1'b1") return true; + if (cond == "false" || cond == "0" || cond == "1'b0") return false; + ExprParser parser(cond); + auto ast = parser.parse(); + return eval_ast(ast.get()) != 0; + } + + bool wait_for_condition(const std::string& wait_cond) { + if (wait_cond.empty() || wait_cond == "true" || wait_cond == "1" || wait_cond == "1'b1") { tick(); return true; } + // Parse once, evaluate each cycle for efficiency + ExprParser parser(wait_cond); + auto ast = parser.parse(); + // Include the boundary cycle so timeout=N allows checking after N+1 ticks. + // This avoids off-by-one misses for conditions like timer >= K-1. + for (int cycle = 0; cycle <= transition_timeout; cycle++) { + tick(); + if (eval_ast(ast.get()) != 0) return true; + } + timeout_transitions++; + return false; + } +}; + +// ============================================================================ +// DFS Coverage Engine +// ============================================================================ + +class DFSEngine { +public: + FSMGraph& graph; + StateCoverageTB& tb; + int sm_idx; + std::set covered; // Edges successfully traversed (persists across passes) + std::set pass_attempted; // Edges attempted this pass (reset each pass) + std::set failed; // Edges permanently failed after repeated timeouts + std::set impossible; // Edges with unsatisfiable input constraints + std::map fail_count; // Per-edge failure count across all passes + std::map edge_attempts; // Per-edge total attempt count across all passes + std::map edge_lines_executed; + std::map edge_failure_reason; + static constexpr int MAX_FAIL_ATTEMPTS = 2; // Mark as permanently failed after this many failures + + DFSEngine(FSMGraph& g, StateCoverageTB& t, int idx) : graph(g), tb(t), sm_idx(idx) {} + + std::string read_actual_state() { + uint64_t val = 0; +{%- for sm in state_machines %} + if (sm_idx == {{ loop.index0 }}) val = tb.get_state_raw_{{ loop.index0 }}(); +{%- endfor %} + return graph.value_to_name(val); + } + + bool try_edge(int eidx) { + const auto& e = graph.edges[eidx]; + total_transitions_tested++; + pass_attempted.insert(eidx); + edge_attempts[eidx]++; + bool input_ok = tb.apply_input_condition(e.input_condition); + if (!input_ok) { + impossible.insert(eidx); + failed.insert(eidx); + impossible_transitions++; + edge_failure_reason[eidx] = "impossible"; +{%- if debug %} + std::cout << " Edge " << eidx << ": " << e.from_state << " -> " << e.to_state + << " [" << e.input_condition << "] IMPOSSIBLE" << std::endl; +{%- endif %} + return false; + } +#ifdef LINE_COVERAGE_CHECK + CovSnapshot cov_before = take_coverage_snapshot(); +#endif + bool wait_ok = tb.wait_for_condition(e.wait_condition); +#ifdef LINE_COVERAGE_CHECK + CovSnapshot cov_after = take_coverage_snapshot(); + bool lines_executed = any_lines_executed(cov_before, cov_after); + edge_lines_executed[eidx] = lines_executed; +#else + bool lines_executed = true; +#endif + tb.compare_outputs(); + std::string actual = read_actual_state(); + bool ok = wait_ok && (actual == e.to_state) && lines_executed; +#ifdef LINE_COVERAGE_CHECK + if (lines_executed) line_check_passed_transitions++; + else line_check_failed_transitions++; +#endif + if (ok) { + covered.insert(eidx); + covered_transitions++; + failed.erase(eidx); // Succeeded — remove from failed if previously marked + impossible.erase(eidx); + edge_failure_reason.erase(eidx); + } else { + if (!wait_ok) edge_failure_reason[eidx] = "timeout"; + else if (actual != e.to_state) edge_failure_reason[eidx] = "state_mismatch"; + else if (!lines_executed) edge_failure_reason[eidx] = "no_lines_executed"; + else edge_failure_reason[eidx] = "failed"; + fail_count[eidx]++; + if (fail_count[eidx] >= MAX_FAIL_ATTEMPTS) { + failed.insert(eidx); + } + } +{%- if debug %} + std::cout << " Edge " << eidx << ": " << e.from_state << " -> " << e.to_state + << " [" << e.input_condition << "]" + << (e.wait_condition.empty() ? "" : " wait=" + e.wait_condition) + << (ok ? " OK" : (wait_ok ? (actual == e.to_state ? " NO_LINES_EXECUTED" : " STATE_MISMATCH") : " TIMEOUT")) + << " (actual: " << actual << ")" << std::endl; +{%- endif %} + return ok; + } + + std::vector find_bridge(const std::string& start) { + std::queue>> q; + std::set seen; + seen.insert(start); + q.push(std::make_pair(start, std::vector())); + while (!q.empty()) { + auto front = q.front(); q.pop(); + std::string state = front.first; std::vector path = front.second; + if (auto it = graph.adj.find(state); it != graph.adj.end()) { + if (!path.empty()) { for (int eidx : it->second) { if (!covered.count(eidx) && !pass_attempted.count(eidx) && !failed.count(eidx)) return path; } } + for (int eidx : it->second) { + if (covered.count(eidx)) { + const auto& e = graph.edges[eidx]; + if (!seen.count(e.to_state)) { seen.insert(e.to_state); auto np = path; np.push_back(eidx); q.push(std::make_pair(e.to_state, np)); } + } + } + } + } + return {}; + } + + std::vector find_path_to(const std::string& start, const std::string& target) { + if (start == target) return {}; + std::queue>> q; + std::set seen; + seen.insert(start); + q.push(std::make_pair(start, std::vector())); + while (!q.empty()) { + auto front = q.front(); q.pop(); + std::string state = front.first; std::vector path = front.second; + if (auto it = graph.adj.find(state); it != graph.adj.end()) { + for (int eidx : it->second) { + const auto& e = graph.edges[eidx]; + if (e.to_state == target) { auto r = path; r.push_back(eidx); return r; } + if (!seen.count(e.to_state)) { seen.insert(e.to_state); auto np = path; np.push_back(eidx); q.push(std::make_pair(e.to_state, np)); } + } + } + } + return {}; + } + + bool follow_path(const std::vector& path, std::string& current) { + for (int eidx : path) { if (!try_edge(eidx)) return false; current = graph.edges[eidx].to_state; } + return true; + } + + void run_pass() { + tb.do_reset(); + tb.randomize_data_inputs(); + pass_attempted.clear(); + std::string current = graph.initial_state; + int max_iter = static_cast(graph.edges.size()) * 3 + 1; + for (int iter = 0; iter < max_iter; iter++) { + int next_edge = -1; + if (auto it = graph.adj.find(current); it != graph.adj.end()) { + for (int eidx : it->second) { if (!covered.count(eidx) && !pass_attempted.count(eidx) && !failed.count(eidx)) { next_edge = eidx; break; } } + } + if (next_edge >= 0) { + if (try_edge(next_edge)) { current = graph.edges[next_edge].to_state; } + else { tb.do_reset(); tb.randomize_data_inputs(); current = graph.initial_state; } + } else { + auto bridge = find_bridge(current); + if (!bridge.empty()) { + if (follow_path(bridge, current)) continue; + else { tb.do_reset(); tb.randomize_data_inputs(); current = graph.initial_state; } + } else { + bool found = false; + for (size_t i = 0; i < graph.edges.size(); i++) { + int ii = static_cast(i); + if (covered.count(ii) || pass_attempted.count(ii) || failed.count(ii)) continue; + auto path = find_path_to(graph.initial_state, graph.edges[i].from_state); + tb.do_reset(); tb.randomize_data_inputs(); current = graph.initial_state; + if (follow_path(path, current)) { + if (try_edge(ii)) current = graph.edges[i].to_state; + else { tb.do_reset(); tb.randomize_data_inputs(); current = graph.initial_state; } + found = true; break; + } + } + if (!found) break; + } + } + } + } + + void run(int passes) { + for (int p = 0; p < passes; p++) { + if (covered.size() + failed.size() >= graph.edges.size()) break; + dfs_passes_completed++; +{%- if debug %} + std::cout << "=== DFS Pass " << (p+1) << "/" << passes << " (SM: " << graph.state_variable + << ", covered: " << covered.size() << "/" << graph.edges.size() << ") ===" << std::endl; +{%- endif %} + run_pass(); + } + } + + int get_covered() const { return static_cast(covered.size()); } + int get_total() const { return static_cast(graph.edges.size()); } + int get_failed_count() const { return static_cast(failed.size()); } + int get_impossible_count() const { return static_cast(impossible.size()); } + bool is_covered(int idx) const { return covered.count(idx) > 0; } + bool is_failed(int idx) const { return failed.count(idx) > 0; } + bool is_impossible(int idx) const { return impossible.count(idx) > 0; } + int get_edge_attempts(int idx) const { auto it = edge_attempts.find(idx); return it != edge_attempts.end() ? it->second : 0; } + bool get_edge_lines_executed(int idx) const { + auto it = edge_lines_executed.find(idx); + return it != edge_lines_executed.end() ? it->second : false; + } + std::string get_failure_reason(int idx) const { + auto it = edge_failure_reason.find(idx); + return it != edge_failure_reason.end() ? it->second : ""; + } + std::string get_edge_status(int idx) const { + if (is_covered(idx)) return "covered"; + if (is_impossible(idx)) return "impossible"; + if (is_failed(idx)) { + auto reason = get_failure_reason(idx); + return reason.empty() ? "failed" : reason; + } + return "unreachable"; + } +}; + +// ============================================================================ +// Graph Initialization +// ============================================================================ + +void init_graphs(std::vector& graphs) { +{% for sm in state_machines %} + { + FSMGraph g; + g.state_variable = "{{ sm.state_variable }}"; + g.initial_state = "{{ sm.initial_state }}"; +{%- for s in sm.states %} + g.state_names.push_back("{{ s.name }}"); + g.state_values.push_back({{ s.value_numeric }}ULL); +{%- endfor %} +{%- for t in sm.transitions %} + { Edge e; e.id = {{ loop.index0 }}; e.from_state = "{{ t.from_state }}"; e.to_state = "{{ t.to_state }}"; e.input_condition = "{{ t.input_condition }}"; e.wait_condition = "{{ t.wait_condition }}"; e.priority = {{ t.priority }}; g.edges.push_back(e); } +{%- endfor %} + g.build(); + graphs.push_back(std::move(g)); + } +{% endfor %} +} + +// ============================================================================ +// Main +// ============================================================================ + +int main(int argc, char** argv) { + Verilated::commandArgs(argc, argv); + StateCoverageTB tb({{ transition_timeout }}); + + uint64_t seed = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + std::string stats_file = "test_stats.json"; + for (int i = 1; i < argc; i++) { + std::string arg(argv[i]); + if (arg.find("+SEED=") == 0) seed = std::stoull(arg.substr(6)); + if (arg.find("+STATS_FILE=") == 0) stats_file = arg.substr(12); + } + rng.seed(seed); + + std::vector graphs; + init_graphs(graphs); + int dfs_passes = {{ dfs_passes }}; + int total_edges = 0; + for (auto& g : graphs) total_edges += static_cast(g.edges.size()); + + std::cout << "=====================================" << std::endl; + std::cout << "State Coverage Testbench (C++ DFS)" << std::endl; + std::cout << "Module: {{ module_name }}" << std::endl; + std::cout << "State machines: " << graphs.size() << std::endl; + for (size_t i = 0; i < graphs.size(); i++) { + std::cout << " " << graphs[i].state_variable << ": " << graphs[i].state_names.size() + << " states, " << graphs[i].edges.size() << " edges" << std::endl; + } + std::cout << "DFS passes: " << dfs_passes << std::endl; + std::cout << "Transition timeout: " << tb.transition_timeout << " cycles" << std::endl; +#ifdef LINE_COVERAGE_CHECK + std::cout << "Line execution checks: enabled" << std::endl; +#else + std::cout << "Line execution checks: disabled" << std::endl; +#endif + std::cout << "Random seed: " << seed << std::endl; + std::cout << "=====================================" << std::endl; + + std::vector engines; + for (size_t i = 0; i < graphs.size(); i++) { + auto* engine = new DFSEngine(graphs[i], tb, static_cast(i)); +{%- if debug %} + std::cout << std::endl << "--- DFS for SM: " << graphs[i].state_variable + << " (" << graphs[i].edges.size() << " edges) ---" << std::endl; +{%- endif %} + engine->run(dfs_passes); + engines.push_back(engine); + } + + // Additional random testing + std::cout << std::endl << "--- Additional Random Testing ({{ random_samples }} cycles) ---" << std::endl; + tb.do_reset(); + for (int64_t i = 0; i < {{ random_samples }}; i++) { + tb.randomize_data_inputs(); +{%- for p in control_signals %} + { uint64_t val = get_random_bits({{ p.width }}); tb.dut->{{ p.name }} = val; tb.golden->{{ p.name }} = val; } +{%- endfor %} + tb.tick(); + tb.compare_outputs(); + } + + // ---- Summary ---- + int total_covered = 0; + int total_failed = 0; + for (auto* eng : engines) { total_covered += eng->get_covered(); total_failed += eng->get_failed_count(); } + + std::cout << std::endl << "=====================================" << std::endl; + std::cout << "Test Summary" << std::endl; + std::cout << "=====================================" << std::endl; + std::cout << "DFS passes completed: " << dfs_passes_completed << std::endl; + std::cout << "Edge coverage: " << total_covered << "/" << total_edges + << " (" << std::fixed << std::setprecision(1) + << (total_edges > 0 ? 100.0 * total_covered / total_edges : 0.0) << "%)" << std::endl; + std::cout << "Total transitions tested: " << total_transitions_tested << std::endl; + std::cout << "Successful transitions: " << covered_transitions << std::endl; + std::cout << "Timed out transitions: " << timeout_transitions << std::endl; + std::cout << "Impossible transitions: " << impossible_transitions << std::endl; +#ifdef LINE_COVERAGE_CHECK + std::cout << "Line checks passed: " << line_check_passed_transitions << std::endl; + std::cout << "Line checks failed: " << line_check_failed_transitions << std::endl; +#endif + if (total_failed > 0) + std::cout << "Permanently failed edges: " << total_failed << std::endl; + for (auto* eng : engines) { + std::cout << " SM '" << eng->graph.state_variable << "': " + << eng->get_covered() << "/" << eng->get_total() << " edges covered"; + if (eng->get_failed_count() > 0) + std::cout << ", " << eng->get_failed_count() << " failed"; + std::cout << std::endl; + for (size_t j = 0; j < eng->graph.edges.size(); j++) { + int jj = static_cast(j); + if (eng->is_impossible(jj)) { + const auto& e = eng->graph.edges[j]; + std::cout << " IMPOSSIBLE: " << e.from_state << " -> " << e.to_state + << " [" << e.input_condition << "]" << std::endl; + } else if (eng->is_failed(jj)) { + const auto& e = eng->graph.edges[j]; + std::cout << " FAILED: " << e.from_state << " -> " << e.to_state + << " [" << e.input_condition << "]" + << (e.wait_condition.empty() ? "" : " wait=" + e.wait_condition) + << " (" << eng->get_edge_attempts(jj) << " attempts)" << std::endl; + } else if (!eng->is_covered(jj)) { + const auto& e = eng->graph.edges[j]; + std::cout << " UNREACHABLE: " << e.from_state << " -> " << e.to_state + << " [" << e.input_condition << "]" << std::endl; + } + } + } +{%- for p in outputs %} + std::cout << "{{ p.name }}: " << {{ p.name }}_total_checks << " checks, " << {{ p.name }}_error_checks << " errors" << std::endl; +{%- endfor %} + + if (simulation_passed && total_covered == total_edges) + std::cout << std::endl << "*** SIMULATION PASSED (100% edge coverage, outputs correct) ***" << std::endl; + else if (!simulation_passed && total_covered == total_edges) + std::cout << std::endl << "*** SIMULATION FAILED (100% edge coverage, output mismatches) ***" << std::endl; + else if (!simulation_passed) + std::cout << std::endl << "*** SIMULATION FAILED (" << total_covered << "/" << total_edges << " edges covered, output mismatches) ***" << std::endl; + else if (total_failed > 0) + std::cout << std::endl << "*** SIMULATION COMPLETED (" << total_covered << "/" << total_edges << " edges covered, " << total_failed << " edges failed) ***" << std::endl; + else + std::cout << std::endl << "*** SIMULATION COMPLETED (" << total_covered << "/" << total_edges << " edges covered) ***" << std::endl; + std::cout << "=====================================" << std::endl; + + // ---- JSON Statistics ---- + { + std::ofstream json(stats_file); + json << "{\n"; + json << " \"summary\": {\n"; + json << " \"dfs_passes\": " << dfs_passes_completed << ",\n"; + json << " \"total_edges\": " << total_edges << ",\n"; + json << " \"covered_edges\": " << total_covered << ",\n"; + json << " \"failed_edges\": " << total_failed << ",\n"; + json << " \"edge_coverage_pct\": " << std::fixed << std::setprecision(2) << (total_edges > 0 ? 100.0 * total_covered / total_edges : 0.0) << ",\n"; + json << " \"total_transitions_tested\": " << total_transitions_tested << ",\n"; + json << " \"timeout_transitions\": " << timeout_transitions << ",\n"; + json << " \"impossible_transitions\": " << impossible_transitions << ",\n"; +#ifdef LINE_COVERAGE_CHECK + json << " \"line_coverage_check_enabled\": true,\n"; + json << " \"line_check_passed_transitions\": " << line_check_passed_transitions << ",\n"; + json << " \"line_check_failed_transitions\": " << line_check_failed_transitions << ",\n"; +#else + json << " \"line_coverage_check_enabled\": false,\n"; +#endif + json << " \"random_seed\": " << seed << ",\n"; + json << " \"simulation_passed\": " << (simulation_passed ? "true" : "false") << "\n"; + json << " },\n"; + json << " \"state_machines\": [\n"; + for (size_t i = 0; i < engines.size(); i++) { + auto* eng = engines[i]; + json << " {\n"; + json << " \"name\": \"" << eng->graph.state_variable << "\",\n"; + json << " \"total_edges\": " << eng->get_total() << ",\n"; + json << " \"covered_edges\": " << eng->get_covered() << ",\n"; + json << " \"edges\": [\n"; + for (size_t j = 0; j < eng->graph.edges.size(); j++) { + const auto& e = eng->graph.edges[j]; + int jj = static_cast(j); + json << " {\"id\": " << e.id << ", \"from\": \"" << e.from_state << "\", \"to\": \"" << e.to_state + << "\", \"input_condition\": \"" << e.input_condition << "\", \"wait_condition\": \"" << e.wait_condition + << "\", \"status\": \"" << eng->get_edge_status(jj) + << "\", \"attempts\": " << eng->get_edge_attempts(jj) + << ", \"lines_executed\": " << (eng->get_edge_lines_executed(jj) ? "true" : "false") << "}"; + if (j + 1 < eng->graph.edges.size()) json << ","; + json << "\n"; + } + json << " ]\n }"; + if (i + 1 < engines.size()) json << ","; + json << "\n"; + } + json << " ],\n"; + json << " \"output_comparison\": {\n"; +{%- for p in outputs %} + json << " \"{{ p.name }}\": {" << "\"tests\": " << {{ p.name }}_total_checks + << ", \"success\": " << ({{ p.name }}_total_checks - {{ p.name }}_error_checks) + << ", \"score\": " << std::fixed << std::setprecision(2) + << ({{ p.name }}_total_checks > 0 ? 100.0 * ({{ p.name }}_total_checks - {{ p.name }}_error_checks) / {{ p.name }}_total_checks : 0.0) + << "}{%- if not loop.last %},{%- endif %}\n"; +{%- endfor %} + json << " }\n}\n"; + json.close(); + std::cout << "Statistics written to " << stats_file << std::endl; + } + + for (auto* eng : engines) delete eng; + +#ifdef COVERAGE + Verilated::threadContextp()->coveragep()->write("coverage.dat"); + std::cout << "Coverage data written to coverage.dat" << std::endl; +#endif + + return simulation_passed ? 0 : 1; +} diff --git a/src/tools/compiler.rs b/src/tools/compiler.rs index 7895f86..c74ef9c 100644 --- a/src/tools/compiler.rs +++ b/src/tools/compiler.rs @@ -14,9 +14,15 @@ pub struct CompileArgs { pub verilator_ignore_warnings: bool, pub verilator_mpi: bool, pub verilator_jobs: i32, + pub line_coverage_check: bool, pub compile_flags: Option>, } +fn verilator_cmd() -> Command { + let path = std::env::var("STG_VERILATOR_PATH").unwrap_or_else(|_| "verilator".to_string()); + Command::new(path) +} + fn print_command(cmd: &Command) { let program = cmd.get_program().to_string_lossy(); let args: Vec = cmd @@ -119,7 +125,7 @@ pub fn compile_testbench_verilator( fs::remove_file(out_exe_path).context("Failed to remove existing executable")?; } - let version_check = Command::new("verilator") + let version_check = verilator_cmd() .arg("--version") .output() .context("Failed to run verilator. Is it installed?")?; @@ -234,13 +240,16 @@ pub fn compile_testbench_verilator( .canonicalize() .context("Failed to canonicalize tmpdir")?; - let mut cmd = Command::new("verilator"); + let mut cmd = verilator_cmd(); for file in &files { cmd.arg(file); } for flag in &compile_flags { cmd.arg(flag); } + // Suppress MULTITOP warning since we intentionally pass both DUT and golden + // as separate top-level modules + cmd.arg("-Wno-MULTITOP"); cmd.arg("--main") .arg("--timing") .arg("--Mdir") @@ -317,7 +326,7 @@ pub fn compile_testbench_cpp_verilator( is_systemc: bool, ) -> Result { // Check if verilator is installed - let version_check = Command::new("verilator") + let version_check = verilator_cmd() .arg("--version") .output() .context("Failed to run verilator. Is it installed?")?; @@ -424,7 +433,7 @@ pub fn compile_testbench_cpp_verilator( for (idx, (dut_path, dut_module_name)) in dut_paths.iter().zip(dut_module_names.iter()).enumerate() { - let mut cmd = Command::new("verilator"); + let mut cmd = verilator_cmd(); cmd.arg(dut_path); cmd.arg("--top-module").arg(dut_module_name); @@ -466,7 +475,7 @@ pub fn compile_testbench_cpp_verilator( log::info!("Compiling testbench with generated DUT classes..."); // Find Verilator include directory - let verilator_root_output = Command::new("verilator") + let verilator_root_output = verilator_cmd() .arg("--getenv") .arg("VERILATOR_ROOT") .output() @@ -570,3 +579,193 @@ pub fn compile_testbench_cpp_verilator( Ok(out_exe_path.exists()) } + +/// Compile a state-coverage C++ testbench where BOTH DUT and golden are Verilog files +/// that need to be verilated into C++ classes. +/// +/// This differs from `compile_testbench_cpp_verilator` in that the golden model +/// is also a Verilog file (not a C++ header), so we verilate both DUT and golden +/// separately with different prefixes. +pub fn compile_state_coverage_cpp_verilator( + args: &CompileArgs, + dut_paths: &[PathBuf], + dut_module_name: &str, + golden_path: &Path, + golden_module_name: &str, + cpp_testbench_path: &Path, + out_exe_path: &Path, +) -> Result { + // Check prerequisites + let version_check = verilator_cmd() + .arg("--version") + .output() + .context("Failed to run verilator. Is it installed?")?; + if !version_check.status.success() { + anyhow::bail!("Verilator is not installed or not found in PATH"); + } + + let version_check = Command::new("g++") + .arg("--version") + .output() + .context("Failed to run g++. Is it installed?")?; + if !version_check.status.success() { + anyhow::bail!("G++ is not installed or not found in PATH"); + } + + if out_exe_path.exists() { + log::info!("Removing existing executable: {}", out_exe_path.display()); + fs::remove_file(out_exe_path).context("Failed to remove existing executable")?; + } + + let jobs_str = args.verilator_jobs.to_string(); + let mut compile_flags = vec!["-Wno-TIMESCALEMOD", "-j", &jobs_str]; + + let mut insert_dump_coverage = false; + if let Some(args_compile_flags) = &args.compile_flags { + for flag in args_compile_flags { + compile_flags.push(flag.as_str()); + if flag.contains("--coverage") { + insert_dump_coverage = true; + } + } + } + if args.line_coverage_check { + if !compile_flags.iter().any(|f| f.contains("--coverage")) { + compile_flags.push("--coverage"); + } + insert_dump_coverage = true; + } + + if args.verilator_ignore_warnings { + compile_flags.extend_from_slice(&[ + "-Wno-WIDTHTRUNC", + "-Wno-WIDTHCONCAT", + "-Wno-WIDTHEXPAND", + ]); + } + + let mut c_flags = String::new(); + let ld_flags = String::new(); + + if insert_dump_coverage { + c_flags.push_str("-DCOVERAGE "); + } + if args.line_coverage_check { + c_flags.push_str("-DLINE_COVERAGE_CHECK "); + } + + // Create temporary directory for Verilator build + let tmpdir = TempDir::new().context("Failed to create temporary directory")?; + + // Step 1: Verilate DUT + log::info!("Verilating DUT ({})...", dut_module_name); + { + let mut cmd = verilator_cmd(); + for dut_path in dut_paths { + cmd.arg(dut_path); + } + cmd.arg("--top-module").arg(dut_module_name); + for flag in &compile_flags { + cmd.arg(flag); + } + cmd.arg("--cc"); + cmd.arg("--Mdir").arg(tmpdir.path()); + run_command(&mut cmd, "verilator (DUT)") + .with_context(|| format!("Failed to verilate DUT ({})", dut_module_name))?; + } + + // Step 2: Verilate golden with a different prefix + log::info!("Verilating golden ({})...", golden_module_name); + let _golden_prefix = format!("V{}", golden_module_name); + { + let mut cmd = verilator_cmd(); + cmd.arg(golden_path); + cmd.arg("--top-module").arg(golden_module_name); + // Use a prefix to avoid name collisions if DUT and golden have the same module name + // Note: if they already have different names, Verilator defaults work fine + for flag in &compile_flags { + cmd.arg(flag); + } + cmd.arg("--cc"); + cmd.arg("--Mdir").arg(tmpdir.path()); + run_command(&mut cmd, "verilator (golden)") + .with_context(|| format!("Failed to verilate golden ({})", golden_module_name))?; + } + + // Step 3: Compile the C++ testbench with all generated classes + log::info!("Compiling C++ testbench..."); + + // Find Verilator include directory + let verilator_root_output = verilator_cmd() + .arg("--getenv") + .arg("VERILATOR_ROOT") + .output() + .context("Failed to get VERILATOR_ROOT")?; + let verilator_root = String::from_utf8_lossy(&verilator_root_output.stdout) + .trim() + .to_string(); + let verilator_include = format!("{}/include", verilator_root); + + // Add include path for generated headers + if !c_flags.is_empty() { + c_flags.push(' '); + } + c_flags.push_str(&format!("-I{}", tmpdir.path().display())); + + let mut cmd = Command::new("g++"); + cmd.arg("-std=c++20"); + cmd.arg("-I").arg(tmpdir.path()); + cmd.arg("-I").arg(&verilator_include); + + // Add testbench source + cmd.arg(cpp_testbench_path); + + // Add Verilator support files + let verilated_cpp = format!("{}/verilated.cpp", verilator_include); + let verilated_timing_cpp = format!("{}/verilated_timing.cpp", verilator_include); + let verilated_threads_cpp = format!("{}/verilated_threads.cpp", verilator_include); + let verilated_cov_cpp = format!("{}/verilated_cov.cpp", verilator_include); + + cmd.arg(&verilated_cpp); + if std::path::Path::new(&verilated_timing_cpp).exists() { + cmd.arg(&verilated_timing_cpp); + } + if std::path::Path::new(&verilated_threads_cpp).exists() { + cmd.arg(&verilated_threads_cpp); + } + if insert_dump_coverage && std::path::Path::new(&verilated_cov_cpp).exists() { + cmd.arg(&verilated_cov_cpp); + } + + // Add all generated .cpp files from Verilator + let cpp_pattern = tmpdir.path().join("V*.cpp"); + for cpp_file in glob::glob(&cpp_pattern.to_string_lossy()) + .context("Failed to glob V*.cpp files")? + .filter_map(Result::ok) + { + cmd.arg(&cpp_file); + } + + cmd.arg("-o").arg(out_exe_path); + + if !c_flags.is_empty() { + for flag in c_flags.split_whitespace() { + cmd.arg(flag); + } + } + + if !ld_flags.is_empty() { + for flag in ld_flags.split_whitespace() { + cmd.arg(flag); + } + } + + run_command(&mut cmd, "g++ (state coverage testbench)") + .with_context(|| "Failed to compile state coverage testbench")?; + + if insert_dump_coverage { + log::info!("Coverage enabled. Data will be written to coverage.dat on exit."); + } + + Ok(out_exe_path.exists()) +} diff --git a/src/tools/file_utils.rs b/src/tools/file_utils.rs index f87d195..1451214 100644 --- a/src/tools/file_utils.rs +++ b/src/tools/file_utils.rs @@ -57,7 +57,7 @@ pub fn concatenate_verilog_files(verilog_paths: &[PathBuf]) -> Result { .with_context(|| format!("Failed to read Verilog file: {}", verilog_path.display()))?; concatenated_content.push_str(&content); - concatenated_content.push_str("\n"); + concatenated_content.push('\n'); } // Write concatenated content to temporary file @@ -83,7 +83,7 @@ mod tests { let temp1 = NamedTempFile::new().unwrap(); let path1 = temp1.path().to_path_buf(); - let result = concatenate_verilog_files(&[path1.clone()]).unwrap(); + let result = concatenate_verilog_files(std::slice::from_ref(&path1)).unwrap(); assert_eq!(result, path1); } diff --git a/src/tools/fsm/condition_splitter.rs b/src/tools/fsm/condition_splitter.rs new file mode 100644 index 0000000..43d3d3b --- /dev/null +++ b/src/tools/fsm/condition_splitter.rs @@ -0,0 +1,133 @@ +//! Condition splitting logic for separating input and wait conditions in FSM transitions. + +use super::types::StateMachine; +use std::collections::HashSet; + +/// Split transition conditions into input_condition (controllable) and wait_condition (internal). +/// A condition is "controllable" if it only references control_signals (module inputs). +/// A condition is "internal" if it references internal_signals (state variables, counters, etc.). +pub fn split_transition_conditions( + sm: &mut StateMachine, + control_signal_names: &HashSet, +) { + for trans in &mut sm.transitions { + let cond = if !trans.input_condition.is_empty() { + &trans.input_condition + } else { + &trans.condition + }; + + if cond.is_empty() || cond == "true" { + trans.input_condition = String::new(); + trans.wait_condition = String::new(); + continue; + } + + // Parse the condition to separate input-based parts from internal-based parts + let (input_part, wait_part) = + separate_condition(cond, control_signal_names, &sm.internal_signals); + + trans.input_condition = input_part; + trans.wait_condition = wait_part; + } +} + +/// Separate a condition into input-based and internal-based parts. +/// Returns (input_condition, wait_condition). +fn separate_condition( + cond: &str, + control_signals: &HashSet, + internal_signals: &[String], +) -> (String, String) { + // Simple heuristic: check if condition references any internal signal + let has_internal = internal_signals.iter().any(|sig| cond.contains(sig)); + + if !has_internal { + // Only references control signals (or is a simple boolean) + return (cond.to_string(), String::new()); + } + + // Check if condition also references control signals + let has_control = control_signals.iter().any(|sig| cond.contains(sig)); + + if !has_control { + // Only references internal signals - it's a pure wait condition + return (String::new(), cond.to_string()); + } + + // Condition references both - we need to split it + // For now, use a simple split on && or || + // Extract clauses and classify each + let mut input_clauses = Vec::new(); + let mut wait_clauses = Vec::new(); + + for clause in split_condition_clauses(cond) { + let has_int = internal_signals.iter().any(|sig| clause.contains(sig)); + if has_int { + wait_clauses.push(clause); + } else { + input_clauses.push(clause); + } + } + + let input_part = if input_clauses.is_empty() { + String::new() + } else { + input_clauses.join(" && ") + }; + + let wait_part = if wait_clauses.is_empty() { + String::new() + } else { + wait_clauses.join(" && ") + }; + + (input_part, wait_part) +} + +/// Split a condition expression into clauses on && and ||. +/// This is a simple splitter that doesn't handle nested parens perfectly. +fn split_condition_clauses(cond: &str) -> Vec { + let mut clauses = Vec::new(); + let mut current = String::new(); + let mut paren_depth = 0; + + let chars: Vec = cond.chars().collect(); + let mut i = 0; + + while i < chars.len() { + let ch = chars[i]; + + if ch == '(' { + paren_depth += 1; + current.push(ch); + } else if ch == ')' { + paren_depth -= 1; + current.push(ch); + } else if paren_depth == 0 && ch == '&' && i + 1 < chars.len() && chars[i + 1] == '&' { + // Found && at top level + clauses.push(current.trim().to_string()); + current.clear(); + i += 1; // Skip second & + } else if paren_depth == 0 && ch == '|' && i + 1 < chars.len() && chars[i + 1] == '|' { + // Found || at top level - treat as separator for now + clauses.push(current.trim().to_string()); + current.clear(); + i += 1; // Skip second | + } else { + current.push(ch); + } + + i += 1; + } + + if !current.trim().is_empty() { + clauses.push(current.trim().to_string()); + } + + if clauses.is_empty() { + clauses.push(cond.to_string()); + } + + clauses +} diff --git a/src/tools/fsm/extractor/constant_parser.rs b/src/tools/fsm/extractor/constant_parser.rs new file mode 100644 index 0000000..bd3f25b --- /dev/null +++ b/src/tools/fsm/extractor/constant_parser.rs @@ -0,0 +1,102 @@ +//! Constant extraction and parsing from Verilog parameters. + +use regex::Regex; +use std::collections::HashMap; + +/// Extract all `localparam` and `parameter` constant values from the Verilog source. +/// +/// Returns a map from constant name to its resolved numeric value. +/// Handles common declarations like: +/// localparam int GREEN_TICKS = 10; +/// localparam GREEN_TICKS = 10; +/// localparam logic [3:0] YELLOW_TICKS = 3; +/// parameter N = 8; +pub fn extract_constants(source: &str) -> HashMap { + let mut constants: HashMap = HashMap::new(); + + // Match individual constant declarations: + // localparam [type] NAME = VALUE; + // parameter [type] NAME = VALUE; + // + // Also handles grouped declarations like: + // localparam int GREEN_TICKS = 10, YELLOW_TICKS = 3; + let decl_re = Regex::new( + r"(?:localparam|parameter)\s+(?:(?:int|integer|logic|reg|wire)\s*(?:\[\s*\d+\s*:\s*\d+\s*\])?\s*)?([^;]+);", + ) + .unwrap(); + + let assign_re = Regex::new(r"(\w+)\s*=\s*([^,;]+)").unwrap(); + + for cap in decl_re.captures_iter(source) { + let body = &cap[1]; + for acap in assign_re.captures_iter(body) { + let name = acap[1].trim().to_string(); + let value_str = acap[2].trim(); + if let Some(val) = parse_verilog_constant(value_str, &constants) { + constants.insert(name, val); + } + } + } + + constants +} + +/// Parse a Verilog constant expression to a u64. +/// +/// Handles: +/// - Plain decimal: "10", "42" +/// - Verilog literals: "3'd5", "8'hFF", "4'b1010", "32'd100" +/// - Simple arithmetic: "GREEN_TICKS - 1", "N + 2", "N * 2" +/// - References to previously resolved constants +pub fn parse_verilog_constant(expr: &str, known: &HashMap) -> Option { + let trimmed = expr.trim(); + + // Try plain decimal + if let Ok(v) = trimmed.parse::() { + return Some(v); + } + + // Try Verilog literal: ' + let lit_re = Regex::new(r"^\d*'([bBdDhHoO])([0-9a-fA-F_]+)$").ok()?; + if let Some(cap) = lit_re.captures(trimmed) { + let base = match &cap[1].to_lowercase()[..] { + "b" => 2, + "o" => 8, + "d" => 10, + "h" => 16, + _ => return None, + }; + let digits = cap[2].replace('_', ""); + return u64::from_str_radix(&digits, base).ok(); + } + + // Try known constant reference + if let Some(val) = known.get(trimmed) { + return Some(*val); + } + + // Try simple binary arithmetic: + for op in &[" + ", " - ", " * ", " / "] { + if let Some(pos) = trimmed.find(op) { + let lhs_str = trimmed[..pos].trim(); + let rhs_str = trimmed[pos + op.len()..].trim(); + let lhs = parse_verilog_constant(lhs_str, known)?; + let rhs = parse_verilog_constant(rhs_str, known)?; + return match *op { + " + " => Some(lhs.wrapping_add(rhs)), + " - " => Some(lhs.wrapping_sub(rhs)), + " * " => Some(lhs.wrapping_mul(rhs)), + " / " => { + if rhs == 0 { + None + } else { + Some(lhs / rhs) + } + } + _ => None, + }; + } + } + + None +} diff --git a/src/tools/fsm/extractor/enum_parser.rs b/src/tools/fsm/extractor/enum_parser.rs new file mode 100644 index 0000000..1cfebc0 --- /dev/null +++ b/src/tools/fsm/extractor/enum_parser.rs @@ -0,0 +1,76 @@ +//! Enum type parsing for FSM state types. + +use regex::Regex; + +/// Represents an enum type definition. +#[derive(Debug)] +pub struct EnumDef { + pub name: String, + pub values: Vec<(String, String)>, // (name, value) +} + +/// Extract `typedef enum` definitions from the source. +pub fn extract_enum_types(source: &str) -> Vec { + let mut enums = Vec::new(); + + // Match: typedef enum logic [N:0] { ... } name_t; + let re = Regex::new( + r"(?s)typedef\s+enum\s+(?:logic\s*(?:\[\s*\d+\s*:\s*\d+\s*\])?\s*)?\{([^}]+)\}\s*(\w+)\s*;", + ) + .unwrap(); + + let val_re = Regex::new(r"(\w+)\s*=\s*([^,\s]+)").unwrap(); + let name_re = Regex::new(r"(\w+)").unwrap(); + + for cap in re.captures_iter(source) { + let body = &cap[1]; + let type_name = cap[2].to_string(); + + let mut values = Vec::new(); + // Parse enum values: NAME = VALUE + for vcap in val_re.captures_iter(body) { + values.push((vcap[1].to_string(), vcap[2].to_string())); + } + + // If no explicit values, extract names and assign indices + if values.is_empty() { + for (idx, ncap) in name_re.captures_iter(body).enumerate() { + values.push((ncap[1].to_string(), format!("{}", idx))); + } + } + + enums.push(EnumDef { + name: type_name, + values, + }); + } + + enums +} + +/// Find state variables: variables declared with an enum type, looking for pairs like +/// `state_t state, next_state;` or `state_t current_state;` +pub fn find_state_variables(source: &str, enums: &[EnumDef]) -> Vec<(String, String)> { + let mut vars = Vec::new(); + + for e in enums { + // Match: type_name var1, var2, ...; + let pattern = format!(r"{}\s+([\w,\s]+);", regex::escape(&e.name)); + let re = Regex::new(&pattern).unwrap(); + + for cap in re.captures_iter(source) { + let var_list = &cap[1]; + let var_names: Vec<&str> = var_list.split(',').map(|s| s.trim()).collect(); + + // The state variable is typically the one without "next" in the name + for var_name in &var_names { + if !var_name.contains("next") && !var_name.is_empty() { + vars.push((var_name.to_string(), e.name.clone())); + break; // Take first non-next variable + } + } + } + } + + vars +} diff --git a/src/tools/fsm/extractor/mod.rs b/src/tools/fsm/extractor/mod.rs new file mode 100644 index 0000000..79ce8d6 --- /dev/null +++ b/src/tools/fsm/extractor/mod.rs @@ -0,0 +1,232 @@ +//! Deterministic FSM extraction from Verilog/SystemVerilog source. +//! +//! Extracts state machines by analyzing: +//! 1. `typedef enum` declarations for state encodings +//! 2. `always_ff` blocks for state register assignments (to find initial/reset state) +//! 3. `always_comb` blocks with `case` statements for next-state logic +//! +//! This is a best-effort static analysis that works well for standard FSM coding styles. + +mod constant_parser; +mod enum_parser; +mod signal_classifier; +mod transition_parser; + +use super::types::{FsmAnalysis, State, StateMachine}; +use crate::tools::verilog_parser::ModuleInfo; +use anyhow::{Context, Result}; +use std::fs; +use std::path::Path; + +pub use constant_parser::{extract_constants, parse_verilog_constant}; +pub use enum_parser::{extract_enum_types, find_state_variables, EnumDef}; +pub use signal_classifier::classify_transition_signals; +pub use transition_parser::{ + extract_transitions_from_case, find_initial_state, find_next_state_variable, +}; + +/// Extract FSM analysis from a Verilog source file using deterministic parsing. +pub fn extract_fsm( + verilog_path: &Path, + module_info: &ModuleInfo, + clock_name: &str, + reset_name: &str, + reset_active_high: bool, +) -> Result { + let source = fs::read_to_string(verilog_path) + .with_context(|| format!("Failed to read {}", verilog_path.display()))?; + + let state_machines = extract_state_machines(&source, module_info)?; + + Ok(FsmAnalysis { + module_name: module_info.name.clone(), + state_machines, + clock_signal: clock_name.to_string(), + reset_signal: reset_name.to_string(), + reset_active_high, + }) +} + +/// Extract all state machines from the source. +fn extract_state_machines(source: &str, module_info: &ModuleInfo) -> Result> { + let mut machines = Vec::new(); + + // Step 0: Extract all localparam/parameter constants from the source + let constants = extract_constants(source); + + // Step 1: Find enum type definitions + let enums = extract_enum_types(source); + + // Step 2: Find state variable declarations (variables using enum types) + let state_vars = find_state_variables(source, &enums); + + // Step 3: For each state variable, find the FSM structure + for (state_var, enum_name) in &state_vars { + if let Some(enum_def) = enums.iter().find(|e| &e.name == enum_name) { + // Find the initial/reset state from always_ff blocks + let initial_state = find_initial_state(source, state_var).unwrap_or_else(|| { + enum_def + .values + .first() + .map(|v| v.0.clone()) + .unwrap_or_default() + }); + + // Find transitions from always_comb case statements + let next_state_var = find_next_state_variable(source, state_var); + let case_var = next_state_var.as_deref().unwrap_or(state_var); + let transitions = + extract_transitions_from_case(source, state_var, case_var, &enum_def.values); + + // Classify signals + let input_names: std::collections::HashSet = module_info + .ports + .iter() + .filter(|p| p.direction == "input") + .map(|p| p.name.clone()) + .collect(); + + let (control_signals, _data_signals, raw_internal_signals) = + classify_transition_signals(&transitions, &input_names); + + // Separate compile-time constants from runtime signals + let state_names: std::collections::HashSet = + enum_def.values.iter().map(|(n, _)| n.clone()).collect(); + let (internal_signals, parameters) = + signal_classifier::classify_internal_signals(&raw_internal_signals, &constants, &state_names); + + let states: Vec = enum_def + .values + .iter() + .map(|(name, value)| State { + name: name.clone(), + value: value.clone(), + description: String::new(), + }) + .collect(); + + machines.push(StateMachine { + state_variable: state_var.clone(), + state_type: "enum".to_string(), + states, + initial_state, + transitions, + control_signals, + data_signals: vec![], + internal_signals, + parameters, + }); + } + } + + // Fallback: if no enum-based FSMs found, try localparam/parameter-based + if machines.is_empty() + && let Some(sm) = try_extract_param_fsm(source, module_info, &constants) + { + machines.push(sm); + } + + Ok(machines) +} + +/// Fallback: try to extract FSM from localparam/parameter-based state encoding. +fn try_extract_param_fsm( + source: &str, + module_info: &ModuleInfo, + constants: &std::collections::HashMap, +) -> Option { + use regex::Regex; + use std::collections::HashSet; + + // Look for localparam patterns like: + // localparam S0 = 3'b000, S1 = 3'b001, ... + // localparam IDLE = 0, RUN = 1, ... + let param_re = Regex::new( + r"(?s)(?:localparam|parameter)\s+((?:\w+\s*=\s*[^;,]+(?:,\s*\w+\s*=\s*[^;,]+)*))\s*;", + ) + .ok()?; + + let mut best_states: Vec<(String, String)> = Vec::new(); + let val_re = Regex::new(r"(\w+)\s*=\s*(\S+)").unwrap(); + + for cap in param_re.captures_iter(source) { + let param_body = &cap[1]; + let values: Vec<(String, String)> = val_re + .captures_iter(param_body) + .map(|v| (v[1].to_string(), v[2].to_string())) + .collect(); + + // Heuristic: if there are 3+ values and names look like state names + if values.len() >= 3 && values.len() > best_states.len() { + let looks_like_states = values.iter().any(|(n, _)| { + n.starts_with('S') + || n.contains("IDLE") + || n.contains("INIT") + || n.contains("STATE") + }); + if looks_like_states { + best_states = values; + } + } + } + + if best_states.is_empty() { + return None; + } + + // Find the state variable by looking for case statements using these names + let state_name_pattern = best_states + .iter() + .map(|(n, _)| regex::escape(n)) + .collect::>() + .join("|"); + + let case_re = Regex::new(&format!( + r"case\s*\(\s*(\w+)\s*\).*?(?:{})", + state_name_pattern + )) + .ok()?; + + let state_var = case_re.captures(source).map(|c| c[1].to_string())?; + + let next_state_var = find_next_state_variable(source, &state_var); + let case_var = next_state_var.as_deref().unwrap_or(&state_var); + + let transitions = extract_transitions_from_case(source, &state_var, case_var, &best_states); + + let initial_state = + find_initial_state(source, &state_var).unwrap_or_else(|| best_states[0].0.clone()); + + let input_names: HashSet = module_info + .ports + .iter() + .filter(|p| p.direction == "input") + .map(|p| p.name.clone()) + .collect(); + + let (control_signals, _, raw_internal_signals) = + classify_transition_signals(&transitions, &input_names); + + let state_name_set: HashSet = best_states.iter().map(|(n, _)| n.clone()).collect(); + let (internal_signals, parameters) = + signal_classifier::classify_internal_signals(&raw_internal_signals, constants, &state_name_set); + + Some(StateMachine { + state_variable: state_var, + state_type: "parameter".to_string(), + states: best_states + .iter() + .map(|(name, value)| State { + name: name.clone(), + value: value.clone(), + description: String::new(), + }) + .collect(), + initial_state, + transitions, + control_signals, + data_signals: vec![], + internal_signals, + parameters, + }) +} diff --git a/src/tools/fsm/extractor/signal_classifier.rs b/src/tools/fsm/extractor/signal_classifier.rs new file mode 100644 index 0000000..23a55ac --- /dev/null +++ b/src/tools/fsm/extractor/signal_classifier.rs @@ -0,0 +1,111 @@ +//! Signal classification for FSM transitions. + +use super::super::types::{Parameter, Transition}; +use regex::Regex; +use std::collections::{HashMap, HashSet}; + +/// Classify signals referenced in transitions as control, data, or internal. +pub fn classify_transition_signals( + transitions: &[Transition], + input_names: &HashSet, +) -> (Vec, Vec, Vec) { + let mut control = HashSet::new(); + let mut internal = HashSet::new(); + let word_re = Regex::new(r"\b([a-zA-Z_]\w*)\b").unwrap(); + // Strip Verilog numeric literals before extracting identifiers, e.g.: + // 8'd11, 4'b1010, 'hFF, 16'sd3 + // Otherwise, literals like 8'd11 can be mis-tokenized as fake identifiers ("d11"). + let verilog_lit_sized_re = Regex::new(r"(?i)\b\d+\s*'\s*[s]?[bdoh]\s*[0-9a-f_xz?]+").unwrap(); + let verilog_lit_unsized_re = Regex::new(r"(?i)'\s*[s]?[bdoh]\s*[0-9a-f_xz?]+").unwrap(); + + for t in transitions { + let cond_no_sized = verilog_lit_sized_re.replace_all(&t.condition, " "); + let cond_clean = verilog_lit_unsized_re.replace_all(&cond_no_sized, " "); + // Extract signal names from conditions + for cap in word_re.captures_iter(&cond_clean) { + let name = &cap[1]; + // Skip Verilog keywords + if is_verilog_keyword(name) { + continue; + } + if input_names.contains(name) { + control.insert(name.to_string()); + } else if !name.is_empty() { + // Could be an internal signal or a state name — skip state names + // We check if it looks like a signal (not all caps) + internal.insert(name.to_string()); + } + } + } + + // Refine input/wait conditions + // (This would modify transitions in a mutable version, but we return classifications) + + ( + control.into_iter().collect(), + vec![], // data signals need separate analysis + internal.into_iter().collect(), + ) +} + +/// Given internal_signals and the extracted constants, separate them into: +/// - runtime signals (actual registers/wires accessible via hierarchical access) +/// - parameters (compile-time constants with known values) +/// +/// Also removes state names from internal_signals. +pub fn classify_internal_signals( + internal_signals: &[String], + constants: &HashMap, + state_names: &HashSet, +) -> (Vec, Vec) { + let mut runtime = Vec::new(); + let mut params = Vec::new(); + + for sig in internal_signals { + if state_names.contains(sig) { + // Skip state names — they're not signals + continue; + } + if let Some(&val) = constants.get(sig) { + params.push(Parameter { + name: sig.clone(), + value: val, + }); + } else { + runtime.push(sig.clone()); + } + } + + (runtime, params) +} + +fn is_verilog_keyword(s: &str) -> bool { + matches!( + s, + "if" | "else" + | "begin" + | "end" + | "case" + | "endcase" + | "default" + | "always" + | "always_comb" + | "always_ff" + | "assign" + | "wire" + | "reg" + | "logic" + | "input" + | "output" + | "inout" + | "module" + | "endmodule" + | "posedge" + | "negedge" + | "or" + | "and" + | "not" + | "true" + | "false" + ) +} diff --git a/src/tools/fsm/extractor/transition_parser.rs b/src/tools/fsm/extractor/transition_parser.rs new file mode 100644 index 0000000..721a5ae --- /dev/null +++ b/src/tools/fsm/extractor/transition_parser.rs @@ -0,0 +1,300 @@ +//! Transition parsing from Verilog case statements. + +use super::super::types::Transition; +use regex::Regex; +use std::collections::HashSet; + +/// Find the next_state variable name associated with a state variable. +pub fn find_next_state_variable(source: &str, state_var: &str) -> Option { + // Common patterns: next_state, nstate, ns, state_next + let candidates = [ + format!("next_{}", state_var), + format!("{}_next", state_var), + "next_state".to_string(), + "nstate".to_string(), + "ns".to_string(), + ]; + + for candidate in &candidates { + let pattern = format!(r"\b{}\b", regex::escape(candidate)); + if Regex::new(&pattern).unwrap().is_match(source) { + return Some(candidate.clone()); + } + } + + None +} + +/// Find the initial/reset state from always_ff blocks. +pub fn find_initial_state(source: &str, state_var: &str) -> Option { + // Look for reset assignment pattern: + // if (!rst_n) state <= S0; + // if (rst) state <= IDLE; + let pattern = format!( + r"(?:if\s*\(.*?(?:rst|reset).*?\))\s*(?:begin\s*)?{}(?:\s*<=\s*|\s*=\s*)(\w+)", + regex::escape(state_var) + ); + let re = Regex::new(&pattern).ok()?; + + if let Some(cap) = re.captures(source) { + return Some(cap[1].to_string()); + } + + None +} + +/// Extract transitions from `case(state)` blocks in `always_comb` or `always @*` blocks. +pub fn extract_transitions_from_case( + source: &str, + state_var: &str, + next_state_var: &str, + enum_values: &[(String, String)], +) -> Vec { + let mut transitions = Vec::new(); + + // Find the case block for the state variable + // Match: case (state) or case(state) + let case_pattern = format!( + r"(?s)case\s*\(\s*{}\s*\)(.*?)endcase", + regex::escape(state_var) + ); + let case_re = Regex::new(&case_pattern).unwrap(); + + let state_names: HashSet = enum_values.iter().map(|(n, _)| n.clone()).collect(); + + for case_match in case_re.captures_iter(source) { + let case_body = &case_match[1]; + + // Split case body into items manually (no lookahead needed) + // Find all positions where a state label starts: "STATE_NAME :" or "default :" + let label_pattern = format!( + r"\b({}|default)\s*:", + enum_values + .iter() + .map(|(n, _)| regex::escape(n)) + .collect::>() + .join("|"), + ); + let label_re = Regex::new(&label_pattern).unwrap(); + + // Collect (start_of_match, label_name, start_of_body) tuples + let mut items: Vec<(usize, String, usize)> = Vec::new(); + for m in label_re.captures_iter(case_body) { + let full_match = m.get(0).unwrap(); + let label = m[1].to_string(); + items.push((full_match.start(), label, full_match.end())); + } + + // Extract the body for each item (text between this label and the next) + for i in 0..items.len() { + let from_state = &items[i].1; + let body_start = items[i].2; + let body_end = if i + 1 < items.len() { + items[i + 1].0 + } else { + case_body.len() + }; + let item_body = &case_body[body_start..body_end]; + + let item_transitions = + extract_transitions_from_item(from_state, item_body, next_state_var, &state_names); + transitions.extend(item_transitions); + } + } + + transitions +} + +/// Extract transitions from a single case item body (e.g., the code after `S0: begin ... end`). +fn extract_transitions_from_item( + from_state: &str, + body: &str, + next_state_var: &str, + state_names: &HashSet, +) -> Vec { + let mut transitions = Vec::new(); + + // Find all assignments to next_state_var + let assign_pattern = format!(r"(?s){}\s*(?:<=|=)\s*(\w+)", regex::escape(next_state_var)); + let assign_re = Regex::new(&assign_pattern).unwrap(); + + // Strategy: parse if/else chains to extract conditions per assignment + // Split the body into condition blocks + let conditions = parse_if_else_chain(body, next_state_var, state_names); + + if !conditions.is_empty() { + for (priority, (condition, to_state)) in conditions.iter().enumerate() { + transitions.push(Transition { + from_state: from_state.to_string(), + to_state: to_state.clone(), + condition: condition.clone(), + input_condition: condition.clone(), // Will be refined later + wait_condition: String::new(), + priority: (priority + 1) as i32, + }); + } + } else { + // No if/else found — unconditional assignment + for cap in assign_re.captures_iter(body) { + let to_state = cap[1].to_string(); + if state_names.contains(&to_state) || to_state == from_state { + transitions.push(Transition { + from_state: from_state.to_string(), + to_state, + condition: "true".to_string(), + input_condition: String::new(), + wait_condition: String::new(), + priority: 1, + }); + break; + } + } + } + + transitions +} + +/// Extract the content between balanced parentheses starting at byte position `start`. +/// Returns the content (without the outer parens) if balanced parens are found. +fn extract_balanced_paren(s: &str, start: usize) -> Option { + let bytes = s.as_bytes(); + if bytes.get(start) != Some(&b'(') { + return None; + } + let mut depth: i32 = 0; + for (i, &b) in bytes[start..].iter().enumerate() { + match b { + b'(' => depth += 1, + b')' => { + depth -= 1; + if depth == 0 { + return Some(s[start + 1..start + i].trim().to_string()); + } + } + _ => {} + } + } + None +} + +/// Parse if/else chains to extract (condition, next_state) pairs. +/// +/// Uses a line-based approach that handles common Verilog FSM patterns: +/// if (cond) next_state = X; else next_state = Y; +/// if (cond) begin next_state = X; end else begin next_state = Y; end +/// if (cond1) ... else if (cond2) ... else ... +fn parse_if_else_chain( + body: &str, + next_state_var: &str, + state_names: &HashSet, +) -> Vec<(String, String)> { + let mut results = Vec::new(); + + let assign_pattern = format!(r"{}\s*(?:<=|=)\s*(\w+)", regex::escape(next_state_var)); + let assign_re = Regex::new(&assign_pattern).unwrap(); + + // Regex patterns for line-level detection (condition extracted via balanced parens) + let if_detect = Regex::new(r"\bif\s*\(").unwrap(); + let else_if_detect = Regex::new(r"\belse\s+if\s*\(").unwrap(); + let else_re = Regex::new(r"\belse\b").unwrap(); + + // Track the current condition context + let mut current_conditions: Vec = Vec::new(); + let mut in_else = false; + + for line in body.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with("//") { + continue; + } + + // Check for else if (must be checked before else) + if let Some(m) = else_if_detect.find(trimmed) + && let Some(cond) = extract_balanced_paren(trimmed, m.end() - 1) + { + current_conditions.push(cond); + in_else = false; + + // Also check if there's an assignment on the same line + if let Some(acap) = assign_re.captures(trimmed) { + let to_state = acap[1].to_string(); + if state_names.contains(&to_state) { + let condition = current_conditions.last().unwrap().clone(); + results.push((condition, to_state)); + } + } + continue; + } + + // Check for plain else (not else if) + if else_re.is_match(trimmed) && else_if_detect.find(trimmed).is_none() { + in_else = true; + + // Also check if there's an assignment on the same line after else + if let Some(acap) = assign_re.captures(trimmed) { + let to_state = acap[1].to_string(); + if state_names.contains(&to_state) { + let condition = if current_conditions.len() == 1 { + format!("!({})", current_conditions[0]) + } else { + current_conditions + .iter() + .map(|c| format!("!({})", c)) + .collect::>() + .join(" && ") + }; + results.push((condition, to_state)); + } + } + continue; + } + + // Check for if (not else if) + if else_if_detect.find(trimmed).is_none() + && let Some(m) = if_detect.find(trimmed) + && let Some(cond) = extract_balanced_paren(trimmed, m.end() - 1) + { + current_conditions.clear(); + current_conditions.push(cond); + in_else = false; + + // Also check if there's an assignment on the same line + if let Some(acap) = assign_re.captures(trimmed) { + let to_state = acap[1].to_string(); + if state_names.contains(&to_state) { + results.push((current_conditions.last().unwrap().clone(), to_state)); + } + } + continue; + } + + // Check for assignment on a line that's not an if/else line + if let Some(acap) = assign_re.captures(trimmed) { + let to_state = acap[1].to_string(); + if state_names.contains(&to_state) { + let condition = if in_else && !current_conditions.is_empty() { + if current_conditions.len() == 1 { + format!("!({})", current_conditions[0]) + } else { + current_conditions + .iter() + .map(|c| format!("!({})", c)) + .collect::>() + .join(" && ") + } + } else if !current_conditions.is_empty() { + current_conditions.last().unwrap().clone() + } else { + "true".to_string() + }; + + results.push((condition, to_state)); + } + } + } + + // Deduplicate + results.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1); + + results +} diff --git a/src/tools/fsm/lm_analyzer.rs b/src/tools/fsm/lm_analyzer.rs new file mode 100644 index 0000000..f0da6cf --- /dev/null +++ b/src/tools/fsm/lm_analyzer.rs @@ -0,0 +1,81 @@ +//! LM-based FSM analysis using Python scripts. + +use super::types::{load_fsm_analysis, FsmAnalysis}; +use anyhow::{Context, Result}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Run LM-based FSM analysis using the Python script via `uv`. +pub fn run_lm_fsm_analysis( + verilog_path: &Path, + out_path: &Path, + provider: Option<&str>, + model: Option<&str>, + endpoint: Option<&str>, +) -> Result { + let provider = provider.unwrap_or("gemini"); + let model_default = match provider { + "gemini" => "gemini-2.5-flash", + "openai" => "gpt-4", + "openrouter" => "anthropic/claude-3-opus", + _ => "gpt-4", + }; + let model = model.unwrap_or(model_default); + + log::info!( + "Running LM-based FSM analysis (provider={}, model={})...", + provider, + model + ); + + // Get the fsm_analyzer directory from embedded cache + let fsm_analyzer_dir = get_fsm_analyzer_dir()?; + + // Output path for the analysis JSON + let analysis_output = out_path.with_extension("state_analysis.json"); + + // Build the uv command + let mut cmd = Command::new("uv"); + cmd.arg("run"); + cmd.arg("--project").arg(&fsm_analyzer_dir); + cmd.arg("fsm-analyzer"); + cmd.arg("--verilog-source").arg(verilog_path); + cmd.arg("--output").arg(&analysis_output); + cmd.arg("--provider").arg(provider); + cmd.arg("--model").arg(model); + + if let Some(ep) = endpoint { + cmd.arg("--endpoint").arg(ep); + } + + log::info!( + "Running: uv run --project {} fsm-analyzer ...", + fsm_analyzer_dir.display() + ); + + let output = cmd + .output() + .context("Failed to run `uv`. Is uv installed? (https://docs.astral.sh/uv/)")?; + + // Log stderr output + let stderr = String::from_utf8_lossy(&output.stderr); + for line in stderr.lines() { + log::info!("[fsm-analyzer] {}", line); + } + + if !output.status.success() { + anyhow::bail!( + "LM-based FSM analysis failed (exit code {:?}):\n{}", + output.status.code(), + stderr + ); + } + + // Load the result + load_fsm_analysis(&analysis_output) +} + +/// Get the fsm_analyzer directory path (from embedded cache) +fn get_fsm_analyzer_dir() -> Result { + crate::python_runtime::get_fsm_analyzer_path() +} diff --git a/src/tools/fsm/mod.rs b/src/tools/fsm/mod.rs new file mode 100644 index 0000000..8fbceb8 --- /dev/null +++ b/src/tools/fsm/mod.rs @@ -0,0 +1,17 @@ +//! FSM (Finite State Machine) analysis and extraction tools. +//! +//! This module provides comprehensive FSM extraction, analysis, and testbench generation +//! capabilities for Verilog/SystemVerilog designs. + +pub mod condition_splitter; +pub mod extractor; +pub mod lm_analyzer; +pub mod types; +pub mod template; + +// Re-export commonly used types and functions +pub use condition_splitter::split_transition_conditions; +pub use extractor::extract_fsm; +pub use lm_analyzer::run_lm_fsm_analysis; +pub use template::{parse_state_value, TemplateState, TemplateStateMachine}; +pub use types::{load_fsm_analysis, save_fsm_analysis, FsmAnalysis, Parameter, State, StateMachine, Transition}; diff --git a/src/tools/fsm/template.rs b/src/tools/fsm/template.rs new file mode 100644 index 0000000..b81b0fb --- /dev/null +++ b/src/tools/fsm/template.rs @@ -0,0 +1,52 @@ +//! Template-specific data structures and utilities for FSM testbench generation. + +use super::types::{Parameter, Transition}; +use serde::Serialize; + +/// Template-specific state machine representation with numeric state values. +/// The DFS algorithm is now implemented in C++, so we only pass the graph data. +#[derive(Debug, Clone, Serialize)] +pub struct TemplateStateMachine { + pub state_variable: String, + pub state_type: String, + pub initial_state: String, + pub states: Vec, + pub transitions: Vec, + pub control_signals: Vec, + pub data_signals: Vec, + pub internal_signals: Vec, + pub parameters: Vec, +} + +/// Template-specific state with both the original value string and a parsed numeric value. +#[derive(Debug, Clone, Serialize)] +pub struct TemplateState { + pub name: String, + pub value: String, + pub value_numeric: u64, +} + +/// Parse a Verilog state value string (e.g., "3'd0", "4'b1010", "0") to a numeric u64. +pub fn parse_state_value(value: &str) -> u64 { + let trimmed = value.trim(); + // Try plain integer + if let Ok(v) = trimmed.parse::() { + return v; + } + // Try Verilog literal + if let Some(pos) = trimmed.find('\'') + && pos + 2 <= trimmed.len() + { + let base_char = trimmed.as_bytes().get(pos + 1).copied().unwrap_or(b'd'); + let digits = &trimmed[pos + 2..]; + let radix = match base_char { + b'b' | b'B' => 2, + b'o' | b'O' => 8, + b'd' | b'D' => 10, + b'h' | b'H' => 16, + _ => 10, + }; + return u64::from_str_radix(digits, radix).unwrap_or(0); + } + 0 +} diff --git a/src/tools/fsm/types.rs b/src/tools/fsm/types.rs new file mode 100644 index 0000000..30d192e --- /dev/null +++ b/src/tools/fsm/types.rs @@ -0,0 +1,97 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; + +/// Top-level FSM analysis result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FsmAnalysis { + pub module_name: String, + pub state_machines: Vec, + pub clock_signal: String, + pub reset_signal: String, + pub reset_active_high: bool, +} + +/// A single state machine within a module +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StateMachine { + pub state_variable: String, + #[serde(default)] + pub state_type: String, + pub states: Vec, + pub initial_state: String, + pub transitions: Vec, + #[serde(default)] + pub control_signals: Vec, + #[serde(default)] + pub data_signals: Vec, + #[serde(default)] + pub internal_signals: Vec, + /// Compile-time constants (localparam/parameter) referenced in transition conditions. + /// Each entry is { "name": "...", "value": }. + #[serde(default)] + pub parameters: Vec, +} + +/// A compile-time constant (localparam or parameter) extracted from the RTL. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Parameter { + pub name: String, + pub value: u64, +} + +/// A state in the FSM +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct State { + pub name: String, + #[serde(default)] + pub value: String, + #[serde(default)] + pub description: String, +} + +/// A transition between states +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transition { + pub from_state: String, + pub to_state: String, + #[serde(default)] + pub condition: String, + #[serde(default)] + pub input_condition: String, + #[serde(default)] + pub wait_condition: String, + #[serde(default = "default_priority")] + pub priority: i32, +} + +fn default_priority() -> i32 { + 1 +} + +/// A test sequence: a series of transitions to execute starting from reset +#[derive(Debug, Clone, Serialize)] +pub struct TestSequence { + pub id: usize, + pub transitions: Vec, +} + +/// Load FSM analysis from a JSON file +pub fn load_fsm_analysis(path: &Path) -> Result { + let content = + fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?; + serde_json::from_str(&content) + .with_context(|| format!("Failed to parse FSM analysis JSON from {}", path.display())) +} + +/// Save FSM analysis to a JSON file +pub fn save_fsm_analysis(analysis: &FsmAnalysis, path: &Path) -> Result<()> { + let json = serde_json::to_string_pretty(analysis) + .context("Failed to serialize FSM analysis to JSON")?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, json) + .with_context(|| format!("Failed to write FSM analysis to {}", path.display())) +} diff --git a/src/tools/verilog_parser.rs b/src/tools/verilog_parser.rs index 41b8d1c..89a6204 100644 --- a/src/tools/verilog_parser.rs +++ b/src/tools/verilog_parser.rs @@ -262,13 +262,12 @@ fn try_parse_with_sv_parser(verilog_text: &str) -> Result> { scc_in_degree[scc_child] += 1; scc_edges[scc_parent].push(scc_child); // Count how many instances of this child SCC the parent has - if let Some(counts) = graph.instantiation_counts.get(parent) { - if let Some(&child_instances) = counts.get(child) { + if let Some(counts) = graph.instantiation_counts.get(parent) + && let Some(&child_instances) = counts.get(child) { *scc_instance_counts[scc_parent] .entry(scc_child) .or_insert(0) += child_instances; } - } } } // Count actual instantiations, not just unique types @@ -690,14 +689,14 @@ fn sort_with_sv_parser_priority( // Use sv-parser's order but with iverilog's port information and sv-parser's submodule_count let mut result: Vec = sv_modules .into_iter() - .filter_map(|sv_module| { + .map(|sv_module| { if let Some(mut iverilog_module) = iverilog_map.remove(&sv_module.name) { // Use iverilog's ports but sv-parser's submodule_count iverilog_module.submodule_count = sv_module.submodule_count; - Some(iverilog_module) + iverilog_module } else { // Module found by sv-parser but not iverilog, keep sv-parser version - Some(sv_module) + sv_module } }) .collect(); diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 8a29773..738f830 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -23,7 +23,7 @@ fn cli_missing_required_args() { #[test] fn cli_invalid_design_type() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let (success, _, stderr) = env.run_stg(&[ @@ -46,7 +46,7 @@ fn cli_invalid_design_type() { #[test] fn cli_conflicting_flags() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); // Both --cc and --sc @@ -72,7 +72,7 @@ fn cli_conflicting_flags() { #[test] fn cli_cc_without_golden_or_header() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let (success, _, stderr) = env.run_stg(&[ @@ -99,7 +99,7 @@ fn cli_cc_without_golden_or_header() { #[test] fn cli_sv_mode_without_golden() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let (success, _, stderr) = env.run_stg(&[ @@ -151,7 +151,7 @@ fn cli_version() { #[test] fn identify_command() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let golden = example_dir.join("golden.v"); let out_yaml = env.temp_path().join("signals.yaml"); @@ -182,7 +182,7 @@ fn identify_command() { #[test] fn emplace_module_flag() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let golden = example_dir.join("golden.v"); @@ -224,7 +224,7 @@ fn emplace_module_flag() { #[test] fn config_file() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("pingpong"); + let example_dir = env.copy_example_files("V1/pingpong"); let dut = example_dir.join("up_only.v"); let golden = example_dir.join("golden.v"); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9c52e0d..37f45f8 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -41,10 +41,14 @@ impl TestEnv { let example_dst = self.temp_path().join(example_name); // Create destination directory - fs::create_dir_all(&example_dst).expect("Failed to create example dir"); + fs::create_dir_all(&example_dst).unwrap_or_else(|e| { + panic!("Failed to create example dir {:?}: {}", example_dst, e) + }); // Copy all files from example directory - for entry in fs::read_dir(&example_src).expect("Failed to read example dir") { + for entry in fs::read_dir(&example_src).unwrap_or_else(|e| { + panic!("Failed to read example dir {:?}: {}", example_src, e) + }) { let entry = entry.expect("Failed to read entry"); let file_type = entry.file_type().expect("Failed to get file type"); @@ -52,7 +56,9 @@ impl TestEnv { let src = entry.path(); let filename = src.file_name().expect("Failed to get filename"); let dst = example_dst.join(filename); - fs::copy(&src, &dst).expect("Failed to copy file"); + fs::copy(&src, &dst).unwrap_or_else(|e| { + panic!("Failed to copy file {:?} to {:?}: {}", src, dst, e) + }); } } diff --git a/tests/compile_tests.rs b/tests/compile_tests.rs index 7f0ae88..c0f8be0 100644 --- a/tests/compile_tests.rs +++ b/tests/compile_tests.rs @@ -14,7 +14,7 @@ fn compile_command_iverilog() { return; } - let example_dir = env.copy_example_files("pingpong"); + let example_dir = env.copy_example_files("V1/pingpong"); let dut = example_dir.join("up_only.v"); let golden = example_dir.join("golden.v"); @@ -77,7 +77,7 @@ fn compile_command_verilator() { return; } - let example_dir = env.copy_example_files("pingpong"); + let example_dir = env.copy_example_files("V1/pingpong"); let dut = example_dir.join("up_only.v"); let golden = example_dir.join("golden.v"); @@ -144,7 +144,7 @@ fn compile_command_cpp_testbench() { return; } - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let dut = example_dir.join("gate_level.v"); let golden_header = example_dir.join("golden_model.h"); @@ -215,7 +215,7 @@ fn compile_command_systemc_testbench() { return; } - let example_dir = env.copy_example_files("ALU_sc"); + let example_dir = env.copy_example_files("V3/ALU_sc"); let dut = example_dir.join("gate_level.v"); let golden_header = example_dir.join("golden_model_sc.h"); @@ -289,7 +289,7 @@ fn compile_command_with_verilator_mpi() { return; } - let example_dir = env.copy_example_files("pingpong"); + let example_dir = env.copy_example_files("V1/pingpong"); let dut = example_dir.join("up_only.v"); let golden = example_dir.join("golden.v"); @@ -361,7 +361,7 @@ fn compile_command_with_custom_compile_flags() { return; } - let example_dir = env.copy_example_files("pingpong"); + let example_dir = env.copy_example_files("V1/pingpong"); let dut = example_dir.join("up_only.v"); let golden = example_dir.join("golden.v"); @@ -515,7 +515,7 @@ fn compile_command_nonexistent_files() { #[test] fn compile_command_conflicting_flags() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let dut = example_dir.join("gate_level.v"); let golden = example_dir.join("golden_model.h"); @@ -552,7 +552,7 @@ fn compile_command_conflicting_flags() { #[test] fn compile_command_cpp_without_module() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let dut = example_dir.join("gate_level.v"); let golden = example_dir.join("golden_model.h"); @@ -611,7 +611,7 @@ fn compile_command_preserves_user_testbench() { return; } - let example_dir = env.copy_example_files("pingpong"); + let example_dir = env.copy_example_files("V1/pingpong"); let dut = example_dir.join("up_only.v"); let golden = example_dir.join("golden.v"); diff --git a/tests/cpp_tests.rs b/tests/cpp_tests.rs index c41c0dc..9a99c67 100644 --- a/tests/cpp_tests.rs +++ b/tests/cpp_tests.rs @@ -8,7 +8,7 @@ use common::TestEnv; #[test] fn cpp_golden_header_generation() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let gate_level = example_dir.join("gate_level.v"); let out_tb = env.temp_path().join("tb_cpp.cpp"); @@ -77,7 +77,7 @@ fn cpp_header_compilation() { return; } - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); // Copy the implemented golden model let golden_model = example_dir.join("golden_model.h"); @@ -130,7 +130,7 @@ fn cpp_testbench_with_faulty_design() { return; } - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let gate_level = example_dir.join("gate_level.v"); let golden_model = example_dir.join("golden_model.h"); @@ -183,7 +183,7 @@ fn cpp_testbench_with_golden_design() { return; } - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let golden_v = example_dir.join("golden.v"); let golden_model = example_dir.join("golden_model.h"); @@ -240,7 +240,7 @@ fn cpp_testbench_with_mpi() { return; } - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let golden_v = example_dir.join("golden.v"); let golden_model = example_dir.join("golden_model.h"); @@ -285,7 +285,7 @@ fn cpp_testbench_coverage() { return; } - let example_dir = env.copy_example_files("ALU_cc"); + let example_dir = env.copy_example_files("V3/ALU_cc"); let golden_v = example_dir.join("golden.v"); let golden_model = example_dir.join("golden_model.h"); diff --git a/tests/multi_dut_tests.rs b/tests/multi_dut_tests.rs index eb93b52..e3781d1 100644 --- a/tests/multi_dut_tests.rs +++ b/tests/multi_dut_tests.rs @@ -7,7 +7,7 @@ use common::TestEnv; #[test] fn multi_dut_different_names_sv() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut2_buggy_sub.v"); @@ -85,7 +85,7 @@ fn multi_dut_different_names_sv() { #[test] fn multi_dut_same_names_with_emplace() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut3_same_name.v"); // Same module name @@ -133,7 +133,7 @@ fn multi_dut_same_names_with_emplace() { #[test] fn multi_dut_same_names_without_emplace_should_fail() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut3_same_name.v"); // Same module name @@ -172,7 +172,7 @@ fn multi_dut_same_names_without_emplace_should_fail() { #[test] fn multi_dut_sequential_sv() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/pingpong"); + let example_dir = env.copy_example_files("V3.2/multi_dut/pingpong"); let dut1 = example_dir.join("dut1_up_only.v"); let dut2 = example_dir.join("dut2_down_only.v"); @@ -231,7 +231,7 @@ fn multi_dut_sequential_sv() { #[test] fn multi_dut_seq_done_sv() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/GCD"); + let example_dir = env.copy_example_files("V3.2/multi_dut/GCD"); let dut1 = example_dir.join("dut1_always_one.sv"); let dut2 = example_dir.join("dut2_off_by_one.sv"); @@ -362,7 +362,7 @@ endmodule #[test] fn multi_dut_cpp_mode() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut2_buggy_sub.v"); @@ -420,7 +420,7 @@ fn multi_dut_cpp_mode() { #[test] fn multi_dut_systemc_mode() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/pingpong"); + let example_dir = env.copy_example_files("V3.2/multi_dut/pingpong"); let dut1 = example_dir.join("dut1_up_only.v"); let dut2 = example_dir.join("dut2_down_only.v"); @@ -475,7 +475,7 @@ fn multi_dut_systemc_mode() { #[test] fn multi_dut_with_module_names() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut2_buggy_sub.v"); @@ -603,7 +603,7 @@ endmodule #[test] fn multi_dut_cpp_sync_inputs_test() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut3_same_name.v"); @@ -676,7 +676,7 @@ fn multi_dut_cpp_sync_inputs_test() { #[test] fn multi_dut_cpp_reset_methods_test() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/GCD"); + let example_dir = env.copy_example_files("V3.2/multi_dut/GCD"); let dut1 = example_dir.join("dut1_always_one.sv"); let dut2 = example_dir.join("dut3_same_name.sv"); @@ -740,7 +740,7 @@ fn multi_dut_cpp_reset_methods_test() { #[test] fn multi_dut_systemc_sync_inputs_test() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut3_same_name.v"); @@ -793,7 +793,7 @@ fn multi_dut_systemc_sync_inputs_test() { #[test] fn multi_dut_json_output_verification() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut3_same_name.v"); @@ -871,7 +871,7 @@ fn multi_dut_json_output_verification() { #[test] fn multi_dut_seq_done_mode_verification() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/GCD"); + let example_dir = env.copy_example_files("V3.2/multi_dut/GCD"); let dut1 = example_dir.join("dut1_always_one.sv"); let dut2 = example_dir.join("dut3_same_name.sv"); @@ -931,7 +931,7 @@ fn multi_dut_seq_done_mode_verification() { #[test] fn multi_dut_mpi_cpp_mode() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut1 = example_dir.join("dut1_buggy_add.v"); let dut2 = example_dir.join("dut3_same_name.v"); @@ -1024,7 +1024,7 @@ fn multi_dut_mpi_cpp_mode() { #[test] fn multi_dut_mpi_systemc_mode() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/pingpong"); + let example_dir = env.copy_example_files("V3.2/multi_dut/pingpong"); let dut1 = example_dir.join("dut1_up_only.v"); let dut2 = example_dir.join("dut2_down_only.v"); @@ -1124,7 +1124,7 @@ fn multi_dut_mpi_systemc_mode() { #[test] fn multi_dut_mpi_sv_mode() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/GCD"); + let example_dir = env.copy_example_files("V3.2/multi_dut/GCD"); let dut1 = example_dir.join("dut1_always_one.sv"); let dut2 = example_dir.join("dut3_same_name.sv"); @@ -1208,7 +1208,7 @@ fn multi_dut_mpi_sv_mode() { #[test] fn multi_dut_mpi_seq_done_mode() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/GCD"); + let example_dir = env.copy_example_files("V3.2/multi_dut/GCD"); let dut1 = example_dir.join("dut1_always_one.sv"); let dut2 = example_dir.join("dut2_off_by_one.sv"); @@ -1277,7 +1277,7 @@ fn multi_dut_mpi_seq_done_mode() { #[test] fn multi_dut_mpi_sv_seq_done_mode() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/GCD"); + let example_dir = env.copy_example_files("V3.2/multi_dut/GCD"); let dut1 = example_dir.join("dut1_always_one.sv"); let dut2 = example_dir.join("dut2_off_by_one.sv"); diff --git a/tests/seq_done_tests.rs b/tests/seq_done_tests.rs index a87b8e7..e1c44cd 100644 --- a/tests/seq_done_tests.rs +++ b/tests/seq_done_tests.rs @@ -8,7 +8,7 @@ use common::TestEnv; #[test] fn seq_done_testbench_generation_sv() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let dut = example_dir.join("gcd.sv"); let golden = example_dir.join("gcd_golden.sv"); @@ -79,7 +79,7 @@ fn seq_done_testbench_generation_sv() { #[test] fn seq_done_testbench_generation_cpp() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let dut = example_dir.join("gcd.sv"); let golden = example_dir.join("gcd_golden.h"); @@ -142,7 +142,7 @@ fn seq_done_testbench_generation_cpp() { #[test] fn seq_done_testbench_generation_systemc() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let dut = example_dir.join("gcd.sv"); let golden = example_dir.join("gcd_golden.h"); @@ -207,7 +207,7 @@ fn seq_done_compilation_and_execution_cpp() { return; } - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let dut = example_dir.join("gcd.sv"); let golden = example_dir.join("gcd_golden.h"); @@ -288,7 +288,7 @@ fn seq_done_bug_detection() { return; } - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let buggy_dut = example_dir.join("gcd_buggy.sv"); let golden = example_dir.join("gcd_golden.h"); @@ -364,7 +364,7 @@ fn seq_done_iverilog_execution() { return; } - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let dut = example_dir.join("gcd.sv"); let golden = example_dir.join("gcd_golden.sv"); @@ -433,7 +433,7 @@ fn seq_done_iverilog_execution() { #[test] fn seq_done_signal_detection() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let dut = example_dir.join("gcd.sv"); let golden = example_dir.join("gcd_golden.sv"); @@ -486,7 +486,7 @@ fn seq_done_signal_detection() { #[test] fn seq_done_control_signal_handling() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("GCD"); + let example_dir = env.copy_example_files("V3/GCD"); let dut = example_dir.join("gcd.sv"); let golden = example_dir.join("gcd_golden.sv"); diff --git a/tests/skip_loop_tests.rs b/tests/skip_loop_tests.rs index b125afc..6996d08 100644 --- a/tests/skip_loop_tests.rs +++ b/tests/skip_loop_tests.rs @@ -7,7 +7,7 @@ use common::TestEnv; #[test] fn test_skip_loop_cpp_timeout() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut = example_dir.join("dut1_buggy_add.v"); let golden = example_dir.join("golden.v"); @@ -77,7 +77,7 @@ fn test_skip_loop_cpp_timeout() { #[test] fn test_skip_loop_sv_timeout() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/ALU"); + let example_dir = env.copy_example_files("V3.2/multi_dut/ALU"); let dut = example_dir.join("dut1_buggy_add.v"); let golden = example_dir.join("golden.v"); @@ -134,7 +134,7 @@ fn test_skip_loop_sv_timeout() { #[test] fn test_skip_loop_seq_done_timeout() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("multi_dut/GCD"); + let example_dir = env.copy_example_files("V3.2/multi_dut/GCD"); let dut = example_dir.join("dut1_always_one.sv"); let golden = example_dir.join("golden.sv"); diff --git a/tests/systemc_tests.rs b/tests/systemc_tests.rs index f98c75e..d3cf24c 100644 --- a/tests/systemc_tests.rs +++ b/tests/systemc_tests.rs @@ -8,7 +8,7 @@ use common::TestEnv; #[test] fn systemc_golden_header_generation() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU_sc"); + let example_dir = env.copy_example_files("V3/ALU_sc"); let gate_level = example_dir.join("gate_level.v"); let out_tb = env.temp_path().join("tb_sc.cpp"); @@ -70,7 +70,7 @@ fn systemc_testbench_with_faulty_design() { return; } - let example_dir = env.copy_example_files("ALU_sc"); + let example_dir = env.copy_example_files("V3/ALU_sc"); let gate_level = example_dir.join("gate_level.v"); let golden_model = example_dir.join("golden_model_sc.h"); @@ -127,7 +127,7 @@ fn systemc_testbench_with_golden_design() { return; } - let example_dir = env.copy_example_files("ALU_sc"); + let example_dir = env.copy_example_files("V3/ALU_sc"); let golden_v = example_dir.join("golden.v"); let golden_model = example_dir.join("golden_model_sc.h"); @@ -189,7 +189,7 @@ fn systemc_testbench_with_mpi() { return; } - let example_dir = env.copy_example_files("ALU_sc"); + let example_dir = env.copy_example_files("V3/ALU_sc"); let golden_v = example_dir.join("golden.v"); let golden_model = example_dir.join("golden_model_sc.h"); diff --git a/tests/verilog_tests.rs b/tests/verilog_tests.rs index 1ab7655..c76231a 100644 --- a/tests/verilog_tests.rs +++ b/tests/verilog_tests.rs @@ -8,7 +8,7 @@ use common::TestEnv; #[test] fn verilog_testbench_generation_combinational() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let golden = example_dir.join("golden.v"); @@ -51,7 +51,7 @@ fn verilog_testbench_generation_combinational() { #[test] fn verilog_testbench_generation_sequential() { let env = TestEnv::new(); - let example_dir = env.copy_example_files("pingpong"); + let example_dir = env.copy_example_files("V1/pingpong"); let dut = example_dir.join("up_only.v"); let golden = example_dir.join("golden.v"); @@ -98,7 +98,7 @@ fn iverilog_compilation() { return; } - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let golden = example_dir.join("golden.v"); @@ -141,7 +141,7 @@ fn iverilog_execution_finds_bug() { return; } - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let golden = example_dir.join("golden.v"); @@ -190,7 +190,7 @@ fn verilator_compilation() { return; } - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let golden = example_dir.join("golden.v"); @@ -239,7 +239,7 @@ fn verilator_mpi_compilation() { return; } - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let golden = example_dir.join("golden.v"); @@ -283,7 +283,7 @@ fn verilator_execution() { return; } - let example_dir = env.copy_example_files("ALU"); + let example_dir = env.copy_example_files("V1/ALU"); let gate_level = example_dir.join("gate_level.v"); let golden = example_dir.join("golden.v"); diff --git a/tools/fsm_analyzer/fsm_analyzer/__init__.py b/tools/fsm_analyzer/fsm_analyzer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/fsm_analyzer/fsm_analyzer/main.py b/tools/fsm_analyzer/fsm_analyzer/main.py new file mode 100644 index 0000000..9756076 --- /dev/null +++ b/tools/fsm_analyzer/fsm_analyzer/main.py @@ -0,0 +1,174 @@ +"""CLI entry point for the FSM analyzer. + +Usage: + uv run fsm-analyzer --verilog-source path/to/dut.sv --output analysis.json --provider gemini --model gemini-2.5-flash +""" + +import argparse +import json +import os +import sys +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser( + description="Analyze Verilog RTL to extract FSM state machines using LLMs" + ) + parser.add_argument( + "--verilog-source", + required=True, + help="Path to the Verilog/SystemVerilog source file", + ) + parser.add_argument( + "--output", + required=True, + help="Output path for the state analysis JSON", + ) + parser.add_argument( + "--provider", + choices=["openai", "gemini", "openrouter"], + default="gemini", + help="LLM provider to use (default: gemini)", + ) + parser.add_argument( + "--model", + default=None, + help="Model name (default depends on provider)", + ) + parser.add_argument( + "--endpoint", + default=None, + help="Custom API endpoint URL (for openai-compatible providers)", + ) + parser.add_argument( + "--env-file", + default=None, + help="Path to .env file for API keys", + ) + + args = parser.parse_args() + + # Load .env file if specified or look for one in common locations + env_paths = [] + if args.env_file: + env_paths.append(args.env_file) + else: + # Search for .env in parent directories + current = Path(args.verilog_source).resolve().parent + for _ in range(10): + env_path = current / ".env" + if env_path.exists(): + env_paths.append(str(env_path)) + break + parent = current.parent + if parent == current: + break + current = parent + + if env_paths: + try: + from dotenv import load_dotenv + + for env_path in env_paths: + load_dotenv(env_path, override=False) + print(f"Loaded environment from: {env_path}", file=sys.stderr) + except ImportError: + print( + "Warning: python-dotenv not installed, .env file ignored", + file=sys.stderr, + ) + + # Read Verilog source + verilog_path = Path(args.verilog_source) + if not verilog_path.exists(): + print(f"Error: Verilog file not found: {verilog_path}", file=sys.stderr) + sys.exit(1) + + verilog_source = verilog_path.read_text() + + # Set default model based on provider + model = args.model + if model is None: + defaults = { + "openai": "gpt-4", + "gemini": "gemini-2.5-flash", + "openrouter": "anthropic/claude-3-opus", + } + model = defaults.get(args.provider, "gpt-4") + + # Run analysis + print( + f"Analyzing FSM with provider={args.provider}, model={model}", + file=sys.stderr, + ) + + try: + if args.provider == "gemini": + from .providers.gemini_provider import analyze + + api_key = os.environ.get("GOOGLE_API_KEY") + if not api_key: + print( + "Error: GOOGLE_API_KEY not set. Set it in .env or environment.", + file=sys.stderr, + ) + sys.exit(1) + result = analyze(verilog_source, model=model, api_key=api_key) + + elif args.provider in ("openai", "openrouter"): + from .providers.openai_provider import analyze + + if args.provider == "openrouter": + api_key = os.environ.get("OPENROUTER_API_KEY") + endpoint = args.endpoint or "https://openrouter.ai/api/v1" + if not api_key: + print( + "Error: OPENROUTER_API_KEY not set.", + file=sys.stderr, + ) + sys.exit(1) + else: + api_key = os.environ.get("OPENAI_API_KEY") + endpoint = args.endpoint + if not api_key: + print( + "Error: OPENAI_API_KEY not set.", + file=sys.stderr, + ) + sys.exit(1) + + result = analyze( + verilog_source, + model=model, + endpoint=endpoint, + api_key=api_key, + ) + else: + print(f"Error: Unknown provider: {args.provider}", file=sys.stderr) + sys.exit(1) + + except Exception as e: + print(f"Error during LLM analysis: {e}", file=sys.stderr) + sys.exit(1) + + # Write output + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(result, indent=2) + "\n") + print(f"FSM analysis written to: {output_path}", file=sys.stderr) + + # Print summary + state_machines = result.get("state_machines", []) + print(f"Found {len(state_machines)} state machine(s):", file=sys.stderr) + for sm in state_machines: + states = sm.get("states", []) + transitions = sm.get("transitions", []) + print( + f" - {sm.get('state_variable', '?')}: {len(states)} states, {len(transitions)} transitions", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main() diff --git a/tools/fsm_analyzer/fsm_analyzer/prompt.py b/tools/fsm_analyzer/fsm_analyzer/prompt.py new file mode 100644 index 0000000..702a9b8 --- /dev/null +++ b/tools/fsm_analyzer/fsm_analyzer/prompt.py @@ -0,0 +1,341 @@ +"""LLM prompt template for FSM state machine extraction from Verilog RTL.""" + +import re + +SYSTEM_PROMPT = """\ +You are an expert digital design engineer specializing in FSM (Finite State Machine) analysis. +Your task is to analyze Verilog/SystemVerilog RTL code and extract all state machines present in the design. + +You must output ONLY valid JSON (no markdown, no explanation, no code fences) matching the schema below. +""" + +_FSM_ANALYSIS_TEMPLATE = """\ +Analyze the following Verilog/SystemVerilog module and extract all finite state machines (FSMs). + +For each state machine, identify: +1. The state variable name (e.g., "state", "current_state", "cs") +2. The state encoding type ("enum", "parameter", "localparam", "integer") +3. All states with their names, values, and descriptions +4. The initial/reset state +5. All transitions between states, including: + - from_state: source state name + - to_state: destination state name + - condition: the full transition condition expression + - input_condition: the part of the condition that depends on INPUT signals (external pins) + - wait_condition: the part of the condition that depends on INTERNAL signals (counters, registers, etc.) + - priority: transition priority (1 = highest, checked first in if/else chain) +6. Control signals: input signals that directly affect state transitions +7. Data signals: input signals that don't affect state transitions +8. Internal signals: internal RUNTIME registers/counters/wires that affect state transitions (e.g., timer, ped_latch). + Do NOT include localparam or parameter constants here — put those in "parameters". +9. Parameters: compile-time constants (localparam, parameter) that appear in transition conditions. + For each, provide the name and its resolved integer value. + +Also identify the clock signal, reset signal, and whether reset is active high. + +IMPORTANT rules for conditions: +- input_condition should ONLY reference input port signals +- wait_condition should ONLY reference internal signals (registers, counters, wires defined inside the module) +- If a transition condition mixes input and internal signals, split them appropriately +- If a transition depends only on inputs, wait_condition should be "" +- If a transition depends only on internal state (e.g., counter >= THRESHOLD - 1), input_condition should be "" and wait_condition should contain the expression +- Use Verilog syntax for conditions (e.g., "din", "!(din)", "sel == 2'b01", "counter == 8'd255") +- ONLY use signal and parameter names that actually exist in the module + +IMPORTANT rules for internal_signals vs parameters: +- "internal_signals" are RUNTIME signals: logic, reg, wire declarations that hold state at runtime (e.g., timer, counter, ped_latch) +- "parameters" are COMPILE-TIME constants: localparam or parameter declarations with fixed numeric values (e.g., GREEN_TICKS = 10, THRESHOLD = 255) +- A localparam or parameter MUST go in "parameters" with its resolved integer value, NOT in "internal_signals" + +IMPORTANT rules for signal references in conditions: +- Use ONLY the exact signal names as declared in the module +- Do NOT abbreviate or modify signal names +- Do NOT reference signals that do not exist in the module +- Every signal name in "control_signals", "data_signals", and "internal_signals" arrays must match an actual declaration in the source code +%s +Output ONLY a JSON object with this exact structure (no markdown, no extra text): +{ + "module_name": "", + "state_machines": [ + { + "state_variable": "", + "state_type": "enum|parameter|localparam|integer", + "states": [ + {"name": "", "value": "", "description": ""} + ], + "initial_state": "", + "transitions": [ + { + "from_state": "", + "to_state": "", + "condition": "", + "input_condition": "", + "wait_condition": "", + "priority": + } + ], + "control_signals": [""], + "data_signals": [""], + "internal_signals": [""], + "parameters": [ + {"name": "", "value": } + ] + } + ], + "clock_signal": "", + "reset_signal": "", + "reset_active_high": true|false +} + +Here is the Verilog source code to analyze: + +```verilog +%s +``` +""" + + +def _strip_comments(source: str) -> str: + """Strip Verilog comments from source code. + + Removes both line comments (// ...) and block comments (/* ... */) + to prevent the regex-based extractor from matching inside comments. + """ + # Remove block comments first (can span multiple lines) + source = re.sub(r"/\*.*?\*/", " ", source, flags=re.DOTALL) + # Remove line comments + source = re.sub(r"//[^\n]*", " ", source) + return source + + +def extract_module_symbols(verilog_source: str) -> str: + """Extract available symbols from Verilog source to provide as context to the LLM. + + Performs a lightweight regex-based scan of the Verilog source to discover + module ports, internal signals, parameters, and enum definitions. The result + is a human-readable summary suitable for inclusion in the LLM prompt so the + model knows exactly which symbol names are available for use in conditions. + + Returns: + A formatted string section listing all discoverable symbols, + or an empty string if nothing was found. + """ + # Strip comments to avoid false matches from text inside comments + clean_src = _strip_comments(verilog_source) + lines: list[str] = [] + + # --- Module name --- + m = re.search(r"\bmodule\s+(\w+)", clean_src) + if m: + lines.append(f"Module: {m.group(1)}") + + # --- Port declarations --- + # Handles both ANSI-style (inside module header) and non-ANSI (body declarations). + port_re = re.compile( + r"\b(input|output|inout)\s+" + r"(?:wire\s+|reg\s+|logic\s+)?" + r"(?:signed\s+)?" + r"(?:\[([^\]]*)\]\s*)?" + r"(\w+)", + ) + inputs: list[str] = [] + outputs: list[str] = [] + inouts: list[str] = [] + port_names: set[str] = set() + + for m in port_re.finditer(clean_src): + direction, width_expr, name = m.group(1), m.group(2), m.group(3) + port_names.add(name) + width_str = f" [{width_expr}]" if width_expr else "" + entry = f" - {name}{width_str}" + if direction == "input": + inputs.append(entry) + elif direction == "output": + outputs.append(entry) + else: + inouts.append(entry) + + if inputs: + lines.append( + "\nINPUT PORTS (candidates for control_signals or data_signals):" + ) + lines.extend(inputs) + if outputs: + lines.append("\nOUTPUT PORTS:") + lines.extend(outputs) + if inouts: + lines.append("\nINOUT PORTS:") + lines.extend(inouts) + + # --- Internal signal declarations --- + sig_re = re.compile( + r"\b(reg|wire|logic|integer)\s+" + r"(?:signed\s+)?" + r"(?:\[([^\]]*)\]\s*)?" + r"(\w+)", + ) + seen_sigs: set[str] = set() + internals: list[str] = [] + + for m in sig_re.finditer(clean_src): + sig_type, width_expr, name = m.group(1), m.group(2), m.group(3) + if name in port_names or name in seen_sigs: + continue + seen_sigs.add(name) + width_str = f" [{width_expr}]" if width_expr else "" + internals.append(f" - {sig_type}{width_str} {name}") + + if internals: + lines.append( + "\nINTERNAL SIGNALS (candidates for internal_signals or state variables):" + ) + lines.extend(internals) + + # --- Parameters and localparams --- + param_re = re.compile( + r"\b(localparam|parameter)\s+" + r"(?:\w+\s+)?" # optional type keyword + r"(?:\[([^\]]*)\]\s*)?" # optional width + r"(\w+)\s*=\s*([^;,\)\n]+)", + ) + params: list[str] = [] + + for m in param_re.finditer(clean_src): + kind, _, name, value = ( + m.group(1), + m.group(2), + m.group(3), + m.group(4).strip(), + ) + params.append(f" - {kind} {name} = {value}") + + if params: + lines.append( + '\nPARAMETERS / LOCALPARAMS (must go in "parameters" array, NOT in "internal_signals"):' + ) + lines.extend(params) + + # --- Enum / typedef enum state values --- + enum_re = re.compile(r"typedef\s+enum\s+[^{]*\{([^}]+)\}", re.DOTALL) + enum_vals: list[str] = [] + + for m in enum_re.finditer(clean_src): + body = m.group(1) + for item in body.split(","): + item = item.strip() + if item: + name_part = item.split("=")[0].strip() + if name_part: + enum_vals.append(f" - {name_part}") + + if enum_vals: + lines.append("\nENUM / STATE VALUES:") + lines.extend(enum_vals) + + if not lines: + return "" + + return ( + "\n=== Available Symbols in this Module ===\n" + + "\n".join(lines) + + "\n=== End of Available Symbols ===\n" + ) + + +def build_fsm_analysis_prompt(verilog_source: str) -> str: + """Build the complete FSM analysis prompt including extracted symbol context. + + This is the primary API for constructing the user prompt. It: + 1. Extracts available symbols from the Verilog source (ports, internals, params) + 2. Inserts them into the prompt template so the LLM knows which names to use + 3. Appends the full Verilog source for analysis + + Args: + verilog_source: The raw Verilog/SystemVerilog source code. + + Returns: + A fully-formatted prompt string ready to send to the LLM. + """ + symbols_section = extract_module_symbols(verilog_source) + return _FSM_ANALYSIS_TEMPLATE % (symbols_section, verilog_source) + + +# Keep backward compatibility: the old single-%s prompt (without symbols). +# Providers should prefer build_fsm_analysis_prompt() instead. +FSM_ANALYSIS_PROMPT = """\ +Analyze the following Verilog/SystemVerilog module and extract all finite state machines (FSMs). + +For each state machine, identify: +1. The state variable name (e.g., "state", "current_state", "cs") +2. The state encoding type ("enum", "parameter", "localparam", "integer") +3. All states with their names, values, and descriptions +4. The initial/reset state +5. All transitions between states, including: + - from_state: source state name + - to_state: destination state name + - condition: the full transition condition expression + - input_condition: the part of the condition that depends on INPUT signals (external pins) + - wait_condition: the part of the condition that depends on INTERNAL signals (counters, registers, etc.) + - priority: transition priority (1 = highest, checked first in if/else chain) +6. Control signals: input signals that directly affect state transitions +7. Data signals: input signals that don't affect state transitions +8. Internal signals: internal RUNTIME registers/counters/wires that affect state transitions (e.g., timer, ped_latch). + Do NOT include localparam or parameter constants here — put those in "parameters". +9. Parameters: compile-time constants (localparam, parameter) that appear in transition conditions. + For each, provide the name and its resolved integer value. + +Also identify the clock signal, reset signal, and whether reset is active high. + +IMPORTANT rules for conditions: +- input_condition should ONLY reference input port signals +- wait_condition should ONLY reference internal signals (registers, counters, wires defined inside the module) +- If a transition condition mixes input and internal signals, split them appropriately +- If a transition depends only on inputs, wait_condition should be "" +- If a transition depends only on internal state (e.g., counter >= THRESHOLD - 1), input_condition should be "" and wait_condition should contain the expression +- Use Verilog syntax for conditions (e.g., "din", "!(din)", "sel == 2'b01", "counter == 8'd255") + +IMPORTANT rules for internal_signals vs parameters: +- "internal_signals" are RUNTIME signals: logic, reg, wire declarations that hold state at runtime (e.g., timer, counter, ped_latch) +- "parameters" are COMPILE-TIME constants: localparam or parameter declarations with fixed numeric values (e.g., GREEN_TICKS = 10, THRESHOLD = 255) +- A localparam or parameter MUST go in "parameters" with its resolved integer value, NOT in "internal_signals" + +Output ONLY a JSON object with this exact structure (no markdown, no extra text): +{ + "module_name": "", + "state_machines": [ + { + "state_variable": "", + "state_type": "enum|parameter|localparam|integer", + "states": [ + {"name": "", "value": "", "description": ""} + ], + "initial_state": "", + "transitions": [ + { + "from_state": "", + "to_state": "", + "condition": "", + "input_condition": "", + "wait_condition": "", + "priority": + } + ], + "control_signals": [""], + "data_signals": [""], + "internal_signals": [""], + "parameters": [ + {"name": "", "value": } + ] + } + ], + "clock_signal": "", + "reset_signal": "", + "reset_active_high": true|false +} + +Here is the Verilog source code to analyze: + +```verilog +%s +``` +""" diff --git a/tools/fsm_analyzer/fsm_analyzer/providers/__init__.py b/tools/fsm_analyzer/fsm_analyzer/providers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/fsm_analyzer/fsm_analyzer/providers/gemini_provider.py b/tools/fsm_analyzer/fsm_analyzer/providers/gemini_provider.py new file mode 100644 index 0000000..f8739ab --- /dev/null +++ b/tools/fsm_analyzer/fsm_analyzer/providers/gemini_provider.py @@ -0,0 +1,63 @@ +"""Google Gemini provider for FSM analysis.""" + +import json +import os + +from google import genai +from google.genai import types + +from ..prompt import SYSTEM_PROMPT, build_fsm_analysis_prompt + + +def analyze( + verilog_source: str, + model: str = "gemini-2.5-flash", + api_key: str | None = None, +) -> dict: + """Analyze Verilog source using Google Gemini API. + + Args: + verilog_source: The Verilog source code to analyze. + model: The model name (e.g., "gemini-2.5-flash", "gemini-2.5-pro"). + api_key: API key (optional, falls back to GOOGLE_API_KEY env var). + + Returns: + Parsed JSON dict with FSM analysis. + """ + # The new SDK reads GOOGLE_API_KEY from env by default, + # but also accepts GEMINI_API_KEY. Pass explicitly if provided. + client_kwargs = {} + if api_key: + client_kwargs["api_key"] = api_key + elif os.environ.get("GOOGLE_API_KEY"): + client_kwargs["api_key"] = os.environ["GOOGLE_API_KEY"] + + client = genai.Client(**client_kwargs) + + user_prompt = build_fsm_analysis_prompt(verilog_source) + + response = client.models.generate_content( + model=model, + contents=user_prompt, + config=types.GenerateContentConfig( + system_instruction=SYSTEM_PROMPT, + temperature=0.0, + response_mime_type="application/json", + ), + ) + + content = response.text + if not content: + raise ValueError("Empty response from Gemini API") + + # Strip markdown code fences if present + content = content.strip() + if content.startswith("```"): + # Remove opening fence + first_newline = content.index("\n") + content = content[first_newline + 1 :] + if content.endswith("```"): + content = content[: -3] + content = content.strip() + + return json.loads(content) diff --git a/tools/fsm_analyzer/fsm_analyzer/providers/openai_provider.py b/tools/fsm_analyzer/fsm_analyzer/providers/openai_provider.py new file mode 100644 index 0000000..a7020e2 --- /dev/null +++ b/tools/fsm_analyzer/fsm_analyzer/providers/openai_provider.py @@ -0,0 +1,50 @@ +"""OpenAI-compatible provider for FSM analysis. Supports custom endpoints.""" + +import json +from openai import OpenAI + +from ..prompt import SYSTEM_PROMPT, build_fsm_analysis_prompt + + +def analyze( + verilog_source: str, + model: str = "gpt-4", + endpoint: str | None = None, + api_key: str | None = None, +) -> dict: + """Analyze Verilog source using OpenAI-compatible API. + + Args: + verilog_source: The Verilog source code to analyze. + model: The model name (e.g., "gpt-4", "gpt-4o"). + endpoint: Custom API endpoint URL (optional). + api_key: API key (optional, falls back to OPENAI_API_KEY env var). + + Returns: + Parsed JSON dict with FSM analysis. + """ + kwargs = {} + if endpoint: + kwargs["base_url"] = endpoint + if api_key: + kwargs["api_key"] = api_key + + client = OpenAI(**kwargs) + + user_prompt = build_fsm_analysis_prompt(verilog_source) + + response = client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_prompt}, + ], + temperature=0.0, + response_format={"type": "json_object"}, + ) + + content = response.choices[0].message.content + if not content: + raise ValueError("Empty response from OpenAI API") + + return json.loads(content) diff --git a/tools/fsm_analyzer/pyproject.toml b/tools/fsm_analyzer/pyproject.toml new file mode 100644 index 0000000..efa0c6d --- /dev/null +++ b/tools/fsm_analyzer/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "fsm-analyzer" +version = "0.1.0" +description = "LLM-based FSM state machine analyzer for Verilog RTL" +requires-python = ">=3.10" +dependencies = [ + "openai", + "google-genai", + "python-dotenv", +] + +[project.scripts] +fsm-analyzer = "fsm_analyzer.main:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/tools/fsm_analyzer/uv.lock b/tools/fsm_analyzer/uv.lock new file mode 100644 index 0000000..2d4139c --- /dev/null +++ b/tools/fsm_analyzer/uv.lock @@ -0,0 +1,833 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, + { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, + { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, + { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, + { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, + { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, + { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, + { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, + { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, + { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, + { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fsm-analyzer" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "google-genai" }, + { name = "openai" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "google-genai" }, + { name = "openai" }, + { name = "python-dotenv" }, +] + +[[package]] +name = "google-auth" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.62.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/4c/71b32b5c8db420cf2fd0d5ef8a672adbde97d85e5d44a0b4fca712264ef1/google_genai-1.62.0.tar.gz", hash = "sha256:709468a14c739a080bc240a4f3191df597bf64485b1ca3728e0fb67517774c18", size = 490888, upload-time = "2026-02-04T22:48:41.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/5f/4645d8a28c6e431d0dd6011003a852563f3da7037d36af53154925b099fd/google_genai-1.62.0-py3-none-any.whl", hash = "sha256:4c3daeff3d05fafee4b9a1a31f9c07f01bc22051081aa58b4d61f58d16d1bcc0", size = 724166, upload-time = "2026-02-04T22:48:39.956Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "openai" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/a2/677f22c4b487effb8a09439fb6134034b5f0a39ca27df8b95fac23a93720/openai-2.17.0.tar.gz", hash = "sha256:47224b74bd20f30c6b0a6a329505243cb2f26d5cf84d9f8d0825ff8b35e9c999", size = 631445, upload-time = "2026-02-05T16:27:40.953Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/4a/c3357c8742f361785e3702bb4c9c68c4cb37a80aa657640b820669be5af1/tenacity-9.1.3.tar.gz", hash = "sha256:a6724c947aa717087e2531f883bde5c9188f603f6669a9b8d54eb998e604c12a", size = 49002, upload-time = "2026-02-05T06:33:12.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6b/cdc85edb15e384d8e934aad89638cc8646e118c80de94c60125d0fc0a185/tenacity-9.1.3-py3-none-any.whl", hash = "sha256:51171cfc6b8a7826551e2f029426b10a6af189c5ac6986adcd7eb36d42f17954", size = 28858, upload-time = "2026-02-05T06:33:11.219Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +]