Skip to content

Commit

Permalink
Merge branch 'coverage-report-per-module-breakdown' of github.com:rob…
Browse files Browse the repository at this point in the history
…in-aws/dafny into per-backend-code-support-in-stdlibs

# Conflicts:
#	Source/DafnyStandardLibraries/binaries/DafnyStandardLibraries-arithmetic.doo
#	Source/DafnyStandardLibraries/binaries/DafnyStandardLibraries.doo
  • Loading branch information
robin-aws committed Nov 13, 2023
2 parents f3cd39f + 73c278a commit 121745d
Show file tree
Hide file tree
Showing 18 changed files with 1,758 additions and 14 deletions.
18 changes: 16 additions & 2 deletions Source/DafnyCore/CoverageReport/CoverageReport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public class CoverageReport {
private static int nextUniqueId = 0;

private readonly Dictionary<Uri, List<CoverageSpan>> labelsByFile;
private readonly Dictionary<Uri, HashSet<ModuleDefinition>> modulesByFile;
public readonly string Name; // the name to assign to this coverage report
public readonly string Units; // the units of coverage (plural). This will be written in the coverage report table.
private readonly string suffix; // user-provided suffix to add to filenames that are part of this report
Expand All @@ -31,6 +32,7 @@ public CoverageReport(string name, string units, string suffix, Program program)
Units = units;
this.suffix = suffix;
labelsByFile = new();
modulesByFile = new();
if (program != null) {
RegisterFiles(program);
}
Expand All @@ -50,6 +52,10 @@ public IEnumerable<CoverageSpan> CoverageSpansForFile(Uri uri) {
return labelsByFile.GetOrDefault(uri, () => new List<CoverageSpan>());
}

public IEnumerable<ModuleDefinition> ModulesInFile(Uri uri) {
return modulesByFile.GetOrDefault(uri, () => new HashSet<ModuleDefinition>());
}

public IEnumerable<Uri> AllFiles() {
return labelsByFile.Keys;
}
Expand All @@ -65,8 +71,16 @@ public void RegisterFile(Uri uri) {
}

private void RegisterFiles(Node astNode) {
if (astNode.StartToken.ActualFilename != null && !labelsByFile.ContainsKey(astNode.StartToken.Uri)) {
labelsByFile[astNode.StartToken.Uri] = new();
if (astNode.StartToken.ActualFilename != null) {
labelsByFile.GetOrCreate(astNode.StartToken.Uri, () => new List<CoverageSpan>());
}

if (astNode is LiteralModuleDecl moduleDecl) {
if (astNode.StartToken.ActualFilename != null) {
modulesByFile.GetOrCreate(astNode.StartToken.Uri, () => new HashSet<ModuleDefinition>()).Add(moduleDecl.ModuleDef);
}

RegisterFiles(moduleDecl.ModuleDef);
}

foreach (var declaration in astNode.Children.OfType<LiteralModuleDecl>()) {
Expand Down
38 changes: 31 additions & 7 deletions Source/DafnyDriver/CoverageReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ private void SerializeCoverageReports(List<CoverageReport> reports, string repor

private string MakeIndexFileTableRow(List<object> row) {
var result = new StringBuilder("<tr>\n");
foreach (var cell in row) {
foreach (var cell in row.Take(2)) {
result.Append($"\t<td class=\"name\">{cell}</td>\n");
}
foreach (var cell in row.Skip(2)) {
result.Append($"\t<td class=\"ctr2\">{cell}</td>\n");
}
result.Append("</tr>\n");
Expand All @@ -215,24 +218,45 @@ private void CreateIndexFile(CoverageReport report, Dictionary<Uri, string> sour
return;
}
var coverageLabels = Enum.GetValues(typeof(CoverageLabel)).Cast<CoverageLabel>().ToList();
List<object> header = new() { "File" };
List<object> header = new() { "File", "Module" };
header.AddRange(coverageLabels
.Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable)
.Select(label => $"{report.Units} {CoverageLabelExtension.ToString(label)}"));

List<List<object>> body = new();
foreach (var sourceFile in sourceFileToCoverageReportFile.Keys) {
var relativePath = Path.GetRelativePath(baseDirectory, sourceFileToCoverageReportFile[sourceFile]);

body.Add(new() {
$"<a href = \"{relativePath}{report.UniqueSuffix}.html\"" +
$"class = \"el_package\">{relativePath}</a>"
$"class = \"el_package\">{relativePath}</a>",
"All modules"
});

body.Last().AddRange(coverageLabels
.Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable)
.Select(label => report.CoverageSpansForFile(sourceFile).Count(span => span.Label == label)).OfType<object>());
.Select(label => report.CoverageSpansForFile(sourceFile)
.Count(span => span.Label == label)).OfType<object>());

foreach (var module in report.ModulesInFile(sourceFile).OrderBy(m => m.FullName)) {
body.Add(new() {
"",
module.FullName
});

var moduleRange = module.RangeToken.ToDafnyRange();
body.Last().AddRange(coverageLabels
.Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable)
.Select(label => report.CoverageSpansForFile(sourceFile)
// span.Span.Intersects(module.RangeToken) would be cleaner,
// but unfortunately coverage span tokens don't currently always
// have Token.pos set correctly. :(
.Where(span => moduleRange.Contains(span.Span.ToDafnyRange().Start))
.Count(span => span.Label == label)).OfType<object>());
}
}

List<object> footer = new() { "Total" };
List<object> footer = new() { "Total", "" };
footer.AddRange(coverageLabels
.Where(label => label != CoverageLabel.None && label != CoverageLabel.NotApplicable)
.Select(label => report.AllFiles().Select(sourceFile =>
Expand All @@ -243,8 +267,8 @@ private void CreateIndexFile(CoverageReport report, Dictionary<Uri, string> sour
templateText = FileNameRegex.Replace(templateText, report.Name);
templateText = TableHeaderRegex.Replace(templateText, MakeIndexFileTableRow(header));
templateText = TableFooterRegex.Replace(templateText, MakeIndexFileTableRow(footer));
File.WriteAllText(Path.Combine(baseDirectory, $"index{report.UniqueSuffix}.html"),
TableBodyRegex.Replace(templateText, string.Join("\n", body.Select(MakeIndexFileTableRow))));
templateText = TableBodyRegex.Replace(templateText, string.Join("\n", body.Select(MakeIndexFileTableRow)));
File.WriteAllText(Path.Combine(baseDirectory, $"index{report.UniqueSuffix}.html"), templateText);
}

/// <summary>
Expand Down
11 changes: 11 additions & 0 deletions Source/DafnyDriver/assets/.resources/coverage.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ table.coverage thead td {
border-bottom:#b0b0b0 1px solid;
}

table.coverage thead td.name {
text-align:left;
padding-left:2px;
}

table.coverage thead td.ctr2 {
text-align:right;
padding-left:2px;
Expand All @@ -64,6 +69,12 @@ table.coverage tbody tr:hover {
background: #f0f0d0 !important;
}

table.coverage tbody td.name {
text-align:left;
padding-right:14px;
padding-left:2px;
}

table.coverage tbody td.ctr2 {
text-align:right;
padding-right:14px;
Expand Down
3 changes: 2 additions & 1 deletion Source/DafnyStandardLibraries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This project contains the source for the standard libraries
that are packaged together with the Dafny distribution.
The libraries in this directory are available automatically
The libraries in this directory are available automatically
when you provide the `--standard-libraries` option.
No need to `include` any files! For example:

Expand Down Expand Up @@ -42,6 +42,7 @@ The sections below describe how to use each library:
- [DafnyStdLibs.Wrappers](src/DafnyStdLibs/Wrappers) -- simple datatypes to support common patterns, such as optional values or the result of operations that can fail
- [DafnyStdLibs.Relations](src/DafnyStdLibs/Relations) -- properties of relations
- [DafnyStdLibs.Functions](src/DafnyStdLibs/Functions) -- properties of functions
- [DafnyStdLibs.Collections](src/DafnyStdLibs/Collections) -- properties of the built-in collection types (seq, set, iset, map, imap, array)
- DafnyStdLibs.DynamicArray -- an array that can grow and shrink
- [DafnyStdLibs.Base64](src/DafnyStdLibs/Base64) -- base-64 encoding and decoding

Expand Down
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions Source/DafnyStandardLibraries/examples/Base64Examples.dfy
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ module Base64Examples {
}

method {:test} TestRoundTripMedium() {
var medUints := seq(512, _ => 22);
var medBytes := seq(512, _ => 22);
var medUints := seq(512, i => (i % 256) as uint8);
var medBytes := seq(512, i => (i % 256) as bv8);
CheckEncodeDecode(medUints, medBytes);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*******************************************************************************
* Copyright by the contributors to the Dafny Project
* SPDX-License-Identifier: MIT
*******************************************************************************/

/**
This module defines useful properties and functions relating to the built-in `array` type.
*/
module DafnyStdLibs.Collections.Arrays {

import opened Wrappers
import opened Relations
import opened Seq

method BinarySearch<T>(a: array<T>, key: T, less: (T, T) -> bool) returns (r: Option<nat>)
requires Seq.SortedBy(a[..], (x, y) => less(x, y) || x == y)
requires StrictTotalOrdering(less)
ensures r.Some? ==> r.value < a.Length && a[r.value] == key
ensures r.None? ==> key !in a[..]
{
var lo, hi : nat := 0, a.Length;
while lo < hi
invariant 0 <= lo <= hi <= a.Length
invariant key !in a[..lo] && key !in a[hi..]
invariant a[..] == old(a[..])
{
var mid := (lo + hi) / 2;

if less(key, a[mid]) {
hi := mid;
} else if less(a[mid], key) {
lo:= mid + 1;
} else {
return Some(mid);
}
}

return None;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*******************************************************************************
* Copyright by the contributors to the Dafny Project
* SPDX-License-Identifier: MIT
*******************************************************************************/

/**
This module contains submodules each defining useful properties and functions
relating to the built-in collection types.
*/
module DafnyStdLibs.Collections {
// This module currently contains no definitions,
// but is defined so that the module comment appears in generated documentation.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## The `Collections` module

This module includes a variety of submodules that define properties of
various collections:

- Sets: functions and lemmas expressing properties of Dafny's `set<T>` type, such as properties of subsets, unions, and intersections, filtering of sets using predicates, mapping sets to other sets using functions

- Isets: function and lemmas to manipulate `iset`s

- Seqs: functions and lemmas expressing properties of Dafny's `seq<T>` type,
such as properties of subsequences, filtering of sequences,
finding elements (i.e., index-of, last-index-of),
inserting or removing elements,
reversing, repeating or combining (zipping or flattening) sequences,
sorting, and
conversion to sets.

- Maps: functions and lemmas to manipulate `map`s

- Imaps: functions and lemmas to manipulate `imap`s

- Arrays: manipulations of arrays

<!-- TODO
- LittleEndianNat: properties of sequences that represent natural numbers expressed as a given base, with the least significant digit at position 0 of the sequence and each element in the range 0 to the base.
- LittleEndianNatConversions: conversions between two little-endian representations of nats in different bases
-->
109 changes: 109 additions & 0 deletions Source/DafnyStandardLibraries/src/DafnyStdLibs/Collections/Imaps.dfy
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*******************************************************************************
* Original: Copyright 2018-2021 VMware, Inc., Microsoft Inc., Carnegie Mellon University,
* ETH Zurich, and University of Washington
* SPDX-License-Identifier: BSD-2-Clause
*
* Modifications and Extensions: Copyright by the contributors to the Dafny Project
* SPDX-License-Identifier: MIT
*******************************************************************************/

/**
This module defines useful properties and functions relating to the built-in `imap` type.
*/
module DafnyStdLibs.Collections.Imaps {

import opened Wrappers

function Get<X, Y>(m: imap<X, Y>, x: X): Option<Y>
{
if x in m then Some(m[x]) else None
}

/* Remove all key-value pairs corresponding to the iset of keys provided. */
ghost function {:opaque} RemoveKeys<X, Y>(m: imap<X, Y>, xs: iset<X>): (m': imap<X, Y>)
ensures forall x {:trigger m'[x]} :: x in m && x !in xs ==> x in m' && m'[x] == m[x]
ensures forall x {:trigger x in m'} :: x in m' ==> x in m && x !in xs
ensures m'.Keys == m.Keys - xs
{
imap x | x in m && x !in xs :: m[x]
}

/* Remove a key-value pair. Returns unmodified imap if key is not found. */
ghost function {:opaque} RemoveKey<X, Y>(m: imap<X, Y>, x: X): (m': imap<X, Y>)
ensures m' == RemoveKeys(m, iset{x})
ensures forall x' {:trigger m'[x']} :: x' in m' ==> m'[x'] == m[x']
{
imap i | i in m && i != x :: m[i]
}

/* Keep all key-value pairs corresponding to the iset of keys provided. */
ghost function {:opaque} Restrict<X, Y>(m: imap<X, Y>, xs: iset<X>): (m': imap<X, Y>)
ensures m' == RemoveKeys(m, m.Keys - xs)
{
imap x | x in xs && x in m :: m[x]
}

/* True iff x maps to the same value or does not exist in m and m'. */
ghost predicate EqualOnKey<X, Y>(m: imap<X, Y>, m': imap<X, Y>, x: X)
{
(x !in m && x !in m') || (x in m && x in m' && m[x] == m'[x])
}

/* True iff m is a subset of m'. */
ghost predicate IsSubset<X, Y>(m: imap<X, Y>, m': imap<X, Y>)
{
&& m.Keys <= m'.Keys
&& forall x {:trigger EqualOnKey(m, m', x)}{:trigger x in m} :: x in m ==> EqualOnKey(m, m', x)
}

/* Union of two imaps. Does not require disjoint domains; on the intersection,
values from the second imap are chosen. */
ghost function {:opaque} Union<X, Y>(m: imap<X, Y>, m': imap<X, Y>): (r: imap<X, Y>)
ensures r.Keys == m.Keys + m'.Keys
ensures forall x {:trigger r[x]} :: x in m' ==> r[x] == m'[x]
ensures forall x {:trigger r[x]} :: x in m && x !in m' ==> r[x] == m[x]
{
m + m'
}

/* True iff an imap is injective. */
ghost predicate {:opaque} Injective<X, Y>(m: imap<X, Y>)
{
forall x, x' {:trigger m[x], m[x']} :: x != x' && x in m && x' in m ==> m[x] != m[x']
}

/* Swaps imap keys and values. Values are not required to be unique; no
promises on which key is chosen on the intersection. */
ghost function {:opaque} Invert<X, Y>(m: imap<X, Y>): imap<Y, X>
{
imap y | y in m.Values :: var x :| x in m.Keys && m[x] == y; x
}

/* Inverted maps are injective. */
lemma LemmaInvertIsInjective<X, Y>(m: imap<X, Y>)
ensures Injective(Invert(m))
{
reveal Injective();
reveal Invert();
}

/* True iff an imap contains all valid keys. */
ghost predicate {:opaque} Total<X(!new), Y>(m: imap<X, Y>)
{
forall i {:trigger m[i]}{:trigger i in m} :: i in m
}

/* True iff an imap is monotonic. */
ghost predicate {:opaque} Monotonic(m: imap<int, int>)
{
forall x, x' {:trigger m[x], m[x']} :: x in m && x' in m && x <= x' ==> m[x] <= m[x']
}

/* True iff an imap is monotonic. Only considers keys greater than or
equal to start. */
ghost predicate {:opaque} MonotonicFrom(m: imap<int, int>, start: int)
{
forall x, x' {:trigger m[x], m[x']} :: x in m && x' in m && start <= x <= x' ==> m[x] <= m[x']
}

}
Loading

0 comments on commit 121745d

Please sign in to comment.