diff --git a/GUIDE.md b/GUIDE.md index f151dee..fcbbc9e 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -195,6 +195,10 @@ CALL ### Arithmetic - `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result) +### Bitwise +- `BIT_AND`, `BIT_OR`, `BIT_XOR` - Bitwise logical ops (pop 2, push result) +- `BIT_SHL`, `BIT_SHR`, `BIT_USHR` - Bitwise shift ops (pop 2, push result) + ### Comparison - `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean @@ -385,6 +389,51 @@ SUB ; Result based on operand order - String concatenation with specific order - Preparing arguments for functions that care about position +### Bitwise Operations +All bitwise operations work with 32-bit signed integers: + +``` +; Bitwise AND (masking) +PUSH 5 +PUSH 3 +BIT_AND ; → 1 (0101 & 0011 = 0001) + +; Bitwise OR (combining flags) +PUSH 5 +PUSH 3 +BIT_OR ; → 7 (0101 | 0011 = 0111) + +; Bitwise XOR (toggling bits) +PUSH 5 +PUSH 3 +BIT_XOR ; → 6 (0101 ^ 0011 = 0110) + +; Left shift (multiply by power of 2) +PUSH 5 +PUSH 2 +BIT_SHL ; → 20 (5 << 2 = 5 * 4) + +; Arithmetic right shift (divide by power of 2, preserves sign) +PUSH 20 +PUSH 2 +BIT_SHR ; → 5 (20 >> 2 = 20 / 4) + +PUSH -20 +PUSH 2 +BIT_SHR ; → -5 (sign preserved) + +; Logical right shift (zero-fill) +PUSH -1 +PUSH 1 +BIT_USHR ; → 2147483647 (unsigned shift) +``` + +**Common Use Cases**: +- Flags and bit masks: `flags band MASK` to test, `flags bor FLAG` to set +- Fast multiplication/division by powers of 2 +- Color manipulation: extract RGB components +- Low-level bit manipulation for protocols or file formats + ### Try-Catch ``` PUSH_TRY .catch @@ -622,6 +671,8 @@ Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty a **Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers. +**Bitwise ops** (BIT_AND, BIT_OR, BIT_XOR, BIT_SHL, BIT_SHR, BIT_USHR) coerce both operands to 32-bit signed integers. + **Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers. **Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts. diff --git a/SPEC.md b/SPEC.md index dd6e71e..d639c71 100644 --- a/SPEC.md +++ b/SPEC.md @@ -220,6 +220,62 @@ Performs different operations depending on operand types: #### MOD **Stack**: [a, b] → [a % b] +### Bitwise Operations + +All bitwise operations coerce operands to 32-bit signed integers, perform the operation, and push the result as a number. + +#### BIT_AND +**Operand**: None +**Stack**: [a, b] → [a & b] + +Performs bitwise AND operation. Both operands are coerced to 32-bit signed integers. + +**Example**: `5 & 3` → `1` (binary: `0101 & 0011` → `0001`) + +#### BIT_OR +**Operand**: None +**Stack**: [a, b] → [a | b] + +Performs bitwise OR operation. Both operands are coerced to 32-bit signed integers. + +**Example**: `5 | 3` → `7` (binary: `0101 | 0011` → `0111`) + +#### BIT_XOR +**Operand**: None +**Stack**: [a, b] → [a ^ b] + +Performs bitwise XOR (exclusive OR) operation. Both operands are coerced to 32-bit signed integers. + +**Example**: `5 ^ 3` → `6` (binary: `0101 ^ 0011` → `0110`) + +#### BIT_SHL +**Operand**: None +**Stack**: [a, b] → [a << b] + +Performs left shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). + +**Example**: `5 << 2` → `20` (binary: `0101` shifted left 2 positions → `10100`) + +#### BIT_SHR +**Operand**: None +**Stack**: [a, b] → [a >> b] + +Performs sign-preserving right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). The sign bit is preserved (arithmetic shift). + +**Example**: +- `20 >> 2` → `5` (binary: `10100` shifted right 2 positions → `0101`) +- `-20 >> 2` → `-5` (sign bit preserved) + +#### BIT_USHR +**Operand**: None +**Stack**: [a, b] → [a >>> b] + +Performs zero-fill right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). Zeros are shifted in from the left (logical shift). + +**Example**: +- `-1 >>> 1` → `2147483647` (all bits shift right, zero fills from left) +- `-8 >>> 1` → `2147483644` + ### Comparison Operations All comparison operations pop two values, compare, push boolean result. diff --git a/src/bytecode.ts b/src/bytecode.ts index be9f475..9175e80 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -32,6 +32,9 @@ type InstructionTuple = // Arithmetic | ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"] + // Bitwise + | ["BIT_AND"] | ["BIT_OR"] | ["BIT_XOR"] | ["BIT_SHL"] | ["BIT_SHR"] | ["BIT_USHR"] + // Comparison | ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"] diff --git a/src/opcode.ts b/src/opcode.ts index d66ebd5..3a7df0f 100644 --- a/src/opcode.ts +++ b/src/opcode.ts @@ -17,6 +17,14 @@ export enum OpCode { DIV, // operand: none | stack: [a, b] → [a / b] MOD, // operand: none | stack: [a, b] → [a % b] + // bitwise operations (coerce to 32-bit integers, pop 2, push result) + BIT_AND, // operand: none | stack: [a, b] → [a & b] + BIT_OR, // operand: none | stack: [a, b] → [a | b] + BIT_XOR, // operand: none | stack: [a, b] → [a ^ b] + BIT_SHL, // operand: none | stack: [a, b] → [a << b] + BIT_SHR, // operand: none | stack: [a, b] → [a >> b] (sign-preserving) + BIT_USHR, // operand: none | stack: [a, b] → [a >>> b] (zero-fill) + // comparison (pop 2, push boolean) EQ, // operand: none | stack: [a, b] → [a == b] (deep equality) NEQ, // operand: none | stack: [a, b] → [a != b] diff --git a/src/validator.ts b/src/validator.ts index 69770ae..1d7b6a8 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -55,6 +55,12 @@ const OPCODES_WITHOUT_OPERANDS = new Set([ OpCode.MUL, OpCode.DIV, OpCode.MOD, + OpCode.BIT_AND, + OpCode.BIT_OR, + OpCode.BIT_XOR, + OpCode.BIT_SHL, + OpCode.BIT_SHR, + OpCode.BIT_USHR, OpCode.EQ, OpCode.NEQ, OpCode.LT, diff --git a/src/vm.ts b/src/vm.ts index f342a6b..f0fc1e1 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -233,6 +233,31 @@ export class VM { this.stack.push({ type: 'boolean', value: !isTrue(val) }) break + // Bitwise operations + case OpCode.BIT_AND: + this.binaryOp((a, b) => (toNumber(a) | 0) & (toNumber(b) | 0)) + break + + case OpCode.BIT_OR: + this.binaryOp((a, b) => (toNumber(a) | 0) | (toNumber(b) | 0)) + break + + case OpCode.BIT_XOR: + this.binaryOp((a, b) => (toNumber(a) | 0) ^ (toNumber(b) | 0)) + break + + case OpCode.BIT_SHL: + this.binaryOp((a, b) => (toNumber(a) | 0) << (toNumber(b) | 0)) + break + + case OpCode.BIT_SHR: + this.binaryOp((a, b) => (toNumber(a) | 0) >> (toNumber(b) | 0)) + break + + case OpCode.BIT_USHR: + this.binaryOp((a, b) => (toNumber(a) | 0) >>> (toNumber(b) | 0)) + break + case OpCode.HALT: this.stopped = true break diff --git a/tests/bitwise.test.ts b/tests/bitwise.test.ts new file mode 100644 index 0000000..66f80ec --- /dev/null +++ b/tests/bitwise.test.ts @@ -0,0 +1,107 @@ +import { expect, describe, test } from 'bun:test' +import { toBytecode, run } from '#reef' + +describe('bitwise operations', () => { + test('BIT_AND', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 3], ["BIT_AND"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 1 }) + }) + + test('BIT_AND with zero', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 0], ["BIT_AND"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 0 }) + }) + + test('BIT_OR', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 3], ["BIT_OR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 7 }) + }) + + test('BIT_OR with zero', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 0], ["BIT_OR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + }) + + test('BIT_XOR', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 3], ["BIT_XOR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 6 }) + }) + + test('BIT_XOR with itself returns zero', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 5], ["BIT_XOR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 0 }) + }) + + test('BIT_SHL', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 2], ["BIT_SHL"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 20 }) + }) + + test('BIT_SHL by zero', async () => { + const bytecode = toBytecode([ + ["PUSH", 5], ["PUSH", 0], ["BIT_SHL"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + }) + + test('BIT_SHR', async () => { + const bytecode = toBytecode([ + ["PUSH", 20], ["PUSH", 2], ["BIT_SHR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + }) + + test('BIT_SHR preserves sign for negative numbers', async () => { + const bytecode = toBytecode([ + ["PUSH", -20], ["PUSH", 2], ["BIT_SHR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: -5 }) + }) + + test('BIT_USHR', async () => { + const bytecode = toBytecode([ + ["PUSH", -1], ["PUSH", 1], ["BIT_USHR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 2147483647 }) + }) + + test('BIT_USHR does not preserve sign', async () => { + const bytecode = toBytecode([ + ["PUSH", -8], ["PUSH", 1], ["BIT_USHR"], ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 2147483644 }) + }) + + test('compound bitwise operations', async () => { + const bytecode = toBytecode([ + // (5 & 3) | (8 ^ 12) + ["PUSH", 5], ["PUSH", 3], ["BIT_AND"], // stack: [1] + ["PUSH", 8], ["PUSH", 12], ["BIT_XOR"], // stack: [1, 4] + ["BIT_OR"], // stack: [5] + ["HALT"] + ]) + expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + }) + + test('shift with large shift amounts', async () => { + const bytecode = toBytecode([ + ["PUSH", 1], ["PUSH", 31], ["BIT_SHL"], ["HALT"] + ]) + // 1 << 31 = -2147483648 (most significant bit set) + expect(await run(bytecode)).toEqual({ type: 'number', value: -2147483648 }) + }) +})