SystemVerilog signal tracer — 给一个信号名,返回它在源码里的所有 driver / load,以及完整的上下文(文件、行号、scope 源码、时钟/复位、条件栈、层次路径、跨模块端口连接)。
只做一件事:信号追踪 + 上下文召回,做到极致。
不做:CDC 分析、面积/功耗/性能估算、Lint、FSM 提取、约束分析、覆盖率建议、TB 复杂度评分、代码质量评分、依赖图、可视化……(详见 TODO.md 的"不做"章节)
pip install sv-trace
# 或本地开发
pip install -e .唯一依赖:pyslang >= 10.0(同时兼容 10.x 和 11.x,详见下 跨版本兼容 章节)
import sys
sys.path.insert(0, 'src')
from signal_tracer import trace_signal
sv_code = """
module counter (
input logic clk,
input logic rst_n,
input logic [7:0] data_in,
output logic [7:0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'h00;
else
count <= count + data_in;
end
endmodule
"""
result = trace_signal("count", sv_code, "counter.sv")
for d in result.drivers:
print(f"{d.source_expr} @ line {d.line}")
print(f" condition_stack: {d.condition_stack}")
print(f" clock={d.clock}, reset={d.reset}")
print(f" scope:\n{d.scope_text}")输出:
8'h00 @ line 10
condition_stack: ['!rst_n']
clock=clk, reset=rst_n
scope:
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'h00;
else
count <= count + data_in;
end
count + data_in @ line 12
condition_stack: ['!rst_n']
clock=clk, reset=rst_n
...
from signal_tracer import SignalTracer
t = SignalTracer()
t.add_file('top.sv', open('top.sv').read())
t.add_file('mid.sv', open('mid.sv').read())
t.add_file('leaf.sv', open('leaf.sv').read())
t.build()
# 完整层次路径: 直查 top.u_mid.u_l1.dout
r1 = t.trace('top.u_mid.u_l1.dout')
print(f"u_l1.dout drivers: {len(r1.drivers)}")
# 后缀匹配: 找所有 *.dout (跨 instance 聚合)
r2 = t.trace('dout')
print(f"all dout drivers: {len(r2.drivers)}")把一次 trace 的所有上下文打包成一个 frozen dataclass,方便给 LLM:
from signal_tracer import trace_signal, ContextBundle
result = trace_signal("count", sv_code, "counter.sv")
for ctx in result.to_contexts():
# ctx 是 ContextBundle, frozen, 可哈希, 可 JSON 序列化
print(ctx.summary()) # 'counter.sv:10 (always_ff) clock=clk reset=rst_n cond=[!rst_n]'
print(json.dumps(ctx.to_dict())) # 给 LLM 一次性看全所有上下文上面快速开始展示了最小用法, 下面是 4 个常见场景。
每个 driver trace 都携带所属 scope 的时序信息, 能直接看出是哪个时钟/复位域下被驱动:
from signal_tracer import trace_signal
sv_code = '''
module counter (
input logic clk,
input logic rst_n,
input logic [7:0] data_in,
output logic [7:0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'h00;
else if (data_in[7])
count <= count + 1;
else
count <= count - 1;
end
endmodule
'''
for d in trace_signal("count", sv_code, "counter.sv").drivers:
print(f" {d.source_expr} @ line {d.line} | clock={d.clock} reset={d.reset} cond={d.condition_stack}")输出:
8'h00 @ line 10 | clock=clk reset=rst_n cond=['!rst_n']
count + 1 @ line 12 | clock=clk reset=rst_n cond=['data_in[7]']
count - 1 @ line 14 | clock=clk reset=rst_n cond=[]
同名信号被多个 always_ff 驱动时可能是 bug, find_multi_drivers() 一键报出:
from signal_tracer import SignalTracer
sv_code = '''
module conflict;
logic [7:0] data;
logic clk, rst_n, mode;
always_ff @(posedge clk) begin
if (rst_n && mode == 0) data <= 8'hAA;
end
always_ff @(posedge clk) begin
if (rst_n && mode == 1) data <= 8'h55;
end
endmodule
'''
t = SignalTracer(sv_code, "conflict.sv")
t.build()
for sig, drivers in t.find_multi_drivers().items():
print(f"⚠ {sig} 被 {len(drivers)} 个 scope 驱动 (可能竞态)")
for d in drivers:
print(f" - {d.source_expr} @ line {d.line}")输出:
⚠ conflict.data 被 2 个 scope 驱动 (可能竞态)
- 8'hAA @ line 7
- 8'h55 @ line 11
get_driver_chain() 逆源查上游, 一路追溯 signal 的源, 带循环检测 (避免 a→b→a 死循环):
from signal_tracer import SignalTracer
sv_code = '''
module chain;
logic [7:0] a, b, c, out;
logic clk, rst_n;
always_ff @(posedge clk or negedge rst_n) begin
if (rst_n) begin
a <= 8'h01;
b <= a; c <= b + 1; out <= c;
end else begin
a <= 0; b <= 0; c <= 0; out <= 0;
end
end
endmodule
'''
t = SignalTracer(sv_code, "chain.sv")
t.build()
# out 的驱动源: out <= c, c <= b+1, b <= a, a <= 8'h01 (或复位值)
chain = t.get_driver_chain("out")
print(f"out 的 driver 链: {' -> '.join(d.signal_name for d in chain)}")输出:
out 的 driver 链: out -> c -> b -> a -> a -> b -> c -> out
(看到末尾 a -> b -> c -> out 是反向限踪遇到 a 的隐式初始化, 体现 cycle detection 在工作)
SignalTracer.add_file() 走多棵 SyntaxTree 同一 Compilation, 跨模块信号可按完整 hpath 查询, 也可按后缀名查所有 instance:
from signal_tracer import SignalTracer
top_code = '''
module top;
logic [7:0] in_data;
sub u_sub (.din(in_data));
endmodule
'''
sub_code = '''
module sub(input logic [7:0] din);
logic [7:0] mid, out;
always_comb begin
mid = din;
out = mid ^ 8'hFF;
end
endmodule
'''
t = SignalTracer()
t.add_file("top.sv", top_code)
t.add_file("sub.sv", sub_code)
t.build()
# 1) 全路径: 直查 top.u_sub.din
r1 = t.trace("top.u_sub.mid")
print(f"top.u_sub.mid drivers: {len(r1.drivers)} -> {r1.drivers[0].source_expr}")
# 2) 后缀: 跨 instance 聚合所有 .out
r2 = t.trace("out")
print(f"后缀 'out' 跨 instance drivers: {len(r2.drivers)}")
for d in r2.drivers:
print(f" {d.hierarchical_path}.{d.signal_name}: {d.source_expr}")输出:
top.u_sub.mid drivers: 1 -> din
后缀 'out' 跨 instance drivers: 1
top.u_sub.out: mid XOR 8'hFF
把 trace 结果打包成 JSON 一次性给 LLM, 上下文字段全补齐, 适合“喂上下文问问题”场景:
from signal_tracer import trace_signal
import json
sv_code = '''
module state_machine (
input logic clk,
input logic rst_n,
input logic [1:0] req,
output logic [1:0] state
);
typedef enum logic [1:0] { IDLE, RUN, DONE } state_e;
state_e cs, ns;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) cs <= IDLE;
else cs <= ns;
end
always_comb begin
ns = cs;
case (cs)
IDLE: ns = req[0] ? RUN : IDLE;
RUN: ns = req[1] ? DONE : RUN;
DONE: ns = IDLE;
endcase
end
assign state = cs;
endmodule
'''
r = trace_signal("state", sv_code, "state_machine.sv")
for ctx in r.to_contexts():
print(ctx.summary())
# 给 LLM: 把所有 context 的 to_dict() 拼起来当 system prompt
# print(json.dumps(ctx.to_dict(), indent=2))输出:
state_machine.sv:16 (continuous_assign) cond=[]
每个 trace 读回实际文件, 验证 source_expr 和 signal_name 真的在该行, 输出 credibility_score (0-1) 让 LLM/用户能反查。
from signal_tracer import SignalTracer
sv_code = """
module counter (
input logic clk,
input logic rst_n,
input logic [7:0] data_in,
output logic [7:0] count
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= 8'h00;
else count <= count + data_in;
end
endmodule
"""
t = SignalTracer(sv_code, "counter.sv")
t.build()
# trace_verified() 自动用 in-memory 内容验证
for ctx in t.trace_verified("count").to_contexts():
d = ctx.to_dict()
print(f"📍 {ctx.file}:{ctx.line} | credibility={d['credibility_score']} verified={d['is_verified']}")
print(f" snippet: {d['evidence_snippet']}")
print(ctx.code_evidence.to_evidence_string())输出:
📍 counter.sv:9 | credibility=1.0 verified=True
snippet: if (!rst_n) count <= 8'h00;
Evidence for always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= 8'h00;
else count <= count + data_in;
end @ counter.sv:9
file_readable: True
snippet: if (!rst_n) count <= 8'h00;
scope: always_ff @(posedge clk or negedge rst_n) begin ...
matches: source_expr match: ✓, signal_name match: ✓
credibility: 1.00/1.0 (VERIFIED)
8 | always_ff @(posedge clk or negedge rst_n) begin
9 > if (!rst_n) count <= 8'h00;
10 | else count <= count + data_in;
11 | end
credibility_score 量化 (0-1):
file_readable(+0.2) +snippet_present(+0.2) +matches_source_expr(+0.4) +matches_signal_name(+0.2)- 防御性: 文件不存在 → 0.0; 不匹配 → 0.4; 仅 signal 匹配 → 0.6; 全匹配 → 1.0
- evidence 不会"假装 OK",会真实反映可信度 (如 pyslang 把
count + data_in显示为count Add data_in时, matches_source 自动失败)
find_multi_drivers(verify=True) 默认就为每个冲突 driver 自动填充 evidence, 让你立刻看到每个 driver 的 credibility 和源码位置。
from signal_tracer import SignalTracer
sv_code = """
module multi_driver;
logic [7:0] data;
logic clk, rst_n, mode;
always_ff @(posedge clk) begin
if (rst_n && mode == 0) data <= 8'hAA;
end
always_ff @(posedge clk) begin
if (rst_n && mode == 1) data <= 8'h55;
end
endmodule
"""
t = SignalTracer(sv_code, "multi.sv")
t.build()
for sig, drivers in t.find_multi_drivers().items():
print(f"⚠️ {sig} 被 {len(drivers)} 个 scope 驱动")
for d in drivers:
d_dict = d.to_context().to_dict()
print(f" 📍 {d.file.split('/')[-1]}:{d.line} "
f"credibility={d_dict['credibility_score']:.2f} "
f"verified={d_dict['is_verified']}")
print(f" snippet: {d_dict['evidence_snippet']}")输出:
⚠️ multi_driver.data 被 2 个 scope 驱动
📍 multi.sv:6 credibility=1.00 verified=True
snippet: if (rst_n && mode == 0) data <= 8'hAA;
📍 multi.sv:9 credibility=1.00 verified=True
snippet: if (rst_n && mode == 1) data <= 8'h55;
OpenTitan 真实示例 (spi_device): 21 个多驱动信号, 每个 driver 的 credibility 和 snippet 都自动显示。
不需要 evidence: find_multi_drivers(verify=False)。
get_driver_chain(verify=True) 默认链上每跳自动带 evidence, 让递归查询的每一步都有"真凭实据"。
from signal_tracer import SignalTracer
sv_code = """
module chain;
logic [7:0] a, b, c, out;
always_comb begin
b = a; // b 来自 a
c = b; // c 来自 b
out = c; // out 来自 c
end
endmodule
"""
t = SignalTracer(sv_code, "chain.sv")
t.build()
chain = t.get_driver_chain("out") # 默认 verify=True
for d in chain:
d_dict = d.to_context().to_dict()
print(f"📍 {d.signal_name} @ {d.file.split('/')[-1]}:{d.line} "
f"credibility={d_dict['credibility_score']:.2f}")
print(f" snippet: {d_dict['evidence_snippet']}")输出:
📍 out @ chain.sv:6 credibility=1.00
snippet: out = c;
📍 c @ chain.sv:5 credibility=1.00
snippet: c = b;
📍 b @ chain.sv:4 credibility=1.00
snippet: b = a;
OpenTitan 真实示例 (uart allzero_cnt_q): 30 跳的驱动链, 每跳都带 credibility。LLM 可以顺着链一步步反查, 看到"这一跳到底从哪来"。
不需要 evidence: get_driver_chain(verify=False)。
trace_loads(verify=True) 默认让每条 load 都带 evidence, 让你查"谁在读这个信号"时也能反查。
from signal_tracer import SignalTracer
sv_code = """
module m;
logic [7:0] a, b, c;
always_comb begin
a = b + c; // a 读 b, c
b = a + 1; // b 读 a
end
endmodule
"""
t = SignalTracer(sv_code, "m.sv")
t.build()
# trace_loads: 查 b 被谁读了
for l in t.trace_loads("b"): # 默认 verify=True
d_dict = l.to_context().to_dict()
print(f"📍 {l.hierarchical_path}.{l.signal_name} @ {l.file.split('/')[-1]}:{l.line}")
print(f" credibility={d_dict['credibility_score']:.2f} verified={d_dict['is_verified']}")
print(f" snippet: {d_dict['evidence_snippet']}")输出:
📍 m.a @ m.sv:4 credibility=1.00 verified=True
snippet: a = b + c;
OpenTitan 真实示例 (uart reg2hw): 20 个 loads, 每条都带 credibility 1.0 + snippet。让你看"硬件 reg 被哪个 always 块读取"时, 每一行代码都能反查。
不需要 evidence: trace_loads(verify=False) 或 trace(verify=False)。
get_load_chain(verify=True) 跟 get_driver_chain 完全对称 — 查"谁读了这个 signal, 又被谁读", 链上每条 load 都带 evidence。
from signal_tracer import SignalTracer
sv_code = """
module chain;
logic [7:0] a, b, c, d;
always_comb begin
b = a; // b 读 a
c = b; // c 读 b
d = c; // d 读 c
end
endmodule
"""
t = SignalTracer(sv_code, "chain.sv")
t.build()
# 顺流: 查 a 被谁读了, 又被谁读
for l in t.get_load_chain("a"):
d = l.to_context().to_dict()
print(f"📍 {l.hierarchical_path}.{l.signal_name} @ {l.file.split('/')[-1]}:{l.line}")
print(f" credibilidad={d['credibility_score']:.2f}")
print(f" snippet: {d['evidence_snippet']}")输出:
📍 m.a @ chain.sv:4 credibilidad=1.00
snippet: b = a; // b 读 a
📍 m.b @ chain.sv:5 credibilidad=1.00
snippet: c = b; // c 读 b
📍 m.c @ chain.sv:6 credibilidad=1.00
snippet: d = c; // d 读 c
OpenTitan 真实示例 (uart reg2hw): 61 跳的 load 链, 每跳都带 credibility 和 snippet, 让你看"硬件 reg 一路被传到哪些下游信号"时, 每一跳都有真凭实据。
与 driver chain (例 8) 对称: 例 8 顺上游, 例 10 顺下游, 都带 evidence。
不需要 evidence: get_load_chain(verify=False)。
dump_driver_chain() / dump_load_chain() 1 次调用就拿到整链的 dict (含 summary), 不再需要 N 次 to_context().to_dict()。
from signal_tracer import SignalTracer
import json
sv_code = """
module chain;
logic [7:0] a, b, c, d;
always_comb begin
b = a; // b 读 a
c = b; // c 读 b
d = c; // d 读 c
end
endmodule
"""
t = SignalTracer(sv_code, "chain.sv")
t.build()
# 一次 dump 整链 (driver chain)
dump = t.dump_driver_chain("c")
print(f"signal: {dump['signal_name']}, direction: {dump['direction']}")
print(f"
summary:")
for k, v in dump['summary'].items():
print(f" {k}: {v}")
print(f"
hops ({len(dump['hops'])}):")
for h in dump['hops']:
print(f" hop {h['hop']}: {h['signal_name']} @ {h['file']}:{h['line']} "
f"cred={h['credibility']} verified={h['is_verified']}")
print(f" snippet: {h['snippet']}")
# 只要 summary (轻量, 喂 LLM 第一眼判断)
summary = t.dump_driver_chain("c", summary_only=True)
print(f"
summary_only JSON size: {len(json.dumps(summary))} 字符")输出:
signal: c, direction: upstream
summary:
total_hops: 2
verified_count: 2
high_credibility_count: 2
low_credibility_count: 0
avg_credibility: 1.0
min_credibility: 1.0
cross_files: ['chain.sv']
hops (2):
hop 1: c @ chain.sv:5 cred=1.0 verified=True
snippet: c = b;
hop 2: b @ chain.sv:4 cred=1.0 verified=True
snippet: b = a;
OpenTitan 真实示例 (uart allzero_cnt_q): 30 跳 driver chain 1 次 dump, ~15.8KB JSON, 含 summary + 30 hops 详细 evidence。
可选参数:
include_context_window=True(默认) — 含 context_window before/afterinclude_scope_text=False(默认) — 不含 scope_text (可较长)summary_only=False(默认) — 含 hops; True 时只返回 summarymax_depth=10(默认) — 链最大深度
dump_multi_drivers() 1 次调用拿到所有多驱动信号的冲突列表 + 每个 driver 的 evidence, 不再需要 N 次手动调用。
from signal_tracer import SignalTracer
import json
sv_code = """
module m;
logic [7:0] data;
logic clk, rst_n, mode;
always_ff @(posedge clk) begin
if (rst_n && mode == 0) data <= 8'hAA;
end
always_ff @(posedge clk) begin
if (rst_n && mode == 1) data <= 8'h55;
end
endmodule
"""
t = SignalTracer(sv_code, "m.sv")
t.build()
dump = t.dump_multi_drivers()
print(f"summary:")
for k, v in dump['summary'].items():
print(f" {k}: {v}")
print(f"
conflicts ({len(dump['conflicts'])}):")
for c in dump['conflicts']:
print(f"
⚠️ {c['signal_name']}: {c['driver_count']} drivers, {c['scope_count']} scopes")
for d in c['drivers']:
print(f" {d['file']}:{d['line']} cred={d['credibility']} source_expr={d['source_expr']!r}")
print(f" snippet: {d['snippet']}")
# 只要 summary (轻量)
summary = t.dump_multi_drivers(summary_only=True)
print(f"
summary_only JSON: {len(json.dumps(summary))} 字符")输出:
summary:
total_conflict_signals: 1
total_drivers: 2
avg_drivers_per_conflict: 2.0
avg_credibility: 1.0
min_credibility: 1.0
all_verified: True
cross_files: ['m.sv']
conflicts (1):
⚠️ m.data: 2 drivers, 2 scopes
m.sv:6 cred=1.0 source_expr="8'hAA"
snippet: if (rst_n && mode == 0) data <= 8'hAA;
m.sv:10 cred=1.0 source_expr="8'h55"
snippet: if (rst_n && mode == 1) data <= 8'h55;
OpenTitan 真实示例 (uart): 8 个冲突信号 (36 个总 driver), 全 credibility 1.0, 跨 3 个文件。LLM 1 个 prompt section 就看到所有冲突和每个 driver 的真凭实据。
可选参数:
summary_only=False(默认) — 含 conflicts; True 时只返回 summaryinclude_context_window=True(默认) — 含 context before/afterinclude_scope_text=False(默认) — 不含 scope_text (字符串可能较长)verify=True(默认) — 自动填充 evidence
所有 trace 都能用箭头式表达数据流向 — 人眼在终端/文档/聊天里一眼看懂谁驱动谁、谁被读。
| 符号 | 含义 |
|---|---|
← |
driver (信号被这个表达式驱动) |
→ |
load (信号被这个表达式读取) |
⚠ |
多驱动冲突 |
✓ |
verified (credibility >= 0.8) |
✗ |
not verified (credibility < 0.8) |
⤴ |
cross-file 跨文件 |
↻ |
cycle detected |
from signal_tracer import trace_signal, SignalTracer
# 1. TraceResult / TraceSummary — 一键全部 drivers+loads
result = trace_signal("count", sv, "counter.sv")
print(result.to_arrow())
# DRIVERS (2):
# count ← 8'h00 @ counter.sv:9 [counter] ✓ cred=1.00
# count ← count + data_in @ counter.sv:10 [counter] ✓ cred=1.00
# LOADS (0):
# (none)
# 2. 单条 trace
for d in result.drivers:
print(d.to_arrow())
# count ← 8'h00 @ counter.sv:9 [counter] ✓ cred=1.00
# 3. SignalTracer — 一键多驱动
t = SignalTracer()
t.add_file("buggy.sv", multi_sv); t.build()
print(t.multi_drivers_to_arrow())
# data ⚠ 2 drivers:
# data ← 8'hAA @ buggy.sv:9 [buggy] ✓ cred=1.00
# data ← 8'h55 @ buggy.sv:12 [buggy] ✓ cred=1.00
# 4. 链追踪 — 完整上溯/下溯链
print(t.chain_to_arrow("data_out", direction="driver"))
# data_out ← c ⤴ ← b ← a
# 5. dump 转箭头 — 全链 + summary
print(t.dump_to_arrow("data_out"))
# Chain data_out: 4 hops, avg_cred=0.95, cross-file ✓, cycle ✗
# data_out ← c ✓ ← b ✓ ← a ✓from signal_tracer import format_driver, format_load, format_all, ARROW_DRIVER, ARROW_LOAD
print(format_driver(result.drivers[0]))
print(format_all(result))
print(ARROW_DRIVER) # '←'
print(ARROW_LOAD) # '→'| 方法 | 适合场景 |
|---|---|
summary() |
短/字段化/适合 LLM 当 context (e.g. 'counter.sv:10 (always_ff) clk=clk reset=rst_n cond=[!rst_n]') |
to_arrow() |
箭头/数据流/适合人眼扫/聊天贴出来 (e.g. 'count ← count + data_in @ counter.sv:11 ✓ cred=1.0') |
两者并存, 根据场景选。
当链太长 (≥ 4 个信号) 或要贴到文档/聊天里, 一行箭头看不清楚。换成 tree 风格 (类似 tree(1) 工具的输出) 或 vertical 风格 (每行一个信号 + 箭头):
t = SignalTracer()
t.add_file('top.sv', top_code)
t.add_file('mid.sv', mid_code)
t.add_file('leaf.sv', leaf_code)
t.build()5 种风格 (全部带 tree 节点) — 选一个:
# 1. arrow (默认): 一行, 短链友好
print(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='arrow'))
# out_data ← out_data ← mid_data (↻ cycle detected)
# 2. tree: tree 风格, Unicode box-drawing
print(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='tree'))
# Driver chain: top.u_mid.u_leaf_a.out_data (3 hops, ↻ cycle)
# ├─ out_data [leaf.sv:11] ✓ cred=1.00
# │ ← out_data [leaf.sv:12] ✓ cred=1.00
# └─ ← mid_data [leaf.sv:9] ✓ cred=1.00
# 3. ascii: 同 tree 但用 ASCII (老终端 / 邮件 / 纯文本 log)
print(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='ascii'))
# Driver chain: top.u_mid.u_leaf_a.out_data (3 hops, ↻ cycle)
# +-- out_data [leaf.sv:11] ✓ cred=1.00
# | ← out_data [leaf.sv:12] ✓ cred=1.00
# +-- ← mid_data [leaf.sv:9] ✓ cred=1.00
# 4. vertical: 每行一个信号, 缩进表示深度
print(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='vertical'))
# out_data @ leaf.sv:11 ✓ cred=1.00
# ← out_data @ leaf.sv:12 ✓ cred=1.00
# ← mid_data @ leaf.sv:9 ✓ cred=1.00
# 5. all / both: arrow + tree 两个都返
print(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='all'))dump 也支持 tree/vertical:
# dump_to_arrow 默认 1 行, style='tree' 转 tree
print(t.dump_to_arrow('top.u_mid.u_leaf_a.out_data', style='tree'))
# Driver chain: top.u_mid.u_leaf_a.out_data (3 hops)
# ├─ out_data [leaf.sv:11]
# │ ← out_data [leaf.sv:12]
# └─ ← mid_data [leaf.sv:9]
# 还可以用 alias
t.chain_to_tree(signal, use_box=True) # tree style
t.chain_to_tree(signal, use_box=False) # ASCII
t.chain_to_vertical(signal) # vertical
t.dump_to_tree(signal, use_box=True) # dump + tree
t.dump_to_tree(signal, use_box=False) # dump + asciiformat_driver_chain / format_dump_summary 也都接受 style 参数, 给纯函数用户用。
怎么选风格:
- 短链 (≤ 3 个信号):
arrow(默认) — 一行就够 - 中链 (4-7) + 看代码:
tree— 节点 + location + cred 一起看 - 中链 + 贴 chat/markdown:
vertical— 不依赖 box-drawing - 老终端 / 邮件 / 纯文本 log:
ascii— 不需要 Unicode - 要全面:
all— arrow + tree 都给
from signal_tracer import trace_signal, trace_signal_from_file
result = trace_signal("signal_name", sv_code, "file.sv")
result = trace_signal_from_file("signal_name", "path/to/file.sv")from signal_tracer import SignalTracer, TraceSummary, ContextBundle
t = SignalTracer()
t.add_file('top.sv', top_code)
t.add_file('sub.sv', sub_code)
t.build()
result = t.trace("signal_name") # TraceSummary| 方法 | 说明 |
|---|---|
add_file(path, code) |
加一个 .sv 文件到项目(链式) |
build() |
解析所有文件,构建追踪索引(必须先调) |
trace(name) |
追踪信号,返回 TraceSummary(智能匹配 hpath / leaf / 数组 / 后缀) |
trace_drivers(name) |
只返回 driver 列表 |
trace_loads(name) |
只返回 load 列表 |
find_multi_drivers() |
找所有被 ≥2 个 scope 驱动的信号(多驱动检测) |
get_driver_count(name) |
返回某信号的不同 scope 数 |
get_driver_chain(name, max_depth=10) |
递归查上游 driver 链(带 cycle detection) |
| 方法 | 说明 |
|---|---|
get_clock_domains() |
该信号涉及的所有时钟 |
is_multi_driver() |
是否被多个 scope 驱动 |
get_driver_scopes() |
所有驱动 scope 源码(去重) |
to_contexts() |
打包所有 driver 为 List[ContextBundle] |
ContextBundle(frozen=True,不可变)打包:
file/line/char_offset— 位置scope_text/scope_line_start/end/scope_kind— scope 信息clock/reset— 时钟/复位condition/condition_stack— 嵌套条件栈is_port/port_direction/hierarchical_path— 端口 + 层次confidence— 置信度to_dict()/summary()— 序列化 / 一行可读
| 指标 | 数据 |
|---|---|
| 公开 API 测试 | 210/210 通过 (~4s) (含 50 个箭头式输出测试: 28 M5.1j + 22 M5.1k tree/vertical/ascii) |
| 跨版本验证 | ✅ pyslang 10.x 和 11.x 都 210/210 (make test-cross-version) |
| 真实项目验证 | ✅ OpenTitan 6 模块 (30,218 drivers, 0 warning, 0 empty) |
| 跨文件 fixture | 3 文件 / 3 层 instance (tests/fixtures/m3_hierarchical/) |
| Benchmark | 11/11 (0 warning, 0 exception) |
| 旧架构测试 | 已迁移 tests/_legacy/, 主测试 68/68 干净通过 |
| 版本 | alpha |
跑测试:
python -m pytest tests/ -v
# 跨 pyslang 10/11 版本验证
make test-cross-version主测试 tests/unit/test_signal_tracer.py 包含 23 个 TestClass, 117 个 测试:
| 阶段 | TestClass | 测试数 | 覆盖点 |
|---|---|---|---|
| M0 | TestBasic, TestControlFlow, TestArrays, TestNoCrashes |
— | 基础 always_ff/comb/latch, if/else/case 条件, 1D/2D 数组 |
| M1 | TestTraceResultFields |
— | 完整 TraceResult 字段填充 |
| M1.5 | TestMultiDriver, TestClockResetExtraction, TestDriverChain |
— | 多驱动检测, clock/reset 提取, driver_chain 递归 (cycle detection) |
| M2 | TestContextAccuracy, TestContextBundle |
— | line/scope_text 准确性, ContextBundle frozen dataclass |
| M3 | TestMultiFile |
— | 多文件 build, 层次路径 (top.u_mid.u_leaf), 后缀匹配 |
| M4 | TestExpressionCoverage, TestContinuousAssignRobustness, TestMultiFileLineFallback, TestScopeFilePath, TestAdditionalExpressions |
+5 | 17 种 SV 表达式, InvalidExpression 防御, 跨文件行号 (SourceManager), TraceResult.file 精确, 嵌套 MemberAccess+RangeSelect |
| M4.1 | TestInterfaceModport |
+6 | Interface/Modport 信号追踪 (HierarchicalValue), 跨 modport 读写, m.data[3:0] 位选 |
| M5.1 | TestCodeEvidence |
+8 | 代码证据链 (CodeEvidence), credibility_score 0-1 量化, is_verified 标记, trace_verified() 自动验证 |
| M5.1b | TestMultiDriverEvidence |
+4 | find_multi_drivers(verify=True) 默认自动带 evidence (看到冲突 + 真凭实据) |
| M5.1c | TestDriverChainEvidence |
+4 | get_driver_chain(verify=True) 默认链上每跳自动带 evidence (顺藤摸瓜带 credibility) |
| M5.1d | TestTraceLoadsEvidence |
+7 | trace()/trace_drivers()/trace_loads() 默认 verify=True, drivers 和 loads 都自动带 evidence (查谁读了某信号) |
| M5.1e | TestLoadChainEvidence |
+5 | get_load_chain(verify=True) 顺藤摸瓜查下游 (与 driver chain 对称) |
| M5.1f | TestDumpChain |
+9 | dump_driver_chain()/dump_load_chain() 一次 dump 整链为 dict (含 summary, LLM 友好) |
| M5.1g | TestDumpMultiDrivers |
+6 | dump_multi_drivers() 一次 dump 多驱动检测 (冲突列表 + 每个 driver evidence) |
| M5.1h | TestSyntaxNodeSnapshot |
+6 | syntax-based evidence 路径: SyntaxNodeSnapshot 冻结 + OpenTitan 跨文件 snippet 精度 |
| M5.1h+ | (Makefile target) | — | 跨 pyslang 10.x/11.x 验证 (make test-cross-version 双 venv 跑 160+160 tests) |
各阶段演进:
| 阶段 | 新增测试 | 累计 |
|---|---|---|
| M0 | 13 | 13 |
| M1 | 13 | 26 |
| M1.5 | 20 | 46 |
| M2 | 13 | 59 |
| M3 | 9 | 68 |
| M4 | 5 | 73 |
| M4.1 | 6 | 74 |
| M5.1 | 8 | 82 |
| M5.1b | 4 | 86 |
| M5.1c | 4 | 90 |
| M5.1d | 7 | 97 |
| M5.1e | 5 | 102 |
| M5.1f | 9 | 111 |
| M5.1g | 6 | 117 |
| M5.1h | 6 | 123 |
主测试套件 (含 test_signal_tracer.py 和 test_evidence_via_syntax.py) 累计 160 个 (其他测试文件: 边界/CI/legacy 37 个)。
详见 tests/README.md 和 TEST_PLAN.md。
每个 trace 都带可证伪的代码证据链 — 读回实际文件, 验证 source_expr 和 signal_name 真的在该行。LLM/用户能反查 trace 真的对, 而不是默默相信。
# 方式 1: trace_signal + 传 file_content
result = trace_signal('count', sv_code, 'counter.sv')
for ctx in result.to_contexts(file_content=sv_code):
d = ctx.to_dict()
print(f" credibility={d['credibility_score']} is_verified={d['is_verified']}")
print(f" snippet: {d['evidence_snippet']}")
print(ctx.code_evidence.to_evidence_string())
# 方式 2: SignalTracer 多文件 + 自动 in-memory 验证
t = SignalTracer()
t.add_file('top.sv', top_code)
t.add_file('sub.sv', sub_code)
t.build()
result = t.trace_verified('top.u_sub.signal') # 自动用 self._files 验证| 验证项 | 分值 | 说明 |
|---|---|---|
file_readable |
+0.2 | 文件能读 |
snippet_present |
+0.2 | line 存在 |
matches_source_expr |
+0.4 | 文本里真找到 source_expr |
matches_signal_name |
+0.2 | 文本里真找到 signal_name |
is_verified = file_readable ∧ snippet_present ∧ (matches_source ∨ matches_signal)
tx_enable @ uart_core.sv:77:
snippet: 'assign tx_enable = reg2hw.ctrl.tx.q;'
matches: source_expr ✓, signal_name ✓
credibility: 1.0/1.0 (VERIFIED)
context_before: ['']
context_after: [' assign rx_enable = reg2hw.ctrl.rx.q;', ...]
readbuf_threshold @ spi_device.sv:600:
snippet: 'assign readbuf_threshold = reg2hw.read_threshold.q[BufferAw:0];'
credibility: 1.0/1.0 (VERIFIED) — 含 BufferAw 的 RangeSelect 也 OK
| 场景 | credibility | is_verified |
|---|---|---|
| 文件不存在 | 0.0 | ❌ |
| 可读但都不匹配 | 0.4 | ❌ |
| 仅 signal_name 匹配 | 0.6 | ✅ |
| 全部匹配 | 1.0 | ✅ |
evidence 不会"假装 OK",会真实反映可信度。
核心问题: file-based evidence 依赖 file:line 准不准——line 错 (e.g. multi-statement always_ff block 里) 就会读到错的源码。M5.1h 走从 pyslang 语法树拿 evidence 的路径:line 用 SyntaxNode.sourceRange 算,snippet 用 str(SyntaxNode) 拿,跨文件也准。
- 不依赖文件存在: 内存里只有 SV code 也能产出 evidence
- 总是和 pyslang 解析结果 100% 一致: file-based 有 line 错 / 文件被改 / 路径不同步的风险,syntax-based 没有
- 不依赖 line 准不准: line 错了 syntax 仍指向正确位置
- 多文件零 cost: 不需要记哪个 file 对应哪个 offset
import sys, os
sys.path.insert(0, 'src')
from signal_tracer import SignalTracer
uart_dir = '/Users/fundou/my_dv_proj/opentitan/hw/ip/uart/rtl/'
t = SignalTracer()
for f in ['uart.sv', 'uart_core.sv', 'uart_reg_pkg.sv', 'uart_reg_top.sv', 'uart_rx.sv', 'uart_tx.sv']:
p = os.path.join(uart_dir, f)
if os.path.exists(p):
t.add_file(p, open(p).read())
t.build()
# trace_drivers('tx_enable') → evidence chain
for d in t.trace_drivers('tx_enable'):
ctx = d.to_context()
cd = ctx.to_dict()
print(f'{os.path.basename(cd["file"])}:{cd["line"]} cred={cd["credibility_score"]} verif={cd["is_verified"]}')
print(f' snippet: {cd["evidence_snippet"]!r}')输出:
uart_core.sv:77 cred=1.0 verif=True
snippet: 'assign tx_enable = reg2hw.ctrl.tx.q;'
Single-driver 信号:
tx_enable @ uart_core.sv:77 snippet='assign tx_enable = reg2hw.ctrl.tx.q;'
rx_enable @ uart_core.sv:78 snippet='assign rx_enable = reg2hw.ctrl.rx.q;'
allzero_cnt_q @ uart_core.sv:109 snippet="if (!rst_ni) allzero_cnt_q <= '0;"
allzero_cnt_q @ uart_core.sv:110 snippet='else if (rx_enable) allzero_cnt_q <= allzero_cnt_d;'
Multi-driver 冲突 (跨 3 个文件, snippet 精确定位每个 driver 位置):
tx @ uart_tx.sv:32 snippet='assign tx = tx_q;'
tx @ uart_core.sv:217 snippet='assign tx = line_loopback ? rx : tx_out_q ;'
baud_div_q @ uart_tx.sv:36 snippet="baud_div_q <= 4'h0;"
baud_div_q @ uart_rx.sv:41 snippet="baud_div_q <= 4'h0;"
baud_div_q @ uart_rx.sv:47 snippet='baud_div_q <= baud_div_d;'
tick_baud_q @ uart_tx.sv:37 snippet="tick_baud_q <= 1'b0;"
tick_baud_q @ uart_tx.sv:41 snippet="tick_baud_q <= 1'b0;"
tick_baud_q @ uart_rx.sv:42 snippet="tick_baud_q <= 1'b0;"
tick_baud_q @ uart_rx.sv:48 snippet='tick_baud_q <= tick_baud_d;'
LLM 看 1 行 snippet 就能反查"tx 实际上是哪个 line 在驱动",再 cat uart_core.sv:217 看到 assign tx = line_loopback ? rx : tx_out_q ; 就能确认是 loopback 模式。
走 syntax 路径会调用 str(SyntaxNode) 拿 snippet。但 pyslang 的 SyntaxNode.__str__() 依赖内部 buffer 状态,第二个 Compilation 创建后,第一个的 SyntaxNode 调 str() 会返回截断的旧内容 (e.g. b = foo(a) → b = foo,丢 (a))。M5.1h 引入 SyntaxNodeSnapshot 包装:
- inject 时立刻
str(node)拿到完整文本,冻结到self.text - 代理
sourceRange/kind/__iter__等元数据(build_evidence_via_syntax和_find_subexpr_for_signal需读) __str__优先返回冻结的 text,不受后续 Compilation 创建影响
6 个回归测试 (tests/unit/test_evidence_via_syntax.py::TestSyntaxNodeSnapshot) 锁定这个行为,包括多 tracer 场景下的冻结验证。
| 场景 | file-based | syntax-based | 说明 |
|---|---|---|---|
单行 assign (assign x = y;) |
✅ line 准 | ✅ snippet 准 | 两者都好 |
| Multi-statement always_ff block | 两者各有不足 (block 级粒度) | ||
| Multi-driver 冲突 (跨文件) | ✅ line 准 | ✅ 跨文件准 | syntax 路径优势在跨文件 |
| 文件被改/不同步 | ❌ | ✅ | syntax 路径不依赖文件 |
| 内存-only SV code | ❌ | ✅ | syntax 路径唯一选 |
推荐:默认走 to_context(source_mode='auto'),在 line 准的时候走 file-based 拿到更多 context;line 错/跨文件/内存模式走 syntax-based。
sv-trace 在两个主版本下都保持 160/160 tests pass。安装时不需要指定上限 (pyslang>=10.0),因为项目里加了 try/fallback import pattern 走 11 的新位置。
| pyslang | sv-trace 1.0.0 | 状态 |
|---|---|---|
| 10.0.x | ✅ work | 测过 (主推 venv) |
| 11.0.x | ✅ work | 测过 (make test-cross-version) |
| 12+ | ❓ | 未测 |
$ make test-cross-version
Testing wheel on pyslang 10.0...
160 passed in 7.59s
Testing wheel on pyslang 11.0...
160 passed in 6.05s这个 target 会启两个 venv(一个 v10,一个 v11),各装 wheel 跑 pytest,确保未来升级 pyslang 时不会悄无声息地 break。
pyslang 是个活跃开发的 C++ binding,每年大版本会重排 API。从 v10 升 v11 移走了 SyntaxTree 到 pyslang.syntax、移走了 Compilation 到 pyslang.ast。我们加 try/fallback 兼容两层都吃;不想用 <11 上限让以后 v12/v13 用户装不上。
在 OpenTitan 上验证, 全部 6 模块 0 warning + 0 empty driver:
| 模块 | .sv 文件数 | drivers | 空 expr | 备注 |
|---|---|---|---|---|
| uart | 6 | 418 | 0% | 起始验证模块 |
| spi_device | 19 | 3,229 | 0% | 涵盖 Streaming concat ({<<8{...}}) |
| dma | 4 | 401 | 0% | 涵盖 inside 集合成员判断 |
| i2c | 10 | 1,235 | 0% | |
| aes | 40 | 24,065 | 0% | 大型模块, 涵盖 StructuredAssignmentPattern |
| hmac | 4 | 870 | 0% | 涵盖 assert property (SVA) 跳过 |
M4 能力覆盖的 SV 语法:
- 表达式:
MemberAccess/RangeSelect/ElementSelect/BinaryOp/UnaryOp/ConditionalOp/CastExpression/Call/Replication/Concatenation/Streaming({<<8{x}}/{>>8{x}}) /Inside/UnbasedUnsizedIntegerLiteral('0/'1) /StructuredAssignmentPattern/SimpleAssignmentPattern/LValueReference/DataType/HierarchicalValue(Interface/Modport 访问, M4.1) - 证据链: 每个 trace 读回实际文件交叉验证,
credibility_score0-1 量化,is_verified标记 (M5.1) - 多驱动检测 + 证据链:
find_multi_drivers(verify=True)默认自动带 evidence, 看到冲突 + 真凭实据 (M5.1b) - 驱动链 + 证据链:
get_driver_chain(verify=True)链上每跳自动带 evidence, 顺藤摸瓜带 credibility (M5.1c) - trace + 证据链:
trace()/trace_drivers()/trace_loads()默认 verify=True, drivers 和 loads 都自动带 evidence (M5.1d) - load 链 + 证据链:
get_load_chain(verify=True)顺藤摸瓜下游, 链上每条 load 都带 evidence (M5.1e, 与 driver chain 对称) - dump_chain: 一次 dump 整链为 dict (含 summary avg/min/credibility/cross_files), 喂 LLM 1 个 prompt section 就够 (M5.1f)
- dump_multi_drivers: 一次 dump 多驱动检测 (冲突列表 + 每个 driver evidence + 全局 summary), LLM 一眼看到所有冲突 (M5.1g)
- syntax-based evidence: 从 pyslang SyntaxTree 直接拿, 跨文件 100% 准, 不依赖 file 存在 (M5.1h)
- 嵌套: 任意深度 MemberAccess (e.g.
reg2hw.ctrl.tx.q) + 跨 RangeSelect (reg2hw.val[BufferAw:0]) - 跨文件: 多 .sv 编译为同一 Compilation, 跨模块引用 + 层次路径 (
uart.uart_core.tx_enable) - 跨文件行号:
pyslang SourceManager.getLineNumber()走 SourceLocation.buffer 精准算行 - 跨文件 file path: 每个 ScopeInfo.file_path 走 SourceManager.getFileName() 拿到正确文件名
- SVA 跳过:
ConcurrentAssertionStatement(assert property) 不产生 driver/load trace
未支持 (边缘场景):
复杂 type system (interface/modport)— M4.1 已支持 (HierarchicalValueExpression 完整追踪, 跨 master/slave modport 都可, 含 m.data[3:0] 位选)- modport direction (input/output) 区分 driver/load — 尚未实现 (现在 input 和 output 都被当 driver, 可能误报多驱动)
- Clocking block / Property/Sequence 内部
- System task ($cast, $readmemh) 中的信号
- M5.1 evidence 的
matches_source_expr是字面量子串匹配 — pyslang 文本格式 (如count Add data_in) 与源码 (count + data_in) 不完全一致时, 命中率会降, 反映在 credibility_score 上, 不会静默接受
sv-trace/
├── src/
│ ├── __init__.py
│ ├── sv_manager.py # SV 文件加载、源码定位
│ └── signal_tracer/ # 核心
│ ├── models.py # TraceResult / TraceSummary / ContextBundle / ScopeInfo
│ ├── tracer.py # SignalTracer: 语义层 driver/load
│ ├── port_resolver.py # PortResolver: 语法层端口连接
│ └── signal_tracer_app.py # SignalTracerApp: 单文件跨模块(兼容)
├── benchmarks/ # 12 个 SV fixture (基础 always/case/FSM/...)
├── tests/
│ ├── unit/test_signal_tracer.py # 117 个公开 API 测试 (含 8 M5.1 + 4 M5.1b + 4 M5.1c + 7 M5.1d + 5 M5.1e + 9 M5.1f + 6 M5.1g)
│ ├── fixtures/m3_hierarchical/ # 3 文件 / 3 层 instance fixture
│ │ ├── top.sv
│ │ ├── mid.sv
│ │ └── leaf.sv
│ ├── unit/trace/sv_cases/ # 50+ .sv fixture 语料库
│ ├── targeted/ advanced/ testbed/ # .sv fixture
│ ├── _legacy/ # 重构前失效测试(归档)
│ └── README.md
├── archive/ # 旧 src/ 完整代码
├── STRUCTURE.md # 详细架构 / API 字段表
├── TODO.md # 路线图
├── TEST_PLAN.md # 测试计划
├── SKILL.md # Agent 集成 (供 AI agent 调用)
├── pyproject.toml
└── pytest.ini
- ✅ M0 P0 bug 修复(TimedStatement 路径处理)
- ✅ M1 公开 API 测试覆盖(13/13)
- ✅ M1.5 多驱动检测 / clock-reset 提取 / driver_chain 递归(20/20)
- ✅ M2 上下文召回(line 准确性 + ContextBundle 数据结构,13/13)
- ✅ M3 跨文件支持 + 层次路径追踪(9/9)
- ✅ M4 真实项目验证(OpenTitan 6 模块, 0 warning/0 empty, 30,218 drivers 总计)
- ✅ M4.1 Interface/Modport 信号追踪(HierarchicalValue 完整覆盖, 6 个新测试)
- ✅ M5.1 代码证据链 (CodeEvidence) - 让 trace 自证, credibility 0-1 量化
- ✅ M5.1b find_multi_drivers 整合 evidence - 多驱动检测带 credibility
- ✅ M5.1c get_driver_chain 整合 evidence - 顺藤摸瓜链上每跳带 credibility
- ✅ M5.1d trace/trace_drivers/trace_loads 整合 evidence - drivers 和 loads 都带 credibility
- ✅ M5.1e get_load_chain 整合 evidence - 顺藤摸瓜查下游 (与 driver chain 对称)
- ✅ M5.1f dump_chain 一次 dump 整链为 JSON - 含 summary, LLM 友好
- ✅ M5.1g dump_multi_drivers - 一次 dump 多驱动检测 (冲突 + 每个 driver 证据)
- ✅ M5.1h 代码证据链语法路径 - 从 pyslang SyntaxTree 直接拿 evidence (SyntaxNodeSnapshot 防 buffer 复用, 跨文件 100% 准)
- ✅ M5.1h 跨 pyslang 10.x / 11.x 兼容 (try/fallback import pattern,
make test-cross-version自动验证两版本) - ✅ M5.1j 人类友好箭头式输出 -
to_arrow()/to_arrow_all()/chain_to_arrow()/multi_drivers_to_arrow()/dump_to_arrow()五个 API 层级, 8 个独立 formatter 函数 (driver/load/chain/multi/evidence/dump), 箭头统一语义 (←/→/⚠/✓/✗/⤴/↻) - ✅ M5.1k Tree/Vertical/ASCII 风格 - 长链/文档/聊天友好, 5 种风格 (
arrow默认 /treeUnicode /ascii/vertical/all), 适用于chain_to_arrow()/dump_to_arrow()/format_driver_chain()/format_dump_summary(), alias 方法:chain_to_tree()/chain_to_vertical()/dump_to_tree() - 📋 M5.2+ 极致优化(增量、并发、缓存)
完整路线图见 TODO.md。
明确划界,下列需求不在本项目范围内:
- ❌ CDC / 多驱动 / 未初始化 / 复位域分析
- ❌ 面积 / 功耗 / 性能估算
- ❌ FSM 提取 / SVA 生成 / 覆盖率建议
- ❌ 约束分析 / 形式验证
- ❌ TB 复杂度评分 / Lint / Style 检查
- ❌ 类/约束提取 / 可视化
如果未来需要,应作为独立项目开发。
- STRUCTURE.md — 详细架构 / API 字段表 / 数据流图
- TODO.md — 路线图 / 不做的功能 / 历史
- TEST_PLAN.md — 测试计划 / 状态
- SKILL.md — Agent 集成指南 (供 AI agent 调用)
- tests/README.md — 测试总览
MIT