-
Notifications
You must be signed in to change notification settings - Fork 0
/
assembly.js
461 lines (398 loc) · 14.3 KB
/
assembly.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
"use-strict";
export class Error {
static machine = {
unknownInstruction: "Unknown instruction",
invalidInputType: "Invalid input type",
invalidInstructionFormat: "Invalid instruction format",
inputOutsideRange: "Input outside valid range",
};
}
/// A specific instance of a virtual machine running and executing code.
export class Machine {
/// Size of one word, in bytes, for instructions and addresses in memory.
static WORD_SIZE = Uint16Array.BYTES_PER_ELEMENT;
/// Max unsigned value of one word, and size of memory in bytes.
static WORD_RANGE = 0x1 << (8 * Uint16Array.BYTES_PER_ELEMENT);
/// Total count of general-purpose registers.
static NUM_REGISTERS = 2;
/// A value for the PC register that is guaranteed to halt execution.
/// Note that the machine halts with _any_ PC value outside the range of available statements; `PC_HALT` is just a constant that's guaranteed to work.
static PC_HALT = -1;
assemblyLanguage; // AssemblyLanguage
registers; // Array of values
#statements; // array of AssemblyStatement
/// Index to the `statements` array, indicating the next instruction to execute.
/// A value outside of the statements array's bounds will halt the machine, in this case the `halting` property will be true.
#pc;
constructor() {
this.registers = new Array(Machine.NUM_REGISTERS);
this.assemblyLanguage = new AssemblyLanguage([
AssemblyInstruction.setRegister(this.registers.length),
AssemblyInstruction.addRegisters,
AssemblyInstruction.branchIfZero(this.registers.length)
]);
this.reset({ pc: true, registers: true, statements: true });
}
/// Appends AssemblyStatements to the end of the current set of stored statements.
append(statements) {
if (!Array.isArray(statements)) { return; }
this.#statements = this.#statements.concat(statements);
}
get instructionCount() {
return this.#statements.length;
}
/// True if `pc` points to an invalid instruction index, false if `pc` points to a valid instruction index.
get halting() {
return this.#pc < 0 || this.#pc >= this.#statements.length;
}
get pc() {
return this.#pc;
}
/// The AssemblyStatement currently indicated by `pc`.
/// If currently in `halting` state, returns null.
get nextInstruction() {
if (this.halting) {
return null;
} else {
return this.#statements[this.#pc];
}
}
isRegisterIndexValid(rIndex) {
return rIndex >= 0 && rIndex < this.registers.length;
}
reset(config) {
if (config.pc) {
this.#pc = 0;
}
if (config.registers) {
this.registers.fill(0);
}
if (config.statements) {
this.#statements = [];
}
}
/// Executes the single next instruction indicated by `pc` and then returns immediately. Does nothing if the machine is currently `halting`.
step() {
let next = this.nextInstruction;
if (!next) { return; }
this.#pc = this.#pc + 1;
if (next.instruction) {
next.instruction.microcode.apply(this, next.operands);
}
}
/// Begins execution at the next instruction indicated by `pc`.
/// Execution halts when pc no longer points to a valid instruction index (`halting` is true).
run() {
// TODO: add a max-cycles argument, to give a chance to interrupt infinite loops?
while (!this.halting) {
this.step();
}
}
get stateSummary() {
return this.registers
.map(r => `[${r}]`)
.join(" ");
}
// TODO: instead, REPL should append one instruction then run.
execute(instruction, input) {
instruction.execute(input, this);
}
// Microcode implementation
setPC(value) {
this.#pc = value;
}
setRegister(rIndex, value) {
if (this.isRegisterIndexValid(rIndex)) {
this.registers[rIndex] = value;
}
}
addRegisters() {
// TODO: handle integer overflow
this.registers[0] = this.registers[0] + this.registers[1];
}
branchIfZero(statementIndex, rIndex) {
if (this.isRegisterIndexValid(rIndex)
&& this.registers[rIndex] == 0) {
this.setPC(statementIndex);
}
}
}
/// An instance of DataType describes a specific format of data used in registers or instruction operands.
export class DataType {
name;
min;
max;
constructor(config) {
this.name = config.name;
this.min = config.min;
this.max = config.max;
}
get helpText() {
return `${this.name} [${this.min} - ${this.max}]`;
}
parse(code) {
let integer = parseInt(code);
if (isNaN(integer)) {
throw Error.machine.invalidInputType;
}
if (integer < this.min || integer > this.max) {
throw Error.machine.inputOutsideRange;
}
return integer;
}
// Enumerated DataType instances.
static register = new DataType({
name: "register",
min: 0,
max: Machine.NUM_REGISTERS - 1
});
static address = new DataType({
name: "address",
width: Machine.WORD_SIZE,
min: 0x0,
max: Machine.WORD_RANGE
});
static word = new DataType({
name: "word",
width: Machine.WORD_SIZE,
min: 0,
max: Machine.WORD_RANGE
});
} // end class DataType.
/// Specifications for instructions and other details of the Machine's assembly language.
export class AssemblyLanguage {
instructionSpecs; // Array of AssemblyInstruction
#syntax;
constructor(instructionSpecs) {
this.instructionSpecs = instructionSpecs;
this.#syntax = new AssemblySyntax();
}
getInstruction(keyword) {
let instruction = this.instructionSpecs.find(i => i.keyword == keyword);
if (!instruction) {
throw Error.machine.unknownInstruction;
}
return instruction;
}
/// Parses a single line of assembly code into an AssemblyStatement.
/// Returns a null value, or an AssemblyStatement with a null instruction value, for various types of valid but empty statements.
/// Throws a Machine.Error if it fails to parse.
assembleStatement(text) {
let line = this.#syntax.tokenizeLine(text);
if (!line.keyword && !line.comment) { return null; }
if (line.keyword) {
let instruction = this.getInstruction(line.keyword);
if (line.operands.length != instruction.operands.length) {
throw Error.machine.invalidInstructionFormat;
}
let operands = instruction.operands.map((spec, index) => {
return spec.dataType.parse(line.operands[index]);
});
return new AssemblyStatement(instruction, operands, text, line.comment);
} else {
return new AssemblyStatement(null, [], text, line.comment);
}
}
}
export class AssemblySyntax {
/// Enumeration of token types.
static TokenCategory = {
comment: "comment",
keyword: "keyword",
operand: "operand"
};
/// Parses a single line of assembly code into cleaned tokens with metadata.
tokenizeLine(text) {
let comment = null;
let index = text.indexOf("#");
if (index >= 0) {
comment = text.substring(index + 1).trim();
text = text.substring(0, index);
}
let tokens = text.trim().split(" ").filter(item => item.length > 0);
let keyword = tokens.length > 0 ? tokens.shift().toUpperCase() : null;
return {
keyword: keyword,
operands: tokens,
comment: comment
};
}
}
/// Specifications for a single operand in an assembly instruction, and how to encode/decode its value within a machine code instruction.
export class OperandSpec {
placeholder;
dataType;
constructor(config) {
this.placeholder = config.placeholder;
this.dataType = config.dataType;
}
get helpText() {
return `${this.placeholder}: ${this.dataType.helpText}`;
}
}
/// An abstract specification of the behavior of a specific assembly language instruction.
export class AssemblyInstruction {
keyword;
operands; // array of OperandSpec
microcode; // Machine.prototype.someFunction
description;
constructor(config) {
this.keyword = config.keyword;
this.operands = config.operands;
this.microcode = config.microcode;
this.description = config.description;
}
/// Executes this instruction in `machine` with the given tokenized operand text.
execute(tokens, machine) {
// TODO: this is obsolete, use Machine.append + Machine.step or .run
throw Error.machine.invalidInstructionFormat;
if (tokens.length != this.operands.length) {
throw Error.machine.invalidInstructionFormat;
}
let values = this.operands.map((spec, index) => {
return spec.dataType.parse(tokens[index]);
});
this.microcode.apply(machine, values);
}
get helpText() {
let exampleTokens = [this.keyword];
for (const spec of this.operands) {
exampleTokens.push(spec.placeholder);
}
let lines = [
`${this.keyword}: ${this.description}`,
`Example: ${exampleTokens.join(" ")}`
];
for (const spec of this.operands) {
lines.push(spec.helpText);
}
return lines;
}
// Enumerated AssemblyInstruction instances.
static setRegister(registerCount) {
return new AssemblyInstruction({
keyword: "SET",
operands: [
new OperandSpec({
placeholder: "n",
dataType: DataType.register
}),
new OperandSpec({
placeholder: "i",
dataType: DataType.word
})
],
microcode: Machine.prototype.setRegister,
description: "Sets $Rn to integer value i",
});
}
static addRegisters = new AssemblyInstruction({
keyword: "ADD",
operands: [],
microcode: Machine.prototype.addRegisters,
description: "Sets $R0 = $R0 + $R1"
});
static branchIfZero(registerCount) {
return new AssemblyInstruction({
keyword: "BZ",
operands: [
new OperandSpec({
placeholder: "p",
dataType: DataType.address
}),
new OperandSpec({
placeholder: "n",
dataType: DataType.register
})
],
microcode: Machine.prototype.branchIfZero,
description: "Jumps (sets PC) to index p if $Rn == 0"
});
}
} // end class AssemblyInstruction.
/// A specific invocation of one AssemblyInstruction, with operand values.
/// Assumes that all values are already validated per the instruction's specifications.
export class AssemblyStatement {
/// AssemblyInstruction. Null indicates no-op, such as a comment line.
instruction;
/// Array of operand values, parsed to correct data types, not text input.
operands;
/// The original raw text of the statement.
text;
/// Null if no comment, otherwise a single line of text, no comment delimiter included.
comment;
constructor(instruction, operands, text, comment) {
this.instruction = instruction;
this.operands = operands;
this.text = text || "";
this.comment = (!!comment && comment.length > 0) ? comment : null;
}
}
/// An instance of Program is a single execution of a block of code.
///
/// Each Program instance creates a new Machine and runs the given code on that machine, leaving the machine in its final state after execution completes.
export class Program {
static tokenize(input) {
if (!input) {
throw Error.machine.unknownInstruction;
}
input = input.toUpperCase();
let tokens = input.split(" ");
let keyword = tokens.shift();
return [keyword, tokens];
}
constructor(text) {
this.machine = new Machine();
this.input = text.split("\n");
this.output = [];
this.run();
}
run() {
try {
this.input.forEach(line => {
let [keyword, tokens] = Program.tokenize(line);
let instruction = this.machine.assemblyLanguage.getInstruction(keyword);
this.machine.execute(instruction, tokens);
});
this.appendOutput("HALT");
} catch (e) {
this.appendOutput(`ERROR: ${e}`);
}
}
appendOutput(text) {
this.output.push(text);
}
}
/// Maintains a single Machine instance, upon which individual instructions can be ran interactively. Exposes the current state of the machine for inspection and manipulation after each instruction.
export class REPL {
constructor() {
this.machine = new Machine();
}
get helpText() {
let lines = [
`I am running on a ${Machine.WORD_SIZE * 8}-bit virtual machine. Instructions:`,
];
for (const instruction of this.machine.assemblyLanguage.instructionSpecs) {
instruction.helpText.forEach((line, index) => {
lines.push((index == 0 ? "# " : "") + line);
});
}
return lines.join("\n");
}
errorMessage(text) {
return `ERROR: ${text}.`;
}
run(input) {
try {
let [keyword, tokens] = Program.tokenize(input);
if (keyword == "HELP") {
return this.helpText;
}
let instruction = this.machine.assemblyLanguage.getInstruction(keyword);
this.machine.execute(instruction, tokens);
return this.machine.stateSummary;
} catch (e) {
return this.errorMessage(e);
}
}
}