import { describe, it } from "mocha"; import assert from "assert"; import { Assembler } from "../../src/worker/assembler"; describe('Assembler', function () { describe('Basic Assembly', function () { it('Should assemble simple 8-bit instructions', function () { const spec = { name: 'test8', width: 8, vars: { reg: { bits: 3, toks: ['r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7'] }, imm8: { bits: 8 } }, rules: [ { fmt: 'nop', bits: ['00000000'] }, { fmt: 'mov ~reg,~reg', bits: ['10', 0, 1] }, { fmt: 'add ~reg,~imm8', bits: ['00001', 0, 1] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('nop'); asm.assemble('mov r1,r2'); asm.assemble('add r3,$42'); const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[0], 0x00, 'NOP should be 0x00'); assert.equal(state.output[1], 0b10001010, 'MOV r1,r2'); assert.equal(state.output[2], 0b00001011, 'ADD r3,imm first byte'); assert.equal(state.output[3], 0x42, 'ADD r3,imm second bytes'); }); it('Should handle labels', function () { const spec = { name: 'test8', width: 8, vars: { imm8: { bits: 8 } }, rules: [ { fmt: 'jmp ~imm8', bits: ['11110000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('jmp target'); asm.assemble('nop: jmp nop'); asm.assemble('target: jmp target'); const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[0], 0xf0, 'JMP opcode'); assert.equal(state.output[1], 4, 'Should jump to address 4'); assert.equal(state.output[2], 0xf0, 'JMP opcode'); assert.equal(state.output[3], 2, 'Should jump to itself (address 2)'); assert.equal(state.output[4], 0xf0, 'JMP opcode'); assert.equal(state.output[5], 4, 'Should jump to itself (address 4)'); }); }); describe('PC-Relative Addressing', function () { it('Should handle simple PC-relative branches', function () { const spec = { name: 'test8', width: 8, vars: { rel8: { bits: 8, iprel: true, ipofs: 0, ipmul: 1 } }, rules: [ { fmt: 'br ~rel8', bits: ['10000000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('br forward'); // offset 0 asm.assemble('br forward'); // offset 2 asm.assemble('br forward'); // offset 4 asm.assemble('forward: br forward'); // offset 6 const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[1], 6, 'First branch offset should be 6'); assert.equal(state.output[3], 4, 'Second branch offset should be 4'); assert.equal(state.output[5], 2, 'Third branch offset should be 2'); assert.equal(state.output[7], 0, 'Fourth branch offset should be 0 (self)'); }); it('Should handle PC-relative with instruction multiplier', function () { // Simulate word-addressed architecture where PC increments by 4 const spec = { name: 'test32', width: 32, vars: { rel13: { bits: 13, iprel: true, ipofs: 0, ipmul: 4 } }, rules: [ { fmt: 'beq ~rel13', bits: ['1100011000000000000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('beq target'); // offset 0 asm.assemble('beq target'); // offset 4 asm.assemble('target: beq target'); // offset 8 const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); // PC-relative offset = (target - current) * ipmul // First: (8 - 0) * 1 = 8 // Second: (8 - 4) * 1 = 4 // Third: (8 - 8) * 1 = 0 const first = state.output[0]; const second = state.output[1]; const third = state.output[2]; // Extract the 13-bit immediate from the instruction // It's in the lower 13 bits const offset1 = first & 0x1fff; const offset2 = second & 0x1fff; const offset3 = third & 0x1fff; assert.equal(offset1, 8, 'First branch offset should be 8'); assert.equal(offset2, 4, 'Second branch offset should be 4'); assert.equal(offset3, 0, 'Third branch offset should be 0'); }); }); describe('Bit Slicing', function () { it('Should extract bit slices correctly', function () { // RISC-V style branch with scrambled immediate const spec = { name: 'riscv', width: 32, vars: { reg: { bits: 5, toks: ['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7'] }, rel13: { bits: 13, iprel: true, ipofs: 0, ipmul: 1 } }, rules: [ // beq rs1, rs2, offset // Format: imm[12] | imm[10:5] | rs2 | rs1 | 000 | imm[4:1] | imm[11] | 1100011 { fmt: 'beq ~reg,~reg,~rel13', bits: [ { a: 2, b: 12, n: 1 }, // imm[12] { a: 2, b: 5, n: 6 }, // imm[10:5] 1, // rs2 0, // rs1 '000', // funct3 { a: 2, b: 1, n: 4 }, // imm[4:1] { a: 2, b: 11, n: 1 }, // imm[11] '1100011' // opcode ] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('target: beq x1,x2,target'); // Self-branch with offset 0 const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); const insn = state.output[0]; // Decode the instruction to verify bit positions const opcode = insn & 0x7f; const imm_11 = (insn >> 7) & 1; const imm_4_1 = (insn >> 8) & 0xf; const funct3 = (insn >> 12) & 0x7; const rs1 = (insn >> 15) & 0x1f; const rs2 = (insn >> 20) & 0x1f; const imm_10_5 = (insn >> 25) & 0x3f; const imm_12 = (insn >> 31) & 1; assert.equal(opcode, 0x63, 'Opcode should be 0x63 (branch)'); assert.equal(funct3, 0, 'funct3 should be 0 (BEQ)'); assert.equal(rs1, 1, 'rs1 should be 1 (x1)'); assert.equal(rs2, 2, 'rs2 should be 2 (x2)'); // All immediate bits should be 0 for self-branch assert.equal(imm_12, 0, 'imm[12] should be 0'); assert.equal(imm_11, 0, 'imm[11] should be 0'); assert.equal(imm_10_5, 0, 'imm[10:5] should be 0'); assert.equal(imm_4_1, 0, 'imm[4:1] should be 0'); }); it('Should handle non-zero bit slice offsets', function () { const spec = { name: 'riscv', width: 32, vars: { brop: { bits: 3, toks: ["beq","bne","bx2","bx3","blt","bge","bltu","bgeu"] }, reg: { bits: 5, toks: ['x0', 'x1', 'x2'] }, rel13: { bits: 13, iprel: true, ipofs: 0, ipmul: 4 } }, rules: [ { fmt: '~brop ~reg,~reg,~rel13', bits: [ { a: 3, b: 12, n: 1 }, { a: 3, b: 5, n: 6 }, 2, 1, '000', { a: 3, b: 1, n: 4 }, { a: 3, b: 11, n: 1 }, '1100011' ] } ] }; const asm = new Assembler(spec); asm.assemble('.org 4096'); asm.assemble('.len 1024'); asm.assemble('beq x1,x2,target'); // offset 0 asm.assemble('beq x1,x2,target'); // offset 4 asm.assemble('beq x1,x2,target'); // offset 8 asm.assemble('target: beq x1,x2,target'); // offset 12 (self) asm.assemble('beq x1,x2,target'); // offset 16 asm.assemble('beq x1,x2,target'); // offset 20 /* 00208663 00208463 00208263 00208063 fe208ee3 fe208ce3 */ const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[0], 0x208663, 'insn 0'); assert.equal(state.output[1], 0x208463, 'insn 1'); assert.equal(state.output[2], 0x208263, 'insn 2'); assert.equal(state.output[3], 0x208063, 'insn 3'); assert.equal(state.output[4], 0xfe208ee3|0, 'insn 4'); assert.equal(state.output[5], 0xfe208ce3|0, 'insn 5'); // Check that offset 12 was correctly calculated and sliced const insn0 = state.output[0]; // Reconstruct the immediate from the instruction const imm_11 = (insn0 >> 7) & 1; const imm_4_1 = (insn0 >> 8) & 0xf; const imm_10_5 = (insn0 >> 25) & 0x3f; const imm_12 = (insn0 >> 31) & 1; //console.log('insn0: $', insn0.toString(16), 'imm_12:', imm_12, 'imm_11:', imm_11, 'imm_10_5:', imm_10_5, 'imm_4_1:', imm_4_1); // Reconstruct the 13-bit signed offset (bit 0 is implicit 0) const offset = (imm_12 << 12) | (imm_11 << 11) | (imm_10_5 << 5) | (imm_4_1 << 1); assert.equal(offset, 12, 'Offset should be 12 bytes'); }); }); describe('Endianness', function () { it('Should handle little-endian values', function () { const spec = { name: 'test8', width: 8, vars: { imm16: { bits: 16, endian: 'little' as const } }, rules: [ { fmt: 'ldi ~imm16', bits: ['10000000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('ldi $1234'); const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[0], 0x80, 'Opcode'); assert.equal(state.output[1], 0x34, 'Low byte first (little-endian)'); assert.equal(state.output[2], 0x12, 'High byte second'); }); it('Should handle big-endian values', function () { const spec = { name: 'test8', width: 8, vars: { imm16: { bits: 16, endian: 'big' as const } }, rules: [ { fmt: 'ldi ~imm16', bits: ['10000000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('ldi $1234'); const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[0], 0x80, 'Opcode'); assert.equal(state.output[1], 0x12, 'High byte first (big-endian)'); assert.equal(state.output[2], 0x34, 'Low byte second'); }); }); describe('Directives', function () { it('Should handle .org directive', function () { const spec = { name: 'test8', width: 8, vars: {}, rules: [ { fmt: 'nop', bits: ['00000000'] } ] }; const asm = new Assembler(spec); asm.assemble('.org 100'); asm.assemble('.len 256'); asm.assemble('nop'); const state = asm.finish(); assert.equal(state.origin, 100, 'Origin should be 100'); assert.equal(state.ip, 101, 'IP should be at 101'); assert.equal(state.output[0], 0x00, 'NOP at origin'); }); it('Should handle .data directive', function () { const spec = { name: 'test8', width: 8, vars: {}, rules: [] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('.data 10 20 $30'); const state = asm.finish(); assert.equal(state.output[0], 10); assert.equal(state.output[1], 20); assert.equal(state.output[2], 0x30); }); it('Should handle .string directive', function () { const spec = { name: 'test8', width: 8, vars: {}, rules: [] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('.string HELLO'); const state = asm.finish(); assert.equal(state.output[0], 'H'.charCodeAt(0)); assert.equal(state.output[1], 'E'.charCodeAt(0)); assert.equal(state.output[2], 'L'.charCodeAt(0)); assert.equal(state.output[3], 'L'.charCodeAt(0)); assert.equal(state.output[4], 'O'.charCodeAt(0)); }); it('Should handle .align directive', function () { const spec = { name: 'test8', width: 8, vars: {}, rules: [ { fmt: 'nop', bits: ['00000000'] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('nop'); // offset 0 asm.assemble('nop'); // offset 1 asm.assemble('.align 4'); // align to 4 asm.assemble('nop'); // offset 4 const state = asm.finish(); assert.equal(state.lines[2].offset, 4, 'Should align to offset 4'); }); }); describe('Error Handling', function () { it('Should detect undefined labels', function () { const spec = { name: 'test8', width: 8, vars: { imm8: { bits: 8 } }, rules: [ { fmt: 'jmp ~imm8', bits: ['11110000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('jmp undefined_label'); const state = asm.finish(); assert.equal(state.errors.length, 1, 'Should have one error'); assert(state.errors[0].msg.includes('undefined_label'), 'Error should mention undefined_label'); }); it('Should detect value overflow', function () { const spec = { name: 'test8', width: 8, vars: { imm4: { bits: 4 } }, rules: [ { fmt: 'mov ~imm4', bits: ['1111', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('mov 20'); // 20 > 15 (max 4-bit value) const state = asm.finish(); assert.equal(state.errors.length, 1, 'Should have one error'); assert(state.errors[0].msg.includes('does not fit'), 'Error should mention overflow'); }); it('Should detect invalid instructions', function () { const spec = { name: 'test8', width: 8, vars: {}, rules: [ { fmt: 'nop', bits: ['00000000'] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('invalid_instruction'); const state = asm.finish(); assert.equal(state.errors.length, 1, 'Should have one error'); assert(state.errors[0].msg.includes('Could not decode'), 'Error should mention decode failure'); }); }); describe('32-bit Width', function () { it('Should handle 32-bit instructions', function () { const spec = { name: 'test32', width: 32, vars: { reg: { bits: 5, toks: ['r0', 'r1', 'r2', 'r3', 'r4'] }, imm12: { bits: 12 } }, rules: [ // RISC-V ADDI format: imm[11:0] | rs1 | 000 | rd | 0010011 { fmt: 'addi ~reg,~reg,~imm12', bits: [2, 1, '000', 0, '0010011'] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('addi r1,r2,$123'); const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); const insn = state.output[0]; const opcode = insn & 0x7f; const rd = (insn >> 7) & 0x1f; const funct3 = (insn >> 12) & 0x7; const rs1 = (insn >> 15) & 0x1f; const imm = (insn >> 20) & 0xfff; assert.equal(opcode, 0x13, 'Opcode should be 0x13 (OP-IMM)'); assert.equal(rd, 1, 'rd should be 1'); assert.equal(rs1, 2, 'rs1 should be 2'); assert.equal(funct3, 0, 'funct3 should be 0 (ADDI)'); assert.equal(imm, 0x123, 'Immediate should be 0x123'); }); }); describe('Symbol Definition', function () { it('Should allow symbol definition with .define', function () { const spec = { name: 'test8', width: 8, vars: { imm8: { bits: 8 } }, rules: [ { fmt: 'ldi ~imm8', bits: ['10000000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('.define MYCONST 42'); asm.assemble('ldi MYCONST'); const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[1], 42, 'Should use defined constant'); }); }); describe('Complex Fixups', function () { it('Should handle multiple fixups to same location', function () { const spec = { name: 'test8', width: 8, vars: { imm8: { bits: 8 } }, rules: [ { fmt: 'jmp ~imm8', bits: ['11110000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('jmp target'); asm.assemble('jmp target'); asm.assemble('target: jmp target'); const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[1], 4, 'First jump to target'); assert.equal(state.output[3], 4, 'Second jump to target'); assert.equal(state.output[5], 4, 'Third jump to itself'); }); it('Should handle backward references', function () { const spec = { name: 'test8', width: 8, vars: { rel8: { bits: 8, iprel: true, ipofs: 0, ipmul: 1 } }, rules: [ { fmt: 'br ~rel8', bits: ['10000000', 0] } ] }; const asm = new Assembler(spec); asm.assemble('.org 0'); asm.assemble('.len 256'); asm.assemble('start: br forward'); // offset 0 asm.assemble('br start'); // offset 2 (backward) asm.assemble('forward: br start'); // offset 4 (backward) const state = asm.finish(); assert.equal(state.errors.length, 0, 'Should have no errors'); assert.equal(state.output[1], 4, 'Forward branch'); // Backward branches: offset = target - current // For offset 2: target=0, current=2, offset=-2 (0xfe in 8-bit signed) assert.equal(state.output[3] & 0xff, 0xfe, 'Backward branch from offset 2'); // For offset 4: target=0, current=4, offset=-4 (0xfc in 8-bit signed) assert.equal(state.output[5] & 0xff, 0xfc, 'Backward branch from offset 4'); }); }); });