Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Language Atlas (mini-courses for more languages)  Module 6 – Tcl 9 for engineers and automators

Project: a timing-report summariser in Tcl

Build a Tcl 9 timing-report summariser: parse with regexp and scan, sum slack per clock in exact picoseconds, report bad input by line, pass 25 tests.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and tcl 9.0.4
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Parse a structured text report with regexp, scan and string commands
  • Aggregate results per clock with dicts and print a summary sorted by two keys
  • Read files as strict UTF-8 and report malformed bytes and missing fields with their line number
  • Test the summariser with tcltest, including malformed and Unicode input and a time budget

Before you start

What you’ll learn · Before you start

What you will learn

  • Parse a structured text report with regexp, scan and string commands
  • Aggregate results per clock with dicts and print a summary sorted by two keys
  • Read files as strict UTF-8 and report malformed bytes and missing fields with their line number
  • Test the summariser with tcltest, including malformed and Unicode input and a time budget

Before you start

On this page

block_a.txt holds five timing paths on three clocks, and the summary a designer wants from it is three lines: core’s worst slack is −0.160 ns and its total negative slack −0.215 ns, mem’s both −0.060 ns, and io meets timing with 0.420 ns to spare. Get one rule wrong, how slacks are added, how ties sort, what a bad byte does, and the summary is wrong without a warning.

This project builds that summariser in Tcl 9 from a specification: regexp and scan to read the report, whole picoseconds so that every sum is exact, a dict per clock and two stable sorts, a line number and exit status 2 for any bad input, and 25 tcltest cases, all of them public, with malformed and Unicode input and a report of 10,000 paths.

A specification fixes the report format and every message

The report format is this course’s own, written for the project; no tool’s report format is copied. A path is a block that starts with path <number>, followed by six indented fields. The slack: In a timing report, the time a path has to spare: its required time minus its arrival time. A negative slack means the signal arrives too late, so the path fails timing. of a path is its required time minus its arrival time, and a negative slack means the signal arrives too late. Chip-design flows judge timing by two numbers, which OpenROAD’s flow documentation calls WNS and TNS when it repairs timing; the specification computes both per clock and defines them exactly:

The specification says Tested by
Blank lines and lines whose first non-space character is # are ignored parse-1.3
path <number> opens a path; each indented line <field> <value> gives one of from, to, clock, required, arrival, slack, once each, in any order parse-1.2, error-1.2, error-1.3, error-1.4, error-1.6
Times are ns with an optional minus sign and at most 3 decimals; -0.000 is 0 ps-1.1 to ps-1.6
Every path has all six fields, and its slack equals required minus arrival error-1.1, error-1.5
Per clock: paths, failing paths (slack below 0), the worst negative slack (WNS): The smallest slack among a clock's paths, the path most in need of repair. The summariser of this course prints it for every clock, as a positive number when no path fails. (the smallest, even if positive) and the total negative slack (TNS): The sum of the negative slacks of a clock's paths, and 0 when none fails. It measures the work left across all failing paths, where the worst negative slack shows only the worst one. (0 when none fails) summary-1.1, summary-1.3
Rows sorted by worst slack, then by clock name summary-1.1, summary-1.2
The file is UTF-8; LF or CR LF line ends; names keep their Unicode characters parse-1.4, parse-1.5, error-1.8
A bad report gets one line, line N: reason, on standard error; standard output stays empty and the exit status is 2 program-1.2
10,000 paths are summarised in under 1 s speed-1.1

Here are the sample report and the program, all of it in one file of about 170 lines:

The sample report and the summariser

report/block_a.txt

# Synthetic timing report of block_a, written for this course in its own format.
path 1
  from      core/acc_reg_3
  to        core/out_reg_3
  clock     core
  required  2.450
  arrival   2.610
  slack     -0.160
path 2
  from      core/pc_reg_0
  to        core/ir_reg_7
  clock     core
  required  2.450
  arrival   2.395
  slack     0.055
path 3
  from      io/rx_sync_1
  to        io/fifo_wr_4
  clock     io
  required  9.800
  arrival   9.380
  slack     0.420
path 4
  from      core/mul_reg_12
  to        core/acc_reg_12
  clock     core
  required  2.450
  arrival   2.505
  slack     -0.055
path 5
  from      mem/addr_reg_2
  to        mem/bank_sel_0
  clock     mem
  required  4.900
  arrival   4.960
  slack     -0.060

report/summarise.tcl

# summarise.tcl: worst and total negative slack per clock, from a timing report in the format of this course's
# project. Run it as tclsh9.0 summarise.tcl report.txt; the tests source it instead.
package require Tcl 9.0-

namespace eval report {
    # The six fields of every path.
    variable fields {from to clock required arrival slack}
}

# An input error: the message starts with the line number, and the error code says the input is at fault.
proc report::fail {line message} {
    return -code error -errorcode {REPORT INPUT} "line $line: $message"
}

# "-0.160" (ns) -> -160 (whole picoseconds). Three decimals at most, so the result is exact.
proc report::picoseconds {line name text} {
    if {![regexp {^-?[0-9]+(?:\.[0-9]{1,3})?$} $text]} {
        fail $line "$name must be a number of ns with at most 3 decimals, got \"$text\""
    }
    return [expr {round($text * 1000)}]
}

# The lines of a report file, read as UTF-8; a malformed byte is an input error on its line.
proc report::lines {path} {
    set chan [open $path r]
    chan configure $chan -encoding utf-8 -profile strict
    try {
        set text [chan read $chan]
    } trap {POSIX EILSEQ} {- options} {
        fail [llength [split [dict get $options -data] \n]] "the bytes are not valid UTF-8"
    } finally {
        chan close $chan
    }
    return [split [string trimright $text \n] \n]
}

# A report -> a list of paths, each a dict of its six fields, its number and its first line.
proc report::parse {path} {
    variable fields
    set paths {}
    set current {}
    set number 0
    foreach text [lines $path] {
        incr number
        set first [string index [string trimleft $text] 0]
        if {$first eq "" || $first eq "#"} {
            continue
        }
        if {[regexp {^path\s+[0-9]+\s*$} $text]} {
            if {$current ne ""} {
                lappend paths [complete $current]
            }
            set current [dict create id [lindex $text 1] line $number]
            continue
        }
        # An indented field: exactly two words, the field's name and its value.
        if {![string is space [string index $text 0]] || [scan $text {%s %s %s} key value extra] != 2} {
            fail $number "expected \"path <number>\" or an indented field, got \"$text\""
        }
        if {$current eq ""} {
            fail $number "the field \"$key\" comes before the first path line"
        }
        if {$key ni $fields} {
            fail $number "unknown field \"$key\""
        }
        if {[dict exists $current $key]} {
            fail $number "path [dict get $current id] gives $key twice"
        }
        if {$key in {required arrival slack}} {
            set value [picoseconds $number $key $value]
        }
        dict set current $key $value
    }
    if {$current ne ""} {
        lappend paths [complete $current]
    }
    if {![llength $paths]} {
        fail [expr {max($number, 1)}] "the report has no paths"
    }
    return $paths
}

# A finished path: every field present, and the slack equal to required - arrival.
proc report::complete {path} {
    variable fields
    set line [dict get $path line]
    set id [dict get $path id]
    foreach key $fields {
        if {![dict exists $path $key]} {
            fail $line "path $id has no $key"
        }
    }
    if {[dict get $path slack] != [dict get $path required] - [dict get $path arrival]} {
        fail $line "path $id: slack is not required - arrival"
    }
    return $path
}

# Per clock: the number of paths, the failing ones, the worst slack and the total negative slack, in ps.
proc report::summarise {paths} {
    set clocks [dict create]
    foreach path $paths {
        set clock [dict get $path clock]
        set slack [dict get $path slack]
        if {![dict exists $clocks $clock]} {
            dict set clocks $clock [dict create paths 0 failing 0 wns $slack tns 0]
        }
        dict update clocks $clock stats {
            dict incr stats paths
            if {$slack < [dict get $stats wns]} {
                dict set stats wns $slack
            }
            if {$slack < 0} {
                dict incr stats failing
                dict incr stats tns $slack
            }
        }
    }
    return $clocks
}

# -160 -> "-0.160", 2450 -> "2.450", 0 -> "0.000"
proc report::ns {ps} {
    set sign [expr {$ps < 0 ? "-" : ""}]
    set ps [expr {abs($ps)}]
    return [format "%s%d.%03d" $sign [expr {$ps / 1000}] [expr {$ps % 1000}]]
}

# The summary as a table: worst slack first, equal worst slacks by clock name.
proc report::table {clocks} {
    set rows {}
    dict for {clock stats} $clocks {
        dict with stats {
            lappend rows [list $clock $paths $failing $wns $tns]
        }
    }
    set rows [lsort -integer -index 3 [lsort -index 0 $rows]]
    set lines [list [format "%-10s %5s %7s %9s %9s" clock paths failing wns_ns tns_ns]]
    foreach row $rows {
        lassign $row clock paths failing wns tns
        lappend lines [format "%-10s %5d %7d %9s %9s" $clock $paths $failing [ns $wns] [ns $tns]]
    }
    return [join $lines \n]
}

# The program: 0 with the table on standard output, or 2 with one line on standard error.
proc report::main {argv} {
    if {[llength $argv] != 1} {
        puts stderr "usage: tclsh9.0 summarise.tcl report.txt"
        return 2
    }
    set file [lindex $argv 0]
    try {
        puts [table [summarise [parse $file]]]
    } trap {REPORT INPUT} {message} {
        puts stderr $message
        return 2
    } trap {POSIX ENOENT} {} {
        puts stderr "cannot read $file: no such file"
        return 2
    }
    return 0
}

# Run as a program, not when a test file sources this one.
if {[info exists ::argv0] && [file tail [info script]] eq [file tail $::argv0]} {
    exit [report::main $::argv]
}

Follow one report through the program, and the branch that every bad report takes:

Interactive · stage by stage From a timing report to a summary, or to one line on standard error Step through the summariser's stages for block_a.txt, then follow the branch that a bad report takes instead.
From a timing report to a summary, or to one line on standard error, step 7 of 7. Showing Read the file as UTF-8, Classify each line, Add a field to the path, Close the path, Gather each clock, Sort and print, line N: reason. In focus: Sort and print.not UTF-8a bad field or valuemissing or wrongRead the file as UTF-8strict: bad bytes stop itClassify each lineblank, comment, path, fieldAdd a field to the pathsix fields, each onceClose the pathall six, slack checkedGather each clockpaths, failing, wns, tnsSort and printworst slack first, status 0line N: reasonstandard error, status 2

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Read. The file is read in one go with -encoding utf-8 and the strict profile, and split into lines; CR LF ends become LF on the way in.
  2. Bytes that are not UTF-8. A malformed byte raises POSIX EILSEQ. The text decoded before it gives the line number: bad_bytes.txt stopped with line 4: the bytes are not valid UTF-8.
  3. Each line. Blank lines and comments are skipped. A line such as path 1 opens a new path; an indented line holds one field; anything else is an error with its line number.
  4. Fields. scan splits a field line into exactly two words. The name must be one of the six fields and new in this path, and times become whole picoseconds.
  5. Close the path. At the next path line, or at the end of the file, the path must have all six fields and a slack equal to required minus arrival. missing_slack.txt stopped with line 1: path 1 has no slack.
  6. Gather. Each path adds to its clock in a dict: one more path, one more failing path when its slack is below 0, a new worst slack, and a negative slack into the total.
  7. Print. The clocks are sorted by worst slack, ties by name, and printed. For block_a.txt: core -0.160 and -0.215, mem -0.060, io 0.420; the exit status is 0.

Checkpoint

Check: The report format and the summary it asks for

  1. Question 1 of 2 A clock has two paths, with slacks of 0.090 ns and 0.030 ns. What worst slack does the specification ask for?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0.030 ns, the smallest slack, although it is positive

    The specification defines both numbers: the worst slack is the minimum, and the total negative slack is the sum of the slacks below 0, which is 0 here.

  2. Question 2 of 2 core's three paths in block_a.txt have slacks of -0.160, 0.055 and -0.055 ns. What is core's total negative slack, in ns?

    Type a number.

    A hint

    Only the slacks below 0 are added.

    Show the answer: Question 2 of 2

    Answer: -0.215 ns

    -0.160 + -0.055 = -0.215; the positive 0.055 is not added. The run printed -0.215 in core's row.

regexp and scan read the lines, and picoseconds keep sums exact

Each line is one of four kinds, and the parser tells them apart with the cheapest command that is still exact. A path line must match ^path\s+[0-9]+\s*$: the regexp manual says regexp returns 1 for a match and 0 otherwise, and the anchors make the whole line match, not part of it. A field line is split with scan $text {%s %s %s} key value extra, which returns how many words it found: exactly 2 is a field, 3 means text after the value, 1 a missing value. The walkthrough runs each piece on its own:

walkthrough.tcl: the parser's pieces, one line at a time TCL · report/walkthrough.tcl
# walkthrough.tcl: the parser's pieces, one line of the report at a time, then the dicts they build.
source summarise.tcl
puts "1. Which lines start a path:"
foreach line {{path 12} {path 12 x} {Path 12} {path twelve}} {
    puts "   [format %-15s "\"$line\""] [regexp {^path\s+[0-9]+\s*$} $line]"
}
puts "2. Words that scan finds in a field line:"
foreach line {{  required  2.450} {  from a/q extra} {  slack}} {
    puts "   [format %-20s "\"$line\""] [scan $line {%s %s %s} key value extra]"
}
set line "  from mémoire/bank_0"
scan $line {%s %s%n} key value end
puts "3. scan's %n on \"$line\": $end, string length: [string length $line]"
puts "4. Nanoseconds to whole picoseconds:"
foreach text {2.450 -0.160 10 -0.000} {
    puts "   $text -> [report::picoseconds 1 slack $text]"
}
puts "   -0.120 + -0.050 as reals: [expr {-0.120 + -0.050}], as picoseconds: [expr {-120 + -50}]"
set paths [report::parse block_a.txt]
puts "5. Path 1 as a dict: [lindex $paths 0]"
puts "6. Per clock:"
dict for {clock stats} [report::summarise $paths] {
    puts "   $clock: $stats"
}
The pieces of the parser on lines of the report Python · walk_session.py
from session import run

# The pieces of the parser, line by line: the path regexp, scan, the picoseconds, and the dicts of block_a.
run("""
cd report
tclsh9.0 walkthrough.tcl
""")

Output

$ cd report
$ tclsh9.0 walkthrough.tcl
1. Which lines start a path:
   "path 12"       1
   "path 12 x"     0
   "Path 12"       0
   "path twelve"   0
2. Words that scan finds in a field line:
   "  required  2.450"  2
   "  from a/q extra"   3
   "  slack"            1
3. scan's %n on "  from mémoire/bank_0": 22, string length: 21
4. Nanoseconds to whole picoseconds:
   2.450 -> 2450
   -0.160 -> -160
   10 -> 10000
   -0.000 -> 0
   -0.120 + -0.050 as reals: -0.16999999999999998, as picoseconds: -170
5. Path 1 as a dict: id 1 line 2 from core/acc_reg_3 to core/out_reg_3 clock core required 2450 arrival 2610 slack -160
6. Per clock:
   core: paths 3 failing 2 wns -160 tns -215
   io: paths 1 failing 0 wns 420 tns 0
   mem: paths 1 failing 1 wns -60 tns -60

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 walk_session.py

Step through how the first path of block_a.txt turns into a dict:

Interactive · step by step Path 1 of block_a.txt, read one line at a time Step through the lines of the first path and watch the path's dict grow until the next path line closes it.
Path 1 of block_a.txt, read one line at a time, step 6 of 6. Showing The line just read (line 9: path 2), The open path (id 2 line 9), The checks (numbers, 3 decimals), Finished paths (1: path 1). In focus: Finished paths.addswhen closedThe line just readline 9: path 2The open pathid 2 line 9The checksnumbers, 3 decimalsFinished paths1: path 1

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A comment. Line 1 starts with #, so it is skipped. Comments and blank lines may appear anywhere in a report.
  2. path 1. Line 2 matches ^path\s+[0-9]+\s*$. It opens a path: a dict with its id, 1, and the line it starts on, 2.
  3. from and to. Lines 3 and 4 are indented, and scan finds two words in each. from and to are fields of the six, not given yet, so both are added as text.
  4. clock. Line 5 adds clock core. The clock name is text too; it will decide which row of the summary this path counts towards.
  5. Three times. required 2.450, arrival 2.610 and slack -0.160 are times. Each must be a number with at most three decimals, and each is stored as whole picoseconds: 2450, 2610 and -160.
  6. The next path closes it. Line 9, path 2, closes path 1. All six fields are there and -160 is 2450 - 2610, so the dict goes into the list of finished paths.

Times become whole picoseconds as soon as they are read. A time with at most three decimals in nanoseconds is a whole number of picoseconds, so round($text * 1000) gives it exactly, the rounding removing the tiny binary error of the product, and every later sum is a sum of integers, exact at any size. The fourth part of the walkthrough shows why: -0.120 + -0.050 as reals printed -0.16999999999999998, where the picoseconds gave −170. -0.000 became 0, so it never counts as failing.

Checking the end of a line with scan's %n

The obvious check for text after the value is scan $text {%s %s%n} key value end, comparing end with the length of the line. The scan manual says %n stores the number of characters scanned, but on Tcl 9.0.4 it counted 22 for the 21-character line from mémoire/bank_0: it counted UTF-8 bytes. With that check every ASCII report passes and a name with é is refused. Counting words with a third %s, as the parser does, gives the same answer for every character.

Regex Tester Try the path pattern ^path\s+[0-9]+\s*$ on path 12, path 12 x and Path 12.

Checkpoint

Check: Reading lines with regexp and scan, in picoseconds

  1. Question 1 of 2 What does scan $text {%s %s %s} key value extra return for the line ' from a/q extra'?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 3, so the parser refuses the line

    Two words is a field line, three means text after the value, one means a missing value; the walkthrough printed 2, 3 and 1.

  2. Question 2 of 2 Why does the parser turn -0.160 into -160 when it reads it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Three decimals in ns are whole picoseconds, and sums of whole numbers are exact

    Convert once, on the way in, and every later sum and comparison is integer arithmetic.

A dict per clock and two stable sorts make the table

report::summarise keeps one dict entry per clock, created when the clock’s first path arrives, and updates it with dict update, which the dict manual says maps the entry to a variable for the body and writes it back when the body ends. Each path adds one to paths, one to failing when its slack is below 0, its slack into tns when negative, and lowers wns when it is the worst so far. Step through the five paths of block_a.txt:

Interactive · step by step Five paths gathered per clock, then sorted Step through the five paths of block_a.txt. Each box reads paths, failing, worst slack and total negative slack, in ps; then come the two sorts.
Five paths gathered per clock, then sorted, step 7 of 7. Showing core (3 · 2 · -160 · -215), io (1 · 0 · 420 · 0), mem (1 · 1 · -60 · -60), Table order (core, mem, io). In focus: Table order.core3 · 2 · -160 · -215io1 · 0 · 420 · 0mem1 · 1 · -60 · -60Table ordercore, mem, io

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Path 1. Path 1 is core with a slack of -160 ps. core is new, so it starts with this path: 1 path, 1 failing, a worst slack of -160 and a total of -160.
  2. Path 2. Path 2 is core at +55 ps. It counts as a path, but not as failing; the worst slack stays -160 and the total stays -160.
  3. Path 3. Path 3 is io at +420 ps: 1 path, none failing, a worst slack of 420, which is positive, and a total negative slack of 0.
  4. Path 4. Path 4 is core at -55 ps: a second failing path. The worst slack stays -160, and the total becomes -160 + -55 = -215.
  5. Path 5. Path 5 is mem at -60 ps, the first path of mem: 1 path, 1 failing, -60 and -60.
  6. Sort by name. table sorts the rows by clock name first: core, io, mem. It then sorts them by worst slack, and lsort is stable, so rows with the same worst slack keep this name order.
  7. Then by worst slack. By worst slack the order is core at -160, mem at -60 and io at 420, the order of the printed table.

The table needs two keys: worst slack first, clock name for ties. lsort sorts by one key at a time, and its manual says it is a stable merge sort, so report::table sorts by name first and then by worst slack, and rows with the same worst slack keep their name order. Here is the program on the sample, and on three inputs it must refuse:

The summary of block_a.txt, then three inputs refused with exit status 2 Python · run_session.py
from session import run

# The summariser on the sample report, then on three inputs it must refuse, each with exit status 2.
run("""
cd report
tclsh9.0 summarise.tcl block_a.txt
!tclsh9.0 summarise.tcl missing_slack.txt
!tclsh9.0 summarise.tcl bad_bytes.txt
!tclsh9.0 summarise.tcl
""")

Output

$ cd report
$ tclsh9.0 summarise.tcl block_a.txt
clock      paths failing    wns_ns    tns_ns
core           3       2    -0.160    -0.215
mem            1       1    -0.060    -0.060
io             1       0     0.420     0.000
$ tclsh9.0 summarise.tcl missing_slack.txt
line 1: path 1 has no slack
[exit status 2]
$ tclsh9.0 summarise.tcl bad_bytes.txt
line 4: the bytes are not valid UTF-8
[exit status 2]
$ tclsh9.0 summarise.tcl
usage: tclsh9.0 summarise.tcl report.txt
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 run_session.py

core has 3 paths, 2 of them failing, a worst slack of −0.160 and a total of −0.215 ns, the sum of −160 and −55 ps. format "%-10s" pads the clock name to 10 characters; Tcl counts characters there, so a name with é would line up too. report::ns writes the picoseconds back with exactly three decimals, from the whole number and its remainder, so no rounding happens on the way out either.

Key idea

Keep every time as whole picoseconds from the moment it is read, and sort by one key at a time with a stable sort: then the sums are exact and the order of every tie is decided by the specification, not by chance.

Checkpoint

Check: A dict per clock and a two-key sort

  1. Question 1 of 2 In which order did the summary of block_a.txt list its clocks?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: core, mem, io: by worst slack, -0.160, -0.060, 0.420

    The run printed core, mem and io: worst slack first, then by name for ties.

  2. Question 2 of 2 report::table runs lsort -index 0 (by name) and then lsort -integer -index 3 (by worst slack). Why in that order?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: lsort is stable, so the second sort keeps the name order among rows with the same worst slack

    Sort by the least important key first and by the most important last, with a stable sort, and ties follow the earlier key.

Bad bytes and bad lines end the run with exit status 2

A report is read in one go, chan read after chan configure $chan -encoding utf-8 -profile strict. The chan manual says that with the strict profile a read of invalid data raises an error with the POSIX code EILSEQ, and that the text decoded before the bad bytes comes with the error, in its -data entry; strict is also Tcl 9’s default for channels. report::lines counts the lines of that text to name the line of the bad byte, and the run above printed line 4: the bytes are not valid UTF-8 for bad_bytes.txt, whose fourth line holds the byte 0xFF. The default -translation auto turns CR LF into LF on input, so a report saved on Windows reads the same.

Every other problem is one call of report::fail, which raises an error with the code REPORT INPUT and a message that starts with the line number. Only report::main catches it, with try … trap {REPORT INPUT}, writes the message to standard error: The second output stream every program has, meant for error messages. Kept apart from standard output, it lets a report be saved to a file while its errors still reach the terminal or a log. More in “Project: a marks report in Object Pascal” and returns 2, the exit status: The whole number a program returns to whatever started it: 0 for success by convention, anything else for a failure. Shells and build pipelines read it to decide what happens next. More in “Project: a marks report in Object Pascal” that a shell or a build reads. An error with any other code is a bug, not bad input, and is not caught: tclsh prints its trace and exits with status 1. Each kind of bad input has its own message, and every one is tested:

Input Message on standard error
a byte that is not UTF-8 line 4: the bytes are not valid UTF-8
a path without one of its fields line 1: path 1 has no slack
a field given twice line 8: path 1 gives clock twice
a value with four decimals line 3: slack must be a number of ns with at most 3 decimals, got "2.4501"
a slack that is not required − arrival line 1: path 4: slack is not required - arrival
an empty file line 1: the report has no paths
Text Encoding Converter & Mojibake Fixer Open a report that another tool saved as Windows-1252 or Latin-1 and save it as UTF-8, which the summariser reads.

Checkpoint

Check: Strict UTF-8, line numbers and exit status 2

  1. Question 1 of 2 A report's first three lines are valid UTF-8 and its fourth holds the byte 0xFF. Which line number does the summariser report?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 4 the line number

    The text decoded before the bad byte comes with the EILSEQ error; it holds three full lines and the start of the fourth, so splitting it at newlines gives 4 pieces. The run printed line 4: the bytes are not valid UTF-8.

  2. Question 2 of 2 missing_slack.txt has a path without its slack. What does the summariser do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It prints line 1: path 1 has no slack on standard error and exits with status 2

    Status 2 means bad input, so a script or a build can tell it from a bug, which keeps tclsh's status 1.

Twenty-five tcltest cases pin the summariser down

tcltest is Tcl’s own test package. A test is test name description -body {…} -result {…}, and the tcltest manual lists options that this suite uses: -returnCodes error for a test that expects an error, -match glob for a result compared with a pattern, -setup for work that must not be timed, and makeFile, which writes a temporary file and lets the next cleanupTests remove it. The suite sources summarise.tcl and tests its procedures directly, and two tests run the program as a user would, with exec, reading the exit status from the CHILDSTATUS error code, as the exec manual’s examples do:

summarise.test: the 25 cases TEST · report/summarise.test
# summarise.test: the project's tests, run with tclsh9.0 summarise.test. All 25 are public.
package require tcltest 2.5
namespace import ::tcltest::*
source [file join [file dirname [info script]] summarise.tcl]

# One path as report text, with any field left out or changed.
proc path_text {id args} {
    set fields [dict create from a/q to b/d clock core required 2.450 arrival 2.610 slack -0.160]
    foreach {key value} $args {
        if {$value eq "-"} { dict unset fields $key } else { dict set fields $key $value }
    }
    set lines [list "path $id"]
    dict for {key value} $fields { lappend lines "  $key $value" }
    return [join $lines \n]
}
# The summary table of a report given as text, or the input error it raises.
proc summary_of {text} {
    set file [makeFile $text report.txt]
    try {
        return [report::table [report::summarise [report::parse $file]]]
    } trap {REPORT INPUT} {message} {
        return "error: $message"
    }
}
# Runs the program on a file, as a user would: its exit status and what it printed.
proc run_program {file} {
    set script [file join [file dirname [info script]] summarise.tcl]
    set status [catch {exec [info nameofexecutable] $script $file} output options]
    if {$status} {
        set status [lindex [dict get $options -errorcode] 2]
    }
    return [list $status $output]
}

test parse-1.1 {one path becomes a dict of whole picoseconds} -body {
    set path [lindex [report::parse [makeFile [path_text 7] one.txt]] 0]
    list [dict get $path id] [dict get $path required] [dict get $path arrival] [dict get $path slack]
} -result {7 2450 2610 -160}
test parse-1.2 {fields may come in any order} -body {
    summary_of "path 1\n  slack -0.160\n  clock core\n  arrival 2.610\n  required 2.450\n  to b/d\n  from a/q"
} -result [join {{clock      paths failing    wns_ns    tns_ns} {core           1       1    -0.160    -0.160}} \n]
test parse-1.3 {comments and blank lines are ignored} -body {
    summary_of "# a comment\n\n[path_text 1]\n\n   # an indented comment"
} -match glob -result {*core           1       1    -0.160    -0.160}
test parse-1.4 {CR LF line ends read like LF} -body {
    summary_of [string map {\n \r\n} [path_text 1]]
} -match glob -result {*core           1       1    -0.160    -0.160}
test parse-1.5 {names keep their Unicode characters} -body {
    set path [lindex [report::parse [makeFile [path_text 1 from mémoire/bank_0 clock cœur] u.txt]] 0]
    list [dict get $path from] [dict get $path clock] [string length [dict get $path clock]]
} -result {mémoire/bank_0 cœur 4}

test ps-1.1 {a negative slack in ns} -body { report::picoseconds 1 slack -0.160 } -result -160
test ps-1.2 {two decimals} -body { report::picoseconds 1 required 2.45 } -result 2450
test ps-1.3 {no decimals} -body { report::picoseconds 1 required 10 } -result 10000
test ps-1.4 {negative zero is zero} -body { report::picoseconds 1 slack -0.000 } -result 0
test ps-1.5 {four decimals are refused} -body {
    report::picoseconds 3 slack 2.4501
} -returnCodes error -result {line 3: slack must be a number of ns with at most 3 decimals, got "2.4501"}
test ps-1.6 {an exponent is refused} -body {
    report::picoseconds 4 arrival 1e3
} -returnCodes error -result {line 4: arrival must be a number of ns with at most 3 decimals, got "1e3"}

test error-1.1 {a missing field names the path and its line} -body {
    summary_of "# report\n[path_text 1 slack -]"
} -result {error: line 2: path 1 has no slack}
test error-1.2 {a field given twice} -body {
    summary_of "[path_text 1]\n  clock io"
} -result {error: line 8: path 1 gives clock twice}
test error-1.3 {an unknown field} -body {
    summary_of "path 1\n  from a/q\n  slak -0.160"
} -result {error: line 3: unknown field "slak"}
test error-1.4 {a field before the first path line} -body {
    summary_of "  clock core\n[path_text 1]"
} -result {error: line 1: the field "clock" comes before the first path line}
test error-1.5 {the slack must be required - arrival} -body {
    summary_of [path_text 4 slack -0.150]
} -result {error: line 1: path 4: slack is not required - arrival}
test error-1.6 {a line that is neither a path nor a field} -body {
    summary_of "[path_text 1]\nslack -0.160"
} -result {error: line 8: expected "path <number>" or an indented field, got "slack -0.160"}
test error-1.7 {an empty file has no paths} -body {
    set file [makeFile {} empty.txt]
    writeFile $file {}
    try { report::parse $file } trap {REPORT INPUT} {message} { set message }
} -result {line 1: the report has no paths}
test error-1.8 {a byte that is not UTF-8 is reported on its line} -body {
    set file [makeFile {} bytes.txt]
    writeFile $file binary "path 1\n  from a/q\n  to b/d\n  clock c\xffre\n"
    try { report::parse $file } trap {REPORT INPUT} {message} { set message }
} -result {line 4: the bytes are not valid UTF-8}

test summary-1.1 {block_a: worst slack first, then clock name} -body {
    report::table [report::summarise [report::parse [file join [file dirname [info script]] block_a.txt]]]
} -result [join {
    {clock      paths failing    wns_ns    tns_ns}
    {core           3       2    -0.160    -0.215}
    {mem            1       1    -0.060    -0.060}
    {io             1       0     0.420     0.000}
} \n]
test summary-1.2 {equal worst slacks are sorted by clock name} -body {
    summary_of "[path_text 1 clock io]\n[path_text 2 clock core]"
} -result [join {
    {clock      paths failing    wns_ns    tns_ns}
    {core           1       1    -0.160    -0.160}
    {io             1       1    -0.160    -0.160}
} \n]
test summary-1.3 {a clock without failing paths has a positive wns and no tns} -body {
    summary_of [path_text 1 required 2.700 arrival 2.610 slack 0.090]
} -match glob -result {*core           1       0     0.090     0.000}

test program-1.1 {a good report: exit status 0 and the table} -body {
    run_program [file join [file dirname [info script]] block_a.txt]
} -match glob -result {0 {clock *io             1       0     0.420     0.000}}
test program-1.2 {a bad report: exit status 2 and one line on standard error} -body {
    run_program [makeFile [path_text 1 arrival -] bad.txt]
} -result {2 {line 1: path 1 has no arrival}}

test speed-1.1 {10,000 paths in under 1 s, the fastest of three runs} -setup {
    set lines {}
    for {set i 1} {$i <= 10000} {incr i} {
        lappend lines [path_text $i clock [lindex {core io mem} [expr {$i % 3}]]]
    }
    set file [makeFile [join $lines \n] big.txt]
} -body {
    set best Inf
    for {set run 0} {$run < 3} {incr run} {
        set start [clock microseconds]
        report::summarise [report::parse $file]
        set best [expr {min($best, [clock microseconds] - $start)}]
    }
    expr {$best < 1000000}
} -result 1

set failed $::tcltest::numTests(Failed)
cleanupTests
exit [expr {$failed > 0}]
The 25 cases, run with tcltest Python · tests_session.py
from session import run

# The project's 25 tests, with tcltest.
run("""
cd report
tclsh9.0 summarise.test
""")

Output

$ cd report
$ tclsh9.0 summarise.test
summarise.test:	Total	25	Passed	25	Skipped	0	Failed	0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tests_session.py

The cases fall into four groups: parsing, the conversion to picoseconds, the error messages and the summary, plus the two black-box test: A test that runs the whole program as a user would, with an input file, and compares only what comes out, standard output, standard error and the exit status, with expected files, without looking at the code inside. More in “Project: a marks report in Object Pascal” and a time budget. The Unicode case, parse-1.5, is the one that would catch the %n trap of the second section: a check built on %n passes every ASCII case and refuses the line with mémoire/bank_0, whose count was 22 for 21 characters. The speed test builds 10,000 paths in -setup and asserts that the fastest of three runs of parsing and summarising takes under 1 s; taking the fastest of three keeps a short burst of other work on the same computer from failing the test. This session times the same work on its own:

10,000 paths, the fastest of three runs Python · speed_session.py
from session import run

# A report of 10,000 paths, parsed and summarised three times; the fastest run is printed.
run("""
cd report
tclsh9.0 timing.tcl
""")

Output

$ cd report
$ tclsh9.0 timing.tcl
10,000 paths: 164 ms, the fastest of three runs
clock      paths failing    wns_ns    tns_ns
core        3333    1444    -0.584  -297.756
io          3334    1450    -0.584  -299.796
mem         3333    1445    -0.584  -297.748

This output changes from run to run: the time depends on the computer and changes from run to run

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 speed_session.py

The time on the Mac that recorded the run is in its output; the table under it is the same on every run. Write new expected results from the specification, never by copying what the program printed: then the tests define the summariser, and a second implementation, such as the one you write next, becomes a test oracle: A second, trusted way to work out the expected results of tests, such as a short program written straight from the specification, used to check an implementation on many inputs. for it.

Checkpoint

Check: The tcltest suite

  1. Question 1 of 2 Which tcltest options make ps-1.5 pass when report::picoseconds raises an error with the message it expects?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: -returnCodes error with the message as -result

    The suite uses -returnCodes error for every test that checks a refusal at the procedure level.

  2. Question 2 of 2 speed-1.1 times three runs on 10,000 paths and checks the fastest against 1 s. Why the fastest, and not the average?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Other programs on the computer can only make a run slower, so the fastest run measures the summariser best

    The file is built in -setup, outside the timing, and only the parse and the summary are timed.

Practise: write the numbers a second time, in Python

In the lab you write the two halves of the summariser’s arithmetic again from the specification, in Python: first the conversion between nanosecond text and whole picoseconds, then the summary per clock with its two-key order. Their sample tests replay block_a.txt, the walkthrough and the error rules, so passing code agrees with the Tcl program.

Code lab · 2 steps

Check the summariser's numbers with a second implementation

Convert report times to whole picoseconds and back, then summarise the slack of each clock, worst first.

  1. Turn nanoseconds into whole picoseconds and back To do
  2. Summarise the slack of each clock, worst first To do

Exercise · Easy · Python

Turn nanoseconds into whole picoseconds and back

The summariser keeps every time in whole picoseconds, so that sums of slacks are exact. Write the two conversions of the project's specification in picoseconds.py:

  • picoseconds(text) turns a value of the report, in nanoseconds, into an int of picoseconds: an optional minus sign, one or more digits, and at most three decimals after a point. "-0.160" is -160, "2.45" is 2450, "10" is 10000 and "-0.000" is 0. Anything else raises ValueError with the summariser's words: must be a number of ns with at most 3 decimals, got "2.4501".
  • ns_text(ps) writes picoseconds back as the table prints them, with exactly three decimals: -160 is "-0.160", 2450 is "2.450", 0 is "0.000" and -297756 is "-297.756".

The sample tests use the values of the lesson's walkthrough and tables. Work with whole numbers: the walkthrough's sum of -0.120 and -0.050 as floats printed -0.16999999999999998, where whole picoseconds give -170.

Starter code · picoseconds.py

def picoseconds(text):
    """'-0.160' (ns) -> -160 (whole picoseconds)."""
    # Replace this with your code: a float loses digits and accepts text the report must refuse.
    return int(float(text) * 1000)


def ns_text(ps):
    """-160 -> '-0.160', with exactly three decimals."""
    return str(ps / 1000)
The sample tests · test_picoseconds.py
from picoseconds import ns_text, picoseconds


def _refusal(text):
    """The message of the ValueError that picoseconds raises for text, or None."""
    try:
        picoseconds(text)
    except ValueError as error:
        return str(error)
    return None


def test_values_of_the_walkthrough():
    """the four conversions that walkthrough.tcl printed"""
    assert picoseconds("2.450") == 2450
    assert picoseconds("-0.160") == -160
    assert picoseconds("10") == 10000
    assert picoseconds("-0.000") == 0


def test_fewer_decimals_and_leading_zeros():
    """one or two decimals, and digits with leading zeros, are whole picoseconds too"""
    assert picoseconds("2.45") == 2450
    assert picoseconds("0.1") == 100
    assert picoseconds("007.5") == 7500
    assert picoseconds("-12.005") == -12005


def test_values_the_report_must_refuse():
    """four decimals, an exponent, a plus sign, a comma or spaces are refused"""
    for text in ["2.4501", "1e3", "+2.450", "2,450", " 2.450", "", "-", "2."]:
        assert _refusal(text) == f'must be a number of ns with at most 3 decimals, got "{text}"', text


def test_ns_text_of_the_tables():
    """the values that the lesson's tables printed"""
    assert ns_text(-160) == "-0.160"
    assert ns_text(2450) == "2.450"
    assert ns_text(0) == "0.000"
    assert ns_text(-297756) == "-297.756"
    assert ns_text(420) == "0.420"


def test_round_trip():
    """writing a value and reading it back gives the same picoseconds"""
    for ps in [-215, -60, 55, 1, -1, 999, 1000, 123456]:
        assert picoseconds(ns_text(ps)) == ps
A hint

For picoseconds, check the whole text with re.fullmatch(r"-?[0-9]+(\.[0-9]{1,3})?", text) first. Then split at the point, pad the decimals with zeros to three digits, and build the number from the two parts with whole numbers. For ns_text, divmod(abs(ps), 1000) gives the part before the point and the three digits after it.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · Python

Summarise the slack of each clock, worst first

This is the summariser's second half, written again from the specification: a second implementation that the Tcl program's results can be checked against. Write summarise(paths) in summary.py. Each path is a dict with at least "clock" (a name) and "slack" (whole picoseconds, as the first step of this lab returns them). Return one tuple per clock, (clock, paths, failing, wns, tns):

  • paths counts the clock's paths, and failing those whose slack is below 0;
  • wns, the worst slack, is the smallest slack of the clock's paths, even when it is positive;
  • tns, the total negative slack, is the sum of the slacks below 0, and 0 when none fails.

Sort the tuples by wns, smallest first, and clocks with the same wns by name. For the lesson's block_a.txt that is core, mem, io, as the Tcl table printed. An empty list of paths gives an empty list.

Starter code · summary.py

def summarise(paths):
    """One (clock, paths, failing, wns, tns) tuple per clock, worst slack first, then by name."""
    # Replace this with your code: it sorts by name and adds every slack, positive ones included.
    clocks = {}
    for path in paths:
        clocks.setdefault(path["clock"], []).append(path["slack"])
    return [(clock, len(s), 0, min(s), sum(s)) for clock, s in sorted(clocks.items())]
The sample tests · test_summary.py
from summary import summarise

BLOCK_A = [
    {"clock": "core", "slack": -160},
    {"clock": "core", "slack": 55},
    {"clock": "io", "slack": 420},
    {"clock": "core", "slack": -55},
    {"clock": "mem", "slack": -60},
]


def test_block_a_from_the_lesson():
    """the table that summarise.tcl printed for block_a.txt"""
    assert summarise(BLOCK_A) == [
        ("core", 3, 2, -160, -215),
        ("mem", 1, 1, -60, -60),
        ("io", 1, 0, 420, 0),
    ]


def test_equal_worst_slack_sorted_by_name():
    """clocks with the same worst slack come in name order"""
    paths = [{"clock": "io", "slack": -160}, {"clock": "core", "slack": -160}]
    assert summarise(paths) == [("core", 1, 1, -160, -160), ("io", 1, 1, -160, -160)]


def test_a_clock_that_meets_timing():
    """no failing path: failing 0, tns 0, and the worst slack stays positive"""
    paths = [{"clock": "core", "slack": 90}, {"clock": "core", "slack": 30}]
    assert summarise(paths) == [("core", 2, 0, 30, 0)]


def test_zero_slack_is_not_failing():
    """a slack of exactly 0, as -0.000 became, does not fail"""
    assert summarise([{"clock": "core", "slack": 0}]) == [("core", 1, 0, 0, 0)]


def test_no_paths():
    """no paths, no rows"""
    assert summarise([]) == []
A hint

Collect the slacks of each clock in a dict of lists first, then compute the four numbers of each clock from its list. sorted(rows, key=lambda row: (row[3], row[0])) sorts by the worst slack and then by the name in one step.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then test yourself on the five parts of the project, from its format to its tests. Your least certain part comes up first, and each choice is explained once you pick it:

Practice that adapts to you

Practise this lesson

14 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 What does a slack of -0.160 ns mean for a path?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: The signal arrives 0.160 ns after it is required: the path fails timing

    Path 1 of block_a.txt: required 2.450, arrival 2.610, slack 2.450 - 2.610 = -0.160 ns.

  2. Question 2 of 14 A path gives required 2.450, arrival 2.600 and slack -0.160. What does the summariser do?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: It refuses the report: the slack is not required - arrival, which would be -0.150

    Refusing with line N: path 1: slack is not required - arrival tells the report's author exactly what to fix.

  3. Question 3 of 14 Two people write the summariser from a loose description. Which detail of the specification most often makes their outputs differ?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: The order of clocks with the same worst slack

    Every rule that decides output needs a test: summary-1.2 exists for the tie order alone.

  4. Question 4 of 14 Which line does ^path\s+[0-9]+\s*$ accept?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: path 12

    Anchors make the whole line match, so a path line cannot carry anything else.

  5. Question 5 of 14 On Tcl 9.0.4, scan's %n counted 22 for the 21-character line ' from mémoire/bank_0'. What does that mean for the end-of-line check?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: It counts UTF-8 bytes, so a check against string length refuses every line with a non-ASCII character

    Count words with a third %s instead, which gives the same answer for every character, and keep a Unicode test.

  6. Question 6 of 14 A path's slack is written -0.000. How many picoseconds does the parser store?

    Type a number.

    Show the answer: Question 6 of 14

    Answer: 0 ps

    round(-0.000 * 1000) is 0, so the path does not count as failing, as ps-1.4 and the walkthrough show.

  7. Question 7 of 14 What does dict update clocks $clock stats {…} do in report::summarise?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: It puts the clock's entry into the variable stats for the body and writes it back when the body ends

    Inside the body, dict incr stats paths and the other changes act on one clock's numbers.

  8. Question 8 of 14 Before sorting, in which order are the clocks of block_a.txt in the dict that report::summarise returns?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: core, io, mem: the order their first paths appeared

    The walkthrough printed core, then io, then mem; the table then sorted them into core, mem, io.

  9. Question 9 of 14 In Python, which call orders the rows like report::table, worst slack first and then by name?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: sorted(rows, key=lambda r: (r.wns, r.clock))

    One sort with a tuple key, or two stable sorts from the last key to the first: the same order either way.

  10. Question 10 of 14 What does a Tcl 9 channel do by default when it reads bytes that are not valid in its encoding?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: It raises an error with the POSIX code EILSEQ

    bad_bytes.txt stopped with line 4: the bytes are not valid UTF-8, which the summariser built from that error.

  11. Question 11 of 14 A build script runs the summariser. How can it tell a bad report from a bug in the summariser?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: By the exit status: 2 for bad input, 1 for an uncaught error

    An error code of its own for input problems is what lets main catch exactly those and nothing else.

  12. Question 12 of 14 Why does report::fail raise an error with the code REPORT INPUT instead of printing the message and calling exit 2 itself?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: So tests can catch it and check the message, and only main decides to write and exit

    Raise in the parser, decide in main: the same procedures then serve the program and its tests.

  13. Question 13 of 14 How do the two program tests run the summariser?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: With exec on tclsh and summarise.tcl, reading the exit status from the CHILDSTATUS error code

    Most tests call procedures; two black-box tests check the program from outside, as a build would run it.

  14. Question 14 of 14 You change the format of the table. Where should the new expected results of summary-1.1 come from?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: From the specification, written by hand, then compared with what the program prints

    The tests define the program; a second implementation checks both against the specification.

Interview questions

Warm-up (fresher to mid-level): Why does the summariser keep slack in whole picoseconds instead of in nanoseconds as reals?

Because sums of reals are not exact, and the total negative slack is a sum. A time such as 0.120 ns has no exact binary form, so adding reals collects tiny errors: in the walkthrough, -0.120 + -0.050 printed -0.16999999999999998. Over 10,000 paths such errors grow, and a total printed with three decimals can come out one digit off, or a value that should be exactly zero can come out as a tiny negative number and count as failing. The report gives times with at most three decimals in nanoseconds, so each one is a whole number of picoseconds: the parser converts once, every sum and comparison after that is integer arithmetic and exact, and report::ns writes the result back with exactly three decimals. The same habit fits money in paise or cents.

Two longer questions about tools that read reports like this one:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    The summariser has worked for months. A team switches to a new tool that writes the same report format, and now one clock's total negative slack is wrong by a few picoseconds while every test passes. How do you find the cause and keep it from coming back?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    You inherit a Tcl report parser with no tests and must change its output format next week. Which tests do you write first, how do you choose their inputs and expected results, and how do you keep the suite fast and reliable?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is the slack of a timing path, and what does a negative one mean?

    Required time minus arrival time. A negative slack means the signal arrives too late, so the path fails timing.

  2. How does the summariser define a clock's worst slack and total negative slack?

    Worst slack: the smallest slack of its paths, even when positive. Total negative slack: the sum of the negative slacks, 0 if none fails. core's were −0.160 and −0.215 ns.

  3. How does the parser check that a field line holds exactly two words?

    scan $text {%s %s %s} key value extra returns how many words it filled: 2 is a field, 3 means text after the value, and 1 a value that is missing.

  4. Why is scan's %n no safe way to find the end of a line on Tcl 9.0.4?

    It counted UTF-8 bytes, 22 for a 21-character line holding mémoire. A check built on it passes every ASCII report and refuses names with letters such as é.

  5. Why are all times turned into whole picoseconds as soon as they are read?

    A time of at most three decimals in ns is a whole number of ps, so every sum after that is exact. As reals, −0.120 + −0.050 printed −0.16999999999999998.

  6. How does the summariser order its rows by worst slack, with ties by clock name?

    It sorts twice, first by name and then by worst slack. lsort is a stable merge sort, so rows with equal worst slack keep the name order of the first sort.

  7. How does the summariser name the line that holds a byte which is not UTF-8?

    Its strict UTF-8 read raises EILSEQ and returns the text decoded before the bad byte in -data. Counting the lines of that text gave line 4 for bad_bytes.txt.

  8. Why does the speed test time the fastest of three runs over 10,000 paths?

    So that a brief burst of other work on the same computer cannot fail the test. The budget is under 1 s to parse and summarise them.

Key takeaways

  • A specification that fixes the format, the two summary numbers, the order of ties and every error message, each with its test, is what lets the summariser be checked to the byte.
  • regexp with anchors tells path lines apart and scan with three %s finds exactly two words in a field line; on Tcl 9.0.4, %n counted UTF-8 bytes, so it is not a safe end-of-line check for Unicode text.
  • Times become whole picoseconds as they are read, so sums such as −160 + −55 = −215 are exact, where reals printed −0.16999999999999998 for −0.120 + −0.050.
  • A dict per clock gathers paths, failing paths, the worst slack and the total; two stable lsort calls order the table by worst slack and then by name.
  • A strict UTF-8 read reports the line of a bad byte, every input error is line N: reason on standard error with exit status 2, and 25 tcltest cases, Unicode and a 1 s budget for 10,000 paths included, pin all of it down.

Terms in this lesson

slack
In a timing report, the time a path has to spare: its required time minus its arrival time. A negative slack means the signal arrives too late, so the path fails timing.
total negative slack (TNS)
The sum of the negative slacks of a clock's paths, and 0 when none fails. It measures the work left across all failing paths, where the worst negative slack shows only the worst one.
worst negative slack (WNS)
The smallest slack among a clock's paths, the path most in need of repair. The summariser of this course prints it for every clock, as a positive number when no path fails.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.