Compare commits
No commits in common. "b16351ac95138a20c72d3dde245fcca713df54f6" and "dd9ec2a7d11d12e08ac0d605feb32d2cb264e615" have entirely different histories.
b16351ac95
...
dd9ec2a7d1
52
CLAUDE.md
52
CLAUDE.md
|
|
@ -75,17 +75,14 @@ No build step required - Bun runs TypeScript directly.
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
Tests are organized by feature area:
|
Tests are organized by feature area:
|
||||||
- **opcodes.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
|
- **basic.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
|
||||||
- **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args
|
- **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args
|
||||||
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
||||||
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
||||||
- **native.test.ts**: Native function interop (sync and async)
|
- **native.test.ts**: Native function interop (sync and async)
|
||||||
- **functions-parameter.test.ts**: Convenience parameter for passing functions to run() and VM
|
|
||||||
- **bytecode.test.ts**: Bytecode string parser, label resolution, constants
|
- **bytecode.test.ts**: Bytecode string parser, label resolution, constants
|
||||||
- **programmatic.test.ts**: Array format API, typed tuples, labels, functions
|
- **programmatic.test.ts**: Array format API, typed tuples, labels, functions
|
||||||
- **validator.test.ts**: Bytecode validation rules
|
- **validator.test.ts**: Bytecode validation rules
|
||||||
- **unicode.test.ts**: Unicode and emoji identifiers
|
|
||||||
- **regex.test.ts**: RegExp support
|
|
||||||
- **examples.test.ts**: Integration tests for example programs
|
- **examples.test.ts**: Integration tests for example programs
|
||||||
|
|
||||||
When adding features:
|
When adding features:
|
||||||
|
|
@ -140,35 +137,48 @@ Array format features:
|
||||||
|
|
||||||
### Native Function Registration
|
### Native Function Registration
|
||||||
|
|
||||||
**Option 1**: Pass to `run()` or `VM` constructor (convenience)
|
ReefVM supports two ways to register native functions:
|
||||||
```typescript
|
|
||||||
const result = await run(bytecode, {
|
|
||||||
add: (a: number, b: number) => a + b,
|
|
||||||
greet: (name: string) => `Hello, ${name}!`
|
|
||||||
})
|
|
||||||
|
|
||||||
// Or with VM constructor
|
**1. Native TypeScript functions (recommended)** - Auto-converts between native TS and ReefVM types:
|
||||||
const vm = new VM(bytecode, { add, greet })
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option 2**: Register with `vm.registerFunction()` (manual)
|
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
vm.registerFunction('add', (a: number, b: number) => a + b)
|
|
||||||
|
// Works with native TypeScript types!
|
||||||
|
vm.registerFunction('add', (a: number, b: number) => {
|
||||||
|
return a + b
|
||||||
|
})
|
||||||
|
|
||||||
|
// Supports defaults (like NOSE commands)
|
||||||
|
vm.registerFunction('ls', (path: string, link = false) => {
|
||||||
|
return link ? `listing ${path} with links` : `listing ${path}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Async functions work too
|
||||||
|
vm.registerFunction('fetch', async (url: string) => {
|
||||||
|
const response = await fetch(url)
|
||||||
|
return await response.text()
|
||||||
|
})
|
||||||
|
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 3**: Register Value-based functions (for direct Value access)
|
**2. Value-based functions (manual)** - For functions that need direct Value access:
|
||||||
```typescript
|
```typescript
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
|
// Direct access to Value types
|
||||||
return toValue(toNumber(a) + toNumber(b))
|
return toValue(toNumber(a) + toNumber(b))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
Auto-wrapping handles:
|
The auto-wrapping handles:
|
||||||
- Value ↔ native type conversion (`fromValue`/`toValue`)
|
- Converting Value → native types on input (using `fromValue`)
|
||||||
- Sync and async functions
|
- Converting native types → Value on output (using `toValue`)
|
||||||
- Arrays, objects, primitives, null, RegExp
|
- Both sync and async functions
|
||||||
|
- Arrays, objects, primitives, and null
|
||||||
|
|
||||||
### Label Usage (Preferred)
|
### Label Usage (Preferred)
|
||||||
Use labels instead of numeric offsets for readability:
|
Use labels instead of numeric offsets for readability:
|
||||||
|
|
|
||||||
22
GUIDE.md
22
GUIDE.md
|
|
@ -547,28 +547,6 @@ All calls push arguments in order:
|
||||||
### CALL_NATIVE Behavior
|
### CALL_NATIVE Behavior
|
||||||
Unlike CALL, CALL_NATIVE consumes the **entire stack** as arguments and clears the stack. The native function receives all values that were on the stack at the time of the call.
|
Unlike CALL, CALL_NATIVE consumes the **entire stack** as arguments and clears the stack. The native function receives all values that were on the stack at the time of the call.
|
||||||
|
|
||||||
### Registering Native Functions
|
|
||||||
|
|
||||||
**Method 1**: Pass to `run()` or `VM` constructor
|
|
||||||
```typescript
|
|
||||||
const result = await run(bytecode, {
|
|
||||||
add: (a: number, b: number) => a + b,
|
|
||||||
greet: (name: string) => `Hello, ${name}!`
|
|
||||||
})
|
|
||||||
|
|
||||||
// Or with VM
|
|
||||||
const vm = new VM(bytecode, { add, greet })
|
|
||||||
```
|
|
||||||
|
|
||||||
**Method 2**: Register manually
|
|
||||||
```typescript
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
vm.registerFunction('add', (a, b) => a + b)
|
|
||||||
await vm.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
Functions are auto-wrapped to convert between native TypeScript and ReefVM Value types. Both sync and async functions work.
|
|
||||||
|
|
||||||
### Empty Stack
|
### Empty Stack
|
||||||
- RETURN with empty stack returns null
|
- RETURN with empty stack returns null
|
||||||
- HALT with empty stack returns null
|
- HALT with empty stack returns null
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Mixed positional and named arguments with proper priority binding
|
- Mixed positional and named arguments with proper priority binding
|
||||||
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
||||||
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
- Native function interop (CALL_NATIVE) with auto-wrapping for native TypeScript types
|
- Native function interop (CALL_NATIVE) with sync and async functions
|
||||||
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
- Write native functions with regular TypeScript types instead of Shrimp's internal Value types
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import type { Bytecode } from "./bytecode"
|
||||||
import { type Value } from "./value"
|
import { type Value } from "./value"
|
||||||
import { VM } from "./vm"
|
import { VM } from "./vm"
|
||||||
|
|
||||||
export async function run(bytecode: Bytecode, functions?: Record<string, Function>): Promise<Value> {
|
export async function run(bytecode: Bytecode): Promise<Value> {
|
||||||
const vm = new VM(bytecode, functions)
|
const vm = new VM(bytecode)
|
||||||
return await vm.run()
|
return await vm.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,11 @@ export class VM {
|
||||||
labels: Map<number, string> = new Map()
|
labels: Map<number, string> = new Map()
|
||||||
nativeFunctions: Map<string, NativeFunction> = new Map()
|
nativeFunctions: Map<string, NativeFunction> = new Map()
|
||||||
|
|
||||||
constructor(bytecode: Bytecode, functions?: Record<string, Function>) {
|
constructor(bytecode: Bytecode) {
|
||||||
this.instructions = bytecode.instructions
|
this.instructions = bytecode.instructions
|
||||||
this.constants = bytecode.constants
|
this.constants = bytecode.constants
|
||||||
this.labels = bytecode.labels || new Map()
|
this.labels = bytecode.labels || new Map()
|
||||||
this.scope = new Scope()
|
this.scope = new Scope()
|
||||||
|
|
||||||
if (functions)
|
|
||||||
for (const name of Object.keys(functions))
|
|
||||||
this.registerFunction(name, functions[name]!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerFunction(name: string, fn: Function) {
|
registerFunction(name: string, fn: Function) {
|
||||||
|
|
|
||||||
1064
tests/basic.test.ts
Normal file
1064
tests/basic.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -1,189 +0,0 @@
|
||||||
import { test, expect, describe } from "bun:test"
|
|
||||||
import { run, VM } from "#index"
|
|
||||||
import { toBytecode } from "#bytecode"
|
|
||||||
|
|
||||||
describe("functions parameter", () => {
|
|
||||||
test("pass functions to run()", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 5
|
|
||||||
PUSH 3
|
|
||||||
CALL_NATIVE add
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode, {
|
|
||||||
add: (a: number, b: number) => a + b
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toEqual({ type: 'number', value: 8 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("pass functions to VM constructor", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 10
|
|
||||||
PUSH 2
|
|
||||||
CALL_NATIVE multiply
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode, {
|
|
||||||
multiply: (a: number, b: number) => a * b
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 20 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("pass multiple functions", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 10
|
|
||||||
PUSH 5
|
|
||||||
CALL_NATIVE add
|
|
||||||
PUSH 3
|
|
||||||
CALL_NATIVE multiply
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode, {
|
|
||||||
add: (a: number, b: number) => a + b,
|
|
||||||
multiply: (a: number, b: number) => a * b
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toEqual({ type: 'number', value: 45 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("auto-wraps native functions", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH "hello"
|
|
||||||
PUSH "world"
|
|
||||||
CALL_NATIVE concat
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode, {
|
|
||||||
concat: (a: string, b: string) => a + " " + b
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toEqual({ type: 'string', value: 'hello world' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("works with async functions", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 100
|
|
||||||
CALL_NATIVE delay
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode, {
|
|
||||||
delay: async (n: number) => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1))
|
|
||||||
return n * 2
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toEqual({ type: 'number', value: 200 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("can combine with manual registerFunction", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 5
|
|
||||||
PUSH 3
|
|
||||||
CALL_NATIVE add
|
|
||||||
PUSH 2
|
|
||||||
CALL_NATIVE subtract
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode, {
|
|
||||||
add: (a: number, b: number) => a + b
|
|
||||||
})
|
|
||||||
|
|
||||||
// Register another function manually
|
|
||||||
vm.registerFunction('subtract', (a: number, b: number) => a - b)
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 6 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("no functions parameter (undefined)", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 42
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 42 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("empty functions object", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 99
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode, {})
|
|
||||||
expect(result).toEqual({ type: 'number', value: 99 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("function throws error", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 0
|
|
||||||
CALL_NATIVE divide
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await run(bytecode, {
|
|
||||||
divide: (n: number) => {
|
|
||||||
if (n === 0) throw new Error("Cannot divide by zero")
|
|
||||||
return 100 / n
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(true).toBe(false) // Should not reach here
|
|
||||||
} catch (e: any) {
|
|
||||||
expect(e.message).toContain("Cannot divide by zero")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("complex workflow with multiple function calls", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 5
|
|
||||||
PUSH 3
|
|
||||||
CALL_NATIVE add
|
|
||||||
STORE result
|
|
||||||
LOAD result
|
|
||||||
PUSH 2
|
|
||||||
CALL_NATIVE multiply
|
|
||||||
STORE final
|
|
||||||
LOAD final
|
|
||||||
CALL_NATIVE format
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode, {
|
|
||||||
add: (a: number, b: number) => a + b,
|
|
||||||
multiply: (a: number, b: number) => a * b,
|
|
||||||
format: (n: number) => `Result: ${n}`
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toEqual({ type: 'string', value: 'Result: 16' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("function overriding - later registration wins", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 5
|
|
||||||
CALL_NATIVE getValue
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode, {
|
|
||||||
getValue: () => 100
|
|
||||||
})
|
|
||||||
|
|
||||||
// Override with manual registration
|
|
||||||
vm.registerFunction('getValue', () => 200)
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'number', value: 200 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,450 +0,0 @@
|
||||||
import { test, expect, describe } from "bun:test"
|
|
||||||
import { run } from "#index"
|
|
||||||
import { toBytecode } from "#bytecode"
|
|
||||||
|
|
||||||
describe("RegExp", () => {
|
|
||||||
test("basic pattern parsing", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /hello/
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('hello')
|
|
||||||
expect(result.value.flags).toBe('')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("pattern with flags", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /test/gi
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('test')
|
|
||||||
expect(result.value.global).toBe(true)
|
|
||||||
expect(result.value.ignoreCase).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("multiple flag combinations", async () => {
|
|
||||||
// Test i flag
|
|
||||||
const str1 = `
|
|
||||||
PUSH /pattern/i
|
|
||||||
`
|
|
||||||
const result1 = await run(toBytecode(str1))
|
|
||||||
expect(result1.type).toBe('regex')
|
|
||||||
if (result1.type === 'regex') {
|
|
||||||
expect(result1.value.ignoreCase).toBe(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test g flag
|
|
||||||
const str2 = `
|
|
||||||
PUSH /pattern/g
|
|
||||||
`
|
|
||||||
const result2 = await run(toBytecode(str2))
|
|
||||||
expect(result2.type).toBe('regex')
|
|
||||||
if (result2.type === 'regex') {
|
|
||||||
expect(result2.value.global).toBe(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test m flag
|
|
||||||
const str3 = `
|
|
||||||
PUSH /pattern/m
|
|
||||||
`
|
|
||||||
const result3 = await run(toBytecode(str3))
|
|
||||||
expect(result3.type).toBe('regex')
|
|
||||||
if (result3.type === 'regex') {
|
|
||||||
expect(result3.value.multiline).toBe(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test combined flags
|
|
||||||
const str4 = `
|
|
||||||
PUSH /pattern/gim
|
|
||||||
`
|
|
||||||
const result4 = await run(toBytecode(str4))
|
|
||||||
expect(result4.type).toBe('regex')
|
|
||||||
if (result4.type === 'regex') {
|
|
||||||
expect(result4.value.global).toBe(true)
|
|
||||||
expect(result4.value.ignoreCase).toBe(true)
|
|
||||||
expect(result4.value.multiline).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("complex patterns", async () => {
|
|
||||||
// Character class
|
|
||||||
const str1 = `
|
|
||||||
PUSH /[a-z0-9]+/
|
|
||||||
`
|
|
||||||
const result1 = await run(toBytecode(str1))
|
|
||||||
expect(result1.type).toBe('regex')
|
|
||||||
if (result1.type === 'regex') {
|
|
||||||
expect(result1.value.source).toBe('[a-z0-9]+')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quantifiers
|
|
||||||
const str2 = `
|
|
||||||
PUSH /a{2,4}/
|
|
||||||
`
|
|
||||||
const result2 = await run(toBytecode(str2))
|
|
||||||
expect(result2.type).toBe('regex')
|
|
||||||
if (result2.type === 'regex') {
|
|
||||||
expect(result2.value.source).toBe('a{2,4}')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Groups and alternation
|
|
||||||
const str3 = `
|
|
||||||
PUSH /(foo|bar)/
|
|
||||||
`
|
|
||||||
const result3 = await run(toBytecode(str3))
|
|
||||||
expect(result3.type).toBe('regex')
|
|
||||||
if (result3.type === 'regex') {
|
|
||||||
expect(result3.value.source).toBe('(foo|bar)')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anchors and special chars
|
|
||||||
const str4 = `
|
|
||||||
PUSH /^[a-z]+$/
|
|
||||||
`
|
|
||||||
const result4 = await run(toBytecode(str4))
|
|
||||||
expect(result4.type).toBe('regex')
|
|
||||||
if (result4.type === 'regex') {
|
|
||||||
expect(result4.value.source).toBe('^[a-z]+$')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("escaping special characters", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /\\d+\\.\\d+/
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('\\d+\\.\\d+')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("store and load", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /test/i
|
|
||||||
STORE pattern
|
|
||||||
LOAD pattern
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('test')
|
|
||||||
expect(result.value.ignoreCase).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("TRY_LOAD with regex", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /hello/g
|
|
||||||
STORE regex
|
|
||||||
TRY_LOAD regex
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('hello')
|
|
||||||
expect(result.value.global).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("NEQ comparison", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /foo/
|
|
||||||
PUSH /bar/
|
|
||||||
NEQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
||||||
|
|
||||||
const str2 = `
|
|
||||||
PUSH /test/i
|
|
||||||
PUSH /test/i
|
|
||||||
NEQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("is truthy", async () => {
|
|
||||||
// Regex values should be truthy (not null or false)
|
|
||||||
const str = `
|
|
||||||
PUSH /test/
|
|
||||||
JUMP_IF_FALSE .end
|
|
||||||
PUSH 42
|
|
||||||
.end:
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("NOT returns false (regex is truthy)", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /pattern/
|
|
||||||
NOT
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("in arrays", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /first/
|
|
||||||
PUSH /second/i
|
|
||||||
PUSH /third/g
|
|
||||||
MAKE_ARRAY #3
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('array')
|
|
||||||
if (result.type === 'array') {
|
|
||||||
expect(result.value).toHaveLength(3)
|
|
||||||
|
|
||||||
expect(result.value[0]!.type).toBe('regex')
|
|
||||||
if (result.value[0]!.type === 'regex') {
|
|
||||||
expect(result.value[0]!.value.source).toBe('first')
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result.value[1]!.type).toBe('regex')
|
|
||||||
if (result.value[1]!.type === 'regex') {
|
|
||||||
expect(result.value[1]!.value.source).toBe('second')
|
|
||||||
expect(result.value[1]!.value.ignoreCase).toBe(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result.value[2]!.type).toBe('regex')
|
|
||||||
if (result.value[2]!.type === 'regex') {
|
|
||||||
expect(result.value[2]!.value.source).toBe('third')
|
|
||||||
expect(result.value[2]!.value.global).toBe(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("retrieve from array", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /pattern/i
|
|
||||||
PUSH /test/g
|
|
||||||
MAKE_ARRAY #2
|
|
||||||
PUSH 1
|
|
||||||
ARRAY_GET
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('test')
|
|
||||||
expect(result.value.global).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("in dicts", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH 'email'
|
|
||||||
PUSH /^[a-z@.]+$/i
|
|
||||||
PUSH 'phone'
|
|
||||||
PUSH /\\d{3}-\\d{4}/
|
|
||||||
MAKE_DICT #2
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('dict')
|
|
||||||
if (result.type === 'dict') {
|
|
||||||
expect(result.value.size).toBe(2)
|
|
||||||
|
|
||||||
const email = result.value.get('email')
|
|
||||||
expect(email?.type).toBe('regex')
|
|
||||||
if (email?.type === 'regex') {
|
|
||||||
expect(email.value.source).toBe('^[a-z@.]+$')
|
|
||||||
expect(email.value.ignoreCase).toBe(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const phone = result.value.get('phone')
|
|
||||||
expect(phone?.type).toBe('regex')
|
|
||||||
if (phone?.type === 'regex') {
|
|
||||||
expect(phone.value.source).toBe('\\d{3}-\\d{4}')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("retrieve from dict", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH 'pattern'
|
|
||||||
PUSH /test/gim
|
|
||||||
MAKE_DICT #1
|
|
||||||
PUSH 'pattern'
|
|
||||||
DICT_GET
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('test')
|
|
||||||
expect(result.value.global).toBe(true)
|
|
||||||
expect(result.value.ignoreCase).toBe(true)
|
|
||||||
expect(result.value.multiline).toBe(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("with STR_CONCAT converts to string", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH "Pattern: "
|
|
||||||
PUSH /test/gi
|
|
||||||
STR_CONCAT #2
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('string')
|
|
||||||
if (result.type === 'string') {
|
|
||||||
expect(result.value).toBe('Pattern: /test/gi')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("multiple regex in STR_CONCAT", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /foo/
|
|
||||||
PUSH " and "
|
|
||||||
PUSH /bar/i
|
|
||||||
STR_CONCAT #3
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: '/foo/ and /bar/i' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("DUP with regex", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /pattern/i
|
|
||||||
DUP
|
|
||||||
EQ
|
|
||||||
`
|
|
||||||
// Same regex duplicated should be equal
|
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("empty pattern", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH //
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('(?:)')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("pattern with forward slashes escaped", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /https:\\/\\//
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('https:\\/\\/')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("unicode patterns", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /こんにちは/
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('こんにちは')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("emoji in pattern", async () => {
|
|
||||||
const str = `
|
|
||||||
PUSH /🎉+/
|
|
||||||
`
|
|
||||||
const result = await run(toBytecode(str))
|
|
||||||
expect(result.type).toBe('regex')
|
|
||||||
if (result.type === 'regex') {
|
|
||||||
expect(result.value.source).toBe('🎉+')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("comparing different regex types", async () => {
|
|
||||||
// Different patterns
|
|
||||||
const str1 = `
|
|
||||||
PUSH /abc/
|
|
||||||
PUSH /xyz/
|
|
||||||
EQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str1))).toEqual({ type: 'boolean', value: false })
|
|
||||||
|
|
||||||
// Same pattern, different flags
|
|
||||||
const str2 = `
|
|
||||||
PUSH /test/
|
|
||||||
PUSH /test/i
|
|
||||||
EQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
|
||||||
|
|
||||||
// Different order of flags (should be equal)
|
|
||||||
const str3 = `
|
|
||||||
PUSH /test/ig
|
|
||||||
PUSH /test/gi
|
|
||||||
EQ
|
|
||||||
`
|
|
||||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("with native functions", async () => {
|
|
||||||
const { VM } = await import("#vm")
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH "hello world"
|
|
||||||
PUSH /world/
|
|
||||||
CALL_NATIVE match
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Register a native function that takes a string and regex
|
|
||||||
vm.registerFunction('match', (str: string, pattern: RegExp) => {
|
|
||||||
return pattern.test(str)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'boolean', value: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("native function with regex replacement", async () => {
|
|
||||||
const { VM } = await import("#vm")
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH "hello world"
|
|
||||||
PUSH /o/g
|
|
||||||
PUSH "0"
|
|
||||||
CALL_NATIVE replace
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('replace', (str: string, pattern: RegExp, replacement: string) => {
|
|
||||||
return str.replace(pattern, replacement)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result).toEqual({ type: 'string', value: 'hell0 w0rld' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("native function extracting matches", async () => {
|
|
||||||
const { VM } = await import("#vm")
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH "test123abc456"
|
|
||||||
PUSH /\\d+/g
|
|
||||||
CALL_NATIVE extractNumbers
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.registerFunction('extractNumbers', (str: string, pattern: RegExp) => {
|
|
||||||
return str.match(pattern) || []
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await vm.run()
|
|
||||||
expect(result.type).toBe('array')
|
|
||||||
if (result.type === 'array') {
|
|
||||||
expect(result.value).toHaveLength(2)
|
|
||||||
expect(result.value[0]).toEqual({ type: 'string', value: '123' })
|
|
||||||
expect(result.value[1]).toEqual({ type: 'string', value: '456' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import { test, expect, describe } from "bun:test"
|
|
||||||
import { run } from "#index"
|
|
||||||
import { toBytecode } from "#bytecode"
|
|
||||||
|
|
||||||
describe("Unicode and Emoji", () => {
|
|
||||||
test("emoji variable names - string format", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 5
|
|
||||||
STORE 💎
|
|
||||||
LOAD 💎
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 5 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("emoji variable names - array format", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 100],
|
|
||||||
["STORE", "💰"],
|
|
||||||
["LOAD", "💰"],
|
|
||||||
["PUSH", 50],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 150 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("unicode variable names - Japanese", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
PUSH 42
|
|
||||||
STORE 変数
|
|
||||||
LOAD 変数
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 42 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("unicode variable names - Chinese", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 888],
|
|
||||||
["STORE", "数字"],
|
|
||||||
["LOAD", "数字"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 888 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("emoji in function parameters", async () => {
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
MAKE_FUNCTION (💎 🌟) .add
|
|
||||||
STORE add
|
|
||||||
JUMP .after
|
|
||||||
.add:
|
|
||||||
LOAD 💎
|
|
||||||
LOAD 🌟
|
|
||||||
ADD
|
|
||||||
RETURN
|
|
||||||
.after:
|
|
||||||
LOAD add
|
|
||||||
PUSH 10
|
|
||||||
PUSH 20
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 30 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("emoji with defaults and variadic", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["🎯=100", "...🎨"], ".fn"],
|
|
||||||
["STORE", "fn"],
|
|
||||||
["JUMP", ".after"],
|
|
||||||
[".fn:"],
|
|
||||||
["LOAD", "🎯"],
|
|
||||||
["RETURN"],
|
|
||||||
[".after:"],
|
|
||||||
["LOAD", "fn"],
|
|
||||||
["PUSH", 0],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 100 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("mixed emoji and regular names", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 10],
|
|
||||||
["STORE", "💎"],
|
|
||||||
["PUSH", 20],
|
|
||||||
["STORE", "value"],
|
|
||||||
["PUSH", 30],
|
|
||||||
["STORE", "🌟"],
|
|
||||||
["LOAD", "💎"],
|
|
||||||
["LOAD", "value"],
|
|
||||||
["ADD"],
|
|
||||||
["LOAD", "🌟"],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 60 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue
Block a user