fix validator

This commit is contained in:
Chris Wanstrath 2025-10-10 14:02:49 -07:00
parent e1b45452f6
commit 8975bb91bd
2 changed files with 146 additions and 3 deletions

View File

@ -30,7 +30,6 @@ function isValidIdentifier(name: string): boolean {
return !/[\s;()[\]{}='"#@.]/.test(name) return !/[\s;()[\]{}='"#@.]/.test(name)
} }
// Opcodes that require operands
const OPCODES_WITH_OPERANDS = new Set([ const OPCODES_WITH_OPERANDS = new Set([
OpCode.PUSH, OpCode.PUSH,
OpCode.LOAD, OpCode.LOAD,
@ -46,7 +45,6 @@ const OPCODES_WITH_OPERANDS = new Set([
OpCode.CALL_NATIVE, OpCode.CALL_NATIVE,
]) ])
// Opcodes that should NOT have operands
const OPCODES_WITHOUT_OPERANDS = new Set([ const OPCODES_WITHOUT_OPERANDS = new Set([
OpCode.POP, OpCode.POP,
OpCode.DUP, OpCode.DUP,
@ -78,6 +76,21 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
OpCode.DICT_HAS, OpCode.DICT_HAS,
]) ])
// immediate = immediate number, eg #5
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
OpCode.JUMP,
OpCode.JUMP_IF_FALSE,
OpCode.JUMP_IF_TRUE,
OpCode.PUSH_TRY,
OpCode.PUSH_FINALLY,
])
// immediate = immediate number, eg #5
const OPCODES_REQUIRING_IMMEDIATE = new Set([
OpCode.MAKE_ARRAY,
OpCode.MAKE_DICT,
])
export function validateBytecode(source: string): ValidationResult { export function validateBytecode(source: string): ValidationResult {
const errors: ValidationError[] = [] const errors: ValidationError[] = []
const lines = source.split("\n") const lines = source.split("\n")
@ -172,6 +185,26 @@ export function validateBytecode(source: string): ValidationResult {
// Validate specific operand formats // Validate specific operand formats
if (operand) { if (operand) {
if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) {
if (!operand.startsWith('#') && !operand.startsWith('.')) {
errors.push({
line: lineNum,
message: `${opName} requires immediate (#number) or label (.label), got: ${operand}`,
})
continue
}
}
if (OPCODES_REQUIRING_IMMEDIATE.has(opCode)) {
if (!operand.startsWith('#')) {
errors.push({
line: lineNum,
message: `${opName} requires immediate number (#count), got: ${operand}`,
})
continue
}
}
// Check for label references // Check for label references
if (operand.startsWith('.') && !operand.includes('(')) { if (operand.startsWith('.') && !operand.includes('(')) {
const labelName = operand.slice(1) const labelName = operand.slice(1)
@ -246,7 +279,7 @@ export function validateBytecode(source: string): ValidationResult {
} }
} else if (param.includes('=')) { } else if (param.includes('=')) {
// Default parameter // Default parameter
const [name, defaultValue] = param.split('=') const [name] = param.split('=')
if (!isValidIdentifier(name!.trim())) { if (!isValidIdentifier(name!.trim())) {
errors.push({ errors.push({
line: lineNum, line: lineNum,

View File

@ -200,3 +200,113 @@ test("formatValidationErrors produces readable output", () => {
expect(formatted).toContain("Line") expect(formatted).toContain("Line")
expect(formatted).toContain("UNKNOWN") 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)
})