From f3759c2259ef4b9426169a8414db9447756692b3 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 09:35:50 -0700 Subject: [PATCH 01/11] Add regexes --- src/value.ts | 127 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 48 deletions(-) diff --git a/src/value.ts b/src/value.ts index e51368f..8173ff5 100644 --- a/src/value.ts +++ b/src/value.ts @@ -1,22 +1,23 @@ -import { Scope } from "./scope" +import { Scope } from './scope' 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: '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: 'function', - params: string[], - defaults: Record, // indices into constants - body: number, - parentScope: Scope, - variadic: boolean, - named: boolean, - value: '' - } + type: 'function' + params: string[] + defaults: Record // indices into constants + body: number + parentScope: Scope + variadic: boolean + named: boolean + value: '' + } export type Dict = Map @@ -30,14 +31,13 @@ export type FunctionDef = { } export function toValue(v: any): Value /* throws */ { - if (v === null || v === undefined) - return { type: 'null', value: null } + 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 (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 (Array.isArray(v)) return { type: 'array', value: v.map(toValue) } + + if (v instanceof RegExp) return { type: 'regex', value: v } switch (typeof v) { case 'boolean': @@ -51,8 +51,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 +61,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 +87,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 +114,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 +136,35 @@ export function isEqual(a: Value, b: Value): boolean { } return true } - case 'function': return false // functions never equal + case 'regex': { + if (!(b instanceof RegExp)) return false + 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 +189,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 +} -- 2.50.1 From 4a5618e900e0e4deaccb24d58329f170b8156d0f Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 09:53:05 -0700 Subject: [PATCH 02/11] Update value.ts --- src/value.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/value.ts b/src/value.ts index 8173ff5..95334e2 100644 --- a/src/value.ts +++ b/src/value.ts @@ -9,14 +9,14 @@ export type Value = | { type: 'dict'; value: Dict } | { type: 'regex'; value: RegExp } | { - type: 'function' - params: string[] - defaults: Record // indices into constants - body: number - parentScope: Scope - variadic: boolean - named: boolean - value: '' + type: 'function' + params: string[] + defaults: Record // indices into constants + body: number + parentScope: Scope + variadic: boolean + named: boolean + value: '' } export type Dict = Map -- 2.50.1 From 79f449bc6cd40ed7e8cfd3ad0352f0a1ff0f89f6 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 09:53:38 -0700 Subject: [PATCH 03/11] Update value.ts --- src/value.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/value.ts b/src/value.ts index 95334e2..4d02a8f 100644 --- a/src/value.ts +++ b/src/value.ts @@ -1,4 +1,4 @@ -import { Scope } from './scope' +import { Scope } from "./scope" export type Value = | { type: 'null'; value: null } @@ -9,13 +9,13 @@ export type Value = | { type: 'dict'; value: Dict } | { type: 'regex'; value: RegExp } | { - type: 'function' - params: string[] - defaults: Record // indices into constants - body: number - parentScope: Scope - variadic: boolean - named: boolean + type: 'function', + params: string[], + defaults: Record, // indices into constants + body: number, + parentScope: Scope, + variadic: boolean, + named: boolean, value: '' } -- 2.50.1 From 77f86ce82935f241e88bbbe6e8f136c002d54ca6 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 09:53:59 -0700 Subject: [PATCH 04/11] Update value.ts --- src/value.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/value.ts b/src/value.ts index 4d02a8f..273875f 100644 --- a/src/value.ts +++ b/src/value.ts @@ -1,13 +1,13 @@ import { Scope } from "./scope" 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: '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: 'function', params: string[], -- 2.50.1 From 82d727cc744b4b2e6634e76f2aa2ba1114b713f6 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 09:54:48 -0700 Subject: [PATCH 05/11] Update value.ts --- src/value.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/value.ts b/src/value.ts index 273875f..035f036 100644 --- a/src/value.ts +++ b/src/value.ts @@ -11,13 +11,13 @@ export type Value = | { type: 'function', params: string[], - defaults: Record, // indices into constants + defaults: Record, // indices into constants body: number, parentScope: Scope, variadic: boolean, named: boolean, value: '' - } + } export type Dict = Map @@ -31,13 +31,17 @@ export type FunctionDef = { } export function toValue(v: any): Value /* throws */ { - if (v === null || v === undefined) return { type: 'null', value: null } + 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 (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 (Array.isArray(v)) + return { type: 'array', value: v.map(toValue) } - if (v instanceof RegExp) return { type: 'regex', value: v } + if (v instanceof RegExp) + return { type: 'regex', value: v } switch (typeof v) { case 'boolean': -- 2.50.1 From b7cc0fc06462389807a0649c178fa79bc4524a28 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 09:55:10 -0700 Subject: [PATCH 06/11] Update value.ts --- src/value.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/value.ts b/src/value.ts index 035f036..8da40da 100644 --- a/src/value.ts +++ b/src/value.ts @@ -17,7 +17,7 @@ export type Value = variadic: boolean, named: boolean, value: '' - } +} export type Dict = Map -- 2.50.1 From cabdc1525325b79ec9b2699909539c2ec735042e Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 10:25:53 -0700 Subject: [PATCH 07/11] Update index.ts --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- 2.50.1 From f630d7934a0ba91156cd99fd2aaf0d9f2c336c5c Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 13:04:38 -0700 Subject: [PATCH 08/11] Update value.ts --- src/value.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/value.ts b/src/value.ts index 8da40da..3635305 100644 --- a/src/value.ts +++ b/src/value.ts @@ -17,7 +17,7 @@ export type Value = variadic: boolean, named: boolean, value: '' -} + } export type Dict = Map @@ -141,7 +141,7 @@ export function isEqual(a: Value, b: Value): boolean { return true } case 'regex': { - if (!(b instanceof RegExp)) return false + if (!(b.value instanceof RegExp)) return false return String(a.value) === String(b.value) } case 'function': -- 2.50.1 From 2be87c381d2d022f9acddc3ba374226858ab91f2 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 13:42:05 -0700 Subject: [PATCH 09/11] Add test --- src/bytecode.ts | 14 +++++++++++++- tests/basic.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/bytecode.ts b/src/bytecode.ts index 66b21ea..4e8ae75 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -495,7 +495,19 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ { bytecode.constants.push(toValue(null)) operandValue = bytecode.constants.length - 1 - } else { + } 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 // (already checked that it doesn't start with . # or match other patterns) 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 -- 2.50.1 From 1bc46045ba5bd66355a73ded8acea81cb559741b Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 14:09:19 -0700 Subject: [PATCH 10/11] Update value.ts --- src/value.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/value.ts b/src/value.ts index 3635305..4edb9e4 100644 --- a/src/value.ts +++ b/src/value.ts @@ -141,7 +141,6 @@ export function isEqual(a: Value, b: Value): boolean { return true } case 'regex': { - if (!(b.value instanceof RegExp)) return false return String(a.value) === String(b.value) } case 'function': -- 2.50.1 From fa7589293a84235a920348a24f7f093f96156204 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 16 Oct 2025 14:13:39 -0700 Subject: [PATCH 11/11] Update bytecode.ts --- src/bytecode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bytecode.ts b/src/bytecode.ts index 4e8ae75..7824054 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -507,7 +507,7 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ { } 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 // This allows emoji, Unicode, and other creative identifiers // (already checked that it doesn't start with . # or match other patterns) -- 2.50.1