ReefVM/tests/validator.test.ts

314 lines
7.5 KiB
TypeScript

import { test, expect } from "bun:test"
import { validateBytecode, formatValidationErrors } from "#validator"
test("valid bytecode passes validation", () => {
const source = `
PUSH 1
PUSH 2
ADD
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(true)
expect(result.errors).toHaveLength(0)
})
test("valid bytecode with labels passes validation", () => {
const source = `
JUMP .end
PUSH 999
.end:
PUSH 42
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(true)
expect(result.errors).toHaveLength(0)
})
test("detects unknown opcode", () => {
const source = `
PUSH 1
INVALID_OP
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors).toHaveLength(1)
expect(result.errors[0]!.message).toContain("Unknown opcode: INVALID_OP")
})
test("detects undefined label", () => {
const source = `
JUMP .nowhere
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors).toHaveLength(1)
expect(result.errors[0]!.message).toContain("Undefined label: .nowhere")
})
test("detects duplicate labels", () => {
const source = `
.loop:
PUSH 1
.loop:
PUSH 2
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors).toHaveLength(1)
expect(result.errors[0]!.message).toContain("Duplicate label: .loop")
})
test("detects missing operand", () => {
const source = `
PUSH
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors).toHaveLength(1)
expect(result.errors[0]!.message).toContain("PUSH requires an operand")
})
test("detects unexpected operand", () => {
const source = `
ADD 42
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors).toHaveLength(1)
expect(result.errors[0]!.message).toContain("ADD does not take an operand")
})
test("detects invalid MAKE_FUNCTION syntax", () => {
const source = `
MAKE_FUNCTION x y .body
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("MAKE_FUNCTION requires parameter list")
})
test("detects invalid parameter order", () => {
const source = `
MAKE_FUNCTION (x ...rest y) .body
HALT
.body:
RETURN
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("variadic parameter")
})
test("detects invalid parameter name", () => {
const source = `
MAKE_FUNCTION (123invalid) .body
HALT
.body:
RETURN
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("Invalid parameter name")
})
test("detects invalid variable name", () => {
const source = `
LOAD 123invalid
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("Invalid variable name")
})
test("detects unterminated string", () => {
const source = `
PUSH "unterminated
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("Unterminated string")
})
test("detects invalid immediate number", () => {
const source = `
MAKE_ARRAY #abc
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("Invalid immediate number")
})
test("handles comments correctly", () => {
const source = `
PUSH 1 ; this is a comment
; this entire line is a comment
PUSH 2
ADD ; another comment
`
const result = validateBytecode(source)
expect(result.valid).toBe(true)
})
test("validates function with label reference", () => {
const source = `
MAKE_FUNCTION (x y) .body
JUMP .skip
.body:
LOAD x
LOAD y
ADD
RETURN
.skip:
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(true)
})
test("detects multiple errors and sorts by line", () => {
const source = `
UNKNOWN_OP
PUSH
JUMP .undefined
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors.length).toBeGreaterThanOrEqual(2)
// Check that errors are sorted by line number
for (let i = 1; i < result.errors.length; i++) {
expect(result.errors[i]!.line).toBeGreaterThanOrEqual(result.errors[i-1]!.line)
}
})
test("formatValidationErrors produces readable output", () => {
const source = `
PUSH 1
UNKNOWN
`
const result = validateBytecode(source)
const formatted = formatValidationErrors(result)
expect(formatted).toContain("error")
expect(formatted).toContain("Line")
expect(formatted).toContain("UNKNOWN")
})
test("detects JUMP without .label", () => {
const source = `
JUMP 5
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
})
test("detects JUMP_IF_TRUE without .label", () => {
const source = `
PUSH true
JUMP_IF_TRUE 2
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires label (.label)")
})
test("detects JUMP_IF_FALSE without .label", () => {
const source = `
PUSH false
JUMP_IF_FALSE 2
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires label (.label)")
})
test("rejects JUMP with immediate number", () => {
const source = `
JUMP #2
PUSH 999
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
})
test("detects MAKE_ARRAY without #", () => {
const source = `
PUSH 1
PUSH 2
MAKE_ARRAY 2
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("MAKE_ARRAY requires immediate number (#count)")
})
test("detects MAKE_DICT without #", () => {
const source = `
PUSH "key"
PUSH "value"
MAKE_DICT 1
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("MAKE_DICT requires immediate number (#count)")
})
test("allows MAKE_ARRAY with immediate number", () => {
const source = `
PUSH 1
PUSH 2
MAKE_ARRAY #2
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(true)
})
test("detects PUSH_TRY without # or .label", () => {
const source = `
PUSH_TRY 5
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("PUSH_TRY requires immediate (#number) or label (.label)")
})
test("detects PUSH_FINALLY without # or .label", () => {
const source = `
PUSH_FINALLY 5
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("PUSH_FINALLY requires immediate (#number) or label (.label)")
})
test("allows PUSH_TRY with label", () => {
const source = `
PUSH_TRY .catch
PUSH 42
HALT
.catch:
PUSH null
HALT
`
const result = validateBytecode(source)
expect(result.valid).toBe(true)
})