189 lines
5.3 KiB
TypeScript
189 lines
5.3 KiB
TypeScript
import { type Value, type FunctionDef, toValue } from "./value"
|
|
import { OpCode } from "./opcode"
|
|
|
|
export type Bytecode = {
|
|
instructions: Instruction[]
|
|
constants: Constant[]
|
|
}
|
|
|
|
export type Instruction = {
|
|
op: OpCode
|
|
operand?: number | string
|
|
}
|
|
|
|
export type Constant =
|
|
| Value
|
|
| FunctionDef
|
|
|
|
//
|
|
// Parse bytecode from human-readable string format.
|
|
// Operand types are determined by prefix/literal:
|
|
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
|
// name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
|
|
// 42 -> number constant (e.g., PUSH 42)
|
|
// "str" -> string constant (e.g., PUSH "hello")
|
|
// 'str' -> string constant (e.g., PUSH 'hello')
|
|
// true -> boolean constant (e.g., PUSH true)
|
|
// false -> boolean constant (e.g., PUSH false)
|
|
// null -> null constant (e.g., PUSH null)
|
|
//
|
|
// 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 */ {
|
|
const lines = str.trim().split("\n")
|
|
|
|
const bytecode: Bytecode = {
|
|
instructions: [],
|
|
constants: []
|
|
}
|
|
|
|
for (let line of lines) {
|
|
const trimmed = line.trim()
|
|
if (!trimmed) continue
|
|
|
|
const [op, ...rest] = trimmed.split(/\s+/)
|
|
const opCode = OpCode[op as keyof typeof OpCode]
|
|
|
|
if (opCode === undefined) {
|
|
throw new Error(`Unknown opcode: ${op}`)
|
|
}
|
|
|
|
let operandValue: number | string | undefined = undefined
|
|
|
|
if (rest.length > 0) {
|
|
const operand = rest.join(' ')
|
|
|
|
// 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))
|
|
|
|
} else if (/^['"].*['"]$/.test(operand)) {
|
|
// string
|
|
const stringValue = operand.slice(1, operand.length - 1)
|
|
bytecode.constants.push(toValue(stringValue))
|
|
operandValue = bytecode.constants.length - 1
|
|
|
|
} else if (/^-?\d+(\.\d+)?$/.test(operand)) {
|
|
// number
|
|
bytecode.constants.push(toValue(parseFloat(operand)))
|
|
operandValue = bytecode.constants.length - 1
|
|
|
|
} else if (operand === 'true' || operand === 'false') {
|
|
// boolean
|
|
bytecode.constants.push(toValue(operand === 'true'))
|
|
operandValue = bytecode.constants.length - 1
|
|
|
|
} else if (operand === 'null') {
|
|
// null
|
|
bytecode.constants.push(toValue(null))
|
|
operandValue = bytecode.constants.length - 1
|
|
|
|
} else if (/^[a-zA-Z_].*$/.test(operand)) {
|
|
// variable
|
|
operandValue = operand
|
|
|
|
} else {
|
|
throw new Error(`Invalid operand: ${operand}`)
|
|
}
|
|
}
|
|
|
|
bytecode.instructions.push({
|
|
op: opCode,
|
|
operand: operandValue
|
|
})
|
|
}
|
|
|
|
return bytecode
|
|
}
|