Данная статья является переводом моей статьи: Formalization of code in Coq - tactics, написанной в период работы над проектом coq-tezos-of-ocaml.

Суть проекта: часть исходного кода протокола криптовалюты Tezos была переведена на Coq, а затем верифицирована с помощью математических методов и формальной логики.

Статья предназначена для круга читателей, имеющих опыт работы с Coq.

Часто процесс формальной верификации становится утомительным и монотонным из за повторяющихся действий, которые пруф-инженеру (от английского слова proof - доказательство) приходится выполнять. К счастью, в Coq есть инструменты и техники автоматизации доказательств, которые позволяют не только значительно увеличить производительность, но и сделать доказательства короче.

В данной статье мы покажем, как использовать и писать тактики на примере формальной верификации исходного кода криптовалюты Tezos (написанной на OCaml), а именно, мы затронем следующие темы:

  • презентация некоторых функций Ltac (язык для написания тактик, встроенный в Coq),

  • рекурсивный поиск доказательств

  • встроенные тактики Coq

  • создание собственных тактик

Ltac (язык для написания тактик, встроенный в Coq)

Ltac позволяет писать тактики, которые можно использовать в доказательствах лемм и теорем.

Рассмотрим лемму:

Lemma unparse_ty_parse_ty (loc_value : Alpha_context.Script.location)
         (ctxt : Alpha_context.context)
         (ty : Script_typed_ir.ty)
         (legacy allow_lazy_storage allow_operation allow_contract
     allow_ticket : bool) :
  match (unparse_ty loc_value ctxt ty) with
  | Pervasives.Ok (mich_node, ctxt1) =>
      match parse_ty ctxt1 legacy allow_lazy_storage allow_operation
                     allow_contract allow_ticket mich_node with
      | Pervasives.Ok ((Ex_ty ty1), ctxt1) => ty = ty1
      | Pervasives.Error _ => True
      end
  | Pervasives.Error _ => True
  end.

В этой лемме мы доказываем совместимость двух функций: unparse_ty и parse_ty. Рассмотрим лемму детально. Один из параметров — это тип ty. Это довольно громоздкий тип, содержащий 32 конструктора. Чтобы доказать нашу лемму, нам необходимо доказать ее для каждого из конструкторов ty. Итого, разбивая ty командой destruct, мы имеем к доказательству 32 под-цели.

Некоторые из конструкторов типа ty очень похожи между собой, а именно, это 12 конструкторов, принимающих лишь один аргумент ty_metadata:

  | Unit_t : Script_typed_ir.ty_metadata -> Script_typed_ir.ty
  | Int_t : Script_typed_ir.ty_metadata -> Script_typed_ir.ty
  | Nat_t : Script_typed_ir.ty_metadata -> Script_typed_ir.ty
  ...

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

С тем, чтобы автоматизировать решение всех 12 под-целей, мы создаем тактику destr_if и вызываем ее в теле нашего доказательства.

Что мы получаем:

  • код становится значительно короче

  • код становится читаемым

  • мы избегаем повторений в теле доказательства леммы

Ниже приведена тактика destr_if, написанная на Ltac:

Ltac destr_if :=
  match goal with
  | |- match (let? ' _ := Alpha_context.Gas.consume _ _ in _) with _ => _ end =>
      destruct Alpha_context.Gas.consume; [ simpl in * | simpl; trivial]
  | |- match (if ?cond then _ else _) with _ => _ end => 
      destruct cond; simpl; auto
  | |- match (let? ' _ := Script_ir_annot.check_type_annot _ _ in _) with _ =>
       _ end => destruct Script_ir_annot.check_type_annot;
       [ simpl in * | simpl; trivial]
  | |- ?a _ = ?b => unfold b; subst; reflexivity
  end.

Рекурсивный поиск доказательств

Итак, наша тактика может решить 12 целей. Нам нужно вызвать ее 12 раз? Вовсе нет. В доказательстве леммы мы вызываем ее лишь однажды. Рассмотрим приведенный ниже кусочек кода, который автоматически решает все 12 под-целей.

Proof.
  intro H.
  destruct ty eqn:TY; simpl;
    repeat destr_if.

Здесь мы использовали комбинатор тактик repeat. Приведенный выше код (комбинация тактик) пройдет циклом по всем под-целям, пытаясь применить тактики из списка к каждой вновь-сгенерированной цели (заходя, также, и в под-под-цели и т.д., пытаясь их решить с помощью списка тактик). Условие перехода на следующий шаг цикла - это ситуация, когда тактика не может продвинуться в решении очередной цели. При работе с комбинатором repeat следует соблюдать осторожность: если запустить его в купе с тактиками, которые срабатывают всегда, вам, скорее всего, придется перезагружать компьютер. Тактика auto, например, срабатывает всегда (даже если ее применение ничего не меняет). Запуск этой тактики в цикле приведет к проблемам.

Встроенные тактики

Мы можем улучшить нашу тактику destr_if, дополнив ее тактиками, встроенными в систему Coq. В цикле, когда одна из применяемых тактик не срабатывает на каком-либо узле дерева под-целей, к текущей цели автоматически применяется следующая тактика из заданного нами списка, и так далее.

Вот несколько полезных тактик, встроенных в Coq, комбинациями которых можно дополнить собственные тактики или использовать в доказательствах отдельно: lia, nia, intuition, trivial, congruence, ring и т.д.

Иногда для решения конкретных задач, на языке Ltac, программистами на Coq разрабатываются тактики, которые могут давать ощущение, что любая цель в доказательстве может быть решена с их помощью автоматически. И хоть чувство это обманчиво, программисты любят давать своим тактикам говорящие названия: hammer, crush, best…

Заключение

Тактики в Coq — это функции, которые автоматически ищут доказательства и широко применяются в формальной верификации. Мы можем использовать встроенные «родные» тактики Coq, или писать собственные, если нам нужен более высокий уровень автоматизации. Тактики пишутся на встроенном языке Ltac и настраиваются для решения конкретных задач. Во время работы над большим проектом, рекомендуется для разработанных тактик выделить отдельную библиотеку. Узко-специализированные тактики, которые используются только однажды, рекомендуется писать непосредственно над Леммой, в которой эти тактики используются (в том же файле, без вынесения их в библиотеку тактик).

Комментарии (4)


  1. Kakadout
    18.07.2023 16:42
    +1

    Для начинающегося кокера это выглядит очень загадочно. Было бы лучше:

    1) Если был бы дистиллированный пример с теоремой и большим количеством однотипных конструкторов.

    2) Если было бы очевидно, в каком из 4х вариантов тактики destr_if прячутся все эти однотипные 12 вариантов. Я предполагаю, что в последнем, но совсем не уверен.


    1. 0xd34df00d
      18.07.2023 16:42
      +1

      Книжку Certified Programming with Dependent Types для начинающих кокеров определённо можно рекомендовать. Не уверен, что создание и эффективное применение тактик вообще можно впихнуть в статью.


      1. Natasha_Klaus Автор
        18.07.2023 16:42

        Отнюдь. Certified Programming with Dependent Types сложновата. Я бы начинающим рекомендовала Software Foundations -> Logical Foundations (первая часть). Там рассматриваются азы.


    1. Natasha_Klaus Автор
      18.07.2023 16:42

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

      Я уж и сама не помню, где там в destr_if прячутся однотипные варианты, это нужно разбираться, отрывать код.