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)
|
## TODO (tests)
|
||||||
|
|
||||||
- [ ] PUSH
|
### Stack Operations
|
||||||
- [ ] POP
|
- [x] PUSH
|
||||||
- [ ] DUP
|
- [x] POP
|
||||||
|
- [x] DUP
|
||||||
|
|
||||||
- [ ] LOAD
|
### Variables
|
||||||
- [ ] STORE
|
- [x] LOAD
|
||||||
|
- [x] STORE
|
||||||
|
|
||||||
|
### Arithmetic
|
||||||
- [x] ADD
|
- [x] ADD
|
||||||
- [x] SUB
|
- [x] SUB
|
||||||
- [x] MUL
|
- [x] MUL
|
||||||
- [x] DIV
|
- [x] DIV
|
||||||
- [ ] MOD
|
- [x] MOD
|
||||||
- [ ] EQ
|
|
||||||
- [ ] NEQ
|
|
||||||
- [ ] LT
|
|
||||||
- [ ] GT
|
|
||||||
- [ ] LTE
|
|
||||||
- [ ] GTE
|
|
||||||
- [ ] AND
|
|
||||||
- [ ] OR
|
|
||||||
- [ ] NOT
|
|
||||||
|
|
||||||
- [ ] JUMP
|
### Comparison
|
||||||
- [ ] JUMP_IF_FALSE
|
- [x] EQ
|
||||||
- [ ] JUMP_IF_TRUE
|
- [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
|
- [ ] BREAK
|
||||||
- [ ] CONTINUE
|
- [ ] CONTINUE
|
||||||
|
|
||||||
|
### Exception Handling
|
||||||
- [ ] PUSH_TRY
|
- [ ] PUSH_TRY
|
||||||
- [ ] POP_TRY
|
- [ ] POP_TRY
|
||||||
- [ ] THROW
|
- [ ] THROW
|
||||||
|
|
||||||
|
### Functions
|
||||||
- [ ] MAKE_FUNCTION
|
- [ ] MAKE_FUNCTION
|
||||||
- [ ] CALL
|
- [ ] CALL
|
||||||
- [ ] TAIL_CALL
|
- [ ] TAIL_CALL
|
||||||
- [ ] CALL_TYPESCRIPT
|
|
||||||
- [ ] RETURN
|
- [ ] RETURN
|
||||||
|
|
||||||
- [ ] MAKE_ARRAY
|
### Arrays
|
||||||
- [ ] ARRAY_GET
|
- [x] MAKE_ARRAY
|
||||||
- [ ] ARRAY_SET
|
- [x] ARRAY_GET
|
||||||
- [ ] ARRAY_LEN
|
- [x] ARRAY_SET
|
||||||
|
- [x] ARRAY_LEN
|
||||||
|
|
||||||
- [ ] MAKE_DICT
|
### Dictionaries
|
||||||
- [ ] DICT_GET
|
- [x] MAKE_DICT
|
||||||
- [ ] DICT_SET
|
- [x] DICT_GET
|
||||||
- [ ] DICT_HAS
|
- [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: 'boolean', value: boolean }
|
||||||
| { type: 'number', value: number }
|
| { type: 'number', value: number }
|
||||||
| { type: 'string', value: string }
|
| { type: 'string', value: string }
|
||||||
| { type: 'array', items: Value[] }
|
| { type: 'array', value: Value[] }
|
||||||
| { type: 'dict', entries: Map<string, Value> }
|
| { type: 'dict', value: Map<string, Value> }
|
||||||
| { type: 'function', params: string[], defaults: Record<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
|
### Type Coercion
|
||||||
|
|
@ -49,8 +49,7 @@ type Value =
|
||||||
**toString**: string → identity, number → string, boolean → string, null → "null",
|
**toString**: string → identity, number → string, boolean → string, null → "null",
|
||||||
function → "<function>", array → "[item, item]", dict → "{key: value, ...}"
|
function → "<function>", array → "[item, item]", dict → "{key: value, ...}"
|
||||||
|
|
||||||
**isTruthy**: boolean → value, number → value !== 0, string → value !== "",
|
**isTrue**: Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty arrays, empty dicts) is truthy.
|
||||||
null → false, array → length > 0, dict → size > 0, others → true
|
|
||||||
|
|
||||||
## Bytecode Format
|
## Bytecode Format
|
||||||
|
|
||||||
|
|
@ -169,53 +168,73 @@ All arithmetic operations pop two values, perform operation, push result as numb
|
||||||
|
|
||||||
### Comparison Operations
|
### 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
|
#### EQ
|
||||||
**Stack**: [a, b] → [a == b ? 1 : 0]
|
**Stack**: [a, b] → [boolean]
|
||||||
**Note**: Type-aware equality
|
**Note**: Type-aware equality (deep comparison for arrays/dicts)
|
||||||
|
|
||||||
#### NEQ
|
#### NEQ
|
||||||
**Stack**: [a, b] → [a != b ? 1 : 0]
|
**Stack**: [a, b] → [boolean]
|
||||||
|
|
||||||
#### LT
|
#### LT
|
||||||
**Stack**: [a, b] → [a < b ? 1 : 0]
|
**Stack**: [a, b] → [boolean]
|
||||||
|
**Note**: Numeric comparison (values coerced to numbers)
|
||||||
|
|
||||||
#### GT
|
#### GT
|
||||||
**Stack**: [a, b] → [a > b ? 1 : 0]
|
**Stack**: [a, b] → [boolean]
|
||||||
|
**Note**: Numeric comparison (values coerced to numbers)
|
||||||
|
|
||||||
#### LTE
|
#### LTE
|
||||||
**Stack**: [a, b] → [a <= b ? 1 : 0]
|
**Stack**: [a, b] → [boolean]
|
||||||
|
**Note**: Numeric comparison (values coerced to numbers)
|
||||||
|
|
||||||
#### GTE
|
#### GTE
|
||||||
**Stack**: [a, b] → [a >= b ? 1 : 0]
|
**Stack**: [a, b] → [boolean]
|
||||||
|
**Note**: Numeric comparison (values coerced to numbers)
|
||||||
|
|
||||||
### Logical Operations
|
### Logical Operations
|
||||||
|
|
||||||
#### AND
|
|
||||||
**Stack**: [a, b] → [isTruthy(a) && isTruthy(b) ? 1 : 0]
|
|
||||||
|
|
||||||
#### OR
|
|
||||||
**Stack**: [a, b] → [isTruthy(a) || isTruthy(b) ? 1 : 0]
|
|
||||||
|
|
||||||
#### NOT
|
#### 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
|
### Control Flow
|
||||||
|
|
||||||
#### JUMP
|
#### JUMP
|
||||||
**Operand**: Instruction address (number)
|
**Operand**: Offset (number)
|
||||||
**Effect**: Set PC to address
|
**Effect**: Add offset to PC (relative jump)
|
||||||
**Stack**: No change
|
**Stack**: No change
|
||||||
|
|
||||||
#### JUMP_IF_FALSE
|
#### JUMP_IF_FALSE
|
||||||
**Operand**: Instruction address (number)
|
**Operand**: Offset (number)
|
||||||
**Effect**: If top of stack is falsy, jump to address
|
**Effect**: If top of stack is falsy, add offset to PC (relative jump)
|
||||||
**Stack**: [condition] → []
|
**Stack**: [condition] → []
|
||||||
|
|
||||||
#### JUMP_IF_TRUE
|
#### JUMP_IF_TRUE
|
||||||
**Operand**: Instruction address (number)
|
**Operand**: Offset (number)
|
||||||
**Effect**: If top of stack is truthy, jump to address
|
**Effect**: If top of stack is truthy, add offset to PC (relative jump)
|
||||||
**Stack**: [condition] → []
|
**Stack**: [condition] → []
|
||||||
|
|
||||||
#### BREAK
|
#### BREAK
|
||||||
|
|
@ -441,21 +460,19 @@ type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
|
||||||
LOAD 'x'
|
LOAD 'x'
|
||||||
PUSH 5
|
PUSH 5
|
||||||
GT
|
GT
|
||||||
JUMP_IF_FALSE else_label
|
JUMP_IF_FALSE 2 # skip then block, jump to else
|
||||||
# then block
|
# then block (N instructions)
|
||||||
JUMP end_label
|
JUMP M # skip else block
|
||||||
else_label:
|
|
||||||
# else block
|
# else block
|
||||||
end_label:
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### While Loop
|
### While Loop
|
||||||
```
|
```
|
||||||
loop_start:
|
loop_start:
|
||||||
# condition
|
# condition
|
||||||
JUMP_IF_FALSE loop_end
|
JUMP_IF_FALSE N # jump past loop body
|
||||||
# body
|
# body (N-1 instructions)
|
||||||
JUMP loop_start
|
JUMP -N # jump back to loop_start
|
||||||
loop_end:
|
loop_end:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -463,19 +480,19 @@ loop_end:
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION <index>
|
MAKE_FUNCTION <index>
|
||||||
STORE 'functionName'
|
STORE 'functionName'
|
||||||
JUMP skip_body
|
JUMP N # skip function body
|
||||||
function_body:
|
function_body:
|
||||||
# function code
|
# function code (N instructions)
|
||||||
RETURN
|
RETURN
|
||||||
skip_body:
|
skip_body:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Try-Catch
|
### Try-Catch
|
||||||
```
|
```
|
||||||
PUSH_TRY catch_label
|
PUSH_TRY N # catch is N instructions ahead
|
||||||
# try block
|
# try block
|
||||||
POP_TRY
|
POP_TRY
|
||||||
JUMP end_label
|
JUMP M # skip catch block
|
||||||
catch_label:
|
catch_label:
|
||||||
STORE 'errorVar' # Error is on stack
|
STORE 'errorVar' # Error is on stack
|
||||||
# catch block
|
# catch block
|
||||||
|
|
@ -495,12 +512,12 @@ CALL { positional: 1, named: 1 }
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION <factorial_def>
|
MAKE_FUNCTION <factorial_def>
|
||||||
STORE 'factorial'
|
STORE 'factorial'
|
||||||
JUMP main
|
JUMP 10 # skip to main
|
||||||
factorial_body:
|
factorial_body:
|
||||||
LOAD 'n'
|
LOAD 'n'
|
||||||
PUSH 0
|
PUSH 0
|
||||||
EQ
|
EQ
|
||||||
JUMP_IF_FALSE recurse
|
JUMP_IF_FALSE 2 # skip to recurse
|
||||||
LOAD 'acc'
|
LOAD 'acc'
|
||||||
RETURN
|
RETURN
|
||||||
recurse:
|
recurse:
|
||||||
|
|
@ -624,7 +641,7 @@ const result = await vm.execute()
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- PC increment happens after each instruction execution
|
- 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
|
- All async operations (TypeScript functions) must be awaited
|
||||||
- Arrays and dicts are mutable (pass by reference)
|
- Arrays and dicts are mutable (pass by reference)
|
||||||
- Functions are immutable values
|
- Functions are immutable values
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ export type Constant =
|
||||||
| Value
|
| Value
|
||||||
| FunctionDef
|
| 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 */ {
|
export function toBytecode(str: string): Bytecode /* throws */ {
|
||||||
const lines = str.trim().split("\n")
|
const lines = str.trim().split("\n")
|
||||||
|
|
||||||
|
|
@ -25,20 +29,39 @@ export function toBytecode(str: string): Bytecode /* throws */ {
|
||||||
|
|
||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
let [op, operand] = line.trim().split(" ")
|
let [op, operand] = line.trim().split(" ")
|
||||||
|
const opCode = OpCode[op as keyof typeof OpCode]
|
||||||
|
|
||||||
|
let operandValue: number | string | undefined = undefined
|
||||||
|
|
||||||
if (operand) {
|
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)) {
|
if (/^\d+/.test(operand)) {
|
||||||
bytecode.constants.push(toValue(parseFloat(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)))
|
bytecode.constants.push(toValue(operand.slice(1, operand.length - 1)))
|
||||||
} else {
|
} else {
|
||||||
throw `Unknown operand: ${operand}`
|
throw `Unknown operand: ${operand}`
|
||||||
}
|
}
|
||||||
|
operandValue = bytecode.constants.length - 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bytecode.instructions.push({
|
bytecode.instructions.push({
|
||||||
op: OpCode[op as keyof typeof OpCode],
|
op: opCode,
|
||||||
operand: operand ? bytecode.constants.length - 1 : undefined
|
operand: operandValue
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,61 @@ export enum OpCode {
|
||||||
POP, // operand: none
|
POP, // operand: none
|
||||||
DUP, // operand: none
|
DUP, // operand: none
|
||||||
|
|
||||||
|
// variables
|
||||||
|
LOAD, // operand: variable name (string)
|
||||||
|
STORE, // operand: variable name (string)
|
||||||
|
|
||||||
// math
|
// math
|
||||||
ADD,
|
ADD,
|
||||||
SUB,
|
SUB,
|
||||||
MUL,
|
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
|
this.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
get(name: string): Value /* throws */ {
|
get(name: string): Value | undefined {
|
||||||
if (this.locals.has(name))
|
if (this.locals.has(name))
|
||||||
return this.locals.get(name)!
|
return this.locals.get(name)!
|
||||||
|
|
||||||
if (this.parent)
|
if (this.parent)
|
||||||
return this.parent.get(name)
|
return this.parent.get(name)
|
||||||
|
|
||||||
throw new Error(`Undefined variable: ${name}`)
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
set(name: string, value: Value) {
|
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 {
|
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 type { Frame } from "./frame"
|
||||||
import { OpCode } from "./opcode"
|
import { OpCode } from "./opcode"
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
import { type Value, toValue, toNumber } from "./value"
|
import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value"
|
||||||
|
|
||||||
export class VM {
|
export class VM {
|
||||||
pc = 0
|
pc = 0
|
||||||
|
|
@ -37,11 +37,11 @@ export class VM {
|
||||||
async execute(instruction: Instruction) /* throws */ {
|
async execute(instruction: Instruction) /* throws */ {
|
||||||
switch (instruction.op) {
|
switch (instruction.op) {
|
||||||
case OpCode.PUSH:
|
case OpCode.PUSH:
|
||||||
const idx = instruction.operand as number
|
const constIdx = instruction.operand as number
|
||||||
const constant = this.constants[idx]
|
const constant = this.constants[constIdx]
|
||||||
|
|
||||||
if (!constant || constant.type === 'function_def')
|
if (!constant || constant.type === 'function_def')
|
||||||
throw new Error(`Invalid constant index: ${idx}`)
|
throw new Error(`Invalid constant index: ${constIdx}`)
|
||||||
|
|
||||||
this.stack.push(constant)
|
this.stack.push(constant)
|
||||||
break
|
break
|
||||||
|
|
@ -70,6 +70,147 @@ export class VM {
|
||||||
this.binaryOp((a, b) => toNumber(a) / toNumber(b))
|
this.binaryOp((a, b) => toNumber(a) / toNumber(b))
|
||||||
break
|
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:
|
default:
|
||||||
throw `Unknown op: ${instruction.op}`
|
throw `Unknown op: ${instruction.op}`
|
||||||
}
|
}
|
||||||
|
|
@ -81,4 +222,11 @@ export class VM {
|
||||||
const result = fn(a, b)
|
const result = fn(a, b)
|
||||||
this.stack.push({ type: 'number', value: result })
|
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 { run } from "#index"
|
||||||
import { toBytecode } from "#bytecode"
|
import { toBytecode } from "#bytecode"
|
||||||
|
|
||||||
test("adding numbers", async () => {
|
test("ADD - add two numbers", async () => {
|
||||||
const str = `
|
const str = `
|
||||||
PUSH 1
|
PUSH 1
|
||||||
PUSH 5
|
PUSH 5
|
||||||
|
|
@ -18,7 +18,7 @@ test("adding numbers", async () => {
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 })
|
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("subtracting numbers", async () => {
|
test("SUB - subtract two numbers", async () => {
|
||||||
const str = `
|
const str = `
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 2
|
PUSH 2
|
||||||
|
|
@ -27,7 +27,7 @@ test("subtracting numbers", async () => {
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("multiplying numbers", async () => {
|
test("MUL - multiply two numbers", async () => {
|
||||||
const str = `
|
const str = `
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 2
|
PUSH 2
|
||||||
|
|
@ -36,7 +36,7 @@ test("multiplying numbers", async () => {
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("dividing numbers", async () => {
|
test("DIV - divide two numbers", async () => {
|
||||||
const str = `
|
const str = `
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 2
|
PUSH 2
|
||||||
|
|
@ -52,3 +52,440 @@ test("dividing numbers", async () => {
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: Infinity })
|
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