add array bytecode API
This commit is contained in:
parent
3c97039faf
commit
060fa064fe
31
CLAUDE.md
31
CLAUDE.md
|
|
@ -38,7 +38,7 @@ No build step required - Bun runs TypeScript directly.
|
||||||
- Native function registry for TypeScript interop
|
- Native function registry for TypeScript interop
|
||||||
|
|
||||||
**Key subsystems**:
|
**Key subsystems**:
|
||||||
- **bytecode.ts**: Parser that converts human-readable bytecode strings to executable bytecode. Handles label resolution, constant pool management, and function definition parsing.
|
- **bytecode.ts**: Compiler that converts both string and array formats to executable bytecode. Handles label resolution, constant pool management, and function definition parsing. The `toBytecode()` function accepts either a string (human-readable) or typed array format (programmatic).
|
||||||
- **value.ts**: Tagged union Value type system with type coercion functions (toNumber, toString, isTrue, isEqual)
|
- **value.ts**: Tagged union Value type system with type coercion functions (toNumber, toString, isTrue, isEqual)
|
||||||
- **scope.ts**: Linked scope chain for variable resolution with lexical scoping
|
- **scope.ts**: Linked scope chain for variable resolution with lexical scoping
|
||||||
- **frame.ts**: Call frame tracking for function calls and break targets
|
- **frame.ts**: Call frame tracking for function calls and break targets
|
||||||
|
|
@ -72,7 +72,8 @@ Tests are organized by feature area:
|
||||||
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
||||||
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
||||||
- **native.test.ts**: Native function interop (sync and async)
|
- **native.test.ts**: Native function interop (sync and async)
|
||||||
- **bytecode.test.ts**: Bytecode parser, label resolution, constants
|
- **bytecode.test.ts**: Bytecode string parser, label resolution, constants
|
||||||
|
- **programmatic.test.ts**: Array format API, typed tuples, labels, functions
|
||||||
- **validator.test.ts**: Bytecode validation rules
|
- **validator.test.ts**: Bytecode validation rules
|
||||||
- **examples.test.ts**: Integration tests for example programs
|
- **examples.test.ts**: Integration tests for example programs
|
||||||
|
|
||||||
|
|
@ -86,6 +87,10 @@ When adding features:
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
### Writing Bytecode Tests
|
### Writing Bytecode Tests
|
||||||
|
|
||||||
|
ReefVM supports two bytecode formats: string and array.
|
||||||
|
|
||||||
|
**String format** (human-readable):
|
||||||
```typescript
|
```typescript
|
||||||
import { toBytecode, run } from "#reef"
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
|
@ -100,6 +105,28 @@ const result = await run(bytecode)
|
||||||
// result is { type: 'number', value: 42 }
|
// result is { type: 'number', value: 42 }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Array format** (programmatic, type-safe):
|
||||||
|
```typescript
|
||||||
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 42],
|
||||||
|
["STORE", "x"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// result is { type: 'number', value: 42 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Array format features:
|
||||||
|
- Typed tuples for compile-time type checking
|
||||||
|
- Labels defined as `[".label:"]` (single-element arrays with colon suffix)
|
||||||
|
- Label references as strings: `["JUMP", ".label"]` (no colon in references)
|
||||||
|
- Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]`
|
||||||
|
- See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples
|
||||||
|
|
||||||
### Native Function Registration
|
### Native Function Registration
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
|
||||||
129
GUIDE.md
129
GUIDE.md
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
Quick reference for compiling to Reef bytecode.
|
Quick reference for compiling to Reef bytecode.
|
||||||
|
|
||||||
|
## Bytecode Formats
|
||||||
|
|
||||||
|
ReefVM supports two bytecode formats:
|
||||||
|
|
||||||
|
1. **String format**: Human-readable text with opcodes and operands
|
||||||
|
2. **Array format**: TypeScript arrays with typed tuples for programmatic generation
|
||||||
|
|
||||||
|
Both formats are compiled using the same `toBytecode()` function.
|
||||||
|
|
||||||
## Bytecode Syntax
|
## Bytecode Syntax
|
||||||
|
|
||||||
### Instructions
|
### Instructions
|
||||||
|
|
@ -34,6 +43,126 @@ OPCODE operand ; comment
|
||||||
**Native function names**: Registered TypeScript functions
|
**Native function names**: Registered TypeScript functions
|
||||||
- `CALL_NATIVE print`
|
- `CALL_NATIVE print`
|
||||||
|
|
||||||
|
## Array Format
|
||||||
|
|
||||||
|
The programmatic array format uses TypeScript tuples for type safety:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 42], // Atom values: number | string | boolean | null
|
||||||
|
["STORE", "x"], // Variable names as strings
|
||||||
|
["LOAD", "x"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operand Types in Array Format
|
||||||
|
|
||||||
|
**Atoms** (`number | string | boolean | null`): Constants for PUSH
|
||||||
|
```typescript
|
||||||
|
["PUSH", 42]
|
||||||
|
["PUSH", "hello"]
|
||||||
|
["PUSH", true]
|
||||||
|
["PUSH", null]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Variable names**: String identifiers
|
||||||
|
```typescript
|
||||||
|
["LOAD", "counter"]
|
||||||
|
["STORE", "result"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Label definitions**: Single-element arrays starting with `.` and ending with `:`
|
||||||
|
```typescript
|
||||||
|
[".loop:"]
|
||||||
|
[".end:"]
|
||||||
|
[".function_body:"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Label references**: Strings in jump/function instructions
|
||||||
|
```typescript
|
||||||
|
["JUMP", ".loop"]
|
||||||
|
["JUMP_IF_FALSE", ".end"]
|
||||||
|
["MAKE_FUNCTION", ["x", "y"], ".body"]
|
||||||
|
["PUSH_TRY", ".catch"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Counts**: Numbers for array/dict construction
|
||||||
|
```typescript
|
||||||
|
["MAKE_ARRAY", 3] // Pop 3 items
|
||||||
|
["MAKE_DICT", 2] // Pop 2 key-value pairs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Native function names**: Strings for registered functions
|
||||||
|
```typescript
|
||||||
|
["CALL_NATIVE", "print"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functions in Array Format
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic function
|
||||||
|
["MAKE_FUNCTION", ["x", "y"], ".body"]
|
||||||
|
|
||||||
|
// With defaults
|
||||||
|
["MAKE_FUNCTION", ["x", "y=10"], ".body"]
|
||||||
|
|
||||||
|
// Variadic
|
||||||
|
["MAKE_FUNCTION", ["...args"], ".body"]
|
||||||
|
|
||||||
|
// Named args
|
||||||
|
["MAKE_FUNCTION", ["@opts"], ".body"]
|
||||||
|
|
||||||
|
// Mixed
|
||||||
|
["MAKE_FUNCTION", ["x", "y=5", "...rest", "@opts"], ".body"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const factorial = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["n", "acc=1"], ".fact"],
|
||||||
|
["STORE", "factorial"],
|
||||||
|
["JUMP", ".main"],
|
||||||
|
|
||||||
|
[".fact:"],
|
||||||
|
["LOAD", "n"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["LTE"],
|
||||||
|
["JUMP_IF_FALSE", ".recurse"],
|
||||||
|
["LOAD", "acc"],
|
||||||
|
["RETURN"],
|
||||||
|
|
||||||
|
[".recurse:"],
|
||||||
|
["LOAD", "factorial"],
|
||||||
|
["LOAD", "n"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["SUB"],
|
||||||
|
["LOAD", "n"],
|
||||||
|
["LOAD", "acc"],
|
||||||
|
["MUL"],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 0],
|
||||||
|
["TAIL_CALL"],
|
||||||
|
|
||||||
|
[".main:"],
|
||||||
|
["LOAD", "factorial"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(factorial) // { type: "number", value: 120 }
|
||||||
|
```
|
||||||
|
|
||||||
|
## String Format
|
||||||
|
|
||||||
### Functions
|
### Functions
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION (x y) .body ; Basic
|
MAKE_FUNCTION (x y) .body ; Basic
|
||||||
|
|
|
||||||
98
examples/programmatic.ts
Normal file
98
examples/programmatic.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
// Example 1: Simple arithmetic
|
||||||
|
const arithmetic = toBytecode([
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 10],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log("5 + 10 =", (await run(arithmetic)).value)
|
||||||
|
|
||||||
|
// Example 2: Loop with labels
|
||||||
|
const loop = toBytecode([
|
||||||
|
["PUSH", 0],
|
||||||
|
["STORE", "counter"],
|
||||||
|
[".loop:"],
|
||||||
|
["LOAD", "counter"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["LT"],
|
||||||
|
["JUMP_IF_FALSE", ".end"],
|
||||||
|
["LOAD", "counter"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["ADD"],
|
||||||
|
["STORE", "counter"],
|
||||||
|
["JUMP", ".loop"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "counter"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log("Counter result:", (await run(loop)).value)
|
||||||
|
|
||||||
|
// Example 3: Function with defaults
|
||||||
|
const functionExample = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x", "y=10"], ".add_body"],
|
||||||
|
["STORE", "add"],
|
||||||
|
["JUMP", ".after"],
|
||||||
|
[".add_body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["LOAD", "y"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".after:"],
|
||||||
|
["LOAD", "add"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log("add(5) with default y=10:", (await run(functionExample)).value)
|
||||||
|
|
||||||
|
// Example 4: Variadic function
|
||||||
|
const variadic = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["...args"], ".sum_body"],
|
||||||
|
["STORE", "sum"],
|
||||||
|
["JUMP", ".after"],
|
||||||
|
[".sum_body:"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["STORE", "total"],
|
||||||
|
["LOAD", "args"],
|
||||||
|
["ARRAY_LEN"],
|
||||||
|
["STORE", "len"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["STORE", "i"],
|
||||||
|
[".loop:"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["LOAD", "len"],
|
||||||
|
["LT"],
|
||||||
|
["JUMP_IF_FALSE", ".done"],
|
||||||
|
["LOAD", "total"],
|
||||||
|
["LOAD", "args"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["ARRAY_GET"],
|
||||||
|
["ADD"],
|
||||||
|
["STORE", "total"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["ADD"],
|
||||||
|
["STORE", "i"],
|
||||||
|
["JUMP", ".loop"],
|
||||||
|
[".done:"],
|
||||||
|
["LOAD", "total"],
|
||||||
|
["RETURN"],
|
||||||
|
[".after:"],
|
||||||
|
["LOAD", "sum"],
|
||||||
|
["PUSH", 10],
|
||||||
|
["PUSH", 20],
|
||||||
|
["PUSH", 30],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log("sum(10, 20, 30):", (await run(variadic)).value)
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
"name": "reefvm",
|
"name": "reefvm",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"check": "bunx tsc --noEmit"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
293
src/bytecode.ts
293
src/bytecode.ts
|
|
@ -16,6 +16,68 @@ export type Constant =
|
||||||
| Value
|
| Value
|
||||||
| FunctionDef
|
| FunctionDef
|
||||||
|
|
||||||
|
type Atom = number | string | boolean | null
|
||||||
|
|
||||||
|
type InstructionTuple =
|
||||||
|
// Stack
|
||||||
|
| ["PUSH", Atom]
|
||||||
|
| ["POP"]
|
||||||
|
| ["DUP"]
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
| ["LOAD", string]
|
||||||
|
| ["STORE", string]
|
||||||
|
|
||||||
|
// Arithmetic
|
||||||
|
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
||||||
|
|
||||||
|
// Comparison
|
||||||
|
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
||||||
|
|
||||||
|
// Logical
|
||||||
|
| ["NOT"]
|
||||||
|
|
||||||
|
// Control flow
|
||||||
|
| ["JUMP", string | number]
|
||||||
|
| ["JUMP_IF_FALSE", string | number]
|
||||||
|
| ["JUMP_IF_TRUE", string | number]
|
||||||
|
| ["BREAK"]
|
||||||
|
|
||||||
|
// Exception handling
|
||||||
|
| ["PUSH_TRY", string | number]
|
||||||
|
| ["PUSH_FINALLY", string | number]
|
||||||
|
| ["POP_TRY"]
|
||||||
|
| ["THROW"]
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
| ["MAKE_FUNCTION", string[], string | number]
|
||||||
|
| ["CALL"]
|
||||||
|
| ["TAIL_CALL"]
|
||||||
|
| ["RETURN"]
|
||||||
|
|
||||||
|
// Arrays
|
||||||
|
| ["MAKE_ARRAY", number]
|
||||||
|
| ["ARRAY_GET"]
|
||||||
|
| ["ARRAY_SET"]
|
||||||
|
| ["ARRAY_PUSH"]
|
||||||
|
| ["ARRAY_LEN"]
|
||||||
|
|
||||||
|
// Dicts
|
||||||
|
| ["MAKE_DICT", number]
|
||||||
|
| ["DICT_GET"]
|
||||||
|
| ["DICT_SET"]
|
||||||
|
| ["DICT_HAS"]
|
||||||
|
|
||||||
|
// Native
|
||||||
|
| ["CALL_NATIVE", string]
|
||||||
|
|
||||||
|
// Special
|
||||||
|
| ["HALT"]
|
||||||
|
|
||||||
|
type LabelDefinition = [string] // Just ".label_name:"
|
||||||
|
|
||||||
|
export type ProgramItem = InstructionTuple | LabelDefinition
|
||||||
|
|
||||||
//
|
//
|
||||||
// Parse bytecode from human-readable string format.
|
// Parse bytecode from human-readable string format.
|
||||||
// Operand types are determined by prefix/literal:
|
// Operand types are determined by prefix/literal:
|
||||||
|
|
@ -100,7 +162,206 @@ function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
||||||
return { params, defaults, variadic, named: named }
|
return { params, defaults, variadic, named: named }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toBytecode(str: string): Bytecode /* throws */ {
|
function isLabelDefinition(item: ProgramItem): item is LabelDefinition {
|
||||||
|
return item.length === 1 && typeof item[0] === "string" && item[0].startsWith(".") && item[0].endsWith(":")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLabelReference(value: string | number): value is string {
|
||||||
|
return typeof value === "string" && value.startsWith(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFunctionParamsFromArray(params: string[]): {
|
||||||
|
params: string[]
|
||||||
|
defaults: Record<string, number>
|
||||||
|
variadic: boolean
|
||||||
|
named: boolean
|
||||||
|
defaultConstants: Constant[]
|
||||||
|
} {
|
||||||
|
const resultParams: string[] = []
|
||||||
|
const defaults: Record<string, number> = {}
|
||||||
|
const defaultConstants: Constant[] = []
|
||||||
|
let variadic = false
|
||||||
|
let named = false
|
||||||
|
|
||||||
|
for (const param of params) {
|
||||||
|
if (param.startsWith("@")) {
|
||||||
|
named = true
|
||||||
|
resultParams.push(param.slice(1))
|
||||||
|
} else if (param.startsWith("...")) {
|
||||||
|
variadic = true
|
||||||
|
resultParams.push(param.slice(3))
|
||||||
|
} else if (param.includes("=")) {
|
||||||
|
const [name, defaultValue] = param.split("=").map(s => s.trim())
|
||||||
|
resultParams.push(name!)
|
||||||
|
|
||||||
|
if (/^-?\d+(\.\d+)?$/.test(defaultValue!)) {
|
||||||
|
defaultConstants.push(toValue(parseFloat(defaultValue!)))
|
||||||
|
} else if (defaultValue === "true") {
|
||||||
|
defaultConstants.push(toValue(true))
|
||||||
|
} else if (defaultValue === "false") {
|
||||||
|
defaultConstants.push(toValue(false))
|
||||||
|
} else if (defaultValue === "null") {
|
||||||
|
defaultConstants.push(toValue(null))
|
||||||
|
} else if (/^['"].*['"]$/.test(defaultValue!)) {
|
||||||
|
defaultConstants.push(toValue(defaultValue!.slice(1, -1)))
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid default value: ${defaultValue}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[name!] = -1
|
||||||
|
} else {
|
||||||
|
resultParams.push(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { params: resultParams, defaults, variadic, named, defaultConstants }
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
||||||
|
const constants: Constant[] = []
|
||||||
|
const instructions: any[] = []
|
||||||
|
const labels = new Map<string, number>()
|
||||||
|
|
||||||
|
// First pass: collect labels
|
||||||
|
const filteredProgram: InstructionTuple[] = []
|
||||||
|
for (const item of program) {
|
||||||
|
if (isLabelDefinition(item)) {
|
||||||
|
const labelName = item[0].slice(1, -1) // Remove . prefix and : suffix
|
||||||
|
labels.set(labelName, filteredProgram.length)
|
||||||
|
} else {
|
||||||
|
filteredProgram.push(item as InstructionTuple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: build instructions
|
||||||
|
for (let i = 0; i < filteredProgram.length; i++) {
|
||||||
|
const item = filteredProgram[i]!
|
||||||
|
const op = item[0] as string
|
||||||
|
const opCode = OpCode[op as keyof typeof OpCode]
|
||||||
|
|
||||||
|
if (opCode === undefined) {
|
||||||
|
throw new Error(`Unknown opcode: ${op}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let operandValue: number | string | undefined = undefined
|
||||||
|
|
||||||
|
if (item.length > 1) {
|
||||||
|
const operand = item[1]
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case "PUSH":
|
||||||
|
constants.push(toValue(operand as Atom))
|
||||||
|
operandValue = constants.length - 1
|
||||||
|
break
|
||||||
|
|
||||||
|
case "MAKE_FUNCTION": {
|
||||||
|
const params = operand as string[]
|
||||||
|
const body = item[2]
|
||||||
|
|
||||||
|
if (body === undefined) {
|
||||||
|
throw new Error("MAKE_FUNCTION requires body address")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { params: resultParams, defaults, variadic, named, defaultConstants } = parseFunctionParamsFromArray(params)
|
||||||
|
|
||||||
|
const defaultIndices: Record<string, number> = {}
|
||||||
|
for (const [paramName, _] of Object.entries(defaults)) {
|
||||||
|
const defaultConst = defaultConstants.shift()!
|
||||||
|
constants.push(defaultConst)
|
||||||
|
defaultIndices[paramName] = constants.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyAddress: number
|
||||||
|
if (isLabelReference(body)) {
|
||||||
|
const labelName = body.slice(1)
|
||||||
|
const labelPos = labels.get(labelName)
|
||||||
|
if (labelPos === undefined) {
|
||||||
|
throw new Error(`Undefined label: ${labelName}`)
|
||||||
|
}
|
||||||
|
bodyAddress = labelPos
|
||||||
|
} else {
|
||||||
|
bodyAddress = body as number
|
||||||
|
}
|
||||||
|
|
||||||
|
constants.push({
|
||||||
|
type: "function_def",
|
||||||
|
params: resultParams,
|
||||||
|
defaults: defaultIndices,
|
||||||
|
body: bodyAddress,
|
||||||
|
variadic,
|
||||||
|
named
|
||||||
|
})
|
||||||
|
|
||||||
|
operandValue = constants.length - 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "JUMP":
|
||||||
|
case "JUMP_IF_FALSE":
|
||||||
|
case "JUMP_IF_TRUE": {
|
||||||
|
if (isLabelReference(operand as string | number)) {
|
||||||
|
const labelName = (operand as string).slice(1)
|
||||||
|
const labelPos = labels.get(labelName)
|
||||||
|
if (labelPos === undefined) {
|
||||||
|
throw new Error(`Undefined label: ${labelName}`)
|
||||||
|
}
|
||||||
|
operandValue = labelPos - (i + 1)
|
||||||
|
} else {
|
||||||
|
operandValue = operand as number
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "PUSH_TRY":
|
||||||
|
case "PUSH_FINALLY": {
|
||||||
|
if (isLabelReference(operand as string | number)) {
|
||||||
|
const labelName = (operand as string).slice(1)
|
||||||
|
const labelPos = labels.get(labelName)
|
||||||
|
if (labelPos === undefined) {
|
||||||
|
throw new Error(`Undefined label: ${labelName}`)
|
||||||
|
}
|
||||||
|
operandValue = labelPos
|
||||||
|
} else {
|
||||||
|
operandValue = operand as number
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "LOAD":
|
||||||
|
case "STORE":
|
||||||
|
case "CALL_NATIVE":
|
||||||
|
operandValue = operand as string
|
||||||
|
break
|
||||||
|
|
||||||
|
case "MAKE_ARRAY":
|
||||||
|
case "MAKE_DICT":
|
||||||
|
operandValue = operand as number
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected operand for ${op}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions.push({
|
||||||
|
op: opCode,
|
||||||
|
operand: operandValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsByIndex = new Map<number, string>()
|
||||||
|
for (const [name, index] of labels.entries()) {
|
||||||
|
labelsByIndex.set(index, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
instructions,
|
||||||
|
constants,
|
||||||
|
labels: labelsByIndex.size > 0 ? labelsByIndex : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
||||||
const lines = str.trim().split("\n")
|
const lines = str.trim().split("\n")
|
||||||
|
|
||||||
// First pass: collect labels and their positions
|
// First pass: collect labels and their positions
|
||||||
|
|
@ -246,7 +507,37 @@ export function toBytecode(str: string): Bytecode /* throws */ {
|
||||||
for (const [name, index] of labels.entries()) {
|
for (const [name, index] of labels.entries()) {
|
||||||
labelsByIndex.set(index, name)
|
labelsByIndex.set(index, name)
|
||||||
}
|
}
|
||||||
|
if (labelsByIndex.size > 0)
|
||||||
bytecode.labels = labelsByIndex
|
bytecode.labels = labelsByIndex
|
||||||
|
|
||||||
return bytecode
|
return bytecode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile bytecode from either a string or programmatic array format.
|
||||||
|
*
|
||||||
|
* String format:
|
||||||
|
* ```
|
||||||
|
* PUSH 42
|
||||||
|
* STORE x
|
||||||
|
* LOAD x
|
||||||
|
* HALT
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Array format:
|
||||||
|
* ```
|
||||||
|
* [
|
||||||
|
* ["PUSH", 42],
|
||||||
|
* ["STORE", "x"],
|
||||||
|
* ["LOAD", "x"],
|
||||||
|
* ["HALT"]
|
||||||
|
* ]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function toBytecode(input: string | ProgramItem[]): Bytecode {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
return toBytecodeFromString(input)
|
||||||
|
} else {
|
||||||
|
return toBytecodeFromArray(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
279
src/programmatic.ts
Normal file
279
src/programmatic.ts
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
import type { Bytecode, Constant } from "./bytecode"
|
||||||
|
import { OpCode } from "./opcode"
|
||||||
|
import { toValue } from "./value"
|
||||||
|
|
||||||
|
// Instruction types
|
||||||
|
type PrimitiveValue = number | string | boolean | null
|
||||||
|
|
||||||
|
type InstructionTuple =
|
||||||
|
// Stack
|
||||||
|
| ["PUSH", PrimitiveValue]
|
||||||
|
| ["POP"]
|
||||||
|
| ["DUP"]
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
| ["LOAD", string]
|
||||||
|
| ["STORE", string]
|
||||||
|
|
||||||
|
// Arithmetic
|
||||||
|
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
||||||
|
|
||||||
|
// Comparison
|
||||||
|
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
||||||
|
|
||||||
|
// Logical
|
||||||
|
| ["NOT"]
|
||||||
|
|
||||||
|
// Control flow
|
||||||
|
| ["JUMP", string | number]
|
||||||
|
| ["JUMP_IF_FALSE", string | number]
|
||||||
|
| ["JUMP_IF_TRUE", string | number]
|
||||||
|
| ["BREAK"]
|
||||||
|
|
||||||
|
// Exception handling
|
||||||
|
| ["PUSH_TRY", string | number]
|
||||||
|
| ["PUSH_FINALLY", string | number]
|
||||||
|
| ["POP_TRY"]
|
||||||
|
| ["THROW"]
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
| ["MAKE_FUNCTION", string[], string | number]
|
||||||
|
| ["CALL"]
|
||||||
|
| ["TAIL_CALL"]
|
||||||
|
| ["RETURN"]
|
||||||
|
|
||||||
|
// Arrays
|
||||||
|
| ["MAKE_ARRAY", number]
|
||||||
|
| ["ARRAY_GET"]
|
||||||
|
| ["ARRAY_SET"]
|
||||||
|
| ["ARRAY_PUSH"]
|
||||||
|
| ["ARRAY_LEN"]
|
||||||
|
|
||||||
|
// Dicts
|
||||||
|
| ["MAKE_DICT", number]
|
||||||
|
| ["DICT_GET"]
|
||||||
|
| ["DICT_SET"]
|
||||||
|
| ["DICT_HAS"]
|
||||||
|
|
||||||
|
// Native
|
||||||
|
| ["CALL_NATIVE", string]
|
||||||
|
|
||||||
|
// Special
|
||||||
|
| ["HALT"]
|
||||||
|
|
||||||
|
type LabelDefinition = [string] // Just ".label_name"
|
||||||
|
|
||||||
|
type ProgramItem = InstructionTuple | LabelDefinition
|
||||||
|
|
||||||
|
function isLabelDefinition(item: ProgramItem): item is LabelDefinition {
|
||||||
|
return item.length === 1 && typeof item[0] === "string" && item[0].startsWith(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLabelReference(value: string | number): value is string {
|
||||||
|
return typeof value === "string" && value.startsWith(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFunctionParams(params: string[]): {
|
||||||
|
params: string[]
|
||||||
|
defaults: Record<string, number>
|
||||||
|
variadic: boolean
|
||||||
|
named: boolean
|
||||||
|
defaultConstants: Constant[]
|
||||||
|
} {
|
||||||
|
const resultParams: string[] = []
|
||||||
|
const defaults: Record<string, number> = {}
|
||||||
|
const defaultConstants: Constant[] = []
|
||||||
|
let variadic = false
|
||||||
|
let named = false
|
||||||
|
|
||||||
|
for (const param of params) {
|
||||||
|
if (param.startsWith("@")) {
|
||||||
|
// Named parameter
|
||||||
|
named = true
|
||||||
|
resultParams.push(param.slice(1))
|
||||||
|
} else if (param.startsWith("...")) {
|
||||||
|
// Variadic parameter
|
||||||
|
variadic = true
|
||||||
|
resultParams.push(param.slice(3))
|
||||||
|
} else if (param.includes("=")) {
|
||||||
|
// Default parameter
|
||||||
|
const [name, defaultValue] = param.split("=").map(s => s.trim())
|
||||||
|
resultParams.push(name!)
|
||||||
|
|
||||||
|
// Parse default value
|
||||||
|
if (/^-?\d+(\.\d+)?$/.test(defaultValue!)) {
|
||||||
|
defaultConstants.push(toValue(parseFloat(defaultValue!)))
|
||||||
|
} else if (defaultValue === "true") {
|
||||||
|
defaultConstants.push(toValue(true))
|
||||||
|
} else if (defaultValue === "false") {
|
||||||
|
defaultConstants.push(toValue(false))
|
||||||
|
} else if (defaultValue === "null") {
|
||||||
|
defaultConstants.push(toValue(null))
|
||||||
|
} else if (/^['"].*['"]$/.test(defaultValue!)) {
|
||||||
|
defaultConstants.push(toValue(defaultValue!.slice(1, -1)))
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid default value: ${defaultValue}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[name!] = -1 // Will be fixed after we know constant indices
|
||||||
|
} else {
|
||||||
|
// Regular parameter
|
||||||
|
resultParams.push(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { params: resultParams, defaults, variadic, named, defaultConstants }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromInstructions(program: ProgramItem[]): Bytecode {
|
||||||
|
const constants: Constant[] = []
|
||||||
|
const instructions: any[] = []
|
||||||
|
const labels = new Map<string, number>()
|
||||||
|
|
||||||
|
// First pass: collect labels and their positions
|
||||||
|
const filteredProgram: InstructionTuple[] = []
|
||||||
|
for (const item of program) {
|
||||||
|
if (isLabelDefinition(item)) {
|
||||||
|
const labelName = item[0].slice(1) // Remove leading "."
|
||||||
|
labels.set(labelName, filteredProgram.length)
|
||||||
|
} else {
|
||||||
|
filteredProgram.push(item as InstructionTuple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: build instructions
|
||||||
|
for (let i = 0; i < filteredProgram.length; i++) {
|
||||||
|
const item = filteredProgram[i]!
|
||||||
|
const op = item[0] as string
|
||||||
|
const opCode = OpCode[op as keyof typeof OpCode]
|
||||||
|
|
||||||
|
if (opCode === undefined) {
|
||||||
|
throw new Error(`Unknown opcode: ${op}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let operandValue: number | string | undefined = undefined
|
||||||
|
|
||||||
|
if (item.length > 1) {
|
||||||
|
const operand = item[1]
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case "PUSH":
|
||||||
|
// Add to constants pool
|
||||||
|
constants.push(toValue(operand as PrimitiveValue))
|
||||||
|
operandValue = constants.length - 1
|
||||||
|
break
|
||||||
|
|
||||||
|
case "MAKE_FUNCTION": {
|
||||||
|
const params = operand as string[]
|
||||||
|
const body = item[2]
|
||||||
|
|
||||||
|
if (body === undefined) {
|
||||||
|
throw new Error("MAKE_FUNCTION requires body address")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { params: resultParams, defaults, variadic, named, defaultConstants } = parseFunctionParams(params)
|
||||||
|
|
||||||
|
// Add default constants to pool and update indices
|
||||||
|
const defaultIndices: Record<string, number> = {}
|
||||||
|
for (const [paramName, _] of Object.entries(defaults)) {
|
||||||
|
const defaultConst = defaultConstants.shift()!
|
||||||
|
constants.push(defaultConst)
|
||||||
|
defaultIndices[paramName] = constants.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve body label or use numeric value
|
||||||
|
let bodyAddress: number
|
||||||
|
if (isLabelReference(body)) {
|
||||||
|
const labelName = body.slice(1)
|
||||||
|
const labelPos = labels.get(labelName)
|
||||||
|
if (labelPos === undefined) {
|
||||||
|
throw new Error(`Undefined label: ${labelName}`)
|
||||||
|
}
|
||||||
|
bodyAddress = labelPos
|
||||||
|
} else {
|
||||||
|
bodyAddress = body as number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add function definition to constants
|
||||||
|
constants.push({
|
||||||
|
type: "function_def",
|
||||||
|
params: resultParams,
|
||||||
|
defaults: defaultIndices,
|
||||||
|
body: bodyAddress,
|
||||||
|
variadic,
|
||||||
|
named
|
||||||
|
})
|
||||||
|
|
||||||
|
operandValue = constants.length - 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "JUMP":
|
||||||
|
case "JUMP_IF_FALSE":
|
||||||
|
case "JUMP_IF_TRUE": {
|
||||||
|
// Relative jump
|
||||||
|
if (isLabelReference(operand as string | number)) {
|
||||||
|
const labelName = (operand as string).slice(1)
|
||||||
|
const labelPos = labels.get(labelName)
|
||||||
|
if (labelPos === undefined) {
|
||||||
|
throw new Error(`Undefined label: ${labelName}`)
|
||||||
|
}
|
||||||
|
operandValue = labelPos - (i + 1) // Relative offset
|
||||||
|
} else {
|
||||||
|
operandValue = operand as number
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "PUSH_TRY":
|
||||||
|
case "PUSH_FINALLY": {
|
||||||
|
// Absolute address
|
||||||
|
if (isLabelReference(operand as string | number)) {
|
||||||
|
const labelName = (operand as string).slice(1)
|
||||||
|
const labelPos = labels.get(labelName)
|
||||||
|
if (labelPos === undefined) {
|
||||||
|
throw new Error(`Undefined label: ${labelName}`)
|
||||||
|
}
|
||||||
|
operandValue = labelPos
|
||||||
|
} else {
|
||||||
|
operandValue = operand as number
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "LOAD":
|
||||||
|
case "STORE":
|
||||||
|
case "CALL_NATIVE":
|
||||||
|
// String operand
|
||||||
|
operandValue = operand as string
|
||||||
|
break
|
||||||
|
|
||||||
|
case "MAKE_ARRAY":
|
||||||
|
case "MAKE_DICT":
|
||||||
|
// Numeric operand
|
||||||
|
operandValue = operand as number
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected operand for ${op}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instructions.push({
|
||||||
|
op: opCode,
|
||||||
|
operand: operandValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build labels map for debugger (instruction index -> label name)
|
||||||
|
const labelsByIndex = new Map<number, string>()
|
||||||
|
for (const [name, index] of labels.entries()) {
|
||||||
|
labelsByIndex.set(index, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
instructions,
|
||||||
|
constants,
|
||||||
|
labels: labelsByIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export type Value =
|
||||||
body: number,
|
body: number,
|
||||||
parentScope: Scope,
|
parentScope: Scope,
|
||||||
variadic: boolean,
|
variadic: boolean,
|
||||||
named: boolean
|
named: boolean,
|
||||||
|
value: '<function>'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Dict = Map<string, Value>
|
export type Dict = Map<string, Value>
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,8 @@ export class VM {
|
||||||
body: fnDef.body,
|
body: fnDef.body,
|
||||||
variadic: fnDef.variadic,
|
variadic: fnDef.variadic,
|
||||||
named: fnDef.named,
|
named: fnDef.named,
|
||||||
parentScope: this.scope
|
parentScope: this.scope,
|
||||||
|
value: '<function>'
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
||||||
312
tests/programmatic.test.ts
Normal file
312
tests/programmatic.test.ts
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
describe("Programmatic Bytecode API", () => {
|
||||||
|
test("basic stack operations", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 42],
|
||||||
|
["DUP"],
|
||||||
|
["POP"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 42 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("arithmetic operations", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 10],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 15 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("variables", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 100],
|
||||||
|
["STORE", "x"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["PUSH", 50],
|
||||||
|
["SUB"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 50 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("labels and jumps", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 10],
|
||||||
|
["GT"],
|
||||||
|
["JUMP_IF_TRUE", ".skip"],
|
||||||
|
["PUSH", 999],
|
||||||
|
[".skip:"],
|
||||||
|
["PUSH", 42],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 42 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("loop with labels", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 0],
|
||||||
|
["STORE", "i"],
|
||||||
|
[".loop:"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["PUSH", 3],
|
||||||
|
["LT"],
|
||||||
|
["JUMP_IF_FALSE", ".end"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["ADD"],
|
||||||
|
["STORE", "i"],
|
||||||
|
["JUMP", ".loop"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 3 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("simple function", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x", "y"], ".add_body"],
|
||||||
|
["STORE", "add"],
|
||||||
|
["JUMP", ".after_fn"],
|
||||||
|
[".add_body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["LOAD", "y"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".after_fn:"],
|
||||||
|
["LOAD", "add"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 10],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 15 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("function with defaults", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x", "y=10"], ".fn_body"],
|
||||||
|
["STORE", "fn"],
|
||||||
|
["JUMP", ".after"],
|
||||||
|
[".fn_body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["LOAD", "y"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".after:"],
|
||||||
|
["LOAD", "fn"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 15 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("variadic function", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["...args"], ".sum_body"],
|
||||||
|
["STORE", "sum"],
|
||||||
|
["JUMP", ".after"],
|
||||||
|
[".sum_body:"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["STORE", "total"],
|
||||||
|
["LOAD", "args"],
|
||||||
|
["ARRAY_LEN"],
|
||||||
|
["STORE", "len"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["STORE", "i"],
|
||||||
|
[".loop:"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["LOAD", "len"],
|
||||||
|
["LT"],
|
||||||
|
["JUMP_IF_FALSE", ".done"],
|
||||||
|
["LOAD", "total"],
|
||||||
|
["LOAD", "args"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["ARRAY_GET"],
|
||||||
|
["ADD"],
|
||||||
|
["STORE", "total"],
|
||||||
|
["LOAD", "i"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["ADD"],
|
||||||
|
["STORE", "i"],
|
||||||
|
["JUMP", ".loop"],
|
||||||
|
[".done:"],
|
||||||
|
["LOAD", "total"],
|
||||||
|
["RETURN"],
|
||||||
|
[".after:"],
|
||||||
|
["LOAD", "sum"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 6 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("named parameters", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x", "@opts"], ".fn_body"],
|
||||||
|
["STORE", "fn"],
|
||||||
|
["JUMP", ".after"],
|
||||||
|
[".fn_body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".after:"],
|
||||||
|
["LOAD", "fn"],
|
||||||
|
["PUSH", 42],
|
||||||
|
["PUSH", "verbose"],
|
||||||
|
["PUSH", true],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 1],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 42 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("arrays", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 3],
|
||||||
|
["MAKE_ARRAY", 3],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result.type).toBe("array")
|
||||||
|
if (result.type === "array") {
|
||||||
|
expect(result.value).toHaveLength(3)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("dicts", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", "name"],
|
||||||
|
["PUSH", "Alice"],
|
||||||
|
["PUSH", "age"],
|
||||||
|
["PUSH", 30],
|
||||||
|
["MAKE_DICT", 2],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result.type).toBe("dict")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("exception handling", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH_TRY", ".catch"],
|
||||||
|
["PUSH", "error!"],
|
||||||
|
["THROW"],
|
||||||
|
["POP_TRY"],
|
||||||
|
[".catch:"],
|
||||||
|
["STORE", "err"],
|
||||||
|
["LOAD", "err"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "string", value: "error!" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("string values", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", "hello"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "string", value: "hello" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("boolean values", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", true],
|
||||||
|
["NOT"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "boolean", value: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("null values", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", null],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "null", value: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("tail call", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["n", "acc"], ".factorial_body"],
|
||||||
|
["STORE", "factorial"],
|
||||||
|
["JUMP", ".main"],
|
||||||
|
[".factorial_body:"],
|
||||||
|
["LOAD", "n"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["LTE"],
|
||||||
|
["JUMP_IF_FALSE", ".recurse"],
|
||||||
|
["LOAD", "acc"],
|
||||||
|
["RETURN"],
|
||||||
|
[".recurse:"],
|
||||||
|
["LOAD", "factorial"],
|
||||||
|
["LOAD", "n"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["SUB"],
|
||||||
|
["LOAD", "n"],
|
||||||
|
["LOAD", "acc"],
|
||||||
|
["MUL"],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 0],
|
||||||
|
["TAIL_CALL"],
|
||||||
|
[".main:"],
|
||||||
|
["LOAD", "factorial"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
expect(result).toEqual({ type: "number", value: 120 })
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user