个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Prove a function in Lean 4, call it from C#. lean2il re-checks every proof with Tenet, an independent Lean kernel, compiles the kernel terms to .NET IL, and writes the docs from the Lean. With a VS Code extension.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Prove a function in Lean 4, call it from C#. lean2il reads the compiled Lean with
Tenet, an independent Lean kernel on .NET, re-checks every proof,
and then compiles the definitions you mark with @[export] straight to an IL assembly. It writes the
documentation too, from the Lean: each method's summary is its docstring, its remarks are the theorems
proved about it, and its examples are proved equations turned into C# calls, every one of them run against
the IL before it is written down.
It runs wherever .NET 10 and Lean do: macOS, Linux and Windows, all three built and tested on every push.
Start here: the tutorial takes you from nothing to a proved Lean function called from C# in about fifteen minutes. The guide covers every option, what compiles, how Lean types map to .NET, how the docs are chosen, and what each error means.
The demo is money. .NET's Math.Round has three well-known traps in finance code, and this repository
replaces all three with one function proved in Lean:
| What the code does | Input | Gives | Proved | Why |
|---|---|---|---|---|
Math.Round(x, 2) | 0.125m | 0.12 | 0.13 | the default mode is banker's rounding |
Math.Round((double)x, 2, AwayFromZero) | 1.005 | 1 | 1.01 | 1.005 is 1.00499999... as a double |
Math.Floor(x * 100 + 0.5m) / 100 | -2.675m | -2.67 | -2.68 | the hand-rolled fix is not symmetric: a refund comes out a cent short |
The double row is not a corner case. Of the 10,000 prices from 0.005 to 99.995 that end in a half cent,
572 round the wrong way as double on .NET 10 (HowOftenDoubleGetsAHalfCentWrong counts them).
using Finance;
Proven.Round(0.125m, 2, RoundingMode.AwayFromZero); // 0.13, proved by invoice_example
Proven.Round(-2.675m, 2, RoundingMode.AwayFromZero); // -2.68, proved by refund_example
Proven.RoundCents(19.995m); // 20.00, proved by roundCents_example
Proven.SplitEven(10000, 3); // [3334, 3333, 3333], proved by splitEven_example
And one more trap every billing system meets: split a $100.00 bill three ways as total / 3 each and a cent
disappears. Proven.SplitEven gives the first total % n people the extra cent, and it is proved that the
shares always add up to exactly the total, refunds included, and never differ by more than a cent.
lean/Finance/Rounding.lean defines rounding on exact decimals, a mantissa and
a scale like System.Decimal, for all five of .NET's MidpointRounding modes, and
lean/Finance/Split.lean splits a bill, by recursion over a list. They prove:
| Theorem | Says |
|---|---|
round_error_le_half | The result is within half a unit of its last digit (the two midpoint modes). |
round_nearest | No other value with that many digits is closer. |
round_tie_awayFromZero | An exact midpoint goes to the neighbor further from zero. |
round_tie_toEven | An exact midpoint goes to the neighbor whose last digit is even. |
round_neg | round (-x) = -(round x): a refund rounds to minus its sale. |
round_round | Rounding twice to the same precision is rounding once. |
round_of_scale_le | A value already at the precision is returned unchanged. |
roundDiv_toEven_eq_awayFromZero | Banker's rounding and away-from-zero differ only on exact ties. |
double_rounding_example | Rounding 2.4449 to three places and then two gives 2.45; directly, 2.44. |
splitEven_sum | The shares of a split add up to exactly the total, for any number of people, refunds included. |
splitEven_fair | Every share is the total divided by n, rounded down, or one cent more. |
splitEven_length | There are exactly n shares. |
plus floor, ceiling and truncation specs for the directed modes, and the worked examples above. Every
theorem rests on at most Lean's three standard axioms (propext, Quot.sound, Classical.choice); none
on sorry. Browse them in LeanViz:
And a test makes the comparison the theorems invite: 300,000 random decimals, over every scale and all five
modes, with midpoints heavily over-represented, through Proven.Round and through .NET's
Math.Round(decimal, int, MidpointRounding). They agree on every one. .NET's decimal rounding is
correct; the bugs above come from its default and from double.
Lean source ──lake build──▶ .olean ──Tenet──▶ re-checked kernel terms ──lean2il──▶ Finance.Proven.dll
(definitions, (Lean's (an independent (IL) Finance.Proven.xml
theorems, compiled kernel; nothing is Finance.Proven.md
docstrings) output) emitted if it rejects) Finance.Proven.proof.json
lean2il maps the project's .olean files with Tenet and re-checks every declaration.
By default that covers Lean's own library under the project too (64,855 declarations here, about a
minute and a half); --trust-imports checks only the project. If Tenet rejects anything, nothing is emitted.brecOn or WellFounded.fix, which says nothing a machine can run well. But Lean also proves, for every
recursive f, the equation f.eq_def : ∀ xs, f xs = rhs, where rhs is the body as written. lean2il has
Lean generate those equations (in a small module it writes, .lake/lean2il/Lean2IlEqns.lean), Tenet re-checks
them with everything else, and each recursive method is compiled from its equation's right-hand side. The IL
computes something both kernels agree equals f. This covers Lean's own library too: List.map,
List.foldr, List.reverse and friends compile the same way.Nat and Int
operations become BigInteger calls with Lean's meaning (Euclidean division, truncated Nat
subtraction, x / 0 = 0); a match or if is a recursor, reduced at compile time when its target is a
known constructor and turned into a branch otherwise; types and proofs are erased, and Decidable keeps
its answer and drops its proof. A call to itself in tail position becomes a jump, so a loop runs in constant
stack; any other recursion that outgrows the stack is rerun, transparently, on a thread with a 1 GB stack.
That is safe to do because a compiled Lean function has no side effects.Int and a Nat) also gets a decimal overload, so the
Lean round (x : Dec) (digits : Nat) (mode : RoundingMode) : Dec is callable as
decimal Round(decimal x, int digits, RoundingMode mode). The assembly is written with
PersistedAssemblyBuilder against the reference assemblies, so any .NET 10 project can reference it.The question this answers: can Lean's own documentation be the .NET documentation? Almost all of it can,
and all of it is read from the compiled .olean, the same file the proofs are in.
| From Lean | Becomes |
|---|---|
A definition's docstring (/-- ... -/) | the method's <summary> in IntelliSense and its section in the Markdown |
| Docstrings of a structure's fields and an enum's constructors | the docs of the .NET fields and enum members |
| Documented theorems that mention the definition | its <remarks>: name, statement, what it rests on, a LeanViz link |
Proved equations with literal arguments (round ⟨125, 3⟩ 2 .awayFromZero = ⟨13, 2⟩) | <example>: Proven.Round(0.125m, 2, RoundingMode.AwayFromZero); // 0.13, replayed on the IL |
The module docstring (/-! ... -/) | the introduction of the Markdown |
| Declaration ranges | links to the exact source lines |
The rule for what appears is one line: a theorem with a docstring is documentation; a helper lemma without one is not. Statements are printed with LeanViz's printer, and the same project renders as a browsable LeanViz site, so every theorem in the .NET docs links to its own page. Lean's other documentation tools fit around this rather than inside it: doc-gen4 renders the whole library as Lean users read it, and Verso is how to write long-form, checked prose about it.
vscode/ builds lean-to-dot-net-0.4.0.vsix. It brings lean2il and Tenet with it, so the VSIX alone is
enough: install it, open a Lean project, and build. It needs the .NET 10 runtime and Lean, and offers to install
either if it is missing.
Proven. completes with what is proved about each method.@[export] definition shows the C# signature and what stands behind it; over a
proved example, the call and the value it returned on the IL.lake build and lean2il in a terminal; Lean errors, lean2il's refusals and
Tenet rejections go to the Problems panel on the right line. Optionally on every save.npm test, npm run test:integration).The quickest way: install Lean to .NET from the VS Code Marketplace (search "Lean to .NET" in the Extensions view), or from a terminal:
code --install-extension keithadler.lean-to-dot-net
Using Cursor, Windsurf or VSCodium? It's on Open VSX too: search "Lean to .NET" in their Extensions view.
Then open a Lean project, mark a definition @[export], and run Lean to .NET: Build .NET Assembly.
The VSIX is also attached to each release.
From source, for the command line, the tests and the demo:
git clone https://github.com/keithadler/lean-to-dot-net
cd lean-to-dot-net
./setup.sh
setup.sh installs what is missing and builds everything: elan (Lean's
version manager, which then fetches the exact Lean in lean/lean-toolchain) and the .NET 10 SDK into
~/.dotnet, both from their official installers. Tenet needs no installing: the compiler references the
Tenet.Olean package from nuget.org, and the tenet command-line checker is pinned in
dotnet-tools.json, so dotnet tool restore fetches it. On Windows, setup.ps1 does
the same. Once set up, ./build.sh rebuilds and retests everything, and ./build.sh --full has Tenet
re-check Lean's own library too.
setup.sh also installs lean2il as a .NET global tool (in ~/.dotnet/tools) and the VS Code extension.
To use it in your own Lean project, mark what to compile and run lean2il on the project (the
tutorial walks through this end to end):
/-- What the .NET docs will say about it. -/
@[export my_function]
def myFunction (x : Int) (n : Nat) : Int := ...
lake build
lean2il .
and reference .lake/dotnet/<Namespace>.Proven.dll and .lake/dotnet/LeanToDotNet.Runtime.dll from C#.
In a .NET project, on every build: add the LeanToDotNet.Build
package and list your Lean project:
<ItemGroup>
<PackageReference Include="LeanToDotNet.Build" Version="0.3.1" />
<LeanProject Include="../lean" />
</ItemGroup>
dotnet build then runs lake build and lean2il (bundled in the package, Tenet included) before the C# compiler,
references the proven assembly with its IntelliSense docs, and copies it to the output. A broken proof fails the
.NET build with Lean's message at its line, in the form Visual Studio and Rider make clickable. Nothing reruns until a .lean file changes. You need Lean and the .NET 10 SDK;
tests/package-smoke.sh does exactly this from scratch, in CI on Linux, macOS and
Windows.
| Lean | .NET |
|---|---|
Nat, Int | BigInteger (a negative Nat argument is refused) |
Bool, Decidable p | bool |
String | string; lengths count characters, as Lean's do |
List α | LeanList<T>, immutable; C# can pass an array where one is expected |
Option α | LeanOption<T> |
Array α | LeanArray<T>, backed by a .NET array: constant-time a[i] and size, linear push loops; C# passes a T[] |
UInt8 ... UInt64, Int8 ... Int64 | byte ... ulong, sbyte ... long: wrapping arithmetic, and Lean's meaning where C#'s differs (x / 0 = 0, shift counts wrap) |
| a structure, an enum-like inductive | a sealed class, a .NET enum |
| an inductive with data, or recursive (trees, syntax, results) | an abstract class with a Tag and a sealed nested class per constructor |
a type with parameters, Pair Int String | a class per use, PairOfIntString |
a structure of an Int and a Nat | also decimal, through an overload |
Definitions compile with if, match, let, instances, numerals and coercions, and recursion: structural
or well-founded, your own or Lean's library's (List.map, foldr, filter, Array.foldl, ++, sum).
Lambdas compile, including ones that use local variables: xs.map (fun x => x + k) becomes a copy of
List.map that takes k as a parameter. No delegates, no allocation per call.
Not yet, and refused with a message that says why rather than compiled wrong: Float, Char, a function stored
or returned as a value, and inductive types with indices, mutual inductives, or a type nested in itself through
a List.
examples/showcase shows every supported feature, and the build compiles and
differential-tests it every time.
lean2il's translation from kernel terms to IL. It is not proved. It is tested, three ways, on every build:
the proved examples are replayed; every export is run on random inputs by Lean's own compiler and by the IL,
which must agree; and the test suite checks the runtime against Lean's #eval output and Proven.Round
against Math.Round(decimal) on 300,000 inputs.LeanToDotNet.Runtime: BigInteger arithmetic with Lean's meaning, strings with Lean's lengths, immutable lists,
and the exact decimal conversion, which refuses a value with no decimal form rather than rounding it.| Path | |
|---|---|
lean/ | the Lake project: Finance/Rounding.lean and Finance/Split.lean, definitions and proofs |
examples/showcase/ | recursion, lists, options, strings, lambdas, your own types, fixed-width integers and arrays, compiled and differential-tested on every build |
src/Lean2Il/ | the compiler: Compiler.cs (kernel term to IL), Equations.cs (equation lemmas), Differential.cs (Lean vs IL), Docs.cs (docs and replay), Primitives.cs |
src/LeanToDotNet.Runtime/ | what the emitted assembly calls: LeanNat, LeanInt, LeanList, LeanOption, LeanString, LeanStack, DecimalBridge |
tests/ | runtime against Lean, Proven.Round against Math.Round(decimal), the three bugs |
samples/Invoice/ | a console app: the three bugs next to the proved fix |
vscode/ | the VS Code extension |
MIT. src/Lean2Il/Vendor/Pretty.cs is from LeanViz, also MIT.
f args = result with literal arguments becomes a C#
example. Before it is written down it is run against the new assembly through both overloads and must
return the proved value. ProvenInfo.Verdict and ProvenInfo.Theorems() carry the result into the
assembly itself.lean --run, which shares no code with lean2il), and by the IL. The build fails on the first
disagreement and names the input. It catches real bugs: counting a string's length in UTF-16 units instead
of characters fails it at once, on "a7zßz😀".docs/| tutorial, guide, the generated API docs, the LeanViz site |