SemVer
SemVer
Section titled “SemVer”Canonical source:
protos/lib/semver/SemVer.protosat locked revisione99b712ecf658a527b1fe82c8beba2c1286d8dc9.
// THE LICENSED WORK IS PROVIDED UNDER THE TERMS OF THE ADAPTIVE PUBLIC LICENSE// ("LICENSE") AS FIRST COMPLETED BY: Guillermo Adrián Molina. ANY USE, PUBLIC// DISPLAY, PUBLIC PERFORMANCE, REPRODUCTION OR DISTRIBUTION OF, OR PREPARATION OF// DERIVATIVE WORKS BASED ON, THE LICENSED WORK CONSTITUTES RECIPIENT'S ACCEPTANCE// OF THIS LICENSE AND ITS TERMS, WHETHER OR NOT SUCH RECIPIENT READS THE TERMS OF// THE LICENSE. "LICENSED WORK" AND "RECIPIENT" ARE DEFINED IN THE LICENSE. A COPY// OF THE LICENSE IS LOCATED IN THE TEXT FILE ENTITLED "LICENSE.TXT" ACCOMPANYING// THE CONTENTS OF THIS FILE. IF A COPY OF THE LICENSE DOES NOT ACCOMPANY THIS// FILE, A COPY OF THE LICENSE MAY ALSO BE OBTAINED AT THE FOLLOWING WEB SITE:// https://github.com/guillermomolina/protos//// Software distributed under the License is distributed on an "AS IS" basis,// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for// the specific language governing rights and limitations under the License.
//! Strict Semantic Versioning 2.0.0 version values. A version is an ordinary//! object with Integer `major`, `minor`, and `patch` slots, a `prerelease` Array//! of Integer (numeric) and String (alphanumeric) identifiers, and a `build`//! Array of String identifiers. Build metadata is preserved exactly.//!//! Versions answer complete semantic equality through `==`: two versions are//! equal when their core components, prerelease identifiers, and build//! identifiers are equal in order, with numeric and alphanumeric identifiers//! never equal to each other. `hash` is coherent with that equality, so//! independently parsed equal versions work as the same Map key. `===` remains//! ordinary object identity. SemVer precedence (`comparePrecedence`) is a//! distinct relation that ignores build metadata.
/// Parses `text` as exactly `MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]` under/// Semantic Versioning 2.0.0 and answers a fresh version object. Core/// components and numeric prerelease identifiers are unbounded Integers;/// alphanumeric prerelease identifiers and every build identifier keep their/// exact ASCII spelling as Strings, so `+001` stays `"001"`. A non-String/// argument, a missing component, a leading zero in a core component or numeric/// prerelease identifier, an empty identifier, or any character outside the/// SemVer grammar signals an Error. No prefix, whitespace, or partial version/// is accepted.parse: (text) => { String.recognizes(text).ifFalse(() => { Error().signal() }) source: Encoding.UTF8.encode(text)
isDigit: (octet) => { (octet >= 48) && (octet <= 57) }
isIdentifierOctet: (octet) => { upper: (octet >= 65) && (octet <= 90) lower: (octet >= 97) && (octet <= 122) isDigit(octet) || upper || lower || (octet == 45) }
// Answers a fresh Array of `count` nulls in O(count): Arrays do not grow, // and doubling halves keeps the copying geometric. sized: null sized = (count) => { result: Array() (count > 0).ifTrue(() => { half: sized(count.div(2)) result = Array(...half, ...half) ((count % 2) == 1).ifTrue(() => { result = Array(...result, null) }) }) result }
// Identifiers are collected under consecutive Integer keys and // materialized once, keeping parsing linear in the input length. materialize: (entries) => { result: sized(entries.size()) index: 0 result.each((ignored) => { result[index] = entries[index] index = index + 1 }) result }
current: Encoding.UTF8.encode("") currentNumeric: true
takeCurrent: () => { (current.size() == 0).ifTrue(() => { Error().signal() }) taken: current current = Encoding.UTF8.encode("") currentNumeric = true taken }
// Numeric core components and prerelease identifiers reject leading zeros. decimal: (bytes) => { ((bytes.size() > 1) && (bytes[0] == 48)).ifTrue(() => { Error().signal() }) value: 0 bytes.each((octet) => { value = value * 10 + (octet - 48) }) value }
components: Map() prerelease: Map() build: Map()
finishCore: () => { components[components.size()] = decimal(takeCurrent()) }
finishPrerelease: () => { numeric: currentNumeric bytes: takeCurrent() identifier: null numeric.ifTrue(() => { identifier = decimal(bytes) }) numeric.ifFalse(() => { identifier = Encoding.UTF8.decode(bytes) }) prerelease[prerelease.size()] = identifier }
finishBuild: () => { build[build.size()] = Encoding.UTF8.decode(takeCurrent()) }
// Sections: "core" (MAJOR.MINOR.PATCH), "prerelease", and "build". section: "core"
source.each((octet) => { previousSection: section
(previousSection == "core").ifTrue(() => { handled: false
isDigit(octet).ifTrue(() => { current.add(octet) handled = true })
handled.ifFalse(() => { (octet == 46).ifTrue(() => { (components.size() < 2).ifFalse(() => { Error().signal() }) finishCore() handled = true }) })
handled.ifFalse(() => { ((octet == 45) || (octet == 43)).ifTrue(() => { (components.size() == 2).ifFalse(() => { Error().signal() }) finishCore() (octet == 45).ifTrue(() => { section = "prerelease" }) (octet == 43).ifTrue(() => { section = "build" }) handled = true }) })
handled.ifFalse(() => { Error().signal() }) })
(previousSection == "prerelease").ifTrue(() => { handled: false
(octet == 46).ifTrue(() => { finishPrerelease() handled = true })
handled.ifFalse(() => { (octet == 43).ifTrue(() => { finishPrerelease() section = "build" handled = true }) })
handled.ifFalse(() => { isIdentifierOctet(octet).ifFalse(() => { Error().signal() }) isDigit(octet).ifFalse(() => { currentNumeric = false }) current.add(octet) }) })
(previousSection == "build").ifTrue(() => { (octet == 46).ifTrue(() => { finishBuild() })
(octet == 46).ifFalse(() => { isIdentifierOctet(octet).ifFalse(() => { Error().signal() }) current.add(octet) }) }) })
(section == "core").ifTrue(() => { (components.size() == 2).ifFalse(() => { Error().signal() }) finishCore() }) (section == "prerelease").ifTrue(() => { finishPrerelease() }) (section == "build").ifTrue(() => { finishBuild() })
isVersion: (value) => { (value !== null) && value.hasSlot("major") && value.hasSlot("minor") && value.hasSlot("patch") && value.hasSlot("prerelease") && value.hasSlot("build") && Integer.recognizes(value.major) && Integer.recognizes(value.minor) && Integer.recognizes(value.patch) && Array.recognizes(value.prerelease) && Array.recognizes(value.build) }
// Identifiers are equal only within the same family: Integer 1 never // equals String "1". sameIdentifiers: (left, right) => { same: left.size() == right.size() index: 0 (() => same && (index < left.size())).whileTrue() { first: left[index] second: right[index] same = (Integer.recognizes(first) == Integer.recognizes(second)) && (String.recognizes(first) == String.recognizes(second)) && (first == second) index = index + 1 } same }
// Hashes are folded modulo a fixed prime so the accumulator stays bounded. mix: (accumulated, value) => { (accumulated * 31 + value) % 2305843009213693951 }
version: { major: components[0] minor: components[1] patch: components[2] prerelease: materialize(prerelease) build: materialize(build)
equals: (other) => { version: this isVersion(other) && (version.major == other.major) && (version.minor == other.minor) && (version.patch == other.patch) && sameIdentifiers(version.prerelease, other.prerelease) && sameIdentifiers(version.build, other.build) }
hash: () => { version: this result: mix(mix(mix(17, version.major.hash()), version.minor.hash()), version.patch.hash()) result = mix(result, version.prerelease.size()) version.prerelease.each((identifier) => { family: 1 String.recognizes(identifier).ifTrue(() => { family = 2 }) result = mix(mix(result, family), identifier.hash()) }) result = mix(result, version.build.size()) version.build.each((identifier) => { result = mix(result, identifier.hash()) }) result } }
// The equality and hash Closures capture this activation; release the // scratch state so a version retains only its own slots. source = null current = null components = null prerelease = null build = null version.alias("equals", "==")}
/// Answers the canonical text `MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]` of a/// version answered by `parse`. Integers are written in decimal and String/// identifiers keep their exact spelling, so `format(parse(text)) == text` for/// every accepted `text`. A value without that representation, an empty/// String identifier, an alphanumeric prerelease identifier made only of/// digits, or a character outside the SemVer identifier alphabet signals an/// Error.format: (version) => { // Requires the representation answered by `parse`. requireVersion: (version) => { (Integer.recognizes(version.major) && Integer.recognizes(version.minor) && Integer.recognizes(version.patch) && Array.recognizes(version.prerelease) && Array.recognizes(version.build)).ifFalse(() => { Error().signal() }) ((version.major >= 0) && (version.minor >= 0) && (version.patch >= 0)).ifFalse(() => { Error().signal() }) version.prerelease.each((identifier) => { numeric: Integer.recognizes(identifier) && (identifier >= 0) (numeric || String.recognizes(identifier)).ifFalse(() => { Error().signal() }) }) version.build.each((identifier) => { String.recognizes(identifier).ifFalse(() => { Error().signal() }) }) }
requireVersion(version)
isDigit: (octet) => { (octet >= 48) && (octet <= 57) }
isIdentifierOctet: (octet) => { upper: (octet >= 65) && (octet <= 90) lower: (octet >= 97) && (octet <= 122) isDigit(octet) || upper || lower || (octet == 45) }
output: Encoding.UTF8.encode("")
// Digits are produced least significant first and written in reverse. appendDecimal: (value) => { digits: Map() remaining: value (value == 0).ifTrue(() => { digits[0] = 48 }) (() => remaining > 0).whileTrue() { digits[digits.size()] = 48 + (remaining % 10) remaining = remaining.div(10) } index: digits.size() - 1 (() => index >= 0).whileTrue() { output.add(digits[index]) index = index - 1 } }
appendText: (text, alphanumeric) => { bytes: Encoding.UTF8.encode(text) (bytes.size() == 0).ifTrue(() => { Error().signal() }) hasNonDigit: false bytes.each((octet) => { isIdentifierOctet(octet).ifFalse(() => { Error().signal() }) isDigit(octet).ifFalse(() => { hasNonDigit = true }) output.add(octet) }) (alphanumeric && (hasNonDigit == false)).ifTrue(() => { Error().signal() }) }
appendIdentifiers: (separator, identifiers, alphanumeric) => { next: separator identifiers.each((identifier) => { output.add(next) next = 46 Integer.recognizes(identifier).ifTrue(() => { appendDecimal(identifier) }) String.recognizes(identifier).ifTrue(() => { appendText(identifier, alphanumeric) }) }) }
appendDecimal(version.major) output.add(46) appendDecimal(version.minor) output.add(46) appendDecimal(version.patch) appendIdentifiers(45, version.prerelease, true) appendIdentifiers(43, version.build, false) Encoding.UTF8.decode(output)}
/// Answers `-1`, `0`, or `1` as version `left` has lower, equal, or higher/// Semantic Versioning 2.0.0 precedence than version `right`. Core components/// compare numerically; a version without prerelease identifiers outranks one/// with them; prerelease identifiers compare left to right, numeric ones/// numerically and below every alphanumeric one, alphanumeric ones by ASCII/// octet order; a longer prerelease list outranks its prefix. Build metadata/// never participates, so precedence `0` does not imply `==`. Arguments must/// have the representation answered by `parse`; otherwise an Error is/// signalled.comparePrecedence: (left, right) => { // Requires the representation answered by `parse`. requireVersion: (version) => { (Integer.recognizes(version.major) && Integer.recognizes(version.minor) && Integer.recognizes(version.patch) && Array.recognizes(version.prerelease) && Array.recognizes(version.build)).ifFalse(() => { Error().signal() }) ((version.major >= 0) && (version.minor >= 0) && (version.patch >= 0)).ifFalse(() => { Error().signal() }) version.prerelease.each((identifier) => { numeric: Integer.recognizes(identifier) && (identifier >= 0) (numeric || String.recognizes(identifier)).ifFalse(() => { Error().signal() }) }) version.build.each((identifier) => { String.recognizes(identifier).ifFalse(() => { Error().signal() }) }) }
requireVersion(left) requireVersion(right)
compareIntegers: (first, second) => { result: 0 (first < second).ifTrue(() => { result = -1 }) (first > second).ifTrue(() => { result = 1 }) result }
compareText: (first, second) => { firstBytes: Encoding.UTF8.encode(first) secondBytes: Encoding.UTF8.encode(second) limit: firstBytes.size() (secondBytes.size() < limit).ifTrue(() => { limit = secondBytes.size() }) result: 0 index: 0 (() => (result == 0) && (index < limit)).whileTrue() { result = compareIntegers(firstBytes[index], secondBytes[index]) index = index + 1 } (result == 0).ifTrue(() => { result = compareIntegers(firstBytes.size(), secondBytes.size()) }) result }
compareIdentifiers: (first, second) => { firstNumeric: Integer.recognizes(first) secondNumeric: Integer.recognizes(second) result: 0 (firstNumeric && secondNumeric).ifTrue(() => { result = compareIntegers(first, second) }) (firstNumeric && (!secondNumeric)).ifTrue(() => { result = -1 }) ((!firstNumeric) && secondNumeric).ifTrue(() => { result = 1 }) ((!firstNumeric) && (!secondNumeric)).ifTrue(() => { result = compareText(first, second) }) result }
result: compareIntegers(left.major, right.major) (result == 0).ifTrue(() => { result = compareIntegers(left.minor, right.minor) }) (result == 0).ifTrue(() => { result = compareIntegers(left.patch, right.patch) }) (result == 0).ifTrue(() => { leftSize: left.prerelease.size() rightSize: right.prerelease.size() ((leftSize == 0) || (rightSize == 0)).ifTrue(() => { // A release outranks any prerelease of the same core. result = compareIntegers(rightSize, leftSize) }) ((leftSize > 0) && (rightSize > 0)).ifTrue(() => { limit: leftSize (rightSize < limit).ifTrue(() => { limit = rightSize }) index: 0 (() => (result == 0) && (index < limit)).whileTrue() { result = compareIdentifiers(left.prerelease[index], right.prerelease[index]) index = index + 1 } (result == 0).ifTrue(() => { result = compareIntegers(leftSize, rightSize) }) }) }) result}