ReefVM/tests/basic.test.ts
2025-10-16 14:27:39 -07:00

1514 lines
32 KiB
TypeScript

import { test, expect } from "bun:test"
import { run } from "#index"
import { toBytecode } from "#bytecode"
test("ADD - add two numbers", async () => {
const str = `
PUSH 1
PUSH 5
ADD
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 6 })
const str2 = `
PUSH 100
PUSH 500
ADD
`
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 })
})
test("SUB - subtract two numbers", async () => {
const str = `
PUSH 5
PUSH 2
SUB
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
})
test("MUL - multiply two numbers", async () => {
const str = `
PUSH 5
PUSH 2
MUL
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
})
test("DIV - divide two numbers", async () => {
const str = `
PUSH 10
PUSH 2
DIV
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 })
const str2 = `
PUSH 10
PUSH 0
DIV
`
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: Infinity })
})
test("MOD - modulo two numbers", async () => {
const str = `
PUSH 17
PUSH 5
MOD
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
})
test("PUSH - pushes value onto stack", async () => {
const str = `
PUSH 42
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("POP - removes top value", async () => {
const str = `
PUSH 10
PUSH 20
POP
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
})
test("DUP - duplicates top value", async () => {
const str = `
PUSH 5
DUP
ADD
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
})
test("EQ - equality comparison", async () => {
const str = `
PUSH 5
PUSH 5
EQ
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
const str2 = `
PUSH 5
PUSH 10
EQ
`
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
PUSH 10
NEQ
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
})
test("LT - less than", async () => {
const str = `
PUSH 5
PUSH 10
LT
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
})
test("GT - greater than", async () => {
const str = `
PUSH 10
PUSH 5
GT
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
})
test("LTE - less than or equal", async () => {
// equal case
const str = `
PUSH 5
PUSH 5
LTE
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
// less than case
const str2 = `
PUSH 3
PUSH 5
LTE
`
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
// greater than case (false)
const str3 = `
PUSH 10
PUSH 5
LTE
`
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
})
test("GTE - greater than or equal", async () => {
// equal case
const str = `
PUSH 5
PUSH 5
GTE
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
// greater than case
const str2 = `
PUSH 10
PUSH 5
GTE
`
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
// less than case (false)
const str3 = `
PUSH 3
PUSH 5
GTE
`
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
})
test("AND pattern - short circuits when false", async () => {
// false && <anything> should short-circuit and return false
const str = `
PUSH 1
PUSH 0
EQ
DUP
JUMP_IF_FALSE .end
POP
PUSH 999
.end:
`
const result = await run(toBytecode(str))
expect(result.type).toBe('boolean')
if (result.type === 'boolean') {
expect(result.value).toBe(false)
}
})
test("AND pattern - evaluates both when true", async () => {
const str = `
PUSH 1
DUP
JUMP_IF_FALSE .end
POP
PUSH 2
.end:
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
})
test("OR pattern - short circuits when true", async () => {
const str = `
PUSH 1
DUP
JUMP_IF_TRUE .end
POP
PUSH 2
.end:
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 })
})
test("OR pattern - evaluates second when false", async () => {
const str = `
PUSH 1
PUSH 0
EQ
DUP
JUMP_IF_TRUE .end
POP
PUSH 2
.end:
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
})
test("NOT - logical not", async () => {
// number is truthy, so NOT returns false
const str = `
PUSH 1
NOT
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
// 0 is truthy in this language, so NOT returns false
const str2 = `
PUSH 0
NOT
`
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
// boolean false is falsy, so NOT returns true
const str3 = `
PUSH 1
PUSH 0
EQ
NOT
`
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
})
test("isTruthy - only null and false are falsy", async () => {
// 0 is truthy (unlike JS)
const str1 = `
PUSH 0
JUMP_IF_FALSE .end
PUSH 1
.end:
`
expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 })
// empty string is truthy (unlike JS)
const str2 = `
PUSH ''
JUMP_IF_FALSE .end
PUSH 1
.end:
`
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 })
// false is falsy
const str3 = `
PUSH 0
PUSH 0
EQ
JUMP_IF_FALSE .end
PUSH 999
.end:
`
expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 })
})
test("HALT - stops execution", async () => {
const str = `
PUSH 42
HALT
PUSH 100
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("STORE and LOAD - variables", async () => {
const str = `
PUSH 42
STORE x
PUSH 21
LOAD x
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("STORE and LOAD - multiple variables", async () => {
const str = `
PUSH 10
STORE a
PUSH 20
STORE b
PUSH 44
LOAD a
LOAD b
ADD
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
})
test("TRY_LOAD - variable found", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD name
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'Bobby' })
})
test("TRY_LOAD - variable missing", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count1
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count1' })
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD full-name
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'full-name' })
})
test("TRY_LOAD - with different value types", async () => {
// Array
const str1 = `
PUSH 1
PUSH 2
PUSH 3
MAKE_ARRAY #3
STORE arr
TRY_LOAD arr
`
const result1 = await run(toBytecode(str1))
expect(result1.type).toBe('array')
// Dict
const str2 = `
PUSH 'key'
PUSH 'value'
MAKE_DICT #1
STORE dict
TRY_LOAD dict
`
const result2 = await run(toBytecode(str2))
expect(result2.type).toBe('dict')
// Boolean
const str3 = `
PUSH true
STORE flag
TRY_LOAD flag
`
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
// Null
const str4 = `
PUSH null
STORE empty
TRY_LOAD empty
`
expect(await run(toBytecode(str4))).toEqual({ type: 'null', value: null })
})
test("TRY_LOAD - in nested scope", async () => {
// Function should be able to TRY_LOAD variable from parent scope
const str = `
PUSH 42
STORE outer
MAKE_FUNCTION () .fn
PUSH 0
PUSH 0
CALL
HALT
.fn:
TRY_LOAD outer
RETURN
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("TRY_LOAD - missing variable in nested scope returns name", async () => {
// If variable doesn't exist in any scope, should return name as string
const str = `
PUSH 42
STORE outer
MAKE_FUNCTION () .fn
PUSH 0
PUSH 0
CALL
HALT
.fn:
TRY_LOAD inner
RETURN
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner' })
})
test("TRY_LOAD - used for conditional variable existence check", async () => {
// Pattern: use TRY_LOAD to check if variable exists and get its value or name
const str = `
PUSH 100
STORE count
TRY_LOAD count
PUSH 'count'
EQ
`
// Variable exists, so TRY_LOAD returns 100, which != 'count'
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
const str2 = `
PUSH 100
STORE count
TRY_LOAD missing
PUSH 'missing'
EQ
`
// Variable missing, so TRY_LOAD returns 'missing', which == 'missing'
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
})
test("TRY_LOAD - with function value", async () => {
const str = `
MAKE_FUNCTION () .fn
STORE myFunc
JUMP .skip
.fn:
PUSH 99
RETURN
.skip:
TRY_LOAD myFunc
`
const result = await run(toBytecode(str))
expect(result.type).toBe('function')
})
test("JUMP - relative jump forward", async () => {
const str = `
PUSH 1
JUMP .skip
PUSH 100
.skip:
PUSH 2
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
})
test("JUMP - forward jump skips instructions", async () => {
// Use forward jump to skip, demonstrating relative addressing
const str = `
PUSH 100
JUMP .end
PUSH 200
PUSH 300
.end:
PUSH 400
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 })
})
test("JUMP_IF_FALSE - conditional jump when false", async () => {
const str = `
PUSH 1
PUSH 0
EQ
JUMP_IF_FALSE .skip
PUSH 100
.skip:
PUSH 42
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("JUMP_IF_FALSE - no jump when true", async () => {
const str = `
PUSH 1
JUMP_IF_FALSE .skip
PUSH 100
.skip:
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
})
test("JUMP_IF_TRUE - conditional jump when true", async () => {
const str = `
PUSH 1
JUMP_IF_TRUE .skip
PUSH 100
.skip:
PUSH 42
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("MAKE_ARRAY - creates array", async () => {
const str = `
PUSH 10
PUSH 20
PUSH 30
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]).toEqual({ type: 'number', value: 10 })
expect(result.value[1]).toEqual({ type: 'number', value: 20 })
expect(result.value[2]).toEqual({ type: 'number', value: 30 })
}
})
test("ARRAY_GET - gets element", async () => {
const str = `
PUSH 10
PUSH 20
PUSH 30
MAKE_ARRAY #3
PUSH 1
ARRAY_GET
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 20 })
})
test("ARRAY_SET - sets element", async () => {
const str = `
PUSH 10
PUSH 20
PUSH 30
MAKE_ARRAY #3
DUP
PUSH 1
PUSH 99
ARRAY_SET
PUSH 1
ARRAY_GET
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 99 })
})
test("ARRAY_PUSH - appends to array", async () => {
const str = `
PUSH 10
PUSH 20
MAKE_ARRAY #2
DUP
PUSH 30
ARRAY_PUSH
ARRAY_LEN
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
})
test("ARRAY_PUSH - mutates original array", async () => {
const str = `
PUSH 10
PUSH 20
MAKE_ARRAY #2
DUP
PUSH 30
ARRAY_PUSH
PUSH 2
ARRAY_GET
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
})
test("ARRAY_LEN - gets length", async () => {
const str = `
PUSH 10
PUSH 20
PUSH 30
MAKE_ARRAY #3
ARRAY_LEN
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
})
test("MAKE_DICT - creates dict", async () => {
const str = `
PUSH 'name'
PUSH 'Alice'
PUSH 'age'
PUSH 30
MAKE_DICT #2
`
const result = await run(toBytecode(str))
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(2)
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
expect(result.value.get('age')).toEqual({ type: 'number', value: 30 })
}
})
test("DICT_GET - gets value", async () => {
const str = `
PUSH 'name'
PUSH 'Bob'
MAKE_DICT #1
PUSH 'name'
DICT_GET
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Bob' })
})
test("DICT_SET - sets value", async () => {
const str = `
MAKE_DICT #0
DUP
PUSH 'key'
PUSH 'value'
DICT_SET
PUSH 'key'
DICT_GET
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value' })
})
test("DICT_HAS - checks key exists", async () => {
const str = `
PUSH 'key'
PUSH 'value'
MAKE_DICT #1
PUSH 'key'
DICT_HAS
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
})
test("DICT_HAS - checks key missing", async () => {
const str = `
MAKE_DICT #0
PUSH 'missing'
DICT_HAS
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
})
test("STR_CONCAT - concats together strings", async () => {
const str = `
PUSH "Hi "
PUSH "friend"
PUSH "!"
STR_CONCAT #3
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hi friend!" })
const str2 = `
PUSH "Holy smokes!"
PUSH "It's "
PUSH "alive!"
STR_CONCAT #2
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "It's alive!" })
const str3 = `
PUSH 1
PUSH " + "
PUSH 1
PUSH " = "
PUSH 1
PUSH 1
ADD
STR_CONCAT #5
`
expect(await run(toBytecode(str3))).toEqual({ type: 'string', value: "1 + 1 = 2" })
})
test("STR_CONCAT - empty concat (count=0)", async () => {
const str = `
PUSH "leftover"
STR_CONCAT #0
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "" })
})
test("STR_CONCAT - single string", async () => {
const str = `
PUSH "hello"
STR_CONCAT #1
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "hello" })
})
test("STR_CONCAT - converts numbers to strings", async () => {
const str = `
PUSH 42
PUSH 100
PUSH 7
STR_CONCAT #3
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "421007" })
})
test("STR_CONCAT - converts booleans to strings", async () => {
const str = `
PUSH "Result: "
PUSH true
STR_CONCAT #2
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: true" })
const str2 = `
PUSH false
PUSH " is false"
STR_CONCAT #2
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "false is false" })
})
test("STR_CONCAT - converts null to strings", async () => {
const str = `
PUSH "Value: "
PUSH null
STR_CONCAT #2
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Value: null" })
})
test("STR_CONCAT - mixed types", async () => {
const str = `
PUSH "Count: "
PUSH 42
PUSH ", Active: "
PUSH true
PUSH ", Total: "
PUSH null
STR_CONCAT #6
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Count: 42, Active: true, Total: null" })
})
test("STR_CONCAT - array format", async () => {
const bytecode = toBytecode([
["PUSH", "Hello"],
["PUSH", " "],
["PUSH", "World"],
["STR_CONCAT", 3],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'string', value: "Hello World" })
})
test("STR_CONCAT - with variables", async () => {
const str = `
PUSH "Alice"
STORE name
PUSH "Hello, "
LOAD name
PUSH "!"
STR_CONCAT #3
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello, Alice!" })
})
test("STR_CONCAT - composable (multiple concatenations)", async () => {
const str = `
PUSH "Hello"
PUSH " "
PUSH "World"
STR_CONCAT #3
PUSH "!"
STR_CONCAT #2
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello World!" })
})
test("STR_CONCAT - with emoji and unicode", async () => {
const str = `
PUSH "Hello "
PUSH "🌍"
PUSH "!"
STR_CONCAT #3
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello 🌍!" })
const str2 = `
PUSH "こんにちは"
PUSH "世界"
STR_CONCAT #2
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "こんにちは世界" })
})
test("STR_CONCAT - with expressions", async () => {
const str = `
PUSH "Result: "
PUSH 10
PUSH 5
ADD
STR_CONCAT #2
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: 15" })
})
test("STR_CONCAT - large concat", async () => {
const str = `
PUSH "a"
PUSH "b"
PUSH "c"
PUSH "d"
PUSH "e"
PUSH "f"
PUSH "g"
PUSH "h"
PUSH "i"
PUSH "j"
STR_CONCAT #10
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "abcdefghij" })
})
test("BREAK - throws error when no break target", async () => {
// BREAK requires a break target frame on the call stack
// A single function call has no previous frame to mark as break target
const bytecode = toBytecode(`
MAKE_FUNCTION () .fn
PUSH 0
PUSH 0
CALL
HALT
.fn:
BREAK
`)
try {
await run(bytecode)
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.message).toContain('no break target found')
}
})
test("BREAK - exits from nested function call", async () => {
// BREAK unwinds to the break target (the outer function's frame)
// Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main)
const bytecode = toBytecode(`
MAKE_FUNCTION () .outer
PUSH 0
PUSH 0
CALL
PUSH 42
HALT
.outer:
MAKE_FUNCTION () .inner
PUSH 0
PUSH 0
CALL
PUSH 99
RETURN
.inner:
BREAK
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("JUMP backward - simple loop", async () => {
// Very simple: counter starts at 0, loops 3 times incrementing
// On 3rd iteration (counter==3), exits and returns counter
const bytecode = toBytecode(`
PUSH 0
STORE counter
.loop:
LOAD counter
PUSH 3
EQ
JUMP_IF_FALSE .body
LOAD counter
HALT
.body:
LOAD counter
PUSH 1
ADD
STORE counter
JUMP .loop
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 3 })
})
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 })
})
// ========================================
// RegExp Tests
// ========================================
test("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - NOT returns false (regex is truthy)", async () => {
const str = `
PUSH /pattern/
NOT
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
})
test("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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("RegExp - 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' })
}
})