Compare commits

...

3 Commits

3 changed files with 205 additions and 2 deletions

202
src/format.ts Normal file
View File

@ -0,0 +1,202 @@
import type { Bytecode, Constant } from "./bytecode"
import { OpCode } from "./opcode"
import type { Value, FunctionDef } from "./value"
/**
* Converts a Bytecode object back to human-readable string format.
* This is the inverse of toBytecode().
*/
export const bytecodeToString = (bytecode: Bytecode): string => {
const lines: string[] = []
const { instructions, constants, labels } = bytecode
for (let i = 0; i < instructions.length; i++) {
// Check if there's a label at this position
if (labels?.has(i)) {
lines.push(`.${labels.get(i)}:`)
}
const instr = instructions[i]!
const opName = OpCode[instr.op] // Get string name from enum
// Format based on whether operand exists
if (instr.operand === undefined) {
lines.push(opName)
} else {
const operandStr = formatOperand(instr.op, instr.operand, constants, labels, i)
lines.push(`${opName} ${operandStr}`)
}
}
return lines.join('\n')
}
/**
* Format an operand based on the opcode type
*/
const formatOperand = (
op: OpCode,
operand: number | string,
constants: Constant[],
labels: Map<number, string> | undefined,
currentIndex: number
): string => {
// Handle string operands (variable names)
if (typeof operand === 'string') {
return operand
}
// Handle numeric operands based on opcode
switch (op) {
case OpCode.PUSH: {
// Look up constant value
const value = constants[operand]
if (!value) {
throw new Error(`Invalid constant index: ${operand}`)
}
return formatConstant(value)
}
case OpCode.MAKE_FUNCTION: {
// Look up function definition and format as (params) .label
const funcDef = constants[operand]
if (!funcDef || !('type' in funcDef) || funcDef.type !== 'function_def') {
throw new Error(`Invalid function definition at constant index: ${operand}`)
}
return formatFunctionDef(funcDef as FunctionDef, labels, constants)
}
case OpCode.JUMP:
case OpCode.JUMP_IF_FALSE:
case OpCode.JUMP_IF_TRUE: {
// Convert relative offset to absolute position
const targetIndex = currentIndex + 1 + operand
const labelName = labels?.get(targetIndex)
return labelName ? `.${labelName}` : `#${operand}`
}
case OpCode.PUSH_TRY:
case OpCode.PUSH_FINALLY: {
// These use absolute positions
const labelName = labels?.get(operand)
return labelName ? `.${labelName}` : `#${operand}`
}
case OpCode.MAKE_ARRAY:
case OpCode.MAKE_DICT:
case OpCode.STR_CONCAT:
// These are just counts
return `#${operand}`
default:
return `#${operand}`
}
}
/**
* Format a constant value (from constants pool)
*/
const formatConstant = (constant: Constant): string => {
// Handle function definitions (shouldn't happen in PUSH, but be safe)
if ('type' in constant && constant.type === 'function_def') {
return '<function_def>'
}
// Handle Value types
const value = constant as Value
switch (value.type) {
case 'null':
return 'null'
case 'boolean':
return value.value.toString()
case 'number':
return value.value.toString()
case 'string':
// Use single quotes and escape special characters
return `'${escapeString(value.value)}'`
case 'regex': {
// Format as /pattern/flags
const pattern = value.value.source
const flags = value.value.flags
return `/${pattern}/${flags}`
}
case 'array': {
// Format as [item1, item2, ...]
const items = value.value.map(formatConstant).join(', ')
return `[${items}]`
}
case 'dict': {
// Format as {key1: value1, key2: value2}
const entries = Array.from(value.value.entries())
.map(([k, v]) => `${k}: ${formatConstant(v)}`)
.join(', ')
return `{${entries}}`
}
case 'function':
case 'native':
return '<function>'
default:
return '<unknown>'
}
}
/**
* Escape special characters in strings for output
*/
const escapeString = (str: string): string => {
return str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
.replace(/\r/g, '\\r')
.replace(/\$/g, '\\$')
}
/**
* Format a function definition as (params) .label
*/
const formatFunctionDef = (
funcDef: FunctionDef,
labels: Map<number, string> | undefined,
constants: Constant[]
): string => {
const params: string[] = []
for (let i = 0; i < funcDef.params.length; i++) {
const paramName = funcDef.params[i]!
const defaultIndex = funcDef.defaults[paramName]
if (defaultIndex !== undefined) {
// Parameter has a default value
const defaultValue = constants[defaultIndex]
if (!defaultValue) {
throw new Error(`Invalid default value index: ${defaultIndex}`)
}
params.push(`${paramName}=${formatConstant(defaultValue)}`)
} else if (i === funcDef.params.length - 1 && funcDef.variadic) {
// Last parameter and function is variadic
params.push(`...${paramName}`)
} else if (i === funcDef.params.length - 1 && funcDef.named) {
// Last parameter and function accepts named args
params.push(`@${paramName}`)
} else {
// Regular parameter
params.push(paramName)
}
}
// Format body address (prefer label name if available)
const bodyLabel = labels?.get(funcDef.body)
const bodyStr = bodyLabel ? `.${bodyLabel}` : `#${funcDef.body}`
return `(${params.join(' ')}) ${bodyStr}`
}

View File

@ -11,3 +11,4 @@ export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
export { wrapNative } from "./function"
export { type Value, toValue, toString, toNumber, fromValue, toNull } from "./value"
export { VM } from "./vm"
export { bytecodeToString } from "./format"

View File

@ -386,7 +386,7 @@ export class VM {
const varName = instruction.operand as string
const value = this.scope.get(varName)
if (value?.type === 'function') {
if (value?.type === 'function' || value?.type === 'native') {
this.stack.push(value)
this.stack.push(toValue(0))
this.stack.push(toValue(0))