Испытание Floor_Log2 в Искре - PullRequest
       78

Испытание Floor_Log2 в Искре

0 голосов
/ 13 декабря 2018

Новый для Spark и новый для Ada, поэтому этот вопрос может быть слишком широким.Тем не менее, это спрашивается добросовестно, как часть попытки понять Spark.Помимо прямых ответов на вопросы ниже, я приветствую критику стиля, рабочего процесса и т. Д.

В качестве моего первого набега на Spark я решил попытаться реализовать (легко) и доказать правильность (пока что безуспешно) функции\left \lfloor{log_2(x)}\right \rfloor.

Вопрос: Как правильно реализовать и доказать правильность этой функции?

Я начал со следующего util.ads:

package Util is
   function Floor_Log2(X : Positive) return Natural with
     Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);    
end Util;

У меня нет предварительных условий, потому что диапазоны ввода полностью выражают единственное интересное предварительное условие.Постусловие, которое я написал на основе математического определения;однако, у меня есть непосредственная проблема здесь.Если X равно Positive'Last, то 2**(Floor_Log2'Result + 1) превышает Positive'Last и Natural'Last.Здесь я уже сталкиваюсь с ограниченным знанием Ады, поэтому: Подвопрос 1: Какого типа подвыражение в условии post, и является ли это переполнением проблемой?Есть ли общий способ решить это?Чтобы избежать этой проблемы в данном конкретном случае, я изменил спецификацию на менее интуитивно понятную, но эквивалентную:

package Util is
   function Floor_Log2(X : Positive) return Natural with
     Post => 2**Floor_Log2'Result <= X and then X/2 < 2**Floor_Log2'Result;
end Util;

Существует много способов реализации этой функции, и меня это не особо беспокоитточка, так что я был бы счастлив с любым из них.Я бы посчитал, что «естественная» реализация (учитывая мой конкретный C-фон) выглядит примерно так: util.adb:

package body Util is
   function Floor_Log2 (X : Positive) return Natural is
      I : Natural := 0;
      Remaining : Positive := X;
   begin
      while Remaining > 1 loop
         I := I + 1;
         Remaining := Remaining / 2;
      end loop;
      return I;
   end Floor_Log2;
end Util;

Попытка доказать это без инвариантов цикла не удалась, как и ожидалось.Результаты (это и все результаты GNATprove уровня 4, вызванные из GPS как gnatprove -P%PP -j0 %X --ide-progress-bar -u %fp --level=4 --report=all):

util.adb:6:13: info: initialization of "Remaining" proved[#2]
util.adb:7:15: info: initialization of "I" proved[#0]
util.adb:7:17: medium: overflow check might fail[#5]
util.adb:8:23: info: initialization of "Remaining" proved[#1]
util.adb:8:33: info: range check proved[#4]
util.adb:8:33: info: division check proved[#8]
util.adb:10:14: info: initialization of "I" proved[#3]
util.ads:3:14: medium: postcondition might fail, cannot prove 2**Floor_Log2'Result <= X[#7]
util.ads:3:15: medium: overflow check might fail[#9]
util.ads:3:50: info: division check proved[#6]
util.ads:3:56: info: overflow check proved[#10]

Большинство ошибок здесь имеют для меня основной смысл.Начиная с первой проверки переполнения, GNATprove не может доказать, что цикл завершается менее чем за Natural'Last итераций (или вообще?), Поэтому он не может доказать, что I := I + 1 не переполняется.Мы знаем, что это не так, потому что Remaining уменьшается.Я попытался выразить это добавлением варианта цикла pragma Loop_Variant (Decreases => Remaining), и GNATprove смог доказать этот вариант цикла, но потенциальное переполнение I := I + 1 не изменилось, предположительно, потому что доказательство завершения цикла вообще не эквивалентно доказательству того, что он завершаетсяменее чем за Positive'Last итераций.Более жесткое ограничение показало бы, что цикл завершается не более чем Positive'Size итераций, но я не уверен, как это доказать.Вместо этого я «принудил это», добавив pragma Assume (I <= Remaining'Size);Я знаю, что это плохая практика, цель здесь состояла лишь в том, чтобы показать мне, как далеко я смогу продвинуться с этим первым выпуском, «скрытым под одеялом».Как и ожидалось, это предположение позволяет проверяющему доказать все проверки диапазона в файле реализации. Подвопрос 2: Как правильно доказать, что I не переполняется в этом цикле?

Однако мы до сих пор не достигли прогресса в доказательстве постусловия.Инвариант цикла явно необходим.Один инвариант цикла, который выполняется в верхней части цикла, состоит в том, что pragma Loop_Invariant (Remaining * 2**I <= X and then X/2 < Remaining * 2**I);Помимо того, что это истина, этот инвариант обладает хорошим свойством, что он явно эквивалентен постусловию, когда условие завершения цикла истинно.Однако, как и ожидалось, GNATprove не может доказать этот инвариант: medium: loop invariant might fail after first iteration, cannot prove Remaining * 2**I <= X[#20].Это имеет смысл, потому что индуктивный шаг здесь неочевиден.С делением на действительные числа можно представить простую лемму о том, что for all I, X * 2**I = (X/2) * 2**(I+1), но (а) я не ожидаю, что GNATprove узнает об этом без предоставления леммы, и (б) это сложнее с целочисленным делением.Итак, Подвопрос 3a: Является ли этот цикл подходящим инвариантом, который нужно использовать для доказательства этой реализации? Подвопрос 3b: Если да, то как правильно доказать это?Внешне доказать лемму и использовать это?Если это так, что именно это означает?

На этом этапе я подумал, что исследую совершенно другую реализацию, просто чтобы посмотреть, приведет ли она к чему-то другому:

package body Util is
   function Floor_Log2 (X : Positive) return Natural is
   begin
      for I in 1 .. X'Size - 1 loop
         if 2**I > X then
            return I - 1;
         end if;
      end loop;
      return X'Size - 1;
   end Floor_Log2;
end Util;

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

Идея состояла в том, чтобы обойти некоторые доказательства вокруг переполнения I и условий завершения, сделав окончание и диапазоны явными.К моему удивлению, испытатель сначала подавился переполнением, проверяя выражение 2**I.Я ожидал, что 2**(X'Size - 1) будет доказуемо в пределах X - но, опять же, я столкнулся с пределами моего знания Ады. Подвопрос 4: Является ли это выражение на самом деле свободным от переполнения в Аде, и как это можно доказать?

Это оказалось длинным вопросом ... но я думаю, чтоВопросы, которые я поднимаю, в контексте почти тривиального примера, являются относительно общими и, вероятно, будут полезны для других, которые, как и я, пытаются понять, относится ли Spark к ним и насколько они важны.

Ответы [ 3 ]

0 голосов
/ 07 апреля 2019

Эта реализация подтверждает все проверки внутри тела, но предварительные условия все еще не доказаны:

package body Util is
    pragma SPARK_Mode (On);

    function Floor_Log2 (X : Positive) return Natural is
       I : Natural := 30;
       Prod : Natural := 2**30;
       type Large_Natural is range 0 .. 2**31;
       Prev_Prod : Large_Natural := Large_Natural'Last with Ghost;
    begin
       while I > 0 loop
          if X >= Prod then
             pragma Assert (Large_Natural (X) < Prev_Prod);
             return I;
          end if;
          pragma Loop_Invariant (I > 0);
          pragma Loop_Invariant (Prod >= X and Prev_Prod >= Large_Natural (X));
          --  pragma Loop_Invariant (2**I > X);
          Prod := Prod / 2;
          I := I - 1;
       end loop;

       pragma Assert (I = 0);

       return 0;
    end Floor_Log2;

end Util;

Это дает следующий вывод с gnatprove:

gnatprove -P/Users/pnoffke/projects/ada/spark/floor_log2/floor_log2.gpr -j0 --ide-progress-bar -u util.adb --level=2 --report=all
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
util.adb:10:13: info: initialization of "I" proved
util.adb:11:18: info: initialization of "Prod" proved
util.adb:12:28: info: assertion proved
util.adb:12:48: info: initialization of "Prev_Prod" proved
util.adb:13:20: info: initialization of "I" proved
util.adb:15:33: info: initialization of "I" proved
util.adb:15:33: info: loop invariant preservation proved
util.adb:15:33: info: loop invariant initialization proved
util.adb:16:33: info: initialization of "Prod" proved
util.adb:16:33: info: loop invariant preservation proved
util.adb:16:33: info: loop invariant initialization proved
util.adb:16:47: info: initialization of "Prev_Prod" proved
util.adb:18:18: info: initialization of "Prod" proved
util.adb:18:23: info: division check proved
util.adb:19:15: info: initialization of "I" proved
util.adb:19:17: info: range check proved
util.adb:22:22: info: initialization of "I" proved
util.adb:22:22: info: assertion proved
util.ads:5:15: info: overflow check proved
util.ads:5:44: medium: postcondition might fail, cannot prove X / 2 < 2**Floor_Log2'result (e.g. when Floor_Log2'Result = 0 and X = 2)
util.ads:5:46: info: division check proved
util.ads:5:53: medium: overflow check might fail (e.g. when Floor_Log2'Result = 30)

Я непонять, почему gnatprove не может доказать прокомментированное Loop_Invariant.Если я попытаюсь это сделать, я получу следующий дополнительный вывод:

util.adb:17:33: medium: loop invariant might fail after first iteration, cannot prove 2**I > X (e.g. when I = 0 and X = 0)
util.adb:17:33: medium: loop invariant might fail in first iteration, cannot prove 2**I > X (e.g. when I = 30 and X = 1)
util.adb:17:34: medium: overflow check might fail (e.g. when I = 0)

В контрпримере написано "when I = 0 and X = 0", но I не может быть 0 для первого Loop_Invariant.

Также, если я инициализирую Prod на 2**I вместо 2**30, я получаю:

util.adb:6:26: medium: overflow check might fail (e.g. when I = 30 and Prod = 0)

Я подозреваю, что у gnatprove есть некоторые фундаментальные проблемы с оператором **.Я надеялся использовать Prev_Prod, чтобы подтвердить ваши предварительные условия, но я не понимаю, как справиться с вышеуказанными проблемами, с которыми я столкнулся.

0 голосов
/ 16 июля 2019

Хотя этому вопросу уже более 6 месяцев, я думаю, что он все еще интересен, поэтому вот моя (поздняя) ставка: -).

Предотвращение переполнения в состоянии после публикации

Учитывая оригинальную функциюподпись

function Floor_Log2 (X : Positive) return Natural with
   Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);  

Я заметил, что мне нужно ограничить домен X, чтобы предотвратить переполнение во втором слагаемом условия публикации.Учитывая определения в Standard.ads, то есть

type Integer is range -(2**31) .. +(2**31 - 1);
for Integer'Size use 32;

subtype Natural  is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;

, я заключаю, что для предотвращения переполнения

X < 2**(Floor_Log2'Result + 1) <= 2**31 - 1

и, следовательно, X <= 2**30 - 1.Поэтому я изменил сигнатуру функции на:

subtype Pos is Positive range 1 .. 2**30 - 1

function Floor_Log2 (X : Pos) return Natural with
  Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);

Первый подход

В принципе, теперь я мог бы доказать условие публикации в GNAT CE 2019 следующим образом (обратите внимание, что я использую другой алгоритмпо сравнению с указанным в вопросе):

util.ads

package Util with SPARK_Mode is

   subtype Pos is Positive range 1 .. 2**30 - 1

   function Floor_Log2 (X : Pos) return Natural with
     Post => 2**Floor_Log2'Result <= X and then X < 2**(Floor_Log2'Result + 1);

end Util;

util.adb

package body Util with SPARK_Mode is  

   ----------------
   -- Floor_Log2 --
   ----------------

   function Floor_Log2 (X : Pos) return Natural is      
      L : Positive := 1;
      H : Positive := L * 2;
      I : Natural  := 0;
   begin

      while not (L <= X and then X < H) loop

         pragma Loop_Invariant
           (L = 2 ** I and H = 2 ** (I+1));

         pragma Loop_Invariant
           (for all J in 0 .. I =>
               not (2 ** J <= X and then X < 2 ** (J+1)));

         L := H;         
         H := H * 2;    
         I := I + 1;

      end loop;

      return I;

   end Floor_Log2;  

end Util;

К сожалению, однако, испытатели испытывают трудности с нелинейной арифметикой (то есть возведением в степень), и все сеансы доказательства (на моем компьютере) заканчиваются тайм-аутом.Фактически, если я запускаю gnatprove с уровнем усилия 0, то я могу проверить состояние поста только тогда, когда ограничиваю верхнюю границу Pos до 2**7 - 1, то есть

subtype Pos is Positive range 1 .. 2**7 - 1;

Увеличение усилияУровень (или тайм-аут) позволяет мне проверить состояние поста для больших значений Pos'Last.

Второй подход

Чтобы обойти ограничение доказателей, я применил небольшой трюкпереопределив функцию возведения в степень.Затем я мог бы использовать следующий код, чтобы доказать условие публикации для всего диапазона Pos, когда я запускаю gnatprove с уровнем усилия 1:

spark_exp.ads

generic
   type Int is range <>;   
   Base  : Int;
   N_Max : Natural;
package SPARK_Exp with SPARK_Mode is

   subtype Exp_T is Natural range 0 .. N_Max;

   function Exp (N : Exp_T) return Int with Ghost;

private

   type Seq_T is array (Exp_T range <>) of Int;

   function Exp_Seq return Seq_T with
     Ghost,
     Post =>  (Exp_Seq'Result'First = 0)
     and then (Exp_Seq'Result'Last  = N_Max)
     and then (Exp_Seq'Result (0) = 1)
     and then (for all I in 1 .. N_Max =>
                 Exp_Seq'Result (I) = Base * Exp_Seq'Result (I - 1) and
                   Int'First < Exp_Seq'Result (I) and Exp_Seq'Result (I) < Int'Last);

   function Exp (N : Exp_T) return Int is (Exp_Seq (N));   

end SPARK_Exp;

spark_exp.adb

package body SPARK_Exp with SPARK_Mode is

   -------------
   -- Exp_Seq --
   -------------

   function Exp_Seq return Seq_T is
      S : Seq_T (Exp_T'Range) := (others => 1);
   begin

      for I in 1 .. N_Max loop

         pragma Loop_Invariant
           (for all J in 1 .. I - 1 =>
              S (J) = Base * S (J - 1) and
                (Int'First / Base) < S (J) and S (J) < (Int'Last / Base));

         S (I) := Base * S (I - 1);

      end loop;

      return S;

   end Exp_Seq;

end SPARK_Exp;

util.ads

with SPARK_Exp;

package Util with SPARK_Mode is

   subtype Pos is Positive range 1 .. 2**30 - 1;


   package SPARK_Exp_2 is
     new SPARK_Exp (Positive, 2, 30);

   function Exp2 (N : SPARK_Exp_2.Exp_T) return Positive
     renames SPARK_Exp_2.Exp;   


   function Floor_Log2 (X : Pos) return Natural with
     Post => (Exp2 (Floor_Log2'Result) <= X) and then
                (X < Exp2 (Floor_Log2'Result + 1));

end Util;

util.adb

package body Util with SPARK_Mode is

   ----------------
   -- Floor_Log2 --
   ----------------

   function Floor_Log2 (X : Pos) return Natural is      
      L : Positive := 1;
      H : Positive := L * 2;
      I : Natural  := 0;
   begin

      while not (L <= X and then X < H) loop

         pragma Loop_Invariant
           (L = Exp2 (I) and H = Exp2 (I + 1));

         pragma Loop_Invariant
           (for all J in 0 .. I =>
               not (Exp2 (J) <= X and then X < Exp2 (J + 1)));

         L := H;         
         H := H * 2;    
         I := I + 1;

      end loop;

      return I;

   end Floor_Log2;

end Util;
0 голосов
/ 16 декабря 2018

Я не могу помочь с вашими вопросами SPARK, но я могу ответить на некоторые ваши подвопросы.

Подвопрос 1: Поскольку вы используете "<" для Integer, подвыражениетакже будет иметь тип Integer.Для Positive'Last (2 ** 31 - 1 с GNAT) результат вашей функции должен быть 30, а подвыражение будет переполнено.(Это с точки зрения SPARK; компиляторам разрешается использовать более ранжированные типы при оценке выражений для получения математически / логически правильного результата, даже если подвыражение будет переполнено, и GNAT сделает это для некоторых значений -gnato.)

Подвопрос 4: 2 ** (X'Size - 1) может переполниться.Причина связана с двумя значениями 'Size: Positive'Size - это минимальное количество бит, необходимое для хранения значения подтипа Positive;X'Size - это фактическое количество бит, выделенных для X. Поскольку вы используете GNAT,

Integer'Last = Positive'Last = 2 ** 31 - 1.X'Size = 32.Positive'Size = 31.

Итак, 2 ** (X'Size - 1) = 2 ** 31 > Positive'Last.Возможно, вы захотите использовать Positive'Size вместо X'Size.

(Опять же, с точки зрения SPARK; компиляторам разрешено получать логически правильный результат.)

В стороне:Формы короткого замыкания and then и or else следует использовать только тогда, когда они действительно необходимы.Современные процессоры выполняют все виды оптимизаций на уровне машинного кода, которые должны быть отключены для оценки короткого замыкания.Хотя они могут выглядеть как оптимизация, на практике они часто бывают противоположными.

HTH.

(Возможно, вы захотите пометить это как [ada]. Я видел это только потому, что вы ссылались на него вclada.)

...