add MAKE_FUNCTION to bytecode text format

This commit is contained in:
Chris Wanstrath 2025-10-05 19:50:01 -07:00
parent 45e4c29df4
commit 8198c555ac
2 changed files with 204 additions and 1 deletions

View File

@ -23,6 +23,74 @@ export type Constant =
// 42 -> number constant (e.g., PUSH 42) // 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello") // "str" -> string constant (e.g., PUSH "hello")
// '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<string, number>
variadic: boolean
named: boolean
} {
const params: string[] = []
const defaults: Record<string, number> = {}
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 */ { export function toBytecode(str: string): Bytecode /* throws */ {
const lines = str.trim().split("\n") const lines = str.trim().split("\n")
@ -47,7 +115,33 @@ export function toBytecode(str: string): Bytecode /* throws */ {
if (rest.length > 0) { if (rest.length > 0) {
const operand = rest.join(' ') 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 // immediate number
operandValue = parseInt(operand.slice(1)) operandValue = parseInt(operand.slice(1))

View File

@ -1,6 +1,7 @@
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import { toBytecode } from "#bytecode" import { toBytecode } from "#bytecode"
import { OpCode } from "#opcode" import { OpCode } from "#opcode"
import { VM } from "#vm"
test("string compilation", () => { test("string compilation", () => {
const str = ` 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' })
})