fix validator
This commit is contained in:
parent
e1b45452f6
commit
8975bb91bd
|
|
@ -30,7 +30,6 @@ function isValidIdentifier(name: string): boolean {
|
|||
return !/[\s;()[\]{}='"#@.]/.test(name)
|
||||
}
|
||||
|
||||
// Opcodes that require operands
|
||||
const OPCODES_WITH_OPERANDS = new Set([
|
||||
OpCode.PUSH,
|
||||
OpCode.LOAD,
|
||||
|
|
@ -46,7 +45,6 @@ const OPCODES_WITH_OPERANDS = new Set([
|
|||
OpCode.CALL_NATIVE,
|
||||
])
|
||||
|
||||
// Opcodes that should NOT have operands
|
||||
const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||
OpCode.POP,
|
||||
OpCode.DUP,
|
||||
|
|
@ -78,6 +76,21 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
|
|||
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 {
|
||||
const errors: ValidationError[] = []
|
||||
const lines = source.split("\n")
|
||||
|
|
@ -172,6 +185,26 @@ export function validateBytecode(source: string): ValidationResult {
|
|||
|
||||
// Validate specific operand formats
|
||||
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
|
||||
if (operand.startsWith('.') && !operand.includes('(')) {
|
||||
const labelName = operand.slice(1)
|
||||
|
|
@ -246,7 +279,7 @@ export function validateBytecode(source: string): ValidationResult {
|
|||
}
|
||||
} else if (param.includes('=')) {
|
||||
// Default parameter
|
||||
const [name, defaultValue] = param.split('=')
|
||||
const [name] = param.split('=')
|
||||
if (!isValidIdentifier(name!.trim())) {
|
||||
errors.push({
|
||||
line: lineNum,
|
||||
|
|
|
|||
|
|
@ -200,3 +200,113 @@ test("formatValidationErrors produces readable output", () => {
|
|||
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)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user