Sai Kumar Taraka commited on
Commit
7ee417c
·
1 Parent(s): ea3135d

Address review: register metadata engine, coverage wiring, virtual seqr fix, Xcelium wrapper, generic RAL access

Browse files
src/api/server.py CHANGED
@@ -46,7 +46,7 @@ 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):
 
46
  status: str = "ok"
47
  version: str = "0.3.0"
48
  api_version: str = "v1"
49
+ simulators: List[str] = ["stub", "icarus", "vcs", "questa", "xcelium"]
50
 
51
 
52
  class VersionInfo(BaseModel):
src/config.py CHANGED
@@ -125,7 +125,7 @@ class AutoTrainConfig(BaseModel):
125
  max_iterations: int = Field(default=5, ge=1, le=50)
126
  coverage_target: float = Field(default=90.0, ge=0.0, le=100.0)
127
  coverage_gain_min: float = Field(default=2.0, ge=0.0, description="Min % gain per iteration to continue")
128
- simulator: str = Field(default="stub", pattern=r"^(stub|icarus|vcs|questa)$")
129
  sim_timeout: int = Field(default=300, ge=10)
130
  num_seeds: int = Field(default=3, ge=1, le=20, description="Number of regression seeds per iteration")
131
  generate_regression_test: bool = True
 
125
  max_iterations: int = Field(default=5, ge=1, le=50)
126
  coverage_target: float = Field(default=90.0, ge=0.0, le=100.0)
127
  coverage_gain_min: float = Field(default=2.0, ge=0.0, description="Min % gain per iteration to continue")
128
+ simulator: str = Field(default="stub", pattern=r"^(stub|icarus|vcs|questa|xcelium|xrun)$")
129
  sim_timeout: int = Field(default=300, ge=10)
130
  num_seeds: int = Field(default=3, ge=1, le=20, description="Number of regression seeds per iteration")
131
  generate_regression_test: bool = True
src/generation/templates/register_info_pkg.sv.j2 ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // UVM Register Metadata Package for {{ spec.design_name }}
2
+ // Auto-generated from YAML spec — drives metadata-driven sequences
3
+ // Enables generic register access without protocol-specific knowledge
4
+ {% set regs = spec.registers if spec.registers else [] %}
5
+ {% set addr_ints = regs|map(attribute='address')|map('replace', '0x', '')|map('replace', 'h', '')|map('int', 16)|list %}
6
+ {% set max_addr_val = addr_ints|max if addr_ints else 0 %}
7
+ {% set addr_bits = [max_addr_val.bit_length(), 3]|max %}
8
+ {% set sizes = regs|map(attribute='size')|select('number')|list %}
9
+ {% set data_width = sizes|max if sizes else 8 %}
10
+
11
+ package {{ spec.design_name }}_reg_info_pkg;
12
+
13
+ // -----------------------------------------------------------------------
14
+ // Access Policy Enum
15
+ // -----------------------------------------------------------------------
16
+ typedef enum bit [2:0] {
17
+ RO = 3'b001, // Read-only
18
+ WO = 3'b010, // Write-only
19
+ RW = 3'b011, // Read-Write
20
+ RC = 3'b100, // Read-Clear
21
+ W1C = 3'b101, // Write-1-to-Clear
22
+ NA = 3'b000 // Not applicable
23
+ } access_policy_e;
24
+
25
+ // -----------------------------------------------------------------------
26
+ // Field Information
27
+ // -----------------------------------------------------------------------
28
+ typedef struct {
29
+ string name;
30
+ int msb;
31
+ int lsb;
32
+ int width;
33
+ access_policy_e access;
34
+ logic [63:0] reset_val;
35
+ } field_info_t;
36
+
37
+ // -----------------------------------------------------------------------
38
+ // Register Information
39
+ // -----------------------------------------------------------------------
40
+ typedef struct {
41
+ string name;
42
+ logic [{{ addr_bits-1 }}:0] address;
43
+ int size;
44
+ access_policy_e access;
45
+ logic [63:0] reset_val;
46
+ int num_fields;
47
+ field_info_t fields[16];
48
+ } register_info_t;
49
+
50
+ // -----------------------------------------------------------------------
51
+ // Register Database
52
+ // -----------------------------------------------------------------------
53
+ function automatic register_info_t get_register_by_name(string name);
54
+ register_info_t r;
55
+ r.name = "";
56
+ {% for reg in regs %}
57
+ if (name == "{{ reg.name|lower }}") begin
58
+ r.name = "{{ reg.name }}";
59
+ r.address = {{ addr_bits }}'h{{ reg.address.replace('0x', '').replace('h', '') }};
60
+ r.size = {{ reg.size if reg.size else 8 }};
61
+ r.reset_val = {% if reg.reset is defined and reg.reset %}{{ reg.size if reg.size else 8 }}'h{{ reg.reset|string|replace('0x', '')|replace('h', '') }}{% else %}'h0{% endif %};
62
+ {% if reg.access is defined and reg.access %}
63
+ {% set ra = reg.access|lower %}
64
+ {% if ra == 'ro' %}r.access = RO;
65
+ {% elif ra == 'wo' %}r.access = WO;
66
+ {% elif ra == 'rw' %}r.access = RW;
67
+ {% elif ra == 'rc' %}r.access = RC;
68
+ {% elif ra == 'w1c' %}r.access = W1C;
69
+ {% else %}r.access = RW;
70
+ {% endif %}
71
+ {% else %}r.access = RW;
72
+ {% endif %}
73
+ {% if reg.fields %}
74
+ r.num_fields = {{ reg.fields|length }};
75
+ {% for f in reg.fields %}
76
+ {% if ':' in f.bits %}{% set parts = f.bits.split(':') %}{% set msb = parts[0]|int %}{% set lsb = parts[1]|int %}{% else %}{% set msb = f.bits|int - 1 %}{% set lsb = 0 %}{% endif %}
77
+ r.fields[{{ loop.index0 }}].name = "{{ f.name }}";
78
+ r.fields[{{ loop.index0 }}].msb = {{ msb }};
79
+ r.fields[{{ loop.index0 }}].lsb = {{ lsb }};
80
+ r.fields[{{ loop.index0 }}].width = {{ msb - lsb + 1 }};
81
+ r.fields[{{ loop.index0 }}].reset_val = {% if f.reset is defined and f.reset %}{{ msb - lsb + 1 }}'h{{ f.reset|string|replace('0x', '')|replace('h', '') }}{% else %}'h0{% endif %};
82
+ {% endfor %}
83
+ {% else %}
84
+ r.num_fields = 0;
85
+ {% endif %}
86
+ return r;
87
+ end
88
+ {% endfor %}
89
+ return r;
90
+ endfunction
91
+
92
+ function automatic register_info_t get_register_by_offset(logic [{{ addr_bits-1 }}:0] addr);
93
+ {% for reg in regs %}
94
+ if (addr == {{ addr_bits }}'h{{ reg.address.replace('0x', '').replace('h', '') }})
95
+ return get_register_by_name("{{ reg.name|lower }}");
96
+ {% endfor %}
97
+ register_info_t r;
98
+ r.name = "";
99
+ return r;
100
+ endfunction
101
+
102
+ // -----------------------------------------------------------------------
103
+ // Register Count
104
+ // -----------------------------------------------------------------------
105
+ function automatic int get_num_registers();
106
+ return {{ regs|length }};
107
+ endfunction
108
+
109
+ // -----------------------------------------------------------------------
110
+ // Register Name List (for iterating in sequences)
111
+ // -----------------------------------------------------------------------
112
+ function automatic void get_all_register_names(ref string names[$]);
113
+ {% for reg in regs %}
114
+ names.push_back("{{ reg.name|lower }}");
115
+ {% endfor %}
116
+ endfunction
117
+
118
+ // -----------------------------------------------------------------------
119
+ // Access Policy Helpers
120
+ // -----------------------------------------------------------------------
121
+ function automatic bit is_readable(access_policy_e ap);
122
+ return (ap inside {RO, RW, RC});
123
+ endfunction
124
+
125
+ function automatic bit is_writable(access_policy_e ap);
126
+ return (ap inside {WO, RW, W1C});
127
+ endfunction
128
+
129
+ endpackage
src/generation/templates/scoreboard.sv.j2 CHANGED
@@ -282,12 +282,14 @@ class {{ spec.design_name }}_scoreboard extends uvm_scoreboard;
282
  end
283
  {% endif %}
284
 
285
- if (tr.addr inside {3'h3, 3'h4, 3'h7}) begin
286
  if (tr.data === expected) begin
287
  match_count++;
 
288
  `uvm_info("SB", $sformatf("READ MATCH: addr=0x%0h data=0x%02h", tr.addr, tr.data), UVM_HIGH)
289
  end else begin
290
  mismatch_count++;
 
291
  log_error($sformatf("READ MISMATCH: addr=0x%0h expected=0x%02h got=0x%02h",
292
  tr.addr, expected, tr.data));
293
  end
@@ -636,6 +638,10 @@ class {{ spec.design_name }}_scoreboard extends uvm_scoreboard;
636
  end
637
  cov_wordlen_seen[dbits - 5] = 1;
638
  cov_parity_seen[pen + eps*2] = 1;
 
 
 
 
639
  `uvm_info("SB_REC_CFG", $sformatf("record_config: baud=%0d dbits=%0d sbits=%0d pen=%0d eps=%0d",
640
  baud, dbits, sbits, pen, eps), UVM_MEDIUM)
641
  endfunction
 
282
  end
283
  {% endif %}
284
 
285
+ if (tr.addr inside { {% for r in regs %}{{ addr_bits }}'h{{ r.address.replace('0x', '').replace('h', '') }}{% if not loop.last %}, {% endif %}{% endfor %} }) begin
286
  if (tr.data === expected) begin
287
  match_count++;
288
+ reg_match_count[tr.addr]++;
289
  `uvm_info("SB", $sformatf("READ MATCH: addr=0x%0h data=0x%02h", tr.addr, tr.data), UVM_HIGH)
290
  end else begin
291
  mismatch_count++;
292
+ reg_mismatch_count[tr.addr]++;
293
  log_error($sformatf("READ MISMATCH: addr=0x%0h expected=0x%02h got=0x%02h",
294
  tr.addr, expected, tr.data));
295
  end
 
638
  end
639
  cov_wordlen_seen[dbits - 5] = 1;
640
  cov_parity_seen[pen + eps*2] = 1;
641
+ {% if p == "uart" %}
642
+ if (cov_handle != null)
643
+ cov_handle.sample_uart_config(baud, dbits, sbits, pen + eps*2);
644
+ {% endif %}
645
  `uvm_info("SB_REC_CFG", $sformatf("record_config: baud=%0d dbits=%0d sbits=%0d pen=%0d eps=%0d",
646
  baud, dbits, sbits, pen, eps), UVM_MEDIUM)
647
  endfunction
src/generation/templates/virtual_sequencer.sv.j2 CHANGED
@@ -13,8 +13,17 @@ class {{ spec.design_name }}_virtual_sequencer extends uvm_sequencer;
13
 
14
  {% if p == "uart" %}
15
  // Additional UART sub-sequencers for multi-port / layered protocols
 
16
  uvm_sequencer #({{ spec.design_name }}_seq_item) uart0_seqr;
 
17
  uvm_sequencer #({{ spec.design_name }}_seq_item) uart1_seqr;
 
 
 
 
 
 
 
18
  {% elif p == "apb" %}
19
  uvm_sequencer #({{ spec.design_name }}_seq_item) apb_seqr;
20
  {% elif p == "axi4lite" %}
@@ -36,7 +45,15 @@ class {{ spec.design_name }}_virtual_sequencer extends uvm_sequencer;
36
  {% if p == "uart" %}
37
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "bus_seqr", bus_seqr));
38
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "uart0_seqr", uart0_seqr));
 
39
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "uart1_seqr", uart1_seqr));
 
 
 
 
 
 
 
40
  {% else %}
41
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "bus_seqr", bus_seqr));
42
  {% endif %}
 
13
 
14
  {% if p == "uart" %}
15
  // Additional UART sub-sequencers for multi-port / layered protocols
16
+ // Number of sub-seqrs = number of interfaces in spec
17
  uvm_sequencer #({{ spec.design_name }}_seq_item) uart0_seqr;
18
+ {% if spec.interfaces|length > 1 %}
19
  uvm_sequencer #({{ spec.design_name }}_seq_item) uart1_seqr;
20
+ {% endif %}
21
+ {% if spec.interfaces|length > 2 %}
22
+ uvm_sequencer #({{ spec.design_name }}_seq_item) uart2_seqr;
23
+ {% endif %}
24
+ {% if spec.interfaces|length > 3 %}
25
+ uvm_sequencer #({{ spec.design_name }}_seq_item) uart3_seqr;
26
+ {% endif %}
27
  {% elif p == "apb" %}
28
  uvm_sequencer #({{ spec.design_name }}_seq_item) apb_seqr;
29
  {% elif p == "axi4lite" %}
 
45
  {% if p == "uart" %}
46
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "bus_seqr", bus_seqr));
47
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "uart0_seqr", uart0_seqr));
48
+ {% if spec.interfaces|length > 1 %}
49
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "uart1_seqr", uart1_seqr));
50
+ {% endif %}
51
+ {% if spec.interfaces|length > 2 %}
52
+ void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "uart2_seqr", uart2_seqr));
53
+ {% endif %}
54
+ {% if spec.interfaces|length > 3 %}
55
+ void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "uart3_seqr", uart3_seqr));
56
+ {% endif %}
57
  {% else %}
58
  void'(uvm_config_db #(uvm_sequencer #({{ spec.design_name }}_seq_item))::get(this, "", "bus_seqr", bus_seqr));
59
  {% endif %}
src/main.py CHANGED
@@ -34,7 +34,7 @@ Examples:
34
  parser.add_argument("--auto-train", action="store_true", help="Enable coverage-driven auto-training loop")
35
  parser.add_argument("--max-iterations", type=int, default=5, help="Max auto-training iterations (default: 5)")
36
  parser.add_argument("--coverage-target", type=float, default=90.0, help="Coverage target %% (default: 90)")
37
- parser.add_argument("--simulator", default="stub", choices=["stub", "icarus", "vcs", "questa"],
38
  help="Simulator backend (default: stub)")
39
  return parser
40
 
 
34
  parser.add_argument("--auto-train", action="store_true", help="Enable coverage-driven auto-training loop")
35
  parser.add_argument("--max-iterations", type=int, default=5, help="Max auto-training iterations (default: 5)")
36
  parser.add_argument("--coverage-target", type=float, default=90.0, help="Coverage target %% (default: 90)")
37
+ parser.add_argument("--simulator", default="stub", choices=["stub", "icarus", "vcs", "questa", "xcelium", "xrun"],
38
  help="Simulator backend (default: stub)")
39
  return parser
40
 
src/models/template_model.py CHANGED
@@ -21,19 +21,21 @@ class TemplateModel(GenerationModel):
21
  }
22
 
23
  TEMPLATE_MAP = {
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  "testbench.sv": "testbench.sv.j2",
25
- "interface_{name}.sv": "interface.sv.j2",
26
- "sequence_item_{name}.sv": "sequence_item.sv.j2",
27
- "driver_{name}.sv": "driver.sv.j2",
28
- "monitor_{name}.sv": "monitor.sv.j2",
29
- "serial_monitor_{name}.sv": "serial_monitor.sv.j2",
30
- "agent_{name}.sv": "agent.sv.j2",
31
- "scoreboard_{name}.sv": "scoreboard.sv.j2",
32
- "coverage_collector_{name}.sv": "coverage_collector.sv.j2",
33
- "ral_model_{name}.sv": "ral_model.sv.j2",
34
- "base_sequence_{name}.sv": "sequence.sv.j2",
35
- "test_{name}.sv": "test.sv.j2",
36
- "environment_{name}.sv": "env.sv.j2",
37
  }
38
 
39
  RTL_MAP = {
 
21
  }
22
 
23
  TEMPLATE_MAP = {
24
+ "env/{name}_env.sv": "env.sv.j2",
25
+ "env/{name}_scoreboard.sv": "scoreboard.sv.j2",
26
+ "env/{name}_coverage_collector.sv": "coverage_collector.sv.j2",
27
+ "agent/{name}_driver.sv": "driver.sv.j2",
28
+ "agent/{name}_monitor.sv": "monitor.sv.j2",
29
+ "agent/{name}_sequencer.sv": "sequencer.sv.j2",
30
+ "agent/{name}_sequence_item.sv": "sequence_item.sv.j2",
31
+ "sequences/{name}_sequence.sv": "sequence.sv.j2",
32
+ "tests/{name}_test.sv": "test.sv.j2",
33
+ "sequences/{name}_reg_model_adapter.sv": "reg_model_adapter.sv.j2",
34
+ "sequences/{name}_register_info_pkg.sv": "register_info_pkg.sv.j2",
35
+ "{name}_reg_block.sv": "reg_block.sv.j2",
36
+ "{name}_interface.sv": "interface.sv.j2",
37
  "testbench.sv": "testbench.sv.j2",
38
+ "rtl/protocol_core.v": "rtl/protocol_core.v.j2",
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
 
41
  RTL_MAP = {
src/pipeline.py CHANGED
@@ -364,6 +364,9 @@ class TBPipeline:
364
  if sim_type == "questa":
365
  from src.simulation.questa import QuestaSimulator
366
  return QuestaSimulator(work_dir=sim_output_path(self.cfg))
 
 
 
367
  return StubSimulator(work_dir=sim_output_path(self.cfg))
368
 
369
  def _merge_cfg(self, loaded: PipelineConfig) -> None:
 
364
  if sim_type == "questa":
365
  from src.simulation.questa import QuestaSimulator
366
  return QuestaSimulator(work_dir=sim_output_path(self.cfg))
367
+ if sim_type == "xcelium" or sim_type == "xrun":
368
+ from src.simulation.xcelium import XceliumSimulator
369
+ return XceliumSimulator(work_dir=sim_output_path(self.cfg))
370
  return StubSimulator(work_dir=sim_output_path(self.cfg))
371
 
372
  def _merge_cfg(self, loaded: PipelineConfig) -> None:
src/simulation/__init__.py CHANGED
@@ -1,6 +1,10 @@
1
  from src.simulation.base import CoverageBin, CoverageDB, SimResult, Simulator
2
  from src.simulation.icarus import IcarusSimulator
3
  from src.simulation.stub_sim import StubSimulator
 
 
 
4
 
5
  __all__ = ["Simulator", "SimResult", "CoverageBin", "CoverageDB",
6
- "IcarusSimulator", "StubSimulator"]
 
 
1
  from src.simulation.base import CoverageBin, CoverageDB, SimResult, Simulator
2
  from src.simulation.icarus import IcarusSimulator
3
  from src.simulation.stub_sim import StubSimulator
4
+ from src.simulation.vcs import VcsSimulator
5
+ from src.simulation.questa import QuestaSimulator
6
+ from src.simulation.xcelium import XceliumSimulator
7
 
8
  __all__ = ["Simulator", "SimResult", "CoverageBin", "CoverageDB",
9
+ "IcarusSimulator", "StubSimulator", "VcsSimulator",
10
+ "QuestaSimulator", "XceliumSimulator"]
src/simulation/xcelium.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+
10
+ from src.simulation.base import CoverageBin, SimResult, Simulator
11
+
12
+
13
+ class XceliumSimulator(Simulator):
14
+ """Cadence Xcelium simulation backend.
15
+
16
+ Uses xrun for compile+elaborate+simulate in one step.
17
+ Detects xrun at runtime; falls back gracefully if not installed.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ work_dir: str = "sim_output",
23
+ xrun_path: str = "xrun",
24
+ compile_flags: Optional[List[str]] = None,
25
+ run_flags: Optional[List[str]] = None,
26
+ ):
27
+ super().__init__(work_dir)
28
+ self.xrun_path = xrun_path
29
+ self.compile_flags = compile_flags or [
30
+ "-64bit",
31
+ "-sv",
32
+ "-access",
33
+ "+rwc",
34
+ "-coverage",
35
+ "all",
36
+ "-nowarn",
37
+ "NONPORTTOPCONN",
38
+ ]
39
+ self.run_flags = run_flags or [
40
+ "-coverage",
41
+ "all",
42
+ "-exit",
43
+ ]
44
+
45
+ def run(
46
+ self,
47
+ files: List[str],
48
+ top: str = "testbench",
49
+ plusargs: Optional[List[str]] = None,
50
+ ) -> SimResult:
51
+ work_dir = Path(self.work_dir)
52
+ work_dir.mkdir(parents=True, exist_ok=True)
53
+
54
+ # Check if xrun is available
55
+ try:
56
+ subprocess.run(
57
+ [self.xrun_path, "-version"],
58
+ capture_output=True,
59
+ timeout=10,
60
+ check=True,
61
+ )
62
+ except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError):
63
+ return SimResult(
64
+ passed=False,
65
+ errors=[f"Xcelium ({self.xrun_path}) not found or not executable"],
66
+ )
67
+
68
+ # Write file list
69
+ flist_path = work_dir / "xrun_files.f"
70
+ flist_path.write_text("\n".join(str(f) for f in files), encoding="utf-8")
71
+
72
+ plusargs = plusargs or []
73
+ seed = None
74
+ for pa in plusargs:
75
+ if "+seed=" in pa:
76
+ seed = pa.split("=")[1]
77
+ break
78
+
79
+ xrun_cmd = [
80
+ self.xrun_path,
81
+ *self.compile_flags,
82
+ "-f",
83
+ str(flist_path),
84
+ "-top",
85
+ top,
86
+ "-l",
87
+ str(work_dir / "xrun_sim.log"),
88
+ ]
89
+ if seed:
90
+ xrun_cmd += [f"+ntb_random_seed={seed}"]
91
+ xrun_cmd += self.run_flags
92
+
93
+ try:
94
+ result = subprocess.run(
95
+ xrun_cmd,
96
+ cwd=str(work_dir),
97
+ capture_output=True,
98
+ timeout=600,
99
+ )
100
+ log = result.stdout + "\n" + result.stderr
101
+ log_file = work_dir / "xrun_sim.log"
102
+ if log_file.exists():
103
+ log = log_file.read_text(encoding="utf-8", errors="replace")
104
+
105
+ if result.returncode != 0:
106
+ errors = self._parse_errors(log)
107
+ return SimResult(
108
+ passed=False,
109
+ errors=errors or ["Xcelium simulation failed"],
110
+ log_output=log[:5000],
111
+ )
112
+
113
+ return self.parse_coverage(log)
114
+
115
+ except subprocess.TimeoutExpired:
116
+ return SimResult(
117
+ passed=False,
118
+ errors=["Xcelium simulation timed out (>600s)"],
119
+ )
120
+ except Exception as e:
121
+ return SimResult(
122
+ passed=False,
123
+ errors=[f"Xcelium simulation error: {e}"],
124
+ )
125
+
126
+ def parse_coverage(self, log: str) -> SimResult:
127
+ bins: List[CoverageBin] = []
128
+ passed = True
129
+ errors: List[str] = []
130
+
131
+ xrun_cov = re.compile(
132
+ r"COVERAGE:\s+(\S+)\s+(\d+)/(\d+)\s+\[(HIT|MISS)\]"
133
+ )
134
+ for match in xrun_cov.finditer(log):
135
+ name = match.group(1)
136
+ hit = int(match.group(2))
137
+ goal = int(match.group(3))
138
+ bins.append(CoverageBin(name=name, hit_count=hit, goal=goal))
139
+ if hit < goal:
140
+ passed = False
141
+
142
+ # Xcelium coverage summary
143
+ total_cov = re.compile(
144
+ r"(Line|Toggle|Functional|Assertion)\s+Coverage:\s+([\d.]+)%"
145
+ )
146
+ for match in total_cov.finditer(log):
147
+ cov_type = match.group(1)
148
+ pct = float(match.group(2))
149
+ bins.append(CoverageBin(
150
+ name=f"xrun_{cov_type.lower()}_total",
151
+ hit_count=int(pct),
152
+ goal=100,
153
+ ))
154
+ if pct < 100:
155
+ passed = False
156
+
157
+ # Error extraction
158
+ err_patterns = re.compile(
159
+ r"\*[WE]|(Error|Warning|Fatal):\s*(.*)", re.IGNORECASE
160
+ )
161
+ for match in err_patterns.finditer(log):
162
+ err_text = match.group(0).strip()
163
+ if len(err_text) > 20:
164
+ errors.append(err_text[:200])
165
+
166
+ if not bins:
167
+ # No structured coverage; check for pass/fail
168
+ if re.search(r"TEST.*PASSED|UVM.*PASSED", log, re.IGNORECASE):
169
+ passed = True
170
+ elif re.search(r"TEST.*FAILED|UVM.*FAILED|#\s*FAIL", log, re.IGNORECASE):
171
+ passed = False
172
+ errors.append("Simulation FAILED (no structured coverage found)")
173
+
174
+ return SimResult(
175
+ passed=passed,
176
+ total_bins=len(bins),
177
+ covered_bins=sum(1 for b in bins if b.covered),
178
+ bins=bins,
179
+ errors=errors[:20],
180
+ log_output=log[:5000],
181
+ )
182
+
183
+ def _parse_errors(self, log: str) -> List[str]:
184
+ errors = []
185
+ for line in log.split("\n"):
186
+ if any(kw in line for kw in ["*E", "*W", "Error:", "Fatal:", "FAIL"]):
187
+ errors.append(line.strip()[:200])
188
+ return errors[:20]