Semver Checker — Zig source
Parse, compare, and validate Semantic Versioning 2.0.0 strings. Check which of two versions is greater (with full prerelease precedence), test whether a version satisfies an npm-style range (^, ~, comparators, hyphen, ||), and bump major/minor/patch/prerelease. Runs 100% client-side.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// semver — Semantic Versioning 2.0.0: strict parse, precedence compare (incl. prerelease ordering), npm-style ranges (^ ~ comparators partials * AND ||) and bumping. Language: Zig (0.13, standard library only). Port of src/lib/semver.ts — same core as this dir's python.py; hyphen ranges and build echo omitted for the 80-line budget (see python.py / go.go).
const std = @import("std");
/// A parsed version: major.minor.patch plus dot-separated prerelease identifiers.
const SemVer = struct {
major: u32, minor: u32, patch: u32,
pre: [8][]const u8 = undefined,
npre: usize = 0,
/// Parse strict "M.m.p[-pre][+build]"; a leading v/V is tolerated. Null on invalid.
fn parse(input: []const u8) ?SemVer {
var s = std.mem.trim(u8, input, " \t");
if (s.len > 0 and (s[0] == 'v' or s[0] == 'V')) s = s[1..]; // tolerate "v1.2.3"
if (std.mem.indexOfScalar(u8, s, '+')) |i| s = s[0..i]; // build never affects precedence
var v = SemVer{ .major = 0, .minor = 0, .patch = 0 };
const core = if (std.mem.indexOfScalar(u8, s, '-')) |d| s[0..d] else s; // pre starts at first '-'
var fields: [3]u32 = undefined;
var nf: usize = 0;
var it = std.mem.splitScalar(u8, core, '.');
while (it.next()) |f| { if (nf == 3) return null; fields[nf] = num(f) orelse return null; nf += 1; }
if (nf != 3) return null;
v.major = fields[0]; v.minor = fields[1]; v.patch = fields[2];
if (std.mem.indexOfScalar(u8, s, '-')) |d| {
if (d + 1 >= s.len) return null;
var pit = std.mem.splitScalar(u8, s[d + 1 ..], '.');
while (pit.next()) |id| { if (!ident(id) or v.npre >= 8) return null; v.pre[v.npre] = id; v.npre += 1; }
}
return v;
}
};
fn num(s: []const u8) ?u32 { // strict numeric field: digits, no leading zero
if (s.len == 0 or (s[0] == '0' and s.len > 1)) return null;
var n: u32 = 0;
for (s) |c| { if (c < '0' or c > '9') return null; n = n * 10 + (c - '0'); }
return n;
}
fn allNum(s: []const u8) bool { for (s) |c| if (c < '0' or c > '9') return false; return s.len > 0; }
fn ident(s: []const u8) bool { // alphanumerics + '-', numeric ids lack leading zeros
if (s.len == 0) return false;
var d = true;
for (s) |c| { const ok = (c >= '0' and c <= '9') or (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or c == '-'; if (!ok) return false; d = d and (c >= '0' and c <= '9'); }
return !(d and s.len > 1 and s[0] == '0');
}
fn identOrder(x: []const u8, y: []const u8) std.math.Order {
const (xd, yd) = .{ allNum(x), allNum(y) };
if (xd and yd) return std.math.order(num(x).?, num(y).?); // numeric ids compare numerically
if (xd) return .lt; // numeric ids rank below alphanumeric
if (yd) return .gt;
return std.mem.order(u8, x, y);
}
/// Compare by precedence. Build metadata is ignored.
fn compare(a: SemVer, b: SemVer) std.math.Order {
var c = std.math.order(a.major, b.major);
if (c != .eq) return c;
c = std.math.order(a.minor, b.minor); if (c != .eq) return c;
c = std.math.order(a.patch, b.patch); if (c != .eq) return c;
if (a.npre == 0 or b.npre == 0) return std.math.order(b.npre, a.npre); // release > prerelease
var i: usize = 0;
while (i < a.npre and i < b.npre) : (i += 1) { c = identOrder(a.pre[i], b.pre[i]); if (c != .eq) return c; }
return std.math.order(a.npre, b.npre); // equal shared ids: larger set wins
}
/// Partial range version: null field = absent/wildcard (fields cascade down).
const Partial = struct { major: ?u32 = null, minor: ?u32 = null, patch: ?u32 = null };
const Op = enum { gt, gte, lt, lte, eq };
const Test = struct { op: Op, v: SemVer };
var tests: [8]Test = undefined;
var ntests: usize = 0;
fn add(op: Op, M: u32, m: u32, p: u32) void { tests[ntests] = .{ .op = op, .v = .{ .major = M, .minor = m, .patch = p } }; ntests += 1; }
fn parsePartial(s0: []const u8) ?Partial { // "1", "1.2", "1.2.3", "1.x", "*"
var s = s0;
if (s.len > 0 and (s[0] == 'v' or s[0] == 'V')) s = s[1..];
if (s.len == 0 or std.mem.eql(u8, s, "*") or std.mem.eql(u8, s, "x") or std.mem.eql(u8, s, "X")) return Partial{};
var vals = [3]?u32{ null, null, null };
var it = std.mem.splitScalar(u8, s, '.');
var i: usize = 0;
while (it.next()) |f| {
if (i == 3) return null;
if (!(std.mem.eql(u8, f, "*") or std.mem.eql(u8, f, "x") or std.mem.eql(u8, f, "X"))) vals[i] = num(f) orelse return null;
i += 1; }
if (vals[0] == null) return Partial{};
if (vals[1] == null) return Partial{ .major = vals[0] };
return Partial{ .major = vals[0], .minor = vals[1], .patch = vals[2] };
}
fn desugar(op: []const u8, t: Partial) void { // one comparator -> primitive >=/< tests
const M = t.major orelse return; // wildcard matches everything
const m = t.minor orelse 0;
const p = t.patch orelse 0;
if (std.mem.eql(u8, op, "^")) { // never break the left-most non-zero part
add(.gte, M, m, p);
if (M > 0 or t.minor == null) add(.lt, M + 1, 0, 0)
else if (t.minor.? > 0 or t.patch == null) add(.lt, 0, t.minor.? + 1, 0)
else add(.lt, 0, 0, t.patch.? + 1);
} else if (std.mem.eql(u8, op, "~")) {
add(.gte, M, m, p);
if (t.minor == null) add(.lt, M + 1, 0, 0) else add(.lt, M, t.minor.? + 1, 0);
} else if (std.mem.eql(u8, op, "bare") or std.mem.eql(u8, op, "=")) { // "1.2" = >=1.2.0 <1.3.0
if (t.minor == null) { add(.gte, M, 0, 0); add(.lt, M + 1, 0, 0); }
else if (t.patch == null) { add(.gte, M, t.minor.?, 0); add(.lt, M, t.minor.? + 1, 0); }
else add(.eq, M, t.minor.?, t.patch.?);
} else if (std.mem.eql(u8, op, ">=")) add(.gte, M, m, p)
else if (std.mem.eql(u8, op, ">")) { if (t.minor == null) add(.gte, M + 1, 0, 0) else if (t.patch == null) add(.gte, M, t.minor.? + 1, 0) else add(.gt, M, t.minor.?, t.patch.?); }
else if (std.mem.eql(u8, op, "<=")) { if (t.minor == null) add(.lt, M + 1, 0, 0) else if (t.patch == null) add(.lt, M, t.minor.? + 1, 0) else add(.lte, M, t.minor.?, t.patch.?); }
else { if (t.minor == null) add(.lt, M, 0, 0) else if (t.patch == null) add(.lt, M, t.minor.?, 0) else add(.lt, M, t.minor.?, t.patch.?); }
}
fn check(t: Test, v: SemVer) bool {
const c = compare(v, t.v);
return switch (t.op) { .gt => c == .gt, .gte => c != .lt, .lt => c == .lt, .lte => c != .gt, .eq => c == .eq };
}
fn clauseMatches(v: SemVer, cl: []const u8) bool { // one AND-clause of the "||" split
ntests = 0;
var toks: [16][]const u8 = undefined;
var n: usize = 0;
var it = std.mem.tokenizeAny(u8, cl, " \t");
while (it.next()) |t| { toks[n] = t; n += 1; }
for (toks[0..n]) |tok0| {
var tok = tok0;
var op: []const u8 = "bare";
if (tok.len > 0 and (tok[0] == '^' or tok[0] == '~')) { op = tok[0..1]; tok = tok[1..]; }
else if (std.mem.startsWith(u8, tok, ">=")) { op = ">="; tok = tok[2..]; }
else if (std.mem.startsWith(u8, tok, "<=")) { op = "<="; tok = tok[2..]; }
else if (tok.len > 0 and (tok[0] == '>' or tok[0] == '<' or tok[0] == '=')) { op = tok[0..1]; tok = tok[1..]; }
const part = parsePartial(tok) orelse return false; // invalid comparator -> unsatisfiable
desugar(op, part);
}
for (tests[0..ntests]) |t| if (!check(t, v)) return false;
return true; // no tests ("*" clause) -> true
}
/// Does `version` satisfy the npm-style `range`? An invalid version yields false.
fn satisfies(version: []const u8, range: []const u8) bool {
const v = SemVer.parse(version) orelse return false;
var it = std.mem.splitSequence(u8, range, "||");
while (it.next()) |cl| if (clauseMatches(v, cl)) return true;
return false;
}
/// Bump by kind; the prerelease bump increments a trailing numeric id (or appends -0 / .1).
fn bump(input: []const u8, kind: []const u8, out: []u8) []const u8 {
const s = SemVer.parse(input) orelse return input;
if (std.mem.eql(u8, kind, "major")) return std.fmt.bufPrint(out, "{d}.0.0", .{s.major + 1}) catch input;
if (std.mem.eql(u8, kind, "minor")) return std.fmt.bufPrint(out, "{d}.{d}.0", .{s.major, s.minor + 1}) catch input;
if (std.mem.eql(u8, kind, "patch")) return std.fmt.bufPrint(out, "{d}.{d}.{d}", .{s.major, s.minor, s.patch + 1}) catch input;
if (s.npre == 0) return std.fmt.bufPrint(out, "{d}.{d}.{d}-0", .{s.major, s.minor, s.patch + 1}) catch input;
var fbs = std.io.fixedBufferStream(out);
const w = fbs.writer();
w.print("{d}.{d}.{d}-", .{s.major, s.minor, s.patch}) catch return input;
for (s.pre[0 .. s.npre - 1]) |id| w.print("{s}.", .{id}) catch return input;
const last = s.pre[s.npre - 1];
if (allNum(last)) w.print("{d}", .{(num(last) orelse 0) + 1}) catch return input else w.print("{s}.1", .{last}) catch return input;
return fbs.getWritten();
}
pub fn main() void {
const out = std.io.getStdOut().writer();
const a = SemVer.parse("1.2.3-alpha.1+build.5").?;
out.print("parse 1.2.3-alpha.1+build.5 -> {d}.{d}.{d}-{s}.{s}\n", .{a.major, a.minor, a.patch, a.pre[0], a.pre[1]}) catch {};
const chain = [_][]const u8{ "1.0.0-alpha", "1.0.0-alpha.1", "1.0.0-beta", "1.0.0-beta.11", "1.0.0-rc.1", "1.0.0" };
for (chain[0 .. chain.len - 1], 0..) |_, i| {
const c = compare(SemVer.parse(chain[i]).?, SemVer.parse(chain[i + 1]).?);
out.print("compare {s} {s} {s}\n", .{chain[i], if (c == .lt) "<" else "!", chain[i + 1]}) catch {}; }
const vr = [_][2][]const u8{ .{ "1.2.3", "^1.2.0" }, .{ "1.9.0", "~1.2.0" }, .{ "0.2.5", "^0.2.3" }, .{ "0.3.0", "^0.2.3" }, .{ "3.1.0", ">=1.0.0 <2.0.0 || >=3.0.0" }, .{ "1.2.3", "*" } };
for (vr) |r| out.print("satisfies {s} {s} -> {}\n", .{r[0], r[1], satisfies(r[0], r[1])}) catch {};
var buf: [128]u8 = undefined;
var buf2: [128]u8 = undefined;
out.print("bump 1.2.3 minor -> {s}\n", .{bump("1.2.3", "minor", &buf)}) catch {};
out.print("bump 1.2.3-beta.2 pre -> {s}\n", .{bump("1.2.3-beta.2", "prerelease", &buf2)}) catch {};
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →