From 83ab5fb220a1d8f402c89c8eadad3aa203018f0e Mon Sep 17 00:00:00 2001 From: DanieCuevas <43822444+DanielCuevas1208@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:05:26 -0700 Subject: [PATCH] feat: extend gatework --- .github/workflows/ci.yml | 4 ++ CHANGELOG.md | 17 +++++ CONTRIBUTING.md | 2 + README.md | 57 +++++++++++++-- app/Main.hs | 89 +++++++++++++++-------- fixtures/adder.golden.vcd | 2 +- fixtures/assert.golden.vcd | 2 +- fixtures/bus.golden.vcd | 2 +- fixtures/counter.golden.report | 11 +++ fixtures/counter.golden.vcd | 2 +- fixtures/gates.golden.vcd | 2 +- fixtures/haddader.golden.vcd | 2 +- fixtures/hcounter.golden.vcd | 2 +- fixtures/libadder.golden.vcd | 2 +- fixtures/reg2.golden.vcd | 2 +- fixtures/register.golden.vcd | 2 +- fixtures/reset.golden.vcd | 2 +- fixtures/shared.golden.vcd | 2 +- fixtures/tristate.golden.vcd | 2 +- fixtures/unknown.golden.vcd | 2 +- gatework.cabal | 5 +- src/Gatework/Report.hs | 37 ++++++++++ test/Spec.hs | 126 ++++++++++++++++++++++++++++++++- 23 files changed, 326 insertions(+), 50 deletions(-) create mode 100644 fixtures/counter.golden.report create mode 100644 src/Gatework/Report.hs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b71d68d..3be5a57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,10 @@ jobs: run: cabal run gatework -- --netlist fixtures/counter.net --duration 8 --output /tmp/counter.vcd - name: Compare the counter demo with the golden file run: diff fixtures/counter.golden.vcd /tmp/counter.vcd + - name: Run the waveform report demo + run: cabal run gatework -- report --netlist fixtures/counter.net --duration 8 --output /tmp/counter.report + - name: Compare the report demo with the golden file + run: diff fixtures/counter.golden.report /tmp/counter.report - name: Run the register demo run: cabal run gatework -- --netlist fixtures/register.net --duration 8 --output /tmp/register.vcd --set d=1 --at 2 d=0 --at 4 d=1 --at 6 d=0 - name: Compare the register demo with the golden file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4af858f..a63884f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to Gatework appear in this file. The format follows the Keep a Changelog convention. This project uses semantic versioning. +## [0.10.0.0] - 2026-08-03 + +### Added + +- Add the report command to print the signal values as a text table. +- Each row shows one change time and every signal value. +- Print the report to standard output without the --output option. +- Write the report to a file with the --output option. +- Add a counter report golden file. +- Add deterministic tests and a QuickCheck property for the report. +- Extend the CI workflow to run the report demo. + +### Changed + +- Bump the package version to 0.10.0.0. +- Update the golden VCD files to the new version string. + ## [0.9.0.0] - 2026-08-03 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a170ebf..92bf5d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,8 @@ The CI workflow runs these checks. - `cabal test --enable-tests` runs the test suite. - Every demo writes a VCD file. - Each demo output must match its golden file. +- The report demo writes a text table. +- The report output must match its golden file. Run the checks before you open a pull request. diff --git a/README.md b/README.md index 40134ca..9ce0bd3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Gatework is an event-driven digital logic simulator in Haskell. It parses a plain-text netlist, runs the circuit, and writes a VCD waveform file. +A report command prints the signal values as a text table. GTKWave and other waveform viewers can open the output. The simulator uses four logic values: low, high, unknown, and floating. Multi-bit buses use bracketed widths and slices. @@ -25,6 +26,7 @@ You can do these tasks: - Load a module library with the `--library` option. - Build a hierarchical adder from a module library file. - Open the VCD output in GTKWave and inspect the waveforms. +- Print a text waveform report with the report command. - Verify gate behavior with QuickCheck property tests. - Compare the counter waveform with a repository golden file. - Check netlist behavior with waveform assertions. @@ -45,12 +47,13 @@ You can do these tasks: ## Architecture -The project has four library modules. +The project has five library modules. | Module | Responsibility | | --- | --- | | `Gatework.Logic` | Defines logic values and gate functions | | `Gatework.Netlist` | Parses, validates, and flattens circuit files | +| `Gatework.Report` | Renders the waveform as a text table | | `Gatework.Simulator` | Schedules signal changes | | `Gatework.VCD` | Renders the waveform text | @@ -58,7 +61,8 @@ The data flow is: ```text library text -> parser -> library module table -netlist text -> parser -> module table -> flattened netlist -> event queue -> waveform recorder -> assertion check -> VCD +netlist text -> parser -> module table -> flattened netlist -> event queue -> waveform recorder -> VCD +waveform recorder -> text table (report command) ``` The parser validates names, gate arity, drivers, clocks, and references. @@ -76,6 +80,7 @@ An asserted reset forces flip-flop outputs to their initial values. The recorder keeps the initial value and every later transition. The assertion checker compares declared expectations with the waveform. The VCD writer uses stable signal order and stable identifiers. +The report writer prints one row per change time. The repository layout is: @@ -158,6 +163,37 @@ Its value sequence is 0, 1, 2, 3, and 4. | 5 | 0 | 0 | 1 | 1 | 3 | | 7 | 0 | 1 | 0 | 0 | 4 | +## Waveform report + +Print the counter as a text table. + +```powershell +cabal run gatework -- report --netlist fixtures/counter.net --duration 8 +``` + +The command prints this table: + +```text +time | q0 | q1 | q2 | q3 | clk | d0 | d1 | carry2 | d2 | carry3 | d3 +---- | -- | -- | -- | -- | --- | -- | -- | ------ | -- | ------ | -- + 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 + 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 + 2 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 + 3 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 + 4 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 + 5 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 + 6 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 + 7 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 + 8 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 +``` + +The table shows one row per change time. +Each column shows one signal. +Each cell shows the settled value at that time. +Use `--output FILE` to write the table to a file. +Without `--output`, the table prints to standard output. +The report uses the same options as the VCD command. + ## Ripple-carry adder Run three plus five with explicit input values. @@ -491,6 +527,9 @@ This excerpt shows the first timestamp: Each identifier is one signal. The header maps identifiers to signal names. +The file `fixtures/counter.golden.report` holds the counter report table. +It shows one row per change time. + The file `fixtures/register.golden.vcd` holds the complete register waveform. This excerpt shows the first two timestamps: @@ -845,12 +884,14 @@ Deterministic tests also cover undefined and floating values, tri-state buffers, Deterministic tests also cover bus declarations, bitwise gates, bit references, slices, bus registers, bus assertions, bus module ports, and error cases. Deterministic tests also cover multi-driver resolution, scheduled driver changes, flip-flop output exclusivity, the shared-bus fixture, and its golden output. Deterministic tests also cover module library loading, cross-library module references, duplicate module names, and library file validation. +Deterministic tests also cover the report command, its header order, its counter table, and its golden output. QuickCheck properties cover gate algebra, full adder correctness, scheduled input sampling, reset sampling, register width, and assertion soundness. QuickCheck properties also compare the hierarchical adder and counter with their flat versions. QuickCheck properties also cover the four-state model and the tri-state buffer truth table. QuickCheck properties also compare a bus circuit with a bitwise reference model. QuickCheck properties also compare the shared bus with a per-time resolution model. QuickCheck properties also compare a library adder with its flat version. +QuickCheck properties also compare the counter report with the simulated waveform. QuickCheck runs one hundred random cases for each property. The gate properties cover the complete truth table. @@ -868,13 +909,16 @@ The bus register property compares each register bit with a reference value. The bus hierarchy property shows that a bus module matches its flat circuit. The shared-bus property compares each resolved wire value with a per-time model. The library adder property compares a library netlist with the flat adder. +The counter report property compares each report cell with the simulated value. ## Test status All tests pass on GHC 9.6.7 with Cabal 3.14 in the bundled container. The CI workflow runs the same checks on Ubuntu with GHC 9.6.6. Golden tests compare the counter, register, reset, two-bit register, assertion, gate, hierarchical adder, library adder, hierarchical counter, adder, tri-state, undefined-state, bus, and shared-bus VCD text. +The golden report test compares the counter report table with its golden file. CI runs every demo and compares its output with the golden file. +CI runs the report demo and compares it with the report golden file. CI confirms that a missing library file stops the run. ## Limitations @@ -913,9 +957,13 @@ The CLI does not accept whole-bus values. One module declaration cannot live inside another. An instance output must connect to a declared signal. The dotted instance names are part of the VCD signal names. +The report prints one row per change time. +The report uses the settled value at each time. +The report prints every signal in the stable signal order. ## Roadmap +Release 0.10.0.0 completed the waveform report command. Release 0.9.0.0 completed module libraries. Release 0.8.0.0 completed multi-driver wire resolution. Release 0.7.0.0 completed multi-bit buses, bit references, slices, and bus module ports. @@ -927,9 +975,8 @@ Release 0.2.0.0 completed scheduled input transitions. Remaining work: -1. Add a waveform report command. -2. Add whole-bus input values on the command line. -3. Add multi-bit values in the VCD timeline. +1. Add whole-bus input values on the command line. +2. Add multi-bit values in the VCD timeline. ## License diff --git a/app/Main.hs b/app/Main.hs index f5d19a7..a7e02f7 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -3,6 +3,7 @@ module Main (main) where import Data.List (intercalate) import Gatework.Logic (Logic, logicChar, parseLogic) import Gatework.Netlist (netlistSignals, parseNetlistFileWithLibraries) +import Gatework.Report (renderReport) import Gatework.Simulator ( AssertionFailure (..) , Simulation @@ -14,52 +15,82 @@ import Gatework.Simulator import Gatework.VCD (writeVCD) import System.Environment (getArgs) import System.Exit (exitFailure) -import System.IO (hPutStrLn, stderr) +import System.IO (Handle, hPutStrLn, stderr, stdout) import Text.Read (readMaybe) data Options = Options { optionNetlist :: Maybe FilePath , optionDuration :: Integer , optionOutput :: FilePath + , optionOutputExplicit :: Bool , optionInputs :: [(String, Logic)] , optionScheduled :: [(Time, String, Logic)] , optionLibraries :: [FilePath] } +data Command = CommandRun Options | CommandReport Options + defaultOptions :: Options -defaultOptions = Options Nothing 16 "gatework.vcd" [] [] [] +defaultOptions = Options Nothing 16 "gatework.vcd" False [] [] [] main :: IO () main = do arguments <- getArgs - case parseOptions arguments of + case parseCommand arguments of Left message -> failWith message Right Nothing -> putStrLn usage - Right (Just options) -> run options - -run :: Options -> IO () -run options = case optionNetlist options of - Nothing -> failWith "--netlist is required" - Just path -> do - parsed <- parseNetlistFileWithLibraries (optionLibraries options) path - case parsed of - Left message -> failWith message - Right netlist -> - case simulateWithScheduledInputs netlist (optionInputs options) (optionScheduled options) (optionDuration options) of - Left message -> failWith message - Right simulation -> do - writeVCD (optionOutput options) simulation - putStrLn ("Wrote " ++ optionOutput options) - putStrLn ("Signals: " ++ show (length (netlistSignals netlist))) - putStrLn ("Duration: " ++ show (optionDuration options) ++ " time units") - reportAssertions simulation - -reportAssertions :: Simulation -> IO () -reportAssertions simulation = case simulationFailures simulation of + Right (Just command) -> run command + +parseCommand :: [String] -> Either String (Maybe Command) +parseCommand ("report" : rest) = fmap CommandReport <$> parseOptions rest +parseCommand arguments = fmap CommandRun <$> parseOptions arguments + +run :: Command -> IO () +run command = do + let options = commandOptions command + case optionNetlist options of + Nothing -> failWith "--netlist is required" + Just path -> do + parsed <- parseNetlistFileWithLibraries (optionLibraries options) path + case parsed of + Left message -> failWith message + Right netlist -> + case simulateWithScheduledInputs netlist (optionInputs options) (optionScheduled options) (optionDuration options) of + Left message -> failWith message + Right simulation -> do + wroteFile <- emit command simulation + let summaryHandle = if wroteFile then stdout else stderr + if wroteFile + then putStrLn ("Wrote " ++ optionOutput options) + else pure () + hPutStrLn summaryHandle ("Signals: " ++ show (length (netlistSignals netlist))) + hPutStrLn summaryHandle ("Duration: " ++ show (optionDuration options) ++ " time units") + reportAssertions summaryHandle simulation + +commandOptions :: Command -> Options +commandOptions (CommandRun options) = options +commandOptions (CommandReport options) = options + +emit :: Command -> Simulation -> IO Bool +emit (CommandRun options) simulation = do + writeVCD (optionOutput options) simulation + pure True +emit (CommandReport options) simulation = + let text = renderReport simulation + in if optionOutputExplicit options + then do + writeFile (optionOutput options) text + pure True + else do + putStr text + pure False + +reportAssertions :: Handle -> Simulation -> IO () +reportAssertions summaryHandle simulation = case simulationFailures simulation of [] -> case simulationAssertions simulation of [] -> pure () assertions -> - putStrLn ("Assertions: " ++ show (length assertions) ++ " passed") + hPutStrLn summaryHandle ("Assertions: " ++ show (length assertions) ++ " passed") failures -> do mapM_ (hPutStrLn stderr . renderFailure) failures exitFailure @@ -86,7 +117,7 @@ parseOptions arguments = parseMore defaultOptions arguments "--duration" : value : rest -> do duration <- maybe (Left "--duration requires an integer") Right (readMaybe value) parseMore options {optionDuration = duration} rest - "--output" : path : rest -> parseMore options {optionOutput = path} rest + "--output" : path : rest -> parseMore options {optionOutput = path, optionOutputExplicit = True} rest "--library" : path : rest -> parseMore options {optionLibraries = optionLibraries options ++ [path]} rest "--set" : assignments : rest -> do @@ -120,14 +151,14 @@ failWith message = do usage :: String usage = intercalate "\n" - [ "gatework --netlist FILE [--library FILE] [--duration N] [--output FILE] [--set signal=0,signal=1,signal=x,signal=z] [--at TIME signal=0,signal=1,signal=x,signal=z]" + [ "gatework [report] --netlist FILE [--library FILE] [--duration N] [--output FILE] [--set signal=0,signal=1,signal=x,signal=z] [--at TIME signal=0,signal=1,signal=x,signal=z]" , "" , "Simulate a netlist and write a VCD waveform." + , "Run 'gatework report' to write a text waveform table instead." + , "Without --output, the report prints to standard output." , "Use --library to load reusable module definitions from another file." , "Repeat --library to load more than one module file." , "Use --at to change input signals at a fixed time during the run." , "Use x for an unknown value and z for a floating value." , "Check assert declarations in the netlist against the waveform." ] - - diff --git a/fixtures/adder.golden.vcd b/fixtures/adder.golden.vcd index 659727e..79330cf 100644 --- a/fixtures/adder.golden.vcd +++ b/fixtures/adder.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/assert.golden.vcd b/fixtures/assert.golden.vcd index adbf145..f0ae630 100644 --- a/fixtures/assert.golden.vcd +++ b/fixtures/assert.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $comment assert y = 1 at 0 diff --git a/fixtures/bus.golden.vcd b/fixtures/bus.golden.vcd index 2a447eb..d2d4e3c 100644 --- a/fixtures/bus.golden.vcd +++ b/fixtures/bus.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $comment assert x[0] = 1 at 0 diff --git a/fixtures/counter.golden.report b/fixtures/counter.golden.report new file mode 100644 index 0000000..8cf3334 --- /dev/null +++ b/fixtures/counter.golden.report @@ -0,0 +1,11 @@ +time | q0 | q1 | q2 | q3 | clk | d0 | d1 | carry2 | d2 | carry3 | d3 +---- | -- | -- | -- | -- | --- | -- | -- | ------ | -- | ------ | -- + 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 + 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 + 2 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 + 3 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 + 4 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 + 5 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 0 + 6 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 + 7 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 + 8 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 diff --git a/fixtures/counter.golden.vcd b/fixtures/counter.golden.vcd index 8b2f596..00bc6c0 100644 --- a/fixtures/counter.golden.vcd +++ b/fixtures/counter.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/gates.golden.vcd b/fixtures/gates.golden.vcd index b48f092..ea35367 100644 --- a/fixtures/gates.golden.vcd +++ b/fixtures/gates.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $comment assert nand_out = 1 at 0 diff --git a/fixtures/haddader.golden.vcd b/fixtures/haddader.golden.vcd index 11ebc19..96b4acd 100644 --- a/fixtures/haddader.golden.vcd +++ b/fixtures/haddader.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/hcounter.golden.vcd b/fixtures/hcounter.golden.vcd index 2211b2f..6a79210 100644 --- a/fixtures/hcounter.golden.vcd +++ b/fixtures/hcounter.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/libadder.golden.vcd b/fixtures/libadder.golden.vcd index 11ebc19..96b4acd 100644 --- a/fixtures/libadder.golden.vcd +++ b/fixtures/libadder.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/reg2.golden.vcd b/fixtures/reg2.golden.vcd index a0a4dc1..1730604 100644 --- a/fixtures/reg2.golden.vcd +++ b/fixtures/reg2.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/register.golden.vcd b/fixtures/register.golden.vcd index 975d612..8cf1130 100644 --- a/fixtures/register.golden.vcd +++ b/fixtures/register.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/reset.golden.vcd b/fixtures/reset.golden.vcd index e2bdfa9..cca9050 100644 --- a/fixtures/reset.golden.vcd +++ b/fixtures/reset.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $timescale 1ns $end $scope module gatework $end diff --git a/fixtures/shared.golden.vcd b/fixtures/shared.golden.vcd index fd3ba91..e4231cf 100644 --- a/fixtures/shared.golden.vcd +++ b/fixtures/shared.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $comment assert y = z at 0 diff --git a/fixtures/tristate.golden.vcd b/fixtures/tristate.golden.vcd index c997a15..417ae2e 100644 --- a/fixtures/tristate.golden.vcd +++ b/fixtures/tristate.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $comment assert y = z at 0 diff --git a/fixtures/unknown.golden.vcd b/fixtures/unknown.golden.vcd index 7823fc1..621768a 100644 --- a/fixtures/unknown.golden.vcd +++ b/fixtures/unknown.golden.vcd @@ -2,7 +2,7 @@ $date generated deterministically by gatework $end $version - gatework 0.9.0.0 + gatework 0.10.0.0 $end $comment assert q = x at 0 diff --git a/gatework.cabal b/gatework.cabal index 9596c5d..e11b398 100644 --- a/gatework.cabal +++ b/gatework.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: gatework -version: 0.9.0.0 +version: 0.10.0.0 synopsis: Event-driven digital logic simulation in Haskell description: Gatework parses typed netlists, runs combinational gates and D @@ -12,6 +12,7 @@ description: bracketed widths and slices. Modules and instances build reusable circuits. A module library file stores reusable module definitions. The command-line tool loads libraries with the --library option. + A waveform report command prints the signal values as a text table. GTKWave and other waveform viewers can open the output. homepage: https://github.com/DanielCuevas1208/gatework bug-reports: https://github.com/DanielCuevas1208/gatework/issues @@ -24,6 +25,7 @@ build-type: Simple extra-source-files: fixtures/*.net fixtures/*.golden.vcd + fixtures/*.golden.report extra-doc-files: README.md CHANGELOG.md @@ -43,6 +45,7 @@ library exposed-modules: Gatework.Logic Gatework.Netlist + Gatework.Report Gatework.Simulator Gatework.VCD other-modules: diff --git a/src/Gatework/Report.hs b/src/Gatework/Report.hs new file mode 100644 index 0000000..f5ec1a8 --- /dev/null +++ b/src/Gatework/Report.hs @@ -0,0 +1,37 @@ +module Gatework.Report + ( renderReport + ) where + +import Data.List (intercalate, nub, sort) +import qualified Data.Map.Strict as Map +import Gatework.Logic (Logic (..), logicChar) +import Gatework.Simulator (Simulation (..), Time, signalChanges) + +renderReport :: Simulation -> String +renderReport simulation = unlines (header : divider : rows) + where + signals = simulationSignals simulation + times = changeTimes simulation + headers = "time" : signals + valueWidths = timeWidth : replicate (length signals) 1 + widths = zipWith max (map length headers) valueWidths + timeWidth = maximum (length "time" : map (length . show) times) + renderCell width value = replicate (width - length value) ' ' ++ value + row cells = intercalate " | " (zipWith renderCell widths cells) + header = row headers + divider = intercalate " | " [replicate width '-' | width <- widths] + rows = [row (show time : map (renderSignal time) signals) | time <- times] + renderSignal time signal = [logicChar (signalValueAt simulation signal time)] + +changeTimes :: Simulation -> [Time] +changeTimes simulation = sort . nub $ + [ time + | entries <- Map.elems (simulationChanges simulation) + , (time, _) <- entries + ] + +signalValueAt :: Simulation -> String -> Time -> Logic +signalValueAt simulation signal time = + case reverse [(changeTime, value) | (changeTime, value) <- signalChanges simulation signal, changeTime <= time] of + (_, value) : _ -> value + [] -> Low diff --git a/test/Spec.hs b/test/Spec.hs index f66655a..d382bd6 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -2,8 +2,10 @@ module Main (main) where import Control.Monad (forM) import Data.Bits (xor) +import Data.Char (isSpace) import Data.Either (isLeft) -import Data.List (foldl', isPrefixOf, sortOn) +import Data.List (elemIndex, find, foldl', isPrefixOf, nub, sort, sortOn) +import qualified Data.Map.Strict as Map import Gatework.Logic (GateType (..), Logic (..), evalGate, parseLogic) import Gatework.Netlist ( Assertion (..) @@ -14,6 +16,7 @@ import Gatework.Netlist , parseNetlistFileWithLibraries , parseNetlistWithLibraries ) +import Gatework.Report (renderReport) import Gatework.Simulator ( AssertionFailure (..) , Simulation @@ -24,6 +27,7 @@ import Gatework.Simulator , simulateWithInputs , simulateWithScheduledInputs , simulationAssertions + , simulationChanges , simulationFailures ) import Gatework.VCD (renderVCD) @@ -39,6 +43,7 @@ import Test.QuickCheck , vector ) import Test.QuickCheck.Test (isSuccess) +import Text.Read (readMaybe) newtype Logical = Logical Logic deriving (Eq, Show) @@ -256,6 +261,9 @@ main = do , testLibraryFileEntryPoint , testLibraryMissingFileFails , testGoldenLibadderVCD + , testReportHeader + , testReportCounterTable + , testGoldenCounterReport ] properties <- forM [ ("AND is commutative", quickCheckResult propAndCommutative) @@ -300,6 +308,7 @@ main = do , ("bus module matches the flat circuit", quickCheckResult propBusModuleMatchesFlat) , ("shared bus follows the resolution model", quickCheckResult (propSharedBusResolution shared)) , ("library adder matches the flat adder", quickCheckResult (propLibraryAdderMatchesFlat adder libadder)) + , ("counter report matches the waveform", quickCheckResult (propCounterReportMatchesWaveform counter)) ] $ \(label, action) -> do result <- action @@ -1748,6 +1757,95 @@ testGoldenLibadderVCD = do pure (renderVCD simulation) check "library adder VCD matches golden file" (actual == Right golden) +data ReportTable = ReportTable + { reportHeaderSignals :: [String] + , reportTableRows :: [(Time, [Logic])] + } + +parseReport :: String -> Maybe ReportTable +parseReport text = case lines text of + header : divider : body + | not (null body) + , not (null header) + , "time" == head (map trim (splitOnPipe header)) + , isDivider divider -> + let signals = drop 1 (map trim (splitOnPipe header)) + in do + rows <- mapM parseRow body + pure (ReportTable signals rows) + _ -> Nothing + where + isDivider line = + all (\cell -> not (null cell) && all (== '-') cell) (map trim (splitOnPipe line)) + parseRow line = case map trim (splitOnPipe line) of + [] -> Nothing + timeCell : valueCells -> do + time <- (fromIntegral :: Int -> Time) <$> readMaybe timeCell + values <- mapM parseLogic valueCells + pure (time, values) + splitOnPipe line = splitOn '|' line + +reportCell :: ReportTable -> String -> Time -> Maybe Logic +reportCell table signal time = do + index <- elemIndex signal (reportHeaderSignals table) + (_, values) <- find ((== time) . fst) (reportTableRows table) + if index < length values then Just (values !! index) else Nothing + +testReportHeader :: IO Bool +testReportHeader = do + source <- readFile "fixtures/counter.net" + let result = do + netlist <- parseNetlist source + simulation <- simulate netlist 8 + pure (renderReport simulation) + check "report header lists stable signals" $ case result of + Left _ -> False + Right text -> case parseReport text of + Nothing -> False + Just table -> + reportHeaderSignals table + == [ "q0", "q1", "q2", "q3", "clk" + , "d0", "d1", "carry2", "d2", "carry3", "d3" + ] + +testReportCounterTable :: IO Bool +testReportCounterTable = do + source <- readFile "fixtures/counter.net" + let result = do + netlist <- parseNetlist source + simulation <- simulate netlist 8 + pure (renderReport simulation) + check "report rows show the counter state" $ case result of + Left _ -> False + Right text -> case parseReport text of + Nothing -> False + Just table -> + reportCell table "q0" 0 == Just Low + && reportCell table "q0" 1 == Just High + && reportCell table "q1" 3 == Just High + && reportCell table "q0" 7 == Just Low + && reportCell table "clk" 7 == Just High + && reportCell table "carry2" 5 == Just High + && reportCell table "d3" 0 == Just Low + +testGoldenCounterReport :: IO Bool +testGoldenCounterReport = do + source <- readFile "fixtures/counter.net" + golden <- readFile "fixtures/counter.golden.report" + let actual = do + netlist <- parseNetlist source + simulation <- simulate netlist 8 + pure (renderReport simulation) + check "counter report matches golden file" (actual == Right golden) + +splitOn :: Char -> String -> [String] +splitOn delimiter value = case break (== delimiter) value of + (part, _ : rest) -> part : splitOn delimiter rest + (part, []) -> [part] + +trim :: String -> String +trim = dropWhile isSpace . reverse . dropWhile isSpace . reverse + evalTwoState :: GateType -> [Logic] -> Logic evalTwoState And inputs = if all (== High) inputs then High else Low evalTwoState Or inputs = if any (== High) inputs then High else Low @@ -2022,6 +2120,32 @@ propHierarchicalCounterMatchesFlat flat hierarchical = (\name -> valueAt flatResult name time == valueAt hierarchicalResult name time) (map fst counterBits) +propCounterReportMatchesWaveform :: Netlist -> Property +propCounterReportMatchesWaveform netlist = + forAll (choose (0, 30)) $ \duration -> + case simulate netlist duration of + Left _ -> property False + Right simulation -> + let text = renderReport simulation + in case parseReport text of + Nothing -> property False + Just table -> + property + ( all (rowMatches simulation table) (reportTableRows table) + && map fst (reportTableRows table) == reportChangeTimes simulation + ) + where + rowMatches simulation table (time, values) = + and + [ valueAt simulation signal time == expected + | (signal, expected) <- zip (reportHeaderSignals table) values + ] + reportChangeTimes simulation = sort . nub $ + [ time + | entries <- Map.elems (simulationChanges simulation) + , (time, _) <- entries + ] + propGatesFixture :: Netlist -> Property propGatesFixture netlist = forAll (elements [Low, High]) $ \a ->