Может ли Дафни проверить суммирование элементов справа?

Привет, я понимаю, что при выполнении индукции Дафни раскрывает спецификацию функции. Таким образом, при написании метода, реализующего функцию, лучше всего перемещаться по массиву в том же направлении. Этому пониманию соответствует поведение sumLeftChange (приведенное ниже) Но я никак не могу проверить sumRightChange. Кто-нибудь может объяснить, что мне не хватает?

function method sumSpec(input:seq<nat>) : nat
decreases input 
{  if |input| == 0 then 0 else input[0]+ sumSpec(input[1..]) }
function method sumSpecR(input:seq<nat>) : nat
decreases input 
{ if |input| == 0 then 0 else input[|input|-1]+ sumSpecR(input[..|input|-1]) }
method sumLeftChange(input:seq<nat>) returns (r:nat)
  ensures r == sumSpec(input)
//  ensures r == sumSpecR(input)
{   r :=0; var i:nat := 0;
    while (i<|input|)
     decreases |input| - i
     invariant i<= |input|
     invariant  r == sumSpec(input[|input| - i..])
   //  invariant  r == sumSpecR(input[|input| - i..]) //fails and should fail
    {   r:= r+input[|input| - i-1]; // first element is from right |input|-1
        i := i+1;
        print "sumLeftChange  ", i," r  ", r,"  ", sumSpec(input[|input| - i..]) ,"\n";
    } 
}
method sumRightChange(input:seq<nat>) returns (r:nat)
{  r :=0; var i:nat := 0;
    while (i<|input|)
     decreases |input| - i
     invariant i <= |input|    
    // invariant  r == sumSpecR(input[..i]) //I can get nothing to work
    {  
        r:= r+input[i];  // first element is from left  0     
        i := i+1; 
    print "sumRightChange   ", i," r= ", r,"  sumSpecR(input[..i])= ", sumSpecR(input[..i]) ,"\n";
    }
}
method Main() {
    var sl:nat := sumSpec([1,2,3,4]);
    print "\n";
    var sr:nat := sumSpecR([1,2,3,4]);
    print "\n";
    var sLc:nat := sumLeftChange([1,2,3,4]);
    print "\n";
    var sRc:nat := sumRightChange([1,2,3,4]);
    print "sl ", sl,"  sL= ",sLc, "\n";
    print "sr ", sr,"  sR= ",sRc, "\n";
}



person david streader    schedule 12.09.2020    source источник


Ответы (1)


Добавление assert input[..i+1][..i] == input[..i]; в начало тела цикла вызовет проверку sumRightChange.

Это случай, когда у нас есть истинный факт, что Дафни не подумает попробовать самостоятельно, пока вы не спросите об этом, но как только вы спросите об этом, эй, это input[..i+1][..i] == input[..i]? тогда он говорит о, да, очевидно. И затем у него есть этот факт, чтобы помочь в доказательстве позже. (Такое поведение, которое вы должны запрашивать, очень распространено при работе с уравнениями между коллекциями, такими как наборы, списки или карты в Dafny. Для получения дополнительной информации см. этот ответ.)


Вероятно, более важный вопрос заключается в том, как я определил, что это правильный факт, на который нужно указать Дафни?

Вот как. (Я прошу прощения за то, что это заняло много времени, но я хотел показать вам весь мой процесс и объяснить, что я делал на каждом этапе. Я не обижусь, если вы не прочитаете это все.)

Я начал со следующего цикла внутри sumRightChange, раскомментировав ошибочный инвариант. (Я удалил оператор print для краткости.)

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {
    r := r + input[i]; 
    i := i + 1;
  }

Инвариант отчетов Dafny может не поддерживаться. Я знаю, что это означает, что Дафни пытался утвердить инвариант в нижней части цикла, и это не удалось. Просто чтобы перепроверить себя, я копирую и вставляю инвариант и преобразовываю его в утверждение в нижней части цикла.

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {
    r := r + input[i]; 
    i := i + 1;
    assert r == sumSpecR(input[..i]);  // added this line
  }

Как и ожидалось, Дафни сообщает о нарушении утверждения. Но ошибка в инварианте исчезает, что дает мне уверенность, что если я смогу доказать это утверждение, то все готово.

Я также знаю, что могу принять инвариант в начале цикла. Я хотел бы сдвинуть эти два факта (предполагаемый инвариант вверху и заявленный инвариант внизу) друг к другу. Оказывается, что двигаться назад легче, чем вперед, поэтому я продолжу пытаться постепенно перемещать утверждение в нижней части цикла вверх.

Хитрость перемещения утверждения назад по присваиванию состоит в том, чтобы вручную отменить присваивание внутри утверждения. Итак, чтобы переместить утверждения r == sumSpecR(input[..i]) назад по заданию i := i + 1, я заменю все вхождения i на i + 1. Это тот же факт, только констатированный в другое время. Поскольку значение i в это разное время отличается, утверждение должно быть синтаксически изменено, чтобы быть одним и тем же фактом. Надеюсь, это имеет какой-то смысл... В любом случае, выполнение этого преобразования дает этот цикл:

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {
    r := r + input[i];
    assert r == sumSpecR(input[..i+1]);  // moved this assertion up one line
    i := i + 1;
  }

Дафни по-прежнему сообщает о нарушении утверждения. И критически он по-прежнему не сообщает о нарушении инварианта. Таким образом, мы по-прежнему знаем, что если мы сможем доказать утверждение, мы проверим весь цикл.

(Обратите внимание, что произойдет, если мы допустим ошибку при перемещении утверждения. Например, если бы мы не заменили вручную i на i+1, а вместо этого просто вслепую переместили утверждение на одну строку вверх. Затем Дафни сообщает о нарушении утверждения и инвариантное нарушение, так что мы знаем, что облажались!)

Теперь давайте переместим утверждение еще на одну строку вверх по присваиванию r := r + input[i];. Опять же, хитрость заключается в том, чтобы вручную отменить это назначение, заменив r на r + input[i] в утверждении, например:

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {  
    assert r + input[i] == sumSpecR(input[..i+1]);  // moved this assertion up one line
    r := r + input[i];
    i := i + 1;
  }

Опять же, Дафни сообщает о нарушении утверждения, но не о нарушении инварианта. (И, опять же, если бы мы ошиблись, он бы сообщил о нарушении инварианта.)

Теперь у нас есть утверждение в верхней части цикла, где мы знаем, что инвариант выполняется. Пришло время сделать доказательство.

Мы пытаемся доказать уравнение. Мне нравится использовать оператор Дафни calc для отладки доказательств уравнений. Если вы не знакомы с оператором calc, основная форма

calc {
  A;
  B;
  C;
  D;
}

что доказывает A == D, доказывая A == B, B == C и C == D. Это дает удобный способ добавить промежуточные шаги к эквациональному доказательству и точно определить, где Дафни запутался.

Чтобы преобразовать утверждение уравнения в утверждение calc, мы можем просто поместить левую часть уравнения в первую строку, а правую часть во вторую строку. Пока это ничего не меняет, но давайте просто убедимся:

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {
    // replaced old assertion with the following calc statement
    calc {
      r + input[i];
      sumSpecR(input[..i+1]);
    }
    r := r + input[i];
    i := i + 1;
  }

Дафни сообщает об ошибке в операторе calc, но не в операторе invariant, так что мы по-прежнему знаем, что доказываем правильный факт, чтобы завершить проверку цикла.

Ошибка находится во второй строке тела оператора calc, и в сообщении говорится, что шаг вычисления между предыдущей строкой и этой строкой может не выполняться. По сути, Дафни не смог доказать, что две линии равны. Неудивительно, поскольку мы создали этот оператор calc из ошибочного утверждения.

Теперь мы можем начать добавлять промежуточные шаги. Иногда имеет смысл двигаться вперед от верхней строки, а в других случаях имеет смысл работать от последней строки вверх. Я думаю, что вторая стратегия имеет больше смысла здесь.

Давайте вручную развернем определение sumSpecR и посмотрим, где мы застряли. Глядя на определение sumSpecR, он сначала проверяет, является ли |input| == 0. Будьте осторожны, потому что input здесь относится к аргументу sumSpecR, не к аргументу sumRightChange! В контексте последней строки нашего оператора calc мы передаем input[..i+1] в sumSpecR, поэтому определение спрашивает, имеет ли этот список нулевую длину. Но мы знаем, что это не так, так как i >= 0 и мы добавляем к нему 1. Итак, мы окажемся в else ветке определения.

Ветвь else разделяет список справа. Давайте попробуем быть систематичными и просто скопируем-вставим ветку else определения, заменив фактический аргумент input[..i+1] на имя параметра input. (Мне нравится использовать текстовый редактор для поиска и замены на этом шаге.)

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {  
    calc {
      r + input[i];
      // added the following line by copy-pasting the else branch of the 
      // definition of sumSpecR and then replacing its parameter
      // input with the actual argument input[..i+1]
      input[..i+1][|input[..i+1]|-1] + sumSpecR(input[..i+1][..|input[..i+1]|-1]);
      sumSpecR(input[..i+1]);
    }
    r := r + input[i];
    i := i + 1;
  }

Теперь обратите внимание на то, что происходит с сообщением об ошибке. Он движется вверх по строке! Это означает, что мы делаем успехи, потому что Дафни согласен с нами в том, что наша новая средняя строка тела оператора calc равна последней строке.

Сейчас мы можем сделать множество упрощений. Начнем с упрощения |input[..i+1]| до i+1. Вы можете сделать это, изменив строку, которую мы только что добавили, но мне нравится делать это, добавляя еще одну строку над ней, чтобы я мог записывать свой прогресс и убедиться, что Дафни согласен с тем, что я двигаюсь в правильном направлении.

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {  
    calc {
      r + input[i];
      // added the following line simplifying |input[..i+1]| into i+1
      input[..i+1][i+1-1] + sumSpecR(input[..i+1][..i+1-1]);
      input[..i+1][|input[..i+1]|-1] + sumSpecR(input[..i+1][..|input[..i+1]|-1]);
      sumSpecR(input[..i+1]);
    }
    r := r + input[i];
    i := i + 1;
  }

Опять же, сообщение об ошибке перемещается вверх по строке. Ура!

Теперь мы можем упростить i+1-1 до i, а также input[..i+1][i] до input[i]. Опять же, я предпочитаю делать это на новой строке.

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {  
    calc {
      r + input[i];
      input[i] + sumSpecR(input[..i+1][..i]);  // added this line
      input[..i+1][i+1-1] + sumSpecR(input[..i+1][..i+1-1]);
      input[..i+1][|input[..i+1]|-1] + sumSpecR(input[..i+1][..|input[..i+1]|-1]);
      sumSpecR(input[..i+1]);
    }
    r := r + input[i];
    i := i + 1;
  }

Сообщение об ошибке продолжает двигаться вверх.

Следующее упрощение, которое я хотел бы сделать, это преобразовать input[..i+1][..i] в input[..i]. Опять же, я использую новую строку.

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {  
    calc {
      r + input[i];
      input[i] + sumSpecR(input[..i]);  // added this line
      input[i] + sumSpecR(input[..i+1][..i]);
      input[..i+1][i+1-1] + sumSpecR(input[..i+1][..i+1-1]);
      input[..i+1][|input[..i+1]|-1] + sumSpecR(input[..i+1][..|input[..i+1]|-1]);
      sumSpecR(input[..i+1]);
    }
    r := r + input[i];
    i := i + 1;
  }

Внимательно следите за тем, что происходит с сообщением об ошибке. Он не двигается! Это говорит нам о двух вещах. Во-первых, Дафни не согласен с нашим последним упрощением. Во-вторых, Дафни говорит, что наша новая добавленная строка равна первой строке оператора calc! (Здесь Дафни использует предположение об инварианте цикла, сообщающем нам r == sumSpecR(input[..i]).) Таким образом, хотя мы работали с последней строки вверх, теперь мы фактически достигли вершины с оставшимся пробелом между строками 2 и 3.

Значит, Дафни этого не видит.

input[i] + sumSpecR(input[..i]) == input[i] + sumSpecR(input[..i+1][..i])

Что дает? Выражение input[i] появляется с обеих сторон, поэтому остается показать следующее:

sumSpecR(input[..i]) == sumSpecR(input[..i+1][..i])

Здесь у нас есть функция sumSpecR, примененная к двум синтаксически различным аргументам, которые, как мы считаем, одинаковы. На данный момент не имеет значения, каково определение sumSpecR, важно только то, что это функция, применяемая к равным аргументам. Таким образом, мы можем попытаться проверить, что аргументы действительно равны.

assert input[..i] == input[..i+1][..i];

И это утверждение нам было нужно, чтобы получить полное доказательство.


В конце такого процесса отладки я обычно подчищаю и оставляю только те факты, которые действительно нужны Дафни для доказательства. Поэтому я просто пытаюсь удалить материал и посмотреть, работает ли доказательство. В этом случае требуется только самое последнее обнаруженное нами утверждение; все остальное можно удалить.

Вот мой последний цикл:

  while (i < |input|)
    decreases |input| - i
    invariant i <= |input|    
    invariant r == sumSpecR(input[..i])
  {
    assert input[..i] == input[..i+1][..i];  // load-bearing assertion due to sequence extensionality
    r := r + input[i];
    i := i + 1;
  }

Мне нравится оставлять себе маленькую заметку, напоминающую мне, что я думаю, что доказательство не будет работать без этого утверждения, вместе с моими лучшими предположениями о том, почему это утверждение необходимо.

person James Wilcox    schedule 12.09.2020
comment
Спасибо за долгий урок. Это было именно то, что я был после. Хотя только на полпути через многие пункты были полезны. Когда я пытаюсь построить свое собственное объяснение, мое понимание, лучшее предположение о том, как работает Дафни, продолжает меня подводить. Тем не менее большое спасибо, Джеймс. - person david streader; 13.09.2020
comment
Прочитав ваш ответ подробно, я многому научился. Большое спасибо. Но мой главный вопрос все еще остается: почему sunChangeLeft было так легко доказать, а sumChangeRight так сложно? Должен ли я был в состоянии предвидеть относительную сложность, прежде чем исследовать каждый из путей? - person david streader; 14.09.2020
comment
Спасибо за подробный ответ по отладке, Джеймс! Что касается определений левого и правого, см. также этот эпизод Уголка проверки: youtube.com/watch?v =BLQo5d3hI4M - person Rustan Leino; 14.09.2020