mirror of
https://github.com/Asraelite/littlebigcomputer.git
synced 2025-07-17 16:16:51 +00:00
Add compiler
This commit is contained in:
parent
476972f85a
commit
3f3125ef43
31 changed files with 2625 additions and 3 deletions
|
@ -10,8 +10,10 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<script type="module" src="main.js"></script>
|
||||
<style>
|
||||
|
||||
<title>LittleBigComputer assembler</title>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
@ -24,6 +26,7 @@
|
|||
<select id="config-target-arch-select">
|
||||
<option value="v8">V8</option>
|
||||
<option value="parva_0_1">Parva 0.1</option>
|
||||
<option value="bitzzy">Bitzzy</option>
|
||||
<option value="lodestar">Lodestar</option>
|
||||
</select>
|
||||
<button type="button" id="config-target-arch-docs-button">Show docs</button>
|
||||
|
|
|
@ -2,13 +2,14 @@ import * as bitUtils from './bit_utils.js';
|
|||
|
||||
import targetV8 from './targets/v8.js';
|
||||
import targetParva_0_1 from './targets/parva_0_1.js';
|
||||
import targetBitzzy from './targets/bitzzy.js';
|
||||
import targetLodestar from './targets/lodestar.js';
|
||||
import * as targetTest from './targets/test/test.js';
|
||||
import { ArchName, ArchSpecification, AssemblyInput, AssemblyOutput, InstructionOutputLine } from './assembly.js';
|
||||
import { toNote } from './util.js';
|
||||
|
||||
const CONFIG_VERSION: string = '1';
|
||||
const EMULATOR_SPEED = 30;
|
||||
const EMULATOR_SPEED = 70;
|
||||
|
||||
window.addEventListener('load', init);
|
||||
|
||||
|
@ -194,6 +195,8 @@ function getArch(name: string): ArchSpecification {
|
|||
return targetV8;
|
||||
} else if (name === 'parva_0_1') {
|
||||
return targetParva_0_1;
|
||||
} else if (name === 'bitzzy') {
|
||||
return targetBitzzy;
|
||||
} else if (name === 'lodestar') {
|
||||
return targetLodestar as any;
|
||||
} else {
|
||||
|
|
792
assembler/targets/bitzzy.ts
Normal file
792
assembler/targets/bitzzy.ts
Normal file
|
@ -0,0 +1,792 @@
|
|||
import type { OutputLine, AssemblyInput, ArchSpecification, AssemblyOutput, InputError, LineSource, Emulator } from '../assembly';
|
||||
import { toBitString } from '../util.js';
|
||||
|
||||
export type Reference = {
|
||||
sourceAddress: number;
|
||||
addressRelative: boolean;
|
||||
bitCount: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type ParsedLabel = {
|
||||
tag: 'label';
|
||||
source: LineSource,
|
||||
name: string;
|
||||
};
|
||||
|
||||
type ParsedInstruction = {
|
||||
tag: 'instruction';
|
||||
source: LineSource,
|
||||
parts: Array<ParsedLinePart>;
|
||||
address: number;
|
||||
};
|
||||
|
||||
export type ParsedLinePart = string | Reference;
|
||||
type ParsedLine = ParsedInstruction | ParsedLabel;
|
||||
|
||||
function isNumber(value: string): boolean {
|
||||
return toNumber(value) !== null;
|
||||
}
|
||||
|
||||
function toNumber(value: string): number {
|
||||
let result = null;
|
||||
if (value.startsWith('$')) {
|
||||
result = parseInt(value.slice(1), 16);
|
||||
} else if (value.startsWith('%')) {
|
||||
result = parseInt(value.slice(1), 2);
|
||||
} else if (value.startsWith('@')) {
|
||||
result = parseInt(value.slice(1), 8);
|
||||
} else {
|
||||
result = parseInt(value, 10);
|
||||
}
|
||||
if (isNaN(result)) {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type Binding = { tag: 'binding', name: string };
|
||||
|
||||
const NUMBER_RE = String.raw`%-?[01][01_]*|\$-?[\da-f][\da-f_]*|@-?[0-7][0-7_]*|-?\d[\d_]*`;
|
||||
const VALUE_ATOM_RE = String.raw`(?:${NUMBER_RE}|[a-z0-9_-]+)`;
|
||||
const VALUE_PAIR_RE = String.raw`\((${VALUE_ATOM_RE})\s*([\+\-])\s*(${VALUE_ATOM_RE})\)`;
|
||||
const VALUE_RE = String.raw`${VALUE_ATOM_RE}|${VALUE_PAIR_RE}`;
|
||||
const REGISTER_RE = 'x|y|z|X|Y|Z|YX|yx';
|
||||
|
||||
class Program {
|
||||
currentAddress: number;
|
||||
output: Array<ParsedLine>;
|
||||
labels: Map<string, number>;
|
||||
constants: Map<string, number>;
|
||||
outputEnabled: boolean | null;
|
||||
|
||||
constructor() {
|
||||
this.currentAddress = 0;
|
||||
this.output = [];
|
||||
this.labels = new Map();
|
||||
this.constants = new Map();
|
||||
this.outputEnabled = null;
|
||||
}
|
||||
|
||||
instruction(parts: Array<ParsedLinePart>, wordCount: number, source: LineSource, count: number) {
|
||||
if (this.outputEnabled === null || this.outputEnabled) {
|
||||
this.output.push({
|
||||
tag: 'instruction',
|
||||
source,
|
||||
address: this.currentAddress,
|
||||
parts,
|
||||
});
|
||||
}
|
||||
this.currentAddress += wordCount * count;
|
||||
}
|
||||
|
||||
label(name: string, source: LineSource) {
|
||||
if (this.labels.has(name)) {
|
||||
throw new Error(`Label '${name}' already defined`);
|
||||
}
|
||||
this.labels.set(name, this.currentAddress);
|
||||
this.output.push({
|
||||
tag: 'label',
|
||||
source,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
address(value: number) {
|
||||
this.currentAddress = value;
|
||||
}
|
||||
|
||||
align(value: number) {
|
||||
this.currentAddress = Math.ceil(this.currentAddress / value) * value;
|
||||
}
|
||||
|
||||
enable() {
|
||||
if (this.outputEnabled === null) {
|
||||
this.output = [];
|
||||
}
|
||||
this.outputEnabled = true;
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.outputEnabled = false;
|
||||
}
|
||||
|
||||
data(values: Array<string>, source: LineSource) {
|
||||
const numbers: Array<ParsedLinePart> = values.map(n => this.value(n, 8, true));
|
||||
for (const number of numbers) {
|
||||
this.instruction([number], 1, { ...source, realInstruction: '(data)' }, 1);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
constant(name: string, value: number) {
|
||||
this.constants.set(name, value);
|
||||
}
|
||||
|
||||
value(numberOrLabelText: string, bits: number, dynamic: boolean = false): ParsedLinePart {
|
||||
if (isNumber(numberOrLabelText)) {
|
||||
return toBitString(toNumber(numberOrLabelText), bits, false, 8);
|
||||
} else {
|
||||
return {
|
||||
addressRelative: false,
|
||||
sourceAddress: this.currentAddress,
|
||||
bitCount: dynamic ? 16 : bits,
|
||||
value: numberOrLabelText,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
literal(numberText: string, bits: number, signed: boolean): number {
|
||||
if (!isNumber(numberText)) {
|
||||
throw new Error(`Expected number, got ${numberText}`);
|
||||
} else {
|
||||
return toNumber(numberText);
|
||||
}
|
||||
}
|
||||
|
||||
parseSourceLine(sourceLine: string, lineNumber: number) {
|
||||
// Remove comments beginning with ; and #
|
||||
let commentIndex = sourceLine.length;
|
||||
if (sourceLine.indexOf(';') !== -1) {
|
||||
commentIndex = Math.min(commentIndex, sourceLine.indexOf(';'));
|
||||
}
|
||||
if (sourceLine.indexOf('//') !== -1) {
|
||||
commentIndex = Math.min(commentIndex, sourceLine.indexOf('//'));
|
||||
}
|
||||
const uncommented = sourceLine.slice(0, commentIndex).trim();
|
||||
this.parse({
|
||||
lineNumber,
|
||||
realInstruction: uncommented,
|
||||
sourceInstruction: uncommented,
|
||||
sourceInstructionCommented: sourceLine,
|
||||
});
|
||||
}
|
||||
|
||||
parse(source: LineSource) {
|
||||
const line = source.realInstruction;
|
||||
|
||||
if (line === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
let matchFound = false;
|
||||
|
||||
const match = (...args: Array<string | ((bindings: { [key: string]: string }) => void)>) => {
|
||||
if (matchFound) {
|
||||
return;
|
||||
}
|
||||
const fn = args.pop() as any;
|
||||
const expression = new RegExp('^' + args.join('') + '$', 'i');
|
||||
const result = line.match(expression);
|
||||
if (result === null) {
|
||||
return;
|
||||
}
|
||||
fn(result.groups);
|
||||
matchFound = true;
|
||||
}
|
||||
const i = (...parts: Array<ParsedLinePart>) => {
|
||||
this.instruction(parts, 1, source, 1);
|
||||
};
|
||||
const i2 = (...parts: Array<ParsedLinePart>) => {
|
||||
this.instruction(parts, 1, source, 2);
|
||||
};
|
||||
const i3 = (...parts: Array<ParsedLinePart>) => {
|
||||
this.instruction(parts, 1, source, 3);
|
||||
};
|
||||
const i4 = (...parts: Array<ParsedLinePart>) => {
|
||||
this.instruction(parts, 1, source, 4);
|
||||
};
|
||||
const pseudo = (realInstruction: string) => this.parse({ ...source, realInstruction });
|
||||
const binding = (name: string): Binding => ({ tag: 'binding', name });
|
||||
const r = binding('r');
|
||||
const a = binding('a');
|
||||
const b = binding('b');
|
||||
|
||||
const bindablePattern = (regex: string, prefix: string = '', suffix: string = '') => (binding: Binding) =>
|
||||
`${prefix}(?<${binding.name}>${regex})${suffix}`;
|
||||
const token = bindablePattern(VALUE_RE);
|
||||
const register = bindablePattern(REGISTER_RE);
|
||||
const immediate = bindablePattern(VALUE_RE, '#');
|
||||
const remainder = bindablePattern('.*');
|
||||
|
||||
window['numberRegex'] = NUMBER_RE;
|
||||
|
||||
const s = String.raw`\s+`;
|
||||
const so = String.raw`\s*`;
|
||||
const sep = String.raw`\s*[,\s]\s*`;
|
||||
const hardSep = String.raw`\s*,\s*`;
|
||||
|
||||
const addr = bindablePattern(VALUE_RE);
|
||||
const registerAndA = bindablePattern(REGISTER_RE, '', `${so},${so}A`);
|
||||
const valueAndA = bindablePattern(VALUE_RE, '', `${so},${so}A`);
|
||||
const x = '[xX]';
|
||||
const y = '[yY]';
|
||||
const z = '[zZ]';
|
||||
const yx = '[yY][xX]';
|
||||
|
||||
match('hlt', () => i('00000000'));
|
||||
match('rti', () => i('00000010'));
|
||||
match('eni', () => i('00000011'));
|
||||
match('dsi', () => i('00000100'));
|
||||
match('nop', () => i('00000101'));
|
||||
|
||||
match('rem', s, x, () => i('00001000'));
|
||||
match('rem', s, y, () => i('00001001'));
|
||||
match('rem', s, z, () => i('00001010'));
|
||||
match('clr', () => i('00001011'));
|
||||
|
||||
match('jmp', s, addr(a), ({ a }) => { i3('00010000', this.value(a, 16)) });
|
||||
match('jmp', s, addr(a), sep, x, ({ a }) => { i3('00010001', this.value(a, 16)) });
|
||||
match('jmp', s, addr(a), sep, y, ({ a }) => { i3('00010010', this.value(a, 16)) });
|
||||
match('jmp', s, addr(a), sep, z, ({ a }) => { i3('00010011', this.value(a, 16)) });
|
||||
|
||||
match('jsr', s, addr(a), ({ a }) => { i3('00010100', this.value(a, 16)) });
|
||||
match('jsr', s, addr(a), sep, x, ({ a }) => { i3('00010101', this.value(a, 16)) });
|
||||
match('jsr', s, addr(a), sep, y, ({ a }) => { i3('00010110', this.value(a, 16)) });
|
||||
match('jsr', s, addr(a), sep, z, ({ a }) => { i3('00010111', this.value(a, 16)) });
|
||||
|
||||
match('swp', s, x, sep, y, () => { i('00011000') });
|
||||
match('swp', s, y, sep, x, () => pseudo('SWP X, Y'));
|
||||
match('swp', s, x, sep, z, () => { i('00011001') });
|
||||
match('swp', s, z, sep, x, () => pseudo('SWP X, Z'));
|
||||
match('swp', s, y, sep, z, () => { i('00011011') });
|
||||
match('swp', s, z, sep, y, () => pseudo('SWP Y, Z'));
|
||||
|
||||
match('jmpez', s, x, sep, addr(a), ({ a }) => { i3('00100000', this.value(a, 16)) });
|
||||
match('jmpez', s, z, sep, addr(a), ({ a }) => { i3('00100001', this.value(a, 16)) });
|
||||
match('jmpez', s, register(r), sep, addr(a), () => { throw new Error('JMPEZ can only be used with registers X and Z') });
|
||||
match('jmpgt', s, x, sep, y, sep, addr(a), ({ a }) => { i3('00100010', this.value(a, 16)) });
|
||||
match('jmpgt', s, register(a), sep, register(b), sep, addr(r), () => { throw new Error('JMPGT can only be used with registers X and Y') });
|
||||
match('jmpeq', s, x, sep, y, sep, addr(a), ({ a }) => { i3('00100011', this.value(a, 16)) });
|
||||
match('jmpeq', s, y, sep, x, sep, addr(a), ({ a }) => pseudo(`JMPEQ X, Y, ${a}`));
|
||||
match('jmpeq', s, x, sep, z, sep, addr(a), ({ a }) => { i3('11110010', this.value(a, 16)) });
|
||||
match('jmpeq', s, z, sep, x, sep, addr(a), ({ a }) => pseudo(`JMPEQ X, Z, ${a}`));
|
||||
match('jmpeq', s, y, sep, z, sep, addr(a), ({ a }) => { i3('11110011', this.value(a, 16)) });
|
||||
match('jmpeq', s, z, sep, y, sep, addr(a), ({ a }) => pseudo(`JMPEQ Y, Z, ${a}`));
|
||||
match('jmprez', s, addr(a), ({ a }) => { i3('11110000', this.value(a, 16)) });
|
||||
match('jmprnz', s, addr(a), ({ a }) => { i3('11110001', this.value(a, 16)) });
|
||||
|
||||
match('jsrez', s, x, sep, addr(a), ({ a }) => { i3('00100100', this.value(a, 16)) });
|
||||
match('jsrez', s, z, sep, addr(a), ({ a }) => { i3('00100101', this.value(a, 16)) });
|
||||
match('jsrez', s, register(r), sep, addr(a), () => { throw new Error('JSREZ can only be used with registers X and Z') });
|
||||
match('jsrgt', s, x, sep, y, sep, addr(a), ({ a }) => { i3('00100110', this.value(a, 16)) });
|
||||
match('jsrgt', s, register(a), sep, register(b), sep, () => { throw new Error('JSRGT can only be used with registers X and Y') });
|
||||
match('jsreq', s, x, sep, y, sep, addr(a),
|
||||
({ a }) => { i3('00100111', this.value(a, 16)) });
|
||||
match('jsreq', s, y, sep, x, sep, addr(a), ({ a }) => pseudo(`JSREQ X, Y, ${a}`));
|
||||
match('jsreq', s, register(a), sep, register(b), sep, addr(r), () => { throw new Error('JSREQ can only be used with registers X and Y') });
|
||||
|
||||
match('ret', () => i('00000001'));
|
||||
match('retrez', () => i('11110100'));
|
||||
match('retrnz', () => i('11110101'));
|
||||
|
||||
match('djnz', s, x, sep, addr(a), ({ a }) => i3('11100000', this.value(a, 16)));
|
||||
match('djnz', s, y, sep, addr(a), ({ a }) => i3('11100001', this.value(a, 16)));
|
||||
match('djnz', s, z, sep, addr(a), ({ a }) => i3('11100010', this.value(a, 16)));
|
||||
|
||||
match('lod', s, x, sep, immediate(a), ({ a }) => i2('01111100', this.value(a, 8)));
|
||||
match('lod', s, y, sep, immediate(a), ({ a }) => i2('01111101', this.value(a, 8)));
|
||||
match('lod', s, z, sep, immediate(a), ({ a }) => i2('01111110', this.value(a, 8)));
|
||||
match('lod', s, y, sep, x, () => i('00101000'));
|
||||
match('lod', s, z, sep, x, () => i('00101001'));
|
||||
match('lod', s, x, sep, y, () => i('00101010'));
|
||||
match('lod', s, z, sep, y, () => i('00101011'));
|
||||
match('lod', s, x, sep, z, () => i('00101100'));
|
||||
match('lod', s, y, sep, z, () => i('00101101'));
|
||||
|
||||
match('lod', s, x, sep, addr(a), ({ a }) => { i3('10100000', this.value(a, 16)) });
|
||||
match('lod', s, y, sep, addr(a), ({ a }) => { i3('10110000', this.value(a, 16)) });
|
||||
match('lod', s, z, sep, addr(a), ({ a }) => { i3('11000000', this.value(a, 16)) });
|
||||
match('lod', s, x, sep, addr(a), sep, y,
|
||||
({ a }) => { i3('10100001', this.value(a, 16)) });
|
||||
match('lod', s, x, sep, addr(a), sep, z,
|
||||
({ a }) => { i3('10100010', this.value(a, 16)) });
|
||||
match('lod', s, y, sep, addr(a), sep, x,
|
||||
({ a }) => { i3('10110001', this.value(a, 16)) });
|
||||
match('lod', s, y, sep, addr(a), sep, z,
|
||||
({ a }) => { i3('10110010', this.value(a, 16)) });
|
||||
match('lod', s, z, sep, addr(a), sep, x,
|
||||
({ a }) => { i3('11000001', this.value(a, 16)) });
|
||||
match('lod', s, z, sep, addr(a), sep, y,
|
||||
({ a }) => { i3('11000010', this.value(a, 16)) });
|
||||
|
||||
match('str', s, x, sep, addr(a), ({ a }) => { i3('10100100', this.value(a, 16)) });
|
||||
match('str', s, y, sep, addr(a), ({ a }) => { i3('10110100', this.value(a, 16)) });
|
||||
match('str', s, z, sep, addr(a), ({ a }) => { i3('11000100', this.value(a, 16)) });
|
||||
match('str', s, x, sep, addr(a), sep, y,
|
||||
({ a }) => { i3('10100101', this.value(a, 16)) });
|
||||
match('str', s, x, sep, addr(a), sep, z,
|
||||
({ a }) => { i3('10100110', this.value(a, 16)) });
|
||||
match('str', s, y, sep, addr(a), sep, x,
|
||||
({ a }) => { i3('10110101', this.value(a, 16)) });
|
||||
match('str', s, y, sep, addr(a), sep, z,
|
||||
({ a }) => { i3('10110110', this.value(a, 16)) });
|
||||
match('str', s, z, sep, addr(a), sep, x,
|
||||
({ a }) => { i3('11000101', this.value(a, 16)) });
|
||||
match('str', s, z, sep, addr(a), sep, y,
|
||||
({ a }) => { i3('11000110', this.value(a, 16)) });
|
||||
|
||||
match('str', s, immediate(a), sep, addr(b), sep, x,
|
||||
({ a, b }) => { i4('11010000', this.value(b, 16), this.value(a, 8)) });
|
||||
match('str', s, immediate(a), sep, addr(b), sep, y,
|
||||
({ a, b }) => { i4('11010001', this.value(b, 16), this.value(a, 8)) });
|
||||
match('str', s, immediate(a), sep, addr(b), sep, z,
|
||||
({ a, b }) => { i4('11010010', this.value(b, 16), this.value(a, 8)) });
|
||||
match('str', s, immediate(a), sep, addr(b),
|
||||
({ a, b }) => { i4('11010011', this.value(b, 16), this.value(a, 8)) });
|
||||
match('str', s, immediate(a), sep, addr(b), sep, yx,
|
||||
({ a, b }) => { i4('11010100', this.value(b, 16), this.value(a, 8)) });
|
||||
|
||||
match('lod', s, z, sep, addr(a), sep, yx,
|
||||
({ a }) => { i3('11000011', this.value(a, 16)) });
|
||||
match('str', s, z, sep, addr(a), sep, yx,
|
||||
({ a }) => { i3('11000111', this.value(a, 16)) });
|
||||
|
||||
match('jmp', s, addr(a), ({ a }) => { i3('00010000', this.value(a, 16)) });
|
||||
|
||||
match('inc', s, x, () => i('00110000'));
|
||||
match('inc', s, y, () => i('00110001'));
|
||||
match('inc', s, z, () => i('00110010'));
|
||||
match('inc', s, addr(a), ({ a }) => i3('00110011', this.value(a, 16)));
|
||||
|
||||
match('dec', s, x, () => i('00110100'));
|
||||
match('dec', s, y, () => i('00110101'));
|
||||
match('dec', s, z, () => i('00110110'));
|
||||
match('dec', s, addr(a), ({ a }) => i3('00110111', this.value(a, 16)));
|
||||
|
||||
match('add', s, x, sep, immediate(a), ({ a }) => i2('01000000', this.value(a, 8)));
|
||||
match('add', s, y, sep, immediate(a), ({ a }) => i2('01010000', this.value(a, 8)));
|
||||
match('add', s, z, sep, immediate(a), ({ a }) => i2('01100000', this.value(a, 8)));
|
||||
// TODO: Check where result is stored and maybe remove the duplicate encodings if needed
|
||||
match('add', s, x, sep, y, () => i('01000100'));
|
||||
match('add', s, y, sep, x, () => i('01000100'));
|
||||
match('add', s, x, sep, z, () => i('01000101'));
|
||||
match('add', s, z, sep, x, () => i('01000101'));
|
||||
match('add', s, y, sep, z, () => i('01010101'));
|
||||
match('add', s, z, sep, y, () => i('01010101'));
|
||||
|
||||
match('sub', s, x, sep, immediate(a), ({ a }) => i2('01000001', this.value(a, 8)));
|
||||
match('sub', s, y, sep, immediate(a), ({ a }) => i2('01010001', this.value(a, 8)));
|
||||
match('sub', s, z, sep, immediate(a), ({ a }) => i2('01100001', this.value(a, 8)));
|
||||
match('sub', s, x, sep, y, () => i('01000110'));
|
||||
match('sub', s, x, sep, z, () => i('01000111'));
|
||||
match('sub', s, y, sep, x, () => i('01010110'));
|
||||
match('sub', s, y, sep, z, () => i('01010111'));
|
||||
match('sub', s, z, sep, x, () => i('01100110'));
|
||||
match('sub', s, z, sep, y, () => i('01100111'));
|
||||
|
||||
match('mul', s, x, sep, immediate(a), ({ a }) => i2('01000010', this.value(a, 8)));
|
||||
match('mul', s, y, sep, immediate(a), ({ a }) => i2('01010010', this.value(a, 8)));
|
||||
match('mul', s, z, sep, immediate(a), ({ a }) => i2('01100010', this.value(a, 8)));
|
||||
match('mul', s, x, sep, y, () => i('01001000'));
|
||||
match('mul', s, y, sep, x, () => i('01001000'));
|
||||
match('mul', s, x, sep, z, () => i('01001001'));
|
||||
match('mul', s, z, sep, x, () => i('01001001'));
|
||||
match('mul', s, y, sep, z, () => i('01011001'));
|
||||
match('mul', s, z, sep, y, () => i('01011001'));
|
||||
|
||||
match('div', s, x, sep, immediate(a), ({ a }) => i2('01000011', this.value(a, 8)));
|
||||
match('div', s, y, sep, immediate(a), ({ a }) => i2('01010011', this.value(a, 8)));
|
||||
match('div', s, z, sep, immediate(a), ({ a }) => i2('01100011', this.value(a, 8)));
|
||||
match('div', s, x, sep, y, () => i('01001010'));
|
||||
match('div', s, x, sep, z, () => i('01001011'));
|
||||
match('div', s, y, sep, x, () => i('01011010'));
|
||||
match('div', s, y, sep, z, () => i('01011011'));
|
||||
match('div', s, z, sep, x, () => i('01101010'));
|
||||
match('div', s, z, sep, y, () => i('01101011'));
|
||||
|
||||
match('mod', s, x, sep, immediate(a), ({ a }) => i2('10011100', this.value(a, 8)));
|
||||
match('mod', s, y, sep, immediate(a), ({ a }) => i2('10011101', this.value(a, 8)));
|
||||
match('mod', s, z, sep, immediate(a), ({ a }) => i2('10011110', this.value(a, 8)));
|
||||
match('mod', s, x, sep, y, () => i('01001100'));
|
||||
match('mod', s, x, sep, z, () => i('01001101'));
|
||||
match('mod', s, y, sep, x, () => i('01011100'));
|
||||
match('mod', s, y, sep, z, () => i('01011101'));
|
||||
match('mod', s, z, sep, x, () => i('01101100'));
|
||||
match('mod', s, z, sep, y, () => i('01101101'));
|
||||
|
||||
match('lsl', s, x, () => i('01110000'));
|
||||
match('lsl', s, y, () => i('01110001'));
|
||||
match('lsl', s, z, () => i('01110010'));
|
||||
|
||||
match('lsr', s, x, () => i('01110100'));
|
||||
match('lsr', s, y, () => i('01110101'));
|
||||
match('lsr', s, z, () => i('01110110'));
|
||||
|
||||
match('not', s, x, () => i('01111000'));
|
||||
match('not', s, y, () => i('01111001'));
|
||||
match('not', s, z, () => i('01111010'));
|
||||
|
||||
match('and', s, x, sep, immediate(a), ({ a }) => i2('10010000', this.value(a, 8)));
|
||||
match('and', s, y, sep, immediate(a), ({ a }) => i2('10010001', this.value(a, 8)));
|
||||
match('and', s, z, sep, immediate(a), ({ a }) => i2('10010010', this.value(a, 8)));
|
||||
match('and', s, x, sep, y, () => i('10000000'));
|
||||
match('and', s, y, sep, x, () => i('10000000'));
|
||||
match('and', s, x, sep, z, () => i('10000001'));
|
||||
match('and', s, z, sep, x, () => i('10000001'));
|
||||
match('and', s, y, sep, z, () => i('10000010'));
|
||||
match('and', s, z, sep, y, () => i('10000010'));
|
||||
|
||||
match('xor', s, x, sep, immediate(a), ({ a }) => i2('10011000', this.value(a, 8)));
|
||||
match('xor', s, y, sep, immediate(a), ({ a }) => i2('10011001', this.value(a, 8)));
|
||||
match('xor', s, z, sep, immediate(a), ({ a }) => i2('10011010', this.value(a, 8)));
|
||||
match('xor', s, x, sep, y, () => i('10000100'));
|
||||
match('xor', s, y, sep, x, () => i('10000100'));
|
||||
match('xor', s, x, sep, z, () => i('10000101'));
|
||||
match('xor', s, z, sep, x, () => i('10000101'));
|
||||
match('xor', s, y, sep, z, () => i('10000110'));
|
||||
match('xor', s, z, sep, y, () => i('10000110'));
|
||||
|
||||
match('or', s, x, sep, immediate(a), ({ a }) => i2('10010100', this.value(a, 8)));
|
||||
match('or', s, y, sep, immediate(a), ({ a }) => i2('10010101', this.value(a, 8)));
|
||||
match('or', s, z, sep, immediate(a), ({ a }) => i2('10010110', this.value(a, 8)));
|
||||
match('or', s, x, sep, y, () => i('10001000'));
|
||||
match('or', s, y, sep, x, () => i('10001000'));
|
||||
match('or', s, x, sep, z, () => i('10001001'));
|
||||
match('or', s, z, sep, x, () => i('10001001'));
|
||||
match('or', s, y, sep, z, () => i('10001010'));
|
||||
match('or', s, z, sep, y, () => i('10001010'));
|
||||
|
||||
// Directives
|
||||
|
||||
match(token(a), ':', ({ a }) => this.label(a, source));
|
||||
match('.data', s, remainder(a),
|
||||
({ a }) => this.data(a.split(new RegExp(hardSep)), { ...source, sourceInstruction: '.data' }));
|
||||
match('.repeat', s, token(a), sep, token(b),
|
||||
({ a, b }) => this.data(new Array(this.literal(b, 8, false)).fill(a), { ...source, sourceInstruction: '.repeat' }));
|
||||
match('.eq', s, token(a), sep, token(b), ({ a, b }) => this.constant(a, this.literal(b, 8, false)));
|
||||
match('.address', s, token(a), ({ a }) => this.address(this.literal(a, 8, false)));
|
||||
match('.align', s, token(a), ({ a }) => this.align(this.literal(a, 8, false)));
|
||||
match('.start', () => this.enable());
|
||||
match('.end', () => this.disable());
|
||||
|
||||
if (!matchFound) {
|
||||
throw new Error(`Unknown instruction: ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
resolveParsedLine(instruction: ParsedLine): OutputLine {
|
||||
if (instruction.tag === 'label') {
|
||||
return {
|
||||
tag: 'label',
|
||||
name: instruction.name,
|
||||
};
|
||||
}
|
||||
|
||||
let bits = '';
|
||||
for (const part of instruction.parts) {
|
||||
bits += this.resolveParsedLinePart(part);
|
||||
}
|
||||
if (bits.length % 8 !== 0) {
|
||||
throw new Error(`Instruction ${instruction.source.realInstruction} is ${bits.length} bits long, but should be a multiple of 8`);
|
||||
}
|
||||
return {
|
||||
tag: 'instruction',
|
||||
bits,
|
||||
address: instruction.address,
|
||||
source: instruction.source,
|
||||
};
|
||||
}
|
||||
|
||||
resolveParsedLinePart(part: ParsedLinePart, littleEndian: boolean = true): string {
|
||||
if (typeof part === 'string') {
|
||||
return part;
|
||||
} else if (isNumber(part.value)) {
|
||||
return toBitString(toNumber(part.value), part.bitCount, part.addressRelative, littleEndian ? 8 : null);
|
||||
} else if (this.labels.has(part.value)) {
|
||||
const targetLabelAddress = this.labels.get(part.value);
|
||||
const value = part.addressRelative ? targetLabelAddress - part.sourceAddress : targetLabelAddress;
|
||||
return toBitString(value, part.bitCount, part.addressRelative, littleEndian ? 8 : null);
|
||||
}
|
||||
|
||||
const offsetMatch = part.value.match(new RegExp(VALUE_PAIR_RE, 'i'));
|
||||
if (offsetMatch) {
|
||||
const [base, operator, offset] = offsetMatch.slice(1);
|
||||
|
||||
const baseValue = parseInt(this.resolveParsedLinePart({
|
||||
...part,
|
||||
value: base,
|
||||
}, false), 2,);
|
||||
const offsetValue = parseInt(this.resolveParsedLinePart({
|
||||
...part,
|
||||
addressRelative: false,
|
||||
value: offset,
|
||||
}, false), 2) * (operator === '+' ? 1 : -1);
|
||||
return toBitString(baseValue + offsetValue, part.bitCount, part.addressRelative, littleEndian ? 8 : null);
|
||||
}
|
||||
|
||||
if (this.constants.has(part.value)) {
|
||||
const value = this.constants.get(part.value);
|
||||
return toBitString(value, part.bitCount, false, littleEndian ? 8 : null);
|
||||
} else {
|
||||
throw new Error(`Unknown label: ${part.value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assemble(input: AssemblyInput): AssemblyOutput {
|
||||
const program = new Program();
|
||||
const errors: Array<InputError> = [];
|
||||
|
||||
for (const [lineNumber, line] of input.source.split('\n').entries()) {
|
||||
try {
|
||||
program.parseSourceLine(line, lineNumber);
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
line: lineNumber,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const outputLines: Array<OutputLine> = [];
|
||||
for (const instruction of program.output) {
|
||||
let resolved: OutputLine;
|
||||
try {
|
||||
resolved = program.resolveParsedLine(instruction);
|
||||
outputLines.push(resolved);
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
line: instruction.source.lineNumber,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lines: outputLines,
|
||||
errors,
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
const STACK_POINTER_ADDRESS = 0x7f;
|
||||
const INTERRUPT_VECTOR_ADDRESS = 0xfe;
|
||||
const RESET_VECTOR_ADDRESS = 0xff;
|
||||
|
||||
class V8Emulator implements Emulator {
|
||||
memory: Array<number> = [];
|
||||
registers: Array<number> = [];
|
||||
pc: number;
|
||||
cycle: number;
|
||||
carryFlag: boolean;
|
||||
zeroFlag: boolean;
|
||||
interruptsEnabled: boolean;
|
||||
|
||||
constructor() {
|
||||
this.init([]);
|
||||
}
|
||||
|
||||
init(memory: Array<number>) {
|
||||
this.memory = memory;
|
||||
this.memory[STACK_POINTER_ADDRESS] = STACK_POINTER_ADDRESS - 1;
|
||||
this.registers = new Array(8).fill(0);
|
||||
this.pc = this.memory[RESET_VECTOR_ADDRESS] ?? 0;
|
||||
this.cycle = 0;
|
||||
this.carryFlag = false;
|
||||
this.zeroFlag = false;
|
||||
this.interruptsEnabled = true;
|
||||
}
|
||||
|
||||
step() {
|
||||
|
||||
}
|
||||
|
||||
printState(): string {
|
||||
return 'Emulator not implemented';
|
||||
}
|
||||
}
|
||||
|
||||
const syntaxHighlighting = new window['Parser']({
|
||||
whitespace: /\s+/,
|
||||
number: /#?(\$-?[\dA-Fa-f_]+|-?(\d+)|%-?[01_]+|@-?[0-7_]+)/,
|
||||
comment: /\/\/[^\r\n]*|;[^\r\n]*/,
|
||||
directive: /\.[a-zA-Z0-9_]+/,
|
||||
label: /[a-zA-Z0-9_]+:/,
|
||||
string: /"(\\.|[^"\r\n])*"|'(\\.|[^'\r\n])*'/,
|
||||
register: /\b([xyz]|yx)\b/i,
|
||||
instruction: /^[a-zA-Z0-9\.]+/,
|
||||
other: /\S/,
|
||||
});
|
||||
|
||||
const archSpecification: ArchSpecification = {
|
||||
documentation: '',
|
||||
syntaxHighlighting,
|
||||
assemble,
|
||||
maxWordsPerInstruction: 4,
|
||||
wordSize: 8,
|
||||
emulator: new V8Emulator(),
|
||||
};
|
||||
export default archSpecification;
|
||||
|
||||
archSpecification.documentation = `
|
||||
# Bitzzy
|
||||
|
||||
An 8-bit CPU by Mazzetip with 16-bit addressing.
|
||||
|
||||
|
||||
## Registers
|
||||
|
||||
There are three registers: X, Y, and Z. Z is the accumulator.
|
||||
|
||||
|
||||
## Assembler syntax
|
||||
|
||||
Instructions and registers are case-insensitive. For example, \`INC X\` and \`inc x\` are both valid.
|
||||
|
||||
Labels and constants are case-sensitive. For example, \`myLabel:\` and \`mylabel:\` are different labels.
|
||||
|
||||
Comments begin with a semicolon. For example, \`; this is a comment\`.
|
||||
|
||||
Labels can be offset by a value by placing them in parentheses. For example, \`(myLabel + 2)\` is two bytes after \`myLabel\`.
|
||||
|
||||
<reg> indicates a register, i.e. X, Y, or Z. For example, the instruction \`INC <reg>\` can be used as \`INC X\`, \`INC Y\`, or \`INC Z\`.
|
||||
|
||||
When an instruction takes two registers, they must not be the same. E.g. \`SWP x, y\` is okay but \`SWP x, x\` is illegal.
|
||||
|
||||
<imm> indicates an 8-bit immediate value. This must be prefixed with '#' and can be signed or unsigned. For example, \`LOD X, #\$ff\` loads the value -1 into register X.
|
||||
|
||||
<addr> indicates a 16-bit address. For example, \`JMP \$fa08\` jumps to address 0xfa08.
|
||||
|
||||
When registers X and Y are used together to form a 16-bit address, they are written as YX. Y forms the upper 8 bits while X forms the lower 8 bits.
|
||||
|
||||
|
||||
## Instructions
|
||||
|
||||
HLT: Halt the CPU.
|
||||
|
||||
RTI: Return from interrupt.
|
||||
|
||||
ENI: Enable interrupts.
|
||||
|
||||
DSI: Disable interrupts.
|
||||
|
||||
NOP: No operation.
|
||||
|
||||
REM <reg>: Set register <reg> to the remainder (the carry/borrow flag).
|
||||
|
||||
CLR: Clear remainder.
|
||||
|
||||
JMP <addr>: Jump to the address <addr>.
|
||||
|
||||
JMP <addr>, <reg>: Jump to the address <addr> + register <reg>.
|
||||
|
||||
JMPEZ X, <addr>: Jump to the address <addr> if register X is zero.
|
||||
|
||||
JMPEZ Z, <addr>: Jump to the address <addr> if register Z is zero.
|
||||
|
||||
JMPGT X, Z, <addr>: Jump to the address <addr> if X is greater than Y.
|
||||
|
||||
JMPEQ <reg-a>, <reg-b>, <addr>: Jump to the address <addr> if register <reg-a> is equal to register <reg-b>.
|
||||
|
||||
JMPRNZ <addr>: Jump to the address <addr> if the remainder is not zero.
|
||||
|
||||
JMPREZ <addr>: Jump to the address <addr> if the remainder is zero.
|
||||
|
||||
JSR <addr>: Jump to the address <addr> and push the return address onto the stack.
|
||||
|
||||
JSR <addr>, <reg>: Jump to the address <addr> + register <reg> and push the return address onto the stack.
|
||||
|
||||
JSREZ X, <addr>: Jump to the address <addr> if register X is zero and push the return address onto the stack.
|
||||
|
||||
JSREZ Z, <addr>: Jump to the address <addr> if register Z is zero and push the return address onto the stack.
|
||||
|
||||
JSRGT X, Z, <addr>: Jump to the address <addr> if X is greater than Y and push the return address onto the stack.
|
||||
|
||||
JSREQ <reg-a>, <reg-b>, <addr>: Jump to the address <addr> if register <reg-a> is equal to register <reg-b> and push the return address onto the stack.
|
||||
|
||||
DJNZ <reg>, <addr>: Decrement register <reg> and jump to the address <addr> if it is now not zero.
|
||||
|
||||
RET: Return from subroutine.
|
||||
|
||||
RETRNZ: Return from subroutine if the remainder is not zero.
|
||||
|
||||
RETREZ: Jump to the address <addr> if the remainder is zero.
|
||||
|
||||
INC <reg>: Increment register <reg>.
|
||||
|
||||
INC <addr>: Increment value at address <addr>.
|
||||
|
||||
DEC <reg>: Decrement register <reg>.
|
||||
|
||||
DEC <addr>: Decrement value at address <addr>.
|
||||
|
||||
ADD <reg-a>, <reg-b>: Add register <reg-b> to register <reg-a> and store the result in Z.
|
||||
|
||||
ADD <reg>, <imm>: Add the immediate value <imm> to register <reg> and store the result in register <reg>.
|
||||
|
||||
SUB <reg-a>, <reg-b>: Subtract register <reg-b> from register <reg-a> and store the result in Z.
|
||||
|
||||
SUB <reg>, <imm>: Subtract the immediate value <imm> from register <reg> and store the result in register <reg>.
|
||||
|
||||
LSL <reg>: Logical shift left register <reg> by one bit.
|
||||
|
||||
LSR <reg>: Logical shift right register <reg> by one bit.
|
||||
|
||||
AND <reg-a>, <reg-b>: Bitwise AND register <reg-a> with register <reg-b> and store the result in Z.
|
||||
|
||||
AND <reg>, <imm>: Bitwise AND register <reg> with the immediate value <imm> and store the result in register <reg>.
|
||||
|
||||
OR <reg-a>, <reg-b>: Bitwise OR register <reg-a> with register <reg-b> and store the result in Z.
|
||||
|
||||
OR <reg>, <imm>: Bitwise OR register <reg> with the immediate value <imm> and store the result in register <reg>.
|
||||
|
||||
XOR <reg-a>, <reg-b>: Bitwise XOR register <reg-a> with register <reg-b> and store the result in Z.
|
||||
|
||||
XOR <reg>, <imm>: Bitwise XOR register <reg> with the immediate value <imm> and store the result in register <reg>.
|
||||
|
||||
LOD <reg>, <imm>: Load the immediate value <imm> into register <reg>.
|
||||
|
||||
LOD <reg-a>, <reg-b>: Load the value in register <reg-b> into register <reg-a>.
|
||||
|
||||
LOD <reg>, <addr>: Load the value at address <addr> into register <reg>.
|
||||
|
||||
LOD <reg-a>, <addr>, <reg-b>: Load the value at address <addr> + register <reg-b> into register <reg-a>.
|
||||
|
||||
LOD Z, <addr>, YX: Load the value at address <addr> + the combined value of registers X and Y ($YYXX) into register Z.
|
||||
|
||||
SWP <reg-a>, <reg-b>: Swap the values in registers <reg-a> and <reg-b>.
|
||||
|
||||
STR <reg>, <addr>: Store the value in register <reg> at address <addr>.
|
||||
|
||||
STR <reg-a>, <addr>, <reg-b>: Store the value in register <reg-a> at address <addr> + register <reg-b>.
|
||||
|
||||
STR <imm>, <addr>: Store the immediate value <imm> to address <addr>.
|
||||
|
||||
STR <imm>, <addr>, <reg>: Store the immediate value <imm> to address <addr> + register <reg>.
|
||||
|
||||
STR Z, <addr>, YX: Store the value in register Z at address <addr> + the combined value of registers X and Y ($YYXX).
|
||||
|
||||
MUL <reg-a>, <reg-b>: Multiply register <reg-a> by register <reg-b> and store the result in in Z.
|
||||
|
||||
MUL <reg>, <imm>: Multiply register <reg> by the immediate value <imm> and store the result in register <reg>.
|
||||
|
||||
DIV <reg-a>, <reg-b>: Divide register <reg-a> by register <reg-b> and store the result in in Z.
|
||||
|
||||
DIV <reg>, <imm>: Divide register <reg> by the immediate value <imm> and store the result in register <reg>.
|
||||
|
||||
MOD <reg-a>, <reg-b>: Divide register <reg-a> by register <reg-b> and store the remainder in in Z.
|
||||
|
||||
MOD <reg>, <imm>: Divide register <reg> by the immediate value <imm> and store the remainder in register <reg>.
|
||||
|
||||
|
||||
## Assembler directives
|
||||
|
||||
\`<name>:\` : Define a label.
|
||||
|
||||
\`.data <imm>, <imm>, <imm>, ...\`: Store 8-bit values in memory. You can also use labels, in which case the 16-bit address of the label will be stored.
|
||||
|
||||
\`.repeat <imm>, <repetitions>\`: Repeat <imm> in memory multiple times.
|
||||
|
||||
\`.address <addr>\`: Set the current address to <addr>.
|
||||
|
||||
\`.align <imm>\`: Align the current address to the next multiple of <imm>.
|
||||
|
||||
\`.end\`: Ignore all following instructions.
|
||||
|
||||
\`.start\`: Resume assembling instructions.
|
||||
|
||||
\`.eq\ <name>, <imm>\`: Define a constant.
|
||||
|
||||
|
||||
`;
|
|
@ -633,7 +633,12 @@ class ParvaEmulator implements Emulator {
|
|||
}
|
||||
|
||||
step() {
|
||||
const instructionsFetched = (2 - (this.pc % 2)) + (this.previousFrameAccessedMemory ? 0 : 2);
|
||||
this.previousFrameAccessedMemory = false;
|
||||
|
||||
for (let i = 0; i < instructionsFetched; i++) {
|
||||
if (this.stepCore()) break;
|
||||
}
|
||||
}
|
||||
|
||||
stepCore(): boolean {
|
||||
|
@ -724,6 +729,7 @@ class ParvaEmulator implements Emulator {
|
|||
}
|
||||
|
||||
this.previousFrameAccessedMemory = true;
|
||||
coreTerminates = true;
|
||||
} else if (operationType === '11') {
|
||||
// branching
|
||||
const special = condition.startsWith('11');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export function toBitString(n: number, bits: number, signed: boolean = false): string {
|
||||
export function toBitString(n: number, bits: number, signed: boolean = false, littleEndianByteSize: null | number = null): string {
|
||||
const minimum = -(2 ** (bits - 1));
|
||||
const maximum = signed ? (2 ** (bits - 1)) - 1 : 2 ** bits - 1;
|
||||
|
||||
|
@ -18,6 +18,15 @@ export function toBitString(n: number, bits: number, signed: boolean = false): s
|
|||
if (n !== 0) {
|
||||
throw new Error(`Internal error: ${n} not zero after conversion to ${bits}-bit ${signed ? 'signed' : 'unsigned'} integer`);
|
||||
}
|
||||
|
||||
if (littleEndianByteSize !== null) {
|
||||
const bytes = [];
|
||||
for (let i = 0; i < result.length / littleEndianByteSize; i++) {
|
||||
bytes.push(result.substring(i * littleEndianByteSize, (i + 1) * littleEndianByteSize));
|
||||
}
|
||||
result = bytes.reverse().join('');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue