forked from defunkt/ReefVM
add... a lot
This commit is contained in:
parent
d6aec409f0
commit
499584c5fe
96
README.md
96
README.md
|
|
@ -9,52 +9,92 @@ It's where Shrimp live.
|
|||
|
||||
## TODO (tests)
|
||||
|
||||
- [ ] PUSH
|
||||
- [ ] POP
|
||||
- [ ] DUP
|
||||
### Stack Operations
|
||||
- [x] PUSH
|
||||
- [x] POP
|
||||
- [x] DUP
|
||||
|
||||
- [ ] LOAD
|
||||
- [ ] STORE
|
||||
### Variables
|
||||
- [x] LOAD
|
||||
- [x] STORE
|
||||
|
||||
### Arithmetic
|
||||
- [x] ADD
|
||||
- [x] SUB
|
||||
- [x] MUL
|
||||
- [x] DIV
|
||||
- [ ] MOD
|
||||
- [ ] EQ
|
||||
- [ ] NEQ
|
||||
- [ ] LT
|
||||
- [ ] GT
|
||||
- [ ] LTE
|
||||
- [ ] GTE
|
||||
- [ ] AND
|
||||
- [ ] OR
|
||||
- [ ] NOT
|
||||
- [x] MOD
|
||||
|
||||
- [ ] JUMP
|
||||
- [ ] JUMP_IF_FALSE
|
||||
- [ ] JUMP_IF_TRUE
|
||||
### Comparison
|
||||
- [x] EQ
|
||||
- [x] NEQ
|
||||
- [x] LT
|
||||
- [x] GT
|
||||
- [x] LTE
|
||||
- [x] GTE
|
||||
|
||||
### Logical
|
||||
- [x] NOT
|
||||
- [x] AND pattern (using JUMP_IF_FALSE for short-circuiting)
|
||||
- [x] OR pattern (using JUMP_IF_TRUE for short-circuiting)
|
||||
|
||||
### Control Flow
|
||||
- [x] JUMP
|
||||
- [x] JUMP_IF_FALSE
|
||||
- [x] JUMP_IF_TRUE
|
||||
- [ ] BREAK
|
||||
- [ ] CONTINUE
|
||||
|
||||
### Exception Handling
|
||||
- [ ] PUSH_TRY
|
||||
- [ ] POP_TRY
|
||||
- [ ] THROW
|
||||
|
||||
### Functions
|
||||
- [ ] MAKE_FUNCTION
|
||||
- [ ] CALL
|
||||
- [ ] TAIL_CALL
|
||||
- [ ] CALL_TYPESCRIPT
|
||||
- [ ] RETURN
|
||||
|
||||
- [ ] MAKE_ARRAY
|
||||
- [ ] ARRAY_GET
|
||||
- [ ] ARRAY_SET
|
||||
- [ ] ARRAY_LEN
|
||||
### Arrays
|
||||
- [x] MAKE_ARRAY
|
||||
- [x] ARRAY_GET
|
||||
- [x] ARRAY_SET
|
||||
- [x] ARRAY_LEN
|
||||
|
||||
- [ ] MAKE_DICT
|
||||
- [ ] DICT_GET
|
||||
- [ ] DICT_SET
|
||||
- [ ] DICT_HAS
|
||||
### Dictionaries
|
||||
- [x] MAKE_DICT
|
||||
- [x] DICT_GET
|
||||
- [x] DICT_SET
|
||||
- [x] DICT_HAS
|
||||
|
||||
- [ ] HALT
|
||||
### TypeScript Interop
|
||||
- [ ] CALL_TYPESCRIPT
|
||||
|
||||
### Special
|
||||
- [x] HALT
|
||||
|
||||
## Test Status
|
||||
|
||||
✅ **37 tests passing** covering:
|
||||
- All stack operations (PUSH, POP, DUP)
|
||||
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
|
||||
- All comparison operations (EQ, NEQ, LT, GT, LTE, GTE)
|
||||
- Logical operations (NOT, AND/OR patterns with short-circuiting)
|
||||
- Variable operations (LOAD, STORE)
|
||||
- Control flow with **relative jumps** (JUMP, JUMP_IF_FALSE, JUMP_IF_TRUE)
|
||||
- All array operations (MAKE_ARRAY, ARRAY_GET, ARRAY_SET, ARRAY_LEN)
|
||||
- All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
||||
- HALT instruction
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Relative jumps**: All JUMP instructions use PC-relative offsets instead of absolute addresses, making bytecode position-independent
|
||||
- **Simple truthiness**: Only `null` and `false` are falsy (unlike JavaScript where `0`, `""`, etc. are also falsy)
|
||||
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
||||
|
||||
🚧 **Still TODO**:
|
||||
- Exception handling (PUSH_TRY, POP_TRY, THROW)
|
||||
- Function operations (MAKE_FUNCTION, CALL, TAIL_CALL, RETURN)
|
||||
- Advanced control flow (BREAK, CONTINUE)
|
||||
- TypeScript interop (CALL_TYPESCRIPT)
|
||||
99
SPEC.md
99
SPEC.md
|
|
@ -36,10 +36,10 @@ type Value =
|
|||
| { type: 'boolean', value: boolean }
|
||||
| { type: 'number', value: number }
|
||||
| { type: 'string', value: string }
|
||||
| { type: 'array', items: Value[] }
|
||||
| { type: 'dict', entries: Map<string, Value> }
|
||||
| { type: 'array', value: Value[] }
|
||||
| { type: 'dict', value: Map<string, Value> }
|
||||
| { type: 'function', params: string[], defaults: Record<string, Value>,
|
||||
body: number, scope: Scope, variadic: boolean, kwargs: boolean }
|
||||
body: number, parentScope: Scope, variadic: boolean, kwargs: boolean }
|
||||
```
|
||||
|
||||
### Type Coercion
|
||||
|
|
@ -49,8 +49,7 @@ type Value =
|
|||
**toString**: string → identity, number → string, boolean → string, null → "null",
|
||||
function → "<function>", array → "[item, item]", dict → "{key: value, ...}"
|
||||
|
||||
**isTruthy**: boolean → value, number → value !== 0, string → value !== "",
|
||||
null → false, array → length > 0, dict → size > 0, others → true
|
||||
**isTrue**: Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty arrays, empty dicts) is truthy.
|
||||
|
||||
## Bytecode Format
|
||||
|
||||
|
|
@ -169,53 +168,73 @@ All arithmetic operations pop two values, perform operation, push result as numb
|
|||
|
||||
### Comparison Operations
|
||||
|
||||
All comparison operations pop two values, compare, push boolean (as number 1/0).
|
||||
All comparison operations pop two values, compare, push boolean result.
|
||||
|
||||
#### EQ
|
||||
**Stack**: [a, b] → [a == b ? 1 : 0]
|
||||
**Note**: Type-aware equality
|
||||
**Stack**: [a, b] → [boolean]
|
||||
**Note**: Type-aware equality (deep comparison for arrays/dicts)
|
||||
|
||||
#### NEQ
|
||||
**Stack**: [a, b] → [a != b ? 1 : 0]
|
||||
**Stack**: [a, b] → [boolean]
|
||||
|
||||
#### LT
|
||||
**Stack**: [a, b] → [a < b ? 1 : 0]
|
||||
**Stack**: [a, b] → [boolean]
|
||||
**Note**: Numeric comparison (values coerced to numbers)
|
||||
|
||||
#### GT
|
||||
**Stack**: [a, b] → [a > b ? 1 : 0]
|
||||
**Stack**: [a, b] → [boolean]
|
||||
**Note**: Numeric comparison (values coerced to numbers)
|
||||
|
||||
#### LTE
|
||||
**Stack**: [a, b] → [a <= b ? 1 : 0]
|
||||
**Stack**: [a, b] → [boolean]
|
||||
**Note**: Numeric comparison (values coerced to numbers)
|
||||
|
||||
#### GTE
|
||||
**Stack**: [a, b] → [a >= b ? 1 : 0]
|
||||
**Stack**: [a, b] → [boolean]
|
||||
**Note**: Numeric comparison (values coerced to numbers)
|
||||
|
||||
### Logical Operations
|
||||
|
||||
#### AND
|
||||
**Stack**: [a, b] → [isTruthy(a) && isTruthy(b) ? 1 : 0]
|
||||
|
||||
#### OR
|
||||
**Stack**: [a, b] → [isTruthy(a) || isTruthy(b) ? 1 : 0]
|
||||
|
||||
#### NOT
|
||||
**Stack**: [a] → [!isTruthy(a)]
|
||||
**Stack**: [a] → [!isTrue(a)]
|
||||
|
||||
**Note on AND/OR**: There are no AND/OR opcodes. Short-circuiting logical operations are implemented at the compiler level using JUMP instructions:
|
||||
|
||||
**AND pattern** (short-circuits if left side is false):
|
||||
```
|
||||
<evaluate left>
|
||||
DUP
|
||||
JUMP_IF_FALSE 2 # skip POP and <evaluate right>
|
||||
POP
|
||||
<evaluate right>
|
||||
end:
|
||||
```
|
||||
|
||||
**OR pattern** (short-circuits if left side is true):
|
||||
```
|
||||
<evaluate left>
|
||||
DUP
|
||||
JUMP_IF_TRUE 2 # skip POP and <evaluate right>
|
||||
POP
|
||||
<evaluate right>
|
||||
end:
|
||||
```
|
||||
|
||||
### Control Flow
|
||||
|
||||
#### JUMP
|
||||
**Operand**: Instruction address (number)
|
||||
**Effect**: Set PC to address
|
||||
**Operand**: Offset (number)
|
||||
**Effect**: Add offset to PC (relative jump)
|
||||
**Stack**: No change
|
||||
|
||||
#### JUMP_IF_FALSE
|
||||
**Operand**: Instruction address (number)
|
||||
**Effect**: If top of stack is falsy, jump to address
|
||||
**Operand**: Offset (number)
|
||||
**Effect**: If top of stack is falsy, add offset to PC (relative jump)
|
||||
**Stack**: [condition] → []
|
||||
|
||||
#### JUMP_IF_TRUE
|
||||
**Operand**: Instruction address (number)
|
||||
**Effect**: If top of stack is truthy, jump to address
|
||||
**Operand**: Offset (number)
|
||||
**Effect**: If top of stack is truthy, add offset to PC (relative jump)
|
||||
**Stack**: [condition] → []
|
||||
|
||||
#### BREAK
|
||||
|
|
@ -441,21 +460,19 @@ type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
|
|||
LOAD 'x'
|
||||
PUSH 5
|
||||
GT
|
||||
JUMP_IF_FALSE else_label
|
||||
# then block
|
||||
JUMP end_label
|
||||
else_label:
|
||||
JUMP_IF_FALSE 2 # skip then block, jump to else
|
||||
# then block (N instructions)
|
||||
JUMP M # skip else block
|
||||
# else block
|
||||
end_label:
|
||||
```
|
||||
|
||||
### While Loop
|
||||
```
|
||||
loop_start:
|
||||
# condition
|
||||
JUMP_IF_FALSE loop_end
|
||||
# body
|
||||
JUMP loop_start
|
||||
JUMP_IF_FALSE N # jump past loop body
|
||||
# body (N-1 instructions)
|
||||
JUMP -N # jump back to loop_start
|
||||
loop_end:
|
||||
```
|
||||
|
||||
|
|
@ -463,19 +480,19 @@ loop_end:
|
|||
```
|
||||
MAKE_FUNCTION <index>
|
||||
STORE 'functionName'
|
||||
JUMP skip_body
|
||||
JUMP N # skip function body
|
||||
function_body:
|
||||
# function code
|
||||
# function code (N instructions)
|
||||
RETURN
|
||||
skip_body:
|
||||
```
|
||||
|
||||
### Try-Catch
|
||||
```
|
||||
PUSH_TRY catch_label
|
||||
PUSH_TRY N # catch is N instructions ahead
|
||||
# try block
|
||||
POP_TRY
|
||||
JUMP end_label
|
||||
JUMP M # skip catch block
|
||||
catch_label:
|
||||
STORE 'errorVar' # Error is on stack
|
||||
# catch block
|
||||
|
|
@ -495,12 +512,12 @@ CALL { positional: 1, named: 1 }
|
|||
```
|
||||
MAKE_FUNCTION <factorial_def>
|
||||
STORE 'factorial'
|
||||
JUMP main
|
||||
JUMP 10 # skip to main
|
||||
factorial_body:
|
||||
LOAD 'n'
|
||||
PUSH 0
|
||||
EQ
|
||||
JUMP_IF_FALSE recurse
|
||||
JUMP_IF_FALSE 2 # skip to recurse
|
||||
LOAD 'acc'
|
||||
RETURN
|
||||
recurse:
|
||||
|
|
@ -624,7 +641,7 @@ const result = await vm.execute()
|
|||
## Notes
|
||||
|
||||
- PC increment happens after each instruction execution
|
||||
- Jump instructions compensate for automatic PC increment (subtract 1)
|
||||
- Jump instructions use relative offsets (added to current PC after increment)
|
||||
- All async operations (TypeScript functions) must be awaited
|
||||
- Arrays and dicts are mutable (pass by reference)
|
||||
- Functions are immutable values
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ export type Constant =
|
|||
| Value
|
||||
| FunctionDef
|
||||
|
||||
const opsWithVarNames = new Set([OpCode.LOAD, OpCode.STORE, OpCode.CALL_TYPESCRIPT])
|
||||
const opsWithAddresses = new Set([OpCode.JUMP, OpCode.JUMP_IF_FALSE, OpCode.JUMP_IF_TRUE, OpCode.PUSH_TRY])
|
||||
const opsWithNumbers = new Set([OpCode.MAKE_ARRAY, OpCode.MAKE_DICT])
|
||||
|
||||
export function toBytecode(str: string): Bytecode /* throws */ {
|
||||
const lines = str.trim().split("\n")
|
||||
|
||||
|
|
@ -25,20 +29,39 @@ export function toBytecode(str: string): Bytecode /* throws */ {
|
|||
|
||||
for (let line of lines) {
|
||||
let [op, operand] = line.trim().split(" ")
|
||||
const opCode = OpCode[op as keyof typeof OpCode]
|
||||
|
||||
let operandValue: number | string | undefined = undefined
|
||||
|
||||
if (operand) {
|
||||
// Variable names for LOAD, STORE, CALL_TYPESCRIPT
|
||||
if (opsWithVarNames.has(opCode)) {
|
||||
operandValue = operand
|
||||
}
|
||||
// Direct addresses for JUMP operations
|
||||
else if (opsWithAddresses.has(opCode)) {
|
||||
operandValue = parseInt(operand)
|
||||
}
|
||||
// Direct numbers for MAKE_ARRAY, MAKE_DICT
|
||||
else if (opsWithNumbers.has(opCode)) {
|
||||
operandValue = parseInt(operand)
|
||||
}
|
||||
// Constants (numbers, strings) for PUSH
|
||||
else {
|
||||
if (/^\d+/.test(operand)) {
|
||||
bytecode.constants.push(toValue(parseFloat(operand)))
|
||||
} else if (/^['"]\w+/.test(operand)) {
|
||||
} else if (/^['"]/.test(operand)) {
|
||||
bytecode.constants.push(toValue(operand.slice(1, operand.length - 1)))
|
||||
} else {
|
||||
throw `Unknown operand: ${operand}`
|
||||
}
|
||||
operandValue = bytecode.constants.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
bytecode.instructions.push({
|
||||
op: OpCode[op as keyof typeof OpCode],
|
||||
operand: operand ? bytecode.constants.length - 1 : undefined
|
||||
op: opCode,
|
||||
operand: operandValue
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,61 @@ export enum OpCode {
|
|||
POP, // operand: none
|
||||
DUP, // operand: none
|
||||
|
||||
// variables
|
||||
LOAD, // operand: variable name (string)
|
||||
STORE, // operand: variable name (string)
|
||||
|
||||
// math
|
||||
ADD,
|
||||
SUB,
|
||||
MUL,
|
||||
DIV
|
||||
DIV,
|
||||
MOD,
|
||||
|
||||
// comparison
|
||||
EQ,
|
||||
NEQ,
|
||||
LT,
|
||||
GT,
|
||||
LTE,
|
||||
GTE,
|
||||
|
||||
// logical
|
||||
NOT,
|
||||
|
||||
// control flow
|
||||
JUMP,
|
||||
JUMP_IF_FALSE,
|
||||
JUMP_IF_TRUE,
|
||||
BREAK,
|
||||
CONTINUE,
|
||||
|
||||
// exception handling
|
||||
PUSH_TRY,
|
||||
POP_TRY,
|
||||
THROW,
|
||||
|
||||
// functions
|
||||
MAKE_FUNCTION,
|
||||
CALL,
|
||||
TAIL_CALL,
|
||||
RETURN,
|
||||
|
||||
// arrays
|
||||
MAKE_ARRAY,
|
||||
ARRAY_GET,
|
||||
ARRAY_SET,
|
||||
ARRAY_LEN,
|
||||
|
||||
// dicts
|
||||
MAKE_DICT,
|
||||
DICT_GET,
|
||||
DICT_SET,
|
||||
DICT_HAS,
|
||||
|
||||
// typescript interop
|
||||
CALL_TYPESCRIPT,
|
||||
|
||||
// special
|
||||
HALT
|
||||
}
|
||||
|
|
@ -8,14 +8,14 @@ export class Scope {
|
|||
this.parent = parent
|
||||
}
|
||||
|
||||
get(name: string): Value /* throws */ {
|
||||
get(name: string): Value | undefined {
|
||||
if (this.locals.has(name))
|
||||
return this.locals.get(name)!
|
||||
|
||||
if (this.parent)
|
||||
return this.parent.get(name)
|
||||
|
||||
throw new Error(`Undefined variable: ${name}`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
set(name: string, value: Value) {
|
||||
|
|
|
|||
63
src/value.ts
63
src/value.ts
|
|
@ -57,5 +57,66 @@ export function toValue(v: any): Value /* throws */ {
|
|||
}
|
||||
|
||||
export function toNumber(v: Value): number {
|
||||
return v.type === 'number' ? v.value : 0
|
||||
switch (v.type) {
|
||||
case 'number': return v.value
|
||||
case 'boolean': return v.value ? 1 : 0
|
||||
case 'string': {
|
||||
const parsed = parseFloat(v.value)
|
||||
return isNaN(parsed) ? 0 : parsed
|
||||
}
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
export function isTrue(v: Value): boolean {
|
||||
switch (v.type) {
|
||||
case 'null':
|
||||
return false
|
||||
case 'boolean':
|
||||
return v.value
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export function toString(v: Value): string {
|
||||
switch (v.type) {
|
||||
case 'string': return v.value
|
||||
case 'number': return String(v.value)
|
||||
case 'boolean': return String(v.value)
|
||||
case 'null': return 'null'
|
||||
case 'function': return '<function>'
|
||||
case 'array': return `[${v.value.map(toString).join(', ')}]`
|
||||
case 'dict': {
|
||||
const pairs = Array.from(v.value.entries())
|
||||
.map(([k, v]) => `${k}: ${toString(v)}`)
|
||||
return `{${pairs.join(', ')}}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isEqual(a: Value, b: Value): boolean {
|
||||
if (a.type !== b.type) return false
|
||||
|
||||
switch (a.type) {
|
||||
case 'null': return true
|
||||
case 'boolean': return a.value === (b as typeof a).value
|
||||
case 'number': return a.value === (b as typeof a).value
|
||||
case 'string': return a.value === (b as typeof a).value
|
||||
case 'array': {
|
||||
const bArr = b as typeof a
|
||||
if (a.value.length !== bArr.value.length) return false
|
||||
return a.value.every((v, i) => isEqual(v, bArr.value[i]!))
|
||||
}
|
||||
case 'dict': {
|
||||
const bDict = b as typeof a
|
||||
if (a.value.size !== bDict.value.size) return false
|
||||
for (const [k, v] of a.value) {
|
||||
const bVal = bDict.value.get(k)
|
||||
if (!bVal || !isEqual(v, bVal)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
case 'function': return false // functions never equal
|
||||
}
|
||||
}
|
||||
156
src/vm.ts
156
src/vm.ts
|
|
@ -3,7 +3,7 @@ import type { ExceptionHandler } from "./exception"
|
|||
import type { Frame } from "./frame"
|
||||
import { OpCode } from "./opcode"
|
||||
import { Scope } from "./scope"
|
||||
import { type Value, toValue, toNumber } from "./value"
|
||||
import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value"
|
||||
|
||||
export class VM {
|
||||
pc = 0
|
||||
|
|
@ -37,11 +37,11 @@ export class VM {
|
|||
async execute(instruction: Instruction) /* throws */ {
|
||||
switch (instruction.op) {
|
||||
case OpCode.PUSH:
|
||||
const idx = instruction.operand as number
|
||||
const constant = this.constants[idx]
|
||||
const constIdx = instruction.operand as number
|
||||
const constant = this.constants[constIdx]
|
||||
|
||||
if (!constant || constant.type === 'function_def')
|
||||
throw new Error(`Invalid constant index: ${idx}`)
|
||||
throw new Error(`Invalid constant index: ${constIdx}`)
|
||||
|
||||
this.stack.push(constant)
|
||||
break
|
||||
|
|
@ -70,6 +70,147 @@ export class VM {
|
|||
this.binaryOp((a, b) => toNumber(a) / toNumber(b))
|
||||
break
|
||||
|
||||
case OpCode.MOD:
|
||||
this.binaryOp((a, b) => toNumber(a) % toNumber(b))
|
||||
break
|
||||
|
||||
case OpCode.EQ:
|
||||
this.comparisonOp((a, b) => isEqual(a, b))
|
||||
break
|
||||
|
||||
case OpCode.NEQ:
|
||||
this.comparisonOp((a, b) => !isEqual(a, b))
|
||||
break
|
||||
|
||||
case OpCode.LT:
|
||||
this.comparisonOp((a, b) => toNumber(a) < toNumber(b))
|
||||
break
|
||||
|
||||
case OpCode.GT:
|
||||
this.comparisonOp((a, b) => toNumber(a) > toNumber(b))
|
||||
break
|
||||
|
||||
case OpCode.LTE:
|
||||
this.comparisonOp((a, b) => toNumber(a) <= toNumber(b))
|
||||
break
|
||||
|
||||
case OpCode.GTE:
|
||||
this.comparisonOp((a, b) => toNumber(a) >= toNumber(b))
|
||||
break
|
||||
|
||||
case OpCode.NOT:
|
||||
const val = this.stack.pop()!
|
||||
this.stack.push({ type: 'boolean', value: !isTrue(val) })
|
||||
break
|
||||
|
||||
case OpCode.HALT:
|
||||
this.stopped = true
|
||||
break
|
||||
|
||||
case OpCode.LOAD:
|
||||
const varName = instruction.operand as string
|
||||
const value = this.scope.get(varName)
|
||||
if (value === undefined)
|
||||
throw new Error(`Undefined variable: ${varName}`)
|
||||
this.stack.push(value)
|
||||
break
|
||||
|
||||
case OpCode.STORE:
|
||||
const name = instruction.operand as string
|
||||
const toStore = this.stack.pop()!
|
||||
this.scope.set(name, toStore)
|
||||
break
|
||||
|
||||
case OpCode.JUMP:
|
||||
this.pc += (instruction.operand as number)
|
||||
break
|
||||
|
||||
case OpCode.JUMP_IF_FALSE:
|
||||
const cond = this.stack.pop()!
|
||||
if (!isTrue(cond))
|
||||
this.pc += (instruction.operand as number)
|
||||
break
|
||||
|
||||
case OpCode.JUMP_IF_TRUE:
|
||||
const condTrue = this.stack.pop()!
|
||||
if (isTrue(condTrue))
|
||||
this.pc += (instruction.operand as number)
|
||||
break
|
||||
|
||||
case OpCode.MAKE_ARRAY:
|
||||
const arraySize = instruction.operand as number
|
||||
const items: Value[] = []
|
||||
for (let i = 0; i < arraySize; i++)
|
||||
items.unshift(this.stack.pop()!)
|
||||
this.stack.push({ type: 'array', value: items })
|
||||
break
|
||||
|
||||
case OpCode.ARRAY_GET:
|
||||
const index = this.stack.pop()!
|
||||
const array = this.stack.pop()!
|
||||
if (array.type !== 'array')
|
||||
throw new Error('ARRAY_GET: not an array')
|
||||
const idx = Math.floor(toNumber(index))
|
||||
if (idx < 0 || idx >= array.value.length)
|
||||
throw new Error(`ARRAY_GET: index ${idx} out of bounds`)
|
||||
this.stack.push(array.value[idx]!)
|
||||
break
|
||||
|
||||
case OpCode.ARRAY_SET:
|
||||
const setValue = this.stack.pop()!
|
||||
const setIndex = this.stack.pop()!
|
||||
const setArray = this.stack.pop()!
|
||||
if (setArray.type !== 'array')
|
||||
throw new Error('ARRAY_SET: not an array')
|
||||
const setIdx = Math.floor(toNumber(setIndex))
|
||||
if (setIdx < 0 || setIdx >= setArray.value.length)
|
||||
throw new Error(`ARRAY_SET: index ${setIdx} out of bounds`)
|
||||
setArray.value[setIdx] = setValue
|
||||
break
|
||||
|
||||
case OpCode.ARRAY_LEN:
|
||||
const lenArray = this.stack.pop()!
|
||||
if (lenArray.type !== 'array')
|
||||
throw new Error('ARRAY_LEN: not an array')
|
||||
this.stack.push({ type: 'number', value: lenArray.value.length })
|
||||
break
|
||||
|
||||
case OpCode.MAKE_DICT:
|
||||
const dictPairs = instruction.operand as number
|
||||
const dict = new Map<string, Value>()
|
||||
for (let i = 0; i < dictPairs; i++) {
|
||||
const value = this.stack.pop()!
|
||||
const key = this.stack.pop()!
|
||||
dict.set(toString(key), value)
|
||||
}
|
||||
this.stack.push({ type: 'dict', value: dict })
|
||||
break
|
||||
|
||||
case OpCode.DICT_GET:
|
||||
const getKey = this.stack.pop()!
|
||||
const getDict = this.stack.pop()!
|
||||
if (getDict.type !== 'dict')
|
||||
throw new Error('DICT_GET: not a dict')
|
||||
this.stack.push(getDict.value.get(toString(getKey)) || { type: 'null', value: null })
|
||||
break
|
||||
|
||||
case OpCode.DICT_SET:
|
||||
const dictSetValue = this.stack.pop()!
|
||||
const dictSetKey = this.stack.pop()!
|
||||
const dictSet = this.stack.pop()!
|
||||
if (dictSet.type !== 'dict')
|
||||
throw new Error('DICT_SET: not a dict')
|
||||
dictSet.value.set(toString(dictSetKey), dictSetValue)
|
||||
break
|
||||
|
||||
case OpCode.DICT_HAS:
|
||||
const hasKey = this.stack.pop()!
|
||||
const hasDict = this.stack.pop()!
|
||||
if (hasDict.type !== 'dict')
|
||||
throw new Error('DICT_HAS: not a dict')
|
||||
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
||||
break
|
||||
|
||||
default:
|
||||
throw `Unknown op: ${instruction.op}`
|
||||
}
|
||||
|
|
@ -81,4 +222,11 @@ export class VM {
|
|||
const result = fn(a, b)
|
||||
this.stack.push({ type: 'number', value: result })
|
||||
}
|
||||
|
||||
comparisonOp(fn: (a: Value, b: Value) => boolean) {
|
||||
const b = this.stack.pop()!
|
||||
const a = this.stack.pop()!
|
||||
const result = fn(a, b)
|
||||
this.stack.push({ type: 'boolean', value: result })
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { test, expect } from "bun:test"
|
|||
import { run } from "#index"
|
||||
import { toBytecode } from "#bytecode"
|
||||
|
||||
test("adding numbers", async () => {
|
||||
test("ADD - add two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 5
|
||||
|
|
@ -18,7 +18,7 @@ test("adding numbers", async () => {
|
|||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 })
|
||||
})
|
||||
|
||||
test("subtracting numbers", async () => {
|
||||
test("SUB - subtract two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 2
|
||||
|
|
@ -27,7 +27,7 @@ test("subtracting numbers", async () => {
|
|||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
||||
})
|
||||
|
||||
test("multiplying numbers", async () => {
|
||||
test("MUL - multiply two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 2
|
||||
|
|
@ -36,7 +36,7 @@ test("multiplying numbers", async () => {
|
|||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
||||
})
|
||||
|
||||
test("dividing numbers", async () => {
|
||||
test("DIV - divide two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 2
|
||||
|
|
@ -52,3 +52,440 @@ test("dividing numbers", async () => {
|
|||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: Infinity })
|
||||
})
|
||||
|
||||
test("MOD - modulo two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 17
|
||||
PUSH 5
|
||||
MOD
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("PUSH - pushes value onto stack", async () => {
|
||||
const str = `
|
||||
PUSH 42
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("POP - removes top value", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
POP
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
||||
})
|
||||
|
||||
test("DUP - duplicates top value", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
DUP
|
||||
ADD
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
||||
})
|
||||
|
||||
test("EQ - equality comparison", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 5
|
||||
EQ
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
const str2 = `
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
EQ
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("NEQ - not equal comparison", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
NEQ
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("LT - less than", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
LT
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("GT - greater than", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
GT
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("LTE - less than or equal", async () => {
|
||||
// equal case
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 5
|
||||
LTE
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// less than case
|
||||
const str2 = `
|
||||
PUSH 3
|
||||
PUSH 5
|
||||
LTE
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// greater than case (false)
|
||||
const str3 = `
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
LTE
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("GTE - greater than or equal", async () => {
|
||||
// equal case
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 5
|
||||
GTE
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// greater than case
|
||||
const str2 = `
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
GTE
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// less than case (false)
|
||||
const str3 = `
|
||||
PUSH 3
|
||||
PUSH 5
|
||||
GTE
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("AND pattern - short circuits when false", async () => {
|
||||
// false && <anything> should short-circuit and return false
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
DUP
|
||||
JUMP_IF_FALSE 2
|
||||
POP
|
||||
PUSH 999
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('boolean')
|
||||
if (result.type === 'boolean') {
|
||||
expect(result.value).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
test("AND pattern - evaluates both when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
DUP
|
||||
JUMP_IF_FALSE 2
|
||||
POP
|
||||
PUSH 2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("OR pattern - short circuits when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
DUP
|
||||
JUMP_IF_TRUE 2
|
||||
POP
|
||||
PUSH 2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 })
|
||||
})
|
||||
|
||||
test("OR pattern - evaluates second when false", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
DUP
|
||||
JUMP_IF_TRUE 2
|
||||
POP
|
||||
PUSH 2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("NOT - logical not", async () => {
|
||||
// number is truthy, so NOT returns false
|
||||
const str = `
|
||||
PUSH 1
|
||||
NOT
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
||||
|
||||
// 0 is truthy in this language, so NOT returns false
|
||||
const str2 = `
|
||||
PUSH 0
|
||||
NOT
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
||||
|
||||
// boolean false is falsy, so NOT returns true
|
||||
const str3 = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
NOT
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("isTruthy - only null and false are falsy", async () => {
|
||||
// 0 is truthy (unlike JS)
|
||||
const str1 = `
|
||||
PUSH 0
|
||||
JUMP_IF_FALSE 1
|
||||
PUSH 1
|
||||
`
|
||||
expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 })
|
||||
|
||||
// empty string is truthy (unlike JS)
|
||||
const str2 = `
|
||||
PUSH ''
|
||||
JUMP_IF_FALSE 1
|
||||
PUSH 1
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 })
|
||||
|
||||
// false is falsy
|
||||
const str3 = `
|
||||
PUSH 0
|
||||
PUSH 0
|
||||
EQ
|
||||
JUMP_IF_FALSE 1
|
||||
PUSH 999
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 })
|
||||
})
|
||||
|
||||
test("HALT - stops execution", async () => {
|
||||
const str = `
|
||||
PUSH 42
|
||||
HALT
|
||||
PUSH 100
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("STORE and LOAD - variables", async () => {
|
||||
const str = `
|
||||
PUSH 42
|
||||
STORE x
|
||||
PUSH 21
|
||||
LOAD x
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("STORE and LOAD - multiple variables", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
STORE a
|
||||
PUSH 20
|
||||
STORE b
|
||||
PUSH 44
|
||||
LOAD a
|
||||
LOAD b
|
||||
ADD
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
|
||||
})
|
||||
|
||||
test("JUMP - relative jump forward", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
JUMP 1
|
||||
PUSH 100
|
||||
PUSH 2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("JUMP - backward offset demonstrates relative jumps", async () => {
|
||||
// Use forward jump to skip, demonstrating relative addressing
|
||||
const str = `
|
||||
PUSH 100
|
||||
JUMP 2
|
||||
PUSH 200
|
||||
PUSH 300
|
||||
PUSH 400
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 })
|
||||
})
|
||||
|
||||
test("JUMP_IF_FALSE - conditional jump when false", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
JUMP_IF_FALSE 1
|
||||
PUSH 100
|
||||
PUSH 42
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("JUMP_IF_FALSE - no jump when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
JUMP_IF_FALSE 1
|
||||
PUSH 100
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
|
||||
})
|
||||
|
||||
test("JUMP_IF_TRUE - conditional jump when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
JUMP_IF_TRUE 1
|
||||
PUSH 100
|
||||
PUSH 42
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("MAKE_ARRAY - creates array", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY 3
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('array')
|
||||
if (result.type === 'array') {
|
||||
expect(result.value).toHaveLength(3)
|
||||
expect(result.value[0]).toEqual({ type: 'number', value: 10 })
|
||||
expect(result.value[1]).toEqual({ type: 'number', value: 20 })
|
||||
expect(result.value[2]).toEqual({ type: 'number', value: 30 })
|
||||
}
|
||||
})
|
||||
|
||||
test("ARRAY_GET - gets element", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY 3
|
||||
PUSH 1
|
||||
ARRAY_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 20 })
|
||||
})
|
||||
|
||||
test("ARRAY_SET - sets element", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY 3
|
||||
DUP
|
||||
PUSH 1
|
||||
PUSH 99
|
||||
ARRAY_SET
|
||||
PUSH 1
|
||||
ARRAY_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 99 })
|
||||
})
|
||||
|
||||
test("ARRAY_LEN - gets length", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY 3
|
||||
ARRAY_LEN
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
||||
})
|
||||
|
||||
test("MAKE_DICT - creates dict", async () => {
|
||||
const str = `
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
PUSH 'age'
|
||||
PUSH 30
|
||||
MAKE_DICT 2
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.size).toBe(2)
|
||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
||||
expect(result.value.get('age')).toEqual({ type: 'number', value: 30 })
|
||||
}
|
||||
})
|
||||
|
||||
test("DICT_GET - gets value", async () => {
|
||||
const str = `
|
||||
PUSH 'name'
|
||||
PUSH 'Bob'
|
||||
MAKE_DICT 1
|
||||
PUSH 'name'
|
||||
DICT_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Bob' })
|
||||
})
|
||||
|
||||
test("DICT_SET - sets value", async () => {
|
||||
const str = `
|
||||
MAKE_DICT 0
|
||||
DUP
|
||||
PUSH 'key'
|
||||
PUSH 'value'
|
||||
DICT_SET
|
||||
PUSH 'key'
|
||||
DICT_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value' })
|
||||
})
|
||||
|
||||
test("DICT_HAS - checks key exists", async () => {
|
||||
const str = `
|
||||
PUSH 'key'
|
||||
PUSH 'value'
|
||||
MAKE_DICT 1
|
||||
PUSH 'key'
|
||||
DICT_HAS
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("DICT_HAS - checks key missing", async () => {
|
||||
const str = `
|
||||
MAKE_DICT 0
|
||||
PUSH 'missing'
|
||||
DICT_HAS
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user