diff --git a/CLAUDE.md b/CLAUDE.md index ee664ec..33b0ad9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ No build step required - Bun runs TypeScript directly. - Native function registry for TypeScript interop **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) - **scope.ts**: Linked scope chain for variable resolution with lexical scoping - **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 - **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers - **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 - **examples.test.ts**: Integration tests for example programs @@ -86,6 +87,10 @@ When adding features: ## Common Patterns ### Writing Bytecode Tests + +ReefVM supports two bytecode formats: string and array. + +**String format** (human-readable): ```typescript import { toBytecode, run } from "#reef" @@ -100,6 +105,28 @@ const result = await run(bytecode) // 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 ```typescript const vm = new VM(bytecode) diff --git a/GUIDE.md b/GUIDE.md index d6f6762..6541c29 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -2,6 +2,15 @@ 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 ### Instructions @@ -34,6 +43,126 @@ OPCODE operand ; comment **Native function names**: Registered TypeScript functions - `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 ``` MAKE_FUNCTION (x y) .body ; Basic diff --git a/examples/programmatic.ts b/examples/programmatic.ts new file mode 100644 index 0000000..bdc4aad --- /dev/null +++ b/examples/programmatic.ts @@ -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) diff --git a/package.json b/package.json index 897b2a7..30337a2 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "reefvm", "module": "src/index.ts", "type": "module", + "scripts": { + "check": "bunx tsc --noEmit" + }, "devDependencies": { "@types/bun": "latest" }, diff --git a/src/bytecode.ts b/src/bytecode.ts index d157b94..5acab21 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -16,6 +16,68 @@ export type Constant = | Value | 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. // Operand types are determined by prefix/literal: @@ -100,7 +162,206 @@ function parseFunctionParams(paramStr: string, constants: Constant[]): { 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 + variadic: boolean + named: boolean + defaultConstants: Constant[] +} { + const resultParams: string[] = [] + const defaults: Record = {} + 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() + + // 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 = {} + 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() + 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") // 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()) { labelsByIndex.set(index, name) } - bytecode.labels = labelsByIndex + if (labelsByIndex.size > 0) + bytecode.labels = labelsByIndex 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) + } +} diff --git a/src/programmatic.ts b/src/programmatic.ts new file mode 100644 index 0000000..969b017 --- /dev/null +++ b/src/programmatic.ts @@ -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 + variadic: boolean + named: boolean + defaultConstants: Constant[] +} { + const resultParams: string[] = [] + const defaults: Record = {} + 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() + + // 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 = {} + 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() + for (const [name, index] of labels.entries()) { + labelsByIndex.set(index, name) + } + + return { + instructions, + constants, + labels: labelsByIndex + } +} diff --git a/src/value.ts b/src/value.ts index 14acdd0..f71410b 100644 --- a/src/value.ts +++ b/src/value.ts @@ -14,7 +14,8 @@ export type Value = body: number, parentScope: Scope, variadic: boolean, - named: boolean + named: boolean, + value: '' } export type Dict = Map diff --git a/src/vm.ts b/src/vm.ts index cbbe609..f9f4e44 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -330,7 +330,8 @@ export class VM { body: fnDef.body, variadic: fnDef.variadic, named: fnDef.named, - parentScope: this.scope + parentScope: this.scope, + value: '' }) break diff --git a/tests/programmatic.test.ts b/tests/programmatic.test.ts new file mode 100644 index 0000000..65faf48 --- /dev/null +++ b/tests/programmatic.test.ts @@ -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 }) + }) +})