Skip to content

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 →