%% leanify.sty -- a LaTeX article checked by Leanify.
%% Copyright (C) 2026 Marcus Rockel.
%% Licensed under the Apache License, Version 2.0
%% (https://www.apache.org/licenses/LICENSE-2.0).
%% Home: https://github.com/Corrram/leanify
%%
%% Leanify reads the article source itself: its sentences, formulas and the
%% macros of its preamble. This package does not change what the article says,
%% and Leanify reads no markup from it. It
%%   * loads the packages for the notation the grammar reads (\coloneqq from
%%     mathtools, \mathbb and \le from amssymb);
%%   * runs Leanify while the article compiles, when it can: if the compiler
%%     may run programs (shell escape) and the article lies in a Lean project
%%     (a lakefile in its folder or up to three folders above), every compile
%%     runs the quick check, and its findings appear as warnings at their lines
%%     of the article, where editors list them. With `certify`, it also updates
%%     the pin and builds the certificate, so that the margin marks are current;
%%     the first time, it writes the Lean files of the article. Without shell
%%     escape, as on arXiv, nothing runs;
%%   * with the option `status`, marks every labelled result in the margin with
%%     its verification status, read from \jobname.leanify.tex, which
%%     #leanify_certify writes next to the article. Without the option the
%%     typeset article is unchanged (use it for journal submission);
%%   * records the Lean module that certifies the article (\leanifymodule).
%%
%% Usage, in the preamble:
%%   \usepackage[status]{leanify}               % marks from the last build
%%   \usepackage[certify, vocab=unitsquare]{leanify}  % build on every compile
%% Options: status, certify (implies status), check=false (no quick check),
%% vocab=core|unitsquare (the vocabulary of the article), lean=FILE (its
%% compiled Lean file, default Compiled.lean next to the article).
%% Compile with shell escape (pdflatex -shell-escape, or the editor's setting)
%% and with `lake` on the PATH. Leanify's output is in \jobname.leanify.log.
%% Optionally, \leanifyreport prints a one-line summary where it appears.
\NeedsTeXFormat{LaTeX2e}[2022/06/01]
\ProvidesPackage{leanify}[2026/11/06 v0.3 Leanify checks and status marks]
\RequirePackage{mathtools}
\RequirePackage{amssymb}

\ExplSyntaxOn
\bool_new:N \l__leanify_status_bool
\bool_new:N \l__leanify_check_bool
\bool_new:N \l__leanify_certify_bool
\str_new:N \l__leanify_vocab_str
\str_new:N \l__leanify_lean_str
\keys_define:nn { leanify }
  {
    status  .bool_set:N = \l__leanify_status_bool ,
    check   .bool_set:N = \l__leanify_check_bool ,
    check   .initial:n  = true ,
    certify .code:n     =
      {
        \bool_set_true:N \l__leanify_certify_bool
        \bool_set_true:N \l__leanify_status_bool
      } ,
    certify .value_forbidden:n = true ,
    vocab   .str_set:N  = \l__leanify_vocab_str ,
    lean    .str_set:N  = \l__leanify_lean_str ,
    lean    .initial:n  = Compiled.lean ,
  }
\ProcessKeyOptions [ leanify ]
\ExplSyntaxOff

% The Lean module whose #leanify_certify checks this article.
\newcommand*\leanifymodule[1]{\gdef\leanify@module{#1}}
\newcommand*\leanify@module{}

% Entries of the status file.
\newcommand*\leanifystatus[2]{\expandafter\gdef\csname leanify@st@#1\endcsname{#2}}
\newcommand*\leanifysummary[6]{%
  \gdef\leanify@summary{#2 of #1 claims verified (#3 derived in the text from
    their citations); #5 of #6 sentences compiled; #4
    \ifnum#4=1 definition\else definitions\fi\ well defined}}
\newcommand*\leanify@summary{no status file (build the Lean module first)}
\newcommand*\leanifyreport{\textsf{Leanify:} \leanify@summary.}

% The margin marks.
\newcommand*\leanify@markof[1]{%
  \ifcsname leanify@mk@#1\endcsname\csname leanify@mk@#1\endcsname\fi}
\newcommand*\leanify@mk@text{$\checkmark$\,derived}
\newcommand*\leanify@mk@verified{$\checkmark$\,checked}
\newcommand*\leanify@mk@partial{$\circ$\,partly checked}
\newcommand*\leanify@mk@compiled{$\circ$\,compiled}
\newcommand*\leanify@mk@refuted{$\times$\,false}
\newcommand*\leanify@mark[1]{%
  \ifcsname leanify@st@#1\endcsname
    \edef\leanify@st{\csname leanify@st@#1\endcsname}%
    \ifx\leanify@st\leanify@none\else
      \ifmmode\else\leavevmode\marginpar{\raggedright\footnotesize\sffamily
        \leanify@markof{\leanify@st}}\fi
    \fi
  \fi}
\def\leanify@none{none}

% Running Leanify during the compile.
\ExplSyntaxOn
\str_new:N \l__leanify_root_str
\str_new:N \l__leanify_job_str
\str_new:N \l__leanify_line_str
\str_new:N \l__leanify_first_str
\str_new:N \l__leanify_ns_str
\str_new:N \l__leanify_module_str
\seq_new:N \l__leanify_match_seq
\bool_new:N \l__leanify_report_bool
\bool_new:N \l__leanify_built_bool
\bool_new:N \l__leanify_initialized_bool
\ior_new:N \g__leanify_ior
\str_set:Nx \l__leanify_job_str { \c_sys_jobname_str }
\str_remove_all:Nn \l__leanify_job_str { " }
\str_const:Nx \c__leanify_article_str { \l__leanify_job_str .tex }
\str_const:Nx \c__leanify_log_str { \l__leanify_job_str .leanify.log }
\str_const:Nx \c__leanify_status_str { \l__leanify_job_str .leanify.tex }
\str_new:N \l__leanify_redirect_str
\str_set:Nn \l__leanify_redirect_str { > }
\seq_new:N \l__leanify_lib_seq
\int_new:N \l__leanify_seen_int
\int_new:N \l__leanify_lineno_int
\prg_generate_conditional_variant:Nnn \regex_extract_once:NnN { NV } { T , TF }
\prg_generate_conditional_variant:Nnn \regex_match:Nn { NV } { T }
\cs_generate_variant:Nn \msg_warning:nnnn { nnee }
\cs_generate_variant:Nn \msg_warning:nnn { nne }
\cs_generate_variant:Nn \msg_warning:nnnnnn { nneeee }
\cs_new_protected:Npn \__leanify_summary:n #1 { \typeout { Leanify: ~ #1 } }
\cs_generate_variant:Nn \__leanify_summary:n { e }
\cs_generate_variant:Nn \iow_term:n { e }

% A finding at line #1 of the article. \PackageWarningNoLine keeps `on input line
% N' on one line of the log, where editors look for it (l3msg would wrap it).
\cs_new_protected:Npn \__leanify_finding:nn #1#2
  { \PackageWarningNoLine { leanify } { #2 ~ on ~ input ~ line ~ #1 } }
\cs_generate_variant:Nn \__leanify_finding:nn { ee }
\msg_new:nnn { leanify } { lean } { #1 }
\msg_new:nnn { leanify } { no-shell }
  {
    Leanify ~ did ~ not ~ run, ~ because ~ shell ~ escape ~ is ~ off. ~
    Compile ~ with ~ pdflatex ~ -shell-escape ~ (or ~ turn ~ shell ~ escape ~
    on ~ in ~ the ~ editor) ~ to ~ check ~ the ~ article ~ as ~ it ~ compiles, ~
    or ~ load ~ the ~ package ~ with ~ check=false ~ to ~ silence ~ this
  }
\msg_new:nnn { leanify } { no-report }
  {
    Leanify ~ gave ~ no ~ report ~ on ~ the ~ article. ~ Is ~ lake ~ on ~
    the ~ PATH? ~ Its ~ output ~ (in ~ \c__leanify_log_str) ~ begins: ~ #1
  }
\msg_new:nnn { leanify } { initialized }
  {
    Leanify ~ wrote ~ the ~ Lean ~ files ~ of ~ the ~ article. ~ Add ~ the ~
    library ~ to ~ lakefile.toml ~ (once), ~ then ~ compile ~ again:
    \\ #1 \\ #2 \\ #3 \\ #4
  }
\msg_new:nnn { leanify } { build-failed }
  { The ~ certificate ~ did ~ not ~ build ~ (see ~ \c__leanify_log_str) }
\msg_new:nnn { leanify } { stale }
  {
    \c__leanify_article_str \ changed ~ after ~ its ~ certificate ~ was ~
    built: ~ the ~ margin ~ marks ~ may ~ be ~ out ~ of ~ date. ~ Rebuild, ~ or ~
    load ~ the ~ package ~ with ~ certify
  }

% The Lean project: the nearest folder, up to three above, with a lakefile.
\cs_new_protected:Npn \__leanify_find_root:
  {
    \str_clear:N \l__leanify_root_str
    \clist_map_inline:nn { . , .. , ../.. , ../../.. }
      {
        \file_if_exist:nTF { ##1 / lakefile.toml }
          { \str_set:Nn \l__leanify_root_str {##1} \clist_map_break: }
          {
            \file_if_exist:nT { ##1 / lakefile.lean }
              { \str_set:Nn \l__leanify_root_str {##1} \clist_map_break: }
          }
      }
  }

% `lake -d ROOT ARGS`, its output and errors into the log: the first command
% of the compile starts the log, the others add to it.
\cs_new_protected:Npn \__leanify_lake:n #1
  {
    \sys_shell_now:e
      {
        lake ~ -d ~ " \l__leanify_root_str " ~ #1 ~ \l__leanify_redirect_str ~
        " \c__leanify_log_str " ~ 2>&1
      }
    \str_set:Nn \l__leanify_redirect_str { >> }
  }
\cs_new:Npn \__leanify_vocab:
  { \str_if_empty:NF \l__leanify_vocab_str { ~ --vocab ~ \l__leanify_vocab_str } }

% Read the lines that the last command added to the log: findings of the check
% as warnings at their lines, Lean errors and refutations as warnings, and the
% summaries on the terminal.
\regex_const:Nn \c__leanify_finding_regex
  { \A .*? \.tex : (\d+) : \d+ : \s* (?: error | warning ) : \s* (.*) \Z }
\regex_const:Nn \c__leanify_counts_regex { \A .*? \.tex : \s+ (\d+ \s definitions .*) \Z }
\regex_const:Nn \c__leanify_all_regex { \A \s* all : \s* (.*) \Z }
\regex_const:Nn \c__leanify_lean_error_regex { \A error : \s* (.* \.lean : \d+ : \d+ : .*) \Z }
\regex_const:Nn \c__leanify_certified_regex
  { \A info : \s* .*? \.lean : \d+ : \d+ : \s* (\d+ \s claims : .*?) (?: \. \s* Axioms .* )? \Z }
\regex_const:Nn \c__leanify_false_regex
  { \A info : \s* .*? \.lean : \d+ : \d+ : \s* (claim \s \d+ \s is \s false .*) \Z }
\regex_const:Nn \c__leanify_built_regex { \A Build \s completed \s successfully }
\regex_const:Nn \c__leanify_lib_regex
  { \A (?: \[\[ lean_lib \]\] | name \s = .* | srcDir \s = .* | globs \s = .* ) \Z }

\cs_new_protected:Npn \__leanify_read_log:
  {
    \bool_set_false:N \l__leanify_report_bool
    \str_clear:N \l__leanify_first_str
    \int_zero:N \l__leanify_lineno_int
    \ior_open:NnT \g__leanify_ior { \c__leanify_log_str }
      {
        \ior_str_map_inline:Nn \g__leanify_ior
          {
            \int_incr:N \l__leanify_lineno_int
            \int_compare:nNnT { \l__leanify_lineno_int } > { \l__leanify_seen_int }
              {
                \str_set:Nn \l__leanify_line_str {##1}
                \regex_replace_all:nnN { \r } { } \l__leanify_line_str
                \str_if_empty:NF \l__leanify_line_str
                  { \__leanify_read_line: }
              }
          }
        \ior_close:N \g__leanify_ior
      }
    \int_set_eq:NN \l__leanify_seen_int \l__leanify_lineno_int
  }
\cs_new_protected:Npn \__leanify_read_line:
  {
    \str_if_empty:NT \l__leanify_first_str
      { \str_set_eq:NN \l__leanify_first_str \l__leanify_line_str }
    \regex_extract_once:NVNTF \c__leanify_finding_regex \l__leanify_line_str
      \l__leanify_match_seq
      {
        \__leanify_finding:ee
          { \seq_item:Nn \l__leanify_match_seq { 2 } }
          { \seq_item:Nn \l__leanify_match_seq { 3 } }
      }
      {
        \regex_extract_once:NVNT \c__leanify_counts_regex \l__leanify_line_str
          \l__leanify_match_seq
          {
            \bool_set_true:N \l__leanify_report_bool
            \__leanify_summary:e { \seq_item:Nn \l__leanify_match_seq { 2 } }
          }
        \regex_extract_once:NVNT \c__leanify_all_regex \l__leanify_line_str
          \l__leanify_match_seq
          {
            \__leanify_summary:e
              { sentences: ~ \seq_item:Nn \l__leanify_match_seq { 2 } }
          }
        \regex_extract_once:NVNT \c__leanify_lean_error_regex \l__leanify_line_str
          \l__leanify_match_seq
          { \msg_warning:nne { leanify } { lean } { \seq_item:Nn \l__leanify_match_seq { 2 } } }
        \regex_extract_once:NVNT \c__leanify_false_regex \l__leanify_line_str
          \l__leanify_match_seq
          { \msg_warning:nne { leanify } { lean } { \seq_item:Nn \l__leanify_match_seq { 2 } } }
        \regex_extract_once:NVNT \c__leanify_certified_regex \l__leanify_line_str
          \l__leanify_match_seq
          {
            \bool_set_true:N \l__leanify_report_bool
            \__leanify_summary:e { \seq_item:Nn \l__leanify_match_seq { 2 } }
          }
        \regex_match:NVT \c__leanify_built_regex \l__leanify_line_str
          { \bool_set_true:N \l__leanify_built_bool }
        \regex_match:NVT \c__leanify_lib_regex \l__leanify_line_str
          { \seq_put_right:NV \l__leanify_lib_seq \l__leanify_line_str }
      }
  }

% The quick check: about a second (the first run also builds the checker).
\cs_new_protected:Npn \__leanify_check:
  {
    \__leanify_summary:e { checking ~ \c__leanify_article_str }
    \__leanify_lake:n
      { exe ~ leanify ~ check ~ " \c__leanify_article_str " \__leanify_vocab: }
    \__leanify_read_log:
    \bool_if:NF \l__leanify_report_bool
      { \msg_warning:nne { leanify } { no-report } { \l__leanify_first_str } }
  }

% The namespace of the article: \leanifymodule, or the one its Lean file
% compiles (`#leanify_article NS "article.tex" ...`).
\regex_const:Nn \c__leanify_ns_regex { \A \s* \#leanify_article \s+ (\S+) \s }
\cs_new_protected:Npn \__leanify_module:
  {
    \str_set:Nx \l__leanify_module_str { \leanify@module }
    \str_if_empty:NT \l__leanify_module_str
      {
        \str_clear:N \l__leanify_ns_str
        \ior_open:NnT \g__leanify_ior { \l__leanify_lean_str }
          {
            \ior_str_map_inline:Nn \g__leanify_ior
              {
                \regex_extract_once:NnNT \c__leanify_ns_regex {##1} \l__leanify_match_seq
                  {
                    \str_set:Nx \l__leanify_ns_str { \seq_item:Nn \l__leanify_match_seq { 2 } }
                    \ior_map_break:
                  }
              }
            \ior_close:N \g__leanify_ior
          }
        \str_if_empty:NF \l__leanify_ns_str
          { \str_set:Nx \l__leanify_module_str { \l__leanify_ns_str .Certified } }
      }
  }

% Certify: write the Lean files the first time, then update the pin and build.
\cs_new_protected:Npn \__leanify_certify:
  {
    \file_if_exist:nTF { \l__leanify_lean_str }
      {
        \__leanify_module:
        \__leanify_summary:e
          {
            building ~ the ~ certificate ~
            (lake ~ build ~ \l__leanify_module_str)
          }
        \__leanify_lake:n
          {
            exe ~ leanify ~ prepare ~ " \c__leanify_article_str " ~
            --lean ~ " \l__leanify_lean_str "
          }
        \__leanify_lake:n { build ~ \l__leanify_module_str }
        \bool_set_false:N \l__leanify_built_bool
        \__leanify_read_log:
        \bool_if:NF \l__leanify_built_bool
          { \msg_warning:nn { leanify } { build-failed } }
      }
      {
        \__leanify_lake:n
          { exe ~ leanify ~ init ~ " \c__leanify_article_str " \__leanify_vocab: }
        \seq_clear:N \l__leanify_lib_seq
        \__leanify_read_log:
        \msg_warning:nneeee { leanify } { initialized }
          { \seq_item:Nn \l__leanify_lib_seq { 1 } }
          { \seq_item:Nn \l__leanify_lib_seq { 2 } }
          { \seq_item:Nn \l__leanify_lib_seq { 3 } }
          { \seq_item:Nn \l__leanify_lib_seq { 4 } }
        \bool_set_true:N \l__leanify_initialized_bool
      }
  }

\cs_new_protected:Npn \__leanify_run:
  {
    \__leanify_find_root:
    \str_if_empty:NF \l__leanify_root_str
      {
        \bool_lazy_or:nnT { \l__leanify_check_bool } { \l__leanify_certify_bool }
          {
            \sys_if_shell_unrestricted:TF
              {
                \bool_if:NT \l__leanify_check_bool { \__leanify_check: }
                \bool_if:NT \l__leanify_certify_bool { \__leanify_certify: }
              }
              { \msg_warning:nn { leanify } { no-shell } }
          }
      }
  }

% The marks of a status file older than the article may be out of date.
\cs_new_protected:Npn \__leanify_if_stale:
  {
    \bool_if:NF \l__leanify_built_bool
      {
        \file_if_exist:nT { \c__leanify_status_str }
          {
            \file_compare_timestamp:nNnT
              { \c__leanify_article_str } > { \c__leanify_status_str }
              { \msg_warning:nn { leanify } { stale } }
          }
      }
  }

\AtBeginDocument
  {
    \__leanify_run:
    \bool_if:NT \l__leanify_status_bool
      {
        \file_if_exist:nTF { \c__leanify_status_str }
          {
            \file_input:n { \c__leanify_status_str }
            \__leanify_if_stale:
          }
          {
            \bool_if:NF \l__leanify_initialized_bool
              {
                \PackageWarningNoLine { leanify }
                  { No ~ status ~ file ~ \c__leanify_status_str }
              }
          }
      }
  }
\ExplSyntaxOff

% wrap \label after every package has set it up (hyperref redefines it)
\ExplSyntaxOn
\AddToHook { begindocument/end }
  {
    \bool_if:NT \l__leanify_status_bool
      {
        \cs_set_eq:NN \leanify@label \label
        \renewcommand* \label [1] { \leanify@label {#1} \leanify@mark {#1} }
      }
  }
\ExplSyntaxOff
\endinput
