An Implementation of Semantic Versioning (SemVer) in Lean
About
Semantic Versioning (SemVer) defines a way to use versions, based on
- a grammar in Backus–Naur Form (BNF) for valid versions identifiers,
- a notion of precedence for version identifiers and
- rules for increasing version identifiers in case of changes.
This repository contains an implementation of Semantic Versioning in Lean in file SemVer/Basic.lean.
It defines
- types that coincide with the symbols of the BNF specification,
- functions
ltanddecLtfor most of the types so that comparing two versions based on the precedence for two versions is possible and - parsers for converting strings into terms of the aforementioned types.
Thus, it can be used for
- checking if a string complies to the syntax of versions
- determining if for two versions, a and b, a < b holds tue, i.e. if a has lower precedence than b
as defined by Sematic Versioning.
How to run it
Install Lean, clone this repository and then execute
lake exe lean-semver
How to use it
As Library
Please refer to this page for the documentation of the library.
As Executable
The program prompts you to enter two version identifiers. Based on that, it prints some text to the console.
The expression in the last line, indicates if the version entered first is less than the second one, based on the precedence rules defined by semantic versioning.
Examples
The following two examples show the out put of program lean-semver and user input
(after --> for the first and after -> for the second prompt).
Example 1
please enter the first version identifier --> 1.1.2-alpha.beta.gamma.2+2025-09-07.17-03-42.0000
please enter the second version identifier -> 1.1.2-alpha.beta.gamma.10+2025-09-07.16-23-57.0001
the term representing the first version identifier is:
{ toVersionCore := { major := 1, minor := 1, patch := 2 },
preRelease := some [PreRelIdent.alphanumIdent "alpha",
PreRelIdent.alphanumIdent "beta",
PreRelIdent.alphanumIdent "gamma",
PreRelIdent.numIdent "2"],
build := some [BuildIdent.alphanumIdent "2025-09-07", BuildIdent.alphanumIdent "17-03-42", BuildIdent.digits "0000"] }
the term representing the second version identifier is:
{ toVersionCore := { major := 1, minor := 1, patch := 2 },
preRelease := some [PreRelIdent.alphanumIdent "alpha",
PreRelIdent.alphanumIdent "beta",
PreRelIdent.alphanumIdent "gamma",
PreRelIdent.numIdent "10"],
build := some [BuildIdent.alphanumIdent "2025-09-07", BuildIdent.alphanumIdent "16-23-57", BuildIdent.digits "0001"] }
for the precedence of the first and second version, the following is true:
1.1.2-alpha.beta.gamma.2+2025-09-07.17-03-42.0000 < 1.1.2-alpha.beta.gamma.10+2025-09-07.16-23-57.0001
Example 2
please enter the first version identifier --> 1.1.2-alpha.beta.gamma.2+2025-09-07.16-23-57.0001
please enter the second version identifier -> 1.1.2-alpha.beta.gamma.2+2025-09-07.17-03-42.0000
the term representing the first version identifier is:
{ toVersionCore := { major := 1, minor := 1, patch := 2 },
preRelease := some [PreRelIdent.alphanumIdent "alpha",
PreRelIdent.alphanumIdent "beta",
PreRelIdent.alphanumIdent "gamma",
PreRelIdent.numIdent "2"],
build := some [BuildIdent.alphanumIdent "2025-09-07", BuildIdent.alphanumIdent "16-23-57", BuildIdent.digits "0001"] }
the term representing the second version identifier is:
{ toVersionCore := { major := 1, minor := 1, patch := 2 },
preRelease := some [PreRelIdent.alphanumIdent "alpha",
PreRelIdent.alphanumIdent "beta",
PreRelIdent.alphanumIdent "gamma",
PreRelIdent.numIdent "2"],
build := some [BuildIdent.alphanumIdent "2025-09-07", BuildIdent.alphanumIdent "17-03-42", BuildIdent.digits "0000"] }
for the precedence of the first and second version, the following is true:
¬ 1.1.2-alpha.beta.gamma.2+2025-09-07.16-23-57.0001 < 1.1.2-alpha.beta.gamma.2+2025-09-07.17-03-42.0000
How it is implemented
Parsing
Based on grammar, types are defined.
Each of these types has a function parse that converts a String as input into a term of the
respective type or, if this is not possible, returns an error.
Correct Version Identifiers
#eval Version.parse "1.0.1-alpha.0.1023.xyz"
results in
ParserResult.success
{ toVersionCore := { major := 1, minor := 0, patch := 1 },
preRelease := some [PreRelIdent.alphanumIdent "alpha",
PreRelIdent.numIdent "0",
PreRelIdent.numIdent "1023",
PreRelIdent.alphanumIdent "xyz"],
build := none }
First Example of an Incorrect Version Identifier
#eval Version.parse "1.0.1.1-alpha.0.1023.xyz"
returns
ParserResult.failure
{ message := "exactly three numbers - separated by '.' - must be provided, not one more, not one less",
position := 0 }
Second Example of an Incorrect Version Identifier
#eval Version.parse "1.0.1-alpha.00.1023.xyz"
leads to
ParserResult.failure
{ message := "neither alphanumeric nor numeric identifier found because \n1. alphanumeric identifier must contain a non-digit character in position 14\n2. numeric identifiers must not have leading zeros in position 12",
position := 14 }
Rendering
Each of the types implements toString, which renders a term of this type to a string that follows the grammar.
The following code
#eval (Version.parse "2.0.0-alpha2.1234+nightly-2025-09-06.1234").to!.toString
results in
"2.0.0-alpha2.1234+nightly-2025-09-06.1234"
as output.
fun v: Version ↦ toString v is the inverse operation of fun s: String ↦ (Version.parse s).to!, if the latter is restricted to strings that represent valid versions.
Precedence
Semantic versioning defines precedence rules for version identifiers.
These are implemented by functions lt (less than) and decLt (decidable less than) provided by all types,
where it is required.
For terms t₁ and t₂ of the same type, it can this be determined if t₁ < t₂ or ¬ t₁ < t₂ holds true.
In particular, as terms of type Version, two version identifiers can be compared with each other.
The following propositions are all true:
1.9.0 < 1.10.0, since the minor version 9 is less than 10 (compared as numbers not as strings)1.0.0-alpha.beta < 1.0.0-beta.11, because the version cores (first three numbers) are identical but the first pre-release is less than the second (as strings in lexicographical order)¬ 1.0.0+20130313144700 < 1.0.0+21AF26D3----117B344092BD, because the core versions as well as the pre-releases are (both empty) are identical and the build information has no effect on precedence