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 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..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 -- ------------------- @@ -436,6 +450,28 @@ package body Semantic_Versioning.Extended is end return; end Next_Basic_VS; + ---------------- + -- Is_Keyword -- + ---------------- + + 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; + ---------------- -- Next_Token -- ---------------- @@ -457,6 +493,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 +523,43 @@ 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 Is_Keyword ("and") then + Match ("and"); + else + Match ('&'); + end if; + + when Pipe => + if Is_Keyword ("or") then + Match ("or"); + else + Match ('|'); + end if; + + when Negation => + if Is_Keyword ("not") then + Match ("not"); + 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; @@ -496,7 +577,7 @@ package body Semantic_Versioning.Extended is Trace ("Prod EVS"); 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 +624,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,10 +646,10 @@ 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"); + Error ("Cannot mix 'and' and 'or' operators, use parentheses"); end if; end if; end Check_Mismatch; @@ -589,12 +670,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-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; 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;