forked from defunkt/ReefVM
313 lines
7.5 KiB
TypeScript
313 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 # or .label", () => {
|
|
const source = `
|
|
JUMP 5
|
|
HALT
|
|
`
|
|
const result = validateBytecode(source)
|
|
expect(result.valid).toBe(false)
|
|
expect(result.errors[0]!.message).toContain("JUMP requires immediate (#number) or label (.label)")
|
|
})
|
|
|
|
test("detects JUMP_IF_TRUE without # or .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 immediate (#number) or label (.label)")
|
|
})
|
|
|
|
test("detects JUMP_IF_FALSE without # or .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 immediate (#number) or label (.label)")
|
|
})
|
|
|
|
test("allows JUMP with immediate number", () => {
|
|
const source = `
|
|
JUMP #2
|
|
PUSH 999
|
|
HALT
|
|
`
|
|
const result = validateBytecode(source)
|
|
expect(result.valid).toBe(true)
|
|
})
|
|
|
|
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)
|
|
})
|