Semantic Versioning Parser for voc

This commit is contained in:
Antranig Vartanian 2025-06-16 07:49:29 +04:00
commit 052d41505e
No known key found for this signature in database
GPG key ID: DE3998662D59F21C
7 changed files with 510 additions and 0 deletions

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
# Set the language to Oberon
*.Mod linguist-language=Oberon
*.mod linguist-language=Oberon

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build

22
LICENSE.txt Normal file
View file

@ -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.

25
Makefile Normal file
View file

@ -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)

94
README.md Normal file
View file

@ -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

168
src/SemVer.Mod Normal file
View file

@ -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.

197
tests/SemVerTest.Mod Normal file
View file

@ -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.