Make TRY_CALL work with native functions AND a debug helper #2
202
src/format.ts
Normal file
202
src/format.ts
Normal 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}`
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user