It seems that in order to compare a variable a and a constant c, all we need is to compare them bitwise. Since c is a constant, we do not need to introduce any new witness variables to get its unpacking. We can just directly use the bits of c as constants without specifying anywhere that those bits come from the c constant.
We'll use the convention that the most significant bit goes first and instead of R1CS we'll use the constraint system of Sonic/Bulletproofs.
Everything is summarized in the following truth table:
ai ci r res
0 0 r r
1 0 r 1
0 1 r 0
1 1 r r
Here ai and ci are particular bits of a and c and r is the rest of the computation. This reads as "if ai equals ci (two bits are equal, the 1 and 4 cases), then look at lower bits, otherwise if ai is 1 while ci is 0, then return 1, otherwise return 0". I.e. we compare the bits of the variable and the constant until there's a mismatch, in which case we return the result depending on whether it's the variable's bit is 1 or the constant's one.
Since ci is known statically, we can break this truth table down into two cases: ci = 0 and ci = 1. Which we can compile as
ci = 0: res = ai OR r
ci = 1: res = ai AND r
If we ignore the problem of compiling consecutive ORs and ANDs efficiently, then this becomes:
ci = 0: res = ai + r - ai * r
ci = 1: res = ai * r
Let us now look at some examples. Consider a variable v that fits into 3 bits, which we compile as:
_3 = _3 * _3
_2 = _2 * _2
_1 = _1 * _1
0 = - v + _1 + 2 * _2 + 4 * _3
where _3 is the most significant bit, _1 is the least significant bit and _2 is in the middle. The first three equations ensure that _1, _2 and _3 are indeed bits (i.e. can either be 0 or 1). The last equation ensures that those bits together represent the v variable in binary form.
Now let's see what constraints we add to the above set if we compare the v variable and some constant c (v > c) and compile that.
For c = 7 we get
res = 0
I.e. there is no three-bit number that is greater than 7.
For c = 6 we get
_4 = _2 * _1
res = _3 * _4
The only three-bits number that is greater than 6 is 7, which is represented as 111 in binary form. I.e. all bits must be equal to 1. And this is exactly what the equations above say: if all bits of a three-bits number are 1, then the result is 1, i.e. the number is bigger than 6, otherwise the result is 0.
For c = 5 we get
res = _3 * _2
There are two three-bit numbers that are greater than 5: 6 (110) and 7 (111). I.e. a three-bit number is bigger than 5 whenever its two most significant bits are both 1 and this is exactly what the equation above says.
For c = 4 we get
_4 = _2 * _1
_5 = _1 + _2 - _4
res = _3 * _5
There are three three-bit numbers that are greater than 4: 5 (101), 6 (110) and 7 (111). I.e. the most significant bit must be 1 (the _3 part of the last equation) and (the * part of the last equation) the disjunction of the other two bits must be 1 as well (the _5 part pf the last equation). That disjunction is _2 or _1 and we compile this down to the first two constraints as per usual.
For c = 3 we get
res = _3
There are four three-bit numbers that are greater than 3: 4 (100), 5 (101), 6 (110) and 7 (111) and all of them start with 1, i.e. the most significant bit of v must be equal to 1 in order for v > 3 to hold.
This approach can be easily extended to handle the variable < constant as well as variable < variable cases.