{"id":50928834,"url":"https://github.com/fundou1081/sv-trace","last_synced_at":"2026-06-17T02:03:10.344Z","repository":{"id":354197678,"uuid":"1222542710","full_name":"fundou1081/sv-trace","owner":"fundou1081","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-01T11:31:21.000Z","size":3137,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T13:13:48.842Z","etag":null,"topics":["parser","pyslang","static-analysis","systemverilog"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fundou1081.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-27T13:18:57.000Z","updated_at":"2026-06-01T11:31:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/fundou1081/sv-trace","commit_stats":null,"previous_names":["fundou1081/sv-trace"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/fundou1081/sv-trace","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fundou1081%2Fsv-trace","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fundou1081%2Fsv-trace/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fundou1081%2Fsv-trace/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fundou1081%2Fsv-trace/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fundou1081","download_url":"https://codeload.github.com/fundou1081/sv-trace/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fundou1081%2Fsv-trace/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34430690,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["parser","pyslang","static-analysis","systemverilog"],"created_at":"2026-06-17T02:03:05.376Z","updated_at":"2026-06-17T02:03:10.335Z","avatar_url":"https://github.com/fundou1081.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sv-trace\n\n\u003e SystemVerilog signal tracer — 给一个信号名，返回它在源码里的所有 driver / load，以及完整的上下文（文件、行号、scope 源码、时钟/复位、条件栈、层次路径、跨模块端口连接）。\n\n[![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org)\n[![pyslang](https://img.shields.io/badge/pyslang-10.x_|_11.x-orange.svg)](https://github.com/MikePopoloski/slang)\n[![Status](https://img.shields.io/badge/status-alpha-yellow.svg)]()\n\n## 目标\n\n**只做一件事：信号追踪 + 上下文召回，做到极致。**\n\n不做：CDC 分析、面积/功耗/性能估算、Lint、FSM 提取、约束分析、覆盖率建议、TB 复杂度评分、代码质量评分、依赖图、可视化……（详见 [TODO.md](TODO.md) 的\"不做\"章节）\n\n## 安装\n\n```bash\npip install sv-trace\n# 或本地开发\npip install -e .\n```\n\n唯一依赖：`pyslang \u003e= 10.0`（同时兼容 10.x 和 11.x，详见下 [跨版本兼容](#跨版本兼容) 章节）\n\n## 快速开始\n\n### 1. 单文件（函数式 API）\n\n```python\nimport sys\nsys.path.insert(0, 'src')\n\nfrom signal_tracer import trace_signal\n\nsv_code = \"\"\"\nmodule counter (\n    input  logic       clk,\n    input  logic       rst_n,\n    input  logic [7:0] data_in,\n    output logic [7:0] count\n);\n    always_ff @(posedge clk or negedge rst_n) begin\n        if (!rst_n)\n            count \u003c= 8'h00;\n        else\n            count \u003c= count + data_in;\n    end\nendmodule\n\"\"\"\n\nresult = trace_signal(\"count\", sv_code, \"counter.sv\")\n\nfor d in result.drivers:\n    print(f\"{d.source_expr} @ line {d.line}\")\n    print(f\"  condition_stack: {d.condition_stack}\")\n    print(f\"  clock={d.clock}, reset={d.reset}\")\n    print(f\"  scope:\\n{d.scope_text}\")\n```\n\n输出：\n\n```\n8'h00 @ line 10\n  condition_stack: ['!rst_n']\n  clock=clk, reset=rst_n\n  scope:\nalways_ff @(posedge clk or negedge rst_n) begin\n    if (!rst_n)\n        count \u003c= 8'h00;\n    else\n        count \u003c= count + data_in;\nend\ncount + data_in @ line 12\n  condition_stack: ['!rst_n']\n  clock=clk, reset=rst_n\n  ...\n```\n\n### 2. 多文件 + 层次路径（类式 API）\n\n```python\nfrom signal_tracer import SignalTracer\n\nt = SignalTracer()\nt.add_file('top.sv', open('top.sv').read())\nt.add_file('mid.sv', open('mid.sv').read())\nt.add_file('leaf.sv', open('leaf.sv').read())\nt.build()\n\n# 完整层次路径: 直查 top.u_mid.u_l1.dout\nr1 = t.trace('top.u_mid.u_l1.dout')\nprint(f\"u_l1.dout drivers: {len(r1.drivers)}\")\n\n# 后缀匹配: 找所有 *.dout (跨 instance 聚合)\nr2 = t.trace('dout')\nprint(f\"all dout drivers: {len(r2.drivers)}\")\n```\n\n### 3. ContextBundle (M2)\n\n把一次 trace 的所有上下文打包成一个 frozen dataclass，方便给 LLM：\n\n```python\nfrom signal_tracer import trace_signal, ContextBundle\n\nresult = trace_signal(\"count\", sv_code, \"counter.sv\")\nfor ctx in result.to_contexts():\n    # ctx 是 ContextBundle, frozen, 可哈希, 可 JSON 序列化\n    print(ctx.summary())           # 'counter.sv:10 (always_ff) clock=clk reset=rst_n cond=[!rst_n]'\n    print(json.dumps(ctx.to_dict()))  # 给 LLM 一次性看全所有上下文\n```\n\n## 使用示例\n\n上面快速开始展示了最小用法, 下面是 4 个常见场景。\n\n### 例 1：时序信号追踪 (clock/reset/condition)\n\n每个 driver trace 都携带所属 scope 的时序信息, 能直接看出是哪个时钟/复位域下被驱动：\n\n```python\nfrom signal_tracer import trace_signal\n\nsv_code = '''\nmodule counter (\n    input  logic       clk,\n    input  logic       rst_n,\n    input  logic [7:0] data_in,\n    output logic [7:0] count\n);\n    always_ff @(posedge clk or negedge rst_n) begin\n        if (!rst_n)\n            count \u003c= 8'h00;\n        else if (data_in[7])\n            count \u003c= count + 1;\n        else\n            count \u003c= count - 1;\n    end\nendmodule\n'''\n\nfor d in trace_signal(\"count\", sv_code, \"counter.sv\").drivers:\n    print(f\"  {d.source_expr} @ line {d.line} | clock={d.clock} reset={d.reset} cond={d.condition_stack}\")\n```\n\n输出：\n\n```\n  8'h00 @ line 10 | clock=clk reset=rst_n cond=['!rst_n']\n  count + 1 @ line 12 | clock=clk reset=rst_n cond=['data_in[7]']\n  count - 1 @ line 14 | clock=clk reset=rst_n cond=[]\n```\n\n### 例 2：多驱动检测 (查竞态)\n\n同名信号被多个 always_ff 驱动时可能是 bug, `find_multi_drivers()` 一键报出：\n\n```python\nfrom signal_tracer import SignalTracer\n\nsv_code = '''\nmodule conflict;\n    logic [7:0] data;\n    logic clk, rst_n, mode;\n    always_ff @(posedge clk) begin\n        if (rst_n \u0026\u0026 mode == 0) data \u003c= 8'hAA;\n    end\n    always_ff @(posedge clk) begin\n        if (rst_n \u0026\u0026 mode == 1) data \u003c= 8'h55;\n    end\nendmodule\n'''\n\nt = SignalTracer(sv_code, \"conflict.sv\")\nt.build()\n\nfor sig, drivers in t.find_multi_drivers().items():\n    print(f\"⚠ {sig} 被 {len(drivers)} 个 scope 驱动 (可能竞态)\")\n    for d in drivers:\n        print(f\"   - {d.source_expr} @ line {d.line}\")\n```\n\n输出：\n\n```\n⚠ conflict.data 被 2 个 scope 驱动 (可能竞态)\n   - 8'hAA @ line 7\n   - 8'h55 @ line 11\n```\n\n### 例 3：递归 driver_chain (顺藤摸瓜)\n\n`get_driver_chain()` 逆源查上游, 一路追溯 signal 的源, 带循环检测 (避免 a→b→a 死循环)：\n\n```python\nfrom signal_tracer import SignalTracer\n\nsv_code = '''\nmodule chain;\n    logic [7:0] a, b, c, out;\n    logic clk, rst_n;\n    always_ff @(posedge clk or negedge rst_n) begin\n        if (rst_n) begin\n            a \u003c= 8'h01;\n            b \u003c= a; c \u003c= b + 1; out \u003c= c;\n        end else begin\n            a \u003c= 0; b \u003c= 0; c \u003c= 0; out \u003c= 0;\n        end\n    end\nendmodule\n'''\n\nt = SignalTracer(sv_code, \"chain.sv\")\nt.build()\n\n# out 的驱动源: out \u003c= c, c \u003c= b+1, b \u003c= a, a \u003c= 8'h01 (或复位值)\nchain = t.get_driver_chain(\"out\")\nprint(f\"out 的 driver 链: {' -\u003e '.join(d.signal_name for d in chain)}\")\n```\n\n输出：\n\n```\nout 的 driver 链: out -\u003e c -\u003e b -\u003e a -\u003e a -\u003e b -\u003e c -\u003e out\n```\n\n(看到末尾 `a -\u003e b -\u003e c -\u003e out` 是反向限踪遇到 a 的隐式初始化, 体现 cycle detection 在工作)\n\n### 例 4：跨模块层次路径\n\n`SignalTracer.add_file()` 走多棵 SyntaxTree 同一 Compilation, 跨模块信号可按完整 hpath 查询, 也可按后缀名查所有 instance：\n\n```python\nfrom signal_tracer import SignalTracer\n\ntop_code = '''\nmodule top;\n    logic [7:0] in_data;\n    sub u_sub (.din(in_data));\nendmodule\n'''\n\nsub_code = '''\nmodule sub(input logic [7:0] din);\n    logic [7:0] mid, out;\n    always_comb begin\n        mid = din;\n        out = mid ^ 8'hFF;\n    end\nendmodule\n'''\n\nt = SignalTracer()\nt.add_file(\"top.sv\", top_code)\nt.add_file(\"sub.sv\", sub_code)\nt.build()\n\n# 1) 全路径: 直查 top.u_sub.din\nr1 = t.trace(\"top.u_sub.mid\")\nprint(f\"top.u_sub.mid drivers: {len(r1.drivers)}  -\u003e {r1.drivers[0].source_expr}\")\n\n# 2) 后缀: 跨 instance 聚合所有 .out\nr2 = t.trace(\"out\")\nprint(f\"后缀 'out' 跨 instance drivers: {len(r2.drivers)}\")\nfor d in r2.drivers:\n    print(f\"   {d.hierarchical_path}.{d.signal_name}: {d.source_expr}\")\n```\n\n输出：\n\n```\ntop.u_sub.mid drivers: 1  -\u003e din\n后缀 'out' 跨 instance drivers: 1\n   top.u_sub.out: mid XOR 8'hFF\n```\n\n### 例 5：生成 LLM-ready 上下文 (ContextBundle)\n\n把 trace 结果打包成 JSON 一次性给 LLM, 上下文字段全补齐, 适合“喂上下文问问题”场景：\n\n```python\nfrom signal_tracer import trace_signal\nimport json\n\nsv_code = '''\nmodule state_machine (\n    input  logic       clk,\n    input  logic       rst_n,\n    input  logic [1:0] req,\n    output logic [1:0] state\n);\n    typedef enum logic [1:0] { IDLE, RUN, DONE } state_e;\n    state_e cs, ns;\n    always_ff @(posedge clk or negedge rst_n) begin\n        if (!rst_n) cs \u003c= IDLE;\n        else        cs \u003c= ns;\n    end\n    always_comb begin\n        ns = cs;\n        case (cs)\n            IDLE:  ns = req[0] ? RUN : IDLE;\n            RUN:   ns = req[1] ? DONE : RUN;\n            DONE:  ns = IDLE;\n        endcase\n    end\n    assign state = cs;\nendmodule\n'''\n\nr = trace_signal(\"state\", sv_code, \"state_machine.sv\")\nfor ctx in r.to_contexts():\n    print(ctx.summary())\n    # 给 LLM: 把所有 context 的 to_dict() 拼起来当 system prompt\n    # print(json.dumps(ctx.to_dict(), indent=2))\n```\n\n输出：\n\n```\nstate_machine.sv:16 (continuous_assign) cond=[]\n```\n\n### 例 6：代码证据链 (M5.1) — 让 trace 自证\n\n每个 trace 读回实际文件, 验证 `source_expr` 和 `signal_name` 真的在该行, 输出 `credibility_score` (0-1) 让 LLM/用户能反查。\n\n```python\nfrom signal_tracer import SignalTracer\n\nsv_code = \"\"\"\nmodule counter (\n    input  logic       clk,\n    input  logic       rst_n,\n    input  logic [7:0] data_in,\n    output logic [7:0] count\n);\n    always_ff @(posedge clk or negedge rst_n) begin\n        if (!rst_n) count \u003c= 8'h00;\n        else        count \u003c= count + data_in;\n    end\nendmodule\n\"\"\"\n\nt = SignalTracer(sv_code, \"counter.sv\")\nt.build()\n\n# trace_verified() 自动用 in-memory 内容验证\nfor ctx in t.trace_verified(\"count\").to_contexts():\n    d = ctx.to_dict()\n    print(f\"📍 {ctx.file}:{ctx.line}  |  credibility={d['credibility_score']}  verified={d['is_verified']}\")\n    print(f\"   snippet: {d['evidence_snippet']}\")\n    print(ctx.code_evidence.to_evidence_string())\n```\n\n输出：\n\n```\n📍 counter.sv:9  |  credibility=1.0  verified=True\n   snippet: if (!rst_n) count \u003c= 8'h00;\nEvidence for always_ff @(posedge clk or negedge rst_n) begin\n        if (!rst_n) count \u003c= 8'h00;\n        else        count \u003c= count + data_in;\n    end @ counter.sv:9\n  file_readable: True\n  snippet: if (!rst_n) count \u003c= 8'h00;\n  scope: always_ff @(posedge clk or negedge rst_n) begin  ...\n  matches: source_expr match: ✓, signal_name match: ✓\n  credibility: 1.00/1.0 (VERIFIED)\n     8 |     always_ff @(posedge clk or negedge rst_n) begin\n     9 \u003e if (!rst_n) count \u003c= 8'h00;\n    10 |         else        count \u003c= count + data_in;\n    11 |     end\n```\n\n**credibility_score 量化 (0-1)**:\n- `file_readable` (+0.2) + `snippet_present` (+0.2) + `matches_source_expr` (+0.4) + `matches_signal_name` (+0.2)\n- 防御性: 文件不存在 → 0.0; 不匹配 → 0.4; 仅 signal 匹配 → 0.6; 全匹配 → 1.0\n- evidence 不会\"假装 OK\"，会真实反映可信度 (如 pyslang 把 `count + data_in` 显示为 `count Add data_in` 时, matches_source 自动失败)\n\n### 例 7：多驱动 + 证据链 (M5.1b) — 看到冲突 + 看到冲突的真凭实据\n\n`find_multi_drivers(verify=True)` 默认就为每个冲突 driver 自动填充 evidence, 让你立刻看到每个 driver 的 credibility 和源码位置。\n\n```python\nfrom signal_tracer import SignalTracer\n\nsv_code = \"\"\"\nmodule multi_driver;\n    logic [7:0] data;\n    logic clk, rst_n, mode;\n    always_ff @(posedge clk) begin\n        if (rst_n \u0026\u0026 mode == 0) data \u003c= 8'hAA;\n    end\n    always_ff @(posedge clk) begin\n        if (rst_n \u0026\u0026 mode == 1) data \u003c= 8'h55;\n    end\nendmodule\n\"\"\"\n\nt = SignalTracer(sv_code, \"multi.sv\")\nt.build()\n\nfor sig, drivers in t.find_multi_drivers().items():\n    print(f\"⚠️ {sig} 被 {len(drivers)} 个 scope 驱动\")\n    for d in drivers:\n        d_dict = d.to_context().to_dict()\n        print(f\"   📍 {d.file.split('/')[-1]}:{d.line}  \"\n              f\"credibility={d_dict['credibility_score']:.2f}  \"\n              f\"verified={d_dict['is_verified']}\")\n        print(f\"      snippet: {d_dict['evidence_snippet']}\")\n```\n\n输出：\n\n```\n⚠️ multi_driver.data 被 2 个 scope 驱动\n   📍 multi.sv:6  credibility=1.00  verified=True\n      snippet: if (rst_n \u0026\u0026 mode == 0) data \u003c= 8'hAA;\n   📍 multi.sv:9  credibility=1.00  verified=True\n      snippet: if (rst_n \u0026\u0026 mode == 1) data \u003c= 8'h55;\n```\n\nOpenTitan 真实示例 (spi_device): 21 个多驱动信号, 每个 driver 的 credibility 和 snippet 都自动显示。\n\n不需要 evidence: `find_multi_drivers(verify=False)`。\n\n### 例 8：递归 driver_chain + 证据链 (M5.1c) — 顺藤摸瓜带 credibility\n\n`get_driver_chain(verify=True)` 默认链上每跳自动带 evidence, 让递归查询的每一步都有\"真凭实据\"。\n\n```python\nfrom signal_tracer import SignalTracer\n\nsv_code = \"\"\"\nmodule chain;\n    logic [7:0] a, b, c, out;\n    always_comb begin\n        b = a;     // b 来自 a\n        c = b;     // c 来自 b\n        out = c;   // out 来自 c\n    end\nendmodule\n\"\"\"\n\nt = SignalTracer(sv_code, \"chain.sv\")\nt.build()\n\nchain = t.get_driver_chain(\"out\")  # 默认 verify=True\nfor d in chain:\n    d_dict = d.to_context().to_dict()\n    print(f\"📍 {d.signal_name} @ {d.file.split('/')[-1]}:{d.line}  \"\n          f\"credibility={d_dict['credibility_score']:.2f}\")\n    print(f\"   snippet: {d_dict['evidence_snippet']}\")\n```\n\n输出：\n\n```\n📍 out @ chain.sv:6  credibility=1.00\n   snippet: out = c;\n📍 c @ chain.sv:5  credibility=1.00\n   snippet: c = b;\n📍 b @ chain.sv:4  credibility=1.00\n   snippet: b = a;\n```\n\nOpenTitan 真实示例 (uart `allzero_cnt_q`): 30 跳的驱动链, 每跳都带 credibility。LLM 可以顺着链一步步反查, 看到\"这一跳到底从哪来\"。\n\n不需要 evidence: `get_driver_chain(verify=False)`。\n\n### 例 9：trace_loads + 证据链 (M5.1d) — 查谁读了某信号\n\n`trace_loads(verify=True)` 默认让每条 load 都带 evidence, 让你查\"谁在读这个信号\"时也能反查。\n\n```python\nfrom signal_tracer import SignalTracer\n\nsv_code = \"\"\"\nmodule m;\n    logic [7:0] a, b, c;\n    always_comb begin\n        a = b + c;   // a 读 b, c\n        b = a + 1;   // b 读 a\n    end\nendmodule\n\"\"\"\n\nt = SignalTracer(sv_code, \"m.sv\")\nt.build()\n\n# trace_loads: 查 b 被谁读了\nfor l in t.trace_loads(\"b\"):  # 默认 verify=True\n    d_dict = l.to_context().to_dict()\n    print(f\"📍 {l.hierarchical_path}.{l.signal_name}  @ {l.file.split('/')[-1]}:{l.line}\")\n    print(f\"   credibility={d_dict['credibility_score']:.2f}  verified={d_dict['is_verified']}\")\n    print(f\"   snippet: {d_dict['evidence_snippet']}\")\n```\n\n输出：\n\n```\n📍 m.a  @ m.sv:4  credibility=1.00  verified=True\n   snippet: a = b + c;\n```\n\nOpenTitan 真实示例 (uart `reg2hw`): 20 个 loads, 每条都带 credibility 1.0 + snippet。让你看\"硬件 reg 被哪个 always 块读取\"时, 每一行代码都能反查。\n\n不需要 evidence: `trace_loads(verify=False)` 或 `trace(verify=False)`。\n\n### 例 10：load 链 + 证据链 (M5.1e) — 顺藤摸瓜查下游\n\n`get_load_chain(verify=True)` 跟 `get_driver_chain` 完全对称 — 查\"谁读了这个 signal, 又被谁读\", 链上每条 load 都带 evidence。\n\n```python\nfrom signal_tracer import SignalTracer\n\nsv_code = \"\"\"\nmodule chain;\n    logic [7:0] a, b, c, d;\n    always_comb begin\n        b = a;     // b 读 a\n        c = b;     // c 读 b\n        d = c;     // d 读 c\n    end\nendmodule\n\"\"\"\n\nt = SignalTracer(sv_code, \"chain.sv\")\nt.build()\n\n# 顺流: 查 a 被谁读了, 又被谁读\nfor l in t.get_load_chain(\"a\"):\n    d = l.to_context().to_dict()\n    print(f\"📍 {l.hierarchical_path}.{l.signal_name} @ {l.file.split('/')[-1]}:{l.line}\")\n    print(f\"   credibilidad={d['credibility_score']:.2f}\")\n    print(f\"   snippet: {d['evidence_snippet']}\")\n```\n\n输出：\n\n```\n📍 m.a @ chain.sv:4  credibilidad=1.00\n   snippet: b = a;     // b 读 a\n📍 m.b @ chain.sv:5  credibilidad=1.00\n   snippet: c = b;     // c 读 b\n📍 m.c @ chain.sv:6  credibilidad=1.00\n   snippet: d = c;     // d 读 c\n```\n\nOpenTitan 真实示例 (uart `reg2hw`): 61 跳的 load 链, 每跳都带 credibility 和 snippet, 让你看\"硬件 reg 一路被传到哪些下游信号\"时, 每一跳都有真凭实据。\n\n与 driver chain (例 8) 对称: 例 8 顺上游, 例 10 顺下游, 都带 evidence。\n\n不需要 evidence: `get_load_chain(verify=False)`。\n\n### 例 11：dump_chain 一次 dump 整个链为 JSON (M5.1f) — 喂 LLM 友好\n\n`dump_driver_chain()` / `dump_load_chain()` 1 次调用就拿到整链的 dict (含 summary), 不再需要 N 次 `to_context().to_dict()`。\n\n```python\nfrom signal_tracer import SignalTracer\nimport json\n\nsv_code = \"\"\"\nmodule chain;\n    logic [7:0] a, b, c, d;\n    always_comb begin\n        b = a;     // b 读 a\n        c = b;     // c 读 b\n        d = c;     // d 读 c\n    end\nendmodule\n\"\"\"\n\nt = SignalTracer(sv_code, \"chain.sv\")\nt.build()\n\n# 一次 dump 整链 (driver chain)\ndump = t.dump_driver_chain(\"c\")\nprint(f\"signal: {dump['signal_name']}, direction: {dump['direction']}\")\nprint(f\"\nsummary:\")\nfor k, v in dump['summary'].items():\n    print(f\"  {k}: {v}\")\nprint(f\"\nhops ({len(dump['hops'])}):\")\nfor h in dump['hops']:\n    print(f\"  hop {h['hop']}: {h['signal_name']} @ {h['file']}:{h['line']}  \"\n          f\"cred={h['credibility']}  verified={h['is_verified']}\")\n    print(f\"    snippet: {h['snippet']}\")\n\n# 只要 summary (轻量, 喂 LLM 第一眼判断)\nsummary = t.dump_driver_chain(\"c\", summary_only=True)\nprint(f\"\nsummary_only JSON size: {len(json.dumps(summary))} 字符\")\n```\n\n输出：\n\n```\nsignal: c, direction: upstream\n\nsummary:\n  total_hops: 2\n  verified_count: 2\n  high_credibility_count: 2\n  low_credibility_count: 0\n  avg_credibility: 1.0\n  min_credibility: 1.0\n  cross_files: ['chain.sv']\n\nhops (2):\n  hop 1: c @ chain.sv:5  cred=1.0  verified=True\n    snippet: c = b;\n  hop 2: b @ chain.sv:4  cred=1.0  verified=True\n    snippet: b = a;\n```\n\nOpenTitan 真实示例 (uart `allzero_cnt_q`): 30 跳 driver chain 1 次 dump, ~15.8KB JSON, 含 summary + 30 hops 详细 evidence。\n\n可选参数:\n- `include_context_window=True` (默认) — 含 context_window before/after\n- `include_scope_text=False` (默认) — 不含 scope_text (可较长)\n- `summary_only=False` (默认) — 含 hops; True 时只返回 summary\n- `max_depth=10` (默认) — 链最大深度\n\n### 例 12：dump_multi_drivers 一次 dump 多驱动检测 (M5.1g) — 看到冲突 + 每个 driver 的证据\n\n`dump_multi_drivers()` 1 次调用拿到**所有多驱动信号**的冲突列表 + 每个 driver 的 evidence, 不再需要 N 次手动调用。\n\n```python\nfrom signal_tracer import SignalTracer\nimport json\n\nsv_code = \"\"\"\nmodule m;\n    logic [7:0] data;\n    logic clk, rst_n, mode;\n    always_ff @(posedge clk) begin\n        if (rst_n \u0026\u0026 mode == 0) data \u003c= 8'hAA;\n    end\n    always_ff @(posedge clk) begin\n        if (rst_n \u0026\u0026 mode == 1) data \u003c= 8'h55;\n    end\nendmodule\n\"\"\"\n\nt = SignalTracer(sv_code, \"m.sv\")\nt.build()\n\ndump = t.dump_multi_drivers()\nprint(f\"summary:\")\nfor k, v in dump['summary'].items():\n    print(f\"  {k}: {v}\")\nprint(f\"\nconflicts ({len(dump['conflicts'])}):\")\nfor c in dump['conflicts']:\n    print(f\"\n  ⚠️ {c['signal_name']}: {c['driver_count']} drivers, {c['scope_count']} scopes\")\n    for d in c['drivers']:\n        print(f\"     {d['file']}:{d['line']}  cred={d['credibility']}  source_expr={d['source_expr']!r}\")\n        print(f\"       snippet: {d['snippet']}\")\n\n# 只要 summary (轻量)\nsummary = t.dump_multi_drivers(summary_only=True)\nprint(f\"\nsummary_only JSON: {len(json.dumps(summary))} 字符\")\n```\n\n输出：\n\n```\nsummary:\n  total_conflict_signals: 1\n  total_drivers: 2\n  avg_drivers_per_conflict: 2.0\n  avg_credibility: 1.0\n  min_credibility: 1.0\n  all_verified: True\n  cross_files: ['m.sv']\n\nconflicts (1):\n\n  ⚠️ m.data: 2 drivers, 2 scopes\n     m.sv:6  cred=1.0  source_expr=\"8'hAA\"\n       snippet: if (rst_n \u0026\u0026 mode == 0) data \u003c= 8'hAA;\n     m.sv:10  cred=1.0  source_expr=\"8'h55\"\n       snippet: if (rst_n \u0026\u0026 mode == 1) data \u003c= 8'h55;\n```\n\nOpenTitan 真实示例 (uart): 8 个冲突信号 (36 个总 driver), 全 credibility 1.0, 跨 3 个文件。LLM 1 个 prompt section 就看到所有冲突和每个 driver 的真凭实据。\n\n可选参数:\n- `summary_only=False` (默认) — 含 conflicts; True 时只返回 summary\n- `include_context_window=True` (默认) — 含 context before/after\n- `include_scope_text=False` (默认) — 不含 scope_text (字符串可能较长)\n- `verify=True` (默认) — 自动填充 evidence\n\n## 人类友好箭头式输出 (M5.1j)\n\n所有 trace 都能用箭头式表达数据流向 — 人眼在终端/文档/聊天里一眼看懂谁驱动谁、谁被读。\n\n### 箭头语义 (固定)\n\n| 符号 | 含义 |\n|------|------|\n| `←` | driver (信号被这个表达式驱动) |\n| `→` | load (信号被这个表达式读取) |\n| `⚠` | 多驱动冲突 |\n| `✓` | verified (credibility \u003e= 0.8) |\n| `✗` | not verified (credibility \u003c 0.8) |\n| `⤴` | cross-file 跨文件 |\n| `↻` | cycle detected |\n\n### 5 个 API 层级 (都可以用箭头式)\n\n```python\nfrom signal_tracer import trace_signal, SignalTracer\n\n# 1. TraceResult / TraceSummary — 一键全部 drivers+loads\nresult = trace_signal(\"count\", sv, \"counter.sv\")\nprint(result.to_arrow())\n# DRIVERS (2):\n#   count ← 8'h00 @ counter.sv:9 [counter] ✓ cred=1.00\n#   count ← count + data_in @ counter.sv:10 [counter] ✓ cred=1.00\n# LOADS (0):\n#   (none)\n\n# 2. 单条 trace\nfor d in result.drivers:\n    print(d.to_arrow())\n# count ← 8'h00 @ counter.sv:9 [counter] ✓ cred=1.00\n\n# 3. SignalTracer — 一键多驱动\nt = SignalTracer()\nt.add_file(\"buggy.sv\", multi_sv); t.build()\nprint(t.multi_drivers_to_arrow())\n# data ⚠ 2 drivers:\n#   data ← 8'hAA @ buggy.sv:9 [buggy] ✓ cred=1.00\n#   data ← 8'h55 @ buggy.sv:12 [buggy] ✓ cred=1.00\n\n# 4. 链追踪 — 完整上溯/下溯链\nprint(t.chain_to_arrow(\"data_out\", direction=\"driver\"))\n# data_out ← c ⤴ ← b ← a\n\n# 5. dump 转箭头 — 全链 + summary\nprint(t.dump_to_arrow(\"data_out\"))\n# Chain data_out: 4 hops, avg_cred=0.95, cross-file ✓, cycle ✗\n#   data_out ← c ✓ ← b ✓ ← a ✓\n```\n\n### 直接用 formatter 函数\n\n```python\nfrom signal_tracer import format_driver, format_load, format_all, ARROW_DRIVER, ARROW_LOAD\n\nprint(format_driver(result.drivers[0]))\nprint(format_all(result))\nprint(ARROW_DRIVER)  # '←'\nprint(ARROW_LOAD)    # '→'\n```\n\n### 与 `summary()` 区别\n\n| 方法 | 适合场景 |\n|------|----------|\n| `summary()` | 短/字段化/适合 LLM 当 context (e.g. 'counter.sv:10 (always_ff) clk=clk reset=rst_n cond=[!rst_n]') |\n| `to_arrow()` | 箭头/数据流/适合人眼扫/聊天贴出来 (e.g. 'count ← count + data_in @ counter.sv:11 ✓ cred=1.0') |\n\n两者并存, 根据场景选。\n\n### Tree / Vertical 风格 (M5.1k) — 长链/文档/聊天友好\n\n当链太长 (≥ 4 个信号) 或要贴到文档/聊天里, 一行箭头看不清楚。换成 **tree 风格** (类似 `tree(1)` 工具的输出) 或 **vertical 风格** (每行一个信号 + 箭头):\n\n```python\nt = SignalTracer()\nt.add_file('top.sv', top_code)\nt.add_file('mid.sv', mid_code)\nt.add_file('leaf.sv', leaf_code)\nt.build()\n```\n\n**5 种风格 (全部带 tree 节点) — 选一个**:\n\n```python\n# 1. arrow (默认): 一行, 短链友好\nprint(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='arrow'))\n# out_data ← out_data ← mid_data  (↻ cycle detected)\n\n# 2. tree: tree 风格, Unicode box-drawing\nprint(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='tree'))\n# Driver chain: top.u_mid.u_leaf_a.out_data (3 hops, ↻ cycle)\n#   ├─ out_data  [leaf.sv:11]  ✓ cred=1.00\n#   │  ← out_data  [leaf.sv:12]  ✓ cred=1.00\n#   └─ ← mid_data  [leaf.sv:9]  ✓ cred=1.00\n\n# 3. ascii: 同 tree 但用 ASCII (老终端 / 邮件 / 纯文本 log)\nprint(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='ascii'))\n# Driver chain: top.u_mid.u_leaf_a.out_data (3 hops, ↻ cycle)\n#   +-- out_data  [leaf.sv:11]  ✓ cred=1.00\n#   |  ← out_data  [leaf.sv:12]  ✓ cred=1.00\n#   +-- ← mid_data  [leaf.sv:9]  ✓ cred=1.00\n\n# 4. vertical: 每行一个信号, 缩进表示深度\nprint(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='vertical'))\n# out_data @ leaf.sv:11 ✓ cred=1.00\n#   ← out_data @ leaf.sv:12 ✓ cred=1.00\n#     ← mid_data @ leaf.sv:9 ✓ cred=1.00\n\n# 5. all / both: arrow + tree 两个都返\nprint(t.chain_to_arrow('top.u_mid.u_leaf_a.out_data', style='all'))\n```\n\n**dump 也支持 tree/vertical**:\n\n```python\n# dump_to_arrow 默认 1 行, style='tree' 转 tree\nprint(t.dump_to_arrow('top.u_mid.u_leaf_a.out_data', style='tree'))\n# Driver chain: top.u_mid.u_leaf_a.out_data (3 hops)\n#   ├─ out_data  [leaf.sv:11]\n#   │  ← out_data  [leaf.sv:12]\n#   └─ ← mid_data  [leaf.sv:9]\n\n# 还可以用 alias\nt.chain_to_tree(signal, use_box=True)   # tree style\nt.chain_to_tree(signal, use_box=False)  # ASCII\nt.chain_to_vertical(signal)             # vertical\nt.dump_to_tree(signal, use_box=True)    # dump + tree\nt.dump_to_tree(signal, use_box=False)   # dump + ascii\n```\n\n**`format_driver_chain` / `format_dump_summary` 也都接受 `style` 参数**, 给纯函数用户用。\n\n**怎么选风格**:\n- **短链 (≤ 3 个信号)**: `arrow` (默认) — 一行就够\n- **中链 (4-7) + 看代码**: `tree` — 节点 + location + cred 一起看\n- **中链 + 贴 chat/markdown**: `vertical` — 不依赖 box-drawing\n- **老终端 / 邮件 / 纯文本 log**: `ascii` — 不需要 Unicode\n- **要全面**: `all` — arrow + tree 都给\n\n## 公开 API\n\n### 函数式\n\n```python\nfrom signal_tracer import trace_signal, trace_signal_from_file\nresult = trace_signal(\"signal_name\", sv_code, \"file.sv\")\nresult = trace_signal_from_file(\"signal_name\", \"path/to/file.sv\")\n```\n\n### 类式（多文件 + 层次路径）\n\n```python\nfrom signal_tracer import SignalTracer, TraceSummary, ContextBundle\n\nt = SignalTracer()\nt.add_file('top.sv', top_code)\nt.add_file('sub.sv', sub_code)\nt.build()\n\nresult = t.trace(\"signal_name\")  # TraceSummary\n```\n\n### SignalTracer 主要方法\n\n| 方法 | 说明 |\n|------|------|\n| `add_file(path, code)` | 加一个 .sv 文件到项目（链式） |\n| `build()` | 解析所有文件，构建追踪索引（必须先调） |\n| `trace(name)` | 追踪信号，返回 `TraceSummary`（智能匹配 hpath / leaf / 数组 / 后缀） |\n| `trace_drivers(name)` | 只返回 driver 列表 |\n| `trace_loads(name)` | 只返回 load 列表 |\n| `find_multi_drivers()` | 找所有被 ≥2 个 scope 驱动的信号（多驱动检测） |\n| `get_driver_count(name)` | 返回某信号的不同 scope 数 |\n| `get_driver_chain(name, max_depth=10)` | 递归查上游 driver 链（带 cycle detection） |\n\n### TraceSummary 方法\n\n| 方法 | 说明 |\n|------|------|\n| `get_clock_domains()` | 该信号涉及的所有时钟 |\n| `is_multi_driver()` | 是否被多个 scope 驱动 |\n| `get_driver_scopes()` | 所有驱动 scope 源码（去重） |\n| `to_contexts()` | 打包所有 driver 为 `List[ContextBundle]` |\n\n### ContextBundle 字段\n\n`ContextBundle`（frozen=True，不可变）打包：\n\n- `file` / `line` / `char_offset` — 位置\n- `scope_text` / `scope_line_start/end` / `scope_kind` — scope 信息\n- `clock` / `reset` — 时钟/复位\n- `condition` / `condition_stack` — 嵌套条件栈\n- `is_port` / `port_direction` / `hierarchical_path` — 端口 + 层次\n- `confidence` — 置信度\n- `to_dict()` / `summary()` — 序列化 / 一行可读\n\n## 状态\n\n| 指标 | 数据 |\n|------|------|\n| 公开 API 测试 | **210/210 通过** (~4s) (含 50 个箭头式输出测试: 28 M5.1j + 22 M5.1k tree/vertical/ascii) |\n| 跨版本验证 | ✅ pyslang 10.x **和** 11.x 都 210/210 (make test-cross-version) |\n| 真实项目验证 | ✅ OpenTitan 6 模块 (30,218 drivers, 0 warning, 0 empty) |\n| 跨文件 fixture | 3 文件 / 3 层 instance (`tests/fixtures/m3_hierarchical/`) |\n| Benchmark | 11/11 (0 warning, 0 exception) |\n| 旧架构测试 | 已迁移 `tests/_legacy/`, 主测试 68/68 干净通过 |\n| 版本 | alpha |\n\n跑测试：\n\n```bash\npython -m pytest tests/ -v\n\n# 跨 pyslang 10/11 版本验证\nmake test-cross-version\n```\n\n## 测试覆盖 (M0–M4)\n\n主测试 `tests/unit/test_signal_tracer.py` 包含 **23 个 TestClass, 117 个 测试**：\n\n| 阶段 | TestClass | 测试数 | 覆盖点 |\n|------|-----------|--------|--------|\n| M0 | `TestBasic`, `TestControlFlow`, `TestArrays`, `TestNoCrashes` | — | 基础 always_ff/comb/latch, if/else/case 条件, 1D/2D 数组 |\n| M1 | `TestTraceResultFields` | — | 完整 TraceResult 字段填充 |\n| M1.5 | `TestMultiDriver`, `TestClockResetExtraction`, `TestDriverChain` | — | 多驱动检测, clock/reset 提取, driver_chain 递归 (cycle detection) |\n| M2 | `TestContextAccuracy`, `TestContextBundle` | — | line/scope_text 准确性, ContextBundle frozen dataclass |\n| M3 | `TestMultiFile` | — | 多文件 build, 层次路径 (`top.u_mid.u_leaf`), 后缀匹配 |\n| M4 | `TestExpressionCoverage`, `TestContinuousAssignRobustness`, `TestMultiFileLineFallback`, `TestScopeFilePath`, `TestAdditionalExpressions` | +5 | 17 种 SV 表达式, InvalidExpression 防御, 跨文件行号 (SourceManager), TraceResult.file 精确, 嵌套 MemberAccess+RangeSelect |\n| M4.1 | `TestInterfaceModport` | +6 | Interface/Modport 信号追踪 (HierarchicalValue), 跨 modport 读写, m.data[3:0] 位选 |\n| M5.1 | `TestCodeEvidence` | +8 | 代码证据链 (CodeEvidence), credibility_score 0-1 量化, is_verified 标记, `trace_verified()` 自动验证 |\n| M5.1b | `TestMultiDriverEvidence` | +4 | `find_multi_drivers(verify=True)` 默认自动带 evidence (看到冲突 + 真凭实据) |\n| M5.1c | `TestDriverChainEvidence` | +4 | `get_driver_chain(verify=True)` 默认链上每跳自动带 evidence (顺藤摸瓜带 credibility) |\n| M5.1d | `TestTraceLoadsEvidence` | +7 | `trace()`/`trace_drivers()`/`trace_loads()` 默认 verify=True, drivers 和 loads 都自动带 evidence (查谁读了某信号) |\n| M5.1e | `TestLoadChainEvidence` | +5 | `get_load_chain(verify=True)` 顺藤摸瓜查下游 (与 driver chain 对称) |\n| M5.1f | `TestDumpChain` | +9 | `dump_driver_chain()`/`dump_load_chain()` 一次 dump 整链为 dict (含 summary, LLM 友好) |\n| M5.1g | `TestDumpMultiDrivers` | +6 | `dump_multi_drivers()` 一次 dump 多驱动检测 (冲突列表 + 每个 driver evidence) |\n| M5.1h | `TestSyntaxNodeSnapshot` | +6 | syntax-based evidence 路径: SyntaxNodeSnapshot 冻结 + OpenTitan 跨文件 snippet 精度 |\n| M5.1h+ | (Makefile target) | — | 跨 pyslang 10.x/11.x 验证 (`make test-cross-version` 双 venv 跑 160+160 tests) |\n\n各阶段演进：\n\n| 阶段 | 新增测试 | 累计 |\n|------|---------|------|\n| M0 | 13 | 13 |\n| M1 | 13 | 26 |\n| M1.5 | 20 | 46 |\n| M2 | 13 | 59 |\n| M3 | 9 | 68 |\n| M4 | 5 | 73 |\n| M4.1 | 6 | 74 |\n| M5.1 | 8 | 82 |\n| M5.1b | 4 | 86 |\n| M5.1c | 4 | 90 |\n| M5.1d | 7 | 97 |\n| M5.1e | 5 | 102 |\n| M5.1f | 9 | 111 |\n| M5.1g | 6 | 117 |\n| M5.1h | 6 | 123 |\n\n主测试套件 (含 `test_signal_tracer.py` 和 `test_evidence_via_syntax.py`) 累计 **160 个** (其他测试文件: 边界/CI/legacy 37 个)。\n\n详见 [tests/README.md](tests/README.md) 和 [TEST_PLAN.md](TEST_PLAN.md)。\n\n## 代码证据链 (M5.1)\n\n每个 trace 都带**可证伪的代码证据链** — 读回实际文件, 验证 `source_expr` 和 `signal_name` 真的在该行。LLM/用户能反查 trace 真的对, 而不是默默相信。\n\n### 核心 API\n\n```python\n# 方式 1: trace_signal + 传 file_content\nresult = trace_signal('count', sv_code, 'counter.sv')\nfor ctx in result.to_contexts(file_content=sv_code):\n    d = ctx.to_dict()\n    print(f\"  credibility={d['credibility_score']}  is_verified={d['is_verified']}\")\n    print(f\"  snippet: {d['evidence_snippet']}\")\n    print(ctx.code_evidence.to_evidence_string())\n\n# 方式 2: SignalTracer 多文件 + 自动 in-memory 验证\nt = SignalTracer()\nt.add_file('top.sv', top_code)\nt.add_file('sub.sv', sub_code)\nt.build()\nresult = t.trace_verified('top.u_sub.signal')  # 自动用 self._files 验证\n```\n\n### 可信度评分 (credibility_score 0-1)\n\n| 验证项 | 分值 | 说明 |\n|--------|------|------|\n| `file_readable` | +0.2 | 文件能读 |\n| `snippet_present` | +0.2 | line 存在 |\n| `matches_source_expr` | +0.4 | 文本里真找到 source_expr |\n| `matches_signal_name` | +0.2 | 文本里真找到 signal_name |\n\n`is_verified = file_readable ∧ snippet_present ∧ (matches_source ∨ matches_signal)`\n\n### OpenTitan 验证\n\n```\ntx_enable @ uart_core.sv:77:\n  snippet: 'assign tx_enable        = reg2hw.ctrl.tx.q;'\n  matches: source_expr ✓, signal_name ✓\n  credibility: 1.0/1.0 (VERIFIED)\n  context_before: ['']\n  context_after: ['  assign rx_enable        = reg2hw.ctrl.rx.q;', ...]\n\nreadbuf_threshold @ spi_device.sv:600:\n  snippet: 'assign readbuf_threshold = reg2hw.read_threshold.q[BufferAw:0];'\n  credibility: 1.0/1.0 (VERIFIED) — 含 BufferAw 的 RangeSelect 也 OK\n```\n\n### 防御性: 不匹配会真实反映\n\n| 场景 | credibility | is_verified |\n|------|-------------|-------------|\n| 文件不存在 | 0.0 | ❌ |\n| 可读但都不匹配 | 0.4 | ❌ |\n| 仅 signal_name 匹配 | 0.6 | ✅ |\n| 全部匹配 | 1.0 | ✅ |\n\nevidence 不会\"假装 OK\"，会真实反映可信度。\n\n## 代码证据链语法路径 (M5.1h)\n\n**核心问题**: file-based evidence 依赖 `file:line` 准不准——line 错 (e.g. multi-statement `always_ff` block 里) 就会读到错的源码。M5.1h 走**从 pyslang 语法树拿 evidence** 的路径：line 用 SyntaxNode.sourceRange 算，snippet 用 `str(SyntaxNode)` 拿，跨文件也准。\n\n### 优势\n\n- **不依赖文件存在**: 内存里只有 SV code 也能产出 evidence\n- **总是和 pyslang 解析结果 100% 一致**: file-based 有 line 错 / 文件被改 / 路径不同步的风险，syntax-based 没有\n- **不依赖 line 准不准**: line 错了 syntax 仍指向正确位置\n- **多文件零 cost**: 不需要记哪个 file 对应哪个 offset\n\n### OpenTitan 真实示例 (uart 模块, 6 files / 431 drivers / 616 loads)\n\n```python\nimport sys, os\nsys.path.insert(0, 'src')\nfrom signal_tracer import SignalTracer\n\nuart_dir = '/Users/fundou/my_dv_proj/opentitan/hw/ip/uart/rtl/'\nt = SignalTracer()\nfor f in ['uart.sv', 'uart_core.sv', 'uart_reg_pkg.sv', 'uart_reg_top.sv', 'uart_rx.sv', 'uart_tx.sv']:\n    p = os.path.join(uart_dir, f)\n    if os.path.exists(p):\n        t.add_file(p, open(p).read())\nt.build()\n\n# trace_drivers('tx_enable') → evidence chain\nfor d in t.trace_drivers('tx_enable'):\n    ctx = d.to_context()\n    cd = ctx.to_dict()\n    print(f'{os.path.basename(cd[\"file\"])}:{cd[\"line\"]}  cred={cd[\"credibility_score\"]}  verif={cd[\"is_verified\"]}')\n    print(f'  snippet: {cd[\"evidence_snippet\"]!r}')\n```\n\n输出：\n\n```\nuart_core.sv:77  cred=1.0  verif=True\n  snippet: 'assign tx_enable        = reg2hw.ctrl.tx.q;'\n```\n\n### 多个真实信号的 evidence (OpenTitan uart, 全部 credibility=1.0)\n\n**Single-driver 信号**：\n\n```\ntx_enable     @ uart_core.sv:77    snippet='assign tx_enable        = reg2hw.ctrl.tx.q;'\nrx_enable     @ uart_core.sv:78    snippet='assign rx_enable        = reg2hw.ctrl.rx.q;'\nallzero_cnt_q @ uart_core.sv:109   snippet=\"if (!rst_ni)        allzero_cnt_q \u003c= '0;\"\nallzero_cnt_q @ uart_core.sv:110   snippet='else if (rx_enable) allzero_cnt_q \u003c= allzero_cnt_d;'\n```\n\n**Multi-driver 冲突 (跨 3 个文件, snippet 精确定位每个 driver 位置)**：\n\n```\ntx        @ uart_tx.sv:32     snippet='assign tx = tx_q;'\ntx        @ uart_core.sv:217  snippet='assign tx = line_loopback ? rx : tx_out_q ;'\nbaud_div_q @ uart_tx.sv:36    snippet=\"baud_div_q  \u003c= 4'h0;\"\nbaud_div_q @ uart_rx.sv:41    snippet=\"baud_div_q  \u003c= 4'h0;\"\nbaud_div_q @ uart_rx.sv:47    snippet='baud_div_q  \u003c= baud_div_d;'\ntick_baud_q @ uart_tx.sv:37   snippet=\"tick_baud_q \u003c= 1'b0;\"\ntick_baud_q @ uart_tx.sv:41   snippet=\"tick_baud_q \u003c= 1'b0;\"\ntick_baud_q @ uart_rx.sv:42   snippet=\"tick_baud_q \u003c= 1'b0;\"\ntick_baud_q @ uart_rx.sv:48   snippet='tick_baud_q \u003c= tick_baud_d;'\n```\n\nLLM 看 1 行 snippet 就能反查\"`tx` 实际上是哪个 line 在驱动\"，再 `cat uart_core.sv:217` 看到 `assign tx = line_loopback ? rx : tx_out_q ;` 就能确认是 loopback 模式。\n\n### SyntaxNodeSnapshot: 防 pyslang buffer 复用\n\n走 syntax 路径会调用 `str(SyntaxNode)` 拿 snippet。但 pyslang 的 `SyntaxNode.__str__()` 依赖内部 buffer 状态，第二个 `Compilation` 创建后，第一个的 SyntaxNode 调 `str()` 会返回截断的旧内容 (e.g. `b = foo(a)` → `b = foo`，丢 `(a)`)。M5.1h 引入 **`SyntaxNodeSnapshot`** 包装：\n\n- inject 时立刻 `str(node)` 拿到完整文本，冻结到 `self.text`\n- 代理 `sourceRange` / `kind` / `__iter__` 等元数据（`build_evidence_via_syntax` 和 `_find_subexpr_for_signal` 需读）\n- `__str__` 优先返回冻结的 text，**不受后续 Compilation 创建影响**\n\n**6 个回归测试** (`tests/unit/test_evidence_via_syntax.py::TestSyntaxNodeSnapshot`) 锁定这个行为，包括多 tracer 场景下的冻结验证。\n\n### file-based vs syntax-based 互补\n\n| 场景 | file-based | syntax-based | 说明 |\n|------|-----------|--------------|------|\n| 单行 assign (`assign x = y;`) | ✅ line 准 | ✅ snippet 准 | 两者都好 |\n| Multi-statement always_ff block | ⚠️ line=block 头 | ⚠️ sourceRange=block | 两者各有不足 (block 级粒度) |\n| Multi-driver 冲突 (跨文件) | ✅ line 准 | ✅ 跨文件准 | syntax 路径优势在跨文件 |\n| 文件被改/不同步 | ❌ | ✅ | syntax 路径不依赖文件 |\n| 内存-only SV code | ❌ | ✅ | syntax 路径唯一选 |\n\n**推荐**：默认走 `to_context(source_mode='auto')`，在 line 准的时候走 file-based 拿到更多 context；line 错/跨文件/内存模式走 syntax-based。\n\n## 跨版本兼容\n\n### pyslang 10 / 11 都能跑\n\nsv-trace 在两个主版本下都保持 160/160 tests pass。安装时不需要指定上限 (`pyslang\u003e=10.0`)，因为项目里加了 try/fallback import pattern 走 11 的新位置。\n\n| pyslang | sv-trace 1.0.0 | 状态 |\n|---------|---------------|------|\n| 10.0.x | ✅ work | 测过 (主推 venv) |\n| 11.0.x | ✅ work | 测过 (make test-cross-version) |\n| 12+ | ❓ | 未测 |\n\n### 手动验证\n\n```bash\n$ make test-cross-version\nTesting wheel on pyslang 10.0...\n160 passed in 7.59s\nTesting wheel on pyslang 11.0...\n160 passed in 6.05s\n```\n\n这个 target 会启两个 venv（一个 v10，一个 v11），各装 wheel 跑 pytest，确保未来升级 pyslang 时不会悄无声息地 break。\n\n### 为什么不锁版本\n\n`pyslang` 是个活跃开发的 C++ binding，每年大版本会重排 API。从 v10 升 v11 移走了 `SyntaxTree` 到 `pyslang.syntax`、移走了 `Compilation` 到 `pyslang.ast`。我们加 try/fallback 兼容两层都吃；不想用 `\u003c11` 上限让以后 v12/v13 用户装不上。\n\n## 真实项目验证 (M4)\n\n在 OpenTitan 上验证, 全部 6 模块 **0 warning + 0 empty driver**:\n\n| 模块 | .sv 文件数 | drivers | 空 expr | 备注 |\n|------|-----------|---------|---------|------|\n| uart | 6 | 418 | 0% | 起始验证模块 |\n| spi_device | 19 | 3,229 | 0% | 涵盖 Streaming concat (`{\u003c\u003c8{...}}`) |\n| dma | 4 | 401 | 0% | 涵盖 `inside` 集合成员判断 |\n| i2c | 10 | 1,235 | 0% | |\n| aes | 40 | 24,065 | 0% | 大型模块, 涵盖 StructuredAssignmentPattern |\n| hmac | 4 | 870 | 0% | 涵盖 `assert property` (SVA) 跳过 |\n\n**M4 能力覆盖的 SV 语法**:\n\n- 表达式: `MemberAccess` / `RangeSelect` / `ElementSelect` / `BinaryOp` / `UnaryOp` / `ConditionalOp` / `CastExpression` / `Call` / `Replication` / `Concatenation` / `Streaming` (`{\u003c\u003c8{x}}` / `{\u003e\u003e8{x}}`) / `Inside` / `UnbasedUnsizedIntegerLiteral` (`'0` / `'1`) / `StructuredAssignmentPattern` / `SimpleAssignmentPattern` / `LValueReference` / `DataType` / **`HierarchicalValue` (Interface/Modport 访问, M4.1)**\n- 证据链: 每个 trace 读回实际文件交叉验证, `credibility_score` 0-1 量化, `is_verified` 标记 (M5.1)\n- 多驱动检测 + 证据链: `find_multi_drivers(verify=True)` 默认自动带 evidence, 看到冲突 + 真凭实据 (M5.1b)\n- 驱动链 + 证据链: `get_driver_chain(verify=True)` 链上每跳自动带 evidence, 顺藤摸瓜带 credibility (M5.1c)\n- trace + 证据链: `trace()`/`trace_drivers()`/`trace_loads()` 默认 verify=True, drivers 和 loads 都自动带 evidence (M5.1d)\n- load 链 + 证据链: `get_load_chain(verify=True)` 顺藤摸瓜下游, 链上每条 load 都带 evidence (M5.1e, 与 driver chain 对称)\n- dump_chain: 一次 dump 整链为 dict (含 summary avg/min/credibility/cross_files), 喂 LLM 1 个 prompt section 就够 (M5.1f)\n- dump_multi_drivers: 一次 dump 多驱动检测 (冲突列表 + 每个 driver evidence + 全局 summary), LLM 一眼看到所有冲突 (M5.1g)\n- syntax-based evidence: 从 pyslang SyntaxTree 直接拿, 跨文件 100% 准, 不依赖 file 存在 (M5.1h)\n- 嵌套: 任意深度 MemberAccess (e.g. `reg2hw.ctrl.tx.q`) + 跨 RangeSelect (`reg2hw.val[BufferAw:0]`)\n- 跨文件: 多 .sv 编译为同一 Compilation, 跨模块引用 + 层次路径 (`uart.uart_core.tx_enable`)\n- 跨文件行号: `pyslang SourceManager.getLineNumber()` 走 SourceLocation.buffer 精准算行\n- 跨文件 file path: 每个 ScopeInfo.file_path 走 SourceManager.getFileName() 拿到正确文件名\n- SVA 跳过: `ConcurrentAssertionStatement` (assert property) 不产生 driver/load trace\n\n**未支持 (边缘场景)**:\n\n- ~~复杂 type system (interface/modport)~~ — **M4.1 已支持** (HierarchicalValueExpression 完整追踪, 跨 master/slave modport 都可, 含 m.data[3:0] 位选)\n- modport direction (input/output) 区分 driver/load — 尚未实现 (现在 input 和 output 都被当 driver, 可能误报多驱动)\n- Clocking block / Property/Sequence 内部\n- System task ($cast, $readmemh) 中的信号\n- M5.1 evidence 的 `matches_source_expr` 是**字面量**子串匹配 — pyslang 文本格式 (如 `count Add data_in`) 与源码 (`count + data_in`) 不完全一致时, 命中率会降, 反映在 credibility_score 上, 不会静默接受\n\n## 项目结构\n\n```\nsv-trace/\n├── src/\n│   ├── __init__.py\n│   ├── sv_manager.py                  # SV 文件加载、源码定位\n│   └── signal_tracer/                 # 核心\n│       ├── models.py                  # TraceResult / TraceSummary / ContextBundle / ScopeInfo\n│       ├── tracer.py                  # SignalTracer: 语义层 driver/load\n│       ├── port_resolver.py           # PortResolver: 语法层端口连接\n│       └── signal_tracer_app.py       # SignalTracerApp: 单文件跨模块（兼容）\n├── benchmarks/                        # 12 个 SV fixture (基础 always/case/FSM/...)\n├── tests/\n│   ├── 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)\n│   ├── fixtures/m3_hierarchical/      # 3 文件 / 3 层 instance fixture\n│   │   ├── top.sv\n│   │   ├── mid.sv\n│   │   └── leaf.sv\n│   ├── unit/trace/sv_cases/           # 50+ .sv fixture 语料库\n│   ├── targeted/  advanced/  testbed/ # .sv fixture\n│   ├── _legacy/                       # 重构前失效测试（归档）\n│   └── README.md\n├── archive/                           # 旧 src/ 完整代码\n├── STRUCTURE.md                       # 详细架构 / API 字段表\n├── TODO.md                            # 路线图\n├── TEST_PLAN.md                       # 测试计划\n├── SKILL.md                           # Agent 集成 (供 AI agent 调用)\n├── pyproject.toml\n└── pytest.ini\n```\n\n## 路线图\n\n- ✅ **M0** P0 bug 修复（TimedStatement 路径处理）\n- ✅ **M1** 公开 API 测试覆盖（13/13）\n- ✅ **M1.5** 多驱动检测 / clock-reset 提取 / driver_chain 递归（20/20）\n- ✅ **M2** 上下文召回（line 准确性 + ContextBundle 数据结构，13/13）\n- ✅ **M3** 跨文件支持 + 层次路径追踪（9/9）\n- ✅ **M4** 真实项目验证（OpenTitan 6 模块, 0 warning/0 empty, 30,218 drivers 总计）\n- ✅ **M4.1** Interface/Modport 信号追踪（HierarchicalValue 完整覆盖, 6 个新测试）\n- ✅ **M5.1** 代码证据链 (CodeEvidence) - 让 trace 自证, credibility 0-1 量化\n- ✅ **M5.1b** find_multi_drivers 整合 evidence - 多驱动检测带 credibility\n- ✅ **M5.1c** get_driver_chain 整合 evidence - 顺藤摸瓜链上每跳带 credibility\n- ✅ **M5.1d** trace/trace_drivers/trace_loads 整合 evidence - drivers 和 loads 都带 credibility\n- ✅ **M5.1e** get_load_chain 整合 evidence - 顺藤摸瓜查下游 (与 driver chain 对称)\n- ✅ **M5.1f** dump_chain 一次 dump 整链为 JSON - 含 summary, LLM 友好\n- ✅ **M5.1g** dump_multi_drivers - 一次 dump 多驱动检测 (冲突 + 每个 driver 证据)\n- ✅ **M5.1h** 代码证据链语法路径 - 从 pyslang SyntaxTree 直接拿 evidence (SyntaxNodeSnapshot 防 buffer 复用, 跨文件 100% 准)\n- ✅ **M5.1h** 跨 pyslang 10.x / 11.x 兼容 (try/fallback import pattern, `make test-cross-version` 自动验证两版本)\n- ✅ **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), 箭头统一语义 (←/→/⚠/✓/✗/⤴/↻)\n- ✅ **M5.1k** Tree/Vertical/ASCII 风格 - 长链/文档/聊天友好, 5 种风格 (`arrow` 默认 / `tree` Unicode / `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()`\n- 📋 **M5.2+** 极致优化（增量、并发、缓存）\n\n完整路线图见 [TODO.md](TODO.md)。\n\n## 不做的功能\n\n明确划界，下列需求**不在**本项目范围内：\n\n- ❌ CDC / 多驱动 / 未初始化 / 复位域分析\n- ❌ 面积 / 功耗 / 性能估算\n- ❌ FSM 提取 / SVA 生成 / 覆盖率建议\n- ❌ 约束分析 / 形式验证\n- ❌ TB 复杂度评分 / Lint / Style 检查\n- ❌ 类/约束提取 / 可视化\n\n如果未来需要，应作为独立项目开发。\n\n## 文档\n\n- [STRUCTURE.md](STRUCTURE.md) — 详细架构 / API 字段表 / 数据流图\n- [TODO.md](TODO.md) — 路线图 / 不做的功能 / 历史\n- [TEST_PLAN.md](TEST_PLAN.md) — 测试计划 / 状态\n- [SKILL.md](SKILL.md) — Agent 集成指南 (供 AI agent 调用)\n- [tests/README.md](tests/README.md) — 测试总览\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffundou1081%2Fsv-trace","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffundou1081%2Fsv-trace","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffundou1081%2Fsv-trace/lists"}