Sai Kumar Taraka commited on
Commit
4344b33
·
1 Parent(s): 9e213dd

Initial commit: UVM testbench generator with coverage-driven auto-training

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +17 -0
  2. .github/workflows/ci.yml +153 -0
  3. .github/workflows/release.yml +25 -0
  4. .gitignore +27 -0
  5. .pre-commit-config.yaml +38 -0
  6. Dockerfile +40 -0
  7. Makefile +32 -0
  8. README.md +31 -1
  9. configs/base_config.yaml +26 -0
  10. configs/logging.yaml +25 -0
  11. configs/schema/master_schema.json +219 -0
  12. configs/uart16550-1.5.core +190 -0
  13. configs/uart_demo.yaml +31 -0
  14. docker-compose.yml +23 -0
  15. frontend/package-lock.json +0 -0
  16. frontend/package.json +38 -0
  17. frontend/postcss.config.js +6 -0
  18. frontend/public/favicon.svg +5 -0
  19. frontend/public/index.html +14 -0
  20. frontend/src/App.js +49 -0
  21. frontend/src/components/ErrorBoundary.js +87 -0
  22. frontend/src/components/Footer.js +25 -0
  23. frontend/src/components/Header.js +40 -0
  24. frontend/src/components/PipelineRunner.js +461 -0
  25. frontend/src/components/PreviewPanel.js +246 -0
  26. frontend/src/components/YAMLForm.js +516 -0
  27. frontend/src/hooks/usePipeline.js +778 -0
  28. frontend/src/index.css +58 -0
  29. frontend/src/index.js +11 -0
  30. frontend/src/utils/yamlUtils.js +85 -0
  31. frontend/tailwind.config.js +28 -0
  32. protocols/apb.yaml +51 -0
  33. protocols/axi4lite.yaml +64 -0
  34. protocols/i2c.yaml +61 -0
  35. protocols/spi.yaml +57 -0
  36. protocols/uart.yaml +119 -0
  37. protocols/wishbone.yaml +48 -0
  38. pyproject.toml +61 -0
  39. render.yaml +30 -0
  40. requirements-dev.txt +2 -0
  41. requirements.txt +6 -0
  42. setup.cfg +25 -0
  43. src/__init__.py +0 -0
  44. src/api/__init__.py +0 -0
  45. src/api/server.py +260 -0
  46. src/config.py +175 -0
  47. src/data/__init__.py +0 -0
  48. src/data/collector.py +40 -0
  49. src/data/core_parser.py +117 -0
  50. src/data/preprocessor.py +81 -0
.env.example ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Production environment variables for UVM TB Generator
2
+ # Copy to .env and fill in secrets
3
+
4
+ # Backend
5
+ UVMGEN_LOG_LEVEL=info
6
+ UVMGEN_OUTPUT_DIR=/app/output
7
+ UVMGEN_SIMULATOR=stub
8
+
9
+ # Frontend
10
+ REACT_APP_API_URL=http://localhost:8000
11
+
12
+ # Security (change in production)
13
+ UVMGEN_SECRET_KEY=change-me-in-production
14
+ UVMGEN_CORS_ORIGINS=http://localhost:3000,http://localhost
15
+
16
+ # MLflow (optional)
17
+ # UVMGEN_MLFLOW_TRACKING_URI=http://mlflow:5000
.github/workflows/ci.yml ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI — UVM TB Generator
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ paths-ignore: ["docs/**", "README.md"]
7
+ pull_request:
8
+ branches: [main]
9
+ workflow_dispatch:
10
+
11
+ env:
12
+ PYTHON_VERSION: "3.11"
13
+
14
+ jobs:
15
+ lint:
16
+ name: Lint & Format Check
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ env.PYTHON_VERSION }}
23
+ - name: Install dependencies
24
+ run: |
25
+ pip install flake8 black yamllint
26
+ pip install -r requirements.txt
27
+ - name: Run flake8
28
+ run: flake8 src/ tests/ --max-line-length=120 --extend-ignore=E203
29
+ - name: Run yamllint on configs and protocols
30
+ run: yamllint configs/ protocols/
31
+
32
+ test:
33
+ name: Tests (Python ${{ matrix.python-version }})
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ matrix:
37
+ python-version: ["3.10", "3.11", "3.12"]
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: actions/setup-python@v5
41
+ with:
42
+ python-version: ${{ matrix.python-version }}
43
+ - name: Install dependencies
44
+ run: |
45
+ pip install pytest pytest-cov
46
+ pip install -r requirements.txt
47
+ - name: Run tests with coverage
48
+ run: |
49
+ python -m pytest tests/ -v --cov=src --cov-report=xml --cov-report=term-missing
50
+ - name: Upload coverage
51
+ uses: codecov/codecov-action@v5
52
+ with:
53
+ file: ./coverage.xml
54
+ fail_ci_if_error: false
55
+
56
+ generate:
57
+ name: Generation Smoke Test
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: actions/setup-python@v5
62
+ with:
63
+ python-version: ${{ env.PYTHON_VERSION }}
64
+ - name: Install dependencies
65
+ run: |
66
+ pip install -r requirements.txt
67
+ - name: Generate from YAML spec
68
+ run: |
69
+ python -m src.main --spec configs/uart_demo.yaml --json
70
+ - name: Generate from .core file
71
+ run: |
72
+ python -m src.main --spec configs/uart16550-1.5.core --json
73
+ - name: Verify output files exist
74
+ run: |
75
+ test -d output/uart_tb
76
+ test -f output/uart_tb/testbench.sv
77
+ test -f output/uart_tb/interface_uart.sv
78
+ test -d output/uart16550_tb
79
+ test -f output/uart16550_tb/testbench.sv
80
+ echo "All output files verified"
81
+ - name: Verify API server imports
82
+ run: |
83
+ python -c "from src.api.server import app; print('API server OK')"
84
+
85
+ regression:
86
+ name: Multi-Seed Regression Test
87
+ runs-on: ubuntu-latest
88
+ needs: [generate]
89
+ steps:
90
+ - uses: actions/checkout@v4
91
+ - uses: actions/setup-python@v5
92
+ with:
93
+ python-version: ${{ env.PYTHON_VERSION }}
94
+ - name: Install dependencies
95
+ run: |
96
+ pip install -r requirements.txt
97
+ - name: Run auto-training with 3 seeds
98
+ run: |
99
+ python -m src.main --spec configs/uart16550-1.5.core --auto-train \
100
+ --max-iterations 3 --coverage-target 75 --simulator stub
101
+ - name: Export coverage trend
102
+ run: |
103
+ python -c "
104
+ import json
105
+ from pathlib import Path
106
+ trend_file = next(Path('output').rglob('*coverage_trend*'), None)
107
+ if trend_file:
108
+ print('Coverage trend:')
109
+ print(trend_file.read_text())
110
+ else:
111
+ # Show registry content
112
+ reg_dir = Path('output') / 'model_registry'
113
+ if reg_dir.exists():
114
+ versions = sorted(reg_dir.iterdir())
115
+ print(f'Versions: {[v.name for v in versions]}')
116
+ " || true
117
+ - name: Verify FuseSoC .core file generated
118
+ run: |
119
+ test -f output/uart16550_tb/uart16550.core && echo "FuseSoC .core found" || echo "No FuseSoC core"
120
+
121
+ schema:
122
+ name: Schema Validation
123
+ runs-on: ubuntu-latest
124
+ steps:
125
+ - uses: actions/checkout@v4
126
+ - uses: actions/setup-python@v5
127
+ with:
128
+ python-version: ${{ env.PYTHON_VERSION }}
129
+ - name: Install deps
130
+ run: pip install pyyaml jsonschema
131
+ - name: Validate demo specs against schema
132
+ run: |
133
+ python -c "
134
+ import json, yaml, sys
135
+ from jsonschema import validate
136
+
137
+ with open('configs/schema/master_schema.json') as f:
138
+ schema = json.load(f)
139
+
140
+ for spec_file in ['configs/uart_demo.yaml', 'configs/uart16550-1.5.core']:
141
+ with open(spec_file) as f:
142
+ if spec_file.endswith('.core'):
143
+ # .core files wrap spec in a different format; validate the relevant parts
144
+ data = yaml.safe_load(f)
145
+ else:
146
+ data = yaml.safe_load(f)
147
+ try:
148
+ validate(instance=data, schema=schema)
149
+ print(f'OK: {spec_file} passes schema')
150
+ except Exception as e:
151
+ print(f'FAIL: {spec_file} — {e}')
152
+ sys.exit(1)
153
+ "
.github/workflows/release.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ release:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.11"
17
+ - name: Build package
18
+ run: |
19
+ pip install build
20
+ python -m build
21
+ - name: Create GitHub Release
22
+ uses: softprops/action-gh-release@v2
23
+ with:
24
+ files: dist/*
25
+ generate_release_notes: true
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ .coverage
6
+ htmlcov/
7
+ .env
8
+ .env.local
9
+ output/
10
+ logs/
11
+ model_registry/
12
+ models/saved/
13
+ !frontend/build/.gitkeep
14
+ *.egg-info/
15
+ dist/
16
+ build/
17
+ .DS_Store
18
+ *.log
19
+ *.pid
20
+ *.sock
21
+ node_modules/
22
+ frontend/build/
23
+ .mypy_cache/
24
+ .ruff_cache/
25
+ .venv/
26
+ venv/
27
+
.pre-commit-config.yaml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-json
9
+ - id: check-added-large-files
10
+ - id: detect-private-key
11
+ - id: check-merge-conflict
12
+
13
+ - repo: https://github.com/psf/black
14
+ rev: 25.1.0
15
+ hooks:
16
+ - id: black
17
+ language_version: python3.11
18
+
19
+ - repo: https://github.com/PyCQA/flake8
20
+ rev: 7.1.2
21
+ hooks:
22
+ - id: flake8
23
+ args: [--max-line-length=120, --extend-ignore=E203,W503]
24
+ additional_dependencies: [flake8]
25
+
26
+ - repo: https://github.com/pre-commit/mirrors-mypy
27
+ rev: v1.15.0
28
+ hooks:
29
+ - id: mypy
30
+ args: [--ignore-missing-imports, --follow-imports=silent]
31
+ additional_dependencies: [pydantic]
32
+
33
+ - repo: https://github.com/PyCQA/bandit
34
+ rev: 1.8.3
35
+ hooks:
36
+ - id: bandit
37
+ args: [--skip=B101, --recursive, src/]
38
+ additional_dependencies: [bandit]
Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage: frontend build + Python backend
2
+
3
+ # Stage 1: Build React frontend
4
+ FROM node:20-alpine AS frontend-builder
5
+ WORKDIR /app
6
+ COPY frontend/package.json frontend/package-lock.json ./
7
+ RUN npm ci --omit=dev
8
+ COPY frontend/ .
9
+ RUN npm run build
10
+
11
+ # Stage 2: Python backend
12
+ FROM python:3.11-slim AS backend-builder
13
+
14
+ RUN apt-get update && apt-get install -y --no-install-recommends \
15
+ iverilog \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ WORKDIR /app
19
+ COPY requirements.txt .
20
+ RUN pip install --no-cache-dir --upgrade pip && \
21
+ pip install --no-cache-dir -r requirements.txt
22
+
23
+ COPY . .
24
+ COPY --from=frontend-builder /app/build /app/frontend/build
25
+
26
+ RUN pip install --no-cache-dir -e .
27
+
28
+ RUN groupadd -r uvmgen && useradd -r -g uvmgen -d /app -s /sbin/nologin uvmgen
29
+ RUN mkdir -p /app/output /app/logs /var/data && chown -R uvmgen:uvmgen /app /var/data
30
+
31
+ EXPOSE 8000
32
+
33
+ ENV UVMGEN_OUTPUT_DIR=/var/data/uvmgen_output
34
+
35
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
36
+ CMD python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/api/health').read().decode())"
37
+
38
+ USER uvmgen
39
+
40
+ CMD ["uvicorn", "src.api.server:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1", "--log-level", "info"]
Makefile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: install test clean run
2
+
3
+ install:
4
+ pip install -r requirements.txt
5
+
6
+ install-dev:
7
+ pip install -r requirements.txt -r requirements-dev.txt
8
+
9
+ test:
10
+ python -m pytest tests/ -v --cov=src --cov-report=term-missing
11
+
12
+ run:
13
+ python -m src.main --spec configs/uart_demo.yaml
14
+
15
+ run-json:
16
+ python -m src.main --spec configs/uart_demo.yaml --json
17
+
18
+ eval-only:
19
+ python -m src.main --spec configs/uart_demo.yaml --eval-only
20
+
21
+ clean:
22
+ rm -rf output/* logs/* .pytest_cache __pycache__ */__pycache__ */*/__pycache__
23
+ rm -rf models/saved/*.json
24
+
25
+ docker-build:
26
+ docker build -t uvm-tb-generator .
27
+
28
+ docker-run:
29
+ docker run --rm -v $(PWD)/output:/app/output uvm-tb-generator --spec configs/uart_demo.yaml
30
+
31
+ lint:
32
+ python -m flake8 src/ tests/
README.md CHANGED
@@ -1 +1,31 @@
1
- # UVM-verification
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UVM-verification
2
+
3
+ Automated UVM testbench generation from YAML / `.core` specifications — with **protocol libraries**, **schema validation**, **coverage-driven auto-training**, and **CI/CD integration**.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pip install -r requirements.txt
9
+
10
+ python -m src.main --spec configs/uart_demo.yaml
11
+ python -m src.main --spec configs/uart16550-1.5.core
12
+ python -m src.main --spec configs/uart16550-1.5.core --auto-train --max-iterations 3
13
+ ```
14
+
15
+ ## API Server
16
+
17
+ ```bash
18
+ uvicorn src.api.server:app --host 0.0.0.0 --port 8000
19
+ ```
20
+
21
+ ## Frontend
22
+
23
+ ```bash
24
+ cd frontend && npm install && npm run build
25
+ ```
26
+
27
+ ## Docker
28
+
29
+ ```bash
30
+ docker-compose up --build
31
+ ```
configs/base_config.yaml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base pipeline configuration — inherit + override per environment
2
+
3
+ generation:
4
+ templates_dir: src/generation/templates
5
+ output_dir: output
6
+ overwrite: false
7
+ strict_validation: true
8
+
9
+ evaluation:
10
+ enabled: true
11
+ metrics:
12
+ - completeness
13
+ - syntax_validity
14
+ - coverage_readiness
15
+ threshold: 0.7
16
+
17
+ tracking:
18
+ enabled: false
19
+ backend: local
20
+ experiment_name: uvm_tb_generator
21
+ tracking_uri: null
22
+
23
+ logging:
24
+ level: INFO
25
+ file: null
26
+ format: "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
configs/logging.yaml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 1
2
+ formatters:
3
+ standard:
4
+ format: "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
5
+ handlers:
6
+ console:
7
+ class: logging.StreamHandler
8
+ level: INFO
9
+ formatter: standard
10
+ stream: ext://sys.stdout
11
+ file:
12
+ class: logging.handlers.RotatingFileHandler
13
+ level: DEBUG
14
+ formatter: standard
15
+ filename: logs/generator.log
16
+ maxBytes: 10485760
17
+ backupCount: 5
18
+ loggers:
19
+ uvmgen:
20
+ level: DEBUG
21
+ handlers: [console, file]
22
+ propagate: no
23
+ root:
24
+ level: INFO
25
+ handlers: [console]
configs/schema/master_schema.json ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/your-org/uvm-tb-generator/schema/master_schema.json",
4
+ "title": "UVM TB Generator — Master Specification Schema",
5
+ "description": "Industry-grade schema for describing digital IP cores for automated UVM testbench generation. Supports multi-protocol, multi-clock, multi-power-domain designs.",
6
+ "type": "object",
7
+ "required": ["design_name", "interfaces"],
8
+
9
+ "definitions": {
10
+ "signal": {
11
+ "type": "object",
12
+ "required": ["name", "direction"],
13
+ "properties": {
14
+ "name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", "description": "Signal name" },
15
+ "direction": { "type": "string", "enum": ["input", "output", "inout"] },
16
+ "width": { "type": "integer", "minimum": 1, "default": 1, "description": "Bus width in bits" },
17
+ "description":{ "type": "string" }
18
+ }
19
+ },
20
+
21
+ "field": {
22
+ "type": "object",
23
+ "required": ["name", "bits"],
24
+ "properties": {
25
+ "name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
26
+ "bits": { "type": "string", "pattern": "^([0-9]+|[0-9]+:[0-9]+)$", "description": "Single bit (e.g. '0') or range (e.g. '7:0')" },
27
+ "description": { "type": "string" },
28
+ "access": { "type": "string", "enum": ["ro", "wo", "rw", "rc", "rs", "w1c"], "default": "rw" },
29
+ "reset": { "type": "string", "description": "Reset value in hex (e.g. '0x00')" },
30
+ "hw_impl": { "type": "boolean", "default": true, "description": "Hardware-implemented field" },
31
+ "sw_access": { "type": "string", "enum": ["read-only", "write-only", "read-write", "read-clear"], "default": "read-write" }
32
+ }
33
+ },
34
+
35
+ "register": {
36
+ "type": "object",
37
+ "required": ["name", "address"],
38
+ "properties": {
39
+ "name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
40
+ "address": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$", "description": "Hex address" },
41
+ "size": { "type": "integer", "minimum": 1, "default": 1, "description": "Number of bytes" },
42
+ "access": { "type": "string", "enum": ["ro", "wo", "rw", "rc", "rs", "w1c"], "default": "rw" },
43
+ "description": { "type": "string" },
44
+ "fields": { "type": "array", "items": { "$ref": "#/definitions/field" } },
45
+ "reset_value": { "type": "string", "description": "Full register reset value (hex)" },
46
+ "volatile": { "type": "boolean", "default": false }
47
+ }
48
+ },
49
+
50
+ "clock_domain": {
51
+ "type": "object",
52
+ "required": ["name"],
53
+ "properties": {
54
+ "name": { "type": "string" },
55
+ "frequency":{ "type": "integer", "description": "Frequency in Hz" },
56
+ "primary": { "type": "boolean", "default": false }
57
+ }
58
+ },
59
+
60
+ "reset_domain": {
61
+ "type": "object",
62
+ "required": ["name", "signal", "active"],
63
+ "properties": {
64
+ "name": { "type": "string" },
65
+ "signal": { "type": "string" },
66
+ "active": { "type": "integer", "enum": [0, 1], "description": "0 = active-low, 1 = active-high" },
67
+ "async": { "type": "boolean", "default": false }
68
+ }
69
+ },
70
+
71
+ "interface": {
72
+ "type": "object",
73
+ "required": ["name", "signals"],
74
+ "properties": {
75
+ "name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
76
+ "protocol": { "type": "string", "enum": ["uart", "spi", "i2c", "axi", "axi4", "axi4lite", "apb", "ahb", "wishbone", "custom"], "description": "Bus protocol standard" },
77
+ "direction": { "type": "string", "enum": ["master", "slave", "monitor"], "default": "slave" },
78
+ "description": { "type": "string" },
79
+ "signals": { "type": "array", "items": { "$ref": "#/definitions/signal" }, "minItems": 1 }
80
+ }
81
+ },
82
+
83
+ "interrupt": {
84
+ "type": "object",
85
+ "required": ["name", "signal"],
86
+ "properties": {
87
+ "name": { "type": "string" },
88
+ "signal": { "type": "string" },
89
+ "type": { "type": "string", "enum": ["level", "edge", "pulse"], "default": "level" },
90
+ "sensitivity": { "type": "string", "enum": ["high", "low", "rising", "falling"], "default": "high" },
91
+ "description": { "type": "string" }
92
+ }
93
+ },
94
+
95
+ "memory_map": {
96
+ "type": "object",
97
+ "required": ["name", "base_address", "size"],
98
+ "properties": {
99
+ "name": { "type": "string" },
100
+ "base_address": { "type": "string", "pattern": "^0x[0-9a-fA-F]+$" },
101
+ "size": { "type": "integer", "description": "Size in bytes" },
102
+ "description": { "type": "string" }
103
+ }
104
+ },
105
+
106
+ "coverage": {
107
+ "type": "object",
108
+ "properties": {
109
+ "bins": { "type": "array", "items": { "type": "object", "properties": {
110
+ "name": { "type": "string" },
111
+ "type": { "type": "string", "enum": ["coverpoint", "cross", "assertion"] },
112
+ "expression": { "type": "string" },
113
+ "description": { "type": "string" }
114
+ }}}
115
+ }
116
+ },
117
+
118
+ "parameter": {
119
+ "type": "object",
120
+ "required": ["name", "type"],
121
+ "properties": {
122
+ "name": { "type": "string" },
123
+ "type": { "type": "string", "enum": ["int", "string", "bool", "float"], "default": "int" },
124
+ "default": { "type": ["integer", "string", "boolean", "number"], "description": "Default value" },
125
+ "minimum": { "type": ["integer", "number"] },
126
+ "maximum": { "type": ["integer", "number"] },
127
+ "description": { "type": "string" }
128
+ }
129
+ },
130
+
131
+ "assertion": {
132
+ "type": "object",
133
+ "required": ["name", "expression"],
134
+ "properties": {
135
+ "name": { "type": "string" },
136
+ "expression": { "type": "string", "description": "SVA assertion expression" },
137
+ "severity": { "type": "string", "enum": ["error", "warning", "info"], "default": "error" },
138
+ "description": { "type": "string" }
139
+ }
140
+ }
141
+ },
142
+
143
+ "properties": {
144
+ "design_name": { "type": "string", "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$" },
145
+ "version": { "type": "string", "description": "IP version (semver)" },
146
+ "vendor": { "type": "string", "description": "IP vendor name" },
147
+ "description": { "type": "string" },
148
+ "technology": { "type": "string", "description": "Target technology (e.g. 28nm, FPGA)" },
149
+ "language": { "type": "string", "enum": ["verilog", "vhdl", "systemverilog"], "default": "systemverilog" },
150
+
151
+ "parameters": {
152
+ "type": "object",
153
+ "additionalProperties": { "$ref": "#/definitions/parameter" },
154
+ "description": "Design parameters / generics"
155
+ },
156
+
157
+ "clock_reset": {
158
+ "type": "object",
159
+ "required": ["clock", "reset"],
160
+ "properties": {
161
+ "clock": { "type": "string" },
162
+ "reset": { "type": "string" },
163
+ "reset_active": { "type": "integer", "enum": [0, 1], "default": 0 },
164
+ "clock_freq": { "type": "integer", "description": "Clock frequency in Hz" }
165
+ }
166
+ },
167
+
168
+ "clock_domains": {
169
+ "type": "array",
170
+ "items": { "$ref": "#/definitions/clock_domain" },
171
+ "description": "Multi-clock domain definitions"
172
+ },
173
+
174
+ "reset_domains": {
175
+ "type": "array",
176
+ "items": { "$ref": "#/definitions/reset_domain" },
177
+ "description": "Multi-reset domain definitions"
178
+ },
179
+
180
+ "interfaces": {
181
+ "type": "array",
182
+ "items": { "$ref": "#/definitions/interface" },
183
+ "minItems": 1
184
+ },
185
+
186
+ "registers": {
187
+ "type": "array",
188
+ "items": { "$ref": "#/definitions/register" }
189
+ },
190
+
191
+ "interrupts": {
192
+ "type": "array",
193
+ "items": { "$ref": "#/definitions/interrupt" }
194
+ },
195
+
196
+ "memory_maps": {
197
+ "type": "array",
198
+ "items": { "$ref": "#/definitions/memory_map" }
199
+ },
200
+
201
+ "coverage": { "$ref": "#/definitions/coverage" },
202
+
203
+ "assertions": {
204
+ "type": "array",
205
+ "items": { "$ref": "#/definitions/assertion" }
206
+ },
207
+
208
+ "validation_rules": {
209
+ "type": "object",
210
+ "properties": {
211
+ "min_registers": { "type": "boolean", "default": false },
212
+ "require_reset": { "type": "boolean", "default": true },
213
+ "require_interrupt": { "type": "boolean", "default": false },
214
+ "address_alignment": { "type": "integer", "default": 1, "description": "Required address alignment in bytes" },
215
+ "no_reserved_holes": { "type": "boolean", "default": false, "description": "No gaps in register address map" }
216
+ }
217
+ }
218
+ }
219
+ }
configs/uart16550-1.5.core ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UART 16550 v1.5 Core Description
2
+ # FuseSoC-compatible .core format
3
+ # Ref: https://github.com/fusesoc/fusesoc
4
+
5
+ name: uart16550
6
+ version: "1.5"
7
+ description: Universal Asynchronous Receiver-Transmitter (UART) 16550-compatible core
8
+ author: DV Automation Team
9
+
10
+ parameters:
11
+ clk_freq:
12
+ datatype: int
13
+ default: 50000000
14
+ description: System clock frequency in Hz
15
+ baud_rate:
16
+ datatype: int
17
+ default: 115200
18
+ description: Target baud rate
19
+ data_bits:
20
+ datatype: int
21
+ default: 8
22
+ paramtype: vlogparam
23
+ fifo_depth:
24
+ datatype: int
25
+ default: 16
26
+ description: Depth of TX/RX FIFOs
27
+
28
+ interfaces:
29
+ - name: bus
30
+ type: wb
31
+ description: Wishbone bus interface
32
+ signals:
33
+ - {name: clk, direction: input, width: 1}
34
+ - {name: rstn, direction: input, width: 1}
35
+ - {name: addr, direction: input, width: 3}
36
+ - {name: we, direction: input, width: 1}
37
+ - {name: cs, direction: input, width: 1}
38
+ - {name: data_in, direction: input, width: 8}
39
+ - {name: data_out, direction: output, width: 8}
40
+ - {name: irq, direction: output, width: 1}
41
+ - name: serial
42
+ type: uart
43
+ description: Serial interface
44
+ signals:
45
+ - {name: srx, direction: input, width: 1}
46
+ - {name: stx, direction: output, width: 1}
47
+ - name: modem
48
+ type: modem
49
+ description: Modem control interface
50
+ signals:
51
+ - {name: cts_n, direction: input, width: 1}
52
+ - {name: rts_n, direction: output, width: 1}
53
+ - {name: dsr_n, direction: input, width: 1}
54
+ - {name: dtr_n, direction: output, width: 1}
55
+ - {name: ri_n, direction: input, width: 1}
56
+ - {name: dcd_n, direction: input, width: 1}
57
+
58
+ clock_reset:
59
+ clock: clk
60
+ reset: rstn
61
+ reset_active: 0
62
+
63
+ registers:
64
+ - name: RBR
65
+ description: Receiver Buffer Register (read-only, DLAB=0)
66
+ address: '0x00'
67
+ access: ro
68
+ fields:
69
+ - {name: rbr_data, bits: '7:0', description: Received data}
70
+ - name: THR
71
+ description: Transmitter Holding Register (write-only, DLAB=0)
72
+ address: '0x00'
73
+ access: wo
74
+ fields:
75
+ - {name: thr_data, bits: '7:0', description: Data to transmit}
76
+ - name: IER
77
+ description: Interrupt Enable Register
78
+ address: '0x01'
79
+ access: rw
80
+ fields:
81
+ - {name: erbfi, bits: '0', description: Enable RX Data Available Interrupt}
82
+ - {name: etbei, bits: '1', description: Enable TX Holding Register Empty Interrupt}
83
+ - {name: elsi, bits: '2', description: Enable RX Line Status Interrupt}
84
+ - {name: edssi, bits: '3', description: Enable Modem Status Interrupt}
85
+ - name: IIR
86
+ description: Interrupt Identification Register (read-only)
87
+ address: '0x02'
88
+ access: ro
89
+ fields:
90
+ - {name: int_id, bits: '3:0', description: Interrupt identification}
91
+ - {name: fifos_en, bits: '7:6', description: FIFO enable status}
92
+ - name: FCR
93
+ description: FIFO Control Register (write-only)
94
+ address: '0x02'
95
+ access: wo
96
+ fields:
97
+ - {name: fifo_en, bits: '0', description: Enable FIFOs}
98
+ - {name: rclr, bits: '1', description: Clear RX FIFO}
99
+ - {name: tclr, bits: '2', description: Clear TX FIFO}
100
+ - {name: dma_mode, bits: '3', description: DMA mode select}
101
+ - {name: rx_trigger, bits: '7:6', description: RX FIFO trigger level}
102
+ - name: LCR
103
+ description: Line Control Register
104
+ address: '0x03'
105
+ access: rw
106
+ fields:
107
+ - {name: wls, bits: '1:0', description: Word length select}
108
+ - {name: stb, bits: '2', description: Stop bits}
109
+ - {name: pen, bits: '3', description: Parity enable}
110
+ - {name: eps, bits: '4', description: Even parity select}
111
+ - {name: sp, bits: '5', description: Stick parity}
112
+ - {name: bc, bits: '6', description: Break control}
113
+ - {name: dlab, bits: '7', description: Divisor latch access bit}
114
+ - name: MCR
115
+ description: Modem Control Register
116
+ address: '0x04'
117
+ access: rw
118
+ fields:
119
+ - {name: dtr, bits: '0', description: Data Terminal Ready}
120
+ - {name: rts, bits: '1', description: Request To Send}
121
+ - {name: out1, bits: '2', description: Output 1}
122
+ - {name: out2, bits: '3', description: Output 2}
123
+ - {name: loop, bits: '4', description: Loopback mode}
124
+ - name: LSR
125
+ description: Line Status Register (read-only)
126
+ address: '0x05'
127
+ access: ro
128
+ fields:
129
+ - {name: dr, bits: '0', description: Data Ready}
130
+ - {name: oe, bits: '1', description: Overrun Error}
131
+ - {name: pe, bits: '2', description: Parity Error}
132
+ - {name: fe, bits: '3', description: Framing Error}
133
+ - {name: bi, bits: '4', description: Break Interrupt}
134
+ - {name: thre, bits: '5', description: TX Holding Register Empty}
135
+ - {name: temt, bits: '6', description: TX Empty}
136
+ - {name: err, bits: '7', description: Error in RX FIFO}
137
+ - name: MSR
138
+ description: Modem Status Register (read-only)
139
+ address: '0x06'
140
+ access: ro
141
+ fields:
142
+ - {name: dcts, bits: '0', description: Delta Clear To Send}
143
+ - {name: ddsr, bits: '1', description: Delta Data Set Ready}
144
+ - {name: teri, bits: '2', description: Trailing Edge RI}
145
+ - {name: ddcd, bits: '3', description: Delta Data Carrier Detect}
146
+ - {name: cts, bits: '4', description: Clear To Send}
147
+ - {name: dsr, bits: '5', description: Data Set Ready}
148
+ - {name: ri, bits: '6', description: Ring Indicator}
149
+ - {name: dcd, bits: '7', description: Data Carrier Detect}
150
+ - name: SCR
151
+ description: Scratch Register
152
+ address: '0x07'
153
+ access: rw
154
+ fields:
155
+ - {name: scratch, bits: '7:0', description: Scratch value}
156
+ - name: DLL
157
+ description: Divisor Latch LSB (DLAB=1)
158
+ address: '0x00'
159
+ access: rw
160
+ fields:
161
+ - {name: dll, bits: '7:0', description: Divisor latch low byte}
162
+ - name: DLH
163
+ description: Divisor Latch MSB (DLAB=1)
164
+ address: '0x01'
165
+ access: rw
166
+ fields:
167
+ - {name: dlh, bits: '7:0', description: Divisor latch high byte}
168
+
169
+ filesets:
170
+ - name: rtl
171
+ files:
172
+ - uart16550.v
173
+ - uart_baud.v
174
+ - uart_fifo.v
175
+ file_type: verilogSource
176
+
177
+ targets:
178
+ default:
179
+ filesets: [rtl]
180
+ parameters:
181
+ - clk_freq
182
+ - baud_rate
183
+ sim:
184
+ filesets: [rtl]
185
+ parameters:
186
+ - clk_freq
187
+ - baud_rate
188
+ tools:
189
+ - verilator
190
+ - iverilog
configs/uart_demo.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ design_name: uart
2
+ clock_reset:
3
+ clock: clk
4
+ reset: rst_n
5
+ reset_active: 0
6
+
7
+ interfaces:
8
+ - name: uart_if
9
+ signals:
10
+ - name: tx
11
+ direction: output
12
+ - name: rx
13
+ direction: input
14
+ - name: baud_tick
15
+ direction: input
16
+
17
+ registers:
18
+ - name: ctrl
19
+ address: '0x00'
20
+ fields:
21
+ - name: enable
22
+ bits: '0'
23
+ - name: baud_div
24
+ bits: '7:2'
25
+ - name: status
26
+ address: '0x04'
27
+ fields:
28
+ - name: tx_full
29
+ bits: '0'
30
+ - name: rx_empty
31
+ bits: '1'
docker-compose.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ app:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ container_name: uvmgen
7
+ ports:
8
+ - "8000:8000"
9
+ environment:
10
+ - PYTHONUNBUFFERED=1
11
+ - LOG_LEVEL=info
12
+ volumes:
13
+ - uvmgen_data:/var/data
14
+ healthcheck:
15
+ test: ["CMD", "python", "-c", "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/api/health').read().decode())"]
16
+ interval: 30s
17
+ timeout: 5s
18
+ retries: 3
19
+ start_period: 15s
20
+ restart: unless-stopped
21
+
22
+ volumes:
23
+ uvmgen_data:
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "uvm-tb-generator-ui",
3
+ "version": "0.3.0",
4
+ "private": true,
5
+ "description": "Semiconductor Verification Pipeline — Frontend",
6
+ "dependencies": {
7
+ "js-yaml": "^4.1.0",
8
+ "lucide-react": "^0.468.0",
9
+ "react": "^18.3.1",
10
+ "react-dom": "^18.3.1",
11
+ "react-scripts": "^5.0.1",
12
+ "react-syntax-highlighter": "^15.6.1"
13
+ },
14
+ "devDependencies": {
15
+ "autoprefixer": "^10.4.20",
16
+ "postcss": "^8.4.49",
17
+ "tailwindcss": "^3.4.17"
18
+ },
19
+ "scripts": {
20
+ "start": "react-scripts start",
21
+ "build": "react-scripts build",
22
+ "test": "react-scripts test",
23
+ "eject": "react-scripts eject"
24
+ },
25
+ "browserslist": {
26
+ "production": [
27
+ ">0.2%",
28
+ "not dead",
29
+ "not op_mini all"
30
+ ],
31
+ "development": [
32
+ "last 1 chrome version",
33
+ "last 1 firefox version",
34
+ "last 1 safari version"
35
+ ]
36
+ },
37
+ "proxy": false
38
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
frontend/public/favicon.svg ADDED
frontend/public/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="theme-color" content="#0f172a" />
7
+ <meta name="description" content="Semiconductor Verification Pipeline — UVM Testbench Generator" />
8
+ <title>Verification Pipeline · UVM Generator</title>
9
+ </head>
10
+ <body class="bg-slate-50 text-slate-900 antialiased">
11
+ <noscript>You need to enable JavaScript to run this app.</noscript>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
frontend/src/App.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback } from "react";
2
+ import Header from "./components/Header";
3
+ import Footer from "./components/Footer";
4
+ import YAMLForm from "./components/YAMLForm";
5
+ import PreviewPanel from "./components/PreviewPanel";
6
+ import PipelineRunner from "./components/PipelineRunner";
7
+ import ErrorBoundary from "./components/ErrorBoundary";
8
+
9
+ export default function App() {
10
+ const [spec, setSpec] = useState(null);
11
+
12
+ const handleSpecChange = useCallback((parsed) => {
13
+ setSpec(parsed);
14
+ }, []);
15
+
16
+ return (
17
+ <div className="min-h-screen flex flex-col bg-slate-50">
18
+ <Header />
19
+
20
+ <main className="flex-1 max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-6 space-y-6">
21
+ {/* Intro */}
22
+ <div className="bg-gradient-to-r from-brand-600 to-brand-800 rounded-xl px-6 py-5 text-white">
23
+ <h2 className="text-lg font-bold">UVM Testbench Generator</h2>
24
+ <p className="text-sm text-brand-100 mt-1 max-w-2xl">
25
+ Upload or paste a FuseSoC-style YAML / .core specification, edit signals and registers
26
+ directly, then run the pipeline to generate a complete UVM testbench.
27
+ </p>
28
+ </div>
29
+
30
+ {/* Form + Preview side by side */}
31
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
32
+ <ErrorBoundary>
33
+ <YAMLForm onSpecChange={handleSpecChange} />
34
+ </ErrorBoundary>
35
+ <ErrorBoundary>
36
+ <PreviewPanel spec={spec} />
37
+ </ErrorBoundary>
38
+ </div>
39
+
40
+ {/* Pipeline runner */}
41
+ <ErrorBoundary>
42
+ <PipelineRunner spec={spec} />
43
+ </ErrorBoundary>
44
+ </main>
45
+
46
+ <Footer />
47
+ </div>
48
+ );
49
+ }
frontend/src/components/ErrorBoundary.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ const ERROR_STYLES = {
4
+ container: {
5
+ minHeight: "200px",
6
+ display: "flex",
7
+ alignItems: "center",
8
+ justifyContent: "center",
9
+ },
10
+ card: {
11
+ maxWidth: "480px",
12
+ padding: "24px",
13
+ background: "#fff",
14
+ borderRadius: "12px",
15
+ border: "1px solid #fecaca",
16
+ boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1)",
17
+ textAlign: "center",
18
+ },
19
+ title: {
20
+ fontSize: "16px",
21
+ fontWeight: 600,
22
+ color: "#991b1b",
23
+ marginBottom: "8px",
24
+ },
25
+ message: {
26
+ fontSize: "13px",
27
+ color: "#7f1d1d",
28
+ marginBottom: "16px",
29
+ lineHeight: 1.5,
30
+ },
31
+ button: {
32
+ padding: "8px 20px",
33
+ fontSize: "13px",
34
+ fontWeight: 500,
35
+ background: "#dc2626",
36
+ color: "#fff",
37
+ border: "none",
38
+ borderRadius: "8px",
39
+ cursor: "pointer",
40
+ },
41
+ };
42
+
43
+ export default class ErrorBoundary extends React.Component {
44
+ constructor(props) {
45
+ super(props);
46
+ this.state = { hasError: false, error: null };
47
+ }
48
+
49
+ static getDerivedStateFromError(error) {
50
+ return { hasError: true, error };
51
+ }
52
+
53
+ componentDidCatch(error, errorInfo) {
54
+ console.error("[ErrorBoundary]", error, errorInfo);
55
+ if (this.props.onError) {
56
+ this.props.onError(error, errorInfo);
57
+ }
58
+ }
59
+
60
+ handleReset = () => {
61
+ this.setState({ hasError: false, error: null });
62
+ };
63
+
64
+ render() {
65
+ if (this.state.hasError) {
66
+ if (this.props.fallback) {
67
+ return this.props.fallback;
68
+ }
69
+ return (
70
+ <div style={ERROR_STYLES.container}>
71
+ <div style={ERROR_STYLES.card}>
72
+ <div style={ERROR_STYLES.title}>Something went wrong</div>
73
+ <div style={ERROR_STYLES.message}>
74
+ {this.state.error?.message || "An unexpected error occurred."}
75
+ <br />
76
+ Try refreshing the page or resetting the component.
77
+ </div>
78
+ <button style={ERROR_STYLES.button} onClick={this.handleReset}>
79
+ Reset
80
+ </button>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+ return this.props.children;
86
+ }
87
+ }
frontend/src/components/Footer.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Cpu } from "lucide-react";
3
+
4
+ export default function Footer() {
5
+ return (
6
+ <footer className="bg-white border-t border-slate-200 mt-12">
7
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
8
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-3">
9
+ <div className="flex items-center gap-2 text-sm text-slate-500">
10
+ <Cpu size={16} className="text-brand-500" />
11
+ <span>
12
+ Semiconductor Verification Pipeline &mdash; Developed by{" "}
13
+ <span className="font-semibold text-slate-700">Sai Kumar Taraka</span>
14
+ </span>
15
+ </div>
16
+ <div className="flex items-center gap-4 text-xs text-slate-400">
17
+ <span>UVM TB Generator v0.3.0</span>
18
+ <span className="hidden sm:inline">&middot;</span>
19
+ <span className="hidden sm:inline">MIT License</span>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </footer>
24
+ );
25
+ }
frontend/src/components/Header.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Cpu, GitBranch } from "lucide-react";
3
+
4
+ export default function Header() {
5
+ return (
6
+ <header className="bg-white border-b border-slate-200 sticky top-0 z-40">
7
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
8
+ <div className="flex items-center justify-between h-16">
9
+ <div className="flex items-center gap-3">
10
+ <div className="w-9 h-9 rounded-lg bg-brand-600 flex items-center justify-center">
11
+ <Cpu size={20} className="text-white" />
12
+ </div>
13
+ <div>
14
+ <h1 className="text-base font-bold text-slate-900 leading-tight">
15
+ Verification Pipeline
16
+ </h1>
17
+ <p className="text-[11px] text-slate-500 leading-tight">
18
+ UVM Testbench Generator
19
+ </p>
20
+ </div>
21
+ </div>
22
+
23
+ <div className="flex items-center gap-4">
24
+ <a
25
+ href="#"
26
+ className="hidden sm:inline-flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-700 transition-colors"
27
+ >
28
+ <GitBranch size={14} />
29
+ v0.3.0
30
+ </a>
31
+ <span className="hidden sm:inline-flex items-center gap-1.5 text-xs text-slate-400">
32
+ <span className="w-2 h-2 rounded-full bg-emerald-400" />
33
+ API Ready
34
+ </span>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </header>
39
+ );
40
+ }
frontend/src/components/PipelineRunner.js ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useEffect, useState } from "react";
2
+ import {
3
+ Play,
4
+ Square,
5
+ FileText,
6
+ Download,
7
+ Terminal,
8
+ CheckCircle2,
9
+ XCircle,
10
+ Loader2,
11
+ AlertTriangle,
12
+ Info,
13
+ BarChart3,
14
+ GitBranch,
15
+ RefreshCw,
16
+ Target,
17
+ TrendingUp,
18
+ } from "lucide-react";
19
+ import usePipeline from "../hooks/usePipeline";
20
+ import { toYAML } from "../utils/yamlUtils";
21
+
22
+ const LOG_ICONS = {
23
+ info: Info,
24
+ success: CheckCircle2,
25
+ error: XCircle,
26
+ warn: AlertTriangle,
27
+ };
28
+
29
+ const LOG_COLORS = {
30
+ info: "text-slate-600",
31
+ success: "text-emerald-600",
32
+ error: "text-red-600",
33
+ warn: "text-amber-600",
34
+ };
35
+
36
+ function LogEntry({ entry }) {
37
+ const Icon = LOG_ICONS[entry.level] || Info;
38
+ return (
39
+ <div className={`flex items-start gap-2 ${LOG_COLORS[entry.level] || "text-slate-600"}`}>
40
+ <Icon size={14} className="shrink-0 mt-0.5" />
41
+ <span className="flex-1">{entry.message}</span>
42
+ <span className="text-[10px] text-slate-400 font-mono shrink-0">{entry.timestamp}</span>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ function CoverageBar({ pct }) {
48
+ const color =
49
+ pct >= 90 ? "bg-emerald-500" : pct >= 70 ? "bg-amber-500" : "bg-red-500";
50
+ return (
51
+ <div className="flex items-center gap-2">
52
+ <div className="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
53
+ <div
54
+ className={`h-full rounded-full transition-all duration-500 ${color}`}
55
+ style={{ width: `${Math.min(pct, 100)}%` }}
56
+ />
57
+ </div>
58
+ <span className="text-xs font-mono text-slate-600 w-10 text-right">{pct.toFixed(0)}%</span>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ function CoverageTrendChart({ trend }) {
64
+ if (!trend || trend.length < 2) return null;
65
+ const max = Math.max(...trend.map((t) => t.coverage));
66
+ const min = Math.min(...trend.map((t) => t.coverage));
67
+ const range = max - min || 1;
68
+
69
+ return (
70
+ <div className="rounded-lg border border-slate-200 bg-white p-4">
71
+ <div className="flex items-center gap-2 mb-3">
72
+ <TrendingUp size={16} className="text-brand-600" />
73
+ <span className="text-sm font-semibold text-slate-800">Coverage Trend</span>
74
+ {trend.length >= 2 && (
75
+ <span className="text-xs text-slate-400 ml-auto">
76
+ Delta: {(trend[trend.length-1].coverage - trend[0].coverage) > 0 ? '+' : ''}
77
+ {(trend[trend.length-1].coverage - trend[0].coverage).toFixed(1)}%
78
+ </span>
79
+ )}
80
+ </div>
81
+ <div className="flex items-end gap-1 h-28">
82
+ {trend.map((t, i) => {
83
+ const h = ((t.coverage - min) / range) * 100;
84
+ const color =
85
+ t.coverage >= 90 ? "bg-emerald-400" :
86
+ t.coverage >= 70 ? "bg-amber-400" : "bg-red-400";
87
+ const prevCov = i > 0 ? trend[i-1].coverage : t.coverage;
88
+ const gain = t.coverage - prevCov;
89
+ return (
90
+ <div key={i} className="flex-1 flex flex-col items-center gap-1">
91
+ <span className="text-[10px] font-mono text-slate-500">{t.coverage.toFixed(0)}%</span>
92
+ <div className="flex flex-col items-center w-full">
93
+ {i > 0 && gain !== 0 && (
94
+ <span className={`text-[8px] ${gain > 0 ? 'text-emerald-500' : 'text-red-400'}`}>
95
+ {gain > 0 ? '+' : ''}{gain.toFixed(1)}%
96
+ </span>
97
+ )}
98
+ <div
99
+ className={`w-full rounded-t transition-all duration-500 ${color}`}
100
+ style={{ height: `${Math.max(h, 5)}%`, minHeight: '8px' }}
101
+ title={`${t.version}: ${t.coverage.toFixed(1)}%`}
102
+ />
103
+ </div>
104
+ <span className="text-[9px] text-slate-400">{t.version}</span>
105
+ </div>
106
+ );
107
+ })}
108
+ </div>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ function SeedCoverageSummary({ seeds }) {
114
+ if (!seeds || seeds.length === 0) return null;
115
+ const best = Math.max(...seeds.map(s => s.pct));
116
+ const worst = Math.min(...seeds.map(s => s.pct));
117
+ return (
118
+ <div className="flex items-center gap-3 text-xs text-slate-500">
119
+ <span className="font-medium text-slate-600">Seeds:</span>
120
+ {seeds.map((s, i) => {
121
+ const color = s.pct >= 90 ? "text-emerald-600" : s.pct >= 70 ? "text-amber-600" : "text-red-600";
122
+ return <span key={i} className={`font-mono ${color}`}>s{i+1}={s.pct.toFixed(0)}%</span>;
123
+ })}
124
+ <span className="text-slate-300">|</span>
125
+ <span className="font-mono text-slate-500">Δ{best - worst > 0 ? '+' : ''}{(best - worst).toFixed(0)}%</span>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ function VersionHistory({ versions, latestGaps }) {
131
+ if (!versions || versions.length === 0) return null;
132
+ const passed = versions.filter(v => v.coverage >= 90).length;
133
+ const failed = versions.length - passed;
134
+ const bestCov = Math.max(...versions.map(v => v.coverage));
135
+ return (
136
+ <div className="rounded-lg border border-slate-200 bg-white">
137
+ <div className="flex items-center gap-2 px-4 py-3 border-b border-slate-100">
138
+ <GitBranch size={16} className="text-brand-600" />
139
+ <span className="text-sm font-semibold text-slate-800">Version History</span>
140
+ <span className="text-xs text-slate-400 ml-auto">{versions.length} versions</span>
141
+ </div>
142
+ {/* Pass/Fail summary */}
143
+ <div className="flex items-center gap-4 px-4 py-2 bg-slate-50 border-b border-slate-100">
144
+ <div className="flex items-center gap-1.5">
145
+ <CheckCircle2 size={14} className="text-emerald-500" />
146
+ <span className="text-xs font-medium text-slate-700">{passed} passed</span>
147
+ </div>
148
+ <div className="flex items-center gap-1.5">
149
+ <XCircle size={14} className="text-red-500" />
150
+ <span className="text-xs font-medium text-slate-700">{failed} failed</span>
151
+ </div>
152
+ <div className="flex items-center gap-1.5 ml-auto">
153
+ <span className="text-xs text-slate-400">Best:</span>
154
+ <span className="text-xs font-bold text-slate-700">{bestCov.toFixed(1)}%</span>
155
+ </div>
156
+ </div>
157
+ <div className="divide-y divide-slate-100 max-h-64 overflow-y-auto">
158
+ {[...versions].reverse().map((v, i) => {
159
+ const prev = i < versions.length - 1 ? versions[versions.length - 1 - (i + 1)] : null;
160
+ const delta = prev ? v.coverage - prev.coverage : null;
161
+ return (
162
+ <div key={i} className="flex items-center gap-3 px-4 py-2.5">
163
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center ${
164
+ v.coverage >= 90 ? 'bg-emerald-50' : v.coverage >= 70 ? 'bg-amber-50' : 'bg-red-50'
165
+ }`}>
166
+ <span className={`text-xs font-bold ${
167
+ v.coverage >= 90 ? 'text-emerald-700' : v.coverage >= 70 ? 'text-amber-700' : 'text-red-700'
168
+ }`}>{v.version}</span>
169
+ </div>
170
+ <div className="flex-1">
171
+ <div className="flex items-center gap-2">
172
+ <span className="text-sm text-slate-700">{v.files} files</span>
173
+ <span className="text-[11px] text-slate-300">|</span>
174
+ <span className={`text-sm font-medium ${
175
+ v.coverage >= 90 ? 'text-emerald-700' : 'text-slate-800'
176
+ }`}>{v.coverage.toFixed(1)}%</span>
177
+ {delta !== null && (
178
+ <span className={`text-[11px] font-mono ${
179
+ delta > 0 ? 'text-emerald-500' : delta < 0 ? 'text-red-400' : 'text-slate-400'
180
+ }`}>
181
+ {delta > 0 ? '+' : ''}{delta.toFixed(1)}%
182
+ </span>
183
+ )}
184
+ </div>
185
+ <CoverageBar pct={v.coverage} />
186
+ </div>
187
+ </div>
188
+ );
189
+ })}
190
+ </div>
191
+ </div>
192
+ );
193
+ }
194
+
195
+ function CoverageGapsList({ gaps }) {
196
+ if (!gaps || gaps.length === 0) return null;
197
+ return (
198
+ <div className="rounded-lg border border-amber-200 bg-amber-50">
199
+ <div className="flex items-center gap-2 px-4 py-3 border-b border-amber-100">
200
+ <Target size={16} className="text-amber-600" />
201
+ <span className="text-sm font-semibold text-amber-800">Coverage Gaps</span>
202
+ <span className="text-xs text-amber-600 ml-auto">{gaps.length} uncovered</span>
203
+ </div>
204
+ <div className="p-3 space-y-1 max-h-32 overflow-y-auto">
205
+ {gaps.map((g, i) => (
206
+ <div key={i} className="flex items-center gap-2 text-xs text-amber-700 font-mono">
207
+ <span className="w-2 h-2 rounded-full bg-amber-400 shrink-0" />
208
+ {g.bin}
209
+ </div>
210
+ ))}
211
+ </div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ export default function PipelineRunner({ spec }) {
217
+ const {
218
+ running,
219
+ autoTraining,
220
+ logs,
221
+ artifacts,
222
+ error,
223
+ versions,
224
+ coverageTrend,
225
+ coverageGaps,
226
+ seedResults,
227
+ autoTrainMax,
228
+ runPipeline,
229
+ runAutoTrain,
230
+ } = usePipeline();
231
+ const logEndRef = useRef(null);
232
+ const [maxIter, setMaxIter] = useState(5);
233
+
234
+ useEffect(() => {
235
+ logEndRef.current?.scrollIntoView({ behavior: "smooth" });
236
+ }, [logs]);
237
+
238
+ const handleRun = () => {
239
+ if (!spec) return;
240
+ const yamlText = toYAML(spec);
241
+ runPipeline(yamlText);
242
+ };
243
+
244
+ const handleAutoTrain = () => {
245
+ if (!spec) return;
246
+ const yamlText = toYAML(spec);
247
+ runAutoTrain(yamlText, maxIter);
248
+ };
249
+
250
+ const handleDownload = (file) => {
251
+ const blob = new Blob([file.content || ""], { type: "text/plain" });
252
+ const url = URL.createObjectURL(blob);
253
+ const a = document.createElement("a");
254
+ a.href = url;
255
+ a.download = file.name;
256
+ document.body.appendChild(a);
257
+ a.click();
258
+ document.body.removeChild(a);
259
+ URL.revokeObjectURL(url);
260
+ };
261
+
262
+ const handleDownloadAll = () => {
263
+ artifacts.forEach((file) => handleDownload(file));
264
+ };
265
+
266
+ const canRun = spec && Object.keys(spec).length > 0 && !running;
267
+
268
+ return (
269
+ <div className="card">
270
+ <div className="card-header">
271
+ <Terminal size={18} className="text-brand-600" />
272
+ <h2>Pipeline Runner</h2>
273
+ {autoTraining && (
274
+ <span className="badge badge-warning text-xs ml-2">
275
+ <RefreshCw size={12} className="animate-spin inline mr-1" />
276
+ Auto-Training
277
+ </span>
278
+ )}
279
+ {versions.length > 0 && !running && (
280
+ <span className="badge badge-success text-xs ml-2">v{versions.length}</span>
281
+ )}
282
+
283
+ <div className="ml-auto flex items-center gap-3">
284
+ {spec && (
285
+ <span className="text-xs text-slate-400">
286
+ Spec: <span className="font-mono text-slate-600">{spec.design_name || "unnamed"}</span>
287
+ </span>
288
+ )}
289
+ <div className="flex items-center gap-2">
290
+ <label className="text-[11px] text-slate-400">Iterations</label>
291
+ <input
292
+ type="number"
293
+ min={1}
294
+ max={20}
295
+ value={maxIter}
296
+ onChange={(e) => setMaxIter(Math.max(1, Math.min(20, parseInt(e.target.value) || 5)))}
297
+ className="w-14 px-2 py-1 text-xs border border-slate-200 rounded-md text-center"
298
+ disabled={running}
299
+ />
300
+ </div>
301
+ <button
302
+ className="btn-primary text-xs"
303
+ onClick={handleRun}
304
+ disabled={!canRun}
305
+ title="Single pass generation"
306
+ >
307
+ {running && !autoTraining ? (
308
+ <>
309
+ <Loader2 size={14} className="animate-spin" />
310
+ Running...
311
+ </>
312
+ ) : (
313
+ <>
314
+ <Play size={14} />
315
+ Run
316
+ </>
317
+ )}
318
+ </button>
319
+ <button
320
+ className={running ? "btn-secondary text-xs opacity-50" : "btn-primary text-xs bg-amber-600 hover:bg-amber-700"}
321
+ onClick={handleAutoTrain}
322
+ disabled={!canRun}
323
+ title={`Coverage-driven auto-training (${maxIter} iterations max)`}
324
+ >
325
+ {running && autoTraining ? (
326
+ <>
327
+ <Loader2 size={14} className="animate-spin" />
328
+ Training...
329
+ </>
330
+ ) : (
331
+ <>
332
+ <RefreshCw size={14} />
333
+ Auto-Train
334
+ </>
335
+ )}
336
+ </button>
337
+ </div>
338
+ </div>
339
+
340
+ <div className="card-body space-y-4">
341
+ {/* Logs */}
342
+ <div className="rounded-lg bg-slate-900 border border-slate-700">
343
+ <div className="flex items-center justify-between px-4 py-2 border-b border-slate-700">
344
+ <span className="text-[11px] font-medium text-slate-400 uppercase tracking-wider">
345
+ Pipeline Logs
346
+ </span>
347
+ <span className="text-[11px] text-slate-500">{logs.length} entries</span>
348
+ </div>
349
+ <div className="p-4 space-y-1 max-h-48 overflow-y-auto" style={{ minHeight: "80px" }}>
350
+ {logs.length === 0 && (
351
+ <p className="text-xs text-slate-500 italic">Click "Run" or "Auto-Train" to start</p>
352
+ )}
353
+ {logs.map((entry, i) => (
354
+ <LogEntry key={i} entry={entry} />
355
+ ))}
356
+ <div ref={logEndRef} />
357
+ </div>
358
+ </div>
359
+
360
+ {/* Error */}
361
+ {error && (
362
+ <div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3">
363
+ <div className="flex items-center gap-2 text-sm text-red-700">
364
+ <XCircle size={16} />
365
+ {error}
366
+ </div>
367
+ </div>
368
+ )}
369
+
370
+ {/* Coverage Trend */}
371
+ {coverageTrend && coverageTrend.length > 1 && <CoverageTrendChart trend={coverageTrend} />}
372
+
373
+ {/* Seed Coverage Summary */}
374
+ {seedResults && seedResults.length > 0 && (
375
+ <SeedCoverageSummary seeds={seedResults} />
376
+ )}
377
+
378
+ {/* Coverage Gaps */}
379
+ <CoverageGapsList gaps={coverageGaps} />
380
+
381
+ {/* Version History + Artifacts side by side */}
382
+ {versions.length > 0 && (
383
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
384
+ {/* Version History */}
385
+ <div className="lg:col-span-1">
386
+ <VersionHistory versions={versions} latestGaps={coverageGaps} />
387
+ </div>
388
+
389
+ {/* Artifacts */}
390
+ <div className="lg:col-span-2">
391
+ <div className="rounded-lg border border-slate-200 bg-white">
392
+ <div className="flex items-center justify-between px-4 py-3 border-b border-slate-100">
393
+ <div className="flex items-center gap-2">
394
+ <FileText size={16} className="text-brand-600" />
395
+ <h3 className="text-sm font-semibold text-slate-800">Generated Artifacts</h3>
396
+ </div>
397
+ <div className="flex items-center gap-2">
398
+ <span className="text-xs text-slate-400">{artifacts.length} files</span>
399
+ <button className="btn-secondary text-xs" onClick={handleDownloadAll}>
400
+ <Download size={14} />
401
+ Download All
402
+ </button>
403
+ </div>
404
+ </div>
405
+ <div className="divide-y divide-slate-100 max-h-80 overflow-y-auto">
406
+ {artifacts.map((file, i) => (
407
+ <div
408
+ key={i}
409
+ className="flex items-center gap-3 px-4 py-2.5 hover:bg-slate-50 cursor-pointer transition-colors"
410
+ onClick={() => handleDownload(file)}
411
+ >
412
+ <div className="w-8 h-8 rounded-md bg-brand-50 flex items-center justify-center shrink-0">
413
+ <FileText size={16} className="text-brand-600" />
414
+ </div>
415
+ <div className="flex-1 min-w-0">
416
+ <p className="text-sm font-medium text-slate-800 truncate">{file.name}</p>
417
+ <p className="text-[11px] text-slate-400 truncate">{file.path}</p>
418
+ </div>
419
+ <div className="flex items-center gap-2 shrink-0">
420
+ <span className="text-[11px] text-slate-400">{file.size}</span>
421
+ <button className="p-1.5 rounded-md text-slate-400 hover:text-brand-600 hover:bg-brand-50 transition-all">
422
+ <Download size={14} />
423
+ </button>
424
+ </div>
425
+ </div>
426
+ ))}
427
+ </div>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ )}
432
+
433
+ {/* Summary footer */}
434
+ {versions.length > 0 && !running && (
435
+ <div className="flex items-center justify-between text-xs text-slate-400 border-t border-slate-100 pt-3">
436
+ <div className="flex items-center gap-4">
437
+ <span className="flex items-center gap-1">
438
+ <GitBranch size={12} />
439
+ {versions.length} versions
440
+ </span>
441
+ <span className="flex items-center gap-1">
442
+ <BarChart3 size={12} />
443
+ Best: {Math.max(...versions.map((v) => v.coverage)).toFixed(1)}%
444
+ </span>
445
+ <span className="flex items-center gap-1">
446
+ <FileText size={12} />
447
+ {artifacts.length} files
448
+ </span>
449
+ </div>
450
+ {coverageGaps && coverageGaps.length > 0 && (
451
+ <span className="text-amber-600 flex items-center gap-1">
452
+ <Target size={12} />
453
+ {coverageGaps.length} gaps remaining
454
+ </span>
455
+ )}
456
+ </div>
457
+ )}
458
+ </div>
459
+ </div>
460
+ );
461
+ }
frontend/src/components/PreviewPanel.js ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo } from "react";
2
+ import { Eye, AlertCircle, CheckCircle2, Layers } from "lucide-react";
3
+ import { toYAML, validateYAML, PROTOCOLS } from "../utils/yamlUtils";
4
+
5
+ function MissingField({ label }) {
6
+ return (
7
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-red-50 text-red-600 text-[11px] font-medium">
8
+ <AlertCircle size={12} />
9
+ missing: {label}
10
+ </span>
11
+ );
12
+ }
13
+
14
+ function ProtocolBadge({ protocol }) {
15
+ const colorMap = {
16
+ uart: "bg-sky-50 text-sky-700",
17
+ spi: "bg-violet-50 text-violet-700",
18
+ i2c: "bg-amber-50 text-amber-700",
19
+ axi4lite: "bg-rose-50 text-rose-700",
20
+ apb: "bg-teal-50 text-teal-700",
21
+ wishbone: "bg-indigo-50 text-indigo-700",
22
+ };
23
+ const cls = colorMap[protocol] || "bg-slate-100 text-slate-600";
24
+ return (
25
+ <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${cls}`}>
26
+ {protocol.toUpperCase()}
27
+ </span>
28
+ );
29
+ }
30
+
31
+ function Section({ title, children, icon, count, error }) {
32
+ return (
33
+ <div className={`rounded-lg border ${error ? "border-red-200 bg-red-50/30" : "border-slate-200"} p-4 space-y-3`}>
34
+ <div className="flex items-center justify-between">
35
+ <div className="flex items-center gap-2 text-sm font-semibold text-slate-800">
36
+ {icon}
37
+ {title}
38
+ {count !== undefined && (
39
+ <span className="text-xs font-normal text-slate-400">({count})</span>
40
+ )}
41
+ </div>
42
+ {error && <MissingField label={error} />}
43
+ </div>
44
+ {children}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export default function PreviewPanel({ spec }) {
50
+ const errors = useMemo(() => (spec ? validateYAML(toYAML(spec)) : ["No spec loaded"]), [spec]);
51
+
52
+ if (!spec || Object.keys(spec).length === 0) {
53
+ return (
54
+ <div className="card">
55
+ <div className="card-header">
56
+ <Eye size={18} className="text-slate-400" />
57
+ <h2 className="text-slate-400">Preview</h2>
58
+ </div>
59
+ <div className="card-body">
60
+ <div className="flex flex-col items-center justify-center py-12 text-slate-400">
61
+ <Layers size={40} className="mb-3 opacity-40" />
62
+ <p className="text-sm">Load a spec to see the parsed preview</p>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ const hasErrors = errors.length > 0;
70
+
71
+ return (
72
+ <div className="card">
73
+ <div className="card-header">
74
+ <Eye size={18} className="text-brand-600" />
75
+ <h2>Parsed Specification</h2>
76
+ <span className={hasErrors ? "badge-error ml-auto" : "badge-success ml-auto"}>
77
+ {hasErrors ? `${errors.length} issue${errors.length > 1 ? "s" : ""}` : "Valid"}
78
+ </span>
79
+ </div>
80
+
81
+ <div className="card-body space-y-4">
82
+ {/* Errors */}
83
+ {hasErrors && (
84
+ <div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3">
85
+ <div className="text-xs text-red-700 space-y-0.5">
86
+ {errors.map((e, i) => (
87
+ <p key={i} className="flex items-center gap-1.5">
88
+ <AlertCircle size={12} className="shrink-0" />
89
+ {e}
90
+ </p>
91
+ ))}
92
+ </div>
93
+ </div>
94
+ )}
95
+
96
+ {/* Design metadata */}
97
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
98
+ <div className="bg-slate-50 rounded-lg p-3">
99
+ <p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Design</p>
100
+ <p className="text-sm font-semibold text-slate-900 mt-0.5">
101
+ {spec.design_name || <MissingField label="design_name" />}
102
+ </p>
103
+ </div>
104
+ <div className="bg-slate-50 rounded-lg p-3">
105
+ <p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Version</p>
106
+ <p className="text-sm text-slate-900 mt-0.5">{spec.version || "—"}</p>
107
+ </div>
108
+ <div className="bg-slate-50 rounded-lg p-3">
109
+ <p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Vendor</p>
110
+ <p className="text-sm text-slate-900 mt-0.5">{spec.vendor || "—"}</p>
111
+ </div>
112
+ <div className="bg-slate-50 rounded-lg p-3">
113
+ <p className="text-[11px] text-slate-500 font-medium uppercase tracking-wider">Protocol</p>
114
+ <p className="mt-0.5">
115
+ {spec.protocol ? (
116
+ <ProtocolBadge protocol={spec.protocol} />
117
+ ) : (
118
+ <span className="badge-warning">Auto</span>
119
+ )}
120
+ </p>
121
+ </div>
122
+ </div>
123
+
124
+ {/* Clock / Reset */}
125
+ <Section
126
+ title="Clock & Reset"
127
+ icon={<span className="w-2 h-2 rounded-full bg-amber-400" />}
128
+ error={!spec.clock_reset ? "clock_reset" : null}
129
+ >
130
+ <div className="grid grid-cols-3 gap-3 text-sm">
131
+ <div>
132
+ <span className="text-[11px] text-slate-500">Clock</span>
133
+ <p className="font-mono text-slate-900">{spec.clock_reset?.clock || "—"}</p>
134
+ </div>
135
+ <div>
136
+ <span className="text-[11px] text-slate-500">Reset</span>
137
+ <p className="font-mono text-slate-900">{spec.clock_reset?.reset || "—"}</p>
138
+ </div>
139
+ <div>
140
+ <span className="text-[11px] text-slate-500">Active</span>
141
+ <p className="font-mono text-slate-900">
142
+ {spec.clock_reset?.reset_active === 0 ? "Low" : spec.clock_reset?.reset_active === 1 ? "High" : "—"}
143
+ </p>
144
+ </div>
145
+ </div>
146
+ </Section>
147
+
148
+ {/* Interfaces */}
149
+ <Section
150
+ title="Interfaces"
151
+ icon={<span className="w-2 h-2 rounded-full bg-brand-500" />}
152
+ count={spec.interfaces?.length}
153
+ error={!spec.interfaces?.length ? "interfaces" : null}
154
+ >
155
+ {(spec.interfaces || []).map((iface, i) => (
156
+ <div key={i} className="border-t border-slate-100 pt-2 first:border-0 first:pt-0">
157
+ <div className="flex items-center gap-2 mb-1.5">
158
+ <span className="text-sm font-medium text-slate-800">{iface.name || `iface_${i}`}</span>
159
+ {iface.protocol && <ProtocolBadge protocol={iface.protocol} />}
160
+ <span className="text-xs text-slate-400">
161
+ {Array.isArray(iface.signals) ? iface.signals.length : 0} signals
162
+ </span>
163
+ </div>
164
+ <div className="flex flex-wrap gap-1.5">
165
+ {Array.isArray(iface.signals) && iface.signals.map((sig, j) => (
166
+ <span
167
+ key={j}
168
+ className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-mono
169
+ ${sig.direction === "output"
170
+ ? "bg-emerald-50 text-emerald-700"
171
+ : sig.direction === "inout"
172
+ ? "bg-amber-50 text-amber-700"
173
+ : "bg-sky-50 text-sky-700"
174
+ }`}
175
+ >
176
+ {sig.name}
177
+ <span className="opacity-60">
178
+ {(sig.width || 1) > 1 ? `[${sig.width - 1}:0]` : ""}
179
+ </span>
180
+ <span className="opacity-50 text-[10px]">({sig.direction[0]})</span>
181
+ </span>
182
+ ))}
183
+ </div>
184
+ </div>
185
+ ))}
186
+ </Section>
187
+
188
+ {/* Registers */}
189
+ <Section
190
+ title="Registers"
191
+ icon={<span className="w-2 h-2 rounded-full bg-violet-500" />}
192
+ count={spec.registers?.length}
193
+ >
194
+ {(spec.registers || []).length === 0 && (
195
+ <p className="text-xs text-slate-400 italic">No registers defined</p>
196
+ )}
197
+ {(spec.registers || []).map((reg, i) => (
198
+ <div key={i} className="border-t border-slate-100 pt-2 first:border-0 first:pt-0">
199
+ <div className="flex items-center gap-2 mb-1">
200
+ <span className="text-sm font-mono font-semibold text-slate-800">{reg.name}</span>
201
+ <span className="text-xs font-mono text-slate-500">@{reg.address}</span>
202
+ <span className={`badge text-[10px] ${
203
+ reg.access === "ro" ? "badge-warning" :
204
+ reg.access === "wo" ? "badge-error" :
205
+ "badge-info"
206
+ }`}>
207
+ [{reg.access || "rw"}]
208
+ </span>
209
+ {reg.description && (
210
+ <span className="text-xs text-slate-400 truncate max-w-[200px]">{reg.description}</span>
211
+ )}
212
+ </div>
213
+ {(reg.fields || []).length > 0 && (
214
+ <div className="ml-3 flex flex-wrap gap-1">
215
+ {(reg.fields || []).map((f, j) => (
216
+ <span key={j} className="inline-flex items-center gap-1 rounded bg-slate-100 px-1.5 py-0.5 text-[11px] font-mono text-slate-600">
217
+ {f.name}[{f.bits}]
218
+ </span>
219
+ ))}
220
+ </div>
221
+ )}
222
+ </div>
223
+ ))}
224
+ </Section>
225
+
226
+ {/* Parameters */}
227
+ {spec.parameters && Object.keys(spec.parameters).length > 0 && (
228
+ <Section
229
+ title="Parameters"
230
+ icon={<span className="w-2 h-2 rounded-full bg-slate-400" />}
231
+ count={Object.keys(spec.parameters).length}
232
+ >
233
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
234
+ {Object.entries(spec.parameters).map(([k, v]) => (
235
+ <div key={k} className="bg-slate-50 rounded px-2.5 py-1.5">
236
+ <p className="text-[11px] font-mono text-slate-700">{k}</p>
237
+ <p className="text-xs text-slate-500">{v ?? "—"}</p>
238
+ </div>
239
+ ))}
240
+ </div>
241
+ </Section>
242
+ )}
243
+ </div>
244
+ </div>
245
+ );
246
+ }
frontend/src/components/YAMLForm.js ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useCallback } from "react";
2
+ import {
3
+ Upload,
4
+ FileCode,
5
+ Plus,
6
+ Trash2,
7
+ AlertCircle,
8
+ CheckCircle2,
9
+ Settings2,
10
+ } from "lucide-react";
11
+ import { parseYAML, validateYAML, toYAML, PROTOCOLS, DEFAULT_YAML } from "../utils/yamlUtils";
12
+
13
+ export default function YAMLForm({ onSpecChange, validationErrors }) {
14
+ const [yamlText, setYamlText] = useState(DEFAULT_YAML);
15
+ const [parsed, setParsed] = useState(() => parseYAML(DEFAULT_YAML));
16
+ const [mode, setMode] = useState("editor"); // editor | parsed | split
17
+ const [activeTab, setActiveTab] = useState("spec"); // spec | signals | registers
18
+ const fileRef = useRef(null);
19
+
20
+ const handleChange = useCallback(
21
+ (text) => {
22
+ setYamlText(text);
23
+ try {
24
+ const obj = parseYAML(text);
25
+ setParsed(obj);
26
+ onSpecChange(obj);
27
+ } catch {
28
+ // keep previous parsed state
29
+ }
30
+ },
31
+ [onSpecChange]
32
+ );
33
+
34
+ const handleFileUpload = useCallback(
35
+ (e) => {
36
+ const file = e.target.files?.[0];
37
+ if (!file) return;
38
+ const reader = new FileReader();
39
+ reader.onload = (ev) => handleChange(ev.target.result);
40
+ reader.readAsText(file);
41
+ },
42
+ [handleChange]
43
+ );
44
+
45
+ const handlePasteExample = useCallback(() => {
46
+ handleChange(DEFAULT_YAML);
47
+ }, [handleChange]);
48
+
49
+ const errors = validateYAML(yamlText);
50
+
51
+ // ── Signal / Register editors ────────────────────────────────────
52
+
53
+ const updateInterface = (idx, field, value) => {
54
+ const ifaces = [...(parsed.interfaces || [])];
55
+ ifaces[idx] = { ...ifaces[idx], [field]: value };
56
+ const updated = { ...parsed, interfaces: ifaces };
57
+ setParsed(updated);
58
+ handleChange(toYAML(updated));
59
+ };
60
+
61
+ const addSignal = (ifaceIdx) => {
62
+ const ifaces = [...(parsed.interfaces || [])];
63
+ const existing = Array.isArray(ifaces[ifaceIdx]?.signals) ? ifaces[ifaceIdx].signals : [];
64
+ const signals = [...existing, { name: "", direction: "input", width: 1 }];
65
+ ifaces[ifaceIdx] = { ...ifaces[ifaceIdx], signals };
66
+ const updated = { ...parsed, interfaces: ifaces };
67
+ setParsed(updated);
68
+ handleChange(toYAML(updated));
69
+ };
70
+
71
+ const updateSignal = (ifaceIdx, sigIdx, field, value) => {
72
+ const ifaces = [...(parsed.interfaces || [])];
73
+ const existing = Array.isArray(ifaces[ifaceIdx]?.signals) ? ifaces[ifaceIdx].signals : [];
74
+ const signals = [...existing];
75
+ signals[sigIdx] = { ...signals[sigIdx], [field]: value };
76
+ ifaces[ifaceIdx] = { ...ifaces[ifaceIdx], signals };
77
+ const updated = { ...parsed, interfaces: ifaces };
78
+ setParsed(updated);
79
+ handleChange(toYAML(updated));
80
+ };
81
+
82
+ const removeSignal = (ifaceIdx, sigIdx) => {
83
+ const ifaces = [...(parsed.interfaces || [])];
84
+ const sigs = Array.isArray(ifaces[ifaceIdx].signals) ? ifaces[ifaceIdx].signals : [];
85
+ ifaces[ifaceIdx] = {
86
+ ...ifaces[ifaceIdx],
87
+ signals: sigs.filter((_, i) => i !== sigIdx),
88
+ };
89
+ const updated = { ...parsed, interfaces: ifaces };
90
+ setParsed(updated);
91
+ handleChange(toYAML(updated));
92
+ };
93
+
94
+ const addRegister = () => {
95
+ const regs = [...(parsed.registers || []), { name: "", address: "0x00", access: "rw", fields: [] }];
96
+ const updated = { ...parsed, registers: regs };
97
+ setParsed(updated);
98
+ handleChange(toYAML(updated));
99
+ };
100
+
101
+ const updateRegister = (idx, field, value) => {
102
+ const regs = [...(parsed.registers || [])];
103
+ regs[idx] = { ...regs[idx], [field]: value };
104
+ const updated = { ...parsed, registers: regs };
105
+ setParsed(updated);
106
+ handleChange(toYAML(updated));
107
+ };
108
+
109
+ const removeRegister = (idx) => {
110
+ const updated = { ...parsed, registers: parsed.registers.filter((_, i) => i !== idx) };
111
+ setParsed(updated);
112
+ handleChange(toYAML(updated));
113
+ };
114
+
115
+ const addField = (regIdx) => {
116
+ const regs = [...(parsed.registers || [])];
117
+ const fields = [...(regs[regIdx]?.fields || []), { name: "", bits: "0", description: "" }];
118
+ regs[regIdx] = { ...regs[regIdx], fields };
119
+ const updated = { ...parsed, registers: regs };
120
+ setParsed(updated);
121
+ handleChange(toYAML(updated));
122
+ };
123
+
124
+ const updateField = (regIdx, fldIdx, field, value) => {
125
+ const regs = [...(parsed.registers || [])];
126
+ const fields = [...(regs[regIdx]?.fields || [])];
127
+ fields[fldIdx] = { ...fields[fldIdx], [field]: value };
128
+ regs[regIdx] = { ...regs[regIdx], fields };
129
+ const updated = { ...parsed, registers: regs };
130
+ setParsed(updated);
131
+ handleChange(toYAML(updated));
132
+ };
133
+
134
+ const removeField = (regIdx, fldIdx) => {
135
+ const regs = [...(parsed.registers || [])];
136
+ regs[regIdx] = {
137
+ ...regs[regIdx],
138
+ fields: regs[regIdx].fields.filter((_, i) => i !== fldIdx),
139
+ };
140
+ const updated = { ...parsed, registers: regs };
141
+ setParsed(updated);
142
+ handleChange(toYAML(updated));
143
+ };
144
+
145
+ return (
146
+ <div className="card">
147
+ <div className="card-header">
148
+ <FileCode size={18} className="text-brand-600" />
149
+ <h2>Design Specification</h2>
150
+ </div>
151
+
152
+ <div className="card-body space-y-4">
153
+ {/* Toolbar */}
154
+ <div className="flex flex-wrap items-center gap-2">
155
+ <button className="btn-secondary text-xs" onClick={() => fileRef.current?.click()}>
156
+ <Upload size={14} /> Upload .core / .yaml
157
+ </button>
158
+ <input
159
+ ref={fileRef}
160
+ type="file"
161
+ accept=".yaml,.yml,.core,.json"
162
+ className="hidden"
163
+ onChange={handleFileUpload}
164
+ />
165
+ <button className="btn-secondary text-xs" onClick={handlePasteExample}>
166
+ Load Example
167
+ </button>
168
+ <div className="ml-auto flex items-center gap-1 bg-slate-100 rounded-lg p-0.5">
169
+ {["spec", "signals", "registers"].map((t) => (
170
+ <button
171
+ key={t}
172
+ onClick={() => setActiveTab(t)}
173
+ className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
174
+ activeTab === t
175
+ ? "bg-white text-slate-900 shadow-sm"
176
+ : "text-slate-500 hover:text-slate-700"
177
+ }`}
178
+ >
179
+ {t === "spec" ? "Spec" : t === "signals" ? "Signals" : "Registers"}
180
+ </button>
181
+ ))}
182
+ </div>
183
+ </div>
184
+
185
+ {/* Validation status */}
186
+ {errors.length > 0 && (
187
+ <div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3">
188
+ <div className="flex items-start gap-2">
189
+ <AlertCircle size={16} className="text-red-500 mt-0.5 shrink-0" />
190
+ <div className="text-xs text-red-700 space-y-0.5">
191
+ {errors.map((e, i) => (
192
+ <p key={i}>{e}</p>
193
+ ))}
194
+ </div>
195
+ </div>
196
+ </div>
197
+ )}
198
+ {errors.length === 0 && yamlText.trim() && (
199
+ <div className="flex items-center gap-2 text-xs text-emerald-600">
200
+ <CheckCircle2 size={14} />
201
+ YAML syntax valid
202
+ </div>
203
+ )}
204
+
205
+ {/* ── Spec tab ─────────────────────────────────── */}
206
+ {activeTab === "spec" && (
207
+ <div className="space-y-3">
208
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
209
+ <div>
210
+ <label className="block text-xs font-medium text-slate-600 mb-1">Design Name</label>
211
+ <input
212
+ className="input-field text-sm"
213
+ value={parsed.design_name || ""}
214
+ onChange={(e) => {
215
+ const updated = { ...parsed, design_name: e.target.value };
216
+ setParsed(updated);
217
+ handleChange(toYAML(updated));
218
+ }}
219
+ placeholder="uart16550"
220
+ />
221
+ </div>
222
+ <div>
223
+ <label className="block text-xs font-medium text-slate-600 mb-1">Version</label>
224
+ <input
225
+ className="input-field text-sm"
226
+ value={parsed.version || ""}
227
+ onChange={(e) => {
228
+ const updated = { ...parsed, version: e.target.value };
229
+ setParsed(updated);
230
+ handleChange(toYAML(updated));
231
+ }}
232
+ placeholder="1.5"
233
+ />
234
+ </div>
235
+ <div>
236
+ <label className="block text-xs font-medium text-slate-600 mb-1">Vendor</label>
237
+ <input
238
+ className="input-field text-sm"
239
+ value={parsed.vendor || ""}
240
+ onChange={(e) => {
241
+ const updated = { ...parsed, vendor: e.target.value };
242
+ setParsed(updated);
243
+ handleChange(toYAML(updated));
244
+ }}
245
+ placeholder="DV Automation"
246
+ />
247
+ </div>
248
+ </div>
249
+
250
+ <div>
251
+ <label className="block text-xs font-medium text-slate-600 mb-1">Description</label>
252
+ <textarea
253
+ className="input-field text-sm resize-none"
254
+ rows={2}
255
+ value={parsed.description || ""}
256
+ onChange={(e) => {
257
+ const updated = { ...parsed, description: e.target.value };
258
+ setParsed(updated);
259
+ handleChange(toYAML(updated));
260
+ }}
261
+ placeholder="Core description"
262
+ />
263
+ </div>
264
+
265
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
266
+ <div>
267
+ <label className="block text-xs font-medium text-slate-600 mb-1">Protocol</label>
268
+ <select
269
+ className="select-field text-sm"
270
+ value={parsed.protocol || ""}
271
+ onChange={(e) => {
272
+ const updated = { ...parsed, protocol: e.target.value };
273
+ setParsed(updated);
274
+ handleChange(toYAML(updated));
275
+ }}
276
+ >
277
+ <option value="">Auto-detect</option>
278
+ {PROTOCOLS.map((p) => (
279
+ <option key={p} value={p}>{p.toUpperCase()}</option>
280
+ ))}
281
+ </select>
282
+ </div>
283
+ <div>
284
+ <label className="block text-xs font-medium text-slate-600 mb-1">Language</label>
285
+ <select
286
+ className="select-field text-sm"
287
+ value={parsed.language || "systemverilog"}
288
+ onChange={(e) => {
289
+ const updated = { ...parsed, language: e.target.value };
290
+ setParsed(updated);
291
+ handleChange(toYAML(updated));
292
+ }}
293
+ >
294
+ <option value="systemverilog">SystemVerilog</option>
295
+ <option value="verilog">Verilog</option>
296
+ <option value="vhdl">VHDL</option>
297
+ </select>
298
+ </div>
299
+ </div>
300
+
301
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
302
+ <div>
303
+ <label className="block text-xs font-medium text-slate-600 mb-1">Clock (clk)</label>
304
+ <input className="input-field text-sm" value={parsed.clock_reset?.clock || "clk"} onChange={(e) => {
305
+ const cr = { ...(parsed.clock_reset || {}), clock: e.target.value };
306
+ const updated = { ...parsed, clock_reset: cr };
307
+ setParsed(updated); handleChange(toYAML(updated));
308
+ }} />
309
+ </div>
310
+ <div>
311
+ <label className="block text-xs font-medium text-slate-600 mb-1">Reset (rst_n)</label>
312
+ <input className="input-field text-sm" value={parsed.clock_reset?.reset || "rst_n"} onChange={(e) => {
313
+ const cr = { ...(parsed.clock_reset || {}), reset: e.target.value };
314
+ const updated = { ...parsed, clock_reset: cr };
315
+ setParsed(updated); handleChange(toYAML(updated));
316
+ }} />
317
+ </div>
318
+ <div>
319
+ <label className="block text-xs font-medium text-slate-600 mb-1">Reset Active</label>
320
+ <select className="select-field text-sm" value={parsed.clock_reset?.reset_active ?? 0} onChange={(e) => {
321
+ const cr = { ...(parsed.clock_reset || {}), reset_active: Number(e.target.value) };
322
+ const updated = { ...parsed, clock_reset: cr };
323
+ setParsed(updated); handleChange(toYAML(updated));
324
+ }}>
325
+ <option value={0}>Active Low (0)</option>
326
+ <option value={1}>Active High (1)</option>
327
+ </select>
328
+ </div>
329
+ </div>
330
+
331
+ {/* Raw YAML editor */}
332
+ <div>
333
+ <label className="block text-xs font-medium text-slate-600 mb-1">
334
+ Raw YAML / .core Editor
335
+ </label>
336
+ <textarea
337
+ className="input-field text-xs font-mono resize-y"
338
+ rows={10}
339
+ value={yamlText}
340
+ onChange={(e) => handleChange(e.target.value)}
341
+ spellCheck={false}
342
+ />
343
+ </div>
344
+ </div>
345
+ )}
346
+
347
+ {/* ── Signals tab ─────────────────────────────────── */}
348
+ {activeTab === "signals" && (
349
+ <div className="space-y-4">
350
+ {(parsed.interfaces || []).length === 0 && (
351
+ <p className="text-xs text-slate-400">No interfaces defined. Add one in the Spec tab.</p>
352
+ )}
353
+ {(parsed.interfaces || []).map((iface, iIdx) => (
354
+ <div key={iIdx} className="rounded-lg border border-slate-200 p-4 space-y-3">
355
+ <div className="flex items-center justify-between">
356
+ <div className="flex items-center gap-2">
357
+ <span className="badge-info">{iface.name || `Interface #${iIdx + 1}`}</span>
358
+ <select
359
+ className="select-field text-xs py-1 w-28"
360
+ value={iface.protocol || ""}
361
+ onChange={(e) => updateInterface(iIdx, "protocol", e.target.value)}
362
+ >
363
+ <option value="">Protocol</option>
364
+ {PROTOCOLS.map((p) => (
365
+ <option key={p} value={p}>{p.toUpperCase()}</option>
366
+ ))}
367
+ </select>
368
+ </div>
369
+ </div>
370
+
371
+ <div className="space-y-2">
372
+ {Array.isArray(iface.signals) && iface.signals.map((sig, sIdx) => (
373
+ <div key={sIdx} className="flex items-center gap-2">
374
+ <input
375
+ className="input-field text-xs py-1.5 w-28"
376
+ value={sig.name}
377
+ onChange={(e) => updateSignal(iIdx, sIdx, "name", e.target.value)}
378
+ placeholder="signal_name"
379
+ />
380
+ <select
381
+ className="select-field text-xs py-1.5 w-22"
382
+ value={sig.direction}
383
+ onChange={(e) => updateSignal(iIdx, sIdx, "direction", e.target.value)}
384
+ >
385
+ <option value="input">input</option>
386
+ <option value="output">output</option>
387
+ <option value="inout">inout</option>
388
+ </select>
389
+ <div className="flex items-center gap-1">
390
+ <span className="text-[11px] text-slate-500">w:</span>
391
+ <input
392
+ className="input-field text-xs py-1.5 w-16 text-center"
393
+ type="number"
394
+ min={1}
395
+ value={sig.width || 1}
396
+ onChange={(e) => updateSignal(iIdx, sIdx, "width", Number(e.target.value))}
397
+ />
398
+ </div>
399
+ <button
400
+ className="p-1.5 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50 transition-colors"
401
+ onClick={() => removeSignal(iIdx, sIdx)}
402
+ title="Remove signal"
403
+ >
404
+ <Trash2 size={14} />
405
+ </button>
406
+ </div>
407
+ ))}
408
+ </div>
409
+ <button
410
+ className="btn-secondary text-xs"
411
+ onClick={() => addSignal(iIdx)}
412
+ >
413
+ <Plus size={14} /> Add Signal
414
+ </button>
415
+ </div>
416
+ ))}
417
+ </div>
418
+ )}
419
+
420
+ {/* ── Registers tab ─────────────────────────────────── */}
421
+ {activeTab === "registers" && (
422
+ <div className="space-y-4">
423
+ {(parsed.registers || []).length === 0 && (
424
+ <p className="text-xs text-slate-400">No registers defined.</p>
425
+ )}
426
+ {(parsed.registers || []).map((reg, rIdx) => (
427
+ <div key={rIdx} className="rounded-lg border border-slate-200 p-4 space-y-3">
428
+ <div className="flex items-center gap-2 flex-wrap">
429
+ <input
430
+ className="input-field text-xs py-1.5 w-28"
431
+ value={reg.name}
432
+ onChange={(e) => updateRegister(rIdx, "name", e.target.value)}
433
+ placeholder="REG_NAME"
434
+ />
435
+ <input
436
+ className="input-field text-xs py-1.5 w-22 font-mono"
437
+ value={reg.address}
438
+ onChange={(e) => updateRegister(rIdx, "address", e.target.value)}
439
+ placeholder="0x00"
440
+ />
441
+ <select
442
+ className="select-field text-xs py-1.5 w-20"
443
+ value={reg.access || "rw"}
444
+ onChange={(e) => updateRegister(rIdx, "access", e.target.value)}
445
+ >
446
+ <option value="rw">rw</option>
447
+ <option value="ro">ro</option>
448
+ <option value="wo">wo</option>
449
+ <option value="rc">rc</option>
450
+ <option value="w1c">w1c</option>
451
+ </select>
452
+ <input
453
+ className="input-field text-xs py-1.5 w-32"
454
+ value={reg.description || ""}
455
+ onChange={(e) => updateRegister(rIdx, "description", e.target.value)}
456
+ placeholder="Description"
457
+ />
458
+ <button
459
+ className="p-1.5 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50 transition-colors ml-auto"
460
+ onClick={() => removeRegister(rIdx)}
461
+ title="Remove register"
462
+ >
463
+ <Trash2 size={14} />
464
+ </button>
465
+ </div>
466
+
467
+ {/* Fields */}
468
+ {(reg.fields || []).length > 0 && (
469
+ <div className="ml-2 pl-3 border-l-2 border-brand-200 space-y-2">
470
+ <span className="text-[11px] font-medium text-slate-500 uppercase tracking-wider">
471
+ Fields
472
+ </span>
473
+ {(reg.fields || []).map((fld, fIdx) => (
474
+ <div key={fIdx} className="flex items-center gap-2">
475
+ <input
476
+ className="input-field text-xs py-1 w-24"
477
+ value={fld.name}
478
+ onChange={(e) => updateField(rIdx, fIdx, "name", e.target.value)}
479
+ placeholder="field_name"
480
+ />
481
+ <input
482
+ className="input-field text-xs py-1 w-16 font-mono text-center"
483
+ value={fld.bits}
484
+ onChange={(e) => updateField(rIdx, fIdx, "bits", e.target.value)}
485
+ placeholder="7:0"
486
+ />
487
+ <input
488
+ className="input-field text-xs py-1 flex-1"
489
+ value={fld.description || ""}
490
+ onChange={(e) => updateField(rIdx, fIdx, "description", e.target.value)}
491
+ placeholder="Description"
492
+ />
493
+ <button
494
+ className="p-1 rounded-md text-slate-400 hover:text-red-500 hover:bg-red-50"
495
+ onClick={() => removeField(rIdx, fIdx)}
496
+ >
497
+ <Trash2 size={12} />
498
+ </button>
499
+ </div>
500
+ ))}
501
+ </div>
502
+ )}
503
+ <button className="btn-secondary text-xs" onClick={() => addField(rIdx)}>
504
+ <Plus size={14} /> Add Field
505
+ </button>
506
+ </div>
507
+ ))}
508
+ <button className="btn-secondary text-xs" onClick={addRegister}>
509
+ <Plus size={14} /> Add Register
510
+ </button>
511
+ </div>
512
+ )}
513
+ </div>
514
+ </div>
515
+ );
516
+ }
frontend/src/hooks/usePipeline.js ADDED
@@ -0,0 +1,778 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from "react";
2
+
3
+ const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000";
4
+
5
+ const FILE_CONTENT_TEMPLATES = {
6
+ testbench: (design) => [
7
+ `// Generated UVM Testbench for ${design} — with DUT instantiation`,
8
+ "`timescale 1ns/1ps",
9
+ "",
10
+ "module testbench;",
11
+ " logic clk;",
12
+ " logic rst_n;",
13
+ "",
14
+ ` ${design}_intf intf(clk, rst_n);`,
15
+ "",
16
+ " uart_top #(",
17
+ " .CLK_FREQ (50000000),",
18
+ " .BAUD_RATE (115200)",
19
+ " ) dut (",
20
+ " .clk (clk),",
21
+ " .rst_n (rst_n),",
22
+ " .wb_cyc (intf.wb_cyc),",
23
+ " .wb_stb (intf.wb_stb),",
24
+ " .wb_we (intf.wb_we),",
25
+ " .wb_addr (intf.wb_addr),",
26
+ " .wb_data_i (intf.wb_data_o),",
27
+ " .wb_data_o (intf.wb_data_i),",
28
+ " .wb_ack (intf.wb_ack),",
29
+ " .uart_tx (intf.uart_tx),",
30
+ " .uart_rx (intf.uart_rx),",
31
+ " .cts_n (intf.cts_n),",
32
+ " .rts_n (intf.rts_n),",
33
+ " .dsr_n (intf.dsr_n),",
34
+ " .dtr_n (intf.dtr_n),",
35
+ " .ri_n (intf.ri_n),",
36
+ " .dcd_n (intf.dcd_n),",
37
+ " .out1_n (intf.out1_n),",
38
+ " .out2_n (intf.out2_n),",
39
+ " .uart_intr (intf.uart_intr)",
40
+ " );",
41
+ "",
42
+ " initial begin",
43
+ ` uvm_config_db#(virtual ${design}_intf)::set(null, "*", "vif", intf);`,
44
+ " run_test();",
45
+ " end",
46
+ "",
47
+ " initial begin",
48
+ " clk = 0;",
49
+ " forever #5 clk = ~clk;",
50
+ " end",
51
+ "",
52
+ " initial begin",
53
+ " rst_n = 0;",
54
+ " repeat (10) @(posedge clk);",
55
+ " rst_n = 1;",
56
+ " end",
57
+ "",
58
+ " initial begin",
59
+ ' $dumpfile("sim.vcd");',
60
+ " $dumpvars(0, testbench);",
61
+ " end",
62
+ "endmodule",
63
+ ].join("\n"),
64
+
65
+ interface: (design) => [
66
+ `// Generated UVM Interface for ${design}`,
67
+ `interface ${design}_intf (input logic clk, input logic rst_n);`,
68
+ "",
69
+ " logic wb_cyc, wb_stb, wb_we;",
70
+ " logic [2:0] wb_addr;",
71
+ " logic [7:0] wb_data_o, wb_data_i;",
72
+ " logic wb_ack;",
73
+ " logic uart_tx, uart_rx;",
74
+ " logic cts_n, rts_n, dsr_n, dtr_n, ri_n, dcd_n, out1_n, out2_n;",
75
+ " logic uart_intr;",
76
+ "",
77
+ " clocking drv_cb @(posedge clk);",
78
+ " default input #1ns output #1ns;",
79
+ " output wb_cyc, wb_stb, wb_we, wb_addr, wb_data_o;",
80
+ " output uart_rx, cts_n, dsr_n, ri_n, dcd_n;",
81
+ " input wb_ack, wb_data_i, uart_tx, uart_intr, rts_n, dtr_n, out1_n, out2_n;",
82
+ " endclocking",
83
+ "",
84
+ " clocking mon_cb @(posedge clk);",
85
+ " default input #1ns;",
86
+ " input all;",
87
+ " endclocking",
88
+ "",
89
+ " modport driver (clocking drv_cb, input clk, rst_n);",
90
+ " modport monitor (clocking mon_cb, input clk, rst_n);",
91
+ "endinterface",
92
+ ].join("\n"),
93
+
94
+ seq_item: (design) => [
95
+ `class ${design}_seq_item extends uvm_sequence_item;`,
96
+ ` \`uvm_object_utils(${design}_seq_item)`,
97
+ " rand logic we;",
98
+ " rand logic [2:0] addr;",
99
+ " rand logic [7:0] data;",
100
+ " rand int delay;",
101
+ " constraint c_default { delay inside {[0:5]}; soft we == 1'b1; }",
102
+ ` function new(string name = "${design}_seq_item"); super.new(name); endfunction`,
103
+ "endclass",
104
+ ].join("\n"),
105
+
106
+ driver: (design) => [
107
+ `class ${design}_driver extends uvm_driver #(${design}_seq_item);`,
108
+ ` \`uvm_component_utils(${design}_driver)`,
109
+ ` virtual ${design}_intf vif;`,
110
+ " function new(string n, uvm_component p); super.new(n, p); endfunction",
111
+ " function void build_phase(uvm_phase phase);",
112
+ ` if (!uvm_config_db#(virtual ${design}_intf)::get(this, "", "vif", vif))`,
113
+ ' `uvm_fatal("NOVIF", "")',
114
+ " endfunction",
115
+ " task run_phase(uvm_phase phase);",
116
+ " forever begin",
117
+ " seq_item_port.get_next_item(req);",
118
+ " vif.drv_cb.wb_cyc <= 1; vif.drv_cb.wb_stb <= 1;",
119
+ " vif.drv_cb.wb_we <= req.we;",
120
+ " vif.drv_cb.wb_addr <= req.addr;",
121
+ " vif.drv_cb.wb_data_o <= req.data;",
122
+ " @(vif.drv_cb); wait(vif.drv_cb.wb_ack); @(vif.drv_cb);",
123
+ " vif.drv_cb.wb_cyc <= 0; vif.drv_cb.wb_stb <= 0;",
124
+ " seq_item_port.item_done();",
125
+ " end",
126
+ " endtask",
127
+ "endclass",
128
+ ].join("\n"),
129
+
130
+ monitor: (design) => [
131
+ `class ${design}_monitor extends uvm_monitor;`,
132
+ ` \`uvm_component_utils(${design}_monitor)`,
133
+ ` virtual ${design}_intf vif;`,
134
+ ` uvm_analysis_port #(${design}_seq_item) item_collected_port;`,
135
+ " function new(string n, uvm_component p); super.new(n, p);",
136
+ ` item_collected_port = new("item_collected_port", this); endfunction`,
137
+ " function void build_phase(uvm_phase phase);",
138
+ ` if (!uvm_config_db#(virtual ${design}_intf)::get(this, "", "vif", vif))`,
139
+ ' `uvm_fatal("NOVIF", "")',
140
+ " endfunction",
141
+ " task run_phase(uvm_phase phase); forever begin",
142
+ " @(vif.mon_cb);",
143
+ " if (vif.mon_cb.wb_stb && vif.mon_cb.wb_cyc) begin",
144
+ ` ${design}_seq_item item = ${design}_seq_item::type_id::create("item");`,
145
+ " item.we = vif.mon_cb.wb_we; item.addr = vif.mon_cb.wb_addr;",
146
+ " if (vif.mon_cb.wb_we) item.data = vif.mon_cb.wb_data_o;",
147
+ " else begin wait(vif.mon_cb.wb_ack); item.data = vif.mon_cb.wb_data_i; end",
148
+ " item_collected_port.write(item);",
149
+ " end end",
150
+ " endtask",
151
+ "endclass",
152
+ ].join("\n"),
153
+
154
+ agent: (design) => [
155
+ `class ${design}_agent extends uvm_agent;`,
156
+ ` \`uvm_component_utils(${design}_agent)`,
157
+ ` ${design}_driver driver;`,
158
+ ` ${design}_monitor monitor;`,
159
+ ` uvm_sequencer #(${design}_seq_item) sequencer;`,
160
+ " function new(string n, uvm_component p); super.new(n, p); endfunction",
161
+ " function void build_phase(uvm_phase phase);",
162
+ ` driver = ${design}_driver::type_id::create("driver", this);`,
163
+ ` monitor = ${design}_monitor::type_id::create("monitor", this);`,
164
+ ` sequencer = uvm_sequencer#(${design}_seq_item)::type_id::create("sequencer", this);`,
165
+ " endfunction",
166
+ " function void connect_phase(uvm_phase phase);",
167
+ " driver.seq_item_port.connect(sequencer.seq_item_export);",
168
+ " endfunction",
169
+ "endclass",
170
+ ].join("\n"),
171
+
172
+ scoreboard: (design) => [
173
+ `class ${design}_scoreboard extends uvm_scoreboard;`,
174
+ ` \`uvm_component_utils(${design}_scoreboard)`,
175
+ ` uvm_analysis_imp #(${design}_seq_item, ${design}_scoreboard) act_export;`,
176
+ " int match_count, mismatch_count;",
177
+ " function new(string n, uvm_component p); super.new(n, p);",
178
+ ` act_export = new("act_export", this); endfunction`,
179
+ ` function void write(${design}_seq_item t);`,
180
+ " match_count++;",
181
+ ' `uvm_info("SCOREBOARD", $sformatf("item: %s", t.convert2string()), UVM_LOW)',
182
+ " endfunction",
183
+ " function void report_phase(uvm_phase phase);",
184
+ ' if (mismatch_count == 0) `uvm_info("SCOREBOARD", "PASS", UVM_LOW)',
185
+ ' else `uvm_error("SCOREBOARD", "FAIL")',
186
+ " endfunction",
187
+ "endclass",
188
+ ].join("\n"),
189
+
190
+ coverage_collector: (design) => {
191
+ const cg = [
192
+ " covergroup bus_cg @(negedge vif.clk);",
193
+ " option.name = \"bus_cg\";",
194
+ " ADDR: coverpoint vif.mon_cb.wb_addr { bins regs[] = {[0:7]}; }",
195
+ " DIR: coverpoint vif.mon_cb.wb_we { bins read = {0}; bins write = {1}; }",
196
+ " ADRxDIR: cross ADDR, DIR;",
197
+ " endgroup",
198
+ "",
199
+ " covergroup data_cg @(negedge vif.clk);",
200
+ " option.name = \"data_cg\";",
201
+ " DATA: coverpoint vif.mon_cb.wb_data_o {",
202
+ " bins zero = {8'h00}; bins ones = {8'hFF};",
203
+ " bins pattern = {8'h55, 8'hAA, 8'hA5, 8'h5A}; bins others = default;",
204
+ " }",
205
+ " endgroup",
206
+ ].join("\n");
207
+ return [
208
+ `class ${design}_coverage_collector extends uvm_subscriber #(${design}_seq_item);`,
209
+ ` \`uvm_component_utils(${design}_coverage_collector)`,
210
+ ` virtual ${design}_intf vif;`,
211
+ "",
212
+ cg,
213
+ "",
214
+ " function new(string n, uvm_component p); super.new(n, p);",
215
+ " bus_cg = new(); data_cg = new();",
216
+ " endfunction",
217
+ " function void build_phase(uvm_phase phase);",
218
+ ` if (!uvm_config_db#(virtual ${design}_intf)::get(this, "", "vif", vif))`,
219
+ ' `uvm_fatal("NOVIF", "")',
220
+ " endfunction",
221
+ " function void write(${design}_seq_item t); endfunction",
222
+ " function void report_phase(uvm_phase phase);",
223
+ ` \`uvm_info(get_type_name(), $sformatf("bus_cg=%.1f%% data_cg=%.1f%%",`,
224
+ " bus_cg.get_coverage(), data_cg.get_coverage()), UVM_LOW)",
225
+ " endfunction",
226
+ "endclass",
227
+ ].join("\n");
228
+ },
229
+
230
+ base_sequence: (design) => {
231
+ const seqs = [
232
+ `class ${design}_base_seq extends uvm_sequence #(${design}_seq_item);`,
233
+ ` \`uvm_object_utils(${design}_base_seq)`,
234
+ ` function new(string n = "${design}_base_seq"); super.new(n); endfunction`,
235
+ ' virtual task body(); `uvm_info(get_type_name(), "base", UVM_LOW); endtask',
236
+ "endclass",
237
+ "",
238
+ `class ${design}_write_reg_seq extends ${design}_base_seq;`,
239
+ ` \`uvm_object_utils(${design}_write_reg_seq)`,
240
+ " rand logic [2:0] reg_addr; rand logic [7:0] reg_data;",
241
+ ` function new(string n = "${design}_write_reg_seq"); super.new(n); endfunction`,
242
+ " virtual task body();",
243
+ ` ${design}_seq_item item = ${design}_seq_item::type_id::create("item");`,
244
+ " start_item(item); item.we = 1; item.addr = reg_addr; item.data = reg_data; finish_item(item);",
245
+ ' `uvm_info(get_type_name(), $sformatf("W 0x%0h <- 0x%0h", reg_addr, reg_data), UVM_MEDIUM)',
246
+ " endtask",
247
+ "endclass",
248
+ "",
249
+ `class ${design}_read_reg_seq extends ${design}_base_seq;`,
250
+ ` \`uvm_object_utils(${design}_read_reg_seq)`,
251
+ " rand logic [2:0] reg_addr; logic [7:0] read_data;",
252
+ ` function new(string n = "${design}_read_reg_seq"); super.new(n); endfunction`,
253
+ " virtual task body();",
254
+ ` ${design}_seq_item item = ${design}_seq_item::type_id::create("item");`,
255
+ " start_item(item); item.we = 0; item.addr = reg_addr; finish_item(item);",
256
+ " read_data = item.data;",
257
+ ' `uvm_info(get_type_name(), $sformatf("R 0x%0h -> 0x%0h", reg_addr, read_data), UVM_MEDIUM)',
258
+ " endtask",
259
+ "endclass",
260
+ "",
261
+ `class ${design}_send_byte_seq extends ${design}_base_seq;`,
262
+ ` \`uvm_object_utils(${design}_send_byte_seq)`,
263
+ " rand logic [7:0] tx_byte;",
264
+ ` function new(string n = "${design}_send_byte_seq"); super.new(n); endfunction`,
265
+ " virtual task body();",
266
+ ` ${design}_write_reg_seq wseq;`,
267
+ ` wseq = ${design}_write_reg_seq::type_id::create("wseq");`,
268
+ " wseq.reg_addr = 3'h3; wseq.reg_data = 8'b00000011; wseq.start(m_sequencer);",
269
+ " wseq.reg_addr = 3'h0; wseq.reg_data = tx_byte; wseq.start(m_sequencer);",
270
+ ' `uvm_info(get_type_name(), $sformatf("TX 0x%0h", tx_byte), UVM_LOW)',
271
+ " endtask",
272
+ "endclass",
273
+ ].join("\n");
274
+ return seqs;
275
+ },
276
+
277
+ test: (design) => [
278
+ `class test_${design} extends uvm_test;`,
279
+ ` \`uvm_component_utils(test_${design})`,
280
+ ` environment_${design} env;`,
281
+ ` function new(string n = "test_${design}", uvm_component p = null); super.new(n, p); endfunction`,
282
+ " function void build_phase(uvm_phase phase);",
283
+ ` env = environment_${design}::type_id::create("env", this);`,
284
+ " endfunction",
285
+ " task run_phase(uvm_phase phase);",
286
+ ` ${design}_send_byte_seq seq;`,
287
+ " phase.raise_objection(this);",
288
+ ` seq = ${design}_send_byte_seq::type_id::create("seq");`,
289
+ " seq.randomize() with { tx_byte == 8'hAB; };",
290
+ " seq.start(env.agent.sequencer);",
291
+ ` ${design}_write_reg_seq wseq = ${design}_write_reg_seq::type_id::create("wseq");`,
292
+ " wseq.reg_addr = 3'h7; wseq.reg_data = 8'hA5; wseq.start(env.agent.sequencer);",
293
+ ` ${design}_read_reg_seq rseq = ${design}_read_reg_seq::type_id::create("rseq");`,
294
+ " rseq.reg_addr = 3'h7; rseq.start(env.agent.sequencer);",
295
+ " if (rseq.read_data == 8'hA5)",
296
+ ' `uvm_info(get_type_name(), "SCR PASS", UVM_LOW)',
297
+ " else",
298
+ ' `uvm_error(get_type_name(), "SCR FAIL")',
299
+ " phase.drop_objection(this);",
300
+ " endtask",
301
+ "endclass",
302
+ ].join("\n"),
303
+
304
+ regression: (design) => [
305
+ `// Auto-generated regression test — coverage-driven`,
306
+ `class ${design}_regression_test extends test_${design};`,
307
+ ` \`uvm_component_utils(${design}_regression_test)`,
308
+ " function new(string n, uvm_component p); super.new(n, p); endfunction",
309
+ " task run_phase(uvm_phase phase);",
310
+ " super.run_phase(phase); phase.raise_objection(this);",
311
+ ` ${design}_write_reg_seq wseq;`,
312
+ " for (int a = 0; a < 8; a++) begin",
313
+ ` wseq = ${design}_write_reg_seq::type_id::create($sformatf("w_%0d", a));`,
314
+ " wseq.reg_addr = a; wseq.reg_data = 8'(a << 4 | a); wseq.start(env.agent.sequencer);",
315
+ " end",
316
+ ` ${design}_read_reg_seq rseq;`,
317
+ " for (int a = 0; a < 8; a++) begin",
318
+ ` rseq = ${design}_read_reg_seq::type_id::create($sformatf("r_%0d", a));`,
319
+ " rseq.reg_addr = a; rseq.start(env.agent.sequencer);",
320
+ " end",
321
+ " phase.drop_objection(this);",
322
+ " endtask",
323
+ "endclass",
324
+ ].join("\n"),
325
+
326
+ env: (design) => [
327
+ `class environment_${design} extends uvm_env;`,
328
+ ` \`uvm_component_utils(environment_${design})`,
329
+ ` ${design}_agent agent;`,
330
+ ` ${design}_scoreboard sb;`,
331
+ ` ${design}_coverage_collector cov;`,
332
+ "",
333
+ " function new(string n, uvm_component p); super.new(n, p); endfunction",
334
+ " function void build_phase(uvm_phase phase);",
335
+ ` agent = ${design}_agent::type_id::create("agent", this);`,
336
+ ` sb = ${design}_scoreboard::type_id::create("sb", this);`,
337
+ ` cov = ${design}_coverage_collector::type_id::create("cov", this);`,
338
+ " endfunction",
339
+ " function void connect_phase(uvm_phase phase);",
340
+ " agent.monitor.item_collected_port.connect(sb.act_export);",
341
+ " agent.monitor.item_collected_port.connect(cov.analysis_export);",
342
+ " endfunction",
343
+ "endclass",
344
+ ].join("\n"),
345
+
346
+ rtl_baud_gen: () => [
347
+ "module uart_baud_gen #(parameter CLK_FREQ=50000000, parameter BAUD_RATE=115200)",
348
+ " (input logic clk, rst_n, input logic [7:0] divisor, output logic baud_tick);",
349
+ " logic [15:0] period = CLK_FREQ / (BAUD_RATE * 16);",
350
+ " logic [15:0] counter;",
351
+ " always_ff @(posedge clk or negedge rst_n)",
352
+ " if (!rst_n) begin counter <= '0; baud_tick <= 0; end",
353
+ " else if (counter >= period-1) begin counter <= '0; baud_tick <= 1; end",
354
+ " else begin counter <= counter+1; baud_tick <= 0; end",
355
+ "endmodule",
356
+ ].join("\n"),
357
+
358
+ rtl_transmitter: () => [
359
+ "module uart_transmitter (input logic clk, rst_n, baud_tick,",
360
+ " input logic [7:0] data_in, input logic we, output logic tx);",
361
+ " typedef enum {IDLE,START,DATA,STOP} state_t; state_t state;",
362
+ " logic [7:0] shift_reg; logic [2:0] bit_cnt;",
363
+ " always_ff @(posedge clk or negedge rst_n)",
364
+ " if (!rst_n) begin state <= IDLE; tx <= 1; end",
365
+ " else case (state)",
366
+ " IDLE: if (we) begin shift_reg <= data_in; bit_cnt<=0; state<=START; end",
367
+ " START: begin tx<=0; if(baud_tick) state<=DATA; end",
368
+ " DATA: if(baud_tick) begin tx<=shift_reg[bit_cnt];",
369
+ " if(bit_cnt==7) state<=STOP; else bit_cnt<=bit_cnt+1; end",
370
+ " STOP: begin tx<=1; if(baud_tick) state<=IDLE; end",
371
+ " endcase",
372
+ "endmodule",
373
+ ].join("\n"),
374
+
375
+ rtl_receiver: () => [
376
+ "module uart_receiver (input logic clk, rst_n, baud_tick, rx,",
377
+ " output logic [7:0] data_out, output logic data_ready);",
378
+ " typedef enum {IDLE,START,DATA,STOP} state_t; state_t state;",
379
+ " logic [7:0] shift_reg; logic [2:0] bit_cnt; logic rx_sync;",
380
+ " always_ff @(posedge clk or negedge rst_n)",
381
+ " if (!rst_n) begin state<=IDLE; rx_sync<=1; data_ready<=0; end",
382
+ " else case (state)",
383
+ " IDLE: begin data_ready<=0; rx_sync<=rx; if(!rx_sync) begin state<=START; end end",
384
+ " START: if(baud_tick) state<=DATA;",
385
+ " DATA: if(baud_tick) begin shift_reg[bit_cnt]<=rx_sync;",
386
+ " if(bit_cnt==7) state<=STOP; else bit_cnt<=bit_cnt+1; end",
387
+ " STOP: if(baud_tick) begin data_out<=shift_reg; data_ready<=1; state<=IDLE; end",
388
+ " endcase",
389
+ "endmodule",
390
+ ].join("\n"),
391
+
392
+ rtl_regs: () => [
393
+ "module uart_regs (input logic clk, rst_n, wb_cyc, wb_stb, wb_we,",
394
+ " input logic [2:0] wb_addr, input logic [7:0] wb_data_i,",
395
+ " output logic [7:0] wb_data_o, output logic wb_ack);",
396
+ " logic [7:0] reg_lcr, reg_scr;",
397
+ " assign wb_ack = wb_cyc & wb_stb;",
398
+ " always_comb case (wb_addr)",
399
+ " 0: wb_data_o = 8'h00; 1: wb_data_o = 8'h00; 2: wb_data_o = 8'hC0;",
400
+ " 3: wb_data_o = reg_lcr; 4: wb_data_o = 8'h00;",
401
+ " 5: wb_data_o = 8'h60; 6: wb_data_o = 8'h00;",
402
+ " 7: wb_data_o = reg_scr; default: wb_data_o = 8'h00;",
403
+ " endcase",
404
+ " always_ff @(posedge clk or negedge rst_n)",
405
+ " if (!rst_n) begin reg_lcr <= 0; reg_scr <= 0; end",
406
+ " else if (wb_cyc & wb_stb & wb_we)",
407
+ " case (wb_addr) 3: reg_lcr <= wb_data_i; 7: reg_scr <= wb_data_i; endcase",
408
+ "endmodule",
409
+ ].join("\n"),
410
+
411
+ rtl_top: () => [
412
+ "module uart_top (input logic clk, rst_n, wb_cyc, wb_stb, wb_we,",
413
+ " input logic [2:0] wb_addr, input logic [7:0] wb_data_i,",
414
+ " output logic [7:0] wb_data_o, output logic wb_ack,",
415
+ " output logic uart_tx, input logic uart_rx,",
416
+ " input logic cts_n, output logic rts_n,",
417
+ " input logic dsr_n, output logic dtr_n,",
418
+ " input logic ri_n, dcd_n, output logic out1_n, out2_n, uart_intr);",
419
+ " logic baud_tick;",
420
+ " uart_baud_gen u_baud(.clk(clk),.rst_n(rst_n),.divisor(8'h01),.baud_tick(baud_tick));",
421
+ " uart_transmitter u_tx(.clk(clk),.rst_n(rst_n),.baud_tick(baud_tick),",
422
+ " .data_in(wb_data_i), .we(wb_cyc&wb_stb&wb_we&(wb_addr==0)), .tx(uart_tx));",
423
+ " uart_receiver u_rx(.clk(clk),.rst_n(rst_n),.baud_tick(baud_tick),.rx(uart_rx));",
424
+ " uart_regs u_regs(.clk(clk),.rst_n(rst_n),.wb_cyc(wb_cyc),.wb_stb(wb_stb),",
425
+ " .wb_we(wb_we),.wb_addr(wb_addr),.wb_data_i(wb_data_i),",
426
+ " .wb_data_o(wb_data_o),.wb_ack(wb_ack));",
427
+ " assign uart_intr = 0; assign rts_n = 0; assign dtr_n = 0;",
428
+ " assign out1_n = 1; assign out2_n = 1;",
429
+ "endmodule",
430
+ ].join("\n"),
431
+ };
432
+
433
+ function buildFileContent(design, version, iteration) {
434
+ const t = FILE_CONTENT_TEMPLATES;
435
+ const prefix = `v${version}_it${iteration}_`;
436
+ const files = {
437
+ "testbench.sv": t.testbench(design),
438
+ [`interface_${design}.sv`]: t.interface(design),
439
+ [`sequence_item_${design}.sv`]: t.seq_item(design),
440
+ [`driver_${design}.sv`]: t.driver(design),
441
+ [`monitor_${design}.sv`]: t.monitor(design),
442
+ [`agent_${design}.sv`]: t.agent(design),
443
+ [`scoreboard_${design}.sv`]: t.scoreboard(design),
444
+ [`coverage_collector_${design}.sv`]: t.coverage_collector(design),
445
+ [`base_sequence_${design}.sv`]: t.base_sequence(design),
446
+ [`test_${design}.sv`]: t.test(design),
447
+ [`environment_${design}.sv`]: t.env(design),
448
+ [`regression_${design}.sv`]: t.regression(design),
449
+ "rtl/uart_baud_gen.v": t.rtl_baud_gen(),
450
+ "rtl/uart_transmitter.v": t.rtl_transmitter(),
451
+ "rtl/uart_receiver.v": t.rtl_receiver(),
452
+ "rtl/uart_regs.v": t.rtl_regs(),
453
+ "rtl/uart_top.v": t.rtl_top(),
454
+ "compile.f": [
455
+ "// Compile list",
456
+ "./rtl/uart_baud_gen.v ./rtl/uart_transmitter.v",
457
+ "./rtl/uart_receiver.v ./rtl/uart_regs.v ./rtl/uart_top.v",
458
+ `./sequence_item_${design}.sv ./interface_${design}.sv`,
459
+ `./driver_${design}.sv ./monitor_${design}.sv ./agent_${design}.sv`,
460
+ `./scoreboard_${design}.sv ./coverage_collector_${design}.sv`,
461
+ `./base_sequence_${design}.sv ./test_${design}.sv ./environment_${design}.sv`,
462
+ "./testbench.sv",
463
+ "+define+UVM_NO_DPI",
464
+ ].join("\n"),
465
+ };
466
+
467
+ return Object.entries(files).map(([name, content]) => ({
468
+ name,
469
+ path: `output/${design}_tb/${prefix}${name}`,
470
+ content,
471
+ size: `${new Blob([content]).size} B`,
472
+ }));
473
+ }
474
+
475
+ // Coverage stub simulator with multi-seed emulation
476
+ function simulateCoverage(artifacts, seed = 1) {
477
+ const allText = artifacts.map((a) => a.content).join("\n");
478
+ const bins = [];
479
+ const rng = ((seed) => {
480
+ let s = seed;
481
+ return () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
482
+ })(seed);
483
+
484
+ for (let addr = 0; addr < 8; addr++) {
485
+ bins.push({ name: `bus_cg.ADDR.regs[${addr}]`, hit: false, goal: 1 });
486
+ bins.push({ name: `cross_ADRxDIR.addr${addr}_read`, hit: false, goal: 1 });
487
+ bins.push({ name: `cross_ADRxDIR.addr${addr}_write`, hit: false, goal: 1 });
488
+ }
489
+ bins.push({ name: "bus_cg.DIR.read", hit: false, goal: 1 });
490
+ bins.push({ name: "bus_cg.DIR.write", hit: false, goal: 1 });
491
+ bins.push({ name: "data_cg.DATA.zero", hit: false, goal: 1 });
492
+ bins.push({ name: "data_cg.DATA.ones", hit: false, goal: 1 });
493
+ // Protocol checker SVA bins
494
+ bins.push({ name: "sva_cp.ack_timing", hit: false, goal: 1 });
495
+ bins.push({ name: "sva_cp.data_stable", hit: false, goal: 1 });
496
+
497
+ for (const bin of bins) {
498
+ if (bin.name.includes("DIR.read") && allText.includes("item.we = 0")) bin.hit = true;
499
+ else if (bin.name.includes("DIR.write") && allText.includes("item.we = 1")) bin.hit = true;
500
+ else {
501
+ const m = bin.name.match(/regs\[(\d+)\]/);
502
+ if (m) {
503
+ const addr = m[1];
504
+ if (allText.includes(`reg_addr = 3'h${addr}`) || allText.includes(`reg_addr=${addr}`)) {
505
+ bin.hit = true;
506
+ }
507
+ }
508
+ const cm = bin.name.match(/addr(\d+)_(read|write)/);
509
+ if (cm) {
510
+ const addr = cm[1];
511
+ const dir = cm[2] === "write" ? "1" : "0";
512
+ if (
513
+ allText.includes(`reg_addr = 3'h${addr}`) &&
514
+ allText.includes(`item.we = ${dir}`)
515
+ ) {
516
+ bin.hit = true;
517
+ }
518
+ }
519
+ }
520
+ if (bin.name.includes("DATA.zero") && allText.includes("8'h00")) bin.hit = true;
521
+ if (bin.name.includes("DATA.ones") && allText.includes("8'hFF")) bin.hit = true;
522
+ // SVA bins: hit if assertions present
523
+ if (bin.name.includes("sva_cp.ack_timing") && allText.includes("assert property")) bin.hit = true;
524
+ if (bin.name.includes("sva_cp.data_stable") && allText.includes("cover property")) bin.hit = true;
525
+
526
+ // Seed-based randomization: add noise per seed
527
+ if (!bin.hit && rng() < 0.15 * seed) bin.hit = true;
528
+ }
529
+
530
+ const total = bins.length;
531
+ const covered = bins.filter((b) => b.hit).length;
532
+ return { total, covered, pct: total > 0 ? (covered / total) * 100 : 0, bins };
533
+ }
534
+
535
+ // Merge coverage across seeds
536
+ function mergeCoverage(seedResults) {
537
+ const merged = {};
538
+ for (const res of seedResults) {
539
+ for (const bin of res.bins) {
540
+ if (!merged[bin.name]) merged[bin.name] = { name: bin.name, hit: false, goal: 1 };
541
+ if (bin.hit) merged[bin.name].hit = true;
542
+ }
543
+ }
544
+ const bins = Object.values(merged);
545
+ const total = bins.length;
546
+ const covered = bins.filter((b) => b.hit).length;
547
+ return { total, covered, pct: total > 0 ? (covered / total) * 100 : 0, bins };
548
+ }
549
+
550
+ export default function usePipeline() {
551
+ const [state, setState] = useState({
552
+ running: false,
553
+ autoTraining: false,
554
+ logs: [],
555
+ artifacts: [],
556
+ error: null,
557
+ versions: [],
558
+ currentVersion: 0,
559
+ coverageTrend: [],
560
+ coverageGaps: [],
561
+ seedResults: [],
562
+ autoTrainIteration: 0,
563
+ autoTrainMax: 5,
564
+ });
565
+
566
+ const appendLog = useCallback((level, message) => {
567
+ const timestamp = new Date().toISOString().slice(11, 23);
568
+ setState((prev) => ({
569
+ ...prev,
570
+ logs: [...prev.logs, { level, message, timestamp }],
571
+ }));
572
+ }, []);
573
+
574
+ const runSingle = useCallback(
575
+ async (yamlSpec, version, iteration, totalIterations, numSeeds = 3) => {
576
+ appendLog("info", `Iteration ${iteration}/${totalIterations} — generating...`);
577
+
578
+ const spec = (() => {
579
+ try {
580
+ return JSON.parse(yamlSpec);
581
+ } catch {
582
+ return {};
583
+ }
584
+ })();
585
+ const design = spec?.design_name || "uart";
586
+
587
+ // Generate artifacts for this version
588
+ const artifacts = buildFileContent(design, version, iteration);
589
+ appendLog("success", `Generated ${artifacts.length} files (version v${version})`);
590
+
591
+ // Multi-seed regression
592
+ await new Promise((r) => setTimeout(r, 200));
593
+ const seedResults = [];
594
+ for (let s = 1; s <= numSeeds; s++) {
595
+ const cov = simulateCoverage(artifacts, s);
596
+ seedResults.push(cov);
597
+ }
598
+ const merged = mergeCoverage(seedResults);
599
+ const uncovered = merged.bins.filter((b) => !b.hit);
600
+ const gaps = uncovered.map((b) => ({
601
+ bin: b.name,
602
+ covered: false,
603
+ }));
604
+
605
+ appendLog(
606
+ merged.pct >= 90 ? "success" : "warn",
607
+ `Coverage: ${merged.covered}/${merged.total} (${merged.pct.toFixed(1)}%, merged ${numSeeds} seeds)`
608
+ );
609
+
610
+ if (gaps.length > 0) {
611
+ appendLog("warn", `Coverage gaps: ${gaps.length} uncovered bins`);
612
+ }
613
+
614
+ return { artifacts, cov: merged, gaps, version, iteration, seedResults, numSeeds };
615
+ },
616
+ [appendLog]
617
+ );
618
+
619
+ const runPipeline = useCallback(
620
+ async (yamlSpec) => {
621
+ const spec = (() => {
622
+ try {
623
+ return JSON.parse(yamlSpec);
624
+ } catch {
625
+ return {};
626
+ }
627
+ })();
628
+ const design = spec?.design_name || "uart";
629
+
630
+ setState((prev) => ({
631
+ ...prev,
632
+ running: true,
633
+ logs: [],
634
+ artifacts: [],
635
+ error: null,
636
+ currentVersion: 0,
637
+ }));
638
+
639
+ appendLog("info", "Pipeline started — validation passed");
640
+ appendLog("info", "Generating UVM testbench...");
641
+
642
+ try {
643
+ const result = await runSingle(yamlSpec, 1, 1, 1);
644
+ const trend = [{ version: "v1", coverage: result.cov.pct }];
645
+
646
+ appendLog("success", "Pipeline completed");
647
+ setState((prev) => ({
648
+ ...prev,
649
+ running: false,
650
+ artifacts: result.artifacts,
651
+ versions: [{ version: "v1", coverage: result.cov.pct, files: result.artifacts.length }],
652
+ currentVersion: 1,
653
+ coverageTrend: trend,
654
+ coverageGaps: result.gaps,
655
+ autoTrainIteration: 1,
656
+ autoTrainMax: 1,
657
+ }));
658
+ } catch (err) {
659
+ appendLog("error", `Pipeline failed: ${err.message}`);
660
+ setState((prev) => ({ ...prev, running: false, error: err.message }));
661
+ }
662
+ },
663
+ [appendLog, runSingle]
664
+ );
665
+
666
+ const runAutoTrain = useCallback(
667
+ async (yamlSpec, maxIterations) => {
668
+ const spec = (() => {
669
+ try {
670
+ return JSON.parse(yamlSpec);
671
+ } catch {
672
+ return {};
673
+ }
674
+ })();
675
+ const design = spec?.design_name || "uart";
676
+
677
+ setState((prev) => ({
678
+ ...prev,
679
+ running: true,
680
+ autoTraining: true,
681
+ logs: [],
682
+ artifacts: [],
683
+ error: null,
684
+ versions: [],
685
+ currentVersion: 0,
686
+ coverageTrend: [],
687
+ coverageGaps: [],
688
+ autoTrainIteration: 0,
689
+ autoTrainMax: maxIterations,
690
+ }));
691
+
692
+ appendLog("info", `Auto-training started — max ${maxIterations} iterations`);
693
+ appendLog("info", "Pipeline started — validation passed");
694
+
695
+ let allVersions = [];
696
+ let trend = [];
697
+ let latestArtifacts = [];
698
+ let latestGaps = [];
699
+ let latestSeedResults = [];
700
+ let iteration = 0;
701
+
702
+ for (iteration = 1; iteration <= maxIterations; iteration++) {
703
+ appendLog("info", `=== Auto-train iteration ${iteration}/${maxIterations} ===`);
704
+ appendLog("info", `Generating testbench (v${iteration})...`);
705
+
706
+ try {
707
+ const result = await runSingle(yamlSpec, iteration, iteration, maxIterations, 3);
708
+
709
+ const ver = {
710
+ version: `v${iteration}`,
711
+ coverage: result.cov.pct,
712
+ files: result.artifacts.length,
713
+ };
714
+ allVersions.push(ver);
715
+ trend.push({ version: `v${iteration}`, coverage: result.cov.pct });
716
+ latestArtifacts = result.artifacts;
717
+ latestGaps = result.gaps;
718
+ latestSeedResults = result.seedResults || [];
719
+
720
+ // Check convergence
721
+ if (result.cov.pct >= 90) {
722
+ appendLog("success", `Coverage target ≥90% reached (${result.cov.pct.toFixed(1)}%)`);
723
+ break;
724
+ }
725
+
726
+ if (iteration >= 2) {
727
+ const prev = trend[trend.length - 2]?.coverage || 0;
728
+ const gain = result.cov.pct - prev;
729
+ appendLog("info", `Coverage gain: ${gain > 0 ? "+" : ""}${gain.toFixed(1)}%`);
730
+ if (gain < 2 && iteration >= 3) {
731
+ appendLog("warn", `Gain too low (<2%) — stopping early`);
732
+ break;
733
+ }
734
+ }
735
+
736
+ // Generate targeted sequences for next iteration
737
+ if (result.gaps.length > 0) {
738
+ const addrSet = new Set();
739
+ result.gaps.forEach((g) => {
740
+ const m = g.bin.match(/regs\[(\d+)\]/);
741
+ if (m) addrSet.add(parseInt(m[1]));
742
+ });
743
+ if (addrSet.size > 0) {
744
+ appendLog("info", `Targeting uncovered regs: [${[...addrSet].join(", ")}]`);
745
+ }
746
+ }
747
+
748
+ await new Promise((r) => setTimeout(r, 200));
749
+ } catch (err) {
750
+ appendLog("error", `Iteration ${iteration} failed: ${err.message}`);
751
+ break;
752
+ }
753
+ }
754
+
755
+ appendLog(
756
+ iteration >= maxIterations ? "warn" : "success",
757
+ `Auto-training complete — ${iteration} iterations, ${allVersions.length} versions`
758
+ );
759
+
760
+ setState((prev) => ({
761
+ ...prev,
762
+ running: false,
763
+ autoTraining: false,
764
+ artifacts: latestArtifacts,
765
+ versions: allVersions,
766
+ currentVersion: allVersions.length,
767
+ coverageTrend: trend,
768
+ coverageGaps: latestGaps,
769
+ seedResults: latestSeedResults,
770
+ autoTrainIteration: iteration,
771
+ autoTrainMax: maxIterations,
772
+ }));
773
+ },
774
+ [appendLog, runSingle]
775
+ );
776
+
777
+ return { ...state, runPipeline, runAutoTrain };
778
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
6
+
7
+ @layer base {
8
+ body {
9
+ @apply font-sans text-sm leading-relaxed;
10
+ }
11
+ h1 { @apply text-2xl font-bold tracking-tight; }
12
+ h2 { @apply text-lg font-semibold; }
13
+ h3 { @apply text-base font-semibold; }
14
+ }
15
+
16
+ @layer components {
17
+ .card {
18
+ @apply bg-white rounded-xl border border-slate-200 shadow-sm;
19
+ }
20
+ .card-header {
21
+ @apply px-5 py-4 border-b border-slate-100 flex items-center gap-2;
22
+ }
23
+ .card-body {
24
+ @apply p-5;
25
+ }
26
+ .btn-primary {
27
+ @apply inline-flex items-center gap-2 rounded-lg bg-brand-600 px-5 py-2.5 text-sm font-semibold text-white
28
+ shadow-sm hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
29
+ transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
30
+ }
31
+ .btn-secondary {
32
+ @apply inline-flex items-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium
33
+ text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2
34
+ transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
35
+ }
36
+ .input-field {
37
+ @apply block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm
38
+ placeholder:text-slate-400 focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20
39
+ transition-colors;
40
+ }
41
+ .select-field {
42
+ @apply block w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm
43
+ focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20 transition-colors;
44
+ }
45
+ .badge {
46
+ @apply inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium;
47
+ }
48
+ .badge-success { @apply badge bg-emerald-50 text-emerald-700; }
49
+ .badge-error { @apply badge bg-red-50 text-red-700; }
50
+ .badge-warning { @apply badge bg-amber-50 text-amber-700; }
51
+ .badge-info { @apply badge bg-sky-50 text-sky-700; }
52
+
53
+ .log-entry { @apply font-mono text-xs leading-6; }
54
+ .log-info { @apply log-entry text-slate-600; }
55
+ .log-error { @apply log-entry text-red-600; }
56
+ .log-success { @apply log-entry text-emerald-600; }
57
+ .log-warn { @apply log-entry text-amber-600; }
58
+ }
frontend/src/index.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App";
5
+
6
+ const root = ReactDOM.createRoot(document.getElementById("root"));
7
+ root.render(
8
+ <React.StrictMode>
9
+ <App />
10
+ </React.StrictMode>
11
+ );
frontend/src/utils/yamlUtils.js ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yaml from "js-yaml";
2
+
3
+ const PROTOCOLS = ["uart", "spi", "i2c", "axi4lite", "apb", "wishbone", "custom"];
4
+
5
+ const DEFAULT_YAML = `# UART 16550 Core Specification
6
+ design_name: uart16550
7
+ version: "1.5"
8
+ vendor: "DV Automation"
9
+ description: "Universal Asynchronous Receiver-Transmitter"
10
+ clock_reset:
11
+ clock: clk
12
+ reset: rst_n
13
+ reset_active: 0
14
+ interfaces:
15
+ - name: bus
16
+ protocol: wishbone
17
+ signals:
18
+ - { name: addr, direction: input, width: 3 }
19
+ - { name: data_in, direction: input, width: 8 }
20
+ - { name: data_out,direction: output, width: 8 }
21
+ - { name: we, direction: input, width: 1 }
22
+ - { name: irq, direction: output, width: 1 }
23
+ - name: serial
24
+ protocol: uart
25
+ signals:
26
+ - { name: srx, direction: input, width: 1 }
27
+ - { name: stx, direction: output, width: 1 }
28
+ registers:
29
+ - name: LCR
30
+ address: '0x03'
31
+ access: rw
32
+ fields:
33
+ - { name: wls, bits: '1:0', description: "Word length" }
34
+ - { name: stb, bits: '2', description: "Stop bits" }
35
+ - { name: dlab, bits: '7', description: "Divisor latch" }
36
+ - name: LSR
37
+ address: '0x05'
38
+ access: ro
39
+ fields:
40
+ - { name: dr, bits: '0', description: "Data ready" }
41
+ - { name: thre, bits: '5', description: "THR empty" }
42
+ `;
43
+
44
+ export function parseYAML(text) {
45
+ const doc = yaml.load(text);
46
+ return doc || {};
47
+ }
48
+
49
+ export function validateYAML(text) {
50
+ const errors = [];
51
+ try {
52
+ const doc = yaml.load(text);
53
+ if (!doc || typeof doc !== "object") {
54
+ errors.push("Root must be a YAML mapping (object)");
55
+ return errors;
56
+ }
57
+ if (!doc.design_name) errors.push("Missing required field: design_name");
58
+ if (!doc.interfaces || !doc.interfaces.length)
59
+ errors.push("At least one interface required");
60
+ if (doc.interfaces) {
61
+ doc.interfaces.forEach((iface, i) => {
62
+ if (!iface.name) errors.push(`Interface #${i + 1} missing name`);
63
+ if (!Array.isArray(iface.signals) || iface.signals.length === 0)
64
+ errors.push(`Interface "${iface.name || "#" + (i + 1)}" has no signals`);
65
+ });
66
+ }
67
+ if (doc.registers) {
68
+ doc.registers.forEach((reg, i) => {
69
+ if (!reg.name) errors.push(`Register #${i + 1} missing name`);
70
+ if (!reg.address) errors.push(`Register "${reg.name || "#" + (i + 1)}" missing address`);
71
+ if (reg.address && !/^0x[0-9a-fA-F]+$/.test(String(reg.address)))
72
+ errors.push(`Register "${reg.name}" address should be hex (0x...)`);
73
+ });
74
+ }
75
+ } catch (e) {
76
+ errors.push(`YAML parse error: ${e.message}`);
77
+ }
78
+ return errors;
79
+ }
80
+
81
+ export function toYAML(obj) {
82
+ return yaml.dump(obj, { indent: 2, lineWidth: 120, noRefs: true });
83
+ }
84
+
85
+ export { PROTOCOLS, DEFAULT_YAML };
frontend/tailwind.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ["./src/**/*.{js,jsx}"],
4
+ theme: {
5
+ extend: {
6
+ colors: {
7
+ brand: {
8
+ 50: "#eef2ff",
9
+ 100: "#e0e7ff",
10
+ 200: "#c7d2fe",
11
+ 300: "#a5b4fc",
12
+ 400: "#818cf8",
13
+ 500: "#6366f1",
14
+ 600: "#4f46e5",
15
+ 700: "#4338ca",
16
+ 800: "#3730a3",
17
+ 900: "#312e81",
18
+ 950: "#1e1b4b",
19
+ },
20
+ },
21
+ fontFamily: {
22
+ sans: ["Inter", "system-ui", "-apple-system", "sans-serif"],
23
+ mono: ["JetBrains Mono", "Fira Code", "monospace"],
24
+ },
25
+ },
26
+ },
27
+ plugins: [],
28
+ };
protocols/apb.yaml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # APB Protocol Definition — UVM TB Generator
2
+ protocol: apb
3
+ description: Advanced Peripheral Bus (AMBA 3 APB)
4
+ tags: [bus, memory-mapped, synchronous, low-power]
5
+
6
+ interface_template:
7
+ signals:
8
+ - {name: psel, direction: input, width: 1, description: Select}
9
+ - {name: penable, direction: input, width: 1, description: Enable}
10
+ - {name: paddr, direction: input, width: 32, description: Address}
11
+ - {name: pwrite, direction: input, width: 1, description: Write (1) / Read (0)}
12
+ - {name: pwdata, direction: input, width: 32, description: Write data}
13
+ - {name: prdata, direction: output, width: 32, description: Read data}
14
+ - {name: pready, direction: output, width: 1, description: Ready}
15
+ - {name: pslverr, direction: output, width: 1, description: Slave error}
16
+
17
+ config_parameters:
18
+ - {name: ADDR_WIDTH, type: int, default: 32}
19
+ - {name: DATA_WIDTH, type: int, default: 32}
20
+
21
+ register_template:
22
+ - name: APB_CTRL
23
+ address: 0x00
24
+ access: rw
25
+ fields:
26
+ - {name: enable, bits: 0, description: Core enable}
27
+ - name: APB_DATA
28
+ address: 0x04
29
+ access: rw
30
+ fields:
31
+ - {name: data, bits: 31:0, description: Data value}
32
+
33
+ sequence_template:
34
+ name: apb_sequence
35
+ body: |
36
+ // APB write sequence
37
+ paddr = addr;
38
+ pwdata = data;
39
+ pwrite = 1;
40
+ psel = 1;
41
+ @(posedge clk);
42
+ penable = 1;
43
+ wait(pready);
44
+ @(posedge clk);
45
+ psel = 0; penable = 0;
46
+
47
+ coverage_template:
48
+ - name: apb_cg
49
+ type: covergroup
50
+ items:
51
+ - {name: cg_op, type: coverpoint, expression: pwrite}
protocols/axi4lite.yaml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AXI4-Lite Protocol Definition — UVM TB Generator
2
+ protocol: axi4lite
3
+ description: AXI4-Lite — simplified address-only bus
4
+ tags: [bus, memory-mapped, synchronous, AMBA]
5
+
6
+ interface_template:
7
+ signals:
8
+ # Write address channel
9
+ - {name: awvalid, direction: input, width: 1, description: Write address valid}
10
+ - {name: awready, direction: output, width: 1, description: Write address ready}
11
+ - {name: awaddr, direction: input, width: 32, description: Write address}
12
+ - {name: awprot, direction: input, width: 3, description: Protection type}
13
+ # Write data channel
14
+ - {name: wvalid, direction: input, width: 1, description: Write data valid}
15
+ - {name: wready, direction: output, width: 1, description: Write data ready}
16
+ - {name: wdata, direction: input, width: 32, description: Write data}
17
+ - {name: wstrb, direction: input, width: 4, description: Write strobes}
18
+ # Write response channel
19
+ - {name: bvalid, direction: output, width: 1, description: Write response valid}
20
+ - {name: bready, direction: input, width: 1, description: Write response ready}
21
+ - {name: bresp, direction: output, width: 2, description: Write response}
22
+ # Read address channel
23
+ - {name: arvalid, direction: input, width: 1, description: Read address valid}
24
+ - {name: arready, direction: output, width: 1, description: Read address ready}
25
+ - {name: araddr, direction: input, width: 32, description: Read address}
26
+ - {name: arprot, direction: input, width: 3, description: Protection type}
27
+ # Read data channel
28
+ - {name: rvalid, direction: output, width: 1, description: Read data valid}
29
+ - {name: rready, direction: input, width: 1, description: Read data ready}
30
+ - {name: rdata, direction: output, width: 32, description: Read data}
31
+ - {name: rresp, direction: output, width: 2, description: Read response}
32
+
33
+ config_parameters:
34
+ - {name: ADDR_WIDTH, type: int, default: 32, enum: [12, 16, 32]}
35
+ - {name: DATA_WIDTH, type: int, default: 32, enum: [32, 64]}
36
+
37
+ register_template:
38
+ - name: AXI_CTRL
39
+ address: 0x0000
40
+ access: rw
41
+ fields:
42
+ - {name: enable, bits: 0, description: Core enable}
43
+ - {name: irq_en, bits: 1, description: Interrupt enable}
44
+ - name: AXI_STATUS
45
+ address: 0x0004
46
+ access: ro
47
+ fields:
48
+ - {name: ready, bits: 0, description: Core ready}
49
+ - {name: error, bits: 1, description: Error detected}
50
+
51
+ sequence_template:
52
+ name: axi4lite_sequence
53
+ body: |
54
+ // AXI4-Lite read-modify-write sequence
55
+ drv.axi_read_reg(base_addr + offset, read_data);
56
+ read_data[field] = value;
57
+ drv.axi_write_reg(base_addr + offset, read_data);
58
+
59
+ coverage_template:
60
+ - name: axi_cg
61
+ type: covergroup
62
+ items:
63
+ - {name: cg_resp, type: coverpoint, expression: {bresp, rresp}}
64
+ - {name: cg_addr, type: coverpoint, expression: {awaddr, araddr}}
protocols/i2c.yaml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # I2C Protocol Definition — UVM TB Generator
2
+ protocol: i2c
3
+ description: Inter-Integrated Circuit
4
+ tags: [serial, synchronous, multi-master, open-drain]
5
+
6
+ interface_template:
7
+ signals:
8
+ - {name: scl, direction: inout, width: 1, description: Serial clock line}
9
+ - {name: sda, direction: inout, width: 1, description: Serial data line}
10
+
11
+ config_parameters:
12
+ - {name: SLAVE_ADDR, type: int, default: 0x50, minimum: 0x00, maximum: 0x7F, description: 7-bit slave address}
13
+ - {name: CLK_FREQ, type: int, default: 100000, description: I2C bus frequency (Hz)}
14
+ - {name: ADDR_WIDTH, type: int, default: 7, enum: [7, 10], description: Addressing mode}
15
+
16
+ register_template:
17
+ - name: CR # Control Register
18
+ address: 0x00
19
+ access: rw
20
+ fields:
21
+ - {name: enable, bits: 0, description: I2C core enable}
22
+ - {name: irq_en, bits: 1, description: Interrupt enable}
23
+ - {name: addr_mode, bits: 2, description: 0=7bit, 1=10bit}
24
+ - name: ADR # Slave Address Register
25
+ address: 0x04
26
+ access: rw
27
+ fields:
28
+ - {name: addr, bits: 9:0, description: Slave address}
29
+ - name: DR # Data Register
30
+ address: 0x08
31
+ access: rw
32
+ fields:
33
+ - {name: data, bits: 7:0, description: I2C data byte}
34
+ - name: SR # Status Register
35
+ address: 0x0C
36
+ access: ro
37
+ fields:
38
+ - {name: busy, bits: 0, description: Bus busy}
39
+ - {name: rx_ack, bits: 1, description: Receive acknowledge}
40
+ - {name: tx_rdy, bits: 2, description: TX ready}
41
+ - {name: rx_rdy, bits: 3, description: RX ready}
42
+ - {name: arb_lost,bits: 4, description: Arbitration lost}
43
+ - {name: nack, bits: 5, description: No acknowledge received}
44
+
45
+ sequence_template:
46
+ name: i2c_sequence
47
+ body: |
48
+ // I2C write sequence
49
+ drv.write_reg(ADR, slave_addr);
50
+ drv.write_reg(CR, {1'b1, enable});
51
+ foreach (tx_data[i]) begin
52
+ drv.write_reg(DR, tx_data[i]);
53
+ drv.wait_for_field(SR, tx_rdy, 1);
54
+ end
55
+
56
+ coverage_template:
57
+ - name: i2c_cg
58
+ type: covergroup
59
+ items:
60
+ - {name: cg_addr, type: coverpoint, expression: slave_addr}
61
+ - {name: cg_dir, type: coverpoint, expression: {read, write}}
protocols/spi.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPI Protocol Definition — UVM TB Generator
2
+ protocol: spi
3
+ description: Serial Peripheral Interface
4
+ tags: [serial, synchronous, full-duplex, master-slave]
5
+
6
+ interface_template:
7
+ signals:
8
+ - {name: sclk, direction: input, width: 1, description: Serial clock}
9
+ - {name: mosi, direction: input, width: 1, description: Master out slave in}
10
+ - {name: miso, direction: output, width: 1, description: Master in slave out}
11
+ - {name: ss_n, direction: input, width: 1, description: Slave select (active-low)}
12
+
13
+ config_parameters:
14
+ - {name: CPOL, type: int, default: 0, enum: [0, 1], description: Clock polarity}
15
+ - {name: CPHA, type: int, default: 0, enum: [0, 1], description: Clock phase}
16
+ - {name: DATA_WIDTH, type: int, default: 8, enum: [4, 8, 16, 32], description: Data width}
17
+ - {name: MSB_FIRST, type: bool, default: true, description: MSB-first transfer}
18
+
19
+ register_template:
20
+ - name: CR0 # Control Register 0
21
+ address: 0x00
22
+ access: rw
23
+ fields:
24
+ - {name: div, bits: 7:0, description: Clock divider}
25
+ - {name: cpol, bits: 8, description: Clock polarity}
26
+ - {name: cpha, bits: 9, description: Clock phase}
27
+ - {name: data_width,bits: 15:12,description: Data width minus 1}
28
+ - name: DR # Data Register
29
+ address: 0x04
30
+ access: rw
31
+ fields:
32
+ - {name: data, bits: 15:0, description: SPI data}
33
+ - name: SR # Status Register
34
+ address: 0x08
35
+ access: ro
36
+ fields:
37
+ - {name: busy, bits: 0, description: Transfer in progress}
38
+ - {name: rxf, bits: 1, description: RX FIFO full}
39
+ - {name: txe, bits: 2, description: TX FIFO empty}
40
+
41
+ sequence_template:
42
+ name: spi_sequence
43
+ body: |
44
+ // SPI transmit sequence
45
+ cr0 = {cpol, cpha, div};
46
+ drv.write_reg(CR0, cr0);
47
+ foreach (tx_data[i]) begin
48
+ drv.write_reg(DR, tx_data[i]);
49
+ drv.wait_for_field(SR, busy, 0);
50
+ end
51
+
52
+ coverage_template:
53
+ - name: spi_cg
54
+ type: covergroup
55
+ items:
56
+ - {name: cg_mode, type: coverpoint, expression: {cpol, cpha}}
57
+ - {name: cg_width, type: coverpoint, expression: data_width}
protocols/uart.yaml ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UART Protocol Definition — UVM TB Generator
2
+ # Reference: 16550 / NS16C550 compatible
3
+ protocol: uart
4
+ description: Universal Asynchronous Receiver-Transmitter
5
+ tags: [serial, asynchronous, full-duplex]
6
+
7
+ interface_template:
8
+ signals:
9
+ - {name: srx, direction: input, width: 1, description: Serial receive}
10
+ - {name: stx, direction: output, width: 1, description: Serial transmit}
11
+ - {name: cts_n, direction: input, width: 1, description: Clear to send (active-low)}
12
+ - {name: rts_n, direction: output, width: 1, description: Request to send (active-low)}
13
+
14
+ baud_rates: [9600, 19200, 38400, 57600, 115200, 230400, 460800]
15
+
16
+ config_parameters:
17
+ - {name: DATA_BITS, type: int, default: 8, enum: [5, 6, 7, 8]}
18
+ - {name: STOP_BITS, type: int, default: 1, enum: [1, 2]}
19
+ - {name: PARITY, type: string, default: "none", enum: [none, even, odd, mark, space]}
20
+
21
+ register_template:
22
+ - name: RBR # Receiver Buffer Register
23
+ address: 0x00
24
+ access: ro
25
+ fields:
26
+ - {name: rbr_data, bits: 7:0, access: ro, description: Received data byte}
27
+ - name: THR # Transmitter Holding Register
28
+ address: 0x00
29
+ access: wo
30
+ fields:
31
+ - {name: thr_data, bits: 7:0, access: wo, description: Transmit data byte}
32
+ - name: IER # Interrupt Enable Register
33
+ address: 0x01
34
+ access: rw
35
+ fields:
36
+ - {name: erbfi, bits: 0, description: Enable RX data available interrupt}
37
+ - {name: etbei, bits: 1, description: Enable TX holding register empty interrupt}
38
+ - {name: elsi, bits: 2, description: Enable RX line status interrupt}
39
+ - {name: edssi, bits: 3, description: Enable modem status interrupt}
40
+ - name: IIR # Interrupt Identification Register
41
+ address: 0x02
42
+ access: ro
43
+ fields:
44
+ - {name: int_id, bits: 3:0, description: Interrupt type identifier}
45
+ - {name: fifos_en, bits: 7:6, description: FIFO enable status}
46
+ - name: FCR # FIFO Control Register
47
+ address: 0x02
48
+ access: wo
49
+ fields:
50
+ - {name: fifo_en, bits: 0, description: Enable FIFOs}
51
+ - {name: rclr, bits: 1, description: Clear RX FIFO}
52
+ - {name: tclr, bits: 2, description: Clear TX FIFO}
53
+ - {name: dma_mode, bits: 3, description: DMA mode select}
54
+ - {name: rx_trigger,bits: 7:6, description: RX FIFO trigger level}
55
+ - name: LCR # Line Control Register
56
+ address: 0x03
57
+ access: rw
58
+ fields:
59
+ - {name: wls, bits: 1:0, description: Word length select}
60
+ - {name: stb, bits: 2, description: Stop bits}
61
+ - {name: pen, bits: 3, description: Parity enable}
62
+ - {name: eps, bits: 4, description: Even parity select}
63
+ - {name: sp, bits: 5, description: Stick parity}
64
+ - {name: bc, bits: 6, description: Break control}
65
+ - {name: dlab, bits: 7, description: Divisor latch access bit}
66
+ - name: MCR # Modem Control Register
67
+ address: 0x04
68
+ access: rw
69
+ fields:
70
+ - {name: dtr, bits: 0, description: Data Terminal Ready}
71
+ - {name: rts, bits: 1, description: Request To Send}
72
+ - {name: out1, bits: 2, description: Output 1}
73
+ - {name: out2, bits: 3, description: Output 2}
74
+ - {name: loop, bits: 4, description: Loopback mode enable}
75
+ - name: LSR # Line Status Register
76
+ address: 0x05
77
+ access: ro
78
+ fields:
79
+ - {name: dr, bits: 0, description: Data Ready}
80
+ - {name: oe, bits: 1, description: Overrun Error}
81
+ - {name: pe, bits: 2, description: Parity Error}
82
+ - {name: fe, bits: 3, description: Framing Error}
83
+ - {name: bi, bits: 4, description: Break Interrupt}
84
+ - {name: thre, bits: 5, description: TX Holding Register Empty}
85
+ - {name: temt, bits: 6, description: Transmitter Empty}
86
+ - {name: err, bits: 7, description: Error in RX FIFO}
87
+ - name: MSR # Modem Status Register
88
+ address: 0x06
89
+ access: ro
90
+ fields:
91
+ - {name: dcts, bits: 0, description: Delta Clear To Send}
92
+ - {name: ddsr, bits: 1, description: Delta Data Set Ready}
93
+ - {name: teri, bits: 2, description: Trailing Edge Ring Indicator}
94
+ - {name: ddcd, bits: 3, description: Delta Data Carrier Detect}
95
+ - {name: cts, bits: 4, description: Clear To Send}
96
+ - {name: dsr, bits: 5, description: Data Set Ready}
97
+ - {name: ri, bits: 6, description: Ring Indicator}
98
+ - {name: dcd, bits: 7, description: Data Carrier Detect}
99
+ - name: SCR # Scratch Register
100
+ address: 0x07
101
+ access: rw
102
+ fields:
103
+ - {name: scratch, bits: 7:0, description: Scratch value}
104
+
105
+ sequence_template:
106
+ name: uart_sequence
107
+ body: |
108
+ // UART transmit sequence
109
+ repeat (num_bytes) begin
110
+ drv.write_reg(THR, data_q.pop_front());
111
+ drv.wait_for_field(LSR, thre, 1);
112
+ end
113
+
114
+ coverage_template:
115
+ - name: uart_cg
116
+ type: covergroup
117
+ items:
118
+ - {name: cg_baud, type: coverpoint, expression: baud_rate, bins: [9600, 19200, 115200]}
119
+ - {name: cg_frame, type: coverpoint, expression: {data_bits, stop_bits, parity}}
protocols/wishbone.yaml ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Wishbone Protocol Definition — UVM TB Generator
2
+ protocol: wishbone
3
+ description: Wishbone B4 bus interface
4
+ tags: [bus, memory-mapped, synchronous, pipelined]
5
+
6
+ interface_template:
7
+ signals:
8
+ - {name: cyc, direction: input, width: 1, description: Cycle start}
9
+ - {name: stb, direction: input, width: 1, description: Strobe}
10
+ - {name: we, direction: input, width: 1, description: Write enable}
11
+ - {name: adr, direction: input, width: 32, description: Address bus}
12
+ - {name: dat_i,direction: input, width: 32, description: Write data}
13
+ - {name: dat_o,direction: output, width: 32, description: Read data}
14
+ - {name: ack, direction: output, width: 1, description: Acknowledge}
15
+ - {name: err, direction: output, width: 1, description: Error}
16
+ - {name: rty, direction: output, width: 1, description: Retry}
17
+
18
+ config_parameters:
19
+ - {name: ADDR_WIDTH, type: int, default: 32}
20
+ - {name: DATA_WIDTH, type: int, default: 32, enum: [8, 16, 32, 64]}
21
+ - {name: GRANULARITY,type: string, default: "byte", enum: [byte, word]}
22
+
23
+ register_template:
24
+ - name: WB_CTRL
25
+ address: 0x00
26
+ access: rw
27
+ fields:
28
+ - {name: enable, bits: 0, description: Core enable}
29
+ - name: WB_STATUS
30
+ address: 0x04
31
+ access: ro
32
+ fields:
33
+ - {name: ready, bits: 0, description: Core ready}
34
+
35
+ sequence_template:
36
+ name: wishbone_sequence
37
+ body: |
38
+ // Wishbone classic write sequence
39
+ adr = addr; dat_i = data; we = 1;
40
+ cyc = 1; stb = 1;
41
+ wait(ack);
42
+ cyc = 0; stb = 0;
43
+
44
+ coverage_template:
45
+ - name: wb_cg
46
+ type: covergroup
47
+ items:
48
+ - {name: cg_op, type: coverpoint, expression: we}
pyproject.toml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.backends._legacy:_Backend"
4
+
5
+ [project]
6
+ name = "uvm-tb-generator"
7
+ version = "0.3.0"
8
+ description = "Industry-grade UVM Testbench Generator with protocol libraries, schema validation, and CI/CD"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "DV Automation Team"},
14
+ ]
15
+ keywords = ["uvm", "testbench", "verification", "systemverilog", "eda", "automation"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
25
+ ]
26
+ dependencies = [
27
+ "pyyaml>=6.0",
28
+ "jinja2>=3.0",
29
+ "pydantic>=2.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7.0",
35
+ "pytest-cov>=4.0",
36
+ "flake8>=7.0",
37
+ "black>=24.0",
38
+ "yamllint>=1.35",
39
+ "jsonschema>=4.0",
40
+ "build>=1.0",
41
+ ]
42
+ mlflow = ["mlflow>=2.0"]
43
+
44
+ [project.urls]
45
+ Repository = "https://github.com/your-org/uvm-tb-generator"
46
+ Documentation = "https://github.com/your-org/uvm-tb-generator/docs"
47
+
48
+ [project.scripts]
49
+ uvmgen = "src.main:main"
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ addopts = "-v --tb=short"
54
+
55
+ [tool.coverage.run]
56
+ source = ["src"]
57
+ omit = ["*/tests/*"]
58
+
59
+ [tool.flake8]
60
+ max-line-length = 120
61
+ extend-ignore = ["E203", "W503"]
render.yaml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Render.com deployment — free tier, single service
2
+ # Steps:
3
+ # 1. Push repo to GitHub
4
+ # 2. Go to https://dashboard.render.com/select-repo
5
+ # 3. Connect this repo — Render auto-reads this file
6
+ # 4. Deploy (zero config)
7
+
8
+ services:
9
+ - type: web
10
+ name: uvm-tb-generator
11
+ runtime: python
12
+ region: oregon
13
+ plan: free
14
+ buildCommand: |
15
+ pip install -r requirements.txt
16
+ cd frontend && npm ci && npm run build && cd ..
17
+ startCommand: gunicorn src.api.server:app --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT --workers 1 --timeout 120
18
+ envVars:
19
+ - key: PYTHONUNBUFFERED
20
+ value: "1"
21
+ - key: LOG_LEVEL
22
+ value: info
23
+ - key: UVMGEN_OUTPUT_DIR
24
+ value: /var/data/uvmgen_output
25
+ healthCheckPath: /api/health
26
+ autoDeploy: true
27
+ disk:
28
+ name: uvmgen-data
29
+ mountPath: /var/data
30
+ sizeGB: 1
requirements-dev.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ pytest>=7.0
2
+ pytest-cov>=4.0
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ pyyaml>=6.0
2
+ jinja2>=3.0
3
+ pydantic>=2.0
4
+ fastapi>=0.115.0
5
+ uvicorn[standard]>=0.34.0
6
+ gunicorn>=23.0
setup.cfg ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [metadata]
2
+ name = uvm-tb-generator
3
+ version = 0.2.0
4
+ description = ML-style UVM Testbench Generator
5
+ author = DV Automation Team
6
+ license = MIT
7
+
8
+ [options]
9
+ packages = find:
10
+ python_requires = >=3.10
11
+ install_requires =
12
+ pyyaml>=6.0
13
+ jinja2>=3.0
14
+ pydantic>=2.0
15
+
16
+ [options.extras_require]
17
+ dev =
18
+ pytest>=7.0
19
+ pytest-cov>=4.0
20
+ mlflow =
21
+ mlflow>=2.0
22
+
23
+ [options.entry_points]
24
+ console_scripts =
25
+ uvmgen = src.main:main
src/__init__.py ADDED
File without changes
src/api/__init__.py ADDED
File without changes
src/api/server.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI backend for UVM TB Generator.
3
+ Serves both the REST API and the React frontend from a single process
4
+ for free-tier deployment (Render, Fly.io, PythonAnywhere).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ import traceback
12
+ from contextlib import asynccontextmanager
13
+ from pathlib import Path
14
+ from typing import List, Optional
15
+
16
+ from fastapi import FastAPI, HTTPException
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
19
+ from pydantic import BaseModel, Field
20
+
21
+ from src.config import ConfigLoader
22
+ from src.exceptions import UVMGenError
23
+ from src.pipeline import TBPipeline
24
+
25
+ logger = logging.getLogger("uvmgen")
26
+ logger.setLevel(logging.INFO)
27
+
28
+ # ── Find frontend build directory ─────────────────────────────────
29
+ HERE = Path(__file__).resolve().parent
30
+ PROJECT_ROOT = HERE.parent.parent
31
+ FRONTEND_BUILD = PROJECT_ROOT / "frontend" / "build"
32
+
33
+ # ── Request / Response models ─────────────────────────────────────
34
+
35
+ class PipelineRequest(BaseModel):
36
+ spec_yaml: str = Field(..., description="YAML or .core specification content")
37
+ design_name: str = Field(default="unnamed")
38
+ auto_train: bool = Field(default=False)
39
+ max_iterations: int = Field(default=5, ge=1, le=50)
40
+ coverage_target: float = Field(default=90.0, ge=0, le=100)
41
+ num_seeds: int = Field(default=3, ge=1, le=20)
42
+ overwrite: bool = Field(default=False)
43
+
44
+
45
+ class HealthResponse(BaseModel):
46
+ status: str = "ok"
47
+ version: str = "0.3.0"
48
+ api_version: str = "v1"
49
+ simulators: List[str] = ["stub", "icarus"]
50
+
51
+
52
+ class VersionInfo(BaseModel):
53
+ version: str
54
+ coverage: float
55
+ files: int
56
+ iteration: int
57
+
58
+
59
+ class PipelineResponse(BaseModel):
60
+ design_name: str
61
+ status: str
62
+ versions: List[VersionInfo]
63
+ coverage_trend: list
64
+ coverage_gaps: list
65
+ artifacts: list
66
+ total_files: int
67
+ iterations: int
68
+ simulator: str
69
+
70
+
71
+ # ── Pipeline singleton ─────────────────────────────────────────────
72
+
73
+ pipeline_instance: Optional[TBPipeline] = None
74
+
75
+
76
+ @asynccontextmanager
77
+ async def lifespan(app: FastAPI):
78
+ global pipeline_instance
79
+ logger.info("UVM TB Generator starting...")
80
+ pipeline_instance = TBPipeline()
81
+ yield
82
+ logger.info("UVM TB Generator shutting down...")
83
+
84
+
85
+ app = FastAPI(title="UVM TB Generator", version="0.3.0", lifespan=lifespan)
86
+
87
+ app.add_middleware(
88
+ CORSMiddleware,
89
+ allow_origins=["*"],
90
+ allow_credentials=True,
91
+ allow_methods=["*"],
92
+ allow_headers=["*"],
93
+ )
94
+
95
+
96
+ # ── Exception handlers ─────────────────────────────────────────────
97
+
98
+ @app.exception_handler(UVMGenError)
99
+ async def uvmgen_error_handler(request, exc: UVMGenError):
100
+ return JSONResponse(status_code=exc.status_code, content=exc.to_dict())
101
+
102
+
103
+ @app.exception_handler(Exception)
104
+ async def generic_error_handler(request, exc: Exception):
105
+ logger.error("Unhandled: %s\n%s", exc, traceback.format_exc())
106
+ return JSONResponse(
107
+ status_code=500,
108
+ content={"error": "INTERNAL_ERROR", "message": str(exc)},
109
+ )
110
+
111
+
112
+ # ── API Routes ─────────────────────────────────────────────────────
113
+
114
+ @app.get("/api/health", response_model=HealthResponse)
115
+ async def health_check():
116
+ return HealthResponse()
117
+
118
+
119
+ @app.get("/api/versions")
120
+ async def list_versions():
121
+ if not pipeline_instance:
122
+ raise HTTPException(503, "Pipeline not initialized")
123
+ trend = pipeline_instance.registry.coverage_trend()
124
+ return {"versions": trend}
125
+
126
+
127
+ @app.post("/api/run-pipeline", response_model=PipelineResponse)
128
+ async def run_pipeline(req: PipelineRequest):
129
+ global pipeline_instance
130
+ if not pipeline_instance:
131
+ pipeline_instance = TBPipeline()
132
+
133
+ try:
134
+ import tempfile, os as _os
135
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, encoding="utf-8") as f:
136
+ f.write(req.spec_yaml)
137
+ spec_path = f.name
138
+
139
+ pipeline_instance.cfg.auto_train.enabled = req.auto_train
140
+ pipeline_instance.cfg.auto_train.max_iterations = req.max_iterations
141
+ pipeline_instance.cfg.auto_train.coverage_target = req.coverage_target
142
+ pipeline_instance.cfg.auto_train.num_seeds = req.num_seeds
143
+ pipeline_instance.cfg.generation.overwrite = req.overwrite
144
+
145
+ result = pipeline_instance.run(spec_path)
146
+
147
+ try:
148
+ _os.unlink(spec_path)
149
+ except OSError:
150
+ pass
151
+
152
+ analysis = result.get("coverage_analysis") or {}
153
+ gaps = [{"bin": g["bin"], "addr": g.get("addr"), "dir": g.get("dir")}
154
+ for g in (analysis.get("gaps") or [])]
155
+
156
+ versions = []
157
+ for t in (result.get("coverage_trend") or []):
158
+ if isinstance(t, dict):
159
+ versions.append(VersionInfo(
160
+ version=t.get("version", "v0"),
161
+ coverage=float(t.get("coverage", 0)),
162
+ files=t.get("files", 0),
163
+ iteration=t.get("iteration", 0),
164
+ ))
165
+
166
+ artifacts = [
167
+ {"name": Path(p).name, "path": p}
168
+ for p in result.get("generated_files", {}).values()
169
+ ]
170
+
171
+ return PipelineResponse(
172
+ design_name=result.get("design_name", "unknown"),
173
+ status="passed" if result.get("passed") else "failed",
174
+ versions=versions,
175
+ coverage_trend=result.get("coverage_trend") or [],
176
+ coverage_gaps=gaps,
177
+ artifacts=artifacts,
178
+ total_files=len(artifacts),
179
+ iterations=result.get("auto_train_iterations", 0),
180
+ simulator=result.get("simulator", "stub"),
181
+ )
182
+ except UVMGenError:
183
+ raise
184
+ except Exception as e:
185
+ logger.error("Pipeline failed: %s", e)
186
+ raise HTTPException(500, detail=str(e))
187
+
188
+
189
+ @app.post("/api/validate-spec")
190
+ async def validate_spec(req: PipelineRequest):
191
+ try:
192
+ import tempfile, os as _os, yaml
193
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, encoding="utf-8") as f:
194
+ f.write(req.spec_yaml)
195
+ spec_path = f.name
196
+
197
+ loader = ConfigLoader()
198
+ spec, _ = loader.load(spec_path)
199
+ from src.data.validators import SpecValidator
200
+ validator = SpecValidator()
201
+ vr = validator.validate(spec, strict=True)
202
+ try:
203
+ _os.unlink(spec_path)
204
+ except OSError:
205
+ pass
206
+ return {"valid": bool(vr), "design_name": spec.design_name,
207
+ "errors": vr if vr else [], "registers": len(spec.registers),
208
+ "interfaces": len(spec.interfaces)}
209
+ except Exception as e:
210
+ raise HTTPException(422, detail=str(e))
211
+
212
+
213
+ # ── Serve frontend (single deploy) ─────────────────────────────────
214
+
215
+ _IS_BUILT = FRONTEND_BUILD.exists()
216
+ if _IS_BUILT:
217
+ logger.info("Serving frontend from %s", FRONTEND_BUILD)
218
+
219
+ from fastapi.responses import HTMLResponse
220
+
221
+
222
+ @app.get("/", include_in_schema=False)
223
+ @app.get("/index.html", include_in_schema=False)
224
+ async def serve_index():
225
+ if _IS_BUILT:
226
+ index = FRONTEND_BUILD / "index.html"
227
+ if index.exists():
228
+ return FileResponse(str(index))
229
+ return HTMLResponse("<h1>UVM TB Generator API</h1><p>Frontend not built. Run <code>cd frontend && npm run build</code></p>")
230
+
231
+
232
+ @app.get("/static/{rest_of_path:path}", include_in_schema=False)
233
+ async def serve_static(rest_of_path: str):
234
+ if not _IS_BUILT:
235
+ return JSONResponse(404, {"error": "Not found"})
236
+ file_path = FRONTEND_BUILD / "static" / rest_of_path
237
+ if file_path.exists() and file_path.is_file():
238
+ return FileResponse(str(file_path))
239
+ return JSONResponse(404, {"error": "Not found"})
240
+
241
+
242
+ @app.get("/{full_path:path}", include_in_schema=False)
243
+ async def serve_spa(full_path: str):
244
+ if full_path.startswith("api/") or full_path.startswith("docs") or full_path.startswith("openapi"):
245
+ return JSONResponse(status_code=404, content={"error": "Not found"})
246
+ if _IS_BUILT:
247
+ file_path = FRONTEND_BUILD / full_path
248
+ if file_path.exists() and file_path.is_file():
249
+ return FileResponse(str(file_path))
250
+ index = FRONTEND_BUILD / "index.html"
251
+ if index.exists():
252
+ return FileResponse(str(index))
253
+ return JSONResponse(404, {"error": "Not found"})
254
+
255
+
256
+ # ── Direct execution ───────────────────────────────────────────────
257
+
258
+ if __name__ == "__main__":
259
+ import uvicorn
260
+ uvicorn.run("src.api.server:app", host="0.0.0.0", port=8000, reload=True)
src/config.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/config.py — Central configuration with Pydantic validation
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional, Any
8
+
9
+ from pydantic import BaseModel, Field
10
+ import yaml
11
+
12
+
13
+ # ── Data Models ──────────────────────────────────────────────────────────────
14
+
15
+ class SignalDef(BaseModel):
16
+ name: str
17
+ direction: str = Field(pattern=r"^(input|output|inout)$")
18
+ width: Optional[int] = 1
19
+
20
+ class InterfaceDef(BaseModel):
21
+ name: str
22
+ signals: List[SignalDef] = Field(min_length=1)
23
+
24
+ class FieldDef(BaseModel):
25
+ name: str
26
+ bits: str
27
+ description: Optional[str] = None
28
+
29
+ class RegisterDef(BaseModel):
30
+ name: str
31
+ address: str
32
+ fields: List[FieldDef] = []
33
+ description: Optional[str] = None
34
+ access: Optional[str] = None
35
+ size: Optional[int] = None
36
+ reset_value: Optional[str] = None
37
+ volatile: bool = False
38
+
39
+ class ClockResetDef(BaseModel):
40
+ clock: str = "clk"
41
+ reset: str = "rst_n"
42
+ reset_active: int = Field(default=0, ge=0, le=1)
43
+
44
+ class DesignSpec(BaseModel):
45
+ design_name: str = Field(min_length=1, pattern=r"^[a-zA-Z_][a-zA-Z0-9_]*$")
46
+ clock_reset: ClockResetDef = ClockResetDef()
47
+ interfaces: List[InterfaceDef] = Field(min_length=1)
48
+ registers: List[RegisterDef] = []
49
+ protocol: str = Field(default="", pattern=r"^(uart|spi|i2c|axi4lite|apb|wishbone|)$")
50
+
51
+
52
+ # ── Pipeline / Engine Config ─────────────────────────────────────────────────
53
+
54
+ class LoggingConfig(BaseModel):
55
+ level: str = Field(default="INFO", pattern=r"^(DEBUG|INFO|WARNING|ERROR)$")
56
+ file: Optional[str] = None
57
+ format: str = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
58
+
59
+ class EvaluationConfig(BaseModel):
60
+ enabled: bool = True
61
+ metrics: List[str] = ["completeness", "syntax_validity", "coverage_readiness"]
62
+ threshold: float = Field(default=0.7, ge=0.0, le=1.0)
63
+
64
+ class TrackingConfig(BaseModel):
65
+ enabled: bool = False
66
+ backend: str = Field(default="local", pattern=r"^(local|mlflow)$")
67
+ experiment_name: Optional[str] = None
68
+ tracking_uri: Optional[str] = None
69
+
70
+ class GenerationConfig(BaseModel):
71
+ templates_dir: str = "src/generation/templates"
72
+ output_dir: str = "output"
73
+ overwrite: bool = False
74
+ strict_validation: bool = True
75
+ iteration: int = Field(default=0, ge=0)
76
+
77
+ class AutoTrainConfig(BaseModel):
78
+ enabled: bool = False
79
+ max_iterations: int = Field(default=5, ge=1, le=50)
80
+ coverage_target: float = Field(default=90.0, ge=0.0, le=100.0)
81
+ coverage_gain_min: float = Field(default=2.0, ge=0.0, description="Min % gain per iteration to continue")
82
+ simulator: str = Field(default="stub", pattern=r"^(stub|icarus|vcs|questa)$")
83
+ sim_timeout: int = Field(default=300, ge=10)
84
+ num_seeds: int = Field(default=3, ge=1, le=20, description="Number of regression seeds per iteration")
85
+ generate_regression_test: bool = True
86
+
87
+ class PipelineConfig(BaseModel):
88
+ generation: GenerationConfig = GenerationConfig()
89
+ evaluation: EvaluationConfig = EvaluationConfig()
90
+ tracking: TrackingConfig = TrackingConfig()
91
+ logging: LoggingConfig = LoggingConfig()
92
+ auto_train: AutoTrainConfig = AutoTrainConfig()
93
+
94
+
95
+ # ── Config Loader ────────────────────────────────────────────────────────────
96
+
97
+ class ConfigLoader:
98
+ """Hierarchical config loader with env override support.
99
+
100
+ Load order (later overrides earlier):
101
+ 1. Base defaults
102
+ 2. <env>.yaml (e.g. configs/production.yaml)
103
+ 3. Environment variables (UVMGEN_* prefix)
104
+ """
105
+
106
+ ENV_PREFIX = "UVMGEN_"
107
+
108
+ def __init__(self, root: Optional[str] = None):
109
+ self.root = Path(root or os.getcwd())
110
+
111
+ def load(self, spec_path: str, pipeline_path: Optional[str] = None) -> tuple[DesignSpec, PipelineConfig]:
112
+ design_spec = self._load_design_spec(spec_path)
113
+ pipeline_cfg = self._load_pipeline(pipeline_path)
114
+ self._apply_env_overrides(pipeline_cfg)
115
+ return design_spec, pipeline_cfg
116
+
117
+ def _load_design_spec(self, path: str) -> DesignSpec:
118
+ from src.data.preprocessor import SpecPreprocessor
119
+ from src.data.core_parser import CoreParser
120
+
121
+ ext = Path(path).suffix.lower()
122
+ if ext == ".core":
123
+ raw = CoreParser().parse(Path(path).read_text(encoding="utf-8"))
124
+ else:
125
+ raw = self._read_yaml(path)
126
+ raw = SpecPreprocessor().preprocess(raw)
127
+ return DesignSpec(**raw)
128
+
129
+ def _load_pipeline(self, path: Optional[str] = None) -> PipelineConfig:
130
+ base = PipelineConfig()
131
+ if path and Path(path).exists():
132
+ overrides = self._read_yaml(path)
133
+ base = self._deep_merge(base, overrides)
134
+ return base
135
+
136
+ def _apply_env_overrides(self, cfg: PipelineConfig) -> None:
137
+ prefix = self.ENV_PREFIX
138
+ for key, val in os.environ.items():
139
+ if key.startswith(prefix):
140
+ parts = key[len(prefix):].lower().split("__")
141
+ target = cfg
142
+ for part in parts[:-1]:
143
+ target = getattr(target, part, None)
144
+ if target is None:
145
+ break
146
+ else:
147
+ last = parts[-1]
148
+ if hasattr(target, last):
149
+ setattr(target, last, self._coerce(val, type(getattr(target, last))))
150
+
151
+ @staticmethod
152
+ def _read_yaml(path: str) -> dict:
153
+ with open(path, "r") as f:
154
+ return yaml.safe_load(f)
155
+
156
+ @staticmethod
157
+ def _coerce(val: str, typ: type) -> Any:
158
+ if typ is bool:
159
+ return val.lower() in ("1", "true", "yes")
160
+ if typ is int:
161
+ return int(val)
162
+ if typ is float:
163
+ return float(val)
164
+ return val
165
+
166
+ @staticmethod
167
+ def _deep_merge(base: PipelineConfig, overrides: dict) -> PipelineConfig:
168
+ import json
169
+ base_dict = json.loads(base.model_dump_json())
170
+ for k, v in overrides.items():
171
+ if k in base_dict and isinstance(base_dict[k], dict) and isinstance(v, dict):
172
+ base_dict[k].update(v)
173
+ else:
174
+ base_dict[k] = v
175
+ return PipelineConfig(**base_dict)
src/data/__init__.py ADDED
File without changes
src/data/collector.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/data/collector.py — Collect specs from multiple sources (YAML, JSON, DB)
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional, Any
8
+
9
+ import yaml
10
+
11
+
12
+ class SpecCollector:
13
+ """Collects raw design specifications from various sources."""
14
+
15
+ SUPPORTED_EXTENSIONS = {".yaml", ".yml", ".json"}
16
+
17
+ def __init__(self, source_paths: Optional[List[str]] = None):
18
+ self.source_paths = source_paths or []
19
+
20
+ def collect(self) -> List[Dict[str, Any]]:
21
+ specs: List[Dict[str, Any]] = []
22
+ for path in self.source_paths:
23
+ p = Path(path)
24
+ if p.is_file() and p.suffix in self.SUPPORTED_EXTENSIONS:
25
+ specs.append(self._read_file(p))
26
+ elif p.is_dir():
27
+ for f in sorted(p.glob("*.*")):
28
+ if f.suffix in self.SUPPORTED_EXTENSIONS:
29
+ specs.append(self._read_file(f))
30
+ return specs
31
+
32
+ def collect_from_database(self, connection_string: str, query: str) -> List[Dict[str, Any]]:
33
+ raise NotImplementedError("Database collector — implement for your ORM / DB backend")
34
+
35
+ @staticmethod
36
+ def _read_file(path: Path) -> Dict[str, Any]:
37
+ with open(path, "r") as f:
38
+ if path.suffix in (".yaml", ".yml"):
39
+ return yaml.safe_load(f)
40
+ return json.load(f)
src/data/core_parser.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/data/core_parser.py — FuseSoC .core file parser
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import yaml
8
+
9
+ from src.data.preprocessor import SpecPreprocessor
10
+
11
+
12
+ class CoreParser:
13
+ """Parses FuseSoC .core files into the internal DesignSpec-compatible dict.
14
+
15
+ The .core format (YAML) describes IP cores with:
16
+ - name / version / description
17
+ - parameters
18
+ - interfaces (each with signals, type, bus standard)
19
+ - registers (address, access, fields with bit positions)
20
+ - clock_reset
21
+ - filesets, targets
22
+ """
23
+
24
+ ACCESS_MAP = {
25
+ "ro": "read-only",
26
+ "wo": "write-only",
27
+ "rw": "read-write",
28
+ "rc": "read-clear",
29
+ "rs": "read-set",
30
+ "w1c": "write-1-to-clear",
31
+ }
32
+
33
+ def parse(self, content: str) -> Dict[str, Any]:
34
+ raw = yaml.safe_load(content)
35
+ spec: Dict[str, Any] = {}
36
+
37
+ spec["design_name"] = self._extract_name(raw)
38
+ spec["clock_reset"] = self._extract_clock_reset(raw)
39
+ spec["interfaces"] = self._extract_interfaces(raw)
40
+ spec["registers"] = self._extract_registers(raw)
41
+ spec["parameters"] = self._extract_parameters(raw)
42
+
43
+ return SpecPreprocessor().preprocess(spec)
44
+
45
+ @staticmethod
46
+ def _extract_name(raw: Dict[str, Any]) -> str:
47
+ name = raw.get("name", "unknown")
48
+ if isinstance(name, str):
49
+ return name.strip().lower()
50
+ return "unknown"
51
+
52
+ @staticmethod
53
+ def _extract_clock_reset(raw: Dict[str, Any]) -> Dict[str, Any]:
54
+ cr = raw.get("clock_reset")
55
+ if cr:
56
+ return {
57
+ "clock": cr.get("clock", "clk"),
58
+ "reset": cr.get("reset", "rst_n"),
59
+ "reset_active": cr.get("reset_active", 0),
60
+ }
61
+ return {"clock": "clk", "reset": "rst_n", "reset_active": 0}
62
+
63
+ @staticmethod
64
+ def _extract_interfaces(raw: Dict[str, Any]) -> List[Dict[str, Any]]:
65
+ interfaces = raw.get("interfaces", [])
66
+ result: List[Dict[str, Any]] = []
67
+ for iface in interfaces:
68
+ entry: Dict[str, Any] = {
69
+ "name": iface.get("name", "bus"),
70
+ "signals": [],
71
+ }
72
+ for sig in iface.get("signals", []):
73
+ entry["signals"].append({
74
+ "name": sig.get("name", "sig"),
75
+ "direction": sig.get("direction", "input"),
76
+ "width": sig.get("width", 1),
77
+ })
78
+ result.append(entry)
79
+ return result
80
+
81
+ @staticmethod
82
+ def _extract_registers(raw: Dict[str, Any]) -> List[Dict[str, Any]]:
83
+ registers = raw.get("registers", [])
84
+ result: List[Dict[str, Any]] = []
85
+ seen = set()
86
+ for reg in registers:
87
+ name = reg.get("name", f"reg_{len(result)}")
88
+ if name in seen:
89
+ name = f"{name}_{len(result)}"
90
+ seen.add(name)
91
+
92
+ entry: Dict[str, Any] = {
93
+ "name": name,
94
+ "address": reg.get("address", "0x00"),
95
+ "description": reg.get("description", ""),
96
+ "access": reg.get("access", "rw"),
97
+ "fields": [],
98
+ }
99
+ for fld in reg.get("fields", []):
100
+ entry["fields"].append({
101
+ "name": fld.get("name", "field"),
102
+ "bits": fld.get("bits", "0"),
103
+ "description": fld.get("description", ""),
104
+ })
105
+ result.append(entry)
106
+ return result
107
+
108
+ @staticmethod
109
+ def _extract_parameters(raw: Dict[str, Any]) -> Dict[str, Any]:
110
+ params = raw.get("parameters", {})
111
+ return {k: v.get("default") if isinstance(v, dict) else v for k, v in params.items()}
112
+
113
+
114
+ def parse_core_file(path: str) -> Dict[str, Any]:
115
+ """Convenience: read .core file and return DesignSpec-compatible dict."""
116
+ with open(path, "r") as f:
117
+ return CoreParser().parse(f.read())
src/data/preprocessor.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Dict, List, Optional
5
+
6
+
7
+ PROTOCOL_SIGNATURES = {
8
+ "uart": {"tx", "rx", "baud"},
9
+ "spi": {"mosi", "miso", "sclk", "ss_n", "cs"},
10
+ "i2c": {"scl", "sda"},
11
+ "axi4lite": {"awvalid", "awready", "arvalid", "arready",
12
+ "wvalid", "wready", "rvalid", "rready",
13
+ "bvalid", "bready"},
14
+ "apb": {"psel", "penable", "paddr", "pwrite", "pready"},
15
+ "wishbone": {"wb_cyc", "wb_stb", "wb_we", "wb_ack", "wb_addr"},
16
+ }
17
+
18
+
19
+ class SpecPreprocessor:
20
+ def preprocess(self, raw: Dict[str, Any]) -> Dict[str, Any]:
21
+ raw = self._normalise_names(raw)
22
+ raw = self._default_clock_reset(raw)
23
+ raw = self._expand_signals(raw)
24
+ raw = self._validate_address_formats(raw)
25
+ raw = self._detect_protocol(raw)
26
+ return raw
27
+
28
+ @staticmethod
29
+ def _normalise_names(raw: Dict[str, Any]) -> Dict[str, Any]:
30
+ if "design_name" in raw:
31
+ raw["design_name"] = re.sub(r"[^a-zA-Z0-9_]", "_", raw["design_name"]).lower()
32
+ return raw
33
+
34
+ @staticmethod
35
+ def _default_clock_reset(raw: Dict[str, Any]) -> Dict[str, Any]:
36
+ raw.setdefault("clock_reset", {"clock": "clk", "reset": "rst_n", "reset_active": 0})
37
+ return raw
38
+
39
+ @staticmethod
40
+ def _expand_signals(raw: Dict[str, Any]) -> Dict[str, Any]:
41
+ for iface in raw.get("interfaces", []):
42
+ for sig in iface.get("signals", []):
43
+ sig.setdefault("width", 1)
44
+ return raw
45
+
46
+ @staticmethod
47
+ def _validate_address_formats(raw: Dict[str, Any]) -> Dict[str, Any]:
48
+ for reg in raw.get("registers", []):
49
+ addr = reg.get("address", "0x00")
50
+ if not isinstance(addr, str) or not addr.startswith("0x"):
51
+ reg["address"] = f"0x{int(addr):02X}" if isinstance(addr, int) else f"0x{addr}"
52
+ for field in reg.get("fields", []):
53
+ bits = field.get("bits")
54
+ if bits is not None and not isinstance(bits, str):
55
+ field["bits"] = str(bits)
56
+ return raw
57
+
58
+ @staticmethod
59
+ def _detect_protocol(raw: Dict[str, Any]) -> Dict[str, Any]:
60
+ signal_names = set()
61
+ for iface in raw.get("interfaces", []):
62
+ for sig in iface.get("signals", []):
63
+ signal_names.add(sig.get("name", "").lower())
64
+
65
+ if raw.get("protocol"):
66
+ return raw
67
+
68
+ detected = None
69
+ rank = 0
70
+ for proto, sigs in PROTOCOL_SIGNATURES.items():
71
+ matches = sum(1 for kw in sigs if any(kw in s for s in signal_names))
72
+ if matches > rank:
73
+ rank = matches
74
+ detected = proto
75
+
76
+ if detected and rank >= 2:
77
+ raw["protocol"] = detected
78
+ elif not raw.get("protocol"):
79
+ raw["protocol"] = "wishbone"
80
+
81
+ return raw