import { OpCode } from "./opcode" import { Scope } from "./scope" import { VM } from "./vm" export type NativeFunction = (...args: Value[]) => Promise | Value export type TypeScriptFunction = (this: VM, ...args: any[]) => any export type Value = | { type: 'null', value: null } | { type: 'boolean', value: boolean } | { type: 'number', value: number } | { type: 'string', value: string } | { type: 'array', value: Value[] } | { type: 'dict', value: Dict } | { type: 'regex', value: RegExp } | { type: 'native', fn: NativeFunction, value: '' } | { type: 'function', params: string[], defaults: Record, // indices into constants body: number, parentScope: Scope, variadic: boolean, named: boolean, value: '' } export type Dict = Map export type FunctionDef = { type: 'function_def' params: string[] defaults: Record body: number variadic: boolean named: boolean } export function toValue(v: any): Value /* throws */ { if (v === null || v === undefined) return { type: 'null', value: null } if (v && typeof v === 'object' && 'type' in v && 'value' in v) return v as Value if (Array.isArray(v)) return { type: 'array', value: v.map(toValue) } if (v instanceof RegExp) return { type: 'regex', value: v } switch (typeof v) { case 'boolean': return { type: 'boolean', value: v } case 'number': return { type: 'number', value: v } case 'string': return { type: 'string', value: v } case 'function': throw "can't toValue() a js function yet" case 'object': const dict: Dict = new Map() for (const key of Object.keys(v)) dict.set(key, toValue(v[key])) return { type: 'dict', value: dict } default: throw `can't toValue this: ${v}` } } export function toNumber(v: Value): number { switch (v.type) { case 'number': return v.value case 'boolean': return v.value ? 1 : 0 case 'string': { const parsed = parseFloat(v.value) return isNaN(parsed) ? 0 : parsed } default: return 0 } } export function isTrue(v: Value): boolean { switch (v.type) { case 'null': return false case 'boolean': return v.value default: return true } } export function toString(v: Value): string { switch (v.type) { case 'string': return v.value case 'number': return String(v.value) case 'boolean': return String(v.value) case 'null': return 'null' case 'function': return '' case 'native': return '' case 'array': return `[${v.value.map(toString).join(', ')}]` case 'dict': { const pairs = Array.from(v.value.entries()).map(([k, v]) => `${k}: ${toString(v)}`) return `{${pairs.join(', ')}}` } case 'regex': return String(v.value) default: return String(v) } } export function isEqual(a: Value, b: Value): boolean { if (a.type !== b.type) return false switch (a.type) { case 'null': return true case 'boolean': case 'number': case 'string': return a.value === b.value case 'array': { const bArr = b as typeof a if (a.value.length !== bArr.value.length) return false return a.value.every((v, i) => isEqual(v, bArr.value[i]!)) } case 'dict': { const bDict = b as typeof a if (a.value.size !== bDict.value.size) return false for (const [k, v] of a.value) { const bVal = bDict.value.get(k) if (!bVal || !isEqual(v, bVal)) return false } return true } case 'regex': { return String(a.value) === String(b.value) } case 'function': case 'native': return false // functions never equal default: return false } } export function fromValue(v: Value, vm?: VM): any { switch (v.type) { case 'null': case 'boolean': case 'number': case 'string': return v.value case 'array': return v.value.map(x => fromValue(x, vm)) case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v, vm)])) case 'regex': return v.value case 'function': if (!vm || !(vm instanceof VM)) throw new Error('VM is required for function conversion') return fnFromValue(v, vm) case 'native': return '' } } export function toNull(): Value { return toValue(null) } export function fnFromValue(fn: Value, vm: VM): Function { if (fn.type !== 'function') throw new Error('Value is not a function') return async function (...args: any[]) { let positional: any[] = args let named: Record = {} if (args.length > 0 && !Array.isArray(args[args.length - 1]) && args[args.length - 1].constructor === Object) { named = args[args.length - 1] positional = args.slice(0, -1) } const newVM = new VM({ instructions: vm.instructions, constants: vm.constants, labels: vm.labels }) newVM.scope = fn.parentScope newVM.stack.push(fn) newVM.stack.push(...positional.map(toValue)) for (const [key, val] of Object.entries(named)) { newVM.stack.push(toValue(key)) newVM.stack.push(toValue(val)) } newVM.stack.push(toValue(positional.length)) newVM.stack.push(toValue(Object.keys(named).length)) const targetDepth = newVM.callStack.length await newVM.execute({ op: OpCode.CALL }) newVM.pc++ while (newVM.callStack.length > targetDepth && newVM.pc < newVM.instructions.length) { await newVM.execute(newVM.instructions[newVM.pc]!) newVM.pc++ } return fromValue(newVM.stack.pop() || toNull(), vm) } }