From 052d41505ef743c21883d8e05ef1a17b687e5a8f Mon Sep 17 00:00:00 2001 From: Antranig Vartanian Date: Mon, 16 Jun 2025 07:49:29 +0400 Subject: [PATCH] Semantic Versioning Parser for voc --- .gitattributes | 3 + .gitignore | 1 + LICENSE.txt | 22 +++++ Makefile | 25 ++++++ README.md | 94 +++++++++++++++++++++ src/SemVer.Mod | 168 ++++++++++++++++++++++++++++++++++++ tests/SemVerTest.Mod | 197 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 510 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 README.md create mode 100644 src/SemVer.Mod create mode 100644 tests/SemVerTest.Mod diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8e09bda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Set the language to Oberon +*.Mod linguist-language=Oberon +*.mod linguist-language=Oberon diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..9f17d2b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright 2025 Antranig Vartanian. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bd225e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.POSIX: + +SRC = ../src +TESTS = ../tests +BUILD = build + +OBJS = $(BUILD)/SemVer.o +SEMVERTEST = $(BUILD)/SemVerTest + +all: build + +build: + mkdir -p $(BUILD) + cd $(BUILD) && voc $(SRC)/SemVer.Mod -s 2>/dev/null + +test: $(SEMVERTEST) + cd $(BUILD) \ + && ./SemVerTest + +$(SEMVERTEST): build + cd $(BUILD) \ + && voc $(TESTS)/SemVerTest.Mod -m 2>/dev/null + +clean: + rm -rf $(BUILD) diff --git a/README.md b/README.md new file mode 100644 index 0000000..db4985f --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Semantic Versioning for Vishap Oberon Compiler (voc) + +A strict, specification-compliant Semantic Versioning parser for [Vishap Oberon](https://vishap.oberon.am). +Designed for use in package managers, version comparison tools, and system-level +Oberon utilities. + +## Features + +- Parses full SemVer 2.0.0 strings (`MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]`) +- Validates all pre-release and build metadata +- Outputs parsed results into a structured Version record + +## Example + +Input: +``` +1.1.2-prerelease+meta +``` + +Result (parsed record): +``` +major=1; +minor=1; +patch=2; +preRelease=prerelease; +build=meta; +``` + +## Usage + +``` +MODULE SemVerExample; + IMPORT SemVer, Out; +VAR + v: SemVer.Version; + ok: BOOLEAN; +BEGIN + SemVer.Parse("1.2.3-alpha.1+build.5", v, ok); + IF ok THEN + Out.String("major="); Out.Int(v.Major, 1); Out.String("; "); + Out.String("minor="); Out.Int(v.Minor, 1); Out.String("; "); + Out.String("patch="); Out.Int(v.Patch, 1); Out.String("; "); + Out.String("preRelease="); Out.String(v.PreRelease); Out.String("; "); + Out.String("build="); Out.String(v.Build); Out.String(";"); + Out.Ln; + ELSE + Out.String("Invalid version string."); Out.Ln; + END; +END SemVerExample. +``` + +``` + $ voc ../src/SemVer.Mod -s SemVerExample.Mod -m 2>/dev/null && ./SemVerExample +../src/SemVer.Mod Compiling SemVer. New symbol file. 5302 chars. +SemVerExample.Mod Compiling SemVerExample. Main program. 1204 chars. +major=1; minor=2; patch=3; preRelease=alpha.1; build=build.5; +``` + + + +## Building + +``` +make +``` + +## Tests + +``` +make test +``` + +Includes various valid and invalid test cases to confirm compliance with +the SemVer 2.0.0 specification. + +## Dependencies + +- For Building: `voc` (duh!) +- Test runner: none + +## TODO + +- Compare +- Equals +- ToString +- ParseConstraint +- MatchConstraint +- IsStable +- IsPreRelease + +## License + +BSD 2-Clause License + diff --git a/src/SemVer.Mod b/src/SemVer.Mod new file mode 100644 index 0000000..fba82af --- /dev/null +++ b/src/SemVer.Mod @@ -0,0 +1,168 @@ +MODULE SemVer; + + IMPORT Out; + +TYPE + StringIdentifier = ARRAY 256 OF CHAR; + Version* = RECORD + Major*, Minor*, Patch*: INTEGER; + PreRelease*, Build*: StringIdentifier; + END; + +PROCEDURE IsDigit(ch: CHAR): BOOLEAN; +BEGIN + RETURN (ch >= '0') & (ch <= '9'); +END IsDigit; + +PROCEDURE HasLeadingZero(s: ARRAY OF CHAR; i: INTEGER): BOOLEAN; +BEGIN + RETURN (s[i] = '0') & IsDigit(s[i + 1]); +END HasLeadingZero; + +PROCEDURE ParseInt(s: ARRAY OF CHAR; VAR i, n: INTEGER; VAR ok: BOOLEAN); +VAR digit: INTEGER; +BEGIN + n := 0; + IF ~IsDigit(s[i]) THEN ok := FALSE; RETURN END; + + WHILE (i < LEN(s)) & IsDigit(s[i]) DO + digit := ORD(s[i]) - ORD('0'); + n := n * 10 + digit; + INC(i); + END; + ok := TRUE; +END ParseInt; + +PROCEDURE IsIdentChar(ch: CHAR): BOOLEAN; +BEGIN + RETURN ((ch >= '0') & (ch <= '9')) OR + ((ch >= 'A') & (ch <= 'Z')) OR + ((ch >= 'a') & (ch <= 'z')) OR + (ch = '-'); +END IsIdentChar; + +PROCEDURE ParseIdentifiers( + s: ARRAY OF CHAR; + VAR i: INTEGER; + VAR dest: StringIdentifier; + checkLeadingZero: BOOLEAN; + VAR ok: BOOLEAN); +VAR + j, segLen: INTEGER; + ch: CHAR; + isNumeric, hasLeadingZero: BOOLEAN; +BEGIN + j := 0; ok := FALSE; + + (* Require at least one character *) + IF (s[i] = '.') OR (s[i] = '+') OR (s[i] = 0X) THEN RETURN END; + + LOOP + segLen := 0; + isNumeric := TRUE; + hasLeadingZero := FALSE; + + (* Empty identifier is invalid *) + IF (s[i] = '.') OR (s[i] = '+') OR (s[i] = 0X) THEN RETURN END; + + WHILE (s[i] # '.') & (s[i] # '+') & (s[i] # 0X) DO + ch := s[i]; + + IF ~IsIdentChar(ch) THEN RETURN END; + + (* Check for leading-zero numeric identifiers *) + IF checkLeadingZero THEN + IF segLen = 0 THEN + IF ch = '0' THEN hasLeadingZero := TRUE END; + ELSE + IF hasLeadingZero & IsDigit(ch) THEN RETURN END; + END; + IF ~IsDigit(ch) THEN isNumeric := FALSE END; + END; + + IF j >= LEN(dest) - 1 THEN RETURN END; + dest[j] := ch; INC(i); INC(j); INC(segLen); + END; + + (* Add dot if needed *) + IF s[i] = '.' THEN + IF j >= LEN(dest) - 1 THEN RETURN END; + dest[j] := '.'; INC(i); INC(j); + ELSE + EXIT; + END; + END; + + dest[j] := 0X; + ok := TRUE; +END ParseIdentifiers; + +PROCEDURE ParsePreRelease( + s: ARRAY OF CHAR; + VAR i: INTEGER; + VAR PreRelease: StringIdentifier; + VAR ok: BOOLEAN); +BEGIN + IF s[i] # '-' THEN ok := FALSE; RETURN END; + INC(i); + ParseIdentifiers(s, i, PreRelease, TRUE (* checkLeadingZero *), ok); +END ParsePreRelease; + +PROCEDURE ParseBuild( + s: ARRAY OF CHAR; + VAR i: INTEGER; + VAR Build: StringIdentifier; + VAR ok: BOOLEAN); +BEGIN + IF s[i] # '+' THEN ok := FALSE; RETURN END; + INC(i); + ParseIdentifiers(s, i, Build, FALSE (* checkLeadingZero *), ok); +END ParseBuild; + + +PROCEDURE Parse*(s: ARRAY OF CHAR; VAR v: Version; VAR finalok: BOOLEAN); +VAR + i: INTEGER; + ok: BOOLEAN; +BEGIN + (* Make sure it's clean *) + v.Major := 0; + v.Minor := 0; + v.Patch := 0; + v.PreRelease[0] := 0X; + v.Build[0] := 0X; + + ok := FALSE; + finalok := FALSE; + i := 0; + + (* Major *) + IF HasLeadingZero(s, i) THEN RETURN END; + ParseInt(s, i, v.Major, ok); IF ~ok THEN RETURN END; + IF s[i] # '.' THEN RETURN END; INC(i); + + (* Minor *) + IF HasLeadingZero(s, i) THEN RETURN END; + ParseInt(s, i, v.Minor, ok); IF ~ok THEN RETURN END; + IF s[i] # '.' THEN RETURN END; INC(i); + + (* Patch *) + IF HasLeadingZero(s, i) THEN RETURN END; + ParseInt(s, i, v.Patch, ok); IF ~ok THEN RETURN END; + + (* PreRelease, if exists *) + IF s[i] = '-' THEN + ParsePreRelease(s, i, v.PreRelease, ok) + END; + + (* Build, if exists *) + IF s[i] = '+' THEN + ParseBuild(s, i, v.Build, ok) + END; + + (* Must end cleanly *) + IF s[i] # 0X THEN ok := FALSE; RETURN END; + + finalok := TRUE; +END Parse; +END SemVer. diff --git a/tests/SemVerTest.Mod b/tests/SemVerTest.Mod new file mode 100644 index 0000000..83a98f5 --- /dev/null +++ b/tests/SemVerTest.Mod @@ -0,0 +1,197 @@ +MODULE SemVerTest; + +IMPORT SemVer, Out; + +TYPE + TestCase = RECORD + input: ARRAY 128 OF CHAR; + valid: BOOLEAN; + END; + +VAR + v: SemVer.Version; + ok: BOOLEAN; + i: INTEGER; + tests: ARRAY 71 OF TestCase; + +PROCEDURE PrintVersion(v: SemVer.Version); +BEGIN + Out.String("major="); Out.Int(v.Major, 1); Out.String("; "); + Out.String("minor="); Out.Int(v.Minor, 1); Out.String("; "); + Out.String("patch="); Out.Int(v.Patch, 1); Out.String("; "); + + Out.String("preRelease="); + Out.String(v.PreRelease); Out.String("; "); + + Out.String("build="); + Out.String(v.Build); Out.String("; "); + Out.Ln; +END PrintVersion; + +PROCEDURE PrintBool(b: BOOLEAN); +BEGIN + IF b THEN Out.String("valid") ELSE Out.String("invalid") END +END PrintBool; + +BEGIN + tests[0].input := "0.0.4"; + tests[0].valid := TRUE; + tests[1].input := "1.2.3"; + tests[1].valid := TRUE; + tests[2].input := "10.20.30"; + tests[2].valid := TRUE; + tests[3].input := "1.1.2-prerelease+meta"; + tests[3].valid := TRUE; + tests[4].input := "1.1.2+meta"; + tests[4].valid := TRUE; + tests[5].input := "1.1.2+meta-valid"; + tests[5].valid := TRUE; + tests[6].input := "1.0.0-alpha"; + tests[6].valid := TRUE; + tests[7].input := "1.0.0-beta"; + tests[7].valid := TRUE; + tests[8].input := "1.0.0-alpha.beta"; + tests[8].valid := TRUE; + tests[9].input := "1.0.0-alpha.beta.1"; + tests[9].valid := TRUE; + tests[10].input := "1.0.0-alpha.1"; + tests[10].valid := TRUE; + tests[11].input := "1.0.0-alpha0.valid"; + tests[11].valid := TRUE; + tests[12].input := "1.0.0-alpha.0valid"; + tests[12].valid := TRUE; + tests[13].input := "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay"; + tests[13].valid := TRUE; + tests[14].input := "1.0.0-rc.1+build.1"; + tests[14].valid := TRUE; + tests[15].input := "2.0.0-rc.1+build.123"; + tests[15].valid := TRUE; + tests[16].input := "1.2.3-beta"; + tests[16].valid := TRUE; + tests[17].input := "10.2.3-DEV-SNAPSHOT"; + tests[17].valid := TRUE; + tests[18].input := "1.2.3-SNAPSHOT-123"; + tests[18].valid := TRUE; + tests[19].input := "1.0.0"; + tests[19].valid := TRUE; + tests[20].input := "2.0.0"; + tests[20].valid := TRUE; + tests[21].input := "1.1.7"; + tests[21].valid := TRUE; + tests[22].input := "2.0.0+build.1848"; + tests[22].valid := TRUE; + tests[23].input := "2.0.1-alpha.1227"; + tests[23].valid := TRUE; + tests[24].input := "1.0.0-alpha+beta"; + tests[24].valid := TRUE; + tests[25].input := "1.2.3----RC-SNAPSHOT.12.9.1--.12+788"; + tests[25].valid := TRUE; + tests[26].input := "1.2.3----R-S.12.9.1--.12+meta"; + tests[26].valid := TRUE; + tests[27].input := "1.2.3----RC-SNAPSHOT.12.9.1--.12"; + tests[27].valid := TRUE; + tests[28].input := "1.0.0+0.build.1-rc.10000aaa-kk-0.1"; + tests[28].valid := TRUE; + tests[29].input := "99999999999999999999999.999999999999999999.99999999999999999"; + tests[29].valid := TRUE; + tests[30].input := "1.0.0-0A.is.legal"; + tests[30].valid := TRUE; + + tests[31].input := "1"; + tests[31].valid := FALSE; + tests[32].input := "1.2"; + tests[32].valid := FALSE; + tests[33].input := "1.2.3-0123"; + tests[33].valid := FALSE; + tests[34].input := "1.2.3-0123.0123"; + tests[34].valid := FALSE; + tests[35].input := "1.1.2+.123"; + tests[35].valid := FALSE; + tests[36].input := "+invalid"; + tests[36].valid := FALSE; + tests[37].input := "-invalid"; + tests[37].valid := FALSE; + tests[38].input := "-invalid+invalid"; + tests[38].valid := FALSE; + tests[39].input := "-invalid.01"; + tests[39].valid := FALSE; + tests[40].input := "alpha"; + tests[40].valid := FALSE; + tests[41].input := "alpha.beta"; + tests[41].valid := FALSE; + tests[42].input := "alpha.beta.1"; + tests[42].valid := FALSE; + tests[43].input := "alpha.1"; + tests[43].valid := FALSE; + tests[44].input := "alpha+beta"; + tests[44].valid := FALSE; + tests[45].input := "alpha_beta"; + tests[45].valid := FALSE; + tests[46].input := "alpha."; + tests[46].valid := FALSE; + tests[47].input := "alpha.."; + tests[47].valid := FALSE; + tests[48].input := "beta"; + tests[48].valid := FALSE; + tests[49].input := "1.0.0-alpha_beta"; + tests[49].valid := FALSE; + tests[50].input := "-alpha."; + tests[50].valid := FALSE; + tests[51].input := "1.0.0-alpha.."; + tests[51].valid := FALSE; + tests[52].input := "1.0.0-alpha..1"; + tests[52].valid := FALSE; + tests[53].input := "1.0.0-alpha...1"; + tests[53].valid := FALSE; + tests[54].input := "1.0.0-alpha....1"; + tests[54].valid := FALSE; + tests[55].input := "1.0.0-alpha.....1"; + tests[55].valid := FALSE; + tests[56].input := "1.0.0-alpha......1"; + tests[56].valid := FALSE; + tests[57].input := "1.0.0-alpha.......1"; + tests[57].valid := FALSE; + tests[58].input := "01.1.1"; + tests[58].valid := FALSE; + tests[59].input := "1.01.1"; + tests[59].valid := FALSE; + tests[60].input := "1.1.01"; + tests[60].valid := FALSE; + tests[61].input := "1.2"; + tests[61].valid := FALSE; + tests[62].input := "1.2.3.DEV"; + tests[62].valid := FALSE; + tests[63].input := "1.2-SNAPSHOT"; + tests[63].valid := FALSE; + tests[64].input := "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788"; + tests[64].valid := FALSE; + tests[65].input := "1.2-RC-SNAPSHOT"; + tests[65].valid := FALSE; + tests[66].input := "-1.0.3-gamma+b7718"; + tests[66].valid := FALSE; + tests[67].input := "+justmeta"; + tests[67].valid := FALSE; + tests[68].input := "9.8.7+meta+meta"; + tests[68].valid := FALSE; + tests[69].input := "9.8.7-whatever+meta+meta"; + tests[69].valid := FALSE; + tests[70].input := "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12"; + tests[70].valid := FALSE; + + FOR i := 0 TO LEN(tests)-1 DO + Out.String("Test "); Out.Int(i, 1); Out.String(": "); + Out.String(tests[i].input); + + SemVer.Parse(tests[i].input, v, ok); + Out.String("; expected="); PrintBool(tests[i].valid); + Out.String("; actual="); PrintBool(ok); Out.Ln; + + IF ok # tests[i].valid THEN Out.String("Test failed!!!"); Out.Ln; RETURN END; + + IF ok THEN + PrintVersion(v); + END; + Out.Ln; + END; + Out.String("Test success!!!"); Out.Ln; +END SemVerTest.