mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-03-11 13:41:43 +00:00
597 lines
19 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|