diff --git a/src/bytecode.ts b/src/bytecode.ts index db39eda..ae8b190 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -23,6 +23,74 @@ export type Constant = // 42 -> number constant (e.g., PUSH 42) // "str" -> string constant (e.g., PUSH "hello") // 'str' -> string constant (e.g., PUSH 'hello') +// +// Function definitions: +// MAKE_FUNCTION (x y) #7 -> basic function +// MAKE_FUNCTION (x y=42) #7 -> with defaults +// MAKE_FUNCTION (x ...rest) #7 -> variadic +// MAKE_FUNCTION (x @named) #7 -> kwargs +// + +function parseFunctionParams(paramStr: string, constants: Constant[]): { + params: string[] + defaults: Record + variadic: boolean + named: boolean +} { + const params: string[] = [] + const defaults: Record = {} + let variadic = false + let kwargs = false + + // Remove parens and split by whitespace + const paramList = paramStr.slice(1, -1).trim() + if (!paramList) { + return { params, defaults, variadic, named: kwargs } + } + + const parts = paramList.split(/\s+/) + + for (const part of parts) { + // Check for named args (@name) + if (part.startsWith('@')) { + kwargs = true + params.push(part.slice(1)) + + } else if (part.startsWith('...')) { + // Check for variadic (...name) + variadic = true + params.push(part.slice(3)) + + } else if (part.includes('=')) { + // Check for default value (name=value) + const [name, defaultValue] = part.split('=').map(s => s.trim()) + params.push(name!) + + // Parse default value and add to constants + if (/^-?\d+(\.\d+)?$/.test(defaultValue!)) { + constants.push(toValue(parseFloat(defaultValue!))) + } else if (/^['\"].*['\"]$/.test(defaultValue!)) { + constants.push(toValue(defaultValue!.slice(1, -1))) + } else if (defaultValue === 'true') { + constants.push(toValue(true)) + } else if (defaultValue === 'false') { + constants.push(toValue(false)) + } else if (defaultValue === 'null') { + constants.push(toValue(null)) + } else { + throw new Error(`Invalid default value: ${defaultValue}`) + } + + defaults[name!] = constants.length - 1 + + } else { + params.push(part) + } + } + + return { params, defaults, variadic, named: kwargs } +} + export function toBytecode(str: string): Bytecode /* throws */ { const lines = str.trim().split("\n") @@ -47,7 +115,33 @@ export function toBytecode(str: string): Bytecode /* throws */ { if (rest.length > 0) { const operand = rest.join(' ') - if (operand.startsWith('#')) { + // Special handling for MAKE_FUNCTION with paren syntax + if (opCode === OpCode.MAKE_FUNCTION && operand.startsWith('(')) { + // Parse: MAKE_FUNCTION (params) #body + const match = operand.match(/^(\(.*?\))\s+(#\d+)$/) + if (!match) { + throw new Error(`Invalid MAKE_FUNCTION syntax: ${operand}`) + } + + const paramStr = match[1]! + const bodyStr = match[2]! + const body = parseInt(bodyStr.slice(1)) + + const { params, defaults, variadic, named: kwargs } = parseFunctionParams(paramStr, bytecode.constants) + + // Add function definition to constants + bytecode.constants.push({ + type: 'function_def', + params, + defaults, + body, + variadic, + kwargs + }) + + operandValue = bytecode.constants.length - 1 + } + else if (operand.startsWith('#')) { // immediate number operandValue = parseInt(operand.slice(1)) diff --git a/tests/bytecode.test.ts b/tests/bytecode.test.ts index 1ebbe3a..bf81867 100644 --- a/tests/bytecode.test.ts +++ b/tests/bytecode.test.ts @@ -1,6 +1,7 @@ import { test, expect } from "bun:test" import { toBytecode } from "#bytecode" import { OpCode } from "#opcode" +import { VM } from "#vm" test("string compilation", () => { const str = ` @@ -21,3 +22,111 @@ test("string compilation", () => { }) }) + +test("MAKE_FUNCTION - basic function", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION () #3 + CALL #0 + HALT + PUSH 42 + RETURN + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 42 }) +}) + +test("MAKE_FUNCTION - function with parameters", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (x y) #5 + PUSH 10 + PUSH 20 + CALL #2 + HALT + LOAD x + LOAD y + ADD + RETURN + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 30 }) +}) + +test("MAKE_FUNCTION - function with default parameters", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (x y=100) #4 + PUSH 10 + CALL #1 + HALT + LOAD x + LOAD y + ADD + RETURN + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 110 }) +}) + +test("MAKE_FUNCTION - tail recursive countdown", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (n) #6 + STORE countdown + LOAD countdown + PUSH 5 + CALL #1 + HALT + LOAD n + PUSH 0 + EQ + JUMP_IF_FALSE #2 + PUSH "done" + RETURN + LOAD countdown + LOAD n + PUSH 1 + SUB + TAIL_CALL #1 + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'string', value: 'done' }) +}) + +test("MAKE_FUNCTION - multiple default values", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (a=1 b=2 c=3) #3 + CALL #0 + HALT + LOAD a + LOAD b + LOAD c + ADD + ADD + RETURN + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 6 }) +}) + +test("MAKE_FUNCTION - default with string", async () => { + const bytecode = toBytecode(` + MAKE_FUNCTION (name="World") #3 + CALL #0 + HALT + LOAD name + RETURN + `) + + const vm = new VM(bytecode) + const result = await vm.run() + expect(result).toEqual({ type: 'string', value: 'World' }) +}) +