很自然地认为 Dafny 静态验证器可以评估任何代码,例如您的断言语句中的表达式。验证器确实会尝试评估像这样的表达式,其中参数作为常量给出(例如 your "hello world"
、'l'
和3
)。但是,静态验证器希望避免永远递归(甚至递归太久),因此它并不总是完全遍历这些表达式。在您的情况下,验证者能够对序列操作执行的操作也存在限制。因此,简而言之,尽管验证者试图提供帮助,但它并不总是能深入到递归的底部。
有两种方法可以解决这些限制,让验证者接受你的断言。
调试情况的最可靠方法是从较小的输入开始,然后逐步增加您正在使用的较长输入。然而,这很乏味,当 Dafny 能够自动完成这些事情时,你会很感激。以下(验证)说明了您将执行的操作:
var s := "hello world";
assert tally("he",'l') == 0;
assert tally("hel",'l') == 1;
assert "hell"[..3] == "hel";
assert tally("hell",'l') == 2;
assert "hello"[..4] == "hell";
assert tally("hello",'l') == 2;
assert "hello "[..5] == "hello";
assert tally("hello ",'l') == 2;
assert "hello w"[..6] == "hello ";
assert tally("hello w",'l') == 2;
assert "hello wo"[..7] == "hello w";
assert tally("hello wo",'l') == 2;
assert "hello wor"[..8] == "hello wo";
assert tally("hello wor",'l') == 2;
assert "hello worl"[..9] == "hello wor";
assert tally("hello worl",'l') == 3;
assert s[..10] == "hello worl";
assert tally(s,'l') == 3;
实际上,Dafny 验证器不会为您扩展(太多次)的东西是“take”操作(即 form 的表达式s[..E]
)。以下中间断言也将自我验证,并有助于验证最终断言。这些中间断言显示了验证者不会自动为您做的事情。
var s := "hello world";
assert "he"[..1] == "h";
assert "hel"[..2] == "he";
assert "hell"[..3] == "hel";
assert "hello"[..4] == "hell";
assert "hello "[..5] == "hello";
assert "hello w"[..6] == "hello ";
assert "hello wo"[..7] == "hello w";
assert "hello wor"[..8] == "hello wo";
assert "hello worl"[..9] == "hello wor";
assert s[..10] == "hello worl";
assert tally(s,'l') == 3;
你可能想知道,“我到底怎么会想出这个?”。最系统的方法是像我上面的第一个例子一样开始。然后,您可以尝试修剪那里的许多断言,以查看验证者需要什么。
(我现在想也许可以增强 Dafny 验证器来执行这些操作。它可能会在其他地方导致性能问题。我会看看。)
绕过验证者限制的另一种方法tally
是以不同的方式定义函数,特别是避免验证者不希望大量扩展的“获取”操作。目前尚不清楚在这些情况下要更改什么以使验证者满意,或者这是否可能,因此这种解决方法可能不是最好的。尽管如此,我尝试了以下定义,
tally
它使您的断言通过:
function method tally'(s: string, letter: char): nat
{
tally_from(s, letter, 0)
}
function method tally_from(s: string, letter: char, start: nat): nat
requires start <= |s|
decreases |s| - start
{
if start == |s| then 0
else (if s[start] == letter then 1 else 0) + tally_from(s, letter, start+1)
}
请注意,这些定义不使用任何“采取”操作。在这里,验证者很乐意扩展所有递归调用,直到找到最终答案。
鲁斯坦