diff --git a/src/bytecode.ts b/src/bytecode.ts index 66b21ea..7824054 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -495,6 +495,18 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ { bytecode.constants.push(toValue(null)) 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 { // Assume it's a variable name if it doesn't match any other pattern // This allows emoji, Unicode, and other creative identifiers diff --git a/src/index.ts b/src/index.ts index 51fe917..b2606cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,6 @@ export async function run(bytecode: Bytecode): Promise { return await vm.run() } -export { type Bytecode, toBytecode } from "./bytecode" +export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode" export { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value" export { VM } from "./vm" \ No newline at end of file diff --git a/src/value.ts b/src/value.ts index e51368f..4edb9e4 100644 --- a/src/value.ts +++ b/src/value.ts @@ -7,6 +7,7 @@ export type Value = | { type: 'string', value: string } | { type: 'array', value: Value[] } | { type: 'dict', value: Dict } + | { type: 'regex', value: RegExp } | { type: 'function', params: string[], @@ -36,9 +37,12 @@ export function toValue(v: any): Value /* throws */ { if (v && typeof v === 'object' && 'type' in v && 'value' in v) return v as Value - if (Array.isArray(v)) + 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 } @@ -51,8 +55,7 @@ export function toValue(v: any): Value /* throws */ { case 'object': 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 } default: @@ -62,13 +65,16 @@ export function toValue(v: any): Value /* throws */ { export function toNumber(v: Value): number { switch (v.type) { - case 'number': return v.value - case 'boolean': return v.value ? 1 : 0 + 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 + default: + return 0 } } @@ -85,17 +91,26 @@ export function isTrue(v: Value): boolean { 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 'array': return `[${v.value.map(toString).join(', ')}]` + 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 'array': + return `[${v.value.map(toString).join(', ')}]` 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(', ')}}` } + case 'regex': + return String(v.value) + default: + return String(v) } } @@ -103,10 +118,14 @@ export function isEqual(a: Value, b: Value): boolean { if (a.type !== b.type) return false switch (a.type) { - case 'null': return true - case 'boolean': return a.value === (b as typeof a).value - case 'number': return a.value === (b as typeof a).value - case 'string': return a.value === (b as typeof a).value + case 'null': + return true + case 'boolean': + return a.value === b.value + case 'number': + return a.value === b.value + case 'string': + return a.value === b.value case 'array': { const bArr = b as typeof a if (a.value.length !== bArr.value.length) return false @@ -121,19 +140,34 @@ export function isEqual(a: Value, b: Value): boolean { } return true } - case 'function': return false // functions never equal + case 'regex': { + return String(a.value) === String(b.value) + } + case 'function': + return false // functions never equal + default: + return false } } export function fromValue(v: Value): any { switch (v.type) { - case 'null': return null - case 'boolean': return v.value - case 'number': return v.value - case 'string': 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 'function': return '' + case 'null': + return null + case 'boolean': + return v.value + case 'number': + return v.value + case 'string': + 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 '' } } @@ -158,4 +192,4 @@ export function wrapNative(fn: Function): (...args: Value[]) => Promise { export function isWrapped(fn: Function): boolean { return !!(fn as any)[WRAPPED_MARKER] -} \ No newline at end of file +} diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 4d7d22b..288d3c1 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -102,6 +102,29 @@ test("EQ - equality comparison", async () => { 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 () => { const str = ` PUSH 5