@@ -269,3 +269,113 @@ impl Propagate for BoolNot {
269269 . chain ( core:: iter:: once ( self . operand ) )
270270 }
271271}
272+
273+ /// Boolean XOR constraint: `result = a XOR b`.
274+ /// This constraint enforces that the result variable equals the logical XOR of two boolean operands.
275+ /// XOR is true (1) when exactly one operand is true (non-zero).
276+ /// Variables are treated as boolean: 0 = false, non-zero = true.
277+ #[ derive( Clone , Debug ) ]
278+ #[ doc( hidden) ]
279+ pub struct BoolXor {
280+ x : VarId ,
281+ y : VarId ,
282+ result : VarId ,
283+ }
284+
285+ impl BoolXor {
286+ pub fn new ( x : VarId , y : VarId , result : VarId ) -> Self {
287+ Self { x, y, result }
288+ }
289+ }
290+
291+ impl Prune for BoolXor {
292+ fn prune ( & self , ctx : & mut Context ) -> Option < ( ) > {
293+ // XOR truth table (treating as boolean: 0 = false, 1 = true):
294+ // x | y | x XOR y
295+ // --+---+--------
296+ // 0 | 0 | 0
297+ // 0 | 1 | 1
298+ // 1 | 0 | 1
299+ // 1 | 1 | 0
300+ //
301+ // So: result = 1 iff (x = 0 and y = 1) or (x = 1 and y = 0)
302+ // result = 0 iff (x = 0 and y = 0) or (x = 1 and y = 1)
303+
304+ let x_min = self . x . min ( ctx) ;
305+ let x_max = self . x . max ( ctx) ;
306+ let y_min = self . y . min ( ctx) ;
307+ let y_max = self . y . max ( ctx) ;
308+ let result_min = self . result . min ( ctx) ;
309+ let result_max = self . result . max ( ctx) ;
310+
311+ // Case 1: result is fixed to 1 (must be true)
312+ if result_min >= Val :: ValI ( 1 ) {
313+ // x XOR y must be true
314+ // This means: (x=0 AND y=1) OR (x=1 AND y=0)
315+
316+ // If x is definitely 0, then y must be definitely 1
317+ if x_max <= Val :: ValI ( 0 ) {
318+ let _min = self . y . try_set_min ( Val :: ValI ( 1 ) , ctx) ?;
319+ }
320+ // If x is definitely 1 (>= 1), then y must be definitely 0
321+ if x_min >= Val :: ValI ( 1 ) {
322+ let _max = self . y . try_set_max ( Val :: ValI ( 0 ) , ctx) ?;
323+ }
324+ // Similarly for y
325+ if y_max <= Val :: ValI ( 0 ) {
326+ let _min = self . x . try_set_min ( Val :: ValI ( 1 ) , ctx) ?;
327+ }
328+ if y_min >= Val :: ValI ( 1 ) {
329+ let _max = self . x . try_set_max ( Val :: ValI ( 0 ) , ctx) ?;
330+ }
331+ }
332+
333+ // Case 2: result is fixed to 0 (must be false)
334+ if result_max <= Val :: ValI ( 0 ) {
335+ // x XOR y must be false
336+ // This means: (x=0 AND y=0) OR (x=1 AND y=1)
337+
338+ // If x is definitely 0, then y must be definitely 0
339+ if x_max <= Val :: ValI ( 0 ) {
340+ let _max = self . y . try_set_max ( Val :: ValI ( 0 ) , ctx) ?;
341+ }
342+ // If x is definitely 1, then y must be definitely 1
343+ if x_min >= Val :: ValI ( 1 ) {
344+ let _min = self . y . try_set_min ( Val :: ValI ( 1 ) , ctx) ?;
345+ }
346+ // Similarly for y
347+ if y_max <= Val :: ValI ( 0 ) {
348+ let _max = self . x . try_set_max ( Val :: ValI ( 0 ) , ctx) ?;
349+ }
350+ if y_min >= Val :: ValI ( 1 ) {
351+ let _min = self . x . try_set_min ( Val :: ValI ( 1 ) , ctx) ?;
352+ }
353+ }
354+
355+ // Case 3: Propagate from fixed x and y to result
356+ // If both x and y are fixed (or determinable)
357+ if x_min == x_max && y_min == y_max {
358+ let x_bool = x_min >= Val :: ValI ( 1 ) ;
359+ let y_bool = y_min >= Val :: ValI ( 1 ) ;
360+ let xor_result = x_bool != y_bool; // XOR operation
361+
362+ if xor_result {
363+ let _min = self . result . try_set_min ( Val :: ValI ( 1 ) , ctx) ?;
364+ let _max = self . result . try_set_max ( Val :: ValI ( 1 ) , ctx) ?;
365+ } else {
366+ let _min = self . result . try_set_min ( Val :: ValI ( 0 ) , ctx) ?;
367+ let _max = self . result . try_set_max ( Val :: ValI ( 0 ) , ctx) ?;
368+ }
369+ }
370+
371+ Some ( ( ) )
372+ }
373+ }
374+
375+ impl Propagate for BoolXor {
376+ fn list_trigger_vars ( & self ) -> impl Iterator < Item = VarId > {
377+ core:: iter:: once ( self . result )
378+ . chain ( core:: iter:: once ( self . x ) )
379+ . chain ( core:: iter:: once ( self . y ) )
380+ }
381+ }
0 commit comments