Skip to content
VLSI Mentor

Python Part II · Module 28 · Report Parsing

Parsing a Synthesis Report

A synthesis report is mostly a table of totals, which makes it look like the easiest thing in this module to parse. It is the one where a wrong answer is hardest to notice: a timing slack that parses wrongly is usually implausible, and an area that parses wrongly is just a number. The regex everybody writes for it is correct on one of the spellings a tool uses and silently produces a believable wrong value on another — and the harder half of the chapter is which of these numbers can be compared between two runs at all.

Advanced19 min readPythonPart IIAutomationSynthesisReport Parsing

Part II · Module 28 · Chapter 28.3 · Parsing a Synthesis Report

1. What the Report Says

The specimen is constructed in the shape these tools emit — no synthesis tool is installed here, per Chapter 23.1 Section 1:

Azvya Education Pvt. Ltd.VLSI Mentor
synth.rpt
****************************************
Report : area
Design : axi_dma
Library: tcbn05_base_lvt_ss0p675v125c
****************************************

Number of ports:                         1842
Number of nets:                         48213
Number of cells:                        45231
Number of combinational cells:          38904
Number of sequential cells:              6327
Number of buf/inv:                       4102
Number of references:                      87

Combinational area:              98214.721680
Buf/Inv area:                     6120.330000
Noncombinational area:           30241.606445
Total cell area:                128456.328125

Warning: Design 'axi_dma' has 3 unmapped cells. (OPT-170)
Warning: Latch inferred for signal 'ctrl/state_next'. (ELAB-350)
Information: Timing loop detected. (OPT-150)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from pathlib import Path

lines = Path("synth.rpt").read_text(encoding="utf-8").splitlines()

counts = [l for l in lines if l.startswith("Number of")]
areas = [l for l in lines if "area:" in l]
warnings = [l for l in lines if l.startswith("Warning:")]

print("lines     :", len(lines))
print("count rows:", len(counts))
print("area rows :", len(areas))
print("warnings  :", len(warnings))
print()
print("the shape is:", "label, whitespace, number — one per line")
print("no sections :", "unlike the timing report's per-path records")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines     : 22
count rows: 7
area rows : 4
warnings  : 2

the shape is: label, whitespace, number — one per line
no sections : unlike the timing report's per-path records

No stateful parsing needed, unlike Chapter 28.1's multi-line path records. Every fact is one line: a label, a run of whitespace, a number. That simplicity is exactly why the parsing gets less attention than it deserves.

2. The Annotation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
re.search(r"Total cell area:\s+([\d.]+)", line)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

PATTERN = re.compile(r"Total cell area:\s+([\d.]+)")

SPELLINGS = [
    "Total cell area:                 128456.3281",
    "Total cell area:               1,284,563.33",
    "Total cell area:                  1.2846e+05",
    "Total cell area:                            0",
    "Total cell area:                         -1.5",
    "Total cell area: 128456.3281",
]

for line in SPELLINGS:
    m = PATTERN.search(line)
    print(f"{line.split(':')[1].strip():<16} -> {m.group(1) if m else None}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
128456.3281      -> 128456.3281
1,284,563.33     -> 1
1.2846e+05       -> 1.2846
0                -> 0
-1.5             -> None
128456.3281      -> 128456.3281

Four of six correct and the two failures are different kinds of bad.

-1.5 returns None, which is the good failure: something downstream will notice a missing value, and a negative area is nonsense anyway.

1,284,563.33 returns "1". That is the bad one, and it is worth being precise about how bad:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

PATTERN = re.compile(r"Total cell area:\s+([\d.]+)")
LINE = "Total cell area:               1,284,563.33"

m = PATTERN.search(LINE)
captured = m.group(1)

print("the line says    :", LINE.split(":")[1].strip())
print("the regex captures:", repr(captured))
print("float() accepts it:", float(captured))
print("an exception     :", "no")
print("a None           :", "no")
print()
true_value = 1284563.33
print("true area        :", true_value)
print("parsed area      :", float(captured))
print("wrong by a factor of:", f"{true_value / float(captured):,.0f}")
print()
print("what Chapter 28.6 would see:", "area collapsed to 1.0 — a catastrophic improvement")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the line says    : 1,284,563.33
the regex captures: '1'
float() accepts it: 1.0
an exception     : no
a None           : no

true area        : 1284563.33
parsed area      : 1.0
wrong by a factor of: 1,284,563

what Chapter 28.6 would see: area collapsed to 1.0 — a catastrophic improvement

[\d.]+ stops at the first comma and captures a single character. float("1") is perfectly happy, nothing raises, and the flow now believes the design is one square micron.

This is the characteristic failure of report parsing and it is worse than a crash in every way: a crash names the line, and this produces a number that flows into Chapter 28.5's comparison and Chapter 28.6's threshold as though it were real. An area that collapsed by six orders of magnitude would be reported as an improvement.

3. A Number Pattern That Holds

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

NUMBER = r"([-+]?[\d,]*\.?\d+(?:[eE][-+]?\d+)?)"
PATTERN = re.compile(rf"Total cell area:\s*{NUMBER}")

SPELLINGS = [
    "Total cell area:                 128456.3281",
    "Total cell area:               1,284,563.33",
    "Total cell area:                  1.2846e+05",
    "Total cell area:                            0",
    "Total cell area:                         -1.5",
    "Total cell area: 128456.3281",
]


def parse_number(text):
    return float(text.replace(",", ""))


for line in SPELLINGS:
    m = PATTERN.search(line)
    raw = m.group(1) if m else None
    print(f"{str(raw):<16} -> {parse_number(raw) if raw else None}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
128456.3281      -> 128456.3281
1,284,563.33     -> 1284563.33
1.2846e+05       -> 128460.0
0                -> 0.0
-1.5             -> -1.5
128456.3281      -> 128456.3281

Six of six, including the two the annotation lost.

Four pieces of that pattern each earn their place.

[-+]? admits a sign. Area should never be negative and a delta often is, and the same helper parses both.

[\d,]* admits thousands separators, and .replace(",", "") removes them before float. Stripping the comma inside the regex is not possible; it has to be a second step, which is why parsing is a function rather than a pattern.

\.?\d+ requires at least one digit after any decimal point, so a stray . does not match on its own.

(?:[eE][-+]?\d+)? admits scientific notation, which tools use for very large or very small values and which is where the annotation lost five orders of magnitude.

\s* rather than \s+ tolerates a report with no padding — the last specimen line — which happens when a tool is run with a different formatting switch.

4. Extracting the Whole Report

Azvya Education Pvt. Ltd.VLSI Mentor
synth.rpt
Report : area
Design : axi_dma
Library: tcbn05_base_lvt_ss0p675v125c

Number of ports:                         1842
Number of nets:                         48213
Number of cells:                        45231
Number of combinational cells:          38904
Number of sequential cells:              6327
Number of buf/inv:                       4102

Combinational area:              98214.721680
Buf/Inv area:                     6120.330000
Noncombinational area:           30241.606445
Total cell area:                128456.328125
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re
from pathlib import Path

NUMBER = r"([-+]?[\d,]*\.?\d+(?:[eE][-+]?\d+)?)"

COUNTS = {
    "ports": r"Number of ports:",
    "nets": r"Number of nets:",
    "cells": r"Number of cells:",
    "combinational_cells": r"Number of combinational cells:",
    "sequential_cells": r"Number of sequential cells:",
    "buf_inv": r"Number of buf/inv:",
}
AREAS = {
    "combinational_area": r"Combinational area:",
    "buf_inv_area": r"Buf/Inv area:",
    "noncombinational_area": r"Noncombinational area:",
    "total_area": r"Total cell area:",
}
META = {"design": r"^Design\s*:\s*(\S+)", "library": r"^Library\s*:\s*(\S+)"}


def to_float(text):
    return float(text.replace(",", ""))


def to_int(text):
    value = to_float(text)
    if value != int(value):
        raise ValueError(f"expected an integer count, got {text!r}")
    return int(value)


def parse_synthesis(path):
    text = Path(path).read_text(encoding="utf-8", errors="replace")
    out = {}
    for name, pattern in META.items():
        m = re.search(pattern, text, re.M)
        out[name] = m.group(1) if m else None
    for name, label in COUNTS.items():
        m = re.search(rf"{label}\s*{NUMBER}", text)
        out[name] = to_int(m.group(1)) if m else None
    for name, label in AREAS.items():
        m = re.search(rf"{re.escape(label)}\s*{NUMBER}", text)
        out[name] = to_float(m.group(1)) if m else None
    return out


data = parse_synthesis("synth.rpt")
for key in ("design", "library", "cells", "sequential_cells", "total_area"):
    print(f"{key:<22} {data[key]}")

print()
missing = sorted(k for k, v in data.items() if v is None)
print("fields parsed :", len(data) - len(missing), "of", len(data))
print("missing       :", missing or "none")
parts = data["combinational_area"] + data["noncombinational_area"]
print("parts sum to  :", round(parts, 3))
print("total says    :", data["total_area"])
print("they agree    :", abs(parts - data["total_area"]) < 1e-6)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
design                 axi_dma
library                tcbn05_base_lvt_ss0p675v125c
cells                  45231
sequential_cells       6327
total_area             128456.328125

fields parsed : 12 of 12
missing       : none
parts sum to  : 128456.328
total says    : 128456.328125
they agree    : True

Four decisions, and the second is the one that catches a tool change.

to_int refuses a non-integral count. A tool that prints 45231.0 for consistency with the area column beside it would pass a float-shaped pattern and then fail at int() — Section 6's hazard — and this converts it into a message naming the value.

Every field's presence is checked, so a regex that stops matching after a tool update is visible as a missing list rather than as a None three functions later. Chapter 28.1 Section 2 made the same argument for timing paths.

The parts are summed and compared against the total. Combinational plus non-combinational should equal the total, and a mismatch means a mis-parse — the same internal cross-check as Chapter 28.1's required - arrival.

The library is captured, because Section 7 is about to make it the thing that decides whether any of these numbers are comparable at all.

5. Warnings Worth Escalating

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re
from collections import Counter

WARNINGS = [
    "Warning: Design 'axi_dma' has 3 unmapped cells. (OPT-170)",
    "Warning: Latch inferred for signal 'ctrl/state_next'. (ELAB-350)",
    "Warning: Latch inferred for signal 'ctrl/busy_next'. (ELAB-350)",
    "Warning: Signal 'dbg_unused' is unconnected. (LINT-1)",
    "Warning: Timing loop detected in path through 'u_alu'. (OPT-150)",
    "Information: Area optimisation complete. (OPT-999)",
]

ESCALATE = {"ELAB-350": "a latch where a flop was intended",
            "OPT-170": "cells that did not map to the library",
            "OPT-150": "a combinational loop"}
IGNORE = {"LINT-1"}

CODE = re.compile(r"\((\w+-\d+)\)\s*$")


def triage(line):
    if not line.startswith("Warning:"):
        return None, "not a warning"
    m = CODE.search(line)
    code = m.group(1) if m else None
    if code in ESCALATE:
        return code, "ESCALATE"
    if code in IGNORE:
        return code, "ignore"
    return code, "unknown — review once, then classify"


counts = Counter()
for line in WARNINGS:
    code, verdict = triage(line)
    counts[verdict] += 1
    print(f"{str(code):<10} {verdict:<34} {line[9:52]}")

print()
print("verdicts:", dict(sorted(counts.items())))
print()
print("escalated by code, not by text:", True)
print("why                           :", "the text contains the signal name and varies per line")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
OPT-170    ESCALATE                           Design 'axi_dma' has 3 unmapped cells. (OPT
ELAB-350   ESCALATE                           Latch inferred for signal 'ctrl/state_next'
ELAB-350   ESCALATE                           Latch inferred for signal 'ctrl/busy_next'.
LINT-1     ignore                             Signal 'dbg_unused' is unconnected. (LINT-1
OPT-150    ESCALATE                           Timing loop detected in path through 'u_alu
None       not a warning                      on: Area optimisation complete. (OPT-999)

verdicts: {'ESCALATE': 4, 'ignore': 1, 'not a warning': 1}

escalated by code, not by text: True
why                           : the text contains the signal name and varies per line

Classify on the code, never on the message text. ELAB-350 is stable; "Latch inferred for signal 'ctrl/state_next'" contains a signal name and is therefore a different string for every occurrence — which is Chapter 26.7 Section 5's normalisation rule arriving in a report rather than a log.

Three properties of the triage are worth copying.

An unknown code is neither escalated nor ignored, it is unknown. A new tool version introduces codes, and a classifier with only two buckets puts them in the wrong one — silently, and in whichever direction its default happens to be.

Information: lines are excluded by prefix, not by code. The last row's OPT-999 looks like an OPT code and is not a warning at all.

The two latch warnings share a code and are two findings. Chapter 26.7's bucketing applies: group by code for the summary, keep the individual lines for the detail, and ELAB-350 x2 is the line somebody acts on.

6. A Count Is Not a Float

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
VALUES = ["45231", "45231.0", "45,231", "45231.5"]

for text in VALUES:
    cleaned = text.replace(",", "")
    try:
        direct = int(cleaned)
    except ValueError as exc:
        direct = f"ValueError: {exc}"
    via_float = float(cleaned)
    integral = via_float == int(via_float)
    print(f"{text:<10} int() -> {str(direct):<58} float -> {via_float:<10} integral {integral}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
45231      int() -> 45231                                                      float -> 45231.0    integral True
45231.0    int() -> ValueError: invalid literal for int() with base 10: '45231.0' float -> 45231.0    integral True
45,231     int() -> 45231                                                      float -> 45231.0    integral True
45231.5    int() -> ValueError: invalid literal for int() with base 10: '45231.5' float -> 45231.5    integral False

int("45231.0") raises. A tool that prints a trailing .0 on its counts — for column alignment with the areas beside them — breaks a parser that calls int() on the captured text.

The two failures above are not the same, and collapsing them is the mistake:

  • 45231.0 is an integer written as a float, and should parse to 45231.
  • 45231.5 is not a count at all, and should raise.

Going through float first and then checking integrality separates them, which is what to_int in Section 4 does. A bare int(text) rejects both; a bare int(float(text)) accepts both and silently truncates the second to 45231.

7. Which Numbers Compare

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
METRICS = [
    ("total_area", "same library AND same corner", "um^2", "the library's cells differ in size"),
    ("combinational_area", "same library", "um^2", "same reason"),
    ("cells", "same ungrouping and flattening", "count", "a hierarchy boundary changes the count"),
    ("sequential_cells", "almost always comparable", "count", "flops are flops"),
    ("buf_inv", "same optimisation effort", "count", "effort decides buffering"),
    ("gate_equivalents", "same NAND2 reference", "GE", "the divisor is library-specific"),
]

print(f"{'metric':<22} {'comparable when':<32} {'unit':<7} why")
for name, when, unit, why in METRICS:
    print(f"{name:<22} {when:<32} {unit:<7} {why}")

print()
always = [n for n, w, _, _ in METRICS if w.startswith("almost always")]
print("comparable with no conditions:", always)
print("everything else needs         :", "the two runs to agree on something")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
metric                 comparable when                  unit    why
total_area             same library AND same corner     um^2    the library's cells differ in size
combinational_area     same library                     um^2    same reason
cells                  same ungrouping and flattening   count   a hierarchy boundary changes the count
sequential_cells       almost always comparable         count   flops are flops
buf_inv                same optimisation effort         count   effort decides buffering
gate_equivalents       same NAND2 reference             GE      the divisor is library-specific

comparable with no conditions: ['sequential_cells']
everything else needs         : the two runs to agree on something

One of six metrics compares unconditionally.

That is the canonical purpose's second half and it matters more than the parsing. Chapter 28.5 puts two runs side by side and Chapter 28.6 turns a difference into a verdict — and both are meaningless if the runs used different libraries, corners or effort levels. Chapter 27.2 Section 6 made exactly this argument about a parallelisation baseline: a comparison across two different experiments measures nothing.

The defence is the same one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BEFORE = {"library": "tcbn05_base_lvt_ss0p675v125c", "corner": "ss0p675v125c",
          "effort": "high", "flatten": False, "total_area": 128456.3}
AFTER_OK = dict(BEFORE, total_area=131002.7)
AFTER_DRIFT = dict(BEFORE, total_area=96120.4, library="tcbn05_base_svt_tt0p75v25c",
                   corner="tt0p75v25c")

PINNED = ("library", "corner", "effort", "flatten")


def comparable(a, b):
    differ = sorted(k for k in PINNED if a.get(k) != b.get(k))
    return (not differ), differ


for label, other in (("same setup", AFTER_OK), ("different library", AFTER_DRIFT)):
    ok, differ = comparable(BEFORE, other)
    delta = other["total_area"] - BEFORE["total_area"]
    print(f"{label:<19} comparable={str(ok):<6} area delta {delta:+10.1f}  differing: {differ}")

print()
print("the second run's area looks 25% better:", True)
print("what actually changed                 :", "the library and the corner")
print("what the area delta measures          :", "nothing")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
same setup          comparable=True   area delta    +2546.4  differing: []
different library   comparable=False  area delta   -32335.9  differing: ['corner', 'library']

the second run's area looks 25% better: True
what actually changed                 : the library and the corner
what the area delta measures          : nothing

A 25% area improvement that is entirely a library change. The number is real, the delta is arithmetic, and it says nothing about the design.

The pinned fields go in the record beside the metrics for the same reason Chapter 28.1 Section 3 recorded the report switches and Chapter 24.6 recorded the environment: a measurement is only interpretable alongside what produced it, and the cheapest way to enforce that is a comparability check that refuses rather than a convention nobody remembers.

8. The Record

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import json
import re
from pathlib import Path

NUMBER = r"([-+]?[\d,]*\.?\d+(?:[eE][-+]?\d+)?)"


def to_float(t):
    return float(t.replace(",", ""))


def to_int(t):
    v = to_float(t)
    if v != int(v):
        raise ValueError(f"expected an integer count, got {t!r}")
    return int(v)


def extract(text, label, convert):
    m = re.search(rf"{re.escape(label)}\s*{NUMBER}", text)
    return convert(m.group(1)) if m else None


REPORT = """Design : axi_dma
Library: tcbn05_base_lvt_ss0p675v125c
Number of cells:                        45231
Number of sequential cells:              6327
Combinational area:              98214.721680
Noncombinational area:           30241.606445
Total cell area:                128456.328125
Warning: Latch inferred for signal 'ctrl/state_next'. (ELAB-350)
"""


def synthesis_record(text, *, corner, effort, flatten):
    design = re.search(r"^Design\s*:\s*(\S+)", text, re.M)
    library = re.search(r"^Library\s*:\s*(\S+)", text, re.M)
    codes = re.findall(r"^Warning:.*\((\w+-\d+)\)\s*$", text, re.M)

    metrics = {
        "cells": extract(text, "Number of cells:", to_int),
        "sequential_cells": extract(text, "Number of sequential cells:", to_int),
        "combinational_area": extract(text, "Combinational area:", to_float),
        "noncombinational_area": extract(text, "Noncombinational area:", to_float),
        "total_area": extract(text, "Total cell area:", to_float),
    }
    problems = [k for k, v in metrics.items() if v is None]
    parts = (metrics["combinational_area"] or 0) + (metrics["noncombinational_area"] or 0)
    if metrics["total_area"] and abs(parts - metrics["total_area"]) > 1e-6:
        problems.append(f"areas do not sum: {parts} != {metrics['total_area']}")

    return {"design": design.group(1) if design else None,
            "context": {"library": library.group(1) if library else None,
                        "corner": corner, "effort": effort, "flatten": flatten},
            "metrics": metrics,
            "warning_codes": sorted(set(codes)),
            "problems": problems}


record = synthesis_record(REPORT, corner="ss0p675v125c", effort="high", flatten=False)
print(json.dumps(record, indent=2, sort_keys=True))
print()
print("problems:", record["problems"] or "none")
print("comparable against another run only if its context matches:",
      sorted(record["context"]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{
  "context": {
    "corner": "ss0p675v125c",
    "effort": "high",
    "flatten": false,
    "library": "tcbn05_base_lvt_ss0p675v125c"
  },
  "design": "axi_dma",
  "metrics": {
    "cells": 45231,
    "combinational_area": 98214.72168,
    "noncombinational_area": 30241.606445,
    "sequential_cells": 6327,
    "total_area": 128456.328125
  },
  "problems": [],
  "warning_codes": [
    "ELAB-350"
  ]
}

problems: none
comparable against another run only if its context matches: ['corner', 'effort', 'flatten', 'library']

Five decisions, and the first is the structural one.

context and metrics are separate objects. Chapter 28.5 compares metrics and checks context for equality first — two different operations on two different parts of the record, which a flat dictionary would blur.

problems is a list, not an exception. A report with a missing field still produces a record, which Chapter 26.6's reconciliation needs; the problems travel with it rather than replacing it.

The area cross-check is one of the problems, so a mis-parse shows up in the same place as a missing field.

Warning codes are deduplicated and sorted, giving a stable set for Chapter 26.7-style comparison between runs; the individual lines belong in the detail report rather than the record.

Context fields the report does not state are passed in. corner, effort and flatten come from the flow that invoked the tool, because most reports do not echo them — which makes them exactly the fields most likely to differ unnoticed.

9. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import re

OLD = re.compile(r"Total cell area:\s+([\d.]+)")
NEW = re.compile(r"Total cell area:\s*([-+]?[\d,]*\.?\d+(?:[eE][-+]?\d+)?)")
L1 = "Total cell area:   128456.33"
L2 = "Total cell area:   1,284,563.33"
L3 = "Total cell area:   1.2846e+05"

# A
print("A:", OLD.search(L1).group(1))

# B
print("B:", OLD.search(L2).group(1))

# C
print("C:", float(OLD.search(L2).group(1)))

# D
print("D:", NEW.search(L2).group(1))

# E
print("E:", float(NEW.search(L2).group(1).replace(",", "")))

# F
print("F:", float(NEW.search(L3).group(1)))

# G
try:
    int("45231.0")
except ValueError:
    print("G: ValueError")

# H
print("H:", int(float("45231.0")), float("45231.5") == int(float("45231.5")))

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 128456.33
B: 1
C: 1.0
D: 1,284,563.33
E: 1284563.33
F: 128460.0
G: ValueError
H: 45231 False

A — the annotation works on the plain spelling.

B — and captures one character from the comma spelling.

C — which float accepts without complaint.

D — the tolerant pattern captures the whole thing.

E — and stripping the comma gives the real value.

F — scientific notation parses too, where the annotation lost five orders of magnitude.

G — int() on a float-shaped count raises.

H — going through float first works, and the integrality test is what separates a formatted count from a genuinely fractional value.

10. Exercises

Exercise 1 — the shape. Say why a synthesis report needs no stateful parsing and a timing report does.

Exercise 2 — six spellings. Run the annotation's regex over all six and report which fail.

Exercise 3 — the bad failure. Explain why None is a better outcome than "1".

Exercise 4 — the factor. Compute how wrong the comma case is for your own design's area.

Exercise 5 — downstream. Describe what Chapter 28.6 would report given a parsed area of 1.0.

Exercise 6 — four pieces. For each part of the tolerant pattern, give a line it rescues.

Exercise 7 — why two steps. Say why the comma cannot be stripped inside the regex.

Exercise 8 — the locale. Construct a European-format number and show the tolerant pattern mis-parsing it.

Exercise 9 — the plausibility bound. Implement one from your previous run's area.

Exercise 10 — presence checks. Add a missing list and break a label to see it fire.

Exercise 11 — the internal sum. Add the combinational-plus-noncombinational check and construct a mis-parse it catches.

Exercise 12 — triage by code. Classify your own tool's warning codes into escalate, ignore and unknown.

Exercise 13 — why not the text. Say what makes the message text unsuitable as a key, citing Chapter 26.7.

Exercise 14 — the third bucket. Explain what a two-bucket classifier does to a code from a new tool version.

Exercise 15 — Information lines. Find one whose code looks like a warning code.

Exercise 16 — two kinds of non-integer. Distinguish 45231.0 from 45231.5 and handle each correctly.

Exercise 17 — one of six. List your own metrics and say which compares unconditionally.

Exercise 18 — the free improvement. Construct two runs whose area differs only because of the library.

Exercise 19 — pin the context. Implement the comparability check and say which fields it must cover for your flow.

Exercise 20 — not in the report. Name the context fields your tool does not echo, and say where they come from.

11. Summary

  • A synthesis report is one fact per line, which is why its parsing gets less scrutiny than it deserves.
  • [\d.]+ captures "1" from a comma-separated number — not an exception, not None, a plausible value wrong by a factor of 1,284,563.
  • A wrong area is harder to notice than a wrong slack, because any number is plausible.
  • A tolerant pattern admits a sign, thousands separators, a decimal point and an exponent, and the comma is stripped in a second step.
  • Use \s* rather than \s+, so an unpadded report still matches.
  • A locale that swaps comma and point defeats any regex — the defence is a plausibility bound, not a cleverer pattern.
  • Check that every expected field was found, and report the missing ones as a list.
  • Cross-check the parts against the total; combinational plus non-combinational must equal the total.
  • Triage warnings by code, never by message text, which contains signal names and varies per line.
  • Keep a third bucket for unknown codes, or a new tool version's codes land in whichever default you chose.
  • int("45231.0") raises, and a tool may print a trailing .0 for column alignment.
  • Separate an integer written as a float from a genuinely fractional value by going through float and testing integrality.
  • Only one of six synthesis metrics compares unconditionally — area needs the same library and corner, counts need the same ungrouping, gate equivalents need the same reference.
  • A 25% area improvement can be entirely a library change, and the delta then measures nothing.
  • Keep context and metrics as separate objects, and check the context for equality before comparing the metrics.
  • Pass in the context fields the report does not echo — corner, effort and flattening are the ones most likely to differ unnoticed.

The next chapter applies the same techniques to place-and-route output, where the numbers are percentages and the sections are back.

Where this fits

Part of the Python curriculum.