Skip to content
VLSI Mentor

Python Part II · Module 25 · Simulation Automation

Compiling the Design and Keeping the Compile Log

Step three takes the filelist and turns it into something runnable. It is the first step that invokes a real tool, so everything Modules 23 and 24 built arrives at once — the run directory, the log file, the timeout, the recorded command. What is new is the decision at the end: a design that does not compile has not failed a test, and the single most consequential line in this chapter is the one that stops before running one.

Intermediate18 min readPythonPart IIAutomationSimulationVerification Flow

Part II · Module 25 · Chapter 25.3 · Compiling the Design and Keeping the Compile Log

1. Stop Before Running

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
res = compile_design(run_dir, files_f, sim=sim)
if res.status != "OK":
    return res

Two lines, and the second is the one worth arguing for.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
COMPILE_FAILED = True

print("without the early return:")
print("   compile  : Error-[SE] Syntax error, core/regfile.sv line 18")
print("   run      : ./simv: No such file or directory")
print("   log ends : the LAST error is the one about simv")
print()
print("with the early return:")
print("   compile  : Error-[SE] Syntax error, core/regfile.sv line 18")
print("   run      : not attempted")
print("   status   : COMPILE_ERROR")
print()
print("errors a person sees, without:", 2)
print("errors a person sees, with   :", 1)
print("which one is read first      :", "the last one printed")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
without the early return:
   compile  : Error-[SE] Syntax error, core/regfile.sv line 18
   run      : ./simv: No such file or directory
   log ends : the LAST error is the one about simv

with the early return:
   compile  : Error-[SE] Syntax error, core/regfile.sv line 18
   run      : not attempted
   status   : COMPILE_ERROR

errors a person sees, without: 2
errors a person sees, with   : 1
which one is read first      : the last one printed

Running the simulator after a failed compile manufactures a second, louder, useless error. It arrives last, so it is what a reader sees first, and it points at a missing binary rather than at line 18 of regfile.sv.

Chapter 23.4 made this argument for a single tool: a non-zero return code is a decision, not a log line. Here it decides the shape of a two-step function — and Chapter 23.11 Section 6's runner stopped at the first failing step for exactly this reason.

2. The Compile Step

Azvya Education Pvt. Ltd.VLSI Mentor
fake_vcs.py
import os
import sys

# A stand-in compiler. Writes a log, and a binary unless told to fail.
mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
    log.write("FakeVCS Version 1.0\n")
    log.write("Parsing design file 'core/alu.sv'\n")
    if mode == "syntax":
        log.write("Error-[SE] Syntax error\n")
        log.write('  "core/regfile.sv", 18: token is \';\'\n')
        sys.exit(1)
    if mode == "nobinary":
        log.write("Compilation completed\n")
        sys.exit(0)
    log.write("Compilation completed\n")
with open("simv", "w", encoding="utf-8") as binary:
    binary.write("#!/bin/sh\necho simulating\n")
os.chmod("simv", 0o755)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional


@dataclass(frozen=True)
class CompileResult:
    status: str
    returncode: Optional[int] = None
    log_path: Optional[str] = None
    binary: Optional[str] = None
    errors: tuple = ()


COMPILER = Path("fake_vcs.py").resolve()


def compile_design(run_dir, files_f, *, sim, mode="ok", timeout_s=600):
    run_dir = Path(run_dir)
    run_dir.mkdir(parents=True, exist_ok=True)
    cmd = [sys.executable, str(COMPILER), "-f", str(Path(files_f).resolve())]
    env = {**os.environ, "FAKE_MODE": mode}
    cp = subprocess.run(cmd, cwd=run_dir, env=env, timeout=timeout_s,
                        stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
    log = run_dir / "compile.log"
    return CompileResult(status="OK" if cp.returncode == 0 else "COMPILE_ERROR",
                         returncode=cp.returncode, log_path=str(log),
                         binary=str(run_dir / "simv"))


Path("files.f").write_text("core/alu.sv\n", encoding="utf-8")

for mode in ("ok", "syntax"):
    res = compile_design(f"runs/{mode}", "files.f", sim="vcs", mode=mode)
    print(f"{mode:<8} status={res.status:<14} rc={res.returncode} "
          f"log exists={Path(res.log_path).exists()}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok       status=OK             rc=0 log exists=True
syntax   status=COMPILE_ERROR  rc=1 log exists=True

Everything from Modules 23 and 24 is in that call. The command is a list (23.2), shell=False is the default (23.3), the return code decides the status (23.4), output goes to a log file in the run directory rather than being captured (23.5), cwd=run_dir keeps the artefacts contained (23.7), and a timeout_s bounds it (23.8).

The log exists in both cases, which is the point of writing it to a file: a failing compile's log is the entire content of the failure, and capturing it into a variable that is then discarded on the error path is the classic way to lose it.

3. COMPILE_ERROR Is Not FAIL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RESULTS = [("axi_smoke", "COMPILE_ERROR"), ("axi_reset", "COMPILE_ERROR"),
           ("axi_burst", "COMPILE_ERROR"), ("axi_soak", "COMPILE_ERROR")]

print("a regression where the design does not compile:")
for test, status in RESULTS:
    print(f"   {test:<11} {status}")

print()
print("if these were reported as FAIL:")
print("   it reads as          :", "four tests found four bugs")
print("   it actually is       :", "one syntax error, zero tests run")
print("   who gets paged       :", "the verification team, wrongly")
print()
print("tests that exercised the design:", 0)
print("distinct root causes            :", 1)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a regression where the design does not compile:
   axi_smoke   COMPILE_ERROR
   axi_reset   COMPILE_ERROR
   axi_burst   COMPILE_ERROR
   axi_soak    COMPILE_ERROR

if these were reported as FAIL:
   it reads as          : four tests found four bugs
   it actually is       : one syntax error, zero tests run
   who gets paged       : the verification team, wrongly

tests that exercised the design: 0
distinct root causes            : 1

Chapter 25.1 Section 6 put COMPILE_ERROR in the status vocabulary and the third column of that table was who looks at it. This is that argument with numbers on it: a compile error sends somebody to the RTL, and a FAIL sends somebody to the testbench, and reporting one as the other sends the wrong team to the wrong file.

It also shares ERROR's property from Chapter 23.9 Section 7: none of those runs tested anything, so four COMPILE_ERROR rows in a hundred-test regression make the pass rate 96 out of 96, not 96 out of 100.

The two are still distinct. ERROR means the infrastructure broke; COMPILE_ERROR means the infrastructure worked perfectly and told you the design is wrong. That is a real result about the design — just not a test result.

4. Reading the Compile Log

The log below is a constructed specimen — no simulator is installed here, per Chapter 23.1 Section 1 — written in the shape this family of tools uses. What is demonstrated is the parsing design, not any vendor's exact wording:

Azvya Education Pvt. Ltd.VLSI Mentor
compile.log
FakeVCS Version 1.0 -- Build Date Jan 01 2024
Parsing design file 'core/alu.sv'
Parsing design file 'core/fifo.sv'
Warning-[PCWM-W] Port connection width mismatch
  core/fifo.sv, 42
Error-[SE] Syntax error
  Following verilog source has syntax error :
  "core/regfile.sv", 18: token is ';'
Error-[IND] Identifier not declared
  core/regfile.sv, 25: 'wdata_q' is not declared
Warning-[LCA_FEATURES_ENABLED] Usage warning
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re
from pathlib import Path

TAG = re.compile(r"^(Error|Warning)-\[([\w-]+)\]\s*(.*)")
LOC = re.compile(r'"?([\w./]+\.sv)"?,\s*(\d+)')

lines = Path("compile.log").read_text(encoding="utf-8").splitlines()
findings = []
for i, line in enumerate(lines):
    m = TAG.match(line)
    if not m:
        continue
    kind, tag, text = m.groups()
    where = None
    for ahead in lines[i + 1:i + 4]:
        hit = LOC.search(ahead)
        if hit:
            where = f"{hit.group(1)}:{hit.group(2)}"
            break
    findings.append((kind, tag, text, where))

for kind, tag, text, where in findings:
    print(f"{kind:<8} {tag:<22} {str(where):<22} {text}")

print()
print("errors  :", sum(1 for k, *_ in findings if k == "Error"))
print("warnings:", sum(1 for k, *_ in findings if k == "Warning"))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Warning  PCWM-W                 core/fifo.sv:42        Port connection width mismatch
Error    SE                     core/regfile.sv:18     Syntax error
Error    IND                    core/regfile.sv:25     Identifier not declared
Warning  LCA_FEATURES_ENABLED   None                   Usage warning

errors  : 2
warnings: 2

Three design decisions in that parser, and the second is the one most people get wrong first.

The tag is the classifier, not the message text. SE and IND are stable identifiers; the human-readable text after them changes between releases. Chapter 19's advice applies — anchor on the stable part.

The location is on a following line. Error-[SE] says what happened and the file and line arrive one or two lines later, so a scanner that looks at one line at a time finds every error and knows where none of them are. The three-line lookahead is the whole fix, and the last row shows what happens when there is no location at all: None, recorded rather than crashed.

Errors cascade. Error-[IND] says wdata_q is not declared — which is almost certainly true because of the syntax error two lines above. Two errors, one root cause, and the count is the less useful number:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
FINDINGS = [("Error", "SE", "core/regfile.sv:18"),
            ("Error", "IND", "core/regfile.sv:25"),
            ("Error", "IND", "core/regfile.sv:31"),
            ("Error", "IND", "core/regfile.sv:44")]

print("error count        :", len(FINDINGS))
print("first error        :", FINDINGS[0][1], "at", FINDINGS[0][2])
print("distinct tags      :", sorted({t for _, t, _ in FINDINGS}))
print("distinct files     :", sorted({w.split(':')[0] for *_, w in FINDINGS}))
print()
print("what to put in the result:", "the first error, and the count")
print("what to send somebody to :", FINDINGS[0][2])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error count        : 4
first error        : SE at core/regfile.sv:18
distinct tags      : ['IND', 'SE']
distinct files     : ['core/regfile.sv']

what to put in the result: the first error, and the count
what to send somebody to : core/regfile.sv:18

Four errors and one thing to fix. This is Chapter 20.10's first-failure argument, which was about a simulation log, arriving unchanged in the compile log: the count tells you how bad it looks and the first one tells you where to go. Record both; lead with the first.

5. A Zero Return Code Is Not a Binary

Azvya Education Pvt. Ltd.VLSI Mentor
fake_vcs.py
import os
import sys

mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
    log.write("FakeVCS Version 1.0\n")
    if mode == "syntax":
        log.write("Error-[SE] Syntax error\n")
        log.write('  "core/regfile.sv", 18: token is \';\'\n')
        sys.exit(1)
    log.write("Compilation completed\n")
if mode != "nobinary":
    with open("simv", "w", encoding="utf-8") as binary:
        binary.write("#!/bin/sh\necho simulating\n")
    os.chmod("simv", 0o755)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import os
import subprocess
import sys
from pathlib import Path

COMPILER = Path("fake_vcs.py").resolve()


def compile_once(run_dir, mode):
    run_dir = Path(run_dir)
    run_dir.mkdir(parents=True, exist_ok=True)
    env = {**os.environ, "FAKE_MODE": mode}
    cp = subprocess.run([sys.executable, str(COMPILER)], cwd=run_dir, env=env,
                        stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
    binary = run_dir / "simv"
    return cp.returncode, binary.exists(), os.access(binary, os.X_OK) if binary.exists() else False


for mode in ("ok", "nobinary", "syntax"):
    rc, exists, executable = compile_once(f"runs/b_{mode}", mode)
    verdict = "OK" if rc == 0 and exists and executable else "COMPILE_ERROR"
    print(f"{mode:<10} rc={rc}  binary={exists!s:<6} executable={executable!s:<6} -> {verdict}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok         rc=0  binary=True   executable=True   -> OK
nobinary   rc=0  binary=False  executable=False  -> COMPILE_ERROR
syntax     rc=1  binary=False  executable=False  -> COMPILE_ERROR

The middle row exited zero and produced nothing.

A return code is the tool's claim about its own work. Real compilers manage this surprisingly often: an analysis-only invocation that was never asked to elaborate, a flag combination that produces a library rather than a binary, a tool that reports success and then fails to link. Chapter 23.4 Section 3 said the return code is necessary and not sufficient; here is the sufficient part.

Checking the artefact as well as the status costs one exists() and converts a bewildering downstream failure — Section 1's "cannot execute ./simv" — into a compile error reported at the compile step, which is where somebody can act on it.

The executable bit is checked too, for Chapter 23.9 Section 4's reason: a file that exists and cannot be run produces PermissionError at launch, three steps later.

6. The Step, Assembled

Azvya Education Pvt. Ltd.VLSI Mentor
fake_vcs.py
import os
import sys

mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
    log.write("FakeVCS Version 1.0\n")
    if mode == "syntax":
        log.write("Error-[SE] Syntax error\n")
        log.write('  "core/regfile.sv", 18: token is \';\'\n')
        sys.exit(1)
    log.write("Compilation completed\n")
if mode != "nobinary":
    with open("simv", "w", encoding="utf-8") as binary:
        binary.write("#!/bin/sh\necho simulating\n")
    os.chmod("simv", 0o755)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import os
import re
import shlex
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

TAG = re.compile(r"^(Error|Warning)-\[([\w-]+)\]\s*(.*)")
LOC = re.compile(r'"?([\w./]+\.sv)"?,\s*(\d+)')
COMPILER = Path("fake_vcs.py").resolve()


@dataclass(frozen=True)
class CompileResult:
    status: str
    returncode: Optional[int] = None
    log_path: Optional[str] = None
    binary: Optional[str] = None
    first_error: Optional[str] = None
    error_count: int = 0
    warning_count: int = 0
    runtime_s: float = field(default=0.0, compare=False)


def parse_compile_log(path):
    lines = Path(path).read_text(encoding="utf-8", errors="replace").splitlines()
    out = []
    for i, line in enumerate(lines):
        m = TAG.match(line)
        if not m:
            continue
        where = next((f"{h.group(1)}:{h.group(2)}"
                      for h in (LOC.search(a) for a in lines[i + 1:i + 4]) if h), None)
        out.append((m.group(1), m.group(2), where))
    return out


def compile_design(run_dir, files_f, *, sim, mode="ok", timeout_s=600):
    """Compile once. Always returns a CompileResult; never raises for a bad design."""
    run_dir = Path(run_dir)
    run_dir.mkdir(parents=True, exist_ok=True)
    cmd = [sys.executable, str(COMPILER), "-f", str(Path(files_f).resolve())]
    (run_dir / "compile_cmd.sh").write_text(
        "#!/bin/sh\nexec " + shlex.join(cmd) + "\n", encoding="utf-8")

    env = {**os.environ, "FAKE_MODE": mode}
    try:
        cp = subprocess.run(cmd, cwd=run_dir, env=env, timeout=timeout_s,
                            stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
    except subprocess.TimeoutExpired:
        return CompileResult(status="NO_FINISH", log_path=str(run_dir / "compile.log"))

    log = run_dir / "compile.log"
    findings = parse_compile_log(log) if log.exists() else []
    errors = [f for f in findings if f[0] == "Error"]
    binary = run_dir / "simv"
    ok = cp.returncode == 0 and binary.exists() and os.access(binary, os.X_OK)

    return CompileResult(
        status="OK" if ok else "COMPILE_ERROR",
        returncode=cp.returncode,
        log_path=str(log),
        binary=str(binary) if binary.exists() else None,
        first_error=(f"{errors[0][1]} at {errors[0][2]}" if errors else None),
        error_count=len(errors),
        warning_count=len(findings) - len(errors))


Path("files.f").write_text("core/alu.sv\n", encoding="utf-8")

for mode in ("ok", "syntax", "nobinary"):
    res = compile_design(f"runs/{mode}", "files.f", sim="vcs", mode=mode)
    print(f"{mode:<9} {res.status:<14} errors={res.error_count} "
          f"first={str(res.first_error):<28} binary={res.binary is not None}")

print()
print("compile_cmd.sh written:", Path("runs/ok/compile_cmd.sh").exists())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok        OK             errors=0 first=None                         binary=True
syntax    COMPILE_ERROR  errors=1 first=SE at core/regfile.sv:18     binary=False
nobinary  COMPILE_ERROR  errors=0 first=None                         binary=False

compile_cmd.sh written: True

The nobinary row is the one to read twice: zero errors, zero warnings, and COMPILE_ERROR. The log says nothing is wrong and the artefact is missing, and the status reflects the artefact. A step that trusted the log alone would have called that a success.

Three other decisions carry forward.

A compile timeout is NO_FINISH, not COMPILE_ERROR. Chapter 23.8 Section 5's distinction: the tool did not finish, so it never reached a verdict about the design. A six-hour gate-level compile hitting its limit is a resource problem, not an RTL problem.

compile_cmd.sh is written before the compile runs, not after. Chapter 23.10's artefact is most useful on the run that failed, and writing it afterwards means the timeout path never writes it at all.

runtime_s carries compare=False — Chapter 25.1 Section 7, applied to this result type too, so two compiles of the same design compare equal.

7. Compiling Once for Many Tests

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import hashlib

SOURCES = ["core/alu.sv", "core/fifo.sv", "core/regfile.sv"]
TESTS = ["axi_smoke", "axi_reset", "axi_burst", "axi_soak", "axi_hang"]


def compile_key(sources, defines, sim):
    """Everything that changes the binary. Anything omitted is a stale-cache bug."""
    canonical = "\n".join([sim, *sorted(f"{k}={v}" for k, v in sorted(defines.items())),
                           *sorted(sources)])
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:12]


cache = {}
compiles = 0
for test in TESTS:
    key = compile_key(SOURCES, {"DUMP_WAVES": 1}, "vcs")
    if key not in cache:
        cache[key] = f"runs/build_{key}/simv"
        compiles += 1
    print(f"{test:<11} key={key} binary={cache[key]}")

print()
print("tests   :", len(TESTS))
print("compiles:", compiles)
print()
other = compile_key(SOURCES + ["core/newmod.sv"], {"DUMP_WAVES": 1}, "vcs")
print("a changed source set gives a different key:", other != key)
print("a changed define likewise                 :",
      compile_key(SOURCES, {"DUMP_WAVES": 0}, "vcs") != key)
print("a changed simulator likewise              :",
      compile_key(SOURCES, {"DUMP_WAVES": 1}, "xrun") != key)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
axi_smoke   key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_reset   key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_burst   key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_soak    key=8b542636c900 binary=runs/build_8b542636c900/simv
axi_hang    key=8b542636c900 binary=runs/build_8b542636c900/simv

tests   : 5
compiles: 1

a changed source set gives a different key: True
a changed define likewise                 : True
a changed simulator likewise              : True

Five tests, one compile, and in a four-hundred-test regression that is the difference between a nightly that finishes and one that does not. A compile is typically the longest single step in a flow and it produces the same binary for every test that shares its inputs.

The whole risk lives in the key, and it is worth stating starkly:

Anything that changes the binary and is not in the key is a stale-binary bug — and every test in the regression inherits it.

That is the worst failure mode in this module, because it is silent and it is global. A key that omits the defines serves a DUMP_WAVES=0 binary to a run that asked for waves, and the waveform is simply absent. A key that omits the simulator serves a VCS binary to an Xcelium run. A key that omits a source file serves yesterday's RTL to today's regression, and the results look completely normal.

The three checks at the end are not decoration — they are the test that the key covers its inputs, and a cache without them is a cache nobody should trust. Chapter 25.2 Section 8's source fingerprint is the right first component, because it already answers "did the source set change?" in a form that is stable across checkouts.

8. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
fake_vcs.py
import os
import sys

mode = os.environ.get("FAKE_MODE", "ok")
with open("compile.log", "w", encoding="utf-8") as log:
    log.write("Error-[SE] Syntax error\n" if mode == "syntax" else "done\n")
    if mode == "syntax":
        log.write('  "a.sv", 7: token is \';\'\n')
sys.exit(1 if mode == "syntax" else 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import os
import re
import subprocess
import sys
from pathlib import Path

C = Path("fake_vcs.py").resolve()
TAG = re.compile(r"^(Error|Warning)-\[([\w-]+)\]")
LOC = re.compile(r'"?([\w./]+\.sv)"?,\s*(\d+)')


def run(mode):
    d = Path("r_" + mode)
    d.mkdir(exist_ok=True)
    cp = subprocess.run([sys.executable, str(C)], cwd=d,
                        env={**os.environ, "FAKE_MODE": mode},
                        stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
    return cp.returncode, (d / "compile.log").read_text(encoding="utf-8")


# A
rc, log = run("ok")
print("A:", rc, repr(log.strip()))

# B
rc, log = run("syntax")
print("B:", rc)

# C
print("C:", [TAG.match(l).group(2) for l in log.splitlines() if TAG.match(l)])

# D
print("D:", TAG.match(log.splitlines()[0]) is not None,
      LOC.search(log.splitlines()[0]) is not None)

# E
print("E:", LOC.search(log.splitlines()[1]).groups())

# F
print("F:", Path("r_ok/simv").exists())

# G
print("G:", "COMPILE_ERROR" if rc != 0 or not Path("r_syntax/simv").exists() else "OK")

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 0 'done'
B: 1
C: ['SE']
D: True False
E: ('a.sv', '7')
F: False
G: COMPILE_ERROR

A — a clean compile exits zero and the log is kept either way.

B — a syntax error exits non-zero.

C — the tag is what identifies the error class.

D — the error line matches the tag pattern and carries no location.

E — the location is on the next line. Section 4's whole point.

F — this stand-in writes no binary, so even the clean run has none.

G — and the artefact check is what turns that into COMPILE_ERROR regardless of the code.

9. Exercises

Exercise 1 — the second error. Run a simulator after a failed compile and record both errors. Say which a reader sees first.

Exercise 2 — the early return. Add it and count the errors a person now sees.

Exercise 3 — every module so far. For your own compile step, name the chapter each of its six decisions came from.

Exercise 4 — the log survives. Show the compile log present after a failing compile, and say what capturing instead would have cost.

Exercise 5 — four of one. Take a regression where the design does not compile and compute the pass rate with and without those rows in the denominator.

Exercise 6 — who gets paged. Say where COMPILE_ERROR sends somebody and where FAIL does.

Exercise 7 — ERROR versus COMPILE_ERROR. Distinguish them in one sentence each.

Exercise 8 — the tag. Extract error tags from a real compile log. Say why the tag beats the message text.

Exercise 9 — the missing location. Show that the error line has no file or line, and implement the lookahead.

Exercise 10 — no location at all. Produce a finding with no location and show your parser recording None rather than crashing.

Exercise 11 — the cascade. Find a real log where one root cause produced several errors. Report the count and the first.

Exercise 12 — count or first. Say what each number is for, and which belongs at the top of a report.

Exercise 13 — zero and nothing. Construct a compile that exits zero and produces no binary. Say how your step catches it.

Exercise 14 — the executable bit. Say what a non-executable binary produces three steps later, citing Chapter 23.9.

Exercise 15 — the timeout status. Argue in two sentences why a compile timeout is NO_FINISH.

Exercise 16 — write the command first. Say what is lost if compile_cmd.sh is written after the compile.

Exercise 17 — compile once. Implement the cache and report compiles against tests for your regression.

Exercise 18 — cover the key. Write the three checks that prove your key changes when its inputs do. Add a fourth for something your flow has that this example does not.

Exercise 19 — the stale binary. Describe the symptom of a key that omits the defines, and say why it is worse than a crash.

Exercise 20 — which world. Decide whether your cache must handle in-place edits, and implement the consequence.

10. Summary

  • Return early on a failed compile. Running the simulator afterwards manufactures a louder, useless error that arrives last and is read first.
  • The compile step is every lesson from Modules 23 and 24 at once: list form, return code, log to a file, cwd=, timeout, recorded command.
  • Write the log to a file, not a variable, because a failing compile's log is the failure.
  • COMPILE_ERROR sends somebody to the RTL; FAIL sends somebody to the testbench. Reporting one as the other sends the wrong team.
  • Compile errors tested nothing, so they must not count in a pass-rate denominator — the same rule as ERROR.
  • ERROR means the infrastructure broke; COMPILE_ERROR means it worked and the design is wrong.
  • Classify on the error tag, which is stable, not the message text, which is not.
  • The file and line are on a following line, so a one-line-at-a-time scan knows every error and no locations. Use a short lookahead.
  • Record None for a finding with no location rather than crashing on it.
  • Errors cascade: the count says how bad it looks, the first error says where to go. Record both, lead with the first.
  • A zero return code is not a binary. Check the artefact exists and is executable.
  • A compile timeout is NO_FINISH, because the tool never reached a verdict about the design.
  • Write compile_cmd.sh before the compile, or the timeout path never writes it.
  • Compile once per distinct input set — five tests, one compile, and in a regression that is the difference between finishing and not.
  • Anything that changes the binary and is not in the cache key is a stale-binary bug, inherited silently by every test.
  • Test that the key changes when sources, defines or the simulator change.
  • The key identifies the input set, not the contents — decide whether your flow needs content hashing or a clean-checkout guarantee.

The next chapter runs the binary this one produced, with a test name and a seed.

Where this fits

Part of the Python curriculum.