Add regex to reef #1

Merged
defunkt merged 11 commits from regex into main 2025-10-16 21:15:42 +00:00
4 changed files with 97 additions and 28 deletions

View File

@ -495,6 +495,18 @@ 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

View File

@ -7,6 +7,6 @@ export async function run(bytecode: Bytecode): Promise<Value> {
return await vm.run() 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 { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value"
export { VM } from "./vm" export { VM } from "./vm"

View File

@ -7,6 +7,7 @@ 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[],
@ -36,9 +37,12 @@ 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 }
@ -51,8 +55,7 @@ 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)) for (const key of Object.keys(v)) dict.set(key, toValue(v[key]))
dict.set(key, toValue(v[key]))
return { type: 'dict', value: dict } return { type: 'dict', value: dict }
default: default:
@ -62,13 +65,16 @@ 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': return v.value case 'number':
case 'boolean': return v.value ? 1 : 0 return v.value
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: return 0 default:
return 0
} }
} }
@ -85,17 +91,26 @@ 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': return v.value case 'string':
case 'number': return String(v.value) return v.value
case 'boolean': return String(v.value) case 'number':
case 'null': return 'null' return String(v.value)
case 'function': return '<function>' case 'boolean':
case 'array': return `[${v.value.map(toString).join(', ')}]` return String(v.value)
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()) const pairs = Array.from(v.value.entries()).map(([k, v]) => `${k}: ${toString(v)}`)
.map(([k, v]) => `${k}: ${toString(v)}`)
return `{${pairs.join(', ')}}` 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 if (a.type !== b.type) return false
switch (a.type) { switch (a.type) {
case 'null': return true case 'null':
case 'boolean': return a.value === (b as typeof a).value return true
case 'number': return a.value === (b as typeof a).value case 'boolean':
case 'string': return a.value === (b as typeof a).value return a.value === b.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
@ -121,19 +140,34 @@ export function isEqual(a: Value, b: Value): boolean {
} }
return true 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 { export function fromValue(v: Value): any {
switch (v.type) { switch (v.type) {
case 'null': return null case 'null':
case 'boolean': return v.value return null
case 'number': return v.value case 'boolean':
case 'string': return v.value return v.value
case 'array': return v.value.map(fromValue) case 'number':
case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)])) return v.value
case 'function': return '<function>' 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 '<function>'
} }
} }
@ -158,4 +192,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]
} }

View File

@ -102,6 +102,29 @@ 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