diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb2c04ec3..dc953e178 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,7 +161,7 @@ jobs: - name: Generate HTML (non-release) if: github.event_name != 'release' run: | - lake --quiet exe generate-manual --depth 2 --with-word-count "words.txt" --verbose --without-html-single + ./generate-html.sh --mode preview - name: Generate HTML (release) if: github.event_name == 'release' @@ -172,11 +172,16 @@ jobs: run: | scripts/check-examples-isolated.sh + - name: Verify tutorial project zip files build + if: github.event_name != 'release' + run: | + scripts/check-tutorial-zips.sh + - name: Generate proofreading HTML if: github.event_name == 'pull_request' id: generate run: | - lake --quiet exe generate-manual --depth 2 --verbose --draft --without-html-single --output "_out/draft" + ./generate-html.sh --mode preview --draft --output _out/draft-site - name: Report status to Lean PR if: always() && github.repository == 'leanprover/reference-manual' diff --git a/.github/workflows/overlay.yml b/.github/workflows/overlay.yml index b304c9ae9..d1edfb37c 100644 --- a/.github/workflows/overlay.yml +++ b/.github/workflows/overlay.yml @@ -1,5 +1,12 @@ name: Apply Overlays +# This workflow applies HTML overlays (canonical URLs, robots noindex, metadata.js) to +# deployed content and pushes to the postdeploy branches. +# +# In normal operation, the workflow_run trigger fires when release-tag.yml completes. +# Pushes made by release-tag.yml using GITHUB_TOKEN do NOT trigger the push events +# (GitHub prevents this to avoid infinite loops). + on: workflow_run: workflows: ["Deploy Tagged Version"] @@ -9,6 +16,7 @@ on: push: branches: - "deploy" + - "deploy-tutorials" workflow_dispatch: @@ -34,11 +42,12 @@ jobs: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" git fetch origin deploy:deploy git fetch origin postdeploy:postdeploy - # TODO: Fetch tutorials branches when ready for production deployment: - # git fetch origin deploy-tutorials:deploy-tutorials - # git fetch origin postdeploy-tutorials:postdeploy-tutorials + git fetch origin deploy-tutorials:deploy-tutorials + git fetch origin postdeploy-tutorials:postdeploy-tutorials + # Run reference overlay if triggered by workflow_run, workflow_dispatch, or push to deploy - name: Run overlay script + if: github.event_name != 'push' || github.ref == 'refs/heads/deploy' run: | echo "Adding overlays to predeploy" # -B to prevent creation of __pycache__ directory @@ -46,15 +55,18 @@ jobs: python -B deploy/overlay.py . deploy postdeploy --site-dir reference - name: Push deploy branch + if: github.event_name != 'push' || github.ref == 'refs/heads/deploy' run: | git push origin postdeploy - # TODO: Add these steps when tutorials are ready for production deployment: - # - name: Run overlay script for tutorials - # run: | - # echo "Adding overlays to tutorials predeploy" - # python -B deploy/overlay.py . deploy-tutorials postdeploy-tutorials --site-dir tutorials - # - # - name: Push tutorials deploy branch - # run: | - # git push origin postdeploy-tutorials + # Run tutorials overlay if triggered by workflow_run, workflow_dispatch, or push to deploy-tutorials + - name: Run overlay script for tutorials + if: github.event_name != 'push' || github.ref == 'refs/heads/deploy-tutorials' + run: | + echo "Adding overlays to tutorials predeploy" + python -B deploy/overlay.py . deploy-tutorials postdeploy-tutorials --site-dir tutorials + + - name: Push tutorials deploy branch + if: github.event_name != 'push' || github.ref == 'refs/heads/deploy-tutorials' + run: | + git push origin postdeploy-tutorials diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index ad54bce36..2fd5f8666 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -115,7 +115,11 @@ jobs: - name: Detect deployment structure id: detect-structure run: | - # Check if we have the new combined site structure or old single-site structure + # Check if we have the new combined site structure or old single-site structure. + # The single-site structure is used for older builds that don't have tutorials. + # Because this workflow runs on tag push, it runs in the context of the tagged + # commit; after tutorial deployments are confirmed working, we can get rid of this + # conditional and just assume the new structure in this workflow. if [ -d "html/site/reference" ]; then STRUCTURE="new" REFERENCE_DIR="html/site/reference" @@ -134,11 +138,13 @@ jobs: echo "Making deployment from tag: '$TAG_NAME'" python deploy/release.py ${{ steps.detect-structure.outputs.reference_dir }} "$VERSION" deploy - # TODO: Add tutorials deployment when ready for production (requires new structure): - # python deploy/release.py html/site/tutorials "$VERSION" deploy-tutorials + if [ "${{ steps.detect-structure.outputs.structure }}" = "new" ]; then + python deploy/release.py html/site/tutorials "$VERSION" deploy-tutorials + fi - name: Push deploy branch run: | git push origin deploy - # TODO: Push tutorials branch when ready for production: - # git push origin deploy-tutorials + if [ "${{ steps.detect-structure.outputs.structure }}" = "new" ]; then + git push origin deploy-tutorials + fi diff --git a/.github/workflows/upload-snapshots.yml b/.github/workflows/upload-snapshots.yml index f3467832f..f8a423b96 100644 --- a/.github/workflows/upload-snapshots.yml +++ b/.github/workflows/upload-snapshots.yml @@ -42,31 +42,30 @@ jobs: # This is the site called 'lean-reference-manual-versions' NETLIFY_SITE_ID: "91dc33ef-6016-4ac7-bac1-d574e2254405" - # TODO: Add parallel job for tutorials when ready for production deployment: - # deploy-tutorials: - # name: Deploy tutorials release branch to hosting - # runs-on: ubuntu-latest - # if: - # ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == - # 'success' }} - # steps: - # - name: Checkout postdeploy-tutorials branch - # uses: actions/checkout@v4 - # with: - # ref: "postdeploy-tutorials" - # - # - name: Upload the current state of the postdeploy-tutorials branch - # uses: nwtgck/actions-netlify@v2.0 - # with: - # publish-dir: "." - # production-branch: postdeploy-tutorials - # production-deploy: true - # github-token: ${{ secrets.GITHUB_TOKEN }} - # deploy-message: | - # Deploy tutorials from ${{ github.ref }} - # enable-commit-comment: false - # enable-pull-request-comment: false - # fails-without-credentials: true - # env: - # NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - # NETLIFY_SITE_ID: "e76b4246-fee7-491e-91ef-a87fffff6ce1" + deploy-tutorials: + name: Deploy tutorials release branch to hosting + runs-on: ubuntu-latest + if: + ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == + 'success' }} + steps: + - name: Checkout postdeploy-tutorials branch + uses: actions/checkout@v4 + with: + ref: "postdeploy-tutorials" + + - name: Upload the current state of the postdeploy-tutorials branch + uses: nwtgck/actions-netlify@v2.0 + with: + publish-dir: "." + production-branch: postdeploy-tutorials + production-deploy: true + github-token: ${{ secrets.GITHUB_TOKEN }} + deploy-message: | + Deploy tutorials from ${{ github.ref }} + enable-commit-comment: false + enable-pull-request-comment: false + fails-without-credentials: true + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: "e76b4246-fee7-491e-91ef-a87fffff6ce1" diff --git a/.linkchecker/linkcheckerrc b/.linkchecker/linkcheckerrc index 0cd632270..0e8d40256 100644 --- a/.linkchecker/linkcheckerrc +++ b/.linkchecker/linkcheckerrc @@ -1,2 +1,6 @@ [filtering] ignorewarnings=url-content-too-large,url-too-long + +[checking] +# Moderate rate limit for localhost - higher values overwhelm Python's simple HTTP server +maxrequestspersecond=30 diff --git a/.linkchecker/server.py b/.linkchecker/server.py new file mode 100644 index 000000000..f50e8ae8b --- /dev/null +++ b/.linkchecker/server.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +""" +Simple HTTP server that adds a LinkChecker header to allow unlimited request rate. +""" +import http.server +import sys + +class LinkCheckerHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + # Add LinkChecker header to allow maxrequestspersecond > 10 + self.send_header('LinkChecker', 'allowed') + super().end_headers() + +if __name__ == '__main__': + port = int(sys.argv[1]) if len(sys.argv) > 1 else 8877 + directory = sys.argv[2] if len(sys.argv) > 2 else '.' + + import os + os.chdir(directory) + + with http.server.HTTPServer(('', port), LinkCheckerHTTPRequestHandler) as httpd: + print(f"Serving at port {port} with LinkChecker header enabled") + httpd.serve_forever() diff --git a/Main.lean b/Main.lean index a2a8d6ade..4018f4097 100644 --- a/Main.lean +++ b/Main.lean @@ -1,5 +1,5 @@ /- -Copyright (c) 2024 Lean FRO LLC. All rights reserved. +Copyright (c) 2024-2025 Lean FRO LLC. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Author: David Thrane Christiansen -/ @@ -44,7 +44,7 @@ where extraFiles := [("static", "static")], extraHead := #[plausible, staticJs, staticCss], emitTeX := false, - emitHtmlSingle := true, -- for proofreading + emitHtmlSingle := .no, logo := some "/static/lean_logo.svg", sourceLink := some "https://github.com/leanprover/reference-manual", issueLink := some "https://github.com/leanprover/reference-manual/issues", diff --git a/Manual.lean b/Manual.lean index c11321c2f..894fd2ad9 100644 --- a/Manual.lean +++ b/Manual.lean @@ -18,7 +18,6 @@ import Manual.ErrorExplanations import Manual.Tactics import Manual.Simp import Manual.Grind -import Manual.VCGen import Manual.BasicTypes import Manual.Iterators import Manual.BasicProps @@ -31,6 +30,7 @@ import Manual.Releases import Manual.Namespaces import Manual.Runtime import Manual.SupportedPlatforms +import Manual.VCGen open Verso.Genre Manual open Verso.Genre.Manual.InlineLean @@ -58,7 +58,7 @@ Along with many other parts of Lean, the tactic language is user-extensible, so Tactics are written in Lean itself, and can be used immediately upon definition; rebuilding the prover or loading external modules is not required. Lean is also a pure *functional programming language*, with features such as a run-time system based on reference counting that can efficiently work with packed array structures, multi-threading, and monadic {name}`IO`. -As befits a programming language, Lean is primarily implemented in itself, including the language server, build tool, {tech}[elaborator], and tactic system. +As befits a programming language, Lean is primarily implemented in itself, including the language server, build tool, {tech (key := "Lean elaborator") -normalize}[elaborator], and tactic system. This very book is written in [Verso](https://github.com/leanprover/verso), a documentation authoring tool written in Lean. Familiarity with Lean's programming features is valuable even for users whose primary interest is in writing proofs, because Lean programs are used to implement new tactics and proof automation. diff --git a/Manual/BuildTools/Lake/CLI.lean b/Manual/BuildTools/Lake/CLI.lean index d29cbef3a..9b1828114 100644 --- a/Manual/BuildTools/Lake/CLI.lean +++ b/Manual/BuildTools/Lake/CLI.lean @@ -308,7 +308,7 @@ Single-character flags cannot be combined; `-HR` is not equivalent to `-H -R`. : {lakeOptDef flag}`--reconfigure` or {lakeOptDef flag}`-R` - Normally, the {tech}[package configuration] file is {tech (key := "elaborator")}[elaborated] when a package is first configured, with the result cached to a {tech}[`.olean` file] that is used for future invocations until the package configuration + Normally, the {tech}[package configuration] file is {tech (key := "elaborator") -normalize}[elaborated] when a package is first configured, with the result cached to a {tech}[`.olean` file] that is used for future invocations until the package configuration Providing this flag causes the configuration file to be re-elaborated. : {lakeOptDef flag}`--keep-toolchain` diff --git a/Manual/Defs.lean b/Manual/Defs.lean index 4d128cc76..7027b4030 100644 --- a/Manual/Defs.lean +++ b/Manual/Defs.lean @@ -30,7 +30,7 @@ The following commands in Lean are definition-like: {TODO}[Render commands as th * {keyword}`theorem` * {keyword}`opaque` -All of these commands cause Lean to {tech (key := "elaborator")}[elaborate] a term based on a {tech}[signature]. +All of these commands cause Lean to {tech (key := "elaborator") -normalize}[elaborate] a term based on a {tech}[signature]. With the exception of {keywordOf Lean.Parser.Command.example}`example`, which discards the result, the resulting expression in Lean's core language is saved for future use in the environment. The {keywordOf Lean.Parser.Command.declaration}`instance` command is described in the {ref "instance-declarations"}[section on instance declarations]. diff --git a/Manual/Elaboration.lean b/Manual/Elaboration.lean index 24d41dc3a..2e7f9e816 100644 --- a/Manual/Elaboration.lean +++ b/Manual/Elaboration.lean @@ -39,7 +39,7 @@ Roughly speaking, Lean's processing of a source file can be divided into the fol : Elaboration - {deftech (key := "elaborator")}[Elaboration] is the process of transforming Lean's user-facing syntax into its core type theory. + {deftech (key := "Lean elaborator") -normalize}[Elaboration] is the process of transforming Lean's user-facing syntax into its core type theory. This core theory is much simpler, enabling the trusted kernel to be very small. Elaboration additionally produces metadata, such as proof states or the types of expressions, used for Lean's interactive features, storing them in a side table. @@ -105,7 +105,7 @@ tag := "macro-and-elab" %%% Having parsed a command, the next step is to elaborate it. -The precise meaning of {deftech}_elaboration_ depends on what is being elaborated: elaborating a command effects a change in the state of Lean, while elaborating a term results in a term in Lean's core type theory. +The precise meaning of {deftech -normalize}_elaboration_ depends on what is being elaborated: elaborating a command effects a change in the state of Lean, while elaborating a term results in a term in Lean's core type theory. Elaboration of both commands and terms may be recursive, both because of command combinators such as {keywordOf Lean.Parser.Command.in}`in` and because terms may contain other terms. Command and term elaboration have different capabilities. @@ -293,7 +293,7 @@ The compiler stores an intermediate representation in an environment extension. For straightforwardly structurally recursive functions, the translation will use the type's recursor. These functions tend to be relatively efficient when run in the kernel, their defining equations hold definitionally, and they are easy to understand. -Functions that use other patterns of recursion that cannot be captured by the type's recursor are translated using {deftech}[well-founded recursion], which is structural recursion on a proof that some {deftech}_measure_ decreases at each recursive call, or using {ref "partial-fixpoint"}[partial fixpoints], which logically capture at least part of a function's specification by appealing to domain-theoretic constructions. +Functions that use other patterns of recursion that cannot be captured by the type's recursor are translated using {tech}[well-founded recursion], which is structural recursion on a proof that some {tech}[measure] decreases at each recursive call, or using {ref "partial-fixpoint"}[partial fixpoints], which logically capture at least part of a function's specification by appealing to domain-theoretic constructions. Lean can automatically derive many of these termination proofs, but some require manual proofs. Well-founded recursion is more flexible, but the resulting functions are often slower to execute in the kernel due to the proof terms that show that a measure decreases, and their defining equations may hold only propositionally. To provide a uniform interface to functions defined via structural and well-founded recursion and to check its own correctness, the elaborator proves {deftech}[equational lemmas] that relate the function to its original definition. diff --git a/Manual/Grind/EMatching.lean b/Manual/Grind/EMatching.lean index a1840315e..d6bb3c1dd 100644 --- a/Manual/Grind/EMatching.lean +++ b/Manual/Grind/EMatching.lean @@ -32,7 +32,7 @@ Each fact added to the whiteboard by E-matching is referred to as an {deftech (k Annotating theorems for E-matching, thus adding them to the index, is essential for enabling {tactic}`grind` to make effective use of a library. In addition to user-specified theorems, {tactic}`grind` uses automatically generated equations for {keywordOf Lean.Parser.Term.match}`match`-expressions as E-matching theorems. -Behind the scenes, the {tech}[elaborator] generates auxiliary functions that implement pattern matches, along with equational theorems that specify their behavior. +Behind the scenes, the {tech (key := "Lean elaborator")}[elaborator] generates auxiliary functions that implement pattern matches, along with equational theorems that specify their behavior. Using these equations with E-matching enables {tactic}`grind` to reduce these instances of pattern matching. diff --git a/Manual/Grind/ExtendedExamples.lean b/Manual/Grind/ExtendedExamples.lean index 7ac200443..bc880375c 100644 --- a/Manual/Grind/ExtendedExamples.lean +++ b/Manual/Grind/ExtendedExamples.lean @@ -12,7 +12,6 @@ import Manual.Meta import Manual.Grind.ExtendedExamples.Integration import Manual.Grind.ExtendedExamples.IfElseNorm -import Manual.Grind.ExtendedExamples.IndexMap open Verso.Genre Manual open Verso.Genre.Manual.InlineLean @@ -27,8 +26,10 @@ open Lean.Grind tag := "grind-bigger-examples" %%% +:::TODO +Properly link to tutorial section +::: + {include 1 Manual.Grind.ExtendedExamples.Integration} {include 1 Manual.Grind.ExtendedExamples.IfElseNorm} - -{include 1 Manual.Grind.ExtendedExamples.IndexMap} diff --git a/Manual/IO/Threads.lean b/Manual/IO/Threads.lean index 1de2a7115..01f76a140 100644 --- a/Manual/IO/Threads.lean +++ b/Manual/IO/Threads.lean @@ -30,7 +30,7 @@ variable {α : Type u} ``` {deftech}_Tasks_ are the fundamental primitive for writing multi-threaded code. -A {lean}`Task α` represents a computation that, at some point, will {deftech}_resolve_ to a value of type `α`; it may be computed on a separate thread. +A {lean}`Task α` represents a computation that, at some point, will {tech (key := "resolve promise")}_resolve_ to a value of type `α`; it may be computed on a separate thread. When a task has resolved, its value can be read; attempting to get the value of a task before it resolves causes the current thread to block until the task has resolved. Tasks are similar to promises in JavaScript, `JoinHandle` in Rust, and `Future` in Scala. @@ -52,7 +52,7 @@ Tasks may be explicitly cancelled using {name}`IO.cancel`. The Lean runtime maintains a thread pool for running tasks. The size of the thread pool is determined by the environment variable {envVar +def}`LEAN_NUM_THREADS` if it is set, or by the number of logical processors on the current machine otherwise. The size of the thread pool is not a hard limit; in certain situations it may be exceeded to avoid deadlocks. -By default, these threads are used to run tasks; each task has a {deftech}_priority_ ({name}`Task.Priority`), and higher-priority tasks take precedence over lower-priority tasks. +By default, these threads are used to run tasks; each task has a {deftech (key := "task priority")}_priority_ ({name}`Task.Priority`), and higher-priority tasks take precedence over lower-priority tasks. Tasks may also be assigned to dedicated threads by spawning them with a sufficiently high priority. {docstring Task (label := "type") +hideStructureConstructor +hideFields} @@ -162,7 +162,7 @@ Pure tasks are terminated automatically upon cancellation. # Promises Promises represent a value that will be supplied in the future. -Supplying the value is called {deftech (key := "resolve")}_resolving_ the promise. +Supplying the value is called {deftech (key := "resolve promise")}_resolving_ the promise. Once created, a promise can be stored in a data structure or passed around like any other value, and attempts to read from it will block until it is resolved. diff --git a/Manual/Interaction.lean b/Manual/Interaction.lean index 6e7a6df0d..5ec270049 100644 --- a/Manual/Interaction.lean +++ b/Manual/Interaction.lean @@ -28,7 +28,7 @@ Lean's interactive features are based on a different paradigm. Rather than a separate command prompt outside of the program, Lean provides {tech}[commands] for accomplishing the same tasks in the context of a source file. By convention, commands that are intended for interactive use rather than as part of a durable code artifact are prefixed with {keyword}`#`. -Information from Lean commands is available in the {deftech}_message log_, which accumulates output from the {tech}[elaborator]. +Information from Lean commands is available in the {deftech}_message log_, which accumulates output from the {tech (key := "Lean elaborator")}[elaborator]. Each entry in the message log is associated with a specific source range and has a {deftech}_severity_. There are three severities: {lean (type := "Lean.MessageSeverity")}`information` is used for messages that do not indicate a problem, {lean (type := "Lean.MessageSeverity")}`warning` indicates a potential problem, and {lean (type := "Lean.MessageSeverity")}`error` indicates a definite problem. For interactive commands, results are typically returned as informational messages that are associated with the command's leading keyword. @@ -56,7 +56,7 @@ Use {keywordOf Lean.reduceCmd}`#reduce` to instead reduce terms using the reduct ::: -{keywordOf Lean.Parser.Command.eval}`#eval` always {tech (key := "elaborator")}[elaborates] and compiles the provided term. +{keywordOf Lean.Parser.Command.eval}`#eval` always {tech (key := "Lean elaborator")}[elaborates] and compiles the provided term. It then checks whether the term transitively depends on any uses of {lean}`sorry`, in which case evaluation is terminated unless the command was invoked as {keywordOf Lean.Parser.Command.eval}`#eval!`. This is because compiled code may rely on compile-time invariants (such as array lookups being in-bounds) that are ensured by proofs of suitable statements, and running code that contains incomplete proofs (or uses of {lean}`sorry` that “prove” incorrect statements) can cause Lean itself to crash. diff --git a/Manual/Language/Namespaces.lean b/Manual/Language/Namespaces.lean index 1831c8e0f..ddc50f86e 100644 --- a/Manual/Language/Namespaces.lean +++ b/Manual/Language/Namespaces.lean @@ -65,7 +65,7 @@ end Forest # Namespaces and Section Scopes -Every {tech}[section scope] has a {deftech}_current namespace_, which is determined by the {keywordOf Lean.Parser.Command.namespace}`namespace` command.{margin}[The {keywordOf Lean.Parser.Command.namespace}`namespace` command is described in the {ref "scope-commands"}[section on commands that introduce section scopes].] +Every {tech}[section scope] has a {tech}[current namespace], which is determined by the {keywordOf Lean.Parser.Command.namespace}`namespace` command.{margin}[The {keywordOf Lean.Parser.Command.namespace}`namespace` command is described in the {ref "scope-commands"}[section on commands that introduce section scopes].] Names that are declared within the section scope are added to the current namespace. If the declared name has more than one component, then its namespace is nested within the current namespace; the body of the declaration's current namespace is the nested namespace. Section scopes also include a set of {deftech}_opened namespaces_, which are namespaces whose contents are in scope without additional qualification. diff --git a/Manual/Meta.lean b/Manual/Meta.lean index 48067ce20..8be89346c 100644 --- a/Manual/Meta.lean +++ b/Manual/Meta.lean @@ -35,7 +35,6 @@ import Manual.Meta.Syntax import Manual.Meta.Tactics import Manual.Meta.SpliceContents import Manual.Meta.Markdown -import Manual.Meta.Imports import Manual.Meta.Namespace @@ -392,6 +391,15 @@ def ffi.descr : BlockDescr where toTeX := some <| fun _goI goB _ _ contents => contents.mapM goB -- TODO +open Verso.Output.Html in +inline_extension Inline.multiCode where + traverse _ _ _ := pure none + toHtml := some <| fun goI _id _data contents => do return {{{{← contents.mapM goI}}}} + toTeX := none + +@[role] +def multiCode : RoleExpanderOf Unit + | (), contents => do ``(Inline.other Inline.multiCode #[$(← contents.mapM elabInline),*]) structure LeanSectionConfig where diff --git a/Manual/Meta/Example.lean b/Manual/Meta/Example.lean index 78be9402d..7c5d34644 100644 --- a/Manual/Meta/Example.lean +++ b/Manual/Meta/Example.lean @@ -6,7 +6,6 @@ Author: David Thrane Christiansen import VersoManual import Manual.Meta.Figure -import Manual.Meta.Imports import Manual.Meta.LzCompress import Lean.Elab.InfoTree.Types diff --git a/Manual/Meta/Imports.lean b/Manual/Meta/Imports.lean deleted file mode 100644 index 03fe356ec..000000000 --- a/Manual/Meta/Imports.lean +++ /dev/null @@ -1,54 +0,0 @@ -/- -Copyright (c) 2025 Lean FRO LLC. All rights reserved. -Released under Apache 2.0 license as described in the file LICENSE. -Author: David Thrane Christiansen --/ - -import VersoManual -import Lean.Elab.InfoTree.Types -import SubVerso.Highlighting.Code -import Manual.Meta.Basic - -open scoped Lean.Doc.Syntax - -open Verso Doc Elab -open Lean Elab -open Verso.Genre.Manual InlineLean Scopes -open Verso.SyntaxUtils -open SubVerso.Highlighting -open ArgParse - -namespace Manual - -structure ImportsParams where - «show» : Bool := true - -instance : FromArgs ImportsParams m where - fromArgs := ImportsParams.mk <$> .flag `show true (some "Whether to show the import header") - -@[code_block] -def imports : CodeBlockExpanderOf ImportsParams - | { «show» }, str => do - let altStr ← parserInputString str - let p := Parser.whitespace >> Parser.Module.header.fn - let headerStx ← p.parseString altStr - let hl ← highlight headerStx #[] {} - if «show» then - ``(Block.other (Block.lean $(quote hl) {}) #[Block.code $(quote str.getString)]) - else - ``(Block.empty) - -@[role] -def «import» : RoleExpanderOf Unit - | (), contents => do - let some str ← oneCodeStr? contents - | ``(Verso.Doc.Inline.empty) - - let p := Lean.Parser.Module.import.fn - let stx ← withoutModifyingEnv do - modifyEnv fun env => - Parser.parserExtension.modifyState env fun st => - { st with tokens := Parser.Module.updateTokens st.tokens } - Lean.Doc.parseStrLit p str - let hl ← highlight stx #[] {} - ``(Inline.other (Inline.lean $(quote hl) {}) #[Inline.code $(quote str.getString)]) diff --git a/Manual/Meta/ParserAlias.lean b/Manual/Meta/ParserAlias.lean index 882558c69..bcc4ee9b2 100644 --- a/Manual/Meta/ParserAlias.lean +++ b/Manual/Meta/ParserAlias.lean @@ -69,7 +69,7 @@ def parserAlias : DirectiveExpander pure #[← ``(Verso.Doc.Block.other (Block.parserAlias $(quote opts.name) $(quote declName) $(quote opts.show) $(quote stackSz?) $(quote autoGroupArgs) $(quote docs?) $(quote argCount)) #[$(contents ++ userContents),*])] @[inline] -private def getFromJson {α} [Inhabited α] [FromJson α] (v : Json) : HtmlT Genre.Manual (ReaderT ExtensionImpls IO) α:= +private def getFromJson {α} [Monad m] [Inhabited α] [FromJson α] (v : Json) : HtmlT Genre.Manual m α:= match FromJson.fromJson? (α := α) v with | .error e => do Verso.Doc.Html.HtmlT.logError diff --git a/Manual/Meta/Syntax.lean b/Manual/Meta/Syntax.lean index 994adbf7c..9c8939ec7 100644 --- a/Manual/Meta/Syntax.lean +++ b/Manual/Meta/Syntax.lean @@ -830,7 +830,7 @@ where return bar ++ .nest 2 (← production which stx |>.run' {}) def testGetBnf (config : FreeSyntaxConfig) (isFirst : Bool) (stxs : List Syntax) : TermElabM String := do - let (tagged, _) ← getBnf config isFirst stxs |>.run (← moduleGenreElabContext) {} {partContext := ⟨⟨default, default, default, default, default⟩, default⟩} + let (tagged, _) ← getBnf config isFirst stxs |>.run ⟨← ``(Manual), mkConst ``Manual, .always, none⟩ {} {partContext := ⟨⟨default, default, default, default, default⟩, default⟩} pure tagged.stripTags namespace Tests @@ -1352,7 +1352,7 @@ window.addEventListener("load", () => { "# open Verso.Output Html HtmlT in -private def nonTermHtmlOf (kind : Name) (doc? : Option String) (rendered : Html) : HtmlT Manual (ReaderT ExtensionImpls IO) Html := do +private def nonTermHtmlOf (kind : Name) (doc? : Option String) (rendered : Html) : HtmlT Manual (ReaderT Multi.AllRemotes (ReaderT ExtensionImpls IO)) Html := do let xref ← match (← state).resolveDomainObject syntaxKindDomain kind.toString with | .error _ => pure none @@ -1398,7 +1398,7 @@ def noLook (ctx : GrammarHtmlContext) : GrammarHtmlContext := end GrammarHtmlContext open Verso.Output Html in -abbrev GrammarHtmlM := ReaderT GrammarHtmlContext (HtmlT Manual (ReaderT ExtensionImpls IO)) +abbrev GrammarHtmlM := ReaderT GrammarHtmlContext (HtmlT Manual (ReaderT Multi.AllRemotes (ReaderT ExtensionImpls IO))) private def lookingAt (k : Name) : GrammarHtmlM α → GrammarHtmlM α := withReader (·.look k) @@ -1492,7 +1492,7 @@ where let inner ← go if let some k := (← read).lookingAt then unless k == nullKind do - if let some tgt := ((← HtmlT.state (genre := Manual) (m := ReaderT ExtensionImpls IO)).localTargets.keyword k none)[0]? then + if let some tgt := ((← HtmlT.state (genre := Manual) (m := ReaderT Multi.AllRemotes (ReaderT ExtensionImpls IO))).localTargets.keyword k none)[0]? then return {{{{inner}}}} return {{{{inner}}}} | .nonterminal k doc? => do diff --git a/Manual/NotationsMacros.lean b/Manual/NotationsMacros.lean index 0231ffb93..28ccd2455 100644 --- a/Manual/NotationsMacros.lean +++ b/Manual/NotationsMacros.lean @@ -62,7 +62,7 @@ They can be combined flexibly to achieve the necessary results: tag := "macros" %%% -{deftech}_Macros_ are transformations from {name Lean.Syntax}`Syntax` to {name Lean.Syntax}`Syntax` that occur during {tech (key := "elaborator")}[elaboration] and during {ref "tactic-macros"}[tactic execution]. +{deftech}_Macros_ are transformations from {name Lean.Syntax}`Syntax` to {name Lean.Syntax}`Syntax` that occur during {tech (key := "elaborator") -normalize}[elaboration] and during {ref "tactic-macros"}[tactic execution]. Replacing syntax with the result of transforming it with a macro is called {deftech}_macro expansion_. Multiple macros may be associated with a single {tech}[syntax kind], and they are attempted in order of definition. Macros are run in a {tech}[monad] that has access to some compile-time metadata and has the ability to either emit an error message or to delegate to subsequent macros, but the macro monad is much less powerful than the elaboration monads. diff --git a/Manual/NotationsMacros/SyntaxDef.lean b/Manual/NotationsMacros/SyntaxDef.lean index db1ab2e24..bda993def 100644 --- a/Manual/NotationsMacros/SyntaxDef.lean +++ b/Manual/NotationsMacros/SyntaxDef.lean @@ -718,7 +718,7 @@ A variant of list literals that requires double square brackets and allows a tra syntax "[[" term,*,? "]]" : term ``` -Adding a {deftech}[macro] that describes how to translate it into an ordinary list literal allows it to be used in tests. +Adding a {tech}[macro] that describes how to translate it into an ordinary list literal allows it to be used in tests. ```lean macro_rules | `(term|[[$e:term,*]]) => `([$e,*]) diff --git a/Manual/SourceFiles.lean b/Manual/SourceFiles.lean index f1fe6fc35..c83753149 100644 --- a/Manual/SourceFiles.lean +++ b/Manual/SourceFiles.lean @@ -17,7 +17,7 @@ tag := "files" htmlSplit := .never %%% -The smallest unit of compilation in Lean is a single {deftech}[source file]. +The smallest unit of compilation in Lean is a single {tech}[source file]. Source files may import other source files based on their file names. In other words, the names and folder structures of files are significant in Lean code. @@ -642,7 +642,7 @@ tag := "meta-phase" Definitions in Lean result in both a representation in the type theory that is designed for formal reasoning and a compiled representation that is designed for execution. This compiled representation is used to generate machine code, but it can also be executed directly using an interpreter. -The code runs during {tech}[elaboration], such as {ref "tactics"}[tactics] or {ref "macros"}[macros], is the compiled form of definitions. +The code runs during {tech -normalize}[elaboration], such as {ref "tactics"}[tactics] or {ref "macros"}[macros], is the compiled form of definitions. If this compiled representation changes, then any code created by it may no longer be up to date, and it must be re-run. Because the compiler performs non-trivial optimizations, changes to any definition in the transitive dependency chain of a function could in principle invalidate its compiled representation. This means that metaprograms exported by modules induce a much stronger coupling than ordinary definitions. @@ -856,7 +856,7 @@ tag := "code-distribution" %%% -Lean modules are organized into {deftech}_packages_, which are units of code distribution. +Lean modules are organized into {tech}_packages_, which are units of code distribution. A {tech}[package] may contain multiple libraries or executables. Code in a package that is intended for use by other Lean packages is organized into {deftech (key:="library")}[libraries]. diff --git a/Manual/Terms.lean b/Manual/Terms.lean index 090545f91..fb9e7ff59 100644 --- a/Manual/Terms.lean +++ b/Manual/Terms.lean @@ -28,7 +28,7 @@ tag := "terms" {deftech}_Terms_ are the principal means of writing mathematics and programs in Lean. -The {tech}[elaborator] translates them to Lean's minimal core language, which is then checked by the kernel and compiled for execution. +The {deftech (key := "Lean elaborator")}[elaborator] translates them to Lean's minimal core language, which is then checked by the kernel and compiled for execution. The syntax of terms is {ref "syntax-ext"}[arbitrarily extensible]; this chapter documents the term syntax that Lean provides out-of-the-box. # Identifiers @@ -410,7 +410,7 @@ Implicit parameters come in three varieties: : Instance implicit parameters - Arguments for {deftech}_instance implicit_ parameters are found via {ref "instance-synth"}[type class synthesis]. + Arguments for {tech}_instance implicit_ parameters are found via {ref "instance-synth"}[type class synthesis]. Instance implicit parameters are written in square brackets (`[` and `]`). Unlike the other kinds of implicit parameter, instance implicit parameters that are written without a `:` specify the parameter's type rather than providing a name. Furthermore, only a single name is allowed. diff --git a/Manual/Types.lean b/Manual/Types.lean index 1c1b49ccb..6939b5f99 100644 --- a/Manual/Types.lean +++ b/Manual/Types.lean @@ -24,7 +24,7 @@ shortContextTitle := "Type System" %%% {deftech}_Terms_, also known as {deftech}_expressions_, are the fundamental units of meaning in Lean's core language. -They are produced from user-written syntax by the {tech}[elaborator]. +They are produced from user-written syntax by the {tech (key := "Lean elaborator")}[elaborator]. Lean's type system relates terms to their _types_, which are also themselves terms. Types can be thought of as denoting sets, while terms denote individual elements of these sets. A term is {deftech}_well-typed_ if it has a type under the rules of Lean's type theory. diff --git a/Manual/VCGen.lean b/Manual/VCGen.lean index a0989bb7a..0a0e8051c 100644 --- a/Manual/VCGen.lean +++ b/Manual/VCGen.lean @@ -8,7 +8,6 @@ import VersoManual import Manual.Meta import Manual.Papers -import Manual.VCGen.Tutorial import Std.Tactic.Do @@ -37,8 +36,8 @@ tag := "mvcgen-tactic" %%% The {tactic}`mvcgen` tactic implements a _monadic verification condition generator_: -It breaks down a goal involving a program written using Lean's imperative {keywordOf Lean.Parser.Term.do}`do` notation into a number of smaller {deftech}_verification conditions_ ({deftech}[VCs]) that are sufficient to prove the goal. -In addition to a reference that describes the use of {tactic}`mvcgen`, this chapter includes a {ref "mvcgen-tactic-tutorial"}[tutorial] that can be read independently of the reference. +It breaks down a goal involving a program written using Lean's imperative {keywordOf Lean.Parser.Term.do}`do` notation into a number of smaller {tech}_verification conditions_ ({deftech}[VCs]) that are sufficient to prove the goal. +In addition to a reference that describes the use of {tactic}`mvcgen`, this chapter includes a {ref "mvcgen-tactic-tutorial" (remote := "tutorials")}[tutorial] that can be read independently of the reference. In order to use the {tactic}`mvcgen` tactic, {module}`Std.Tactic.Do` must be imported and the namespace {namespace}`Std.Do` must be opened. @@ -933,5 +932,3 @@ theorem bump_correct' : simp_all [bump] ``` ::: - -{include 0 Manual.VCGen.Tutorial} diff --git a/README.md b/README.md index fbb5e08ed..472c5ab9b 100644 --- a/README.md +++ b/README.md @@ -110,15 +110,15 @@ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for more information. TL;DR: push a tag of the form `vX.Y.Z` onto the commit that should be released as the manual for that version, and the rest is automatic. -This directory contains the deployment infrastructure for the -reference manual. Deployment happens in GitHub Actions, in response to -certain tags being pushed. Because the latest version of the GH action -file will always be used, and we want to be able to mutate tags to -re-deploy old manual versions (e.g. to update CSS for consistent look -and feel while keeping content version-accurate, or add a "THIS IS -OBSOLETE" banner in a few years). Thus, the steps of the workflow that -might change are captured in scripts that are versioned along with the -code. +This repository contains the deployment infrastructure for both the +reference manual and the tutorials site. Deployment happens in GitHub +Actions, in response to certain tags being pushed. Because the latest +version of the GH action file will always be used, and we want to be +able to mutate tags to re-deploy old manual versions (e.g. to update +CSS for consistent look and feel while keeping content +version-accurate, or add a "THIS IS OBSOLETE" banner in a few years), +the steps of the workflow that might change are captured in scripts +that are versioned along with the code. The files are: @@ -128,19 +128,22 @@ The files are: - `build.sh` is used to build the executable that generates the manual. -- `generate.sh` actually generates release-ready HTML, saving it in - `/html` in the root of this repository. +- `generate.sh` builds both the reference manual and tutorials, saving + them in `/html/site/reference` and `/html/site/tutorials`. - `release.py` puts the generated HTML in the right place on a new - commit on the branch `deploy` + commit on a deployment branch (`deploy` for the reference manual, + `deploy-tutorials` for tutorials). Everything above is what needs to happen specifically to the single version of the documentation that is being updated in the course of the deploy. There is one further step, which is computing the desired -state of the final `postdeploy` branch from the state in the branch -`deploy`. This is done by the script `overlay.py`, which is triggered -by pushes to `deploy`, and therefore runs at branch `main` rather than -at the tag being pushed. +state of the final `postdeploy` branches from the state in the +`deploy` branches. This is done by the script `overlay.py`, which is +triggered by pushes to `deploy`, and therefore runs at branch `main` +rather than at the tag being pushed. It processes both the reference +manual (`deploy` → `postdeploy`) and tutorials (`deploy-tutorials` → +`postdeploy-tutorials`). We might have named the two branches `predeploy` and `deploy`, but chose instead `deploy` and `postdeploy` so that we cold leave @@ -149,24 +152,40 @@ still have workflows that emit commits to `deploy`. ## Deployment Overview -The goal is to have versioned snapshots of the manual, with a -structure like: +The goal is to have versioned snapshots of both the reference manual +and tutorials, with a structure like: -- `https://lean-lang.org/doc/reference/latest/`- latest version -- `https://lean-lang.org/doc/reference/4.19.0/` - manual for v4.19.0 -- `https://lean-lang.org/doc/reference/4.20.0/` - manual for v4.19.0 - -and so forth. `https://lean-lang.org/doc/reference/` should redirect -to `latest`. It's important to be able to edit past deployments as -well. - -An orphan branch, called `deploy`, should at all times contain this -structure. With the three URLs above, the branch would contain three -directories: - -- `/4.20.0/` - built HTML served for 4.20.0 -- `/4.19.0/` - built HTML served for 4.19.0 -- `/latest` - symlink to `/4.20.0` +- `https://lean-lang.org/doc/reference/latest/` - latest version +- `https://lean-lang.org/doc/reference/stable/` - latest stable + version +- `https://lean-lang.org/doc/reference/4.19.0/` - reference for + v4.19.0 +- `https://lean-lang.org/doc/reference/4.20.0/` - reference for + v4.20.0 +- `https://lean-lang.org/doc/tutorials/latest/` - latest tutorials +- `https://lean-lang.org/doc/tutorials/stable/` - latest stable + tutorials +- `https://lean-lang.org/doc/tutorials/4.19.0/` - tutorials for + v4.19.0 +- `https://lean-lang.org/doc/tutorials/4.20.0/` - tutorials for + v4.20.0 + +and so forth. The base URLs should redirect to `latest`. It's +important to be able to edit past deployments as well. + +Orphan branches `deploy` and `deploy-tutorials` contain the versioned +content for each site. For example, the `deploy` branch might contain: + +- `/4.25.0-rc1/` - built HTML for 4.25.0-rc1 +- `/4.24.0/` - built HTML for 4.24.0 +- `/4.23.0/` - built HTML for 4.23.0 +- `/latest/` - copy of `/4.25.0-rc1/` (the most recent version) +- `/stable/` - copy of `/4.24.0/` (the most recent non-RC version) + +The `latest` and `stable` directories are full copies rather than +symlinks because Netlify deployment doesn't support symlinks. + +The `deploy-tutorials` branch has the same structure for tutorials. The `release.py` script is responsible for updating this structure. It takes the generated HTML directory, the version number, and the @@ -174,31 +193,34 @@ deployment branch name as arguments, and then does the following: 1. It copies the HTML to the branch (deleting an existing directory first if needed). -2. It updates the `latest` symlink to point at the most recent +2. It updates the `latest` directory to be a copy of the most recent version, with all numbered releases being considered more recent than any nightly and real releases being more recent than their RCs. -3. It commits the changes to the branch `deploy`, then switches back - to the original branch. - -A successful push to deploy in this way triggers a GH action that runs -the `overlay.py` script, which is then responsible for creating a new -commit to `postdeploy`, based on `deploy`. This commit includes all -desired overlays. At time of writing, this is just a single file -`static/metadata.js` in each version of the reference manual that +3. It updates the `stable` directory to be a copy of the most recent + non-RC version. +4. It commits the changes to the deployment branch, then switches + back to the original branch. + +A successful push to `deploy` triggers a GH action that runs the +`overlay.py` script, which creates commits to `postdeploy` (based on +`deploy`) and `postdeploy-tutorials` (based on `deploy-tutorials`). +These commits include all desired overlays. At time of writing, this +is just a single file `static/metadata.js` in each version that contains information about whether the version is in fact stable or latest. -A successful push to `postdeploy` in this way triggers a GH Action -which actually publishes the content to netlify. +A successful push to `postdeploy` or `postdeploy-tutorials` triggers a +GH Action which publishes the content to Netlify. ## Overlays The script `overlay.py` computes `postdeploy` from `deploy` any time -`deploy` changes. Its purpose is to add metadata or make in-place -changes to `deploy` content that is best thought of as a unified -overlay on top of the data that exists at the historical tags -`4.19.0`, `4.20.0`, etc. +`deploy` changes, and `postdeploy-tutorials` from `deploy-tutorials` +any time `deploy-tutorials` changes. Its purpose is to add metadata or +make in-place changes to deployed content that is best thought of as a +unified overlay on top of the data that exists at the historical +version tags. Examples of the sorts of things we might like to achieve with this overlay mechanism are: @@ -233,7 +255,7 @@ Therefore we can be careful on both sides: To test `overlay.py` locally before pushing, do the following. -- Ensure branches `deploy` and `postdeploy` exist locally. +- Ensure the deployment branches exist locally. - You'll probably want to do ``` @@ -242,6 +264,10 @@ git checkout deploy git reset --hard remotes/upstream/deploy git checkout postdeploy git reset --hard remotes/upstream/postdeploy +git checkout deploy-tutorials +git reset --hard remotes/upstream/deploy-tutorials +git checkout postdeploy-tutorials +git reset --hard remotes/upstream/postdeploy-tutorials ``` - From the `reference-manual` checkout directory, on branch `main`, @@ -249,7 +275,8 @@ git reset --hard remotes/upstream/postdeploy you've made) run ```shell -python3 -B deploy/overlay.py . deploy postdeploy +python3 -B deploy/overlay.py . deploy postdeploy --site-dir reference +python3 -B deploy/overlay.py . deploy-tutorials postdeploy-tutorials --site-dir tutorials ``` - Inspect whatever `postdeploy` results you're interested in, e.g. diff --git a/Tutorial.lean b/Tutorial.lean new file mode 100644 index 000000000..6f892783b --- /dev/null +++ b/Tutorial.lean @@ -0,0 +1,7 @@ +/- +Copyright (c) 2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: David Thrane Christiansen +-/ +import Tutorial.VCGen +import Tutorial.Grind.IndexMap diff --git a/Manual/Grind/ExtendedExamples/IndexMap.lean b/Tutorial/Grind/IndexMap.lean similarity index 93% rename from Manual/Grind/ExtendedExamples/IndexMap.lean rename to Tutorial/Grind/IndexMap.lean index 8bfb4bb7c..cae3f7b43 100644 --- a/Manual/Grind/ExtendedExamples/IndexMap.lean +++ b/Tutorial/Grind/IndexMap.lean @@ -5,13 +5,14 @@ Author: Leo de Moura, Kim Morrison -/ import VersoManual +import VersoTutorial import Lean.Parser.Term import Manual.Meta -open Verso.Genre Manual +open Verso.Genre Manual Tutorial open Verso.Genre.Manual.InlineLean hiding module open Verso.Doc.Elab (CodeBlockExpander) open Verso.Code.External @@ -30,8 +31,12 @@ set_option verso.exampleProject "." set_option verso.exampleModule "IndexMapGrind" -#doc (Manual) "`IndexMap`" => - +#doc (Tutorial) "Using `grind` for Ordered Maps" => +%%% +slug := "grind-index-map" +summary := "A demonstration of how to use `grind` to automate essentially all proofs in a new data structure, with an interface that finds proofs automatically." +exampleStyle := .inlineLean `IndexMap +%%% In this section we'll build an example of a new data structure and basic API for it, illustrating the use of {tactic}`grind`. The example will be derived from Rust's [`indexmap`](https://docs.rs/indexmap/latest/indexmap/) data structure. @@ -60,6 +65,18 @@ Our goals will be: Ideally, we don't even need to see the proofs; they should mostly be handled invisibly by {tactic}`grind`. + +:::paragraph +The first step is to import the necessary data structures: +```anchor imports +import Std.Data.HashMap +``` +::: + +# Skeleton + +:::displayOnly + To begin with, we'll write out a skeleton of what we want to achieve, liberally using {lean}`sorry` as a placeholder for all proofs. In particular, this version makes no use of {tactic}`grind`. @@ -212,6 +229,9 @@ theorem findIdx_insert_self end IndexMap ``` +::: + +# Header 2 Let's get started. We'll aspire to never writing a proof by hand, and the first step of that is to install auto-parameters for the `size_keys` and `WF` field, @@ -219,6 +239,8 @@ so we can omit these fields whenever `grind` can prove them. While we're modifying the definition of `IndexMap` itself, let's make all the fields private, since we're planning on having complete encapsulation. ```anchor IndexMap +open Std + structure IndexMap (α : Type u) (β : Type v) [BEq α] [Hashable α] where private indices : HashMap α Nat @@ -229,6 +251,14 @@ structure IndexMap keys[i]? = some a ↔ indices[a]? = some i := by grind ``` +For the rest of this tutorial, the following namespace and variable declarations are in effect: +```anchor variables +namespace IndexMap + +variable {α : Type u} {β : Type v} [BEq α] [Hashable α] +variable {m : IndexMap α β} {a : α} {b : β} {i : Nat} +``` + Let's give {tactic}`grind` access to the definition of `size`, and `size_keys` private field: ```anchor size @[inline] def size (m : IndexMap α β) : Nat := @@ -250,6 +280,21 @@ def emptyWithCapacity (capacity := 8) : IndexMap α β where ``` ::: +:::codeOnly +```anchor Membership +@[inline] def contains (m : IndexMap α β) + (a : α) : Bool := + m.indices.contains a + +instance : Membership α (IndexMap α β) where + mem m a := a ∈ m.indices + +instance {m : IndexMap α β} {a : α} : Decidable (a ∈ m) := + inferInstanceAs (Decidable (a ∈ m.indices)) +``` +::: + +:::displayOnly Our next task is to deal with the {lean}`sorry` in our construction of the original {anchorTerm GetElem?}`GetElem?` instance: ```anchor GetElem? (module := IndexMap) instance : @@ -261,6 +306,7 @@ instance : getElem! m a := m.indices[a]?.bind (m.values[·]?) |>.getD default ``` +::: The goal at this sorry is ``` @@ -331,7 +377,7 @@ However this proof is going to work, we know the following: * It must use the well-formedness condition of the map. * It can't do so without relating `m.indices[a]` and `m.indices[a]?` (because the later is what appears in the well-formedness condition). * The expected relationship there doesn't even hold unless the map `m.indices` satisfies {lean}`LawfulGetElem`, - for which we need {tech}[instances] of {lean}`LawfulBEq α` and {lean}`LawfulHashable α`. + for which we need {tech (remote:="reference")}[instances] of {lean}`LawfulBEq α` and {lean}`LawfulHashable α`. ::: :::TODO @@ -401,7 +447,13 @@ macro_rules | `(tactic| get_elem_tactic_extensible) => `(tactic| grind) ::: :::paragraph -We can now return to constructing our {anchorName GetElem?}`GetElem?` instance, and simply write: +We can now return to constructing our {anchorName GetElem?}`GetElem?` instance. +In order to use the well-formedness condition, {tactic}`grind` must be able to unfold {anchorName size}`size`: +```anchor local_grind_size +attribute [local grind] size +``` +The {anchorTerm local_grind_size}`local` modifier restricts this unfolding to the current file. +With this in place, we can simply write: ```anchor GetElem? instance : GetElem? (IndexMap α β) α β (fun m a => a ∈ m) where getElem m a h := @@ -630,6 +682,23 @@ Trying again with {anchorName eraseSwap}`eraseSwap`, everything goes through cle | none => m ``` +:::codeOnly +```anchor getFindIdx +@[inline] def findIdx? (m : IndexMap α β) (a : α) : Option Nat := + m.indices[a]? + +@[inline] def findIdx (m : IndexMap α β) (a : α) + (h : a ∈ m := by get_elem_tactic) : Nat := + m.indices[a] + +@[inline] def getIdx? (m : IndexMap α β) (i : Nat) : Option β := + m.values[i]? + +@[inline] def getIdx (m : IndexMap α β) (i : Nat) + (h : i < m.size := by get_elem_tactic) : β := + m.values[i] +``` +::: Finally we turn to the verification theorems about the basic operations, relating {anchorName Verification}`getIdx`, {anchorName Verification}`findIdx`, and {anchorName Verification}`insert`. By adding a {anchorTerm Verification}`local grind` annotation allowing {tactic}`grind` to unfold the definitions of these operations, diff --git a/Tutorial/Meta/Theme.lean b/Tutorial/Meta/Theme.lean new file mode 100644 index 000000000..6070f94bd --- /dev/null +++ b/Tutorial/Meta/Theme.lean @@ -0,0 +1,370 @@ +/- +Copyright (c) 2024-2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: David Thrane Christiansen +-/ +import VersoBlog +import VersoWeb.Theme + +import VersoWeb.Components.Footer +import VersoWeb.Components.NavBar + +open Verso Genre Blog Output Html Multi +open Web Components Theme + +namespace LeanLangOrg + +/-- +Default footer configuration with all standard links +-/ +def footer : FooterConfig := { + columns := #[ + -- Get Started column + { + heading := "Get Started" + headingId := some "get-started" + ariaLabel := some "LEAN" + items := #[ + { title := "Install", url := "/install" }, + { title := "Learn", url := "/learn" }, + { title := "Community", url := "/community" }, + { title := "Reservoir", url := "https://reservoir.lean-lang.org/", blank := true } + ] + }, + -- Documentation column + { + heading := "Documentation" + headingId := some "documentation" + ariaLabel := some "Documentation" + items := #[ + { title := "Language reference", url := "/doc/reference/latest/" }, + { title := "Lean API", url := "/doc/api/" }, + { title := "Use cases", url := "/use-cases" }, + { title := "Cite Lean", url := "/learn#how-to-cite-lean" } + ] + }, + -- Resources column + { + heading := "Resources" + headingId := some "resources" + ariaLabel := some "Resources" + items := #[ + { title := "Lean playground", url := "https://live.lean-lang.org/?from=lean", blank := true }, + { title := "VS Code extension", url := "https://marketplace.visualstudio.com/items?itemName=leanprover.lean4", blank := true }, + { title := "Loogle", url := "https://loogle.lean-lang.org/", blank := true }, + { title := "Mathlib", url := "https://github.com/leanprover-community/mathlib4", blank := true } + ] + }, + -- FRO column + { + heading := "FRO" + headingId := some "fro" + ariaLabel := some "FRO" + items := #[ + { title := "Vision", url := "/fro" }, + { title := "Team", url := "/fro/team" }, + { title := "Roadmap", url := "/fro/roadmap/y3" }, + { title := "Contact", url := "/fro/contact" } + ] + }, + -- Policies column + { + heading := "Policies" + headingId := some "policies" + ariaLabel := some "Policies" + items := #[ + { title := "Privacy Policy", url := "/privacy" }, + { title := "Terms of Use", url := "/terms" }, + { title := "Lean Trademark Policy", url := "/trademark-policy" } + ] + } + ] + socialLinks := #[ + { url := "https://bsky.app/profile/lean-lang.org", icon := Icon.blueskyLogo, ariaLabel := some "Bluesky" }, + { url := "https://www.linkedin.com/company/lean-fro", icon := Icon.linkedinLogo, ariaLabel := some "LinkedIn" }, + { url := "https://functional.cafe/@leanprover", icon := Icon.mastodonLogo, ariaLabel := some "Mastodon" }, + { url := "https://x.com/leanprover", icon := Icon.xLogo, ariaLabel := some "X (Twitter)" }, + { url := "https://leanprover.zulipchat.com/", icon := Icon.zulipLogo, ariaLabel := some "Zulip" }, + { url := "https://github.com/leanprover/", icon := Icon.githubLogo, ariaLabel := some "GitHub" } + ] + copyrightText := "© 2025 Lean FRO. All rights reserved." + showThemeSwitcher := true +} + +/-- +Helper to create FRO home navigation item +-/ +def navFroItem (path : Path) : NavBarItem := + { title := .text false "Home" + , url := some "/fro" + , active := path == #["fro"] } + +/-- +Function to get all the items that redirect to pages. +-/ +def getPageItems : TemplateM (Array NavBarItem) := do + let links ← Verso.Web.Util.getDirLinks + return links.map fun (active, url, title) => { title, url, active } + +def isFro (path : Path) : Bool := path[0]?.isEqSome "fro" + +/-- +Build NavBarConfig for FRO section +-/ +def buildFroNavBarConfig : TemplateM NavBarConfig := do + let leftItems ← getPageItems + let path ← currentPath + + let froPathItems (path : Path) : Array NavBarItem := #[ + { title := .text false "About", url := some "/fro/about", active := path == #["fro", "about"] }, + { title := .text false "Team", url := some "/fro/team", active := path == #["fro", "team"] }, + { title := .text false "Roadmap", url := some "/fro/roadmap", active := path == #["fro", "roadmap"] }, + { title := .text false "Contact", url := some "/fro/contact", active := path == #["fro", "contact"] } + ] + + let externalLinks : Array NavBarItem := #[ + { title := .text false "Playground", url := some "https://live.lean-lang.org/?from=lean", blank := true }, + { title := .text false "Reservoir", url := some "https://reservoir.lean-lang.org/", blank := true } + ] + + let rightItems : Array NavBarItem := #[ + { title := Icon.moon, alt := some "Change Theme", classes := some "change-theme" }, + { title := Icon.github, alt := some "Github", url := some "https://github.com/leanprover/lean4", blank := true } + ] + + let menuItems := #[navFroItem path] ++ froPathItems path + + return { + leftItems := leftItems + rightItems := rightItems + menuItems := menuItems + externalLinks := externalLinks + subNavBar := if isFro path then some (SubNavBarConfig.mk (froPathItems path)) else none + } + +def socialMeta : SocialMeta := + { title := "Lean Programming Language", + description := "Lean is an open-source programming language and proof assistant that enables correct, maintainable, and formally verified code.", + image := "https://lean-lang.org/static/png/banner.png", + url := "https://lean-lang.org", + siteName := "Lean Language", + alt := "Lean Programming Language", + articleCreator := "@leanprover", + } + +def headConfig : HeadConfig := + { description := socialMeta.description, + faviconWhite := "https://lean-lang.org/static/favicon-light.ico", + faviconDark := "https://lean-lang.org/static/favicon-dark.ico", + appleTouchIcon := "https://lean-lang.org/static/apple-touch-icon.png", + color := "#3D6AC9" + } + +/-- +Default theme configuration with all design tokens +-/ +def colorTheme : ThemeConfig := { + variables := [ + -- Typography + { name := "font-primary", value := "'Open Sans', Arial, sans-serif" }, + { name := "font-secondary", value := "'Oranienbaum', serif" }, + { name := "fs-xs", value := "0.75rem" }, + { name := "fs-sm", value := "0.875rem" }, + { name := "fs-base", value := "1rem" }, + { name := "fs-md", value := "17px" }, + { name := "fs-lg", value := "1.25rem" }, + { name := "fs-xl", value := "2rem" }, + { name := "fs-2xl", value := "3.3rem" }, + + -- Spacing + { name := "space-1", value := "0.25rem" }, + { name := "space-2", value := "0.5rem" }, + { name := "space-3", value := "0.75rem" }, + { name := "space-4", value := "1rem" }, + { name := "space-5", value := "1.25rem" }, + { name := "space-6", value := "1.5rem" }, + { name := "space-8", value := "2rem" }, + { name := "space-10", value := "2.5rem" }, + { name := "space-12", value := "3rem" }, + { name := "space-13", value := "3.5rem" }, + { name := "space-14", value := "4rem" }, + { name := "space-16", value := "5rem" }, + + -- Semantic spacing + { name := "gap-sm", value := "var(--space-2)" }, + { name := "gap-md", value := "10px" }, + { name := "gap-lg", value := "30px" }, + { name := "gap-xl", value := "100px" }, + + -- Section padding + { name := "section-padding", value := "var(--space-10)" }, + { name := "section-padding-top", value := "var(--space-16)" }, + + -- Border Radius + { name := "radius-sm", value := "0.25rem" }, + { name := "radius-md", value := "0.5rem" }, + { name := "radius-lg", value := "1rem" }, + { name := "radius-pill", value := "9999px" }, + + -- Sizes + { name := "container-width", value := "1240px" }, + { name := "logo-size", value := "1.25rem" }, + { name := "logo-footer-size", value := "60px" }, + { name := "icon-size", value := "64px" }, + + -- Layout + { name := "nav-padding-y", value := "var(--space-6)" }, + { name := "nav-padding-x", value := "10vw" }, + { name := "nav-height", value := "calc(var(--nav-padding-y) * 2 + 2em)" }, + + -- Transitions + { name := "transition-fast", value := "0.2s" }, + { name := "transition-base", value := "0.3s" }, + { name := "transition-slow", value := "0.6s" }, + { name := "transition-delay-none", value := "0s" }, + { name := "transition-delay-small", value := "0.05s" }, + { name := "transition-delay-medium", value := "0.1s" }, + { name := "transition-delay-large", value := "0.15s" }, + + -- Animation + { name := "animation-delay", value := "10000ms" }, + + -- Z-Index + { name := "z-below", value := "-1" }, + { name := "z-normal", value := "0" }, + { name := "z-above", value := "1" }, + { name := "z-header", value := "1000" }, + + -- Colors + { name := "color-surface", value := "#fff" }, + { name := "color-primary", value := "#386EE0" }, + { name := "color-primary-focus", value := "#1D4ED8" }, + { name := "color-primary-light", value := "#4a90e2" }, + { name := "color-secondary", value := "#607D8B" }, + { name := "color-text", value := "#333" }, + { name := "color-text-contrast", value := "white" }, + { name := "color-text-light", value := "#64748b" }, + { name := "color-muted", value := "#607D8B" }, + { name := "color-bg", value := "#F9FBFD" }, + { name := "color-bg-translucent", value := "rgba(249, 251, 253, 0.81)" }, + { name := "color-white", value := "#fff" }, + { name := "color-border", value := "#E4EBF3" }, + { name := "color-border-nav", value := "#E4EBF3" }, + { name := "color-border-light", value := "#D1D9E2" }, + { name := "color-hover", value := "rgba(56, 110, 224, 0.08)" }, + { name := "color-link-hover", value := "#0073e6" }, + { name := "color-shadow", value := "rgba(35, 55, 139, 0.1)" }, + + -- Components + { name := "btn-bg", value := "var(--color-primary)" }, + { name := "btn-text", value := "var(--color-white)" }, + { name := "btn-font", value := "var(--font-primary)" }, + { name := "btn-radius", value := "var(--radius-md)" }, + + -- Card specific + { name := "card-bg", value := "var(--color-white)" }, + { name := "card-border", value := "var(--color-border-light)" }, + + -- Testimonial specific + { name := "testimonial-bg", value := "var(--color-primary)" }, + { name := "testimonial-text", value := "var(--color-white)" } + ], + + darkVariables := [ + + -- Dark theme color overrides + { name := "color-surface", value := "#121212" }, + { name := "color-primary", value := "#3b94ff" }, + { name := "color-primary-focus", value := "#669df6" }, + { name := "color-primary-light", value := "#6aadfe" }, + { name := "color-secondary", value := "#aabfc9" }, + { name := "color-text", value := "#eee" }, + { name := "color-text-light", value := "#bbb" }, + { name := "color-text-contrast", value := "white" }, + { name := "color-muted", value := "#90a4ae" }, + { name := "color-bg", value := "#181818" }, + { name := "color-bg-translucent", value := "rgba(24, 24, 24, 0.85)" }, + { name := "color-white", value := "#1e1e1e" }, + { name := "color-border", value := "#333" }, + { name := "color-border-nav", value := "#333" }, + { name := "color-border-light", value := "#444" }, + { name := "color-hover", value := "rgba(255, 255, 255, 0.08)" }, + { name := "color-link-hover", value := "#4d9efc" }, + { name := "color-shadow", value := "rgba(0, 0, 0, 0.5)" }, + + -- Component overrides + { name := "btn-bg", value := "var(--color-primary)" }, + { name := "btn-text", value := "var(--color-white)" }, + { name := "card-bg", value := "#1f1f1f" }, + { name := "card-border", value := "#2a2a2a" }, + { name := "testimonial-bg", value := "#2e3a59" }, + { name := "testimonial-text", value := "#fff" } + ] +} + +/-- +Lean-specific page type detection functions. +-/ + +def isMarkdownPage : Path → Bool + | _ => true + +def indexPage : Path → Bool + | _ => false + +def needsTitle : Path → Bool + | #["learn"] | #["install"] | #["404"] => false + | _ => true + +def isInstallPage (path : Path) : Bool := + path[0]?.isEqSome "install" + +def isUseCases : Path → Bool + | #["use-cases"] => true + | _ => false + +def isRoadmap : Path → Bool + | #["fro", "roadmap"] => true + | _ => false + +def isPagePost : Path → Bool + | #["use-cases", _] | #["fro", "roadmap", _] => true + | _ => false + + +/-- +Lean-specific post configuration. +-/ +def postConfig : PostConfig := + { hasSpecialStyling := fun path => if isFro path then some "fro" else none } + +/-- +Lean website layout configuration. +-/ +def layoutConfig : LayoutConfig := + { isMarkdownPage := isMarkdownPage, + isIndexPage := indexPage, + needsTitle := needsTitle, + isPagePost := isPagePost, + postConfig := postConfig, + hasSpecialStyling := fun path => if isFro path then some "fro" else if path.isEmpty then "tutorials" else none, + renderPostList := fun path html => + if isUseCases path then + {{
{{ html }}
}} + else + html + } + +def theme : Theme := + Verso.Web.theme + { siteName := "Lean Lang", rootTitle := "Lean enables correct, maintainable, and formally verified code", socialMeta, headConfig, variables := colorTheme } + layoutConfig + buildFroNavBarConfig + {{ + + + }} + (pure footer) diff --git a/Tutorial/VCGen.lean b/Tutorial/VCGen.lean new file mode 100644 index 000000000..bede6d693 --- /dev/null +++ b/Tutorial/VCGen.lean @@ -0,0 +1,1006 @@ +/- +Copyright (c) 2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: Sebastian Graf +-/ + +import VersoManual +import VersoTutorial + +import Manual.Meta +import Manual.Papers + +import Std.Tactic.Do + +open Verso.Genre Manual +open Verso.Genre.Manual.InlineLean +open Verso.Code.External (lit) +open Verso.Genre.Tutorial + +set_option pp.rawOnError true + +set_option verso.docstring.allowMissing true + +set_option linter.unusedVariables false + +set_option linter.typography.quotes true +set_option linter.typography.dashes true + +set_option mvcgen.warning false + +#doc (Tutorial) "Verifying Imperative Programs Using `mvcgen`" => +%%% +tag := "mvcgen-tactic-tutorial" +slug := "mvcgen" +summary := "A demonstration of how to use Lean's verification condition generator to conveniently and compositionally prove properties of monadic programs." +exampleStyle := .inlineLean `MVCGenTutorial +%%% + +This section is a tutorial that introduces the most important concepts of {tactic}`mvcgen` top-down. +Recall that you need to import {module}`Std.Tactic.Do` and open {namespace}`Std.Do` to run these examples: + +:::codeOnly +```imports +import Std.Data.HashMap +import Std.Data.HashSet +``` +::: +```imports +import Std.Tactic.Do +``` +```lean +open Std.Do +``` + +# Preconditions and Postconditions + +One style in which program specifications can be written is to provide a {tech (remote := "reference")}_precondition_ $`P`, which the caller of a program $`\mathit{prog}` is expected to ensure, and a {tech (remote := "reference")}_postcondition_ $`Q`, which the $`\mathit{prog}` is expected to ensure. +The program $`\mathit{prog}` satisfies the specification if running it when the precondition $`P` holds always results in the postcondition $`Q` holding. + +In general, many different preconditions might suffice for a program to ensure the postcondition. +After all, new preconditions can be generated by replacing a precondition $`P_1` with $`P_1 \wedge P_2`. +The {tech (remote := "reference")}_weakest precondition_ $`\textbf{wp}⟦\mathit{prog}⟧(Q)` of a program $`\mathit{prog}` and postcondition $`Q` is a precondition for which $`\mathit{prog}` ensures the postcondition $`Q` and is implied by all other such preconditions. + +One way to prove something about the result of a program is to find the weakest precondition that guarantees the desired result, and then to show that this weakest precondition is simply true. +This means that the postcondition holds no matter what. + + +# Loops and Invariants + +:::leanFirst +As a first example of {tactic}`mvcgen`, the function {name}`mySum` computes the sum of an array using {ref "let-mut" (remote := "reference")}[local mutable state] and a {keywordOf Lean.Parser.Term.doFor}`for` loop: + +```lean +def mySum (l : Array Nat) : Nat := Id.run do + let mut out := 0 + for i in l do + out := out + i + return out +``` +::: + +If {name}`mySum` is correct, then it is equal to {name}`Array.sum`. +In {name}`mySum`, the use of {keywordOf Lean.Parser.Term.do}`do` is an internal implementation detail—the function's signature makes no mention of any monad. +Thus, the proof first manipulates the goal into a form that is amenable to the use of {tactic}`mvcgen`, using the lemma {name}`Id.of_wp_run_eq`. +This lemma states that facts about the result of running a computation in the {name}`Id` monad that terminates normally (`Id` computations never throw exceptions) can be proved by showing that the {tech (remote := "reference")}[weakest precondition] that ensures the desired result is true. +Next, the proof uses {tactic}`mvcgen` to replace the formulation in terms of weakest preconditions with a set of {tech (remote := "reference")}[verification conditions]. + +While {tactic}`mvcgen` is mostly automatic, it does require an invariant for the loop. +A {tech (remote := "reference")}_loop invariant_ is a statement that is both assumed and guaranteed by the body of the loop; if it is true when the loop begins, then it will be true when the loop terminates. + +```lean +theorem mySum_correct (l : Array Nat) : mySum l = l.sum := by + -- Focus on the part of the program with the `do` block (`Id.run ...`) + generalize h : mySum l = x + apply Id.of_wp_run_eq h + -- Break down into verification conditions + mvcgen + -- Specify the invariant which should hold throughout the loop + -- * `out` refers to the current value of the `let mut` variable + -- * `xs` is a `List.Cursor`, which is a data structure representing + -- a list that is split into `xs.prefix` and `xs.suffix`. + -- It tracks how far into the loop we have gotten. + -- Our invariant is that `out` holds the sum of the prefix. + -- The notation ⌜p⌝ embeds a `p : Prop` into the assertion language. + case inv1 => exact ⇓⟨xs, out⟩ => ⌜xs.prefix.sum = out⌝ + -- After specifying the invariant, we can further simplify our goals + -- by "leaving the proof mode". `mleave` is just + -- `simp only [...] at *` with a stable simp subset. + all_goals mleave + -- Prove that our invariant is preserved at each step of the loop + case vc1 ih => + -- The goal here mentions `pref`, which binds the `prefix` field of + -- the cursor passed to the invariant. Unpacking the + -- (dependently-typed) cursor makes it easier for `grind`. + grind + -- Prove that the invariant is true at the start + case vc2 => + grind + -- Prove that the invariant at the end of the loop implies the + -- property we wanted + case vc3 h => + grind +``` + +:::paragraph +Note that the case labels are actually unique prefixes of the full case labels. +Whenever referring to cases, only this prefix should be used; the suffix is merely a hint to the user of where that particular {tech (remote := "reference")}[VC] came from. +For example: + +* `vc1.step` conveys that this {tech (remote := "reference")}[VC] proves the inductive step for the loop +* `vc2.a.pre` is meant to prove that the hypotheses of a goal imply the precondition of a specification (of {name}`forIn`). +* `vc3.a.post.success` is meant to prove that the postcondition of a specification (of {name}`forIn`) implies the desired property. +::: + +:::paragraph +After specifying the loop invariant, the proof can be shortened to just {keyword}`all_goals mleave; grind` (where {tactic}`mleave` leaves the stateful proof mode, cleaning up the proof state). +```lean +theorem mySum_correct_short (l : Array Nat) : mySum l = l.sum := by + generalize h : mySum l = x + apply Id.of_wp_run_eq h + mvcgen + case inv1 => exact ⇓⟨xs, out⟩ => ⌜xs.prefix.sum = out⌝ + all_goals mleave; grind +``` +This pattern is so common that {tactic}`mvcgen` comes with special syntax for it: +```lean +theorem mySum_correct_shorter (l : Array Nat) : mySum l = l.sum := by + generalize h : mySum l = x + apply Id.of_wp_run_eq h + mvcgen + invariants + · ⇓⟨xs, out⟩ => ⌜xs.prefix.sum = out⌝ + with grind +``` +The {multiCode}[{keyword}`mvcgen invariants `{lit}`...`{keyword}` with `{lit}`...`] is an abbreviation for the +tactic sequence {multiCode}[{keyword}`mvcgen; case`{lit}` inv1 => ...`{keyword}`; all_goals mleave; grind`] +above. It is the form that we will be using from now on. +::: + +:::paragraph +It is helpful to compare the proof of {name}`mySum_correct_shorter` to a traditional correctness proof: + +```lean +theorem mySum_correct_vanilla (l : Array Nat) : mySum l = l.sum := by + -- Turn the array into a list + cases l with | mk l => + -- Unfold `mySum` and rewrite `forIn` to `foldl` + simp [mySum] + -- Generalize the inductive hypothesis + suffices h : ∀ out, List.foldl (· + ·) out l = out + l.sum by simp [h] + -- Grind away + induction l with grind +``` +::: + +:::paragraph +This proof is similarly succinct as the proof in {name}`mySum_correct_shorter` that uses {tactic}`mvcgen`. +However, the traditional approach relies on important properties of the program: + +* The {keywordOf Lean.Parser.Term.doFor}`for` loop does not {keywordOf Lean.Parser.Term.doBreak}`break` or {keywordOf Lean.Parser.Term.doReturn}`return` early. Otherwise, the {name}`forIn` could not be rewritten to a {name Array.foldl}`foldl`. +* The loop body {lean (type := "Nat → Nat → Nat")}`(· + ·)` is small enough to be repeated in the proof. +* The loop body does not carry out any effects in the underlying monad (that is, the only effects are those introduced by {keywordOf Lean.Parser.Term.do}`do`-notation). + The {name}`Id` monad has no effects, so all of its comptutations are pure. + While {name}`forIn` could still be rewritten to a {name Array.foldlM}`foldlM`, reasoning about the monadic loop body can be tough for {tactic}`grind`. + +In the following sections, we will go through several examples to learn about {tactic}`mvcgen` and its support library, and also see where traditional proofs become difficult. +This is usually caused by: + +* {keywordOf Lean.Parser.Term.do}`do` blocks using control flow constructs such as {keywordOf Lean.Parser.Term.doFor}`for` loops, {keywordOf Lean.Parser.Term.doBreak}`break`s and early {keywordOf Lean.Parser.Term.doReturn}`return`. +* The use of effects in non-{name}`Id` monads, which affects the implicit monadic context (state, exceptions) in ways that need to be reflected in loop invariants. + +{tactic}`mvcgen` scales to these challenges with reasonable effort. + +::: + +# Control Flow + +:::leanFirst +Let us consider another example that combines {keywordOf Lean.Parser.Term.doFor}`for` loops with an early return. +{name}`List.Nodup` is a predicate that asserts that a given list does not contain any duplicates. +The function {name}`nodup` below decides this predicate: + +```lean +def nodup (l : List Int) : Bool := Id.run do + let mut seen : Std.HashSet Int := ∅ + for x in l do + if x ∈ seen then + return false + seen := seen.insert x + return true +``` +::: + +:::paragraph +This function is correct if it returns {name}`true` for every list that satisfies {name}`List.Nodup` and {name}`false` for every list that does not. +Just as it was in {name}`mySum`, the use of {keywordOf Lean.Parser.Term.do}`do`-notation and the {name}`Id` monad is an internal implementation detail of {name}`nodup`. +Thus, the proof begins by using {name}`Id.of_wp_run_eq` to make the proof state amenable to {tactic}`mvcgen`: +```lean +theorem nodup_correct (l : List Int) : nodup l ↔ l.Nodup := by + generalize h : nodup l = r + apply Id.of_wp_run_eq h + mvcgen + invariants + · Invariant.withEarlyReturn + (onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝) + (onContinue := fun xs seen => + ⌜(∀ x, x ∈ seen ↔ x ∈ xs.prefix) ∧ xs.prefix.Nodup⌝) + with grind +``` +::: + + +:::paragraph +```lean -show +section +variable {l : List Int} {ret : Bool} {seen : Std.HashSet Int} {xs : l.Cursor} +axiom onReturn : Bool → Std.HashSet Int → SPred PostShape.pure.args +axiom onContinue : l.Cursor → Std.HashSet Int → SPred PostShape.pure.args +axiom onExcept : ExceptConds PostShape.pure +``` +The proof has the same succinct structure as for the initial {name}`mySum` example, because we again offload all proofs to {tactic}`grind` and its existing automation around {name}`List.Nodup`. +Therefore, the only difference is in the {tech (remote := "reference")}[loop invariant]. +Since our loop has an {ref "early-return" (remote := "reference")}[early return], we construct the invariant using the helper function {lean}`Invariant.withEarlyReturn`. +This function allows us to specify the invariant in three parts: + +* {lean}`onReturn ret seen` holds after the loop was left through an early return with value {lean}`ret`. + In case of {name}`nodup`, the only value that is ever returned is {name}`false`, in which case {name}`nodup` has decided there _is_ a duplicate in the list. +* {lean}`onContinue xs seen` is the regular induction step that proves the invariant is preserved each loop iteration. + The iteration state is captured by the cursor {lean}`xs`. + The given example asserts that the set {lean}`seen` contains all the elements of previous loop iterations and asserts that there were no duplicates so far. +* {lean}`onExcept` must hold when the loop throws an exception. + There are no exceptions in {lean}`Id`, so we leave it unspecified to use the default. + (Exceptions will be discussed at a later point.) +```lean -show +end +``` +::: + +:::paragraph +Note that the form `mvcgen invariants?` will suggest an initial invariant using {name}`Invariant.withEarlyReturn`, so there is no need to memorize the exact syntax for specifying invariants: +```lean (name := invariants?) +example (l : List Int) : nodup l ↔ l.Nodup := by + generalize h : nodup l = r + apply Id.of_wp_run_eq h + mvcgen invariants? <;> sorry +``` +The tactic suggests a starting invariant. +This starting point will not allow the proof to succeed—after all, if the invariant can be inferred by the system, then there's no need to make the user specify it—but it does provide a reminder of the correct syntax to use for assertions in the current monad: +```leanOutput invariants? +Try this: + [apply] invariants + · + Invariant.withEarlyReturn (onReturn := fun r letMuts => ⌜l.Nodup ∧ (r = true ↔ l.Nodup)⌝) (onContinue := + fun xs letMuts => ⌜xs.prefix = [] ∧ letMuts = ∅ ∨ xs.suffix = [] ∧ l.Nodup⌝) +``` +::: + +:::paragraph +Now consider the following direct (and excessively golfed) proof without {tactic}`mvcgen`: + +```lean +theorem nodup_correct_directly (l : List Int) : nodup l ↔ l.Nodup := by + rw [nodup] + generalize hseen : (∅ : Std.HashSet Int) = seen + change ?lhs ↔ l.Nodup + suffices h : ?lhs ↔ l.Nodup ∧ ∀ x ∈ l, x ∉ seen by grind + clear hseen + induction l generalizing seen with grind [Id.run_pure, Id.run_bind] +``` +::: + +:::paragraph +Some observations: + +* The proof is even shorter than the one with {tactic}`mvcgen`. +* The use of {tactic}`generalize` to generalize the accumulator relies on there being exactly one occurrence of {lean (type := "Std.HashSet Int")}`∅` to generalize. If that were not the case, we would have to copy parts of the program into the proof. This is a no-go for larger functions. +* {tactic}`grind` splits along the control flow of the function and reasons about {name}`Id`, given the right lemmas. + While this works for {name}`Id.run_pure` and {name}`Id.run_bind`, it would not work for {name}`Id.run_seq`, for example, because that lemma is not {tech (key := "E-matching") (remote := "reference")}[E-matchable]. + If {tactic}`grind` would fail, we would be forced to do all the control flow splitting and monadic reasoning by hand until {tactic}`grind` could pick up again. +::: + +The usual way to avoid replicating the control flow of a definition in a proof is to use the {tactic}`fun_cases` or {tactic}`fun_induction` tactics. +Unfortunately, {tactic}`fun_cases` does not help with control flow inside a {name}`forIn` application. +The {tactic}`mvcgen` tactic, on the other hand, ships with support for many {name}`forIn` implementations. +It can easily be extended (with {attrs}`@[spec]` annotations) to support custom {name}`forIn` implementations. +Furthermore, an {tactic}`mvcgen`-powered proof will never need to copy any part of the original program. + +# Compositional Reasoning About Effectful Programs Using Hoare Triples + +:::leanSection +```lean -show +variable (M : Type u → Type v) [Monad M] (α : Type u) +axiom M.run : M α → β → α +``` +The previous examples reasoned about functions defined using {multiCode}[{lean}`Id.run`{lit}` `{keywordOf Lean.Parser.Term.do}`do`{lit}` `] to make use of local mutability and early return in {lit}``. +However, real-world programs often use {keywordOf Lean.Parser.Term.do}`do` notation and monads {lean}`M` to hide away state and failure conditions as implicit “effects”. +In this use case, functions usually omit the {name}`M.run`. +Instead they have a monadic return type {lean}`M α` and compose well with other functions of that return type. +In other words, the monad is part of the function's _interface_, not merely its implementation. +::: + +:::leanFirst +Here is an example involving a stateful function {name}`mkFresh` that returns auto-incremented counter values: + +```lean +structure Supply where + counter : Nat + +def mkFresh : StateM Supply Nat := do + let n ← (·.counter) <$> get + modify fun s => { s with counter := s.counter + 1 } + pure n + +def mkFreshN (n : Nat) : StateM Supply (List Nat) := do + let mut acc := #[] + for _ in [:n] do + acc := acc.push (← mkFresh) + pure acc.toList +``` +::: + +::::leanFirst +:::leanSection +```lean -show +variable (n : Nat) +``` + +{lean}`mkFreshN n` returns {lean}`n` “fresh” numbers, modifying the internal {name}`Supply` state through {name}`mkFresh`. +Here, “fresh” refers to all previously generated numbers being distinct from the next generated number. +We can formulate and prove a correctness property {name}`mkFreshN_correct` in terms of {name}`List.Nodup`: the returned list of numbers should contain no duplicates. +In this proof, {name}`StateM.of_wp_run'_eq` serves the same role that {name}`Id.of_wp_run_eq` did in the preceding examples. +::: + +```lean +theorem mkFreshN_correct (n : Nat) : ((mkFreshN n).run' s).Nodup := by + -- Focus on `(mkFreshN n).run' s`. + generalize h : (mkFreshN n).run' s = x + apply StateM.of_wp_run'_eq h + -- Show something about monadic program `mkFresh n`. + -- The `mkFreshN` and `mkFresh` arguments to `mvcgen` add to an + -- internal `simp` set and makes `mvcgen` unfold these definitions. + mvcgen [mkFreshN, mkFresh] + invariants + -- Invariant: The counter is larger than any accumulated number, + -- and all accumulated numbers are distinct. + -- Note that the invariant may refer to the state through function + -- argument `state : Supply`. Since the next number to accumulate is + -- the counter, it is distinct to all accumulated numbers. + · ⇓⟨xs, acc⟩ state => + ⌜(∀ x ∈ acc, x < state.counter) ∧ acc.toList.Nodup⌝ + with grind +``` +:::: + +## Hoare Triples + +::::::leanSection +```lean -show +universe u v +variable {m : Type u → Type v} {ps : PostShape.{u}} [Monad m] [WP m ps] {α σ ε : Type u} {P : Assertion ps} {Q : PostCond α ps} {prog : m α} {c : Nat} +``` + +A {tech (remote := "reference")}_Hoare triple_ consists of a precondition, a statement, and a postcondition; it asserts that if the precondition holds, then the postcondition holds after running the statement. +In Lean syntax, this is written {lean}`⦃ P ⦄ prog ⦃ Q ⦄`, where {lean}`P` is the precondition, {typed}`prog : m α` is the statement, and {lean}`Q` is the postcondition. +{lean}`P` and {lean}`Q` are written in an assertion language that is determined by the specific monad {lean}`m`.{margin}[In particular, monad's instance of the type class {name}`WP` specifies the ways in which assertions may refer to the monad's state or the exceptions it may throw.] + +:::leanSection +```lean -show +variable {stmt1 stmt2 : m PUnit} {ps : PostShape.{0}} {P : Assertion ps} {Q : PostCond Unit ps} {P' : Assertion ps} {Q' : PostCond Unit ps} +``` + +Specifications as Hoare triples are compositional because they allow statements to be sequenced. +Given {lean}`⦃P⦄ stmt1 ⦃Q⦄` and {lean}`⦃P'⦄ stmt2 ⦃Q'⦄`, if {lean}`Q` implies {lean}`P'` then {lean}`⦃P⦄ (do stmt1; stmt2) ⦃Q'⦄`. +Just as proofs about ordinary functions can rely on lemmas about the functions that they call, proofs about monadic programs can use lemmas that are specified in terms of Hoare triples. +::: + +:::::paragraph +One suitable specification for {name}`mkFresh` as a Hoare triple is this translation of {name}`mkFreshN_correct`: +::::displayOnly +:::leanSection +```lean -show +variable {n : Nat} +``` +```leanTerm +⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄ +``` +::: +:::: +```lean -show +variable {p : Prop} +``` +Corner brackets embed propositions into the monadic assertion language, so {lean}`⌜p⌝` is the assertion of the proposition {lean}`p`. +The precondition {lean}`⌜True⌝` asserts that {lean}`True` is true; this trivial precondition is used to state that the specification imposes no requirements on the state in which it is called. +The postcondition states that the result value is a list with no duplicate elements. +::::: + +:::::paragraph +A specification for the single-step {name}`mkFresh` describes its effects on the monad's state: +::::displayOnly +:::leanSection +```lean -show +variable {n : Nat} +``` +```leanTerm +∀ (c : Nat), +⦃fun state => ⌜state.counter = c⌝⦄ +mkFresh +⦃⇓ r state => ⌜r = c ∧ c < state.counter⌝⦄ +``` +When working in a state monad, preconditions may be parameterized over the value of the state prior to running the code. +Here, the universally quantified {name}`Nat` is used to _relate_ the initial state to the final state; the precondition is used to connect it to the initial state. +Similarly, the postcondition may also accept the final state as a parameter. +This Hoare triple states: + +> If {lean}`c` refers to the {name}`Supply.counter` field of the {name}`Supply` prestate, then running {name}`mkFresh` returns {lean}`c` and modifies the {name}`Supply.counter` of the poststate to be larger than {lean}`c`. + +Note that this specification is lossy: {name}`mkFresh` could increment its state by an arbitrary non-negative amount and still satisfy the specification. +This is good, because specifications may _abstract over_ uninteresting implementation details, ensuring resilient and small proofs. +::: +:::: +::::: + + +:::paragraph +Hoare triples are defined in terms of a logic of stateful predicates plus a {tech (remote := "reference")}[weakest precondition] semantics {lean}`wp⟦prog⟧` that translates monadic programs into this logic. +A weakest precondition semantics is an interpretation of programs as mappings from postconditions to the weakest precondition that the program would require to ensure the postcondition; in this interpretation, programs are understood as {tech (key := "predicate transformer semantics") (remote := "reference")}_predicate transformers_. +The Hoare triple syntax is notation for {name}`Std.Do.Triple`: + +```lean -keep +-- This is the definition of Std.Do.Triple: +def Triple [WP m ps] {α : Type u} (prog : m α) + (P : Assertion ps) (Q : PostCond α ps) : Prop := + P ⊢ₛ wp⟦prog⟧ Q +``` +::: + +```lean -show +variable {σ : Type u} +``` +:::paragraph +The {name}`WP` type class maps a monad {lean}`m` to its {name}`PostShape` {lean}`ps`, and this {name}`PostShape` governs the exact shape of the {name}`Std.Do.Triple`. +Many of the standard monad transformers such as {name}`StateT`, {name}`ReaderT` and {name}`ExceptT` come with a canonical {name}`WP` instance. +For example, {lean}`StateT σ` comes with a {name}`WP` instance that adds a {lean}`σ` argument to every {name}`Assertion`. +Stateful entailment `⊢ₛ` eta-expands through these additional {lean}`σ` arguments. +For {name}`StateM` programs, the following type is definitionally equivalent to {name}`Std.Do.Triple`: + +```lean +def StateMTriple {α σ : Type u} (prog : StateM σ α) + (P : σ → ULift Prop) (Q : (α → σ → ULift Prop) × PUnit) : Prop := + ∀ s, (P s).down → let (a, s') := prog.run s; (Q.1 a s').down +``` +```lean -show +example : @StateMTriple α σ = Std.Do.Triple (m := StateM σ) := rfl +``` +::: + + +```lean -show +variable {p : Prop} +``` + +The common postcondition notation `⇓ r => ...` injects an assertion of type {lean}`α → Assertion ps` into +{lean}`PostCond α ps` (the `⇓` is meant to be parsed like `fun`); in case of {name}`StateM` by adjoining it with an empty tuple {name}`PUnit.unit`. +The shape of postconditions becomes more interesting once exceptions enter the picture. +The notation {lean}`⌜p⌝` embeds a pure hypotheses {lean}`p` into a stateful assertion. +Vice versa, any stateful hypothesis {lean}`P` is called _pure_ if it is equivalent to {lean}`⌜p⌝` +for some {lean}`p`. +Pure, stateful hypotheses may be freely moved into the regular Lean context and back. +(This can be done manually with the {tactic}`mpure` tactic.) + +:::::: + +## Composing Specifications + +Nested unfolding of definitions as in {multiCode}[{tactic}`mvcgen`{lit}` [`{name}`mkFreshN`{lit}`, `{name}`mkFresh`{lit}`]`] is quite blunt but effective for small programs. +A more compositional way is to develop individual {tech (remote := "reference")}_specification lemmas_ for each monadic function. +A specification lemma is a Hoare triple that is automatically used during {tech (remote := "reference")}[verification condition] generation to obtain the pre- and postconditions of each statement in a {keywordOf Lean.Parser.Term.do}`do`-block. +When the system cannot automatically prove that the postcondition of one statement implies the precondition of the next, then this missing reasoning step becomes a verification condition. + +:::paragraph +Specification lemmas can either be passed as arguments to {tactic}`mvcgen` or registered in a global (or {keyword}`scoped`, or {keyword}`local`) database of specifications using the {attrs}`@[spec]` attribute: + +```lean +@[spec] +theorem mkFresh_spec (c : Nat) : + ⦃fun state => ⌜state.counter = c⌝⦄ + mkFresh + ⦃⇓ r state => ⌜r = c ∧ c < state.counter⌝⦄ := by + -- Unfold `mkFresh` and blast away: + mvcgen [mkFresh] with grind + +@[spec] +theorem mkFreshN_spec (n : Nat) : + ⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄ := by + -- `mvcgen [mkFreshN, mkFresh_spec]` if `mkFresh_spec` were not + -- registered with `@[spec]` + mvcgen [mkFreshN] + invariants + -- As before: + · ⇓⟨xs, acc⟩ state => + ⌜(∀ x ∈ acc, x < state.counter) ∧ acc.toList.Nodup⌝ + with grind +``` +::: + +:::paragraph +The original correctness theorem can now be proved using {tactic}`mvcgen` alone: +```lean +theorem mkFreshN_correct_compositional (n : Nat) : + ((mkFreshN n).run' s).Nodup := by + generalize h : (mkFreshN n).run' s = x + apply StateM.of_wp_run'_eq h + mvcgen +``` +The specification lemma {name}`mkFreshN_spec` is automatically used by {tactic}`mvcgen`. +::: + + +## An Advanced Note About Pure Preconditions and a Notion of Frame Rule + +This subsection is a bit of a digression and can be skipped on first reading. + +::::leanSection + +:::codeOnly +```lean +axiom M : Type → Type +variable {x y : UInt8} [Monad M] [WP M .pure] +def addQ (x y : UInt8) : M UInt8 := pure (x + y) +local infix:1023 " +? " => addQ +``` +```lean -show +axiom dots {α} : α +local notation "…" => dots +``` +::: + +Say the specification for some [`Aeneas`](https://github.com/AeneasVerif/aeneas)-inspired monadic addition function {typed}`x +? y : M UInt8` has the +requirement that the addition won't overflow, that is, `h : x.toNat + y.toNat ≤ UInt8.size`. +Should this requirement be encoded as a regular Lean hypothesis of the specification (`add_spec_hyp`) or should this requirement be encoded as a pure precondition of the Hoare triple, using `⌜·⌝` notation (`add_spec_pre`)? + +:::displayOnly +```lean +theorem add_spec_hyp (x y : UInt8) + (h : x.toNat + y.toNat ≤ UInt8.size) : + ⦃⌜True⌝⦄ x +? y ⦃⇓ r => ⌜r.toNat = x.toNat + y.toNat⌝⦄ := … + +theorem add_spec_pre (x y : UInt8) : + ⦃⌜x.toNat + y.toNat ≤ UInt8.size⌝⦄ + x +? y + ⦃⇓ r => ⌜r.toNat = x.toNat + y.toNat⌝⦄ := … +``` +::: + +:::: + +The first approach is advisable, although it should not make a difference in practice. +The VC generator will move pure hypotheses from the stateful context into the regular Lean context, so the second form turns effectively into the first form. +This is referred to as {deftech}_framing_ hypotheses (cf. the {tactic}`mpure` and {tactic}`mframe` tactics). +Hypotheses in the Lean context are part of the immutable {deftech}_frame_ of the stateful logic, because in contrast to stateful hypotheses they survive the rule of consequence. + +# Monad Transformers and Lifting + +Real-world programs often use monads that are built from multiple {tech (remote := "reference")}[monad transformers], with operations being frequently {ref "lifting-monads" (remote := "reference")}[lifted] from one monad to another. +Verification of these programs requires taking this into account. +We can tweak the previous example to demonstrate this. + +:::codeOnly +```lean +namespace Transformers +``` +```lean -show +variable {m : Type → Type} {α : Type} {ps : PostShape.{0}} +attribute [-instance] Lake.instMonadLiftTOfMonadLift_lake +``` +::: + +::::paragraph +:::leanFirst +Now, there is an application with two separate monads, both built using transformers: +```lean +abbrev CounterM := StateT Supply (ReaderM String) + +abbrev AppM := StateT Bool CounterM +``` + +Instead of using {lean}`StateM Supply`, {name}`mkFresh` uses {lean}`CounterM`: +```lean +def mkFresh : CounterM Nat := do + let n ← (·.counter) <$> get + modify fun s => { s with counter := s.counter + 1 } + pure n +``` + +{name}`mkFreshN` is defined in terms of {name}`AppM`, which includes multiple states and a reader effect. +The definition of {name}`mkFreshN` lifts {name}`mkFresh` into {name}`AppM`: +```lean +def mkFreshN (n : Nat) : AppM (List Nat) := do + let mut acc := #[] + for _ in [:n] do + let n ← mkFresh + acc := acc.push n + return acc.toList +``` +::: +:::: + +::::paragraph +Then the {tactic}`mvcgen`-based proof goes through unchanged: +```lean +@[spec] +theorem mkFresh_spec (c : Nat) : + ⦃fun state => ⌜state.counter = c⌝⦄ + mkFresh + ⦃⇓ r state => ⌜r = c ∧ c < state.counter⌝⦄ := by + mvcgen [mkFresh] with grind + +@[spec] +theorem mkFreshN_spec (n : Nat) : + ⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄ := by + -- `liftCounterM` here ensures unfolding + mvcgen [mkFreshN] + invariants + · ⇓⟨xs, acc⟩ _ state => + ⌜(∀ n ∈ acc, n < state.counter) ∧ acc.toList.Nodup⌝ + with grind +``` +:::: + +:::leanSection +```lean -show +universe u v +variable {m : Type u → Type v} {ps : PostShape.{u}} [WP m ps] {α : Type u} {prog : m α} +``` +The {name}`WPMonad` type class asserts that {lean}`wp⟦prog⟧` distributes over the {name}`Monad` operations (“monad morphism”). +This proof might not look much more exciting than when only a single monad was involved. +However, under the radar of the user the proof builds on a cascade of specifications for {name}`MonadLift` instances. + +::: + +:::codeOnly +```lean +end Transformers +``` +::: + +# Exceptions + +::::leanSection +```lean -show +universe u v +variable {m : Type u → Type v} {ps : PostShape.{u}} [WP m ps] {P : Assertion ps} {α : Type u} {prog : m α} {Q' : α → Assertion ps} +``` + +If {keyword}`let mut` is the {keywordOf Lean.Parser.Term.do}`do`-equivalent of {name}`StateT`, then early {keywordOf Lean.Parser.Term.doReturn}`return` is the equivalent of {name}`ExceptT`. +We have seen how the {tactic}`mvcgen` copes with {name}`StateT`; here we will look at the program logic's support for {name}`ExceptT`. + +Exceptions are the reason why the type of postconditions {lean}`PostCond α ps` is not simply a single condition of type {lean}`α → Assertion ps` for the success case. +To see why, suppose the latter was the case, and suppose that program {lean}`prog` throws an exception in a prestate satisfying {lean}`P`. +Should we be able to prove {lean}`⦃P⦄ prog ⦃⇓ r => Q' r⦄`? +(Recall that `⇓` is grammatically similar to `fun`.) +There is no result `r`, so it is unclear what this proof means for {lean}`Q'`! + +So there are two reasonable options, inspired by non-termination in traditional program logics: + +: The {tech (remote := "reference")}_total correctness interpretation_ + + {lean}`⦃P⦄ prog ⦃⇓ r => Q' r⦄` asserts that, given {lean}`P` holds, then {lean}`prog` terminates _and_ {lean}`Q'` holds for the result. + +: The {tech (remote := "reference")}_partial correctness interpretation_ + + {lean}`⦃P⦄ prog ⦃⇓ r => Q' r⦄` asserts that, given {lean}`P` holds, and _if_ {lean}`prog` terminates _then_ {lean}`Q'` holds for the result. + +The notation {lean}`⇓ r => Q' r` has the total interpretation, while {lean}`⇓? r => Q' r` has the partial interpretation. + +In the running example, {lean}`⦃P⦄ prog ⦃⇓ r => Q' r⦄` is unprovable, but {lean}`⦃P⦄ prog ⦃⇓? r => Q' r⦄` is trivially provable. +However, the binary choice suggests that there is actually a _spectrum_ of correctness properties to express. +The notion of postconditions {name}`PostCond` in `Std.Do` supports this spectrum. + +:::: + +:::codeOnly +```lean +namespace Exceptions +``` +::: + + +For example, suppose that our {name}`Supply` of fresh numbers is bounded and we want to throw an exception if the supply is exhausted. +Then {name}`mkFreshN` should throw an exception _only if_ the supply is indeed exhausted, as in this implementation: +```lean +structure Supply where + counter : Nat + limit : Nat + property : counter ≤ limit + +def mkFresh : EStateM String Supply Nat := do + let supply ← get + if h : supply.counter = supply.limit then + throw s!"Supply exhausted: {supply.counter} = {supply.limit}" + else + let n := supply.counter + have := supply.property + set { supply with counter := n + 1, property := by grind } + pure n +``` + +The following correctness property expresses this: +```lean +@[spec] +theorem mkFresh_spec (c : Nat) : + ⦃fun state => ⌜state.counter = c⌝⦄ + mkFresh + ⦃post⟨fun r state => ⌜r = c ∧ c < state.counter⌝, + fun _ state => ⌜c = state.counter ∧ c = state.limit⌝⟩⦄ := by + mvcgen [mkFresh] with grind +``` + +In this property, the postcondition has two branches: the first covers successful termination, and the second applies when an exception is thrown. +The monad's {name}`WP` instance determines both how many branches the postcondition may have and the number of parameters in each branch: each exception that might be triggered gives rise to an extra branch, and each state gives an extra parameter. + +:::leanFirst +In this new monad, {name}`mkFreshN`'s implementation is unchanged, except for the type signature: +```lean +def mkFreshN (n : Nat) : EStateM String Supply (List Nat) := do + let mut acc := #[] + for _ in [:n] do + acc := acc.push (← mkFresh) + pure acc.toList +``` +::: + +:::paragraph +However, the specification lemma must account for both successful termination and exceptions being thrown, in both the postcondition and the loop invariant: +```lean +@[spec] +theorem mkFreshN_spec (n : Nat) : + ⦃⌜True⌝⦄ + mkFreshN n + ⦃post⟨fun r => ⌜r.Nodup⌝, + fun _msg state => ⌜state.counter = state.limit⌝⟩⦄ := by + mvcgen [mkFreshN] + invariants + · post⟨fun ⟨xs, acc⟩ state => + ⌜(∀ n ∈ acc, n < state.counter) ∧ acc.toList.Nodup⌝, + fun _msg state => ⌜state.counter = state.limit⌝⟩ + with grind +``` +::: + +:::paragraph +The final proof uses the specification lemmas and {tactic}`mvcgen`, just as before: +```lean +theorem mkFreshN_correct (n : Nat) : + match (mkFreshN n).run s with + | .ok l _ => l.Nodup + | .error _ s' => s'.counter = s'.limit := by + generalize h : (mkFreshN n).run s = x + apply EStateM.of_wp_run_eq h + mvcgen +``` +::: + +:::codeOnly +```lean +end Exceptions +``` +::: + +:::leanSection +```lean -show +universe u v +variable {m : Type u → Type v} {ps : PostShape.{u}} [WP m ps] {P : Assertion ps} {α σ ε : Type u} {prog : m α} {Q' : α → Assertion ps} +``` + +Just as any {lean}`StateT σ`-like monad transformer gives rise to a {lean}`PostShape.arg σ` layer in the {lean}`ps` that {name}`WP` maps into, any {lean}`ExceptT ε`-like layer gives rise to a {lean}`PostShape.except ε` layer. + +Every {lean}`PostShape.arg σ` adds another `σ → ...` layer to the language of {lean}`Assertion`s. +Every {lean}`PostShape.except ε` leaves the {lean}`Assertion` language unchanged, but adds another exception +condition to the postcondition. +Hence the {name}`WP` instance for {lean}`EStateM ε σ` maps to the {name}`PostShape` {lean}`PostShape.except ε (.arg σ .pure)`, just +as for {lean}`ExceptT ε (StateM σ)`. +::: + + +# Extending `mvcgen` With Support for Custom Monads + +The {tactic}`mvcgen` framework is designed to be extensible. +None of the monads presented so far have in any way been hard-coded into {tactic}`mvcgen`. +Rather, {tactic}`mvcgen` relies on instances of the {name}`WP` and {name}`WPMonad` type class and user-provided specifications to generate {tech (remote := "reference")}[verification conditions]. + +:::leanSection +```lean -show +variable {m : Type u → Type v} [Monad m] {ps : PostShape.{u}} +``` + +The {name}`WP` instance defines the weakest precondition interpretation of a monad {lean}`m` into a predicate transformer {lean}`PredTrans ps`, +and the matching {name}`WPMonad` instance asserts that this translation distributes over the {name}`Monad` operations. +::: + +:::::paragraph +::::leanFirst +Suppose one wants to use `mvcgen` to generate verification conditions for programs generated by [`Aeneas`](https://github.com/AeneasVerif/aeneas). +`Aeneas` translates Rust programs into Lean programs in the following {name}`Result` monad: + +```lean +inductive Error where + | integerOverflow: Error + -- ... more error kinds ... + +inductive Result (α : Type u) where + | ok (v: α): Result α + | fail (e: Error): Result α + | div +``` +:::codeOnly +```lean +instance Result.instMonad : Monad Result where + pure x := .ok x + bind x f := match x with + | .ok v => f v + | .fail e => .fail e + | .div => .div + +instance Result.instLawfulMonad : LawfulMonad Result := by + apply LawfulMonad.mk' <;> (simp only [Result.instMonad]; grind) +``` +::: +:::: +::::: + +:::paragraph +There are both {inst}`Monad Result` and {inst}`LawfulMonad Result` instances. +Supporting this monad in {tactic}`mvcgen` is a matter of: + +1. Adding {name}`WP` and {name}`WPMonad` instances for {name}`Result` +2. Registering specification lemmas for the translation of basic Rust primitives such as addition etc. +::: + +::::paragraph +:::leanSection +```lean -show +universe u v +variable {m : Type u → Type v} {ps : PostShape.{u}} [WP m ps] {P : Assertion ps} {α σ ε : Type u} {prog : m α} {Q' : α → Assertion ps} +``` +The {name}`WP` instance for {name}`Result` specifies a postcondition shape {lean (type := "PostShape.{0}")}`.except Error .pure` because there are no state-like effects, but there is a single exception of type {lean}`Error`. +The {name}`WP` instance translates programs in {lean}`Result α` to predicate transformers in {lean}`PredTrans ps α`. +That is, a function in {lean}`PostCond α ps → Assertion ps`, mapping a postcondition to its weakest precondition. +The implementation of {name}`WP.wp` reuses the implementation for {lean}`Except Error` for two of its cases, and maps diverging programs to {lean}`False`. +The instance is named so that it can be more easily unfolded in proofs about it. +::: +```lean +instance Result.instWP : WP Result (.except Error .pure) where + wp + | .ok v => wp (pure v : Except Error _) + | .fail e => wp (throw e : Except Error _) + | .div => PredTrans.const ⌜False⌝ +``` +:::: + +:::paragraph +The implementation of {name}`WP.wp` should distribute over the basic monad operators: +```lean +instance : WPMonad Result (.except Error .pure) where + wp_pure := by + intros + ext Q + simp [wp, PredTrans.pure, pure, Except.pure, Id.run] + wp_bind x f := by + simp only [Result.instWP, bind] + ext Q + cases x <;> simp [PredTrans.bind, PredTrans.const] +``` +::: + +```lean +theorem Result.of_wp {α} {x : Result α} (P : Result α → Prop) : + (⊢ₛ wp⟦x⟧ post⟨fun a => ⌜P (.ok a)⌝, + fun e => ⌜P (.fail e)⌝⟩) → P x := by + intro hspec + simp only [instWP] at hspec + split at hspec <;> simp_all +``` + +:::leanSection +```lean -show +universe u v +variable {m : Type u → Type v} {ps : PostShape.{u}} [WP m ps] {P : Assertion ps} {α σ ε : Type u} {prog : m α} {Q' : α → Assertion ps} +``` + +The definition of the {name}`WP` instance determines what properties can be derived from proved specifications via {lean}`Result.of_wp`. +This lemma defines what “weakest precondition” means. +::: + +:::paragraph +To exemplify the second part, here is an example definition of {name}`UInt32` addition in {name}`Result` that models integer overflow: + +```lean +instance : MonadExcept Error Result where + throw e := .fail e + tryCatch x h := match x with + | .ok v => pure v + | .fail e => h e + | .div => .div + +def addOp (x y : UInt32) : Result UInt32 := + if x.toNat + y.toNat ≥ UInt32.size then + throw .integerOverflow + else + pure (x + y) +``` +::: + +:::paragraph +There are two relevant specification lemmas to register: + +```lean +@[spec] +theorem Result.throw_spec {α Q} (e : Error) : + ⦃Q.2.1 e⦄ throw (m := Result) (α := α) e ⦃Q⦄ := id + +@[spec] +theorem addOp_ok_spec {x y} (h : x.toNat + y.toNat < UInt32.size) : + ⦃⌜True⌝⦄ + addOp x y + ⦃⇓ r => ⌜r = x + y ∧ (x + y).toNat = x.toNat + y.toNat⌝⦄ := by + mvcgen [addOp] with (simp_all; try grind) +``` +::: + +:::paragraph +This is already enough to prove the following example: + +```lean +example : + ⦃⌜True⌝⦄ + do let mut x ← addOp 1 3 + for _ in [:4] do + x ← addOp x 5 + return x + ⦃⇓ r => ⌜r.toNat = 24⌝⦄ := by + mvcgen + invariants + · ⇓⟨xs, x⟩ => ⌜x.toNat = 4 + 5 * xs.prefix.length⌝ + with (simp_all [UInt32.size]; try grind) +``` +::: + +# Proof Mode for Stateful Goals + +```lean -show +variable {σs : List (Type u)} {H T : SPred σs} +``` + +It is a priority of {tactic}`mvcgen` to break down monadic programs into {tech (remote := "reference")}[verification conditions] that are straightforward to understand. +For example, when the monad is monomorphic and all loop invariants have been instantiated, an invocation of {multiCode}[{tactic}`all_goals`{lit}` `{tactic}`mleave`] should simplify away any {name}`Std.Do.SPred`-specific constructs and leave behind a goal that is easily understood by humans and {tactic}`grind`. +This {multiCode}[{tactic}`all_goals`{lit}` `{tactic}`mleave`]step is carried out automatically by {tactic}`mvcgen` after loop invariants have been instantiated. + +However, there are times when {tactic}`mleave` will be unable to remove all {name}`Std.Do.SPred` constructs. +In this case, verification conditions of the form {lean}`H ⊢ₛ T` will be left behind. +The assertion language {name}`Assertion` translates into an {name}`Std.Do.SPred` as follows: + +```lean -keep +abbrev PostShape.args : PostShape.{u} → List (Type u) + | .pure => [] + | .arg σ s => σ :: PostShape.args s + | .except _ s => PostShape.args s + +abbrev Assertion (ps : PostShape.{u}) : Type u := + SPred (PostShape.args ps) +``` + + +:::leanSection +```lean -show +universe u v +variable {m : Type u → Type v} {ps : PostShape.{u}} [WP m ps] {P : Assertion ps} {α σ ε : Type u} {prog : m α} {Q' : α → Assertion ps} +``` + +A common case for when a VC of the form {lean}`H ⊢ₛ T` is left behind is when the base monad {lean}`m` is polymorphic. +In this case, the proof will depend on a {lean}`WP m ps` instance which governs the translation into the {name}`Assertion` language, but the exact correspondence to `σs : List (Type u)` is yet unknown. +To successfully discharge such a VC, `mvcgen` comes with an entire proof mode that is inspired by that of the Iris concurrent separation logic. +(In fact, the proof mode was adapted in large part from its Lean clone, [`iris-lean`](https://github.com/leanprover-community/iris-lean).) +The {ref "tactic-ref-spred" (remote := "reference")}[tactic reference] contains a list of all proof mode tactics. + +::: diff --git a/TutorialMain.lean b/TutorialMain.lean new file mode 100644 index 000000000..586e18f70 --- /dev/null +++ b/TutorialMain.lean @@ -0,0 +1,204 @@ +/- +Copyright (c) 2024-2025 Lean FRO LLC. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Author: David Thrane Christiansen +-/ + +import VersoTutorial +import VersoBlog +import Tutorial +import Tutorial.Meta.Theme +import VersoWeb.Theme + +open Verso.Genre.Manual +open Verso.Genre.Tutorial +open Verso.Genre.Blog (Page) + +open Verso.Output.Html in +def plausible := {{ + + }} + + +def version (_content : Array (Verso.Doc.Inline g)) : Verso.Doc.Inline g := .text Lean.versionString + +def manualLink (args : Array (Verso.Doc.Inline g)) : Verso.Doc.Inline g:= + Verso.Doc.Inline.link args Lean.manualRoot + +open Verso.Doc Concrete in +def tutorials : Tutorials where + content := + (verso (Page) "Tutorials" + ::::::: + These tutorials cover version {version}[] of Lean. + While the {manualLink}[reference manual] describes the system and its features in detail, these tutorials introduce one specific aspect. + :::::::).toPart + + topics := #[ + { title := #[inlines!"Tactics"], + titleString := "Tactics" + description := #[blocks!"These tutorials demonstrate Lean's advanced proof automation."] + tutorials := #[%doc Tutorial.VCGen, %doc Tutorial.Grind.IndexMap] + } + + ] + +open Verso.Genre.Blog Site.Syntax in +open Verso.Doc in +def leanSite : Site := + site home / + "install" install + "learn" learn + "community" community + "use-cases" «use-cases» + "fro" fro +where + placeholder title : Part Page := { title := #[.text title], titleString := title, metadata := none, content := #[], subParts := #[] } + home : VersoDoc Page := { construct := fun _ => placeholder "Lean" } + install : VersoDoc Page := { construct := fun _ => placeholder "Install" } + learn : VersoDoc Page := { construct := fun _ => placeholder "Learn" } + community : VersoDoc Page := { construct := fun _ => placeholder "Community" } + «use-cases» : VersoDoc Page := { construct := fun _ => placeholder "Use Cases" } + «trademark-policy» : VersoDoc Page := { construct := fun _ => placeholder "Trademark Policy" } + privacy : VersoDoc Page := { construct := fun _ => placeholder "Privacy" } + terms : VersoDoc Page := { construct := fun _ => placeholder "Terms" } + logos : VersoDoc Page := { construct := fun _ => placeholder "Logos" } + fro : VersoDoc Page := { construct := fun _ => placeholder "FRO" } + documentation : VersoDoc Page := { construct := fun _ => placeholder "Documentation" } + examples : VersoDoc Page :={ construct := fun _ => placeholder "Examples" } + publications : VersoDoc Page := { construct := fun _ => placeholder "Publications" } + links : VersoDoc Page := { construct := fun _ => placeholder "Links" } + people : VersoDoc Page := { construct := fun _ => placeholder "People" } + + +def codeColors := r#" +:root { + --verso-code-color: var(--color-text) !important; +} +"# + +def localToCStyle := r#" +/* +nav.local-toc { + position: fixed; + top: calc(var(--nav-height) + 1rem + 60px); + right: calc((100vw - var(--container-width)) / 2 - 15rem - 2rem); + width: 15rem; + font-size: 0.875rem; + max-height: calc(100vh - 2rem - var(--nav-height) - 60px); + overflow-y: auto; +} +*/ + +nav.local-toc h1 { + display: none; +} + +nav.local-toc ol { + list-style: none; + margin: 0; + padding: 0; + display: none; +} + +nav.local-toc li { + margin: 0.5rem 0; +} + +nav.local-toc a { + color: var(--verso-text-color); + text-decoration: none; +} + +nav.local-toc a:hover { + color: var(--color-accent); +} + +/* On smaller screens, display normally */ +/* +@media (max-width: 90rem) { + nav.local-toc { + position: static; + right: auto; + width: auto; + max-height: none; + border-left: 3px solid var(--color-border); + padding-left: 1rem; + margin-bottom: 1rem; + } +} +*/ + +nav.local-toc ol ol { + padding-left: 1rem; + margin-top: 0.25rem; +} + +nav.local-toc ol ol li { + margin: 0.25rem 0; +} + +/*******/ +nav.local-toc > div:first-child { + margin-bottom: 1rem; +} + +nav.local-toc h1 { + font-size: 1rem; + margin: 0 0 0.5rem 0; + font-weight: 600; +} + +nav.local-toc .code-links { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +nav.local-toc .code-link { + font-size: 0.8125rem; + padding: 0.25rem 0.5rem; + border: 1px solid var(--color-border); + border-radius: 6px; + color: var(--verso-text-color); + text-decoration: none; + display: inline-block; +} + +nav.local-toc .code-link:hover { + background: var(--verso-selected-color); + border-color: var(--color-accent); +} + +nav.local-toc .code-link code { + background: none; + padding: 0; + font-size: inherit; +} + +nav.local-toc .code-link.live::before { + content: "↪ "; + margin-right: 0.25rem; +} + +nav.local-toc .code-link.download::before { + content: "📦 "; + margin-right: 0.25rem; +} +"# + + +open Verso.Genre.Blog.Template in +open Verso.Output Html in +def theme := + { LeanLangOrg.theme with + pageTemplate := do + let tutorialNav : Option Html ← param? "tutorialNav" + let content : Html ← param "content" + let content := tutorialNav.getD .empty ++ content + withTheReader Context (fun ρ => { ρ with params := ρ.params.insert "content" content }) LeanLangOrg.theme.pageTemplate + cssFiles := LeanLangOrg.theme.cssFiles.push ("code-colors.css", codeColors) |>.push ("local-toc.css", localToCStyle) + } + +def main := + tutorialsMain tutorials (config := {destination := "_tutorial-out"}) (theme := theme) (navSite := leanSite) diff --git a/config/production-remotes-reference.json.template b/config/production-remotes-reference.json.template new file mode 100644 index 000000000..8aef1ccdf --- /dev/null +++ b/config/production-remotes-reference.json.template @@ -0,0 +1,12 @@ +{ + "version": 0, + "sources": { + "tutorials": { + "root": "/doc/tutorials/__VERSION__", + "shortName": "tutorials", + "longName": "Lean Tutorials", + "updateFrequency": "always", + "sources": [{"local": "_tutorial-out/xref.json"}] + } + } +} diff --git a/config/production-remotes-tutorials.json.template b/config/production-remotes-tutorials.json.template new file mode 100644 index 000000000..1f03aa81e --- /dev/null +++ b/config/production-remotes-tutorials.json.template @@ -0,0 +1,12 @@ +{ + "version": 0, + "sources": { + "reference": { + "root": "/doc/reference/__VERSION__", + "updateFrequency": "always", + "shortName": "ref", + "longName": "Lean Language Reference", + "sources": [{"local": "_out/html-multi/xref.json"}, "default"] + } + } +} diff --git a/deploy/generate.sh b/deploy/generate.sh index 21fe62ac7..6cd7f8963 100755 --- a/deploy/generate.sh +++ b/deploy/generate.sh @@ -1,3 +1,11 @@ #!/usr/bin/env bash +set -euo pipefail -lake --quiet exe generate-manual --depth 2 --verbose --without-html-single --output "html" +# VERSION is set by release-tag.yml from the git tag +if [ -z "${VERSION:-}" ]; then + echo "Error: VERSION environment variable must be set" + exit 1 +fi + +# Build both manual and tutorials using the combined generate script +./generate-html.sh --mode production --version "$VERSION" --output "html/site" diff --git a/deploy/overlay.py b/deploy/overlay.py index 9829be465..431247e47 100644 --- a/deploy/overlay.py +++ b/deploy/overlay.py @@ -60,16 +60,18 @@ def apply_overlays(deploy_dir, site_dir): print (f"overlay.py: the latest stable lean version is {latest_stable_version}") header = "// This file was automatically generated by reference-manual/deploy/overlay.py" for inner in Path(deploy_dir).iterdir(): - if inner.is_dir() and (inner / "static").is_dir(): - # Generate some version metadata - filename = inner / "static" / "metadata.js" - content = f"{header}\nwindow.metadata = {{versionString: \"{inner}\"}};\n" - if str(inner) == latest_version or str(inner) == "latest": - content += f"window.metadata.latest = true;\n" - if str(inner) == latest_stable_version or str(inner) == "stable": - content += f"window.metadata.stable = true;\n" - with open(filename, 'a') as file: - file.write(content) + # Check for index.html to identify version directories + if inner.is_dir() and (inner / "index.html").is_file(): + # Generate metadata.js only for reference manual (it has static/) + if site_dir == "reference" and (inner / "static").is_dir(): + filename = inner / "static" / "metadata.js" + content = f"{header}\nwindow.metadata = {{versionString: \"{inner}\"}};\n" + if str(inner) == latest_version or str(inner) == "latest": + content += f"window.metadata.latest = true;\n" + if str(inner) == latest_stable_version or str(inner) == "stable": + content += f"window.metadata.stable = true;\n" + with open(filename, 'a') as file: + file.write(content) # Add appropriate metadata to every file at every version add_metadata(inner, str(inner), site_dir) @@ -93,6 +95,11 @@ def deploy_overlays(deploy_dir, src_branch, tgt_branch, site_dir): if is_git_ancestor(tgt_branch, src_branch): raise Exception(f"Git merge will have bad behavior if {tgt_branch} is an ancestor of {src_branch}, try creating a vacuous commit on {tgt_branch} first.") run_git_command(["git", "switch", src_branch]) + + if find_latest_version(deploy_dir) is None: + print(f"No version directories found on {src_branch}, nothing to overlay") + return + print(f"Applying overlays...") apply_overlays(deploy_dir, site_dir) print(f"Creating merge commit...") diff --git a/extended-examples/IndexMapGrind.lean b/extended-examples/IndexMapGrind.lean index 5e385293a..19cee95b8 100644 --- a/extended-examples/IndexMapGrind.lean +++ b/extended-examples/IndexMapGrind.lean @@ -1,4 +1,6 @@ +--ANCHOR: imports import Std.Data.HashMap +--ANCHOR_END: imports import IndexMapGrind.CheckMsgs open Std in @@ -19,9 +21,11 @@ example (m : HashMap Nat Nat) : (m.insert 1 2).size ≤ m.size + 1 := by get_elem_tactic -open Std + -- ANCHOR: IndexMap +open Std + structure IndexMap (α : Type u) (β : Type v) [BEq α] [Hashable α] where private indices : HashMap α Nat @@ -32,10 +36,13 @@ structure IndexMap keys[i]? = some a ↔ indices[a]? = some i := by grind -- ANCHOR_END: IndexMap + +-- ANCHOR: variables namespace IndexMap variable {α : Type u} {β : Type v} [BEq α] [Hashable α] variable {m : IndexMap α β} {a : α} {b : β} {i : Nat} +-- ANCHOR_END: variables -- ANCHOR: size @[inline] def size (m : IndexMap α β) : Nat := @@ -58,6 +65,7 @@ instance : EmptyCollection (IndexMap α β) where instance : Inhabited (IndexMap α β) where default := ∅ +-- ANCHOR: Membership @[inline] def contains (m : IndexMap α β) (a : α) : Bool := m.indices.contains a @@ -67,6 +75,7 @@ instance : Membership α (IndexMap α β) where instance {m : IndexMap α β} {a : α} : Decidable (a ∈ m) := inferInstanceAs (Decidable (a ∈ m.indices)) +-- ANCHOR_END: Membership discarding /-- @@ -110,6 +119,7 @@ stop discarding a ∈ m ↔ a ∈ m.indices := Iff.rfl -- ANCHOR_END: mem_indices_of_mem +-- ANCHOR: getFindIdx @[inline] def findIdx? (m : IndexMap α β) (a : α) : Option Nat := m.indices[a]? @@ -123,6 +133,7 @@ stop discarding @[inline] def getIdx (m : IndexMap α β) (i : Nat) (h : i < m.size := by get_elem_tactic) : β := m.values[i] +-- ANCHOR_END: getFindIdx -- ANCHOR: Lawfuls variable [LawfulBEq α] [LawfulHashable α] @@ -153,7 +164,9 @@ end grind_pattern getElem_indices_lt => m.indices[a] -- ANCHOR_END: getElem_indices_lt_pattern +-- ANCHOR: local_grind_size attribute [local grind] size +-- ANCHOR_END: local_grind_size -- ANCHOR: GetElem? instance : GetElem? (IndexMap α β) α β (fun m a => a ∈ m) where diff --git a/generate-html.sh b/generate-html.sh new file mode 100755 index 000000000..87e1f432a --- /dev/null +++ b/generate-html.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate the manual and tutorials with support for preview and production modes + +# Default values +MODE="preview" +VERSION="" +OUTPUT="_out/site" +DRAFT_FLAG="" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --mode) + MODE="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --output) + OUTPUT="$2" + shift 2 + ;; + --draft) + DRAFT_FLAG="--draft" + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--mode preview|production] [--version VERSION] [--output DIR] [--draft]" + exit 1 + ;; + esac +done + +# Validate production mode requirements +if [ "$MODE" = "production" ]; then + if [ -z "$VERSION" ]; then + echo "Error: --version required for production mode" + exit 1 + fi + if [ -n "$DRAFT_FLAG" ]; then + echo "Error: --draft not supported in production mode" + exit 1 + fi +fi + +# Set up remote configs based on mode +if [ "$MODE" = "preview" ]; then + REF_REMOTE_CONFIG="test-data/reference-remotes.json" + TUT_REMOTE_CONFIG="test-data/tutorial-remotes.json" +else + # Production mode: generate temporary configs from templates + REF_REMOTE_CONFIG="_build/production-remotes-reference.json" + TUT_REMOTE_CONFIG="_build/production-remotes-tutorials.json" + + # Replace __VERSION__ in templates and write to temporary files + mkdir -p _build + sed "s/__VERSION__/$VERSION/g" config/production-remotes-reference.json.template > "$REF_REMOTE_CONFIG" + sed "s/__VERSION__/$VERSION/g" config/production-remotes-tutorials.json.template > "$TUT_REMOTE_CONFIG" + + echo "Generated production configs with version $VERSION" +fi + +# Set up output locations based on draft mode +if [ -n "$DRAFT_FLAG" ]; then + MANUAL_OUTPUT_FLAG="--output _out/draft" + REF_SOURCE="_out/draft/html-multi" +else + MANUAL_OUTPUT_FLAG="" + REF_SOURCE="_out/html-multi" +fi + +# Generate the manual and tutorials +echo "Running generate-manual with args --depth 2 --verbose --delay-html-multi multi.json --remote-config $REF_REMOTE_CONFIG --with-word-count words.txt $MANUAL_OUTPUT_FLAG $DRAFT_FLAG" +lake --quiet exe generate-manual --depth 2 --verbose --delay-html-multi multi.json --remote-config "$REF_REMOTE_CONFIG" --with-word-count "words.txt" $MANUAL_OUTPUT_FLAG $DRAFT_FLAG + +echo "Running generate-tutorials with args --verbose --delay tutorials.json --remote-config $TUT_REMOTE_CONFIG" +lake --quiet exe generate-tutorials --verbose --delay tutorials.json --remote-config "$TUT_REMOTE_CONFIG" + +echo "Running generate-manual with args --verbose --resume-html-multi multi.json --remote-config $REF_REMOTE_CONFIG $MANUAL_OUTPUT_FLAG $DRAFT_FLAG" +lake --quiet exe generate-manual --verbose --resume-html-multi multi.json --remote-config "$REF_REMOTE_CONFIG" $MANUAL_OUTPUT_FLAG $DRAFT_FLAG + +echo "Running generate-tutorials with args --verbose --resume tutorials.json --remote-config $TUT_REMOTE_CONFIG" +lake --quiet exe generate-tutorials --verbose --resume tutorials.json --remote-config "$TUT_REMOTE_CONFIG" + +# Set up output directories +echo "Setting up output directories" +mkdir -p "$OUTPUT/reference" +mkdir -p "$OUTPUT/tutorials" + +# Copy files +echo "Copying files to site directory" +if [ "$MODE" = "preview" ]; then + cp test-data/index.html "$OUTPUT/index.html" +fi +cp -r "$REF_SOURCE"/* "$OUTPUT/reference/" +cp -r _tutorial-out/* "$OUTPUT/tutorials/" + +echo "Done! Site generated at $OUTPUT/" diff --git a/lake-manifest.json b/lake-manifest.json index afea7924d..1ca28207f 100644 --- a/lake-manifest.json +++ b/lake-manifest.json @@ -1,16 +1,36 @@ {"version": "1.1.0", "packagesDir": ".lake/packages", "packages": - [{"url": "https://github.com/leanprover/verso.git", + [{"url": "https://github.com/leanprover/verso-web-components", "type": "git", "subDir": null, "scope": "", - "rev": "4abb9845e9019e09769c8f0656c0948c5336d2be", + "rev": "845fadc0cbb156e0f4ded4da2749531a7f913f7c", + "name": "versowebcomponents", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": false, + "configFile": "lakefile.lean"}, + {"url": "https://github.com/leanprover/verso.git", + "type": "git", + "subDir": null, + "scope": "", + "rev": "d46ba7b5429e54864e7061205908d6bdf6e9ab19", "name": "verso", "manifestFile": "lake-manifest.json", "inputRev": "main", "inherited": false, "configFile": "lakefile.lean"}, + {"url": "https://github.com/leanprover/subverso", + "type": "git", + "subDir": null, + "scope": "", + "rev": "7ada0eebb41eefab7c27aeaba0587cc44e0de194", + "name": "subverso", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.lean"}, {"url": "https://github.com/leanprover-community/plausible", "type": "git", "subDir": null, @@ -30,16 +50,6 @@ "manifestFile": "lake-manifest.json", "inputRev": "main", "inherited": true, - "configFile": "lakefile.lean"}, - {"url": "https://github.com/leanprover/subverso", - "type": "git", - "subDir": null, - "scope": "", - "rev": "1e55697c44a646f8a22e2a91878efc4496aa5743", - "name": "subverso", - "manifestFile": "lake-manifest.json", - "inputRev": "main", - "inherited": true, "configFile": "lakefile.lean"}], "name": "«verso-manual»", "lakeDir": ".lake"} diff --git a/lakefile.lean b/lakefile.lean index b1457369d..1bbec958b 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -9,6 +9,8 @@ open Lake DSL open System (FilePath) require verso from git "https://github.com/leanprover/verso.git"@"main" +require versowebcomponents from git "https://github.com/leanprover/verso-web-components"@"main" + package "verso-manual" where -- building the C code cost much more than the optimizations save @@ -99,3 +101,20 @@ target figures : Array FilePath := do lean_exe "generate-manual" where needs := #[`@/figures, `@/subversoExtractMod] root := `Main + +@[default_target] +lean_lib Tutorial where + +@[default_target] +lean_exe "generate-tutorials" where + root := `TutorialMain + +def lakeExe (prog : String) (args : Array String) : IO Unit := do + IO.println s!"Running {prog} with args {args}" + -- Using spawn and wait here causes the process to inherit stdio streams from Lake, so output is immediately visible + let code ← IO.Process.Child.wait <| (← IO.Process.spawn { cmd := "lake", args := #["--quiet", "exe", prog] ++ args }) + if code ≠ 0 then + let code' := code.toUInt8 + let code := if code' ≠ 0 then code' else 1 + IO.eprintln s!"Failed to run {prog} with args {args}" + IO.Process.exit code diff --git a/scripts/check-tutorial-zips.sh b/scripts/check-tutorial-zips.sh new file mode 100755 index 000000000..26e4cdad2 --- /dev/null +++ b/scripts/check-tutorial-zips.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Verify that tutorial project zip files can be unpacked and built with lake + +# First check that we actually found some zip files +zip_count=$(find _out/site/tutorials -name "*.zip" 2>/dev/null | wc -l) +if [ "$zip_count" -eq 0 ]; then + echo "Error: No tutorial zip files found in _out/site/tutorials" + echo "If the output structure changed, update this script." + exit 1 +fi + +successes=0 +failures=0 + +while IFS= read -r zip; do + echo "=== Testing: $zip" + tmpdir=$(mktemp -d) + unzip -q "$zip" -d "$tmpdir" + + # Find the project directory (may be nested inside the zip) + project_dir=$(find "$tmpdir" -name "lakefile.lean" -o -name "lakefile.toml" | head -1 | xargs dirname 2>/dev/null) + + if [ -z "$project_dir" ]; then + echo "Warning: No lakefile found in $zip" + ((failures++)) + else + pushd "$project_dir" > /dev/null + if lake build 2>&1; then + echo "Build succeeded" + ((successes++)) + else + echo "Build failed" + ((failures++)) + fi + popd > /dev/null + fi + + rm -rf "$tmpdir" +done < <(find _out/site/tutorials -name "*.zip" 2>/dev/null) + +echo +echo "Summary:" +echo " Succeeded: $successes" +echo " Failed: $failures" + +if [ $failures -ne 0 ]; then + exit 1 +else + exit 0 +fi diff --git a/test-data/index.html b/test-data/index.html new file mode 100644 index 000000000..e9eb32a31 --- /dev/null +++ b/test-data/index.html @@ -0,0 +1,105 @@ + + + + + + Lean Manual + + + + +
+

Lean Reference Manual and Tutorials

+

Preview Version

+ +
+ + diff --git a/test-data/reference-remotes.json b/test-data/reference-remotes.json new file mode 100644 index 000000000..9d01e354e --- /dev/null +++ b/test-data/reference-remotes.json @@ -0,0 +1,12 @@ +{ + "version": 0, + "sources": { + "tutorials": { + "root": "/tutorials", + "shortName": "tutorials", + "longName": "Lean Tutorials", + "updateFrequency": "always", + "sources": [{ "local": "_tutorial-out/xref.json" }] + } + } +} diff --git a/test-data/tutorial-remotes.json b/test-data/tutorial-remotes.json new file mode 100644 index 000000000..91f701473 --- /dev/null +++ b/test-data/tutorial-remotes.json @@ -0,0 +1,12 @@ +{ + "version": 0, + "sources": { + "reference": { + "root": "/reference", + "updateFrequency": "always", + "shortName": "ref", + "longName": "Lean Language Reference", + "sources": [{ "local": "_out/html-multi/xref.json" }, "default"] + } + } +} diff --git a/verso-sources.json b/verso-sources.json new file mode 100644 index 000000000..ddae7b82d --- /dev/null +++ b/verso-sources.json @@ -0,0 +1,19 @@ +{ + "version": 0, + "sources": { + "reference": { + "root": "/reference", + "shortName": "reference", + "longName": "The Lean Language Reference", + "updateFrequency": "always", + "sources": [{ "local": "_out/html-multi/xref.json" }] + }, + "tutorials": { + "root": "/tutorials", + "shortName": "tutorials", + "longName": "Lean Tutorials", + "updateFrequency": "always", + "sources": [{ "local": "_tutorial-out/xref.json" }] + } + } +}