From caf08696d49a2b232c829f88321d0b9170dd67a7 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Fri, 16 Jan 2026 17:40:12 +0100 Subject: [PATCH 1/5] Word operators as alternatives to symbols --- grammar.txt | 6 +- src/semantic_versioning-extended.adb | 86 +++++++++++++++++-- .../src/semver_tests-extended_expressions.adb | 3 + tests/src/semver_tests-word_operators.adb | 18 ++++ 4 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 tests/src/semver_tests-word_operators.adb diff --git a/grammar.txt b/grammar.txt index 6f6c1a7..1e5fbc0 100644 --- a/grammar.txt +++ b/grammar.txt @@ -13,9 +13,9 @@ EVS_Nested ::= '(' EVS ')' VS ::= V | OP V V ::= list ::= list_and | list_or -list_and ::= '&' EVS_and -list_or ::= '|' EVS_or +list_and ::= ('&' | 'and') EVS_and +list_or ::= ('|' | 'or') EVS_or EVS_and ::= (EVS_Nested | VS) [and_list] EVS_or ::= (EVS_Nested | VS) [or_list] OP ::= '<' | '>' etc -NEG ::= '!' +NEG ::= '!' | 'not' diff --git a/src/semantic_versioning-extended.adb b/src/semantic_versioning-extended.adb index ff6dbb8..38a508f 100644 --- a/src/semantic_versioning-extended.adb +++ b/src/semantic_versioning-extended.adb @@ -441,6 +441,25 @@ package body Semantic_Versioning.Extended is ---------------- function Next_Token (Skip_Whitespace : Boolean := True) return Tokens is + + function Is_Keyword (Word : String) return Boolean is + Last : constant Integer := I + Word'Length - 1; + begin + if Last > Str'Last then + return False; + end if; + + if ACH.To_Lower (Str (I .. Last)) /= Word then + return False; + end if; + + if Last < Str'Last and then ACH.Is_Alphanumeric (Str (Last + 1)) then + return False; + end if; + + return True; + end Is_Keyword; + function Internal return Tokens is begin if I > Str'Last then @@ -457,6 +476,14 @@ package body Semantic_Versioning.Extended is return End_Of_Input; end if; + if Is_Keyword ("and") then + return Ampersand; + elsif Is_Keyword ("or") then + return Pipe; + elsif Is_Keyword ("not") then + return Negation; + end if; + if Begins_With_Relational (Str (I .. Str'Last), Unicode) then return VS; end if; @@ -479,6 +506,49 @@ package body Semantic_Versioning.Extended is end return; end Next_Token; + ---------------- + -- Match_Token -- + ---------------- + + procedure Match_Token (T : Tokens) is + begin + case T is + when Ampersand => + if I <= Str'Last and then ACH.To_Lower (Str (I .. Integer'Min (Str'Last, I + 2))) = "and" + and then (I + 3 > Str'Last or else not ACH.Is_Alphanumeric (Str (I + 3))) + then + I := I + 3; + else + Match ('&'); + end if; + + when Pipe => + if I <= Str'Last and then ACH.To_Lower (Str (I .. Integer'Min (Str'Last, I + 1))) = "or" + and then (I + 2 > Str'Last or else not ACH.Is_Alphanumeric (Str (I + 2))) + then + I := I + 2; + else + Match ('|'); + end if; + + when Negation => + if I <= Str'Last and then ACH.To_Lower (Str (I .. Integer'Min (Str'Last, I + 2))) = "not" + and then (I + 3 > Str'Last or else not ACH.Is_Alphanumeric (Str (I + 3))) + then + I := I + 3; + else + Match ('!'); + end if; + + when Lparen => + Match ('('); + when Rparen => + Match (')'); + when others => + raise Program_Error; + end case; + end Match_Token; + function Prod_EVS_Nested return Version_Set; function Prod_List (Head : Version_Set; Kind : List_Kinds) return Version_Set; @@ -494,9 +564,9 @@ package body Semantic_Versioning.Extended is Next : Version_Set; begin Trace ("Prod EVS"); - case Next_Token is + case Next_Token is when Negation => - Match ('!'); + Match_Token (Negation); declare Child : constant Version_Set := Prod_EVS (List_Kind, With_List => False); begin @@ -543,10 +613,10 @@ package body Semantic_Versioning.Extended is function Prod_EVS_Nested return Version_Set is begin Trace ("Prod EVS Nested"); - Match ('('); + Match_Token (Lparen); return VS : Version_Set := Prod_EVS (Any, With_List => True) do VS.Image := '(' & VS.Image & ')'; - Match (')'); + Match_Token (Rparen); end return; end Prod_EVS_Nested; @@ -565,8 +635,8 @@ package body Semantic_Versioning.Extended is procedure Check_Mismatch is begin if I <= Str'Last then - if (Kind = Anded and then Str (I) = '|') or else - (Kind = Ored and then Str (I) = '&') + if (Kind = Anded and then Next_Token = Pipe) or else + (Kind = Ored and then Next_Token = Ampersand) then Error ("Cannot mix '&' and '|' operators, use parentheses"); end if; @@ -589,12 +659,12 @@ package body Semantic_Versioning.Extended is when Anded => Check_Mismatch; - Match ('&'); + Match_Token (Ampersand); return New_Pair (Head, Prod_EVS (Anded, With_List => True), Anded); when Ored => Check_Mismatch; - Match ('|'); + Match_Token (Pipe); return New_Pair (Head, Prod_EVS (Ored, With_List => True), Ored); end case; end Prod_List; diff --git a/tests/src/semver_tests-extended_expressions.adb b/tests/src/semver_tests-extended_expressions.adb index c9d6fa0..37f0f83 100644 --- a/tests/src/semver_tests-extended_expressions.adb +++ b/tests/src/semver_tests-extended_expressions.adb @@ -16,4 +16,7 @@ begin Assert (X.Parse ("((1&(2|3)))").Valid); Assert (X.Value ("*").Image = X.Any.Image); Assert (X.Value ("*").Image = X.Value ("any").Image); + Assert (X.Is_In (V ("1.1"), X.Value ("^1 and <2"))); + Assert (X.Is_In (V ("2"), X.Value ("not =1 or =2"))); -- equivalent to "(!=1)|(=3)" + Assert (not X.Is_In (V ("1"), X.Value ("not =1 and =2"))); -- equivalent to "(!=1)&(=3)" end Semver_Tests.Extended_Expressions; diff --git a/tests/src/semver_tests-word_operators.adb b/tests/src/semver_tests-word_operators.adb new file mode 100644 index 0000000..cbc1ea6 --- /dev/null +++ b/tests/src/semver_tests-word_operators.adb @@ -0,0 +1,18 @@ +procedure Semver_Tests.Word_Operators is +begin + Assert (X.Parse ("1 and 2").Valid); + Assert (X.Parse ("1 or 2").Valid); + Assert (X.Parse ("not 1").Valid); + Assert (X.Parse ("NoT 1").Valid); + Assert (X.Parse ("1 and (2 or 3)").Valid); + Assert (X.Value ("not 1 and 2").Synthetic_Image = "!(=1.0.0)&=2.0.0"); + Assert (X.Value ("not (1 or 2)").Synthetic_Image = "!(=1.0.0|=2.0.0)"); + + Assert (not X.Parse ("and 1").Valid); + Assert (not X.Parse ("1 and").Valid); + Assert (not X.Parse ("1 or").Valid); + Assert (not X.Parse ("not").Valid); + Assert (not X.Parse ("1 or not").Valid); + Assert (not X.Parse ("1 and or 2").Valid); + Assert (not X.Parse ("1 and 2 or 3").Valid); +end Semver_Tests.Word_Operators; From 2d1f9d581e4b082101d08a3b83246df9863278ab Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Fri, 16 Jan 2026 17:49:24 +0100 Subject: [PATCH 2/5] Precedences test --- src/semantic_versioning-extended.adb | 2 +- tests/src/semver_tests-precedences.adb | 45 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/src/semver_tests-precedences.adb diff --git a/src/semantic_versioning-extended.adb b/src/semantic_versioning-extended.adb index 38a508f..eebedc3 100644 --- a/src/semantic_versioning-extended.adb +++ b/src/semantic_versioning-extended.adb @@ -638,7 +638,7 @@ package body Semantic_Versioning.Extended is if (Kind = Anded and then Next_Token = Pipe) or else (Kind = Ored and then Next_Token = Ampersand) then - Error ("Cannot mix '&' and '|' operators, use parentheses"); + Error ("Cannot mix 'and' and 'or' operators, use parentheses"); end if; end if; end Check_Mismatch; diff --git a/tests/src/semver_tests-precedences.adb b/tests/src/semver_tests-precedences.adb new file mode 100644 index 0000000..d57877c --- /dev/null +++ b/tests/src/semver_tests-precedences.adb @@ -0,0 +1,45 @@ +procedure Semver_Tests.Precedences is +begin + Assert (X.Value ("not 1 and 2").Synthetic_Image = "!(=1.0.0)&=2.0.0"); + Assert (X.Value ("not (1 and 2)").Synthetic_Image = "!(=1.0.0&=2.0.0)"); + Assert (X.Value ("not 1 or 2").Synthetic_Image = "!(=1.0.0)|=2.0.0"); + Assert (X.Value ("not (1 or 2)").Synthetic_Image = "!(=1.0.0|=2.0.0)"); + + -- Multiple and/or with not + Assert (X.Value ("not 1 and 2 and 3").Synthetic_Image = + "!(=1.0.0)&=2.0.0&=3.0.0"); + Assert (X.Value ("not 1 or 2 or 3").Synthetic_Image = + "!(=1.0.0)|=2.0.0|=3.0.0"); + Assert (X.Value ("not 1 and (2 or 3)").Synthetic_Image = + "!(=1.0.0)&(=2.0.0|=3.0.0)"); + Assert (X.Value ("(not 1 and 2) or 3").Synthetic_Image = + "(!(=1.0.0)&=2.0.0)|=3.0.0"); + Assert (X.Value ("not (1 and 2) or 3").Synthetic_Image = + "!(=1.0.0&=2.0.0)|=3.0.0"); + Assert (X.Value ("not (1 or 2) and 3").Synthetic_Image = + "!(=1.0.0|=2.0.0)&=3.0.0"); + Assert (X.Value ("not (1 or (2 and 3)) and (4 or 5)").Synthetic_Image = + "!(=1.0.0|(=2.0.0&=3.0.0))&(=4.0.0|=5.0.0)"); + Assert (X.Value ("not 1 and 2 and (3 or not 4)").Synthetic_Image = + "!(=1.0.0)&=2.0.0&(=3.0.0|!(=4.0.0))"); + + -- Mixing and/or with/without parentheses + Assert (not X.Parse ("1 and 2 or 3").Valid); + Assert (not X.Parse ("1 or 2 and 3").Valid); + Assert (not X.Parse ("1 & 2 or 3").Valid); + Assert (not X.Parse ("1 | 2 and 3").Valid); + Assert (not X.Parse ("1 and 2 | 3").Valid); + Assert (not X.Parse ("1 or 2 & 3").Valid); + Assert (X.Value ("(1 and 2) | (3 and 4)").Synthetic_Image = + "(=1.0.0&=2.0.0)|(=3.0.0&=4.0.0)"); + Assert (X.Value ("(1 or 2) & (3 or 4)").Synthetic_Image = + "(=1.0.0|=2.0.0)&(=3.0.0|=4.0.0)"); + Assert (X.Value ("(1 & 2) or (3 & 4)").Synthetic_Image = + "(=1.0.0&=2.0.0)|(=3.0.0&=4.0.0)"); + Assert (X.Value ("(1 | 2) and (3 | 4)").Synthetic_Image = + "(=1.0.0|=2.0.0)&(=3.0.0|=4.0.0)"); + + -- Some more + Assert (X.Parse ("1 and not 2").Valid); + Assert (X.Value ("1 and not 2").Synthetic_Image = "=1.0.0&!(=2.0.0)"); +end Semver_Tests.Precedences; From 32809e9ba303f34345cbce77d71818465be3bc71 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Fri, 16 Jan 2026 18:19:25 +0100 Subject: [PATCH 3/5] Fix test runners --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 144f05a..50f2665 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: platform: - - os: macos-13 # x64 + - os: macos-15-intel # x64 id: x86_64-macos - os: macos-latest # arm64 @@ -29,8 +29,8 @@ jobs: id: x86_64-linux # Unavailable until setup-alire/alr-install are updated to support alr 2.1 -# - os: ubuntu-24.04-arm # new ARM runners -# id: aarch64-linux + - os: ubuntu-24.04-arm # new ARM runners + id: aarch64-linux - os: windows-latest # x64 id: x86_64-windows From 0e21464891b6aa2cc0dffd612c5d6dd1f29170e2 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Fri, 16 Jan 2026 18:58:27 +0100 Subject: [PATCH 4/5] Fix redundancy --- src/semantic_versioning-extended.adb | 49 +++++++++++++--------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/semantic_versioning-extended.adb b/src/semantic_versioning-extended.adb index eebedc3..1f2a986 100644 --- a/src/semantic_versioning-extended.adb +++ b/src/semantic_versioning-extended.adb @@ -437,29 +437,32 @@ package body Semantic_Versioning.Extended is end Next_Basic_VS; ---------------- - -- Next_Token -- + -- Is_Keyword -- ---------------- - function Next_Token (Skip_Whitespace : Boolean := True) return Tokens is + function Is_Keyword (Word : String) return Boolean is + Last : constant Integer := I + Word'Length - 1; + begin + if Last > Str'Last then + return False; + end if; - function Is_Keyword (Word : String) return Boolean is - Last : constant Integer := I + Word'Length - 1; - begin - if Last > Str'Last then - return False; - end if; + if ACH.To_Lower (Str (I .. Last)) /= Word then + return False; + end if; - if ACH.To_Lower (Str (I .. Last)) /= Word then - return False; - end if; + if Last < Str'Last and then ACH.Is_Alphanumeric (Str (Last + 1)) then + return False; + end if; - if Last < Str'Last and then ACH.Is_Alphanumeric (Str (Last + 1)) then - return False; - end if; + return True; + end Is_Keyword; - return True; - end Is_Keyword; + ---------------- + -- Next_Token -- + ---------------- + function Next_Token (Skip_Whitespace : Boolean := True) return Tokens is function Internal return Tokens is begin if I > Str'Last then @@ -514,27 +517,21 @@ package body Semantic_Versioning.Extended is begin case T is when Ampersand => - if I <= Str'Last and then ACH.To_Lower (Str (I .. Integer'Min (Str'Last, I + 2))) = "and" - and then (I + 3 > Str'Last or else not ACH.Is_Alphanumeric (Str (I + 3))) - then + if Is_Keyword ("and") then I := I + 3; else Match ('&'); end if; when Pipe => - if I <= Str'Last and then ACH.To_Lower (Str (I .. Integer'Min (Str'Last, I + 1))) = "or" - and then (I + 2 > Str'Last or else not ACH.Is_Alphanumeric (Str (I + 2))) - then + if Is_Keyword ("or") then I := I + 2; else Match ('|'); end if; when Negation => - if I <= Str'Last and then ACH.To_Lower (Str (I .. Integer'Min (Str'Last, I + 2))) = "not" - and then (I + 3 > Str'Last or else not ACH.Is_Alphanumeric (Str (I + 3))) - then + if Is_Keyword ("not") then I := I + 3; else Match ('!'); @@ -564,7 +561,7 @@ package body Semantic_Versioning.Extended is Next : Version_Set; begin Trace ("Prod EVS"); - case Next_Token is + case Next_Token is when Negation => Match_Token (Negation); declare From bc8c8ce263d591c7215dceb47b5b65bcdece904d Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Fri, 16 Jan 2026 19:01:40 +0100 Subject: [PATCH 5/5] More readable logic --- src/semantic_versioning-extended.adb | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/semantic_versioning-extended.adb b/src/semantic_versioning-extended.adb index 1f2a986..ab455b2 100644 --- a/src/semantic_versioning-extended.adb +++ b/src/semantic_versioning-extended.adb @@ -410,6 +410,20 @@ package body Semantic_Versioning.Extended is end if; end Match; + procedure Match (S : String) is + begin + if I > Str'Last then + Error ("Incomplete expression when expecting: " & S); + elsif I + S'Length - 1 > Str'Last then + Error ("Incomplete expression when expecting: " & S); + elsif ACH.To_Lower (Str (I .. I + S'Length - 1)) /= ACH.To_Lower (S) then + Error ("Got a '" & Str (I) & "' when expecting: " & S); + else + Trace ("Matching " & S & " at pos" & I'Img); + I := I + S'Length; + end if; + end Match; + ------------------- -- Next_Basic_VS -- ------------------- @@ -518,21 +532,21 @@ package body Semantic_Versioning.Extended is case T is when Ampersand => if Is_Keyword ("and") then - I := I + 3; + Match ("and"); else Match ('&'); end if; when Pipe => if Is_Keyword ("or") then - I := I + 2; + Match ("or"); else Match ('|'); end if; when Negation => if Is_Keyword ("not") then - I := I + 3; + Match ("not"); else Match ('!'); end if;