Add two integers without ever writing + or -. The only tools you get are bitwise operators — the same XOR, AND, and shift gates a CPU's hardware adder uses to add numbers. Your job is to rebuild that adder in JavaScript: XOR produces the digit-by-digit sum, AND followed by a left shift produces the carry, and you repeat until there's no carry left.
// a, b: 32-bit signed integers (negatives allowed)
// returns: the integer sum a + b, using only bitwise operators
function sumWithoutAddition(a: number, b: number): number;
sumWithoutAddition(2, 3); // → 5
sumWithoutAddition(-2, 3); // → 1
sumWithoutAddition(-5, -3); // → -8
sumWithoutAddition(0, 0); // → 0
sumWithoutAddition(100, 250); // → 350
sumWithoutAddition(-10, -20); // → -30
+ or - — not in expressions, not in ++/--/+=. Only bitwise operators (^, &, |, <<, >>, >>>, ~) are allowed.Math helpers, build strings, or loop with a counter you increment with +. The arithmetic itself must be bitwise.-2, -5, and mixed-sign pairs must work. The same bit trick handles them because JS stores negatives in two's complement.number equal to a + b, not a string or array.You'll rebuild a hardware adder in software: add two integers using only the bitwise gates a CPU uses, never the + operator.
Adding 2 + 3 feels atomic, but a processor doesn't have a magic "add" wire. Underneath, an arithmetic logic unit builds addition out of simpler logic gates: XOR, AND, and a shift. When you add two numbers by hand in binary, you do the same thing — add each column, and when a column overflows, carry a 1 to the next column left. We're going to express exactly that, but with no + and no - anywhere. The only tools allowed are JavaScript's bitwise operators: ^ (XOR), & (AND), | (OR), and the shifts.
The whole trick rests on splitting addition into two independent halves: the part that doesn't carry, and the carry itself. Each half is one bitwise operation.
Add two binary numbers column by column. In any single column you have two input bits, and the result is "their sum" plus "maybe a carry into the next column." Look at what each operator gives you:
a ^ b (XOR) is 1 exactly when the two bits differ. That's the column's sum digit if you ignore carrying: 0+0=0, 0+1=1, 1+0=1, and 1+1=0 (sum digit 0, with a carry we're deliberately dropping for now). So XOR is "addition without carry."a & b (AND) is 1 exactly when both bits are 1 — which is precisely the case 1+1 that overflows and needs to carry. Shifting that left by one (<< 1) moves the carry into the next column, exactly where a carry belongs.So a ^ b is the sum with carries dropped, and (a & b) << 1 is those dropped carries, relocated to the columns they belong in. Add those two together and you have the real sum — except we're not allowed to add. The way out: feed them back in and repeat. Adding the carry can itself produce new carries, but each round pushes the remaining carries further left, and eventually there are none.
The instinct when "+" is banned is to lean on ++ and count your way there. Start from a and step up b times:
function sumByCounting(a, b) {
let result = a;
for (let i = 0; i < b; i++) result++; // ++ is just +1 in disguise
return result;
}
This has two fatal problems. First, ++ is still addition — result++ means result = result + 1, so it violates the "no +" rule outright; we've only hidden the operator. Second, even if you allowed it, it's O(b): sumByCounting(1, 2000000000) loops two billion times, and sumByCounting(5, -3) loops zero times (the condition 0 < -3 is false immediately) and wrongly returns 5. Counting can't go backwards, so negatives break it. We need arithmetic that works on the bits directly, in a number of steps that depends on bit width — at most 32 — not on the magnitude of the inputs.
Replace counting with the XOR-and-carry split, looped until no carry remains:
function sumWithoutAddition(a, b) {
// Loop while there is still a carry to fold in.
while (b !== 0) {
const carry = (a & b) << 1; // columns where BOTH bits are 1, shifted into the next column
a = a ^ b; // the sum with every carry dropped
b = carry; // next round: add the carry back into a
}
return a;
}
module.exports = { sumWithoutAddition };
Read it as: a always holds "the answer so far, ignoring carries," and b holds "the carries still waiting to be added." Each iteration recomputes both. We must capture carry before we overwrite a, because the carry is computed from the old a — that's why carry is a separate const evaluated first. When b (the carry) hits 0, there is nothing left to fold in, so a is the final sum.
Why does this terminate? Every iteration the carry shifts one position further left (<< 1). After at most 32 shifts a 32-bit carry has fallen off the top of the integer and become 0 — JavaScript's bitwise operators work on signed 32-bit integers, so bits beyond position 31 simply vanish. That same 32-bit behavior is what makes negatives work for free, with no special-casing.
Trace sumWithoutAddition(5, 3). In 4-bit binary, 5 = 0101 and 3 = 0011.
Iteration 1. b = 0011, not zero, so we enter the loop.
a & b = 0101 & 0011 = 0001 (only the rightmost column has both bits set). Shift left: carry = 0010 (decimal 2).a = a ^ b = 0101 ^ 0011 = 0110 (decimal 6) — the sum ignoring carries.b = carry = 0010 (decimal 2).Iteration 2. b = 0010, not zero, loop again.
a & b = 0110 & 0010 = 0010. Shift left: carry = 0100 (decimal 4).a = a ^ b = 0110 ^ 0010 = 0100 (decimal 4).b = carry = 0100 (decimal 4).Iteration 3. b = 0100, not zero, loop again.
a & b = 0100 & 0100 = 0100. Shift left: carry = 1000 (decimal 8).a = a ^ b = 0100 ^ 0100 = 0000 (decimal 0).b = carry = 1000 (decimal 8).Iteration 4. b = 1000, not zero, loop again.
a & b = 0000 & 1000 = 0000. Shift left: carry = 0000.a = a ^ b = 0000 ^ 1000 = 1000 (decimal 8).b = carry = 0000.Iteration 5. b = 0 → loop exits. Return a = 1000 = 8. And 5 + 3 = 8. Notice the carry marched left each round — 0010 → 0100 → 1000 → 0 — exactly as promised, which is why the loop can't run forever.
For negatives the same loop runs. sumWithoutAddition(-2, 3): JS holds -2 as the 32-bit pattern …11110 and 3 as …00011. The carries propagate up through the 32 bits, the final carry runs off bit 31 and disappears, b becomes 0, and a is 1. No branch for the sign — two's complement makes subtraction and addition the same operation.
5 ^ 3 = 6, not 8. If you return a ^ b directly you're correct only when no two set bits line up (no column has 1+1). The carry is the missing piece — you can't skip it.(a & b) << 1, and the shift is mandatory. a & b marks the columns that overflow, but a carry belongs in the next column left. Forget the << 1 and you add the carry back into the same column it came from — the loop converges to a wrong number or spins.a. Both carry and the new a are computed from the old a. If you write a = a ^ b first and then (a & b) << 1, you've used the new a for the carry and the math is wrong. Compute carry first, or swap with a temp.b !== 0, the carry — not on a fixed count. The number of iterations depends on how far the carries have to propagate (up to 32), not on the size of a or b. A for (i = 0; i < b; i++) style loop reintroduces both the + ban violation and the negatives bug.b never shrinks — while (b !== 0) never exits. The invariant that saves you is that a correct carry always moves left and eventually falls off bit 31.| 0 if you ever need to force coercion. ^, &, and << all coerce their operands to signed 32-bit integers and return a signed 32-bit result, which is why two's-complement negatives and the run-off-the-top carry behave correctly here. If you store an intermediate that you want pinned to 32 bits explicitly, x | 0 coerces it without changing its value. Numbers outside the 32-bit range are out of scope for this problem (see Going further).a - b is a + (-b), and in two's complement -b is ~b + 1 — flip every bit and add one. You can't write + 1, but you already have sumWithoutAddition, so subtract(a, b) = sumWithoutAddition(a, sumWithoutAddition(~b, 1)). The adder you just wrote is the whole subtractor too.i in b, add a << i into an accumulator using sumWithoutAddition. That's O(bits) adds — the grade-school long-multiplication algorithm expressed in gates.BigInt supports ^, &, and << with no width limit, so the identical XOR-and-carry loop on BigInt operands adds integers of any size — though negative BigInts aren't two's complement, so the sign handling would need rethinking.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Add two integers without ever writing + or -. The only tools you get are bitwise operators — the same XOR, AND, and shift gates a CPU's hardware adder uses to add numbers. Your job is to rebuild that adder in JavaScript: XOR produces the digit-by-digit sum, AND followed by a left shift produces the carry, and you repeat until there's no carry left.
// a, b: 32-bit signed integers (negatives allowed)
// returns: the integer sum a + b, using only bitwise operators
function sumWithoutAddition(a: number, b: number): number;
sumWithoutAddition(2, 3); // → 5
sumWithoutAddition(-2, 3); // → 1
sumWithoutAddition(-5, -3); // → -8
sumWithoutAddition(0, 0); // → 0
sumWithoutAddition(100, 250); // → 350
sumWithoutAddition(-10, -20); // → -30
+ or - — not in expressions, not in ++/--/+=. Only bitwise operators (^, &, |, <<, >>, >>>, ~) are allowed.Math helpers, build strings, or loop with a counter you increment with +. The arithmetic itself must be bitwise.-2, -5, and mixed-sign pairs must work. The same bit trick handles them because JS stores negatives in two's complement.number equal to a + b, not a string or array.You'll rebuild a hardware adder in software: add two integers using only the bitwise gates a CPU uses, never the + operator.
Adding 2 + 3 feels atomic, but a processor doesn't have a magic "add" wire. Underneath, an arithmetic logic unit builds addition out of simpler logic gates: XOR, AND, and a shift. When you add two numbers by hand in binary, you do the same thing — add each column, and when a column overflows, carry a 1 to the next column left. We're going to express exactly that, but with no + and no - anywhere. The only tools allowed are JavaScript's bitwise operators: ^ (XOR), & (AND), | (OR), and the shifts.
The whole trick rests on splitting addition into two independent halves: the part that doesn't carry, and the carry itself. Each half is one bitwise operation.
Add two binary numbers column by column. In any single column you have two input bits, and the result is "their sum" plus "maybe a carry into the next column." Look at what each operator gives you:
a ^ b (XOR) is 1 exactly when the two bits differ. That's the column's sum digit if you ignore carrying: 0+0=0, 0+1=1, 1+0=1, and 1+1=0 (sum digit 0, with a carry we're deliberately dropping for now). So XOR is "addition without carry."a & b (AND) is 1 exactly when both bits are 1 — which is precisely the case 1+1 that overflows and needs to carry. Shifting that left by one (<< 1) moves the carry into the next column, exactly where a carry belongs.So a ^ b is the sum with carries dropped, and (a & b) << 1 is those dropped carries, relocated to the columns they belong in. Add those two together and you have the real sum — except we're not allowed to add. The way out: feed them back in and repeat. Adding the carry can itself produce new carries, but each round pushes the remaining carries further left, and eventually there are none.
The instinct when "+" is banned is to lean on ++ and count your way there. Start from a and step up b times:
function sumByCounting(a, b) {
let result = a;
for (let i = 0; i < b; i++) result++; // ++ is just +1 in disguise
return result;
}
This has two fatal problems. First, ++ is still addition — result++ means result = result + 1, so it violates the "no +" rule outright; we've only hidden the operator. Second, even if you allowed it, it's O(b): sumByCounting(1, 2000000000) loops two billion times, and sumByCounting(5, -3) loops zero times (the condition 0 < -3 is false immediately) and wrongly returns 5. Counting can't go backwards, so negatives break it. We need arithmetic that works on the bits directly, in a number of steps that depends on bit width — at most 32 — not on the magnitude of the inputs.
Replace counting with the XOR-and-carry split, looped until no carry remains:
function sumWithoutAddition(a, b) {
// Loop while there is still a carry to fold in.
while (b !== 0) {
const carry = (a & b) << 1; // columns where BOTH bits are 1, shifted into the next column
a = a ^ b; // the sum with every carry dropped
b = carry; // next round: add the carry back into a
}
return a;
}
module.exports = { sumWithoutAddition };
Read it as: a always holds "the answer so far, ignoring carries," and b holds "the carries still waiting to be added." Each iteration recomputes both. We must capture carry before we overwrite a, because the carry is computed from the old a — that's why carry is a separate const evaluated first. When b (the carry) hits 0, there is nothing left to fold in, so a is the final sum.
Why does this terminate? Every iteration the carry shifts one position further left (<< 1). After at most 32 shifts a 32-bit carry has fallen off the top of the integer and become 0 — JavaScript's bitwise operators work on signed 32-bit integers, so bits beyond position 31 simply vanish. That same 32-bit behavior is what makes negatives work for free, with no special-casing.
Trace sumWithoutAddition(5, 3). In 4-bit binary, 5 = 0101 and 3 = 0011.
Iteration 1. b = 0011, not zero, so we enter the loop.
a & b = 0101 & 0011 = 0001 (only the rightmost column has both bits set). Shift left: carry = 0010 (decimal 2).a = a ^ b = 0101 ^ 0011 = 0110 (decimal 6) — the sum ignoring carries.b = carry = 0010 (decimal 2).Iteration 2. b = 0010, not zero, loop again.
a & b = 0110 & 0010 = 0010. Shift left: carry = 0100 (decimal 4).a = a ^ b = 0110 ^ 0010 = 0100 (decimal 4).b = carry = 0100 (decimal 4).Iteration 3. b = 0100, not zero, loop again.
a & b = 0100 & 0100 = 0100. Shift left: carry = 1000 (decimal 8).a = a ^ b = 0100 ^ 0100 = 0000 (decimal 0).b = carry = 1000 (decimal 8).Iteration 4. b = 1000, not zero, loop again.
a & b = 0000 & 1000 = 0000. Shift left: carry = 0000.a = a ^ b = 0000 ^ 1000 = 1000 (decimal 8).b = carry = 0000.Iteration 5. b = 0 → loop exits. Return a = 1000 = 8. And 5 + 3 = 8. Notice the carry marched left each round — 0010 → 0100 → 1000 → 0 — exactly as promised, which is why the loop can't run forever.
For negatives the same loop runs. sumWithoutAddition(-2, 3): JS holds -2 as the 32-bit pattern …11110 and 3 as …00011. The carries propagate up through the 32 bits, the final carry runs off bit 31 and disappears, b becomes 0, and a is 1. No branch for the sign — two's complement makes subtraction and addition the same operation.
5 ^ 3 = 6, not 8. If you return a ^ b directly you're correct only when no two set bits line up (no column has 1+1). The carry is the missing piece — you can't skip it.(a & b) << 1, and the shift is mandatory. a & b marks the columns that overflow, but a carry belongs in the next column left. Forget the << 1 and you add the carry back into the same column it came from — the loop converges to a wrong number or spins.a. Both carry and the new a are computed from the old a. If you write a = a ^ b first and then (a & b) << 1, you've used the new a for the carry and the math is wrong. Compute carry first, or swap with a temp.b !== 0, the carry — not on a fixed count. The number of iterations depends on how far the carries have to propagate (up to 32), not on the size of a or b. A for (i = 0; i < b; i++) style loop reintroduces both the + ban violation and the negatives bug.b never shrinks — while (b !== 0) never exits. The invariant that saves you is that a correct carry always moves left and eventually falls off bit 31.| 0 if you ever need to force coercion. ^, &, and << all coerce their operands to signed 32-bit integers and return a signed 32-bit result, which is why two's-complement negatives and the run-off-the-top carry behave correctly here. If you store an intermediate that you want pinned to 32 bits explicitly, x | 0 coerces it without changing its value. Numbers outside the 32-bit range are out of scope for this problem (see Going further).a - b is a + (-b), and in two's complement -b is ~b + 1 — flip every bit and add one. You can't write + 1, but you already have sumWithoutAddition, so subtract(a, b) = sumWithoutAddition(a, sumWithoutAddition(~b, 1)). The adder you just wrote is the whole subtractor too.i in b, add a << i into an accumulator using sumWithoutAddition. That's O(bits) adds — the grade-school long-multiplication algorithm expressed in gates.BigInt supports ^, &, and << with no width limit, so the identical XOR-and-carry loop on BigInt operands adds integers of any size — though negative BigInts aren't two's complement, so the sign handling would need rethinking.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.