As others have noted, your original example
test(A, B, C, D) :- cond(A), cond(B); cond(C), cond(D).
completely correct (it is assumed that the analysis is the way you thought it up). Have you tried?
Primer
Logical and
foo :- a , b .
Logical OR
foo :- a ; b .
Combined
foo :- a , b ; c , d .
The above analysis:
foo :- ( a , b ) ; ( c , d ) .
Use parentheses to indicate another desired binding:
foo :- a , ( b ; c ) , d .
Better yet, avoid the operator ; OR and break alternatives into separate sentences. Consistency is much easier for people to understand than branching tree structures. Breaking the rotation of several articles simplifies testing / debugging and improves understanding. Therefore prefer
foo :- a , b . foo :- c , d .
over
foo :- a , b ; c , d .
and prefer
foo :- a , bar , d . bar :- b . bar :- c .
over
foo :- a , ( b ; c ) , d .
Perhaps the most important thing is to break it down into several articles, as this simplifies subsequent maintenance. With a structure like:
foo :- a , b ; c , d .
what do you do when you add another case? How about when it expands to 50 alternatives?
Each additional alternative increases the number of code paths through a sentence, making testing and understanding difficult. In order to get full coverage of the code during testing, it is necessary to individually inspect many alternative paths.
With equivalent structure
foo :- a , b . foo :- c , d .
Adding alternatives is simply a matter of adding an additional offer or offers, each of which can be tested in isolation.
A professional programmer writes first for people who within a few years after that should understand, modify and correct this code ( hint: that this person can be himself).