{"article":{"slug":"creusot-0-14-0","title":"Creusot 0.14.0","subtitle":null,"summary":"The Creusot team announces version 0.14.0 of their formal verification tool for Rust, highlighting selected new features for contracts and proofs and pointing to the full changelog for the release.","content_type":"changelog","language":"en","canonical_url":"https://devlog.creusot.rs/v0-14-0/","author":{"name":null,"url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Creusot","url":"https://creusot.rs/","listing_slug":null,"listing":null},"topics":[{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Developer Tools","slug":"developer-tools","url":"https://listedarticles.com/topics/developer-tools"},{"name":"Software Engineering","slug":"software-engineering","url":"https://listedarticles.com/topics/software-engineering"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":983,"reading_minutes":4,"published_at":"2026-10-13T00:00:00.000Z","added_at":"2026-10-13T08:13:56.727Z","updated_at":"2026-10-13T08:13:56.727Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/creusot-0-14-0","markdown_url":"https://listedarticles.com/articles/creusot-0-14-0.md","example":false,"citation":"Creusot. \"Creusot 0.14.0.\" 13 Oct 2026. https://devlog.creusot.rs/v0-14-0/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://devlog.creusot.rs/v0-14-0/"},"body_markdown":"# Creusot 0.14.0\n\n2026-10-13\n·\n5min\n\nWe are pleased to announce the release of version 0.14.0 of [Creusot](https://creusot.rs),\nour formal verification tool for Rust.\n\nIn this post, we highlight a small selection of new features.\nSee the full [changelog](https://github.com/creusot-rs/creusot/blob/master/CHANGELOG.md#0140---2026-10-09)\nfor the rest.\n\n# What's new in Creusot?\n\n## Partial orders\n\nPreviously, Creusot restricted its specification of `PartialOrd` to total\norders. Since version 0.13.0, we have relaxed it to allow partial orders.\n(We skipped a devlog entry for 0.13.0 so let's talk about this here.)\n\nOur previous definition of total order (`OrdLogic`) relied on the\ncomparison function `cmp_log` (the logic counterpart to `cmp`), which returns an `Ordering`.\n\n```\n// Old definition of total order\npub trait OrdLogic {\n    #[logic]\n    fn cmp_log(self, other: Self) -> Ordering;\n\n    // ... (laws and other comparison operators)\n}\n```\n\nThat had the benefit of making totality an implicit consequence of the type of the function:\nany two elements must compare either `Less` , `Equal`, or `Greater`.\n\nWith the switch to partial orders, a traditional formulation based on a\nboolean comparison function (\"`<`\", aka. `lt_log`) becomes more natural.\nA strict partial order (`PartialOrdLogic`) consists of a comparison function `lt_log` which\nis irreflexive and transitive. Totality becomes another law: for all `x`, `y`,\nthe trijunction `x < y || x == y || y < x` must hold.\n\n```\n// (New) definition of partial order\npub trait PartialOrdLogic {\n    /// `self < other` in logic\n    #[logic]\n    fn lt_log(self, other: Self) -> bool;\n\n    // ... (laws (irreflexivity, transitivity) and other comparison operators)\n}\n\n// New definition of total order\npub trait OrdLogic {\n    /// The order is total\n    #[logic(law)]\n    #[ensures(self < other || self == other || other < self)]\n    fn lt_log_total(self, other: Self);\n}\n```\n\nInterestingly, none of our existing proofs were affected by this change.\nOur automated provers just took it in stride.\n\n## Logic VC gen\n\nCreusot now generates verification conditions (VCs) for snapshots,\nwhich are logic expressions embedded in Rust programs.\nIn particular, this enhances the use of \"lemma functions\",\na common idiom in verification tools similar to Creusot.\n\nFor example, you can encode a lemma as a logic function such as this,\nwhich logically amounts to the proposition `forall<x> hyp(x) -> concl(x)`:\n\n```\n#[logic]\n#[requires(hyp(x))]\n#[ensures(concl(x))]\nfn lemma(x: usize) {}\n```\n\nThen, inside a program function you can call `lemma` inside a `snapshot!`:\n\n```\nfn f(n: usize) {\n   snapshot!(lemma(n));\n}\n```\n\nCreusot now translates this call to an assertion that `hyp(n)` (the precondition of `lemma`) is true\nand an assumption that `concl(n)` (its postcondition) is true. Notably, these statements\nare specialized to `n`, the argument of `lemma`, so we avoid relying on provers to\ninstantiate the universal quantifier in `forall<x> hyp(x) -> concl(x)`.\n\nIt should be said that this new feature specifically enables *logic* functions to be\nused as lemma functions.\nBefore, you could already do something similar with program functions:\n\n```\n#[requires(hyp(x))]\n#[ensures(concl(x))]\nfn lemma(x: usize) {}\n\nfn f(n: usize) {\n    lemma(n);\n}\n```\n\nThe difference is that (1) this version introduces a Rust function call, however\ntrivial it may be, and (2) it must pass the borrow checker, which may require\nboilerplate to work around when all you wanted is to state some facts.\nIn contrast, snapshots are entirely erased, so it is clear that\nthey are there only for the sake of formal proof.\nThus, these newly generated VCs for snapshots let us draw clearer boundaries\nbetween the worlds of programs and proofs.\n\nIt may seem surprising that Creusot did not already do this. This comes down to\nthe fact that programs and specifications are written in two separate languages,\nwith their own semantics, and thus with different mechanisms and motivations for generating VCs.\nWithout going into too much details, VCs were originally not generated for `snapshot!`\nbecause it isn't necessary for soundness, but it turns out to help catch mistakes\nin specifications quicker (such as out-of-bounds indexing), on top of enabling\nnew techniques such as that described above.\n\n## rust-analyzer integration\n\nIt is finally possible to use Go-to-definition in Pearlite specifications.\n\n![Screenshot of go-to-definition working in the middle of a Creusot specification](https://devlog.creusot.rs/creusot-go-to-definition.png)\n\nTo start, we needed to make rust-analyzer expand the code of specifications.\nIn normal compilation, specification macros (`#[requires]`, `#[ensures]`,\n`proof_assert!`, `snapshot!`, etc.) erase themselves, discarding their contents.\nWe just needed to pass options to rust-analyzer for it to expand those macros the same way Creusot does\n(see [our guide for the relevant rust-analyzer settings](https://guide.creusot.rs/quickstart/editor_config.html)).\n\nOne worry we had was that Pearlite macros expand to not quite valid Rust code,\nnot meant to be compiled by rustc:\nit passes the type checker, but not the borrow checker.\nLuckily for us, rust-analyzer does not do borrow checking,\nso it doesn't see any problem with the expanded code.\n\nWith that done, Go-to-definition did start to work, but it returned numerous irrelevant results.\nThe problem was that the tokens produced by Pearlite macros carried spans that were\ntoo coarse-grained.\n\nTo explain it very roughly, when you activate Go-to-definition,\nrust-analyzer looks for identifiers in the final AST whose span ranges over your cursor.\nFor example, `a + b` in Pearlite expands to a method call\n`::creusot_std::logic::ops::AddLogic::add_logic(a, b)`,\nwhere every generated token carries the span of the whole expression `a + b`.\nHence, Go-to-definition from anywhere in that expression ends up looking up the definitions\nof all of `creusot_std`, `logic`, `ops`, `AddLogic`, and `add_logic`.\n\nIn this case, we'd like to only look up `add_logic`, and only when the cursor is right next to `+`.\nThe fix is to make the token `add_logic` have the span of `+`, and\nto make the other identifiers have a zero-width span,\nwhich seems to be enough for rust-analyzer to ignore them.\n\nMany thanks to Sarah Quiñones for figuring all this out.\nThis fix makes navigating specifications in Creusot much more pleasant.\n\n---\n\nThanks for tuning in. If you have any questions about Creusot, you are welcome to join\n[our Zulip channel](https://why3.zulipchat.com/#narrow/channel/341707-creusot),\nor to open a [discussion](https://github.com/creusot-rs/creusot/discussions)\nor an [issue](https://github.com/creusot-rs/creusot/issues) on the Creusot repository.\n\n---\n","body_html":"<h1 id=\"creusot-0-14-0\">Creusot 0.14.0</h1>\n<p>2026-10-13\n·\n5min</p>\n<p>We are pleased to announce the release of version 0.14.0 of <a href=\"https://creusot.rs\" rel=\"nofollow ugc noopener\">Creusot</a>,\nour formal verification tool for Rust.</p>\n<p>In this post, we highlight a small selection of new features.\nSee the full <a href=\"https://github.com/creusot-rs/creusot/blob/master/CHANGELOG.md#0140---2026-10-09\" rel=\"nofollow ugc noopener\">changelog</a>\nfor the rest.</p>\n<h1 id=\"what-s-new-in-creusot\">What&#39;s new in Creusot?</h1>\n<h2 id=\"partial-orders\">Partial orders</h2>\n<p>Previously, Creusot restricted its specification of <code>PartialOrd</code> to total\norders. Since version 0.13.0, we have relaxed it to allow partial orders.\n(We skipped a devlog entry for 0.13.0 so let&#39;s talk about this here.)</p>\n<p>Our previous definition of total order (<code>OrdLogic</code>) relied on the\ncomparison function <code>cmp_log</code> (the logic counterpart to <code>cmp</code>), which returns an <code>Ordering</code>.</p>\n<pre><code>// Old definition of total order\npub trait OrdLogic {\n    #[logic]\n    fn cmp_log(self, other: Self) -&gt; Ordering;\n\n    // ... (laws and other comparison operators)\n}</code></pre>\n<p>That had the benefit of making totality an implicit consequence of the type of the function:\nany two elements must compare either <code>Less</code> , <code>Equal</code>, or <code>Greater</code>.</p>\n<p>With the switch to partial orders, a traditional formulation based on a\nboolean comparison function (&quot;<code>&lt;</code>&quot;, aka. <code>lt_log</code>) becomes more natural.\nA strict partial order (<code>PartialOrdLogic</code>) consists of a comparison function <code>lt_log</code> which\nis irreflexive and transitive. Totality becomes another law: for all <code>x</code>, <code>y</code>,\nthe trijunction <code>x &lt; y || x == y || y &lt; x</code> must hold.</p>\n<pre><code>// (New) definition of partial order\npub trait PartialOrdLogic {\n    /// `self &lt; other` in logic\n    #[logic]\n    fn lt_log(self, other: Self) -&gt; bool;\n\n    // ... (laws (irreflexivity, transitivity) and other comparison operators)\n}\n\n// New definition of total order\npub trait OrdLogic {\n    /// The order is total\n    #[logic(law)]\n    #[ensures(self &lt; other || self == other || other &lt; self)]\n    fn lt_log_total(self, other: Self);\n}</code></pre>\n<p>Interestingly, none of our existing proofs were affected by this change.\nOur automated provers just took it in stride.</p>\n<h2 id=\"logic-vc-gen\">Logic VC gen</h2>\n<p>Creusot now generates verification conditions (VCs) for snapshots,\nwhich are logic expressions embedded in Rust programs.\nIn particular, this enhances the use of &quot;lemma functions&quot;,\na common idiom in verification tools similar to Creusot.</p>\n<p>For example, you can encode a lemma as a logic function such as this,\nwhich logically amounts to the proposition <code>forall&lt;x&gt; hyp(x) -&gt; concl(x)</code>:</p>\n<pre><code>#[logic]\n#[requires(hyp(x))]\n#[ensures(concl(x))]\nfn lemma(x: usize) {}</code></pre>\n<p>Then, inside a program function you can call <code>lemma</code> inside a <code>snapshot!</code>:</p>\n<pre><code>fn f(n: usize) {\n   snapshot!(lemma(n));\n}</code></pre>\n<p>Creusot now translates this call to an assertion that <code>hyp(n)</code> (the precondition of <code>lemma</code>) is true\nand an assumption that <code>concl(n)</code> (its postcondition) is true. Notably, these statements\nare specialized to <code>n</code>, the argument of <code>lemma</code>, so we avoid relying on provers to\ninstantiate the universal quantifier in <code>forall&lt;x&gt; hyp(x) -&gt; concl(x)</code>.</p>\n<p>It should be said that this new feature specifically enables <em>logic</em> functions to be\nused as lemma functions.\nBefore, you could already do something similar with program functions:</p>\n<pre><code>#[requires(hyp(x))]\n#[ensures(concl(x))]\nfn lemma(x: usize) {}\n\nfn f(n: usize) {\n    lemma(n);\n}</code></pre>\n<p>The difference is that (1) this version introduces a Rust function call, however\ntrivial it may be, and (2) it must pass the borrow checker, which may require\nboilerplate to work around when all you wanted is to state some facts.\nIn contrast, snapshots are entirely erased, so it is clear that\nthey are there only for the sake of formal proof.\nThus, these newly generated VCs for snapshots let us draw clearer boundaries\nbetween the worlds of programs and proofs.</p>\n<p>It may seem surprising that Creusot did not already do this. This comes down to\nthe fact that programs and specifications are written in two separate languages,\nwith their own semantics, and thus with different mechanisms and motivations for generating VCs.\nWithout going into too much details, VCs were originally not generated for <code>snapshot!</code>\nbecause it isn&#39;t necessary for soundness, but it turns out to help catch mistakes\nin specifications quicker (such as out-of-bounds indexing), on top of enabling\nnew techniques such as that described above.</p>\n<h2 id=\"rust-analyzer-integration\">rust-analyzer integration</h2>\n<p>It is finally possible to use Go-to-definition in Pearlite specifications.</p>\n<figure><img src=\"https://devlog.creusot.rs/creusot-go-to-definition.png\" alt=\"Screenshot of go-to-definition working in the middle of a Creusot specification\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>To start, we needed to make rust-analyzer expand the code of specifications.\nIn normal compilation, specification macros (<code>#[requires]</code>, <code>#[ensures]</code>,\n<code>proof_assert!</code>, <code>snapshot!</code>, etc.) erase themselves, discarding their contents.\nWe just needed to pass options to rust-analyzer for it to expand those macros the same way Creusot does\n(see <a href=\"https://guide.creusot.rs/quickstart/editor_config.html\" rel=\"nofollow ugc noopener\">our guide for the relevant rust-analyzer settings</a>).</p>\n<p>One worry we had was that Pearlite macros expand to not quite valid Rust code,\nnot meant to be compiled by rustc:\nit passes the type checker, but not the borrow checker.\nLuckily for us, rust-analyzer does not do borrow checking,\nso it doesn&#39;t see any problem with the expanded code.</p>\n<p>With that done, Go-to-definition did start to work, but it returned numerous irrelevant results.\nThe problem was that the tokens produced by Pearlite macros carried spans that were\ntoo coarse-grained.</p>\n<p>To explain it very roughly, when you activate Go-to-definition,\nrust-analyzer looks for identifiers in the final AST whose span ranges over your cursor.\nFor example, <code>a + b</code> in Pearlite expands to a method call\n<code>::creusot_std::logic::ops::AddLogic::add_logic(a, b)</code>,\nwhere every generated token carries the span of the whole expression <code>a + b</code>.\nHence, Go-to-definition from anywhere in that expression ends up looking up the definitions\nof all of <code>creusot_std</code>, <code>logic</code>, <code>ops</code>, <code>AddLogic</code>, and <code>add_logic</code>.</p>\n<p>In this case, we&#39;d like to only look up <code>add_logic</code>, and only when the cursor is right next to <code>+</code>.\nThe fix is to make the token <code>add_logic</code> have the span of <code>+</code>, and\nto make the other identifiers have a zero-width span,\nwhich seems to be enough for rust-analyzer to ignore them.</p>\n<p>Many thanks to Sarah Quiñones for figuring all this out.\nThis fix makes navigating specifications in Creusot much more pleasant.</p>\n<hr />\n<p>Thanks for tuning in. If you have any questions about Creusot, you are welcome to join\n<a href=\"https://why3.zulipchat.com/#narrow/channel/341707-creusot\" rel=\"nofollow ugc noopener\">our Zulip channel</a>,\nor to open a <a href=\"https://github.com/creusot-rs/creusot/discussions\" rel=\"nofollow ugc noopener\">discussion</a>\nor an <a href=\"https://github.com/creusot-rs/creusot/issues\" rel=\"nofollow ugc noopener\">issue</a> on the Creusot repository.</p>\n<hr />","headings":[{"level":1,"text":"Creusot 0.14.0","id":"creusot-0-14-0"},{"level":1,"text":"What's new in Creusot?","id":"what-s-new-in-creusot"},{"level":2,"text":"Partial orders","id":"partial-orders"},{"level":2,"text":"Logic VC gen","id":"logic-vc-gen"},{"level":2,"text":"rust-analyzer integration","id":"rust-analyzer-integration"}]}}