add array bytecode API

This commit is contained in:
Chris Wanstrath 2025-10-07 12:53:40 -07:00
parent 3c97039faf
commit 060fa064fe
9 changed files with 1147 additions and 6 deletions

View File

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

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

View File

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

View File

@ -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)
} }
bytecode.labels = labelsByIndex if (labelsByIndex.size > 0)
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
View 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
}
}

View File

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

View File

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