225 lines
5.7 KiB
TypeScript
225 lines
5.7 KiB
TypeScript
import { OpCode } from "./opcode"
|
|
import { Scope } from "./scope"
|
|
import { VM } from "./vm"
|
|
|
|
export type NativeFunction = (...args: Value[]) => Promise<Value> | 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: '<function>' }
|
|
| {
|
|
type: 'function',
|
|
params: string[],
|
|
defaults: Record<string, number>, // indices into constants
|
|
body: number,
|
|
parentScope: Scope,
|
|
variadic: boolean,
|
|
named: boolean,
|
|
value: '<function>'
|
|
}
|
|
|
|
export type Dict = Map<string, Value>
|
|
|
|
export type FunctionDef = {
|
|
type: 'function_def'
|
|
params: string[]
|
|
defaults: Record<string, number>
|
|
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 '<function>'
|
|
case 'native':
|
|
return '<function>'
|
|
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 '<function>'
|
|
}
|
|
}
|
|
|
|
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<string, any> = {}
|
|
|
|
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)
|
|
}
|
|
} |