1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2026-03-11 13:41:43 +00:00
Files
8bitworkshop/test/unit/testassembler.ts
2026-03-03 12:39:16 +01:00

597 lines
19 KiB
TypeScript

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');
});
});
});