add bitwise operators

This commit is contained in:
Chris Wanstrath 2025-11-07 22:51:28 -08:00
parent bffb83a528
commit 15884ac239
7 changed files with 256 additions and 0 deletions

View File

@ -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.

56
SPEC.md
View File

@ -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.

View File

@ -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"]

View File

@ -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]

View File

@ -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,

View File

@ -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

107
tests/bitwise.test.ts Normal file
View File

@ -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 })
})
})