chipgraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +173 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +247 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/cocotb.py +304 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/review/github.py +886 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +122 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
- chipgraph/adapters/runtime/claude_code/agents.py +158 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
- chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
- chipgraph/adapters/runtime/claude_code/loop.py +342 -0
- chipgraph/adapters/runtime/claude_code/previous.py +419 -0
- chipgraph/adapters/runtime/claude_code/review.py +492 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
- chipgraph/adapters/runtime/claude_code/service.py +2285 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/edalize.py +773 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +521 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +332 -0
- chipgraph/app/checks.py +202 -0
- chipgraph/app/context.py +191 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +153 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/format.py +268 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/plan_check.py +901 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +277 -0
- chipgraph/checks/style.py +910 -0
- chipgraph/checks/tb_static.py +599 -0
- chipgraph/checks/trace.py +224 -0
- chipgraph/checks/vendor_guard.py +369 -0
- chipgraph/checks/wrap_check.py +486 -0
- chipgraph/cli/__init__.py +1688 -0
- chipgraph/cli/next_steps.py +137 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +93 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +766 -0
- chipgraph/core/config/models.py +845 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/agent_rule.py +1362 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/fanout.py +1358 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +647 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +637 -0
- chipgraph/core/engine/select.py +1239 -0
- chipgraph/core/engine/targets.py +342 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +41 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +565 -0
- chipgraph/core/runtime/roles/__init__.py +91 -0
- chipgraph/core/runtime/roles/data/author.md +41 -0
- chipgraph/core/runtime/roles/data/critic.md +56 -0
- chipgraph/core/runtime/roles/data/planner.md +38 -0
- chipgraph/core/runtime/roles/data/researcher.md +37 -0
- chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
- chipgraph/core/runtime/roles/data/tb-author.md +48 -0
- chipgraph/core/runtime/roles/data/triage.md +34 -0
- chipgraph/core/runtime/roles/policy.py +299 -0
- chipgraph/core/runtime/roles/registry.py +52 -0
- chipgraph/core/runtime/roles/review.py +275 -0
- chipgraph/core/runtime/roles/schema.py +82 -0
- chipgraph/core/runtime/roles/skills.py +134 -0
- chipgraph/core/runtime/roles/spec.py +232 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +411 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +82 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +787 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/digital_rtl/__init__.py +19 -0
- chipgraph/packs/digital_rtl/layout.py +144 -0
- chipgraph/packs/digital_rtl/pack.yml +25 -0
- chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
- chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
- chipgraph/packs/digital_rtl/plan/context.py +82 -0
- chipgraph/packs/digital_rtl/plan/expand.py +94 -0
- chipgraph/packs/digital_rtl/plan/model.py +150 -0
- chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
- chipgraph/packs/digital_rtl/plan/show.py +176 -0
- chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
- chipgraph/packs/digital_rtl/pr/body.py +165 -0
- chipgraph/packs/digital_rtl/pr/gate.py +110 -0
- chipgraph/packs/digital_rtl/pr/open.py +397 -0
- chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
- chipgraph/packs/digital_rtl/rtl/context.py +272 -0
- chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
- chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
- chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
- chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
- chipgraph/packs/digital_rtl/rules/review.yml +31 -0
- chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
- chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
- chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
- chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
- chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
- chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
- chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
- chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
- chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
- chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
- chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
- chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
- chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
- chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
- chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
- chipgraph/packs/dv/__init__.py +15 -0
- chipgraph/packs/dv/declaration.py +392 -0
- chipgraph/packs/dv/interface.py +596 -0
- chipgraph/packs/dv/pack.yml +14 -0
- chipgraph/packs/dv/rules/tb_module.yml +42 -0
- chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +975 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0.dist-info/METADATA +53 -0
- chipgraph-0.1.0.dist-info/RECORD +252 -0
- chipgraph-0.1.0.dist-info/WHEEL +4 -0
- chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
- chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""`LlmDecideBackend`: `decide()`'s model tiers over an `LlmProvider` (API runtime).
|
|
2
|
+
|
|
3
|
+
Each question becomes one request, to the model the profile's `models.tiers` names for
|
|
4
|
+
the tier, asking for strict JSON::
|
|
5
|
+
|
|
6
|
+
{"value": "<one of the choices>", "confidence": 0.0-1.0, "reason": "<one sentence>"}
|
|
7
|
+
|
|
8
|
+
The reply is read tolerantly (`parse_answer`): the JSON may be alone, in a fenced code
|
|
9
|
+
block, or inside other text; anything unreadable is an answer with no value and
|
|
10
|
+
confidence 0, which `decide()` escalates.
|
|
11
|
+
|
|
12
|
+
Every call goes through a `GuardedProvider` (a bare provider is wrapped in one), so the
|
|
13
|
+
'nda' rule, the run budget and retry apply: a question labeled 'nda' is refused before
|
|
14
|
+
any call unless the provider is local.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import math
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from chipgraph.adapters.llm._errors import ProviderConfigError
|
|
25
|
+
from chipgraph.adapters.llm.guard import GuardedProvider
|
|
26
|
+
from chipgraph.core.config.models import DataCfg, DecideTier, ModelsCfg
|
|
27
|
+
from chipgraph.core.engine.decide import ModelAnswer, Question
|
|
28
|
+
from chipgraph.core.plugin_api.protocols import LlmProvider
|
|
29
|
+
from chipgraph.core.plugin_api.types import LlmMessage, LlmRequest
|
|
30
|
+
|
|
31
|
+
DEFAULT_MAX_TOKENS = 300
|
|
32
|
+
"""Output tokens allowed for one answer: a short JSON object."""
|
|
33
|
+
|
|
34
|
+
SYSTEM_PROMPT = """\
|
|
35
|
+
You answer one multiple-choice question for a chip design tool.
|
|
36
|
+
Reply with a single JSON object and nothing else:
|
|
37
|
+
{"value": "<exactly one of the choices>", "confidence": <number from 0 to 1>, \
|
|
38
|
+
"reason": "<one short sentence>"}
|
|
39
|
+
- "value" must be copied exactly from the choices.
|
|
40
|
+
- "confidence" is how sure you are that "value" is right: 1 is certain, 0.5 is a guess.
|
|
41
|
+
- Use only the question and its context; if they do not tell, pick the most likely
|
|
42
|
+
choice and give a low confidence."""
|
|
43
|
+
"""The system message of every decide request."""
|
|
44
|
+
|
|
45
|
+
_FENCE = re.compile(r"```(?:json|JSON)?\s*\n?(.*?)```", re.DOTALL)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class ParsedAnswer:
|
|
50
|
+
"""A model reply as read by `parse_answer`: `value` None when it had none."""
|
|
51
|
+
|
|
52
|
+
value: str | None
|
|
53
|
+
confidence: float
|
|
54
|
+
reason: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def question_prompt(question: Question) -> str:
|
|
58
|
+
"""The user message for `question`: the question, its choices, and its context."""
|
|
59
|
+
choices = "\n".join(f"- {json.dumps(c, ensure_ascii=False)}" for c in question.choices)
|
|
60
|
+
parts = [f"Question: {question.prompt}", f"Choices:\n{choices}"]
|
|
61
|
+
if question.context:
|
|
62
|
+
parts.append(f"Context:\n<<<\n{question.context}\n>>>")
|
|
63
|
+
parts.append("Answer with the JSON object only.")
|
|
64
|
+
return "\n\n".join(parts)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _objects(text: str) -> list[object]:
|
|
68
|
+
"""JSON values found in `text`: the whole text, fenced blocks, then each `{...}`."""
|
|
69
|
+
found: list[object] = []
|
|
70
|
+
candidates = [text.strip(), *(m.group(1).strip() for m in _FENCE.finditer(text))]
|
|
71
|
+
for candidate in candidates:
|
|
72
|
+
try:
|
|
73
|
+
found.append(json.loads(candidate))
|
|
74
|
+
except ValueError:
|
|
75
|
+
continue
|
|
76
|
+
decoder = json.JSONDecoder()
|
|
77
|
+
for match in re.finditer(r"\{", text):
|
|
78
|
+
try:
|
|
79
|
+
value, _ = decoder.raw_decode(text, match.start())
|
|
80
|
+
except ValueError:
|
|
81
|
+
continue
|
|
82
|
+
found.append(value)
|
|
83
|
+
return found
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _confidence(raw: object) -> float | None:
|
|
87
|
+
if isinstance(raw, bool) or not isinstance(raw, int | float):
|
|
88
|
+
return None
|
|
89
|
+
value = float(raw)
|
|
90
|
+
if not math.isfinite(value) or not 0 <= value <= 1:
|
|
91
|
+
return None
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_answer(text: str) -> ParsedAnswer:
|
|
96
|
+
"""Read `{"value", "confidence", "reason"}` from a model reply, tolerantly.
|
|
97
|
+
|
|
98
|
+
The first JSON object with a string `value` wins. A missing or out-of-range
|
|
99
|
+
`confidence` makes it 0; a reply with no such object is `value=None`, confidence 0.
|
|
100
|
+
"""
|
|
101
|
+
for obj in _objects(text):
|
|
102
|
+
if not isinstance(obj, dict) or not isinstance(obj.get("value"), str):
|
|
103
|
+
continue
|
|
104
|
+
confidence = _confidence(obj.get("confidence"))
|
|
105
|
+
reason = obj.get("reason")
|
|
106
|
+
return ParsedAnswer(
|
|
107
|
+
value=obj["value"],
|
|
108
|
+
confidence=confidence if confidence is not None else 0.0,
|
|
109
|
+
reason=reason if isinstance(reason, str) else "",
|
|
110
|
+
)
|
|
111
|
+
return ParsedAnswer(value=None, confidence=0.0, reason="")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class LlmDecideBackend:
|
|
115
|
+
"""A `ModelBackend` for `decide()` that asks an LLM provider for structured JSON.
|
|
116
|
+
|
|
117
|
+
`models.tiers[tier]` names the model per tier (`small`, `large`); a tier with no
|
|
118
|
+
model is a `ProviderConfigError`. `provider` is wrapped in a `GuardedProvider`
|
|
119
|
+
(with `data`, the profile's `data` section) unless it already is one.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
name = "llm"
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
provider: LlmProvider,
|
|
127
|
+
models: ModelsCfg,
|
|
128
|
+
*,
|
|
129
|
+
data: DataCfg | None = None,
|
|
130
|
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
|
131
|
+
) -> None:
|
|
132
|
+
self.provider = (
|
|
133
|
+
provider
|
|
134
|
+
if isinstance(provider, GuardedProvider)
|
|
135
|
+
else GuardedProvider(provider, data=data)
|
|
136
|
+
)
|
|
137
|
+
self.models = models
|
|
138
|
+
self.max_tokens = max_tokens
|
|
139
|
+
|
|
140
|
+
def model_for(self, tier: DecideTier) -> str:
|
|
141
|
+
"""The model id for `tier`, from `models.tiers`."""
|
|
142
|
+
model = self.models.tiers.get(tier)
|
|
143
|
+
if not model:
|
|
144
|
+
raise ProviderConfigError(
|
|
145
|
+
f"models.tiers.{tier} is not set: decide() needs a model for the {tier!r} tier"
|
|
146
|
+
)
|
|
147
|
+
return model
|
|
148
|
+
|
|
149
|
+
def request(self, question: Question, tier: DecideTier) -> LlmRequest:
|
|
150
|
+
"""The request `ask` sends for `question` to `tier`."""
|
|
151
|
+
return LlmRequest(
|
|
152
|
+
model=self.model_for(tier),
|
|
153
|
+
messages=(
|
|
154
|
+
LlmMessage(role="system", content=SYSTEM_PROMPT),
|
|
155
|
+
LlmMessage(role="user", content=question_prompt(question)),
|
|
156
|
+
),
|
|
157
|
+
max_tokens=self.max_tokens,
|
|
158
|
+
labels=question.labels,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def ask(self, question: Question, tier: DecideTier) -> ModelAnswer:
|
|
162
|
+
response = await self.provider.complete(self.request(question, tier))
|
|
163
|
+
parsed = parse_answer(response.text)
|
|
164
|
+
return ModelAnswer(
|
|
165
|
+
value=parsed.value,
|
|
166
|
+
confidence=parsed.confidence,
|
|
167
|
+
model=response.model,
|
|
168
|
+
reason=parsed.reason,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
__all__ = [
|
|
173
|
+
"DEFAULT_MAX_TOKENS",
|
|
174
|
+
"SYSTEM_PROMPT",
|
|
175
|
+
"LlmDecideBackend",
|
|
176
|
+
"ParsedAnswer",
|
|
177
|
+
"parse_answer",
|
|
178
|
+
"question_prompt",
|
|
179
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""`FakeProvider`: a deterministic, scripted LLM provider for tests and CI (no network)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
|
|
8
|
+
from chipgraph.adapters.llm._errors import LlmError, ProviderError
|
|
9
|
+
from chipgraph.adapters.llm._nda import ensure_nda_allowed
|
|
10
|
+
from chipgraph.core.plugin_api.types import LlmRequest, LlmResponse
|
|
11
|
+
|
|
12
|
+
Script = Iterable[str] | Callable[[LlmRequest], str]
|
|
13
|
+
"""Either a queue of reply texts, used in order, or a function of the request."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def count_tokens(text: str) -> int:
|
|
17
|
+
"""The fake's token count: the number of whitespace-separated words."""
|
|
18
|
+
return len(text.split())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _echo(request: LlmRequest) -> str:
|
|
22
|
+
return request.messages[-1].content
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FakeProvider:
|
|
26
|
+
"""Replies from a script, counts tokens as words, and can fail on purpose.
|
|
27
|
+
|
|
28
|
+
- `script`: a queue of reply texts (an `LlmError` is raised once it runs out) or a
|
|
29
|
+
function of the request. By default it echoes the last message.
|
|
30
|
+
- `fail`: the first `fail` calls raise a `ProviderError`, retryable (HTTP 503) when
|
|
31
|
+
`retryable` is true, fatal (HTTP 400) otherwise.
|
|
32
|
+
- `local`: whether to act as a self-hosted model, the only kind that may see 'nda'.
|
|
33
|
+
- `model`: the model id reported in responses; by default the requested one.
|
|
34
|
+
|
|
35
|
+
Input tokens are the words of every message, output tokens the words of the reply.
|
|
36
|
+
Every request received (failed ones included) is kept in `requests`.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
script: Script | None = None,
|
|
42
|
+
*,
|
|
43
|
+
name: str = "fake",
|
|
44
|
+
local: bool = False,
|
|
45
|
+
fail: int = 0,
|
|
46
|
+
retryable: bool = True,
|
|
47
|
+
model: str | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.name = name
|
|
50
|
+
self.local = local
|
|
51
|
+
self.requests: list[LlmRequest] = []
|
|
52
|
+
self._fail = fail
|
|
53
|
+
self._retryable = retryable
|
|
54
|
+
self._model = model
|
|
55
|
+
self._reply: Callable[[LlmRequest], str]
|
|
56
|
+
if script is None:
|
|
57
|
+
self._reply = _echo
|
|
58
|
+
elif callable(script):
|
|
59
|
+
self._reply = script
|
|
60
|
+
else:
|
|
61
|
+
self._queue = deque(script)
|
|
62
|
+
self._reply = self._next_in_queue
|
|
63
|
+
|
|
64
|
+
def _next_in_queue(self, request: LlmRequest) -> str:
|
|
65
|
+
if not self._queue:
|
|
66
|
+
raise LlmError(f"fake provider {self.name!r}: the scripted replies ran out")
|
|
67
|
+
return self._queue.popleft()
|
|
68
|
+
|
|
69
|
+
async def complete(self, request: LlmRequest) -> LlmResponse:
|
|
70
|
+
ensure_nda_allowed(self.name, self.local, request)
|
|
71
|
+
self.requests.append(request)
|
|
72
|
+
if self._fail > 0:
|
|
73
|
+
self._fail -= 1
|
|
74
|
+
status = 503 if self._retryable else 400
|
|
75
|
+
kind = "retryable" if self._retryable else "fatal"
|
|
76
|
+
raise ProviderError(
|
|
77
|
+
f"fake provider {self.name!r}: scripted {kind} failure (HTTP {status})",
|
|
78
|
+
retryable=self._retryable,
|
|
79
|
+
status=status,
|
|
80
|
+
)
|
|
81
|
+
text = self._reply(request)
|
|
82
|
+
return LlmResponse(
|
|
83
|
+
text=text,
|
|
84
|
+
input_tokens=sum(count_tokens(m.content) for m in request.messages),
|
|
85
|
+
output_tokens=count_tokens(text),
|
|
86
|
+
model=self._model or request.model,
|
|
87
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""`GuardedProvider`: the nda rule, the budget and retry, wrapped around any provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import random
|
|
7
|
+
|
|
8
|
+
from chipgraph.adapters.llm._budget import Ledger, Usage
|
|
9
|
+
from chipgraph.adapters.llm._nda import ensure_nda_allowed
|
|
10
|
+
from chipgraph.adapters.llm._retry import Rand, RetryPolicy, Sleep, call_with_retry
|
|
11
|
+
from chipgraph.core.config.models import DataCfg
|
|
12
|
+
from chipgraph.core.plugin_api.protocols import LlmProvider
|
|
13
|
+
from chipgraph.core.plugin_api.types import LlmRequest, LlmResponse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GuardedProvider:
|
|
17
|
+
"""Wraps an `LlmProvider` with the checks every API-runtime call needs.
|
|
18
|
+
|
|
19
|
+
For each `complete`, in order:
|
|
20
|
+
|
|
21
|
+
1. **nda**: a request labeled 'nda' is refused with `NdaBlocked`, before any call,
|
|
22
|
+
unless `inner.local` is true and (when `data` is given) `data.nda_model` is 'local'.
|
|
23
|
+
2. **budget**: before each try, `ledger.check()` raises `BudgetExceeded` if the run's
|
|
24
|
+
budget is spent, so the call does not start.
|
|
25
|
+
3. **retry**: transient failures (429, 5xx, timeouts, connection errors) are retried
|
|
26
|
+
under `retry` with exponential backoff and jitter; any other failure propagates at
|
|
27
|
+
once, and `RetriesExhausted` is raised when every try failed.
|
|
28
|
+
4. **accounting**: the response's tokens (and cost, when the model has a price) are
|
|
29
|
+
added to `ledger.usage`.
|
|
30
|
+
|
|
31
|
+
It satisfies `LlmProvider` itself, with the inner provider's `name` and `local`.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
inner: LlmProvider,
|
|
37
|
+
*,
|
|
38
|
+
ledger: Ledger | None = None,
|
|
39
|
+
data: DataCfg | None = None,
|
|
40
|
+
retry: RetryPolicy | None = None,
|
|
41
|
+
sleep: Sleep = asyncio.sleep,
|
|
42
|
+
rand: Rand = random.random,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.inner = inner
|
|
45
|
+
self.name = inner.name
|
|
46
|
+
self.local = inner.local
|
|
47
|
+
self.ledger = ledger if ledger is not None else Ledger()
|
|
48
|
+
self.data = data
|
|
49
|
+
self.retry = retry if retry is not None else RetryPolicy()
|
|
50
|
+
self._sleep = sleep
|
|
51
|
+
self._rand = rand
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def usage(self) -> Usage:
|
|
55
|
+
"""The usage so far of this provider's ledger."""
|
|
56
|
+
return self.ledger.usage
|
|
57
|
+
|
|
58
|
+
async def complete(self, request: LlmRequest) -> LlmResponse:
|
|
59
|
+
ensure_nda_allowed(self.inner.name, self.inner.local, request, self.data)
|
|
60
|
+
response = await call_with_retry(
|
|
61
|
+
lambda: self.inner.complete(request),
|
|
62
|
+
self.retry,
|
|
63
|
+
sleep=self._sleep,
|
|
64
|
+
rand=self._rand,
|
|
65
|
+
before_try=self.ledger.check,
|
|
66
|
+
)
|
|
67
|
+
self.ledger.record(response)
|
|
68
|
+
return response
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Log parser adapters: turn a tool's raw log text into structured `Issue`s."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from chipgraph.adapters.parser.generic_regex import GenericRegexParser
|
|
8
|
+
from chipgraph.adapters.parser.verible import VeribleParser
|
|
9
|
+
from chipgraph.adapters.parser.verilator import VerilatorParser
|
|
10
|
+
from chipgraph.core.plugin_api.protocols import LogParser
|
|
11
|
+
|
|
12
|
+
__all__ = ["PARSERS", "GenericRegexParser", "VeribleParser", "VerilatorParser"]
|
|
13
|
+
|
|
14
|
+
#: Built-in parsers that need no arguments, keyed by `LogParser.name`.
|
|
15
|
+
#:
|
|
16
|
+
#: `generic-regex` (`GenericRegexParser`) is deliberately not listed here: it always
|
|
17
|
+
#: needs a `pattern` (and optionally `severity_group`/`default_severity`), so callers
|
|
18
|
+
#: construct it directly, e.g. `GenericRegexParser(pattern=spec.args["regex"])`.
|
|
19
|
+
PARSERS: dict[str, Callable[[], LogParser]] = {
|
|
20
|
+
"verilator": VerilatorParser,
|
|
21
|
+
"verible": VeribleParser,
|
|
22
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Parser for cocotb 2.x results: `results.xml` (+ the build and run logs) into `Issue`s.
|
|
2
|
+
|
|
3
|
+
cocotb writes a JUnit-style `results.xml` at `COCOTB_RESULTS_FILE`. It is the **only**
|
|
4
|
+
signal of what the tests did (docs/spikes/S3.md): a failing test, an exception in a
|
|
5
|
+
test, a test module that fails to import and a GPI that never loaded Python all leave
|
|
6
|
+
the simulator's exit code at 0. So:
|
|
7
|
+
|
|
8
|
+
- each `<testcase>` with a `<failure>` (or `<error>`) is one `Issue` with rule
|
|
9
|
+
``cocotb/<ExceptionType>`` (``cocotb/AssertionError`` usually means the DUT is wrong,
|
|
10
|
+
any other type that the testbench is), its file and line taken from the **last
|
|
11
|
+
traceback frame in the test's own file** (the testcase's `file` property; its `line`
|
|
12
|
+
property, the line of the test's `def`, is only a fallback), and the message
|
|
13
|
+
``<module>.<test>: <message>``; a passing test produces no issue;
|
|
14
|
+
- a `<skipped>` test is an `info` issue (``cocotb/skipped``); it never turns "nothing
|
|
15
|
+
ran" into a pass;
|
|
16
|
+
- a missing, empty or unparseable `results.xml`, one with zero testcases, or one whose
|
|
17
|
+
every test was skipped is ``status="error"`` with rule ``cocotb/no_results``, never
|
|
18
|
+
``pass``; the reason is the last Python exception or ``ERROR gpi`` line of the run log;
|
|
19
|
+
- a failed build is ``error`` with the compiler's messages: Verilator's through
|
|
20
|
+
`VerilatorParser` (rule ``verilator/<code>``, ``verilator/error`` for a plain
|
|
21
|
+
``%Error``) and Icarus's ``file:line: error: ...``/``file:line: syntax error`` lines
|
|
22
|
+
(rule ``icarus/error``).
|
|
23
|
+
|
|
24
|
+
`CocotbParser.parse(text)` is the `LogParser` view (the text of a `results.xml` in, its
|
|
25
|
+
issues out; a bad or empty one yields the ``cocotb/no_results`` error issue), and
|
|
26
|
+
`CocotbParser.parse_run(...)` classifies a whole build + run into a `CocotbOutcome` with
|
|
27
|
+
a `CheckStatus`, which is what `EdalizeTool` uses.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import xml.etree.ElementTree as ET
|
|
35
|
+
from collections.abc import Callable
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Literal
|
|
39
|
+
|
|
40
|
+
from chipgraph.adapters.parser.verilator import VerilatorParser
|
|
41
|
+
from chipgraph.core.contracts import Issue
|
|
42
|
+
from chipgraph.core.contracts.types import CheckStatus, Severity
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"NO_RESULTS_RULE",
|
|
46
|
+
"CocotbOutcome",
|
|
47
|
+
"CocotbParser",
|
|
48
|
+
"CocotbTest",
|
|
49
|
+
"build_issues",
|
|
50
|
+
"parse_icarus_log",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
NO_RESULTS_RULE = "cocotb/no_results"
|
|
54
|
+
|
|
55
|
+
_FRAME_RE = re.compile(r'^\s*File "(?P<file>[^"]+)", line (?P<line>\d+)', re.MULTILINE)
|
|
56
|
+
_PY_EXC_RE = re.compile(
|
|
57
|
+
r"^(?P<type>[A-Za-z_][\w.]*(?:Error|Exception|Exit|Interrupt)): (?P<msg>.*)$", re.MULTILINE
|
|
58
|
+
)
|
|
59
|
+
_GPI_ERR_RE = re.compile(r"\bERROR\s+gpi\b.*?\bin \S+\s+(?P<msg>\S.*)$", re.MULTILINE)
|
|
60
|
+
_ICARUS_RE = re.compile(
|
|
61
|
+
r"^(?P<file>[^\s:][^:\n]*):(?P<line>\d+):\s+"
|
|
62
|
+
r"(?:(?P<kind>error|warning|sorry):\s*(?P<msg>.*)|(?P<syntax>syntax error.*)"
|
|
63
|
+
r"|vvp\.tgt sorry:\s*(?P<tgt>.*))$",
|
|
64
|
+
re.MULTILINE,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
Simulator = Literal["verilator", "icarus"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class CocotbTest:
|
|
72
|
+
"""One `<testcase>` of a `results.xml`: its full name and what happened to it."""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
outcome: Literal["pass", "fail", "skipped"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class CocotbOutcome:
|
|
80
|
+
"""A classified build + run: the check status, its issues, and the tests seen."""
|
|
81
|
+
|
|
82
|
+
status: CheckStatus
|
|
83
|
+
issues: tuple[Issue, ...]
|
|
84
|
+
tests: tuple[CocotbTest, ...] = ()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def summary(self) -> str:
|
|
88
|
+
"""``tests=N pass=P fail=F skipped=S``, for a log line."""
|
|
89
|
+
count = {k: sum(t.outcome == k for t in self.tests) for k in ("pass", "fail", "skipped")}
|
|
90
|
+
return (
|
|
91
|
+
f"tests={len(self.tests)} pass={count['pass']} fail={count['fail']} "
|
|
92
|
+
f"skipped={count['skipped']}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CocotbParser:
|
|
97
|
+
"""Turns cocotb's `results.xml` (and the simulator's logs) into `Issue`s."""
|
|
98
|
+
|
|
99
|
+
name = "cocotb"
|
|
100
|
+
|
|
101
|
+
def parse(self, log: str) -> tuple[Issue, ...]:
|
|
102
|
+
"""`LogParser` view: `log` is the text of a `results.xml`.
|
|
103
|
+
|
|
104
|
+
A passing run yields no error issue (skipped tests are `info`). An empty or
|
|
105
|
+
unparseable text, or one with no test that actually ran, yields one
|
|
106
|
+
``cocotb/no_results`` error issue, so a caller that only looks at issue
|
|
107
|
+
severities can never read "nothing ran" as a pass.
|
|
108
|
+
"""
|
|
109
|
+
return self.parse_results(log).issues
|
|
110
|
+
|
|
111
|
+
def parse_results(self, xml_text: str | None, *, run_log: str = "") -> CocotbOutcome:
|
|
112
|
+
"""Classify the text of a `results.xml` (None: the file does not exist)."""
|
|
113
|
+
if xml_text is None:
|
|
114
|
+
return _no_results("cocotb wrote no results.xml: no test ran", run_log)
|
|
115
|
+
if not xml_text.strip():
|
|
116
|
+
return _no_results("results.xml is empty: no test ran", run_log)
|
|
117
|
+
try:
|
|
118
|
+
root = ET.fromstring(xml_text)
|
|
119
|
+
except ET.ParseError as exc:
|
|
120
|
+
return _no_results(f"results.xml is not valid XML ({exc})", run_log)
|
|
121
|
+
|
|
122
|
+
tests: list[CocotbTest] = []
|
|
123
|
+
issues: list[Issue] = []
|
|
124
|
+
for case in root.iter("testcase"):
|
|
125
|
+
name = ".".join(p for p in (case.get("classname", ""), case.get("name", "")) if p)
|
|
126
|
+
props = {p.get("name"): p.get("value") for p in case.iter("property")}
|
|
127
|
+
bad = case.find("failure")
|
|
128
|
+
if bad is None:
|
|
129
|
+
bad = case.find("error")
|
|
130
|
+
if bad is not None:
|
|
131
|
+
tests.append(CocotbTest(name, "fail"))
|
|
132
|
+
issues.append(_failure_issue(name, bad, props))
|
|
133
|
+
elif case.find("skipped") is not None:
|
|
134
|
+
tests.append(CocotbTest(name, "skipped"))
|
|
135
|
+
issues.append(Issue(msg=f"{name}: skipped", rule="cocotb/skipped", severity="info"))
|
|
136
|
+
else:
|
|
137
|
+
tests.append(CocotbTest(name, "pass"))
|
|
138
|
+
|
|
139
|
+
if not tests:
|
|
140
|
+
return _no_results("results.xml has no testcase: no test ran", run_log)
|
|
141
|
+
if all(t.outcome == "skipped" for t in tests):
|
|
142
|
+
why = f"every test was skipped ({len(tests)}): no test ran"
|
|
143
|
+
no_run = _no_results(why, run_log).issues
|
|
144
|
+
return CocotbOutcome("error", (*issues, *no_run), tuple(tests))
|
|
145
|
+
status: CheckStatus = "fail" if any(t.outcome == "fail" for t in tests) else "pass"
|
|
146
|
+
return CocotbOutcome(status, tuple(issues), tuple(tests))
|
|
147
|
+
|
|
148
|
+
def parse_run(
|
|
149
|
+
self,
|
|
150
|
+
results: Path | None,
|
|
151
|
+
*,
|
|
152
|
+
simulator: Simulator,
|
|
153
|
+
build_rc: int,
|
|
154
|
+
build_log: str = "",
|
|
155
|
+
build_timed_out: bool = False,
|
|
156
|
+
run_rc: int | None = None,
|
|
157
|
+
run_log: str = "",
|
|
158
|
+
run_timed_out: bool = False,
|
|
159
|
+
relativize: Callable[[str], str] | None = None,
|
|
160
|
+
) -> CocotbOutcome:
|
|
161
|
+
"""Classify one build + run of a cocotb testbench.
|
|
162
|
+
|
|
163
|
+
`build_rc` is the build's exit code; `run_rc` is the run's (None when the run did
|
|
164
|
+
not start). `results` is the path cocotb was told to write (it may not exist, and
|
|
165
|
+
the caller must have removed any stale one before the run). `relativize` maps a
|
|
166
|
+
file path in an issue to the form the caller wants (e.g. repo-relative).
|
|
167
|
+
|
|
168
|
+
The outcome is `pass` only with a `results.xml` that has at least one test that
|
|
169
|
+
ran, none that failed, a run that exited 0 and no timeout. Failing tests are
|
|
170
|
+
`fail`; everything else (build failure, timeout, no or bad results) is `error`.
|
|
171
|
+
"""
|
|
172
|
+
rel = relativize or (lambda path: path)
|
|
173
|
+
if build_rc != 0 or build_timed_out:
|
|
174
|
+
issues = list(build_issues(build_log, simulator))
|
|
175
|
+
if build_timed_out:
|
|
176
|
+
issues.append(Issue(msg="the build timed out", rule="sim/timeout"))
|
|
177
|
+
return CocotbOutcome("error", _relativize(issues, rel))
|
|
178
|
+
if run_rc is None:
|
|
179
|
+
msg = "the simulation did not start"
|
|
180
|
+
return CocotbOutcome("error", (Issue(msg=msg, rule=NO_RESULTS_RULE),))
|
|
181
|
+
|
|
182
|
+
text = _read_text(results) if results is not None else None
|
|
183
|
+
outcome = self.parse_results(text, run_log=run_log)
|
|
184
|
+
issues = list(outcome.issues)
|
|
185
|
+
status = outcome.status
|
|
186
|
+
if run_timed_out:
|
|
187
|
+
issues.append(Issue(msg="the simulation timed out", rule="sim/timeout"))
|
|
188
|
+
status = "fail" if status == "fail" else "error"
|
|
189
|
+
elif status == "pass" and run_rc != 0:
|
|
190
|
+
msg = f"the simulator exited with {run_rc} after the tests"
|
|
191
|
+
issues.append(Issue(msg=msg, rule="sim/exit"))
|
|
192
|
+
status = "error"
|
|
193
|
+
return CocotbOutcome(status, _relativize(issues, rel), outcome.tests)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_issues(log: str, simulator: Simulator) -> tuple[Issue, ...]:
|
|
197
|
+
"""Compiler messages from a failed build; one generic issue if none is recognised."""
|
|
198
|
+
found: list[Issue] = []
|
|
199
|
+
for issue in VerilatorParser().parse(log):
|
|
200
|
+
code = issue.rule or ("error" if issue.severity == "error" else "warning")
|
|
201
|
+
found.append(issue.model_copy(update={"rule": f"verilator/{code}"}))
|
|
202
|
+
found.extend(parse_icarus_log(log))
|
|
203
|
+
if not any(i.severity == "error" for i in found):
|
|
204
|
+
last = _last_meaningful_line(log)
|
|
205
|
+
detail = f": {last}" if last else ""
|
|
206
|
+
found.append(Issue(msg=f"the {simulator} build failed{detail}", rule="sim/build"))
|
|
207
|
+
return tuple(found)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_icarus_log(log: str) -> tuple[Issue, ...]:
|
|
211
|
+
"""Icarus Verilog's ``file:line: error: msg`` / ``file:line: syntax error`` lines.
|
|
212
|
+
|
|
213
|
+
Errors get rule ``icarus/error``; ``warning:`` lines ``icarus/warning`` and
|
|
214
|
+
``sorry:`` lines (unsupported constructs Icarus ignores) ``icarus/sorry``, both
|
|
215
|
+
warnings.
|
|
216
|
+
"""
|
|
217
|
+
issues: list[Issue] = []
|
|
218
|
+
for m in _ICARUS_RE.finditer(log):
|
|
219
|
+
kind = m["kind"] or ("error" if m["syntax"] else "sorry")
|
|
220
|
+
msg = (m["msg"] or m["syntax"] or m["tgt"] or "").strip()
|
|
221
|
+
severity: Severity = "error" if kind == "error" else "warning"
|
|
222
|
+
issues.append(
|
|
223
|
+
Issue(
|
|
224
|
+
file=m["file"],
|
|
225
|
+
line=int(m["line"]),
|
|
226
|
+
rule=f"icarus/{kind}",
|
|
227
|
+
severity=severity,
|
|
228
|
+
msg=msg,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
return tuple(issues)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _failure_issue(name: str, bad: ET.Element, props: dict[str | None, str | None]) -> Issue:
|
|
235
|
+
kind = bad.get("type") or bad.tag
|
|
236
|
+
message = (bad.get("message") or "").strip() or kind
|
|
237
|
+
test_file = props.get("file")
|
|
238
|
+
file, line = _failure_location(bad.text or "", test_file)
|
|
239
|
+
if line is None and props.get("line"):
|
|
240
|
+
line = _int_or_none(props.get("line"))
|
|
241
|
+
return Issue(file=file, line=line, rule=f"cocotb/{kind}", msg=f"{name}: {message}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _failure_location(traceback: str, test_file: str | None) -> tuple[str | None, int | None]:
|
|
245
|
+
"""The last traceback frame in `test_file`; else the test's own file, no line yet."""
|
|
246
|
+
frames = [(m["file"], int(m["line"])) for m in _FRAME_RE.finditer(traceback)]
|
|
247
|
+
if test_file:
|
|
248
|
+
own = [f for f in frames if _same_file(f[0], test_file)]
|
|
249
|
+
if own:
|
|
250
|
+
return own[-1]
|
|
251
|
+
return test_file, None
|
|
252
|
+
return frames[-1] if frames else (None, None)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _same_file(a: str, b: str) -> bool:
|
|
256
|
+
return a == b or os.path.realpath(a) == os.path.realpath(b)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _no_results(why: str, run_log: str) -> CocotbOutcome:
|
|
260
|
+
reason = _run_log_reason(run_log)
|
|
261
|
+
msg = f"{why} ({reason})" if reason else why
|
|
262
|
+
return CocotbOutcome("error", (Issue(msg=msg, rule=NO_RESULTS_RULE),))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _run_log_reason(run_log: str) -> str:
|
|
266
|
+
"""The last Python exception, else the last ``ERROR gpi`` message, of a run log."""
|
|
267
|
+
exc = list(_PY_EXC_RE.finditer(run_log))
|
|
268
|
+
if exc:
|
|
269
|
+
return f"{exc[-1]['type']}: {exc[-1]['msg'].strip()}"
|
|
270
|
+
gpi = list(_GPI_ERR_RE.finditer(run_log))
|
|
271
|
+
if gpi:
|
|
272
|
+
return f"gpi: {gpi[-1]['msg'].strip()}"
|
|
273
|
+
return ""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _last_meaningful_line(log: str) -> str:
|
|
277
|
+
for line in reversed(log.splitlines()):
|
|
278
|
+
text = line.strip()
|
|
279
|
+
if text and not text.startswith("make") and not text.startswith("%Error: Exiting"):
|
|
280
|
+
return text[:200]
|
|
281
|
+
return ""
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _relativize(issues: list[Issue], rel: Callable[[str], str]) -> tuple[Issue, ...]:
|
|
285
|
+
return tuple(
|
|
286
|
+
i.model_copy(update={"file": rel(i.file)}) if i.file is not None else i for i in issues
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _read_text(path: Path) -> str | None:
|
|
291
|
+
try:
|
|
292
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
293
|
+
except FileNotFoundError:
|
|
294
|
+
return None
|
|
295
|
+
except OSError:
|
|
296
|
+
return ""
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _int_or_none(raw: str | None) -> int | None:
|
|
300
|
+
try:
|
|
301
|
+
value = int(raw) if raw is not None else None
|
|
302
|
+
except ValueError:
|
|
303
|
+
return None
|
|
304
|
+
return value if value is not None and value >= 1 else None
|