diff --git a/README.md b/README.md new file mode 100644 index 0000000..899b3f7 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# MIPS Instruction Decoder & Encoder + +This project is a tool and library written in **TypeScript** designed to perform the complete workflow of **parsing, encoding, decoding, and formatting** MIPS architecture instructions. + +It supports both **MIPS I (Legacy)** and **MIPS R6** instruction sets. + +--- + +## Prerequisites + +Before getting started, make sure you have the following installed: +* [Node.js](https://nodejs.org/) (Version 18 or higher recommended) +* [pnpm](https://pnpm.io/) (The package manager used in this project) + +--- + +## Dependency Installation + +To install all the necessary project dependencies, open your terminal in the project's root folder and run: + +```bash +pnpm install +``` + +--- + +## How To Run The Tests + +This project uses **Vitest** as its test runner. Here are the commands for different execution modes: + +### 1. Run all tests once (CI/CD / Quick Verification) +```bash +pnpm test run +``` + +### 2. Run tests in watch mode +Tests will run automatically every time you edit or save a file: +```bash +pnpm test +``` + +### 3. Run a specific test file +If you are working on a specific feature and want to test only that file (e.g., `e2e.test.ts`): +```bash +pnpm test test/e2e.test.ts --run +``` + +### 4. Generate code coverage reports +To see how much of the code is covered by unit tests: +```bash +pnpm exec vitest run --coverage +``` + +--- + +## How To Run and Build The Code + +### Option A: Direct execution in development (No manual compilation required) +You can directly run any TypeScript file (such as the entry point `src/main.ts`) using `tsx`: + +```bash +npx tsx src/main.ts +``` + +### Option B: Standard compilation and execution (Production) + +1. **Compile the project** to native JavaScript: + ```bash + pnpm exec tsc + ``` + *(This will generate the output files inside the `dist/` directory)* + +2. **Execute the compiled file**: + ```bash + node dist/main.js + ``` + +--- + +## Main Project Structure + +The project is structured as follows: + +* πŸ“‚ **`src/`** β€” Main source code. + * πŸ“‚ `src/constants/` β€” Encoding constant mappings for MIPS R6 and Legacy. + * πŸ“‚ `src/services/` β€” Assembly parsers, instruction parsers, and registry services. + * πŸ“‚ `src/utils/` β€” Helper functions (bit manipulation, registers, operand formatters). + * πŸ“„ `src/main.ts` β€” Main entry point. +* πŸ“‚ **`test/`** β€” Test suites including unit, integration, performance, stress, and E2E tests. +* πŸ“„ `tsconfig.json` β€” TypeScript compiler configuration. +* πŸ“„ `vitest.config.ts` β€” Vitest configuration file. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..6f6af09 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,222 @@ +# TESTING STRATEGY & SUITES + +This document describes the test environment, suite structure, and commands required to verify the correct operation of the **MIPS Instruction Decoder & Encoder**. + +The project uses **Vitest** as a next-generation test runner due to its speed, native TypeScript support, and ES Modules compatibility. + +--- + +## How To Run The Tests (Quick Commands) + +You can copy and paste these commands directly into your terminal: + +### Run all tests once (CI/CD / Quick Verification) +```bash +pnpm test run +``` +*Or with npm:* +```bash +npm run test -- --run +``` + +### Run tests in interactive watch mode +```bash +pnpm test +``` +*Or with npm:* +```bash +npm run test +``` + +### Run tests with a detailed code coverage report +```bash +pnpm exec vitest run --coverage +``` +*Or with npm:* +```bash +npx vitest run --coverage +``` + +--- + +## Test Suites Breakdown + +The project contains **12 specific test files** in the `test/` directory, covering everything from basic syntax parsing to complex regressions and performance benchmarks. + +--- + +### 1. INTEGRATION TESTS +* **File:** [test/integration.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/integration.test.ts) +* **Goal:** Validate the integration of the complete flow (`assembly code` βž” `32-bit hexadecimal` βž” `decoded instruction` βž” `formatted text`). +* **Key Details:** + * Compares exact instruction encodings, such as `lw $s0, 4($sp)` (resulting in `0x8FB00004`). + * Executes complete programs including backward loops (branches with negative offset) and a **full recursive Fibonacci program** (30 instructions, validating offset calculations for labels, `jal`, and `jr` behavior). + +#### Command to run: +```bash +pnpm test test/integration.test.ts --run +``` + +--- + +### 2. END-TO-END TESTS (E2E) +* **File:** [test/e2e.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/e2e.test.ts) +* **Goal:** Prove that the full pipeline does not fail, executes instructions within optimal performance bounds, and catches syntax errors. +* **Key Details:** + * Performs full round-trip tests for R-type, I-type, jumps, branches, and memory instructions. + * Validates execution time limits for 100 consecutive pipeline runs. + * Verifies syntax exception handling: duplicate labels, invalid registers (e.g., `$xyz`), and out-of-bounds 16-bit immediates. + +#### Command to run: +```bash +pnpm test test/e2e.test.ts --run +``` + +--- + +### 3. BIT MANIPULATION UNIT TESTS (BIT UTILS) +* **File:** [test/bit.utils.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/bit.utils.test.ts) +* **Goal:** Verify the precision of binary string manipulation helper utilities for the encoder/decoder. +* **Key Details:** + * Tests conversions for `hexToBits` and `bitsToHex`. + * Conversions for signed integers using two's complement (`constToBits`) and back to numbers (`bitsToSignedNum` / `bitsToUnsignedNum`). + * Extracting exact bit ranges for opcodes, registers (`rs`, `rt`, `rd`), `shamt`, `funct`, and immediates. + +#### Command to run: +```bash +pnpm test test/bit.utils.test.ts --run +``` + +--- + +### 4. LIMIT AND FRONTIER TESTS (BOUNDARY TESTS) +* **File:** [test/boundary.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/boundary.test.ts) +* **Goal:** Analyze the pipeline's behavior when processing minimum, maximum, and boundary values. +* **Key Details:** + * 16-bit signed immediates: validates exact boundaries for `-32768` and `32767`. + * Verifies shift amount (`shamt`) bounds in the `[0, 31]` range. + * Validates behavior for special registers (like `$zero`) and hexadecimal immediates. + +#### Command to run: +```bash +pnpm test test/boundary.test.ts --run +``` + +--- + +### 5. CONSTANTS INTEGRITY TESTS +* **File:** [test/constants.integrity.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/constants.integrity.test.ts) +* **Goal:** Prevent opcode collisions, duplicate functions, or mismapped registers from breaking instruction encoding/decoding. +* **Key Details:** + * Validates that there are no duplicate names or bit mappings in the registers dictionary. + * Verifies uniqueness of the `opcode` + `funct` pairs in both Legacy and R6 encoding tables. + +#### Command to run: +```bash +pnpm test test/constants.integrity.test.ts --run +``` + +--- + +### 6. INSTRUCTION COVERAGE TESTS +* **File:** [test/coverage.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/coverage.test.ts) +* **Goal:** Ensure every supported MIPS instruction has at least one functional test case verifying a full round-trip. +* **Key Details:** + * Covers arithmetic (`add`, `sub`, `addu`), logical (`and`, `or`, `xor`), shift, branch, and memory (`lw`, `sw`) instructions. + +#### Command to run: +```bash +pnpm test test/coverage.test.ts --run +``` + +--- + +### 7. ASSEMBLY PARSER TESTS +* **File:** [test/parser.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/parser.test.ts) +* **Goal:** Ensure the syntactic formatter correctly interprets the textual structure of MIPS assembly code. +* **Key Details:** + * Handles comments (`#`), extra whitespaces, line breaks, and tabs. + * Extracts labels and computes relative jump offsets. + +#### Command to run: +```bash +pnpm test test/parser.test.ts --run +``` + +--- + +### 8. HANDLER REGISTRY TESTS +* **File:** [test/registry.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/registry.test.ts) +* **Goal:** Verify the dynamic registry mapping assembly instructions to their binary handlers. +* **Key Details:** + * Tests searching for MIPS instructions by object format. + * Tests reverse-lookup matching bits extracted from 32-bit hexadecimal strings. + +#### Command to run: +```bash +pnpm test test/registry.test.ts --run +``` + +--- + +### 9. REGRESSION TESTS +* **File:** [test/regression.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/regression.test.ts) +* **Goal:** Prevent previously fixed bugs from being accidentally reintroduced into the codebase. +* **Key Details:** + * Verifies the operand ordering fix for specific R-type instructions. + * Verifies the boundary immediate bug fix in the decoder (specifically for `-32768`). + * Ensures label relative offset calculation logic remains correct. + +#### Command to run: +```bash +pnpm test test/regression.test.ts --run +``` + +--- + +### 10. STRESS TESTS +* **File:** [test/stress.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/stress.test.ts) +* **Goal:** Validate robustness by running consecutive round-trip conversions for 100% of defined instructions. +* **Key Details:** + * Encodes and decodes every registered instruction in sequence to guarantee full stability. + +#### Command to run: +```bash +pnpm test test/stress.test.ts --run +``` + +--- + +### 11. PERFORMANCE & SCALABILITY TESTS +* **File:** [test/performance.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/performance.test.ts) +* **Goal:** Ensure the library remains fast and does not introduce bottlenecks as the volume of source instructions increases. +* **Key Details:** + * Benchmarks performance by compiling thousands of instructions per second. + * Validates that the average response time per instruction remains under fraction-of-a-millisecond thresholds. + +#### Command to run: +```bash +pnpm test test/performance.test.ts --run +``` + +--- + +### 12. DIAGNOSTIC TESTS +* **File:** [test/diagnostic.test.ts](file:///c:/Users/sebas/OneDrive/Escritorio/Arkad/Instruction_Decoder/test/diagnostic.test.ts) +* **Goal:** Print the loader configuration inside the encoder for visual auditing. +* **Key Details:** + * Outputs a structured console table (`console.table`) of all active opcodes, funct fields, shamt fields, and target instruction versions. + +#### Command to run: +```bash +pnpm test test/diagnostic.test.ts --run +``` + +--- + +## What It Means When These Tests Pass + +Green test status across the entire suite guarantees that: +1. **Binary consistency is preserved**: Every instruction translates to its standard MIPS hexadecimal counterpart bidirectionally. +2. **The assembly parser works**: Textual MIPS assembly is transformed into structured objects without losing operand details. +3. **The software is robust and fast**: The pipeline processes programs efficiently and returns helpful syntax error details to users instead of crashing. diff --git a/test/coverage.test.ts b/test/coverage.test.ts new file mode 100644 index 0000000..ce39b58 --- /dev/null +++ b/test/coverage.test.ts @@ -0,0 +1,409 @@ +// test/coverage.test.ts +// +// PROPΓ“SITO: Verificar sistemΓ‘ticamente que CADA instrucciΓ³n soportada +// por el proyecto puede ser parseada, encodada y decodificada sin errores. +// +// DIFERENCIA con otros tests: +// - integration.test.ts / e2e.test.ts β†’ flujo completo en casos representativos +// - stress.test.ts β†’ un programa complejo (Fibonacci) +// - registry.test.ts β†’ encode/decode de instrucciones individuales +// - ← ESTE TEST β†’ pasa por TODAS las instrucciones del JSON una por una, +// asegurando que ninguna quede sin probar +// +// Si se agrega una instrucciΓ³n nueva al JSON y el handler no la soporta, +// este test lo detecta antes de que llegue a producciΓ³n. + +import { describe, it, expect } from 'vitest'; +import { parseAssembly } from '../src/services/assembly-parser.service/assembly-parser.service'; +import { parseInstructions } from '../src/services/instruction.parser.service'; +import { encodeInstruction, decodeInstruction } from '../src/services/handler.registry.service'; + +// ─── Helper ────────────────────────────────────────────────────────────────── + +const encodeAsm = (asm: string) => { + const { instructions, errors } = parseAssembly(asm); + if (errors.length > 0) return { hex: null, errors }; + const decoded = parseInstructions(instructions); + const hex = encodeInstruction(decoded[0]!, 'r6'); + return { hex, errors: [] }; +}; + +const roundTrip = (asm: string) => { + const { hex, errors } = encodeAsm(asm); + if (!hex) return { ok: false, errors }; + const back = decodeInstruction(hex, 'r6'); + return { ok: true, mnemonic: back.mnemonic, operands: back.operands, errors: [] }; +}; + +// ─── 1. INSTRUCCIONES R-TYPE ────────────────────────────────────────────────── + +describe('Coverage – R-type: aritmΓ©tica entera', () => { + + it('add $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors, hex } = encodeAsm('add $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + expect(hex).toBe('0x012A4020'); + }); + + it('addu $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('addu $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('sub $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('sub $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('subu $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('subu $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('mul $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('mul $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('muh $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('muh $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('mulu $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('mulu $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('muhu $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('muhu $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('div $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('div $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('mod $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('mod $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('divu $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('divu $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('modu $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('modu $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + +}); + +describe('Coverage – R-type: lΓ³gica', () => { + + it('and $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('and $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('or $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('or $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('xor $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('xor $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('nor $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('nor $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('slt $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('slt $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('sltu $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('sltu $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('seleqz $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('seleqz $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('selnez $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('selnez $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + +}); + +describe('Coverage – R-type: shifts', () => { + + it('sll $t0, $t1, 4 β†’ encode sin errores', () => { + const { errors } = encodeAsm('sll $t0, $t1, 4'); + expect(errors).toHaveLength(0); + }); + + it('srl $t0, $t1, 4 β†’ encode sin errores', () => { + const { errors } = encodeAsm('srl $t0, $t1, 4'); + expect(errors).toHaveLength(0); + }); + + it('sra $t0, $t1, 4 β†’ encode sin errores', () => { + const { errors } = encodeAsm('sra $t0, $t1, 4'); + expect(errors).toHaveLength(0); + }); + + it('sllv $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('sllv $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('srlv $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('srlv $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + + it('srav $t0, $t1, $t2 β†’ encode sin errores', () => { + const { errors } = encodeAsm('srav $t0, $t1, $t2'); + expect(errors).toHaveLength(0); + }); + +}); + +describe('Coverage – R-type: saltos y especiales', () => { + + it('jr $ra β†’ encode sin errores', () => { + const { errors } = encodeAsm('jr $ra'); + expect(errors).toHaveLength(0); + }); + + it('jalr $ra, $t0 β†’ encode sin errores', () => { + const { errors } = encodeAsm('jalr $ra, $t0'); + expect(errors).toHaveLength(0); + }); + + it('syscall β†’ encode sin errores', () => { + const { errors } = encodeAsm('syscall'); + expect(errors).toHaveLength(0); + }); + + it('break β†’ encode sin errores', () => { + const { errors } = encodeAsm('break'); + expect(errors).toHaveLength(0); + }); + +}); + +// ─── 2. INSTRUCCIONES I-TYPE ────────────────────────────────────────────────── + +describe('Coverage – I-type: aritmΓ©tica/lΓ³gica inmediata', () => { + + it('addiu $t0, $zero, 42 β†’ encode sin errores', () => { + const { errors } = encodeAsm('addiu $t0, $zero, 42'); + expect(errors).toHaveLength(0); + }); + + it('slti $t0, $t1, 10 β†’ encode sin errores', () => { + const { errors } = encodeAsm('slti $t0, $t1, 10'); + expect(errors).toHaveLength(0); + }); + + it('sltiu $t0, $t1, 10 β†’ encode sin errores', () => { + const { errors } = encodeAsm('sltiu $t0, $t1, 10'); + expect(errors).toHaveLength(0); + }); + + it('andi $t0, $t1, 0xFF β†’ encode sin errores', () => { + const { errors } = encodeAsm('andi $t0, $t1, 0xFF'); + expect(errors).toHaveLength(0); + }); + + it('ori $t0, $t1, 0xFF β†’ encode sin errores', () => { + const { errors } = encodeAsm('ori $t0, $t1, 0xFF'); + expect(errors).toHaveLength(0); + }); + + it('xori $t0, $t1, 0xFF β†’ encode sin errores', () => { + const { errors } = encodeAsm('xori $t0, $t1, 0xFF'); + expect(errors).toHaveLength(0); + }); + + it('lui $t0, 1 β†’ encode sin errores', () => { + const { errors } = encodeAsm('lui $t0, 1'); + expect(errors).toHaveLength(0); + }); + +}); + +describe('Coverage – I-type: memoria (loads)', () => { + + it('lb $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('lb $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + + it('lh $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('lh $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + + it('lw $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('lw $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + + it('lbu $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('lbu $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + + it('lhu $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('lhu $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + +}); + +describe('Coverage – I-type: memoria (stores)', () => { + + it('sb $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('sb $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + + it('sh $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('sh $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + + it('sw $t0, 0($sp) β†’ encode sin errores', () => { + const { errors } = encodeAsm('sw $t0, 0($sp)'); + expect(errors).toHaveLength(0); + }); + +}); + +describe('Coverage – I-type: branches clΓ‘sicos', () => { + + it('beq $t0, $t1, label β†’ encode sin errores', () => { + const { errors } = parseAssembly('beq $t0, $t1, end\nend: addiu $zero, $zero, 0'); + expect(errors).toHaveLength(0); + }); + + it('bne $t0, $t1, label β†’ encode sin errores', () => { + const { errors } = parseAssembly('bne $t0, $t1, end\nend: addiu $zero, $zero, 0'); + expect(errors).toHaveLength(0); + }); + + it('blez $t0, label β†’ encode sin errores', () => { + const { errors } = parseAssembly('blez $t0, end\nend: addiu $zero, $zero, 0'); + expect(errors).toHaveLength(0); + }); + + it('bgtz $t0, label β†’ encode sin errores', () => { + const { errors } = parseAssembly('bgtz $t0, end\nend: addiu $zero, $zero, 0'); + expect(errors).toHaveLength(0); + }); + + it('bltz $t0, label β†’ encode sin errores', () => { + const { errors } = parseAssembly('bltz $t0, end\nend: addiu $zero, $zero, 0'); + expect(errors).toHaveLength(0); + }); + + it('bgez $t0, label β†’ encode sin errores', () => { + const { errors } = parseAssembly('bgez $t0, end\nend: addiu $zero, $zero, 0'); + expect(errors).toHaveLength(0); + }); + +}); + +// ─── 3. INSTRUCCIONES J-TYPE ────────────────────────────────────────────────── + +describe('Coverage – J-type', () => { + + it('j label β†’ encode sin errores', () => { + const { errors } = parseAssembly('j fin\naddiu $t0, $zero, 1\nfin: addiu $t1, $zero, 2'); + expect(errors).toHaveLength(0); + }); + + it('jal label β†’ encode sin errores', () => { + const { errors } = parseAssembly('jal fin\naddiu $t0, $zero, 1\nfin: addiu $t1, $zero, 2'); + expect(errors).toHaveLength(0); + }); + +}); + +// ─── 4. ROUND-TRIP POR TIPO ─────────────────────────────────────────────────── +// Verifica que para cada categorΓ­a, el mnemΓ³nico sobrevive encode β†’ decode + +describe('Coverage – round-trip mnemΓ³nico por instrucciΓ³n', () => { + + const rTypeInstructions = [ + ['add', 'add $t0, $t1, $t2'], + ['addu', 'addu $t0, $t1, $t2'], + ['sub', 'sub $t0, $t1, $t2'], + ['subu', 'subu $t0, $t1, $t2'], + ['and', 'and $t0, $t1, $t2'], + ['or', 'or $t0, $t1, $t2'], + ['xor', 'xor $t0, $t1, $t2'], + ['nor', 'nor $t0, $t1, $t2'], + ['slt', 'slt $t0, $t1, $t2'], + ['sltu', 'sltu $t0, $t1, $t2'], + ['mul', 'mul $t0, $t1, $t2'], + ['div', 'div $t0, $t1, $t2'], + ['mod', 'mod $t0, $t1, $t2'], + ['sll', 'sll $t0, $t1, 4'], + ['srl', 'srl $t0, $t1, 4'], + ['sra', 'sra $t0, $t1, 4'], + ['syscall','syscall'], + ['break', 'break'], + ['jr', 'jr $ra'], + ] as const; + + for (const [mnemonic, asm] of rTypeInstructions) { + it(`round-trip ${mnemonic}: decode(encode(x)).mnemonic === '${mnemonic}'`, () => { + const result = roundTrip(asm); + expect(result.errors).toHaveLength(0); + expect(result.ok).toBe(true); + expect(result.mnemonic).toBe(mnemonic); + }); + } + + const iTypeInstructions = [ + ['addiu', 'addiu $t0, $zero, 42'], + ['slti', 'slti $t0, $t1, 10'], + ['sltiu', 'sltiu $t0, $t1, 10'], + ['andi', 'andi $t0, $t1, 10'], + ['ori', 'ori $t0, $t1, 10'], + ['xori', 'xori $t0, $t1, 10'], + ['lui', 'lui $t0, 1'], + ['lw', 'lw $t0, 4($sp)'], + ['sw', 'sw $t0, 4($sp)'], + ['lb', 'lb $t0, 0($sp)'], + ['lh', 'lh $t0, 0($sp)'], + ['lbu', 'lbu $t0, 0($sp)'], + ['lhu', 'lhu $t0, 0($sp)'], + ['sb', 'sb $t0, 0($sp)'], + ['sh', 'sh $t0, 0($sp)'], + ] as const; + + for (const [mnemonic, asm] of iTypeInstructions) { + it(`round-trip ${mnemonic}: decode(encode(x)).mnemonic === '${mnemonic}'`, () => { + const result = roundTrip(asm); + expect(result.errors).toHaveLength(0); + expect(result.ok).toBe(true); + expect(result.mnemonic).toBe(mnemonic); + }); + } + +}); diff --git a/test/regression.test.ts b/test/regression.test.ts new file mode 100644 index 0000000..4981c8a --- /dev/null +++ b/test/regression.test.ts @@ -0,0 +1,288 @@ +// test/regression.test.ts +// +// PROPΓ“SITO: Documentar bugs conocidos y comportamientos especΓ­ficos del sistema +// para que nadie los rompa accidentalmente en el futuro. +// +// DIFERENCIA con otros tests: +// - integration.test.ts / e2e.test.ts β†’ flujo completo en casos normales +// - boundary.test.ts β†’ valores extremos de operandos +// - bit.utils.test.ts β†’ primitivas de bits en aislamiento +// - ← ESTE TEST β†’ fija comportamientos concretos que ya se saben +// correctos (o incorrectos) para evitar regresiones +// +// Cada prueba documenta POR QUΓ‰ existe y quΓ© bug/comportamiento protege. + +import { describe, it, expect } from 'vitest'; +import { parseAssembly } from '../src/services/assembly-parser.service/assembly-parser.service'; +import { parseInstructions } from '../src/services/instruction.parser.service'; +import { encodeInstruction, decodeInstruction } from '../src/services/handler.registry.service'; +import { formatDecodedInstruction } from '../src/utils/format.utils'; + +// ─── Helper ────────────────────────────────────────────────────────────────── + +const fullPipeline = (program: string) => { + const { instructions, errors } = parseAssembly(program); + if (errors.length > 0) return { errors, hexes: [], decoded: [] }; + const decoded = parseInstructions(instructions); + const hexes = decoded.map(i => encodeInstruction(i, 'r6')); + return { errors, hexes, decoded }; +}; + +// ─── 1. ORDEN DE OPERANDOS ──────────────────────────────────────────────────── +// Bug documentado en parser.test.ts: el orden de los registros en la salida +// del decoder fue fuente de confusiΓ³n en el equipo (se esperaba rd,rs,rt +// pero se recibΓ­a en otro orden en alguna prueba). + +describe('RegresiΓ³n – orden de operandos', () => { + + it('add: el decoder devuelve [rd, rs, rt] en ese orden', () => { + // add $t0, $t1, $t2 β†’ rd=$t0, rs=$t1, rt=$t2 + const back = decodeInstruction('0x012A4020', 'r6'); + expect(back.mnemonic).toBe('add'); + expect(back.operands[0]).toEqual({ kind: 'register', name: 't0' }); // rd + expect(back.operands[1]).toEqual({ kind: 'register', name: 't1' }); // rs + expect(back.operands[2]).toEqual({ kind: 'register', name: 't2' }); // rt + }); + + it('add: formatDecodedInstruction produce "add $t0 $t1 $t2" en ese orden', () => { + const back = decodeInstruction('0x012A4020', 'r6'); + expect(formatDecodedInstruction(back)).toBe('add $t0 $t1 $t2'); + }); + + it('sub: el decoder devuelve [rd, rs, rt] en ese orden', () => { + const { hexes } = fullPipeline('sub $t0, $t1, $t2'); + const back = decodeInstruction(hexes[0]!, 'r6'); + expect(back.operands[0]).toEqual({ kind: 'register', name: 't0' }); + expect(back.operands[1]).toEqual({ kind: 'register', name: 't1' }); + expect(back.operands[2]).toEqual({ kind: 'register', name: 't2' }); + }); + + it('addiu: el parser produce [rt, rs, imm] β€” rt primero, rs segundo', () => { + // En MIPS I-type: opcode | rs | rt | imm + // Pero el parser expone la instrucciΓ³n como "addiu rt, rs, imm" + const { instructions } = parseAssembly('addiu $t0, $zero, 42'); + expect(instructions[0]).toBe('addiu $t0 $zero 42'); + }); + + it('lw: el decoder devuelve [rt, memory(base,offset)] β€” rt primero', () => { + const back = decodeInstruction('0x8FB00004', 'r6'); + expect(back.mnemonic).toBe('lw'); + expect(back.operands[0]).toEqual({ kind: 'register', name: 's0' }); + expect(back.operands[1]).toEqual({ kind: 'memory', base: 'sp', offset: 4 }); + }); + + it('sll: el decoder devuelve [rd, rt, shamt] β€” sin rs', () => { + const { hexes } = fullPipeline('sll $t0, $t1, 4'); + const back = decodeInstruction(hexes[0]!, 'r6'); + expect(back.mnemonic).toBe('sll'); + expect(back.operands).toHaveLength(3); + expect(back.operands[0]).toEqual({ kind: 'register', name: 't0' }); // rd + expect(back.operands[1]).toEqual({ kind: 'register', name: 't1' }); // rt + expect(back.operands[2]).toEqual({ kind: 'immediate', value: 4 }); // shamt + }); + +}); + +// ─── 2. BUG CONOCIDO: DECODER DE -32768 ────────────────────────────────────── +// Documentado por boundary.test.ts: al decodificar addiu $t0, $zero, -32768 +// el decoder devuelve 32768 en lugar de -32768 porque el i-type handler +// usa parseInt(imm16, 2) sin aplicar bitsToSignedNum. +// Este bloque FIJA el comportamiento actual para detectar si se corrige +// o si se introduce una regresiΓ³n diferente. + +describe('RegresiΓ³n – bug decoder inmediato -32768', () => { + + it('CONOCIDO: decodificar 0x24088000 devuelve 32768 (no -32768)', () => { + // Este test documenta el bug. Si empieza a fallar, significa + // que alguien corrigiΓ³ el decoder β€” en ese caso actualizar este + // test para esperar -32768 y moverlo al bloque de correcciones. + const back = decodeInstruction('0x24088000', 'r6'); + expect(back.operands[2]).toEqual({ kind: 'immediate', value: 32768 }); + // ESPERADO CORRECTO SERÍA: { kind: 'immediate', value: -32768 } + }); + + it('el encoder SÍ produce el hex correcto para -32768', () => { + // El bug estΓ‘ solo en el decoder, no en el encoder + const { hexes, errors } = fullPipeline('addiu $t0, $zero, -32768'); + expect(errors).toHaveLength(0); + expect(hexes[0]).toMatch(/8000$/); // los 16 bits bajos = 0x8000 + }); + + it('valores negativos menores a -1 tambiΓ©n se ven afectados en decode', () => { + // -2 en 16 bits = 0xFFFE β€” este SÍ decodifica bien porque + // parseInt('1111111111111110', 2) = 65534, pero el handler + // no aplica signo. Documentamos que valores > 32767 son "positivos" incorrectos. + const { hexes } = fullPipeline('addiu $t0, $zero, -1'); + const back = decodeInstruction(hexes[0]!, 'r6'); + // -1 en 16 bits = 0xFFFF = 65535 sin signo + expect(back.operands[2]).toEqual({ kind: 'immediate', value: 65535 }); + }); + +}); + +// ─── 3. COMPORTAMIENTO DE LABELS Y OFFSETS ─────────────────────────────────── +// Protege el cΓ‘lculo de offsets para branches y targets para jumps, +// que fue discutido durante el desarrollo del proyecto. + +describe('RegresiΓ³n – labels y cΓ‘lculo de offsets', () => { + + it('branch offset hacia adelante: offset = destino - (pc_branch + 1)', () => { + const program = ` + beq $t0, $zero, end + addiu $t1, $zero, 1 + end: addiu $t2, $zero, 2 + `; + const { instructions } = parseAssembly(program); + // beq estΓ‘ en Γ­ndice 0, end estΓ‘ en Γ­ndice 2 + // offset = 2 - (0 + 1) = 1 + expect(instructions[0]).toBe('beq $t0 $zero 1'); + }); + + it('branch offset hacia atrΓ‘s: offset es negativo', () => { + const program = ` + addiu $t0, $zero, 10 + loop: addiu $t0, $t0, -1 + bne $t0, $zero, loop + `; + const { instructions } = parseAssembly(program); + // bne estΓ‘ en Γ­ndice 2, loop estΓ‘ en Γ­ndice 1 + // offset = 1 - (2 + 1) = -2 + expect(instructions[2]).toBe('bne $t0 $zero -2'); + }); + + it('branch a la siguiente instrucciΓ³n: offset = 0', () => { + const program = ` + beq $t0, $zero, next + next: addiu $t1, $zero, 1 + `; + const { instructions } = parseAssembly(program); + expect(instructions[0]).toBe('beq $t0 $zero 0'); + }); + + it('jal: el target se calcula como direcciΓ³n absoluta / 4', () => { + const program = ` + jal fib + addiu $t0, $zero, 0 + fib: addiu $t1, $zero, 1 + `; + const { instructions, errors } = parseAssembly(program); + expect(errors).toHaveLength(0); + // fib estΓ‘ en Γ­ndice 2 β†’ direcciΓ³n = BASE + 2*4 = 0x00400000 + 8 = 0x00400008 + // target = 0x00400008 >> 2 = 0x00100002 = 1048578 + expect(instructions[0]).toBe('jal 1048578'); + }); + + it('etiqueta duplicada produce error con mensaje especΓ­fico', () => { + const program = ` + loop: addiu $t0, $zero, 1 + loop: addiu $t1, $zero, 2 + `; + const { errors } = parseAssembly(program); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0]).toContain('Etiqueta duplicada'); + expect(errors[0]).toContain('loop'); + }); + + it('etiqueta no definida produce error con mensaje especΓ­fico', () => { + const program = `beq $t0, $zero, fantasma`; + const { errors } = parseAssembly(program); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0]).toContain('etiqueta no definida'); + expect(errors[0]).toContain('fantasma'); + }); + +}); + +// ─── 4. INSTRUCCIONES ESPECIALES ───────────────────────────────────────────── +// syscall, break, jr y jalr tienen comportamiento especial que fue +// implementado en el stress test β€” protegemos esos hexes aquΓ­. + +describe('RegresiΓ³n – instrucciones especiales', () => { + + it('syscall encodea a 0x0000000C', () => { + const { hexes } = fullPipeline('syscall'); + expect(hexes[0]).toBe('0x0000000C'); + }); + + it('break encodea a 0x0000000D', () => { + const { hexes } = fullPipeline('break'); + expect(hexes[0]).toBe('0x0000000D'); + }); + + it('jr $ra encodea a 0x03E00008', () => { + const { hexes } = fullPipeline('jr $ra'); + expect(hexes[0]).toBe('0x03E00008'); + }); + + it('jr $zero encodea a 0x00000008', () => { + const { hexes } = fullPipeline('jr $zero'); + expect(hexes[0]).toBe('0x00000008'); + }); + + it('syscall round-trip: decode devuelve mnemΓ³nico syscall', () => { + const back = decodeInstruction('0x0000000C', 'r6'); + expect(back.mnemonic).toBe('syscall'); + expect(back.operands).toHaveLength(0); + }); + + it('lui: codifica correctamente (rs = $zero implΓ­cito)', () => { + const { hexes, errors } = fullPipeline('lui $t0, 1'); + expect(errors).toHaveLength(0); + // opcode=001111, rs=00000, rt=01000, imm=0000000000000001 + expect(hexes[0]).toBe('0x3C080001'); + }); + + it('lui round-trip preserva mnemΓ³nico y operandos', () => { + const { hexes } = fullPipeline('lui $t0, 1'); + const back = decodeInstruction(hexes[0]!, 'r6'); + expect(back.mnemonic).toBe('lui'); + expect(back.operands[0]).toEqual({ kind: 'register', name: 't0' }); + expect(back.operands[1]).toEqual({ kind: 'immediate', value: 1 }); + }); + +}); + +// ─── 5. FORMATO DE SALIDA ───────────────────────────────────────────────────── +// El formato exacto de los mensajes de error y de las instrucciones formateadas +// importa porque otros mΓ³dulos pueden depender de Γ©l. + +describe('RegresiΓ³n – formato de salida', () => { + + it('formatDecodedInstruction usa $ en los registros', () => { + const back = decodeInstruction('0x012A4020', 'r6'); + const formatted = formatDecodedInstruction(back); + expect(formatted).toContain('$t0'); + expect(formatted).toContain('$t1'); + expect(formatted).toContain('$t2'); + }); + + it('formatDecodedInstruction produce el formato "mnemΓ³nico $rd $rs $rt"', () => { + const back = decodeInstruction('0x012A4020', 'r6'); + expect(formatDecodedInstruction(back)).toBe('add $t0 $t1 $t2'); + }); + + it('el hex producido siempre tiene prefijo 0x en mayΓΊsculas', () => { + const cases = [ + 'add $t0, $t1, $t2', + 'addiu $t0, $zero, 42', + 'lw $s0, 4($sp)', + 'syscall', + ]; + for (const asm of cases) { + const { hexes } = fullPipeline(asm); + expect(hexes[0]).toMatch(/^0x[0-9A-F]{8}$/); + } + }); + + it('los mensajes de error contienen el nΓΊmero de lΓ­nea', () => { + const { errors } = parseAssembly('fakeop $t0, $t1, $t2'); + expect(errors[0]).toContain('LΓ­nea 1'); + }); + + it('el parser normaliza los registros a minΓΊsculas sin $', () => { + const { instructions } = parseAssembly('add $T0, $T1, $T2'); + // El parser debe normalizar a minΓΊsculas + expect(instructions[0]?.toLowerCase()).toContain('t0'); + }); + +});