diff --git a/src/validator.ts b/src/validator.ts index 7c55e83..9321cec 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -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, diff --git a/tests/validator.test.ts b/tests/validator.test.ts index 8588e78..0afd65b 100644 --- a/tests/validator.test.ts +++ b/tests/validator.test.ts @@ -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) +})