Compare commits
No commits in common. "dd9ec2a7d11d12e08ac0d605feb32d2cb264e615" and "0844e99d2d04fb9ba0999f25248a17430bdc5ee6" have entirely different histories.
dd9ec2a7d1
...
0844e99d2d
|
|
@ -495,18 +495,6 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
||||||
bytecode.constants.push(toValue(null))
|
bytecode.constants.push(toValue(null))
|
||||||
operandValue = bytecode.constants.length - 1
|
operandValue = bytecode.constants.length - 1
|
||||||
|
|
||||||
} else if (/^\/.*\/[a-z]*$/.test(operand)) {
|
|
||||||
// regex literal (/pattern/flags)
|
|
||||||
const lastSlash = operand.lastIndexOf('/')
|
|
||||||
const pattern = operand.slice(1, lastSlash)
|
|
||||||
const flags = operand.slice(lastSlash + 1)
|
|
||||||
try {
|
|
||||||
const regex = new RegExp(pattern, flags)
|
|
||||||
bytecode.constants.push(toValue(regex))
|
|
||||||
operandValue = bytecode.constants.length - 1
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Invalid regex literal: ${operand}`)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Assume it's a variable name if it doesn't match any other pattern
|
// Assume it's a variable name if it doesn't match any other pattern
|
||||||
// This allows emoji, Unicode, and other creative identifiers
|
// This allows emoji, Unicode, and other creative identifiers
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ export async function run(bytecode: Bytecode): Promise<Value> {
|
||||||
return await vm.run()
|
return await vm.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
|
export { type Bytecode, toBytecode } from "./bytecode"
|
||||||
export { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value"
|
export { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value"
|
||||||
export { VM } from "./vm"
|
export { VM } from "./vm"
|
||||||
88
src/value.ts
88
src/value.ts
|
|
@ -7,7 +7,6 @@ export type Value =
|
||||||
| { type: 'string', value: string }
|
| { type: 'string', value: string }
|
||||||
| { type: 'array', value: Value[] }
|
| { type: 'array', value: Value[] }
|
||||||
| { type: 'dict', value: Dict }
|
| { type: 'dict', value: Dict }
|
||||||
| { type: 'regex', value: RegExp }
|
|
||||||
| {
|
| {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
params: string[],
|
params: string[],
|
||||||
|
|
@ -37,12 +36,9 @@ export function toValue(v: any): Value /* throws */ {
|
||||||
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
||||||
return v as Value
|
return v as Value
|
||||||
|
|
||||||
if (Array.isArray(v))
|
if (Array.isArray(v))
|
||||||
return { type: 'array', value: v.map(toValue) }
|
return { type: 'array', value: v.map(toValue) }
|
||||||
|
|
||||||
if (v instanceof RegExp)
|
|
||||||
return { type: 'regex', value: v }
|
|
||||||
|
|
||||||
switch (typeof v) {
|
switch (typeof v) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return { type: 'boolean', value: v }
|
return { type: 'boolean', value: v }
|
||||||
|
|
@ -55,7 +51,8 @@ export function toValue(v: any): Value /* throws */ {
|
||||||
case 'object':
|
case 'object':
|
||||||
const dict: Dict = new Map()
|
const dict: Dict = new Map()
|
||||||
|
|
||||||
for (const key of Object.keys(v)) dict.set(key, toValue(v[key]))
|
for (const key of Object.keys(v))
|
||||||
|
dict.set(key, toValue(v[key]))
|
||||||
|
|
||||||
return { type: 'dict', value: dict }
|
return { type: 'dict', value: dict }
|
||||||
default:
|
default:
|
||||||
|
|
@ -65,16 +62,13 @@ export function toValue(v: any): Value /* throws */ {
|
||||||
|
|
||||||
export function toNumber(v: Value): number {
|
export function toNumber(v: Value): number {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'number':
|
case 'number': return v.value
|
||||||
return v.value
|
case 'boolean': return v.value ? 1 : 0
|
||||||
case 'boolean':
|
|
||||||
return v.value ? 1 : 0
|
|
||||||
case 'string': {
|
case 'string': {
|
||||||
const parsed = parseFloat(v.value)
|
const parsed = parseFloat(v.value)
|
||||||
return isNaN(parsed) ? 0 : parsed
|
return isNaN(parsed) ? 0 : parsed
|
||||||
}
|
}
|
||||||
default:
|
default: return 0
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,26 +85,17 @@ export function isTrue(v: Value): boolean {
|
||||||
|
|
||||||
export function toString(v: Value): string {
|
export function toString(v: Value): string {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'string':
|
case 'string': return v.value
|
||||||
return v.value
|
case 'number': return String(v.value)
|
||||||
case 'number':
|
case 'boolean': return String(v.value)
|
||||||
return String(v.value)
|
case 'null': return 'null'
|
||||||
case 'boolean':
|
case 'function': return '<function>'
|
||||||
return String(v.value)
|
case 'array': return `[${v.value.map(toString).join(', ')}]`
|
||||||
case 'null':
|
|
||||||
return 'null'
|
|
||||||
case 'function':
|
|
||||||
return '<function>'
|
|
||||||
case 'array':
|
|
||||||
return `[${v.value.map(toString).join(', ')}]`
|
|
||||||
case 'dict': {
|
case 'dict': {
|
||||||
const pairs = Array.from(v.value.entries()).map(([k, v]) => `${k}: ${toString(v)}`)
|
const pairs = Array.from(v.value.entries())
|
||||||
|
.map(([k, v]) => `${k}: ${toString(v)}`)
|
||||||
return `{${pairs.join(', ')}}`
|
return `{${pairs.join(', ')}}`
|
||||||
}
|
}
|
||||||
case 'regex':
|
|
||||||
return String(v.value)
|
|
||||||
default:
|
|
||||||
return String(v)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,14 +103,10 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
if (a.type !== b.type) return false
|
if (a.type !== b.type) return false
|
||||||
|
|
||||||
switch (a.type) {
|
switch (a.type) {
|
||||||
case 'null':
|
case 'null': return true
|
||||||
return true
|
case 'boolean': return a.value === (b as typeof a).value
|
||||||
case 'boolean':
|
case 'number': return a.value === (b as typeof a).value
|
||||||
return a.value === b.value
|
case 'string': return a.value === (b as typeof a).value
|
||||||
case 'number':
|
|
||||||
return a.value === b.value
|
|
||||||
case 'string':
|
|
||||||
return a.value === b.value
|
|
||||||
case 'array': {
|
case 'array': {
|
||||||
const bArr = b as typeof a
|
const bArr = b as typeof a
|
||||||
if (a.value.length !== bArr.value.length) return false
|
if (a.value.length !== bArr.value.length) return false
|
||||||
|
|
@ -140,34 +121,19 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case 'regex': {
|
case 'function': return false // functions never equal
|
||||||
return String(a.value) === String(b.value)
|
|
||||||
}
|
|
||||||
case 'function':
|
|
||||||
return false // functions never equal
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromValue(v: Value): any {
|
export function fromValue(v: Value): any {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'null':
|
case 'null': return null
|
||||||
return null
|
case 'boolean': return v.value
|
||||||
case 'boolean':
|
case 'number': return v.value
|
||||||
return v.value
|
case 'string': return v.value
|
||||||
case 'number':
|
case 'array': return v.value.map(fromValue)
|
||||||
return v.value
|
case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
|
||||||
case 'string':
|
case 'function': return '<function>'
|
||||||
return v.value
|
|
||||||
case 'array':
|
|
||||||
return v.value.map(fromValue)
|
|
||||||
case 'dict':
|
|
||||||
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
|
|
||||||
case 'regex':
|
|
||||||
return v.value
|
|
||||||
case 'function':
|
|
||||||
return '<function>'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,4 +158,4 @@ export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
||||||
|
|
||||||
export function isWrapped(fn: Function): boolean {
|
export function isWrapped(fn: Function): boolean {
|
||||||
return !!(fn as any)[WRAPPED_MARKER]
|
return !!(fn as any)[WRAPPED_MARKER]
|
||||||
}
|
}
|
||||||
|
|
@ -102,29 +102,6 @@ test("EQ - equality comparison", async () => {
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('EQ - equality with regexes', async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /cool/i
|
|
||||||
PUSH /cool/i
|
|
||||||
EQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
||||||
|
|
||||||
const str2 = `
|
|
||||||
PUSH /cool/
|
|
||||||
PUSH /cool/i
|
|
||||||
EQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
|
||||||
|
|
||||||
const str3 = `
|
|
||||||
PUSH /not-cool/
|
|
||||||
PUSH /cool/
|
|
||||||
EQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("NEQ - not equal comparison", async () => {
|
test("NEQ - not equal comparison", async () => {
|
||||||
const str = `
|
const str = `
|
||||||
PUSH 5
|
PUSH 5
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user